Add background tasks UI with sidebar rows, bottom panel, and log tailing#40
Add background tasks UI with sidebar rows, bottom panel, and log tailing#40anglinb wants to merge 4 commits intojakemor:mainfrom
Conversation
Replaces the "diffs coming soon" placeholder in the right sidebar with a full unified diff viewer that shows the live `git diff HEAD` for the current project repo. Components: unified diff renderer with collapsible file cards, line-level word-diff highlighting (via the `diff` npm package), and an inline commenting system with thread support and copy-as-prompt for sending review comments back to chat. Wiring: new `git.diff` WebSocket command executes `git diff HEAD` on the server, `useGitDiff` hook fetches on sidebar open, and a refresh button lets users re-fetch manually. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a "Send all" button next to "Copy all" in the diff viewer toolbar. Clicking it sends a message to the active chat with all comment threads formatted as "Address these comments:" followed by the structured prompt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…og tailing Adds first-class background task tracking and management to the UI. When Claude runs bash commands with run_in_background, they are now visible, browsable, and controllable without leaving the chat interface. **Server-side:** - BackgroundTaskRegistry: persistent in-memory registry that tracks tasks independently of draining state, with per-task file-growth polling to detect stopped tasks and instant detection via KillShell tool results - TaskOutputManager: file tailer that streams output via WebSocket subscription - New protocol: task-output subscription topic with snapshot + delta events - Agent scans transcript for background task tool results and registers them **Client-side:** - Inline sidebar rows: running tasks appear indented under their parent chat with a green spinner and truncated command, newest first - Bottom TaskPanel: slides up like the terminal, scoped to the active chat, with a task list (running + collapsed terminated section) and live log viewer showing newest lines on top - Activity button in navbar with running task count badge - Stop action sends a chat message to Claude to use KillShell - Task lifecycle fully per-task: no cross-chat interference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR introduces a full background task monitoring system: a server-side Two server-side bugs need attention before merging:
Confidence Score: 4/5Safe to merge after fixing the two server-side P1 bugs; client-side UI is solid and well-scoped. Two P1 issues exist: a file watcher resource leak on WebSocket close and a byte/character index mismatch that corrupts streaming deltas for non-ASCII output. Both are in newly added server files and are straightforward to fix, but they will manifest in normal usage (any browser tab close, any non-ASCII command output). src/server/task-output-manager.ts and src/server/ws-router.ts require fixes before merging. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant WsRouter
participant TaskOutputManager
participant BackgroundTaskRegistry
participant AgentCoordinator
Client->>WsRouter: subscribe {type:"task-output", taskId, outputPath}
WsRouter->>TaskOutputManager: startTailing(taskId, outputPath)
TaskOutputManager-->>WsRouter: initialContent (snapshot)
WsRouter-->>Client: snapshot {type:"task-output", content}
Note over TaskOutputManager: watchFile polls every 500ms
TaskOutputManager->>TaskOutputManager: checkForNewContent()
TaskOutputManager->>WsRouter: onEvent(TaskOutputEvent)
WsRouter-->>Client: event {type:"task.output", data}
Client->>WsRouter: unsubscribe id
WsRouter->>TaskOutputManager: stopTailing(taskId)
Note over WsRouter: handleClose does NOT call stopTailing
Client--xWsRouter: connection drops (no unsubscribe)
WsRouter->>WsRouter: handleClose — only removes socket
Note over TaskOutputManager: watchFile leaks
AgentCoordinator->>BackgroundTaskRegistry: scanAndRegister(chatId, messages)
BackgroundTaskRegistry->>BackgroundTaskRegistry: register(taskId, outputPath)
BackgroundTaskRegistry->>WsRouter: onChange() → broadcastSnapshots()
WsRouter-->>Client: sidebar snapshot with backgroundTasks[]
|
| try { | ||
| const stats = await stat(entry.outputPath) | ||
| if (stats.size <= entry.lastSize) return | ||
|
|
||
| const content = await readFile(entry.outputPath, "utf-8") | ||
| const newData = content.slice(entry.lastSize) | ||
| entry.lastSize = content.length |
There was a problem hiding this comment.
Byte vs. character-index mismatch corrupts delta output
entry.lastSize is initialised with Buffer.byteLength(initialContent, "utf-8") (bytes) in startTailing. Here, stats.size (bytes) is correctly compared against it the first time, but content.slice(entry.lastSize) treats it as a JavaScript character index. For any non-ASCII byte in the output (Unicode symbols, emojis, accented characters…) byte count ≠ character count, so the slice starts at the wrong position and emits garbage or duplicate characters. After the first update entry.lastSize is reset to content.length (character count), making the stats.size guard in the next poll also wrong.
Fix by tracking sizes consistently in bytes throughout, or keep the text-based approach and measure everything in characters: read the full file each time and compute content.slice(prevCharLength) using character counts consistently.
| ((event: TaskOutputEvent) => { | ||
| if (event.type === "task.output" && event.taskId === task.taskId) { | ||
| setLogContent((prev) => prev + event.data) | ||
| } | ||
| }) as any | ||
| ) |
There was a problem hiding this comment.
as any cast hiding a type mismatch in the event handler
The third argument to socket.subscribe is cast to any to silence a type error. This suggests KannaSocket.subscribe was not designed to accept an event-handler callback as a third argument, or the generic signature doesn't account for it. The cast prevents TypeScript from catching future breakage (e.g. if the event type changes). Consider extending KannaSocket.subscribe's signature to accept an optional event handler with the correct union type, or handle events through a separate onEvent callback.
| {chat.backgroundTasks.filter(t => t.status === "stopped").length > 0 && ( | ||
| <div | ||
| className="pl-6 pr-1 py-0.5 text-[10px] text-muted-foreground/50 cursor-pointer hover:text-muted-foreground transition-colors" | ||
| onClick={() => { | ||
| const firstStopped = chat.backgroundTasks.find(t => t.status === "stopped") | ||
| if (firstStopped) onOpenTask?.(firstStopped) | ||
| }} | ||
| title="Delete chat" | ||
| > | ||
| <Archive className="size-3.5" /> | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| {chat.backgroundTasks.filter(t => t.status === "stopped").length} terminated | ||
| </div> |
There was a problem hiding this comment.
filter called redundantly on the same array
chat.backgroundTasks.filter(t => t.status === "stopped") is evaluated twice in adjacent render paths — once to check .length > 0 and again to get .length for the count. Derive stoppedTasks once at the top of the component render to avoid the duplicate pass.
…ant filter - TaskOutputManager: track file position in character counts consistently instead of mixing bytes (Buffer.byteLength) and characters (string.length), which corrupted delta output for non-ASCII content - TaskPanel/BackgroundTaskDrawer: replace `as any` cast with proper generic type parameters on socket.subscribe<TSnapshot, TEvent>() - ChatRow: derive runningTasks/stoppedTasks once instead of calling .filter() redundantly on the same array Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
BackgroundTaskRegistryon the server persistently tracks bash commands run withrun_in_background, independently of the SDK draining state. Per-task file-growth polling detects stopped tasks; KillShell tool results are detected instantly.TaskOutputManagertails the actual SDK output files viafs.watchFile, streamed to the client over atask-outputWebSocket subscription with initial snapshot + delta events.Test plan
npx tsc --noEmitpasses,bun testpasses (321/321, 1 pre-existing TerminalManager timeout)🤖 Generated with Claude Code