@pellux/goodvibes-tui 1.1.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 (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,496 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // SessionBrowserPanel — browse, search, and resume old sessions.
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { Line } from '../types/grid.ts';
6
- import { BasePanel } from './base-panel.ts';
7
- import type { PanelIntegrationContext } from './types.ts';
8
- import type { SessionInfo } from '@pellux/goodvibes-sdk/platform/sessions';
9
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
10
- import type { SessionBrowserQuery } from '../runtime/ui-service-queries.ts';
11
- import {
12
- buildEmptyState,
13
- buildKeyboardHints,
14
- buildPanelLine,
15
- buildSearchInputLine,
16
- buildStyledPanelLine,
17
- buildPanelWorkspace,
18
- resolveScrollablePanelSection,
19
- DEFAULT_PANEL_PALETTE,
20
- extendPalette,
21
- type PanelWorkspaceSection,
22
- } from './polish.ts';
23
- import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
24
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
25
- import {
26
- getPanelSearchFocusTransition,
27
- isPanelSearchBackspace,
28
- isPanelSearchCancel,
29
- isPanelSearchCommit,
30
- isPanelSearchPrintable,
31
- } from './search-focus.ts';
32
- import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
33
-
34
- // Panel-specific extras only; shared tones come from DEFAULT_PANEL_PALETTE so
35
- // theme changes propagate. `selected` is a deliberate bright-cyan cursor accent
36
- // and the date/model/count column tints have no clean shared equivalent.
37
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
38
- selected: '#00ffff',
39
- dateFg: '#6699aa',
40
- modelFg: '#99aacc',
41
- countFg: '#88bbcc',
42
- });
43
-
44
- // Splits a computed '/command arg1 arg2' string into the { name, args } shape
45
- // ctx.executeCommand expects — same leading-slash-stripping approach used by
46
- // remote-panel.ts for its own dispatched command.
47
- function parseCommand(command: string): { name: string; args: string[] } | null {
48
- const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
49
- const [name, ...args] = parts;
50
- return name ? { name, args } : null;
51
- }
52
-
53
- function shortDate(ts: number): string {
54
- const d = new Date(ts);
55
- const Y = d.getFullYear();
56
- const M = String(d.getMonth() + 1).padStart(2, '0');
57
- const D = String(d.getDate()).padStart(2, '0');
58
- const h = String(d.getHours()).padStart(2, '0');
59
- const m = String(d.getMinutes()).padStart(2, '0');
60
- return `${Y}-${M}-${D} ${h}:${m}`;
61
- }
62
-
63
- function formatReturnContextLines(returnContext: SessionInfo['returnContext']): string[] {
64
- if (!returnContext) return [];
65
- const lines: string[] = [];
66
- if (returnContext.activityLabel) lines.push(`activity: ${returnContext.activityLabel}`);
67
- if (returnContext.statusLabel) lines.push(`status: ${returnContext.statusLabel}`);
68
- if (returnContext.activeTasks || returnContext.blockedTasks || returnContext.pendingApprovals) {
69
- lines.push(`tasks: active=${returnContext.activeTasks ?? 0} blocked=${returnContext.blockedTasks ?? 0} approvals=${returnContext.pendingApprovals ?? 0}`);
70
- }
71
- if (returnContext.remoteRunners?.length) {
72
- lines.push(`remote runners: ${returnContext.remoteRunners.join(', ')}`);
73
- }
74
- if (returnContext.worktreePaths?.length) {
75
- lines.push(`worktrees: ${returnContext.worktreePaths.join(', ')}`);
76
- }
77
- if (returnContext.openPanels?.length) {
78
- lines.push(`open panels: ${returnContext.openPanels.join(', ')}`);
79
- }
80
- return lines;
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Confirmation state for deletion
85
- // ---------------------------------------------------------------------------
86
- // ConfirmState<string> — subject holds the session name to delete
87
-
88
- export class SessionBrowserPanel extends BasePanel {
89
- private sessions: SessionInfo[] = [];
90
- private filtered: SessionInfo[] = [];
91
- private searchQuery = '';
92
- private searching = false; // true when user is actively typing a search
93
- private cursorIndex = 0;
94
- private scrollOffset = 0;
95
- private confirm: ConfirmState<string> | null = null;
96
- private deleteError = '';
97
- private loadError = '';
98
- private hasLoaded = false;
99
- private refreshTimerId: ReturnType<typeof setInterval> | null = null;
100
- // x = dispatch the computed next-step command ('/remote recover <runner>'
101
- // or '/session resume <name>') via the same handleInput -> handlePanelIntegrationAction
102
- // bridge remote-panel.ts uses — handleInput has no ctx.executeCommand.
103
- private pendingCommand: { name: string; args: string[] } | null = null;
104
- // On resume, restore any panels the session had open when it was saved.
105
- private pendingOpenPanels: string[] | null = null;
106
-
107
- constructor(
108
- private readonly sessionManager: SessionBrowserQuery,
109
- private resumeSession?: (sessionId: string) => void,
110
- ) {
111
- super('sessions', 'Sessions', 'H', 'session');
112
- }
113
-
114
- override onActivate(): void {
115
- super.onActivate();
116
- this._load();
117
- this.refreshTimerId = this.registerTimer(setInterval(() => { this._load(); }, 5000));
118
- }
119
-
120
- /**
121
- * Clears any active search/filter and moves the cursor to the session
122
- * matching `sessionId` (SessionInfo.name) so a sibling panel (e.g.
123
- * RoutesPanel's Enter-on-binding jump) can land directly on it. No-ops
124
- * (beyond clearing the filter) if the session isn't found.
125
- */
126
- focusSession(sessionId: string): void {
127
- this.searching = false;
128
- this.searchQuery = '';
129
- this._filter();
130
- const index = this.filtered.findIndex((session) => session.name === sessionId);
131
- if (index >= 0) this.cursorIndex = index;
132
- this.markDirty();
133
- }
134
-
135
- override onDeactivate(): void {
136
- if (this.refreshTimerId !== null) { this.clearTimer(this.refreshTimerId); this.refreshTimerId = null; }
137
- this.searching = false;
138
- this.confirm = null;
139
- super.onDeactivate();
140
- }
141
-
142
- /**
143
- * The `/`-to-search buffer wants every character of a burst (paste, or a
144
- * fast-typed query landing in one input.feed() call), same as it always
145
- * has — see the interface doc on `Panel.isCapturingTextBurst`.
146
- */
147
- isCapturingTextBurst(): boolean {
148
- return this.searching;
149
- }
150
-
151
- handleInput(key: string): boolean {
152
- // Confirmation dialog — use shared handleConfirmInput for y/n/Esc UX
153
- const confirmResult = handleConfirmInput(this.confirm, key);
154
- if (confirmResult === 'confirmed') {
155
- this._deleteConfirmed();
156
- return true;
157
- }
158
- if (confirmResult === 'cancelled') {
159
- this.confirm = null;
160
- this.markDirty();
161
- return true;
162
- }
163
- if (confirmResult === 'absorbed') return true;
164
-
165
- // Search mode
166
- if (this.searching) {
167
- const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.cursorIndex, itemCount: this.filtered.length });
168
- if (transition === 'focus-list') {
169
- this.searching = false;
170
- this.cursorIndex = 0;
171
- this.markDirty();
172
- return true;
173
- }
174
- if (isPanelSearchCancel(key) || isPanelSearchCommit(key)) {
175
- this.searching = false;
176
- this.markDirty();
177
- return true;
178
- }
179
- if (isPanelSearchBackspace(key)) {
180
- this.searchQuery = this.searchQuery.slice(0, -1);
181
- this._filter();
182
- return true;
183
- }
184
- if (isPanelSearchPrintable(key)) {
185
- this.searchQuery += key;
186
- this._filter();
187
- return true;
188
- }
189
- return false;
190
- }
191
-
192
- const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.cursorIndex, itemCount: this.filtered.length });
193
- if (transition === 'focus-search') {
194
- this._startSearch();
195
- return true;
196
- }
197
-
198
- switch (key) {
199
- case 'up': this._move(-1); return true;
200
- case 'down': this._move(1); return true;
201
- case 'pageup': this._move(-10); return true;
202
- case 'pagedown': this._move(10); return true;
203
- case 'return': this._resume(); return true;
204
- case 'd': this._promptDelete(); return true;
205
- case 'r': this._load(); return true;
206
- case 'x': this._dispatchNextStep(); return true;
207
- default: return false;
208
- }
209
- }
210
-
211
- /**
212
- * Drains `pendingCommand` (set by the 'x' key) and `pendingOpenPanels`
213
- * (set by `_resume()`) via the ctx.executeCommand / PanelManager bridge.
214
- * Called by the input router immediately after handleInput() consumes the
215
- * same key (panel-integration-actions.ts's onPanelInputConsumed).
216
- */
217
- handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
218
- let consumed = false;
219
- if (this.pendingCommand) {
220
- const { name, args } = this.pendingCommand;
221
- this.pendingCommand = null;
222
- void ctx.executeCommand?.(name, args);
223
- consumed = true;
224
- }
225
- if (this.pendingOpenPanels) {
226
- const openPanels = this.pendingOpenPanels;
227
- this.pendingOpenPanels = null;
228
- for (const panelId of openPanels) ctx.panelManager.open(panelId);
229
- consumed = true;
230
- }
231
- return consumed;
232
- }
233
-
234
- render(width: number, height: number): Line[] {
235
- if (height <= 0 || width <= 0) return [];
236
- const intro = 'Browse, search, resume, and prune saved conversations.';
237
-
238
- // Lazily load on first render so the panel works even when rendered without
239
- // onActivate(). The session list is a synchronous source, so this resolves
240
- // immediately rather than flashing a loading screen.
241
- if (!this.hasLoaded) this._load();
242
-
243
- const count = this.filtered.length;
244
- const total = this.sessions.length;
245
- const hasSelection = this.filtered.length > 0;
246
- // Search/status line — surfaces the active query or any error.
247
- const searchLine = this.searching
248
- ? ` Search: ${this.searchQuery}_`
249
- : this.loadError
250
- ? ` Error: ${this.loadError}`
251
- : this.deleteError
252
- ? ` Error: ${this.deleteError}`
253
- : this.searchQuery
254
- ? ` Filter: ${this.searchQuery}`
255
- : ` (no filter)`;
256
- const statusFg = this.loadError || this.deleteError ? DEFAULT_PANEL_PALETTE.bad : this.searching ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim;
257
- // Context-aware hints: only advertise keys that work in the current state.
258
- const hints: Array<{ keys: string; label: string }> = [];
259
- if (this.searching) {
260
- hints.push({ keys: 'type', label: 'filter' }, { keys: 'Esc/Enter', label: 'apply' });
261
- } else {
262
- hints.push({ keys: '/', label: this.searchQuery ? 'edit search' : 'search' });
263
- if (hasSelection) hints.push({ keys: 'Enter', label: 'resume' }, { keys: 'x', label: 'run next step' }, { keys: 'd', label: 'delete' });
264
- hints.push({ keys: 'r', label: 'refresh' });
265
- }
266
- const footerLines = [
267
- buildSearchInputLine(width, '', searchLine.trimStart(), DEFAULT_PANEL_PALETTE, { active: this.searching, valueColor: statusFg }),
268
- buildKeyboardHints(width, hints, DEFAULT_PANEL_PALETTE),
269
- ];
270
-
271
- if (this.confirm) {
272
- return buildPanelWorkspace(width, height, {
273
- title: ` Sessions [${count}/${total}]`,
274
- intro,
275
- sections: [
276
- {
277
- title: 'Confirmation',
278
- lines: [
279
- buildPanelLine(width, [[` Delete "${this.confirm.subject}"?`, DEFAULT_PANEL_PALETTE.warn]]),
280
- buildPanelLine(width, [[' y', DEFAULT_PANEL_PALETTE.info], [' confirm delete', DEFAULT_PANEL_PALETTE.dim], [' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim]]),
281
- ],
282
- },
283
- ],
284
- footerLines,
285
- palette: DEFAULT_PANEL_PALETTE,
286
- });
287
- }
288
-
289
- if (this.filtered.length === 0) {
290
- const emptyTitle = this.searchQuery ? ` No sessions match "${this.searchQuery}"` : ' No sessions found';
291
- const emptyBody = this.searchQuery
292
- ? 'Clear or change the current filter to surface saved conversations again.'
293
- : 'Conversations are saved automatically. Once you have saved sessions, they appear here for review and resume.';
294
- const emptyActions = this.searchQuery
295
- ? [{ command: 'Esc', summary: 'clear the filter and show all sessions' }]
296
- : [{ command: '/session save', summary: 'name and persist the current conversation' }];
297
- return buildPanelWorkspace(width, height, {
298
- title: ` Sessions [${count}/${total}]`,
299
- intro,
300
- sections: [
301
- {
302
- lines: buildEmptyState(width, emptyTitle, emptyBody, emptyActions, DEFAULT_PANEL_PALETTE),
303
- },
304
- ],
305
- footerLines,
306
- palette: DEFAULT_PANEL_PALETTE,
307
- });
308
- }
309
-
310
- this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, this.filtered.length - 1));
311
- const summary: PanelWorkspaceSection = {
312
- title: 'Summary',
313
- lines: [
314
- buildPanelLine(width, [
315
- [' Sessions ', DEFAULT_PANEL_PALETTE.label],
316
- [String(total), DEFAULT_PANEL_PALETTE.value],
317
- [' Visible ', DEFAULT_PANEL_PALETTE.label],
318
- [String(count), DEFAULT_PANEL_PALETTE.info],
319
- [' Search ', DEFAULT_PANEL_PALETTE.label],
320
- [this.searchQuery || 'none', this.searchQuery ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
321
- ]),
322
- ],
323
- };
324
-
325
- const selected = this.filtered[this.cursorIndex];
326
- const selectedSection: PanelWorkspaceSection = selected
327
- ? {
328
- title: 'Selected',
329
- lines: [
330
- buildPanelLine(width, [[' Title ', DEFAULT_PANEL_PALETTE.label], [selected.title || selected.name || '(untitled)', DEFAULT_PANEL_PALETTE.value]]),
331
- buildPanelLine(width, [[' Model ', DEFAULT_PANEL_PALETTE.label], [selected.model || 'unknown', DEFAULT_PANEL_PALETTE.info], [' Title ', DEFAULT_PANEL_PALETTE.label], [selected.titleSource === 'user' ? 'user-set' : 'system', selected.titleSource === 'user' ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim]]),
332
- buildPanelLine(width, [[' Date ', DEFAULT_PANEL_PALETTE.label], [shortDate(selected.timestamp), DEFAULT_PANEL_PALETTE.value], [' Messages ', DEFAULT_PANEL_PALETTE.label], [String(selected.messageCount), DEFAULT_PANEL_PALETTE.value]]),
333
- buildPanelLine(width, [
334
- [' Tasks ', DEFAULT_PANEL_PALETTE.label],
335
- [String(selected.returnContext?.activeTasks ?? 0), (selected.returnContext?.activeTasks ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
336
- [' Blocked ', DEFAULT_PANEL_PALETTE.label],
337
- [String(selected.returnContext?.blockedTasks ?? 0), (selected.returnContext?.blockedTasks ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
338
- [' Approvals ', DEFAULT_PANEL_PALETTE.label],
339
- [String(selected.returnContext?.pendingApprovals ?? 0), (selected.returnContext?.pendingApprovals ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
340
- ]),
341
- buildPanelLine(width, [
342
- [' Remote ', DEFAULT_PANEL_PALETTE.label],
343
- [String(selected.returnContext?.remoteRunners?.length ?? 0), (selected.returnContext?.remoteRunners?.length ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
344
- [' Worktrees ', DEFAULT_PANEL_PALETTE.label],
345
- [String(selected.returnContext?.worktreePaths?.length ?? 0), (selected.returnContext?.worktreePaths?.length ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
346
- [' Panels ', DEFAULT_PANEL_PALETTE.label],
347
- [String(selected.returnContext?.openPanels?.length ?? 0), (selected.returnContext?.openPanels?.length ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
348
- ]),
349
- ...formatReturnContextLines(selected.returnContext).map((line) =>
350
- buildPanelLine(width, [[' ', DEFAULT_PANEL_PALETTE.dim], [truncateDisplay(line, Math.max(0, width - 2)), DEFAULT_PANEL_PALETTE.dim]])
351
- ),
352
- buildPanelLine(width, [[' Next ', DEFAULT_PANEL_PALETTE.label], [this._computeNextStepCommand(selected), DEFAULT_PANEL_PALETTE.dim], [' (x to run)', DEFAULT_PANEL_PALETTE.dim]]),
353
- ],
354
- }
355
- : { title: 'Selected', lines: [] };
356
-
357
- const sessionsSection = resolveScrollablePanelSection(width, height, {
358
- intro,
359
- footerLines,
360
- palette: DEFAULT_PANEL_PALETTE,
361
- beforeSections: [summary],
362
- section: {
363
- title: 'Sessions',
364
- scrollableLines: this.filtered.map((sess, index) =>
365
- this._renderSession(width, sess, index === this.cursorIndex),
366
- ),
367
- selectedIndex: this.cursorIndex,
368
- scrollOffset: this.scrollOffset,
369
- minRows: 6,
370
- },
371
- afterSections: [selectedSection],
372
- });
373
- this.scrollOffset = sessionsSection.scrollOffset;
374
-
375
- return buildPanelWorkspace(width, height, {
376
- title: ` Sessions [${count}/${total}]`,
377
- intro,
378
- sections: [
379
- summary,
380
- sessionsSection.section,
381
- selectedSection,
382
- ],
383
- footerLines,
384
- palette: DEFAULT_PANEL_PALETTE,
385
- });
386
- }
387
-
388
- private _renderSession(width: number, sess: SessionInfo, isCursor: boolean): Line {
389
- const bg = isCursor ? C.selectBg : '';
390
- const date = shortDate(sess.timestamp);
391
- const cnt = String(sess.messageCount).padStart(3) + 'm ';
392
- const model = fitDisplay(sess.model || 'unknown', 18) + ' ';
393
- const prefixLength = 1 + 16 + 1 + 4 + 19;
394
- const title = truncateDisplay(sess.title || sess.name || '(untitled)', Math.max(0, width - prefixLength));
395
- return buildStyledPanelLine(width, [
396
- { text: isCursor ? '▸' : ' ', fg: C.selected, bg, bold: isCursor },
397
- { text: date, fg: C.dateFg, bg },
398
- { text: ' ', fg: C.value, bg },
399
- { text: cnt, fg: C.countFg, bg },
400
- { text: model, fg: C.modelFg, bg },
401
- { text: title, fg: isCursor ? C.selected : C.value, bg, bold: isCursor },
402
- ]);
403
- }
404
-
405
- private _load(): void {
406
- try {
407
- this.sessions = this.sessionManager.list();
408
- this._filter();
409
- this.loadError = '';
410
- this.hasLoaded = true;
411
- this.markDirty();
412
- } catch (e) {
413
- logger.debug('SessionBrowserPanel._load failed', { error: summarizeError(e) });
414
- this.loadError = 'Failed to load sessions';
415
- this.hasLoaded = true;
416
- this.markDirty();
417
- }
418
- }
419
-
420
- private _filter(): void {
421
- if (!this.searchQuery.trim()) {
422
- this.filtered = [...this.sessions];
423
- } else {
424
- const q = this.searchQuery.toLowerCase();
425
- try {
426
- const results = this.sessionManager.search(q);
427
- const names = new Set(results.map(r => r.session.name));
428
- this.filtered = this.sessions.filter(s => names.has(s.name));
429
- } catch (e) {
430
- logger.debug('SessionBrowserPanel._filter search failed, falling back', { error: summarizeError(e) });
431
- this.filtered = this.sessions.filter(s =>
432
- (s.title || '').toLowerCase().includes(q) ||
433
- (s.model || '').toLowerCase().includes(q) ||
434
- s.name.toLowerCase().includes(q)
435
- );
436
- }
437
- }
438
- this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.filtered.length - 1));
439
- this.markDirty();
440
- }
441
-
442
- private _startSearch(): void {
443
- this.searching = true;
444
- this.markDirty();
445
- }
446
-
447
- private _move(delta: number): void {
448
- if (this.filtered.length === 0) return;
449
- this.cursorIndex = Math.max(0, Math.min(this.filtered.length - 1, this.cursorIndex + delta));
450
- this.markDirty();
451
- }
452
-
453
- private _resume(): void {
454
- const sess = this.filtered[this.cursorIndex];
455
- if (!sess) return;
456
- if (sess.returnContext?.openPanels?.length) {
457
- this.pendingOpenPanels = [...sess.returnContext.openPanels];
458
- }
459
- this.resumeSession?.(sess.name);
460
- }
461
-
462
- /** The next-step command shown in the 'Selected' section and dispatched by 'x'. */
463
- private _computeNextStepCommand(sess: SessionInfo): string {
464
- const runner = sess.returnContext?.remoteRunners?.[0];
465
- return runner ? `/remote recover ${runner}` : `/session resume ${sess.name}`;
466
- }
467
-
468
- private _dispatchNextStep(): void {
469
- const sess = this.filtered[this.cursorIndex];
470
- if (!sess) return;
471
- const parsed = parseCommand(this._computeNextStepCommand(sess));
472
- if (parsed) this.pendingCommand = parsed;
473
- }
474
-
475
- private _promptDelete(): void {
476
- const sess = this.filtered[this.cursorIndex];
477
- if (!sess) return;
478
- this.confirm = { subject: sess.name, label: sess.name };
479
- this.markDirty();
480
- }
481
-
482
- private _deleteConfirmed(): void {
483
- if (!this.confirm) return;
484
- const name = this.confirm.subject;
485
- this.confirm = null;
486
- try {
487
- this.sessionManager.delete(name);
488
- this.deleteError = '';
489
- this._load();
490
- } catch (e) {
491
- logger.debug('SessionBrowserPanel._deleteConfirmed failed', { error: summarizeError(e) });
492
- this.deleteError = `Delete failed: ${name}`;
493
- }
494
- this.markDirty();
495
- }
496
- }