@pellux/goodvibes-tui 1.7.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +10 -9
  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/config/credential-availability.ts +122 -0
  12. package/src/config/index.ts +15 -0
  13. package/src/core/composer-state.ts +11 -3
  14. package/src/core/conversation-line-cache.ts +27 -3
  15. package/src/core/conversation-rendering.ts +71 -14
  16. package/src/core/stream-event-wiring.ts +20 -1
  17. package/src/core/system-message-noise.ts +87 -0
  18. package/src/core/system-message-router.ts +68 -1
  19. package/src/core/turn-cancellation.ts +7 -2
  20. package/src/core/turn-event-wiring.ts +10 -2
  21. package/src/daemon/cli.ts +29 -2
  22. package/src/daemon/handlers/register.ts +8 -1
  23. package/src/daemon/service-commands.ts +329 -0
  24. package/src/input/autocomplete.ts +27 -1
  25. package/src/input/command-registry.ts +46 -4
  26. package/src/input/commands/codebase-runtime.ts +46 -6
  27. package/src/input/commands/config.ts +43 -3
  28. package/src/input/commands/health-runtime.ts +9 -1
  29. package/src/input/commands/memory.ts +68 -35
  30. package/src/input/commands/operator-panel-runtime.ts +31 -7
  31. package/src/input/commands/planning-runtime.ts +95 -6
  32. package/src/input/commands/qrcode-runtime.ts +25 -5
  33. package/src/input/commands/remote-runtime-setup.ts +5 -3
  34. package/src/input/commands/session-content.ts +20 -9
  35. package/src/input/commands/settings-sync-runtime.ts +15 -3
  36. package/src/input/commands/shell-core.ts +10 -1
  37. package/src/input/commands/workstream-runtime.ts +168 -18
  38. package/src/input/config-modal-types.ts +15 -1
  39. package/src/input/config-modal.ts +227 -12
  40. package/src/input/feed-context-factory.ts +3 -0
  41. package/src/input/handler-command-route.ts +10 -3
  42. package/src/input/handler-content-actions.ts +17 -2
  43. package/src/input/handler-feed-routes.ts +43 -121
  44. package/src/input/handler-feed.ts +32 -4
  45. package/src/input/handler-modal-routes.ts +78 -13
  46. package/src/input/handler-modal-stack.ts +11 -2
  47. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  48. package/src/input/handler-onboarding.ts +71 -59
  49. package/src/input/handler-picker-routes.ts +15 -8
  50. package/src/input/handler-shortcuts.ts +59 -9
  51. package/src/input/handler.ts +6 -1
  52. package/src/input/keybindings.ts +6 -5
  53. package/src/input/model-picker.ts +19 -2
  54. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  55. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  56. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  57. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  58. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  59. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  60. package/src/input/panel-mouse-geometry.ts +97 -0
  61. package/src/input/panel-paste-flood-guard.ts +86 -0
  62. package/src/input/selection-modal.ts +11 -0
  63. package/src/input/session-picker-modal.ts +44 -4
  64. package/src/input/settings-modal-data.ts +51 -2
  65. package/src/input/settings-modal-types.ts +4 -2
  66. package/src/main.ts +28 -27
  67. package/src/panels/builtin/shared.ts +9 -3
  68. package/src/panels/fleet-deep-link.ts +31 -0
  69. package/src/panels/fleet-panel-format.ts +62 -0
  70. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  71. package/src/panels/fleet-panel.ts +89 -131
  72. package/src/panels/fleet-read-model.ts +43 -0
  73. package/src/panels/fleet-steer.ts +35 -2
  74. package/src/panels/fleet-stop.ts +29 -1
  75. package/src/panels/modals/keybindings-modal.ts +16 -1
  76. package/src/panels/modals/modal-theme.ts +35 -29
  77. package/src/panels/modals/pairing-modal.ts +25 -6
  78. package/src/panels/modals/planning-modal.ts +43 -21
  79. package/src/panels/modals/work-plan-modal.ts +28 -0
  80. package/src/panels/panel-manager.ts +15 -4
  81. package/src/panels/polish-core.ts +38 -25
  82. package/src/panels/project-planning-answer-actions.ts +8 -13
  83. package/src/panels/types.ts +24 -0
  84. package/src/permissions/prompt.ts +160 -13
  85. package/src/renderer/autocomplete-overlay.ts +28 -2
  86. package/src/renderer/compositor.ts +2 -3
  87. package/src/renderer/config-modal.ts +19 -5
  88. package/src/renderer/footer-tips.ts +5 -1
  89. package/src/renderer/fullscreen-primitives.ts +32 -22
  90. package/src/renderer/git-status.ts +3 -1
  91. package/src/renderer/layout.ts +0 -4
  92. package/src/renderer/markdown.ts +7 -3
  93. package/src/renderer/modal-factory.ts +25 -20
  94. package/src/renderer/model-workspace.ts +18 -3
  95. package/src/renderer/overlay-box.ts +21 -17
  96. package/src/renderer/process-indicator.ts +14 -3
  97. package/src/renderer/selection-modal-overlay.ts +6 -1
  98. package/src/renderer/session-picker-modal.ts +196 -3
  99. package/src/renderer/settings-modal-helpers.ts +2 -0
  100. package/src/renderer/settings-modal.ts +7 -0
  101. package/src/renderer/shell-surface.ts +8 -1
  102. package/src/renderer/status-glyphs.ts +14 -15
  103. package/src/renderer/system-message.ts +15 -3
  104. package/src/renderer/terminal-bg-probe.ts +339 -0
  105. package/src/renderer/terminal-escapes.ts +20 -0
  106. package/src/renderer/theme-mode-config.ts +67 -0
  107. package/src/renderer/theme.ts +91 -1
  108. package/src/renderer/thinking.ts +11 -3
  109. package/src/renderer/tool-call.ts +15 -9
  110. package/src/renderer/tool-result-summary.ts +148 -0
  111. package/src/renderer/turn-injection.ts +22 -3
  112. package/src/renderer/ui-factory.ts +154 -85
  113. package/src/renderer/ui-primitives.ts +30 -129
  114. package/src/runtime/bootstrap-command-context.ts +6 -0
  115. package/src/runtime/bootstrap-command-parts.ts +8 -4
  116. package/src/runtime/bootstrap-core.ts +46 -24
  117. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  118. package/src/runtime/bootstrap-shell.ts +19 -1
  119. package/src/runtime/bootstrap.ts +118 -5
  120. package/src/runtime/code-index-services.ts +25 -2
  121. package/src/runtime/legacy-daemon-migration.ts +516 -0
  122. package/src/runtime/memory-fold.ts +55 -0
  123. package/src/runtime/onboarding/derivation.ts +7 -2
  124. package/src/runtime/onboarding/snapshot.ts +27 -1
  125. package/src/runtime/onboarding/types.ts +31 -1
  126. package/src/runtime/operator-token-cleanup.ts +82 -1
  127. package/src/runtime/orchestrator-core-services.ts +10 -0
  128. package/src/runtime/resume-notice.ts +209 -0
  129. package/src/runtime/services.ts +12 -8
  130. package/src/runtime/session-inbound-inputs.ts +252 -0
  131. package/src/runtime/session-spine-transport.ts +64 -0
  132. package/src/runtime/terminal-output-guard.ts +15 -8
  133. package/src/runtime/ui-services.ts +19 -3
  134. package/src/runtime/workstream-services.ts +160 -28
  135. package/src/runtime/wrfc-persistence.ts +124 -17
  136. package/src/shell/blocking-input.ts +46 -3
  137. package/src/shell/recovery-input-helpers.ts +170 -1
  138. package/src/shell/ui-openers.ts +42 -9
  139. package/src/utils/terminal-width.ts +52 -0
  140. package/src/version.ts +1 -1
