@pellux/goodvibes-tui 0.27.0 → 0.29.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 (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -29,6 +29,8 @@ export interface WorkspaceTab {
29
29
  export class PanelManager {
30
30
  private registry: PanelRegistration[] = [];
31
31
  private retainedPanels = new Map<string, Panel>();
32
+ /** Old/absorbed panel id -> merged target id (WO-1xx console merges). */
33
+ private aliases = new Map<string, string>();
32
34
  private _visible: boolean = false;
33
35
  private _splitRatio: number = 0.6;
34
36
 
@@ -39,15 +41,48 @@ export class PanelManager {
39
41
  private _verticalSplitRatio: number = 0.5; // top gets 50% of panel height
40
42
  private _bottomPaneVisible: boolean = false;
41
43
 
44
+ // Single source of truth for prompt-vs-panel keyboard focus. Previously this
45
+ // lived as a `panelFocused` boolean scattered across InputHandler and the
46
+ // input-routing seams; centralizing it here is what makes it impossible for
47
+ // panel focus to disagree with workspace visibility (see getFocusTarget).
48
+ private _focusTarget: 'prompt' | 'panel' = 'prompt';
49
+
42
50
  // Cache for getWorkspaceTabs() — invalidated on every panel lifecycle event
43
51
  private _cachedWorkspaceTabs: readonly WorkspaceTab[] | null = null;
44
52
 
53
+ // Most-recently-opened panel ids (front = newest), for the picker's "Recent"
54
+ // group. Session-scoped; capped to keep it a short list.
55
+ private _recentlyOpened: string[] = [];
56
+ private static readonly RECENT_CAP = 8;
57
+
58
+ /** Record a panel id as most-recently-opened (front of the ring, deduped). */
59
+ private _recordRecent(panelId: string): void {
60
+ this._recentlyOpened = [panelId, ...this._recentlyOpened.filter((id) => id !== panelId)]
61
+ .slice(0, PanelManager.RECENT_CAP);
62
+ }
63
+
64
+ /** Most-recently-opened panel ids, newest first. */
65
+ getRecentlyOpened(): readonly string[] {
66
+ return this._recentlyOpened;
67
+ }
68
+
45
69
  // -------------------------------------------------------------------------
46
70
  // Registration
47
71
  // -------------------------------------------------------------------------
48
72
 
49
73
  registerType(registration: PanelRegistration): void {
50
74
  const existing = this.registry.findIndex(r => r.id === registration.id);
75
+ // WO-152: registry-time icon-uniqueness assertion. Tab-bar icons are a
76
+ // single glyph; two panels sharing one silently made the workspace tab
77
+ // strip ambiguous (the historical W/R/U/K/M/Q/Y/J/P collisions). Compare
78
+ // against every OTHER registration (excluding a re-registration of the
79
+ // same id, which is a legitimate update, e.g. tests replacing a factory).
80
+ const iconOwner = this.registry.find(r => r.id !== registration.id && r.icon === registration.icon);
81
+ if (iconOwner) {
82
+ throw new Error(
83
+ `Panel icon '${registration.icon}' for '${registration.id}' collides with already-registered panel '${iconOwner.id}'. Panel icons must be unique across the registry.`,
84
+ );
85
+ }
51
86
  if (existing >= 0) {
52
87
  this.registry[existing] = registration;
53
88
  } else {
@@ -55,6 +90,19 @@ export class PanelManager {
55
90
  }
56
91
  }
57
92
 
93
+ /**
94
+ * Register a compat redirect so an absorbed panel's old id still resolves
95
+ * after a console merge (docs, saved layouts, and muscle memory do not
96
+ * break). Resolved by open/close/activateById/getPanel/getPaneOf.
97
+ */
98
+ registerAlias(aliasId: string, targetId: string): void {
99
+ this.aliases.set(aliasId, targetId);
100
+ }
101
+
102
+ private _resolveId(panelId: string): string {
103
+ return this.aliases.get(panelId) ?? panelId;
104
+ }
105
+
58
106
  getRegisteredTypes(): PanelRegistration[] {
59
107
  return [...this.registry];
60
108
  }
@@ -87,11 +135,20 @@ export class PanelManager {
87
135
  this._cachedWorkspaceTabs = null;
88
136
  }
89
137
 
90
- open(panelId: string, pane?: 'top' | 'bottom'): Panel {
138
+ open(panelIdOrAlias: string, pane?: 'top' | 'bottom'): Panel {
139
+ const panelId = this._resolveId(panelIdOrAlias);
140
+ this._recordRecent(panelId);
91
141
  const existingPane = this._findPaneOf(panelId);
92
142
  if (existingPane) {
93
- this._activateByIdInPane(panelId, existingPane);
94
143
  this._visible = true;
144
+ // Honor an explicitly requested pane so open(id, pane) never lies about
145
+ // where the panel lands: relocate it if it currently lives in the other
146
+ // pane (fixes `/panel open <id> top` and the panel-list T/B move keys).
147
+ if (pane && pane !== existingPane) {
148
+ this._moveBetweenPanes(existingPane, pane, panelId);
149
+ return this.getPanel(panelId)!;
150
+ }
151
+ this._activateByIdInPane(panelId, existingPane);
95
152
  this._focusedPane = existingPane;
96
153
  if (existingPane === 'bottom') this._bottomPaneVisible = true;
97
154
  return this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]!;
@@ -119,7 +176,8 @@ export class PanelManager {
119
176
  return panel;
120
177
  }
121
178
 
122
- close(panelId: string): void {
179
+ close(panelIdOrAlias: string): void {
180
+ const panelId = this._resolveId(panelIdOrAlias);
123
181
  // Search both panes
124
182
  for (const which of ['top', 'bottom'] as const) {
125
183
  const p = this._getPane(which);
@@ -231,7 +289,8 @@ export class PanelManager {
231
289
  this._invalidateWorkspaceTabs();
232
290
  }
233
291
 
234
- activateById(panelId: string): void {
292
+ activateById(panelIdOrAlias: string): void {
293
+ const panelId = this._resolveId(panelIdOrAlias);
235
294
  const which = this._findPaneOf(panelId);
236
295
  if (!which) return;
237
296
  this._activateByIdInPane(panelId, which);
@@ -263,6 +322,49 @@ export class PanelManager {
263
322
  this._invalidateWorkspaceTabs();
264
323
  }
265
324
 
325
+ // -------------------------------------------------------------------------
326
+ // Keyboard focus ownership (prompt vs. panel workspace)
327
+ // -------------------------------------------------------------------------
328
+
329
+ /**
330
+ * Which surface owns keyboard focus. Self-healing: focus can only rest on the
331
+ * panel workspace while it is visible, non-empty, and has an active panel, so
332
+ * panel focus can never disagree with workspace visibility. Any code that
333
+ * asks for the focus target therefore reads a value that is always consistent
334
+ * with what is actually on screen.
335
+ */
336
+ getFocusTarget(): 'prompt' | 'panel' {
337
+ if (this._focusTarget === 'panel' && !this._workspaceIsFocusable()) {
338
+ this._focusTarget = 'prompt';
339
+ }
340
+ return this._focusTarget;
341
+ }
342
+
343
+ /** True when the panel workspace currently owns keyboard focus. */
344
+ isPanelFocused(): boolean {
345
+ return this.getFocusTarget() === 'panel';
346
+ }
347
+
348
+ /**
349
+ * Give keyboard focus to the panel workspace. No-op when there is nothing
350
+ * focusable (no visible, non-empty pane with an active panel) — this is the
351
+ * guard that upholds the focus/visibility invariant on the write path.
352
+ */
353
+ focusPanels(): void {
354
+ if (this._workspaceIsFocusable()) {
355
+ this._focusTarget = 'panel';
356
+ }
357
+ }
358
+
359
+ /** Return keyboard focus to the prompt. */
360
+ focusPrompt(): void {
361
+ this._focusTarget = 'prompt';
362
+ }
363
+
364
+ private _workspaceIsFocusable(): boolean {
365
+ return this._visible && this.getAllOpen().length > 0 && this.getActivePanel() !== null;
366
+ }
367
+
266
368
  // -------------------------------------------------------------------------
267
369
  // Pane visibility
268
370
  // -------------------------------------------------------------------------
@@ -285,10 +387,11 @@ export class PanelManager {
285
387
  this.bottomPane.panels.push(panel);
286
388
  this.bottomPane.activeIndex = 0;
287
389
  } else {
288
- // Open a default panel in bottom pane
289
- const firstType = this.registry[0];
290
- if (firstType) {
291
- this.open(firstType.id, 'bottom');
390
+ // Open a predictable default panel in the bottom pane (the panel list),
391
+ // rather than an arbitrary registration-order-dependent panel.
392
+ const defaultPanel = this._getRegistration('panel-list') ?? this.registry[0];
393
+ if (defaultPanel) {
394
+ this.open(defaultPanel.id, 'bottom');
292
395
  }
293
396
  }
294
397
  }
@@ -335,14 +438,15 @@ export class PanelManager {
335
438
  return p.panels[p.activeIndex] ?? null;
336
439
  }
337
440
 
338
- getPanel(panelId: string): Panel | null {
441
+ getPanel(panelIdOrAlias: string): Panel | null {
442
+ const panelId = this._resolveId(panelIdOrAlias);
339
443
  return this.topPane.panels.find((panel) => panel.id === panelId)
340
444
  ?? this.bottomPane.panels.find((panel) => panel.id === panelId)
341
445
  ?? null;
342
446
  }
343
447
 
344
- getPaneOf(panelId: string): 'top' | 'bottom' | null {
345
- return this._findPaneOf(panelId);
448
+ getPaneOf(panelIdOrAlias: string): 'top' | 'bottom' | null {
449
+ return this._findPaneOf(this._resolveId(panelIdOrAlias));
346
450
  }
347
451
 
348
452
  getWorkspaceTabs(): readonly WorkspaceTab[] {
@@ -460,7 +564,9 @@ export class PanelManager {
460
564
  this.bottomPane = { panels: [], activeIndex: 0 };
461
565
  this.retainedPanels.clear();
462
566
  this.registry = [];
567
+ this._recentlyOpened = [];
463
568
  this._focusedPane = 'top';
569
+ this._focusTarget = 'prompt';
464
570
  this._bottomPaneVisible = false;
465
571
  this._visible = false;
466
572
  this._invalidateWorkspaceTabs();
@@ -543,11 +649,12 @@ export class PanelManager {
543
649
  }
544
650
 
545
651
  private _getRegistration(panelId: string): PanelRegistration | undefined {
546
- return this.registry.find((registration) => registration.id === panelId);
652
+ const resolvedId = this._resolveId(panelId);
653
+ return this.registry.find((registration) => registration.id === resolvedId);
547
654
  }
548
655
 
549
656
  private _shouldRetain(panelId: string): boolean {
550
- return this._getRegistration(panelId)?.preload === true;
657
+ return this._getRegistration(panelId)?.retainOnClose === true;
551
658
  }
552
659
 
553
660
  private _activateByIdInPane(panelId: string, which: 'top' | 'bottom'): void {