@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
@@ -1,487 +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
- handleInput(key: string): boolean {
143
- // Confirmation dialog — use shared handleConfirmInput for y/n/Esc UX
144
- const confirmResult = handleConfirmInput(this.confirm, key);
145
- if (confirmResult === 'confirmed') {
146
- this._deleteConfirmed();
147
- return true;
148
- }
149
- if (confirmResult === 'cancelled') {
150
- this.confirm = null;
151
- this.markDirty();
152
- return true;
153
- }
154
- if (confirmResult === 'absorbed') return true;
155
-
156
- // Search mode
157
- if (this.searching) {
158
- const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.cursorIndex, itemCount: this.filtered.length });
159
- if (transition === 'focus-list') {
160
- this.searching = false;
161
- this.cursorIndex = 0;
162
- this.markDirty();
163
- return true;
164
- }
165
- if (isPanelSearchCancel(key) || isPanelSearchCommit(key)) {
166
- this.searching = false;
167
- this.markDirty();
168
- return true;
169
- }
170
- if (isPanelSearchBackspace(key)) {
171
- this.searchQuery = this.searchQuery.slice(0, -1);
172
- this._filter();
173
- return true;
174
- }
175
- if (isPanelSearchPrintable(key)) {
176
- this.searchQuery += key;
177
- this._filter();
178
- return true;
179
- }
180
- return false;
181
- }
182
-
183
- const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.cursorIndex, itemCount: this.filtered.length });
184
- if (transition === 'focus-search') {
185
- this._startSearch();
186
- return true;
187
- }
188
-
189
- switch (key) {
190
- case 'up': this._move(-1); return true;
191
- case 'down': this._move(1); return true;
192
- case 'pageup': this._move(-10); return true;
193
- case 'pagedown': this._move(10); return true;
194
- case 'return': this._resume(); return true;
195
- case 'd': this._promptDelete(); return true;
196
- case 'r': this._load(); return true;
197
- case 'x': this._dispatchNextStep(); return true;
198
- default: return false;
199
- }
200
- }
201
-
202
- /**
203
- * Drains `pendingCommand` (set by the 'x' key) and `pendingOpenPanels`
204
- * (set by `_resume()`) via the ctx.executeCommand / PanelManager bridge.
205
- * Called by the input router immediately after handleInput() consumes the
206
- * same key (panel-integration-actions.ts's onPanelInputConsumed).
207
- */
208
- handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
209
- let consumed = false;
210
- if (this.pendingCommand) {
211
- const { name, args } = this.pendingCommand;
212
- this.pendingCommand = null;
213
- void ctx.executeCommand?.(name, args);
214
- consumed = true;
215
- }
216
- if (this.pendingOpenPanels) {
217
- const openPanels = this.pendingOpenPanels;
218
- this.pendingOpenPanels = null;
219
- for (const panelId of openPanels) ctx.panelManager.open(panelId);
220
- consumed = true;
221
- }
222
- return consumed;
223
- }
224
-
225
- render(width: number, height: number): Line[] {
226
- if (height <= 0 || width <= 0) return [];
227
- const intro = 'Browse, search, resume, and prune saved conversations.';
228
-
229
- // Lazily load on first render so the panel works even when rendered without
230
- // onActivate(). The session list is a synchronous source, so this resolves
231
- // immediately rather than flashing a loading screen.
232
- if (!this.hasLoaded) this._load();
233
-
234
- const count = this.filtered.length;
235
- const total = this.sessions.length;
236
- const hasSelection = this.filtered.length > 0;
237
- // Search/status line — surfaces the active query or any error.
238
- const searchLine = this.searching
239
- ? ` Search: ${this.searchQuery}_`
240
- : this.loadError
241
- ? ` Error: ${this.loadError}`
242
- : this.deleteError
243
- ? ` Error: ${this.deleteError}`
244
- : this.searchQuery
245
- ? ` Filter: ${this.searchQuery}`
246
- : ` (no filter)`;
247
- const statusFg = this.loadError || this.deleteError ? DEFAULT_PANEL_PALETTE.bad : this.searching ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim;
248
- // Context-aware hints: only advertise keys that work in the current state.
249
- const hints: Array<{ keys: string; label: string }> = [];
250
- if (this.searching) {
251
- hints.push({ keys: 'type', label: 'filter' }, { keys: 'Esc/Enter', label: 'apply' });
252
- } else {
253
- hints.push({ keys: '/', label: this.searchQuery ? 'edit search' : 'search' });
254
- if (hasSelection) hints.push({ keys: 'Enter', label: 'resume' }, { keys: 'x', label: 'run next step' }, { keys: 'd', label: 'delete' });
255
- hints.push({ keys: 'r', label: 'refresh' });
256
- }
257
- const footerLines = [
258
- buildSearchInputLine(width, '', searchLine.trimStart(), DEFAULT_PANEL_PALETTE, { active: this.searching, valueColor: statusFg }),
259
- buildKeyboardHints(width, hints, DEFAULT_PANEL_PALETTE),
260
- ];
261
-
262
- if (this.confirm) {
263
- return buildPanelWorkspace(width, height, {
264
- title: ` Sessions [${count}/${total}]`,
265
- intro,
266
- sections: [
267
- {
268
- title: 'Confirmation',
269
- lines: [
270
- buildPanelLine(width, [[` Delete "${this.confirm.subject}"?`, DEFAULT_PANEL_PALETTE.warn]]),
271
- buildPanelLine(width, [[' y', DEFAULT_PANEL_PALETTE.info], [' confirm delete', DEFAULT_PANEL_PALETTE.dim], [' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim]]),
272
- ],
273
- },
274
- ],
275
- footerLines,
276
- palette: DEFAULT_PANEL_PALETTE,
277
- });
278
- }
279
-
280
- if (this.filtered.length === 0) {
281
- const emptyTitle = this.searchQuery ? ` No sessions match "${this.searchQuery}"` : ' No sessions found';
282
- const emptyBody = this.searchQuery
283
- ? 'Clear or change the current filter to surface saved conversations again.'
284
- : 'Conversations are saved automatically. Once you have saved sessions, they appear here for review and resume.';
285
- const emptyActions = this.searchQuery
286
- ? [{ command: 'Esc', summary: 'clear the filter and show all sessions' }]
287
- : [{ command: '/session save', summary: 'name and persist the current conversation' }];
288
- return buildPanelWorkspace(width, height, {
289
- title: ` Sessions [${count}/${total}]`,
290
- intro,
291
- sections: [
292
- {
293
- lines: buildEmptyState(width, emptyTitle, emptyBody, emptyActions, DEFAULT_PANEL_PALETTE),
294
- },
295
- ],
296
- footerLines,
297
- palette: DEFAULT_PANEL_PALETTE,
298
- });
299
- }
300
-
301
- this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, this.filtered.length - 1));
302
- const summary: PanelWorkspaceSection = {
303
- title: 'Summary',
304
- lines: [
305
- buildPanelLine(width, [
306
- [' Sessions ', DEFAULT_PANEL_PALETTE.label],
307
- [String(total), DEFAULT_PANEL_PALETTE.value],
308
- [' Visible ', DEFAULT_PANEL_PALETTE.label],
309
- [String(count), DEFAULT_PANEL_PALETTE.info],
310
- [' Search ', DEFAULT_PANEL_PALETTE.label],
311
- [this.searchQuery || 'none', this.searchQuery ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
312
- ]),
313
- ],
314
- };
315
-
316
- const selected = this.filtered[this.cursorIndex];
317
- const selectedSection: PanelWorkspaceSection = selected
318
- ? {
319
- title: 'Selected',
320
- lines: [
321
- buildPanelLine(width, [[' Title ', DEFAULT_PANEL_PALETTE.label], [selected.title || selected.name || '(untitled)', DEFAULT_PANEL_PALETTE.value]]),
322
- 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]]),
323
- 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]]),
324
- buildPanelLine(width, [
325
- [' Tasks ', DEFAULT_PANEL_PALETTE.label],
326
- [String(selected.returnContext?.activeTasks ?? 0), (selected.returnContext?.activeTasks ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
327
- [' Blocked ', DEFAULT_PANEL_PALETTE.label],
328
- [String(selected.returnContext?.blockedTasks ?? 0), (selected.returnContext?.blockedTasks ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
329
- [' Approvals ', DEFAULT_PANEL_PALETTE.label],
330
- [String(selected.returnContext?.pendingApprovals ?? 0), (selected.returnContext?.pendingApprovals ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
331
- ]),
332
- buildPanelLine(width, [
333
- [' Remote ', DEFAULT_PANEL_PALETTE.label],
334
- [String(selected.returnContext?.remoteRunners?.length ?? 0), (selected.returnContext?.remoteRunners?.length ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
335
- [' Worktrees ', DEFAULT_PANEL_PALETTE.label],
336
- [String(selected.returnContext?.worktreePaths?.length ?? 0), (selected.returnContext?.worktreePaths?.length ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
337
- [' Panels ', DEFAULT_PANEL_PALETTE.label],
338
- [String(selected.returnContext?.openPanels?.length ?? 0), (selected.returnContext?.openPanels?.length ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
339
- ]),
340
- ...formatReturnContextLines(selected.returnContext).map((line) =>
341
- buildPanelLine(width, [[' ', DEFAULT_PANEL_PALETTE.dim], [truncateDisplay(line, Math.max(0, width - 2)), DEFAULT_PANEL_PALETTE.dim]])
342
- ),
343
- buildPanelLine(width, [[' Next ', DEFAULT_PANEL_PALETTE.label], [this._computeNextStepCommand(selected), DEFAULT_PANEL_PALETTE.dim], [' (x to run)', DEFAULT_PANEL_PALETTE.dim]]),
344
- ],
345
- }
346
- : { title: 'Selected', lines: [] };
347
-
348
- const sessionsSection = resolveScrollablePanelSection(width, height, {
349
- intro,
350
- footerLines,
351
- palette: DEFAULT_PANEL_PALETTE,
352
- beforeSections: [summary],
353
- section: {
354
- title: 'Sessions',
355
- scrollableLines: this.filtered.map((sess, index) =>
356
- this._renderSession(width, sess, index === this.cursorIndex),
357
- ),
358
- selectedIndex: this.cursorIndex,
359
- scrollOffset: this.scrollOffset,
360
- minRows: 6,
361
- },
362
- afterSections: [selectedSection],
363
- });
364
- this.scrollOffset = sessionsSection.scrollOffset;
365
-
366
- return buildPanelWorkspace(width, height, {
367
- title: ` Sessions [${count}/${total}]`,
368
- intro,
369
- sections: [
370
- summary,
371
- sessionsSection.section,
372
- selectedSection,
373
- ],
374
- footerLines,
375
- palette: DEFAULT_PANEL_PALETTE,
376
- });
377
- }
378
-
379
- private _renderSession(width: number, sess: SessionInfo, isCursor: boolean): Line {
380
- const bg = isCursor ? C.selectBg : '';
381
- const date = shortDate(sess.timestamp);
382
- const cnt = String(sess.messageCount).padStart(3) + 'm ';
383
- const model = fitDisplay(sess.model || 'unknown', 18) + ' ';
384
- const prefixLength = 1 + 16 + 1 + 4 + 19;
385
- const title = truncateDisplay(sess.title || sess.name || '(untitled)', Math.max(0, width - prefixLength));
386
- return buildStyledPanelLine(width, [
387
- { text: isCursor ? '▸' : ' ', fg: C.selected, bg, bold: isCursor },
388
- { text: date, fg: C.dateFg, bg },
389
- { text: ' ', fg: C.value, bg },
390
- { text: cnt, fg: C.countFg, bg },
391
- { text: model, fg: C.modelFg, bg },
392
- { text: title, fg: isCursor ? C.selected : C.value, bg, bold: isCursor },
393
- ]);
394
- }
395
-
396
- private _load(): void {
397
- try {
398
- this.sessions = this.sessionManager.list();
399
- this._filter();
400
- this.loadError = '';
401
- this.hasLoaded = true;
402
- this.markDirty();
403
- } catch (e) {
404
- logger.debug('SessionBrowserPanel._load failed', { error: summarizeError(e) });
405
- this.loadError = 'Failed to load sessions';
406
- this.hasLoaded = true;
407
- this.markDirty();
408
- }
409
- }
410
-
411
- private _filter(): void {
412
- if (!this.searchQuery.trim()) {
413
- this.filtered = [...this.sessions];
414
- } else {
415
- const q = this.searchQuery.toLowerCase();
416
- try {
417
- const results = this.sessionManager.search(q);
418
- const names = new Set(results.map(r => r.session.name));
419
- this.filtered = this.sessions.filter(s => names.has(s.name));
420
- } catch (e) {
421
- logger.debug('SessionBrowserPanel._filter search failed, falling back', { error: summarizeError(e) });
422
- this.filtered = this.sessions.filter(s =>
423
- (s.title || '').toLowerCase().includes(q) ||
424
- (s.model || '').toLowerCase().includes(q) ||
425
- s.name.toLowerCase().includes(q)
426
- );
427
- }
428
- }
429
- this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.filtered.length - 1));
430
- this.markDirty();
431
- }
432
-
433
- private _startSearch(): void {
434
- this.searching = true;
435
- this.markDirty();
436
- }
437
-
438
- private _move(delta: number): void {
439
- if (this.filtered.length === 0) return;
440
- this.cursorIndex = Math.max(0, Math.min(this.filtered.length - 1, this.cursorIndex + delta));
441
- this.markDirty();
442
- }
443
-
444
- private _resume(): void {
445
- const sess = this.filtered[this.cursorIndex];
446
- if (!sess) return;
447
- if (sess.returnContext?.openPanels?.length) {
448
- this.pendingOpenPanels = [...sess.returnContext.openPanels];
449
- }
450
- this.resumeSession?.(sess.name);
451
- }
452
-
453
- /** The next-step command shown in the 'Selected' section and dispatched by 'x'. */
454
- private _computeNextStepCommand(sess: SessionInfo): string {
455
- const runner = sess.returnContext?.remoteRunners?.[0];
456
- return runner ? `/remote recover ${runner}` : `/session resume ${sess.name}`;
457
- }
458
-
459
- private _dispatchNextStep(): void {
460
- const sess = this.filtered[this.cursorIndex];
461
- if (!sess) return;
462
- const parsed = parseCommand(this._computeNextStepCommand(sess));
463
- if (parsed) this.pendingCommand = parsed;
464
- }
465
-
466
- private _promptDelete(): void {
467
- const sess = this.filtered[this.cursorIndex];
468
- if (!sess) return;
469
- this.confirm = { subject: sess.name, label: sess.name };
470
- this.markDirty();
471
- }
472
-
473
- private _deleteConfirmed(): void {
474
- if (!this.confirm) return;
475
- const name = this.confirm.subject;
476
- this.confirm = null;
477
- try {
478
- this.sessionManager.delete(name);
479
- this.deleteError = '';
480
- this._load();
481
- } catch (e) {
482
- logger.debug('SessionBrowserPanel._deleteConfirmed failed', { error: summarizeError(e) });
483
- this.deleteError = `Delete failed: ${name}`;
484
- }
485
- this.markDirty();
486
- }
487
- }