@pellux/goodvibes-tui 0.27.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- 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 +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- 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 +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- 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 +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- 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/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- 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/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- 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 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,15 +1,18 @@
|
|
|
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,
|
|
5
|
+
buildKeyboardHints,
|
|
6
6
|
buildPanelLine,
|
|
7
|
-
|
|
7
|
+
buildTreeRow,
|
|
8
8
|
buildPanelWorkspace,
|
|
9
9
|
resolveScrollablePanelSection,
|
|
10
10
|
DEFAULT_PANEL_PALETTE,
|
|
11
|
+
extendPalette,
|
|
11
12
|
} from './polish.ts';
|
|
12
|
-
import {
|
|
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';
|
|
13
16
|
|
|
14
17
|
// ── Symbol types ────────────────────────────────────────────────────────────
|
|
15
18
|
|
|
@@ -25,6 +28,10 @@ export interface SymbolEntry {
|
|
|
25
28
|
|
|
26
29
|
// ── Rendering constants ──────────────────────────────────────────────────────
|
|
27
30
|
|
|
31
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
32
|
+
selectedBg: '236',
|
|
33
|
+
});
|
|
34
|
+
|
|
28
35
|
/** ANSI 256-color fg codes per symbol kind. */
|
|
29
36
|
const KIND_COLORS: Record<SymbolKind, string> = {
|
|
30
37
|
function: '87', // cyan
|
|
@@ -36,43 +43,209 @@ const KIND_COLORS: Record<SymbolKind, string> = {
|
|
|
36
43
|
const: '245', // grey
|
|
37
44
|
};
|
|
38
45
|
|
|
39
|
-
/**
|
|
40
|
-
const
|
|
41
|
-
function: '
|
|
42
|
-
method: '
|
|
43
|
-
class: '
|
|
44
|
-
namespace: '
|
|
45
|
-
interface: '
|
|
46
|
-
type: '
|
|
47
|
-
const: '
|
|
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',
|
|
48
55
|
};
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
240
|
|
|
70
241
|
// ── Panel ────────────────────────────────────────────────────────────────────
|
|
71
242
|
|
|
72
243
|
/**
|
|
73
244
|
* SymbolOutlinePanel — renders a hierarchical symbol outline of the current
|
|
74
|
-
* file. Symbols are parsed from
|
|
75
|
-
*
|
|
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.
|
|
76
249
|
*/
|
|
77
250
|
export class SymbolOutlinePanel extends BasePanel {
|
|
78
251
|
/** Flat list of parsed symbols (methods nested after their parent class). */
|
|
@@ -81,6 +254,17 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
81
254
|
/** Index of the currently highlighted row in the visible flat list. */
|
|
82
255
|
private selectedIndex: number = 0;
|
|
83
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
|
+
|
|
84
268
|
/** Set of container names (class/namespace) that are collapsed. */
|
|
85
269
|
private collapsed: Set<string> = new Set();
|
|
86
270
|
|
|
@@ -90,23 +274,40 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
90
274
|
/** Path of the file currently loaded. */
|
|
91
275
|
private currentPath: string = '';
|
|
92
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
|
+
|
|
93
283
|
constructor() {
|
|
94
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
|
+
});
|
|
95
288
|
}
|
|
96
289
|
|
|
97
290
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
98
291
|
|
|
99
292
|
/**
|
|
100
|
-
* Load and parse symbols from the given file source text.
|
|
101
|
-
* 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.
|
|
102
297
|
*/
|
|
103
298
|
loadFile(path: string, source: string): void {
|
|
104
299
|
this.currentPath = path;
|
|
105
|
-
this.symbols =
|
|
300
|
+
this.symbols = [];
|
|
106
301
|
this.selectedIndex = 0;
|
|
107
302
|
this.scrollOffset = 0;
|
|
108
303
|
this.collapsed.clear();
|
|
109
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);
|
|
110
311
|
}
|
|
111
312
|
|
|
112
313
|
/**
|
|
@@ -114,8 +315,7 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
114
315
|
* caller can jump to it in the file-preview panel.
|
|
115
316
|
*/
|
|
116
317
|
getSelectedLocation(): { path: string; line: number } | null {
|
|
117
|
-
const
|
|
118
|
-
const row = visible[this.selectedIndex];
|
|
318
|
+
const row = this.selectedRow();
|
|
119
319
|
if (!row || row.kind === 'header') return null;
|
|
120
320
|
return { path: this.currentPath, line: row.symbol.line };
|
|
121
321
|
}
|
|
@@ -144,12 +344,13 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
144
344
|
}
|
|
145
345
|
|
|
146
346
|
if (key === 'return' || key === 'enter') {
|
|
147
|
-
//
|
|
148
|
-
|
|
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;
|
|
149
350
|
}
|
|
150
351
|
|
|
151
352
|
if (key === 'space' || key === 'right' || key === 'left') {
|
|
152
|
-
const row =
|
|
353
|
+
const row = this.selectedRow();
|
|
153
354
|
if (row?.kind === 'header') {
|
|
154
355
|
const name = row.name;
|
|
155
356
|
if (this.collapsed.has(name)) {
|
|
@@ -186,10 +387,12 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
186
387
|
width,
|
|
187
388
|
this.currentPath ? ' No symbols found' : ' No file loaded',
|
|
188
389
|
this.currentPath
|
|
189
|
-
? 'The current file did not produce outline entries
|
|
390
|
+
? 'The current file did not produce outline entries from the tree-sitter parse.'
|
|
190
391
|
: 'Load a file in the preview panel to populate its outline here.',
|
|
191
|
-
|
|
192
|
-
|
|
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,
|
|
193
396
|
),
|
|
194
397
|
},
|
|
195
398
|
],
|
|
@@ -201,7 +404,7 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
201
404
|
const outlineSection = resolveScrollablePanelSection(width, height, {
|
|
202
405
|
intro: this.currentPath ? this.currentPath : 'Outline the current file into navigable symbols and lightweight parent/child structure.',
|
|
203
406
|
footerLines: [
|
|
204
|
-
|
|
407
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Space/←/→', label: 'collapse' }, { keys: 'Enter', label: 'jump to source →' }], DEFAULT_PANEL_PALETTE),
|
|
205
408
|
],
|
|
206
409
|
palette: DEFAULT_PANEL_PALETTE,
|
|
207
410
|
beforeSections: [
|
|
@@ -234,7 +437,7 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
234
437
|
{
|
|
235
438
|
title: 'Selected',
|
|
236
439
|
lines: (() => {
|
|
237
|
-
const selected =
|
|
440
|
+
const selected = this.selectedRow();
|
|
238
441
|
return selected
|
|
239
442
|
? [
|
|
240
443
|
buildPanelLine(width, [
|
|
@@ -255,7 +458,7 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
255
458
|
});
|
|
256
459
|
this.scrollOffset = outlineSection.scrollOffset;
|
|
257
460
|
|
|
258
|
-
const selected =
|
|
461
|
+
const selected = this.selectedRow();
|
|
259
462
|
return buildPanelWorkspace(width, height, {
|
|
260
463
|
title: ' Symbols',
|
|
261
464
|
intro: this.currentPath ? this.currentPath : 'Outline the current file into navigable symbols and lightweight parent/child structure.',
|
|
@@ -291,7 +494,7 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
291
494
|
},
|
|
292
495
|
],
|
|
293
496
|
footerLines: [
|
|
294
|
-
|
|
497
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Space/←/→', label: 'collapse' }, { keys: 'Enter', label: 'jump to source →' }], DEFAULT_PANEL_PALETTE),
|
|
295
498
|
],
|
|
296
499
|
palette: DEFAULT_PANEL_PALETTE,
|
|
297
500
|
});
|
|
@@ -299,6 +502,24 @@ export class SymbolOutlinePanel extends BasePanel {
|
|
|
299
502
|
|
|
300
503
|
// ── Private helpers ────────────────────────────────────────────────────────
|
|
301
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
|
+
|
|
302
523
|
private _clampScroll(totalRows: number): void {
|
|
303
524
|
// Ensure selected is within scroll view (assumes last known height ~ 20)
|
|
304
525
|
// We keep a conservative viewport window; render() uses this.scrollOffset.
|
|
@@ -322,80 +543,6 @@ type VisibleRow =
|
|
|
322
543
|
|
|
323
544
|
// ── Pure helpers ─────────────────────────────────────────────────────────────
|
|
324
545
|
|
|
325
|
-
/**
|
|
326
|
-
* Parse symbols from source text. Returns a flat list ordered by line number.
|
|
327
|
-
* Methods are tagged with their parent class name so the renderer can group them.
|
|
328
|
-
*/
|
|
329
|
-
function parseSymbols(source: string): SymbolEntry[] {
|
|
330
|
-
const lines = source.split('\n');
|
|
331
|
-
const result: SymbolEntry[] = [];
|
|
332
|
-
let currentContainer: string | undefined;
|
|
333
|
-
let containerKind: SymbolKind | undefined;
|
|
334
|
-
let containerBraceDepth = 0;
|
|
335
|
-
let braceDepth = 0;
|
|
336
|
-
|
|
337
|
-
for (let i = 0; i < lines.length; i++) {
|
|
338
|
-
const raw = lines[i];
|
|
339
|
-
const trimmed = raw.trimStart();
|
|
340
|
-
const lineNo = i + 1; // 1-based
|
|
341
|
-
|
|
342
|
-
// Strip string literals before counting braces to avoid false positives
|
|
343
|
-
const rawNoBraceStrings = raw.replace(/("|\'|\`)(?:(?!\1|\\).|\\[\s\S])*\1/g, '');
|
|
344
|
-
|
|
345
|
-
// Track brace depth for container scoping
|
|
346
|
-
for (const ch of rawNoBraceStrings) {
|
|
347
|
-
if (ch === '{') braceDepth++;
|
|
348
|
-
else if (ch === '}') {
|
|
349
|
-
braceDepth--;
|
|
350
|
-
if (currentContainer !== undefined && braceDepth <= containerBraceDepth) {
|
|
351
|
-
currentContainer = undefined;
|
|
352
|
-
containerKind = undefined;
|
|
353
|
-
containerBraceDepth = 0;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Skip comment lines and blank lines
|
|
359
|
-
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*') || trimmed === '') continue;
|
|
360
|
-
|
|
361
|
-
// Check container-level patterns first (class/namespace)
|
|
362
|
-
let matched = false;
|
|
363
|
-
for (const { re, kind, isContainer } of SYMBOL_PATTERNS) {
|
|
364
|
-
const m = trimmed.match(re);
|
|
365
|
-
if (!m) continue;
|
|
366
|
-
const name = m[1];
|
|
367
|
-
if (!name) continue;
|
|
368
|
-
|
|
369
|
-
const entry: SymbolEntry = { kind, name, line: lineNo };
|
|
370
|
-
|
|
371
|
-
if (currentContainer && kind === 'method') {
|
|
372
|
-
entry.parentName = currentContainer;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Don't add methods as top-level if they're inside a container
|
|
376
|
-
if (kind === 'method' && !currentContainer) {
|
|
377
|
-
// standalone function-like at wrong indent — skip
|
|
378
|
-
matched = true;
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
result.push(entry);
|
|
383
|
-
|
|
384
|
-
if (isContainer) {
|
|
385
|
-
currentContainer = name;
|
|
386
|
-
containerKind = kind;
|
|
387
|
-
// Opening brace may be on this line or a subsequent line — track from current depth
|
|
388
|
-
containerBraceDepth = braceDepth - (raw.includes('{') ? 1 : 0);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
matched = true;
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return result;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
546
|
/**
|
|
400
547
|
* Build the flat list of rows to render, respecting collapse state.
|
|
401
548
|
* Container symbols become header rows; their children are indented below.
|
|
@@ -434,10 +581,7 @@ function buildVisibleRows(symbols: SymbolEntry[], collapsed: Set<string>): Visib
|
|
|
434
581
|
return rows;
|
|
435
582
|
}
|
|
436
583
|
|
|
437
|
-
/**
|
|
438
|
-
* Write a string into a Line starting at column x, applying fg/bg/style.
|
|
439
|
-
*/
|
|
440
|
-
/** Render a container header row (class / namespace). */
|
|
584
|
+
/** Render a container header row (class / namespace) via the shared tree row. */
|
|
441
585
|
function _renderHeader(
|
|
442
586
|
width: number,
|
|
443
587
|
row: Extract<VisibleRow, { kind: 'header' }>,
|
|
@@ -445,24 +589,19 @@ function _renderHeader(
|
|
|
445
589
|
bgColor: string,
|
|
446
590
|
collapsed: Set<string>,
|
|
447
591
|
): Line {
|
|
448
|
-
// Collapse indicator
|
|
449
592
|
const isCollapsed = collapsed.has(row.name);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
{ text:
|
|
458
|
-
|
|
459
|
-
{ text: row.name, fg: isSelected ? '255' : '252', bold: isSelected },
|
|
460
|
-
{ text: ' '.repeat(gap), fg: DEFAULT_PANEL_PALETTE.dim },
|
|
461
|
-
{ text: lineNumStr, fg: DEFAULT_PANEL_PALETTE.dim },
|
|
462
|
-
], { selected: isSelected, selectedBg: bgColor, fillFg: isSelected ? '255' : '' });
|
|
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 });
|
|
463
602
|
}
|
|
464
603
|
|
|
465
|
-
/** Render a regular symbol row. */
|
|
604
|
+
/** Render a regular symbol row via the shared tree row. */
|
|
466
605
|
function _renderSymbol(
|
|
467
606
|
width: number,
|
|
468
607
|
row: Extract<VisibleRow, { kind: 'symbol' }>,
|
|
@@ -470,17 +609,11 @@ function _renderSymbol(
|
|
|
470
609
|
bgColor: string,
|
|
471
610
|
): Line {
|
|
472
611
|
const { symbol, depth } = row;
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
{ text: kindLabel, fg: KIND_COLORS[symbol.kind] },
|
|
481
|
-
{ text: ' ', fg: isSelected ? '255' : '251' },
|
|
482
|
-
{ text: symbol.name, fg: isSelected ? '255' : '251', bold: isSelected },
|
|
483
|
-
{ text: ' '.repeat(gap), fg: DEFAULT_PANEL_PALETTE.dim },
|
|
484
|
-
{ text: lineNumStr, fg: DEFAULT_PANEL_PALETTE.dim },
|
|
485
|
-
], { selected: isSelected, selectedBg: bgColor, fillFg: isSelected ? '255' : '' });
|
|
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 });
|
|
486
619
|
}
|