@ashlr/hub 3.8.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/dist/build-identity.json +2 -2
  3. package/dist/cli/resource-profile.js +95 -21
  4. package/dist/cli/resource-profile.js.map +1 -1
  5. package/dist/core/resources/claude-account-usage.d.ts +19 -0
  6. package/dist/core/resources/claude-account-usage.js +22 -1
  7. package/dist/core/resources/claude-account-usage.js.map +1 -1
  8. package/dist/core/resources/native-profile.d.ts +56 -0
  9. package/dist/core/resources/native-profile.js +368 -6
  10. package/dist/core/resources/native-profile.js.map +1 -1
  11. package/dist/core/run/model-catalog.js +8 -1
  12. package/dist/core/run/model-catalog.js.map +1 -1
  13. package/dist/core/universe/builtins/preparation/manifest.json +1 -1
  14. package/dist/core/universe/builtins/preparation/preparation-bridge.mjs +9 -2
  15. package/dist/core/universe/builtins/preparation/preparation-verification-fixtures.mjs +8 -1
  16. package/dist/core/verse/accounts.d.ts +6 -5
  17. package/dist/core/verse/accounts.js +9 -8
  18. package/dist/core/verse/accounts.js.map +1 -1
  19. package/dist/core/verse/adapters/claude.d.ts +49 -2
  20. package/dist/core/verse/adapters/claude.js +317 -56
  21. package/dist/core/verse/adapters/claude.js.map +1 -1
  22. package/dist/core/verse/adapters/codex.d.ts +81 -5
  23. package/dist/core/verse/adapters/codex.js +409 -47
  24. package/dist/core/verse/adapters/codex.js.map +1 -1
  25. package/dist/core/verse/adapters/grok.d.ts +13 -1
  26. package/dist/core/verse/adapters/grok.js +28 -2
  27. package/dist/core/verse/adapters/grok.js.map +1 -1
  28. package/dist/core/verse/adapters/index.d.ts +32 -0
  29. package/dist/core/verse/adapters/index.js +2 -0
  30. package/dist/core/verse/adapters/index.js.map +1 -1
  31. package/dist/core/verse/codex-rollout.d.ts +284 -0
  32. package/dist/core/verse/codex-rollout.js +786 -0
  33. package/dist/core/verse/codex-rollout.js.map +1 -0
  34. package/dist/core/verse/context-fit.d.ts +63 -0
  35. package/dist/core/verse/context-fit.js +355 -0
  36. package/dist/core/verse/context-fit.js.map +1 -0
  37. package/dist/core/verse/context-math.d.ts +215 -0
  38. package/dist/core/verse/context-math.js +365 -0
  39. package/dist/core/verse/context-math.js.map +1 -0
  40. package/dist/core/verse/local-models.d.ts +137 -1
  41. package/dist/core/verse/local-models.js +209 -5
  42. package/dist/core/verse/local-models.js.map +1 -1
  43. package/dist/core/verse/model-windows.d.ts +217 -0
  44. package/dist/core/verse/model-windows.js +616 -0
  45. package/dist/core/verse/model-windows.js.map +1 -0
  46. package/dist/core/verse/preferences.d.ts +123 -0
  47. package/dist/core/verse/preferences.js +403 -0
  48. package/dist/core/verse/preferences.js.map +1 -0
  49. package/dist/core/verse/project-memory.d.ts +112 -0
  50. package/dist/core/verse/project-memory.js +295 -0
  51. package/dist/core/verse/project-memory.js.map +1 -0
  52. package/dist/core/verse/seats.d.ts +106 -4
  53. package/dist/core/verse/seats.js +291 -89
  54. package/dist/core/verse/seats.js.map +1 -1
  55. package/dist/core/verse/session-engine.d.ts +76 -2
  56. package/dist/core/verse/session-engine.js +684 -63
  57. package/dist/core/verse/session-engine.js.map +1 -1
  58. package/dist/core/verse/session-handoff.d.ts +94 -0
  59. package/dist/core/verse/session-handoff.js +551 -0
  60. package/dist/core/verse/session-handoff.js.map +1 -0
  61. package/dist/core/verse/session-search.d.ts +63 -0
  62. package/dist/core/verse/session-search.js +213 -0
  63. package/dist/core/verse/session-search.js.map +1 -0
  64. package/dist/core/verse/session-store.d.ts +16 -2
  65. package/dist/core/verse/session-store.js +80 -6
  66. package/dist/core/verse/session-store.js.map +1 -1
  67. package/dist/core/verse/types.d.ts +333 -1
  68. package/dist/core/verse/types.js +54 -2
  69. package/dist/core/verse/types.js.map +1 -1
  70. package/dist/core/verse/verse-api.d.ts +40 -5
  71. package/dist/core/verse/verse-api.js +724 -25
  72. package/dist/core/verse/verse-api.js.map +1 -1
  73. package/dist/core/web/api.d.ts +14 -4
  74. package/dist/core/web/api.js +26 -11
  75. package/dist/core/web/api.js.map +1 -1
  76. package/dist/core/web/public/next/assets/{App-BsbJFA2j.js → App-D2P1AJiD.js} +5 -5
  77. package/dist/core/web/public/next/assets/{App-BsbJFA2j.js.map → App-D2P1AJiD.js.map} +1 -1
  78. package/dist/core/web/public/next/assets/{ApprovalsSection-2CCSoBTJ.js → ApprovalsSection-Dq66NMfx.js} +2 -2
  79. package/dist/core/web/public/next/assets/{ApprovalsSection-2CCSoBTJ.js.map → ApprovalsSection-Dq66NMfx.js.map} +1 -1
  80. package/dist/core/web/public/next/assets/AutonomySection-DyxpeR5I.js +2 -0
  81. package/dist/core/web/public/next/assets/{AutonomySection-yeNNUPur.js.map → AutonomySection-DyxpeR5I.js.map} +1 -1
  82. package/dist/core/web/public/next/assets/ChatSection-BpOBOUvg.css +1 -0
  83. package/dist/core/web/public/next/assets/ChatSection-bzwFdbdw.js +84 -0
  84. package/dist/core/web/public/next/assets/ChatSection-bzwFdbdw.js.map +1 -0
  85. package/dist/core/web/public/next/assets/ConfirmDialog-DkR0z5_a.js +2 -0
  86. package/dist/core/web/public/next/assets/ConfirmDialog-DkR0z5_a.js.map +1 -0
  87. package/dist/core/web/public/next/assets/{Dialog-nK15L1M0.js → Dialog-CIjlQZqc.js} +2 -2
  88. package/dist/core/web/public/next/assets/{Dialog-nK15L1M0.js.map → Dialog-CIjlQZqc.js.map} +1 -1
  89. package/dist/core/web/public/next/assets/{DiffViewer-D369eZe8.js → DiffViewer-BjQZAm5L.js} +2 -2
  90. package/dist/core/web/public/next/assets/{DiffViewer-D369eZe8.js.map → DiffViewer-BjQZAm5L.js.map} +1 -1
  91. package/dist/core/web/public/next/assets/{McpSection-DVgiSYG1.js → McpSection-D2FySMy3.js} +2 -2
  92. package/dist/core/web/public/next/assets/{McpSection-DVgiSYG1.js.map → McpSection-D2FySMy3.js.map} +1 -1
  93. package/dist/core/web/public/next/assets/{MutationTokenDialog-B9I4PAcp.js → MutationTokenDialog-QBPrD1UE.js} +2 -2
  94. package/dist/core/web/public/next/assets/{MutationTokenDialog-B9I4PAcp.js.map → MutationTokenDialog-QBPrD1UE.js.map} +1 -1
  95. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-z6tJj3s9.js → ResourcePoolConsoleApp-Dzq9Ufq_.js} +4 -4
  96. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-z6tJj3s9.js.map → ResourcePoolConsoleApp-Dzq9Ufq_.js.map} +1 -1
  97. package/dist/core/web/public/next/assets/{RouteErrorBoundary-OSajL45F.js → RouteErrorBoundary-Chd-wt6w.js} +2 -2
  98. package/dist/core/web/public/next/assets/{RouteErrorBoundary-OSajL45F.js.map → RouteErrorBoundary-Chd-wt6w.js.map} +1 -1
  99. package/dist/core/web/public/next/assets/{Meter-CLcr-4wS.css → Segmented-8cf0Fypn.css} +1 -1
  100. package/dist/core/web/public/next/assets/{Meter-Cmo1pLbk.js → Segmented-DhGh7Y1s.js} +2 -2
  101. package/dist/core/web/public/next/assets/Segmented-DhGh7Y1s.js.map +1 -0
  102. package/dist/core/web/public/next/assets/SettingsSection-Bqimu_Mg.js +2 -0
  103. package/dist/core/web/public/next/assets/{SettingsSection-B3YH2Npi.js.map → SettingsSection-Bqimu_Mg.js.map} +1 -1
  104. package/dist/core/web/public/next/assets/UniverseConsoleApp-DzAyrE-A.js +2 -0
  105. package/dist/core/web/public/next/assets/{UniverseConsoleApp-wEKIzl1M.js.map → UniverseConsoleApp-DzAyrE-A.js.map} +1 -1
  106. package/dist/core/web/public/next/assets/{UniverseView-CQVpJqqI.js → UniverseView-BGl3DRV7.js} +2 -2
  107. package/dist/core/web/public/next/assets/{UniverseView-CQVpJqqI.js.map → UniverseView-BGl3DRV7.js.map} +1 -1
  108. package/dist/core/web/public/next/assets/UsageSection-PaaJ1L90.js +2 -0
  109. package/dist/core/web/public/next/assets/UsageSection-PaaJ1L90.js.map +1 -0
  110. package/dist/core/web/public/next/assets/VerseConsoleApp-DYH7ottg.js +3 -0
  111. package/dist/core/web/public/next/assets/VerseConsoleApp-DYH7ottg.js.map +1 -0
  112. package/dist/core/web/public/next/assets/VerseConsoleApp-DsdKQoTH.css +1 -0
  113. package/dist/core/web/public/next/assets/{charts-BopViIha.js → charts-CnMDT08S.js} +2 -2
  114. package/dist/core/web/public/next/assets/{charts-BopViIha.js.map → charts-CnMDT08S.js.map} +1 -1
  115. package/dist/core/web/public/next/assets/context-model-DTvb2uZy.js +2 -0
  116. package/dist/core/web/public/next/assets/context-model-DTvb2uZy.js.map +1 -0
  117. package/dist/core/web/public/next/assets/global-F2K14nNo.js +2 -0
  118. package/dist/core/web/public/next/assets/global-F2K14nNo.js.map +1 -0
  119. package/dist/core/web/public/next/assets/{icons-L06pQH6j.js → icons-B2fa40k6.js} +2 -2
  120. package/dist/core/web/public/next/assets/{icons-L06pQH6j.js.map → icons-B2fa40k6.js.map} +1 -1
  121. package/dist/core/web/public/next/assets/{index-CXgVQHek.js → index-DO9a5IaB.js} +3 -3
  122. package/dist/core/web/public/next/assets/{index-CXgVQHek.js.map → index-DO9a5IaB.js.map} +1 -1
  123. package/dist/core/web/public/next/assets/{queries-D3sSlan1.js → queries-BdSsWYSu.js} +2 -2
  124. package/dist/core/web/public/next/assets/{queries-D3sSlan1.js.map → queries-BdSsWYSu.js.map} +1 -1
  125. package/dist/core/web/public/next/assets/use-guarded-action-BrlIU0jJ.js +2 -0
  126. package/dist/core/web/public/next/assets/{use-guarded-action-BxfJ9E-n.js.map → use-guarded-action-BrlIU0jJ.js.map} +1 -1
  127. package/dist/core/web/public/next/assets/verse-model-GWqHIjMG.js +2 -0
  128. package/dist/core/web/public/next/assets/verse-model-GWqHIjMG.js.map +1 -0
  129. package/dist/core/web/public/next/assets/{workspace-model-v0a1gl-C.js → workspace-model-4yzB--1h.js} +2 -2
  130. package/dist/core/web/public/next/assets/{workspace-model-v0a1gl-C.js.map → workspace-model-4yzB--1h.js.map} +1 -1
  131. package/dist/core/web/public/next/index.html +1 -1
  132. package/dist/release-dependency-inventory.json +1 -1
  133. package/docs/README.md +1 -0
  134. package/package.json +1 -1
  135. package/dist/core/web/public/next/assets/AutonomySection-yeNNUPur.js +0 -2
  136. package/dist/core/web/public/next/assets/ChatSection-BfnLe5dT.css +0 -1
  137. package/dist/core/web/public/next/assets/ChatSection-DwDDnCZQ.js +0 -80
  138. package/dist/core/web/public/next/assets/ChatSection-DwDDnCZQ.js.map +0 -1
  139. package/dist/core/web/public/next/assets/ConfirmDialog-C363ty_c.js +0 -2
  140. package/dist/core/web/public/next/assets/ConfirmDialog-C363ty_c.js.map +0 -1
  141. package/dist/core/web/public/next/assets/Meter-Cmo1pLbk.js.map +0 -1
  142. package/dist/core/web/public/next/assets/SettingsSection-B3YH2Npi.js +0 -2
  143. package/dist/core/web/public/next/assets/UniverseConsoleApp-wEKIzl1M.js +0 -2
  144. package/dist/core/web/public/next/assets/UsageSection-dToBThXe.js +0 -2
  145. package/dist/core/web/public/next/assets/UsageSection-dToBThXe.js.map +0 -1
  146. package/dist/core/web/public/next/assets/VerseConsoleApp-CMkCizvL.css +0 -1
  147. package/dist/core/web/public/next/assets/VerseConsoleApp-DHU32JCQ.js +0 -3
  148. package/dist/core/web/public/next/assets/VerseConsoleApp-DHU32JCQ.js.map +0 -1
  149. package/dist/core/web/public/next/assets/global-DMWakEhP.js +0 -2
  150. package/dist/core/web/public/next/assets/global-DMWakEhP.js.map +0 -1
  151. package/dist/core/web/public/next/assets/use-guarded-action-BxfJ9E-n.js +0 -2
  152. package/dist/core/web/public/next/assets/verse-model-CvtK6vP6.js +0 -2
  153. package/dist/core/web/public/next/assets/verse-model-CvtK6vP6.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-model-DTvb2uZy.js","names":[],"sources":["../../../../../../src/web-ui/routes/verse/verse-store.ts","../../../../../../src/web-ui/routes/verse/usage/context-model.ts"],"sourcesContent":["/**\n * routes/verse/verse-store.ts — per-session transcript state for the Verse\n * console. Framework-free (useSyncExternalStore glue lives in\n * useVerseSession.ts), same split as data/cache.ts + data/hooks.ts.\n *\n * One entry per session id holds the ordered, seq-deduplicated event log,\n * the latest session record we know of, and the stream connection state.\n * Snapshots are replaced wholesale on every change (Object.is semantics —\n * see DESIGN.md §3 for the bug that rule prevents).\n *\n * `buildTranscript()` derives what the transcript renders from the raw\n * events: streamed `text-delta`s accumulate into a provisional assistant\n * bubble until the matching `assistant-message` arrives and replaces it;\n * `tool-use` and `tool-result` pair up by toolUseId into one card.\n */\nimport type { VerseEvent, VerseSession, VerseUsage } from '../../data/api-types.js';\nimport type { VerseWindowSource } from '../../../core/verse/types.js';\nimport { reconcileAutoCompactAt } from '../../../core/verse/context-math.js';\n\nexport type VerseStreamState = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';\n\nexport interface VerseSessionState {\n sessionId: string;\n session: VerseSession | null;\n events: VerseEvent[];\n lastSeq: number;\n loaded: boolean;\n loadError: string | null;\n stream: VerseStreamState;\n}\n\nconst EMPTY: Omit<VerseSessionState, 'sessionId'> = {\n session: null,\n events: [],\n lastSeq: 0,\n loaded: false,\n loadError: null,\n stream: 'idle',\n};\n\nconst sessions = new Map<string, VerseSessionState>();\nconst listeners = new Set<() => void>();\n\nfunction emit(): void {\n for (const l of listeners) l();\n}\n\nexport function subscribeVerseStore(listener: () => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n}\n\nfunction entryFor(sessionId: string): VerseSessionState {\n let e = sessions.get(sessionId);\n if (!e) {\n e = { sessionId, ...EMPTY };\n sessions.set(sessionId, e);\n }\n return e;\n}\n\nfunction patch(sessionId: string, delta: Partial<VerseSessionState>): void {\n const current = entryFor(sessionId);\n sessions.set(sessionId, { ...current, ...delta });\n emit();\n}\n\nconst NONE: VerseSessionState = { sessionId: '', ...EMPTY };\n\n/** Stable reference until the entry changes, so useSyncExternalStore is happy. */\nexport function getVerseSessionState(sessionId: string | null): VerseSessionState {\n if (!sessionId) return NONE;\n return entryFor(sessionId);\n}\n\n/** Merge a fetched detail (session + full event log) into the entry. Events\n * already known (same seq) are kept once; the union stays sorted by seq. The\n * detail fetch starts before the EventSource opens, so a `turn-done` that\n * streamed in meanwhile wins over the older `running` snapshot. */\nexport function seedVerseSession(sessionId: string, session: VerseSession, events: VerseEvent[]): void {\n const current = entryFor(sessionId);\n const merged = mergeEvents(current.events, events);\n patch(sessionId, {\n session: settledStatus(session, merged),\n events: merged,\n lastSeq: merged.length ? merged[merged.length - 1]!.seq : 0,\n loaded: true,\n loadError: null,\n });\n}\n\nexport function setVerseLoadError(sessionId: string, message: string): void {\n patch(sessionId, { loadError: message, loaded: true });\n}\n\nexport function setVerseStreamState(sessionId: string, stream: VerseStreamState): void {\n if (entryFor(sessionId).stream === stream) return;\n patch(sessionId, { stream });\n}\n\n/**\n * Replace the session record (after a turn POST / rename / list refresh).\n * `turnId` is the turn the snapshot claims is running (POST …/turns returns\n * it): when the log already holds that turn's `turn-done`/`cancelled` — a\n * spawn failure settles in the same tick as the 202 — the snapshot is stale\n * and must not drag the composer back to running.\n */\nexport function setVerseSession(sessionId: string, session: VerseSession, turnId?: string): void {\n const current = entryFor(sessionId);\n patch(sessionId, { session: settledStatus(session, current.events, turnId) });\n}\n\n/**\n * Reconcile a `running` snapshot against the event log. Without a `turnId`\n * the running turn is the last `user-message`/`turn-started` in the log; a\n * later `turn-done`/`cancelled` means the server already settled it. With a\n * `turnId`, only that turn's terminal event counts (its `user-message` may\n * not have streamed in yet, so a previous turn's `turn-done` proves nothing).\n * Status follows the same rules applyVerseEvent uses: cancelled → idle,\n * `turn-done ok:false` → error only when an `error` event was logged for it.\n */\nexport function settledStatus(session: VerseSession, events: readonly VerseEvent[], turnId?: string): VerseSession {\n if (session.status !== 'running') return session;\n let anchorSeq = -1;\n let anchorTurn: string | null = turnId ?? null;\n if (!turnId) {\n for (const e of events) {\n if (e.type === 'user-message' || e.type === 'turn-started') {\n anchorSeq = e.seq;\n anchorTurn = e.turnId;\n }\n }\n if (anchorTurn === null) return session;\n }\n let terminal: Extract<VerseEvent, { type: 'turn-done' | 'cancelled' }> | null = null;\n let lastError: string | null = null;\n for (const e of events) {\n if (e.seq <= anchorSeq) continue;\n if (e.type === 'error' && e.turnId === anchorTurn) lastError = e.message;\n if ((e.type === 'turn-done' || e.type === 'cancelled') && e.turnId === anchorTurn) terminal = e;\n }\n if (!terminal) return session;\n const failed = terminal.type === 'turn-done' && !terminal.ok && lastError !== null;\n return {\n ...session,\n status: failed ? 'error' : 'idle',\n lastError: failed ? lastError : null,\n updatedAt: terminal.at > session.updatedAt ? terminal.at : session.updatedAt,\n };\n}\n\n/** Optimistic status flip (e.g. right after POST …/turns returns 202). */\nexport function setVerseSessionStatus(sessionId: string, status: VerseSession['status']): void {\n const current = entryFor(sessionId);\n if (!current.session || current.session.status === status) return;\n patch(sessionId, { session: { ...current.session, status } });\n}\n\n/**\n * Apply one live event. Duplicates (a fresh SSE connection replays from the\n * start) are dropped by seq; out-of-order arrivals are inserted in place.\n * Session-level side effects: `usage` and `context` update the live context\n * meter, `compaction` counts a native compaction, `turn-started` flips\n * status to running, `turn-done`/`cancelled`/`error` settle it, and a codex\n * `turn-done.nativeSessionId` is captured.\n */\nexport function applyVerseEvent(sessionId: string, event: VerseEvent): boolean {\n const current = entryFor(sessionId);\n if (!Number.isFinite(event.seq)) return false;\n if (current.events.some((e) => e.seq === event.seq)) return false;\n const events = mergeEvents(current.events, [event]);\n let session = current.session;\n if (session) {\n switch (event.type) {\n case 'turn-started':\n session = { ...session, status: 'running', lastError: null, updatedAt: event.at };\n break;\n case 'usage':\n session = { ...session, usage: applyUsageFrame(session, event.usage), updatedAt: event.at };\n break;\n case 'context':\n session = { ...session, usage: applyContextReading(session, event), updatedAt: event.at };\n break;\n case 'compaction':\n // Counted exactly as the server counts it (session-engine), so a reload\n // shows the same number the live stream did. Occupancy is NOT touched\n // here: the next `usage`/`context` reading is the measurement.\n session = { ...session, compactionCount: (session.compactionCount ?? 0) + 1, updatedAt: event.at };\n break;\n case 'turn-done':\n session = {\n ...session,\n status: event.ok ? 'idle' : session.lastError ? 'error' : 'idle',\n turnCount: session.turnCount + 1,\n nativeSessionId: event.nativeSessionId ?? session.nativeSessionId,\n updatedAt: event.at,\n };\n break;\n case 'error':\n session = { ...session, status: 'error', lastError: event.message, updatedAt: event.at };\n break;\n case 'cancelled':\n session = { ...session, status: 'idle', updatedAt: event.at };\n break;\n default:\n break;\n }\n }\n patch(sessionId, { events, lastSeq: Math.max(current.lastSeq, event.seq), session });\n return true;\n}\n\n/**\n * Fold one `usage` frame into the session record. The frame carries THIS\n * turn's counts; the record keeps the session totals (server: session-engine\n * applyUsage). Occupancy and the window are live values, not sums.\n *\n * V3.9 fields are adopted only when the frame carries them (spread-when-\n * present), so a pre-3.9 server's frames leave the record's shape unchanged.\n * The one exception is a local seat: Verse SETS that window\n * (`CLAUDE_CODE_MAX_CONTEXT_TOKENS`), so a window a frame reports without\n * provenance is not allowed to replace it — the server ignores runtime\n * windows on local for the same reason.\n */\nexport function applyUsageFrame(session: VerseSession, frame: VerseUsage): VerseUsage {\n const prev = session.usage;\n const next: VerseUsage = {\n ...prev,\n inputTokens: prev.inputTokens + frame.inputTokens,\n outputTokens: prev.outputTokens + frame.outputTokens,\n cacheReadTokens: prev.cacheReadTokens + frame.cacheReadTokens,\n cacheCreationTokens: prev.cacheCreationTokens + frame.cacheCreationTokens,\n contextTokens: frame.contextTokens,\n };\n const frameWindow = typeof frame.contextWindow === 'number' && frame.contextWindow > 0 ? frame.contextWindow : null;\n const trusted = frame.contextWindowSource !== undefined || session.engine !== 'local';\n if (frameWindow !== null && trusted) next.contextWindow = frameWindow;\n if (frame.contextWindowSource !== undefined) next.contextWindowSource = frame.contextWindowSource;\n if (frame.autoCompactAt !== undefined) next.autoCompactAt = frame.autoCompactAt;\n // The server marks an EXACT reading by OMITTING the key (it only writes\n // `false`), so a resolved frame without it must clear an earlier \"≤\" — a\n // frame from a pre-3.9 server (no V3.9 fields at all) leaves it alone.\n if (frame.contextTokensExact === false) next.contextTokensExact = false;\n else if (frame.contextWindowSource !== undefined) delete next.contextTokensExact;\n return next;\n}\n\n/** The provenance values a frame may carry; anything else is ignored, never stored. */\nconst WINDOW_SOURCES: ReadonlySet<string> = new Set<VerseWindowSource>(['runtime', 'provider-catalog', 'cli-catalog', 'documented', 'fallback']);\n\nfunction frameWindowSource(value: unknown): VerseWindowSource | null {\n return typeof value === 'string' && WINDOW_SOURCES.has(value) ? (value as VerseWindowSource) : null;\n}\n\n/**\n * Apply a `context` reading: it REPLACES occupancy (never summed). When it\n * names a window, the window's provenance is the one the EVENT states\n * (`contextWindowSource`, V3.9): the engine also emits a `context` event\n * after a MODE SWITCH, carrying the new mode's CATALOG budget, and stamping\n * that 'runtime' made the tooltip say \"reported by the CLI on the last turn\"\n * for a number no CLI reported — and pinned it ahead of later catalog\n * corrections (sessionContextBudget precedence 1). Only an event from a\n * server that predates the field (no source at all) is taken as a runtime\n * reading, which is what every such event was.\n *\n * The engine fills `autoCompactAt` for the window in force; a frame without\n * it (an older server) is reconciled from the previous budget rather than\n * left pointing at a compaction point that belonged to a different window.\n */\nexport function applyContextReading(\n session: VerseSession,\n event: Extract<VerseEvent, { type: 'context' }>,\n): VerseUsage {\n const prev = session.usage;\n const next: VerseUsage = {\n ...prev,\n contextTokens: Math.max(0, Math.floor(event.contextTokens)),\n contextTokensExact: event.exact,\n };\n const window = typeof event.contextWindow === 'number' && event.contextWindow > 0 ? event.contextWindow : null;\n const stated = frameWindowSource(event.contextWindowSource);\n // Local windows are SET by Verse (CLAUDE_CODE_MAX_CONTEXT_TOKENS), not\n // measured: a window without provenance is not allowed to move one (see\n // applyUsageFrame). A V3.9 event that states its source is the engine's\n // own record of the window it just launched with, so it is adopted.\n if (window === null || (session.engine === 'local' && stated === null)) {\n if (event.autoCompactAt !== undefined && event.autoCompactAt !== null) next.autoCompactAt = event.autoCompactAt;\n return next;\n }\n next.contextWindow = window;\n next.contextWindowSource = stated ?? 'runtime';\n if (event.autoCompactAt !== undefined) {\n next.autoCompactAt = event.autoCompactAt;\n } else if (window !== prev.contextWindow) {\n next.autoCompactAt = reconcileAutoCompactAt({\n engine: session.engine,\n runtimeWindow: window,\n budget: prev.contextWindow ? { contextWindow: prev.contextWindow, autoCompactAt: prev.autoCompactAt ?? null } : null,\n });\n }\n return next;\n}\n\n/**\n * When this chat last talked to its provider — the newest `usage`,\n * `turn-done`, `cancelled` or TURN-attributed `context` event — or null when\n * the log holds none (no turn yet, or the log is not loaded).\n *\n * WHY NOT `session.updatedAt`: the idle-cache advice (\"the prompt cache has\n * likely expired\") asks how long the PROVIDER has gone without a request,\n * and `updatedAt` moves on things that never reach a provider: a rename, a\n * context-mode switch (whose `context` event has `turnId: null`), a reload's\n * record save. Measured from `updatedAt`, one rename silenced the warning\n * for another hour while the cache stayed cold.\n */\nexport function lastTurnActivityAt(events: readonly VerseEvent[]): string | null {\n let latest: string | null = null;\n for (const e of events) {\n const turnActivity = e.type === 'usage' || e.type === 'turn-done' || e.type === 'cancelled' ||\n (e.type === 'context' && e.turnId !== null);\n if (turnActivity && typeof e.at === 'string' && (latest === null || e.at > latest)) latest = e.at;\n }\n return latest;\n}\n\nexport function forgetVerseSession(sessionId: string): void {\n if (sessions.delete(sessionId)) emit();\n}\n\n/** Test/logout hygiene. */\nexport function resetVerseStore(): void {\n sessions.clear();\n emit();\n}\n\nfunction mergeEvents(existing: VerseEvent[], incoming: VerseEvent[]): VerseEvent[] {\n if (incoming.length === 0) return existing;\n const bySeq = new Map<number, VerseEvent>();\n for (const e of existing) bySeq.set(e.seq, e);\n for (const e of incoming) if (!bySeq.has(e.seq)) bySeq.set(e.seq, e);\n return [...bySeq.values()].sort((a, b) => a.seq - b.seq);\n}\n\n// ---------------------------------------------------------------------------\n// Transcript derivation\n// ---------------------------------------------------------------------------\n\nexport type TranscriptItem =\n | { kind: 'user'; key: string; turnId: string; at: string; text: string }\n | { kind: 'assistant'; key: string; turnId: string; at: string; text: string; streaming: boolean }\n | { kind: 'thinking'; key: string; turnId: string; at: string; text: string }\n | {\n kind: 'tool';\n key: string;\n turnId: string;\n at: string;\n toolUseId: string;\n name: string;\n input: unknown;\n result: { output: string; isError: boolean } | null;\n /** tool-use → tool-result wall time; null while pending or unstamped. */\n durationMs: number | null;\n }\n | { kind: 'error'; key: string; turnId: string | null; at: string; message: string }\n | {\n /** V3.9: the CLI compacted its own context. Counts are null when the CLI does not report them (codex). */\n kind: 'compaction';\n key: string;\n turnId: string | null;\n at: string;\n trigger: 'auto' | 'manual';\n preTokens: number | null;\n postTokens: number | null;\n durationMs: number | null;\n }\n | { kind: 'cancelled'; key: string; turnId: string; at: string }\n | { kind: 'turn-done'; key: string; turnId: string; at: string; ok: boolean; durationMs: number };\n\nexport interface Transcript {\n items: TranscriptItem[];\n /** True while a turn is open (started and not yet done/cancelled). */\n live: boolean;\n /** Most recent usage frame seen in the log, if any. */\n usage: VerseUsage | null;\n}\n\n/** Wall time between two ISO stamps; null when either is unparseable. */\nfunction spanMs(from: string, to: string): number | null {\n const a = Date.parse(from);\n const b = Date.parse(to);\n return Number.isFinite(a) && Number.isFinite(b) ? Math.max(0, b - a) : null;\n}\n\nexport function buildTranscript(events: VerseEvent[]): Transcript {\n const items: TranscriptItem[] = [];\n const toolIndex = new Map<string, number>();\n let pending: { turnId: string; at: string; text: string; key: string } | null = null;\n let live = false;\n let usage: VerseUsage | null = null;\n\n const flushPending = (streaming: boolean) => {\n if (!pending) return;\n items.push({ kind: 'assistant', key: pending.key, turnId: pending.turnId, at: pending.at, text: pending.text, streaming });\n pending = null;\n };\n\n for (const e of events) {\n switch (e.type) {\n case 'user-message':\n flushPending(false);\n items.push({ kind: 'user', key: `u-${e.seq}`, turnId: e.turnId, at: e.at, text: e.text });\n break;\n case 'turn-started':\n live = true;\n break;\n case 'text-delta':\n if (pending && pending.turnId !== e.turnId) flushPending(false);\n if (!pending) pending = { turnId: e.turnId, at: e.at, text: '', key: `d-${e.seq}` };\n pending.text += e.text;\n break;\n case 'assistant-message':\n // The complete message supersedes whatever deltas streamed before it.\n pending = null;\n items.push({ kind: 'assistant', key: `a-${e.seq}`, turnId: e.turnId, at: e.at, text: e.text, streaming: false });\n break;\n case 'thinking':\n flushPending(false);\n items.push({ kind: 'thinking', key: `t-${e.seq}`, turnId: e.turnId, at: e.at, text: e.text });\n break;\n case 'tool-use':\n flushPending(false);\n toolIndex.set(e.toolUseId, items.length);\n items.push({\n kind: 'tool',\n key: `tu-${e.seq}`,\n turnId: e.turnId,\n at: e.at,\n toolUseId: e.toolUseId,\n name: e.name,\n input: e.input,\n result: null,\n durationMs: null,\n });\n break;\n case 'tool-result': {\n const idx = toolIndex.get(e.toolUseId);\n const existing = idx === undefined ? undefined : items[idx];\n if (existing && existing.kind === 'tool') {\n items[idx!] = { ...existing, result: { output: e.output, isError: e.isError }, durationMs: spanMs(existing.at, e.at) };\n } else {\n items.push({\n kind: 'tool',\n key: `tr-${e.seq}`,\n turnId: e.turnId,\n at: e.at,\n toolUseId: e.toolUseId,\n name: 'tool',\n input: null,\n result: { output: e.output, isError: e.isError },\n durationMs: null,\n });\n }\n break;\n }\n case 'usage':\n usage = e.usage;\n break;\n case 'compaction':\n // Deliberately NO flushPending: CLIs compact between model calls, and\n // flushing a streamed bubble here would leave it in the list when the\n // complete `assistant-message` arrives — the same reply twice.\n items.push({\n kind: 'compaction',\n key: `k-${e.seq}`,\n turnId: e.turnId,\n at: e.at,\n trigger: e.trigger,\n preTokens: e.preTokens,\n postTokens: e.postTokens,\n durationMs: e.durationMs,\n });\n break;\n case 'error':\n flushPending(false);\n items.push({ kind: 'error', key: `e-${e.seq}`, turnId: e.turnId, at: e.at, message: e.message });\n break;\n case 'cancelled':\n flushPending(false);\n live = false;\n items.push({ kind: 'cancelled', key: `c-${e.seq}`, turnId: e.turnId, at: e.at });\n break;\n case 'turn-done':\n flushPending(false);\n live = false;\n items.push({ kind: 'turn-done', key: `td-${e.seq}`, turnId: e.turnId, at: e.at, ok: e.ok, durationMs: e.durationMs });\n break;\n default:\n break;\n }\n }\n // Deltas still arriving for an open turn render as a streaming bubble.\n flushPending(live);\n return { items, live, usage };\n}\n\n// ---------------------------------------------------------------------------\n// Render-time grouping\n// ---------------------------------------------------------------------------\n\nexport type ToolGroupMember = Extract<TranscriptItem, { kind: 'tool' | 'thinking' }>;\n\nexport interface ToolGroupItem {\n kind: 'toolGroup';\n key: string;\n turnId: string;\n at: string;\n items: ToolGroupMember[];\n /** Tool calls only (thinking blocks are not counted). */\n toolCount: number;\n /** \"Read ×6, Edit ×4\" — names in first-seen order. */\n summary: string;\n errorCount: number;\n /** A member is still waiting for its result. */\n pending: boolean;\n /** Wall time between the first and last member, when both have parseable timestamps. */\n spanMs: number | null;\n}\n\nexport type TranscriptRenderItem = TranscriptItem | ToolGroupItem;\n\n/**\n * Fold runs of two or more consecutive `tool`/`thinking` items from the same\n * turn into one `toolGroup`, so an agentic turn reads as \"N tool calls\"\n * with a single disclosure instead of a wall of cards. Single tool calls\n * stay as they are. Pure over buildTranscript()'s output (the index-based\n * tool-result pairing there is untouched).\n */\nexport function groupTranscriptItems(items: readonly TranscriptItem[]): TranscriptRenderItem[] {\n const out: TranscriptRenderItem[] = [];\n let run: ToolGroupMember[] = [];\n\n const flush = () => {\n if (run.length === 0) return;\n if (run.length === 1) out.push(run[0]!);\n else out.push(makeToolGroup(run));\n run = [];\n };\n\n for (const item of items) {\n if ((item.kind === 'tool' || item.kind === 'thinking') && (run.length === 0 || run[0]!.turnId === item.turnId)) {\n run.push(item);\n continue;\n }\n flush();\n if (item.kind === 'tool' || item.kind === 'thinking') run.push(item);\n else out.push(item);\n }\n flush();\n return out;\n}\n\nfunction makeToolGroup(members: ToolGroupMember[]): ToolGroupItem {\n const counts = new Map<string, number>();\n let errorCount = 0;\n let pending = false;\n for (const m of members) {\n if (m.kind !== 'tool') continue;\n counts.set(m.name, (counts.get(m.name) ?? 0) + 1);\n if (m.result === null) pending = true;\n else if (m.result.isError) errorCount += 1;\n }\n const toolCount = [...counts.values()].reduce((a, b) => a + b, 0);\n const summary = [...counts.entries()].map(([name, n]) => (n > 1 ? `${name} ×${n}` : name)).join(', ');\n const span = spanMs(members[0]!.at, members[members.length - 1]!.at);\n return {\n kind: 'toolGroup',\n key: `g-${members[0]!.key}`,\n turnId: members[0]!.turnId,\n at: members[0]!.at,\n items: members,\n toolCount,\n summary,\n errorCount,\n pending,\n spanMs: span,\n };\n}\n\n/**\n * \"123k\" / \"1.2M\" — compact token counts for the meter and usage rows.\n *\n * The unit is chosen AFTER rounding: choosing it first printed 999,500–\n * 999,999 as \"1000k\" (and 999.5–999.9 as \"1000\"), a band a 1M Claude chat in\n * Expansive can actually reach.\n */\nexport function formatTokens(n: number | null | undefined): string {\n if (n === null || n === undefined || !Number.isFinite(n)) return '—';\n if (Math.round(n) < 1000) return String(Math.round(n));\n if (Math.round(n / 1000) < 1000) return `${Math.round(n / 1000)}k`;\n return `${(n / 1_000_000).toFixed(1).replace(/\\.0$/, '')}M`;\n}\n","/**\n * routes/verse/usage/context-model.ts — the ONE projection of context facts\n * for every surface that shows a seat: the seat picker's rows, the new-chat\n * dialog's context block, the resources panel's \"This chat\" efficiency block,\n * and the Usage section's per-seat efficiency table.\n *\n * WHY ONE MODULE. Before V3.9 every surface read `seat.contextWindow` (the\n * DEFAULT model's window) or the session's creation-time window, and three of\n * them rendered \"200k\" for a 1M Claude model. The arithmetic now lives in\n * core/verse/context-math.ts, shared with the server; this module is only the\n * wording and the aggregation, so the picker, the dialog and the panel cannot\n * describe one model's budget three different ways.\n *\n * HONESTY RULES this module enforces (docs/VERSE-CONTEXT.md):\n *\n * 1. UNKNOWN IS NOT A NUMBER. A model whose budget is unknown reads \"window\n * unknown\", never a default dressed as a fact. A Verse fallback window is\n * marked \"est.\", and its source says so in words.\n * 2. AN UPPER BOUND IS NOT A MEASUREMENT. Codex's turn total sums every model\n * call; until its rollout is read, occupancy is \"≤ N\", and any statistic\n * built on such a reading carries the same flag.\n * 3. NO CACHE REPORTED IS NOT A 0% HIT RATE. A provider that reported no\n * cache activity at all (Ollama; codex on its very first call) gets \"none\n * reported\", not a 0% that reads \"caching is broken\".\n * 4. NOTHING HERE SPENDS. Every figure is derived from what the server\n * already returned (seats, sessions, the open chat's event log).\n *\n * Pure: no React, no I/O.\n */\nimport {\n CACHE_IDLE_TTL_MS,\n budgetFor,\n cacheHitRatio,\n fitVerdict,\n hasExpansiveMode,\n sessionOverheadTokens,\n} from '../../../../core/verse/context-math.js';\nimport type {\n VerseContextBudget,\n VerseContextFit,\n VerseContextMode,\n VerseEngine,\n VerseEvent,\n VerseFitVerdict,\n VerseModelOption,\n VerseSeat,\n VerseSession,\n VerseUsage,\n VerseWindowSource,\n} from '../../../../core/verse/types.js';\nimport { CODEX_EXPANSIVE_METERING_NOTE, WINDOW_SOURCE_TEXT, modelOptionFor, seatUnavailableReason, sessionContextBudget } from '../verse-model.js';\nimport { formatTokens } from '../verse-store.js';\n\n// ---------------------------------------------------------------------------\n// Models and modes\n// ---------------------------------------------------------------------------\n\n/**\n * A seat's option for a model id, matching through the alias table (a chat\n * remembered on `claude-opus-5.5` is the catalog's `claude-opus-5-5`). The\n * lookup itself is verse-model's `modelOptionFor`, the one the context meter\n * uses, so the picker and the meter can never resolve one id two ways.\n */\nexport function seatModelOption(seat: VerseSeat | null | undefined, modelId: string): VerseModelOption | null {\n return seat ? modelOptionFor([seat], { seatId: seat.id, model: modelId }) : null;\n}\n\n/** `mode` when this model has a budget for it, else `standard` — never a mode the CLI cannot be told. */\nexport function resolveContextMode(option: VerseModelOption | null | undefined, mode: VerseContextMode): VerseContextMode {\n return mode !== 'standard' && budgetFor(option, mode) !== null ? mode : 'standard';\n}\n\n/** The budget a model runs with in a mode (resolved), or null when even its standard window is unknown. */\nexport function effectiveBudget(option: VerseModelOption | null | undefined, mode: VerseContextMode): VerseContextBudget | null {\n return budgetFor(option, resolveContextMode(option, mode));\n}\n\n/** Why this model cannot be chosen on this seat, or null when it can. The seat's own outage wins. */\nexport function modelUnavailableReason(seat: VerseSeat, option: VerseModelOption): string | null {\n const seatReason = seatUnavailableReason(seat);\n if (seatReason !== null) return seatReason;\n const reason = option.unavailableReason;\n return typeof reason === 'string' && reason.trim().length > 0 ? reason.trim() : null;\n}\n\nexport const CONTEXT_MODE_LABEL: Record<VerseContextMode, string> = {\n standard: 'Standard',\n expansive: 'Expansive',\n};\n\n/** How each seat's CLI is named when its pinned version is shown. */\nexport const ENGINE_CLI_NAME: Record<VerseEngine, string> = {\n claude: 'Claude Code',\n codex: 'Codex CLI',\n grok: 'Grok CLI',\n local: 'Claude Code',\n};\n\n/** \"Claude Code 2.1.257\" — the binary this seat is pinned to; null when unknown (local seats, old servers). */\nexport function seatCliLine(seat: VerseSeat): string | null {\n const version = typeof seat.cliVersion === 'string' ? seat.cliVersion.trim() : '';\n return version.length > 0 ? `${ENGINE_CLI_NAME[seat.engine]} ${version}` : null;\n}\n\n/** The seat's own plain-language context facts (binary skew, catalog not yet fetched, …). */\nexport function seatContextNotes(seat: VerseSeat): string[] {\n return (seat.notes ?? []).filter((n): n is string => typeof n === 'string' && n.trim().length > 0);\n}\n\n// ---------------------------------------------------------------------------\n// Window provenance\n// ---------------------------------------------------------------------------\n\n// Window provenance wording is verse-model's `WINDOW_SOURCE_TEXT` — the\n// meter's tooltip, the picker and the resources panel all read that one table.\n\nexport function isEstimatedWindow(source: VerseWindowSource | null | undefined): boolean {\n return source === 'fallback';\n}\n\n// ---------------------------------------------------------------------------\n// Picker and dialog wording\n// ---------------------------------------------------------------------------\n\n/**\n * The compact per-model phrase a picker row carries: \"1M ctx · compacts ≈367k\".\n * Uses the budget for `mode` when the model has one, and names the mode only\n * when it is not the default, so an ordinary row stays short.\n */\nexport function modelContextPhrase(option: VerseModelOption, mode: VerseContextMode = 'standard'): string {\n const resolved = resolveContextMode(option, mode);\n const budget = budgetFor(option, resolved);\n if (!budget) return 'window unknown';\n const window = `${formatTokens(budget.contextWindow)} ctx${isEstimatedWindow(option.windowSource) ? ' (est.)' : ''}`;\n const compacts = budget.autoCompactAt === null ? '' : ` · compacts ≈${formatTokens(budget.autoCompactAt)}`;\n return `${window}${compacts}${resolved === 'expansive' ? ' (expansive)' : ''}`;\n}\n\n/**\n * The full sentence for a tooltip or the dialog: the window, where each mode\n * compacts, and how the window is known.\n */\nexport function modelContextSentence(option: VerseModelOption): string {\n const standard = budgetFor(option, 'standard');\n const expansive = hasExpansiveMode(option) ? budgetFor(option, 'expansive') : null;\n const parts: string[] = [];\n if (!standard) {\n parts.push('Context window unknown for this model.');\n } else {\n const where = standard.autoCompactAt === null\n ? ''\n : `; compacts at about ${formatTokens(standard.autoCompactAt)}${expansive ? ' in Standard' : ''}`;\n parts.push(`${formatTokens(standard.contextWindow)}-token window${where}.`);\n }\n if (expansive) {\n parts.push(`Expansive runs to about ${formatTokens(expansive.autoCompactAt ?? expansive.contextWindow)} before compacting.`);\n }\n if (option.windowSource) parts.push(`Window ${WINDOW_SOURCE_TEXT[option.windowSource]}.`);\n return parts.join(' ');\n}\n\n/**\n * The metering caveat an Expansive cost sentence must carry for this engine,\n * or null when there is none. The wording is verse-model's\n * CODEX_EXPANSIVE_METERING_NOTE — ONE sentence for the new-chat dialog, the\n * handoff dialog, the in-chat mode menu and the \"Expansive could help\" chip.\n * The ratio those surfaces quote (≈3.2× for a 785k vs 245k codex turn) counts\n * re-sent TOKENS only; GPT-5.6-class requests above 272k reportedly also\n * count about 2× against plan limits (docs/VERSE-CONTEXT.md §2.3, a single\n * secondary source), so the note is worded as reported, never as a measured\n * multiplier. Standard never crosses 272k; Expansive does by design.\n */\nexport function expansiveMeteringNote(engine: VerseEngine | null | undefined): string | null {\n return engine === 'codex' ? CODEX_EXPANSIVE_METERING_NOTE : null;\n}\n\n/**\n * \"A turn at 967k re-sends 2.6× the tokens of one at 367k.\" — the only ratio\n * any Expansive cost sentence quotes, and it is arithmetic on the model's own\n * budgets. Null when either compaction point is unknown. It comes BEFORE the\n * codex metering note, whose \"twice that\" refers to it.\n */\nexport function expansiveRatioSentence(option: VerseModelOption | null | undefined): string | null {\n const standard = budgetFor(option, 'standard');\n const expansive = budgetFor(option, 'expansive');\n const stdCap = standard ? standard.autoCompactAt ?? standard.contextWindow : null;\n const expCap = expansive ? expansive.autoCompactAt ?? expansive.contextWindow : null;\n if (stdCap === null || expCap === null || stdCap <= 0) return null;\n return `A turn at ${formatTokens(expCap)} re-sends ${(expCap / stdCap).toFixed(1)}× the tokens of one at ${formatTokens(stdCap)}.`;\n}\n\n/**\n * What each mode costs, in plain words, for the mode selector. Numbers come\n * from the model's own budgets; the only ratio quoted is arithmetic (every\n * turn re-sends everything held, so a turn at X re-sends X/Y of one at Y) —\n * a token ratio, not a usage multiplier, which is why codex also carries the\n * shared metering caveat.\n */\nexport function contextModeDescription(engine: VerseEngine, option: VerseModelOption, mode: VerseContextMode): string {\n const standard = budgetFor(option, 'standard');\n const expansive = budgetFor(option, 'expansive');\n const stdCap = standard ? standard.autoCompactAt ?? standard.contextWindow : null;\n const expCap = expansive ? expansive.autoCompactAt ?? expansive.contextWindow : null;\n if (mode === 'standard' || expCap === null) {\n if (stdCap === null) return 'The CLI compacts at its own default point for this model.';\n return engine === 'claude'\n ? `Compacts at about ${formatTokens(stdCap)}. Every turn re-sends the whole context, so holding it under this point keeps each turn cheaper and recall sharper.`\n : `Compacts at about ${formatTokens(stdCap)} — the CLI's own default budget for this model.`;\n }\n const ratio = expansiveRatioSentence(option);\n const metering = expansiveMeteringNote(engine);\n return `Runs to about ${formatTokens(expCap)} before compacting — for tightly coupled, cross-cutting work that needs everything in view at once. Each turn re-sends everything held, so a long expansive chat uses noticeably more of your plan per turn.${ratio ? ` ${ratio}` : ''}${metering ? ` ${metering}` : ''}`;\n}\n\n/** Why a model offers no mode choice, for the one line where the selector would be. */\nexport function noModeReason(engine: VerseEngine, option: VerseModelOption): string {\n if (!budgetFor(option, 'standard')) return 'This model’s window is unknown, so Verse cannot offer a larger budget for it.';\n if (engine === 'grok' || engine === 'local') return 'One budget: this CLI compacts at its own fixed point, and Verse cannot raise it per chat.';\n return 'One budget: this model has no larger window to expand into.';\n}\n\n// ---------------------------------------------------------------------------\n// Context fit\n// ---------------------------------------------------------------------------\n\n/** Short form for a picker row, where it sits beside the capacity words. */\nexport const FIT_SHORT: Record<VerseFitVerdict, string> = {\n fits: 'code fits',\n tight: 'code fits, tight',\n expansive: 'code needs expansive',\n split: 'code too big — split',\n};\n\n/** Badge text for the chosen model. */\nexport const FIT_LABEL: Record<VerseFitVerdict, string> = {\n fits: 'Fits',\n tight: 'Tight fit',\n expansive: 'Needs expansive',\n split: 'Split the work',\n};\n\n/** A fit reading that is a FLOOR (a root hit its file or time cap). */\nexport function fitIsFloor(fit: VerseContextFit): boolean {\n return fit.roots.some((r) => r.truncated);\n}\n\n/**\n * The verdict for a model IN THE MODE THE CHAT WOULD RUN IN, or null when its\n * budget is unknown (never guessed).\n *\n * `engine` is the seat's engine: the fixed prompt added to the code is that\n * CLI's estimate (context-math `sessionOverheadTokens` — ≈15k local, ≈25k\n * claude), not one flat figure. With a flat 30k a 64k local seat (compacts\n * ≈32.5k) called anything over ~2.5k tokens of code \"too big — split\". Null\n * falls back to the largest estimate.\n *\n * In Standard this is context-math's `fitVerdict` as is — including\n * \"expansive\", the suggestion that only the bigger budget holds the code. In\n * Expansive the bigger budget IS the budget, so the same thresholds are\n * applied to it: the code fits, fits tightly, or no single context on this\n * model holds it. Without this, a chat already set to Expansive would still be\n * told its code \"fits tightly under ≈367k\" — true of a budget it is not using.\n */\nexport function modelFit(\n tokens: number | null | undefined,\n option: VerseModelOption | null | undefined,\n mode: VerseContextMode,\n engine: VerseEngine | null,\n): VerseFitVerdict | null {\n if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) return null;\n const overhead = sessionOverheadTokens(engine);\n if (resolveContextMode(option, mode) === 'expansive' && option) {\n const expansive = budgetFor(option, 'expansive')!;\n return fitVerdict(tokens, { ...option, contextWindow: expansive.contextWindow, autoCompactAt: expansive.autoCompactAt, expansive: null }, overhead);\n }\n return fitVerdict(tokens, option, overhead);\n}\n\n/**\n * The sentence under the fit badge, for the verdict `modelFit` gave in the\n * chat's mode. In Standard a tight fit also names the Expansive budget when\n * one exists, and \"needs expansive\" says what to do — suggestions only;\n * nothing here switches the mode.\n */\nexport function fitExplanation(input: {\n verdict: VerseFitVerdict;\n tokens: number;\n option: VerseModelOption;\n mode: VerseContextMode;\n floor?: boolean;\n}): string {\n const amount = `${input.floor ? 'at least ' : ''}~${formatTokens(input.tokens)} tokens of tracked code`;\n const inExpansive = resolveContextMode(input.option, input.mode) === 'expansive';\n const standard = budgetFor(input.option, 'standard');\n const expansive = budgetFor(input.option, 'expansive');\n const stdCap = standard ? formatTokens(standard.autoCompactAt ?? standard.contextWindow) : null;\n const expCap = expansive ? formatTokens(expansive.autoCompactAt ?? expansive.contextWindow) : null;\n const cap = inExpansive ? expCap : stdCap;\n const budget = inExpansive ? 'Expansive budget' : 'budget';\n switch (input.verdict) {\n case 'fits':\n return `All ${amount} fits well inside this model's ${budget}${cap ? ` (compacts ≈${cap})` : ''}, with room left for the conversation.`;\n case 'tight': {\n const roomier = !inExpansive && expCap !== null && hasExpansiveMode(input.option)\n ? ` Expansive (≈${expCap}) would hold it with room to spare.`\n : '';\n return `${capitalize(amount)} fits under the ≈${cap ?? '?'} compaction point${inExpansive ? ' of the Expansive budget' : ''}, with little room for the conversation — expect it to compact if the agent reads widely.${roomier}`;\n }\n case 'expansive':\n return `${capitalize(amount)} is more than the Standard budget (≈${stdCap ?? '?'}) holds; only Expansive (≈${expCap ?? '?'}) takes it all. Switch to Expansive, or narrow the folders to the part you are changing.`;\n case 'split':\n return `${capitalize(amount)} is more than any single context on this model holds${expCap ? `, even Expansive (≈${expCap})` : ''}. Split the work: fan it out across several chats, each scoped to one folder or subsystem, or narrow this chat to the part you are changing.`;\n }\n}\n\n/**\n * How the working-set figure is made — shown once, beside the badge. Names the\n * fixed-prompt figure the verdict added, and calls it an estimate: no CLI\n * reports its base prompt before a turn has run.\n */\nexport function fitMethodNote(engine: VerseEngine | null): string {\n const cli = engine ? ` for ${ENGINE_CLI_NAME[engine]}` : '';\n return `Estimated from the size of tracked text files (bytes ÷ 4), plus an estimated ~${formatTokens(sessionOverheadTokens(engine))} of fixed prompt${cli} that every chat starts with. An agent rarely reads everything, so this is a ceiling, not a forecast.`;\n}\n\nfunction capitalize(text: string): string {\n return text.length === 0 ? text : text.charAt(0).toUpperCase() + text.slice(1);\n}\n\n// ---------------------------------------------------------------------------\n// One session's context\n// ---------------------------------------------------------------------------\n\nexport interface SessionContextView {\n tokens: number;\n /** False when `tokens` is an upper bound (codex before its rollout is read). */\n exact: boolean;\n window: number | null;\n autoCompactAt: number | null;\n /** How the window is known; null when nothing says (no window, or a stored one without provenance). */\n source: VerseWindowSource | null;\n mode: VerseContextMode;\n /** The session's model has a real expansive budget on its seat. */\n hasModes: boolean;\n}\n\n/**\n * The window a session is measured against, by the precedence every surface\n * shares — runtime → the seat's CURRENT catalog budget for the model and mode\n * → what the record stored → the seat default. The precedence itself lives in\n * verse-model's `sessionContextBudget` (the context meter reads it too); this\n * only adds whether the model offers a second mode, for the panels here.\n */\nexport function sessionContext(session: VerseSession, seats: readonly VerseSeat[]): SessionContextView {\n const budget = sessionContextBudget(seats, session);\n return {\n tokens: Math.floor(budget.contextTokens),\n exact: budget.exact,\n window: budget.contextWindow,\n autoCompactAt: budget.autoCompactAt,\n source: budget.source,\n mode: budget.mode,\n hasModes: hasExpansiveMode(budget.option),\n };\n}\n\n/**\n * Cache-hit ratio, except when the provider reported no cache activity at all\n * — then null (\"none reported\"), because 0% would claim caching failed when\n * the truth is that nothing was said about it.\n */\nexport function reportedCacheHitRatio(usage: Pick<VerseUsage, 'inputTokens' | 'cacheReadTokens' | 'cacheCreationTokens'>): number | null {\n if (usage.cacheReadTokens <= 0 && usage.cacheCreationTokens <= 0) return null;\n return cacheHitRatio(usage);\n}\n\nexport function formatRatio(ratio: number | null): string {\n if (ratio === null || !Number.isFinite(ratio)) return '—';\n return `${Math.round(ratio * 100)}%`;\n}\n\nexport interface TurnContextStats {\n /** Turns with at least one context reading. */\n turns: number;\n /** Mean of each turn's context (its largest trustworthy reading); null with no turns. */\n average: number | null;\n /** Largest per-turn context seen. */\n peak: number | null;\n /** False when any counted turn had only an upper-bound reading. */\n exact: boolean;\n /** Compactions recorded in the log. */\n compactions: number;\n /** Cache-hit ratio of the most recent turn (see `reportedCacheHitRatio`). */\n lastTurnCacheHit: number | null;\n}\n\n/**\n * Context per turn from the open chat's event log.\n *\n * A turn's context is its LARGEST trustworthy reading: within one turn the\n * prompt only grows, except across a compaction, and the peak is what the\n * turn actually paid for. Exact readings (`context` events, usage frames not\n * flagged as upper bounds) win; a turn whose only reading is codex's turn\n * total contributes that total and flags the statistic as an upper bound —\n * that figure can be many times the real prompt, so it is never mixed in\n * where an exact one exists.\n */\nexport function turnContextStats(events: readonly VerseEvent[]): TurnContextStats {\n const perTurn = new Map<string, { exact: number | null; bound: number | null }>();\n let compactions = 0;\n let lastUsage: VerseUsage | null = null;\n const note = (turnId: string, tokens: number, exact: boolean): void => {\n if (!Number.isFinite(tokens) || tokens < 0) return;\n const entry = perTurn.get(turnId) ?? { exact: null, bound: null };\n if (exact) entry.exact = Math.max(entry.exact ?? 0, tokens);\n else entry.bound = Math.max(entry.bound ?? 0, tokens);\n perTurn.set(turnId, entry);\n };\n for (const event of events) {\n switch (event.type) {\n case 'usage':\n lastUsage = event.usage;\n note(event.turnId, event.usage.contextTokens, event.usage.contextTokensExact !== false);\n break;\n case 'context':\n if (event.turnId !== null) note(event.turnId, event.contextTokens, event.exact);\n break;\n case 'compaction':\n compactions += 1;\n break;\n default:\n break;\n }\n }\n let sum = 0;\n let peak: number | null = null;\n let exact = true;\n let turns = 0;\n for (const entry of perTurn.values()) {\n const value = entry.exact ?? entry.bound;\n if (value === null) continue;\n if (entry.exact === null) exact = false;\n turns += 1;\n sum += value;\n peak = peak === null ? value : Math.max(peak, value);\n }\n return {\n turns,\n average: turns === 0 ? null : Math.round(sum / turns),\n peak,\n exact,\n compactions,\n lastTurnCacheHit: lastUsage === null ? null : reportedCacheHitRatio(lastUsage),\n };\n}\n\nexport interface IdleCacheWarning {\n idleMs: number;\n tokens: number;\n /** A local model: the cost is time, not spend. */\n local: boolean;\n}\n\n/**\n * The next turn of an idle chat re-reads its whole context uncached once the\n * provider's prompt cache has expired (CACHE_IDLE_TTL_MS). Below the fixed\n * prompt every new chat on this engine pays anyway (its estimate) there is\n * nothing worth warning about.\n */\nexport function idleCacheWarning(\n session: Pick<VerseSession, 'status' | 'engine'>,\n tokens: number,\n now: number,\n /**\n * When the chat last TALKED to its provider (`lastTurnActivityAt(events)`),\n * never `session.updatedAt`: a rename or mode switch moves that without\n * warming the provider's cache. Null (no turn yet / log not loaded) → no warning.\n */\n lastActivityAt: string | null,\n): IdleCacheWarning | null {\n if (session.status === 'running' || lastActivityAt === null) return null;\n const last = Date.parse(lastActivityAt);\n if (!Number.isFinite(last)) return null;\n const idleMs = now - last;\n if (idleMs < CACHE_IDLE_TTL_MS || tokens < sessionOverheadTokens(session.engine)) return null;\n return { idleMs, tokens, local: session.engine === 'local' };\n}\n\n// ---------------------------------------------------------------------------\n// Per-seat efficiency across sessions (Usage section)\n// ---------------------------------------------------------------------------\n\nexport interface SeatEfficiencyRow {\n seatId: string;\n label: string;\n engine: VerseEngine;\n /** The seat is no longer in the roster (its sessions remain on disk). */\n retired: boolean;\n sessions: number;\n turns: number;\n /** input + cache read + cache write: every prompt token the seat was sent. */\n promptTokens: number;\n outputTokens: number;\n /** Null when the provider reported no cache activity on any of these sessions. */\n cacheHitRatio: number | null;\n compactions: number;\n expansiveSessions: number;\n /** The fullest context among these sessions right now, as a share of its window. */\n fullest: { sessionId: string; title: string; tokens: number; window: number | null; exact: boolean } | null;\n}\n\n/**\n * \"Fullest\" is a share of the window when both windows are known; a context\n * with a known window outranks one without (its fullness is a fact, the other\n * a size); two unknown windows compare by size.\n */\nfunction fuller(a: NonNullable<SeatEfficiencyRow['fullest']>, b: NonNullable<SeatEfficiencyRow['fullest']>): boolean {\n if (a.window !== null && b.window !== null) return a.tokens / a.window > b.tokens / b.window;\n if (a.window !== null) return true;\n if (b.window !== null) return false;\n return a.tokens > b.tokens;\n}\n\n/**\n * Aggregate every session by seat. Ratios are computed from SUMS, never as a\n * mean of per-session ratios — a two-turn chat must not weigh as much as a\n * two-hundred-turn one. Seats with no sessions are omitted rather than shown\n * as zeros. Sorted by prompt tokens, largest first: that is where efficiency\n * matters most.\n */\nexport function seatEfficiency(sessions: readonly VerseSession[], seats: readonly VerseSeat[]): SeatEfficiencyRow[] {\n const bySeat = new Map<string, VerseSession[]>();\n for (const s of sessions) {\n const list = bySeat.get(s.seatId) ?? [];\n list.push(s);\n bySeat.set(s.seatId, list);\n }\n const rows: SeatEfficiencyRow[] = [];\n for (const [seatId, list] of bySeat) {\n const seat = seats.find((s) => s.id === seatId) ?? null;\n let input = 0;\n let read = 0;\n let write = 0;\n let output = 0;\n let turns = 0;\n let compactions = 0;\n let expansiveSessions = 0;\n let fullest: SeatEfficiencyRow['fullest'] = null;\n for (const s of list) {\n input += s.usage.inputTokens;\n read += s.usage.cacheReadTokens;\n write += s.usage.cacheCreationTokens;\n output += s.usage.outputTokens;\n turns += s.turnCount;\n compactions += s.compactionCount ?? 0;\n // The mode as the meter resolves it (verse-model sessionContextBudget),\n // counted before the empty-reading skip: the mode is a fact about the\n // chat, not about its reading.\n const view = sessionContext(s, seats);\n if (view.mode === 'expansive') expansiveSessions += 1;\n if (view.tokens <= 0) continue;\n const candidate = { sessionId: s.id, title: s.title, tokens: view.tokens, window: view.window, exact: view.exact };\n if (fullest === null || fuller(candidate, fullest)) fullest = candidate;\n }\n rows.push({\n seatId,\n label: seat?.label ?? seatId,\n engine: seat?.engine ?? list[0]!.engine,\n retired: seat === null,\n sessions: list.length,\n turns,\n promptTokens: input + read + write,\n outputTokens: output,\n cacheHitRatio: reportedCacheHitRatio({ inputTokens: input, cacheReadTokens: read, cacheCreationTokens: write }),\n compactions,\n expansiveSessions,\n fullest,\n });\n }\n return rows.sort((a, b) => b.promptTokens - a.promptTokens || a.label.localeCompare(b.label));\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{a as e,n as t,o as n,r}from"./index-DO9a5IaB.js";var i=n(e(),1),a={data:void 0,error:void 0,status:`idle`,updatedAt:null},o=new Map,s=0,c=[],l=0,u=0;function d(){--s;let e=c.shift();e&&e()}function ee(e){let t=()=>{let t=u;s+=1,s>l&&(l=s);let n=()=>{t===u&&d()},r;try{r=e()}catch(e){return n(),Promise.reject(e instanceof Error?e:Error(String(e)))}return r.then(e=>(n(),e),e=>{throw n(),e})};return s<4?t():new Promise((e,n)=>{c.push(()=>{t().then(e,n)})})}function te(){u+=1,s=0,l=0;for(let e of c.splice(0))e()}function f(e){let t=o.get(e);return t||(t={snapshot:a,inFlight:null,fetcher:null,subscribers:new Set,issued:0,issuedAt:0,applied:0},o.set(e,t)),t}function p(e,t){e.snapshot={...e.snapshot,...t}}function m(e){let t=o.get(e);if(t)for(let e of t.subscribers)e()}function h(e,t){if(Object.is(e,t))return!0;if(typeof e!=typeof t||e===null||t===null||typeof e!=`object`||Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,n)=>h(e,t[n]));let n=e,r=t,i=Object.keys(n);return i.length===Object.keys(r).length&&i.every(e=>Object.prototype.hasOwnProperty.call(r,e)&&h(n[e],r[e]))}function ne(e,t){let n=f(e);return n.subscribers.add(t),()=>{n.subscribers.delete(t)}}function g(e){return f(e).snapshot}function re(e,t){return e.snapshot.updatedAt===null||e.snapshot.status===`error`?!1:Date.now()-e.snapshot.updatedAt<t}function ie(e,t){return e.inFlight===null?!1:t.joinWindowMs===void 0||Date.now()-e.issuedAt<t.joinWindowMs}function ae(e,t,n){let r=f(e);if(r.fetcher=t,!n.force){if(ie(r,n))return r.inFlight;if(n.maxAgeMs!==void 0&&re(r,n.maxAgeMs))return Promise.resolve()}let i=r.issued+=1;r.issuedAt=Date.now(),p(r,{status:r.snapshot.data===void 0?`loading`:`refreshing`}),m(e);let a=ee(t).then(e=>{if(i<r.applied)return;r.applied=i;let t=r.snapshot.data!==void 0&&h(r.snapshot.data,e);p(r,{data:t?r.snapshot.data:e,error:void 0,status:`success`,updatedAt:Date.now()})}).catch(e=>{i<r.applied||(r.applied=i,p(r,{error:e instanceof Error?e:Error(String(e)),status:`error`}))}).finally(()=>{r.issued===i&&(r.inFlight=null),m(e)});return r.inFlight=a,a}function oe(e,t){return ae(e,t,{})}function se(e,t,n){return ae(e,t,{maxAgeMs:n})}function ce(e,t,n=!1){return ae(e,t,n?{force:!0}:{joinWindowMs:250})}function le(e){let t=o.get(e);t&&t.fetcher&&oe(e,t.fetcher)}function _(e){for(let t of o.keys())t.startsWith(e)&&le(t)}function ue(e,t){t.snapshot=a,t.fetcher=null,t.inFlight=null,t.issued+=1,t.applied=t.issued,m(e)}function de(){te();for(let[e,t]of[...o]){if(t.subscribers.size===0){o.delete(e);continue}ue(e,t)}}var fe=4e5,pe=2e4,me=13e3,he=.9,ge=36e5,_e={claude:25e3,codex:15e3,grok:2e4,local:15e3},ve=25e3;function ye(e){return e?_e[e]??25e3:ve}_e.local+8e3+pe+me;function v(e){return typeof e==`number`&&Number.isFinite(e)&&e>0?Math.floor(e):null}function be(e,t,n=null){let r=Math.min(e,v(n)??e),i=Math.min(v(t)??2e4,pe);return Math.max(0,r-i-me)}function xe(e){return Math.floor(e*he)}var Se={"claude-opus-5.5":`claude-opus-5-5`};function Ce(e){return Se[e]??e}function y(e,t){if(!e)return null;if(t===`expansive`)return e.expansive??null;let n=v(e.contextWindow);return n===null?null:{contextWindow:n,autoCompactAt:v(e.autoCompactAt??null)}}function we(e){let t=y(e,`standard`),n=y(e,`expansive`);return n?!t||(n.autoCompactAt??n.contextWindow)>(t.autoCompactAt??t.contextWindow):!1}function Te(e,t){if(t===`expansive`)return null;let n=v(e?.contextWindow??null);return n!==null&&n>4e5&&we(e)?fe:null}function Ee(e){let t=v(e.runtimeWindow);if(t===null)return e.budget?.autoCompactAt??null;if(e.budget&&e.budget.contextWindow===t)return e.budget.autoCompactAt;switch(e.engine){case`claude`:case`local`:return be(t,e.maxOutputTokens??null,e.autocompactWindow??null);case`codex`:return xe(Math.round(t*100/95));default:{let n=e.budget;return!n||n.autoCompactAt===null||n.contextWindow<=0?null:Math.floor(t*(n.autoCompactAt/n.contextWindow))}}}var De=.8,Oe=.95;function ke(e){let t=Math.max(0,Math.floor(e.contextTokens||0)),n=v(e.contextWindow),r=v(e.autoCompactAt??null),i=n===null?null:t/n,a=r===null?null:t/r,o=a??i,s=e.contextTokensExact!==!1,c=`unknown`;return!s&&o!==null&&o>=1?c=`unknown`:n!==null&&t>n?c=`over`:o!==null&&(c=o>=.95?`danger`:o>=.8?`warn`:`ok`),{tokens:t,window:n,autoCompactAt:r,ofWindow:i,ofCompaction:a,untilCompaction:r===null?null:Math.max(0,r-t),tone:c,exact:s}}function Ae(e){let t=e.inputTokens+e.cacheReadTokens+e.cacheCreationTokens;return t>0?e.cacheReadTokens/t:null}function je(e){return Math.ceil(Math.max(0,e)/4)}function Me(e,t,n=ve){let r=y(t,`standard`);if(!r)return null;let i=Math.max(0,e)+Math.max(0,n),a=r.autoCompactAt??r.contextWindow;if(i<=a*.6)return`fits`;if(i<=a)return`tight`;let o=y(t,`expansive`);return o&&i<=(o.autoCompactAt??o.contextWindow)?`expansive`:`split`}function b(e){return e>=1e6?`${(e/1e6).toFixed(e%1e6==0?0:1)}M`:e>=1e3?`${Math.round(e/1e3)}k`:String(e)}function Ne(e){let t=ke(e.usage),n=[],r=`none`,i=e=>{(e===`urge`||e===`suggest`&&r===`none`)&&(r=e)};t.exact&&(t.tone===`over`?(i(`urge`),n.push(`Context (${b(t.tokens)}) is past the ${b(t.window??0)} window.`)):t.ofCompaction!==null&&t.ofCompaction>=.8&&(i(`suggest`),n.push(`About ${b(t.untilCompaction??0)} tokens left before the CLI auto-compacts.`)));let a=e.compactionCount??0;if(a>=2&&(i(`suggest`),n.push(`Compacted ${a} times — early turns now survive only as summaries.`)),e.lastActivityAt){let r=e.now-Date.parse(e.lastActivityAt);t.exact&&Number.isFinite(r)&&r>=36e5&&t.tokens>=1e5&&(i(`suggest`),n.push(`Idle over an hour: the prompt cache has likely expired, so the next turn re-reads ~${b(t.tokens)} tokens at full cost.`))}return{level:r,reasons:n}}function Pe(e){if((e.session.contextMode??`standard`)!==`standard`||!we(e.option))return{suggest:!1,reason:null};let t=e.session.compactionCount??0;return t>=2?{suggest:!0,reason:`This session has compacted ${t} times; expansive mode keeps more of it in view.`}:typeof e.workingSetTokens==`number`&&Me(e.workingSetTokens,e.option)===`expansive`?{suggest:!0,reason:`The reachable code (~${b(e.workingSetTokens)} tokens) only fits the expansive budget.`}:{suggest:!1,reason:null}}function Fe(e){return e===`claude`||e===`codex`}var x=`ashlr.verse.drafts.v1`,S=`ashlr.verse.sent.v1`,Ie=40,C=65536;function w(e,t){let n={};try{let r=localStorage.getItem(e),i=r?JSON.parse(r):null;if(i&&typeof i==`object`&&!Array.isArray(i))for(let[e,r]of Object.entries(i)){let i=t(r);i!==null&&(n[e]=i)}}catch{}return n}function Le(e,t){let n=Object.keys(t);for(let e of n.slice(0,Math.max(0,n.length-Ie)))delete t[e];try{return localStorage.setItem(e,JSON.stringify(t)),!0}catch{return!1}}var Re=e=>typeof e==`string`&&e.length>0?e.slice(0,C):null,T=e=>{if(!Array.isArray(e))return null;let t=e.filter(e=>typeof e==`string`&&e.length>0).slice(-25).map(e=>e.slice(0,C));return t.length>0?t:null},E=new Map;function ze(e){if(!e)return``;let t=E.get(e);return t===void 0?w(x,Re)[e]??``:t}function Be(e,t){if(!e)return;let n=t.trim().length>0?t.slice(0,C):``,r=w(x,Re);if(delete r[e],n&&(r[e]=n),E.delete(e),!Le(x,r)){E.set(e,n);for(let e of[...E.keys()].slice(0,Math.max(0,E.size-Ie)))E.delete(e)}}function Ve(e){return e?w(S,T)[e]??[]:[]}function He(e,t){if(!e||t.trim().length===0)return;let n=w(S,T),r=n[e]??[];delete n[e],n[e]=(r[r.length-1]===t?r:[...r,t.slice(0,C)]).slice(-25),Le(S,n)}function Ue(e){if(e){E.delete(e);for(let[t,n]of[[x,Re],[S,T]]){let r=w(t,n);e in r&&(delete r[e],Le(t,r))}}}function We(){E.clear();for(let e of[x,S])try{localStorage.removeItem(e)}catch{}}function Ge(e){let t=e.trim();return t.length===0?0:Math.max(1,Math.ceil(t.length/4))}function Ke(e,t){let n=Ge(e);if(n===0)return null;let r=typeof t.contextWindow==`number`&&t.contextWindow>0?t.contextWindow:null;if(r===null)return null;let i=ke({contextTokens:(typeof t.contextTokens==`number`&&Number.isFinite(t.contextTokens)?Math.max(0,t.contextTokens):0)+n,contextWindow:r,autoCompactAt:t.autoCompactAt??null,contextTokensExact:t.exact});return i.tone===`ok`||i.tone===`unknown`?null:{draftTokens:n,projectedTokens:i.tokens,projectedPercent:Math.min(999,Math.round((i.ofWindow??0)*100)),autoCompactAt:i.autoCompactAt,pastCompaction:i.ofCompaction!==null&&i.ofCompaction>=1,tone:i.tone,exact:i.exact}}var qe=`ashlr.readClientProof.v1`,Je=/^[a-f0-9]{64}$/,D=12e5;function Ye(){let e=new Uint8Array(32);return crypto.getRandomValues(e),Array.from(e,e=>e.toString(16).padStart(2,`0`)).join(``)}function Xe(){try{let e=sessionStorage.getItem(qe);if(e&&Je.test(e))return e}catch{}let e=Ye();try{sessionStorage.setItem(qe,e)}catch{}return e}var O=Xe(),k=null,A=null,Ze=!1,j=null,M=null,N=null,P={phase:`checking`,checked:!1,mutationTokenHeldUntil:null},Qe=new Set;function $e(){for(let e of Qe)e()}function F(e){P={...P,...e},$e()}function I(e){return Qe.add(e),()=>Qe.delete(e)}function L(){return P}function R(){return O}async function et(e){let t=e.trim();if(!/^[a-f0-9]{64}$/.test(t))throw Error(`That does not look like a read token — expected 64 hex characters.`);let n=await fetch(`/api/session`,{method:`POST`,credentials:`same-origin`,headers:{"x-ashlr-token":t,"x-ashlr-read-client":O}});if(n.status===204){j=t,F({phase:`authenticated`,checked:!0});return}throw n.status===401?Error(`Server rejected that token. Double-check the read token printed by this server at startup.`):Error(`Unexpected response establishing session (HTTP ${n.status}).`)}async function tt(){try{await fetch(`/api/session`,{method:`DELETE`,credentials:`same-origin`,headers:{"x-ashlr-read-client":O}})}catch{}j=null,N=null,nt()}function nt(){B(),de(),We(),F({phase:`unauthenticated`,checked:!0})}function rt(){if(P.phase===`authenticated`){if(j){it();return}nt()}}function it(){if(M)return M;let e=j;return e?(M=(async()=>{try{return await et(e),_(``),!0}catch{return j=null,N=null,nt(),!1}finally{M=null}})(),M):Promise.resolve(!1)}function at(e){F({phase:e?`authenticated`:`unauthenticated`,checked:!0})}function ot(){let e=window.__ASHLR_TOKENS__;if(e&&typeof e==`object`){try{delete window.__ASHLR_TOKENS__}catch{window.__ASHLR_TOKENS__=void 0}typeof e.readToken==`string`&&(N={readToken:e.readToken,...typeof e.token==`string`?{token:e.token}:{}})}return N?{...N}:null}async function st(){let e=ot();if(!e)return!1;try{await et(e.readToken)}catch{return N=null,!1}return e.token&&/^[a-f0-9]{64}$/.test(e.token.trim())&&z(e.token.trim(),{host:!0}),!0}function ct(){return k}function lt(){return k!==null}function ut(){A&&clearTimeout(A),A=setTimeout(()=>{Ze&&k!==null?dt():B()},D)}function z(e,t={}){k=e,Ze=t.host===!0,F({mutationTokenHeldUntil:Date.now()+D}),ut()}function dt(){k!==null&&(F({mutationTokenHeldUntil:Date.now()+D}),ut())}function B(){k=null,Ze=!1,A&&=(clearTimeout(A),null),F({mutationTokenHeldUntil:null})}var ft=[`runs`,`swarms`,`inbox`,`daemon`,`daemon-observation`,`fleet-activity-ping`,`fleet-activity-observation`,`snapshot`,`session-expired`],pt={runs:[`runs`],swarms:[`swarms`],inbox:[`inbox`],daemon:[`daemon`,`control-snapshot`,`verse-control`,`verse-usage-control`,`verse-caps`],"daemon-observation":[`daemon`,`control-snapshot`,`verse-control`,`verse-usage-control`,`verse-caps`],"fleet-activity-ping":[`fleet-activity`,`fleet`],"fleet-activity-observation":[`fleet-activity`,`fleet`],snapshot:[`dashboard-snapshot`,`agent-os-snapshot`],"session-expired":[]},mt=new Map,V=null,H=null,U=0;function ht(){return Math.min(1e3*2**U,15e3)}function gt(){V&&=(V.close(),null),H&&=(clearTimeout(H),null)}function _t(){H||=setTimeout(()=>{H=null,U+=1,vt()},ht())}function vt(){if(r()){gt();return}if(L().phase!==`authenticated`)return;gt();let e=`/api/events?client=${encodeURIComponent(R())}`,t=new EventSource(e,{withCredentials:!0});V=t,t.onopen=()=>{U=0};for(let e of ft)t.addEventListener(e,t=>{let n=t,r;try{r=JSON.parse(n.data)}catch{r=void 0}for(let t of pt[e])le(t);e===`inbox`&&(_(`inbox-list:`),_(`proposal-detail-`)),e===`session-expired`&&rt();let i=mt.get(e);if(i)for(let e of i)e(r)});t.onerror=()=>{t.close(),V===t&&(V=null),L().phase===`authenticated`&&_t()}}I(()=>{L().phase===`authenticated`?(U=0,vt()):gt()});var yt=`ashlr.theme.v1`;function bt(e){let t=document.documentElement;e===`system`?t.removeAttribute(`data-theme`):t.setAttribute(`data-theme`,e)}function xt(){try{let e=localStorage.getItem(yt);if(e===`light`||e===`dark`||e===`system`)return e}catch{}return`system`}var W=xt();bt(W);var St=new Set;function Ct(){for(let e of St)e()}function wt(e){return St.add(e),()=>St.delete(e)}function G(){return W}function K(e){W=e,bt(W);try{localStorage.setItem(yt,W)}catch{}Ct()}function Tt(){K(W===`system`?`light`:W===`light`?`dark`:`system`)}var Et=`ashlr.verse.appearance.v1`,Dt=30,Ot=78,q={h:245,s:72,l:58},kt=[{id:`indigo`,label:`Indigo`,h:245,s:72,l:58},{id:`blue`,label:`Blue`,h:212,s:80,l:50},{id:`cyan`,label:`Cyan`,h:190,s:80,l:40},{id:`teal`,label:`Teal`,h:168,s:70,l:38},{id:`green`,label:`Green`,h:145,s:60,l:38},{id:`amber`,label:`Amber`,h:38,s:80,l:42},{id:`rose`,label:`Rose`,h:348,s:72,l:50},{id:`violet`,label:`Violet`,h:278,s:66,l:55}];function At(){try{return typeof window<`u`&&typeof window.matchMedia==`function`&&window.matchMedia(`(prefers-reduced-motion: reduce)`).matches}catch{return!1}}function jt(){return{theme:`system`,accentH:q.h,accentS:q.s,accentL:q.l,density:`comfortable`,uiScale:`default`,displayFont:`grotesk`,radius:`default`,motion:`system`,reduceMotion:At()}}function Mt(e){return e===`system`||e===`reduce`||e===`full`}function Nt(e){return e===`reduce`||e!==`full`&&At()}var Pt=(e,t,n)=>Math.min(n,Math.max(t,e));function Ft(e){return e===`comfortable`||e===`compact`}function It(e){return e==="default"||e===`large`||e===`xlarge`}function Lt(e){return e===`grotesk`||e===`ui`||e===`mono`}function Rt(e){return e===`sharp`||e==="default"||e===`soft`}function zt(e,t){let n={...e,...t},r=t.reduceMotion,i=Mt(t.motion)?t.motion:typeof r==`boolean`?r?`reduce`:`full`:Mt(e.motion)?e.motion:`system`;return{theme:n.theme===`light`||n.theme===`dark`?n.theme:`system`,accentH:Number.isFinite(n.accentH)?(Math.round(n.accentH)%360+360)%360:q.h,accentS:Number.isFinite(n.accentS)?Pt(Math.round(n.accentS),0,100):q.s,accentL:Number.isFinite(n.accentL)?Pt(Math.round(n.accentL),Dt,Ot):q.l,density:Ft(n.density)?n.density:`comfortable`,uiScale:It(n.uiScale)?n.uiScale:`default`,displayFont:Lt(n.displayFont)?n.displayFont:`grotesk`,radius:Rt(n.radius)?n.radius:`default`,motion:i,reduceMotion:Nt(i)}}function Bt(){let e=jt(),t={};try{let e=localStorage.getItem(Et);if(e){let n=JSON.parse(e);n&&typeof n==`object`&&(t=n)}}catch{}let{reduceMotion:n,...r}=t;return zt(e,{...r,theme:G()})}function Vt(e){try{let{theme:t,reduceMotion:n,...r}=e;localStorage.setItem(Et,JSON.stringify(r))}catch{}}var Ht={grotesk:null,ui:`var(--font-ui)`,mono:`var(--font-mono)`};function Ut(e){if(typeof document>`u`)return;let t=document.documentElement;t.style.setProperty(`--accent-h`,String(e.accentH)),t.style.setProperty(`--accent-s`,`${e.accentS}%`),t.style.setProperty(`--accent-l`,`${e.accentL}%`);let n=Ht[e.displayFont];n===null?t.style.removeProperty(`--font-display`):t.style.setProperty(`--font-display`,n),t.setAttribute(`data-density`,e.density),t.setAttribute(`data-ui-scale`,e.uiScale),t.setAttribute(`data-radius`,e.radius),e.motion===`system`?t.removeAttribute(`data-motion`):t.setAttribute(`data-motion`,e.motion)}var J=Bt();Ut(J);var Wt=new Set;function Gt(){for(let e of Wt)e()}wt(()=>{J.theme!==G()&&(J={...J,theme:G()},Gt())});function Kt(e){return Wt.add(e),()=>Wt.delete(e)}function qt(){return J}function Jt(e){let t=zt(J,e),n=t.theme!==J.theme;J=t,Ut(J),Vt(J),n&&K(J.theme),Gt()}function Yt(){try{localStorage.removeItem(Et)}catch{}J=jt(),Ut(J),K(`system`),Gt()}function Xt(e=J){let t=jt();return e.theme===t.theme&&e.accentH===t.accentH&&e.accentS===t.accentS&&e.accentL===t.accentL&&e.density===t.density&&e.uiScale===t.uiScale&&e.displayFont===t.displayFont&&e.radius===t.radius&&e.motion===t.motion}function Zt(e=J){return kt.find(t=>t.h===e.accentH&&t.s===e.accentS&&t.l===e.accentL)??null}var Qt={min:Dt,max:Ot};function $t(e,t){let n=t?.freshMs??1e4,r=(0,i.useSyncExternalStore)((0,i.useCallback)(t=>ne(e.key,t),[e.key]),()=>g(e.key),()=>g(e.key));return(0,i.useEffect)(()=>{se(e.key,()=>e.fetch(),n)},[e.key,n]),r}function en(e){return(0,i.useCallback)(()=>{ce(e.key,()=>e.fetch())},[e])}function tn(e){return(0,i.useCallback)(()=>{ce(e.key,()=>e.fetch(),!0)},[e])}function nn(){return(0,i.useSyncExternalStore)(I,()=>L().phase,()=>L().phase)}function rn(){let e=(0,i.useSyncExternalStore)(I,()=>L().mutationTokenHeldUntil,()=>L().mutationTokenHeldUntil);return{hasHold:lt(),token:ct(),heldUntil:e,setToken:z,touch:dt,clear:B}}function an(){return{theme:(0,i.useSyncExternalStore)(wt,G,G),set:K,cycle:Tt}}function on(){return{appearance:(0,i.useSyncExternalStore)(Kt,qt,qt),set:Jt,reset:Yt}}var Y={badge:`_badge_ytqgz_4`,dot:`_dot_ytqgz_17`,neutral:`_neutral_ytqgz_24`,info:`_info_ytqgz_32`,running:`_running_ytqgz_40`,pulse:`_pulse_ytqgz_1`,success:`_success_ytqgz_49`,warning:`_warning_ytqgz_57`,danger:`_danger_ytqgz_65`,unknown:`_unknown_ytqgz_73`,phaseDot:`_phaseDot_ytqgz_82`},X=t(),sn={pending:`neutral`,queued:`neutral`,waiting:`neutral`,running:`running`,active:`running`,in_progress:`running`,building:`running`,applying:`running`,done:`success`,success:`success`,succeeded:`success`,applied:`success`,merged:`success`,ready:`success`,approved:`success`,ship:`success`,review:`warning`,degraded:`warning`,paused:`warning`,noise:`neutral`,failed:`danger`,error:`danger`,errored:`danger`,rejected:`danger`,harmful:`danger`,cancelled:`neutral`,canceled:`neutral`,unknown:`unknown`};function cn(e){return e?sn[e.toLowerCase()]??`neutral`:`unknown`}function ln({status:e,tone:t,children:n}){let r=t??cn(e);return(0,X.jsxs)(`span`,{className:`${Y.badge} ${Y[r]}`,children:[(0,X.jsx)(`span`,{className:Y.dot,"aria-hidden":`true`}),n??e]})}function un({index:e}){let t=(e%6+6)%6;return(0,X.jsx)(`span`,{className:Y.phaseDot,style:{background:`var(--phase-${t+1})`},"aria-hidden":`true`})}var Z=class extends Error{status;path;detail;code;constructor(e,t,n,r=null,i=null){super(e),this.status=t,this.path=n,this.detail=r,this.code=i,this.name=`ApiError`}},dn=class extends Z{constructor(e){super(`This server was started without --allow-dispatch, so mutations are disabled.`,404,e),this.name=`DispatchDisabledError`}};async function fn(e,t){let n=await fetch(e,{method:`GET`,credentials:`same-origin`,headers:{"x-ashlr-read-client":R()},signal:t});if(n.status===401)throw rt(),new Z(`Read session expired.`,401,e);if(!n.ok)throw new Z(`GET ${e} failed (HTTP ${n.status}).`,n.status,e);return await n.json()}async function pn(e,t,n,r){let i=await fetch(e,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/json`,"x-ashlr-token":n},body:JSON.stringify(t??{}),signal:r});if(i.status===401)throw new Z(`Mutation token was rejected.`,401,e);if(!i.ok){let{detail:t,code:n}=await mn(i);throw i.status===404&&n===null?new dn(e):new Z(`POST ${e} failed (HTTP ${i.status})${t?`: ${t}`:``}.`,i.status,e,t||null,n)}if(i.status!==204)return await i.json()}async function mn(e){try{let t=await e.json();return{detail:typeof t.error==`string`&&t.error?t.error:typeof t.note==`string`?t.note:``,code:typeof t.code==`string`&&t.code?t.code:null}}catch{return{detail:``,code:null}}}function hn(){return`/api/events?client=${encodeURIComponent(R())}`}var gn={root:`_root_1lgxn_1`,spinner:`_spinner_1lgxn_9`,spin:`_spin_1lgxn_9`};function _n({label:e=`Refreshing`}){return(0,X.jsxs)(`span`,{className:gn.root,role:`status`,"aria-live":`polite`,children:[(0,X.jsx)(`span`,{className:gn.spinner,"aria-hidden":`true`}),(0,X.jsx)(`span`,{className:`visually-hidden`,children:e})]})}var Q={grid:`_grid_x7p2y_1`,card:`_card_x7p2y_10`,title:`_title_x7p2y_19`,stat:`_stat_x7p2y_25`,caption:`_caption_x7p2y_31`,line:`_line_x7p2y_37`,row:`_row_x7p2y_42`,cell:`_cell_x7p2y_51`};function vn({width:e=`100%`}){return(0,X.jsx)(`div`,{className:`skeleton ${Q.line}`,style:{width:e}})}function yn(){return(0,X.jsxs)(`div`,{className:Q.card,children:[(0,X.jsx)(`div`,{className:`skeleton ${Q.title}`}),(0,X.jsx)(`div`,{className:`skeleton ${Q.stat}`}),(0,X.jsx)(`div`,{className:`skeleton ${Q.caption}`})]})}function bn(){return(0,X.jsxs)(`div`,{className:Q.row,children:[(0,X.jsx)(`div`,{className:`skeleton ${Q.cell}`,style:{width:`18%`}}),(0,X.jsx)(`div`,{className:`skeleton ${Q.cell}`,style:{width:`38%`}}),(0,X.jsx)(`div`,{className:`skeleton ${Q.cell}`,style:{width:`14%`}}),(0,X.jsx)(`div`,{className:`skeleton ${Q.cell}`,style:{width:`10%`}})]})}function xn({count:e=4}){return(0,X.jsx)(`div`,{className:Q.grid,children:Array.from({length:e},(e,t)=>(0,X.jsx)(yn,{},t))})}var Sn={skipLink:`_skipLink_1uply_1`};function Cn(){return(0,X.jsx)(`button`,{type:`button`,className:Sn.skipLink,onClick:()=>{let e=document.getElementById(`main-content`);e?.focus(),e?.scrollIntoView({block:`start`})},children:`Skip to content`})}var $={wrap:`_wrap_14qw9_1`,card:`_card_14qw9_10`,badge:`_badge_14qw9_19`,heading:`_heading_14qw9_24`,body:`_body_14qw9_31`,code:`_code_14qw9_38`,example:`_example_14qw9_49`,form:`_form_14qw9_56`,label:`_label_14qw9_63`,input:`_input_14qw9_68`,error:`_error_14qw9_82`,submit:`_submit_14qw9_87`,optional:`_optional_14qw9_107`},wn=/^[a-f0-9]{64}$/;function Tn({onAuthenticated:e,heading:t=`Connect to ashlr serve`,command:n=`ashlr serve`,subject:r=`This dashboard`,mutationField:a=!1}){let[o,s]=(0,i.useState)(``),[c,l]=(0,i.useState)(``),[u,d]=(0,i.useState)(null),[ee,te]=(0,i.useState)(!1),f=(0,i.useRef)(null),p=(0,i.useId)(),m=(0,i.useId)(),h=(0,i.useId)(),ne=(0,i.useRef)(e);ne.current=e,(0,i.useEffect)(()=>{let e=!1;return st().then(t=>{t&&!e&&ne.current?.()}),()=>{e=!0}},[]);async function g(t){t.preventDefault(),d(null);let n=c.trim();if(a&&n&&!wn.test(n)){d(`That does not look like a mutation token — expected 64 hex characters (leave it empty to unlock later).`);return}te(!0);try{await et(o),s(``),a&&n&&z(n),l(``),e?.()}catch(e){d(e instanceof Error?e.message:`Could not establish a session.`),f.current?.select()}finally{te(!1)}}return(0,X.jsx)(`div`,{className:$.wrap,children:(0,X.jsxs)(`div`,{className:$.card,children:[(0,X.jsx)(`div`,{className:$.badge,"aria-hidden":`true`,children:`⛺`}),(0,X.jsx)(`h1`,{className:$.heading,children:t}),(0,X.jsxs)(`p`,{className:$.body,children:[r,` only ever talks to the ashlr server running on this machine (`,(0,X.jsx)(`code`,{children:`127.0.0.1`}),`, never a remote host). To read data, paste the`,` `,(0,X.jsx)(`strong`,{children:`read token`}),` printed in the terminal where you ran:`]}),(0,X.jsx)(`pre`,{className:$.code,children:n}),(0,X.jsxs)(`p`,{className:$.body,children:[`It looks like a 64-character hex string, printed once at startup — something like`,` `,(0,X.jsx)(`code`,{className:$.example,children:`a1b2c3…`}),`. This dialog never stores it: it is exchanged once for a short-lived session cookie, then discarded.`]}),(0,X.jsxs)(`form`,{onSubmit:g,className:$.form,noValidate:!0,children:[(0,X.jsx)(`label`,{htmlFor:p,className:$.label,children:`Read token`}),(0,X.jsx)(`input`,{ref:f,id:p,name:`read-token`,type:`password`,autoComplete:`off`,autoCorrect:`off`,spellCheck:!1,inputMode:`text`,className:$.input,value:o,onChange:e=>s(e.target.value),"aria-invalid":u?!0:void 0,"aria-describedby":u?h:void 0,placeholder:`64 hex characters`,autoFocus:!0}),a?(0,X.jsxs)(X.Fragment,{children:[(0,X.jsxs)(`label`,{htmlFor:m,className:$.label,children:[`Mutation token `,(0,X.jsx)(`span`,{className:$.optional,children:`optional — paste it now to enable sending`})]}),(0,X.jsx)(`input`,{id:m,name:`mutation-token`,type:`password`,autoComplete:`off`,autoCorrect:`off`,spellCheck:!1,inputMode:`text`,className:$.input,value:c,onChange:e=>l(e.target.value),placeholder:`64 hex characters, printed under “Dispatch enabled”`})]}):null,u?(0,X.jsx)(`p`,{id:h,className:$.error,role:`alert`,children:u}):null,(0,X.jsx)(`button`,{type:`submit`,className:$.submit,disabled:ee||o.trim().length===0,children:ee?`Connecting…`:`Connect`})]})]})})}export{Pe as $,L as A,ze as B,kt as C,st as D,Tt as E,rt as F,Oe as G,He as H,I,Ae as J,De as K,dt as L,R as M,lt as N,B as O,at as P,je as Q,Ke as R,Qt as S,Zt as T,Be as U,Ve as V,ge as W,Te as X,Ce as Y,Fe as Z,rn as _,bn as a,ye as at,tn as b,dn as c,_ as ct,hn as d,Me as et,un as f,nn as g,on as h,vn as i,Ee as it,ct as j,tt as k,fn as l,oe as lt,cn as m,Cn as n,we as nt,_n as o,g as ot,ln as p,y as q,xn as r,ke as rt,Z as s,le as st,Tn as t,Ne as tt,pn as u,$t as v,Xt as w,an as x,en as y,Ue as z};
2
+ //# sourceMappingURL=global-F2K14nNo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-F2K14nNo.js","names":[],"sources":["../../../../../../src/web-ui/data/cache.ts","../../../../../../src/core/verse/context-math.ts","../../../../../../src/web-ui/routes/verse/chat/composer-state.ts","../../../../../../src/web-ui/data/auth-store.ts","../../../../../../src/web-ui/data/sse.ts","../../../../../../src/web-ui/data/theme-store.ts","../../../../../../src/web-ui/data/appearance-store.ts","../../../../../../src/web-ui/data/hooks.ts","../../../../../../src/web-ui/components/primitives/StatusBadge.module.css","../../../../../../src/web-ui/components/primitives/StatusBadge.tsx","../../../../../../src/web-ui/data/client.ts","../../../../../../src/web-ui/components/primitives/RefreshIndicator.module.css","../../../../../../src/web-ui/components/primitives/RefreshIndicator.tsx","../../../../../../src/web-ui/components/primitives/Skeleton.module.css","../../../../../../src/web-ui/components/primitives/Skeleton.tsx","../../../../../../src/web-ui/app/SkipToContent.module.css","../../../../../../src/web-ui/app/SkipToContent.tsx","../../../../../../src/web-ui/components/auth/SessionGate.module.css","../../../../../../src/web-ui/components/auth/SessionGate.tsx"],"sourcesContent":["/**\n * data/cache.ts — the resource cache every view reads through. This is what\n * makes tab switches instant instead of blanking to \"Loading…\": a resource\n * fetched once stays in the cache and renders immediately (possibly stale)\n * on the next mount, with a background refresh kicked off silently behind\n * it. No framework dependency — a plain keyed store consumed via\n * useSyncExternalStore (see hooks.ts), matching the pattern already used\n * for auth-store.ts.\n *\n * Design choices, spelled out because a future agent will extend this:\n * - In-flight de-dupe: two components requesting the same key at once\n * share one fetch, not two.\n * - Never clears `data` on refetch. `status` distinguishes 'loading'\n * (no data yet — show a skeleton) from 'refreshing' (stale data still\n * shown — show a subtle indicator) from 'success'/'error'.\n * - `invalidate(key)` is how the SSE layer (sse.ts) tells the cache \"the\n * server just told us this changed\" without either module importing\n * React.\n * - useSyncExternalStore requires getSnapshot() to return a NEW object\n * reference whenever the observable state actually changes (it detects\n * updates via Object.is, not deep equality) — mutating one long-lived\n * entry object in place and handing back the same reference would fire\n * subscribers that never actually re-render. Every state transition\n * below therefore replaces `bookkeeping.snapshot` wholesale instead of\n * patching fields on it.\n *\n * ---------------------------------------------------------------------------\n * V2.1 — the request burst, and the three defects that lived in this file\n * ---------------------------------------------------------------------------\n *\n * Mounting the Usage section fires SEVEN reads at once (accounts, control,\n * bootstrap, usage-series, local-models, /api/control, /api/usage), several\n * of which spawn short-lived probe processes server-side. That burst starved\n * the 2s local-runtime probe timeout and made a healthy Ollama report as\n * unreachable roughly one burst in ten. The probe timeouts were hardened\n * server-side; the burst itself was still this file's problem. Three changes:\n *\n * 1. CONCURRENCY GATE. Every fetch this cache issues passes through a\n * process-wide gate capped at QUERY_CONCURRENCY_LIMIT. The same seven\n * reads still happen, but at most four are outstanding at once, so the\n * server is never asked to run seven probe-spawning handlers in\n * parallel. Nothing is dropped, nothing is delayed by a timer — a\n * queued task starts the microtask a slot frees. The gate is what makes\n * the probe timeout survivable, and it costs the caller nothing: the\n * browser already caps at ~6 sockets per origin, so this is a cap the\n * requests were going to hit anyway, applied where we can order it.\n *\n * 2. FRESHNESS. `ensureQuery(key, fetcher, maxAgeMs)` is the MOUNT path: it\n * skips the request entirely when the cached value is younger than\n * `maxAgeMs`. Re-entering a section you were just in is now zero\n * requests instead of seven. `runQuery` keeps its old always-fetch\n * semantics because the SSE layer and the prefetchers depend on it.\n *\n * 3. REFRESH THAT ACTUALLY RE-READS. `runQuery`'s in-flight de-dupe is\n * right for two components mounting at once and WRONG for a person\n * pressing Refresh: a click landing while a read is in flight joined\n * the running promise and returned data fetched before the click — a\n * silent no-op, and with local-models taking seconds under load the\n * window was real. `refetchQuery` fixes it in two steps, because the\n * same hook backs both a person's button and a 3s poll loop:\n * - by default it joins an in-flight read ONLY while that read is\n * younger than REFETCH_JOIN_WINDOW_MS. A poll tick landing on a\n * read that started milliseconds ago still coalesces (which is the\n * documented contract UniverseView's poll relies on); a click\n * landing on a read that started seconds ago issues a new request.\n * - with `force` it never joins at all. That is the strict guarantee\n * for an explicit user-initiated refresh (hooks.ts `useRefresh`).\n * Either way a newer request SUPERSEDES an older one by sequence\n * number, so the last read ISSUED is the one whose result is kept even\n * if an earlier one lands later.\n *\n * And the latent one: `evictAll()` used to call `store.clear()`, which\n * dropped the Bookkeeping objects that mounted components had registered\n * their `useSyncExternalStore` listeners on. Those components stayed\n * subscribed to an orphan Set that nothing would ever notify again — they\n * would never re-render for the rest of their life. Evict now RESETS an\n * entry that still has subscribers (and deletes only the unobserved ones),\n * so a subscriber always survives an evict.\n */\n\nexport type QueryStatus = 'idle' | 'loading' | 'refreshing' | 'success' | 'error';\n\nexport interface QueryEntry<T> {\n data: T | undefined;\n error: Error | undefined;\n status: QueryStatus;\n updatedAt: number | null;\n}\n\ninterface Bookkeeping<T = unknown> {\n snapshot: QueryEntry<T>;\n inFlight: Promise<void> | null;\n fetcher: (() => Promise<T>) | null;\n subscribers: Set<() => void>;\n /** Sequence number of the newest request ISSUED for this key. */\n issued: number;\n /** `Date.now()` when that newest request was issued. */\n issuedAt: number;\n /** Highest sequence number whose result has been written to `snapshot`. */\n applied: number;\n}\n\nconst IDLE_SNAPSHOT: QueryEntry<unknown> = { data: undefined, error: undefined, status: 'idle', updatedAt: null };\n\nconst store = new Map<string, Bookkeeping>();\n\n// ---------------------------------------------------------------------------\n// Concurrency gate\n// ---------------------------------------------------------------------------\n\n/**\n * How many cache-issued fetches may be outstanding at once, process-wide.\n *\n * Four, not one: the point is to stop a seven-way burst from spawning seven\n * concurrent server-side probes, not to serialize the app. Four keeps a\n * section's first paint fast while leaving the server two of its six sockets\n * for the SSE stream and any non-cache request.\n */\nexport const QUERY_CONCURRENCY_LIMIT = 4;\n\nlet activeFetches = 0;\nconst gateQueue: (() => void)[] = [];\n/** Peak concurrency observed since the gate was last reset. */\nlet peakConcurrency = 0;\n/**\n * Bumped by `resetQueryGate`. A fetch issued under an older generation no\n * longer owns a slot, so its eventual settlement must not decrement the new\n * generation's count — otherwise a request that never settles (a hung\n * socket, or a test stubbing fetch with a promise that never resolves) would\n * permanently consume capacity and starve every later read.\n */\nlet gateGeneration = 0;\n\nfunction releaseSlot(): void {\n activeFetches -= 1;\n const next = gateQueue.shift();\n if (next) next();\n}\n\n/**\n * Run `task` when a slot is free. Purely promise-driven — no timers, so this\n * is safe under fake timers and adds no latency beyond one microtask.\n */\nfunction schedule<T>(task: () => Promise<T>): Promise<T> {\n const start = (): Promise<T> => {\n const generation = gateGeneration;\n activeFetches += 1;\n if (activeFetches > peakConcurrency) peakConcurrency = activeFetches;\n const done = (): void => {\n if (generation === gateGeneration) releaseSlot();\n };\n let running: Promise<T>;\n try {\n running = task();\n } catch (err) {\n // A fetcher that throws synchronously must still free its slot.\n done();\n return Promise.reject(err instanceof Error ? err : new Error(String(err)));\n }\n return running.then(\n (value) => {\n done();\n return value;\n },\n (err: unknown) => {\n done();\n throw err;\n },\n );\n };\n\n if (activeFetches < QUERY_CONCURRENCY_LIMIT) return start();\n return new Promise<T>((resolve, reject) => {\n gateQueue.push(() => {\n start().then(resolve, reject);\n });\n });\n}\n\n/**\n * Abandon the gate's accounting: forget how many fetches are outstanding and\n * let everything queued run now. Called from `evictAll()`, which is both the\n * logout path and every test's `beforeEach` — a read that outlives the state\n * it was issued for has no claim on the next one's capacity.\n *\n * Nothing is cancelled. Requests already in flight still settle and still\n * write their result unless the key was evicted underneath them.\n */\nexport function resetQueryGate(): void {\n gateGeneration += 1;\n activeFetches = 0;\n peakConcurrency = 0;\n for (const start of gateQueue.splice(0)) start();\n}\n\n/** Observability for the tests: what the gate is doing right now. */\nexport function queryGateStats(): { active: number; queued: number; peak: number } {\n return { active: activeFetches, queued: gateQueue.length, peak: peakConcurrency };\n}\n\n// ---------------------------------------------------------------------------\n// Entries\n// ---------------------------------------------------------------------------\n\nfunction entryFor<T>(key: string): Bookkeeping<T> {\n let e = store.get(key) as Bookkeeping<T> | undefined;\n if (!e) {\n e = {\n snapshot: IDLE_SNAPSHOT as QueryEntry<T>,\n inFlight: null,\n fetcher: null,\n subscribers: new Set(),\n issued: 0,\n issuedAt: 0,\n applied: 0,\n };\n store.set(key, e);\n }\n return e;\n}\n\nfunction setSnapshot<T>(e: Bookkeeping<T>, patch: Partial<QueryEntry<T>>): void {\n e.snapshot = { ...e.snapshot, ...patch };\n}\n\nfunction notify(key: string): void {\n const e = store.get(key);\n if (!e) return;\n for (const s of e.subscribers) s();\n}\n\n/**\n * Structural equality for API payloads, used to decide whether a refresh\n * actually changed anything.\n *\n * A Refresh that re-reads seven routes and finds six of them byte-identical\n * should re-render nothing for those six. Keeping the PREVIOUS `data`\n * reference when the new body is deep-equal does exactly that: the snapshot\n * object still changes (status and updatedAt did), so useSyncExternalStore\n * still fires, but every `useMemo`/`memo` keyed on `data` downstream skips —\n * and the Usage section's models are all keyed on `data`.\n *\n * Deliberately structural rather than JSON.stringify: key order is not\n * significant in these payloads, and stringify would also throw on the\n * (never-seen, but cheap to survive) cyclic body.\n */\nfunction sameData(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null || typeof a !== 'object') return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((item, i) => sameData(item, b[i]));\n }\n const left = a as Record<string, unknown>;\n const right = b as Record<string, unknown>;\n const leftKeys = Object.keys(left);\n if (leftKeys.length !== Object.keys(right).length) return false;\n return leftKeys.every((k) => Object.prototype.hasOwnProperty.call(right, k) && sameData(left[k], right[k]));\n}\n\nexport function subscribeQuery(key: string, listener: () => void): () => void {\n const e = entryFor(key);\n e.subscribers.add(listener);\n return () => {\n e.subscribers.delete(listener);\n };\n}\n\nexport function getQuerySnapshot<T>(key: string): QueryEntry<T> {\n return entryFor<T>(key).snapshot;\n}\n\n/**\n * How long an in-flight read is accepted as already satisfying a refetch.\n *\n * A poll tick (UniverseView refreshes every 3–15s and again on\n * visibilitychange) that lands on a read issued milliseconds ago should\n * coalesce with it — that is the behavior its own comment and tests pin\n * down. A person's Refresh landing on a read that has been running for\n * seconds should not: they are asking about the world as of the click.\n */\nexport const REFETCH_JOIN_WINDOW_MS = 250;\n\nexport interface QueryRunOptions {\n /**\n * Skip the request when the cached value is younger than this many ms.\n * Undefined (the default) always fetches.\n */\n maxAgeMs?: number;\n /**\n * Join an in-flight read only while it is younger than this. Undefined\n * joins any in-flight read (the original de-dupe).\n */\n joinWindowMs?: number;\n /**\n * User-initiated. Never joins an in-flight read and never honours\n * `maxAgeMs` — pressing Refresh must produce a request that was issued\n * after the click.\n */\n force?: boolean;\n}\n\nfunction isFresh(e: Bookkeeping, maxAgeMs: number): boolean {\n if (e.snapshot.updatedAt === null) return false;\n if (e.snapshot.status === 'error') return false;\n return Date.now() - e.snapshot.updatedAt < maxAgeMs;\n}\n\nfunction mayJoin(e: Bookkeeping, options: QueryRunOptions): boolean {\n if (e.inFlight === null) return false;\n if (options.joinWindowMs === undefined) return true;\n return Date.now() - e.issuedAt < options.joinWindowMs;\n}\n\nfunction execute<T>(key: string, fetcher: () => Promise<T>, options: QueryRunOptions): Promise<void> {\n const e = entryFor<T>(key);\n e.fetcher = fetcher;\n\n if (!options.force) {\n // Two components mounting at once share one request.\n if (mayJoin(e, options)) return e.inFlight!;\n // Already fresh enough for this caller: no request at all.\n if (options.maxAgeMs !== undefined && isFresh(e, options.maxAgeMs)) return Promise.resolve();\n }\n\n const seq = (e.issued += 1);\n e.issuedAt = Date.now();\n setSnapshot(e, { status: e.snapshot.data !== undefined ? 'refreshing' : 'loading' });\n notify(key);\n\n const run = schedule(fetcher)\n .then((data) => {\n // A superseded request (a forced refresh was issued after this one, or\n // the key was evicted) must never overwrite a newer answer.\n if (seq < e.applied) return;\n e.applied = seq;\n const unchanged = e.snapshot.data !== undefined && sameData(e.snapshot.data, data);\n setSnapshot(e, {\n data: unchanged ? e.snapshot.data : data,\n error: undefined,\n status: 'success',\n updatedAt: Date.now(),\n });\n })\n .catch((err: unknown) => {\n if (seq < e.applied) return;\n e.applied = seq;\n setSnapshot(e, { error: err instanceof Error ? err : new Error(String(err)), status: 'error' });\n })\n .finally(() => {\n // Only the newest request owns `inFlight`; an older, superseded one\n // finishing must not clear the newer one's slot.\n if (e.issued === seq) e.inFlight = null;\n notify(key);\n });\n\n e.inFlight = run;\n return run;\n}\n\n/**\n * Kick off (or join) a fetch for `key`. Safe to call redundantly — repeated\n * calls with an in-flight fetch are no-ops. Registers `fetcher` on the\n * entry so invalidate() can re-run it later without the caller re-supplying\n * it (e.g. from the SSE layer, which doesn't have per-view fetch closures).\n */\nexport function runQuery<T>(key: string, fetcher: () => Promise<T>): Promise<void> {\n return execute(key, fetcher, {});\n}\n\n/**\n * The MOUNT path (hooks.ts `useQuery`). Identical to `runQuery` except that a\n * value fetched less than `maxAgeMs` ago is accepted as-is and no request is\n * made. This is what turns \"switch to Usage and back\" from fourteen requests\n * into seven.\n */\nexport function ensureQuery<T>(key: string, fetcher: () => Promise<T>, maxAgeMs: number): Promise<void> {\n return execute(key, fetcher, { maxAgeMs });\n}\n\n/**\n * The REFRESH path (hooks.ts `useRefetch` / `useRefresh`).\n *\n * Default (`force: false`): joins an in-flight read only while that read is\n * younger than REFETCH_JOIN_WINDOW_MS, so a burst of poll ticks still\n * collapses into one request while a click on a long-running read gets a\n * real re-read.\n *\n * `force: true`: never joins. Always issues a request made after the call,\n * and wins the race against anything already running regardless of which\n * lands first. This is the guarantee an explicit user-initiated refresh\n * needs.\n */\nexport function refetchQuery<T>(key: string, fetcher: () => Promise<T>, force = false): Promise<void> {\n return execute(key, fetcher, force ? { force: true } : { joinWindowMs: REFETCH_JOIN_WINDOW_MS });\n}\n\n/** Re-run the last fetcher registered for `key`, if any (used by sse.ts). */\nexport function invalidate(key: string): void {\n const e = store.get(key);\n if (!e || !e.fetcher) return;\n void runQuery(key, e.fetcher);\n}\n\n/** Re-run the fetcher for every cached key starting with `prefix`. For\n * parameterized resources (per-filter inbox list queries, per-id inbox\n * detail queries) the exact live key isn't known ahead of time — used by\n * sse.ts and data/mutations.ts so those views live-update too. */\nexport function invalidatePrefix(prefix: string): void {\n for (const key of store.keys()) {\n if (key.startsWith(prefix)) invalidate(key);\n }\n}\n\n/**\n * Reset one entry to idle WITHOUT dropping its subscriber set, and supersede\n * anything still in flight for it so a late result cannot repopulate the key\n * after it was deliberately cleared.\n */\nfunction resetEntry(key: string, e: Bookkeeping): void {\n e.snapshot = IDLE_SNAPSHOT as QueryEntry<unknown>;\n e.fetcher = null;\n e.inFlight = null;\n e.issued += 1;\n e.applied = e.issued;\n notify(key);\n}\n\n/** Drop a key entirely (rare — e.g. on logout, so stale data never leaks\n * into the next session's first paint). An entry that still has mounted\n * subscribers is RESET rather than deleted: deleting it would orphan the\n * listener set those components registered on, leaving them subscribed to\n * something nothing can notify. */\nexport function evict(key: string): void {\n const e = store.get(key);\n if (!e) return;\n if (e.subscribers.size === 0) {\n store.delete(key);\n return;\n }\n resetEntry(key, e);\n}\n\nexport function evictAll(): void {\n resetQueryGate();\n for (const [key, e] of [...store]) {\n if (e.subscribers.size === 0) {\n store.delete(key);\n continue;\n }\n resetEntry(key, e);\n }\n}\n","/**\n * Ashlr Verse — context arithmetic shared by the server and the browser.\n *\n * PURE and BROWSER-SAFE: no node imports, no I/O. The seat catalog, the\n * session engine, the adapters and the UI all compute thresholds through this\n * one module, so \"compacts at 367k\" can never mean two different numbers in\n * two places. docs/VERSE-CONTEXT.md is the prose authority for every constant.\n *\n * Where each formula comes from (all read from the CLIs themselves, 2026-09-23):\n *\n * Claude Code (2.1.257 and 2.1.280 binaries): the auto-compaction window is\n * `min(--autocompact | autoCompactWindow | auto, model window)`. From it the\n * CLI reserves `min(max output, 20_000)` for the reply and compacts a\n * further 13_000 below that. On a 1M model at `auto` that is 967_000 — the\n * `pre_tokens` observed on real compactions (967_391).\n * Codex (0.136 – 0.155 rollouts): the window the CLI measures against is\n * `context_window × effective_context_window_percent / 100` = 258_400 for\n * every 272k model; auto-compaction triggers at 90% of the RAW window\n * (244_800). The largest uncompacted call across 62,104 was 243_204.\n * Grok (0.2.118 catalog): compacts at `auto_compact_threshold_percent` (80)\n * of `context_window` (500_000) = 400_000.\n * Local seats run the Claude Code binary against Ollama, so they compact by\n * the Claude formula over the window Verse passes in\n * `CLAUDE_CODE_MAX_CONTEXT_TOKENS`.\n */\n\nimport type {\n VerseContextBudget,\n VerseContextMode,\n VerseEngine,\n VerseFitVerdict,\n VerseModelOption,\n VerseSession,\n VerseUsage,\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Standard-mode compaction window for 1M-context Claude models.\n *\n * WHY 400k and not the CLI's ~967k: every turn re-sends the whole prefix, so a\n * session held near 900k costs ~2.3× per call what one held under 400k does,\n * and long-context recall degrades with length on every published benchmark\n * (docs/VERSE-CONTEXT.md §2). 400k keeps a large working set — far above the\n * 150k Anthropic's own API compacts at by default — while leaving the full\n * window one click away in `expansive` mode. Grok's native budget is also\n * 400k, so the two frontier engines share one standard ceiling.\n */\nexport const CLAUDE_STANDARD_AUTOCOMPACT_WINDOW = 400_000;\n\n/** Claude Code reserves min(max output, this) of the window for the reply. */\nexport const CLAUDE_OUTPUT_RESERVE_CAP = 20_000;\n/** …and compacts this many tokens below the reserved window. */\nexport const CLAUDE_COMPACT_BUFFER = 13_000;\n/** Claude Code clamps `--autocompact` to [100k, 1M]. */\nexport const CLAUDE_AUTOCOMPACT_MIN = 100_000;\nexport const CLAUDE_AUTOCOMPACT_MAX = 1_000_000;\n\n/** Codex: default `effective_context_window_percent` in every catalog entry. */\nexport const CODEX_EFFECTIVE_WINDOW_PERCENT = 95;\n/** Codex: auto-compaction fires at this fraction of the RAW window. */\nexport const CODEX_AUTO_COMPACT_FRACTION = 0.9;\n\n/** Grok: `auto_compact_threshold_percent` in the 0.2.118 catalog. */\nexport const GROK_AUTO_COMPACT_PERCENT = 80;\n\n/**\n * How long a provider keeps a conversation's prompt cache warm while idle.\n * Claude Code on a subscription: 1h (5 min without it). Past this, the next\n * turn re-reads the whole context at full price — the single most expensive\n * thing an idle long session can do.\n */\nexport const CACHE_IDLE_TTL_MS = 60 * 60 * 1000;\n\n/**\n * Fixed prompt overhead (system prompt + tool definitions) a FRESH session\n * already occupies before any work, per engine.\n *\n * local 15k — measured: two real turns through Claude Code 2.1.280 against\n * Ollama with `--exclude-dynamic-system-prompt-sections`\n * reported 14,695 and 14,942 context tokens for a one-word reply.\n * claude 25k — the same CLI without that flag measured 17.8k–23.3k on local\n * seats; rounded up. An ESTIMATE, never shown as a measurement.\n * codex 15k / grok 20k — estimates; neither CLI reports its base prompt.\n *\n * Only used by the fit verdicts, which are labelled as estimates in the UI.\n */\nexport const SESSION_BASE_OVERHEAD_BY_ENGINE: Readonly<Record<VerseEngine, number>> = {\n claude: 25_000,\n codex: 15_000,\n grok: 20_000,\n local: 15_000,\n};\n\n/** Fallback overhead when the engine is unknown (the largest estimate). */\nexport const SESSION_BASE_OVERHEAD_TOKENS = 25_000;\n\nexport function sessionOverheadTokens(engine: VerseEngine | null | undefined): number {\n return engine ? SESSION_BASE_OVERHEAD_BY_ENGINE[engine] ?? SESSION_BASE_OVERHEAD_TOKENS : SESSION_BASE_OVERHEAD_TOKENS;\n}\n\n/**\n * Smallest local window worth offering as a seat. Local turns run Claude Code\n * with `CLAUDE_CODE_MAX_CONTEXT_TOKENS=<window>`, which compacts at\n * `window − 20k − 13k`; below this the compaction point leaves under 8k of\n * working room above the ~15k base prompt, so every turn would compact.\n */\nexport const LOCAL_MIN_WORKING_TOKENS = 8_000;\nexport const LOCAL_MIN_USABLE_WINDOW =\n SESSION_BASE_OVERHEAD_BY_ENGINE.local + LOCAL_MIN_WORKING_TOKENS + CLAUDE_OUTPUT_RESERVE_CAP + CLAUDE_COMPACT_BUFFER;\n\n/** True when a local window leaves real working room under Claude Code's compaction rule. */\nexport function localWindowUsable(window: number | null | undefined): boolean {\n return typeof window === 'number' && Number.isFinite(window) && window >= LOCAL_MIN_USABLE_WINDOW;\n}\n\n/** chars → tokens estimator used everywhere a count is estimated rather than measured. */\nexport const CHARS_PER_TOKEN = 4;\n\n// ---------------------------------------------------------------------------\n// Per-engine formulas\n// ---------------------------------------------------------------------------\n\nfunction positiveInt(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : null;\n}\n\n/**\n * Claude Code's auto-compaction point.\n *\n * @param modelWindow the model's context window (1_000_000, 200_000, or\n * the local num_ctx passed in CLAUDE_CODE_MAX_CONTEXT_TOKENS)\n * @param maxOutputTokens the model's default max output; null → assume the cap\n * @param autocompactWindow the `--autocompact` value in force; null → `auto`\n * (the full model window)\n */\nexport function claudeAutoCompactAt(\n modelWindow: number,\n maxOutputTokens: number | null,\n autocompactWindow: number | null = null,\n): number {\n const window = Math.min(modelWindow, positiveInt(autocompactWindow) ?? modelWindow);\n const reserve = Math.min(positiveInt(maxOutputTokens) ?? CLAUDE_OUTPUT_RESERVE_CAP, CLAUDE_OUTPUT_RESERVE_CAP);\n return Math.max(0, window - reserve - CLAUDE_COMPACT_BUFFER);\n}\n\n/** Codex: the window the CLI measures occupancy against. */\nexport function codexEffectiveWindow(rawWindow: number, percent: number | null = null): number {\n const pct = positiveInt(percent) !== null && (percent as number) <= 100 ? (percent as number) : CODEX_EFFECTIVE_WINDOW_PERCENT;\n return Math.floor((rawWindow * pct) / 100);\n}\n\n/** Codex: where auto-compaction fires for a given RAW window. */\nexport function codexAutoCompactAt(rawWindow: number): number {\n return Math.floor(rawWindow * CODEX_AUTO_COMPACT_FRACTION);\n}\n\n/** Grok: where auto-compaction fires. */\nexport function grokAutoCompactAt(window: number, percent: number | null = null): number {\n const pct = positiveInt(percent) !== null && (percent as number) <= 100 ? (percent as number) : GROK_AUTO_COMPACT_PERCENT;\n return Math.floor((window * pct) / 100);\n}\n\n// ---------------------------------------------------------------------------\n// Model ids\n// ---------------------------------------------------------------------------\n\n/**\n * Ids Verse once shipped that the CLI does NOT resolve to the model the label\n * promised. `claude-opus-5.5` is parsed by Claude Code as a fuzzy match for\n * `claude-opus-5` (its normaliser rejects the `.5` suffix and falls back to\n * `includes('claude-opus-5')`), so every \"Opus 5.5\" turn actually ran Opus 5.\n * Old session records keep their stored id; adapters send the canonical one.\n */\nexport const VERSE_MODEL_ID_ALIASES: Readonly<Record<string, string>> = {\n 'claude-opus-5.5': 'claude-opus-5-5',\n};\n\nexport function canonicalModelId(id: string): string {\n return VERSE_MODEL_ID_ALIASES[id] ?? id;\n}\n\n// ---------------------------------------------------------------------------\n// Budgets and occupancy\n// ---------------------------------------------------------------------------\n\n/** The budget a model runs with in a mode, or null when that mode does not exist for it. */\nexport function budgetFor(option: VerseModelOption | null | undefined, mode: VerseContextMode): VerseContextBudget | null {\n if (!option) return null;\n if (mode === 'expansive') return option.expansive ?? null;\n const window = positiveInt(option.contextWindow);\n if (window === null) return null;\n return { contextWindow: window, autoCompactAt: positiveInt(option.autoCompactAt ?? null) };\n}\n\n/** True when this model has a genuine expansive budget larger than its standard one. */\nexport function hasExpansiveMode(option: VerseModelOption | null | undefined): boolean {\n const standard = budgetFor(option, 'standard');\n const expansive = budgetFor(option, 'expansive');\n if (!expansive) return false;\n if (!standard) return true;\n return (expansive.autoCompactAt ?? expansive.contextWindow) > (standard.autoCompactAt ?? standard.contextWindow);\n}\n\n/**\n * The `--autocompact` value Verse passes to Claude Code for a model in a mode,\n * or null for \"let the CLI use `auto`\". Only 1M-native models are capped in\n * standard mode; a 200k model already compacts near 167k natively.\n */\nexport function claudeAutocompactFlag(option: VerseModelOption | null | undefined, mode: VerseContextMode): number | null {\n if (mode === 'expansive') return null;\n const window = positiveInt(option?.contextWindow ?? null);\n return window !== null && window > CLAUDE_STANDARD_AUTOCOMPACT_WINDOW && hasExpansiveMode(option)\n ? CLAUDE_STANDARD_AUTOCOMPACT_WINDOW\n : null;\n}\n\n/**\n * Recompute the compaction point when the CLI reports a window at runtime\n * that differs from the catalog budget (Claude Code clamps a 1M model to 200k\n * when long-context credit runs out; grok can upgrade a window by header).\n *\n * claude/local — fixed-buffer formula over min(runtime window, --autocompact).\n * codex — the runtime figure is the EFFECTIVE window; the compaction\n * point stays 90% of the raw window it implies.\n * grok/other — keep the budget's compaction RATIO.\n */\nexport function reconcileAutoCompactAt(input: {\n engine: VerseEngine;\n runtimeWindow: number;\n budget: VerseContextBudget | null;\n /** The `--autocompact` value in force for claude/local; null = auto. */\n autocompactWindow?: number | null;\n maxOutputTokens?: number | null;\n}): number | null {\n const runtime = positiveInt(input.runtimeWindow);\n if (runtime === null) return input.budget?.autoCompactAt ?? null;\n if (input.budget && input.budget.contextWindow === runtime) return input.budget.autoCompactAt;\n switch (input.engine) {\n case 'claude':\n case 'local':\n return claudeAutoCompactAt(runtime, input.maxOutputTokens ?? null, input.autocompactWindow ?? null);\n case 'codex':\n return codexAutoCompactAt(Math.round((runtime * 100) / CODEX_EFFECTIVE_WINDOW_PERCENT));\n default: {\n const b = input.budget;\n if (!b || b.autoCompactAt === null || b.contextWindow <= 0) return null;\n return Math.floor(runtime * (b.autoCompactAt / b.contextWindow));\n }\n }\n}\n\nexport type VerseOccupancyTone = 'unknown' | 'ok' | 'warn' | 'danger' | 'over';\n\nexport interface VerseOccupancy {\n tokens: number;\n window: number | null;\n autoCompactAt: number | null;\n /** tokens / window, unclamped; null when the window is unknown. */\n ofWindow: number | null;\n /** tokens / autoCompactAt, unclamped; null when the compaction point is unknown. */\n ofCompaction: number | null;\n /** Tokens left before auto-compaction (≥ 0), null when unknown. */\n untilCompaction: number | null;\n tone: VerseOccupancyTone;\n /** False when `tokens` is an upper bound (codex before its rollout is readable). */\n exact: boolean;\n}\n\n/** Warn at 80% of the compaction point, danger from 95% — BEFORE the CLI compacts, not after. */\nexport const OCCUPANCY_WARN = 0.8;\nexport const OCCUPANCY_DANGER = 0.95;\n\nexport function occupancy(usage: Pick<VerseUsage, 'contextTokens' | 'contextWindow' | 'autoCompactAt' | 'contextTokensExact'>): VerseOccupancy {\n const tokens = Math.max(0, Math.floor(usage.contextTokens || 0));\n const window = positiveInt(usage.contextWindow);\n const compactAt = positiveInt(usage.autoCompactAt ?? null);\n const ofWindow = window !== null ? tokens / window : null;\n const ofCompaction = compactAt !== null ? tokens / compactAt : null;\n const gauge = ofCompaction ?? ofWindow;\n const exact = usage.contextTokensExact !== false;\n let tone: VerseOccupancyTone = 'unknown';\n if (!exact && gauge !== null && gauge >= 1) {\n // An UPPER BOUND past the compaction point says nothing about the real\n // prompt: codex's turn total sums every model call (median 29× the last\n // one). Claiming \"over the window\" from it would be invented precision.\n tone = 'unknown';\n } else if (window !== null && tokens > window) tone = 'over';\n else if (gauge !== null) tone = gauge >= OCCUPANCY_DANGER ? 'danger' : gauge >= OCCUPANCY_WARN ? 'warn' : 'ok';\n return {\n tokens,\n window,\n autoCompactAt: compactAt,\n ofWindow,\n ofCompaction,\n untilCompaction: compactAt !== null ? Math.max(0, compactAt - tokens) : null,\n tone,\n exact,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Efficiency\n// ---------------------------------------------------------------------------\n\n/**\n * Share of prompt tokens served from cache: cacheRead / (input + cacheRead +\n * cacheCreation). Adapters already report `inputTokens` EXCLUDING cached\n * tokens (codex's turn total is split by its adapter), so the buckets are\n * disjoint. Null when nothing has been read yet.\n */\nexport function cacheHitRatio(usage: Pick<VerseUsage, 'inputTokens' | 'cacheReadTokens' | 'cacheCreationTokens'>): number | null {\n const denominator = usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens;\n return denominator > 0 ? usage.cacheReadTokens / denominator : null;\n}\n\nexport function estimateTokensFromChars(chars: number): number {\n return Math.ceil(Math.max(0, chars) / CHARS_PER_TOKEN);\n}\n\n// ---------------------------------------------------------------------------\n// Advice (pure; the UI decides how to show it, nothing here spends)\n// ---------------------------------------------------------------------------\n\n/**\n * Would this working set fit one context on this model?\n *\n * fits — working set + fixed overhead ≤ 60% of the standard compaction point\n * tight — ≤ 100% of it: it fits, but the session will compact soon\n * expansive — only the expansive budget holds it\n * split — no single context holds it; fan the work out instead\n *\n * Null when the model's budget is unknown (never guessed). Pass the engine's\n * `sessionOverheadTokens(engine)`; the default is the largest estimate.\n */\nexport function fitVerdict(\n workingSetTokens: number,\n option: VerseModelOption | null | undefined,\n overheadTokens: number = SESSION_BASE_OVERHEAD_TOKENS,\n): VerseFitVerdict | null {\n const standard = budgetFor(option, 'standard');\n if (!standard) return null;\n const need = Math.max(0, workingSetTokens) + Math.max(0, overheadTokens);\n const standardCap = standard.autoCompactAt ?? standard.contextWindow;\n if (need <= standardCap * 0.6) return 'fits';\n if (need <= standardCap) return 'tight';\n const expansive = budgetFor(option, 'expansive');\n if (expansive && need <= (expansive.autoCompactAt ?? expansive.contextWindow)) return 'expansive';\n return 'split';\n}\n\nexport type VerseAdviceLevel = 'none' | 'suggest' | 'urge';\n\nexport interface VerseHandoffAdvice {\n level: VerseAdviceLevel;\n /** Plain sentences, most important first. Empty when level is 'none'. */\n reasons: string[];\n}\n\n/** Compactions after which early-turn detail is mostly summaries. */\nexport const HANDOFF_COMPACTION_THRESHOLD = 2;\n/** Below this, an expired cache is cheap to re-read and not worth a handoff. */\nexport const HANDOFF_IDLE_MIN_TOKENS = 100_000;\n\nfunction compactTokens(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;\n if (n >= 1_000) return `${Math.round(n / 1_000)}k`;\n return String(n);\n}\n\n/**\n * Should the operator continue in a fresh session? Pure: `now` and\n * `lastActivityAt` are passed in, so the verdict is reproducible in tests.\n */\nexport function handoffAdvice(input: {\n usage: VerseUsage;\n compactionCount?: number;\n lastActivityAt?: string | null;\n now: number;\n}): VerseHandoffAdvice {\n const occ = occupancy(input.usage);\n const reasons: string[] = [];\n let level: VerseAdviceLevel = 'none';\n const bump = (to: VerseAdviceLevel): void => {\n // (level only ever rises)\n if (to === 'urge' || (to === 'suggest' && level === 'none')) level = to;\n };\n\n if (!occ.exact) {\n // An upper bound cannot justify \"you are near compaction\"; wait for a reading.\n } else if (occ.tone === 'over') {\n bump('urge');\n reasons.push(`Context (${compactTokens(occ.tokens)}) is past the ${compactTokens(occ.window ?? 0)} window.`);\n } else if (occ.ofCompaction !== null && occ.ofCompaction >= OCCUPANCY_WARN) {\n bump('suggest');\n reasons.push(`About ${compactTokens(occ.untilCompaction ?? 0)} tokens left before the CLI auto-compacts.`);\n }\n\n const compactions = input.compactionCount ?? 0;\n if (compactions >= HANDOFF_COMPACTION_THRESHOLD) {\n bump('suggest');\n reasons.push(`Compacted ${compactions} times — early turns now survive only as summaries.`);\n }\n\n if (input.lastActivityAt) {\n const idle = input.now - Date.parse(input.lastActivityAt);\n if (occ.exact && Number.isFinite(idle) && idle >= CACHE_IDLE_TTL_MS && occ.tokens >= HANDOFF_IDLE_MIN_TOKENS) {\n bump('suggest');\n reasons.push(`Idle over an hour: the prompt cache has likely expired, so the next turn re-reads ~${compactTokens(occ.tokens)} tokens at full cost.`);\n }\n }\n return { level, reasons };\n}\n\nexport interface VerseExpansiveAdvice {\n suggest: boolean;\n reason: string | null;\n}\n\n/**\n * Suggest — never switch on — expansive mode. Only for a session in standard\n * mode whose model has a real expansive budget, and only on evidence: it has\n * already compacted repeatedly, or its working set only fits expansive.\n */\nexport function expansiveAdvice(input: {\n session: Pick<VerseSession, 'contextMode' | 'compactionCount'>;\n option: VerseModelOption | null | undefined;\n workingSetTokens?: number | null;\n}): VerseExpansiveAdvice {\n if ((input.session.contextMode ?? 'standard') !== 'standard' || !hasExpansiveMode(input.option)) {\n return { suggest: false, reason: null };\n }\n const compactions = input.session.compactionCount ?? 0;\n if (compactions >= HANDOFF_COMPACTION_THRESHOLD) {\n return { suggest: true, reason: `This session has compacted ${compactions} times; expansive mode keeps more of it in view.` };\n }\n if (typeof input.workingSetTokens === 'number' && fitVerdict(input.workingSetTokens, input.option) === 'expansive') {\n return { suggest: true, reason: `The reachable code (~${compactTokens(input.workingSetTokens)} tokens) only fits the expansive budget.` };\n }\n return { suggest: false, reason: null };\n}\n\n/** Engines whose CLIs can be told a compaction budget per invocation. */\nexport function engineSupportsModes(engine: VerseEngine): boolean {\n return engine === 'claude' || engine === 'codex';\n}\n","/**\n * routes/verse/chat/composer-state.ts — the composer's memory, and an\n * honest estimate of what a message will cost.\n *\n * Two things a long agentic day needs from a message box and does not get\n * from a bare `useState`:\n *\n * 1. A draft survives switching chats and reloading the app. Half-written\n * instructions are expensive to re-type and are lost today the moment\n * ⌘K lands somewhere else.\n * 2. What was already sent can be recalled with ↑, the way every shell\n * works, because the second attempt at a prompt is usually the first\n * one plus a clause.\n *\n * Both are per-session and stored in `localStorage`, bounded so a long-lived\n * browser cannot accumulate an unbounded map. Every access is wrapped: a\n * private window with storage blocked still runs the app.\n */\n\nimport { CHARS_PER_TOKEN, occupancy } from '../../../../core/verse/context-math.js';\n\nconst DRAFT_KEY = 'ashlr.verse.drafts.v1';\nconst SENT_KEY = 'ashlr.verse.sent.v1';\n\n/** Sessions remembered, newest last. */\nconst SESSION_LIMIT = 40;\n/** Messages recalled per session. */\nconst HISTORY_LIMIT = 25;\n/** Matches VERSE_MAX_TURN_TEXT_BYTES; a draft over it cannot be sent anyway. */\nconst TEXT_LIMIT = 64 * 1024;\n\nfunction readMap<T>(key: string, parse: (value: unknown) => T | null): Record<string, T> {\n const out: Record<string, T> = {};\n try {\n const raw = localStorage.getItem(key);\n const parsed = raw ? (JSON.parse(raw) as unknown) : null;\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n for (const [id, value] of Object.entries(parsed as Record<string, unknown>)) {\n const clean = parse(value);\n if (clean !== null) out[id] = clean;\n }\n }\n } catch {\n /* storage unavailable or corrupt — an empty memory is a valid one */\n }\n return out;\n}\n\n/** True when the write landed; false when storage refused it (blocked, over quota). */\nfunction writeMap(key: string, map: Record<string, unknown>): boolean {\n // Insertion order is recency order: every write deletes before re-inserting.\n const ids = Object.keys(map);\n for (const stale of ids.slice(0, Math.max(0, ids.length - SESSION_LIMIT))) delete map[stale];\n try {\n localStorage.setItem(key, JSON.stringify(map));\n return true;\n } catch {\n /* best-effort — the caller decides whether a refusal loses anything */\n return false;\n }\n}\n\nconst asText = (value: unknown): string | null =>\n typeof value === 'string' && value.length > 0 ? value.slice(0, TEXT_LIMIT) : null;\n\nconst asHistory = (value: unknown): string[] | null => {\n if (!Array.isArray(value)) return null;\n const list = value.filter((v): v is string => typeof v === 'string' && v.length > 0)\n .slice(-HISTORY_LIMIT)\n .map((v) => v.slice(0, TEXT_LIMIT));\n return list.length > 0 ? list : null;\n};\n\n/**\n * Drafts whose last write storage REFUSED, held for the page's lifetime ('' =\n * a clear that could not be written) and consulted BEFORE storage.\n *\n * WHY: `localStorage` writes fail silently (blocked storage, a full quota),\n * and one draft is not a convenience but the only copy of something: the\n * handoff note. The dialog hands it over with `saveDraft` and the new chat's\n * Composer restores it with `loadDraft`; with storage refusing, the edited\n * note — possibly written after a paid summary turn — simply vanished. An\n * entry here is always newer than whatever storage holds for that chat, and a\n * later write that lands removes it, so storage is the answer again.\n */\nconst unsavedDrafts = new Map<string, string>();\n\n/** The unsent draft for this chat, or `''`. */\nexport function loadDraft(sessionId: string | null | undefined): string {\n if (!sessionId) return '';\n const unsaved = unsavedDrafts.get(sessionId);\n if (unsaved !== undefined) return unsaved;\n return readMap(DRAFT_KEY, asText)[sessionId] ?? '';\n}\n\n/** Persist (or clear, when empty) the draft for this chat. */\nexport function saveDraft(sessionId: string | null | undefined, text: string): void {\n if (!sessionId) return;\n const clean = text.trim().length > 0 ? text.slice(0, TEXT_LIMIT) : '';\n const map = readMap(DRAFT_KEY, asText);\n delete map[sessionId];\n if (clean) map[sessionId] = clean;\n unsavedDrafts.delete(sessionId);\n if (!writeMap(DRAFT_KEY, map)) {\n unsavedDrafts.set(sessionId, clean);\n // Bounded like storage: oldest refusals go first.\n for (const stale of [...unsavedDrafts.keys()].slice(0, Math.max(0, unsavedDrafts.size - SESSION_LIMIT))) unsavedDrafts.delete(stale);\n }\n}\n\n/** Messages sent in this chat, oldest first. */\nexport function loadHistory(sessionId: string | null | undefined): string[] {\n if (!sessionId) return [];\n return readMap(SENT_KEY, asHistory)[sessionId] ?? [];\n}\n\n/** Record a sent message; consecutive duplicates are not stored twice. */\nexport function pushHistory(sessionId: string | null | undefined, text: string): void {\n if (!sessionId || text.trim().length === 0) return;\n const map = readMap(SENT_KEY, asHistory);\n const current = map[sessionId] ?? [];\n delete map[sessionId];\n const next = current[current.length - 1] === text ? current : [...current, text.slice(0, TEXT_LIMIT)];\n map[sessionId] = next.slice(-HISTORY_LIMIT);\n writeMap(SENT_KEY, map);\n}\n\n/**\n * Forget everything this chat remembered — its unsent draft and its sent\n * messages.\n *\n * Deleting a chat removes its transcript from disk, but the verbatim prompts\n * stayed in browser storage under the dead session id until forty newer\n * sessions rotated them out.\n */\nexport function forgetComposerMemory(sessionId: string | null | undefined): void {\n if (!sessionId) return;\n unsavedDrafts.delete(sessionId);\n for (const [key, parse] of [\n [DRAFT_KEY, asText],\n [SENT_KEY, asHistory],\n ] as const) {\n const map: Record<string, unknown> = readMap(key, parse as (v: unknown) => unknown);\n if (!(sessionId in map)) continue;\n delete map[sessionId];\n writeMap(key, map);\n }\n}\n\n/**\n * Drop every remembered draft and sent message, for every session.\n *\n * Called on disconnect/logout alongside the query cache's `evictAll`, whose\n * contract is that stale data must not leak into the next session's first\n * paint. Prompt text is precisely the content that wipe exists for, and it is\n * the one thing that used to survive it.\n */\nexport function clearComposerMemory(): void {\n unsavedDrafts.clear();\n for (const key of [DRAFT_KEY, SENT_KEY]) {\n try {\n localStorage.removeItem(key);\n } catch {\n /* storage unavailable — nothing to clear */\n }\n }\n}\n\nexport const VERSE_DRAFT_STORAGE_KEY = DRAFT_KEY;\nexport const VERSE_SENT_STORAGE_KEY = SENT_KEY;\n\n// ---------------------------------------------------------------------------\n// Cost\n// ---------------------------------------------------------------------------\n\n/**\n * Characters per token — context-math's CHARS_PER_TOKEN, the one estimator\n * every surface uses (handoff size, fit badges, this hint). A rough\n * English/code average, and rough is the point: the real count is the\n * provider's, which this app does not have before the turn runs. Every\n * surface that shows this figure marks it `≈` and never presents it as\n * measured (DESIGN §6, VERSE-TELEMETRY-V2).\n */\nexport function estimateTokens(text: string): number {\n const trimmed = text.trim();\n if (trimmed.length === 0) return 0;\n return Math.max(1, Math.ceil(trimmed.length / CHARS_PER_TOKEN));\n}\n\n/** `over` = the projection is past the whole window, not just the compaction point. */\nexport type CostTone = 'warn' | 'danger' | 'over';\n\nexport interface CostHint {\n /** Estimated tokens for the draft itself. */\n draftTokens: number;\n /** Occupancy this message would push the session to (current + draft), in tokens. */\n projectedTokens: number;\n /** …as a percent of the context WINDOW (the meter's track), unclamped. */\n projectedPercent: number;\n /** Where the CLI auto-compacts, when known. */\n autoCompactAt: number | null;\n /** True when the projection reaches the compaction point: the CLI compacts before or during this turn. */\n pastCompaction: boolean;\n tone: CostTone;\n /** False when the current occupancy is itself an upper bound (codex before its rollout is read). */\n exact: boolean;\n}\n\n/** The session's context budget as the composer sees it (verse-model `sessionContextBudget`). */\nexport interface CostBudget {\n contextTokens: number | null | undefined;\n contextWindow: number | null | undefined;\n autoCompactAt?: number | null;\n exact?: boolean;\n}\n\n/**\n * What sending this draft would do to the context — or null when the window\n * is unknown, the box is empty, or the session stays comfortable.\n *\n * The tones are context-math `occupancy()`'s — measured against the\n * COMPACTION point when it is known (warn from 80%, danger from 95%), else\n * against the window — which is exactly what colours the ContextMeter, so the\n * hint and the meter can never disagree about when things are getting tight.\n */\nexport function costHint(text: string, budget: CostBudget): CostHint | null {\n const draftTokens = estimateTokens(text);\n if (draftTokens === 0) return null;\n const window = typeof budget.contextWindow === 'number' && budget.contextWindow > 0 ? budget.contextWindow : null;\n if (window === null) return null;\n const used = typeof budget.contextTokens === 'number' && Number.isFinite(budget.contextTokens) ? Math.max(0, budget.contextTokens) : 0;\n const projected = occupancy({\n contextTokens: used + draftTokens,\n contextWindow: window,\n autoCompactAt: budget.autoCompactAt ?? null,\n contextTokensExact: budget.exact,\n });\n if (projected.tone === 'ok' || projected.tone === 'unknown') return null;\n return {\n draftTokens,\n projectedTokens: projected.tokens,\n projectedPercent: Math.min(999, Math.round((projected.ofWindow ?? 0) * 100)),\n autoCompactAt: projected.autoCompactAt,\n pastCompaction: projected.ofCompaction !== null && projected.ofCompaction >= 1,\n tone: projected.tone,\n exact: projected.exact,\n };\n}\n","/**\n * data/auth-store.ts — the read-session + mutation-token authority for the\n * whole app. Mirrors the two-token model in src/core/web/server.ts exactly:\n *\n * READ authority — exchanged via POST /api/session (raw readToken + a\n * per-tab client proof) for an HttpOnly cookie good\n * for 15 minutes. The raw read token is remembered in\n * MEMORY ONLY (never any storage) for the tab's\n * lifetime so the 15-minute ticket can be renewed\n * silently when a GET/SSE comes back 401 — a live\n * chat must not turn into \"paste a 64-hex token every\n * 15 minutes\". The client proof is not a secret by\n * itself (it has no authority without the signed\n * cookie) and is kept in sessionStorage so a page\n * reload can keep using the still-valid cookie instead\n * of forcing re-entry.\n *\n * MUTATION authority — the raw x-ashlr-token header, required on every\n * mutating call, NEVER exchanged for a session and\n * NEVER persisted to any storage. Held in memory only,\n * for a bounded \"hold\" window, so approving a run of\n * proposals isn't one window.prompt() per click. The\n * hold is cleared on tab close, explicit lock, or\n * idle timeout.\n *\n * This module has no React dependency — it is a plain external store\n * consumed via useSyncExternalStore in hooks.ts, per the same pattern used\n * for the query cache.\n */\n\nimport { evictAll, invalidatePrefix } from './cache.js';\n// The composer owns its own storage format, so the key names live with it\n// rather than being duplicated here where they could drift.\nimport { clearComposerMemory } from '../routes/verse/chat/composer-state.js';\n\nconst READ_CLIENT_STORAGE_KEY = 'ashlr.readClientProof.v1';\nconst READ_CLIENT_RE = /^[a-f0-9]{64}$/;\nconst MUTATION_HOLD_MS = 20 * 60 * 1000; // 20 minutes of inactivity clears the hold.\n\nexport type AuthPhase = 'checking' | 'unauthenticated' | 'authenticated';\n\ninterface AuthState {\n phase: AuthPhase;\n /** True once a session check has actually completed (vs. still booting). */\n checked: boolean;\n mutationTokenHeldUntil: number | null;\n}\n\nfunction randomHex64(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nfunction loadOrCreateClientProof(): string {\n try {\n const existing = sessionStorage.getItem(READ_CLIENT_STORAGE_KEY);\n if (existing && READ_CLIENT_RE.test(existing)) return existing;\n } catch {\n // sessionStorage unavailable (privacy mode, etc.) — fall through to an\n // in-memory-only proof for this page life.\n }\n const proof = randomHex64();\n try {\n sessionStorage.setItem(READ_CLIENT_STORAGE_KEY, proof);\n } catch {\n /* best-effort persistence only */\n }\n return proof;\n}\n\nlet clientProof = loadOrCreateClientProof();\n\n/** In-memory only. Never touches storage. Cleared by clearMutationToken(). */\nlet mutationToken: string | null = null;\nlet mutationHoldTimer: ReturnType<typeof setTimeout> | null = null;\n/**\n * The mutation token came from the desktop host (window.__ASHLR_TOKENS__).\n * The operator never saw it, so an idle timeout must re-arm the hold rather\n * than clear it — a cleared hold would ask for a token nobody can paste.\n */\nlet mutationHeldByHost = false;\n\n/**\n * Last read token that the server accepted (typed or host-injected). Memory\n * only, same trust boundary as the mutation hold; used to renew the cookie\n * silently on 401. Dropped on explicit disconnect or when renewal fails.\n */\nlet rememberedReadToken: string | null = null;\nlet renewal: Promise<boolean> | null = null;\n/** Desktop-injected tokens kept for re-adoption (see the host-injected section below). */\nlet hostTokens: InjectedTokens | null = null;\n\nlet state: AuthState = { phase: 'checking', checked: false, mutationTokenHeldUntil: null };\nconst listeners = new Set<() => void>();\n\nfunction emit(): void {\n for (const l of listeners) l();\n}\n\nfunction setState(patch: Partial<AuthState>): void {\n state = { ...state, ...patch };\n emit();\n}\n\nexport function subscribeAuth(listener: () => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n}\n\nexport function getAuthSnapshot(): AuthState {\n return state;\n}\n\nexport function getReadClientProof(): string {\n return clientProof;\n}\n\n/** Regenerate the client proof (e.g. after an explicit \"forget this device\"). */\nexport function resetReadClientProof(): string {\n clientProof = randomHex64();\n try {\n sessionStorage.setItem(READ_CLIENT_STORAGE_KEY, clientProof);\n } catch {\n /* best-effort */\n }\n return clientProof;\n}\n\n/**\n * Exchange a raw read token for a session cookie. Throws on failure with a\n * message safe to show the operator. The raw token never leaves this call.\n */\nexport async function establishReadSession(rawReadToken: string): Promise<void> {\n const trimmed = rawReadToken.trim();\n if (!/^[a-f0-9]{64}$/.test(trimmed)) {\n throw new Error('That does not look like a read token — expected 64 hex characters.');\n }\n const res = await fetch('/api/session', {\n method: 'POST',\n credentials: 'same-origin',\n headers: {\n 'x-ashlr-token': trimmed,\n 'x-ashlr-read-client': clientProof,\n },\n });\n if (res.status === 204) {\n rememberedReadToken = trimmed;\n setState({ phase: 'authenticated', checked: true });\n return;\n }\n if (res.status === 401) {\n throw new Error('Server rejected that token. Double-check the read token printed by this server at startup.');\n }\n throw new Error(`Unexpected response establishing session (HTTP ${res.status}).`);\n}\n\nexport async function clearReadSession(): Promise<void> {\n try {\n await fetch('/api/session', {\n method: 'DELETE',\n credentials: 'same-origin',\n headers: { 'x-ashlr-read-client': clientProof },\n });\n } catch {\n /* best-effort */\n }\n rememberedReadToken = null;\n hostTokens = null;\n expireNow();\n}\n\nfunction expireNow(): void {\n clearMutationToken();\n evictAll();\n // `evictAll` clears the query cache so nothing stale leaks into the next\n // session's first paint. The composer's drafts and sent-message history live\n // in localStorage, not in that cache, and they are verbatim prompt text —\n // exactly the kind of content this wipe exists for.\n clearComposerMemory();\n setState({ phase: 'unauthenticated', checked: true });\n}\n\n/**\n * Called by data/client.ts whenever any authenticated GET comes back 401\n * (and by sse.ts on `session-expired`). With a remembered read token the\n * cookie is re-issued silently and the phase never flips; the failed call's\n * own retry/reconnect (SSE backoff, the next refetch) then succeeds. Only\n * when renewal fails (server restarted with fresh tokens) does the gate show.\n */\nexport function reportSessionExpired(): void {\n if (state.phase !== 'authenticated') return;\n if (rememberedReadToken) {\n void renewReadSession();\n return;\n }\n expireNow();\n}\n\n/** Re-exchange the remembered read token for a fresh cookie; one flight at a time. */\nexport function renewReadSession(): Promise<boolean> {\n if (renewal) return renewal;\n const token = rememberedReadToken;\n if (!token) return Promise.resolve(false);\n renewal = (async () => {\n try {\n await establishReadSession(token);\n // Whatever 401ed while the ticket was lapsed refetches against the new cookie.\n invalidatePrefix('');\n return true;\n } catch {\n rememberedReadToken = null;\n hostTokens = null;\n expireNow();\n return false;\n } finally {\n renewal = null;\n }\n })();\n return renewal;\n}\n\n/** Test hygiene: forget the remembered read token and host tokens without a network call. */\nexport function forgetRememberedTokens(): void {\n rememberedReadToken = null;\n hostTokens = null;\n renewal = null;\n}\n\nexport function markCheckComplete(authenticated: boolean): void {\n setState({ phase: authenticated ? 'authenticated' : 'unauthenticated', checked: true });\n}\n\n// ---------------------------------------------------------------------------\n// Host-injected tokens (desktop wrapper). The Tauri shell starts the server\n// itself and hands both tokens to the page through an initialization script\n// (`window.__ASHLR_TOKENS__`, see desktop/README.md) so the operator never\n// pastes them. The object is removed from `window` before either token is\n// used and kept in this module's memory instead (`hostTokens`), because the\n// desktop operator has no terminal to paste from: when the 15-minute cookie\n// lapses or the gate re-mounts, adoption re-runs from memory. The read token\n// is exchanged for the cookie exactly like a typed one and the mutation token\n// goes into the memory-only hold. Nothing is persisted and nothing is logged.\n// ---------------------------------------------------------------------------\n\nexport interface InjectedTokens {\n readToken: string;\n /** Mutation token; absent or null when the host started the server without dispatch. */\n token?: string | null;\n}\n\ndeclare global {\n interface Window {\n __ASHLR_TOKENS__?: InjectedTokens;\n }\n}\n\n/**\n * Take host-injected tokens: from `window` (cleared from the page on first\n * sight, remembered here) or, on later calls, from memory.\n */\nexport function takeInjectedTokens(): InjectedTokens | null {\n const injected = window.__ASHLR_TOKENS__;\n if (injected && typeof injected === 'object') {\n try {\n delete window.__ASHLR_TOKENS__;\n } catch {\n window.__ASHLR_TOKENS__ = undefined;\n }\n if (typeof injected.readToken === 'string') {\n hostTokens = { readToken: injected.readToken, ...(typeof injected.token === 'string' ? { token: injected.token } : {}) };\n }\n }\n return hostTokens ? { ...hostTokens } : null;\n}\n\n/**\n * Establish the read session (and set the mutation hold) from host-injected\n * tokens. Resolves true when a session was established; false when nothing\n * was injected or the server rejected the read token (the caller then falls\n * back to the normal SessionGate flow — the failure is never surfaced with\n * the token in it).\n */\nexport async function adoptInjectedTokens(): Promise<boolean> {\n const injected = takeInjectedTokens();\n if (!injected) return false;\n try {\n await establishReadSession(injected.readToken);\n } catch {\n // Wrong or stale tokens are not worth retrying from memory.\n hostTokens = null;\n return false;\n }\n if (injected.token && /^[a-f0-9]{64}$/.test(injected.token.trim())) setMutationToken(injected.token.trim(), { host: true });\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Mutation token — session-scoped hold, memory-only.\n// ---------------------------------------------------------------------------\n\nexport function getMutationToken(): string | null {\n return mutationToken;\n}\n\nexport function hasMutationHold(): boolean {\n return mutationToken !== null;\n}\n\nfunction armIdleClear(): void {\n if (mutationHoldTimer) clearTimeout(mutationHoldTimer);\n mutationHoldTimer = setTimeout(() => {\n // A host-held token has no operator to re-paste it: keep the hold alive.\n if (mutationHeldByHost && mutationToken !== null) touchMutationHold();\n else clearMutationToken();\n }, MUTATION_HOLD_MS);\n}\n\n/**\n * Set the mutation token for this tab's session hold. Never persisted.\n * `host` marks a token injected by the desktop wrapper (see adoptInjectedTokens).\n */\nexport function setMutationToken(token: string, opts: { host?: boolean } = {}): void {\n mutationToken = token;\n mutationHeldByHost = opts.host === true;\n const heldUntil = Date.now() + MUTATION_HOLD_MS;\n setState({ mutationTokenHeldUntil: heldUntil });\n armIdleClear();\n}\n\n/** Extend the hold window on real use (called after every mutating call). */\nexport function touchMutationHold(): void {\n if (mutationToken === null) return;\n const heldUntil = Date.now() + MUTATION_HOLD_MS;\n setState({ mutationTokenHeldUntil: heldUntil });\n armIdleClear();\n}\n\nexport function clearMutationToken(): void {\n mutationToken = null;\n mutationHeldByHost = false;\n if (mutationHoldTimer) {\n clearTimeout(mutationHoldTimer);\n mutationHoldTimer = null;\n }\n setState({ mutationTokenHeldUntil: null });\n}\n","/**\n * data/sse.ts — the live channel. One EventSource against /api/events for\n * the whole app, fanning out NAMED events (see api.ts handleSseEvents:\n * 'runs' | 'swarms' | 'inbox' | 'daemon' | 'daemon-observation' |\n * 'fleet-activity-ping' | 'fleet-activity-observation' | 'snapshot') to\n * subscribers, and invalidating the matching cache key so any mounted\n * useQuery(key) re-fetches in the background.\n *\n * The server polls disk at a fixed interval (api.ts SSE_POLL_MS, ~1.5s at\n * time of writing) — that is the real latency floor for \"live\". This layer\n * does not, and should not, try to beat that; it just makes sure the UI\n * never polls MORE often than the server already pushes.\n */\nimport { getReadClientProof, getAuthSnapshot, reportSessionExpired, subscribeAuth } from './auth-store.js';\nimport { invalidate, invalidatePrefix } from './cache.js';\nimport { isScopedConsolePath } from '../app/console-mode.js';\n\nexport const SSE_EVENT_NAMES = [\n 'runs',\n 'swarms',\n 'inbox',\n 'daemon',\n 'daemon-observation',\n 'fleet-activity-ping',\n 'fleet-activity-observation',\n 'snapshot',\n 'session-expired',\n] as const;\nexport type SseEventName = (typeof SSE_EVENT_NAMES)[number];\n\n/** Cache keys (see data/queries.ts) invalidated when a given SSE event fires. */\nconst EVENT_TO_CACHE_KEYS: Record<SseEventName, string[]> = {\n runs: ['runs'],\n swarms: ['swarms'],\n inbox: ['inbox'],\n // 'daemon' was previously mapped but nothing subscribed to a 'daemon'-keyed\n // query (control/daemon didn't exist yet) — now daemonObservationQuery\n // (data/queries.ts) does. Also refreshing 'control-snapshot' here so\n // /control/daemon and /control/security (both read off controlSnapshotQuery)\n // stay live on the same daemon-tick cadence instead of only on page load.\n // `/api/verse/control` and `/api/verse/audit` ARE the daemon projection, so\n // they ride the same signal. Without this the Autonomy cockpit — running\n // state, last tick outcome, today's spend against the cap — was frozen at\n // whatever was true when the section mounted, and DEFAULT_QUERY_FRESH_MS\n // then made a rail switch away and back inside 10s a no-op too.\n //\n // Deliberately NOT a coarse `verse-` prefix sweep: `verse-usage-accounts`\n // and `verse-usage-local-models` spawn provider probes, and firing those on\n // every daemon tick would turn a liveness fix into a process storm. Those\n // two poll on their own visibility-gated 30s cadence in UsageSection,\n // matching the collector's cycle.\n daemon: ['daemon', 'control-snapshot', 'verse-control', 'verse-usage-control', 'verse-caps'],\n 'daemon-observation': ['daemon', 'control-snapshot', 'verse-control', 'verse-usage-control', 'verse-caps'],\n // fleet-activity pings are the closest live signal fleet state changed;\n // 'fleet' (fleetStatusQuery, /control/fleet's queue depth + lease health)\n // has no dedicated SSE event of its own, so it rides these instead of only\n // refreshing on mount.\n 'fleet-activity-ping': ['fleet-activity', 'fleet'],\n 'fleet-activity-observation': ['fleet-activity', 'fleet'],\n // Agent OS snapshots are immutable records but their current authenticated\n // projection advances alongside the server's general snapshot signal.\n snapshot: ['dashboard-snapshot', 'agent-os-snapshot'],\n 'session-expired': [],\n};\n\ntype RawListener = (payload: unknown) => void;\nconst rawListeners = new Map<SseEventName, Set<RawListener>>();\n\nlet source: EventSource | null = null;\nlet reconnectTimer: ReturnType<typeof setTimeout> | null = null;\nlet reconnectAttempt = 0;\n\nfunction backoffMs(): number {\n // 1s, 2s, 4s, 8s, capped at 15s — the server's own poll floor is ~1.5s so\n // there is no benefit reconnecting faster than that even on attempt 0.\n return Math.min(1000 * 2 ** reconnectAttempt, 15_000);\n}\n\nfunction teardown(): void {\n if (source) {\n source.close();\n source = null;\n }\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n}\n\nfunction scheduleReconnect(): void {\n if (reconnectTimer) return;\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n reconnectAttempt += 1;\n connect();\n }, backoffMs());\n}\n\nfunction connect(): void {\n // Shared hooks import this module, but the dedicated console has no global\n // Hub event channel. Check on every reconnect, not just the initial import.\n if (isScopedConsolePath()) { teardown(); return; }\n if (getAuthSnapshot().phase !== 'authenticated') return;\n teardown();\n\n const url = `/api/events?client=${encodeURIComponent(getReadClientProof())}`;\n const es = new EventSource(url, { withCredentials: true });\n source = es;\n\n es.onopen = () => {\n reconnectAttempt = 0;\n };\n\n for (const name of SSE_EVENT_NAMES) {\n es.addEventListener(name, (evt) => {\n const messageEvent = evt as MessageEvent<string>;\n let payload: unknown = undefined;\n try {\n payload = JSON.parse(messageEvent.data);\n } catch {\n payload = undefined;\n }\n for (const key of EVENT_TO_CACHE_KEYS[name]) invalidate(key);\n if (name === 'inbox') {\n // Parameterized inbox queries (one cache key per filter combo / per\n // proposal id) aren't in EVENT_TO_CACHE_KEYS's static map — sweep\n // both prefixes so the list (any filter) and any open detail view\n // (proposalDetailQuery, data/queries.ts) live-update on the same\n // 'inbox' event as the unfiltered badge.\n invalidatePrefix('inbox-list:');\n invalidatePrefix('proposal-detail-');\n }\n if (name === 'session-expired') reportSessionExpired();\n const listeners = rawListeners.get(name);\n if (listeners) for (const l of listeners) l(payload);\n });\n }\n\n es.onerror = () => {\n // EventSource auto-retries on its own, but its built-in retry delay is\n // opaque and can hammer a server that just lost its read session. Close\n // it and drive reconnection ourselves so we stop entirely once\n // auth-store reports the session as expired (see the subscribeAuth\n // wiring at the bottom of this file).\n es.close();\n if (source === es) source = null;\n if (getAuthSnapshot().phase === 'authenticated') scheduleReconnect();\n };\n}\n\n/** Subscribe to one named SSE event's raw payload (rarely needed directly —\n * most views should prefer useQuery + the cache-key mapping above). */\nexport function onSseEvent(name: SseEventName, listener: RawListener): () => void {\n let set = rawListeners.get(name);\n if (!set) {\n set = new Set();\n rawListeners.set(name, set);\n }\n set.add(listener);\n return () => set!.delete(listener);\n}\n\n// Connect once authenticated; disconnect the instant the session drops so a\n// stale connection never masks a 401 the rest of the app needs to react to.\nsubscribeAuth(() => {\n const phase = getAuthSnapshot().phase;\n if (phase === 'authenticated') {\n reconnectAttempt = 0;\n connect();\n } else {\n teardown();\n }\n});\n","/**\n * data/theme-store.ts — the theme preference, extracted out of Topbar.tsx\n * so the command palette's \"Toggle theme\" action can read/write the same\n * source of truth Topbar's own button uses (same module-level state +\n * subscribe pattern as auth-store.ts — commands.ts's `run` closures have no\n * React context to call useState from, so this can't live in a component).\n */\n\nexport type ThemePreference = 'system' | 'light' | 'dark';\n\nconst THEME_STORAGE_KEY = 'ashlr.theme.v1';\n\nfunction applyTheme(pref: ThemePreference): void {\n const root = document.documentElement;\n if (pref === 'system') root.removeAttribute('data-theme');\n else root.setAttribute('data-theme', pref);\n}\n\nfunction loadTheme(): ThemePreference {\n try {\n const v = localStorage.getItem(THEME_STORAGE_KEY);\n if (v === 'light' || v === 'dark' || v === 'system') return v;\n } catch {\n /* ignore */\n }\n return 'system';\n}\n\nlet theme: ThemePreference = loadTheme();\napplyTheme(theme);\n\nconst listeners = new Set<() => void>();\n\nfunction emit(): void {\n for (const l of listeners) l();\n}\n\nexport function subscribeTheme(listener: () => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n}\n\nexport function getTheme(): ThemePreference {\n return theme;\n}\n\nexport function setTheme(next: ThemePreference): void {\n theme = next;\n applyTheme(theme);\n try {\n localStorage.setItem(THEME_STORAGE_KEY, theme);\n } catch {\n /* best-effort persistence only */\n }\n emit();\n}\n\nexport function cycleTheme(): void {\n setTheme(theme === 'system' ? 'light' : theme === 'light' ? 'dark' : 'system');\n}\n","/**\n * data/appearance-store.ts — the operator's appearance preferences: the\n * \"Linux for agentic engineering\" part of the design language\n * (docs/VERSE-DESIGN-V2.md §3). Theme, accent, density, display face,\n * radius, and motion, applied live by writing custom properties and\n * data-attributes on <html>.\n *\n * Same shape as ./theme-store.ts on purpose — module-level state + a\n * listener set, zero React — so it is unit-testable without a DOM renderer\n * and usable from non-component code (the command palette's closures, the\n * rail's quick-toggle). data/hooks.ts binds it into React via\n * `useAppearance()`, exactly as it does for the theme.\n *\n * THEME IS NOT FORKED. `theme-store.ts` remains the single authority for the\n * light/dark/system preference (key `ashlr.theme.v1`, already read by the\n * topbar and the command palette); this module re-exposes it as one field of\n * the appearance snapshot and delegates writes to `setTheme`. Two stores\n * writing `data-theme` would race; one store that owns it does not.\n *\n * Everything is applied at MODULE LOAD (before React's first render, since\n * hooks.ts imports this and every view imports hooks.ts), so the app never\n * paints the default accent for a frame before swapping to the chosen one.\n */\nimport {\n getTheme,\n setTheme,\n subscribeTheme,\n type ThemePreference,\n} from './theme-store.js';\n\nexport type { ThemePreference };\n\nexport type Density = 'comfortable' | 'compact';\n\n/**\n * How large the whole interface is drawn. Scales TYPE AND SPACING TOGETHER\n * (design/tokens.css multiplies its type ramp by --ui-text-scale and its\n * spacing/geometry ramp by --ui-scale), because scaling type alone produces\n * big text in cramped boxes — worse to read than the default, not better.\n *\n * ORTHOGONAL TO `density`, not a replacement for it: display size answers\n * \"how big is this interface\", density answers \"how tightly are its rows\n * packed\". They compose, which is why tokens.css expresses the scale as a\n * multiplier rather than a second table of literal heights.\n *\n * There is no step below 'default' on purpose — see the [data-ui-scale]\n * block in tokens.css for why (the 12px body floor, and Density already\n * being the tightening control).\n */\nexport type UiScale = 'default' | 'large' | 'xlarge';\n\n/**\n * The operator's motion choice. Tri-state ON PURPOSE.\n *\n * A boolean cannot distinguish \"I want full motion\" from \"I never touched\n * this\", and `persist()` writes the whole snapshot on every appearance change\n * — so dragging the accent hue once used to store `reduceMotion: false` and\n * permanently override the OS `prefers-reduced-motion` preference, because\n * `applyAppearance` then wrote `data-motion=\"full\"`, which is exactly the\n * value every `:root:not([data-motion=\"full\"])` guard exists to defer to.\n *\n * 'system' removes the attribute entirely and lets the media queries decide,\n * which is what DESIGN-V2 §3 and §6 ask for by default.\n */\nexport type MotionPreference = 'system' | 'reduce' | 'full';\nexport type DisplayFont = 'grotesk' | 'ui' | 'mono';\nexport type RadiusScale = 'sharp' | 'default' | 'soft';\n\nexport interface Appearance {\n /** Delegated to theme-store.ts — persisted under its own key. */\n theme: ThemePreference;\n /** Accent hue 0-360. */\n accentH: number;\n /** Accent saturation 0-100 (%). */\n accentS: number;\n /** Accent lightness, clamped to a readable band (%). */\n accentL: number;\n density: Density;\n /** How large the whole interface is drawn. Type and spacing together. */\n uiScale: UiScale;\n displayFont: DisplayFont;\n radius: RadiusScale;\n /** The stored choice: 'system' follows `prefers-reduced-motion`. */\n motion: MotionPreference;\n /**\n * DERIVED, never stored — what the Settings switch shows and what the app\n * behaves as right now. `setAppearance({ reduceMotion })` is the only call\n * that turns this back into an explicit 'reduce'/'full'.\n */\n reduceMotion: boolean;\n}\n\n/**\n * The persisted subset. `theme` lives in theme-store, and `reduceMotion` is\n * derived from `motion` — storing it is what created the override bug.\n */\ntype StoredAppearance = Omit<Appearance, 'theme' | 'reduceMotion'>;\n\nconst STORAGE_KEY = 'ashlr.verse.appearance.v1';\n\n/**\n * Accent lightness band. Below ~30% the accent stops reading as a color on a\n * dark canvas; above ~78% it disappears on a light one. The hue and\n * saturation are unconstrained — this is the one axis where a free slider\n * can make the app genuinely unusable.\n */\nconst ACCENT_L_MIN = 30;\nconst ACCENT_L_MAX = 78;\n\n/** Must match the bare `:root` values in design/tokens.css. */\nexport const DEFAULT_ACCENT = { h: 245, s: 72, l: 58 } as const;\n\nexport interface AccentPreset {\n id: string;\n label: string;\n h: number;\n s: number;\n l: number;\n}\n\n/**\n * Eight presets (design doc §3). Each was checked with design/contrast.ts so\n * its derived link color clears 4.5:1 in BOTH themes — the hue slider can\n * still be dragged anywhere, and the Settings panel warns when it lands\n * somewhere unreadable.\n */\nexport const ACCENT_PRESETS: readonly AccentPreset[] = [\n { id: 'indigo', label: 'Indigo', h: 245, s: 72, l: 58 },\n { id: 'blue', label: 'Blue', h: 212, s: 80, l: 50 },\n { id: 'cyan', label: 'Cyan', h: 190, s: 80, l: 40 },\n { id: 'teal', label: 'Teal', h: 168, s: 70, l: 38 },\n { id: 'green', label: 'Green', h: 145, s: 60, l: 38 },\n { id: 'amber', label: 'Amber', h: 38, s: 80, l: 42 },\n { id: 'rose', label: 'Rose', h: 348, s: 72, l: 50 },\n { id: 'violet', label: 'Violet', h: 278, s: 66, l: 55 },\n] as const;\n\nfunction prefersReducedMotion(): boolean {\n try {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)').matches\n : false;\n } catch {\n // Some embedded webviews throw on an unsupported media feature.\n return false;\n }\n}\n\nexport function defaultAppearance(): Appearance {\n return {\n theme: 'system',\n accentH: DEFAULT_ACCENT.h,\n accentS: DEFAULT_ACCENT.s,\n accentL: DEFAULT_ACCENT.l,\n density: 'comfortable',\n uiScale: 'default',\n displayFont: 'grotesk',\n radius: 'default',\n motion: 'system',\n reduceMotion: prefersReducedMotion(),\n };\n}\n\nfunction isMotion(v: unknown): v is MotionPreference {\n return v === 'system' || v === 'reduce' || v === 'full';\n}\n\n/** What 'system' resolves to right now; an explicit choice answers itself. */\nfunction resolveReduceMotion(motion: MotionPreference): boolean {\n return motion === 'reduce' ? true : motion === 'full' ? false : prefersReducedMotion();\n}\n\nconst clamp = (n: number, min: number, max: number): number => Math.min(max, Math.max(min, n));\n\nfunction isDensity(v: unknown): v is Density {\n return v === 'comfortable' || v === 'compact';\n}\nfunction isUiScale(v: unknown): v is UiScale {\n return v === 'default' || v === 'large' || v === 'xlarge';\n}\nfunction isDisplayFont(v: unknown): v is DisplayFont {\n return v === 'grotesk' || v === 'ui' || v === 'mono';\n}\nfunction isRadius(v: unknown): v is RadiusScale {\n return v === 'sharp' || v === 'default' || v === 'soft';\n}\n\n/** Coerce anything (stored JSON, a caller's patch) into a valid Appearance. */\nfunction normalize(base: Appearance, patch: Partial<Appearance> | Partial<StoredAppearance>): Appearance {\n const merged = { ...base, ...patch } as Appearance;\n // `reduceMotion` in a PATCH is the Settings switch and is authoritative —\n // it is the one call that converts the derived boolean back into a stored\n // choice. `reduceMotion` in a stored blob is not a choice at all, which is\n // why load() strips it before it ever gets here.\n const asked = (patch as Partial<Appearance>).reduceMotion;\n const motion: MotionPreference = isMotion((patch as Partial<Appearance>).motion)\n ? ((patch as Partial<Appearance>).motion as MotionPreference)\n : typeof asked === 'boolean'\n ? (asked ? 'reduce' : 'full')\n : isMotion(base.motion)\n ? base.motion\n : 'system';\n return {\n theme: merged.theme === 'light' || merged.theme === 'dark' ? merged.theme : 'system',\n accentH: Number.isFinite(merged.accentH) ? ((Math.round(merged.accentH) % 360) + 360) % 360 : DEFAULT_ACCENT.h,\n accentS: Number.isFinite(merged.accentS) ? clamp(Math.round(merged.accentS), 0, 100) : DEFAULT_ACCENT.s,\n accentL: Number.isFinite(merged.accentL)\n ? clamp(Math.round(merged.accentL), ACCENT_L_MIN, ACCENT_L_MAX)\n : DEFAULT_ACCENT.l,\n density: isDensity(merged.density) ? merged.density : 'comfortable',\n uiScale: isUiScale(merged.uiScale) ? merged.uiScale : 'default',\n displayFont: isDisplayFont(merged.displayFont) ? merged.displayFont : 'grotesk',\n radius: isRadius(merged.radius) ? merged.radius : 'default',\n motion,\n reduceMotion: resolveReduceMotion(motion),\n };\n}\n\nfunction load(): Appearance {\n const base = defaultAppearance();\n let stored: Partial<StoredAppearance> = {};\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (raw) {\n const parsed: unknown = JSON.parse(raw);\n if (parsed && typeof parsed === 'object') stored = parsed as Partial<StoredAppearance>;\n }\n } catch {\n // Unavailable storage, quota errors, or corrupt JSON all mean the same\n // thing here: fall back to defaults rather than failing to boot.\n }\n // Drop any `reduceMotion` a pre-tri-state build left behind: it was written\n // by every unrelated appearance change, so it records nothing the operator\n // decided. Without `motion`, the media query decides — as it should.\n const { reduceMotion: _legacy, ...rest } = stored as Partial<StoredAppearance> & { reduceMotion?: unknown };\n return normalize(base, { ...rest, theme: getTheme() });\n}\n\nfunction persist(value: Appearance): void {\n try {\n // theme is deliberately excluded — theme-store.ts owns its own key.\n // reduceMotion is excluded because it is derived: persisting it is what\n // let an accent drag silently override the OS preference.\n const { theme: _theme, reduceMotion: _derived, ...rest } = value;\n localStorage.setItem(STORAGE_KEY, JSON.stringify(rest satisfies StoredAppearance));\n } catch {\n /* best-effort persistence only */\n }\n}\n\nconst FONT_VAR: Record<DisplayFont, string | null> = {\n // null = remove the inline override and fall back to the token default.\n grotesk: null,\n ui: 'var(--font-ui)',\n mono: 'var(--font-mono)',\n};\n\n/**\n * Write the settings onto <html>. The accent is written as the three raw\n * channels tokens.css derives its whole ramp from, so one paint recolors\n * links, focus rings, primary buttons, meters and charts together.\n */\nexport function applyAppearance(value: Appearance): void {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n root.style.setProperty('--accent-h', String(value.accentH));\n root.style.setProperty('--accent-s', `${value.accentS}%`);\n root.style.setProperty('--accent-l', `${value.accentL}%`);\n\n const font = FONT_VAR[value.displayFont];\n if (font === null) root.style.removeProperty('--font-display');\n else root.style.setProperty('--font-display', font);\n\n root.setAttribute('data-density', value.density);\n // The display size is an ATTRIBUTE, not an inline custom property, for the\n // same reason the theme is: tokens.css owns the numbers, and a\n // `[data-ui-scale]` block can be read, tested and retuned in the stylesheet\n // that every other token lives in. Writing `--ui-scale` inline here would\n // put half the design system in a TypeScript file.\n root.setAttribute('data-ui-scale', value.uiScale);\n root.setAttribute('data-radius', value.radius);\n // 'system' removes the attribute so the `prefers-reduced-motion` media\n // queries decide on their own. \"full\" is meaningful, not a no-op: it opts an\n // operator explicitly OUT of the OS preference (tokens.css and the four\n // Verse stylesheets guard their media query with :not([data-motion=\"full\"])),\n // which is precisely why it must never be written by default.\n if (value.motion === 'system') root.removeAttribute('data-motion');\n else root.setAttribute('data-motion', value.motion);\n}\n\nlet current: Appearance = load();\napplyAppearance(current);\n\nconst listeners = new Set<() => void>();\n\nfunction emit(): void {\n for (const l of listeners) l();\n}\n\n// The theme can also be changed from the rail's quick-toggle and the command\n// palette (both go through theme-store). Mirror those into the snapshot so a\n// Settings panel rendered at the same time never shows a stale radio.\nsubscribeTheme(() => {\n if (current.theme === getTheme()) return;\n current = { ...current, theme: getTheme() };\n emit();\n});\n\nexport function subscribeAppearance(listener: () => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n}\n\n/** Stable snapshot — a NEW object on every change (useSyncExternalStore uses Object.is). */\nexport function getAppearance(): Appearance {\n return current;\n}\n\n/** Apply and persist a partial change. Unknown/out-of-range values are coerced. */\nexport function setAppearance(patch: Partial<Appearance>): void {\n const next = normalize(current, patch);\n const themeChanged = next.theme !== current.theme;\n current = next;\n applyAppearance(current);\n persist(current);\n // Delegated last so theme-store's own listeners see the final snapshot.\n if (themeChanged) setTheme(current.theme);\n emit();\n}\n\n/** Back to the shipped defaults, including the theme and the stored blob. */\nexport function resetAppearance(): void {\n try {\n localStorage.removeItem(STORAGE_KEY);\n } catch {\n /* best-effort */\n }\n current = defaultAppearance();\n applyAppearance(current);\n setTheme('system');\n emit();\n}\n\n/** True when nothing has been customized — drives the \"Reset\" affordance. */\nexport function isDefaultAppearance(value: Appearance = current): boolean {\n const base = defaultAppearance();\n return (\n value.theme === base.theme &&\n value.accentH === base.accentH &&\n value.accentS === base.accentS &&\n value.accentL === base.accentL &&\n value.density === base.density &&\n value.uiScale === base.uiScale &&\n value.displayFont === base.displayFont &&\n value.radius === base.radius &&\n value.motion === base.motion\n );\n}\n\n/** The preset whose channels match the current accent, if any. */\nexport function matchingAccentPreset(value: Appearance = current): AccentPreset | null {\n return (\n ACCENT_PRESETS.find((p) => p.h === value.accentH && p.s === value.accentS && p.l === value.accentL) ?? null\n );\n}\n\nexport const ACCENT_LIGHTNESS_RANGE = { min: ACCENT_L_MIN, max: ACCENT_L_MAX } as const;\nexport const APPEARANCE_STORAGE_KEY = STORAGE_KEY;\n","/**\n * data/hooks.ts — React bindings over the framework-free stores in\n * cache.ts / auth-store.ts / sse.ts. This is the only file in the data\n * layer that imports React; every module underneath it stays plain\n * TypeScript so it's trivially unit-testable without a DOM.\n */\nimport { useCallback, useEffect, useSyncExternalStore } from 'react';\nimport { ensureQuery, getQuerySnapshot, refetchQuery, subscribeQuery, type QueryEntry } from './cache.js';\nimport type { QueryDef } from './queries.js';\nimport {\n getAuthSnapshot,\n subscribeAuth,\n type AuthPhase,\n hasMutationHold,\n getMutationToken,\n setMutationToken as storeSetMutationToken,\n touchMutationHold,\n clearMutationToken,\n} from './auth-store.js';\nimport { onSseEvent, type SseEventName } from './sse.js';\nimport { subscribeTheme, getTheme, setTheme, cycleTheme, type ThemePreference } from './theme-store.js';\nimport {\n getAppearance,\n resetAppearance,\n setAppearance,\n subscribeAppearance,\n type Appearance,\n} from './appearance-store.js';\n\n/**\n * How long a cached value is accepted on MOUNT without re-reading it.\n *\n * A section that re-mounts inside this window (rail switch, a parent\n * re-keying, React 18's double-invoked effects in StrictMode) issues no\n * request at all. Ten seconds is short enough that any surface a human\n * revisits deliberately still re-reads, and long enough that the Usage\n * section's seven probe-spawning reads happen ONCE per visit rather than\n * once per mount.\n *\n * This does not make data stale: keys with an SSE event still refresh the\n * instant the server says they changed (sse.ts → invalidate, which is not\n * freshness-gated), and `useRefetch` always ignores it.\n */\nexport const DEFAULT_QUERY_FRESH_MS = 10_000;\n\nexport interface UseQueryOptions {\n /** Override `DEFAULT_QUERY_FRESH_MS` for this subscription. 0 = always fetch. */\n freshMs?: number;\n}\n\n/**\n * Subscribe to a resource. Fetches on mount (and on `key` change) unless the\n * cached value is still fresh, stays subscribed to background refreshes\n * triggered by sse.ts, and NEVER clears `data` while refetching —\n * `status === 'refreshing'` is the signal to show a subtle indicator instead\n * of a skeleton, `status === 'loading'` (no data yet at all) is the only\n * state that should render a skeleton.\n */\nexport function useQuery<T>(def: QueryDef<T>, options?: UseQueryOptions): QueryEntry<T> {\n const freshMs = options?.freshMs ?? DEFAULT_QUERY_FRESH_MS;\n const snapshot = useSyncExternalStore(\n useCallback((listener) => subscribeQuery(def.key, listener), [def.key]),\n () => getQuerySnapshot<T>(def.key),\n () => getQuerySnapshot<T>(def.key),\n );\n\n useEffect(() => {\n // Cache refreshers outlive any one component. Binding the stored fetcher\n // to this component's AbortSignal poisons later SSE invalidations after\n // unmount, so the shared cache owns the request lifetime.\n void ensureQuery(def.key, () => def.fetch(), freshMs);\n // Re-fetch only when the resource identity changes, not on every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- def.fetch is re-created per render on purpose; only def.key identifies the resource.\n }, [def.key, freshMs]);\n\n return snapshot;\n}\n\n/**\n * Manually trigger a re-fetch without waiting for SSE — a \"Retry\" button, a\n * \"Refresh\" button, or a poll loop.\n *\n * Ignores freshness always, and joins an in-flight read only while that read\n * is younger than `REFETCH_JOIN_WINDOW_MS`. Both halves matter, because this\n * one hook backs two different callers:\n *\n * - A poll (UniverseView ticks every 3–15s and again on visibilitychange)\n * landing on a read that started milliseconds ago SHOULD coalesce with\n * it; issuing a second request there is pure waste.\n * - A person's click landing on a read that has been running for seconds\n * SHOULD NOT. That was the old behavior: the click joined the running\n * promise and returned data fetched before the click — a silent no-op,\n * and with local-model probes taking seconds under load the window was\n * wide enough to hit in ordinary use.\n *\n * For the strict guarantee — a request issued after the click, every time,\n * no matter what else is running — use `useRefresh`.\n */\nexport function useRefetch<T>(def: QueryDef<T>): () => void {\n return useCallback(() => {\n void refetchQuery(def.key, () => def.fetch());\n }, [def]);\n}\n\n/**\n * The explicit user-initiated refresh: ALWAYS issues a new request, never\n * joins one already running, and the read it issues is the one whose result\n * is kept even if an older read lands after it.\n *\n * Use this behind anything a person presses that promises them a fresh\n * reading — a Refresh button on a surface with no SSE invalidation, where\n * the whole reason the button exists is that nothing else will re-read.\n */\nexport function useRefresh<T>(def: QueryDef<T>): () => void {\n return useCallback(() => {\n void refetchQuery(def.key, () => def.fetch(), true);\n }, [def]);\n}\n\nexport function useAuthPhase(): AuthPhase {\n return useSyncExternalStore(subscribeAuth, () => getAuthSnapshot().phase, () => getAuthSnapshot().phase);\n}\n\nexport interface MutationHold {\n hasHold: boolean;\n token: string | null;\n heldUntil: number | null;\n setToken: (token: string) => void;\n touch: () => void;\n clear: () => void;\n}\n\nexport function useMutationHold(): MutationHold {\n const heldUntil = useSyncExternalStore(\n subscribeAuth,\n () => getAuthSnapshot().mutationTokenHeldUntil,\n () => getAuthSnapshot().mutationTokenHeldUntil,\n );\n return {\n hasHold: hasMutationHold(),\n token: getMutationToken(),\n heldUntil,\n setToken: storeSetMutationToken,\n touch: touchMutationHold,\n clear: clearMutationToken,\n };\n}\n\n/** Subscribe to a raw named SSE event for the lifetime of the component. */\nexport function useSseEvent(name: SseEventName, onEvent: (payload: unknown) => void): void {\n useEffect(() => onSseEvent(name, onEvent), [name, onEvent]);\n}\n\nexport interface ThemeControl {\n theme: ThemePreference;\n set: (next: ThemePreference) => void;\n cycle: () => void;\n}\n\n/** Shared with Topbar's theme button and the command palette's \"Toggle\n * theme\" action — both read/write data/theme-store.ts through this hook so\n * neither can drift out of sync with the other. */\nexport function useTheme(): ThemeControl {\n const theme = useSyncExternalStore(subscribeTheme, getTheme, getTheme);\n return { theme, set: setTheme, cycle: cycleTheme };\n}\n\nexport interface AppearanceControl {\n appearance: Appearance;\n /** Partial change; applied to <html> and persisted immediately (no save button). */\n set: (patch: Partial<Appearance>) => void;\n reset: () => void;\n}\n\n/**\n * The Settings section's appearance controls and anything that needs to READ\n * the current density/accent (e.g. a chart sizing rows) share this hook, for\n * the same reason useTheme exists: two readers of one external store, never\n * a second copy of the state.\n *\n * Importing this module is also what applies the stored appearance before\n * the first paint — every view imports hooks.ts, so appearance-store.ts is\n * evaluated (and has written <html>) before React renders anything.\n */\nexport function useAppearance(): AppearanceControl {\n const appearance = useSyncExternalStore(subscribeAppearance, getAppearance, getAppearance);\n return { appearance, set: setAppearance, reset: resetAppearance };\n}\n","/* V2: a tag, not a pill — the design language reserves fully-round shapes\n for dots and avatars. Height derives from the density tokens so a badge in\n a table row lines up with the controls next to it. */\n.badge {\n display: inline-flex;\n align-items: center;\n gap: var(--space-1);\n height: var(--density-row-sm);\n padding: 0 var(--space-2);\n border-radius: var(--radius-xs);\n font-size: var(--text-xs-size);\n font-weight: var(--font-weight-medium);\n line-height: 1;\n white-space: nowrap;\n}\n\n.dot {\n width: 6px;\n height: 6px;\n border-radius: var(--radius-full);\n flex-shrink: 0;\n}\n\n.neutral {\n background: var(--status-neutral-bg);\n color: var(--status-neutral-fg);\n}\n.neutral .dot {\n background: var(--status-neutral-solid);\n}\n\n.info {\n background: var(--status-info-bg);\n color: var(--status-info-fg);\n}\n.info .dot {\n background: var(--status-info-solid);\n}\n\n.running {\n background: var(--status-running-bg);\n color: var(--status-running-fg);\n}\n.running .dot {\n background: var(--status-running-solid);\n animation: pulse 1.6s ease-in-out infinite;\n}\n\n.success {\n background: var(--status-success-bg);\n color: var(--status-success-fg);\n}\n.success .dot {\n background: var(--status-success-solid);\n}\n\n.warning {\n background: var(--status-warning-bg);\n color: var(--status-warning-fg);\n}\n.warning .dot {\n background: var(--status-warning-solid);\n}\n\n.danger {\n background: var(--status-danger-bg);\n color: var(--status-danger-fg);\n}\n.danger .dot {\n background: var(--status-danger-solid);\n}\n\n.unknown {\n background: var(--status-unknown-bg);\n color: var(--status-unknown-fg);\n font-style: italic;\n}\n.unknown .dot {\n background: var(--status-unknown-solid);\n}\n\n.phaseDot {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: var(--radius-full);\n}\n\n@keyframes pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.4;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n :root:not([data-motion=\"full\"]) .running .dot {\n animation: none;\n }\n}\n\n:root[data-motion=\"reduce\"] .running .dot {\n animation: none;\n}\n","/**\n * components/primitives/StatusBadge.tsx — the ONE status-color mapping for\n * the whole app. See DESIGN.md \"Status-color semantics\" for the reasoning;\n * the short version: `running` is amber (motion, unresolved — matches\n * Vercel/GitHub Actions \"in progress\"), `done` is green (resolved,\n * successful). Neither reuses the accent color, so \"this is clickable\"\n * (accent) never visually collides with \"this succeeded\" (green).\n *\n * Every view should route run/swarm/proposal/daemon status strings through\n * `statusToTone()` rather than switching on raw strings itself, so adding a\n * new backend status value only ever needs one edit, here.\n */\nimport type { ReactNode } from 'react';\nimport styles from './StatusBadge.module.css';\n\nexport type Tone = 'neutral' | 'info' | 'running' | 'success' | 'warning' | 'danger' | 'unknown';\n\nconst STATUS_TONE: Record<string, Tone> = {\n // Run / swarm / proposal lifecycle.\n pending: 'neutral',\n queued: 'neutral',\n waiting: 'neutral',\n running: 'running',\n active: 'running',\n in_progress: 'running',\n building: 'running',\n applying: 'running',\n done: 'success',\n success: 'success',\n succeeded: 'success',\n applied: 'success',\n merged: 'success',\n ready: 'success',\n approved: 'success',\n ship: 'success',\n review: 'warning',\n degraded: 'warning',\n paused: 'warning',\n noise: 'neutral',\n failed: 'danger',\n error: 'danger',\n errored: 'danger',\n rejected: 'danger',\n harmful: 'danger',\n cancelled: 'neutral',\n canceled: 'neutral',\n unknown: 'unknown',\n};\n\nexport function statusToTone(status: string | undefined | null): Tone {\n if (!status) return 'unknown';\n return STATUS_TONE[status.toLowerCase()] ?? 'neutral';\n}\n\nexport interface StatusBadgeProps {\n status: string;\n /** Override the derived tone (rare — prefer letting statusToTone decide). */\n tone?: Tone;\n children?: ReactNode;\n}\n\nexport function StatusBadge({ status, tone, children }: StatusBadgeProps): ReactNode {\n const resolved = tone ?? statusToTone(status);\n return (\n <span className={`${styles.badge} ${styles[resolved]}`}>\n <span className={styles.dot} aria-hidden=\"true\" />\n {children ?? status}\n </span>\n );\n}\n\n/** A named DAG/milestone phase — categorical color, unrelated to health. */\nexport function PhaseDot({ index }: { index: number }): ReactNode {\n const n = ((index % 6) + 6) % 6; // wrap safely for any index, including negatives\n return <span className={styles.phaseDot} style={{ background: `var(--phase-${n + 1})` }} aria-hidden=\"true\" />;\n}\n","/**\n * data/client.ts — thin typed HTTP client for the existing /api/* surface.\n * No redefinition of response shapes here — callers type each call with the\n * interfaces re-exported from ./api-types.ts.\n */\nimport { getReadClientProof, reportSessionExpired } from './auth-store.js';\n\nexport class ApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly path: string,\n /**\n * The server's own sentence about the refusal, when it sent one, without\n * the \"POST … failed (HTTP N)\" wrapper. A surface that wants to show the\n * operator why an action was refused should prefer this over `message`:\n * it is the text the route author wrote for a person to read.\n */\n public readonly detail: string | null = null,\n /**\n * The route's machine-readable refusal code (`VERSE_SESSION_NOT_FOUND`,\n * `VERSE_SESSION_BUSY`, …) when the body carried one; null otherwise. Lets a\n * caller branch on WHAT was refused without parsing the sentence.\n */\n public readonly code: string | null = null,\n ) {\n super(message);\n this.name = 'ApiError';\n }\n}\n\n/**\n * Raised for the 404 a mutation route answers when --allow-dispatch is off.\n *\n * That gate answers a bare `{ error: 'not found' }` — deliberately the same\n * body as a route that does not exist, so a read-only server does not\n * advertise its write surface. A route that DOES exist and refuses with a 404\n * of its own (\"session not found\") says so with a `code`; apiPost keeps those\n * as plain ApiErrors, so a deleted chat is never reported as \"this server is\n * read-only\".\n */\nexport class DispatchDisabledError extends ApiError {\n constructor(path: string) {\n super('This server was started without --allow-dispatch, so mutations are disabled.', 404, path);\n this.name = 'DispatchDisabledError';\n }\n}\n\n/** GET an authenticated read route. 401 reports session-expired and throws. */\nexport async function apiGet<T>(path: string, signal?: AbortSignal): Promise<T> {\n const res = await fetch(path, {\n method: 'GET',\n credentials: 'same-origin',\n headers: { 'x-ashlr-read-client': getReadClientProof() },\n signal,\n });\n if (res.status === 401) {\n reportSessionExpired();\n throw new ApiError('Read session expired.', 401, path);\n }\n if (!res.ok) {\n throw new ApiError(`GET ${path} failed (HTTP ${res.status}).`, res.status, path);\n }\n return (await res.json()) as T;\n}\n\n/**\n * POST a mutating route with the raw mutation token. `mutationToken` must\n * come from the caller (data/mutations.ts pulls it from auth-store) — this\n * function does not read auth-store itself so it stays trivially testable.\n */\nexport async function apiPost<T>(\n path: string,\n body: unknown,\n mutationToken: string,\n signal?: AbortSignal,\n): Promise<T> {\n const res = await fetch(path, {\n method: 'POST',\n credentials: 'same-origin',\n headers: {\n 'Content-Type': 'application/json',\n 'x-ashlr-token': mutationToken,\n },\n body: JSON.stringify(body ?? {}),\n signal,\n });\n if (res.status === 401) {\n throw new ApiError('Mutation token was rejected.', 401, path);\n }\n if (!res.ok) {\n const { detail, code } = await readRefusal(res);\n // Only a CODELESS 404 is the dispatch gate (see DispatchDisabledError).\n // An unknown sub-path on an older server is codeless too and lands here\n // as well — the two are indistinguishable by design, so callers that\n // word this error must not assert which one it was.\n if (res.status === 404 && code === null) {\n throw new DispatchDisabledError(path);\n }\n throw new ApiError(\n `POST ${path} failed (HTTP ${res.status})${detail ? `: ${detail}` : ''}.`,\n res.status,\n path,\n detail || null,\n code,\n );\n }\n if (res.status === 204) return undefined as T;\n return (await res.json()) as T;\n}\n\n/**\n * The refusal sentence and code from a failed POST's body, if it had any.\n *\n * `error` is the documented refusal field, but the Verse control plane\n * answers a refused daemon/scope action with a full result body whose\n * plain-language sentence lives in `note` and which has NO `error` key at all\n * (see VerseDaemonActionResult). Reading only `error` threw away sentences\n * like \"no repositories are enrolled, so the loop would do nothing\" and left\n * the caller with a bare status code to guess from.\n */\nasync function readRefusal(res: Response): Promise<{ detail: string; code: string | null }> {\n try {\n const j = (await res.json()) as { error?: unknown; note?: unknown; code?: unknown };\n const detail = typeof j.error === 'string' && j.error ? j.error : typeof j.note === 'string' ? j.note : '';\n const code = typeof j.code === 'string' && j.code ? j.code : null;\n return { detail, code };\n } catch {\n /* body wasn't JSON */\n return { detail: '', code: null };\n }\n}\n\n/** Build the SSE URL, carrying the client proof as the query-string proof\n * EventSource can't attach as a header (see server.ts readSessionClientProof). */\nexport function eventsUrl(): string {\n return `/api/events?client=${encodeURIComponent(getReadClientProof())}`;\n}\n",".root {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n}\n\n.spinner {\n width: 12px;\n height: 12px;\n border-radius: var(--radius-full);\n border: 2px solid var(--border-strong);\n border-top-color: var(--accent-500);\n animation: spin 0.7s linear infinite;\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n :root:not([data-motion=\"full\"]) .spinner {\n animation: none;\n border-top-color: var(--border-strong);\n }\n}\n\n:root[data-motion=\"reduce\"] .spinner {\n animation: none;\n border-top-color: var(--border-strong);\n}\n","/**\n * components/primitives/RefreshIndicator.tsx — the \"stale data, quietly\n * refreshing\" signal (foundation brief item 4: never blank the screen on\n * refresh). Pair with a QueryEntry's `status`:\n *\n * loading -> render a Skeleton instead of the view\n * refreshing -> render the view with data, plus <RefreshIndicator />\n * success -> render the view with data, no indicator\n * error -> render the view's error state\n */\nimport styles from './RefreshIndicator.module.css';\n\nexport function RefreshIndicator({ label = 'Refreshing' }: { label?: string }) {\n return (\n <span className={styles.root} role=\"status\" aria-live=\"polite\">\n <span className={styles.spinner} aria-hidden=\"true\" />\n <span className=\"visually-hidden\">{label}</span>\n </span>\n );\n}\n",".grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n gap: var(--space-4);\n}\n\n/* No border and no fill: a skeleton stands in for CONTENT, and drawing it as\n a bordered card meant the placeholder had a box the real card does not\n (DESIGN-V2 §1.2) and the layout visibly lost an outline on first paint. */\n.card {\n display: flex;\n flex-direction: column;\n gap: var(--space-3);\n padding: var(--space-4) 0;\n}\n\n/* Each bar matches the LINE BOX of the text it replaces, so nothing reflows\n when the real content lands: a label line, a display-face stat, a caption. */\n.title {\n height: var(--text-xs-line);\n width: 60%;\n border-radius: var(--radius-xs);\n}\n\n.stat {\n height: var(--text-2xl-line);\n width: 40%;\n border-radius: var(--radius-xs);\n}\n\n.caption {\n height: var(--text-2xs-line);\n width: 80%;\n border-radius: var(--radius-xs);\n}\n\n.line {\n height: var(--text-base-line);\n border-radius: var(--radius-xs);\n}\n\n.row {\n display: flex;\n align-items: center;\n gap: var(--space-4);\n min-height: var(--density-row);\n padding: var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--border-subtle);\n}\n\n.cell {\n height: var(--text-sm-line);\n border-radius: var(--radius-xs);\n}\n","/**\n * components/primitives/Skeleton.tsx — loading placeholders. Used ONLY for\n * `status === 'loading'` (no data at all yet). A resource that already has\n * data and is merely refreshing should show <RefreshIndicator/> instead —\n * never re-blank a populated view (see DESIGN.md \"no blank screens\").\n */\nimport styles from './Skeleton.module.css';\n\nexport function SkeletonLine({ width = '100%' }: { width?: string }) {\n return <div className={`skeleton ${styles.line}`} style={{ width }} />;\n}\n\nexport function SkeletonCard() {\n return (\n <div className={styles.card}>\n <div className={`skeleton ${styles.title}`} />\n <div className={`skeleton ${styles.stat}`} />\n <div className={`skeleton ${styles.caption}`} />\n </div>\n );\n}\n\nexport function SkeletonRow() {\n return (\n <div className={styles.row}>\n <div className={`skeleton ${styles.cell}`} style={{ width: '18%' }} />\n <div className={`skeleton ${styles.cell}`} style={{ width: '38%' }} />\n <div className={`skeleton ${styles.cell}`} style={{ width: '14%' }} />\n <div className={`skeleton ${styles.cell}`} style={{ width: '10%' }} />\n </div>\n );\n}\n\nexport function SkeletonCardGrid({ count = 4 }: { count?: number }) {\n return (\n <div className={styles.grid}>\n {Array.from({ length: count }, (_, i) => (\n <SkeletonCard key={i} />\n ))}\n </div>\n );\n}\n",".skipLink {\n position: fixed;\n top: -100px;\n left: var(--space-3);\n z-index: var(--z-toast);\n background: var(--accent-500);\n color: var(--text-on-accent);\n padding: var(--space-2) var(--space-4);\n border-radius: var(--radius-md);\n font-size: var(--text-sm-size);\n font-weight: var(--font-weight-medium);\n transition: top var(--duration-fast) var(--ease-standard);\n}\n\n.skipLink:focus-visible {\n top: var(--space-3);\n box-shadow: var(--shadow-focus-ring);\n}\n","/**\n * app/SkipToContent.tsx — a real skip link, deliberately NOT `<a\n * href=\"#main-content\">`: this app uses HashRouter, so a literal `#`\n * anchor would rewrite location.hash and hijack client-side routing\n * instead of just scrolling. A button that imperatively focuses the\n * `#main-content` element (set on <main> in components/layout/Shell.tsx)\n * gets the same keyboard behavior without touching the route.\n */\nimport styles from './SkipToContent.module.css';\n\nexport function SkipToContent() {\n return (\n <button\n type=\"button\"\n className={styles.skipLink}\n onClick={() => {\n const main = document.getElementById('main-content');\n main?.focus();\n main?.scrollIntoView({ block: 'start' });\n }}\n >\n Skip to content\n </button>\n );\n}\n",".wrap {\n min-height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--space-6);\n background: var(--bg-canvas);\n}\n\n.card {\n width: min(480px, 100%);\n background: var(--bg-surface);\n border: 1px solid var(--border-default);\n border-radius: var(--radius-xl);\n box-shadow: var(--shadow-lg);\n padding: var(--space-8);\n}\n\n.badge {\n font-size: 28px;\n margin-bottom: var(--space-4);\n}\n\n.heading {\n font-size: var(--text-2xl-size);\n line-height: var(--text-2xl-line);\n font-weight: var(--font-weight-semibold);\n margin-bottom: var(--space-3);\n}\n\n.body {\n font-size: var(--text-sm-size);\n line-height: var(--text-sm-line);\n color: var(--text-secondary);\n margin-bottom: var(--space-3);\n}\n\n.code {\n font-family: var(--font-mono);\n font-size: var(--text-sm-size);\n background: var(--bg-code);\n border: 1px solid var(--border-subtle);\n border-radius: var(--radius-sm);\n padding: var(--space-3);\n margin-bottom: var(--space-3);\n overflow-x: auto;\n}\n\n.example {\n font-family: var(--font-mono);\n background: var(--bg-code);\n padding: 1px 4px;\n border-radius: var(--radius-xs);\n}\n\n.form {\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n margin-top: var(--space-5);\n}\n\n.label {\n font-size: var(--text-sm-size);\n font-weight: var(--font-weight-medium);\n}\n\n.input {\n padding: var(--space-3);\n border-radius: var(--radius-md);\n border: 1px solid var(--border-default);\n background: var(--bg-input);\n font-family: var(--font-mono);\n font-size: var(--text-sm-size);\n}\n\n.input:focus-visible {\n border-color: var(--border-focus);\n box-shadow: var(--shadow-focus-ring);\n}\n\n.error {\n color: var(--status-danger-fg);\n font-size: var(--text-sm-size);\n}\n\n.submit {\n margin-top: var(--space-2);\n padding: var(--space-3) var(--space-4);\n background: var(--accent-500);\n color: var(--text-on-accent);\n border-radius: var(--radius-md);\n font-weight: var(--font-weight-medium);\n text-align: center;\n transition: background var(--duration-fast) var(--ease-standard);\n}\n\n.submit:hover:not(:disabled) {\n background: var(--accent-600);\n}\n\n.submit:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.optional { font-weight: var(--font-weight-normal, 400); color: var(--text-tertiary); }\n","/**\n * components/auth/SessionGate.tsx — replaces the old bare \"⚠ HTTP 401 +\n * Retry\" first-run experience (foundation brief item 5). Shown whenever\n * auth-store's phase is 'unauthenticated': on first load before any read\n * token has been entered, AND any time a session expires mid-use (SSE\n * disconnects, the next GET reports 401).\n *\n * By default this screen owns nothing about the MUTATION token — that is a\n * separate, later step (MutationTokenDialog), because read authority and\n * dispatch authority are genuinely different capabilities on this server\n * (see server.ts). Surfaces where sending is the whole point (Verse) can opt\n * into an optional second field with `mutationField`; the authorities stay\n * separate (the mutation token only ever enters the memory-only hold), it\n * just spares the operator a second paste screen on the first Enter.\n */\nimport { useEffect, useId, useRef, useState, type FormEvent } from 'react';\nimport { adoptInjectedTokens, establishReadSession, setMutationToken } from '../../data/auth-store.js';\nimport styles from './SessionGate.module.css';\n\nconst TOKEN_RE = /^[a-f0-9]{64}$/;\n\nexport interface SessionGateProps {\n onAuthenticated?: () => void;\n heading?: string;\n command?: string;\n /** What the copy calls this surface (\"This dashboard\", \"Ashlr Verse\"). */\n subject?: string;\n /**\n * Also offer the mutation token on the same screen. The two authorities\n * stay separate (a pasted mutation token goes straight into the memory-only\n * hold, exactly as MutationTokenDialog would set it); this only collapses\n * two paste screens into one for surfaces where sending is the whole point.\n */\n mutationField?: boolean;\n}\n\nexport function SessionGate({ onAuthenticated, heading = 'Connect to ashlr serve', command = 'ashlr serve', subject = 'This dashboard', mutationField = false }: SessionGateProps) {\n const [token, setToken] = useState('');\n const [mutation, setMutation] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [busy, setBusy] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n const inputId = useId();\n const mutationId = useId();\n const errorId = useId();\n const authenticatedRef = useRef(onAuthenticated);\n authenticatedRef.current = onAuthenticated;\n\n // Desktop wrapper: when the host injected tokens (window.__ASHLR_TOKENS__),\n // establish the session silently instead of asking for a paste.\n useEffect(() => {\n let cancelled = false;\n void adoptInjectedTokens().then((adopted) => {\n if (adopted && !cancelled) authenticatedRef.current?.();\n });\n return () => { cancelled = true; };\n }, []);\n\n async function onSubmit(e: FormEvent) {\n e.preventDefault();\n setError(null);\n const mutationTrimmed = mutation.trim();\n if (mutationField && mutationTrimmed && !TOKEN_RE.test(mutationTrimmed)) {\n setError('That does not look like a mutation token — expected 64 hex characters (leave it empty to unlock later).');\n return;\n }\n setBusy(true);\n try {\n await establishReadSession(token);\n setToken('');\n if (mutationField && mutationTrimmed) setMutationToken(mutationTrimmed);\n setMutation('');\n onAuthenticated?.();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Could not establish a session.');\n inputRef.current?.select();\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <div className={styles.wrap}>\n <div className={styles.card}>\n <div className={styles.badge} aria-hidden=\"true\">\n ⛺\n </div>\n <h1 className={styles.heading}>{heading}</h1>\n <p className={styles.body}>\n {subject} only ever talks to the ashlr server running on this machine\n (<code>127.0.0.1</code>, never a remote host). To read data, paste the{' '}\n <strong>read token</strong> printed in the terminal where you ran:\n </p>\n <pre className={styles.code}>{command}</pre>\n <p className={styles.body}>\n It looks like a 64-character hex string, printed once at startup — something like{' '}\n <code className={styles.example}>a1b2c3…</code>. This dialog never stores it: it is\n exchanged once for a short-lived session cookie, then discarded.\n </p>\n <form onSubmit={onSubmit} className={styles.form} noValidate>\n <label htmlFor={inputId} className={styles.label}>\n Read token\n </label>\n <input\n ref={inputRef}\n id={inputId}\n name=\"read-token\"\n type=\"password\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n inputMode=\"text\"\n className={styles.input}\n value={token}\n onChange={(e) => setToken(e.target.value)}\n aria-invalid={error ? true : undefined}\n aria-describedby={error ? errorId : undefined}\n placeholder=\"64 hex characters\"\n autoFocus\n />\n {mutationField ? (\n <>\n <label htmlFor={mutationId} className={styles.label}>\n Mutation token <span className={styles.optional}>optional — paste it now to enable sending</span>\n </label>\n <input\n id={mutationId}\n name=\"mutation-token\"\n type=\"password\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n inputMode=\"text\"\n className={styles.input}\n value={mutation}\n onChange={(e) => setMutation(e.target.value)}\n placeholder=\"64 hex characters, printed under “Dispatch enabled”\"\n />\n </>\n ) : null}\n {error ? (\n <p id={errorId} className={styles.error} role=\"alert\">\n {error}\n </p>\n ) : null}\n <button type=\"submit\" className={styles.submit} disabled={busy || token.trim().length === 0}>\n {busy ? 'Connecting…' : 'Connect'}\n </button>\n </form>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}