@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
@@ -8,11 +8,9 @@ import { renderModelWorkspace } from './model-workspace.ts';
8
8
  import { renderSelectionModalOverlay } from './selection-modal-overlay.ts';
9
9
  import { renderSearchOverlay } from './search-overlay.ts';
10
10
  import { renderHistorySearchOverlay } from './history-search-overlay.ts';
11
- import { renderProcessModal } from './process-modal.ts';
12
- import { renderAgentDetailModal } from './agent-detail-modal.ts';
13
- import { renderLiveTailModal } from './live-tail-modal.ts';
14
11
  import { renderContextInspector } from './context-inspector.ts';
15
12
  import { renderSettingsModal } from './settings-modal.ts';
13
+ import { renderConfigModal } from './config-modal.ts';
16
14
  import { renderMcpWorkspace } from './mcp-workspace.ts';
17
15
  import { renderSessionPickerModal } from './session-picker-modal.ts';
18
16
  import { renderProfilePickerModal } from './profile-picker-modal.ts';
@@ -78,20 +76,9 @@ export function applyConversationOverlays(
78
76
  next.push(...renderHistorySearchOverlay(input.historySearch, conversationWidth));
79
77
  }
80
78
 
81
- if (!fullscreenClaimed && input.processModal.active) {
82
- const lines = renderProcessModal(input.processModal, conversationWidth, viewportHeight);
83
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
84
- }
85
-
86
- if (!fullscreenClaimed && input.agentDetailModal.active) {
87
- const lines = renderAgentDetailModal(input.agentDetailModal, conversationWidth, viewportHeight);
88
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
89
- }
90
-
91
- if (!fullscreenClaimed && input.liveTailModal.active) {
92
- const lines = renderLiveTailModal(input.liveTailModal, conversationWidth, viewportHeight);
93
- next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
94
- }
79
+ // W6.1 retirement: the process, agent-detail, and live-tail modal overlays
80
+ // were removed with those modals — F2 now opens the Fleet panel, which
81
+ // subsumes the live process tree.
95
82
 
