@pellux/goodvibes-agent 0.1.59 → 0.1.60

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.
@@ -1,176 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { createEmptyLine } from '../types/grid.ts';
3
- import { BasePanel } from './base-panel.ts';
4
- import type { UiIntelligenceSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
5
- import {
6
- buildEmptyState,
7
- buildGuidanceLine,
8
- buildKeyValueLine,
9
- buildPanelLine,
10
- buildPanelWorkspace,
11
- DEFAULT_PANEL_PALETTE,
12
- type PanelWorkspaceSection,
13
- } from './polish.ts';
14
-
15
- const C = {
16
- ...DEFAULT_PANEL_PALETTE,
17
- good: '#22c55e',
18
- warn: '#f59e0b',
19
- bad: '#ef4444',
20
- info: '#38bdf8',
21
- headerBg: '#1e293b',
22
- } as const;
23
-
24
- function statusColor(status: string): string {
25
- switch (status) {
26
- case 'ready':
27
- return C.good;
28
- case 'loading':
29
- return C.info;
30
- case 'degraded':
31
- return C.warn;
32
- case 'unavailable':
33
- default:
34
- return C.bad;
35
- }
36
- }
37
-
38
- export class IntelligencePanel extends BasePanel {
39
- private readonly unsub: (() => void) | null;
40
-
41
- public constructor(private readonly readModel?: UiReadModel<UiIntelligenceSnapshot>) {
42
- super('intelligence', 'Intelligence', 'J', 'development');
43
- this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
44
- }
45
-
46
- public override onDestroy(): void {
47
- this.unsub?.();
48
- }
49
-
50
- public render(width: number, height: number): Line[] {
51
- this.needsRender = false;
52
- if (!this.readModel) {
53
- const lines = buildPanelWorkspace(width, height, {
54
- title: 'Intelligence Control Room',
55
- intro: 'Workspace intelligence posture across diagnostics, symbols, completions, and hover readiness.',
56
- sections: [{
57
- lines: buildEmptyState(
58
- width,
59
- ' Intelligence runtime store unavailable.',
60
- 'This surface needs the live runtime store so it can show diagnostics, symbol readiness, and recovery guidance.',
61
- [{ command: '/intelligence review', summary: 'review intelligence readiness from the command surface' }],
62
- C,
63
- ),
64
- }],
65
- palette: C,
66
- });
67
- while (lines.length < height) lines.push(createEmptyLine(width));
68
- return lines.slice(0, height);
69
- }
70
-
71
- const state = this.readModel.getSnapshot();
72
- const degraded = [
73
- state.diagnosticsStatus,
74
- state.completionsStatus,
75
- state.symbolSearchStatus,
76
- state.hoverStatus,
77
- ].filter((status) => status !== 'ready').length;
78
-
79
- const sections: PanelWorkspaceSection[] = [
80
- {
81
- title: 'Intelligence posture',
82
- lines: [
83
- buildKeyValueLine(width, [
84
- { label: 'diagnostics', value: state.diagnosticsStatus, valueColor: statusColor(state.diagnosticsStatus) },
85
- { label: 'symbols', value: state.symbolSearchStatus, valueColor: statusColor(state.symbolSearchStatus) },
86
- { label: 'completions', value: state.completionsStatus, valueColor: statusColor(state.completionsStatus) },
87
- { label: 'hover', value: state.hoverStatus, valueColor: statusColor(state.hoverStatus) },
88
- ], C),
89
- buildKeyValueLine(width, [
90
- { label: 'errors', value: String(state.errorCount), valueColor: state.errorCount > 0 ? C.bad : C.dim },
91
- { label: 'warnings', value: String(state.warningCount), valueColor: state.warningCount > 0 ? C.warn : C.dim },
92
- { label: 'requests', value: String(state.totalRequests), valueColor: C.value },
93
- { label: 'avg latency', value: `${Math.round(state.avgLatencyMs)}ms`, valueColor: C.info },
94
- ], C),
95
- ],
96
- },
97
- {
98
- title: 'Next Actions',
99
- lines: [
100
- buildGuidanceLine(width, '/intelligence diagnostics', 'review readiness posture and current diagnostics activity', C),
101
- buildGuidanceLine(width, '/intelligence repair', 'surface repair-oriented guidance when symbols, hover, or completions degrade', C),
102
- ],
103
- },
104
- {
105
- title: 'Readiness',
106
- lines: [
107
- buildPanelLine(width, [[` Diagnostics are ${state.diagnosticsStatus}. Symbol search is ${state.symbolSearchStatus}.`, state.diagnosticsStatus === 'ready' && state.symbolSearchStatus === 'ready' ? C.dim : C.warn]]),
108
- buildPanelLine(width, [[` Hover is ${state.hoverStatus}. Completions are ${state.completionsStatus}.`, state.hoverStatus === 'ready' && state.completionsStatus === 'ready' ? C.dim : C.warn]]),
109
- ...(state.hover.active && state.hover.filePath
110
- ? [buildPanelLine(width, [[` Active hover: ${state.hover.filePath}`, C.info]])]
111
- : []),
112
- ],
113
- },
114
- ];
115
-
116
- const diagnosticFiles = [...state.diagnostics.entries()]
117
- .map(([filePath, diagnostics]) => ({
118
- filePath,
119
- errors: diagnostics.filter((entry) => entry.severity === 'error').length,
120
- warnings: diagnostics.filter((entry) => entry.severity === 'warning').length,
121
- }))
122
- .sort((a, b) => (b.errors - a.errors) || (b.warnings - a.warnings) || a.filePath.localeCompare(b.filePath))
123
- .slice(0, 4);
124
- sections.push({
125
- title: 'Diagnostics',
126
- lines: diagnosticFiles.length > 0
127
- ? diagnosticFiles.map((entry) => buildPanelLine(width, [[
128
- ` ${entry.filePath} errors=${entry.errors} warnings=${entry.warnings}`,
129
- entry.errors > 0 ? C.bad : entry.warnings > 0 ? C.warn : C.dim,
130
- ]]))
131
- : [buildPanelLine(width, [[' No tracked diagnostics yet.', C.dim]])],
132
- });
133
-
134
- sections.push({
135
- title: 'Workflows',
136
- lines: [
137
- buildGuidanceLine(width, '/intelligence symbols <file>', 'inspect document symbols for a file and verify symbol-surface readiness', C),
138
- buildGuidanceLine(width, '/intelligence outline <file>', 'review structural outline extraction without leaving the control room', C),
139
- buildGuidanceLine(width, '/intelligence definition <file> <line> <column>', 'check definition lookup for an exact source position', C),
140
- buildGuidanceLine(width, '/intelligence references <file> <line> <column>', 'review reference lookup for a symbol under the cursor', C),
141
- buildGuidanceLine(width, '/intelligence hover <file> <line> <column>', 'inspect hover/details posture for a specific source position', C),
142
- ],
143
- });
144
-
145
- if (degraded > 0) {
146
- sections.push({
147
- title: 'Recovery',
148
- lines: [
149
- buildPanelLine(width, [[' Workspace intelligence is not fully ready. Review LSP/tree-sitter setup and workspace language configuration.', C.warn]]),
150
- buildGuidanceLine(width, '/health review', 'check setup and readiness failures that could block diagnostics and symbol search', C),
151
- buildGuidanceLine(width, '/setup review', 'review startup and environment posture for intelligence dependencies', C),
152
- buildGuidanceLine(width, '/intelligence repair', 'show repair-oriented commands for diagnostics, symbols, hover, and completions', C),
153
- buildGuidanceLine(width, '/health repair intelligence', 'show repair commands plus post-repair verification for the intelligence domain', C),
154
- ],
155
- });
156
- } else {
157
- sections.push({
158
- title: 'Recovery',
159
- lines: [
160
- buildPanelLine(width, [[' Intelligence surfaces are healthy and ready for code-aware workflows.', C.dim]]),
161
- buildGuidanceLine(width, '/health intelligence', 'verify readiness posture after setup changes or dependency recovery', C),
162
- ],
163
- });
164
- }
165
-
166
- const lines = buildPanelWorkspace(width, height, {
167
- title: 'Intelligence Control Room',
168
- intro: 'Workspace intelligence posture across diagnostics, symbol search, hover, and completion readiness.',
169
- sections,
170
- footerLines: [buildPanelLine(width, [[' /symbols /intelligence diagnostics /intelligence symbols <file> /intelligence definition <file> <line> <column> ', C.dim]])],
171
- palette: C,
172
- });
173
- while (lines.length < height) lines.push(createEmptyLine(width));
174
- return lines.slice(0, height);
175
- }
176
- }
@@ -1,369 +0,0 @@
1
- /**
2
- * semantic-diff.ts — Functional change summary after file edits.
3
- *
4
- * Compares AST before/after an edit and extracts:
5
- * - Added / removed / modified functions, methods, classes, etc.
6
- * - Changed import specifiers
7
- * - New / removed exports
8
- *
9
- * Returns a compact SemanticDiff that can be rendered alongside a regular diff.
10
- * Gracefully returns null when tree-sitter is unavailable or the language is
11
- * unsupported — callers should treat null as "no semantic info available".
12
- */
13
-
14
- import { TreeSitterService } from '@pellux/goodvibes-sdk/platform/intelligence';
15
- import { extractSymbols } from '@pellux/goodvibes-sdk/platform/intelligence';
16
- import type { SymbolInfo } from '@pellux/goodvibes-sdk/platform/intelligence';
17
- import { detectLanguage } from '@pellux/goodvibes-sdk/platform/intelligence';
18
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
19
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
20
-
21
- // ---------------------------------------------------------------------------
22
- // Public types
23
- // ---------------------------------------------------------------------------
24
-
25
- export type ChangeKind = 'added' | 'removed' | 'modified';
26
-
27
- export interface SymbolChange {
28
- kind: ChangeKind;
29
- symbolKind: SymbolInfo['kind'];
30
- name: string;
31
- /** Present for 'modified' — the old signature. */
32
- oldSignature?: string;
33
- /** Present for 'added' or 'modified' — the new signature. */
34
- newSignature?: string;
35
- }
36
-
37
- export interface ImportChange {
38
- kind: ChangeKind;
39
- specifier: string; // the module path, e.g. './utils'
40
- /** Specific named imports that changed, if determinable. */
41
- names?: string[];
42
- }
43
-
44
- export interface SemanticDiff {
45
- symbols: SymbolChange[];
46
- imports: ImportChange[];
47
- /** Convenience: total number of changes across all categories. */
48
- totalChanges: number;
49
- }
50
-
51
- // ---------------------------------------------------------------------------
52
- // Import extraction (regex-based — fast, no grammar needed)
53
- // ---------------------------------------------------------------------------
54
-
55
- interface ParsedImport {
56
- specifier: string;
57
- names: string[];
58
- }
59
-
60
- /**
61
- * Extract import specifiers and named imports from source text using a regex
62
- * scan. Covers `import ... from '...'` and `import('...')` dynamic imports.
63
- * Does not require tree-sitter.
64
- */
65
- function extractImports(source: string): Map<string, ParsedImport> {
66
- const map = new Map<string, ParsedImport>();
67
-
68
- // Static imports: import { A, B } from './foo'
69
- // import Foo from './foo'
70
- // import * as Foo from './foo'
71
- // import './side-effect'
72
- const staticRe = /import\s+(?:([\s\S]*?)\s+from\s+)?['"]([^'"]+)['"]/g;
73
- let m: RegExpExecArray | null;
74
-
75
- while ((m = staticRe.exec(source)) !== null) {
76
- const clause = (m[1] ?? '').trim();
77
- const specifier = m[2]!;
78
- const names = parseNamedImports(clause);
79
- const existing = map.get(specifier);
80
- if (existing) {
81
- for (const n of names) existing.names.push(n);
82
- } else {
83
- map.set(specifier, { specifier, names });
84
- }
85
- }
86
-
87
- return map;
88
- }
89
-
90
- /** Parse `{ A, B as C }` or `* as Ns` or `Default` clauses into name strings. */
91
- function parseNamedImports(clause: string): string[] {
92
- if (!clause) return [];
93
- // Namespace import: * as Foo
94
- if (clause.startsWith('*')) return [clause];
95
- // Named imports: { A, B as C }
96
- const braceMatch = clause.match(/\{([^}]*)\}/);
97
- if (braceMatch) {
98
- return braceMatch[1]!
99
- .split(',')
100
- .map(s => s.trim())
101
- .filter(Boolean);
102
- }
103
- // Default import: Foo
104
- return clause ? [clause] : [];
105
- }
106
-
107
- // ---------------------------------------------------------------------------
108
- // Symbol comparison
109
- // ---------------------------------------------------------------------------
110
-
111
- function diffSymbols(
112
- before: SymbolInfo[],
113
- after: SymbolInfo[],
114
- ): SymbolChange[] {
115
- const changes: SymbolChange[] = [];
116
-
117
- // Key by qualified name (container.name or name)
118
- const key = (s: SymbolInfo): string =>
119
- s.container ? `${s.container}.${s.name}` : s.name;
120
-
121
- const beforeMap = new Map<string, SymbolInfo>();
122
- const afterMap = new Map<string, SymbolInfo>();
123
-
124
- for (const s of before) beforeMap.set(key(s), s);
125
- for (const s of after) afterMap.set(key(s), s);
126
-
127
- // Removed or modified
128
- for (const [k, bSym] of beforeMap) {
129
- const aSym = afterMap.get(k);
130
- if (!aSym) {
131
- changes.push({
132
- kind: 'removed',
133
- symbolKind: bSym.kind,
134
- name: k,
135
- oldSignature: bSym.signature,
136
- });
137
- } else if (
138
- bSym.signature !== aSym.signature ||
139
- bSym.exported !== aSym.exported
140
- ) {
141
- changes.push({
142
- kind: 'modified',
143
- symbolKind: aSym.kind,
144
- name: k,
145
- oldSignature: bSym.signature,
146
- newSignature: aSym.signature,
147
- });
148
- }
149
- }
150
-
151
- // Added
152
- for (const [k, aSym] of afterMap) {
153
- if (!beforeMap.has(k)) {
154
- changes.push({
155
- kind: 'added',
156
- symbolKind: aSym.kind,
157
- name: k,
158
- newSignature: aSym.signature,
159
- });
160
- }
161
- }
162
-
163
- return changes;
164
- }
165
-
166
- // ---------------------------------------------------------------------------
167
- // Import comparison
168
- // ---------------------------------------------------------------------------
169
-
170
- function diffImports(
171
- before: Map<string, ParsedImport>,
172
- after: Map<string, ParsedImport>,
173
- ): ImportChange[] {
174
- const changes: ImportChange[] = [];
175
-
176
- // Removed or modified
177
- for (const [spec, bImp] of before) {
178
- const aImp = after.get(spec);
179
- if (!aImp) {
180
- changes.push({ kind: 'removed', specifier: spec, names: bImp.names });
181
- } else {
182
- // Check if named imports changed
183
- const bSet = new Set(bImp.names);
184
- const aSet = new Set(aImp.names);
185
- const added = aImp.names.filter(n => !bSet.has(n));
186
- const removed = bImp.names.filter(n => !aSet.has(n));
187
- if (added.length > 0 || removed.length > 0) {
188
- const names: string[] = [
189
- ...added.map(n => `+${n}`),
190
- ...removed.map(n => `-${n}`),
191
- ];
192
- changes.push({ kind: 'modified', specifier: spec, names });
193
- }
194
- }
195
- }
196
-
197
- // Added
198
- for (const [spec, aImp] of after) {
199
- if (!before.has(spec)) {
200
- changes.push({ kind: 'added', specifier: spec, names: aImp.names });
201
- }
202
- }
203
-
204
- return changes;
205
- }
206
-
207
- // ---------------------------------------------------------------------------
208
- // Public API
209
- // ---------------------------------------------------------------------------
210
-
211
- /**
212
- * Compare `beforeContent` and `afterContent` for the given `filePath` and
213
- * return a SemanticDiff describing functional changes.
214
- *
215
- * Returns null when:
216
- * - The language is not supported by tree-sitter
217
- * - Tree-sitter is not initialized
218
- * - Parsing fails
219
- *
220
- * Import changes are always returned (regex-based, no grammar required).
221
- */
222
- export async function computeSemanticDiff(
223
- filePath: string,
224
- beforeContent: string,
225
- afterContent: string,
226
- ): Promise<SemanticDiff | null> {
227
- // Import diff is always available (regex-based)
228
- const beforeImports = extractImports(beforeContent);
229
- const afterImports = extractImports(afterContent);
230
- const imports = diffImports(beforeImports, afterImports);
231
-
232
- const langId = detectLanguage(filePath);
233
- if (!langId) {
234
- // No language support — return import changes only if any
235
- if (imports.length === 0) return null;
236
- return { symbols: [], imports, totalChanges: imports.length };
237
- }
238
-
239
- const svc = new TreeSitterService();
240
-
241
- try {
242
- await svc.initialize();
243
- } catch {
244
- logger.debug('computeSemanticDiff: tree-sitter init failed', { filePath });
245
- if (imports.length === 0) return null;
246
- return { symbols: [], imports, totalChanges: imports.length };
247
- }
248
-
249
- // Use a temporary key so we don't pollute the service cache with diff content.
250
- // Include a nonce so concurrent calls for the same filePath don't collide.
251
- const nonce = Math.random().toString(36).slice(2);
252
- const beforeKey = `__semantic_diff_before__${nonce}_${filePath}`;
253
- const afterKey = `__semantic_diff_after__${nonce}_${filePath}`;
254
-
255
- try {
256
- const [beforeTree, afterTree] = await Promise.all([
257
- svc.parse(beforeKey, beforeContent, langId),
258
- svc.parse(afterKey, afterContent, langId),
259
- ]);
260
-
261
- if (!beforeTree || !afterTree) {
262
- if (imports.length === 0) return null;
263
- return { symbols: [], imports, totalChanges: imports.length };
264
- }
265
-
266
- // loadLanguage returns from cache since parse() already loaded it
267
- const lang = await svc.loadLanguage(langId);
268
- if (!lang) {
269
- if (imports.length === 0) return null;
270
- return { symbols: [], imports, totalChanges: imports.length };
271
- }
272
-
273
- const beforeSymbols = extractSymbols(beforeTree, lang, langId);
274
- const afterSymbols = extractSymbols(afterTree, lang, langId);
275
- const symbols = diffSymbols(beforeSymbols, afterSymbols);
276
-
277
- // Clean up temporary cache entries
278
- svc.invalidate(beforeKey);
279
- svc.invalidate(afterKey);
280
-
281
- const totalChanges = symbols.length + imports.length;
282
- if (totalChanges === 0) return null;
283
-
284
- return { symbols, imports, totalChanges };
285
- } catch (err) {
286
- logger.error('computeSemanticDiff: comparison failed', {
287
- filePath,
288
- error: summarizeError(err),
289
- });
290
- svc.invalidate(beforeKey);
291
- svc.invalidate(afterKey);
292
- if (imports.length === 0) return null;
293
- return { symbols: [], imports, totalChanges: imports.length };
294
- }
295
- }
296
-
297
- // ---------------------------------------------------------------------------
298
- // Display formatting
299
- // ---------------------------------------------------------------------------
300
-
301
- const KIND_ICON: Record<SymbolInfo['kind'], string> = {
302
- function: 'fn',
303
- method: 'method',
304
- class: 'class',
305
- interface: 'iface',
306
- type: 'type',
307
- variable: 'var',
308
- constant: 'const',
309
- enum: 'enum',
310
- property: 'prop',
311
- namespace: 'ns',
312
- };
313
-
314
- const CHANGE_GLYPH: Record<ChangeKind, string> = {
315
- added: '+',
316
- removed: '-',
317
- modified: '~',
318
- };
319
-
320
- /**
321
- * Render a SemanticDiff as an array of compact summary strings, one change
322
- * per line. Suitable for display in the diff panel status area or a tooltip.
323
- *
324
- * Format examples:
325
- * + fn renderDiffView
326
- * ~ method renderSummary (signature changed)
327
- * - class OldWidget
328
- * + import ./utils { A, B }
329
- * ~ import ./types (+NewType, -OldType)
330
- */
331
- export function formatSemanticDiff(diff: SemanticDiff): string[] {
332
- const lines: string[] = [];
333
-
334
- // Symbol changes first
335
- for (const sc of diff.symbols) {
336
- const glyph = CHANGE_GLYPH[sc.kind];
337
- const icon = KIND_ICON[sc.symbolKind];
338
- let line = `${glyph} ${icon} ${sc.name}`;
339
- if (sc.kind === 'modified') {
340
- line += ' (signature changed)';
341
- }
342
- lines.push(line);
343
- }
344
-
345
- // Import changes
346
- for (const ic of diff.imports) {
347
- const glyph = CHANGE_GLYPH[ic.kind];
348
- let line = `${glyph} import ${ic.specifier}`;
349
- if (ic.names && ic.names.length > 0) {
350
- const nameList = ic.names.slice(0, 4).join(', ');
351
- const overflow = ic.names.length > 4 ? ` +${ic.names.length - 4} more` : '';
352
- line += ` { ${nameList}${overflow} }`;
353
- }
354
- lines.push(line);
355
- }
356
-
357
- return lines;
358
- }
359
-
360
- /**
361
- * Render a one-line summary suitable for a status bar.
362
- * Example: "3 changes: +fn renderX ~method Foo.bar -import ./old"
363
- */
364
- export function formatSemanticDiffSummary(diff: SemanticDiff): string {
365
- if (diff.totalChanges === 0) return '';
366
- const parts = formatSemanticDiff(diff).slice(0, 3);
367
- const overflow = diff.totalChanges > 3 ? ` +${diff.totalChanges - 3} more` : '';
368
- return `${diff.totalChanges} semantic change${diff.totalChanges !== 1 ? 's' : ''}: ${parts.join(' ')}${overflow}`;
369
- }