Fix wave chunk 2: thinking levels auto-populate everywhere; legacy records show the resolved level
- thinking-default.ts: defaultOrderLevel (the ordering's first supported, the client twin of the server's defaultThinkingLevel) + populateLevel (an explicit level always rides; the WRITE clamps server-side, bd68486, and the cross-surface signal re-reads the clamped truth; only absence populates from the ordering) + THINKING_LABEL shared. - Setup dialog (item 5): per-slot thinking selectors pre-populated with the default-order level per picked model, editable; disabled until the model pick; prefillLevelFromPin keeps a stored level as-is and populates legacy pins from the ordering. - Composer + agent section (item 6): fresh model picks always write a level: the stored one rides, absence populates; the unset state can never re-enter. Item 4: records with a model but no stored level display the RESOLVED level (ordering's first supported) instead of a dash, in the composer pill and the settings slot triggers/menus. - Item 7 verified live end-to-end: an explicit level rides into the PUT, the server clamps, the signal re-reads, the pill shows the clamped truth.
This commit is contained in:
+18
-15
@@ -8,6 +8,7 @@ import { ModelMenuList, ThinkingMenuList } from "@/components/settings/model-men
|
||||
import { composeSlotModel } from "@/components/settings/agent-section";
|
||||
import { MenuShell } from "@/components/menu-shell";
|
||||
import { notifySettingsSaved, useSettingsSaved } from "@/components/settings-signal";
|
||||
import { defaultOrderLevel, populateLevel, THINKING_LABEL } from "@/components/thinking-default";
|
||||
|
||||
/** GET /api/models is cached per page load for instant menu opens, but
|
||||
* the federation is LIVE per call — keys saved mid-session widen the
|
||||
@@ -63,16 +64,6 @@ function useModelCatalog(): { catalog: ModelCatalogResponse | undefined; refresh
|
||||
return { catalog, refresh };
|
||||
}
|
||||
|
||||
const LEVEL_LABEL: Record<string, string> = {
|
||||
off: "Off",
|
||||
minimal: "Minimal",
|
||||
low: "Low",
|
||||
medium: "Medium",
|
||||
high: "High",
|
||||
xhigh: "Xhigh",
|
||||
max: "Max",
|
||||
};
|
||||
|
||||
/**
|
||||
* THE STRONG-SLOT CONTROL (fix wave, owner ruling: "one and the same").
|
||||
* The composer's model + thinking pills ARE the strong slot — the tier
|
||||
@@ -120,11 +111,16 @@ export function ModelControl() {
|
||||
const override = models?.strong;
|
||||
const effectiveId = override?.model?.id;
|
||||
const effectiveProvider = override?.model?.provider;
|
||||
const effectiveLevel = override?.thinkingLevel;
|
||||
const storedLevel = override?.thinkingLevel;
|
||||
const entry = catalog?.models.find(
|
||||
(m) => m.id === effectiveId && m.provider === effectiveProvider,
|
||||
);
|
||||
const levels = entry?.supportedLevels ?? [];
|
||||
// Legacy display (chunk 2 item 4, owner): a record with a model but no
|
||||
// stored level shows the RESOLVED level — the ordering's first supported
|
||||
// — because that is exactly what the engine build would run. Display-
|
||||
// only: the record stays unset until a fresh pick populates it.
|
||||
const effectiveLevel = storedLevel ?? (entry ? defaultOrderLevel(levels) : undefined);
|
||||
|
||||
// The intuitive dropdown contract (owner, stats-control round 1): any
|
||||
// click outside the open menu — or an Esc keypress — closes it. The
|
||||
@@ -216,9 +212,16 @@ export function ModelControl() {
|
||||
// thinking level only when the new model supports it — otherwise
|
||||
// drop it (a carry-over would 400 at the server's catalog gate).
|
||||
const pickModel = (m: ModelCatalogEntry) => {
|
||||
const nextLevel =
|
||||
effectiveLevel !== undefined && m.supportedLevels.includes(effectiveLevel) ? effectiveLevel : undefined;
|
||||
void save(composeSlotModel(models, "strong", m.provider, m.id, nextLevel ?? ""));
|
||||
// Auto-populate (chunk 2, owner: "populate everywhere"): a fresh pick
|
||||
// ALWAYS writes a level — the STORED one rides as-is (the write clamps
|
||||
// server-side when the new model doesn't support it; the signal
|
||||
// re-reads and the clamped truth displays), and a record with no
|
||||
// stored level populates the ordering's level. The unset state can
|
||||
// never re-enter through this door. (The DERIVED display level never
|
||||
// rides — it belongs to the OLD model; the fresh pick's level comes
|
||||
// from the fresh model's own ladder.)
|
||||
const nextLevel = populateLevel(storedLevel, m.supportedLevels);
|
||||
void save(composeSlotModel(models, "strong", m.provider, m.id, nextLevel));
|
||||
};
|
||||
|
||||
// Stale-while-revalidate on manual open too (⌘K covers the keyboard
|
||||
@@ -259,7 +262,7 @@ export function ModelControl() {
|
||||
{/* Just the level — the button reads like the model button reads
|
||||
the model name (owner, stats-control round 1). */}
|
||||
<span className="text-card-foreground">
|
||||
{effectiveLevel === undefined ? "—" : (LEVEL_LABEL[effectiveLevel] ?? effectiveLevel)}
|
||||
{effectiveLevel === undefined ? "—" : (THINKING_LABEL[effectiveLevel] ?? effectiveLevel)}
|
||||
</span>
|
||||
<span aria-hidden className="text-xs text-muted-foreground">▾</span>
|
||||
</button>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { MenuShell } from "@/components/menu-shell";
|
||||
import { useSettingsSaved } from "@/components/settings-signal";
|
||||
import { defaultOrderLevel, populateLevel } from "@/components/thinking-default";
|
||||
import type React from "react";
|
||||
import {
|
||||
MODEL_SLOTS,
|
||||
@@ -506,7 +507,10 @@ export function AgentSection({
|
||||
widthClass="min-w-20"
|
||||
/>
|
||||
<MenuTrigger
|
||||
value={thinking}
|
||||
// Legacy display (chunk 2 item 4): no stored level + a known
|
||||
// model → the RESOLVED level (the ordering's first supported —
|
||||
// what the engine build runs). Display-only until a pick writes.
|
||||
value={thinking || (chosen ? defaultOrderLevel(levels) : "")}
|
||||
emptyLabel="—"
|
||||
ariaLabel={`${SLOT_META[slot].label} thinking`}
|
||||
testid={`slot-thinking-${slot}`}
|
||||
@@ -526,7 +530,11 @@ export function AgentSection({
|
||||
effectiveId={modelId}
|
||||
effectiveProvider={provider}
|
||||
onPick={(entry) => {
|
||||
onModelsChange(composeSlotModel(models, slot, entry.provider, entry.id, thinking));
|
||||
// Auto-populate (chunk 2, owner: "populate everywhere"): the
|
||||
// fresh pick writes a level — the stored one rides as-is
|
||||
// (the write clamps server-side), else the ordering's level.
|
||||
const next = populateLevel(thinking || undefined, entry.supportedLevels);
|
||||
onModelsChange(composeSlotModel(models, slot, entry.provider, entry.id, next));
|
||||
setOpenMenu(null);
|
||||
}}
|
||||
footer={
|
||||
@@ -546,7 +554,9 @@ export function AgentSection({
|
||||
>
|
||||
<ThinkingMenuList
|
||||
levels={levels.map(String)}
|
||||
effectiveLevel={thinking || undefined}
|
||||
// The ✓ sits on the RESOLVED level for legacy unset records —
|
||||
// the same one the trigger shows.
|
||||
effectiveLevel={thinking || (chosen ? defaultOrderLevel(levels) : undefined)}
|
||||
onPick={(level) => {
|
||||
onModelsChange(composeSlotModel(models, slot, provider, modelId, level));
|
||||
setOpenMenu(null);
|
||||
|
||||
+105
-16
@@ -37,6 +37,9 @@ import { prettyModel } from "@/components/usage-format";
|
||||
import { ModelMenuList } from "@/components/settings/model-menu";
|
||||
import { MenuShell } from "@/components/menu-shell";
|
||||
import { notifySettingsSaved } from "@/components/settings-signal";
|
||||
import { defaultOrderLevel, populateLevel, THINKING_LABEL } from "@/components/thinking-default";
|
||||
import { ThinkingMenuList } from "@/components/settings/model-menu";
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
|
||||
/** The pickers' slot identity — same roster language as the Agent
|
||||
* section's slot rows. */
|
||||
@@ -54,7 +57,7 @@ interface ModelsBody {
|
||||
/** The three tiers' RESOLVED pins (BYOK: NULL per unset tier) — the
|
||||
* PREFILL + "needed" marks (owner, 2026-09-22): pinned tiers open
|
||||
* with their pick, unpinned tiers carry the mark. */
|
||||
slots?: Record<ModelSlotName, { model: { provider: string; id: string } } | null>;
|
||||
slots?: Record<ModelSlotName, { model: { provider: string; id: string }; thinkingLevel?: ThinkingLevel } | null>;
|
||||
}
|
||||
|
||||
/** Pure: which stage the flow opens at — no keyed provider = keys first
|
||||
@@ -86,6 +89,18 @@ export function prefillFromSlots(
|
||||
return pre;
|
||||
}
|
||||
|
||||
/** Level prefill (chunk 2): a pinned tier opens with its STORED level
|
||||
* as-is (Save clamps server-side if the ladder lapsed), else the
|
||||
* ordering's level for that model — the level a fresh pick would
|
||||
* populate (owner: the ordering's only purpose is first assignment).
|
||||
* Exported pure for tests. */
|
||||
export function prefillLevelFromPin(
|
||||
entry: ModelCatalogEntry,
|
||||
pin: { thinkingLevel?: ThinkingLevel } | null | undefined,
|
||||
): ThinkingLevel {
|
||||
return populateLevel(pin?.thinkingLevel, entry.supportedLevels);
|
||||
}
|
||||
|
||||
export function SetupDialog({
|
||||
open,
|
||||
onClose,
|
||||
@@ -103,10 +118,13 @@ export function SetupDialog({
|
||||
const [body, setBody] = useState<ModelsBody | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [picked, setPicked] = useState<Partial<Record<ModelSlotName, ModelCatalogEntry>>>({});
|
||||
/** Per-slot thinking level (chunk 2): populated from the ordering on
|
||||
* every fresh model pick, editable — an explicit pick always wins. */
|
||||
const [levels, setLevels] = useState<Partial<Record<ModelSlotName, ThinkingLevel>>>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [openSlot, setOpenSlot] = useState<ModelSlotName | null>(null);
|
||||
/** The open slot's trigger wrapper — the shared shell's anchor (fix
|
||||
const [openMenu, setOpenMenu] = useState<{ slot: ModelSlotName; kind: "model" | "thinking" } | null>(null);
|
||||
/** The open menu's trigger wrapper — the shared shell's anchor (fix
|
||||
* wave chunk B): one menu open at a time, so one ref suffices, and
|
||||
* the menu PORTALS out of this dialog's overflow-hidden frame — it
|
||||
* extends beyond the dialog exactly like the settings menus do. */
|
||||
@@ -117,6 +135,7 @@ export function SetupDialog({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setPicked({});
|
||||
setLevels({});
|
||||
setError(null);
|
||||
fetch("/api/models")
|
||||
.then((r) => r.json())
|
||||
@@ -127,7 +146,16 @@ export function SetupDialog({
|
||||
// pick (matched in the FEDERATED catalog — a lapsed key's pin
|
||||
// does not prefill); unpinned tiers stay empty and carry the
|
||||
// "needed" mark. First-run = nothing pinned → all three marked.
|
||||
setPicked(prefillFromSlots(b.models, b.slots));
|
||||
const pre = prefillFromSlots(b.models, b.slots);
|
||||
setPicked(pre);
|
||||
// Levels prefill with the pins (chunk 2): stored level if still
|
||||
// supported, else the ordering's level for the pinned model.
|
||||
const preLevels: Partial<Record<ModelSlotName, ThinkingLevel>> = {};
|
||||
for (const slot of SLOT_ORDER) {
|
||||
const entry = pre[slot];
|
||||
if (entry) preLevels[slot] = prefillLevelFromPin(entry, b.slots?.[slot]);
|
||||
}
|
||||
setLevels(preLevels);
|
||||
})
|
||||
.catch(() => setFailed(true));
|
||||
}, [open]);
|
||||
@@ -140,14 +168,16 @@ export function SetupDialog({
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape") return;
|
||||
if (openSlot !== null) setOpenSlot(null);
|
||||
if (openMenu !== null) setOpenMenu(null);
|
||||
else onClose();
|
||||
};
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (openSlot === null) return;
|
||||
if (openMenu === null) return;
|
||||
const t = e.target as Element;
|
||||
if (t.closest("[data-lh-menu]") || t.closest(`[data-testid="setup-trigger-${openSlot}"]`)) return;
|
||||
setOpenSlot(null);
|
||||
const triggerTestid =
|
||||
openMenu.kind === "model" ? `setup-trigger-${openMenu.slot}` : `setup-thinking-${openMenu.slot}`;
|
||||
if (t.closest("[data-lh-menu]") || t.closest(`[data-testid="${triggerTestid}"]`)) return;
|
||||
setOpenMenu(null);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
document.addEventListener("pointerdown", onPointerDown);
|
||||
@@ -155,7 +185,7 @@ export function SetupDialog({
|
||||
window.removeEventListener("keydown", onKey);
|
||||
document.removeEventListener("pointerdown", onPointerDown);
|
||||
};
|
||||
}, [open, onClose, openSlot]);
|
||||
}, [open, onClose, openMenu]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
@@ -167,7 +197,13 @@ export function SetupDialog({
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
const models: ModelsSettings = {};
|
||||
for (const slot of SLOT_ORDER) models[slot] = { model: { provider: picked[slot]!.provider, id: picked[slot]!.id } };
|
||||
for (const slot of SLOT_ORDER) {
|
||||
const level = levels[slot];
|
||||
models[slot] = {
|
||||
model: { provider: picked[slot]!.provider, id: picked[slot]!.id },
|
||||
...(level ? { thinkingLevel: level } : {}),
|
||||
};
|
||||
}
|
||||
const res = await fetch("/api/settings/models", {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
@@ -235,6 +271,8 @@ export function SetupDialog({
|
||||
</p>
|
||||
{SLOT_ORDER.map((slot) => {
|
||||
const entry = picked[slot];
|
||||
const level = levels[slot];
|
||||
const levelsForEntry = entry?.supportedLevels ?? [];
|
||||
const catalog =
|
||||
slot === "vision" ? body?.models.filter((m) => m.input.includes("image")) : body?.models;
|
||||
return (
|
||||
@@ -251,21 +289,22 @@ export function SetupDialog({
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground/70">{SLOT_META[slot].roster}</span>
|
||||
<div ref={openSlot === slot ? triggerRef : undefined} className="relative">
|
||||
<div className="flex items-center gap-2">
|
||||
<div ref={openMenu?.slot === slot && openMenu.kind === "model" ? triggerRef : undefined} className="relative flex-1">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${SLOT_META[slot].label} model`}
|
||||
data-testid={`setup-trigger-${slot}`}
|
||||
aria-expanded={openSlot === slot}
|
||||
onClick={() => setOpenSlot(openSlot === slot ? null : slot)}
|
||||
className="flex h-7 min-w-40 items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50"
|
||||
aria-expanded={openMenu?.slot === slot && openMenu.kind === "model"}
|
||||
onClick={() => setOpenMenu(openMenu?.slot === slot && openMenu.kind === "model" ? null : { slot, kind: "model" })}
|
||||
className="flex h-7 min-w-40 w-full items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{entry ? prettyModel(entry.id) : "Pick a model…"}</span>
|
||||
<span aria-hidden className="text-muted-foreground">
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
{openSlot === slot && (
|
||||
{openMenu?.slot === slot && openMenu.kind === "model" && (
|
||||
<MenuShell
|
||||
anchorRef={triggerRef}
|
||||
testid={`setup-menu-${slot}`}
|
||||
@@ -279,13 +318,63 @@ export function SetupDialog({
|
||||
effectiveProvider={entry?.provider}
|
||||
onPick={(m) => {
|
||||
setPicked((prev) => ({ ...prev, [slot]: m }));
|
||||
setOpenSlot(null);
|
||||
// Auto-populate (chunk 2, owner: "populate
|
||||
// everywhere"): no explicit level → the
|
||||
// ordering's level for the fresh pick; an
|
||||
// explicit one rides (Save clamps server-side).
|
||||
setLevels((prev) => ({
|
||||
...prev,
|
||||
[slot]: populateLevel(prev[slot], m.supportedLevels),
|
||||
}));
|
||||
setOpenMenu(null);
|
||||
}}
|
||||
footer={catalog && catalog.length === 0 ? "No image-capable models — widen the provider keys" : undefined}
|
||||
/>
|
||||
</MenuShell>
|
||||
)}
|
||||
</div>
|
||||
{/* Thinking selector (chunk 2): editable, pre-populated from
|
||||
the ordering per picked model — no model, no levels. */}
|
||||
<div
|
||||
ref={openMenu?.slot === slot && openMenu.kind === "thinking" ? triggerRef : undefined}
|
||||
className="relative"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${SLOT_META[slot].label} thinking`}
|
||||
data-testid={`setup-thinking-${slot}`}
|
||||
aria-expanded={openMenu?.slot === slot && openMenu.kind === "thinking"}
|
||||
disabled={!entry}
|
||||
onClick={() => setOpenMenu(openMenu?.slot === slot && openMenu.kind === "thinking" ? null : { slot, kind: "thinking" })}
|
||||
className="flex h-7 w-24 items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate text-left">
|
||||
{entry ? (level ? THINKING_LABEL[level] ?? level : "…") : "—"}
|
||||
</span>
|
||||
<span aria-hidden className="text-muted-foreground">
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
{openMenu?.slot === slot && openMenu.kind === "thinking" && (
|
||||
<MenuShell
|
||||
anchorRef={triggerRef}
|
||||
testid={`setup-thinking-menu-${slot}`}
|
||||
preferredSide="down"
|
||||
align="left"
|
||||
menuClass="flex w-44 flex-col gap-0.5 rounded-xl border border-border bg-card p-2 text-xs shadow-xl"
|
||||
>
|
||||
<ThinkingMenuList
|
||||
levels={levelsForEntry.map(String)}
|
||||
effectiveLevel={level}
|
||||
onPick={(lv) => {
|
||||
setLevels((prev) => ({ ...prev, [slot]: lv as ThinkingLevel }));
|
||||
setOpenMenu(null);
|
||||
}}
|
||||
/>
|
||||
</MenuShell>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* The default-order resolution, CLIENT-SIDE (chunk 2, Platform interlude
|
||||
* bd68486): the UI mirrors defaultThinkingLevel from src/agent/models.ts
|
||||
* EXACTLY — first DEFAULT_THINKING_ORDER entry the model supports, else
|
||||
* the first supported level, else "off". One source of truth for the
|
||||
* ordering (the core barrel); this helper is its only client
|
||||
* interpretation, so a fresh model pick can auto-populate the SAME
|
||||
* level the engine would run for an unset record (owner ruling: the
|
||||
* ordering's ONLY purpose is first assignment — an explicit pick always
|
||||
* wins, and after a swap the WRITE clamps server-side, the signal
|
||||
* re-reads, and the clamped truth displays).
|
||||
*/
|
||||
import { DEFAULT_THINKING_ORDER } from "@/src/core";
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
|
||||
/** Display labels for the levels (shared by every trigger that shows one). */
|
||||
export const THINKING_LABEL: Record<string, string> = {
|
||||
off: "Off",
|
||||
minimal: "Minimal",
|
||||
low: "Low",
|
||||
medium: "Medium",
|
||||
high: "High",
|
||||
xhigh: "Xhigh",
|
||||
max: "Max",
|
||||
};
|
||||
|
||||
/** The level a fresh pick of `supported` populates with. Empty support
|
||||
* (shouldn't happen — the catalog always reports at least ["off"])
|
||||
* resolves "off", matching the server's fallback. */
|
||||
export function defaultOrderLevel(supported: readonly ThinkingLevel[]): ThinkingLevel {
|
||||
for (const level of DEFAULT_THINKING_ORDER) {
|
||||
if (supported.includes(level)) return level;
|
||||
}
|
||||
return supported[0] ?? "off";
|
||||
}
|
||||
|
||||
/** An EXPLICIT level always rides — even when the fresh pick's model
|
||||
* doesn't support it: the WRITE clamps it to the nearest supported level
|
||||
* server-side (Platform's interlude bd68486 — the owner's swap-400 case
|
||||
* now just works), and the cross-surface signal re-reads so the clamped
|
||||
* truth displays. The ordering only assigns FIRST: when no explicit
|
||||
* level exists, a fresh model pick populates the ordering's level for
|
||||
* that model (chunk 2: the unset state can never re-enter). */
|
||||
export function populateLevel(
|
||||
explicit: ThinkingLevel | undefined,
|
||||
supported: readonly ThinkingLevel[],
|
||||
): ThinkingLevel {
|
||||
return explicit ?? defaultOrderLevel(supported);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseModelsRefusal } from "../components/chat.tsx";
|
||||
import { prefillFromSlots, setupStage } from "../components/setup-dialog.tsx";
|
||||
import { prefillFromSlots, prefillLevelFromPin, setupStage } from "../components/setup-dialog.tsx";
|
||||
import { isModelsRefusal, modelsNotConfigured } from "../src/core/settings.ts";
|
||||
import type { ModelCatalogEntry, ModelSlotName } from "../src/core/index.ts";
|
||||
|
||||
@@ -90,6 +90,24 @@ describe("prefillFromSlots — the SetupDialog prefill (BYOK wave)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("prefillLevelFromPin — the setup dialog's level prefill (chunk 2)", () => {
|
||||
const entry = (levels: string[]) =>
|
||||
({ id: "m", name: "M", provider: "p", reasoning: true, input: ["text"], supportedLevels: levels, cost: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000, maxTokens: 1000 }) as never;
|
||||
|
||||
it("a stored level the pinned model still supports prefills as-is", () => {
|
||||
assert.equal(prefillLevelFromPin(entry(["low", "high"]), { thinkingLevel: "high" }), "high");
|
||||
});
|
||||
|
||||
it("a stored level the model LOST rides as-is — Save clamps server-side (bd68486)", () => {
|
||||
assert.equal(prefillLevelFromPin(entry(["minimal", "medium"]), { thinkingLevel: "xhigh" }), "xhigh");
|
||||
});
|
||||
|
||||
it("a legacy pin with no stored level prefills the ordering's level", () => {
|
||||
assert.equal(prefillLevelFromPin(entry(["low", "medium"]), {}), "medium");
|
||||
assert.equal(prefillLevelFromPin(entry(["low", "medium"]), null), "medium");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupStage — unchanged by the BYOK wave", () => {
|
||||
it("keys first (owner ordering), pickers when a provider is keyed", () => {
|
||||
assert.equal(setupStage(null), "keys");
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* The default-order resolution, client-side (chunk 2): the UI populates a
|
||||
* fresh model pick's thinking level with the SAME level the engine build
|
||||
* runs for an unset record — first DEFAULT_THINKING_ORDER entry the model
|
||||
* supports (Platform's interlude bd68486 moved the ordering into the core
|
||||
* barrel exactly for this). These tests pin the helper against the
|
||||
* server's defaultThinkingLevel semantics (src/agent/models.ts).
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { DEFAULT_THINKING_ORDER } from "../src/core/settings.ts";
|
||||
import { defaultOrderLevel, populateLevel } from "../components/thinking-default.ts";
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
|
||||
const tl = (levels: string[]) => levels as ThinkingLevel[];
|
||||
|
||||
describe("defaultOrderLevel — the ordering's first supported", () => {
|
||||
it("a reasoning model taking the ladder's head (medium) resolves medium", () => {
|
||||
assert.equal(defaultOrderLevel(tl(["minimal", "low", "medium", "high"])), "medium");
|
||||
});
|
||||
|
||||
it("a model without medium falls to the NEXT order entry it supports", () => {
|
||||
// order: medium, low, high, minimal, xhigh, max — low wins here
|
||||
assert.equal(defaultOrderLevel(tl(["minimal", "low", "high"])), "low");
|
||||
assert.equal(defaultOrderLevel(tl(["high", "xhigh", "max"])), "high");
|
||||
});
|
||||
|
||||
it("a non-reasoning model (pi reports exactly [off]) resolves off", () => {
|
||||
assert.equal(defaultOrderLevel(tl(["off"])), "off");
|
||||
});
|
||||
|
||||
it("empty support resolves off (the server's fallback), never crashes", () => {
|
||||
assert.equal(defaultOrderLevel(tl([])), "off");
|
||||
});
|
||||
|
||||
it("the full ladder resolves its head", () => {
|
||||
const full = tl(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
||||
assert.equal(defaultOrderLevel(full), DEFAULT_THINKING_ORDER[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("populateLevel — an explicit pick always rides; absence populates", () => {
|
||||
it("a supported explicit level rides unchanged", () => {
|
||||
assert.equal(populateLevel("high", tl(["low", "high"])), "high");
|
||||
});
|
||||
|
||||
it("an UNSUPPORTED explicit level STILL rides — the write clamps server-side (bd68486), the signal re-reads the clamped truth", () => {
|
||||
// xhigh rides even though unsupported here; never re-populated client-side
|
||||
assert.equal(populateLevel("xhigh", tl(["minimal", "medium", "high"])), "xhigh");
|
||||
});
|
||||
|
||||
it("no explicit level populates the ordering's level (the fresh-pick case)", () => {
|
||||
assert.equal(populateLevel(undefined, tl(["low", "high"])), "low");
|
||||
});
|
||||
|
||||
it("off as an explicit level rides when supported", () => {
|
||||
assert.equal(populateLevel("off", tl(["off"])), "off");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user