da0cd558d09beb9741d0e3a5e27de2fc53a05fec
19
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3e9a85d60f |
per-session models: every session owns its model + thinking level from birth
The reported bug (2026-09-28): every chat ran glm-5.3-flash regardless of any pick, while composer and settings UIs agreed with each other and lied about the run. Root cause: a fossil default-settings record (settings.json chat section, frozen pre-slots-wave) was stamped verbatim into every new chat at birth by the new-session flow, and resolveConversationSpec prefers the per-chat record over the strong slot forever. Owner redesign — models are per session, always; the strong slot is the new-session default: - POST /api/chats stamps the newborn verbatim from the strong slot (model + thinking level); no client settings PUT on create anymore - the composer control is dual-bound: in-session it reads/writes the session's own record (GET /api/chats/:id, PUT .../settings); on the draft surfaces it binds the strong slot (picking there = changing the default) - thinking levels follow the identical mechanics (confirmed by owner) - the zombie default-settings system dies whole: useDefaultSettings, /api/settings/defaults, src/store/default-settings.ts, the chat section of UserSettings (sanitizer strips fossil copies), the legacy default-settings.json fold, and DEFAULT_SETTINGS_FILE - catalogDefaultsFor collapses to the strong slot alone - thinking display for a legacy unstamped session resolves exactly like the engine (session ?? strong ?? model ladder) Tests: new chat-birth-stamp suite pins the stamp, the unconfigured boot, the unset level, and the copy-not-live-view regression; defaults pins rewritten strong-only; legacy-migration suite deleted. |
||
|
|
6b83d3248b |
Tooltip subtlety round: 500ms rest delay, quiet chip, parens suffixes, drops + trims
Owner round 2026-09-25, over 09c0f6e. The wave's tooltips read as distracting; this round makes them quiet: - Delay: TOOLTIP_DELAY_MS = 500 in the primitive — tooltips appear when you pause on a control, never in passing (Radix focus-opens stay instant by design; keyboard users don't wait). - Vanish on leave: disableHoverableContent — the chip is un-hoverable, so the instant the cursor leaves the trigger it's gone, even mid-move onto the chip's own pixels. - Look: bg-accent (#2a2a2a, the hover-chip tone) instead of the white bg-foreground slab; 11px; px-2 py-1; arrow culled (nothing purely decorative); text-foreground/85. - Suffix: parentheses, not em-dash — "Dictate (⌘D)"; the registry source unchanged (still override-aware). - Drops (owner-selected): send square, start square, stats summary — the icons/numbers describe themselves. - Home logo kept but to the RIGHT with its real shortcut "Home (⌘G)" (jump-graph routes to /, the same destination). - Trims: "Model selector (⌘K)", "Thinking level", "Graph costs — click for details", "How much of this session isn't mapped yet". - Added: Import Notes gets "Import notes into the graph" — the one button whose destination wasn't obvious. Pins: TOOLTIP_DELAY_MS=500; hintText parens expectations updated. Live-verified on :3001 — 508ms hover delay measured, close within 10ms of pointerout, chip styles probed (rgb(42,42,42), 11px, 4px/8px), arrow gone, all new labels + sides confirmed, drops carry no tooltip and no aria-describedby. |
||
|
|
b19e7e7eae |
Production tooltips: styled tooltips everywhere, shortcuts surfaced, dev-speak culled
Owner-approved full pass (tooltip wave, 2026-09-25). Mechanism: icon/pill controls ride the styled Radix tooltip via a new Hint composition (components/ui/hint.tsx) — the shared primitive adopted as-is, no restyling; the shortcut appends as an em-dash suffix off the LIVE registry (a rebind in Settings → Keyboard changes the suffix): "Dictate — ⌘D". Surface-owned keys (Esc closes, Enter submits) ride a raw binding prop. Native title= stays only for truncation-reveals (dock spans, the rename field) and menu-interior abbreviations (MenuShell untouched). One em-dash pattern; per-state truth on the send square (busy = "Stop the reply — Esc", idle = "Send — ↵"), Enter on the plan verdict's accept, "Stop dictation (Esc)" kept verbatim per the owner's flag. Copy: the dev-speak cull — no pipeline/prober/tier/wave vocabulary anywhere a user reads. New pins in tests/tooltip-mechanism.test.tsx (hintText composition, asChild no-wrapper-DOM, the auto-update hint constant); dock truncation titles pinned native; graph-chrome + settings pins updated to the new copy. Live-verified on :3001 — 12.6ms hover appearance (no native 1s lag); glyphs render on every suffixed surface (⌘D, ⌘K, ⌘⇧F, ⌘,, ⌘B, F2, ↵, Esc); per-page probes confirm Settings entry, Home logo, session search, Start session, Close sidebar, model + thinking pills, stats summary, rename title, plan chip, coverage ring, send square, cost button, node search (⌘F), graph gear, settings close (Close — Esc), keyboard section "Fixed — part of this screen", auto-update hint, light-mode "Coming soon", the three slot rosters, graph-tools hint. Esc still closes the settings dialog. |
||
|
|
81709afd22 |
Stats-bar catalog join + dock numerator consistency (context-meter close-out)
Platform's context-meter pair (04e336d, 18cc493) left two client seams in my band; this closes the arc: 1. STATS-BAR CATALOG JOIN: contextWindow's denominator resolves ONLY from pi's live catalog — stats-bar now builds the provider:id -> contextWindow index client-side (catalogLimits in model-picker.tsx, the client twin of the server's catalogContextLimits join) over the picker's existing /api/models fetch/cache (useModelCatalog exported; module-level dedupe, one fetch feeds both ModelControl and the meter). Catalog pending or failed -> undefined index -> limit null -> USAGE-ONLY (Platform's interim render IS the null path; the honest-render precedent, never a fake window). 2. DOCK NUMERATOR CONSISTENCY: toBlobs' context fold was still last-step input+output only — now the same cache-inclusive footprint the session meter uses (all four directions; provider "input" EXCLUDES cacheRead/cacheWrite, so the old fold read wildly low on cache-heavy jobs). Same metric everywhere. Review of Platform's interim edits in this band: verified as-shipped — stats-bar's usage-only render (summary + popover), FeedJob.contextLimit with the line-hides null treatment, and the render-test rewrites all fit. Only fix needed: none beyond the above. Tests: catalogLimits pinned pure (index shape, provider:id keying, missing-model absence); toBlobs pinned on the cache-inclusive numerator (10k+0.5k+120k+30k = 160.5k against a 200k feed limit). 889/889. Live-verified on :3001: seedplan's bar reads "18.7k / 1M" (footprint 18725, catalog 1048576); a nope/unknown-model chat renders "12.8k" usage-only with cost "-"; the popover detail matches; the dock blob's context line renders "946 / 1M" from a server-joined feed (an unintentionally real runner-adopted job — the full chain, live). |
||
|
|
4ded547cb5 |
Context-meter amendment: CONTEXT_LIMITS dies — pi's live catalog is the only limit source
Owner ruling (2026-09-23, amendment to 04e336d): no hardcoded fallback exists anywhere. contextWindow(record, limits) resolves the denominator ONLY from the live index (provider:id -> contextWindow straight from pi's catalog); a model missing from it (historical session, package update, failed fetch) reads limit null and the meter renders usage-only — the honest-render precedent, never a fake window. - src/core/prices.ts: the 1354-entry CONTEXT_LIMITS table (65KB) and its generation apparatus deleted; the fold returns limit: number | null. PRICES untouched (regen verified byte-identical via the reworked sync-pricing script). - scripts/sync-pricing.ts: generates PRICES only; contextWindow leaves the priced map, the guard, and the template. - The dock meter's owner-selected treatment: the activity feed carries the limit server-side. catalogContextLimits() (server/model-catalog.ts) builds the index over the same keyed providers the picker federates; activityFeed() joins it onto every running job (ActivityJob.contextLimit, null when the model is missing — the dock reads it off the feed, no client-side table, no regression while the UI's catalog-join chunk lands). - components/activity-dock.tsx: toBlobs reads job.contextLimit (import of CONTEXT_LIMITS gone); stats-bar renders usage-only on null limit (mechanical interim — UI stream's join chunk follows). - tests: usage.test.ts pins the no-fallback contract (index hit / absent / empty / missing-model -> null); activity-feed.test.ts pins the server join (catalog model -> 1048576, unknown -> null, no-usage -> null); render pins updated to the interim usage-only render. |
||
|
|
7a17230f26 |
Wave 7 close-out: the graphTools surface, wrapped cluster labels, the orphan sweep
Part 1 — the surface:
- Settings ▸ Agent ▸ Tools gains the prober GRAPH-TOOLS row as its FIRST
row (owner ruling: in the Tools section, not the tool-toggles list —
its subject is the map, its mechanism is agent capability). Writes
agent.graphTools {enabled} (absent = on, the toggle discipline) via
the section PUT; composeGraphTools pure seam. Its own key in the agent
contract — never in the tools map.
- The row carries the search-row hint discipline: "Connects prior
sessions' knowledge into new ones — the prober reads the knowledge
map" (owner wording). The honest interplay composes both: when
graph.enabled is off the row dims + disables with a "Graph is
disabled" hint (the effective gate itself is server-side, per-turn
fresh). AgentSection reads the graph section (record.graph, passed by
the dialog) — read-only.
- Cluster labels wrap: wave 7 made labels session names and FILE PATHS,
which the old single-line fillText could not bound. wrapRegionLabel
(pure, exported): word-wrap first, char-split long paths (the
node-label discipline), capped at 3 lines, overflow folded into an
ellipsized last line (owner: wrap, cap 2–3, ellipsize). Pre-wrapped
once per geometry build at the SCREEN-fixed size (viewer-relative
labels — the wrap never goes stale mid-camera-move), drawn centered
line-by-line at the centroid. Deleted-session clusters keep their
hull-only honest silence (label null → no lines).
- The orphan sweep: StatsBar's leftSlot prop is dead (its one consumer,
the knowledge-context slider, died in c690181) — removed, and pre-chat
views now render NOTHING on the left (owner ruling this round: a
"— · —" summary over an empty popover is pure noise) — the dictation
middle just widens. Stale knowledge-context comments swept in
chat.tsx (adoption-window + activity-nudge docs), activity-dock.tsx
(useActivity ground rules), dictation.tsx (×2 adaptive-meter docs),
home.tsx (pre-chat dock comment).
Part 2 — Teacher's c690181 review: clean. Imports, KIND_ICON/KIND_LABEL
entries, render branches and event seams all excised whole; the probe-
handoff branch re-indented intact and merge-adopted; tests updated in
lockstep (merge-adopted, record-adapter, render-new-session retired).
Platform's 4b60e0f roster-string fixes (labeler dropped from both
SLOT_META blocks) reviewed — consistent across agent-section +
setup-dialog.
Gates: tsc clean, 854/854 (8 new: wrapRegionLabel cases, graphTools
SSR rows ×3; stats-bar + ladder fixtures updated to the new rulings),
lint 0, build ok. Live on :3001: graphTools row first-in-group with
hint, off→{enabled:false}→on round-trip, graph-off dimming + hint +
disabled toggle, pre-chat rows summary-free, chat view keeps its
summary, long-title cluster label wrapped without console errors
(visual read unavailable this session — pure wrap is unit-pinned).
|
||
|
|
f6626f1703 |
Fix wave chunk A: no Default option, composer = strong slot, catalog revalidation
Item 1 — no Default in the slot selectors (owner, fix wave): a "default" model is not a thing in this app. Picking Default used to delete the slot and re-arm the first-run gate — incoherent. Now the model menus carry NO clear row at all (a slot holds a pick or reads "—"), the thinking menus carry a "—" row (menu-none) that clears only the thinking pin, and the hint line is the roster only — the resolved factory pin is never named. Master's amendment confirmed: the factory-pin hint is SUPERSEDED entirely (Platform removes the pins). Item 2 — the composer control IS the strong slot (owner: "one and the same"): ModelControl (model-picker.tsx) reads the raw models section (GET /api/settings) and the federated catalog, and writes PUT /api/settings/models via composeSlotModel — the same seam the settings Agent section and SetupDialog use. Props retired: chatId, settings, onSaved, fallbackModelId, noDefaultModel — the control is self-sufficient and identical on every surface (composer, home dock, /chat/new). The per-chat model PUT is gone from the UI; legacy per-chat overrides keep resolving server-side. StatsBar/DockOverlay plumbing slimmed accordingly; chat.tsx drops its per-chat settings state and initialSettings prop. Item 3 — the composer catalog cache no longer freezes per page load: fetchCatalog(force) invalidates, and every model-menu open revalidates in the background (stale-while-revalidate — cached list shows instantly, the fresh federation swaps in). Owner's repro verified live: a key saved mid-session widens the menu without a reload. Item 4 — stale doc comments fixed: model-catalog.ts's "OpenRouter catalog"/"multi-provider future" (Platform's file — mechanical, flagged for review) and model-picker's retired "config-pinned"/ per-chat-PUT narrative. Live-verified on 3001: unpicked slots read "—" everywhere; model menus have no clear row; the "—" thinking row clears the pin but keeps the model; composer pick writes strong and preserves cheap/ vision siblings; thinking pill writes strong.thinkingLevel; groq key added mid-session appears in the open menu without reload. Caught in live verification: the record fetch was mapping the WHOLE settings record into the models section (picks never wrote) — fixed to read body.settings.models. Gates: typecheck, lint, 807/807, build. |
||
|
|
48fbd988f1 |
Wave-5 chunk B: three model slots, search-engine row, first-run setup flow
Chunk B (owner-designed, model-slots wave) on the merged engine surface (main 1d66a17): AGENT SECTION — SLOTS ONLY (owner ruling) - The six per-role model rows are GONE; three slot rows instead: Strong (Prober + Teacher), Cheap (five workhorse roles), Vision (visual maker + PDF reader), roster + resolved-default in every hint. - Slot dropdowns share the app's menu language over the FEDERATED catalog (/api/models = keyed providers only, fresh per call — saving a key in Keys immediately widens the picker); the vision slot filters to image-capable models (Model.input); the menu's highlight now matches provider+id (ids can collide across providers). - Slots write the new models section (PUT /api/settings/models) through the dialog's write seam; per-role overrides stay schema-only. - ModelMenuList rows read 'provider · id' — federation makes the provider bookkeeping-relevant. Usage stamps render provider-qualified (prettyModel(qualified): 'Glm 5.3 Flash · openrouter') in the stats bar's turn lines. SEARCH ENGINE (search wave: no default, owner) - A row in Tools: Tavily/Brave via the shared-menu look, Automatic = the pin removed (auto-activation). The live reason line comes from /api/search; no engine key at all grays the whole row + disables the trigger (a keyless choice would only block auto-activation). FIRST-RUN SETUP FLOW (owner design A: composer CTA + send interception) - Chats cannot start until all three slots are picked — no defaults for 33 providers (slotsConfigured: all three slot models required). - The graph view's new-session dock (home) shows a 'Set up your models' CTA while unconfigured; submitting a topic opens the SetupDialog WITH THE DRAFT PRESERVED; Save stores the models section and the queued message sends. Same interception on /chat/new sends. - STAGED BY DEPENDENCY (owner: keys first, models after): no keyed provider → stage 1 deep-links the settings dialog on Keys (open now takes a section); a saved key reopens at stage 2 on the next submit. - Stage 2: Strong → Cheap → Vision pickers (federated, vision-filtered, all three required). Closable without configuring — browsing stays possible; the gate is the send, not the dialog. Layered Esc: an open slot menu eats Esc before the dialog. - Two closure bugs caught live: onDone→start/send captured the stale pre-save 'configured' (the flow now forces the replayed send); menu-Esc used to close the whole dialog. Settings-dialog provider: open(section?) lands on a section (stage 1 deep-link). Tests: composeSlotModel/composeSearchEngine pure, slot SSR replacing role rows, slotsConfigured/setupStage first-run gates, provider-qualified prettyModel. Gates: typecheck, lint, 792/792, build. |
||
|
|
bd3086194f |
feat: no baked-in default model — the new-session controls ARE the default settings page
Owner reframe (2026-09-18): the model + thinking + context controls on
the home graph view and /chat/new are not a draft overlay on a baked-in
default — they ARE the settings page for the default model, and the
user's last choice IS the default. BYOK first boot has no keys and
possibly no OpenRouter, so there is NO default to show: the controls
render 'no model' ('—') until the first pick.
The flash was the deeper symptom: the surfaces rendered from client
state that started empty and fetched truth afterward, so every visit
flashed the config pin (GLM/low — a dev artifact) for a few frames.
Fix at the root: both pages are now force-dynamic server components
that read the record (readDefaultSettings → initialSettings) and hand
it to the client — the first frame shows truth. Dynamic routes aren't
cached client-side (staleTimes.dynamic = 0), so every navigation
re-reads the record; the useDefaultSettings hook keeps only save-side
state (optimistic set + PUT). The GET-on-mount and its touch-guard are
gone.
ModelControl gains noDefaultModel (the default surfaces only): unset
settings render no model/level, never the catalog pin; the thinking
menu no-ops until a model exists. Chat surfaces keep the catalog
fallback — the engine runs the config pin when a session record has no
override, so the control keeps showing what will actually run.
Renamed draft→default throughout: data/default-settings.json,
GET/PUT /api/settings/defaults, useDefaultSettings. New sessions
still inherit the default on creation exactly as before.
Live-verified: first-boot 'no model' with zero flash, picker +
Shift+Tab cycle, values held across session→/chat/new→home roundtrips
and a reload (10×100ms samples, never the old default), zero console
errors.
|
||
|
|
e3bdbc2f2c |
feat: voice dictation (STT wave 1 — Deepgram cloud)
Two-band architecture from docs/research/stt.md: renderer captures mic audio in an AudioWorklet (linear16 16kHz mono, ~128ms frames, per-quantum RMS for the meter) and streams it directly to Deepgram over WebSocket — audio never loops through our server. The real key stays server-side; /api/stt/token mints a short-lived grant token (TTL 300s) per run. Patterns ported from the owner's pi dictate extension (tuned in real use): generation counter guarding every async handler, the latest-RMS-never-resets meter (-50..-10dB window, 60ms tick), mic-first stop + CloseStream + 3s safety net, endpointing=300 with the utterance_end_ms=1000 backstop. UI (owner placement): mic button in the stats bar's middle slot on all three surfaces (home graph-view, chat, draft); while recording the meter unfurls to fill the row's free width with the interim ghost at its flank. Esc ends dictation from the composer. Token mint comes before getUserMedia so a keyless app fails fast without a TCC prompt; one automatic reconnect on abnormal socket close, errors surface inline. Live-smoked the full error path in the dev shell (bar renders on all three pages, 403 grant failure shows inline, mic untouched). The full audio path needs a Deepgram key with Member permission to mint tokens. |
||
|
|
4c7ad48c73 |
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.
|
||
|
|
873c153525 |
Stats bar round 4 (owner): optical spacing — tight to the composer, floating below
The rhythm agreed with the owner: the stats/controls row hugs the composer (8px — reads as the card's metadata, one object), and the whole cluster floats with deliberate air above the screen bottom (20px). The 8px straight-edge gap optically matches the 6px pill-to- pill gap: two rounded pills' corridor reads wider than geometric, so rounded-rounded gaps run ~2px tighter than straight-edge gaps. - stats-bar.tsx: pt-4 -> pt-2 (8px under the composer) — shared by chat and home automatically - chat.tsx dock block + home.tsx composer wrapper: pb-4 -> pb-5 The dock's reserve auto-measures the block, so the spacer/fold machinery adapts on its own. |
||
|
|
5dac2a6620 |
Stats bar round 3 (owner): symmetric dropdown closing + vertically centered row
The asymmetry: the stats bar's outside-close ref wrapped the WHOLE bar, which CONTAINS the model control — so clicking model/thinking while stats was open counted as 'inside' and nothing closed, while the reverse direction (model's ref wraps only its own buttons) closed fine. The ref now anchors to the summary wrapper (button + popover), matching the model picker's pattern. Both directions close now. Also centers the stats row vertically in the dock: pt-1.5 -> pt-4, equal to the block's pb-4 — the row sits midway between the composer's bottom and the page bottom. (Font size already matched: both sides compute to 11px JetBrains Mono; the owner chose to keep the color difference — muted stats vs bright labels.) Verified live: model->stats, stats->model, stats->thinking all close the open dropdown and open the new one; row has 16px above (visible text) and 16px below. |
||
|
|
e13762fe9a |
Stats bar round 2 (owner): popover centered over the summary button
Round 1 centered the popover over the whole bar ROW — the owner meant the stats bar's own bottom-left content (the context · cost summary button). The popover now anchors to a relative wrapper around the button and centers over IT (left-1/2 -translate-x-1/2 of the wrapper), so it hangs balanced over the trigger, not offset toward the composer row's center. Outside-click + Esc closing unchanged from round 1. |
||
|
|
d4b05a0d90 |
Stats bar round 1 (owner): model-control dropdown discipline for the stats popover
Same treatment the model/thinking controls got (df4433f): the per-turn stats popover now closes on any pointerdown outside the bar and on Escape. Also centered over the stats bar (left-1/2 -translate-x-1/2) instead of left-aligning with the quiz box, which looked odd. Verified live: popover centered over the bar (centers within 2px); Escape closes; click outside closes; click inside stays open. |
||
|
|
3360181e41 |
Wave-5 reskin batch 5: normalize stray pixel text sizes onto the declared ladder
--text-xxs (10px) joins the ladder; every component-level text-[9|10|11px] literal in owned components now pulls from the ladder (graph-view.tsx and home.tsx untouched — graph stream). Cosmetic pins updated where touched. |
||
|
|
953b22be57 |
fix: the stats bar's controls were dead — pointer-events + button affordance
Live-driving the real browser caught it (the user's report: "they don't work as buttons"): the stats row sits OUTSIDE the composer shell's pointer-events-auto div, inside the sticky scroll wrapper's pointer-events-none — every control on the row was unclickable, including the pre-existing stats-detail popover button. The row now re-enables pointer-events itself. Also, per the user's sizing note: bar text 10px → 11px with a touch more breathing room, and the model + thinking controls gain quiet pill affordance (rounded-full border, card bg, ▾ chevron) — still muted by design, but visibly buttons. Verified live end-to-end in the browser: click opens the model menu ABOVE (366 rows, search autofocused), "qwen flash" narrows to 5, picking qwen3.8-flash persists + flips the label, the thinking menu shows exactly that model's supported levels (Off/Minimal/Low/Medium/High), Medium persists on the record via PUT, labels update. Test session settings restored after the drive. Gates 350/350. |
||
|
|
de0c286311 |
feat: model + thinking control — the stats bar's picker, on pi-ai's catalog
The model-control wave (user-decided design, 2026-09-12): ONE controller
for the conversation agents (prober/teacher are functionally one agent);
background sub-agents stay config-pinned. pi-ai owns the registry — the
shipped 366-model OpenRouter catalog (cost, context, modality,
thinkingLevelMap), getSupportedThinkingLevels/clampThinkingLevel for the
per-model thinking truth, and streamSimple's automatic clamp. No second
registry to drift out of sync.
Seams:
- config.ts: ReasoningEffort retired for pi's ThinkingLevel
(off|minimal|low|medium|high|xhigh|max). GLM roles go honest at "low" —
pi's catalog says GLM-5.3-flash accepts exactly low|high|max, and the old
"minimal" was silently clamped to low by streamSimple since the swap;
nothing changes behaviorally. visualMaker → qwen/qwen3.8-flash (fixes
the dead qwen/qwen3.8-max slug — the catalog only ships -0902; also the
user's cost pick, 0.15/0.47 per-1M).
- src/agent/models.ts: piEngineFor(role, settings?) — provider+id resolved
from the catalog, thinking level from settings ?? config, CLAMPED to the
model's supported list (a settings change between models can never send
an invalid level). PiEngine gains `level` (observability/tests).
- ChatRecord.settings {model?: {provider, id}, thinkingLevel?} — written
by PUT /api/chats/[id]/settings (replace-whole, no updatedAt churn),
read by chat-handler at turn composition: agents are stateless per turn,
so the change is next-turn-effective by construction. The turns log
still records the ACTUAL billed model (the honest witness).
- GET /api/models: the configured-provider catalog as client-safe DTOs
(core/contracts/models.ts) + config defaults; a second provider joins
the picker by registering it in piModels() — nothing else changes.
- PUT settings validation gate (src/server/model-catalog.ts): unknown
provider/model or unsupported level → 400 carrying the supported list,
never a silent clamp.
- scripts/sync-pricing.ts: regenerated PRICES/CONTEXT_LIMITS from pi-ai's
shipped openrouter.json (offline; same source as the engine + picker).
All 366 models priced; glm's stale 0.15/0.5 → pi's 0.075/0.25, limit
1310720 → 1048576 — cost is inferred at read time, so history re-prices.
- UI (components/model-picker.tsx): stats-bar right side becomes model +
thinking buttons; menus open ABOVE; fuzzy search over "provider id
name" (core/fuzzy.ts — pi's /model pattern); model rows show name/id/
price/context; the thinking menu lists exactly the chosen model's
supported levels (GLM: low|high|max — no dead options, no "off").
Model switch drops an incompatible pinned level (falls back to config,
which clamps honestly) rather than carrying a 400.
Tests (350/350, +15): catalog projection + defaults, the settings gate,
pi thinking truth pinned against upgrades (glm low|high|max; minimal→low;
off→low — GLM cannot disable thinking), the engine factory's
override/fallback/clamp/throw matrix, fuzzy ranking, store round-trip,
and the re-priced folds (usage, stats bar, activity dock).
Live smoke (disposable driver, deleted): catalog route 366 + defaults;
glm+minimal → 400 + supported; turn 1 billed qwen/qwen3.8-flash under
the override; mid-session switch flipped turn 2 to glm; clear-overrides
restored the config pin. ALL CHECKS PASS on real OpenRouter.
|
||
|
|
df58bb87f2 |
The stats bar: context + cost beneath the composer, per-turn detail
One quiet row beneath the composer/question shell (not flush to the screen bottom; static height, outside the morphing shell): - LEFT: context window + session cost, clickable → per-turn detail popover (context, total, unpriced count, one row per turn with model/steps/cost). All folds from core/prices.ts — sessionCost, contextWindow, turnCost — no cost math in components. - RIGHT: model + "Thinking —" — a placeholder slot for the coming model/thinking-level CONTROL (later work with its contract changes); display-only today, model read from the last turn's usage, pretty-printed (Glm 5.3 Flash). - Data: the record's turns log, seeded on mount, refreshed when the local stream settles, kept fresh by the existing record re-syncs. No turns yet → "—" everywhere; unpriceable turns never fold into a fake $0 (the doctrine). - formatUsd/formatTokens moved to components/usage-format.ts (shared with the activity dock); prettyModel joins them, pure + tested. |