@pellux/goodvibes-tui 1.1.0 → 1.9.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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. 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
- }