@@ -81,12 +81,35 @@ export function renderAutocompleteOverlay(
81
81
  }
82
82
  const endIdx = Math.min(startIdx + maxVisible, total);
83
83
 
84
+ // UX-C palette curation (item 4): on a bare '/' (query === ''), the results
85
+ // list is "common tier" (score 2) followed by "alphabetical rest" (score
86
+ // 1) — see CommandRegistry.fuzzyMatch. commonCount marks that boundary;
87
+ // draw a one-row separator there when it falls inside the visible window,
88
+ // consuming one of the maxVisible slots so the box height never changes.
89
+ const hasCommonSeparator = state.query === '' && state.commonCount > 0 && state.commonCount < total;
90
+ type DisplayRow = { type: 'item'; index: number } | { type: 'separator' };
91
+ const displayRows: DisplayRow[] = [];
92
+ for (let i = startIdx; i < endIdx && displayRows.length < maxVisible; i++) {
93
+ if (hasCommonSeparator && i === state.commonCount) {
94
+ displayRows.push({ type: 'separator' });
95
+ if (displayRows.length >= maxVisible) break;
96
+ }
97
+ displayRows.push({ type: 'item', index: i });
98
+ }
99
+
84
100
  const indicatorWidth = 2;
85
101
  const maxCommandWidth = Math.min(18, Math.max(10, Math.floor(layout.innerWidth * 0.28)));
