From 8892d2ae9b6fd1f46f41232682425747711f43d1 Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:58:25 +0000 Subject: [PATCH 1/2] fix(web): restore thread-header AI usage + host resource gauges Stack product rejoin re-landed HostResourceStatus and ai-usage helpers but dropped the ChatHeader wiring, so conversation-level usage dots and remote CPU/MEM/Load disappeared while settings still showed host metrics. Re-wire both surfaces and add anti-stack-drop existence tests. --- apps/web/src/components/ChatView.tsx | 3 + .../src/components/chat/ChatHeader.test.ts | 16 +++++ apps/web/src/components/chat/ChatHeader.tsx | 63 +++++++++++++++++++ apps/web/src/forkSurfaceExistence.test.ts | 25 ++++++++ 4 files changed, 107 insertions(+) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 66c6e900990..ef5e63f682e 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -6116,6 +6116,9 @@ function ChatViewContent(props: ChatViewProps) { availableEditors={availableEditors} rightPanelOpen={rightPanelOpen} gitCwd={gitCwd} + isPreparingWorktree={isPreparingWorktreeUi} + activeThreadDriverKind={activeThreadModelPresentation?.driverKind ?? null} + activeThreadModel={activeThread.modelSelection.model} onNewThreadInProject={handleStartNewThread} onRunProjectScript={runProjectScript} onAddProjectScript={saveProjectScript} diff --git a/apps/web/src/components/chat/ChatHeader.test.ts b/apps/web/src/components/chat/ChatHeader.test.ts index e4a320a308e..f13f2d31d4d 100644 --- a/apps/web/src/components/chat/ChatHeader.test.ts +++ b/apps/web/src/components/chat/ChatHeader.test.ts @@ -220,3 +220,19 @@ describe("ChatHeader remote Open in VS Code surface (anti stack-drop)", () => { expect(chatHeaderSource).toContain("VisualStudioCode"); }); }); + +describe("ChatHeader AI usage + host resources surface (anti stack-drop)", () => { + it("still wires conversation-level usage status and host resource gauges", () => { + // Stack product rejoins have previously re-landed pure helpers + settings + // HostResourceStatus while dropping the thread-header surfaces. Keep the + // JSX markers co-located with the pure gates so that cannot pass silently. + expect(chatHeaderSource).toContain("useAiUsageSnapshot"); + expect(chatHeaderSource).toContain("resolveDriverUsage"); + expect(chatHeaderSource).toContain('aria-label="provider usage status"'); + expect(chatHeaderSource).toContain("AiUsageStats"); + expect(chatHeaderSource).toContain("HostResourceStatus"); + expect(chatHeaderSource).toContain("isLocalConnectionTarget"); + expect(chatHeaderSource).toContain("activeThreadDriverKind"); + expect(chatHeaderSource).toContain("activeThreadModel"); + }); +}); diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index d1a8e97b124..40fbcf71f77 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -2,6 +2,7 @@ import { type EnvironmentId, type EditorId, type ProjectScript, + type ProviderDriverKind, type ResolvedKeybindingsConfig, type ThreadId, } from "@t3tools/contracts"; @@ -12,6 +13,7 @@ import { memo, useCallback, useMemo } from "react"; import GitActionsControl from "../GitActionsControl"; import { type DraftId } from "~/composerDraftStore"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { AiUsageStats } from "./AiUsageStats"; import ProjectScriptsControl, { type NewProjectScriptInput, type ProjectScriptActionResult, @@ -24,6 +26,10 @@ import { cn } from "~/lib/utils"; import { Button } from "../ui/button"; import { VisualStudioCode } from "../Icons"; import { readLocalApi } from "~/localApi"; +import { useAiUsageSnapshot } from "../../hooks/useAiUsageSnapshot"; +import { resolveDriverUsage, usageDotFillClass, usageDotRingColor } from "../../aiUsageState"; +import { HostResourceStatus } from "../HostResourceStatus"; +import { isLocalConnectionTarget } from "~/connection/desktopLocal"; interface ChatHeaderProps { activeThreadEnvironmentId: EnvironmentId; @@ -39,6 +45,10 @@ interface ChatHeaderProps { availableEditors: ReadonlyArray; rightPanelOpen: boolean; gitCwd: string | null; + isPreparingWorktree?: boolean; + /** For showing usage dot on the active thread's model at conversation level. */ + activeThreadDriverKind?: ProviderDriverKind | null; + activeThreadModel?: string | null; onNewThreadInProject: () => void; onRunProjectScript: (script: ProjectScript) => void; onAddProjectScript: (input: NewProjectScriptInput) => Promise; @@ -130,6 +140,9 @@ export const ChatHeader = memo(function ChatHeader({ availableEditors, rightPanelOpen, gitCwd, + isPreparingWorktree = false, + activeThreadDriverKind = null, + activeThreadModel = null, onNewThreadInProject, onRunProjectScript, onAddProjectScript, @@ -161,6 +174,15 @@ export const ChatHeader = memo(function ChatHeader({ if (!remoteVscodeTarget) return; void readLocalApi()?.shell.openExternal(remoteVscodeTarget.uri); }, [remoteVscodeTarget]); + + const aiUsageSnapshot = useAiUsageSnapshot(activeThreadEnvironmentId); + const headerUsage = useMemo( + () => resolveDriverUsage(aiUsageSnapshot, activeThreadDriverKind, activeThreadModel), + [aiUsageSnapshot, activeThreadDriverKind, activeThreadModel], + ); + const headerDotClass = headerUsage ? usageDotFillClass(headerUsage.marker) : undefined; + const headerRingColor = headerUsage ? usageDotRingColor(headerUsage.marker) : undefined; + return (
@@ -207,6 +229,46 @@ export const ChatHeader = memo(function ChatHeader({ /> {activeThreadTitle} + {headerDotClass && headerUsage ? ( + + + } + /> + + + + + ) : headerDotClass ? ( + + ) : null} +
)} diff --git a/apps/web/src/forkSurfaceExistence.test.ts b/apps/web/src/forkSurfaceExistence.test.ts index 9e86851b839..d86ef512b42 100644 --- a/apps/web/src/forkSurfaceExistence.test.ts +++ b/apps/web/src/forkSurfaceExistence.test.ts @@ -57,6 +57,31 @@ describe("fork surface existence (anti stack-drop)", () => { expect(header).toContain("shell.openExternal"); }); + it("chat header keeps AI usage status and host resource gauges", () => { + const header = readSrc("components/chat/ChatHeader.tsx"); + expect(header).toContain("HostResourceStatus"); + expect(header).toContain('aria-label="provider usage status"'); + expect(header).toContain("AiUsageStats"); + expect(header).toContain("useAiUsageSnapshot"); + const chatView = readSrc("components/ChatView.tsx"); + expect(chatView).toContain("activeThreadDriverKind="); + expect(chatView).toContain("activeThreadModel="); + expect(chatView).toContain("isPreparingWorktree="); + }); + + it("model picker keeps provider usage dots and selection-box stats", () => { + const providerPicker = readSrc("components/chat/ProviderModelPicker.tsx"); + expect(providerPicker).toContain("usageSnapshot"); + expect(providerPicker).toContain("statusDotClassName"); + expect(providerPicker).toContain("AiUsageStats"); + const modelPickerContent = readSrc("components/chat/ModelPickerContent.tsx"); + expect(modelPickerContent).toContain("resolveDriverUsages"); + expect(modelPickerContent).toContain("AiUsageStats"); + const modelPickerSidebar = readSrc("components/chat/ModelPickerSidebar.tsx"); + expect(modelPickerSidebar).toContain("statusDotClassName"); + expect(modelPickerSidebar).toContain("usageDotFillClass"); + }); + it("queued message chips keep edit + steer labels", () => { const chips = readSrc("components/chat/QueuedMessageChips.tsx"); expect(chips).toContain('aria-label="Edit queued message"'); From df6de5fbfffe5bac2eca4cc44e8b48d3b0e5dc76 Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:05:06 +0000 Subject: [PATCH 2/2] fix(web): restore AI usage dots on Sidebar V2 thread rows Classic sidebar already rendered provider usage markers on thread rows; Threads V2 only showed the provider icon. Wire the same ai-usage snapshot + statusDot markers (and tooltip stats) so both list modes stay parity. --- apps/web/src/components/SidebarV2.tsx | 34 ++++++++++++++++++++++- apps/web/src/forkSurfaceExistence.test.ts | 19 +++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index 3d7c3b9fc0c..0d4ce8dbabf 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -148,8 +148,11 @@ import { type SnoozePreset, } from "./Sidebar.snooze"; import { ProjectFavicon } from "./ProjectFavicon"; +import { AiUsageStats } from "./chat/AiUsageStats"; import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon"; import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils"; +import { resolveDriverUsage, usageDotFillClass, usageDotRingColor } from "../aiUsageState"; +import { useAiUsageSnapshot } from "../hooks/useAiUsageSnapshot"; import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../providerInstances"; import { primaryServerProvidersAtom } from "../state/server"; import { stackedThreadToast, toastManager } from "./ui/toast"; @@ -271,6 +274,9 @@ function SidebarV2ThreadTooltip({ modelInstanceId, modelLabel, branchMismatch, + usageDotClass, + usageRingColor, + threadUsage, }: { thread: SidebarThreadSummary; projectTitle: string | null; @@ -283,6 +289,9 @@ function SidebarV2ThreadTooltip({ threadBranch: string; currentBranch: string; } | null; + usageDotClass: string | undefined; + usageRingColor: string | undefined; + threadUsage: ReturnType; }) { return (
{modelLabel}
) : null} + {threadUsage ? ( +
+ +
+ ) : null} {thread.session?.lastError ? (
@@ -626,6 +642,13 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { const modelLabel = selectedModel ? getTriggerDisplayModelLabel(selectedModel) : thread.modelSelection.model; + const aiUsageSnapshot = useAiUsageSnapshot(thread.environmentId); + const threadUsage = useMemo( + () => resolveDriverUsage(aiUsageSnapshot, driverKind, thread.modelSelection.model), + [aiUsageSnapshot, driverKind, thread.modelSelection.model], + ); + const usageDotClass = threadUsage ? usageDotFillClass(threadUsage.marker) : undefined; + const usageRingColor = threadUsage ? usageDotRingColor(threadUsage.marker) : undefined; const isRemote = props.currentEnvironmentId !== null && thread.environmentId !== props.currentEnvironmentId; @@ -646,6 +669,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { modelInstanceId={modelInstanceId} modelLabel={modelLabel} branchMismatch={branchMismatch} + usageDotClass={usageDotClass} + usageRingColor={usageRingColor} + threadUsage={threadUsage} /> ); @@ -1118,11 +1144,17 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { ) : null} {driverKind ? ( - + ) : null} diff --git a/apps/web/src/forkSurfaceExistence.test.ts b/apps/web/src/forkSurfaceExistence.test.ts index d86ef512b42..e309da9083c 100644 --- a/apps/web/src/forkSurfaceExistence.test.ts +++ b/apps/web/src/forkSurfaceExistence.test.ts @@ -82,6 +82,25 @@ describe("fork surface existence (anti stack-drop)", () => { expect(modelPickerSidebar).toContain("usageDotFillClass"); }); + it("classic sidebar thread rows keep provider usage dots + stats", () => { + const sidebar = readSrc("components/Sidebar.tsx"); + expect(sidebar).toContain("useAiUsageSnapshot"); + expect(sidebar).toContain("resolveDriverUsage"); + expect(sidebar).toContain("usageDotFillClass"); + expect(sidebar).toContain("AiUsageStats"); + expect(sidebar).toContain("hasUsageMarker"); + }); + + it("Sidebar V2 thread rows keep provider usage dots + stats", () => { + const sidebarV2 = readSrc("components/SidebarV2.tsx"); + expect(sidebarV2).toContain("useAiUsageSnapshot"); + expect(sidebarV2).toContain("resolveDriverUsage"); + expect(sidebarV2).toContain("usageDotFillClass"); + expect(sidebarV2).toContain("statusDotClassName"); + expect(sidebarV2).toContain("AiUsageStats"); + expect(sidebarV2).toContain('aria-label": "provider usage status"'); + }); + it("queued message chips keep edit + steer labels", () => { const chips = readSrc("components/chat/QueuedMessageChips.tsx"); expect(chips).toContain('aria-label="Edit queued message"');