@pellux/goodvibes-tui 1.0.0 → 1.7.0

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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Per-turn knowledge injection record rendering (Wave-5 W5.2, wo803).
3
+ *
4
+ * The SDK's passive per-turn retrieval engine (wo801, W5.1 —
5
+ * packages/sdk/src/platform/agents/turn-knowledge-injection.ts) stores one
6
+ * `TurnInjectionRecord` per turn on `AgentRecord.turnInjections` (a bounded
7
+ * ring, default 20 entries) and appends the same record to the agent's
8
+ * session transcript as `{type:'knowledge_injection', turn, ...record}`.
9
+ *
10
+ * `TurnInjectionRecord` itself is not re-exported from the SDK's
11
+ * `platform/agents` barrel (only `AgentRecord.turnInjections` carries its
12
+ * structural shape through the already-public `platform/tools` barrel), so
13
+ * the entry type here is DERIVED from `AgentRecord` rather than imported by
14
+ * name — this needs no SDK export change.
15
+ *
16
+ * Reality check (updated wo805): as of Wave-5 wo805 the TUI's main interactive
17
+ * session DOES route through this engine. The SDK `Orchestrator` runs per-turn
18
+ * passive injection on the evolving primary conversation and records each turn
19
+ * on its own bounded ring, exposed via `Orchestrator.getTurnInjections()` — the
20
+ * main-session counterpart to `AgentRecord.turnInjections` (there is no
21
+ * AgentRecord for the primary conversation). `buildMainSessionTurnInjectionsText`
22
+ * renders that ring for `/recall injections` with no agent id. Spawned agents
23
+ * (Task-tool runs, automation-triggered runs, session continuations) still
24
+ * record onto their own `AgentRecord.turnInjections`, rendered per-agent by
25
+ * `buildTurnInjectionsText` when an explicit agent id is given.
26
+ */
27
+ import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
28
+
29
+ /** One TurnInjectionRecord entry, derived from AgentRecord (see module doc). */
30
+ export type TurnInjectionEntry = NonNullable<AgentRecord['turnInjections']>[number];
31
+
32
+ function fmtN(n: number): string {
33
+ return n.toLocaleString();
34
+ }
35
+
36
+ /** Render a single TurnInjectionRecord as one readable line. */
37
+ export function formatTurnInjectionEntry(entry: TurnInjectionEntry): string {
38
+ const backendTag = entry.embeddingBackend === 'fallback-lexical' ? ' [lexical fallback]' : '';
39
+ if (entry.injectedIds.length === 0) {
40
+ // Honest empty state: the engine ran this turn but injected nothing —
41
+ // distinct from "the engine never ran" (see buildTurnInjectionsText).
42
+ const reasonText = entry.reason === 'no records cleared relevance floor'
43
+ ? 'nothing injected this turn — nothing cleared the relevance floor'
44
+ : `nothing injected this turn — ${entry.reason ?? 'unknown reason'}`;
45
+ return ` turn ${entry.turn}: ${reasonText}${backendTag} (considered ${entry.candidatesConsidered}, floor ${entry.relevanceFloor})`;
46
+ }
47
+ const droppedStr = entry.droppedForBudget.length > 0
48
+ ? `, dropped for budget: ${entry.droppedForBudget.join(', ')}`
49
+ : '';
50
+ // The retrieval query is part of the record's honesty contract — without it
51
+ // an injected line can't be traced back to WHY those ids were retrieved
52
+ // (Wave-5 replay flagged the omission). Truncated to keep the line scannable.
53
+ const queryStr = entry.query ? ` for ${JSON.stringify(truncateQuery(entry.query))}` : '';
54
+ return (
55
+ ` turn ${entry.turn}: injected ${entry.injectedIds.join(', ')}${queryStr} ` +
56
+ `(~${fmtN(entry.tokenCost)}/${fmtN(entry.budgetTokens)} tok, floor ${entry.relevanceFloor})${droppedStr}${backendTag}`
57
+ );
58
+ }
59
+
60
+ function truncateQuery(query: string): string {
61
+ const flat = query.replace(/\s+/g, ' ').trim();
62
+ return flat.length > 48 ? `${flat.slice(0, 47)}…` : flat;
63
+ }
64
+
65
+ /**
66
+ * Render the full per-turn injection history for one agent, most-recent-turn
67
+ * first, with an honest empty state when nothing has been recorded yet.
68
+ *
69
+ * An empty `entries` array is deliberately ambiguous about WHY it's empty
70
+ * (flag disabled, no turn with new input has run yet, or every turn's token
71
+ * budget had no headroom) — the SDK does not distinguish these cases in the
72
+ * ring itself, so this renders all three possibilities rather than guessing.
73
+ */
74
+ export function buildTurnInjectionsText(agentId: string, entries: readonly TurnInjectionEntry[]): string {
75
+ if (entries.length === 0) {
76
+ return (
77
+ `[recall] No per-turn injection records for agent ${agentId} yet. This means one of: ` +
78
+ 'passive knowledge injection is disabled, no turn with new input has run yet, or the ' +
79
+ 'token budget had no headroom on every turn so far — there is no record either way.'
80
+ );
81
+ }
82
+ const lines = [`[recall] Per-turn knowledge injections for agent ${agentId} (${entries.length}, most recent first):`];
83
+ for (const entry of [...entries].reverse()) {
84
+ lines.push(formatTurnInjectionEntry(entry));
85
+ }
86
+ return lines.join('\n');
87
+ }
88
+
89
+ /**
90
+ * Render the MAIN interactive session's per-turn injection ring
91
+ * (`Orchestrator.getTurnInjections()`, wo805), most-recent-turn first, with the
92
+ * same honest empty state as the per-agent path. The main-session counterpart
93
+ * to {@link buildTurnInjectionsText}: no agent id, main-session-appropriate
94
+ * wording, but the identical per-entry rendering (`formatTurnInjectionEntry`).
95
+ *
96
+ * As with the agent path, an empty `entries` array is deliberately ambiguous
97
+ * about WHY it's empty (flag disabled, no turn with new input has run yet, or
98
+ * every turn's token budget had no headroom) — the ring does not distinguish
99
+ * these, so this renders all three possibilities rather than guessing.
100
+ */
101
+ export function buildMainSessionTurnInjectionsText(entries: readonly TurnInjectionEntry[]): string {
102
+ if (entries.length === 0) {
103
+ return (
104
+ '[recall] No per-turn injection records for the main session yet. This means one of: ' +
105
+ 'passive knowledge injection is disabled, no turn with new input has run yet, or the ' +
106
+ 'token budget had no headroom on every turn so far — there is no record either way.'
107
+ );
108
+ }
109
+ const lines = [`[recall] Per-turn knowledge injections for the main session (${entries.length}, most recent first):`];
110
+ for (const entry of [...entries].reverse()) {
111
+ lines.push(formatTurnInjectionEntry(entry));
112
+ }
113
+ return lines.join('\n');
114
+ }
@@ -8,13 +8,36 @@ import { GLYPHS, UI_TONES } from './ui-primitives.ts';
8
8
  import { formatElapsed } from '../utils/format-elapsed.ts';
