From acee9f4278a5016ca67e6d735f9357ded3f38f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 10 Jan 2026 17:27:15 +0000 Subject: [PATCH] feat: task page --- apps/web/app/task/[id]/page.tsx | 600 ++++++++++++++++++++++++ apps/web/components/diff-stat.tsx | 43 ++ apps/web/components/kanban-board.tsx | 46 +- apps/web/components/kanban-card.tsx | 5 +- apps/web/components/kanban-column.tsx | 12 +- apps/web/components/resizable-split.tsx | 113 +++++ apps/web/components/ui/badge.tsx | 1 + apps/web/components/ui/resizable.tsx | 34 ++ apps/web/components/ui/tabs.tsx | 57 +++ apps/web/package-lock.json | 11 + apps/web/package.json | 1 + 11 files changed, 876 insertions(+), 47 deletions(-) create mode 100644 apps/web/app/task/[id]/page.tsx create mode 100644 apps/web/components/diff-stat.tsx create mode 100644 apps/web/components/resizable-split.tsx create mode 100644 apps/web/components/ui/resizable.tsx create mode 100644 apps/web/components/ui/tabs.tsx diff --git a/apps/web/app/task/[id]/page.tsx b/apps/web/app/task/[id]/page.tsx new file mode 100644 index 0000000000..40cfe7a1da --- /dev/null +++ b/apps/web/app/task/[id]/page.tsx @@ -0,0 +1,600 @@ +'use client'; + +import { FormEvent, useState } from 'react'; +import Link from 'next/link'; +import { + IconArrowDown, + IconArrowLeft, + IconBrandVscode, + IconChevronDown, + IconChevronRight, + IconChevronUp, + IconCopy, + IconEye, + IconGitBranch, + IconGitFork, + IconGitMerge, + IconGitPullRequest, + IconPencil, + IconX, +} from '@tabler/icons-react'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { cn } from '@/lib/utils'; +import { CommitStatBadge, LineStat } from '@/components/diff-stat'; + +type ChatMessage = { + id: string; + role: 'user' | 'agent'; + content: string; +}; + +type ChatSession = { + id: string; + title: string; + messages: ChatMessage[]; +}; + +const AGENTS = [ + { id: 'codex', label: 'Codex' }, + { id: 'claude', label: 'Claude Code' }, +]; + +const INITIAL_CHATS: ChatSession[] = [ + { + id: 'chat-1', + title: 'Build overview', + messages: [ + { + id: 'm1', + role: 'agent', + content: 'I can help with this task. What should I tackle first?', + }, + { + id: 'm2', + role: 'user', + content: 'Please review the changes and summarize the key diffs.', + }, + ], + }, +]; + +const CHANGED_FILES = [ + { path: 'apps/web/components/kanban-board.tsx', status: 'M', plus: 12, minus: 4 }, + { path: 'apps/web/components/kanban-card.tsx', status: 'M', plus: 3, minus: 2 }, + { path: 'apps/web/components/task-create-dialog.tsx', status: 'A', plus: 55, minus: 0 }, + { path: 'apps/web/components/kanban-column.tsx', status: 'D', plus: 0, minus: 18 }, +]; + +const ALL_FILES = [ + 'apps/web/app/page.tsx', + 'apps/web/app/task/[id]/page.tsx', + 'apps/web/components/kanban-board.tsx', + 'apps/web/components/kanban-card.tsx', + 'apps/web/components/kanban-column.tsx', + 'apps/web/components/task-create-dialog.tsx', +]; + +const COMMANDS = [ + { id: 'dev', label: 'npm run dev' }, + { id: 'lint', label: 'npm run lint' }, + { id: 'test', label: 'npm run test' }, +]; + +const badgeClass = (status: string) => + cn( + 'text-[10px] font-semibold', + status === 'M' && 'bg-yellow-500/15 text-yellow-700', + status === 'A' && 'bg-emerald-500/15 text-emerald-700', + status === 'D' && 'bg-rose-500/15 text-rose-700' + ); + +const splitPath = (path: string) => { + const lastSlash = path.lastIndexOf('/'); + if (lastSlash === -1) return { folder: '', file: path }; + return { + folder: path.slice(0, lastSlash), + file: path.slice(lastSlash + 1), + }; +}; + +export default function TaskPage() { + const [selectedAgent, setSelectedAgent] = useState(AGENTS[0].id); + const [chats, setChats] = useState(INITIAL_CHATS); + const [leftTab, setLeftTab] = useState<'notes' | 'changes' | 'chat'>('chat'); + const [messageInput, setMessageInput] = useState(''); + const [topTab, setTopTab] = useState<'diff' | 'files'>('diff'); + const [activeTerminalId, setActiveTerminalId] = useState(1); + const [terminalIds, setTerminalIds] = useState([1]); + const [notes, setNotes] = useState(''); + const [isBottomCollapsed, setIsBottomCollapsed] = useState(false); + const [branchName, setBranchName] = useState('feature/agent-ui'); + const [isEditingBranch, setIsEditingBranch] = useState(false); + + const activeChat = chats[0]; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + const trimmed = messageInput.trim(); + if (!trimmed) return; + setChats((currentChats) => { + const [first, ...rest] = currentChats; + if (!first) return currentChats; + return [ + { + ...first, + messages: [ + ...first.messages, + { id: crypto.randomUUID(), role: 'user', content: trimmed }, + ], + }, + ...rest, + ]; + }); + setMessageInput(''); + }; + + const addTerminal = () => { + setTerminalIds((ids) => { + const nextId = Math.max(0, ...ids) + 1; + setActiveTerminalId(nextId); + return [...ids, nextId]; + }); + }; + + const removeTerminal = (id: number) => { + setTerminalIds((ids) => { + const nextIds = ids.filter((terminalId) => terminalId !== id); + if (activeTerminalId === id) { + const fallback = nextIds[0] ?? 1; + setActiveTerminalId(fallback); + } + return nextIds.length ? nextIds : [1]; + }); + }; + + const terminalTabValue = activeTerminalId === 0 ? 'commands' : `terminal-${activeTerminalId}`; + + return ( +
+
+
+ + Task details +
+
+ + {isEditingBranch ? ( + setBranchName(event.target.value)} + onBlur={() => setIsEditingBranch(false)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === 'Escape') { + setIsEditingBranch(false); + } + }} + autoFocus + /> + ) : ( + <> + {branchName} +
+ + +
+ + )} +
+ + +
+ + + +
+
+ + + + +
+
+ +
+ + +
+ setLeftTab(value as typeof leftTab)} + className="flex-1 min-h-0" + > + + Notes + All changes + Chat {activeChat ? `• ${activeChat.title}` : ''} + + + +