ai: composer chip inherits active config's reasoning default

Pulls the reasoning storage out of ai.tsx and into the shared
llm-configs.ts helpers so Settings → LLM and the /ai composer
coordinate via one localStorage key (crema.ai.reasoning):

- loadActiveReasoning / saveActiveReasoning: read/write helpers.
- subscribeActiveReasoning: dispatches a CustomEvent on writes
  (same-tab) plus a storage-event listener (cross-tab), so the
  chip updates live when the operator stars a different config in
  another tab or in the settings panel.

Wiring:
- Settings panel onMakeActive() now also calls
  saveActiveReasoning(c.reasoning_effort ?? "off"). Starring a
  config seeds the chip with that config's default.
- /ai chip useEffect subscribes to changes; a star in Settings
  while /ai is open flips the chip in real time.
- resetAndClear no longer wipes reasoningEffort. Clearing the
  conversation shouldn't silently undo the operator's stated
  intent for thinking-mode (which is bound to their active config,
  not to the conversation).

Net behaviour:
- Star a config with reasoning_effort=medium → chip on /ai shows
  THINK MEDIUM next time you visit (or immediately if /ai is open).
- Cycle the chip while on /ai → just an override for the current
  conversation, not back-propagated to the saved config.
- Edit the config in Settings to change its default → propagates to
  the chip on next star (intentional — direct edits don't auto-
  re-activate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-02 20:18:06 +10:00
parent c379ebc37a
commit c640721c8e
3 changed files with 75 additions and 16 deletions

View File

@@ -198,3 +198,49 @@ export function findSpend(
): UsageByModelRow | undefined {
return rows.find((r) => r.provider === config.provider && r.model === config.model)
}
// ---------------------------------------------------------------------------
// Active reasoning_effort (shared between settings panel and /ai composer)
//
// Stored under crema.ai.reasoning. Written when the operator stars a config
// in the settings panel (so the chip on /ai inherits that config's default
// on next mount) and when the operator cycles the THINK chip on /ai (per-
// conversation override). Wiped on Clear conversation.
// ---------------------------------------------------------------------------
const ACTIVE_REASONING_KEY = "crema.ai.reasoning"
const ACTIVE_REASONING_EVENT = "crema:ai-reasoning-change"
export function loadActiveReasoning(): ReasoningEffort {
if (typeof window === "undefined") return "off"
const v = localStorage.getItem(ACTIVE_REASONING_KEY) as ReasoningEffort | null
return v && REASONING_EFFORTS.includes(v) ? v : "off"
}
export function saveActiveReasoning(v: ReasoningEffort): void {
if (typeof window === "undefined") return
if (v === "off") localStorage.removeItem(ACTIVE_REASONING_KEY)
else localStorage.setItem(ACTIVE_REASONING_KEY, v)
window.dispatchEvent(new CustomEvent(ACTIVE_REASONING_EVENT, { detail: v }))
}
export function subscribeActiveReasoning(
listener: (v: ReasoningEffort) => void,
): () => void {
if (typeof window === "undefined") return () => {}
const onChange = (e: Event) => {
const detail = (e as CustomEvent<ReasoningEffort>).detail
if (detail) listener(detail)
else listener(loadActiveReasoning())
}
// Same-tab via the custom event; cross-tab via the storage event.
const onStorage = (e: StorageEvent) => {
if (e.key === ACTIVE_REASONING_KEY) listener(loadActiveReasoning())
}
window.addEventListener(ACTIVE_REASONING_EVENT, onChange)
window.addEventListener("storage", onStorage)
return () => {
window.removeEventListener(ACTIVE_REASONING_EVENT, onChange)
window.removeEventListener("storage", onStorage)
}
}