diff --git a/apps/web/app/task/[id]/page.tsx b/apps/web/app/task/[id]/page.tsx index 5c3bcea5cd..f5c49f2922 100644 --- a/apps/web/app/task/[id]/page.tsx +++ b/apps/web/app/task/[id]/page.tsx @@ -1,80 +1,17 @@ 'use client'; -import type { CSSProperties } from 'react'; -import { FormEvent, useEffect, useMemo, useState } from 'react'; -import Link from 'next/link'; -import { DiffModeEnum, DiffView } from '@git-diff-view/react'; +import { useCallback, useEffect, useState } from 'react'; import '@git-diff-view/react/styles/diff-view.css'; -import { - IconArrowDown, - IconArrowBackUp, - IconArrowLeft, - IconBrandVscode, - IconChevronDown, - IconChevronRight, - IconChevronUp, - IconCopy, - IconEye, - IconGitBranch, - IconGitFork, - IconGitMerge, - IconGitPullRequest, - IconBrain, - IconLayoutColumns, - IconLayoutRows, - IconPencil, - IconFile, - IconFolder, - IconListCheck, - IconPaperclip, - IconExternalLink, - IconX, -} from '@tabler/icons-react'; -import { useTheme } from 'next-themes'; -import { Button } from '@/components/ui/button'; -import { Textarea } from '@/components/ui/textarea'; -import { Badge } from '@/components/ui/badge'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from '@/components/ui/collapsible'; -import { - Sidebar, - SidebarContent, - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarProvider, - SidebarRail, -} from '@/components/ui/sidebar'; -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 { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { cn } from '@/lib/utils'; -import { CommitStatBadge, LineStat } from '@/components/diff-stat'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { Textarea } from '@/components/ui/textarea'; import { getLocalStorage, setLocalStorage } from '@/lib/local-storage'; +import { TaskChatPanel } from '@/components/task/task-chat-panel'; +import { TaskTopBar } from '@/components/task/task-top-bar'; +import { TaskFilesPanel } from '@/components/task/task-files-panel'; +import { TaskChangesPanel } from '@/components/task/task-changes-panel'; +import { TaskRightPanel } from '@/components/task/task-right-panel'; type ChatMessage = { id: string; @@ -112,259 +49,20 @@ const INITIAL_CHATS: ChatSession[] = [ }, ]; -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 DIFF_SAMPLES: Record = { - 'apps/web/components/kanban-board.tsx': { - diff: [ - 'diff --git a/apps/web/components/kanban-board.tsx b/apps/web/components/kanban-board.tsx', - 'index 7b45ad2..9c0f2ee 100644', - '--- a/apps/web/components/kanban-board.tsx', - '+++ b/apps/web/components/kanban-board.tsx', - '@@ -14,6 +14,7 @@ export function KanbanBoard() {', - ' const columns = useMemo(() => getColumns(view), [view]);', - ' const tasks = useMemo(() => getTasks(), []);', - '+ const hasAlerts = tasks.some((task) => task.priority === "high");', - ' return (', - '
', - ' ', - ].join('\n'), - newContent: [ - 'export function KanbanBoard() {', - ' const columns = useMemo(() => getColumns(view), [view]);', - ' const tasks = useMemo(() => getTasks(), []);', - ' const hasAlerts = tasks.some((task) => task.priority === "high");', - ' return (', - '
', - ' ', - '
', - ' );', - '}', - ].join('\n'), - }, - 'apps/web/components/kanban-card.tsx': { - diff: [ - 'diff --git a/apps/web/components/kanban-card.tsx b/apps/web/components/kanban-card.tsx', - 'index a14d022..a0cc12f 100644', - '--- a/apps/web/components/kanban-card.tsx', - '+++ b/apps/web/components/kanban-card.tsx', - '@@ -8,7 +8,8 @@ export function KanbanCard({ task }: KanbanCardProps) {', - ' return (', - ' ', - '-

{task.title}

', - '+

{task.title}

', - '+ {task.assignee}', - '

{task.summary}

', - '
', - ' );', - ].join('\n'), - newContent: [ - 'export function KanbanCard({ task }: KanbanCardProps) {', - ' return (', - ' ', - '

{task.title}

', - ' {task.assignee}', - '

{task.summary}

', - '
', - ' );', - '}', - ].join('\n'), - }, - 'apps/web/components/task-create-dialog.tsx': { - diff: [ - 'diff --git a/apps/web/components/task-create-dialog.tsx b/apps/web/components/task-create-dialog.tsx', - 'new file mode 100644', - 'index 0000000..6c1a1f0', - '--- /dev/null', - '+++ b/apps/web/components/task-create-dialog.tsx', - '@@ -0,0 +1,6 @@', - '+export function TaskCreateDialog() {', - '+ return (', - '+ ', - '+ Create task', - '+ ', - '+ );', - '+}', - ].join('\n'), - newContent: [ - 'export function TaskCreateDialog() {', - ' return (', - ' ', - ' Create task', - ' ', - ' );', - '}', - ].join('\n'), - }, - 'apps/web/components/kanban-column.tsx': { - diff: [ - 'diff --git a/apps/web/components/kanban-column.tsx b/apps/web/components/kanban-column.tsx', - 'deleted file mode 100644', - 'index 9a9b7aa..0000000', - '--- a/apps/web/components/kanban-column.tsx', - '+++ /dev/null', - '@@ -1,5 +0,0 @@', - '-export function KanbanColumn() {', - '- return (', - '-
Column
', - '- );', - '-}', - ].join('\n'), - newContent: '', - }, -}; - -const COMMANDS = [ - { id: 'dev', label: 'npm run dev' }, - { id: 'lint', label: 'npm run lint' }, - { id: 'test', label: 'npm run test' }, -]; - -const FILE_TREE = [ - [ - 'app', - ['api', ['hello', ['route.ts']], 'page.tsx', 'layout.tsx', ['blog', ['page.tsx']]], - ], - ['components', ['ui', 'button.tsx', 'card.tsx'], 'header.tsx', 'footer.tsx'], - ['lib', ['util.ts']], - ['public', 'favicon.ico', 'vercel.svg'], - '.eslintrc.json', - '.gitignore', - 'next.config.js', - 'tailwind.config.js', - 'package.json', - 'README.md', -]; - -type TreeItem = string | TreeItem[]; - -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), - }; -}; - -function Tree({ item }: { item: TreeItem }) { - const [name, ...items] = Array.isArray(item) ? item : [item]; - - if (!items.length) { - return ( - - - {name} - - ); - } - - return ( - - - - - - - {name} - - - - - {items.map((subItem, index) => ( - - ))} - - - - - ); -} - -function buildDiffData(filePath: string) { - const sample = DIFF_SAMPLES[filePath]; - if (!sample) { - return { - hunks: [ - [ - `diff --git a/${filePath} b/${filePath}`, - 'index 0000000..0000000 100644', - `--- a/${filePath}`, - `+++ b/${filePath}`, - '@@ -1,1 +1,1 @@', - '-', - '+', - ].join('\n'), - ], - oldFile: { fileName: filePath, fileLang: 'ts' }, - newFile: { fileName: filePath, fileLang: 'ts' }, - }; - } - - return { - hunks: [sample.diff], - oldFile: { fileName: filePath, fileLang: 'ts' }, - newFile: { fileName: filePath, fileLang: 'ts' }, - }; -} export default function TaskPage() { const defaultHorizontalLayout: [number, number] = [75, 25]; - const defaultRightLayout: [number, number] = [55, 45]; - const defaultDiffMode: 'unified' | 'split' = 'unified'; const [horizontalLayout, setHorizontalLayout] = useState(defaultHorizontalLayout); - const [rightLayout, setRightLayout] = useState(defaultRightLayout); const [horizontalSeed, setHorizontalSeed] = useState(0); - const [rightSeed, setRightSeed] = useState(0); - const [selectedAgent, setSelectedAgent] = useState(AGENTS[0].id); const [chats, setChats] = useState(INITIAL_CHATS); const [leftTab, setLeftTab] = useState('chat'); const [selectedDiffPath, setSelectedDiffPath] = useState(null); - 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 [diffViewMode, setDiffViewMode] = useState<'unified' | 'split'>(defaultDiffMode); - const [planModeEnabled, setPlanModeEnabled] = useState(false); - const { resolvedTheme } = useTheme(); const activeChat = chats[0]; - const selectedDiffLabel = selectedDiffPath ?? 'All files'; - const diffTargets = useMemo( - () => (selectedDiffPath ? [selectedDiffPath] : CHANGED_FILES.map((file) => file.path)), - [selectedDiffPath] - ); - const diffModeEnum = diffViewMode === 'split' ? DiffModeEnum.Split : DiffModeEnum.Unified; - const diffTheme = resolvedTheme === 'dark' ? 'dark' : 'light'; - const isSingleDiffSelected = Boolean(selectedDiffPath && DIFF_SAMPLES[selectedDiffPath]); - const selectedDiffContent = selectedDiffPath - ? DIFF_SAMPLES[selectedDiffPath]?.newContent ?? '' - : ''; useEffect(() => { const storedHorizontal = getLocalStorage('task-layout-horizontal', defaultHorizontalLayout); - const storedRight = getLocalStorage('task-layout-right', defaultRightLayout); - const storedDiffMode = getLocalStorage('task-diff-view-mode', defaultDiffMode); if ( storedHorizontal[0] !== defaultHorizontalLayout[0] || @@ -373,330 +71,33 @@ export default function TaskPage() { setHorizontalLayout(storedHorizontal); setHorizontalSeed((value) => value + 1); } - - if (storedRight[0] !== defaultRightLayout[0] || storedRight[1] !== defaultRightLayout[1]) { - setRightLayout(storedRight); - setRightSeed((value) => value + 1); - } - - if (storedDiffMode !== defaultDiffMode) { - setDiffViewMode(storedDiffMode); - } }, []); - const handleSubmit = (event: FormEvent) => { - event.preventDefault(); - const trimmed = messageInput.trim(); - if (!trimmed) return; + const handleSendMessage = useCallback((content: string) => { setChats((currentChats) => { const [first, ...rest] = currentChats; if (!first) return currentChats; return [ { ...first, - messages: [ - ...first.messages, - { id: crypto.randomUUID(), role: 'user', content: trimmed }, - ], + messages: [...first.messages, { id: crypto.randomUUID(), role: 'user', content }], }, ...rest, ]; }); - setMessageInput(''); - }; + }, []); - const addTerminal = () => { - setTerminalIds((ids) => { - const nextId = Math.max(0, ...ids) + 1; - setActiveTerminalId(nextId); - return [...ids, nextId]; - }); - }; + const handleSelectDiffPath = useCallback((path: string) => { + setSelectedDiffPath(path); + setLeftTab('changes'); + }, []); - 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}`; - const topFilesPanel = ( -
- setTopTab(value as typeof topTab)} - className="flex-1 min-h-0" - > - - - Diff files - - - All files - - - -
-
    - {CHANGED_FILES.map((file) => { - const { folder, file: name } = splitPath(file.path); - return ( -
  • { - setSelectedDiffPath(file.path); - setLeftTab('changes'); - }} - > - -
    -
    - - - - - Discard changes - - - - - - Open in editor - -
    - - {file.status} -
    -
  • - ); - })} -
-
-
- -
- - - - - - - {FILE_TREE.map((item, index) => ( - - ))} - - - - - - - -
-
-
-
- ); + const topFilesPanel = ; return (
-
-
- - Task details -
-
- - {isEditingBranch ? ( - setBranchName(event.target.value)} - onBlur={() => setIsEditingBranch(false)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === 'Escape') { - setIsEditingBranch(false); - } - }} - autoFocus - /> - ) : ( - <> - {branchName} -
- - - - - Edit branch name - - - - - - Copy branch name - -
- - )} -
- - -
-
- - - - - - - Commits ahead of base - - - - - - - - Commits behind base - -
- - - - - - - Lines changed - -
-
- - - - - Open in editor - - - - - - Pull from remote - - - - - - Open review - -
- - - - - Create PR - - - - - - - - - Create PR manually - - - - Merge - - - - Rebase - - - -
-
-
+
-
-
- - {selectedDiffLabel} - -
- {selectedDiffPath && ( - - - - - Show all changes - - )} - - - - - - - Copy file contents - - - - - - - - Discard changes - -
- - - - - - - Unified view - - - - - - - - Split view - -
-
-
-
-
- {diffTargets.map((path) => ( -
- {!selectedDiffPath && ( -
- - {path} - -
- )} - -
- ))} -
-
-
+ setSelectedDiffPath(null)} + />
-
- {(activeChat?.messages ?? []).map((message) => ( -
-

- {message.role === 'user' ? 'You' : 'Agent'} -

-

{message.content}

-
- ))} -
-
-