@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
@@ -140,8 +140,33 @@ export class InputHandler implements InputHandlerLike {
140
140
  public commandMode = false;
141
141
  /** True when the process indicator bar has keyboard focus. */
142
142
  public indicatorFocused = false;
143
- /** True when keyboard focus is on the active panel (arrow/enter go to panel, not prompt). */
144
- public panelFocused = false;
143
+ /**
144
+ * Fallback focus store used only when the panel manager does not implement
145
+ * focus ownership (lightweight test stubs). Production always delegates to
146
+ * PanelManager, which is the single source of truth.
147
+ */
148
+ private _panelFocusedFallback = false;
149
+ /**
150
+ * True when keyboard focus is on the active panel (arrow/enter go to panel,
151
+ * not prompt). Ownership lives in PanelManager (focusTarget); this reads and
152
+ * writes through to it so handler focus can never disagree with what the
153
+ * panel workspace actually shows.
154
+ */
155
+ public get panelFocused(): boolean {
156
+ const pm = this.uiServices.shell.panelManager;
157
+ return typeof pm.getFocusTarget === 'function'
158
+ ? pm.getFocusTarget() === 'panel'
159
+ : this._panelFocusedFallback;
160
+ }
161
+ public set panelFocused(value: boolean) {
162
+ const pm = this.uiServices.shell.panelManager;
163
+ if (typeof pm.focusPanels === 'function' && typeof pm.focusPrompt === 'function') {
164
+ if (value) pm.focusPanels();
165
+ else pm.focusPrompt();
166
+ } else {
167
+ this._panelFocusedFallback = value;
168
+ }
169
+ }
145
170
 
146
171
  public tokenizer = new InputTokenizer();
147
172
  public pasteRegistry = new Map<string, string>();
@@ -468,6 +493,15 @@ export class InputHandler implements InputHandlerLike {
468
493
  };
469
494
 
470
495
  this.requestRender = bufferedRequestRender;
496
+ // Snapshot the overlay flags before the feed: command handlers executed
497
+ // synchronously during token processing (e.g. /shortcuts, /help via
498
+ // ui-openers) mutate these directly on the handler, and an unconditional
499
+ // write-back of the pre-feed snapshot would silently revert them in the
500
+ // same keystroke (the "/shortcuts never displays" defect).
501
+ const helpOverlayActiveBefore = this.helpOverlayActive;
502
+ const helpScrollOffsetBefore = this.helpScrollOffset;
503
+ const shortcutsOverlayActiveBefore = this.shortcutsOverlayActive;
504
+ const shortcutsScrollOffsetBefore = this.shortcutsScrollOffset;
471
505
  try {
472
506
  const context = this.feedContext;
473
507
  // Sync mutable scalars from handler into the reused context.
@@ -506,10 +540,13 @@ export class InputHandler implements InputHandlerLike {
506
540
  this.commandMode = context.commandMode;
507
541
  this.panelFocused = context.panelFocused;
508
542
  this.indicatorFocused = context.indicatorFocused;
509
- this.helpOverlayActive = context.helpOverlayActive;
510
- this.helpScrollOffset = context.helpScrollOffset;
511
- this.shortcutsOverlayActive = context.shortcutsOverlayActive;
512
- this.shortcutsScrollOffset = context.shortcutsScrollOffset;
543
+ // Overlay flags: only apply the pipeline's value when the pipeline
544
+ // itself changed it (e.g. Escape closing the overlay). Otherwise keep
545
+ // the live handler value, which a command handler may have just set.
546
+ if (context.helpOverlayActive !== helpOverlayActiveBefore) this.helpOverlayActive = context.helpOverlayActive;
547
+ if (context.helpScrollOffset !== helpScrollOffsetBefore) this.helpScrollOffset = context.helpScrollOffset;
548
+ if (context.shortcutsOverlayActive !== shortcutsOverlayActiveBefore) this.shortcutsOverlayActive = context.shortcutsOverlayActive;
549
+ if (context.shortcutsScrollOffset !== shortcutsScrollOffsetBefore) this.shortcutsScrollOffset = context.shortcutsScrollOffset;
513
550
  this.selectionCallback = context.selectionCallback;
514
551
  this.nextPasteId = context.nextPasteId;
515
552
  this.nextImageId = context.nextImageId;
@@ -38,6 +38,17 @@ export type KeyAction =
38
38
  | 'panel-close-all'
39
39
  | 'panel-tab-next'
40
40
  | 'panel-tab-prev'
41
+ | 'panel-tab-1'
42
+ | 'panel-tab-2'
43
+ | 'panel-tab-3'
44
+ | 'panel-tab-4'
45
+ | 'panel-tab-5'
46
+ | 'panel-tab-6'
47
+ | 'panel-tab-7'
48
+ | 'panel-tab-8'
49
+ | 'panel-tab-9'
50
+ | 'panel-ops'
51
+ | 'panel-focus-toggle'
41
52
  | 'history-search'
42
53
  | 'search'
43
54
  | 'block-copy'
@@ -51,7 +62,6 @@ export type KeyAction =
51
62
  | 'undo'
52
63
  | 'redo'
53
64
  | 'paste'
54
- | 'replay-panel'
55
65
  | 'word-back'
56
66
  | 'word-forward'
57
67
  | 'kill-to-start'
@@ -69,6 +79,17 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
69
79
  'panel-close-all': 'Close all open panels',
70
80
  'panel-tab-next': 'Next workspace panel tab',
71
81
  'panel-tab-prev': 'Previous workspace panel tab',
82
+ 'panel-tab-1': 'Jump to workspace panel tab 1',
83
+ 'panel-tab-2': 'Jump to workspace panel tab 2',
84
+ 'panel-tab-3': 'Jump to workspace panel tab 3',
85
+ 'panel-tab-4': 'Jump to workspace panel tab 4',
86
+ 'panel-tab-5': 'Jump to workspace panel tab 5',
87
+ 'panel-tab-6': 'Jump to workspace panel tab 6',
88
+ 'panel-tab-7': 'Jump to workspace panel tab 7',
89
+ 'panel-tab-8': 'Jump to workspace panel tab 8',
90
+ 'panel-tab-9': 'Jump to workspace panel tab 9',
91
+ 'panel-ops': 'Open the Ops Control panel',
92
+ 'panel-focus-toggle': 'Switch keyboard focus between top and bottom pane',
72
93
  'history-search': 'Reverse input history search',
73
94
  'search': 'Toggle conversation search',
74
95
  'block-copy': 'Copy nearest block to clipboard',
@@ -82,7 +103,6 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
82
103
  'undo': 'Undo last prompt edit',
83
104
  'redo': 'Redo last undone edit',
84
105
  'paste': 'Paste from clipboard (image priority)',
85
- 'replay-panel': 'Open / close the Replay panel',
86
106
  'word-back': 'Move cursor to start of previous word (Alt+B)',
87
107
  'word-forward': 'Move cursor to end of next word (Alt+F)',
88
108
  'kill-to-start': 'Kill from cursor to start of line into kill ring (Ctrl+U)',
@@ -101,6 +121,27 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
101
121
  'panel-close-all': [{ key: 'x', ctrl: true, shift: true }],
102
122
  'panel-tab-next': [{ key: ']', ctrl: true }],
103
123
  'panel-tab-prev': [{ key: '[', ctrl: true }],
124
+ // Alt+1..9: jump directly to the Nth workspace panel tab (across both panes).
125
+ // The tokenizer delivers Alt as the token's `meta` modifier; comboMatches /
126
+ // lookup treat `meta` as an alias for `alt`, so these alt-combos route through
127
+ // the same rebindable path as every other action.
128
+ 'panel-tab-1': [{ key: '1', alt: true }],
129
+ 'panel-tab-2': [{ key: '2', alt: true }],
130
+ 'panel-tab-3': [{ key: '3', alt: true }],
131
+ 'panel-tab-4': [{ key: '4', alt: true }],
132
+ 'panel-tab-5': [{ key: '5', alt: true }],
133
+ 'panel-tab-6': [{ key: '6', alt: true }],
134
+ 'panel-tab-7': [{ key: '7', alt: true }],
135
+ 'panel-tab-8': [{ key: '8', alt: true }],
136
+ 'panel-tab-9': [{ key: '9', alt: true }],
137
+ // Ctrl+O: open the Ops Control panel (operator intervention console).
138
+ // Routed globally in handleGlobalShortcutToken: prefers commandContext.openOpsPanel()
139
+ // when the operator-control-plane feature flag wired it, else falls back to opening
140
+ // the always-registered 'ops-control' panel type directly via the panel manager.
141
+ 'panel-ops': [{ key: 'o', ctrl: true }],
142
+ // Ctrl+G: toggle keyboard focus between the top and bottom panes. Ctrl+G is
143
+ // otherwise unbound in the default table.
144
+ 'panel-focus-toggle': [{ key: 'g', ctrl: true }],
104
145
  'history-search': [{ key: 'r', ctrl: true }],
105
146
  'search': [{ key: 'f', ctrl: true }],
106
147
  'block-copy': [{ key: 'y', ctrl: true }],
@@ -117,7 +158,6 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
117
158
  'undo': [{ key: 'z', ctrl: true }],
118
159
  'redo': [{ key: 'z', ctrl: true, shift: true }],
119
160
  'paste': [{ key: 'v', ctrl: true }],
120
- 'replay-panel': [{ key: 'r', ctrl: true, shift: true }],
121
161
  // Word navigation (Alt+B / Alt+F — emacs readline standard)
122
162
  'word-back': [{ key: 'b', alt: true }],
123
163
  'word-forward': [{ key: 'f', alt: true }],
@@ -240,9 +280,13 @@ export class KeybindingsManager {
240
280
  * lookup — O(1) keybinding lookup by token.
241
281
  * Returns the matching KeyAction, or null if no binding matches.
242
282
  */
243
- lookup(token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean }): KeyAction | null {
283
+ lookup(token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean }): KeyAction | null {
244
284
  if (!token.logicalName) return null;
245
- const key = `${token.logicalName}:${token.ctrl ? 1 : 0}:${token.shift ? 1 : 0}:${token.alt ? 1 : 0}`;
285
+ // The tokenizer delivers the Alt modifier as `meta`; the binding table stores
286
+ // it as `alt`. Treat the two as one modifier at the matching boundary so
287
+ // Alt-based combos (word-nav, kill-ring, panel-tab-1..9) resolve at runtime.
288
+ const alt = token.alt ?? token.meta;
289
+ const key = `${token.logicalName}:${token.ctrl ? 1 : 0}:${token.shift ? 1 : 0}:${alt ? 1 : 0}`;
246
290
  return this.lookupMap.get(key) ?? null;
247
291
  }
248
292
 
@@ -263,7 +307,7 @@ export class KeybindingsManager {
263
307
  */
264
308
  matches(
265
309
  action: KeyAction,
266
- token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean },
310
+ token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean },
267
311
  ): boolean {
268
312
  const combos = this.bindings[action];
269
313
  if (!combos) return false;
@@ -272,12 +316,13 @@ export class KeybindingsManager {
272
316
 
273
317
  private comboMatches(
274
318
  combo: KeyCombo,
275
- token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean },
319
+ token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean },
276
320
  ): boolean {
277
321
  if (token.logicalName !== combo.key) return false;
278
322
  if (!!combo.ctrl !== !!token.ctrl) return false;
279
323
  if (!!combo.shift !== !!token.shift) return false;
280
- if (!!combo.alt !== !!token.alt) return false;
324
+ // Alt arrives as `meta` from the tokenizer; accept either as the Alt modifier.
325
+ if (!!combo.alt !== !!(token.alt ?? token.meta)) return false;
281
326
  return true;
282
327
  }
283
328
 
@@ -92,6 +92,8 @@ export class ModelPickerModal {
92
92
  public contextCapPendingModel: ModelDefinition | null = null;
93
93
  /** Current input string in contextCap mode. */
94
94
  public contextCapQuery = '';
95
+ /** Validation error from the last context cap commit (e.g. out-of-range value); null when no error. */
96
+ public contextCapError: string | null = null;
95
97
 
96
98
  // ── Search / filter ─────────────────────────────────────────────────────────────────────────────
97
99
  /** Current search query string (empty = no filter). */
@@ -215,6 +217,7 @@ export class ModelPickerModal {
215
217
  this.previousMode = 'model';
216
218
  this.contextCapPendingModel = model;
217
219
  this.contextCapQuery = '';
220
+ this.contextCapError = null;
218
221
  this.mode = 'contextCap';
219
222
  }
220
223
 
@@ -245,6 +248,7 @@ export class ModelPickerModal {
245
248
  this.query = '';
246
249
  this.categoryFilter = 'all';
247
250
  this.capabilityFilter = 'none';
251
+ this.availableOnly = true;
248
252
  const filtered = this.getFilteredModels();
249
253
  const idx = filtered.findIndex(m => m.id === currentModelId);
250
254
  this.selectedIndex = idx >= 0 ? idx : 0;
@@ -310,6 +314,7 @@ export class ModelPickerModal {
310
314
  this.pendingModel = null;
311
315
  this.contextCapPendingModel = null;
312
316
  this.contextCapQuery = '';
317
+ this.contextCapError = null;
313
318
  this.searchFocused = false;
314
319
  this.selectedIndex = 0;
315
320
  this.scrollOffset = 0;
@@ -6,6 +6,10 @@ import { FileExplorerPanel } from '../panels/file-explorer-panel.ts';
6
6
  import { FilePreviewPanel } from '../panels/file-preview-panel.ts';
7
7
  import { SymbolOutlinePanel } from '../panels/symbol-outline-panel.ts';
8
8
  import { ApprovalPanel } from '../panels/approval-panel.ts';
9
+ import { TasksPanel } from '../panels/tasks-panel.ts';
10
+ import { OrchestrationPanel } from '../panels/orchestration-panel.ts';
11
+ import { AgentInspectorPanel } from '../panels/agent-inspector-panel.ts';
12
+ import { DiffPanel } from '../panels/diff-panel.ts';
9
13
 
10
14
  function ensurePreviewPanel(panelManager: PanelManager): FilePreviewPanel | null {
11
15
  const existing = panelManager.getPanel('preview');
@@ -24,7 +28,29 @@ function ensurePreviewPanel(panelManager: PanelManager): FilePreviewPanel | null
24
28
  return opened instanceof FilePreviewPanel ? opened : null;
25
29
  }
26
30
 
27
- function syncSymbolOutlineFromPreview(panelManager: PanelManager, previewPanel: FilePreviewPanel): void {
31
+ // WO-133: shared by explorer's and preview's 'd' (diff) key — same
32
+ // open/focus bridge as ensurePreviewPanel above.
33
+ function ensureDiffPanel(panelManager: PanelManager): DiffPanel | null {
34
+ const existing = panelManager.getPanel('diff');
35
+ if (existing instanceof DiffPanel) {
36
+ const pane = panelManager.getPaneOf('diff');
37
+ panelManager.activateById('diff');
38
+ if (pane) panelManager.focusPane(pane);
39
+ return existing;
40
+ }
41
+ const targetPane: 'top' | 'bottom' = panelManager.isBottomPaneVisible()
42
+ ? (panelManager.getFocusedPane() === 'top' ? 'bottom' : 'top')
43
+ : 'bottom';
44
+ const opened = panelManager.open('diff', targetPane);
45
+ panelManager.show();
46
+ panelManager.focusPane(targetPane);
47
+ return opened instanceof DiffPanel ? opened : null;
48
+ }
49
+
50
+ // Exported so a future preview-reload action (e.g. an explicit "r" reload
51
+ // key) can re-sync the outline against the same file without duplicating
52
+ // this lookup — the panel-integration wiring for that key lands separately.
53
+ export function syncSymbolOutlineFromPreview(panelManager: PanelManager, previewPanel: FilePreviewPanel): void {
28
54
  const symbols = panelManager.getPanel('symbols');
29
55
  const filePath = previewPanel.getCurrentFilePath();
30
56
  const source = previewPanel.getSource();
@@ -41,6 +67,16 @@ export function handlePanelIntegrationAction(
41
67
  ): boolean {
42
68
  if (!activePanel) return false;
43
69
 
70
+ // Prefer the panel's own integration hook when it provides one. Panels migrated
71
+ // onto the formal hook opt out of the instanceof routing below.
72
+ if (activePanel.handlePanelIntegrationAction) {
73
+ const consumed = activePanel.handlePanelIntegrationAction(key, {
74
+ panelManager,
75
+ executeCommand: commandContext?.executeCommand,
76
+ });
77
+ if (consumed) return true;
78
+ }
79
+
44
80
  if ((key === 'enter' || key === 'return' || key === 'right') && activePanel instanceof FileExplorerPanel) {
45
81
  const filePath = activePanel.getFocusedFilePath();
46
82
  if (!filePath) return false;
@@ -51,6 +87,44 @@ export function handlePanelIntegrationAction(
51
87
  return true;
52
88
  }
53
89
 
90
+ // WO-133: 'd' on the explorer diffs the currently focused file, and 'd' on
91
+ // the preview diffs whatever file is currently open — both against HEAD,
92
+ // both via the same DiffPanel.showFileDiffs entry point.
93
+ if (key === 'd' && activePanel instanceof FileExplorerPanel) {
94
+ const filePath = activePanel.getFocusedFilePath();
95
+ if (!filePath) return false;
96
+ const diffPanel = ensureDiffPanel(panelManager);
97
+ if (!diffPanel) return false;
98
+ void diffPanel.showFileDiffs([filePath], 'HEAD').catch((err) => {
99
+ logger.debug('explorer diff dispatch failed', { err });
100
+ });
101
+ return true;
102
+ }
103
+
104
+ if (key === 'd' && activePanel instanceof FilePreviewPanel) {
105
+ const filePath = activePanel.getCurrentFilePath();
106
+ if (!filePath) return false;
107
+ const diffPanel = ensureDiffPanel(panelManager);
108
+ if (!diffPanel) return false;
109
+ void diffPanel.showFileDiffs([filePath], 'HEAD').catch((err) => {
110
+ logger.debug('preview diff dispatch failed', { err });
111
+ });
112
+ return true;
113
+ }
114
+
115
+ // WO-133: 'r' on the preview reloads the open file from disk, then
116
+ // re-syncs the symbol outline against the refreshed content (WO-126's
117
+ // async tree-sitter loadFile via syncSymbolOutlineFromPreview above) once
118
+ // the reload actually settles.
119
+ if (key === 'r' && activePanel instanceof FilePreviewPanel) {
120
+ const reloaded = activePanel.consumePendingReload();
121
+ if (!reloaded) return false;
122
+ void reloaded.then(() => syncSymbolOutlineFromPreview(panelManager, activePanel)).catch((err) => {
123
+ logger.debug('preview reload dispatch failed', { err });
124
+ });
125
+ return true;
126
+ }
127
+
54
128
  if ((key === 'enter' || key === 'return') && activePanel instanceof SymbolOutlinePanel) {
55
129
  const location = activePanel.getSelectedLocation();
56
130
  if (!location) return false;
@@ -74,5 +148,47 @@ export function handlePanelIntegrationAction(
74
148
  return true;
75
149
  }
76
150
 
151
+ // WO-131: Enter on a Tasks row — agent-kind tasks jump straight to the Agent
152
+ // Inspector (which owns the deep per-agent timeline); everything else's
153
+ // advertised worktree follow-up is dispatched for real via ctx.executeCommand
154
+ // instead of being printed as a static "/worktree task <task-id>" signpost.
155
+ // w dispatches the task-family posture review the header line advertises.
156
+ if ((key === 'enter' || key === 'return' || key === 'w') && activePanel instanceof TasksPanel) {
157
+ const followUp = activePanel.consumePendingFollowUp();
158
+ if (!followUp) return false;
159
+ if (followUp.kind === 'agent-jump') {
160
+ const inspector = panelManager.open('inspector');
161
+ if (!(inspector instanceof AgentInspectorPanel)) return false;
162
+ inspector.inspectAgent(followUp.agentId);
163
+ return true;
164
+ }
165
+ if (!commandContext?.executeCommand) return false;
166
+ if (followUp.kind === 'teamwork-review') {
167
+ void commandContext.executeCommand('teamwork', ['review']).catch((err) => {
168
+ logger.debug('tasks panel teamwork review dispatch failed', { err });
169
+ });
170
+ return true;
171
+ }
172
+ void commandContext.executeCommand('worktree', ['task', followUp.taskId]).catch((err) => {
173
+ logger.debug('tasks panel worktree review dispatch failed', { err });
174
+ });
175
+ return true;
176
+ }
177
+
178
+ // WO-131: Enter on a node-focused Orchestration row jumps to the Agent
179
+ // Inspector (agent-backed nodes) or the Tasks panel (task-backed nodes).
180
+ if ((key === 'enter' || key === 'return') && activePanel instanceof OrchestrationPanel) {
181
+ const jump = activePanel.consumePendingNodeJump();
182
+ if (!jump) return false;
183
+ if (jump.kind === 'agent-jump') {
184
+ const inspector = panelManager.open('inspector');
185
+ if (!(inspector instanceof AgentInspectorPanel)) return false;
186
+ inspector.inspectAgent(jump.id);
187
+ return true;
188
+ }
189
+ panelManager.open('tasks');
190
+ return true;
191
+ }
192
+
77
193
  return false;
78
194
  }
package/src/main.ts CHANGED
@@ -29,7 +29,6 @@ import { applyConversationOverlays } from './renderer/conversation-overlays.ts';
29
29
  import { buildPanelCompositeData } from './renderer/panel-composite.ts';
30
30
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
31
31
  import { registerBuiltinPanels } from './panels/builtin-panels.ts';
32
- import { renderPanelTabBar } from './renderer/panel-tab-bar.ts';
33
32
  import { bootstrapRuntime } from './runtime/bootstrap.ts';
34
33
  import type { BootstrapContext } from './runtime/bootstrap.ts';
35
34
  import type { HITLMode } from '@pellux/goodvibes-sdk/platform/state';
@@ -37,6 +36,7 @@ import {
37
36
  checkRecoveryFile,
38
37
  deleteRecoveryFile,
39
38
  loadRecoveryConversation,
39
+ readLastSessionPointer,
40
40
  writeRecoveryFile,
41
41
  } from '@/runtime/index.ts';
42
42
  import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
@@ -59,7 +59,7 @@ import { formatUserFacingErrorLine } from './core/format-user-error.ts';
59
59
  import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
60
60
  import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
61
61
  import { buildContextStatusHint } from './renderer/context-status-hint.ts';
62
- import { evaluateSessionMaintenance } from './panels/session-maintenance.ts';
62
+ import { evaluateSessionMaintenance } from '@/runtime/index.ts';
63
63
 
64
64
  const ALT_SCREEN_ENTER = '\x1b[?1049h'; const ALT_SCREEN_EXIT = '\x1b[?1049l';
65
65
  const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h'; const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
@@ -429,6 +429,7 @@ async function main() {
429
429
  model: runtime.model,
430
430
  provider: runtime.provider,
431
431
  toolCount,
432
+ lastSessionId: readLastSessionPointer({ workingDirectory: workingDir, homeDirectory, surfaceRoot: 'tui' }) ?? undefined,
432
433
  };
433
434
 
434
435
  const render = () => {
@@ -491,6 +492,8 @@ async function main() {
491
492
  provider: runtime.provider,
492
493
  contextWindow,
493
494
  contextStatusHint,
495
+ // Compact footer posture on short terminals so the shell stays usable.
496
+ compact: height < 30,
494
497
  // behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
495
498
  // the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
496
499
  compactThreshold: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),