@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,566 +0,0 @@
1
- /**
2
- * PanelListPanel — Shows all registered panels grouped by category.
3
- *
4
- * Features:
5
- * - Arrow keys to navigate, Enter to open the selected panel
6
- * - Open/closed indicator (● open, ○ closed)
7
- * - Press `/` to filter; typing narrows, Esc clears, action keys work outside filter mode
8
- * - Grouped by category
9
- * - `T` / `B` to place a panel in the top/bottom pane
10
- * - `M` to move an open panel to the other pane
11
- * - `S` to toggle the split and Tab to switch pane focus
12
- * - `w` to close the selected panel if it is currently open (either pane)
13
- *
14
- * Open via /panel list.
15
- */
16
- import type { Line, Cell } from '../types/grid.ts';
17
- import type { PanelCategory, PanelRegistration } from './types.ts';
18
- import { BasePanel } from './base-panel.ts';
19
- import type { PanelManager } from './panel-manager.ts';
20
- import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
21
- import { createEmptyLine } from '../types/grid.ts';
22
- import {
23
- buildEmptyState,
24
- buildKeyValueLine,
25
- buildPanelListRow,
26
- buildPanelLine,
27
- buildSearchInputLine,
28
- buildSummaryBlock,
29
- buildPanelWorkspace,
30
- DEFAULT_PANEL_PALETTE,
31
- extendPalette,
32
- resolvePrimaryScrollableSection,
33
- type PanelWorkspaceSection,
34
- } from './polish.ts';
35
- import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
36
- import { wrapWithHangingIndent } from '../renderer/text-layout.ts';
37
- import { isPanelSearchPrintable } from './search-focus.ts';
38
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
39
-
40
- // ── Colour palette ────────────────────────────────────────────────────────────
41
- // Domain accents only; base chrome (header/headerBg/label/value/dim/good/bad/
42
- // info/selectBg) comes from DEFAULT_PANEL_PALETTE.
43
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
44
- search: '#f97316', // active search-filter highlight
45
- paneBottom: '#a78bfa', // bottom-pane placement marker (distinct from top/info cyan)
46
- } as const);
47
-
48
- // WO-152: the former single 'monitoring' bucket is split into five operator
49
- // domains (see PanelCategory in types.ts). Order groups related domains
50
- // together in the picker: build/dev surfaces, agent-facing surfaces, then
51
- // the five former-monitoring domains (providers -> security -> automation ->
52
- // incidents -> live runtime consoles), then session/AI utility surfaces.
53
- const CATEGORY_ORDER: PanelCategory[] = [
54
- 'development',
55
- 'agent',
56
- 'providers',
57
- 'security-policy',
58
- 'automation-control',
59
- 'incidents-diagnostics',
60
- 'runtime-ops',
61
- 'session',
62
- 'ai',
63
- ];
64
- const CATEGORY_LABELS: Record<PanelCategory, string> = {
65
- development: 'Development',
66
- agent: 'Agent',
67
- providers: 'Providers',
68
- 'security-policy': 'Security & Policy',
69
- 'automation-control': 'Automation Control',
70
- 'incidents-diagnostics': 'Incidents & Diagnostics',
71
- 'runtime-ops': 'Runtime Ops',
72
- session: 'Session',
73
- ai: 'AI',
74
- };
75
-
76
- // ── Layout constants ──────────────────────────────────────────────────────────
77
- const NAME_COL_WIDTH = 22;
78
- const PREFIX_WIDTH = 5; // arrow + dot + health-flag + space + space
79
-
80
- /** A flat entry in the navigable list — either a section header or a panel row. */
81
- type ListEntry =
82
- | { kind: 'header'; label: string }
83
- | { kind: 'panel'; reg: PanelRegistration };
84
-
85
- /** Number of most-recently-opened panels to surface in the Recent group. */
86
- const RECENT_LIMIT = 5;
87
-
88
- function panelPlacementMarker(options: {
89
- isTopOpen: boolean;
90
- isBottomOpen: boolean;
91
- focusedPane: 'top' | 'bottom';
92
- }): { text: string; color: string } {
93
- const { isTopOpen, isBottomOpen, focusedPane } = options;
94
- if (isTopOpen && isBottomOpen) return { text: '◆', color: C.value };
95
- if (isTopOpen) return { text: focusedPane === 'top' ? '▲' : '△', color: C.info };
96
- if (isBottomOpen) return { text: focusedPane === 'bottom' ? '▼' : '▽', color: C.paneBottom };
97
- return { text: ' ', color: C.dim };
98
- }
99
-
100
- /**
101
- * Optional per-row resource-health flag, sourced straight from the injected
102
- * ComponentHealthMonitor (fills the gap left by WO-004's panel-resources
103
- * panel removal — health is still readable, just from here instead of a
104
- * dedicated panel). Blank for panels with no health record yet (never
105
- * rendered) or currently healthy.
106
- */
107
- function panelHealthMarker(health: { healthStatus: 'healthy' | 'warning' | 'overloaded' } | undefined): { text: string; color: string } {
108
- if (!health) return { text: ' ', color: C.dim };
109
- if (health.healthStatus === 'overloaded') return { text: '!', color: C.bad };
110
- if (health.healthStatus === 'warning') return { text: '~', color: C.warn };
111
- return { text: ' ', color: C.dim };
112
- }
113
-
114
- function wrapPanelDescription(text: string, width: number, maxLines = 2): string[] {
115
- if (width <= 0) return [''];
116
- const lines = wrapWithHangingIndent(text, width, '', maxLines);
117
- return lines.length > 0 ? lines.map((line) => truncateDisplay(line, width)) : [''];
118
- }
119
-
120
- // ── PanelListPanel ────────────────────────────────────────────────────────────
121
-
122
- export class PanelListPanel extends BasePanel {
123
- private _selectedIndex = 0;
124
- private _scrollOffset = 0;
125
- private _query = '';
126
- /** WO-153: converged modal '/' filter — mirrors ScrollableListPanel's _handleFilterKey contract. */
127
- private _filterActive = false;
128
- private _cachedEntries: ListEntry[] | null = null;
129
- private _entriesDirty = true;
130
-
131
- public constructor(
132
- private readonly panelManager: PanelManager,
133
- componentHealthMonitor?: ComponentHealthMonitor,
134
- ) {
135
- super('panel-list', 'Panel List', 'L', 'session', componentHealthMonitor);
136
- }
137
-
138
- public override onActivate(): void {
139
- super.onActivate();
140
- this._selectedIndex = 0;
141
- this._scrollOffset = 0;
142
- this._query = '';
143
- this._filterActive = false;
144
- this._entriesDirty = true;
145
- }
146
-
147
- protected override markDirty(): void {
148
- super.markDirty();
149
- this._entriesDirty = true;
150
- }
151
-
152
- /**
153
- * Filter-mode key handling (WO-153: converged modal '/' filter, mirrors
154
- * ScrollableListPanel's private _handleFilterKey). Returns `true`/`false`
155
- * when consumed/ignored in filter context, or `null` to fall through to
156
- * normal navigation and single-letter action keys (T/B/M/S/w/tab).
157
- */
158
- private _handleFilterKey(key: string): boolean | null {
159
- if (this._filterActive) {
160
- if (key === 'escape') {
161
- this._filterActive = false;
162
- this._query = '';
163
- this._selectedIndex = 0;
164
- this._scrollOffset = 0;
165
- this.markDirty();
166
- return true;
167
- }
168
- if (key === 'return' || key === 'enter') {
169
- this._filterActive = false; // commit; keep the query applied
170
- const selectedPanel = this._getSelectedPanelEntry(this._buildEntries());
171
- if (selectedPanel) {
172
- try {
173
- this.panelManager.open(selectedPanel.reg.id);
174
- } catch (err) {
175
- logger.warn(`[panel-list] failed to open panel: ${err}`);
176
- }
177
- }
178
- this.markDirty();
179
- return true;
180
- }
181
- if (key === 'backspace' || key === 'delete') {
182
- this._query = this._query.slice(0, -1);
183
- this._clampSelection(this._buildEntries());
184
- this.markDirty();
185
- return true;
186
- }
187
- // Arrow/paging keys navigate the filtered list — fall through.
188
- if (key === 'up' || key === 'down' || key === 'pageup' || key === 'pagedown' || key === 'home' || key === 'end') {
189
- return null;
190
- }
191
- // Any printable character (including single-letter action keys like T/B) extends the query.
192
- if (isPanelSearchPrintable(key)) {
193
- this._query += key;
194
- this._selectedIndex = 0;
195
- this._scrollOffset = 0;
196
- this.markDirty();
197
- return true;
198
- }
199
- return false;
200
- }
201
- if (key === '/') {
202
- this._filterActive = true;
203
- this.markDirty();
204
- return true;
205
- }
206
- return null;
207
- }
208
-
209
- /**
210
- * The filter input line — pinned rendering contract shared with
211
- * ScrollableListPanel.buildFilterLine: 'Filter: ' unfocused / '[Filter] '
212
- * focused, literal trailing '_' cursor while active (active:false is
213
- * passed to buildSearchInputLine to suppress its block-glyph cursor
214
- * substitution).
215
- */
216
- private _buildFilterLine(width: number): Line {
217
- const label = this._filterActive ? '[Filter] ' : 'Filter: ';
218
- const value = this._filterActive ? `${this._query}_` : this._query;
219
- return buildSearchInputLine(width, label, value, C, {
220
- active: false,
221
- bg: this._filterActive ? C.headerBg : C.sectionBg,
222
- emptyLabel: '(/ to filter)',
223
- valueColor: this._filterActive ? C.search : (this._query ? C.value : C.dim),
224
- });
225
- }
226
-
227
- /**
228
- * The `/`-to-filter buffer wants every character of a burst (paste, or a
229
- * fast-typed query landing in one input.feed() call), same as it always
230
- * has — see the interface doc on `Panel.isCapturingTextBurst`.
231
- */
232
- public isCapturingTextBurst(): boolean {
233
- return this._filterActive;
234
- }
235
-
236
- public handleInput(key: string): boolean {
237
- const filterResult = this._handleFilterKey(key);
238
- if (filterResult !== null) return filterResult;
239
-
240
- const entries = this._buildEntries();
241
-
242
- // Navigation
243
- if (key === 'up' || key === 'k') {
244
- this._movePrevPanel();
245
- this.markDirty();
246
- return true;
247
- }
248
- if (key === 'down' || key === 'j') {
249
- this._moveNextPanel(entries);
250
- this.markDirty();
251
- return true;
252
- }
253
-
254
- // Open selected panel
255
- if (key === 'return' || key === 'enter') {
256
- const selectedPanel = this._getSelectedPanelEntry(entries);
257
- if (selectedPanel) {
258
- try {
259
- this.panelManager.open(selectedPanel.reg.id);
260
- } catch (err) {
261
- logger.warn(`[panel-list] failed to open panel: ${err}`);
262
- }
263
- this.markDirty();
264
- }
265
- return true;
266
- }
267
-
268
- if (key === 'T' || key === 'B') {
269
- const selectedPanel = this._getSelectedPanelEntry(entries);
270
- if (selectedPanel) {
271
- const pane = key === 'T' ? 'top' : 'bottom';
272
- try {
273
- const pm = this.panelManager;
274
- pm.open(selectedPanel.reg.id, pane);
275
- pm.show();
276
- } catch (err) {
277
- logger.warn(`[panel-list] failed to place panel: ${err}`);
278
- }
279
- this.markDirty();
280
- }
281
- return true;
282
- }
283
-
284
- if (key === 'M') {
285
- const selectedPanel = this._getSelectedPanelEntry(entries);
286
- if (selectedPanel) {
287
- try {
288
- this.panelManager.moveToOtherPane(selectedPanel.reg.id);
289
- } catch (err) {
290
- logger.warn(`[panel-list] failed to move panel: ${err}`);
291
- }
292
- this.markDirty();
293
- }
294
- return true;
295
- }
296
-
297
- if (key === 'S') {
298
- this.panelManager.toggleBottomPane();
299
- this.markDirty();
300
- return true;
301
- }
302
-
303
- if (key === 'w') {
304
- const selectedPanel = this._getSelectedPanelEntry(entries);
305
- if (selectedPanel) {
306
- try {
307
- // Existing public PanelManager.close() API only — panel-manager.ts
308
- // itself is WO-150's surface this wave.
309
- this.panelManager.close(selectedPanel.reg.id);
310
- } catch (err) {
311
- logger.warn(`[panel-list] failed to close panel: ${err}`);
312
- }
313
- this.markDirty();
314
- }
315
- return true;
316
- }
317
-
318
- if (key === 'tab') {
319
- this.panelManager.togglePaneFocus();
320
- this.markDirty();
321
- return true;
322
- }
323
-
324
- return false;
325
- }
326
-
327
- public render(width: number, height: number): Line[] {
328
- if (!this.canRenderNow()) {
329
- return Array.from({ length: height }, () => createEmptyLine(width));
330
- }
331
- const start = Date.now();
332
- this.needsRender = false;
333
- const intro = 'Browse, place, split, and move panels without dropping into raw panel-management commands.';
334
- const entries = this._buildEntries();
335
-
336
- if (entries.length === 0) {
337
- const lines = buildPanelWorkspace(width, height, {
338
- title: 'Panel Workspace',
339
- intro,
340
- sections: [{
341
- title: 'Filter',
342
- lines: [this._buildFilterLine(width)],
343
- }, {
344
- lines: buildEmptyState(
345
- width,
346
- ' No panels match filter.',
347
- 'Clear the filter or search for another panel by id, name, description, or category.',
348
- // The real remedy for a stuck filter is clearing it in-panel;
349
- // '/panel list' would just re-activate this same instance with
350
- // the filter still applied. A committed filter needs '/' to
351
- // re-enter editing before Esc clears it.
352
- [{ command: '/ then Esc', summary: 'clear the filter and show every panel' }],
353
- C,
354
- ),
355
- }],
356
- palette: C,
357
- });
358
- while (lines.length < height) lines.push(createEmptyLine(width));
359
- if (lines.length > height) lines.length = height;
360
- this.reportRenderDuration(Date.now() - start);
361
- return lines;
362
- }
363
-
364
- const panelEntries = entries.filter(e => e.kind === 'panel');
365
- const pm = this.panelManager;
366
- const topIds = new Set(pm.getTopPane().panels.map(p => p.id));
367
- const bottomIds = new Set(pm.getBottomPane().panels.map(p => p.id));
368
- const focusedPane = pm.getFocusedPane();
369
- const footerLines = [buildPanelLine(width, [[truncateDisplay(` [${this._selectedIndex + 1}/${panelEntries.length}] ↑/↓ nav Enter open w close T/B place M move S split Tab focus`, width), C.dim]])];
370
- // Optional health rollup — only present when a ComponentHealthMonitor was
371
- // injected (fills the gap left by WO-004's panel-resources panel removal).
372
- const allHealth = this.componentHealthMonitor?.getAllHealth() ?? [];
373
- const unhealthyCount = allHealth.filter((h) => h.healthStatus !== 'healthy').length;
374
-
375
- const postureLines: Line[] = [
376
- buildKeyValueLine(width, [
377
- { label: 'visible panels', value: String(pm.getAllOpen().length), valueColor: pm.getAllOpen().length > 0 ? C.value : C.dim },
378
- { label: 'focused pane', value: focusedPane, valueColor: focusedPane === 'top' ? C.info : C.paneBottom },
379
- { label: 'split', value: pm.isBottomPaneVisible() ? 'dual' : 'single', valueColor: pm.isBottomPaneVisible() ? C.info : C.dim },
380
- { label: 'results', value: String(panelEntries.length), valueColor: C.value },
381
- ...(this.componentHealthMonitor
382
- ? [{ label: 'flagged', value: String(unhealthyCount), valueColor: unhealthyCount > 0 ? C.warn : C.good }]
383
- : []),
384
- ], C),
385
- buildPanelLine(width, [[` Filter owns input only when selected. Open and switch operations should land directly in focused panel state.`, C.header]]),
386
- ];
387
- const entryLines: Line[] = [this._buildFilterLine(width)];
388
- const renderedBlocks: Array<{ entry: ListEntry; lines: Line[]; panelFlatIndex?: number }> = [];
389
- let flatPanelIndex = 0;
390
- for (const entry of entries) {
391
- if (entry.kind === 'header') {
392
- const label = ` ── ${entry.label} ${'─'.repeat(Math.max(0, width - 6 - entry.label.length))}`;
393
- renderedBlocks.push({
394
- entry,
395
- lines: [buildPanelLine(width, [[truncateDisplay(label, width), C.label, C.headerBg]])],
396
- });
397
- } else {
398
- const flatIdx = flatPanelIndex++;
399
- const isSelected = flatIdx === this._selectedIndex;
400
- const isTopOpen = topIds.has(entry.reg.id);
401
- const isBottomOpen = bottomIds.has(entry.reg.id);
402
- const dot = isTopOpen || isBottomOpen ? '●' : '○';
403
- const dotColor = isTopOpen || isBottomOpen ? C.good : C.dim;
404
- const nameColor = C.value;
405
- const nameStr = fitDisplay(entry.reg.name, NAME_COL_WIDTH);
406
- const descStartCol = PREFIX_WIDTH + NAME_COL_WIDTH + 1;
407
- const descWidth = Math.max(1, width - descStartCol);
408
- const descLines = wrapPanelDescription(entry.reg.description, descWidth, 2);
409
- const placement = panelPlacementMarker({ isTopOpen, isBottomOpen, focusedPane });
410
- const health = panelHealthMarker(this.componentHealthMonitor?.getHealth(entry.reg.id));
411
- // When searching, the flat result list loses its category grouping — so
412
- // tag each result with a dim [Category] so the origin stays discoverable.
413
- const categoryTag = this._query
414
- ? `[${CATEGORY_LABELS[entry.reg.category]}] `
415
- : '';
416
- const blockLines: Line[] = [
417
- buildPanelListRow(width, [
418
- { text: dot, fg: dotColor },
419
- { text: placement.text, fg: placement.color },
420
- { text: health.text, fg: health.color },
421
- { text: ' ', fg: C.dim },
422
- { text: `${nameStr} `, fg: nameColor },
423
- { text: categoryTag, fg: C.label },
424
- { text: descLines[0] ?? '', fg: C.label },
425
- ], C, { selected: isSelected, selectedBg: C.selectBg, markerColor: C.info }),
426
- ];
427
- if ((descLines[1] ?? '').length > 0) {
428
- blockLines.push(buildPanelLine(width, [
429
- [' '.repeat(PREFIX_WIDTH), C.dim, isSelected ? C.selectBg : C.surfaceBg],
430
- [' '.repeat(NAME_COL_WIDTH), C.dim, isSelected ? C.selectBg : C.surfaceBg],
431
- [' ', C.dim, isSelected ? C.selectBg : C.surfaceBg],
432
- [descLines[1] ?? '', C.label, isSelected ? C.selectBg : C.surfaceBg],
433
- ]));
434
- }
435
- renderedBlocks.push({ entry, lines: blockLines, panelFlatIndex: flatIdx });
436
- }
437
- }
438
- const blockStarts: number[] = [];
439
- let totalRows = 0;
440
- for (const block of renderedBlocks) {
441
- blockStarts.push(totalRows);
442
- totalRows += block.lines.length;
443
- }
444
- const selectedEntryIndex = renderedBlocks.findIndex((block) => block.panelFlatIndex === this._selectedIndex);
445
- const selectedRow = selectedEntryIndex >= 0 ? blockStarts[selectedEntryIndex] ?? 0 : 0;
446
- const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Panel posture', postureLines, C) };
447
- const resolvedSection = resolvePrimaryScrollableSection(width, height, {
448
- intro,
449
- footerLines,
450
- palette: C,
451
- beforeSections: [postureSection],
452
- section: {
453
- title: 'Panels',
454
- fixedLines: entryLines,
455
- scrollableLines: renderedBlocks.flatMap((block) => block.lines),
456
- selectedIndex: selectedRow,
457
- scrollOffset: this._scrollOffset,
458
- guardRows: 1,
459
- minRows: 1,
460
- appendWindowSummary: {
461
- dimColor: C.dim,
462
- },
463
- },
464
- });
465
- this._scrollOffset = resolvedSection.scrollOffset;
466
-
467
- const sections: PanelWorkspaceSection[] = [
468
- postureSection,
469
- resolvedSection.section,
470
- ];
471
- const lines = buildPanelWorkspace(width, height, {
472
- title: 'Panel Workspace',
473
- intro,
474
- sections,
475
- footerLines,
476
- palette: C,
477
- });
478
-
479
- while (lines.length < height) lines.push(createEmptyLine(width));
480
- if (lines.length > height) lines.length = height;
481
- this.reportRenderDuration(Date.now() - start);
482
- return lines;
483
- }
484
-
485
- // ── Private helpers ───────────────────────────────────────────────────────
486
-
487
- /** Build flat list of entries (headers + panel rows) filtered by query. */
488
- private _buildEntries(): ListEntry[] {
489
- if (this._cachedEntries !== null && !this._entriesDirty) {
490
- return this._cachedEntries;
491
- }
492
-
493
- const manager = this.panelManager;
494
- const byCategory = manager.getTypesByCategory();
495
- const q = this._query.toLowerCase();
496
- const entries: ListEntry[] = [];
497
-
498
- // Recent group (browse mode only): the most-recently-opened panels, newest
499
- // first, so frequently used panels are one keystroke away.
500
- if (!q) {
501
- const byId = new Map(manager.getRegisteredTypes().map((r) => [r.id, r]));
502
- const recent: PanelRegistration[] = [];
503
- for (const id of manager.getRecentlyOpened()) {
504
- const reg = byId.get(id);
505
- if (reg) recent.push(reg);
506
- if (recent.length >= RECENT_LIMIT) break;
507
- }
508
- if (recent.length > 0) {
509
- entries.push({ kind: 'header', label: 'Recent' });
510
- for (const reg of recent) entries.push({ kind: 'panel', reg });
511
- }
512
- }
513
-
514
- for (const cat of CATEGORY_ORDER) {
515
- const regs = byCategory.get(cat) ?? [];
516
- const filtered = q
517
- ? regs.filter(r =>
518
- r.id.toLowerCase().includes(q) ||
519
- r.name.toLowerCase().includes(q) ||
520
- r.description.toLowerCase().includes(q) ||
521
- r.category.toLowerCase().includes(q)
522
- )
523
- : regs;
524
-
525
- if (filtered.length === 0) continue;
526
-
527
- entries.push({ kind: 'header', label: CATEGORY_LABELS[cat] });
528
- for (const reg of filtered) {
529
- entries.push({ kind: 'panel', reg });
530
- }
531
- }
532
-
533
- this._cachedEntries = entries;
534
- this._entriesDirty = false;
535
- return entries;
536
- }
537
-
538
- /** Get the ListEntry for the currently selected panel. */
539
- private _getSelectedPanelEntry(entries: ListEntry[]): Extract<ListEntry, { kind: 'panel' }> | null {
540
- let idx = 0;
541
- for (const e of entries) {
542
- if (e.kind !== 'panel') continue;
543
- if (idx === this._selectedIndex) return e;
544
- idx++;
545
- }
546
- return null;
547
- }
548
-
549
- /** Move selection to the previous panel entry. */
550
- private _movePrevPanel(): void {
551
- this._selectedIndex = Math.max(0, this._selectedIndex - 1);
552
- }
553
-
554
- /** Move selection to the next panel entry. */
555
- private _moveNextPanel(entries: ListEntry[]): void {
556
- const panelCount = entries.filter(e => e.kind === 'panel').length;
557
- this._selectedIndex = Math.min(panelCount - 1, this._selectedIndex + 1);
558
- }
559
-
560
- /** Clamp selectedIndex within available panels. */
561
- private _clampSelection(entries: ListEntry[]): void {
562
- const panelCount = entries.filter(e => e.kind === 'panel').length;
563
- this._selectedIndex = Math.max(0, Math.min(this._selectedIndex, panelCount - 1));
564
- }
565
-
566
- }