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:
@@ -52,6 +52,7 @@ import {
|
|||||||
getUsageSummary,
|
getUsageSummary,
|
||||||
listConfigurations,
|
listConfigurations,
|
||||||
REASONING_EFFORTS,
|
REASONING_EFFORTS,
|
||||||
|
saveActiveReasoning,
|
||||||
updateConfiguration,
|
updateConfiguration,
|
||||||
type CatalogEntry,
|
type CatalogEntry,
|
||||||
type LlmConfiguration,
|
type LlmConfiguration,
|
||||||
@@ -134,6 +135,10 @@ export function LlmConfigurationsPanel() {
|
|||||||
baseURL: c.base_url || undefined,
|
baseURL: c.base_url || undefined,
|
||||||
secretName: c.secret_name || undefined,
|
secretName: c.secret_name || undefined,
|
||||||
})
|
})
|
||||||
|
// Inherit this config's reasoning default. The /ai composer chip
|
||||||
|
// listens for this and updates live; if the operator already
|
||||||
|
// override it via the chip, the next save propagates here.
|
||||||
|
saveActiveReasoning(c.reasoning_effort ?? "off")
|
||||||
setActive(loadActiveSettings())
|
setActive(loadActiveSettings())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -198,3 +198,49 @@ export function findSpend(
|
|||||||
): UsageByModelRow | undefined {
|
): UsageByModelRow | undefined {
|
||||||
return rows.find((r) => r.provider === config.provider && r.model === config.model)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,6 +79,12 @@ import {
|
|||||||
runLLMToolCalls,
|
runLLMToolCalls,
|
||||||
} from "~/lib/admin-tools"
|
} from "~/lib/admin-tools"
|
||||||
import { ARCADIA_KNOWLEDGE } from "~/lib/arcadia-knowledge"
|
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 { formatAdminContextForPrompt } from "~/lib/admin-context"
|
||||||
import { ConfirmCard } from "~/components/assistant/confirm-card"
|
import { ConfirmCard } from "~/components/assistant/confirm-card"
|
||||||
import { renderToolResult } from "~/components/assistant/tool-result-renderers"
|
import { renderToolResult } from "~/components/assistant/tool-result-renderers"
|
||||||
@@ -181,21 +187,11 @@ function clearLive() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Per-conversation reasoning override. Cycle order matters — the composer
|
/* Per-conversation reasoning override. Cycle order matters — the composer
|
||||||
* chip walks this array. */
|
* chip walks this array. Storage helpers (load/save/subscribe) live in
|
||||||
type ReasoningEffort = "off" | "low" | "medium" | "high" | "max"
|
* 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_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>>(
|
function withReasoning<T extends Record<string, unknown>>(
|
||||||
extras: T,
|
extras: T,
|
||||||
effort: ReasoningEffort,
|
effort: ReasoningEffort,
|
||||||
@@ -537,7 +533,9 @@ function ChatSurface({
|
|||||||
setMessages([])
|
setMessages([])
|
||||||
setAgentHistory(new Map())
|
setAgentHistory(new Map())
|
||||||
setMessageAgents(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])
|
}, [setMessages])
|
||||||
|
|
||||||
// Auto tool-loop using native function calls. Reads run automatically;
|
// 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
|
// Per-conversation reasoning override. Persists across page reloads via
|
||||||
// localStorage so the operator's chosen level survives a refresh, but
|
// localStorage so the operator's chosen level survives a refresh, but
|
||||||
// resets when they clear the conversation. "off" = pass nothing through.
|
// 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>(
|
const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort>(
|
||||||
() => loadReasoning(),
|
() => loadActiveReasoning(),
|
||||||
)
|
)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
saveReasoning(reasoningEffort)
|
saveActiveReasoning(reasoningEffort)
|
||||||
reasoningEffortRef.current = reasoningEffort
|
reasoningEffortRef.current = reasoningEffort
|
||||||
}, [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(() => {
|
const cycleReasoning = useCallback(() => {
|
||||||
setReasoningEffort((cur) => {
|
setReasoningEffort((cur) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user