From 4dd9732a94e757d5403fe3e1f3cc673559bbd7c2 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 11 May 2026 16:34:25 -0400 Subject: [PATCH] feat(desktop): hoisted todo widget, JSON tool summaries, history grouping & timer fixes - Hoist todo to first-class widget (shadcn checkboxes, brand colors, no tool-accordion). Header derives label from active task; non-active rows fade. - Replace raw JSON dumps with structured key/value summaries via formatToolResultSummary; nested error extraction for clearer failures. - Fix loaded-session grouping: stitch interleaved assistant/tool iterations into one bubble instead of orphaned synthetic messages. - Stable tool/thinking timers via keyed registry so unmount/scroll doesn't reset elapsed counts; gate "running" on real live thread state. - Reorganize chat-only assistant-ui components under components/chat/. --- apps/desktop/src/app/artifacts/index.tsx | 22 +- apps/desktop/src/app/chat/composer/index.tsx | 63 ++-- .../app/chat/sidebar/session-actions-menu.tsx | 14 +- .../app/session/hooks/use-message-stream.ts | 19 +- .../src/app/shell/statusbar-controls.tsx | 18 +- .../components/assistant-ui/activity-timer.ts | 36 -- .../components/assistant-ui/attachment.tsx | 6 - .../assistant-ui/directive-text.tsx | 10 +- .../components/assistant-ui/markdown-text.tsx | 6 +- .../src/components/assistant-ui/reasoning.tsx | 14 - .../assistant-ui/streaming.test.tsx | 142 +++++++- .../src/components/assistant-ui/thread.tsx | 45 +-- .../src/components/assistant-ui/todo-tool.tsx | 94 +++++ .../components/assistant-ui/tool-fallback.tsx | 198 +++++------ .../components/assistant-ui/tool-group.tsx | 9 - .../activity-timer-text.tsx | 0 .../components/chat/activity-timer.test.tsx | 43 +++ .../src/components/chat/activity-timer.ts | 52 +++ .../compact-markdown.tsx | 0 .../{assistant-ui => chat}/disclosure-row.tsx | 0 .../generated-image-context.tsx | 0 .../image-generation-placeholder.tsx | 0 .../{assistant-ui => chat}/intro-copy.jsonl | 0 .../{assistant-ui => chat}/intro.tsx | 0 .../preview-attachment.tsx | 0 .../shiki-highlighter.tsx | 0 .../{assistant-ui => chat}/zoomable-image.tsx | 0 apps/desktop/src/components/model-picker.tsx | 4 +- apps/desktop/src/lib/chat-messages.test.ts | 104 ++++++ apps/desktop/src/lib/chat-messages.ts | 134 +++++-- apps/desktop/src/lib/todos.test.ts | 35 ++ apps/desktop/src/lib/todos.ts | 38 ++ .../src/lib/tool-result-summary.test.ts | 108 ++++++ apps/desktop/src/lib/tool-result-summary.ts | 326 ++++++++++++++++++ 34 files changed, 1234 insertions(+), 306 deletions(-) delete mode 100644 apps/desktop/src/components/assistant-ui/activity-timer.ts delete mode 100644 apps/desktop/src/components/assistant-ui/attachment.tsx delete mode 100644 apps/desktop/src/components/assistant-ui/reasoning.tsx create mode 100644 apps/desktop/src/components/assistant-ui/todo-tool.tsx delete mode 100644 apps/desktop/src/components/assistant-ui/tool-group.tsx rename apps/desktop/src/components/{assistant-ui => chat}/activity-timer-text.tsx (100%) create mode 100644 apps/desktop/src/components/chat/activity-timer.test.tsx create mode 100644 apps/desktop/src/components/chat/activity-timer.ts rename apps/desktop/src/components/{assistant-ui => chat}/compact-markdown.tsx (100%) rename apps/desktop/src/components/{assistant-ui => chat}/disclosure-row.tsx (100%) rename apps/desktop/src/components/{assistant-ui => chat}/generated-image-context.tsx (100%) rename apps/desktop/src/components/{assistant-ui => chat}/image-generation-placeholder.tsx (100%) rename apps/desktop/src/components/{assistant-ui => chat}/intro-copy.jsonl (100%) rename apps/desktop/src/components/{assistant-ui => chat}/intro.tsx (100%) rename apps/desktop/src/components/{assistant-ui => chat}/preview-attachment.tsx (100%) rename apps/desktop/src/components/{assistant-ui => chat}/shiki-highlighter.tsx (100%) rename apps/desktop/src/components/{assistant-ui => chat}/zoomable-image.tsx (100%) create mode 100644 apps/desktop/src/lib/todos.test.ts create mode 100644 apps/desktop/src/lib/todos.ts create mode 100644 apps/desktop/src/lib/tool-result-summary.test.ts create mode 100644 apps/desktop/src/lib/tool-result-summary.ts diff --git a/apps/desktop/src/app/artifacts/index.tsx b/apps/desktop/src/app/artifacts/index.tsx index 45219522c5..ba9697518a 100644 --- a/apps/desktop/src/app/artifacts/index.tsx +++ b/apps/desktop/src/app/artifacts/index.tsx @@ -2,7 +2,7 @@ import type * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { ZoomableImage } from '@/components/assistant-ui/zoomable-image' +import { ZoomableImage } from '@/components/chat/zoomable-image' import { PageLoader } from '@/components/page-loader' import { Button } from '@/components/ui/button' import { CopyButton } from '@/components/ui/copy-button' @@ -786,9 +786,6 @@ function ArtifactImageCard({ artifact, failedImage, onImageError, onOpenChat }: ) } -const CELL_ACTION_CLASS = - 'flex h-full w-full min-w-0 items-center gap-2 px-2.5 py-1.5 text-left text-sm leading-snug font-medium text-foreground/90 no-underline transition-colors hover:text-foreground hover:underline' - // Single click target for any row cell. External URLs render as ; // local actions render as ) diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 6265b3d2da..06959bddf4 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -50,9 +50,6 @@ import type { ChatBarProps } from './types' import { UrlDialog } from './url-dialog' import { VoiceActivity, VoicePlaybackActivity } from './voice-activity' -const COMPOSER_SHELL_CLASS = - 'group/composer absolute bottom-0 left-1/2 z-30 w-[min(var(--composer-width),calc(100%-2rem))] max-w-full -translate-x-1/2 rounded-2xl pt-2 pb-[var(--composer-shell-pad-block-end)]' - function extractClipboardImageBlobs(clipboard: DataTransfer): Blob[] { const blobs: Blob[] = [] const seen = new Set() @@ -111,21 +108,6 @@ function extractClipboardImageBlobs(clipboard: DataTransfer): Blob[] { const COMPOSER_STACK_BREAKPOINT_PX = 320 -const COMPOSER_SCROLLED_DIM_CLASS = - 'opacity-30 group-hover/composer:opacity-100 group-focus-within/composer:opacity-100' - -const COMPOSER_FROST_CLASS = cn( - 'pointer-events-none absolute inset-0 -z-10 rounded-[inherit]', - 'bg-[color-mix(in_srgb,var(--dt-card)_72%,transparent)]', - 'backdrop-blur-[0.75rem] backdrop-saturate-[1.12]', - '[-webkit-backdrop-filter:blur(0.75rem)_saturate(1.12)]', - 'transition-[background-color] duration-150 ease-out', - 'group-data-[thread-scrolled-up]/composer:bg-[color-mix(in_srgb,var(--dt-card)_48%,transparent)]', - 'group-focus-within/composer:bg-[var(--dt-card)]', - 'group-focus-within/composer:[backdrop-filter:none]', - 'group-focus-within/composer:[-webkit-backdrop-filter:none]' -) - const COMPOSER_GLASS = { fadeBackground: 'linear-gradient(to bottom, transparent, color-mix(in srgb, var(--dt-background) 10%, transparent))', liquidKey: ['standard', '0.950', '0.072', '0', '46', '0.00', '128'].join(':'), @@ -921,7 +903,7 @@ export function ChatBar({ <> -
+
{dragActive && (
@@ -1042,11 +1041,27 @@ export function ChatBar({ export function ChatBarFallback() { return (
-
+
) diff --git a/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx b/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx index d5217748a3..5c58e1b10f 100644 --- a/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx @@ -50,7 +50,6 @@ export function SessionActionsMenu({ align = 'end', sideOffset = 6 }: SessionActionsMenuProps) { - const itemClass = 'gap-2.5 text-foreground focus:bg-accent [&_svg]:size-4' const [renameOpen, setRenameOpen] = useState(false) return ( @@ -59,7 +58,7 @@ export function SessionActionsMenu({ {children} { triggerHaptic('selection') @@ -71,14 +70,14 @@ export function SessionActionsMenu({ { triggerHaptic('selection') @@ -89,7 +88,7 @@ export function SessionActionsMenu({ Export { triggerHaptic('selection') @@ -101,7 +100,10 @@ export function SessionActionsMenu({ { triggerHaptic('warning') diff --git a/apps/desktop/src/app/session/hooks/use-message-stream.ts b/apps/desktop/src/app/session/hooks/use-message-stream.ts index 5f0e52ee62..54cefc1be2 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream.ts @@ -62,6 +62,15 @@ interface QueuedStreamDeltas { const STREAM_DELTA_FLUSH_MS = 16 +// Anonymous progress events that carry todos but no name still belong to the +// todo stream; named todo events are obviously routed there too. +function toTodoPayload(payload: GatewayEventPayload | undefined): GatewayEventPayload | undefined { + if (!payload) {return undefined} + const isTodo = payload.name === 'todo' || (!payload.name && Object.hasOwn(payload, 'todos')) + + return isTodo ? { ...payload, name: 'todo', tool_id: payload.tool_id || 'todo-live' } : undefined +} + export function useMessageStream({ activeSessionIdRef, hydrateFromStoredSession, @@ -552,17 +561,13 @@ export function useMessageStream({ setCurrentUsage(current => ({ ...current, ...payload.usage })) } } else if (event.type === 'tool.start' || event.type === 'tool.progress' || event.type === 'tool.generating') { - if (!sessionId) { - return - } - + if (!sessionId) {return} flushQueuedDeltas(sessionId) - - upsertToolCall(sessionId, payload, 'running') + upsertToolCall(sessionId, toTodoPayload(payload) ?? payload, 'running') } else if (event.type === 'tool.complete') { if (sessionId) { flushQueuedDeltas(sessionId) - upsertToolCall(sessionId, payload, 'complete') + upsertToolCall(sessionId, toTodoPayload(payload) ?? payload, 'complete') } if (typeof payload?.inline_diff === 'string' && payload.inline_diff.trim()) { diff --git a/apps/desktop/src/app/shell/statusbar-controls.tsx b/apps/desktop/src/app/shell/statusbar-controls.tsx index 0c426e2379..6ffad250e1 100644 --- a/apps/desktop/src/app/shell/statusbar-controls.tsx +++ b/apps/desktop/src/app/shell/statusbar-controls.tsx @@ -43,9 +43,6 @@ interface StatusbarControlsProps extends ComponentProps<'footer'> { items?: readonly StatusbarItem[] } -const statusbarItemClass = - 'inline-flex h-full cursor-pointer items-center gap-1 rounded-none px-1.5 text-[0.68rem] text-muted-foreground/95 transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45' - export function StatusbarControls({ className, leftItems = [], items = [], ...props }: StatusbarControlsProps) { const navigate = useNavigate() @@ -91,7 +88,10 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate: