@pellux/goodvibes-tui 1.0.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. package/src/renderer/qr-renderer.ts +0 -120
@@ -18,8 +18,10 @@
18
18
 
19
19
  import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core';
20
20
  import { computeContextUsage } from '../core/context-usage.ts';
21
+ import { formatQualityScoreLine } from './compaction-quality.ts';
21
22
  import type { CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
22
23
  import type { ProviderMessage } from '@pellux/goodvibes-sdk/platform/providers';
24
+ import type { CompactionQualityScore } from './compaction-quality.ts';
23
25
 
24
26
  /**
25
27
  * Default compaction totalCeiling from context-compaction DEFAULT_COMPACTION_CONFIG.
@@ -44,6 +46,12 @@ export interface CompactionAfterOptions {
44
46
  readonly event: CompactionEvent;
45
47
  /** Number of session memories that survived compaction. */
46
48
  readonly pinnedMemoryCount: number;
49
+ /**
50
+ * Out-of-band quality-score grade for this run (W5.4/B28). Omitted or null
51
+ * when no score was computed (e.g. the small-window compaction path, which
52
+ * has no CompactionEvent to key a score by) — no line is rendered then.
53
+ */
54
+ readonly qualityScore?: CompactionQualityScore | null | undefined;
47
55
  }
48
56
 
49
57
  /**
@@ -82,7 +90,7 @@ export function buildCompactionPreview(opts: CompactionPreviewOptions): string {
82
90
  * after token counts. The trigger field controls wording.
83
91
  */
