@pellux/goodvibes-tui 1.1.0 → 1.9.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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,21 +1,21 @@
1
1
  import { type Line, type Cell, createEmptyLine, createEmptyCell } from '../types/grid.ts';
2
2
  import { LAYOUT } from './layout.ts';
3
3
  import { VERSION } from '../version.ts';
4
- import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText, interpolateColor } from '../utils/terminal-width.ts';
4
+ import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText, interpolateColor, joinPrioritizedSegments, type PrioritizedSegment } from '../utils/terminal-width.ts';
5
5
  import type { GitHeaderInfo } from './git-status.ts';
6
6
  import { renderConversationFragment, renderConversationStatusLine, type ConversationStatusSegment } from './conversation-surface.ts';
7
- import { GLYPHS, UI_TONES } from './ui-primitives.ts';
7
+ import { GLYPHS } from './ui-primitives.ts';
8
+ import { activeUiTones } from './theme.ts';
8
9
  import { formatElapsed } from '../utils/format-elapsed.ts';
9
10
  import { abbreviateCount } from '../utils/format-number.ts';
10
11
  import { computeContextUsage } from '../core/context-usage.ts';
11
12
  import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
12
13
  import { buildFooterTip, isAgentActive } from './footer-tips.ts';
13
14
  import type { StreamMetrics } from '../core/stream-event-wiring.ts';
15
+ import { waitingPhrase, type WaitingState } from '@pellux/goodvibes-sdk/platform/presentation';
14
16
 
15
17
  /** Number of frames before the animated gradient completes one full cycle. */
16
18
  const GRADIENT_CYCLE_FRAMES = 50;
17
- /** Number of frames before rotating to the next thinking phrase (~30 seconds at 80ms/frame). */
18
- const PHRASE_ROTATION_FRAMES = 375;
19
19
  /**
20
20
  * Ms since the last STREAM_DELTA before the whimsical phrase rotation freezes
21
21
  * and createThinkingFragment shows an honest "stalled Ns" / "reconnecting"
@@ -72,13 +72,21 @@ function fmtCost(usd: number): string {
72
72
  * UIFactory - Generates standard UI fragments without needing Ink/React overhead.
73
73
  */