9
9
  import { abbreviateCount } from '../utils/format-number.ts';
10
10
  import { computeContextUsage } from '../core/context-usage.ts';
11
- import { calcSessionCost } from '../export/cost-utils.ts';
11
+ import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
12
12
  import { buildFooterTip, isAgentActive } from './footer-tips.ts';
13
+ import type { StreamMetrics } from '../core/stream-event-wiring.ts';
13
14
 
14
15
  /** Number of frames before the animated gradient completes one full cycle. */
15
16
  const GRADIENT_CYCLE_FRAMES = 50;
16
17
  /** Number of frames before rotating to the next thinking phrase (~30 seconds at 80ms/frame). */
17
18
  const PHRASE_ROTATION_FRAMES = 375;
19
+ /**
20
+ * Ms since the last STREAM_DELTA before the whimsical phrase rotation freezes
21
+ * and createThinkingFragment shows an honest "stalled Ns" / "reconnecting"
22
+ * label instead. Deliberately much shorter than the 30s stream-stall-watchdog
23
+ * hint threshold (stream-stall-watchdog.ts) — that threshold gates a
24
+ * low-priority system message about a likely-dead connection; this one gates
25
+ * a cosmetic label so the UI stops claiming "Vibing..." within a couple of
26
+ * seconds of real silence, well before the stall is confirmed as a problem.
27
+ */
28
+ const THINKING_STALL_FREEZE_MS = 2_500;
29
+
30
+ /**
31
+ * Stall/reconnect state for the live thinking indicator, computed by the
32
+ * caller every render frame from streamMetrics (see stream-event-wiring.ts).
33
+ * `reconnect` is populated only once the SDK's STREAM_RETRY event fires
34
+ * (structurally consumed — absent from SDK 0.35.0's TurnEvent union today).
35
+ */
36
+ export interface ThinkingStallInfo {
37
+ /** Ms since the last STREAM_DELTA (or STREAM_START if none yet this turn). */
38
+ readonly msSinceLastDelta: number;
39
+ readonly reconnect?: { readonly attempt: number; readonly maxAttempts: number };
40
+ }
18
41
 
