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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user