74
74
  export class UIFactory {
75
- public static createHeader(width: number, model: string, provider: string, title?: string, gitInfo?: GitHeaderInfo): Line[] {
75
+ // `version` defaults to the live build VERSION; tests pass a pinned fixture so
76
+ // golden snapshots don't break on every release bump (version-decoupled goldens,
77
+ // mirroring the splash fixture-version pattern).
78
+ public static createHeader(width: number, model: string, provider: string, title?: string, gitInfo?: GitHeaderInfo, version: string = VERSION): Line[] {
76
79
  const lines: Line[] = [];
77
- const CYAN = UI_TONES.accent.brand;
78
- const GREY = UI_TONES.fg.dim;
79
- const TITLE_COLOR = UI_TONES.fg.muted;
80
+ // Header/footer/thinking paint on the transparent terminal background, so
81
+ // they read chrome.* (light-terminal-aware) — NOT fg.*/state.*, which stay
82
+ // tuned for the opaque dark modal/panel boxes. Read live per render so a
83
+ // mode flip re-resolves without any module reload (see theme.ts).
84
+ const t = activeUiTones();
85
+ const CYAN = t.accent.brand;
86
+ const GREY = t.chrome.faint;
87
+ const TITLE_COLOR = t.chrome.label;
80
88
  const brand = ` GoodVibes `;
81
- const ver = `v${VERSION} `;
89
+ const ver = `v${version} `;
82
90
  const stats = ` ${model} `;
83
91
  const prov = `(${provider}) `;
84
92
  const line = createEmptyLine(width);
@@ -110,11 +118,11 @@ export class UIFactory {
110
118
  }
111
119
  // Build git info segment
112
120
  let gitStr = '';
113
- let gitFg: string = UI_TONES.fg.dim;
121
+ let gitFg: string = t.chrome.faint;
114
122
  if (gitInfo) {
115
123
  gitStr = buildGitSegment(gitInfo).text;
116
124
  if (gitInfo.dirty || gitInfo.ahead > 0 || gitInfo.behind > 0) {
117
- gitFg = UI_TONES.state.warn; // yellow when dirty or out-of-sync
125
+ gitFg = t.chrome.warn; // yellow when dirty or out-of-sync
118
126
  }
119
127
  }
120
128
  const rightSideText = stats + prov;
@@ -124,7 +132,7 @@ export class UIFactory {
124
132
  for (const char of stats) { if (rightX < width) line[rightX++] = { char, fg: CYAN, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false }; }
125
133
  for (const char of prov) { if (rightX < width) line[rightX++] = { char, fg: GREY, bg: '', bold: false, dim: true, underline: false, italic: false, strikethrough: false }; }
126
134
  lines.push(line);
127
- lines.push(this.stringToLine('━'.repeat(width), width, { fg: UI_TONES.fg.dim }));
135
+ lines.push(this.stringToLine('━'.repeat(width), width, { fg: t.chrome.faint }));
128
136
  return lines;
129
137
  }
130
138
 
@@ -134,12 +142,16 @@ export class UIFactory {
134
142
  */
135
143
  public static createMessageBar(
136
144
  width: number, text: string,
137
- bgColor: string = '#2a2a2a', textColor: string = UI_TONES.fg.secondary, prefixStr: string = ' › ',
145
+ bgColor: string = '#2a2a2a', textColor: string = activeUiTones().fg.secondary, prefixStr: string = ' › ',
138
146
  strikethrough = false
139
147
  ): Line[] {
148
+ // A historical user-message pill: it carries its own dark bodyBg, so its fg
149
+ // reads fg.secondary/state.reasoning (light-on-dark) — this is conversation
150
+ // content, not the transparent-terminal chrome, so it is not part of the
151
+ // light-terminal chrome flip.
140
152
  return renderConversationFragment(text, width, {
141
153
  prefix: prefixStr,
142
- prefixFg: UI_TONES.state.reasoning,
154
+ prefixFg: activeUiTones().state.reasoning,
143
155
  text: textColor,
144
156
  bodyBg: bgColor,
145
157
  strikethrough,
@@ -150,10 +162,11 @@ export class UIFactory {
150
162
  * createQueuedMessageFragment - Renders a dimmed message bar for queued prompts.
151
163
  */
152
164
  public static createQueuedMessageFragment(width: number, text: string): Line[] {
165
+ const t = activeUiTones();
153
166
  return renderConversationFragment(text, width, {
154
167
  prefix: ' (...) ',
155
- prefixFg: UI_TONES.state.reasoning,
156
- text: UI_TONES.fg.dim,
168
+ prefixFg: t.state.reasoning,
169
+ text: t.fg.dim,
157
170
  bodyBg: '#1a1a1a',
158
171
  dim: true,
159
172
  });
@@ -162,7 +175,7 @@ export class UIFactory {
162
175
  public static createFooter(
163
176
  width: number,
164
177
  prompt: string,
165
- usage: { up: number; down: number; max?: number },
178
+ usage: { up: number; down: number; max?: number; fleetCostUsd?: number | null },
166
179
  showExitNotice: boolean,
167
180
  lastCopyTime: number,
168
181
  model?: string,
@@ -182,6 +195,9 @@ export class UIFactory {
182
195
  composerFlags?: readonly string[],
183
196
  composerPendingRisk?: 'none' | 'approval-wait' | 'shell' | 'command' | 'remote',
184
197
  compact: boolean = false,
198
+ // S3d: honest cross-surface spine posture. Defined ONLY in adopted-daemon
199
+ // mode (undefined in embedded/local), so the segment is absent otherwise.
200
+ sessionSpineStatus?: 'online' | 'offline',
185
201
  ): Line[] {
186
202
  const lines: Line[] = [];
187
203
  const promptLines = prompt.split('\n');
@@ -245,6 +261,21 @@ export class UIFactory {
245
261
  }
246
262
  }
247
263
 
264
+ // UX-C focus visibility (item 1c): when the panel workspace owns focus
265
+ // and the composer is empty, name the state and the way back — the
266
+ // dimmed prompt alone (TEXT_COLOR/BG_COLOR above) told you nothing was
267
+ // wrong, but not why keystrokes weren't landing here.
268
+ if (!promptFocused && text === '' && i === 0) {
269
+ const hintText = 'panel focused — Esc returns';
270
+ const hintStartX = boxStartX + 2 + prefix.length;
271
+ let hx = hintStartX;
272
+ for (const ch of hintText) {
273
+ if (hx >= boxStartX + boxWidth - 2) break;
274
+ contentLine[hx] = { char: ch, fg: '244', bg: BG_COLOR, bold: false, dim: true, underline: false, italic: false, strikethrough: false };
275
+ hx++;
276
+ }
277
+ }
278
+
248
279
  // Overlay args hint: dim grey text after cursor on the last prompt line.
249
280
  // Only shown when a commandArgsHint is provided and cursor is at the end of input.
250
281
  if (commandArgsHint && i === promptLines.length - 1) {
@@ -261,10 +292,17 @@ export class UIFactory {
261
292
  if (cursorColOnLine >= text.length) {
262
293
  // Hint starts one cell after the cursor block
263
294
  const hintStartX = boxStartX + 2 + prefix.length + text.length + 1;
264
- const hintText = ' ' + commandArgsHint;
295
+ const hintLimit = boxStartX + boxWidth - 2; // exclusive right bound
296
+ const avail = Math.max(0, hintLimit - hintStartX);
297
+ // Clamp to the available width with an ellipsis so a long arg-spec
298
+ // (e.g. /marketplace) ends in "…" rather than a hard mid-word cut. (5b)
299
+ let hintText = ' ' + commandArgsHint;
300
+ if (hintText.length > avail) {
301
+ hintText = avail > 1 ? `${hintText.slice(0, avail - 1)}…` : hintText.slice(0, Math.max(0, avail));
302
+ }
265
303
  let hx = hintStartX;
266
304
  for (const ch of hintText) {
267
- if (hx >= boxStartX + boxWidth - 2) break;
305
+ if (hx >= hintLimit) break;
268
306
  contentLine[hx] = { char: ch, fg: '238', bg: BG_COLOR, bold: false, dim: true, underline: false, italic: false, strikethrough: false };
269
307
  hx++;
270
308
  }
@@ -290,16 +328,19 @@ export class UIFactory {
290
328
  // --- Composer posture block (mode / risk / status / flags) ------------
291
329
  // Suppressed in compact mode; the ctx-info line no longer duplicates these
292
330
  // tokens, so this block is the single home for mode/status/flags.
331
+ // Footer chrome paints on the transparent terminal bg → chrome.* accents
332
+ // (light-terminal-aware); state.info reads on both light and dark terminals.
333
+ const t = activeUiTones();
293
334
  const composerTokens: Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> = [];
294
- if (composerMode) composerTokens.push({ text: ` ${GLYPHS.status.active} ${composerMode} `, fg: UI_TONES.state.info, bold: true });
335
+ if (composerMode) composerTokens.push({ text: ` ${GLYPHS.status.active} ${composerMode} `, fg: t.state.info, bold: true });
295
336
  if (composerPendingRisk && composerPendingRisk !== 'none') {
296
337
  const riskColor = composerPendingRisk === 'approval-wait'
297
- ? UI_TONES.state.warn
338
+ ? t.chrome.warn
298
339
  : composerPendingRisk === 'shell'
299
- ? UI_TONES.state.bad
340
+ ? t.chrome.bad
300
341
  : composerPendingRisk === 'remote'
301
- ? '#a78bfa'
302
- : UI_TONES.state.warn;
342
+ ? t.chrome.remote
343
+ : t.chrome.warn;
303
344
  composerTokens.push({ text: ` risk:${composerPendingRisk} `, fg: riskColor, bold: true });
304
345
  }
305
346
  if (composerStatus && composerStatus !== 'idle') composerTokens.push({ text: ` state:${composerStatus} `, fg: '244', dim: true });
@@ -329,7 +370,7 @@ export class UIFactory {
329
370
  const isRecentlyCopied = Date.now() - lastCopyTime < 2000;
330
371
  // Token usage line + running ~$ cost estimate (derived from the usage
331
372
  // object and the active model via cost-utils).
332
- const u = usage as { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; up?: number; down?: number };
373
+ const u = usage as { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; up?: number; down?: number; fleetCostUsd?: number | null };
333
374
  const inp = u.input ?? u.up ?? 0;
334
375
  const out = u.output ?? u.down ?? 0;
335
376
  const cr = u.cacheRead ?? 0;
@@ -340,12 +381,29 @@ export class UIFactory {
340
381
  // match the existing "no priceable data" convention used elsewhere
341
382
  // (cockpit-panel formatCost, agent-inspector-shared) — the footer has no
342
383
  // room for a longer marker before truncation kicks in.
343
- const costSegment = model
384
+ const mainCostStr = model
344
385
  ? isModelPriced(model)
345
- ? `${tokenSep}~$${fmtCost(calcSessionCost(inp, out, cr, cw, model))}`
346
- : `${tokenSep}~n/a`
347
- : '';
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} ]`;
386
+ ? `~$${fmtCost(calcSessionCost(inp, out, cr, cw, model))}`
387
+ : '~n/a'
388
+ : null;
389
+ // Honest total = your main session + the delegated fleet. We show a SPLIT
390
+ // ("you ~$X · fleet ~$Y") rather than one summed figure: the two costs are
391
+ // tracked and attributed to different actors, and the split directly answers
392
+ // "where did the money go" — the exact confusion in the eval where the footer
393
+ // showed only the main session (~$0.046) while the fleet cost ~10x more
394
+ // ($0.446). The fleet segment appears only when there is a real fleet cost, so
395
+ // the idle single-session footer is unchanged.
396
+ const fleetCost = u.fleetCostUsd;
397
+ const hasFleetCost = typeof fleetCost === 'number' && fleetCost > 0;
398
+ const costSegment = hasFleetCost
399
+ ? `${tokenSep}you ${mainCostStr ?? '~n/a'} · fleet ~$${fmtCost(fleetCost)}`
400
+ : mainCostStr
401
+ ? `${tokenSep}${mainCostStr}`
402
+ : '';
403
+ // Input tokens are unknown until the first assistant usage lands (often the
404
+ // first tool turn). Show "—" rather than a false 0 during that window. (5c)
405
+ const inpDisplay = inp > 0 ? fmtNum(inp) : '—';
406
+ const tokenLine = ` Token Usage [ Input: ${inpDisplay}${tokenSep}Output: ${fmtNum(out)}${tokenSep}Cache Read: ${fmtNum(cr)}${tokenSep}Cache Write: ${fmtNum(cw)}${tokenSep}Total: ${fmtNum(total)}${costSegment} ]`;
349
407
  const copiedNotice = isRecentlyCopied ? ` [COPIED] ` : '';
350
408
  const statsLine = ' ' + tokenLine + ' '.repeat(Math.max(0, width - 4 - getDisplayWidth(tokenLine) - getDisplayWidth(copiedNotice))) + copiedNotice;
351
409
  lines.push(this.stringToLine(statsLine, width, { fg: isRecentlyCopied ? '81' : '244', bold: isRecentlyCopied }));
@@ -353,7 +411,9 @@ export class UIFactory {
353
411
  if (!compact && contextWindow && contextWindow > 0) {
354
412
  const ctxTokens = lastInputTokens ?? 0;
355
413
  const label = ' Context Usage: ';
356
- const suffix = ` [ ${fmtNum(ctxTokens)} / ${fmtNum(contextWindow)} ]`;
414
+ // "—" until the first input-token count is known, not a false 0. (5c)
415
+ const ctxDisplay = ctxTokens > 0 ? fmtNum(ctxTokens) : '—';
416
+ const suffix = ` [ ${ctxDisplay} / ${fmtNum(contextWindow)} ]`;
357
417
  const barWidth = Math.max(10, Math.min(30, width - getDisplayWidth(label) - getDisplayWidth(suffix) - 8));
358
418
  const ctxPct = computeContextUsage(ctxTokens, contextWindow).clampedRatio;
359
419
  // Clamp threshold to [0..1]; undefined/0 means no threshold marker.
@@ -370,19 +430,30 @@ export class UIFactory {
370
430
  const displayDir = workingDir && home && workingDir.startsWith(home)
371
431
  ? '~' + workingDir.slice(home.length)
372
432
  : workingDir ?? '';
373
- const ctxParts: string[] = [];
374
- if (displayDir) ctxParts.push(displayDir);
433
+ // Segments carry a survival priority (lower = survives longer) so that
434
+ // under width pressure whole low-value segments are dropped before any
435
+ // segment is character-truncated mid-word. cwd/model are the essential
436
+ // orientation segments (priority 0); the spine liveness marker is the
437
+ // daemon-honesty signal and must outlive the two decorative segments
438
+ // (tool count, notify mode) — it is ordered ahead of them too, so a
439
+ // narrow line drops "N tools"/"notify:x" whole before spine is at risk.
440
+ const ctxParts: PrioritizedSegment[] = [];
441
+ if (displayDir) ctxParts.push({ text: displayDir, priority: 0 });
375
442
  if (model) {
376
- ctxParts.push(model + (provider ? ` (${provider})` : ''));
443
+ ctxParts.push({ text: model + (provider ? ` (${provider})` : ''), priority: 0 });
377
444
  }
378
- if (toolCount) ctxParts.push(`${toolCount} tools`);
445
+ // S3d: cross-surface spine posture — plain words, no blame. Adopted mode only.
446
+ if (sessionSpineStatus) ctxParts.push({ text: `spine:${sessionSpineStatus}`, priority: 1 });
447
+ if (toolCount) ctxParts.push({ text: `${toolCount} tools`, priority: 2 });
379
448
  // Labeled "notify" (not "hitl") — /mode (aliased /hitl) governs UX
380
449
  // notification verbosity (quiet/balanced/operator), not tool
381
450
  // 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}`);
384
- const ctxLine = ' ' + ctxParts.join(` ${GLYPHS.navigation.pipeSeparator} `);
385
- lines.push(this.stringToLine(truncateDisplay(ctxLine, width), width, { fg: '240', dim: true }));
451
+ // risk banner rendered a few lines below. Lowest priority: dropped first.
452
+ if (hitlMode) ctxParts.push({ text: `notify:${hitlMode}`, priority: 3 });
453
+ const sep = ` ${GLYPHS.navigation.pipeSeparator} `;
454
+ const ctxBody = joinPrioritizedSegments(ctxParts, sep, width - 3);
455
+ const ctxLine = ' ' + ctxBody;
456
+ lines.push(this.stringToLine(ctxLine, width, { fg: '240', dim: true }));
386
457
  }
387
458
  if (showExitNotice) {
388
459
  const notice = ` !!! Press Ctrl+C again to exit !!! `;
@@ -403,7 +474,7 @@ export class UIFactory {
403
474
  for (const ch of dangerWarn) {
404
475
  if (col >= width) break;
405
476
  const cw = getDisplayWidth(ch);
406
- line[col] = { char: ch, fg: UI_TONES.state.bad, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false };
477
+ line[col] = { char: ch, fg: t.chrome.bad, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false };
407
478
  if (cw === 2 && col + 1 < width) line[col + 1] = { ...line[col], char: '' };
408
479
  col += cw;
409
480
  }
@@ -413,33 +484,20 @@ export class UIFactory {
413
484
  return lines;
414
485
  }
415
486
 
416
- /** Rotating thinking phrases vaporwave / good vibes themed. */
417
- private static readonly THINKING_PHRASES = [
418
- 'Thinking...',
419
- 'Vibing...',
420
- 'Manifesting...',
421
- 'Channeling energy...',
422
- 'Tuning frequencies...',
423
- 'Riding the wave...',
424
- 'Aligning chakras...',
425
- 'Entering flow state...',
426
- 'Consulting the void...',
427
- 'Absorbing aesthetics...',
428
- 'Synthesizing vibes...',
429
- 'Transcending...',
430
- 'Dreaming in neon...',
431
- 'Parsing the cosmos...',
432
- 'Loading good vibes...',
433
- 'Meditating...',
434
- 'Catching a vibe...',
435
- 'Harmonizing...',
436
- 'Feeling it...',
437
- 'In the zone...',
438
- ];
487
+ // W6-P1: the rotating "thinking" phrase pool and the honest waiting-state
488
+ // wording (approval/reconnecting/pre-first-token/stalled/thinking) are no
489
+ // longer minted here. They come from the SDK presentation contract's
490
+ // waitingPhrase() (@pellux/goodvibes-sdk/platform/presentation, landed by
491
+ // W4-S1, already adopted by the agent in W4-R4) — see createThinkingFragment
492
+ // below. This renderer still decides WHICH state applies from its own
493
+ // stall/reconnect/approval signals (computeStallInfo/computeRenderStallInfo
494
+ // stay renderer-local per the extraction decision record); only the exact
495
+ // wording is shared.
439
496
 
440
- /** Gradient colors for thinking text — cyan to purple (matches splash). */
441
- private static readonly THINK_GRADIENT_START = UI_TONES.accent.gradientStart;
442
- private static readonly THINK_GRADIENT_END = UI_TONES.accent.gradientEnd;
497
+ // Gradient colors for thinking text — cyan→purple in dark, teal→purple in
498
+ // light (matches splash/brand). Read live per render inside
499
+ // createThinkingFragment via activeUiTones() rather than baked into a static
500
+ // field, so a mode flip re-resolves without a module reload.
443
501
 
444
502
  /**
445
503
  * Per-frame stall info from stream metrics — computed from lastDeltaAtMs every render (not
@@ -478,25 +536,36 @@ export class UIFactory {
478
536
  : undefined;
479
537
  }
480
538
 
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[] {
539
+ public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number, elapsedMs?: number, ttftMs?: number, stallInfo?: ThinkingStallInfo, approvalPending?: boolean): Line[] {
540
+ // Live thinking row paints on the transparent terminal bg → read the
541
+ // mode-resolved chrome tones per render (gradient/brand/tool accents flip).
542
+ const tones = activeUiTones();
482
543
  // 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.
544
+ // long enough to be misleading (THINKING_STALL_FREEZE_MS). Decide WHICH
545
+ // honest waiting state applies (renderer-local this signal computation
546
+ // stays here per the extraction decision record), then defer the exact
547
+ // wording to the SDK presentation contract's waitingPhrase() (W6-P1;
548
+ // mirrors the agent's W4-R4 adoption). Precedence matches the contract:
549
+ // approval > reconnecting > pre-first-token > stalled > thinking.
486
550
  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
- }
497
- const speedSuffix = (tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
551
+ let state: WaitingState;
552
+ if (approvalPending) state = 'approval';
553
+ else if (stallInfo?.reconnect) state = 'reconnecting';
554
+ else if (isStalled && (outputTokens ?? 0) === 0) state = 'pre-first-token';
555
+ else if (isStalled) state = 'stalled';
556
+ else state = 'thinking';
557
+ const phrase = waitingPhrase(state, {
558
+ reconnectAttempt: stallInfo?.reconnect?.attempt,
559
+ reconnectMaxAttempts: stallInfo?.reconnect?.maxAttempts,
560
+ msSinceLastDelta: stallInfo?.msSinceLastDelta,
561
+ frame,
562
+ });
563
+ // Token-rate and time-to-first-token readouts are meaningless while waiting on the user, and a
564
+ // "tok/s" figure next to an approval prompt reads as if the model were still working — suppress
565
+ // them; keep the elapsed timer since "how long the approval has waited" is honest.
566
+ const speedSuffix = (!approvalPending && tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
498
567
  const elapsedSuffix = elapsedMs !== undefined ? ` (${formatElapsed(elapsedMs)})` : '';
499
- const ttftSuffix = (ttftMs !== undefined && ttftMs > 0) ? ` ttft:${ttftMs}ms` : '';
568
+ const ttftSuffix = (!approvalPending && ttftMs !== undefined && ttftMs > 0) ? ` ttft:${ttftMs}ms` : '';
500
569
  const text = ` ${spinner} ${phrase}${speedSuffix}${elapsedSuffix}${ttftSuffix} `;
501
570
 
502
571
  const textWidth = Math.max(1, getDisplayWidth(text) - 1);
@@ -506,7 +575,7 @@ export class UIFactory {
506
575
  const gradientPos = raw <= 0.5 ? raw * 2 : (1 - raw) * 2;
507
576
  return {
508
577
  text: char,
509
- fg: interpolateColor(this.THINK_GRADIENT_START, this.THINK_GRADIENT_END, gradientPos),
578
+ fg: interpolateColor(tones.accent.gradientStart, tones.accent.gradientEnd, gradientPos),
510
579
  bold: true,
511
580
  };
512
581
  });
@@ -514,7 +583,7 @@ export class UIFactory {
514
583
  const inTok = inputTokens ?? 0;
515
584
  const outTok = outputTokens ?? 0;
516
585
  segments.push({ text: ` in ${fmtNum(inTok)} `, fg: '243', dim: true });
517
- segments.push({ text: `out ${fmtNum(outTok)}`, fg: UI_TONES.accent.brand });
586
+ segments.push({ text: `out ${fmtNum(outTok)}`, fg: tones.accent.brand });
518
587
  }
519
588
  const line = createEmptyLine(width);
520
589
  let col = 1;
@@ -554,7 +623,7 @@ export class UIFactory {
554
623
  if (px >= width) break;
555
624
  previewLine[px] = {
556
625
  char: ch,
557
- fg: UI_TONES.state.info,
626
+ fg: tones.state.info,
558
627
  bg: '',
559
628
  bold: true,
560
629
  dim: false,
@@ -1,134 +1,35 @@
1
- export const GLYPHS = {
2
- frame: {
3
- topLeft: '┌',
4
- topRight: '┐',
5
- bottomLeft: '└',
6
- bottomRight: '┘',
7
- horizontal: '─',
8
- vertical: '│',
9
- teeLeft: '├',
10
- teeRight: '┤',
11
- teeTop: '┬',
12
- teeBottom: '┴',
13
- cross: '┼',
14
- },
15
- surface: {
16
- top: '▄',
17
- bottom: '▀',
18
- cursor: '█',
19
- altCursor: '▌',
20
- },
21
- navigation: {
22
- selected: '▸',
23
- collapsed: '▸',
24
- expanded: '▾',
25
- up: '',
26
- down: '↓',
27
- moreAbove: '▲',
28
- moreBelow: '▼',
29
- next: '→',
30
- back: '←',
31
- pipeSeparator: '│',
32
- },
33
- status: {
34
- success: '✓',
35
- failure: '✕',
36
- pending: '•',
37
- active: '●',
38
- idle: '◌',
39
- info: '○',
40
- warn: '⚠',
41
- blocked: '⊘',
42
- skipped: '◇',
43
- review: '◈',
44
- retry: '↻',
45
- handoff: '⇢',
46
- reference: '↗',
47
- partial: '◐',
48
- dualPane: '◆',
49
- star: '★',
50
- },
51
- meter: {
52
- filled: '█',
53
- medium: '▓',
54
- light: '▒',
55
- empty: '░',
56
- spark: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'],
57
- },
58
- } as const;
1
+ // ---------------------------------------------------------------------------
2
+ // ui-primitives.ts — glyph registry + tone-token table.
3
+ //
4
+ // W6-P1: these four tables (GLYPHS, UI_TONES, DIFF_TONES, SPINNER_FRAMES) are
5
+ // no longer minted locally. They are the SDK presentation contract
6
+ // (@pellux/goodvibes-sdk/platform/presentation, landed by W4-S1 and already
7
+ // adopted by the agent in W4-R4), consumed here so the TUI and the agent
8
+ // share ONE source (Mike's move-to-SDK ruling — machinery needed by 2+
9
+ // surfaces => SDK). The TUI was the reference these values were lifted from
10
+ // verbatim, so this swap is byte-identical. See
11
+ // docs/decisions/2026-07-05-presentation-contract-sdk-extraction.md in the SDK.
12
+ //
13
+ // Re-exported under the historical names (GLYPHS, UI_TONES) so every existing
14
+ // importer keeps working with no call-site churn. UI_TONES is the dark-mode
15
+ // tone table (== resolveTones('dark')); light is resolved via theme.ts's
16
+ // activeUiTones() / resolveUiTones(), which composes the mode dimension over
17
+ // this dark constant unchanged.
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import {
21
+ GLYPHS,
22
+ TONE_TOKENS,
23
+ DIFF_TONES,
24
+ SPINNER_FRAMES,
25
+ } from '@pellux/goodvibes-sdk/platform/presentation';
26
+
27
+ export { GLYPHS, DIFF_TONES, SPINNER_FRAMES };
59
28
 
60
29
  /**
61
30
  * UI_TONES — the single chrome/color-token source for src/renderer and
62
31
  * src/panels. Concrete (dark-mode) values; resolveUiTones(mode) in theme.ts
63
- * is the single mode-resolved read path — this object is the 'dark' entry.
64
- */
65
- export const UI_TONES = {
66
- fg: {
67
- primary: '#e2e8f0',
68
- secondary: '#cbd5e1',
69
- muted: '#94a3b8',
70
- dim: '#475569',
71
- inverse: '#0f172a',
72
- /** Empty-state / placeholder foreground (formerly DEFAULT_PANEL_PALETTE.empty literal). */
73
- empty: '#334155',
74
- },
75
- bg: {
76
- base: '#11131a',
77
- surface: '#161a22',
78
- title: '#0f172a',
79
- section: '#18202b',
80
- summary: '#1b2430',
81
- selected: '#223049',
82
- input: '#1e293b',
83
- warning: '#2b2116',
84
- error: '#2a161b',
85
- success: '#14241b',
86
- /** Fullscreen/shell footer background. */
87
- footer: '#111827',
88
- },
89
- state: {
90
- info: '#38bdf8',
91
- good: '#22c55e',
92
- warn: '#f59e0b',
93
- bad: '#ef4444',
94
- blocked: '#f97316',
95
- active: '#60a5fa',
96
- /** Single canonical reasoning/thinking purple (replaces #9945FF and ad-hoc purples). */
97
- reasoning: '#a855f7',
98
- },
99
- accent: {
100
- browser: '#7dd3fc',
101
- control: '#22d3ee',
102
- inspector: '#c4b5fd',
103
- workflow: '#fbbf24',
104
- conversation: '#93c5fd',
105
- /** Neon brand accent — header/splash/thinking gradient only. */
106
- brand: '#00ffff',
107
- gradientStart: '#00ffff',
108
- gradientEnd: '#d000ff',
109
- },
110
- /** Canonical border stroke color for fullscreen/panel chrome. */
111
- border: '#64748b',
112
- } as const;
113
-
114
- /**
115
- * Diff surface accent color shared across the three diff-rendering surfaces —
116
- * the conversation diff view (diff-view.ts), the file diff panel
117
- * (diff-panel.ts), and the git panel's inline diff (git-panel.ts). Hunk-header
118
- * blue has no matching UI_TONES.state/accent role, so WO-204 gives it one
119
- * named token here (value converged on diff-panel.ts's pre-existing hunk
120
- * color, the only one of the three surfaces that predates this token; add/del
121
- * on all three surfaces already converge on UI_TONES.state.good/bad).
32
+ * is the single mode-resolved read path — this object is the 'dark' entry
33
+ * (== resolveTones('dark') from the SDK presentation contract).
122
34
  */
123
- export const DIFF_TONES = {
124
- // add/del carry diff-panel.ts's shipped colors: the diff panel is the only
125
- // diff surface users have ever seen (diff-view.ts was unwired from v0.9.6
126
- // until WO-204), so its look is the reference — the conversation surface
127
- // adopts it, never the other way around.
128
- add: '#00ff88',
129
- del: '#ff4444',
130
- hunk: '#88aaff',
131
- } as const;
132
-
133
- /** Single spinner-frame source — layout.ts and progress.ts both re-export this. */
134
- export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const;
35
+ export const UI_TONES = TONE_TOKENS;