| 1 | import { useSyncExternalStore } from "react"; |
| 2 | import { applySessionExperience } from "./sessionExperience"; |
| 3 | |
| 4 | export type ReasoningDisplayMode = "hidden" | "summary" | "auto" | "expanded"; |
| 5 | export type ResolvedReasoningDisplayMode = ReasoningDisplayMode | "legacy-collapsed" | "pending"; |
| 6 | |
| 7 | const LEGACY_SUMMARY_KEY = "reasonix-reasoning-summary"; |
| 8 | const DISPLAY_EVENT = "reasonix:reasoning-display-mode"; |
| 9 | |
| 10 | let currentMode: ResolvedReasoningDisplayMode = "auto"; |
| 11 | let currentModeExplicit = false; |
| 12 | const listeners = new Set<() => void>(); |
| 13 | |
| 14 | function emit(): void { |
| 15 | for (const listener of listeners) listener(); |
| 16 | if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent(DISPLAY_EVENT, { detail: currentMode })); |
| 17 | } |
| 18 | |
| 19 | function validMode(value: unknown): ReasoningDisplayMode | undefined { |
| 20 | return value === "hidden" || value === "summary" || value === "auto" || value === "expanded" ? value : undefined; |
| 21 | } |
| 22 | |
| 23 | function legacyValue(): boolean | undefined { |
| 24 | if (typeof localStorage === "undefined") return undefined; |
| 25 | const stored = localStorage.getItem(LEGACY_SUMMARY_KEY); |
| 26 | return stored === "1" ? true : stored === "0" ? false : undefined; |
| 27 | } |
| 28 | |
| 29 | export function resolveReasoningDisplayMode( |
| 30 | configuredMode: unknown, |
| 31 | explicit: boolean, |
| 32 | ): ResolvedReasoningDisplayMode { |
| 33 | const normalized = validMode(configuredMode); |
| 34 | if (explicit && normalized) return normalized; |
| 35 | const legacy = legacyValue(); |
| 36 | if (legacy !== undefined) return legacy ? "summary" : "legacy-collapsed"; |
| 37 | return normalized ?? "auto"; |
| 38 | } |
| 39 | |
| 40 | export function getReasoningDisplayMode(): ResolvedReasoningDisplayMode { |
| 41 | if (!currentModeExplicit && currentMode !== "pending") { |
| 42 | const legacy = legacyValue(); |
| 43 | if (legacy !== undefined) return legacy ? "summary" : "legacy-collapsed"; |
| 44 | } |
| 45 | return currentMode; |
| 46 | } |
| 47 | |
| 48 | export function setReasoningDisplayPending(): void { |
| 49 | if (currentMode === "pending") return; |
| 50 | currentMode = "pending"; |
| 51 | currentModeExplicit = false; |
| 52 | emit(); |
| 53 | } |
| 54 | |
| 55 | /** Hydrates the frontend mirror from the authoritative desktop startup payload. */ |
| 56 | export function hydrateReasoningDisplayMode(configuredMode: unknown, explicit = false): void { |
| 57 | const next = resolveReasoningDisplayMode(configuredMode, explicit); |
| 58 | // Compatibility callers still participate in the canonical two-state |
| 59 | // model. Historical configuration is intentionally normalized to Standard; |
| 60 | // only an explicit legacy "expanded" selection maps to Deep. |
| 61 | if (explicit) { |
| 62 | applySessionExperience(next === "expanded" ? "deep" : "standard"); |
| 63 | } |
| 64 | if (next === currentMode && currentModeExplicit === explicit) return; |
| 65 | currentMode = next; |
| 66 | currentModeExplicit = explicit; |
| 67 | emit(); |
| 68 | } |
| 69 | |
| 70 | /** Applies a successfully persisted user selection and completes legacy migration. */ |
| 71 | export function applyReasoningDisplayMode(mode: ReasoningDisplayMode): void { |
| 72 | applySessionExperience(mode === "expanded" ? "deep" : "standard"); |
| 73 | if (typeof localStorage !== "undefined") localStorage.removeItem(LEGACY_SUMMARY_KEY); |
| 74 | if (mode === currentMode && currentModeExplicit) return; |
| 75 | currentMode = mode; |
| 76 | currentModeExplicit = true; |
| 77 | emit(); |
| 78 | } |
| 79 | |
| 80 | export function onReasoningDisplayModeChange(cb: () => void): () => void { |
| 81 | listeners.add(cb); |
| 82 | return () => listeners.delete(cb); |
| 83 | } |
| 84 | |
| 85 | export function useReasoningDisplayMode(): ResolvedReasoningDisplayMode { |
| 86 | return useSyncExternalStore(onReasoningDisplayModeChange, getReasoningDisplayMode, getReasoningDisplayMode); |
| 87 | } |
| 88 | |
| 89 | // Compatibility helpers for older frontend tests/extensions. They keep the |
| 90 | // legacy localStorage key semantics without reintroducing the old settings UI. |
| 91 | export function getReasoningSummaryEnabled(): boolean { |
| 92 | const mode = getReasoningDisplayMode(); |
| 93 | return mode === "summary" || mode === "auto"; |
| 94 | } |
| 95 | |
| 96 | export function setReasoningSummaryEnabled(enabled: boolean): void { |
| 97 | if (typeof localStorage !== "undefined") localStorage.setItem(LEGACY_SUMMARY_KEY, enabled ? "1" : "0"); |
| 98 | if (currentMode !== "summary" && currentMode !== "legacy-collapsed") return; |
| 99 | currentMode = enabled ? "summary" : "legacy-collapsed"; |
| 100 | currentModeExplicit = false; |
| 101 | emit(); |
| 102 | } |
| 103 | |
| 104 | export function useReasoningSummaryEnabled(): boolean { |
| 105 | const mode = useReasoningDisplayMode(); |
| 106 | return mode === "summary" || mode === "auto"; |
| 107 | } |
| 108 |