96
83
  if (!fullscreenClaimed && input.contextInspectorModal.active) {
97
84
  const lines = renderContextInspector(conversation, conversationWidth, viewportHeight, contextWindow);
@@ -104,6 +91,12 @@ export function applyConversationOverlays(
104
91
  fullscreenClaimed = true;
105
92
  }
106
93
 
94
+ if (!fullscreenClaimed && input.configModal.active) {
95
+ const lines = renderConfigModal(input.configModal, conversationWidth, viewportHeight);
96
+ next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
97
+ fullscreenClaimed = true;
98
+ }
99
+
107
100
  if (input.mcpWorkspace.active) {
108
101
  const lines = renderMcpWorkspace(input.mcpWorkspace, conversationWidth, viewportHeight);
109
102
  next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
@@ -0,0 +1,56 @@
1
+ // ---------------------------------------------------------------------------
2
+ // fleet-tab-strip.ts
3
+ //
4
+ // Wave-3 (W3.1 Part C3) — renders FleetPanel's session-tab strip by reusing
5
+ // renderTabStrip exactly as panel-workspace-bar.ts does for the workspace
6
+ // tab bar. Deliberately a SEPARATE, visually distinct strip from the
7
+ // workspace bar: workspace tabs switch PANELS, fleet session tabs switch
8
+ // ATTACHED PROCESSES within one panel — conflating their styling would blur
9
+ // that distinction for the operator.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ import type { Line } from '../types/grid.ts';
13
+ import type { FleetTabsState } from '../panels/fleet-tabs.ts';
14
+ import { renderTabStrip, type TabHitRegion } from './tab-strip.ts';
15
+ import { UI_TONES } from './ui-primitives.ts';
16
+
17
+ const ACTIVE_FG = UI_TONES.fg.primary;
18
+ const ACTIVE_BG = UI_TONES.bg.selected;
19
+ const INACTIVE_FG = UI_TONES.fg.muted;
20
+ const SEPARATOR_FG = UI_TONES.fg.dim;
21
+ const LABEL_FG = UI_TONES.fg.secondary;
22
+
23
+ /**
24
+ * Render the fleet session-tab strip, or `null` when there are no attached
25
+ * tabs — the panel omits the strip entirely in that case (root-tab-only),
26
+ * which is what keeps the pre-Wave-3 fleet-panel goldens byte-identical.
27
+ */
28
+ export function renderFleetTabStrip(
29
+ state: FleetTabsState,
30
+ width: number,
31
+ onLayout?: (regions: readonly TabHitRegion[]) => void,
32
+ ): Line | null {
33
+ if (state.tabs.length === 0) return null;
34
+ return renderTabStrip({
35
+ width,
36
+ onLayout,
37
+ tabs: [
38
+ { label: 'Tree', active: state.activeTabIndex === 0 },
39
+ ...state.tabs.map((tab, index) => ({
40
+ label: tab.label,
41
+ active: state.activeTabIndex === index + 1,
42
+ })),
43
+ ],
44
+ prefixLabel: ' SESSIONS ',
45
+ style: {
46
+ activeFg: ACTIVE_FG,
47
+ activeBg: ACTIVE_BG,
48
+ activeBold: true,
49
+ inactiveFg: INACTIVE_FG,
50
+ separatorFg: SEPARATOR_FG,
51
+ labelFg: LABEL_FG,
52
+ overflowFg: SEPARATOR_FG,
53
+ trailingFg: INACTIVE_FG,
54
+ },
55
+ });
56
+ }
@@ -24,10 +24,22 @@ export function isAgentActive(composerStatus: string | undefined): boolean {
24
24
  return composerStatus !== undefined && ACTIVE_TURN_STATUSES.has(composerStatus);
25
25
  }
26
26
 
27
- const TIP_PANELS = 'Ctrl+P panels';
28
- const TIP_PROCESSES = 'F2 processes';
27
+ // UX-C: Ctrl+P is a TOGGLE (open+focus when nothing is open or unfocused,
28
+ // hide when the workspace already has focus — see openPanelPicker in
29
+ // shell/ui-openers.ts). The bare noun 'panels' undersold that; naming the verb
30
+ // keeps the tip honest about what the chord actually does.
31
+ const TIP_PANELS = 'Ctrl+P toggle panels';
32
+ // W6.2 e: F2 now opens the Fleet panel (the process modal was retired), so the
33
+ // tip names 'fleet', not 'processes'.
34
+ const TIP_PROCESSES = 'F2 fleet';
29
35
  const TIP_HELP = '? help';
30
- const TIP_QUIT = 'Ctrl+C quit';
36
+ // W6.2 f: an empty-composer Ctrl+C does NOT quit on the first press — it arms a
37
+ // ~1s "press again to exit" confirm, and only a SECOND Ctrl+C within that window
38
+ // exits (an intentional accidental-exit guard). The old 'Ctrl+C quit' tip
39
+ // implied a single press quits, so two presses seconds apart appeared to do
40
+ // nothing. 'Ctrl+C x2 quit' matches the real double-press contract (and the
41
+ // help overlay's "Ctrl+C x2 Exit" row).
42
+ const TIP_QUIT = 'Ctrl+C x2 quit';
31
43
 
32
44
  /**
33
45
  * Build the footer discoverability tip. Segments join with the shared middle
@@ -2,30 +2,40 @@ import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine, createStyledCell } from '../types/grid.ts';
3
3
  import { getDisplayWidth } from '../utils/terminal-width.ts';
4
4
  import { GLYPHS } from './ui-primitives.ts';
5
- import { resolveUiTones } from './theme.ts';
5
+ import { activeUiTones, registerThemeRefresh } from './theme.ts';
6
6
 
7
- // FULLSCREEN_PALETTE is built from the mode-resolved chrome tones
8
- // (resolveUiTones) rather than the static UI_TONES constant WO-001 single
9
- // read path. Mode is fixed to 'dark' until the terminal-bg-probe lands.
10
- const TONES = resolveUiTones('dark');
7
+ // Built from the mode-resolved chrome tones (activeUiTones) and rebuilt IN PLACE
8
+ // on a mode flip via the registered refresher (read by reference across the
9
+ // fullscreen workspace surfaces see theme.ts's active-mode runtime note).
10
+ function buildFullscreenPalette(): Record<string, string> {
11
+ const t = activeUiTones();
12
+ return {
13
+ border: t.border,
14
+ title: t.accent.browser,
15
+ subtitle: t.accent.conversation,
16
+ text: t.fg.primary,
17
+ muted: t.fg.muted,
18
+ dim: t.border,
19
+ selectedBg: t.bg.selected,
20
+ categoryBg: t.bg.section,
21
+ contextBg: t.bg.surface,
22
+ controlsBg: t.bg.base,
23
+ footerBg: t.bg.footer,
24
+ good: t.state.good,
25
+ warn: t.state.warn,
26
+ bad: t.state.bad,
27
+ info: t.state.info,
28
+ };
29
+ }
11
30
 
12
- export const FULLSCREEN_PALETTE = {
13
- border: TONES.border,
14
- title: TONES.accent.browser,
15
- subtitle: TONES.accent.conversation,
16
- text: TONES.fg.primary,
17
- muted: TONES.fg.muted,
18
- dim: TONES.border,
19
- selectedBg: TONES.bg.selected,
20
- categoryBg: TONES.bg.section,
21
- contextBg: TONES.bg.surface,
22
- controlsBg: TONES.bg.base,
23
- footerBg: TONES.bg.footer,
24
- good: TONES.state.good,
25
- warn: TONES.state.warn,
26
- bad: TONES.state.bad,
27
- info: TONES.state.info,
28
- } as const;
31
+ export const FULLSCREEN_PALETTE = buildFullscreenPalette() as {
32
+ readonly border: string; readonly title: string; readonly subtitle: string;
33
+ readonly text: string; readonly muted: string; readonly dim: string;
34
+ readonly selectedBg: string; readonly categoryBg: string; readonly contextBg: string;
35
+ readonly controlsBg: string; readonly footerBg: string; readonly good: string;
36
+ readonly warn: string; readonly bad: string; readonly info: string;
37
+ };
38
+ registerThemeRefresh(() => Object.assign(FULLSCREEN_PALETTE as Record<string, string>, buildFullscreenPalette()));
29
39
 
30
40
  export type FullscreenTextStyle = Partial<Omit<Line[number], 'char'>>;
31
41
 
@@ -24,6 +24,7 @@ export class GitStatusProvider {
24
24
  private lastFetch = 0;
25
25
  private readonly ttlMs = 2000;
26
26
  private fetching = false;
27
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
27
28
 
28
29
  constructor(private readonly workingDirectory: string) {}
29
30
 
@@ -53,6 +54,45 @@ export class GitStatusProvider {
53
54
  return this.cache;
54
55
  }
55
56
 
57
+ /**
58
+ * Start a lightweight live-repo-state poll. Mirrors DiffPanel's principle of
59
+ * never trusting a cached "is this a repo" flag (see diff-panel.ts
60
+ * showGitDiff/showFileDiffs/showStagedDiff, all gated on a fresh
61
+ * GitService.isGitRepo() check) but amortizes the cost for the header: the
62
+ * cheap synchronous isGitRepo() spawn runs every tick; the heavier async
63
+ * status()+branch() fetch (this.refresh()) only runs when that boolean
64
+ * actually flips — e.g. an external `git init`, or `.git` removed.
65
+ */
66
+ startPolling(intervalMs: number, onChange: (info: GitHeaderInfo) => void): void {
67
+ if (this.pollTimer !== null) return;
68
+ const checkIsRepo = (): boolean | null => {
69
+ try {
70
+ return GitService.isGitRepo(this.workingDirectory);
71
+ } catch (err) {
72
+ logger.debug('GitStatusProvider: isGitRepo poll check failed', { error: summarizeError(err) });
73
+ return null;
74
+ }
75
+ };
76
+ let lastKnownIsRepo = checkIsRepo();
77
+ this.pollTimer = setInterval(() => {
78
+ const isRepoNow = checkIsRepo();
79
+ if (isRepoNow !== null && isRepoNow !== lastKnownIsRepo) {
80
+ lastKnownIsRepo = isRepoNow;
81
+ this.refresh().then(onChange).catch((err) => {
82
+ logger.debug('GitStatusProvider: poll-triggered refresh failed', { error: summarizeError(err) });
83
+ });
84
+ }
85
+ }, intervalMs);
86
+ }
87
+
88
+ /** Stop the live-repo-state poll started by startPolling(). No-op if not polling. */
89
+ stopPolling(): void {
90
+ if (this.pollTimer !== null) {
91
+ clearInterval(this.pollTimer);
92
+ this.pollTimer = null;
93
+ }
94
+ }
95
+
56
96
  private async _fetch(): Promise<void> {
57
97
  if (this.fetching) return;
58
98
  this.fetching = true;
@@ -70,7 +110,9 @@ export class GitStatusProvider {
70
110
  statusResult.conflicted.length > 0 ||
71
111
  statusResult.not_added.length > 0;
72
112
  this.cache = {
73
- branch: branchResult.current || '?',
113
+ // Empty `current` means an unborn HEAD — a repo with no commits yet.
114
+ // Show "new" instead of an unfriendly "?" for that first-run case. (5a)
115
+ branch: branchResult.current || 'new',
74
116
  dirty,
75
117
  ahead: statusResult.ahead ?? 0,
76
118
  behind: statusResult.behind ?? 0,
@@ -289,7 +289,7 @@ export function renderShortcutsOverlay(
289
289
  row(kb('block-copy'), 'Copy block to clipboard'),
290
290
  row(kb('block-save'), 'Save block to file'),
291
291
  row(kb('copy-selection'), 'Copy selection'),
292
- row('F2', 'Process monitor'),
292
+ row('F2', 'Open the Fleet panel'),
293
293
  row('?', 'Help overlay'),
294
294
  row(`${kb('clear-cancel')} x2`, 'Exit'),
295
295
  '',
@@ -303,7 +303,7 @@ export function renderShortcutsOverlay(
303
303
  row(kb('panel-focus-toggle'), 'Swap focus between top / bottom pane'),
304
304
  row(kb('panel-close'), 'Close active panel'),
305
305
  row(kb('panel-close-all'), 'Close all panels'),
306
- row(kb('panel-ops'), 'Open the Ops Control panel'),
306
+ row(kb('panel-ops'), 'Open and focus the Fleet panel'),
307
307
  '',
308
308
  ' In-Panel Controls',
309
309
  ' ' + '\u2500'.repeat(40),
@@ -20,10 +20,6 @@ export const TOOL_STATUS = {
20
20
  TOOL_NAME_PAD: 8,
21
21
  } as const;
22
22
 
23
- export const COLORS = {
24
- DIM_TEXT: UI_TONES.fg.dim,
25
- } as const;
26
-
27
23
  export const BORDERS = {
28
24
  THINKING: { char: '▌', color: UI_TONES.state.reasoning },
29
25
  ERROR: { char: '▌', color: UI_TONES.state.bad },
@@ -3,10 +3,11 @@ import { UIFactory } from './ui-factory.ts';
3
3
  import { renderCodeBlock } from './code-block.ts';
4
4
  import { getDisplayWidth } from '../utils/terminal-width.ts';
5
5
  import { LAYOUT } from './layout.ts';
6
- import { DARK_THEME } from './theme.ts';
6
+ import { activeTheme } from './theme.ts';
7
7
 
8
- /** Module-level resolved token set (dark default; replace with resolveTheme(mode) when mode detection lands). */
9
- const T = DARK_THEME;
8
+ // Transcript tokens are read live per render (const T = activeTheme() at the top
9
+ // of each render function below) so a dark→light repaint re-resolves with no
10
+ // module reload. See theme.ts's active-mode runtime note.
10
11
 
11
12
  export interface MarkdownRenderOptions {
12
13
  codeBlockLineNumbers?: boolean;
@@ -62,6 +63,7 @@ export function renderMarkdownTracked(
62
63
  width: number,
63
64
  options: MarkdownRenderOptions = {},
64
65
  ): { lines: ReturnType<typeof renderMarkdown>; codeBlocks: CodeBlockSpan[] } {
66
+ const T = activeTheme();
65
67
  const lines: ReturnType<typeof renderMarkdown> = [];
66
68
  const codeBlocks: CodeBlockSpan[] = [];
67
69
  const rawLines = text.split('\n');
@@ -236,6 +238,7 @@ function stripMarkdown(text: string): string {
236
238
  * truncates long content, and renders with proper styling.
237
239
  */
238
240
  function renderTable(rows: string[], width: number, indent: number): Line[] {
241
+ const T = activeTheme();
239
242
  const lines: Line[] = [];
240
243
 
241
244
  // Parse rows into cells, skip separator
@@ -542,6 +545,7 @@ function compositeInlineLine(
542
545
  prefixStyle: Partial<Cell>,
543
546
  textStartX: number
544
547
  ): Line[] {
548
+ const T = activeTheme();
545
549
  const lines: Line[] = [];
546
550
 
547
551
  // Flatten tokens to [char, style] pairs
@@ -9,7 +9,7 @@ import {
9
9
  } from './overlay-box.ts';
10
10
  import { getOverlayMaxWidth } from './overlay-viewport.ts';
11
11
  import { GLYPHS } from './ui-primitives.ts';
12
- import { resolveUiTones } from './theme.ts';
12
+ import { activeUiTones, registerThemeRefresh } from './theme.ts';
13
13
  import { HINT_SEPARATOR } from './hint-grammar.ts';
14
14
 
15
15
  // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -92,25 +92,30 @@ export interface ModalConfig {
92
92
 
93
93
  // ── Defaults ─────────────────────────────────────────────────────────────────
94
94
 
95
- // DEFAULT_STYLE is built from the mode-resolved chrome tones (resolveUiTones)
96
- // rather than the static UI_TONES constant WO-001 single read path. Mode is
97
- // fixed to 'dark' until the terminal-bg-probe lands.
98
- const TONES = resolveUiTones('dark');
99
-
100
- const DEFAULT_STYLE: Required<ModalStyle> = {
101
- titleFg: TONES.fg.primary,
102
- borderFg: TONES.fg.dim,
103
- hintFg: TONES.fg.muted,
104
- selectedFg: TONES.fg.primary,
105
- selectedBg: TONES.bg.selected,
106
- textFg: TONES.fg.primary,
107
- accentFg: TONES.state.info,
108
- titleRowFg: TONES.fg.secondary,
109
- titleBg: TONES.bg.title,
110
- sectionBg: TONES.bg.section,
111
- inputBg: TONES.bg.input,
112
- surfaceBg: TONES.bg.surface,
113
- };
95
+ // DEFAULT_STYLE is built from the mode-resolved chrome tones (activeUiTones) and
96
+ // rebuilt IN PLACE on a mode flip via the registered refresher. createModal reads
97
+ // it per call via `{ ...DEFAULT_STYLE, ...config.style }`, so the in-place rebuild
98
+ // reaches every modal — see theme.ts's active-mode runtime note.
99
+ function buildDefaultModalStyle(): Required<ModalStyle> {
100
+ const t = activeUiTones();
101
+ return {
102
+ titleFg: t.fg.primary,
103
+ borderFg: t.fg.dim,
104
+ hintFg: t.fg.muted,
105
+ selectedFg: t.fg.primary,
106
+ selectedBg: t.bg.selected,
107
+ textFg: t.fg.primary,
108
+ accentFg: t.state.info,
109
+ titleRowFg: t.fg.secondary,
110
+ titleBg: t.bg.title,
111
+ sectionBg: t.bg.section,
112
+ inputBg: t.bg.input,
113
+ surfaceBg: t.bg.surface,
114
+ };
115
+ }
116
+
117
+ const DEFAULT_STYLE: Required<ModalStyle> = buildDefaultModalStyle();
118
+ registerThemeRefresh(() => Object.assign(DEFAULT_STYLE, buildDefaultModalStyle()));
114
119
 
115
120
  // ── ModalFactory ─────────────────────────────────────────────────────────────
116
121
 
@@ -124,6 +124,10 @@ function modelKey(model: ModelDefinition): string {
124
124
 
125
125
  function targetSummary(info: ModelPickerTargetInfo): string {
126
126
  if (!info.enabled) return 'disabled';
127
+ // 'embeddings' has no model concept — configuredNote carries the honest
128
+ // provider id + dimensions + configured-state summary instead of a
129
+ // computed provider:model route (which would print a phantom "model:").
130
+ if (info.configuredNote) return info.configuredNote;
127
131
  const route = formatRoute(info.provider, info.model);
128
132
  if (info.inherited) return `inherits ${route}`;
129
133
  return route;
@@ -145,6 +149,7 @@ function targetLabelFor(target: string): string {
145
149
  if (target === 'helper') return 'Helper Model';
146
150
  if (target === 'tool') return 'Tool LLM';
147
151
  if (target === 'tts') return 'TTS LLM';
152
+ if (target === 'embeddings') return 'Embeddings';
148
153
  return 'Main Chat';
149
154
  }
150
155
 
@@ -174,10 +179,25 @@ function detailLines(picker: ModelPickerModal, width: number): string[] {
174
179
  }
175
180
  } else if (picker.mode === 'effort') {
176
181
  lines.push(`Reasoning effort applies to the main chat model. Select the default effort for this model.`);
182
+ } else if (picker.mode === 'embeddingProvider') {
183
+ lines.push('Embedding provider selection: choose the provider memory search and the code index use to embed content.');
184
+ const selectedEmbedding = picker.embeddingProviders[picker.selectedIndex];
185
+ if (selectedEmbedding) {
186
+ const status = selectedEmbedding.configured ? 'configured' : 'unconfigured';
187
+ lines.push(`Selected: ${selectedEmbedding.id} | ${selectedEmbedding.dimensions} dimensions | ${status}${selectedEmbedding.detail ? ` | ${selectedEmbedding.detail}` : ''}`);
188
+ }
177
189
  } else {
178
190
  lines.push(`Context cap overrides the detected local-model context window for this selection.`);
179
191
  }
180
- const filterText = `Search: ${picker.query || '(none)'} | Price: ${picker.categoryFilter} | Capability: ${picker.capabilityFilter} | Group: ${picker.groupBy} | Available only: ${picker.availableOnly ? 'yes' : 'no'}`;
192
+ // W3-T2: a visible cursor glyph on the query itself when search is focused
193
+ // the only prior affordance was a footer-hint text swap ('/ search' vs.
194
+ // 'Typing filters search...'), buried at the end of a long hint line and
195
+ // easy to miss (confirmed via live tmux repro). This puts the "you are
196
+ // typing here" signal where the eye already is, on the Search: value.
197
+ const searchDisplay = picker.query.length > 0
198
+ ? `${picker.query}${picker.searchFocused ? GLYPHS.surface.cursor : ''}`
199
+ : (picker.searchFocused ? GLYPHS.surface.cursor : '(none)');
200
+ const filterText = `Search: ${searchDisplay} | Price: ${picker.categoryFilter} | Capability: ${picker.capabilityFilter} | Group: ${picker.groupBy} | Available only: ${picker.availableOnly ? 'yes' : 'no'}`;
181
201
  lines.push(filterText);
182
202
  return lines.flatMap((line) => wrapText(line, Math.max(1, width)));
183
203
  }
@@ -288,6 +308,28 @@ function renderContextCapRows(picker: ModelPickerModal, lines: Line[], rows: num
288
308
  }
289
309
  }
290
310
 
311
+ function renderEmbeddingProviderRows(picker: ModelPickerModal, lines: Line[], rows: number, startX: number, width: number): void {
312
+ const providers = picker.embeddingProviders;
313
+ const { start, end } = stableWindow(providers.length, picker.selectedIndex, rows);
314
+ for (let visibleRow = 0; visibleRow < rows; visibleRow += 1) {
315
+ const absolute = start + visibleRow;
316
+ const provider = providers[absolute];
317
+ const line = lines[visibleRow]!;
318
+ if (!provider) continue;
319
+ const selected = absolute === picker.selectedIndex;
320
+ const bg = selected ? PALETTE.selectedBg : PALETTE.bodyBg;
321
+ fillRange(line, startX, startX + width - 1, bg);
322
+ const marker = selected ? (picker.focusPane === 'items' ? GLYPHS.navigation.selected : '•') : ' ';
323
+ const status = provider.configured ? 'configured' : 'unconfigured';
324
+ writeText(line, startX + 1, width, padDisplay(marker, 2), { fg: PALETTE.text, bg, bold: selected });
325
+ writeText(line, startX + 3, Math.max(0, width - 30), padDisplay(provider.label, Math.max(0, width - 30)), { fg: selected ? PALETTE.text : PALETTE.muted, bg, bold: selected });
326
+ writeText(line, startX + Math.max(4, width - 25), 12, padDisplay(`${provider.dimensions}d`, 12), { fg: PALETTE.dim, bg });
327
+ writeText(line, startX + Math.max(4, width - 13), 12, padDisplay(status, 12), { fg: provider.configured ? PALETTE.good : PALETTE.warn, bg });
328
+ }
329
+ if (start > 0 && lines[0]) writeText(lines[0], startX + 1, width - 2, `${GLYPHS.navigation.moreAbove} ${start} more provider(s) above`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
330
+ if (end < providers.length && lines[rows - 1]) writeText(lines[rows - 1]!, startX + 1, width - 2, `${GLYPHS.navigation.moreBelow} ${providers.length - end} more provider(s) below`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
331
+ }
332
+
291
333
  function writeTableHeader(line: Line, picker: ModelPickerModal, startX: number, width: number): void {
292
334
  fillRange(line, startX, startX + width - 1, PALETTE.footerBg);
293
335
  const style = { fg: PALETTE.muted, bg: PALETTE.footerBg, bold: true };
@@ -303,6 +345,10 @@ function writeTableHeader(line: Line, picker: ModelPickerModal, startX: number,
303
345
  writeText(line, startX + 1, width - 2, 'Context cap input', style);
304
346
  return;
305
347
  }
348
+ if (picker.mode === 'embeddingProvider') {
349
+ writeText(line, startX + 1, width - 2, 'Embedding provider Dimensions Status', style);
350
+ return;
351
+ }
306
352
  const providerW = clamp(Math.floor(width * 0.14), 10, 18);
307
353
  const ctxW = 8;
308
354
  const tierW = 8;
@@ -337,7 +383,7 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
337
383
  const header = contentLine(safeWidth, PALETTE.footerBg);
338
384
  writeText(header, 2, targetW - 2, 'Targets', { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
339
385
  drawVertical(header, targetW, PALETTE.footerBg);
340
- const modeLabel = picker.mode === 'provider' ? 'Provider list' : picker.mode === 'model' ? 'Model list' : picker.mode === 'effort' ? 'Reasoning effort' : 'Context cap';
386
+ const modeLabel = picker.mode === 'provider' ? 'Provider list' : picker.mode === 'model' ? 'Model list' : picker.mode === 'effort' ? 'Reasoning effort' : picker.mode === 'embeddingProvider' ? 'Embedding providers' : 'Context cap';
341
387
  writeText(header, contentX + 1, contentW - 2, `${modeLabel} • ${picker.getItemCount()} item(s)`, { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
342
388
  lines.push(header);
343
389
 
@@ -380,14 +426,23 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
380
426
  renderModelRows(picker, listLines, listLines.length, contentX, contentW - 1);
381
427
  } else if (picker.mode === 'contextCap') {
382
428
  renderContextCapRows(picker, listLines, listLines.length, contentX, contentW - 1);
429
+ } else if (picker.mode === 'embeddingProvider') {
430
+ renderEmbeddingProviderRows(picker, listLines, listLines.length, contentX, contentW - 1);
383
431
  } else {
384
432
  renderEffortRows(picker, listLines, listLines.length, contentX, contentW - 1);
385
433
  }
386
434
 
387
435
  const footer = contentLine(safeWidth, PALETTE.footerBg);
388
436
  const targetHint = picker.focusPane === 'targets' ? 'Focus targets' : 'Focus list';
389
- const searchHint = picker.searchFocused ? 'Typing filters search; Esc clears search' : '/ search';
390
- const hints = `${targetHint} • Up/Down navigate • Left/Right pane Enter select ${searchHint} Tab price • C caps • A available • B benchmark • G group • Esc close`;
437
+ // W3 Finding 2: while search is focused, every printable key (including
438
+ // c/a/b/g) types into the query handler-picker-routes.ts guards those
439
+ // single-key shortcuts with `!searchFocused` (Up/Down blur search first).
440
+ // Advertising them here regardless of focus state was a footer that lied
441
+ // about what the keys currently do. Tab still cycles price in both states
442
+ // (its handler has no searchFocused guard), so it stays in both hint sets.
443
+ const hints = picker.searchFocused
444
+ ? `${targetHint} • Typing filters search; Esc clears search • Down: list shortcuts • Tab price • Esc close`
445
+ : `${targetHint} • Up/Down navigate • Left/Right pane • Enter select • / search • Tab price • C caps • A available • B benchmark • G group • Esc close`;
391
446
  writeText(footer, 2, safeWidth - 4, hints, { fg: PALETTE.muted, bg: PALETTE.footerBg });
392
447
  lines.push(footer);
393
448
  lines.push(borderLine(safeWidth, GLYPHS.frame.bottomLeft, GLYPHS.frame.horizontal, GLYPHS.frame.bottomRight));
@@ -432,6 +487,8 @@ function getRenderCacheKey(picker: ModelPickerModal, width: number, viewportHeig
432
487
  base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
433
488
  } else if (picker.mode === 'contextCap') {
434
489
  base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
490
+ } else if (picker.mode === 'embeddingProvider') {
491
+ base.push(objectId(picker.embeddingProviders), picker.embeddingProviders.length);
435
492
  }
436
493
 
437
494
  return base.join('\u001e');
@@ -467,6 +524,7 @@ function keyForTargets(values: readonly ModelPickerTargetInfo[]): string {
467
524
  entry.model,
468
525
  entry.enabled ? 1 : 0,
469
526
  entry.inherited ? 1 : 0,
527
+ entry.configuredNote ?? '',
470
528
  ].join('\u001d'))
471
529
  .join('\u001f');
472
530
  }
@@ -1,14 +1,9 @@
1
1
  import { createStyledCell, type Line } from '../types/grid.ts';
2
2
  import { getOverlayMaxWidth } from './overlay-viewport.ts';
3
3
  import { GLYPHS } from './ui-primitives.ts';
4
- import { resolveUiTones } from './theme.ts';
4
+ import { activeUiTones, registerThemeRefresh } from './theme.ts';
5
5
  import { fillWidth, makeLine, writeText } from './fullscreen-primitives.ts';
6
6
 
7
- // DEFAULT_OVERLAY_PALETTE is built from the mode-resolved chrome tones
8
- // (resolveUiTones) rather than the static UI_TONES constant — WO-001 single
9
- // read path. Mode is fixed to 'dark' until the terminal-bg-probe lands.
10
- const TONES = resolveUiTones('dark');
11
-
12
7
  export interface OverlayBoxPalette {
13
8
  readonly borderFg: string;
14
9
  readonly titleFg: string;
@@ -21,17 +16,26 @@ export interface OverlayBoxPalette {
21
16
  readonly bodyBg: string;
22
17
  }
23
18
 
24
- export const DEFAULT_OVERLAY_PALETTE: Readonly<OverlayBoxPalette> = {
25
- borderFg: TONES.fg.secondary,
26
- titleFg: TONES.fg.primary,
27
- bodyFg: TONES.fg.primary,
28
- mutedFg: TONES.fg.dim,
29
- selectedBg: TONES.bg.selected,
30
- titleBg: TONES.bg.title,
31
- sectionBg: TONES.bg.section,
32
- inputBg: TONES.bg.input,
33
- bodyBg: TONES.bg.surface,
34
- } as const;
19
+ // Built from the mode-resolved chrome tones (activeUiTones) and rebuilt IN PLACE
20
+ // on a mode flip via the registered refresher (60+ call sites read it by
21
+ // reference — see theme.ts's active-mode runtime note).
22
+ function buildOverlayPalette(): OverlayBoxPalette {
23
+ const t = activeUiTones();
24
+ return {
25
+ borderFg: t.fg.secondary,
26
+ titleFg: t.fg.primary,
27
+ bodyFg: t.fg.primary,
28
+ mutedFg: t.fg.dim,
29
+ selectedBg: t.bg.selected,
30
+ titleBg: t.bg.title,
31
+ sectionBg: t.bg.section,
32
+ inputBg: t.bg.input,
33
+ bodyBg: t.bg.surface,
34
+ };
35
+ }
36
+
37
+ export const DEFAULT_OVERLAY_PALETTE: Readonly<OverlayBoxPalette> = buildOverlayPalette();
38
+ registerThemeRefresh(() => Object.assign(DEFAULT_OVERLAY_PALETTE as OverlayBoxPalette, buildOverlayPalette()));
35
39
 
36
40
  export interface OverlayBoxLayout {
37
41
  readonly margin: number;
@@ -21,12 +21,18 @@ export function renderPanelWorkspaceBar(
21
21
  return renderTabStrip({
22
22
  width,
23
23
  onLayout,
24
- tabs: tabs.map((tab) => ({
24
+ tabs: tabs.map((tab, i) => ({
25
25
  // tab.active = selected in its own pane (drives highlighted background).
26
26
  // tab.focused = has keyboard focus (drives brighter text / focus indicator).
27
27
  // A tab can be active-but-not-focused (selected in the unfocused pane) or
28
28
  // active-and-focused (selected in the focused pane).
29
- label: `${tab.pane === 'bottom' ? 'v' : '^'} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
29
+ //
30
+ // A14: the pane marker uses ▲/▼ (top/bottom pane). The old '^'/'v'
31
+ // prefixes read as a Ctrl caret (^) implying a non-existent Ctrl+<tab>
32
+ // hotkey. The REAL per-tab jump key is Alt+N (panel-tab-1..9), rendered
33
+ // here as ⌥N for the first nine tabs so the shown affordance matches the
34
+ // binding (tabs past nine have no Alt+N chord, so no index is shown).
35
+ label: `${tab.pane === 'bottom' ? '▼' : '▲'}${i < 9 ? ` ⌥${i + 1}` : ''} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
30
36
  active: tab.active,
31
37
  })),
32
38
  prefixLabel: ' PANELS ',