fix: the knowledge-context slider lives in the stats bar's left slot — the session-data spot — with a "tok" readout
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.
This commit is contained in:
@@ -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 (
|
||||
<div
|
||||
data-testid={testId}
|
||||
className="flex items-center gap-2 px-1 text-xxs text-muted-foreground"
|
||||
>
|
||||
<div data-testid={testId} className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<BookOpenIcon className="size-3 shrink-0" />
|
||||
<span className="shrink-0 uppercase tracking-widest">Knowledge context</span>
|
||||
<span className="shrink-0">Knowledge context</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
@@ -46,14 +47,14 @@ export function GraphContextControl({
|
||||
value={value}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span
|
||||
className="w-16 shrink-0 text-right tabular-nums"
|
||||
className="w-14 shrink-0 text-right tabular-nums"
|
||||
data-testid="graph-context-value"
|
||||
>
|
||||
{value === 0 ? "off" : `${formatTokens(value)} tokens`}
|
||||
{value === 0 ? "off" : `${formatTokens(value)} tok`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
+11
-13
@@ -72,17 +72,6 @@ export function Home() {
|
||||
px-4) so nothing shifts when a session starts (owner). */}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 pb-5">
|
||||
<div className="pointer-events-auto mx-auto w-full max-w-[46rem] px-4">
|
||||
{/* 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. */}
|
||||
<div className="mb-1.5">
|
||||
<GraphContextControl
|
||||
value={effectiveContextTokens(settings)}
|
||||
onChange={(tokens) =>
|
||||
setSettings((s) => ({ ...s, graphContextTokens: tokens }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
className="flex items-end gap-1.5 rounded-2xl border bg-card p-1.5 shadow-lg"
|
||||
onSubmit={(e) => {
|
||||
@@ -114,13 +103,22 @@ export function Home() {
|
||||
</button>
|
||||
</form>
|
||||
{/* 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). */}
|
||||
<StatsBar
|
||||
turns={[] as TurnUsage[]}
|
||||
chatId=""
|
||||
settings={settings}
|
||||
onSettingsSaved={setSettings}
|
||||
leftSlot={
|
||||
<GraphContextControl
|
||||
value={effectiveContextTokens(settings)}
|
||||
onChange={(tokens) =>
|
||||
setSettings((s) => ({ ...s, graphContextTokens: tokens }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+17
-11
@@ -99,16 +99,6 @@ export function DraftShell({
|
||||
<div className="h-24 bg-gradient-to-t from-background via-background/85 to-transparent" />
|
||||
<div className="bg-background pb-4">
|
||||
<div className={DOCK}>
|
||||
{/* The session-data strip (vision-context-injection.md) — same
|
||||
control as the home view, riding the same lifted settings. */}
|
||||
<div className="mb-1.5">
|
||||
<GraphContextControl
|
||||
value={effectiveContextTokens(settings)}
|
||||
onChange={(tokens) =>
|
||||
onSettingsSaved({ ...settings, graphContextTokens: tokens })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="pointer-events-auto overflow-hidden rounded-2xl border bg-card shadow-lg">
|
||||
<Composer
|
||||
busy={busy}
|
||||
@@ -120,7 +110,23 @@ export function DraftShell({
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<StatsBar turns={[]} chatId="" settings={settings} onSettingsSaved={onSettingsSaved} />
|
||||
<StatsBar
|
||||
turns={[]}
|
||||
chatId=""
|
||||
settings={settings}
|
||||
onSettingsSaved={onSettingsSaved}
|
||||
// The session-data slot (owner, 2026-09-15): pre-chat the left
|
||||
// side is free — the knowledge-context budget lives exactly
|
||||
// where session data will sit once a session exists.
|
||||
leftSlot={
|
||||
<GraphContextControl
|
||||
value={effectiveContextTokens(settings)}
|
||||
onChange={(tokens) =>
|
||||
onSettingsSaved({ ...settings, graphContextTokens: tokens })
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 ?? (
|
||||
<div ref={barRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
@@ -141,6 +154,7 @@ export function StatsBar({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 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. */}
|
||||
|
||||
@@ -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(
|
||||
<DraftShell
|
||||
busy={false}
|
||||
@@ -96,9 +96,16 @@ describe("DraftShell — the sessionless surface", () => {
|
||||
/>,
|
||||
);
|
||||
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<"));
|
||||
|
||||
Reference in New Issue
Block a user