From 3d32797f6f4614205debf8a61f09e950f5f9a81c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 29 Aug 2026 16:38:04 -0700 Subject: [PATCH] fix(web): unify activity logs and composer banners (#8693) --- apps/web/src/components/BranchToolbar.tsx | 6 +- .../web/src/components/ChatView.logic.test.ts | 19 - apps/web/src/components/ChatView.logic.ts | 11 - apps/web/src/components/ChatView.tsx | 149 ++--- apps/web/src/components/chat/ChatComposer.tsx | 391 +++++++------ .../chat/ComposerActivityStatus.tsx | 106 ++++ .../src/components/chat/ComposerBanner.tsx | 330 +++++++++++ .../chat/ComposerBannerStack.test.tsx | 108 ---- .../components/chat/ComposerBannerStack.tsx | 306 +++++----- .../chat/ComposerCommandMenu.test.tsx | 2 - .../components/chat/ComposerCommandMenu.tsx | 7 +- .../chat/ComposerPendingApprovalPanel.tsx | 4 +- .../chat/ComposerPendingUserInputPanel.tsx | 67 +-- .../chat/ComposerPlanFollowUpBanner.tsx | 17 +- .../chat/ComposerServerUpdateStatus.tsx | 49 ++ .../chat/ComposerStashBadge.test.tsx | 68 --- .../components/chat/ComposerStashBadge.tsx | 54 +- .../chat/ComposerStashMenu.test.tsx | 44 -- .../src/components/chat/ComposerStashMenu.tsx | 168 +++--- .../src/components/chat/ComposerSurface.tsx | 99 ++++ .../chat/ComposerTasksBadge.test.tsx | 190 ------- .../components/chat/ComposerTasksBadge.tsx | 337 ++++++----- .../chat/MessagesTimeline.logic.test.ts | 61 +- .../components/chat/MessagesTimeline.logic.ts | 159 ++---- .../components/chat/MessagesTimeline.test.tsx | 29 +- .../src/components/chat/MessagesTimeline.tsx | 186 +------ .../chat/ThreadSyncStatusPill.test.tsx | 22 - .../components/chat/ThreadSyncStatusPill.tsx | 19 - apps/web/src/index.css | 524 ------------------ apps/web/src/session-logic.test.ts | 28 + apps/web/src/session-logic.ts | 1 + apps/web/src/versionSkew.test.ts | 36 ++ apps/web/src/versionSkew.ts | 13 + 33 files changed, 1511 insertions(+), 2099 deletions(-) create mode 100644 apps/web/src/components/chat/ComposerActivityStatus.tsx create mode 100644 apps/web/src/components/chat/ComposerBanner.tsx delete mode 100644 apps/web/src/components/chat/ComposerBannerStack.test.tsx create mode 100644 apps/web/src/components/chat/ComposerServerUpdateStatus.tsx delete mode 100644 apps/web/src/components/chat/ComposerStashBadge.test.tsx create mode 100644 apps/web/src/components/chat/ComposerSurface.tsx delete mode 100644 apps/web/src/components/chat/ComposerTasksBadge.test.tsx delete mode 100644 apps/web/src/components/chat/ThreadSyncStatusPill.test.tsx delete mode 100644 apps/web/src/components/chat/ThreadSyncStatusPill.tsx diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index 5d11cce11fbe61c483d1013cc8daeae59e94fbdb..8a4dc581992325a5f17c86468d567a88bcb51cf5 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -40,6 +40,7 @@ import { MenuTrigger, } from "./ui/menu"; import { Separator } from "./ui/separator"; +import { ComposerSurface } from "./chat/ComposerSurface"; interface BranchToolbarProps { environmentId: EnvironmentId; @@ -466,10 +467,9 @@ export const BranchToolbar = memo(function BranchToolbar({ if (!hasActiveThread || !activeProject) return null; return ( -
{isMobile && showGitControls ? ( ) : null} -
+ ); }); diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 5ff86bb2381b3a1880973148ed73d28725cfa3f0..6586c62d2f130cf698356534a6d62bc20d3324d1 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -32,7 +32,6 @@ import { resolveSendEnvMode, resolveDraftHeroState, scheduleEnvironmentReconnectWarning, - shoulderTabReserve, startNewThreadForProject, shouldDockDraftHeroForSubmission, shouldReleaseTimelineAnchorForToolActivity, @@ -46,24 +45,6 @@ const projectId = ProjectId.make("project-1"); const threadId = ThreadId.make("thread-1"); const now = "2026-03-29T00:00:00.000Z"; -describe("shoulderTabReserve", () => { - it("ignores the top drawer when measuring the shoulder tab band", () => { - const elementAt = (top: number) => ({ getBoundingClientRect: () => ({ top }) }) as HTMLElement; - const elements = new Map([ - ['[data-chat-composer-form="true"]', elementAt(20)], - [".chat-composer-shoulder-tab", elementAt(100)], - ['[data-chat-composer-main-surface="true"]', elementAt(128)], - ]); - const overlay = { - querySelector: (selector: string) => elements.get(selector) ?? null, - } as HTMLElement; - - expect(shoulderTabReserve(overlay)).toBe(28); - elements.set(".chat-composer-tasks-tab", elementAt(100)); - expect(shoulderTabReserve(overlay)).toBe(0); - }); -}); - describe("draft hero submission transition", () => { it("does not dock the composer before a background submission", () => { expect( diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 25a49f3efa2cbf354a5a3ba28230d79d4fe5520b..b435e7021c9b37127acbbeb0725086f08470e9cc 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -39,17 +39,6 @@ export const ENVIRONMENT_RECONNECT_WARNING_GRACE_MS = 2_000; export const LastInvokedScriptByProjectSchema = Schema.Record(ProjectId, Schema.String); -export function shoulderTabReserve(overlay: HTMLElement): number { - if (overlay.querySelector(".chat-composer-tasks-tab")) return 0; - const tab = overlay.querySelector(".chat-composer-shoulder-tab"); - const surface = overlay.querySelector('[data-chat-composer-main-surface="true"]'); - if (!tab || !surface) return 0; - return Math.max( - 0, - Math.round(surface.getBoundingClientRect().top - tab.getBoundingClientRect().top), - ); -} - export function shouldDockDraftHeroForSubmission(input: { isDraftHeroState: boolean; activeThreadKey: string | null; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index a027a1848b30f269be0b48a358595b38a606c8c2..7f953cfd8300cb2933c3f544964c66663e611fd9 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -183,6 +183,7 @@ import { CheckCircle2Icon, ChevronDownIcon, GitBranchIcon, + InfoIcon, Minimize2Icon, PaperclipIcon, WifiOffIcon, @@ -316,14 +317,14 @@ import { threadChangeRequestSnapshotsAtom, useLinkedThreadPullRequest, } from "./ThreadStatusIndicators"; -import { ComposerBannerStack, type ComposerBannerStackItem } from "./chat/ComposerBannerStack"; +import type { ComposerBannerStackItem } from "./chat/ComposerBannerStack"; +import { ComposerSurface } from "./chat/ComposerSurface"; import { hasAvailableClaudeCompactionProvider, hasDismissedResumeCompaction, shouldOfferResumeCompaction, } from "./chat/ContextWindowMeter.logic"; import { deriveLatestContextWindowSnapshot, formatContextWindowTokens } from "../lib/contextWindow"; -import { ThreadSyncStatusPill } from "./chat/ThreadSyncStatusPill"; import { DRAFT_HERO_TRANSITION_ANIMATION_ID, DRAFT_HERO_TRANSITION_DURATION_MS, @@ -350,7 +351,6 @@ import { shouldDockDraftHeroForSubmission, shouldReleaseTimelineAnchorForToolActivity, shouldShowBranchMismatchBanner, - shoulderTabReserve, shouldShowPlanFollowUpPrompt, getStartedThreadModelChangeBlockReason, LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, @@ -401,10 +401,13 @@ import { AlertDialogTitle, } from "./ui/alert-dialog"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; -import { ServerUpdateAction, ServerUpdateProgress } from "./ServerUpdateAction"; +import { ServerUpdateAction } from "./ServerUpdateAction"; +import { ComposerServerUpdateStatus } from "./chat/ComposerServerUpdateStatus"; import { buildVersionMismatchDismissalKey, + dismissServerUpdateFailure, dismissVersionMismatch, + isServerUpdateFailureDismissed, isVersionMismatchDismissed, resolveServerConfigVersionMismatch, resolveServerSelfUpdateCapability, @@ -2156,6 +2159,12 @@ function ChatViewContent(props: ChatViewProps) { const serverUpdateState = useAtomValue( serverEnvironment.updateStateAtom(serverUpdateEnvironmentId), ); + const [dismissedServerUpdateState, setDismissedServerUpdateState] = useState< + typeof serverUpdateState | null + >(null); + const serverUpdateFailureDismissed = + serverUpdateState === dismissedServerUpdateState || + isServerUpdateFailureDismissed(serverUpdateState); const systemComposerBannerItems = useMemo(() => { const items: ComposerBannerStackItem[] = []; const updateRunning = serverUpdateState.status === "running"; @@ -2183,8 +2192,8 @@ function ChatViewContent(props: ChatViewProps) { items.push({ id: `environment-unavailable:${activeEnvironmentUnavailableState.environmentId}`, variant: "default", - // Live connection status: calm styling, but it must front the stack. - urgent: true, + // Prioritize live connection progress among the notices. + priority: "urgent", icon: (