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.
This commit is contained in:
amosblomqvist
2026-09-22 13:54:29 +03:00
parent 21d288e9f9
commit 48fbd988f1
15 changed files with 814 additions and 155 deletions
+49 -2
View File
@@ -4,6 +4,8 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import { CornerDownLeftIcon } from "lucide-react";
import { GraphView } from "@/components/graph-view";
import { SetupDialog } from "@/components/setup-dialog";
import { useSettingsDialog } from "@/components/settings/settings-dialog.tsx";
import { GraphContextControl, effectiveContextTokens } from "@/components/graph-context-control";
import { useDefaultSettings } from "@/components/default-settings";
import { DictationBar, useDictation } from "@/components/dictation";
@@ -17,9 +19,17 @@ import type { ChatSettings, TurnUsage } from "@/src/core";
* under the header and the new-session dock, both of which float over it
* with no gradient scrims. Sessions live in the toggleable sidebar.
*/
export function Home({ initialSettings }: { initialSettings: ChatSettings | null }) {
export function Home({ initialSettings, initialConfigured }: { initialSettings: ChatSettings | null; initialConfigured: boolean }) {
const router = useRouter();
const [topic, setTopic] = useState("");
// First-run gate (model-slots wave, owner 2026-09-20): chats cannot
// start until the user has picked all three slots — no defaults for
// 33 providers. The dock shows the CTA while unconfigured; submitting
// a topic opens the setup dialog WITH THE DRAFT PRESERVED (Save
// stores the slots and the queued message sends).
const [configured, setConfigured] = useState(initialConfigured);
const [setupOpen, setSetupOpen] = useState(false);
const { open: openSettings } = useSettingsDialog();
// Voice dictation (STT wave, 2026-09-17): finals append at the end of the
// topic draft; the strip lives in the stats bar's middle slot.
const dictation = useDictation({
@@ -34,9 +44,17 @@ export function Home({ initialSettings }: { initialSettings: ChatSettings | null
// defers its save when chatId is empty; see components/model-picker).
const { settings, save: saveDraft } = useDefaultSettings(initialSettings);
const start = async () => {
const start = async (force = false) => {
const t = topic.trim();
if (!t || busy) return;
// Unconfigured: the send INTERCEPTS into the setup flow (the draft
// stays in the textarea; Save completes and this re-runs). `force`
// is the onDone path — the state this closure captured is still
// the pre-save `configured`, the state update hasn't landed yet.
if (!configured && !force) {
setSetupOpen(true);
return;
}
setBusy(true);
try {
const res = await fetch("/api/chats", {
@@ -96,6 +114,18 @@ export function Home({ initialSettings }: { initialSettings: ChatSettings | null
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">
{!configured && (
<div className="mb-2 flex justify-center">
<button
type="button"
data-testid="setup-cta"
onClick={() => setSetupOpen(true)}
className="rounded-sm border border-border bg-card px-3 py-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50"
>
Set up your models — chats start after the three picks
</button>
</div>
)}
<form
className="flex items-end gap-1.5 rounded-2xl border bg-card p-1.5 shadow-lg"
onSubmit={(e) => {
@@ -155,6 +185,23 @@ export function Home({ initialSettings }: { initialSettings: ChatSettings | null
/>
</div>
</div>
{/* First-run setup (unconfigured): the CTA's target and the send
interception's landing surface. onDone replays the intercepted
draft; onOpenKeys deep-links the settings dialog (stage 1). */}
<SetupDialog
open={setupOpen}
onClose={() => setSetupOpen(false)}
onDone={() => {
setConfigured(true);
setSetupOpen(false);
void start(true); // the queued draft sends
}}
onOpenKeys={() => {
setSetupOpen(false);
openSettings("keys");
}}
/>
</div>
);
}