@pellux/goodvibes-tui 1.1.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,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
- }