@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,619 +0,0 @@
1
- import { BasePanel } from './base-panel.ts';
2
- import type { Line } from '../types/grid.ts';
3
- import {
4
- buildEmptyState,
5
- buildKeyboardHints,
6
- buildPanelLine,
7
- buildTreeRow,
8
- buildPanelWorkspace,
9
- resolveScrollablePanelSection,
10
- DEFAULT_PANEL_PALETTE,
11
- extendPalette,
12
- } from './polish.ts';
13
- import { TreeSitterService } from '@pellux/goodvibes-sdk/platform/intelligence';
14
- import type { Node, Tree, Language } from 'web-tree-sitter';
15
- import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
16
-
17
- // ── Symbol types ────────────────────────────────────────────────────────────
18
-
19
- export type SymbolKind = 'function' | 'class' | 'interface' | 'type' | 'const' | 'method' | 'namespace';
20
-
21
- export interface SymbolEntry {
22
- kind: SymbolKind;
23
- name: string;
24
- line: number;
25
- /** If set, this symbol is a child of a parent container (class/namespace). */
26
- parentName?: string;
27
- }
28
-
29
- // ── Rendering constants ──────────────────────────────────────────────────────
30
-
31
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
32
- selectedBg: '236',
33
- });
34
-
35
- /** ANSI 256-color fg codes per symbol kind. */
36
- const KIND_COLORS: Record<SymbolKind, string> = {
37
- function: '87', // cyan
38
- method: '87', // cyan
39
- class: '141', // purple
40
- namespace: '141', // purple
41
- interface: '219', // pink
42
- type: '228', // yellow
43
- const: '245', // grey
44
- };
45
-
46
- /** Single-char type icon for the tree row (kept ASCII for column safety). */
47
- const KIND_ICONS: Record<SymbolKind, string> = {
48
- function: 'ƒ',
49
- method: 'ƒ',
50
- class: 'C',
51
- namespace: 'N',
52
- interface: 'I',
53
- type: 'T',
54
- const: 'k',
55
- };
56
-
57
- // ── Tree-sitter symbol extraction ────────────────────────────────────────────
58
-
59
- type LangId = 'typescript' | 'tsx' | 'javascript';
60
-
61
- /** Map a file extension to the tree-sitter grammar id used to parse it. */
62
- function detectLangId(filePath: string): LangId | null {
63
- const dot = filePath.lastIndexOf('.');
64
- const ext = dot >= 0 ? filePath.slice(dot + 1).toLowerCase() : '';
65
- if (ext === 'ts' || ext === 'mts' || ext === 'cts') return 'typescript';
66
- if (ext === 'tsx') return 'tsx';
67
- if (ext === 'js' || ext === 'mjs' || ext === 'cjs' || ext === 'jsx') return 'javascript';
68
- return null;
69
- }
70
-
71
- // Tree-sitter queries capturing the declaration shapes the outline surfaces.
72
- // Class members (methods, getters/setters, decorated members, and
73
- // arrow-function class fields) are captured structurally rather than by
74
- // indentation, so decorators and modifiers never throw off matching.
75
- const TS_QUERY = `
76
- (class_declaration name: (type_identifier) @class.name) @class.def
77
- (abstract_class_declaration name: (type_identifier) @class.name) @class.def
78
- (interface_declaration name: (type_identifier) @interface.name) @interface.def
79
- (type_alias_declaration name: (type_identifier) @type.name) @type.def
80
- (internal_module name: (identifier) @namespace.name) @namespace.def
81
- (function_declaration name: (identifier) @function.name) @function.def
82
- (method_definition name: (property_identifier) @method.name) @method.def
83
- (public_field_definition
84
- name: (property_identifier) @field.name
85
- value: (arrow_function)) @field.def
86
- (lexical_declaration
87
- (variable_declarator name: (identifier) @const.name) @const.declarator) @const.def
88
- `;
89
-
90
- const JS_QUERY = `
91
- (class_declaration name: (identifier) @class.name) @class.def
92
- (function_declaration name: (identifier) @function.name) @function.def
93
- (method_definition name: (property_identifier) @method.name) @method.def
94
- (field_definition
95
- property: (property_identifier) @field.name
96
- value: (arrow_function)) @field.def
97
- (lexical_declaration
98
- (variable_declarator name: (identifier) @const.name) @const.declarator) @const.def
99
- `;
100
-
101
- const QUERY_BY_LANG: Record<LangId, string> = {
102
- typescript: TS_QUERY,
103
- tsx: TS_QUERY,
104
- javascript: JS_QUERY,
105
- };
106
-
107
- const CLASS_NODE_TYPES = new Set(['class_declaration', 'abstract_class_declaration', 'class_expression']);
108
-
109
- /** True when `node` sits directly at module scope (optionally wrapped in `export`). */
110
- function isTopLevelDeclaration(node: Node): boolean {
111
- const parent = node.parent;
112
- if (!parent) return true;
113
- if (parent.type === 'program') return true;
114
- if (parent.type === 'export_statement') return parent.parent?.type === 'program';
115
- return false;
116
- }
117
-
118
- /**
119
- * Walk up from a class member to its nearest enclosing class. Returns the
120
- * class name only when that class is itself a top-level declaration — this
121
- * keeps the outline to a class + its direct members (no deep nesting), the
122
- * same depth the previous regex-based parser supported.
123
- */
124
- function findEnclosingClassName(node: Node): string | null {
125
- let cursor: Node | null = node.parent;
126
- while (cursor) {
127
- if (CLASS_NODE_TYPES.has(cursor.type)) {
128
- if (!isTopLevelDeclaration(cursor)) return null;
129
- const nameNode = cursor.childForFieldName('name');
130
- return nameNode ? nameNode.text : null;
131
- }
132
- cursor = cursor.parent;
133
- }
134
- return null;
135
- }
136
-
137
- /**
138
- * Extract a flat, line-ordered list of symbols from a parsed tree using a
139
- * tree-sitter query over the real AST (no regex heuristics). Class members
140
- * (methods, getters/setters, decorated members, arrow-function fields) carry
141
- * `parentName` so the renderer can group them under their class header.
142
- */
143
- function extractSymbolsFromTree(tree: Tree, language: Language, langId: LangId, service: TreeSitterService): SymbolEntry[] {
144
- const query = QUERY_BY_LANG[langId];
145
- const matches = service.query(tree, language, query);
146
- const result: SymbolEntry[] = [];
147
-
148
- for (const match of matches) {
149
- const captures = new Map<string, Node>(match.captures.map((c) => [c.name, c.node]));
150
-
151
- const classDef = captures.get('class.def');
152
- if (classDef) {
153
- const nameNode = captures.get('class.name');
154
- if (nameNode && isTopLevelDeclaration(classDef)) {
155
- result.push({ kind: 'class', name: nameNode.text, line: classDef.startPosition.row + 1 });
156
- }
157
- continue;
158
- }
159
-
160
- const interfaceDef = captures.get('interface.def');
161
- if (interfaceDef) {
162
- const nameNode = captures.get('interface.name');
163
- if (nameNode && isTopLevelDeclaration(interfaceDef)) {
164
- result.push({ kind: 'interface', name: nameNode.text, line: interfaceDef.startPosition.row + 1 });
165
- }
166
- continue;
167
- }
168
-
169
- const typeDef = captures.get('type.def');
170
- if (typeDef) {
171
- const nameNode = captures.get('type.name');
172
- if (nameNode && isTopLevelDeclaration(typeDef)) {
173
- result.push({ kind: 'type', name: nameNode.text, line: typeDef.startPosition.row + 1 });
174
- }
175
- continue;
176
- }
177
-
178
- const namespaceDef = captures.get('namespace.def');
179
- if (namespaceDef) {
180
- const nameNode = captures.get('namespace.name');
181
- if (nameNode && isTopLevelDeclaration(namespaceDef)) {
182
- result.push({ kind: 'namespace', name: nameNode.text, line: namespaceDef.startPosition.row + 1 });
183
- }
184
- continue;
185
- }
186
-
187
- const functionDef = captures.get('function.def');
188
- if (functionDef) {
189
- const nameNode = captures.get('function.name');
190
- if (nameNode && isTopLevelDeclaration(functionDef)) {
191
- result.push({ kind: 'function', name: nameNode.text, line: functionDef.startPosition.row + 1 });
192
- }
193
- continue;
194
- }
195
-
196
- // Methods, getters/setters, and constructors — all `method_definition`
197
- // regardless of decorators (decorators are preceding siblings, not
198
- // wrappers, so they never block the match).
199
- const methodDef = captures.get('method.def');
200
- if (methodDef) {
201
- const nameNode = captures.get('method.name');
202
- const parentName = findEnclosingClassName(methodDef);
203
- if (nameNode && parentName) {
204
- result.push({ kind: 'method', name: nameNode.text, line: methodDef.startPosition.row + 1, parentName });
205
- }
206
- continue;
207
- }
208
-
209
- // Arrow-function class fields (`onClick = () => {...}`), decorated or not.
210
- const fieldDef = captures.get('field.def');
211
- if (fieldDef) {
212
- const nameNode = captures.get('field.name');
213
- const parentName = findEnclosingClassName(fieldDef);
214
- if (nameNode && parentName) {
215
- result.push({ kind: 'method', name: nameNode.text, line: fieldDef.startPosition.row + 1, parentName });
216
- }
217
- continue;
218
- }
219
-
220
- const constDef = captures.get('const.def');
221
- if (constDef) {
222
- const nameNode = captures.get('const.name');
223
- const declarator = captures.get('const.declarator');
224
- if (nameNode && isTopLevelDeclaration(constDef)) {
225
- const valueNode = declarator?.childForFieldName('value') ?? null;
226
- const isFunctionValued = valueNode?.type === 'arrow_function' || valueNode?.type === 'function_expression';
227
- result.push({
228
- kind: isFunctionValued ? 'function' : 'const',
229
- name: nameNode.text,
230
- line: constDef.startPosition.row + 1,
231
- });
232
- }
233
- continue;
234
- }
235
- }
236
-
237
- result.sort((a, b) => a.line - b.line);
238
- return result;
239
- }
240
-
241
- // ── Panel ────────────────────────────────────────────────────────────────────
242
-
243
- /**
244
- * SymbolOutlinePanel — renders a hierarchical symbol outline of the current
245
- * file. Symbols are parsed from the real AST via a tree-sitter query (the
246
- * same parser infrastructure syntax-highlighter.ts uses), not regex
247
- * heuristics — so class fields assigned arrow functions, getters/setters,
248
- * and decorated members all appear correctly.
249
- */
250
- export class SymbolOutlinePanel extends BasePanel {
251
- /** Flat list of parsed symbols (methods nested after their parent class). */
252
- private symbols: SymbolEntry[] = [];
253
-
254
- /** Index of the currently highlighted row in the visible flat list. */
255
- private selectedIndex: number = 0;
256
-
257
- /**
258
- * The row under the cursor. This panel owns its own selection state
259
- * (`selectedIndex` navigates the filtered `_visibleRows()` flat list), so
260
- * every selected-row read routes through this one accessor — indexing the
261
- * `_visibleRows()` list by the cursor directly is banned by the
262
- * no-raw-selectedindex-read architecture rule.
263
- */
264
- private selectedRow(): VisibleRow | undefined {
265
- return this._visibleRows().at(this.selectedIndex);
266
- }
267
-
268
- /** Set of container names (class/namespace) that are collapsed. */
269
- private collapsed: Set<string> = new Set();
270
-
271
- /** Scroll offset (top-visible row index in the flat rendered list). */
272
- private scrollOffset: number = 0;
273
-
274
- /** Path of the file currently loaded. */
275
- private currentPath: string = '';
276
-
277
- /** Shared tree-sitter service for this panel instance (mirrors syntax-highlighter.ts). */
278
- private readonly treeSitter = new TreeSitterService();
279
-
280
- /** Bumped on every loadFile() call so a superseded async parse is discarded. */
281
- private parseGeneration: number = 0;
282
-
283
- constructor() {
284
- super('symbols', 'Symbols', 'S', 'development');
285
- this.treeSitter.initialize().catch((err: unknown) => {
286
- logger.warn('SymbolOutlinePanel: tree-sitter init failed', { error: summarizeError(err) });
287
- });
288
- }
289
-
290
- // ── Public API ─────────────────────────────────────────────────────────────
291
-
292
- /**
293
- * Load and parse symbols from the given file source text via tree-sitter.
294
- * Call this when the active file changes in the file-preview panel, and
295
- * again whenever the preview reloads the same file so the outline re-syncs
296
- * with on-disk edits.
297
- */
298
- loadFile(path: string, source: string): void {
299
- this.currentPath = path;
300
- this.symbols = [];
301
- this.selectedIndex = 0;
302
- this.scrollOffset = 0;
303
- this.collapsed.clear();
304
- this.markDirty();
305
-
306
- const generation = ++this.parseGeneration;
307
- const langId = detectLangId(path);
308
- if (!langId) return; // unsupported language — honest "no symbols" empty state
309
-
310
- void this._parseAndSync(path, source, langId, generation);
311
- }
312
-
313
- /**
314
- * Returns the { path, line } for the currently selected symbol so the
315
- * caller can jump to it in the file-preview panel.
316
- */
317
- getSelectedLocation(): { path: string; line: number } | null {
318
- const row = this.selectedRow();
319
- if (!row || row.kind === 'header') return null;
320
- return { path: this.currentPath, line: row.symbol.line };
321
- }
322
-
323
- // ── Input ──────────────────────────────────────────────────────────────────
324
-
325
- handleInput(key: string): boolean {
326
- const visible = this._visibleRows();
327
-
328
- if (key === 'up' || key === 'k') {
329
- if (this.selectedIndex > 0) {
330
- this.selectedIndex--;
331
- this._clampScroll(visible.length);
332
- this.markDirty();
333
- }
334
- return true;
335
- }
336
-
337
- if (key === 'down' || key === 'j') {
338
- if (this.selectedIndex < visible.length - 1) {
339
- this.selectedIndex++;
340
- this._clampScroll(visible.length);
341
- this.markDirty();
342
- }
343
- return true;
344
- }
345
-
346
- if (key === 'return' || key === 'enter') {
347
- // Only consume Enter when there is a real symbol to jump to — otherwise
348
- // let the key fall through instead of swallowing it for nothing.
349
- return this.getSelectedLocation() !== null;
350
- }
351
-
352
- if (key === 'space' || key === 'right' || key === 'left') {
353
- const row = this.selectedRow();
354
- if (row?.kind === 'header') {
355
- const name = row.name;
356
- if (this.collapsed.has(name)) {
357
- this.collapsed.delete(name);
358
- } else {
359
- this.collapsed.add(name);
360
- }
361
- // Clamp selection so it doesn't point into a now-hidden row
362
- const newVisible = this._visibleRows();
363
- if (this.selectedIndex >= newVisible.length) {
364
- this.selectedIndex = Math.max(0, newVisible.length - 1);
365
- }
366
- this._clampScroll(newVisible.length);
367
- this.markDirty();
368
- }
369
- return true;
370
- }
371
-
372
- return false;
373
- }
374
-
375
- // ── Rendering ──────────────────────────────────────────────────────────────
376
-
377
- render(width: number, height: number): Line[] {
378
- this.needsRender = false;
379
-
380
- if (this.symbols.length === 0) {
381
- return buildPanelWorkspace(width, height, {
382
- title: ' Symbols',
383
- intro: 'Outline the current file into navigable symbols and lightweight parent/child structure.',
384
- sections: [
385
- {
386
- lines: buildEmptyState(
387
- width,
388
- this.currentPath ? ' No symbols found' : ' No file loaded',
389
- this.currentPath
390
- ? 'The current file did not produce outline entries from the tree-sitter parse.'
391
- : 'Load a file in the preview panel to populate its outline here.',
392
- this.currentPath
393
- ? []
394
- : [{ command: '/panel open explorer', summary: 'pick a file in the explorer, then Enter to preview and outline it' }],
395
- C,
396
- ),
397
- },
398
- ],
399
- palette: DEFAULT_PANEL_PALETTE,
400
- });
401
- }
402
-
403
- const visible = this._visibleRows();
404
- const outlineSection = resolveScrollablePanelSection(width, height, {
405
- intro: this.currentPath ? this.currentPath : 'Outline the current file into navigable symbols and lightweight parent/child structure.',
406
- footerLines: [
407
- buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Space/←/→', label: 'collapse' }, { keys: 'Enter', label: 'jump to source →' }], DEFAULT_PANEL_PALETTE),
408
- ],
409
- palette: DEFAULT_PANEL_PALETTE,
410
- beforeSections: [
411
- {
412
- title: 'Summary',
413
- lines: [
414
- buildPanelLine(width, [
415
- [' Symbols ', DEFAULT_PANEL_PALETTE.label],
416
- [String(this.symbols.length), DEFAULT_PANEL_PALETTE.value],
417
- [' Collapsed ', DEFAULT_PANEL_PALETTE.label],
418
- [String(this.collapsed.size), this.collapsed.size > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
419
- ]),
420
- ],
421
- },
422
- ],
423
- section: {
424
- title: 'Outline',
425
- scrollableLines: visible.map((row, i) => {
426
- const isSelected = i === this.selectedIndex;
427
- const bgColor = isSelected ? '236' : '';
428
- return row.kind === 'header'
429
- ? _renderHeader(width, row, isSelected, bgColor, this.collapsed)
430
- : _renderSymbol(width, row, isSelected, bgColor);
431
- }),
432
- selectedIndex: this.selectedIndex,
433
- scrollOffset: this.scrollOffset,
434
- minRows: 8,
435
- },
436
- afterSections: [
437
- {
438
- title: 'Selected',
439
- lines: (() => {
440
- const selected = this.selectedRow();
441
- return selected
442
- ? [
443
- buildPanelLine(width, [
444
- [' Kind ', DEFAULT_PANEL_PALETTE.label],
445
- [selected.kind === 'header' ? selected.symbolKind : selected.symbol.kind, DEFAULT_PANEL_PALETTE.info],
446
- [' Line ', DEFAULT_PANEL_PALETTE.label],
447
- [String(selected.kind === 'header' ? selected.line : selected.symbol.line), DEFAULT_PANEL_PALETTE.value],
448
- ]),
449
- buildPanelLine(width, [
450
- [' Name ', DEFAULT_PANEL_PALETTE.label],
451
- [selected.kind === 'header' ? selected.name : selected.symbol.name, DEFAULT_PANEL_PALETTE.value],
452
- ]),
453
- ]
454
- : [];
455
- })(),
456
- },
457
- ],
458
- });
459
- this.scrollOffset = outlineSection.scrollOffset;
460
-
461
- const selected = this.selectedRow();
462
- return buildPanelWorkspace(width, height, {
463
- title: ' Symbols',
464
- intro: this.currentPath ? this.currentPath : 'Outline the current file into navigable symbols and lightweight parent/child structure.',
465
- sections: [
466
- {
467
- title: 'Summary',
468
- lines: [
469
- buildPanelLine(width, [
470
- [' Symbols ', DEFAULT_PANEL_PALETTE.label],
471
- [String(this.symbols.length), DEFAULT_PANEL_PALETTE.value],
472
- [' Collapsed ', DEFAULT_PANEL_PALETTE.label],
473
- [String(this.collapsed.size), this.collapsed.size > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
474
- ]),
475
- ],
476
- },
477
- outlineSection.section,
478
- {
479
- title: 'Selected',
480
- lines: selected
481
- ? [
482
- buildPanelLine(width, [
483
- [' Kind ', DEFAULT_PANEL_PALETTE.label],
484
- [selected.kind === 'header' ? selected.symbolKind : selected.symbol.kind, DEFAULT_PANEL_PALETTE.info],
485
- [' Line ', DEFAULT_PANEL_PALETTE.label],
486
- [String(selected.kind === 'header' ? selected.line : selected.symbol.line), DEFAULT_PANEL_PALETTE.value],
487
- ]),
488
- buildPanelLine(width, [
489
- [' Name ', DEFAULT_PANEL_PALETTE.label],
490
- [selected.kind === 'header' ? selected.name : selected.symbol.name, DEFAULT_PANEL_PALETTE.value],
491
- ]),
492
- ]
493
- : [],
494
- },
495
- ],
496
- footerLines: [
497
- buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Space/←/→', label: 'collapse' }, { keys: 'Enter', label: 'jump to source →' }], DEFAULT_PANEL_PALETTE),
498
- ],
499
- palette: DEFAULT_PANEL_PALETTE,
500
- });
501
- }
502
-
503
- // ── Private helpers ────────────────────────────────────────────────────────
504
-
505
- private async _parseAndSync(path: string, source: string, langId: LangId, generation: number): Promise<void> {
506
- try {
507
- await this.treeSitter.initialize();
508
- const language = await this.treeSitter.loadLanguage(langId);
509
- if (!language) return;
510
- const tree = await this.treeSitter.parse(path, source, langId);
511
- if (!tree) return;
512
- // A newer loadFile() call superseded this one — discard the stale result.
513
- if (generation !== this.parseGeneration) return;
514
- this.symbols = extractSymbolsFromTree(tree, language, langId, this.treeSitter);
515
- this.selectedIndex = 0;
516
- this.scrollOffset = 0;
517
- this.markDirty();
518
- } catch (err) {
519
- logger.warn('SymbolOutlinePanel: tree-sitter parse failed', { path, error: summarizeError(err) });
520
- }
521
- }
522
-
523
- private _clampScroll(totalRows: number): void {
524
- // Ensure selected is within scroll view (assumes last known height ~ 20)
525
- // We keep a conservative viewport window; render() uses this.scrollOffset.
526
- const GUARD = 3;
527
- if (this.selectedIndex < this.scrollOffset + GUARD) {
528
- this.scrollOffset = Math.max(0, this.selectedIndex - GUARD);
529
- }
530
- // We don't know height here, so we defer bottom-clamp to render().
531
- }
532
-
533
- private _visibleRows(): VisibleRow[] {
534
- return buildVisibleRows(this.symbols, this.collapsed);
535
- }
536
- }
537
-
538
- // ── Row types for rendering ──────────────────────────────────────────────────
539
-
540
- type VisibleRow =
541
- | { kind: 'header'; name: string; symbolKind: SymbolKind; line: number; hasChildren: boolean }
542
- | { kind: 'symbol'; symbol: SymbolEntry; depth: number };
543
-
544
- // ── Pure helpers ─────────────────────────────────────────────────────────────
545
-
546
- /**
547
- * Build the flat list of rows to render, respecting collapse state.
548
- * Container symbols become header rows; their children are indented below.
549
- */
550
- function buildVisibleRows(symbols: SymbolEntry[], collapsed: Set<string>): VisibleRow[] {
551
- const rows: VisibleRow[] = [];
552
-
553
- // Pre-compute children map to avoid O(n^2) scans inside the loop
554
- const childrenByParent = new Map<string, SymbolEntry[]>();
555
- for (const sym of symbols) {
556
- if (sym.parentName) {
557
- const arr = childrenByParent.get(sym.parentName);
558
- if (arr) arr.push(sym);
559
- else childrenByParent.set(sym.parentName, [sym]);
560
- }
561
- }
562
-
563
- for (const sym of symbols) {
564
- if (sym.kind === 'class' || sym.kind === 'namespace') {
565
- const children = childrenByParent.get(sym.name) ?? [];
566
- const hasChildren = children.length > 0;
567
- rows.push({ kind: 'header', name: sym.name, symbolKind: sym.kind, line: sym.line, hasChildren });
568
- // If collapsed, skip children
569
- if (collapsed.has(sym.name)) continue;
570
- // Add children immediately after header
571
- for (const child of children) {
572
- rows.push({ kind: 'symbol', symbol: child, depth: 1 });
573
- }
574
- } else if (!sym.parentName) {
575
- // Top-level non-container symbol
576
- rows.push({ kind: 'symbol', symbol: sym, depth: 0 });
577
- }
578
- // Children with parentName are rendered under their header above
579
- }
580
-
581
- return rows;
582
- }
583
-
584
- /** Render a container header row (class / namespace) via the shared tree row. */
585
- function _renderHeader(
586
- width: number,
587
- row: Extract<VisibleRow, { kind: 'header' }>,
588
- isSelected: boolean,
589
- bgColor: string,
590
- collapsed: Set<string>,
591
- ): Line {
592
- const isCollapsed = collapsed.has(row.name);
593
- return buildTreeRow(width, {
594
- depth: 0,
595
- label: row.name,
596
- icon: KIND_ICONS[row.symbolKind],
597
- expandable: row.hasChildren,
598
- expanded: !isCollapsed,
599
- labelColor: KIND_COLORS[row.symbolKind],
600
- metadata: [{ text: `:${row.line}`, fg: DEFAULT_PANEL_PALETTE.dim }],
601
- }, C, { selected: isSelected, selectedBg: bgColor || C.selectedBg });
602
- }
603
-
604
- /** Render a regular symbol row via the shared tree row. */
605
- function _renderSymbol(
606
- width: number,
607
- row: Extract<VisibleRow, { kind: 'symbol' }>,
608
- isSelected: boolean,
609
- bgColor: string,
610
- ): Line {
611
- const { symbol, depth } = row;
612
- return buildTreeRow(width, {
613
- depth,
614
- label: symbol.name,
615
- icon: KIND_ICONS[symbol.kind],
616
- labelColor: KIND_COLORS[symbol.kind],
617
- metadata: [{ text: `:${symbol.line}`, fg: DEFAULT_PANEL_PALETTE.dim }],
618
- }, C, { selected: isSelected, selectedBg: bgColor || C.selectedBg });
619
- }