From 118f185893abe7e719e7565dde34047dced62005 Mon Sep 17 00:00:00 2001 From: jtenniswood Date: Wed, 2 Sep 2026 13:42:19 +0100 Subject: [PATCH 1/8] polish(studio): context meter moves into the composer toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The context strip leaves its floating spot above the composer and becomes the leftmost item in the toolbar row below the input, alongside the mode/ model/memory pills. The redundant 'approximate' tag goes — the ~ prefix on the percentage carries the approximation, and the doc comment keeps the full why. Typography now matches the toolbar: the pills' text size, colored like their value text (the On in Memory On). ChatInput takes the strip as a contextMeter slot, so the composer stays dumb and chat-view keeps ownership of the data. One consequence, deliberate: the toolbar row is hidden on phones (its controls live in the + sheet there), so the meter is desktop-only now. Co-Authored-By: Claude Fable 5 --- .../app/workspace/_components/chat-input.tsx | 14 ++++++++++- .../workspace/chat/_components/chat-view.tsx | 23 +++++++++++-------- .../chat/_components/context-meter.tsx | 20 ++++++++-------- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/studio/src/app/workspace/_components/chat-input.tsx b/studio/src/app/workspace/_components/chat-input.tsx index 863e38e045..371139ed23 100644 --- a/studio/src/app/workspace/_components/chat-input.tsx +++ b/studio/src/app/workspace/_components/chat-input.tsx @@ -20,7 +20,14 @@ import { Shield, SlidersHorizontal, } from "lucide-react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -63,6 +70,9 @@ interface ProjectItem { interface ChatInputProps { placeholder?: string; rows?: number; + /** The context-utilisation strip, rendered leftmost in the toolbar row + below the input (typography matches the pills' value text). */ + contextMeter?: ReactNode; projects?: ProjectItem[]; selectedProjectId?: string | null; onSelectProject?: (id: string | null) => void; @@ -1149,6 +1159,7 @@ function handleMenuNavKey( export function ChatInput({ placeholder: placeholderProp, + contextMeter, projects, selectedProjectId, onSelectProject, @@ -1670,6 +1681,7 @@ export function ChatInput({ container queries when THIS row runs narrow (a ~400px side-panel composer), independent of the viewport width. */}
+ {contextMeter} {projects && ( )}
- {/* B1.1: effective model + approximate context utilisation, - visible only when the window is known and tokens counted. */} - {contextInfo && contextInfo.contextWindow > 0 && ( - - )} onSteerQueued?.(id)} @@ -1318,6 +1308,19 @@ export function ChatView({ onPreviewAttachment={handlePreviewFile} focusKey={session.id} mobileDocked + // B1.1: effective model + approximate context utilisation, + // leftmost in the toolbar, visible only when the window is + // known and tokens counted. + contextMeter={ + contextInfo && contextInfo.contextWindow > 0 ? ( + + ) : undefined + } modelLockedLabel={ live ? session.model || "Auto-routed" : undefined } diff --git a/studio/src/app/workspace/chat/_components/context-meter.tsx b/studio/src/app/workspace/chat/_components/context-meter.tsx index 850dec4111..e5a7381b5a 100644 --- a/studio/src/app/workspace/chat/_components/context-meter.tsx +++ b/studio/src/app/workspace/chat/_components/context-meter.tsx @@ -1,13 +1,14 @@ "use client"; /** - * The slim context strip near the composer (B1.1): the session's effective - * model and an APPROXIMATE context-utilisation meter — cumulative input+output - * tokens this visit counted (summed from the runs' terminal usage frames) vs - * the model's `resolved_model.context_window`. Deliberately labeled - * approximate: the daemon's true compaction trigger also counts the system + * The slim context strip in the composer toolbar (B1.1): the session's + * effective model and an APPROXIMATE context-utilisation meter — cumulative + * input+output tokens this visit counted (summed from the runs' terminal usage + * frames) vs the model's `resolved_model.context_window`. The reading is + * approximate — the daemon's true compaction trigger also counts the system * prompt and tool schemas, which the client never sees, and a page reload - * loses the visit's running total. + * loses the visit's running total — which the `~` prefix conveys. Typography + * matches the toolbar pills' value text (the "On" in "Memory On"). */ import { cn } from "@/lib/utils"; @@ -45,8 +46,8 @@ export function ContextMeter({ if (fraction === null || inputTokens + outputTokens <= 0) return null; const percent = Math.round(fraction * 100); return ( -
- {modelLabel && {modelLabel}} +
+ {modelLabel && {modelLabel}} - - approximate -
); } From 5d96bb2e26c657307a39fe87df3ff8e5312cfc57 Mon Sep 17 00:00:00 2001 From: jtenniswood Date: Wed, 2 Sep 2026 13:47:24 +0100 Subject: [PATCH 2/8] test(studio): pin the composer-bar context meter's rendered contract Three render cases on top of the existing math pins: the ~percentage shows without a standalone approximate tag, the strip uses the toolbar pills' value typography (text-sm, muted), and it stays quiet until the visit has counted tokens. Co-Authored-By: Claude Fable 5 --- .../chat/_components/context-meter.test.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/studio/src/app/workspace/chat/_components/context-meter.test.ts b/studio/src/app/workspace/chat/_components/context-meter.test.ts index 332a8568ee..faf2aa2553 100644 --- a/studio/src/app/workspace/chat/_components/context-meter.test.ts +++ b/studio/src/app/workspace/chat/_components/context-meter.test.ts @@ -1,5 +1,7 @@ +import { render } from "@testing-library/react"; +import { createElement } from "react"; import { describe, expect, it } from "vitest"; -import { contextUtilisation } from "./context-meter"; +import { ContextMeter, contextUtilisation } from "./context-meter"; /** * Pins the context-meter math (B1.1): counted input+output tokens over the @@ -26,3 +28,41 @@ describe("contextUtilisation", () => { expect(contextUtilisation(-5, 100, 1_000)).toBeCloseTo(0.1); }); }); + +/** + * The rendered strip lives in the composer toolbar: the ~ prefix carries the + * approximation (no standalone "approximate" tag), and the text matches the + * toolbar pills' value typography (text-sm, muted — the "On" in "Memory On"). + */ +describe("ContextMeter render", () => { + const props = { + modelLabel: "claude-sonnet-5", + contextWindow: 400_000, + inputTokens: 80_000, + outputTokens: 8_000, + }; + + it("shows the ~percentage without an approximate tag", () => { + const { container } = render(createElement(ContextMeter, props)); + expect(container.textContent).toContain("~22% of context"); + expect(container.textContent).not.toContain("approximate"); + }); + + it("uses the toolbar pills' value typography", () => { + const { container } = render(createElement(ContextMeter, props)); + const root = container.firstElementChild; + expect(root?.className).toContain("text-sm"); + expect(root?.className).toContain("text-muted-foreground"); + }); + + it("stays quiet until this visit has counted tokens", () => { + const { container } = render( + createElement(ContextMeter, { + ...props, + inputTokens: 0, + outputTokens: 0, + }), + ); + expect(container.firstElementChild).toBeNull(); + }); +}); From f77e186c86be8505d7c3b4253e8e6bb6fbba28f2 Mon Sep 17 00:00:00 2001 From: jtenniswood Date: Wed, 2 Sep 2026 13:52:00 +0100 Subject: [PATCH 3/8] polish(studio): context meter docks right, reads N% used, explains itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The strip moves to the right end of the composer toolbar (ml-auto on the slot, rendered after the pills), drops the ~ prefix, and reads 'N% used'. The approximation caveat moves into a tooltip that also explains what a context window is — how much conversation the model can consider at once, and that the agent compacts older history as it fills. The bar graphic spans 40% of the toolbar's width (cqw against the row's existing @container). Render tests updated: plain 'N% used' with no tilde/of-context/approximate, right-docked with the 40cqw bar, quiet until the visit has counted tokens. Co-Authored-By: Claude Fable 5 --- .../app/workspace/_components/chat-input.tsx | 6 +- .../chat/_components/context-meter.test.ts | 21 ++++-- .../chat/_components/context-meter.tsx | 68 ++++++++++++------- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/studio/src/app/workspace/_components/chat-input.tsx b/studio/src/app/workspace/_components/chat-input.tsx index 371139ed23..f37428380b 100644 --- a/studio/src/app/workspace/_components/chat-input.tsx +++ b/studio/src/app/workspace/_components/chat-input.tsx @@ -70,8 +70,8 @@ interface ProjectItem { interface ChatInputProps { placeholder?: string; rows?: number; - /** The context-utilisation strip, rendered leftmost in the toolbar row - below the input (typography matches the pills' value text). */ + /** The context-utilisation strip, rendered at the right end of the toolbar + row below the input (typography matches the pills' value text). */ contextMeter?: ReactNode; projects?: ProjectItem[]; selectedProjectId?: string | null; @@ -1681,7 +1681,6 @@ export function ChatInput({ container queries when THIS row runs narrow (a ~400px side-panel composer), independent of the viewport width. */}
- {contextMeter} {projects && ( )} + {contextMeter}
); diff --git a/studio/src/app/workspace/chat/_components/context-meter.test.ts b/studio/src/app/workspace/chat/_components/context-meter.test.ts index faf2aa2553..9d09e4c819 100644 --- a/studio/src/app/workspace/chat/_components/context-meter.test.ts +++ b/studio/src/app/workspace/chat/_components/context-meter.test.ts @@ -30,9 +30,11 @@ describe("contextUtilisation", () => { }); /** - * The rendered strip lives in the composer toolbar: the ~ prefix carries the - * approximation (no standalone "approximate" tag), and the text matches the - * toolbar pills' value typography (text-sm, muted — the "On" in "Memory On"). + * The rendered strip lives at the right end of the composer toolbar: a plain + * "N% used" (the tooltip owns the approximation caveat and explains what a + * context window is), text matching the toolbar pills' value typography + * (text-sm, muted — the "On" in "Memory On"), and a bar spanning 40% of the + * toolbar's container width. */ describe("ContextMeter render", () => { const props = { @@ -42,12 +44,21 @@ describe("ContextMeter render", () => { outputTokens: 8_000, }; - it("shows the ~percentage without an approximate tag", () => { + it("shows the plain percentage as used, no approximate tag", () => { const { container } = render(createElement(ContextMeter, props)); - expect(container.textContent).toContain("~22% of context"); + expect(container.textContent).toContain("22% used"); + expect(container.textContent).not.toContain("~"); + expect(container.textContent).not.toContain("of context"); expect(container.textContent).not.toContain("approximate"); }); + it("sizes the bar to 40% of the toolbar container and docks right", () => { + const { container } = render(createElement(ContextMeter, props)); + const root = container.firstElementChild; + expect(root?.className).toContain("ml-auto"); + expect(root?.innerHTML).toContain("w-[40cqw]"); + }); + it("uses the toolbar pills' value typography", () => { const { container } = render(createElement(ContextMeter, props)); const root = container.firstElementChild; diff --git a/studio/src/app/workspace/chat/_components/context-meter.tsx b/studio/src/app/workspace/chat/_components/context-meter.tsx index e5a7381b5a..2393f17a06 100644 --- a/studio/src/app/workspace/chat/_components/context-meter.tsx +++ b/studio/src/app/workspace/chat/_components/context-meter.tsx @@ -1,16 +1,22 @@ "use client"; /** - * The slim context strip in the composer toolbar (B1.1): the session's - * effective model and an APPROXIMATE context-utilisation meter — cumulative - * input+output tokens this visit counted (summed from the runs' terminal usage - * frames) vs the model's `resolved_model.context_window`. The reading is - * approximate — the daemon's true compaction trigger also counts the system - * prompt and tool schemas, which the client never sees, and a page reload - * loses the visit's running total — which the `~` prefix conveys. Typography - * matches the toolbar pills' value text (the "On" in "Memory On"). + * The context strip at the right end of the composer toolbar (B1.1): the + * session's effective model and an APPROXIMATE context-utilisation meter — + * cumulative input+output tokens this visit counted (summed from the runs' + * terminal usage frames) vs the model's `resolved_model.context_window`. The + * reading is approximate — the daemon's true compaction trigger also counts + * the system prompt and tool schemas, which the client never sees, and a page + * reload loses the visit's running total — which the tooltip explains. + * Typography matches the toolbar pills' value text (the "On" in "Memory On"); + * the bar spans 40% of the toolbar's width (cqw against its @container). */ +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; /** @@ -46,23 +52,33 @@ export function ContextMeter({ if (fraction === null || inputTokens + outputTokens <= 0) return null; const percent = Math.round(fraction * 100); return ( -
- {modelLabel && {modelLabel}} -
+ + +
+ {modelLabel && {modelLabel}} +
+
+ + The context window is how much conversation the model can consider at + once — your messages, its replies, and tool activity all count toward + it. This is a rough share of the window used so far this visit; as it + fills, the agent compacts older history to make room. + +
); } From 68262052838adf714af44db0e1b4a6a019d47153 Mon Sep 17 00:00:00 2001 From: jtenniswood Date: Wed, 2 Sep 2026 14:03:39 +0100 Subject: [PATCH 4/8] polish(studio): context meter drops the model name, bar back to short MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The strip reads as just the short bar plus 'N% used' — the model name goes (the model pill next to it already says which model the chat runs on), and the bar returns to its fixed 64px width instead of 40% of the toolbar, which dominated the row on wide composers. Tests pinned accordingly (the strip's whole text content IS 'N% used'). Co-Authored-By: Claude Fable 5 --- .../workspace/chat/_components/chat-view.tsx | 1 - .../chat/_components/context-meter.test.ts | 15 ++++++------ .../chat/_components/context-meter.tsx | 23 ++++++++----------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/studio/src/app/workspace/chat/_components/chat-view.tsx b/studio/src/app/workspace/chat/_components/chat-view.tsx index e2d8c115b3..e0e84820c7 100644 --- a/studio/src/app/workspace/chat/_components/chat-view.tsx +++ b/studio/src/app/workspace/chat/_components/chat-view.tsx @@ -1314,7 +1314,6 @@ export function ChatView({ contextMeter={ contextInfo && contextInfo.contextWindow > 0 ? ( { }); /** - * The rendered strip lives at the right end of the composer toolbar: a plain - * "N% used" (the tooltip owns the approximation caveat and explains what a - * context window is), text matching the toolbar pills' value typography - * (text-sm, muted — the "On" in "Memory On"), and a bar spanning 40% of the - * toolbar's container width. + * The rendered strip lives at the right end of the composer toolbar: a short + * bar plus a plain "N% used" (the tooltip owns the approximation caveat and + * explains what a context window is), text matching the toolbar pills' value + * typography (text-sm, muted — the "On" in "Memory On"), no model name. */ describe("ContextMeter render", () => { const props = { - modelLabel: "claude-sonnet-5", contextWindow: 400_000, inputTokens: 80_000, outputTokens: 8_000, @@ -52,11 +50,12 @@ describe("ContextMeter render", () => { expect(container.textContent).not.toContain("approximate"); }); - it("sizes the bar to 40% of the toolbar container and docks right", () => { + it("docks right with a short fixed bar and no model name", () => { const { container } = render(createElement(ContextMeter, props)); const root = container.firstElementChild; expect(root?.className).toContain("ml-auto"); - expect(root?.innerHTML).toContain("w-[40cqw]"); + expect(root?.innerHTML).toContain("w-16"); + expect(container.textContent?.trim()).toBe("22% used"); }); it("uses the toolbar pills' value typography", () => { diff --git a/studio/src/app/workspace/chat/_components/context-meter.tsx b/studio/src/app/workspace/chat/_components/context-meter.tsx index 2393f17a06..d81d7c5b7e 100644 --- a/studio/src/app/workspace/chat/_components/context-meter.tsx +++ b/studio/src/app/workspace/chat/_components/context-meter.tsx @@ -1,15 +1,14 @@ "use client"; /** - * The context strip at the right end of the composer toolbar (B1.1): the - * session's effective model and an APPROXIMATE context-utilisation meter — - * cumulative input+output tokens this visit counted (summed from the runs' - * terminal usage frames) vs the model's `resolved_model.context_window`. The - * reading is approximate — the daemon's true compaction trigger also counts - * the system prompt and tool schemas, which the client never sees, and a page - * reload loses the visit's running total — which the tooltip explains. - * Typography matches the toolbar pills' value text (the "On" in "Memory On"); - * the bar spans 40% of the toolbar's width (cqw against its @container). + * The context strip at the right end of the composer toolbar (B1.1): a short + * bar plus an APPROXIMATE context-utilisation figure — cumulative input+output + * tokens this visit counted (summed from the runs' terminal usage frames) vs + * the model's `resolved_model.context_window`. The reading is approximate — + * the daemon's true compaction trigger also counts the system prompt and tool + * schemas, which the client never sees, and a page reload loses the visit's + * running total — which the tooltip explains. Typography matches the toolbar + * pills' value text (the "On" in "Memory On"). */ import { @@ -35,13 +34,10 @@ export function contextUtilisation( } export function ContextMeter({ - modelLabel, contextWindow, inputTokens, outputTokens, }: { - /** The effective model (resolved_model.model_id). */ - modelLabel: string; contextWindow: number; inputTokens: number; outputTokens: number; @@ -55,9 +51,8 @@ export function ContextMeter({
- {modelLabel && {modelLabel}}
)} -
+
onSteerQueued?.(id)} diff --git a/studio/src/app/workspace/chat/_components/chat-workspace.tsx b/studio/src/app/workspace/chat/_components/chat-workspace.tsx index 968f178fe7..ce3e10e38d 100644 --- a/studio/src/app/workspace/chat/_components/chat-workspace.tsx +++ b/studio/src/app/workspace/chat/_components/chat-workspace.tsx @@ -325,7 +325,7 @@ function DraftView({
-
+
{error &&

{error}

} Date: Wed, 2 Sep 2026 14:55:24 +0100 Subject: [PATCH 8/8] chore(studio): launch config for the desktop-app preview pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One 'studio' configuration: npm --prefix studio run dev (managed mode — controller + supervised mecated) on port 3000, so the Claude Code browser pane can start and attach to Studio directly. Co-Authored-By: Claude Fable 5 --- .claude/launch.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .claude/launch.json diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000000..448f767a0b --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "studio", + "runtimeExecutable": "npm", + "runtimeArgs": ["--prefix", "studio", "run", "dev"], + "port": 3000 + } + ] +}