86
102
  const gapWidth = 2;
87
103
  const descWidth = Math.max(0, layout.innerWidth - indicatorWidth - maxCommandWidth - gapWidth);
88
104
 
89
- for (let i = startIdx; i < endIdx; i++) {
105
+ for (const row of displayRows) {
106
+ if (row.type === 'separator') {
107
+ const sepLine = createOverlayContentLine(width, layout);
108
+ putText(sepLine, layout.margin + 2, layout.innerWidth, '─'.repeat(layout.innerWidth), { fg: BORDER_FG, dim: true });
109
+ lines.push(sepLine);
110
+ continue;
111
+ }
112
+ const i = row.index;
90
113
  const { command } = results[i];
91
114
  const isSelected = i === state.selectedIndex;
92
115
  const line = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : '');
@@ -139,7 +162,10 @@ export function renderAutocompleteOverlay(
139
162
  }
140
163
 
141
164
  const footerLine = createOverlayContentLine(width, layout);
142
- const hints = '[Tab] Complete [Up/Down] Navigate [Enter] Execute [Esc] Cancel';
165
+ // UX-C vocab unification: "Run"/"Close" match the /help selection modal's
166
+ // hint bar (selection-modal-overlay.ts) — both surfaces execute a command
167
+ // on Enter, so they now say so the same way.
168
+ const hints = '[Tab] Complete [Up/Down] Navigate [Enter] Run [Esc] Close';
143
169
  putText(
144
170
  footerLine,
145
171
  layout.margin + 2,
@@ -5,11 +5,9 @@ import { getDisplayWidth } from '../utils/terminal-width.ts';
5
5
  import type { SearchManager } from '../input/search.ts';
6
6
  import { allowTerminalWrite } from '../runtime/terminal-output-guard.ts';
7
7
  import { probeTermCaps, type TermColorCaps } from './term-caps.ts';
8
- import { DARK_THEME } from './theme.ts';
8
+ import { activeTheme } from './theme.ts';
9
9
  import { UI_TONES } from './ui-primitives.ts';
10
10
 
11
- const T = DARK_THEME;
12
-
13
11
  // Accent / dim colors for the panel focus border. The focused pane's left
14
12
  // border column is drawn in the accent tone; the unfocused pane stays dim.
15
13
  const PANEL_FOCUS_ACCENT = UI_TONES.state.active; // bright blue
@@ -252,6 +250,7 @@ export class Compositor {
252
250
 
253
251
  // Apply Search Match Highlighting Overlay (left side only)
254
252
  if (search && search.manager.active && search.manager.query.length > 0 && i >= offset) {
253
+ const T = activeTheme();
255
254
  const absoluteRow = search.scrollTop + (i - offset);
256
255
  const lineMatches = search.manager.getMatchesOnLine(absoluteRow);
257
256
  for (const match of lineMatches) {
@@ -1,10 +1,15 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { ModalFactory, type ModalSection, type ModalTab } from './modal-factory.ts';
3
- import { resolveUiTones } from './theme.ts';
3
+ import { getOverlayMaxWidth } from './overlay-viewport.ts';
4
+ import { activeUiTones } from './theme.ts';
4
5
  import type { ConfigModal, ConfigModalRenderModel } from '../input/config-modal.ts';
5
6
 
6
- const TONES = resolveUiTones('dark');
7
-
7
+ // Matches ModalFactory.createModal's own box-width computation (margin 4,
8
+ // requested max width 76 — see renderConfigModalModel below) so the
9
+ // wrap-clamp in ConfigModal.getRenderModel() measures against the SAME width
10
+ // the list section will actually wrap at, at the current terminal size.
11
+ const MODAL_MARGIN = 4;
12
+ const MODAL_MAX_WIDTH = 76;
8
13
  /**
9
14
  * renderConfigModal — the single render path for every ConfigModalSurface.
10
15
  * Reads the host's frozen-structure-plus-live-values render model and maps it
@@ -17,12 +22,21 @@ export function renderConfigModal(modal: ConfigModal, width: number, height: num
17
22
  // rather than overflowing (posture header + separator + chrome ≈ 8 rows).
18
23
  const visible = Math.max(3, Math.min(16, height - 8));
19
24
  modal.setViewportRows(visible);
20
- const model = modal.getRenderModel();
25
+ // DEBT-5 item 2: the list section wraps a row label at (contentW - 2),
26
+ // where contentW = boxW - 4 (see ModalFactory._renderListSection). Compute
27
+ // that exact column here so getRenderModel's wrap-clamp measures against
28
+ // reality instead of a fixed guess.
29
+ const boxW = Math.max(24, getOverlayMaxWidth(width, MODAL_MARGIN, MODAL_MAX_WIDTH));
30
+ const labelWrapWidth = Math.max(8, boxW - 4 - 2);
31
+ const model = modal.getRenderModel(labelWrapWidth);
21
32
  return renderConfigModalModel(model, width);
22
33
  }
23
34
 
24
35
  /** Pure model→Line[] mapping — the shared inner step of renderConfigModal. */
25
36
  function renderConfigModalModel(model: ConfigModalRenderModel, width: number): Line[] {
37
+ // Read the active-mode chrome tones per call (this render path is not on a
38
+ // hot loop; the modal renders once per keystroke).
39
+ const TONES = activeUiTones();
26
40
  const sections: ModalSection[] = [];
27
41
 
28
42
  if (model.degraded) {
@@ -69,7 +83,7 @@ function renderConfigModalModel(model: ConfigModalRenderModel, width: number): L
69
83
  return ModalFactory.createModal(
70
84
  {
71
85
  title: model.title,
72
- width: 76,
86
+ width: MODAL_MAX_WIDTH,
73
87
  tabs,
74
88
  sections,
75
89
  targetContentRows,
@@ -24,7 +24,11 @@ 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';
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';
28
32
  // W6.2 e: F2 now opens the Fleet panel (the process modal was retired), so the
29
33
  // tip names 'fleet', not 'processes'.
30
34
  const TIP_PROCESSES = 'F2 fleet';
@@ -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
 
@@ -110,7 +110,9 @@ export class GitStatusProvider {
110
110
  statusResult.conflicted.length > 0 ||
111
111
  statusResult.not_added.length > 0;
112
112
  this.cache = {
113
- 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',
114
116
  dirty,
115
117
  ahead: statusResult.ahead ?? 0,
116
118
  behind: statusResult.behind ?? 0,
@@ -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
 
@@ -189,7 +189,15 @@ function detailLines(picker: ModelPickerModal, width: number): string[] {
189
189
  } else {
190
190
  lines.push(`Context cap overrides the detected local-model context window for this selection.`);
191
191
  }
192
- 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'}`;
193
201
  lines.push(filterText);
194
202
  return lines.flatMap((line) => wrapText(line, Math.max(1, width)));
195
203
  }
@@ -426,8 +434,15 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
426
434
 
427
435
  const footer = contentLine(safeWidth, PALETTE.footerBg);
428
436
  const targetHint = picker.focusPane === 'targets' ? 'Focus targets' : 'Focus list';
429
- const searchHint = picker.searchFocused ? 'Typing filters search; Esc clears search' : '/ search';
430
- 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`;
431
446
  writeText(footer, 2, safeWidth - 4, hints, { fg: PALETTE.muted, bg: PALETTE.footerBg });
432
447
  lines.push(footer);
433
448
  lines.push(borderLine(safeWidth, GLYPHS.frame.bottomLeft, GLYPHS.frame.horizontal, GLYPHS.frame.bottomRight));
@@ -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;
@@ -2,6 +2,7 @@ import { type Line } from '../types/grid.ts';
2
2
  import { UIFactory } from './ui-factory.ts';
3
3
  import { truncateDisplay } from '../utils/terminal-width.ts';
4
4
  import { GLYPHS, UI_TONES } from './ui-primitives.ts';
5
+ import { activeUiTones } from './theme.ts';
5
6
  import { formatHints } from './hint-grammar.ts';
6
7
 
7
8
  /**
@@ -12,6 +13,16 @@ import { formatHints } from './hint-grammar.ts';
12
13
  * background exec processes are running. Includes an `[Enter] View` hint
13
14
  * (hint-grammar bracket form) when active.
14
15
  */
16
+ /**
17
+ * Agent-count label. The footer counts only ACTIVE agents, while the fleet
18
+ * lists every node (running, terminal, chains, watchers). Label the count
19
+ * "active" so it is never misread as a grand total — the [Enter] View hint
20
+ * opens the fleet for the full picture. (UX-B item 5d.)
21
+ */
22
+ function agentCountLabel(agentCount: number): string {
23
+ return `${agentCount} agent${agentCount !== 1 ? 's' : ''} active`;
24
+ }
25
+
15
26
  export function renderProcessIndicator(
16
27
  width: number,
17
28
  agentCount: number,
@@ -47,7 +58,7 @@ export function renderProcessIndicator(
47
58
  // --- Focused state: always render before idle/active branches ---
48
59
  if (focused) {
49
60
  const parts: string[] = [];
50
- if (agentCount > 0) parts.push(`${agentCount} agent${agentCount !== 1 ? 's' : ''}`);
61
+ if (agentCount > 0) parts.push(agentCountLabel(agentCount));
51
62
  if (toolCount > 0) parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
52
63
  const label = total === 0
53
64
  ? `No background processes ${formatHints([{ key: 'Esc', verb: 'Back to input' }])}`
@@ -62,7 +73,7 @@ export function renderProcessIndicator(
62
73
  // Build the label: "bg: 2 agents | Turn 3 | write - src/foo.ts"
63
74
  const parts: string[] = [];
64
75
  if (agentCount > 0) {
65
- parts.push(`${agentCount} agent${agentCount !== 1 ? 's' : ''}`);
76
+ parts.push(agentCountLabel(agentCount));
66
77
  }
67
78
  if (toolCount > 0) {
68
79
  parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
@@ -82,5 +93,5 @@ export function renderProcessIndicator(
82
93
  : '';
83
94
  const label = `${parts.join(` ${GLYPHS.navigation.pipeSeparator} `)}${progressSuffix}`;
84
95
  const hint = ` ${formatHints([{ key: 'Enter', verb: 'View' }])}`;
85
- return renderPlainStatus(`${label}${hint}`, { fg: UI_TONES.accent.brand, bold: true });
96
+ return renderPlainStatus(`${label}${hint}`, { fg: activeUiTones().accent.brand, bold: true });
86
97
  }
@@ -189,7 +189,12 @@ export function renderSelectionModalOverlay(
189
189
 
190
190
  const footerLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
191
191
  const selectedItem = modal.getSelected();
192
- const primaryVerb = selectedItem?.primaryAction === 'toggle'
192
+ // UX-C vocab unification: a caller-supplied primaryVerbLabel (e.g. /help's
193
+ // "Run", matching the slash-command palette) wins over the generic
194
+ // per-primaryAction default.
195
+ const primaryVerb = modal.primaryVerbLabel
196
+ ? `[Enter] ${modal.primaryVerbLabel}`
197
+ : selectedItem?.primaryAction === 'toggle'
193
198
  ? '[Enter] Toggle'
194
199
  : selectedItem?.primaryAction === 'edit'
195
200
  ? '[Enter] Edit'