From 44abff649306fdbc55fbb243314134c2c237420f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Thu, 15 Jan 2026 02:55:50 +0000 Subject: [PATCH] feat: improved chat ui topbar --- apps/web/app/globals.css | 22 +- apps/web/app/task/[id]/page-client.tsx | 361 ++-------------- apps/web/components/diff-stat.tsx | 2 +- apps/web/components/empty-state-inline.tsx | 19 + apps/web/components/task-create-dialog.tsx | 255 +++++++++--- apps/web/components/task/file-browser.tsx | 7 +- .../web/components/task/sessions-dropdown.tsx | 248 +++++++++++ apps/web/components/task/task-chat-input.tsx | 4 +- apps/web/components/task/task-files-panel.tsx | 57 ++- apps/web/components/task/task-layout.tsx | 81 ++++ apps/web/components/task/task-left-panel.tsx | 186 +++++++++ apps/web/components/task/task-top-bar.tsx | 392 +++++++++++------- apps/web/hooks/use-task-agent.ts | 113 +++++ apps/web/hooks/use-task-comments.ts | 72 ++++ 14 files changed, 1241 insertions(+), 578 deletions(-) create mode 100644 apps/web/components/empty-state-inline.tsx create mode 100644 apps/web/components/task/sessions-dropdown.tsx create mode 100644 apps/web/components/task/task-layout.tsx create mode 100644 apps/web/components/task/task-left-panel.tsx create mode 100644 apps/web/hooks/use-task-agent.ts create mode 100644 apps/web/hooks/use-task-comments.ts diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 7cedbeca6c..bee55cc7b0 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -138,6 +138,15 @@ .kanban-task-pulse { animation: kanban-task-pulse 5s ease-in-out infinite; } + + .scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + } + + .scrollbar-hide::-webkit-scrollbar { + display: none; + } } @keyframes kanban-task-pulse { @@ -256,21 +265,22 @@ } .task-chat-editor .cm-editor { - background: transparent; - box-shadow: none; - outline: none; + background: transparent !important; + box-shadow: none !important; + outline: none !important; } .task-chat-editor .cm-editor.cm-focused { - outline: none; + outline: none !important; } .task-chat-editor .cm-scroller, .task-chat-editor .cm-content, .task-chat-editor .cm-line, .task-chat-editor .cm-line span { - background: transparent; - color: var(--foreground); + background: transparent !important; + background-color: transparent !important; + color: var(--foreground) !important; } .task-chat-editor .cm-cursor { diff --git a/apps/web/app/task/[id]/page-client.tsx b/apps/web/app/task/[id]/page-client.tsx index 2f3e2ddf81..268d562319 100644 --- a/apps/web/app/task/[id]/page-client.tsx +++ b/apps/web/app/task/[id]/page-client.tsx @@ -2,154 +2,36 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import '@git-diff-view/react/styles/diff-view.css'; -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@kandev/ui/resizable'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@kandev/ui/tabs'; import { TooltipProvider } from '@kandev/ui/tooltip'; -import { Textarea } from '@kandev/ui/textarea'; -import { IconX } from '@tabler/icons-react'; -import { getLocalStorage, setLocalStorage } from '@/lib/local-storage'; -import { TaskChatPanel } from '@/components/task/task-chat-panel'; import { TaskTopBar } from '@/components/task/task-top-bar'; +import { TaskLayout } from '@/components/task/task-layout'; import type { Task } from '@/lib/types/http'; -import type { OpenFileTab } from '@/lib/types/backend'; -import { FILE_EXTENSION_COLORS } from '@/lib/types/backend'; -import { TaskFilesPanel } from '@/components/task/task-files-panel'; -import { TaskChangesPanel } from '@/components/task/task-changes-panel'; -import { TaskRightPanel } from '@/components/task/task-right-panel'; -import { FileViewerContent } from '@/components/task/file-viewer-content'; import { getWebSocketClient } from '@/lib/ws/connection'; -import { useAppStore, useAppStoreApi } from '@/components/state-provider'; import { useRepositories } from '@/hooks/use-repositories'; -import { useRepositoryBranches } from '@/hooks/use-repository-branches'; -import { listTaskComments } from '@/lib/http'; - -const AGENTS = [ - { id: 'codex', label: 'Codex' }, - { id: 'claude', label: 'Claude Code' }, -]; +import { useTaskAgent } from '@/hooks/use-task-agent'; +import { useTaskComments } from '@/hooks/use-task-comments'; type TaskPageClientProps = { task: Task | null; }; -const DEFAULT_HORIZONTAL_LAYOUT: [number, number] = [75, 25]; - export default function TaskPage({ task: initialTask }: TaskPageClientProps) { - const store = useAppStoreApi(); - const commentsState = useAppStore((state) => state.comments); const [isMounted, setIsMounted] = useState(false); - const [horizontalLayout, setHorizontalLayout] = useState<[number, number]>( - getLocalStorage('task-layout-horizontal', DEFAULT_HORIZONTAL_LAYOUT) - ); - const [leftTab, setLeftTab] = useState('chat'); - const [selectedDiffPath, setSelectedDiffPath] = useState(null); - const [notes, setNotes] = useState(''); - const [isLoadingComments, setIsLoadingComments] = useState(false); - const [isAgentRunning, setIsAgentRunning] = useState(false); - const [isAgentLoading, setIsAgentLoading] = useState(false); - // File viewer tabs - const [openFileTabs, setOpenFileTabs] = useState([]); - // Track worktree info separately since it's populated after agent starts - const [worktreePath, setWorktreePath] = useState(initialTask?.worktree_path ?? null); - const [worktreeBranch, setWorktreeBranch] = useState(initialTask?.worktree_branch ?? null); - // Use task from props but allow updates const task = initialTask; - // Track task state from store (kanban.tasks) to determine if agent is running - useAppStore((state) => state.kanban.tasks.find((t) => t.id === task?.id)); + // Custom hooks for state management + const { isAgentRunning, isAgentLoading, worktreePath, worktreeBranch, handleStartAgent, handleStopAgent } = useTaskAgent(task); + const { isLoading: isLoadingComments } = useTaskComments(task?.id ?? null); + useEffect(() => { - setIsMounted(true); + queueMicrotask(() => setIsMounted(true)); }, []); - // Fetch task execution status from orchestrator on mount - useEffect(() => { - if (!task?.id) return; - - const checkExecution = async () => { - const client = getWebSocketClient(); - if (!client) return; - - try { - const response = await client.request<{ - has_execution: boolean; - task_id: string; - status?: string; - }>('task.execution', { task_id: task.id }); - - console.log('[TaskPage] Task execution check:', response); - if (response.has_execution) { - setIsAgentRunning(true); - } - } catch (err) { - console.error('[TaskPage] Failed to check task execution:', err); - } - }; - - checkExecution(); - }, [task?.id]); - const { repositories } = useRepositories(task?.workspace_id ?? null, Boolean(task?.workspace_id)); const repository = useMemo( () => repositories.find((item) => item.local_path === task?.repository_url) ?? null, [repositories, task?.repository_url] ); - const { branches, isLoading: branchesLoading } = useRepositoryBranches( - repository?.id ?? null, - Boolean(repository?.id) - ); - - // Fetch comments on mount and when task changes - useEffect(() => { - if (!task?.id) return; - - // Set taskId immediately so that incoming WebSocket notifications are processed - // before the API call completes (fixes race condition on first agent start) - store.getState().setCommentsTaskId(task.id); - - // Clear git status when switching tasks to avoid showing stale data - store.getState().clearGitStatus(); - - if (commentsState.taskId === task.id) { - return; - } - - const fetchComments = async () => { - setIsLoadingComments(true); - store.getState().setCommentsLoading(true); - - try { - const response = await listTaskComments(task.id, { limit: 10, sort: 'desc' }); - const orderedComments = [...(response.comments ?? [])].reverse(); - store.getState().setComments(task.id, orderedComments, { - hasMore: response.has_more, - oldestCursor: response.cursor || (orderedComments[0]?.id ?? null), - }); - } catch (error) { - console.error('Failed to fetch comments:', error); - store.getState().setComments(task.id, [], { hasMore: false, oldestCursor: null }); - } finally { - setIsLoadingComments(false); - } - }; - - fetchComments(); - }, [commentsState.taskId, task?.id, store]); - - // Subscribe to task for real-time updates - useEffect(() => { - if (!task?.id) return; - - const client = getWebSocketClient(); - if (!client) return; - - // Subscribe to task updates - client.subscribe(task.id); - - return () => { - // Unsubscribe when leaving - client.unsubscribe(task.id); - }; - }, [task?.id]); const handleSendMessage = useCallback(async (content: string) => { if (!task?.id) return; @@ -162,93 +44,7 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) { } catch (error) { console.error('Failed to send comment:', error); } - }, [task?.id]); - - const handleSelectDiffPath = useCallback((path: string) => { - setSelectedDiffPath(path); - setLeftTab('changes'); - }, []); - - const handleOpenFile = useCallback((file: OpenFileTab) => { - setOpenFileTabs((prev) => { - // If file is already open, just switch to it - if (prev.some((t) => t.path === file.path)) { - return prev; - } - // Add new tab (LRU eviction at max 4) - const maxTabs = 4; - const newTabs = prev.length >= maxTabs ? [...prev.slice(1), file] : [...prev, file]; - return newTabs; - }); - setLeftTab(`file:${file.path}`); - }, []); - - const handleCloseFileTab = useCallback((path: string) => { - setOpenFileTabs((prev) => prev.filter((t) => t.path !== path)); - // If closing the active tab, switch to chat - if (leftTab === `file:${path}`) { - setLeftTab('chat'); - } - }, [leftTab]); - - const handleStartAgent = useCallback(async () => { - if (!task?.id) return; - - const client = getWebSocketClient(); - if (!client) return; - - // Require agent_profile_id to be set on the task - if (!task.agent_profile_id) { - console.error('No agent profile configured for this task. Please edit the task to select an agent profile.'); - return; - } - - setIsAgentLoading(true); - try { - interface StartResponse { - success: boolean; - task_id: string; - agent_instance_id: string; - status: string; - worktree_path?: string; - worktree_branch?: string; - } - const response = await client.request('orchestrator.start', { - task_id: task.id, - agent_profile_id: task.agent_profile_id, - }, 15000); - setIsAgentRunning(true); - - // Update worktree info from response - if (response?.worktree_path) { - setWorktreePath(response.worktree_path); - setWorktreeBranch(response.worktree_branch ?? null); - } - } catch (error) { - console.error('Failed to start agent:', error); - } finally { - setIsAgentLoading(false); - } - }, [task?.id, task?.agent_profile_id]); - - const handleStopAgent = useCallback(async () => { - if (!task?.id) return; - - const client = getWebSocketClient(); - if (!client) return; - - setIsAgentLoading(true); - try { - await client.request('orchestrator.stop', { task_id: task.id }, 15000); - setIsAgentRunning(false); - } catch (error) { - console.error('Failed to stop agent:', error); - } finally { - setIsAgentLoading(false); - } - }, [task?.id]); - - const topFilesPanel = ; + }, [task]); if (!isMounted) { return
; @@ -257,126 +53,27 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) { return (
- + -
- { - setHorizontalLayout(sizes as [number, number]); - setLocalStorage('task-layout-horizontal', sizes); - }} - > - -
- setLeftTab(value)} - className="flex-1 min-h-0 flex flex-col" - > -
- - - Notes - - - All changes - - - Chat - - - {openFileTabs.length > 0 && ( - <> -
- - {openFileTabs.map((tab) => { - const ext = tab.name.split('.').pop()?.toLowerCase() || ''; - const dotColor = FILE_EXTENSION_COLORS[ext] || 'bg-muted-foreground'; - return ( - - - {tab.name} - - - ); - })} - - - )} -
- - -