19
42
  /** Build the git segment string and its display width. Single source of truth for header layout. */
20
43
  function buildGitSegment(gitInfo: GitHeaderInfo): { text: string; width: number } {
@@ -313,7 +336,15 @@ export class UIFactory {
313
336
  const cw = u.cacheWrite ?? 0;
314
337
  const total = inp + out + cr + cw;
315
338
  const tokenSep = ` ${GLYPHS.navigation.pipeSeparator} `;
316
- const costSegment = model ? `${tokenSep}~$${fmtCost(calcSessionCost(inp, out, cr, cw, model))}` : '';
339
+ // 'n/a' (not 'unpriced') to stay compact in the single-line footer and
340
+ // match the existing "no priceable data" convention used elsewhere
341
+ // (cockpit-panel formatCost, agent-inspector-shared) — the footer has no
342
+ // room for a longer marker before truncation kicks in.
343
+ const costSegment = model
344
+ ? isModelPriced(model)
345
+ ? `${tokenSep}~$${fmtCost(calcSessionCost(inp, out, cr, cw, model))}`
346
+ : `${tokenSep}~n/a`
347
+ : '';
317
348
  const tokenLine = ` Token Usage [ Input: ${fmtNum(inp)}${tokenSep}Output: ${fmtNum(out)}${tokenSep}Cache Read: ${fmtNum(cr)}${tokenSep}Cache Write: ${fmtNum(cw)}${tokenSep}Total: ${fmtNum(total)}${costSegment} ]`;
318
349
  const copiedNotice = isRecentlyCopied ? ` [COPIED] ` : '';
319
350
  const statsLine = ' ' + tokenLine + ' '.repeat(Math.max(0, width - 4 - getDisplayWidth(tokenLine) - getDisplayWidth(copiedNotice))) + copiedNotice;
@@ -345,7 +376,11 @@ export class UIFactory {
345
376
  ctxParts.push(model + (provider ? ` (${provider})` : ''));
346
377
  }
347
378
  if (toolCount) ctxParts.push(`${toolCount} tools`);
348
- if (hitlMode) ctxParts.push(`hitl:${hitlMode}`);
379
+ // Labeled "notify" (not "hitl") — /mode (aliased /hitl) governs UX
380
+ // notification verbosity (quiet/balanced/operator), not tool
381
+ // auto-approval, so it must not share vocabulary with the DANGER MODE
382
+ // risk banner rendered a few lines below.
383
+ if (hitlMode) ctxParts.push(`notify:${hitlMode}`);
349
384
  const ctxLine = ' ' + ctxParts.join(` ${GLYPHS.navigation.pipeSeparator} `);
350
385
  lines.push(this.stringToLine(truncateDisplay(ctxLine, width), width, { fg: '240', dim: true }));
351
386
  }
@@ -406,10 +441,59 @@ export class UIFactory {
406
441
  private static readonly THINK_GRADIENT_START = UI_TONES.accent.gradientStart;
407
442
  private static readonly THINK_GRADIENT_END = UI_TONES.accent.gradientEnd;
408
443
 
409
- public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number, elapsedMs?: number, ttftMs?: number): Line[] {
410
- // Rotate phrase every ~30 seconds (frame ticks at 80ms, so ~375 frames)
411
- const phraseIndex = Math.floor(frame / PHRASE_ROTATION_FRAMES) % this.THINKING_PHRASES.length;
412
- const phrase = this.THINKING_PHRASES[phraseIndex];
444
+ /**
445
+ * Per-frame stall info from stream metrics computed from lastDeltaAtMs every render (not
446
+ * from any event) so it degrades gracefully with zero new SDK events. Undefined until the
447
+ * first delta clock exists this turn.
448
+ */
449
+ public static computeStallInfo(lastDeltaAtMs: number | undefined, reconnectAttempt: number | undefined, reconnectMaxAttempts: number | undefined, nowMs: number): ThinkingStallInfo | undefined {
450
+ if (lastDeltaAtMs === undefined) return undefined;
451
+ const reconnect = reconnectAttempt !== undefined && reconnectMaxAttempts !== undefined
452
+ ? { attempt: reconnectAttempt, maxAttempts: reconnectMaxAttempts }
453
+ : undefined;
454
+ return { msSinceLastDelta: nowMs - lastDeltaAtMs, reconnect };
455
+ }
456
+
457
+ /**
458
+ * Render-frame stall-info decision used at the main render loop's call
459
+ * site: suppress stall detection entirely while a tool is actively
460
+ * executing. lastDeltaAtMs only tracks STREAM_START/STREAM_DELTA and is
461
+ * never advanced during tool execution (the model isn't producing tokens
462
+ * then), so without this gate any tool call longer than
463
+ * THINKING_STALL_FREEZE_MS would make the thinking fragment print
464
+ * "Stalled Ns..." directly above the ticking "executing (Ns)" tool row — a
465
+ * false positive during ordinary tool execution (see stream-event-wiring.ts
466
+ * TOOL_EXECUTING/TOOL_SUCCEEDED/TOOL_FAILED/TOOL_CANCELLED handlers).
467
+ * Genuine no-delta silence while waiting on the provider — including the
468
+ * pre-first-token case, where lastDeltaAtMs is seeded at STREAM_START —
469
+ * still stall-detects normally here, since no tool is active then; that is
470
+ * the honest stall case this indicator exists for.
471
+ */
472
+ public static computeRenderStallInfo(
473
+ metrics: Pick<StreamMetrics, 'activeToolName' | 'lastDeltaAtMs' | 'reconnectAttempt' | 'reconnectMaxAttempts'>,
474
+ nowMs: number,
475
+ ): ThinkingStallInfo | undefined {
476
+ return metrics.activeToolName === undefined
477
+ ? this.computeStallInfo(metrics.lastDeltaAtMs, metrics.reconnectAttempt, metrics.reconnectMaxAttempts, nowMs)
478
+ : undefined;
479
+ }
480
+
481
+ public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number, elapsedMs?: number, ttftMs?: number, stallInfo?: ThinkingStallInfo): Line[] {
482
+ // Freeze the whimsical phrase rotation once real silence has gone on
483
+ // long enough to be misleading (THINKING_STALL_FREEZE_MS), and show an
484
+ // honest label instead: the SDK's reconnect attempt/maxAttempts once
485
+ // STREAM_RETRY is available, else a plain elapsed-silence readout.
486
+ const isStalled = stallInfo !== undefined && stallInfo.msSinceLastDelta >= THINKING_STALL_FREEZE_MS;
487
+ let phrase: string;
488
+ if (stallInfo?.reconnect) {
489
+ phrase = `Reconnecting (attempt ${stallInfo.reconnect.attempt}/${stallInfo.reconnect.maxAttempts})...`;
490
+ } else if (isStalled) {
491
+ phrase = `Stalled ${Math.floor(stallInfo.msSinceLastDelta / 1000)}s...`;
492
+ } else {
493
+ // Rotate phrase every ~30 seconds (frame ticks at 80ms, so ~375 frames)
494
+ const phraseIndex = Math.floor(frame / PHRASE_ROTATION_FRAMES) % this.THINKING_PHRASES.length;
495
+ phrase = this.THINKING_PHRASES[phraseIndex];
496
+ }
413
497
  const speedSuffix = (tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
414
498
  const elapsedSuffix = elapsedMs !== undefined ? ` (${formatElapsed(elapsedMs)})` : '';
415
499
  const ttftSuffix = (ttftMs !== undefined && ttftMs > 0) ? ` ttft:${ttftMs}ms` : '';
@@ -16,6 +16,7 @@ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
16
16
  import type { ForensicsRegistry } from '@/runtime/index.ts';
17
17
  import type { PolicyRuntimeState } from '@/runtime/index.ts';
18
18
  import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
19
+ import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
19
20
  import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
20
21
  import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
21
22
  import type { IntegrationHelperService } from '@/runtime/index.ts';
@@ -61,6 +62,10 @@ export type CreateBootstrapCommandContextOptions = {
61
62
  mcpRegistry: McpRegistry;
62
63
  voiceProviderRegistry?: VoiceProviderRegistry;
63
64
  voiceService?: VoiceService;
65
+ /** B31: direct-command consumers (`/search`, `/image`) of already-constructed RuntimeServices. */
66
+ webSearchService?: import('@pellux/goodvibes-sdk/platform/web-search').WebSearchService;
67
+ mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
68
+ artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
64
69
  forensicsRegistry: ForensicsRegistry;
65
70
  policyRuntimeState: PolicyRuntimeState;
66
71
  readModels: UiReadModels;
@@ -68,6 +73,7 @@ export type CreateBootstrapCommandContextOptions = {
68
73
  remoteRuntime?: RemoteCommandService;
69
74
  planRuntime?: PlanRuntimeService;
70
75
  fileUndoManager: FileUndoManager;
76
+ workspaceCheckpointManager?: WorkspaceCheckpointManager;
71
77
  memoryRegistry?: MemoryRegistry;
72
78
  integrationHelpers?: IntegrationHelperService;
73
79
  automationManager?: ShellAutomationManagerRuntimeService;
@@ -114,6 +120,10 @@ export type CreateBootstrapCommandContextOptions = {
114
120
  sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
115
121
  wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
116
122
  componentHealthMonitor: import('@/runtime/index.ts').ComponentHealthMonitor;
123
+ hydrateSessionUsage?: () => void;
124
+ workstreamEngine?: import('./workstream-services.ts').WorkstreamCommandService;
125
+ codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
126
+ getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
117
127
  };
118
128
 
119
129
  export function createBootstrapCommandContext(
@@ -131,6 +141,9 @@ export function createBootstrapCommandContext(
131
141
  mcpRegistry,
132
142
  voiceProviderRegistry,
133
143
  voiceService,
144
+ webSearchService,
145
+ mediaProviders,
146
+ artifactStore,
134
147
  forensicsRegistry,
135
148
  policyRuntimeState,
136
149
  readModels,
@@ -138,6 +151,7 @@ export function createBootstrapCommandContext(
138
151
  remoteRuntime,
139
152
  planRuntime,
140
153
  fileUndoManager,
154
+ workspaceCheckpointManager,
141
155
  memoryRegistry,
142
156
  integrationHelpers,
143
157
  automationManager,
@@ -167,6 +181,10 @@ export function createBootstrapCommandContext(
167
181
  sessionLineageTracker,
168
182
  wrfcController,
169
183
  changeTracker,
184
+ hydrateSessionUsage,
185
+ workstreamEngine,
186
+ codeIndexStore,
187
+ getMainSessionTurnInjections,
170
188
  planManager,
171
189
  adaptivePlanner,
172
190
  sessionOrchestration,
@@ -223,6 +241,10 @@ export function createBootstrapCommandContext(
223
241
  sessionLineageTracker,
224
242
  wrfcController,
225
243
  changeTracker,
244
+ hydrateSessionUsage,
245
+ workstreamEngine,
246
+ codeIndexStore,
247
+ getMainSessionTurnInjections,
226
248
  });
227
249
  const provider = createBootstrapCommandProviderSection({
228
250
  providerRegistry,
@@ -233,6 +255,7 @@ export function createBootstrapCommandContext(
233
255
  const workspace = createBootstrapCommandWorkspaceSection({
234
256
  keybindingsManager,
235
257
  fileUndoManager,
258
+ workspaceCheckpointManager,
236
259
  panelManager,
237
260
  profileManager,
238
261
  bookmarkManager,
@@ -240,7 +263,7 @@ export function createBootstrapCommandContext(
240
263
  projectPlanningProjectId,
241
264
  workPlanStore,
242
265
  }, shellServices);
243
- const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService }, shellServices);
266
+ const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService, webSearchService, mediaProviders, artifactStore }, shellServices);
244
267
  const extensions = createBootstrapCommandExtensionsSection({
245
268
  toolRegistry,
246
269
  mcpRegistry,
@@ -16,7 +16,8 @@ import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/pe
16
16
  import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
17
17
  import type { ForensicsRegistry } from '@/runtime/index.ts';
18
18
  import type { PolicyRuntimeState } from '@/runtime/index.ts';
19
- import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
19
+ import type { CodeIndexStore, FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
20
+ import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
20
21
  import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
21
22
  import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
22
23
  import type { IntegrationHelperService } from '@/runtime/index.ts';
@@ -79,11 +80,16 @@ export interface BootstrapCommandSectionOptions {
79
80
  readonly mcpRegistry: McpRegistry;
80
81
  readonly voiceProviderRegistry?: VoiceProviderRegistry;
81
82
  readonly voiceService?: VoiceService;
83
+ /** B31: direct-command consumers (`/search`, `/image`) of already-constructed RuntimeServices. */
84
+ readonly webSearchService?: import('@pellux/goodvibes-sdk/platform/web-search').WebSearchService;
85
+ readonly mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
86
+ readonly artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
82
87
  readonly forensicsRegistry: ForensicsRegistry;
83
88
  readonly policyRuntimeState: PolicyRuntimeState;
84
89
  readonly readModels: UiReadModels;
85
90
  readonly shellPaths: ShellPathService;
86
91
  readonly fileUndoManager: FileUndoManager;
92
+ readonly workspaceCheckpointManager?: WorkspaceCheckpointManager;
87
93
  readonly memoryRegistry?: MemoryRegistry;
88
94
  readonly integrationHelpers?: IntegrationHelperService;
89
95
  readonly knowledgeService?: KnowledgeService;
@@ -109,6 +115,10 @@ export interface BootstrapCommandSectionOptions {
109
115
  readonly sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
110
116
  readonly wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
111
117
  readonly changeTracker?: import('@pellux/goodvibes-sdk/platform/sessions').SessionChangeTracker;
118
+ readonly hydrateSessionUsage?: () => void;
119
+ readonly workstreamEngine?: import('./workstream-services.ts').WorkstreamCommandService;
120
+ readonly codeIndexStore?: CodeIndexStore;
121
+ readonly getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
112
122
  readonly agentManager?: ShellAgentManagerService;
113
123
  readonly modeManager?: ShellModeManagerService;
114
124
  readonly automationManager?: ShellAutomationManagerRuntimeService;
@@ -181,8 +191,15 @@ export function createBootstrapCommandActions(
181
191
  } = options;
182
192
 
183
193
  const showPanel = (panelId: string, pane?: 'top' | 'bottom') => {
194
+ // W6.1 (the purge): a MIGRATE-TO-MODAL id resolves to a modal, not a panel.
195
+ // panelManager.open() fires the injected openModal callback and returns a
196
+ // no-op sentinel — so skip panelManager.show() (which would reveal an empty
197
+ // panel workspace behind the modal) when this id redirects. Keeps every
198
+ // showPanel-based front-door (openHooksPanel/openSecurityPanel/… and the
199
+ // migrated command runtimes) opening the modal cleanly.
200
+ const redirected = panelManager.getModalRedirect(panelId) !== undefined;
184
201
  panelManager.open(panelId, pane);
185
- panelManager.show();
202
+ if (!redirected) panelManager.show();
186
203
  requestRender();
187
204
  };
188
205
 
@@ -278,11 +295,15 @@ export function createBootstrapCommandActions(
278
295
  openMemoryPanel: () => {
279
296
  showPanel('memory');
280
297
  },
298
+ // W6.1: remote/subscription migrated to config-modal surfaces. open() hits
299
+ // the modal redirect and invokes the openModal callback — do NOT go through
300
+ // showPanel here, which would additionally reveal + focus an (empty) panel
301
+ // workspace behind the fullscreen modal.
281
302
  openRemotePanel: () => {
282
- showPanel('remote');
303
+ panelManager.open('remote');
283
304
  },
284
305
  openSubscriptionPanel: () => {
285
- showPanel('subscription');
306
+ panelManager.open('subscription');
286
307
  },
287
308
  openLocalAuthMaskedEntry: (kind, username) => {
288
309
  showPanel('local-auth');
@@ -300,7 +321,7 @@ export function createBootstrapCommandActions(
300
321
  export function createBootstrapCommandSessionSection(
301
322
  options: Pick<
302
323
  BootstrapCommandSectionOptions,
303
- 'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker'
324
+ 'conversation' | 'runtime' | 'sessionManager' | 'sessionMemoryStore' | 'sessionLineageTracker' | 'wrfcController' | 'changeTracker' | 'hydrateSessionUsage' | 'workstreamEngine' | 'codeIndexStore' | 'getMainSessionTurnInjections'
304
325
  >,
305
326
  ): BootstrapCommandSessionSection {
306
327
  return {
@@ -311,6 +332,10 @@ export function createBootstrapCommandSessionSection(
311
332
  sessionLineageTracker: options.sessionLineageTracker,
312
333
  wrfcController: options.wrfcController,
313
334
  changeTracker: options.changeTracker,
335
+ hydrateSessionUsage: options.hydrateSessionUsage,
336
+ workstreamEngine: options.workstreamEngine,
337
+ codeIndexStore: options.codeIndexStore,
338
+ getMainSessionTurnInjections: options.getMainSessionTurnInjections,
314
339
  };
315
340
  }
316
341
 
@@ -331,7 +356,7 @@ export function createBootstrapCommandProviderSection(
331
356
  export function createBootstrapCommandWorkspaceSection(
332
357
  options: Pick<
333
358
  BootstrapCommandSectionOptions,
334
- 'keybindingsManager' | 'fileUndoManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
359
+ 'keybindingsManager' | 'fileUndoManager' | 'workspaceCheckpointManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
335
360
  | 'projectPlanningService' | 'projectPlanningProjectId' | 'workPlanStore'
336
361
  >,
337
362
  shellServices: BootstrapCommandShellServices,
@@ -339,6 +364,7 @@ export function createBootstrapCommandWorkspaceSection(
339
364
  return {
340
365
  keybindingsManager: options.keybindingsManager,
341
366
  fileUndoManager: options.fileUndoManager,
367
+ workspaceCheckpointManager: options.workspaceCheckpointManager,
342
368
  panelManager: options.panelManager,
343
369
  profileManager: options.profileManager,
344
370
  bookmarkManager: options.bookmarkManager,
@@ -352,7 +378,7 @@ export function createBootstrapCommandWorkspaceSection(
352
378
  export function createBootstrapCommandPlatformSection(
353
379
  options: Pick<
354
380
  BootstrapCommandSectionOptions,
355
- 'configManager' | 'voiceProviderRegistry' | 'voiceService'
381
+ 'configManager' | 'voiceProviderRegistry' | 'voiceService' | 'webSearchService' | 'mediaProviders' | 'artifactStore'
356
382
  >,
357
383
  shellServices: BootstrapCommandShellServices,
358
384
  ): BootstrapCommandPlatformSection {
@@ -361,6 +387,9 @@ export function createBootstrapCommandPlatformSection(
361
387
  configManager: options.configManager,
362
388
  voiceProviderRegistry: options.voiceProviderRegistry,
363
389
  voiceService: options.voiceService,
390
+ webSearchService: options.webSearchService,
391
+ mediaProviders: options.mediaProviders,
392
+ artifactStore: options.artifactStore,
364
393
  ...shellServices.platform,
365
394
  };
366
395
  }
@@ -7,7 +7,6 @@ import { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
7
7
  import { registerAllTools } from '@pellux/goodvibes-sdk/platform/tools';
8
8
  import { PermissionManager, createPermissionConfigReader } from '@pellux/goodvibes-sdk/platform/permissions';
9
9
  import { Notifier } from '@pellux/goodvibes-sdk/platform/integrations';
10
- import { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
11
10
  import { Compositor } from '../renderer/compositor.ts';
12
11
  import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions';
13
12
  import type { SystemMessageRouter } from '../core/system-message-router.ts';
@@ -28,11 +27,12 @@ import { loadBootstrapSystemPrompt, syncConfiguredServices } from '@/runtime/ind
28
27
  import { registerBootstrapHookBridge } from '@/runtime/index.ts';
29
28
  import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
30
29
  import { createRuntimeServices, type RuntimeServices } from './services.ts';
30
+ import { setPricingSource } from '../export/cost-utils.ts';
31
31
  import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
32
32
  import { join } from 'node:path';
33
33
  import { installWrfcAgentToolGuard } from '../tools/wrfc-agent-guard.ts';
34
34
  import { createWrfcPersistence, type WrfcPersistence } from './wrfc-persistence.ts';
35
- import type { SystemMessagePriority } from '../panels/system-messages-panel.ts';
35
+ import type { SystemMessagePriority } from '../core/system-message-router.ts';
36
36
 
37
37
  // ---------------------------------------------------------------------------
38
38
  // Pre-router buffer
@@ -255,6 +255,10 @@ export async function initializeBootstrapCore(
255
255
  providerRegistry.initModelLimits();
256
256
  services.benchmarkStore.initBenchmarks();
257
257
  providerRegistry.initCatalog();
258
+ // Wire cost-utils to the live catalog so cost displays distinguish real
259
+ // pricing from unpriced (WO-315) instead of silently reading zero for any
260
+ // model the small static fallback table doesn't cover.
261
+ setPricingSource(() => providerRegistry.getRawCatalogModels());
258
262
  services.keybindingsManager.loadFromDisk();
259
263
  domainDispatch.syncControlPlaneState({
260
264
  enabled: Boolean(configManager.get('controlPlane.enabled')),
@@ -632,10 +636,20 @@ export async function initializeBootstrapCore(
632
636
 
633
637
  providerRegistry.startWatching(runtimeBus);
634
638
 
639
+ // W2.3: attach the SAME WebhookNotifier instance that `/notify add|remove|clear`
640
+ // (notify-runtime.ts) keeps live via ctx.services.webhookNotifier, rather than
641
+ // constructing a second, boot-time-only instance here. Before this fix, a
642
+ // webhook URL added mid-session reached long-task notifications (which read
643
+ // ctx.services.webhookNotifier directly) but never reached this instance's
644
+ // AGENT_FAILED/WORKFLOW_CHAIN_FAILED/WORKFLOW_CHAIN_PASSED runtime-bus
645
+ // listeners until restart — and if the session started with zero URLs
646
+ // configured, attachToRuntimeBus was never even called, so those listeners
647
+ // never existed at all for the rest of the session. Always attaching
648
+ // (regardless of initial URL count) and seeding the shared instance fixes
649
+ // both: `send()` is already a safe no-op with zero URLs configured.
635
650
  const webhookUrls = (configManager.getCategory('notifications') as { webhookUrls?: string[] }).webhookUrls ?? [];
636
651
  if (webhookUrls.length > 0) {
637
- const webhookNotifier = WebhookNotifier.fromConfig(webhookUrls);
638
- webhookNotifier.attachToRuntimeBus(runtimeBus);
652
+ services.webhookNotifier.setUrls(webhookUrls);
639
653
  domainDispatch.syncIntegration({
640
654
  id: 'webhooks',
641
655
  displayName: 'Webhooks',
@@ -647,6 +661,7 @@ export async function initializeBootstrapCore(
647
661
  meta: { urlCount: webhookUrls.length },
648
662
  }, 'bootstrap.webhooks');
649
663
  }
664
+ services.webhookNotifier.attachToRuntimeBus(runtimeBus);
650
665
 
651
666
  const notifier = await Notifier.fromConfig(services.serviceRegistry);
652
667
  const queueStatuses = notifier.getQueueStatus();
@@ -29,6 +29,8 @@ export interface ResumeSessionOptions {
29
29
  readonly configManager: Pick<ConfigManager, 'get' | 'getCategory'>;
30
30
  readonly providerRegistry: Pick<ProviderRegistry, 'get' | 'getCurrentModel' | 'getForModel' | 'require'>;
31
31
  readonly homeDirectory: string;
32
+ /** See CommandSessionServices.hydrateSessionUsage (command-registry.ts). */
33
+ readonly hydrateSessionUsage?: () => void;
32
34
  }
33
35
 
34
36
  export function createResumeSessionHandler(options: ResumeSessionOptions): (sessionId: string) => void {
@@ -65,6 +67,9 @@ export function createResumeSessionHandler(options: ResumeSessionOptions): (sess
65
67
  });
66
68
  },
67
69
  });
70
+ // Hydrate the footer's token counters from the resumed history now that
71
+ // fromJSON()/journal replay are both applied — before requestRender() below.
72
+ options.hydrateSessionUsage?.();
68
73
  options.onSessionIdChanged?.(sessionId);
69
74
  if (meta?.model) options.runtime.model = meta.model;
70
75
  if (meta?.provider) options.runtime.provider = meta.provider;