From 4c7ad48c736ac796e5b255f9fc24a88b0e142e1d Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Tue, 15 Sep 2026 08:00:04 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20the=20knowledge-context=20slider=20lives?= =?UTF-8?q?=20in=20the=20stats=20bar's=20left=20slot=20=E2=80=94=20the=20s?= =?UTF-8?q?ession-data=20spot=20=E2=80=94=20with=20a=20"tok"=20readout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original vision, restored (owner, 2026-09-15): pre-chat the session- data side of the under-composer stats bar is free real estate ("—" only), so the budget slider mounts exactly where session data will sit once a session exists — via a new StatsBar leftSlot — vertically centered with the model + thinking pills by the row's own geometry. Short 96px slider instead of a full-width one; readout abbreviates "1k tok" so >1k budgets stay on one line ("4.1k tokens" wrapped). Home and /chat/new both. --- components/graph-context-control.tsx | 33 ++++++++++++++-------------- components/home.tsx | 24 ++++++++++---------- components/new-session.tsx | 28 +++++++++++++---------- components/stats-bar.tsx | 16 +++++++++++++- tests/render-new-session.test.tsx | 11 ++++++++-- 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/components/graph-context-control.tsx b/components/graph-context-control.tsx index 2f24a39..fa69113 100644 --- a/components/graph-context-control.tsx +++ b/components/graph-context-control.tsx @@ -11,15 +11,19 @@ import { formatTokens } from "@/components/usage-format"; /** * The knowledge-context budget control (vision-context-injection.md) — the * "session data" section's one input, shown where sessions START: the home - * (graph) view's new-session dock and the /chat/new draft. Input before - * output: the panel that will later show what the session contributed to - * the graph asks first what the graph will contribute to the session. + * (graph) view's new-session dock and the /chat/new draft. It renders in + * the StatsBar's LEFT slot — the very place session data (context/cost) + * lives once a session exists, which pre-chat is free real estate (just + * "—"). Input before output: the panel that will later show what the + * session contributed to the graph asks first what the graph will + * contribute to the session. * - * One quiet slider row: the token budget for the prior-knowledge block the - * injection steers in at session start. 0 = off; absent settings show the - * DEFAULT (1024 — on). The value rides ChatSettings (the same lifted - * settings the model picker uses), so it persists pre-chat via the same - * deferred PUT. Sessions never show this control — the injection is + * One quiet compact row sized to sit in the stats band beside the model + + * thinking pills: short slider, "tok" readout (abbreviated — "4.1k tok" + * stays on one line where "4.1k tokens" wrapped). 0 = off; absent settings + * show the DEFAULT (1024 — on). The value rides ChatSettings (the same + * lifted settings the model picker uses), so it persists pre-chat via the + * same deferred PUT. Sessions never show this control — the injection is * one-shot at session start, so mid-session there is nothing to decide. */ export function GraphContextControl({ @@ -32,12 +36,9 @@ export function GraphContextControl({ testId?: string; }) { return ( -
+
- Knowledge context + Knowledge context onChange(Number(e.target.value))} aria-label="Prior-knowledge token budget" - className="h-1 flex-1 accent-primary" + className="h-1 w-24 shrink-0 accent-primary" data-testid="graph-context-slider" /> - {value === 0 ? "off" : `${formatTokens(value)} tokens`} + {value === 0 ? "off" : `${formatTokens(value)} tok`}
); diff --git a/components/home.tsx b/components/home.tsx index 082dac5..5e5885b 100644 --- a/components/home.tsx +++ b/components/home.tsx @@ -72,17 +72,6 @@ export function Home() { px-4) so nothing shifts when a session starts (owner). */}
- {/* The session-data strip (vision-context-injection.md): what the - graph will contribute to the session, decided before the session - starts. Rides the same lifted settings as the model picker. */} -
- - setSettings((s) => ({ ...s, graphContextTokens: tokens })) - } - /> -
{ @@ -114,13 +103,22 @@ export function Home() {
{/* Same as the chat view's under-composer stats bar (owner) — no - session yet, so money/context fold as "—"; the model + - thinking control still works (deferred save). */} + session yet, so the free LEFT side carries the knowledge-context + budget (session-data slot), while the RIGHT keeps the model + + thinking control (deferred save). */} + setSettings((s) => ({ ...s, graphContextTokens: tokens })) + } + /> + } />
diff --git a/components/new-session.tsx b/components/new-session.tsx index 496b505..f309922 100644 --- a/components/new-session.tsx +++ b/components/new-session.tsx @@ -99,16 +99,6 @@ export function DraftShell({
- {/* The session-data strip (vision-context-injection.md) — same - control as the home view, riding the same lifted settings. */} -
- - onSettingsSaved({ ...settings, graphContextTokens: tokens }) - } - /> -
- + + onSettingsSaved({ ...settings, graphContextTokens: tokens }) + } + /> + } + />
diff --git a/components/stats-bar.tsx b/components/stats-bar.tsx index 749d2f9..2520325 100644 --- a/components/stats-bar.tsx +++ b/components/stats-bar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type ReactNode } from "react"; import { contextWindow, sessionCost, turnCost, type ChatSettings, type TurnUsage } from "@/src/core"; import { formatTokens, formatUsd, prettyModel } from "@/components/usage-format"; import { ModelControl } from "@/components/model-picker"; @@ -11,6 +11,12 @@ import { ModelControl } from "@/components/model-picker"; * LEFT-aligned: context window + session cost; RIGHT-aligned: model + * thinking level. Clicking the left side opens the per-turn detail popover. * + * Pre-chat views (home, /chat/new) pass `leftSlot`: with no session there + * IS no session data (the summary would just read "—"), so that left side + * is free real estate — the knowledge-context budget slider lives there + * (vision-context-injection.md), vertically centered beside the model + + * thinking pills by the row's own geometry. + * * All folds come from core/prices.ts (sessionCost, contextWindow, turnCost) * — no cost math lives here. Absent data renders "—", never zero (the * doctrine): no turns yet → every figure is "—"; turns the price table @@ -26,11 +32,15 @@ export function StatsBar({ chatId, settings, onSettingsSaved, + leftSlot, }: { turns: TurnUsage[]; chatId: string; settings?: ChatSettings; onSettingsSaved: (settings: ChatSettings) => void; + /** Pre-chat only: replaces the (empty) session-data summary — home and + * the /chat/new draft mount the knowledge-context slider here. */ + leftSlot?: ReactNode; }) { const [open, setOpen] = useState(false); // Same dropdown discipline as the model control (owner, df4433f treatment, @@ -74,6 +84,9 @@ export function StatsBar({ outside-close ref anchors HERE too (owner, round 3): on the whole bar it wrapped the model control, so clicking model/thinking never counted as outside — the asymmetry the owner observed. */} + {/* The left side: the session-data summary, or — pre-chat — the + caller's slot (the knowledge-context slider) in its place. */} + {leftSlot ?? (
+ )} {/* The model + thinking control — one controller for the conversation agents (prober/teacher); the record's `turns` above still folds the ACTUAL models billed, regardless of what the picker says. */} diff --git a/tests/render-new-session.test.tsx b/tests/render-new-session.test.tsx index 736f962..24aa698 100644 --- a/tests/render-new-session.test.tsx +++ b/tests/render-new-session.test.tsx @@ -84,7 +84,7 @@ describe("DraftShell — the sessionless surface", () => { assert.ok(/rounded-2xl[^>]*pointer-events-auto|pointer-events-auto[^>]*rounded-2xl/.test(html)); }); - it("renders the knowledge-context slider at the decided default (absent settings = on, 1024)", () => { + it("renders the knowledge-context slider in the stats bar's left slot, at the decided default (absent settings = on, 1024)", () => { const html = renderToStaticMarkup( { />, ); assert.ok(html.includes('data-testid="graph-context-control"')); + // INSIDE the stats row (the session-data slot), not a strip above the + // composer: the control renders after the stats bar opens. + assert.ok( + html.indexOf('data-testid="stats-bar"') < html.indexOf('data-testid="graph-context-control"'), + "the slider is the stats bar's left slot", + ); // The slider's default position: 1024 of 4096, step 128 (decided). assert.ok(html.includes('min="0"') && html.includes('max="4096"') && html.includes('step="128"')); - assert.ok(html.includes("1k tokens"), "the readout shows the default budget, not 0 — default is ON"); + // The abbreviated readout (owner: "tokens" wrapped at >1k — "tok" stays one line). + assert.ok(html.includes("1k tok"), "the readout shows the default budget, not 0 — default is ON"); assert.ok(html.includes("Knowledge context"), "the control is labeled"); // Absent settings render the default — never 'off'. assert.ok(!html.includes(">off<"));