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

@@ -79,6 +79,12 @@ import {
runLLMToolCalls,
} from "~/lib/admin-tools"
import { ARCADIA_KNOWLEDGE } from "~/lib/arcadia-knowledge"
import {
loadActiveReasoning,
saveActiveReasoning,
subscribeActiveReasoning,
type ReasoningEffort,
} from "~/lib/arcadia/llm-configs"
import { formatAdminContextForPrompt } from "~/lib/admin-context"
import { ConfirmCard } from "~/components/assistant/confirm-card"
import { renderToolResult } from "~/components/assistant/tool-result-renderers"
@@ -181,21 +187,11 @@ function clearLive() {
}
/* Per-conversation reasoning override. Cycle order matters — the composer
* chip walks this array. */
type ReasoningEffort = "off" | "low" | "medium" | "high" | "max"
* chip walks this array. Storage helpers (load/save/subscribe) live in
* lib/arcadia/llm-configs.ts so the settings panel and the /ai composer
* coordinate via the same crema.ai.reasoning key. */
const REASONING_LEVELS: ReasoningEffort[] = ["off", "low", "medium", "high", "max"]
const REASONING_KEY = "crema.ai.reasoning"
function loadReasoning(): ReasoningEffort {
if (typeof window === "undefined") return "off"
const v = localStorage.getItem(REASONING_KEY) as ReasoningEffort | null
return v && REASONING_LEVELS.includes(v) ? v : "off"
}
function saveReasoning(v: ReasoningEffort) {
if (typeof window === "undefined") return
if (v === "off") localStorage.removeItem(REASONING_KEY)
else localStorage.setItem(REASONING_KEY, v)
}
function withReasoning<T extends Record<string, unknown>>(
extras: T,
effort: ReasoningEffort,
@@ -537,7 +533,9 @@ function ChatSurface({
setMessages([])
setAgentHistory(new Map())
setMessageAgents(new Map())
setReasoningEffort("off")
// Keep reasoningEffort as-is. It's bound to the active config's
// default (set when the operator stars a config in Settings) and
// resetting it would silently undo their intent on every clear.
}, [setMessages])
// Auto tool-loop using native function calls. Reads run automatically;
@@ -873,13 +871,23 @@ function ChatSurface({
// Per-conversation reasoning override. Persists across page reloads via
// localStorage so the operator's chosen level survives a refresh, but
// resets when they clear the conversation. "off" = pass nothing through.
// Initialize from the shared key (settings panel writes this when the
// operator stars a config), persist on change, and live-subscribe so a
// star action in another tab/route updates the chip without a refresh.
const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort>(
() => loadReasoning(),
() => loadActiveReasoning(),
)
useEffect(() => {
saveReasoning(reasoningEffort)
saveActiveReasoning(reasoningEffort)
reasoningEffortRef.current = reasoningEffort
}, [reasoningEffort])
useEffect(() => {
return subscribeActiveReasoning((next) => {
// Don't loop on our own writes — we already wrote `reasoningEffort`
// when it changed. Only pick up writes that disagree with state.
setReasoningEffort((cur) => (cur === next ? cur : next))
})
}, [])
const cycleReasoning = useCallback(() => {
setReasoningEffort((cur) => {