84
92
  export function buildCompactionAfterNotice(opts: CompactionAfterOptions): string {
85
- const { event, pinnedMemoryCount } = opts;
93
+ const { event, pinnedMemoryCount, qualityScore } = opts;
86
94
  const {
87
95
  messagesBeforeCompaction,
88
96
  messagesAfterCompaction,
@@ -102,11 +110,14 @@ export function buildCompactionAfterNotice(opts: CompactionAfterOptions): string
102
110
 
103
111
  const triggerStr = trigger === 'auto' ? 'Auto-compact complete' : 'Compact complete';
104
112
 
113
+ const qualityStr = qualityScore ? `\n ${formatQualityScoreLine(qualityScore)}` : '';
114
+
105
115
  return (
106
116
  `[Context] ${triggerStr}: ${messagesBeforeCompaction} → ${messagesAfterCompaction} messages,` +
107
117
  ` ~${fmtN(tokensBeforeEstimate)} → ~${fmtN(tokensAfterEstimate)} tokens` +
108
118
  ` (saved ~${fmtN(savings)}, ${savingsPct}%).` +
109
- (pinStr ? ` ${pinStr.trim()}` : '')
119
+ (pinStr ? ` ${pinStr.trim()}` : '') +
120
+ qualityStr
110
121
  );
111
122
  }
112
123
 
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Compaction quality-score bridging (Wave-5 W5.4 / B28).
3
+ *
4
+ * Reality check (wo803): the SDK's `computeQualityScore()`
5
+ * (platform/runtime/compaction/quality-score.ts) is only ever called from a
6
+ * SEPARATE pipeline, `CompactionManager` (platform/runtime/compaction/manager.ts).
7
+ * The TUI's actual `/compact` path never instantiates `CompactionManager` — it
8
+ * goes through `platform/core/context-compaction.ts` via
9
+ * `ConversationManager.compact()` (see `compactConversation()` in
10
+ * runtime-services.ts), which has no scoring of its own. Adopting the whole
11
+ * `CompactionManager` pipeline just to get the score would be a far bigger
12
+ * change than this item calls for.
13
+ *
14
+ * Instead, this module calls `computeQualityScore()` directly and out-of-band,
15
+ * fed by the before/after message sets and token estimates
16
+ * `compactConversation()` already holds. That's enough to close the gap:
17
+ * quality scoring reaches `/compact` and `/compact-history` without adopting
18
+ * `CompactionManager`.
19
+ *
20
+ * Honesty note: the `strategy` value passed to `computeQualityScore()` is a
21
+ * BORROWED rubric label describing what `compactConversation()`'s structured
22
+ * multi-section extraction actually does (collapse the whole conversation
23
+ * into one handoff message) — it is NOT a real `CompactionStrategy`
24
+ * escalation result, and no strategy switch is triggered from it. Every
25
+ * rendering of the score says so explicitly (see `formatQualityScoreLine`).
26
+ *
27
+ * Storage: scores are kept in a TUI-local, timestamp-keyed store, never added
28
+ * to the SDK's public `CompactionEvent` type (that would force an
29
+ * api-extractor regen for a value the SDK's own compaction log never
30
+ * populates). Keyed by `CompactionEvent.timestamp` rather than array index,
31
+ * because `compactSmallWindow`'s null-event path (small-context-window
32
+ * models — see runtime-services.ts) can create index gaps.
33
+ */
34
+
35
+ import { computeQualityScore, describeScore } from '@/runtime/index.ts';
36
+ import type { CompactionQualityScore, StrategyInput, StrategyOutput } from '@/runtime/index.ts';
37
+ import type { ProviderMessage } from '@pellux/goodvibes-sdk/platform/providers';
38
+
39
+ export type { CompactionQualityScore } from '@/runtime/index.ts';
40
+
41
+ /**
42
+ * Bound the TUI-local score store the same way the SDK bounds its own
43
+ * compaction event log (context-compaction.ts caps at 50 and shifts the
44
+ * oldest entry) — a long-running session should not accumulate this forever.
45
+ */
46
+ const MAX_TRACKED_SCORES = 50;
47
+
48
+ const scoresByTimestamp = new Map<number, CompactionQualityScore>();
49
+
50
+ /** Record a computed quality score, keyed by the CompactionEvent's timestamp. */
51
+ export function recordCompactionQualityScore(timestamp: number, score: CompactionQualityScore): void {
52
+ scoresByTimestamp.set(timestamp, score);
53
+ if (scoresByTimestamp.size > MAX_TRACKED_SCORES) {
54
+ const oldestKey = scoresByTimestamp.keys().next().value;
55
+ if (oldestKey !== undefined) scoresByTimestamp.delete(oldestKey);
56
+ }
57
+ }
58
+
59
+ /** Look up a previously-recorded quality score for a CompactionEvent, by timestamp. */
60
+ export function getCompactionQualityScore(timestamp: number): CompactionQualityScore | undefined {
61
+ return scoresByTimestamp.get(timestamp);
62
+ }
63
+
64
+ export interface ScoreCompactionRunInput {
65
+ readonly sessionId: string;
66
+ readonly contextWindow: number;
67
+ readonly messagesBefore: readonly ProviderMessage[];
68
+ readonly messagesAfter: readonly ProviderMessage[];
69
+ readonly tokensBefore: number;
70
+ readonly tokensAfter: number;
71
+ }
72
+
73
+ /**
74
+ * Compute a CompactionQualityScore for one compaction run, out-of-band from
75
+ * the SDK's own (unused, in this TUI) CompactionManager pipeline.
76
+ */
77
+ export function scoreCompactionRun(input: ScoreCompactionRunInput): CompactionQualityScore {
78
+ const strategyInput: StrategyInput = {
79
+ sessionId: input.sessionId,
80
+ messages: input.messagesBefore,
81
+ tokensBefore: input.tokensBefore,
82
+ contextWindow: input.contextWindow,
83
+ // Borrowed rubric label, not a real strategy escalation — see module doc.
84
+ strategy: 'collapse',
85
+ };
86
+ const strategyOutput: StrategyOutput = {
87
+ messages: [...input.messagesAfter],
88
+ tokensAfter: input.tokensAfter,
89
+ summary: '',
90
+ strategy: 'collapse',
91
+ durationMs: 0,
92
+ warnings: [],
93
+ };
94
+ return computeQualityScore(strategyInput, strategyOutput);
95
+ }
96
+
97
+ /** One-line grade summary for the /compact after-notice and /compact-history entries. */
98
+ export function formatQualityScoreLine(score: CompactionQualityScore): string {
99
+ return `Quality: ${describeScore(score)} (rubric applied out-of-band; no strategy escalation ran)`;
100
+ }
@@ -0,0 +1,81 @@
1
+ import type { Line } from '../types/grid.ts';
2
+ import { ModalFactory, type ModalSection, type ModalTab } from './modal-factory.ts';
3
+ import { resolveUiTones } from './theme.ts';
4
+ import type { ConfigModal, ConfigModalRenderModel } from '../input/config-modal.ts';
5
+
6
+ const TONES = resolveUiTones('dark');
7
+
8
+ /**
9
+ * renderConfigModal — the single render path for every ConfigModalSurface.
10
+ * Reads the host's frozen-structure-plus-live-values render model and maps it
11
+ * onto ModalFactory.createModal (the same renderer settings-modal uses), so the
12
+ * chrome, tab strip, list, and footer are one dialect across all migrated
13
+ * surfaces. Pure: takes the modal's render model, returns Line[].
14
+ */
15
+ export function renderConfigModal(modal: ConfigModal, width: number, height: number): Line[] {
16
+ // Size the visible row window to the available height so the list scrolls
17
+ // rather than overflowing (posture header + separator + chrome ≈ 8 rows).
18
+ const visible = Math.max(3, Math.min(16, height - 8));
19
+ modal.setViewportRows(visible);
20
+ const model = modal.getRenderModel();
21
+ return renderConfigModalModel(model, width);
22
+ }
23
+
24
+ /** Pure model→Line[] mapping — the shared inner step of renderConfigModal. */
25
+ function renderConfigModalModel(model: ConfigModalRenderModel, width: number): Line[] {
26
+ const sections: ModalSection[] = [];
27
+
28
+ if (model.degraded) {
29
+ sections.push({ type: 'text', content: `⚠ ${model.degraded}`, style: { fg: TONES.state.warn, bold: true } });
30
+ sections.push({ type: 'separator' });
31
+ }
32
+
33
+ for (const line of model.header) {
34
+ sections.push({ type: 'text', content: line, style: { fg: TONES.fg.secondary } });
35
+ }
36
+ if (model.header.length > 0) sections.push({ type: 'separator' });
37
+
38
+ if (model.rows.length === 0) {
39
+ sections.push({ type: 'text', content: model.emptyText ?? 'Nothing to show.', style: { fg: TONES.fg.muted, dim: true } });
40
+ } else {
41
+ sections.push({
42
+ type: 'list',
43
+ items: model.rows.map((row) => ({
44
+ label: row.label,
45
+ selected: row.selected,
46
+ style: row.stale
47
+ ? { fg: TONES.fg.muted, dim: true }
48
+ : row.style,
49
+ })),
50
+ });
51
+ }
52
+
53
+ const tabs: ModalTab[] | undefined = model.tabs.length > 1
54
+ ? model.tabs.map((t) => ({ label: t.label, active: t.active }))
55
+ : undefined;
56
+
57
+ // Keep the content region a stable height so live-value ticks never change
58
+ // the box size: frozen header + separator + the fixed visible-row window.
59
+ const targetContentRows =
60
+ (model.degraded ? 2 : 0) +
61
+ model.header.length +
62
+ (model.header.length > 0 ? 1 : 0) +
63
+ model.scroll.visible;
64
+
65
+ const helpers = model.status
66
+ ? [{ content: model.status, accent: true }]
67
+ : undefined;
68
+
69
+ return ModalFactory.createModal(
70
+ {
71
+ title: model.title,
72
+ width: 76,
73
+ tabs,
74
+ sections,
75
+ targetContentRows,
76
+ helpers,
77
+ hints: [...model.hints],
78
+ },
79
+ width,
80
+ );
81
+ }
@@ -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
+ }
@@ -25,9 +25,17 @@ export function isAgentActive(composerStatus: string | undefined): boolean {
25
25
  }
