@pellux/goodvibes-tui 0.28.0 → 0.29.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.
- package/CHANGELOG.md +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +203 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +309 -128
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BasePanel } from './base-panel.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
2
|
import type { Line } from '../types/grid.ts';
|
|
4
3
|
import {
|
|
5
4
|
buildEmptyState,
|
|
@@ -11,6 +10,9 @@ import {
|
|
|
11
10
|
DEFAULT_PANEL_PALETTE,
|
|
12
11
|
extendPalette,
|
|
13
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';
|
|
14
16
|
|
|
15
17
|
// ── Symbol types ────────────────────────────────────────────────────────────
|
|
16
18
|
|
|
@@ -52,32 +54,198 @@ const KIND_ICONS: Record<SymbolKind, string> = {
|
|
|
52
54
|
const: 'k',
|
|
53
55
|
};
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
240
|
|
|
75
241
|
// ── Panel ────────────────────────────────────────────────────────────────────
|
|
76
242
|
|
|
77
243
|
/**
|
|
78
244
|
* SymbolOutlinePanel — renders a hierarchical symbol outline of the current
|
|
79
|
-
* file. Symbols are parsed from
|
|
80
|
-
*
|
|
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.
|
|
81
249
|
*/
|
|
82
250
|
export class SymbolOutlinePanel extends BasePanel {
|
|
83
251
|
/** Flat list of parsed symbols (methods nested after their parent class). */
|
|
@@ -86,6 +254,17 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
86
254
|
/** Index of the currently highlighted row in the visible flat list. */
|
|
87
255
|
private selectedIndex: number = 0;
|
|
88
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
|
+
|
|
89
268
|
/** Set of container names (class/namespace) that are collapsed. */
|
|
90
269
|
private collapsed: Set<string> = new Set();
|
|
91
270
|
|
|
@@ -95,23 +274,40 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
95
274
|
/** Path of the file currently loaded. */
|
|
96
275
|
private currentPath: string = '';
|
|
97
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
|
+
|
|
98
283
|
constructor() {
|
|
99
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
|
+
});
|
|
100
288
|
}
|
|
101
289
|
|
|
102
290
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
103
291
|
|
|
104
292
|
/**
|
|
105
|
-
* Load and parse symbols from the given file source text.
|
|
106
|
-
* Call this when the active file changes in the file-preview panel
|
|
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.
|
|
107
297
|
*/
|
|
108
298
|
loadFile(path: string, source: string): void {
|
|
109
299
|
this.currentPath = path;
|
|
110
|
-
this.symbols =
|
|
300
|
+
this.symbols = [];
|
|
111
301
|
this.selectedIndex = 0;
|
|
112
302
|
this.scrollOffset = 0;
|
|
113
303
|
this.collapsed.clear();
|
|
114
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);
|
|
115
311
|
}
|
|
116
312
|
|
|
117
313
|
/**
|
|
@@ -119,8 +315,7 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
119
315
|
* caller can jump to it in the file-preview panel.
|
|
120
316
|
*/
|
|
121
317
|
getSelectedLocation(): { path: string; line: number } | null {
|
|
122
|
-
const
|
|
123
|
-
const row = visible[this.selectedIndex];
|
|
318
|
+
const row = this.selectedRow();
|
|
124
319
|
if (!row || row.kind === 'header') return null;
|
|
125
320
|
return { path: this.currentPath, line: row.symbol.line };
|
|
126
321
|
}
|
|
@@ -149,12 +344,13 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
149
344
|
}
|
|
150
345
|
|
|
151
346
|
if (key === 'return' || key === 'enter') {
|
|
152
|
-
//
|
|
153
|
-
|
|
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;
|
|
154
350
|
}
|
|
155
351
|
|
|
156
352
|
if (key === 'space' || key === 'right' || key === 'left') {
|
|
157
|
-
const row =
|
|
353
|
+
const row = this.selectedRow();
|
|
158
354
|
if (row?.kind === 'header') {
|
|
159
355
|
const name = row.name;
|
|
160
356
|
if (this.collapsed.has(name)) {
|
|
@@ -191,11 +387,11 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
191
387
|
width,
|
|
192
388
|
this.currentPath ? ' No symbols found' : ' No file loaded',
|
|
193
389
|
this.currentPath
|
|
194
|
-
? 'The current file did not produce outline entries
|
|
390
|
+
? 'The current file did not produce outline entries from the tree-sitter parse.'
|
|
195
391
|
: 'Load a file in the preview panel to populate its outline here.',
|
|
196
392
|
this.currentPath
|
|
197
393
|
? []
|
|
198
|
-
: [{ command: '/explorer', summary: 'pick a file in the explorer, then Enter to preview and outline it' }],
|
|
394
|
+
: [{ command: '/panel open explorer', summary: 'pick a file in the explorer, then Enter to preview and outline it' }],
|
|
199
395
|
C,
|
|
200
396
|
),
|
|
201
397
|
},
|
|
@@ -241,7 +437,7 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
241
437
|
{
|
|
242
438
|
title: 'Selected',
|
|
243
439
|
lines: (() => {
|
|
244
|
-
const selected =
|
|
440
|
+
const selected = this.selectedRow();
|
|
245
441
|
return selected
|
|
246
442
|
? [
|
|
247
443
|
buildPanelLine(width, [
|
|
@@ -262,7 +458,7 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
262
458
|
});
|
|
263
459
|
this.scrollOffset = outlineSection.scrollOffset;
|
|
264
460
|
|
|
265
|
-
const selected =
|
|
461
|
+
const selected = this.selectedRow();
|
|
266
462
|
return buildPanelWorkspace(width, height, {
|
|
267
463
|
title: ' Symbols',
|
|
268
464
|
intro: this.currentPath ? this.currentPath : 'Outline the current file into navigable symbols and lightweight parent/child structure.',
|
|
@@ -306,6 +502,24 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
306
502
|
|
|
307
503
|
// ── Private helpers ────────────────────────────────────────────────────────
|
|
308
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
|
+
|
|
309
523
|
private _clampScroll(totalRows: number): void {
|
|
310
524
|
// Ensure selected is within scroll view (assumes last known height ~ 20)
|
|
311
525
|
// We keep a conservative viewport window; render() uses this.scrollOffset.
|
|
@@ -329,80 +543,6 @@ type VisibleRow =
|
|
|
329
543
|
|
|
330
544
|
// ── Pure helpers ─────────────────────────────────────────────────────────────
|
|
331
545
|
|
|
332
|
-
/**
|
|
333
|
-
* Parse symbols from source text. Returns a flat list ordered by line number.
|
|
334
|
-
* Methods are tagged with their parent class name so the renderer can group them.
|
|
335
|
-
*/
|
|
336
|
-
function parseSymbols(source: string): SymbolEntry[] {
|
|
337
|
-
const lines = source.split('\n');
|
|
338
|
-
const result: SymbolEntry[] = [];
|
|
339
|
-
let currentContainer: string | undefined;
|
|
340
|
-
let containerKind: SymbolKind | undefined;
|
|
341
|
-
let containerBraceDepth = 0;
|
|
342
|
-
let braceDepth = 0;
|
|
343
|
-
|
|
344
|
-
for (let i = 0; i < lines.length; i++) {
|
|
345
|
-
const raw = lines[i];
|
|
346
|
-
const trimmed = raw.trimStart();
|
|
347
|
-
const lineNo = i + 1; // 1-based
|
|
348
|
-
|
|
349
|
-
// Strip string literals before counting braces to avoid false positives
|
|
350
|
-
const rawNoBraceStrings = raw.replace(/("|\'|\`)(?:(?!\1|\\).|\\[\s\S])*\1/g, '');
|
|
351
|
-
|
|
352
|
-
// Track brace depth for container scoping
|
|
353
|
-
for (const ch of rawNoBraceStrings) {
|
|
354
|
-
if (ch === '{') braceDepth++;
|
|
355
|
-
else if (ch === '}') {
|
|
356
|
-
braceDepth--;
|
|
357
|
-
if (currentContainer !== undefined && braceDepth <= containerBraceDepth) {
|
|
358
|
-
currentContainer = undefined;
|
|
359
|
-
containerKind = undefined;
|
|
360
|
-
containerBraceDepth = 0;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Skip comment lines and blank lines
|
|
366
|
-
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*') || trimmed === '') continue;
|
|
367
|
-
|
|
368
|
-
// Check container-level patterns first (class/namespace)
|
|
369
|
-
let matched = false;
|
|
370
|
-
for (const { re, kind, isContainer } of SYMBOL_PATTERNS) {
|
|
371
|
-
const m = trimmed.match(re);
|
|
372
|
-
if (!m) continue;
|
|
373
|
-
const name = m[1];
|
|
374
|
-
if (!name) continue;
|
|
375
|
-
|
|
376
|
-
const entry: SymbolEntry = { kind, name, line: lineNo };
|
|
377
|
-
|
|
378
|
-
if (currentContainer && kind === 'method') {
|
|
379
|
-
entry.parentName = currentContainer;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Don't add methods as top-level if they're inside a container
|
|
383
|
-
if (kind === 'method' && !currentContainer) {
|
|
384
|
-
// standalone function-like at wrong indent — skip
|
|
385
|
-
matched = true;
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
result.push(entry);
|
|
390
|
-
|
|
391
|
-
if (isContainer) {
|
|
392
|
-
currentContainer = name;
|
|
393
|
-
containerKind = kind;
|
|
394
|
-
// Opening brace may be on this line or a subsequent line — track from current depth
|
|
395
|
-
containerBraceDepth = braceDepth - (raw.includes('{') ? 1 : 0);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
matched = true;
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return result;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
546
|
/**
|
|
407
547
|
* Build the flat list of rows to render, respecting collapse state.
|
|
408
548
|
* Container symbols become header rows; their children are indented below.
|