26
26
 
27
27
  const TIP_PANELS = 'Ctrl+P panels';
28
- const TIP_PROCESSES = 'F2 processes';
28
+ // W6.2 e: F2 now opens the Fleet panel (the process modal was retired), so the
29
+ // tip names 'fleet', not 'processes'.
30
+ const TIP_PROCESSES = 'F2 fleet';
29
31
  const TIP_HELP = '? help';
30
- const TIP_QUIT = 'Ctrl+C quit';
32
+ // W6.2 f: an empty-composer Ctrl+C does NOT quit on the first press — it arms a
33
+ // ~1s "press again to exit" confirm, and only a SECOND Ctrl+C within that window
34
+ // exits (an intentional accidental-exit guard). The old 'Ctrl+C quit' tip
35
+ // implied a single press quits, so two presses seconds apart appeared to do
36
+ // nothing. 'Ctrl+C x2 quit' matches the real double-press contract (and the
37
+ // help overlay's "Ctrl+C x2 Exit" row).
38
+ const TIP_QUIT = 'Ctrl+C x2 quit';
31
39
 
32
40
  /**
33
41
  * Build the footer discoverability tip. Segments join with the shared middle
@@ -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;
@@ -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),
@@ -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,6 +179,13 @@ 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
  }
@@ -288,6 +300,28 @@ function renderContextCapRows(picker: ModelPickerModal, lines: Line[], rows: num
288
300
  }
289
301
  }
290
302
 
303
+ function renderEmbeddingProviderRows(picker: ModelPickerModal, lines: Line[], rows: number, startX: number, width: number): void {
304
+ const providers = picker.embeddingProviders;
305
+ const { start, end } = stableWindow(providers.length, picker.selectedIndex, rows);
306
+ for (let visibleRow = 0; visibleRow < rows; visibleRow += 1) {
307
+ const absolute = start + visibleRow;
308
+ const provider = providers[absolute];
309
+ const line = lines[visibleRow]!;
310
+ if (!provider) continue;
311
+ const selected = absolute === picker.selectedIndex;
312
+ const bg = selected ? PALETTE.selectedBg : PALETTE.bodyBg;
313
+ fillRange(line, startX, startX + width - 1, bg);
314
+ const marker = selected ? (picker.focusPane === 'items' ? GLYPHS.navigation.selected : '•') : ' ';
315
+ const status = provider.configured ? 'configured' : 'unconfigured';
316
+ writeText(line, startX + 1, width, padDisplay(marker, 2), { fg: PALETTE.text, bg, bold: selected });
317
+ 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 });
318
+ writeText(line, startX + Math.max(4, width - 25), 12, padDisplay(`${provider.dimensions}d`, 12), { fg: PALETTE.dim, bg });
319
+ writeText(line, startX + Math.max(4, width - 13), 12, padDisplay(status, 12), { fg: provider.configured ? PALETTE.good : PALETTE.warn, bg });
320
+ }
321
+ 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 });
322
+ 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 });
323
+ }
324
+
291
325
  function writeTableHeader(line: Line, picker: ModelPickerModal, startX: number, width: number): void {
292
326
  fillRange(line, startX, startX + width - 1, PALETTE.footerBg);
293
327
  const style = { fg: PALETTE.muted, bg: PALETTE.footerBg, bold: true };
@@ -303,6 +337,10 @@ function writeTableHeader(line: Line, picker: ModelPickerModal, startX: number,
303
337
  writeText(line, startX + 1, width - 2, 'Context cap input', style);
304
338
  return;
305
339
  }
340
+ if (picker.mode === 'embeddingProvider') {
341
+ writeText(line, startX + 1, width - 2, 'Embedding provider Dimensions Status', style);
342
+ return;
343
+ }
306
344
  const providerW = clamp(Math.floor(width * 0.14), 10, 18);
307
345
  const ctxW = 8;
308
346
  const tierW = 8;
@@ -337,7 +375,7 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
337
375
  const header = contentLine(safeWidth, PALETTE.footerBg);
338
376
  writeText(header, 2, targetW - 2, 'Targets', { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
339
377
  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';
378
+ 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
379
  writeText(header, contentX + 1, contentW - 2, `${modeLabel} • ${picker.getItemCount()} item(s)`, { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
342
380
  lines.push(header);
343
381
 
@@ -380,6 +418,8 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
380
418
  renderModelRows(picker, listLines, listLines.length, contentX, contentW - 1);
381
419
  } else if (picker.mode === 'contextCap') {
382
420
  renderContextCapRows(picker, listLines, listLines.length, contentX, contentW - 1);
421
+ } else if (picker.mode === 'embeddingProvider') {
422
+ renderEmbeddingProviderRows(picker, listLines, listLines.length, contentX, contentW - 1);
383
423
  } else {
384
424
  renderEffortRows(picker, listLines, listLines.length, contentX, contentW - 1);
385
425
  }
@@ -432,6 +472,8 @@ function getRenderCacheKey(picker: ModelPickerModal, width: number, viewportHeig
432
472
  base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
433
473
  } else if (picker.mode === 'contextCap') {
434
474
  base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
475
+ } else if (picker.mode === 'embeddingProvider') {
476
+ base.push(objectId(picker.embeddingProviders), picker.embeddingProviders.length);
435
477
  }
436
478
 
437
479
  return base.join('\u001e');
@@ -467,6 +509,7 @@ function keyForTargets(values: readonly ModelPickerTargetInfo[]): string {
467
509
  entry.model,
468
510
  entry.enabled ? 1 : 0,
469
511
  entry.inherited ? 1 : 0,
512
+ entry.configuredNote ?? '',
470
513
  ].join('\u001d'))
471
514
  .join('\u001f');
472
515
  }
@@ -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 ',
@@ -8,6 +8,13 @@ export interface ShellFooterBuildOptions {
8
8
  readonly promptLineCount: number;
9
9
  readonly promptCursorPos?: number;
10
10
  readonly promptFocused?: boolean;
11
+ /**
12
+ * True when the panel workspace owns keyboard focus. Only consulted as a
13
+ * fallback when `promptFocused` is not supplied — see buildShellFooter's
14
+ * default expression. Callers that already compute `promptFocused`
15
+ * themselves (main.ts does) do not need to also pass this.
16
+ */
17
+ readonly panelFocused?: boolean;
11
18
  readonly usage: { up: number; down: number };
12
19
  readonly showExitNotice: boolean;
13
20
  readonly lastCopyTime: number;
@@ -107,7 +114,7 @@ export function buildShellFooter(
107
114
  options.lastInputTokens,
108
115
  options.commandArgsHint,
109
116
  options.hitlMode,
110
- options.promptFocused ?? !options.indicatorFocused,
117
+ options.promptFocused ?? (!options.indicatorFocused && !options.panelFocused),
111
118
  options.composerMode,
112
119
  options.composerStatus,
113
120
  options.composerFlags,