@pellux/goodvibes-agent 0.1.2 → 0.1.4

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 (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +12 -1
  4. package/docs/README.md +2 -0
  5. package/docs/getting-started.md +19 -1
  6. package/docs/release-and-publishing.md +3 -1
  7. package/package.json +10 -1
  8. package/src/agent/persona-registry.ts +379 -0
  9. package/src/agent/skill-registry.ts +360 -0
  10. package/src/audio/spoken-turn-model-routing.ts +2 -1
  11. package/src/cli/agent-knowledge-command.ts +46 -10
  12. package/src/cli/management-commands.ts +3 -1
  13. package/src/config/surface.ts +1 -0
  14. package/src/input/agent-workspace.ts +32 -2
  15. package/src/input/command-registry.ts +4 -1
  16. package/src/input/commands/agent-skills-runtime.ts +216 -0
  17. package/src/input/commands/knowledge.ts +18 -18
  18. package/src/input/commands/personas-runtime.ts +219 -0
  19. package/src/input/commands/skills-runtime.ts +7 -2
  20. package/src/input/commands.ts +4 -0
  21. package/src/input/panel-integration-actions.ts +0 -52
  22. package/src/main.ts +2 -1
  23. package/src/panels/builtin/session.ts +4 -3
  24. package/src/panels/index.ts +0 -5
  25. package/src/panels/orchestration-panel.ts +4 -5
  26. package/src/panels/qr-panel.ts +3 -2
  27. package/src/panels/tasks-panel.ts +4 -4
  28. package/src/renderer/agent-workspace.ts +2 -0
  29. package/src/runtime/bootstrap-command-context.ts +3 -0
  30. package/src/runtime/bootstrap-command-parts.ts +6 -2
  31. package/src/runtime/bootstrap-core.ts +9 -5
  32. package/src/runtime/bootstrap-shell.ts +3 -1
  33. package/src/runtime/bootstrap.ts +10 -2
  34. package/src/runtime/cloudflare-control-plane.ts +2 -1
  35. package/src/runtime/services.ts +3 -3
  36. package/src/version.ts +1 -1
  37. package/src/daemon/cli.ts +0 -55
  38. package/src/daemon/safe-serve.ts +0 -61
  39. package/src/panels/diff-panel.ts +0 -520
  40. package/src/panels/file-explorer-panel.ts +0 -584
  41. package/src/panels/file-preview-panel.ts +0 -434
  42. package/src/panels/git-panel.ts +0 -638
  43. package/src/panels/sandbox-panel.ts +0 -283
  44. package/src/panels/symbol-outline-panel.ts +0 -486
  45. package/src/panels/worktree-panel.ts +0 -182
  46. package/src/panels/wrfc-panel.ts +0 -609
@@ -1,520 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // DiffPanel — unified diff view of agent file changes
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { Line } from '../types/grid.ts';
6
- import { createStyledCell, createEmptyLine } from '../types/grid.ts';
7
- import { BasePanel } from './base-panel.ts';
8
- import {
9
- buildBodyText,
10
- buildEmptyState,
11
- buildPanelWorkspace,
12
- resolveScrollablePanelSection,
13
- buildStyledPanelLine,
14
- type PanelWorkspaceSection,
15
- DEFAULT_PANEL_PALETTE,
16
- } from './polish.ts';
17
-
18
- // ---------------------------------------------------------------------------
19
- // Colour palette
20
- // ---------------------------------------------------------------------------
21
-
22
- const COLOR = {
23
- addition: '#00ff88',
24
- deletion: '#ff4444',
25
- hunk: '#88aaff',
26
- header: '#aaaaaa',
27
- lineNum: '#555555',
28
- lineNumAdd: '#00aa55',
29
- lineNumDel: '#aa2222',
30
- filename: '#ffffff',
31
- tabActive: '#ffffff',
32
- tabInactive: '#666666',
33
- tabBg: '#222222',
34
- context: '#888888',
35
- statusBar: '#444444',
36
- } as const;
37
-
38
- // ---------------------------------------------------------------------------
39
- // Types
40
- // ---------------------------------------------------------------------------
41
-
42
- interface DiffEntry {
43
- filePath: string;
44
- raw: string; // raw unified diff text
45
- lines: ParsedLine[];
46
- /** One-line semantic summary from computeSemanticDiff, if available. */
47
- semanticSummary?: string;
48
- }
49
-
50
- type LineKind = 'addition' | 'deletion' | 'context' | 'hunk' | 'header';
51
-
52
- interface ParsedLine {
53
- kind: LineKind;
54
- text: string;
55
- beforeNum: number | null; // line number in original file
56
- afterNum: number | null; // line number in new file
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // Diff parser
61
- // ---------------------------------------------------------------------------
62
-
63
- function parseDiff(raw: string): ParsedLine[] {
64
- const result: ParsedLine[] = [];
65
- let before = 0;
66
- let after = 0;
67
-
68
- for (const line of raw.split('\n')) {
69
- // Hunk header: @@ -a,b +c,d @@
70
- const hunkMatch = line.match(/^@@\s+-([0-9]+)(?:,[0-9]+)?\s+\+([0-9]+)(?:,[0-9]+)?\s+@@/);
71
- if (hunkMatch) {
72
- before = parseInt(hunkMatch[1]!, 10);
73
- after = parseInt(hunkMatch[2]!, 10);
74
- result.push({ kind: 'hunk', text: line, beforeNum: null, afterNum: null });
75
- continue;
76
- }
77
-
78
- if (line.startsWith('+++') || line.startsWith('---') ||
79
- line.startsWith('diff ') || line.startsWith('index ') ||
80
- line.startsWith('new file') || line.startsWith('old file') ||
81
- line.startsWith('Binary')) {
82
- result.push({ kind: 'header', text: line, beforeNum: null, afterNum: null });
83
- continue;
84
- }
85
-
86
- if (line.startsWith('+')) {
87
- result.push({ kind: 'addition', text: line.slice(1), beforeNum: null, afterNum: after });
88
- after++;
89
- } else if (line.startsWith('-')) {
90
- result.push({ kind: 'deletion', text: line.slice(1), beforeNum: before, afterNum: null });
91
- before++;
92
- } else if (line.startsWith('\\')) {
93
- // "No newline at end of file" note — treat as header
94
- result.push({ kind: 'header', text: line, beforeNum: null, afterNum: null });
95
- } else {
96
- // context line (starts with space, or empty for blank context)
97
- const text = line.startsWith(' ') ? line.slice(1) : line;
98
- result.push({ kind: 'context', text, beforeNum: before, afterNum: after });
99
- before++;
100
- after++;
101
- }
102
- }
103
-
104
- return result;
105
- }
106
-
107
- // ---------------------------------------------------------------------------
108
- // Split a full `git diff` output into per-file entries
109
- // ---------------------------------------------------------------------------
110
-
111
- function splitIntoDiffEntries(raw: string): DiffEntry[] {
112
- const entries: DiffEntry[] = [];
113
- // Split on "diff --git" lines
114
- const chunks = raw.split(/(?=^diff --git )/m);
115
- for (const chunk of chunks) {
116
- const trimmed = chunk.trim();
117
- if (!trimmed) continue;
118
-
119
- // Extract file path from "diff --git a/foo b/foo"
120
- const match = trimmed.match(/^diff --git a\/.+? b\/(.+)$/m);
121
- const filePath = match ? match[1]! : 'unknown';
122
-
123
- entries.push({
124
- filePath,
125
- raw: chunk,
126
- lines: parseDiff(chunk),
127
- });
128
- }
129
- return entries;
130
- }
131
-
132
- // ---------------------------------------------------------------------------
133
- // Rendering helpers
134
- // ---------------------------------------------------------------------------
135
-
136
- function makeLine(
137
- width: number,
138
- leftNum: string,
139
- rightNum: string,
140
- content: string,
141
- fg: string,
142
- bg: string,
143
- numFg: string,
144
- bold: boolean = false,
145
- ): Line {
146
- // Left line number (5 chars + space)
147
- const LEFT_W = 5;
148
- const usedForNums = LEFT_W + 1 + LEFT_W + 1 + 2; // 14
149
- const contentWidth = Math.max(0, width - usedForNums);
150
- const truncated = content.length > contentWidth
151
- ? content.slice(0, contentWidth)
152
- : content;
153
- return buildStyledPanelLine(width, [
154
- { text: leftNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
155
- { text: ' ', fg: '', bg },
156
- { text: rightNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
157
- { text: ' ', fg: '', bg },
158
- { text: '| ', fg: COLOR.lineNum, bg },
159
- { text: truncated, fg, bg, bold },
160
- ]);
161
- }
162
-
163
- function renderText(width: number, text: string, fg: string, bg: string, bold = false): Line {
164
- const truncated = text.length > width ? text.slice(0, width) : text;
165
- return buildStyledPanelLine(width, [{ text: truncated, fg, bg, bold }]);
166
- }
167
-
168
- // ---------------------------------------------------------------------------
169
- // DiffPanel
170
- // ---------------------------------------------------------------------------
171
-
172
- export class DiffPanel extends BasePanel {
173
- public override isTransient = true;
174
-
175
- private readonly workingDirectory: string;
176
- private entries: DiffEntry[] = [];
177
- private selectedFile = 0;
178
- private scrollOffset = 0;
179
-
180
- constructor(workingDirectory: string) {
181
- super('diff', 'Diff', 'D', 'development');
182
- this.workingDirectory = workingDirectory;
183
- }
184
-
185
- // -------------------------------------------------------------------------
186
- // Public API
187
- // -------------------------------------------------------------------------
188
-
189
- /** Show a unified diff for a specific file. Adds or replaces the entry. */
190
- showDiff(filePath: string, diff: string): void {
191
- const idx = this.entries.findIndex(e => e.filePath === filePath);
192
- const entry: DiffEntry = { filePath, raw: diff, lines: parseDiff(diff), semanticSummary: this.entries[idx]?.semanticSummary };
193
- if (idx >= 0) {
194
- this.entries[idx] = entry;
195
- // Stay on this file if it was already selected
196
- if (this.selectedFile !== idx) {
197
- this.selectedFile = idx;
198
- this.scrollOffset = 0;
199
- }
200
- } else {
201
- this.entries.push(entry);
202
- this.selectedFile = this.entries.length - 1;
203
- this.scrollOffset = 0;
204
- }
205
- this.markDirty();
206
- }
207
-
208
- /** Load a raw multi-file unified diff string directly. */
209
- loadRawDiff(raw: string): void {
210
- this.entries = splitIntoDiffEntries(raw);
211
- this.selectedFile = 0;
212
- this.scrollOffset = 0;
213
- this.markDirty();
214
- }
215
-
216
- /** Run `git diff` against specific files and populate entries. */
217
- async showFileDiffs(files: string[], ref?: string): Promise<void> {
218
- const args = ['diff', ...(ref ? [ref] : []), '--', ...files];
219
- const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', cwd: this.workingDirectory });
220
- const raw = await new Response(proc.stdout).text();
221
- await proc.exited;
222
- this.loadRawDiff(raw);
223
- }
224
-
225
- /** Run `git diff` and populate all changed files. */
226
- async showGitDiff(ref?: string): Promise<void> {
227
- const args = ['diff', ...(ref ? [ref] : [])];
228
- const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
229
- const [raw, errText] = await Promise.all([
230
- new Response(proc.stdout).text(),
231
- new Response(proc.stderr).text(),
232
- ]);
233
- const exitCode = await proc.exited;
234
- if (exitCode !== 0) {
235
- const errorText = errText.trim() || 'git diff failed';
236
- this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
237
- return;
238
- }
239
- if (!raw.trim()) {
240
- this.showDiff('(no changes)', '@@ -0,0 +0,0 @@\n No changes in working tree.');
241
- return;
242
- }
243
- const newEntries = splitIntoDiffEntries(raw);
244
- // Merge: update existing, append new
245
- for (const entry of newEntries) {
246
- const idx = this.entries.findIndex(e => e.filePath === entry.filePath);
247
- if (idx >= 0) {
248
- this.entries[idx] = { ...entry, semanticSummary: this.entries[idx]!.semanticSummary };
249
- } else {
250
- this.entries.push(entry);
251
- }
252
- }
253
- this.selectedFile = Math.min(this.selectedFile, Math.max(0, this.entries.length - 1));
254
- this.scrollOffset = 0;
255
- this.markDirty();
256
- }
257
-
258
- /**
259
- * Attach or update the semantic diff summary for a file entry.
260
- * No-op if the file isn't currently loaded. Safe to call from an async
261
- * callback after the entry has already been replaced.
262
- */
263
- setSemanticSummary(filePath: string, summary: string): void {
264
- const entry = this.entries.find(e => e.filePath === filePath);
265
- if (entry) {
266
- entry.semanticSummary = summary;
267
- this.markDirty();
268
- }
269
- }
270
-
271
- /** Clear all diff entries. */
272
- clear(): void {
273
- this.entries = [];
274
- this.selectedFile = 0;
275
- this.scrollOffset = 0;
276
- this.markDirty();
277
- }
278
-
279
- // -------------------------------------------------------------------------
280
- // Lifecycle
281
- // -------------------------------------------------------------------------
282
-
283
- override onActivate(): void {
284
- this.needsRender = true;
285
- }
286
-
287
- // -------------------------------------------------------------------------
288
- // Input
289
- // -------------------------------------------------------------------------
290
-
291
- handleInput(key: string): boolean {
292
- switch (key) {
293
- case 'up': this.scrollUp(); return true;
294
- case 'down': this.scrollDown(); return true;
295
- case 'tab': this.nextFile(); return true;
296
- case 'pageup': this.scrollPageUp(); return true;
297
- case 'pagedown': this.scrollPageDown(); return true;
298
- default: return false;
299
- }
300
- }
301
-
302
- private scrollUp(): void {
303
- if (this.scrollOffset > 0) {
304
- this.scrollOffset--;
305
- this.markDirty();
306
- }
307
- }
308
-
309
- private scrollDown(): void {
310
- const entry = this.currentEntry();
311
- if (!entry) return;
312
- const max = Math.max(0, entry.lines.length - 1);
313
- if (this.scrollOffset < max) {
314
- this.scrollOffset++;
315
- this.markDirty();
316
- }
317
- }
318
-
319
- private scrollPageUp(): void {
320
- this.scrollOffset = Math.max(0, this.scrollOffset - 20);
321
- this.markDirty();
322
- }
323
-
324
- private scrollPageDown(): void {
325
- const entry = this.currentEntry();
326
- if (!entry) return;
327
- const max = Math.max(0, entry.lines.length - 1);
328
- this.scrollOffset = Math.min(max, this.scrollOffset + 20);
329
- this.markDirty();
330
- }
331
-
332
- private nextFile(): void {
333
- if (this.entries.length === 0) return;
334
- this.selectedFile = (this.selectedFile + 1) % this.entries.length;
335
- this.scrollOffset = 0;
336
- this.markDirty();
337
- }
338
-
339
- private currentEntry(): DiffEntry | null {
340
- return this.entries[this.selectedFile] ?? null;
341
- }
342
-
343
- // -------------------------------------------------------------------------
344
- // Render
345
- // -------------------------------------------------------------------------
346
-
347
- render(width: number, height: number): Line[] {
348
- return this.trackedRender(() => {
349
- if (height <= 0 || width <= 0) return [];
350
-
351
- if (this.entries.length === 0) {
352
- return buildPanelWorkspace(width, height, {
353
- title: 'Diff Workspace',
354
- palette: {
355
- ...DEFAULT_PANEL_PALETTE,
356
- info: COLOR.hunk,
357
- dim: COLOR.context,
358
- value: COLOR.filename,
359
- },
360
- sections: [{
361
- title: 'Diff',
362
- lines: buildEmptyState(
363
- width,
364
- ' No diff to display.',
365
- 'Load a git diff or select a changed file to populate the workspace.',
366
- [{ command: '/git diff', summary: 'load the current working-tree diff into the diff workspace' }],
367
- {
368
- ...DEFAULT_PANEL_PALETTE,
369
- info: COLOR.hunk,
370
- dim: COLOR.context,
371
- value: COLOR.filename,
372
- empty: COLOR.context,
373
- },
374
- ),
375
- }],
376
- });
377
- }
378
-
379
- const entry = this.currentEntry();
380
- if (!entry) {
381
- return Array.from({ length: height }, () => createEmptyLine(width));
382
- }
383
-
384
- const compact = height <= 12;
385
- const summaryLines = entry.semanticSummary
386
- ? buildBodyText(width, `Semantic summary: ${entry.semanticSummary}`, {
387
- ...DEFAULT_PANEL_PALETTE,
388
- dim: COLOR.context,
389
- value: COLOR.filename,
390
- }, COLOR.context)
391
- : [];
392
- const previewSection = resolveScrollablePanelSection(width, height, {
393
- palette: {
394
- ...DEFAULT_PANEL_PALETTE,
395
- info: COLOR.hunk,
396
- dim: COLOR.context,
397
- value: COLOR.filename,
398
- headerBg: COLOR.tabBg,
399
- },
400
- footerLines: [this.renderStatusBar(width, entry)],
401
- beforeSections: [
402
- {
403
- title: compact ? undefined : 'Files',
404
- lines: [
405
- this.renderTabBar(width),
406
- ...summaryLines,
407
- ],
408
- },
409
- ],
410
- section: {
411
- title: compact ? undefined : 'Changes',
412
- scrollableLines: entry.lines.map((pl) => this.renderParsedLine(pl, width)),
413
- scrollOffset: this.scrollOffset,
414
- minRows: 1,
415
- },
416
- });
417
- this.scrollOffset = previewSection.scrollOffset;
418
-
419
- const sections: PanelWorkspaceSection[] = [
420
- {
421
- title: compact ? undefined : 'Files',
422
- lines: [
423
- this.renderTabBar(width),
424
- ...summaryLines,
425
- ],
426
- },
427
- {
428
- title: previewSection.section.title,
429
- lines: previewSection.section.lines,
430
- },
431
- ];
432
- return buildPanelWorkspace(width, height, {
433
- title: 'Diff Workspace',
434
- palette: {
435
- ...DEFAULT_PANEL_PALETTE,
436
- info: COLOR.hunk,
437
- dim: COLOR.context,
438
- value: COLOR.filename,
439
- headerBg: COLOR.tabBg,
440
- },
441
- sections,
442
- footerLines: [this.renderStatusBar(width, entry)],
443
- });
444
- });
445
- }
446
-
447
- // ── Tab bar ──────────────────────────────────────────────────────────────
448
-
449
- private renderTabBar(width: number): Line {
450
- const cells: Line = [];
451
-
452
- for (let i = 0; i < this.entries.length; i++) {
453
- const entry = this.entries[i]!;
454
- const active = i === this.selectedFile;
455
- const label = ` ${basename(entry.filePath)} `;
456
- const fg = active ? COLOR.tabActive : COLOR.tabInactive;
457
- const bg = active ? '#333333' : COLOR.tabBg;
458
-
459
- for (const ch of label) {
460
- if (cells.length >= width) break;
461
- cells.push(createStyledCell(ch, { fg, bg, bold: active }));
462
- }
463
-
464
- if (cells.length < width) {
465
- cells.push(createStyledCell('│', { fg: COLOR.lineNum, bg: COLOR.tabBg }));
466
- }
467
- }
468
-
469
- // Fill remaining
470
- while (cells.length < width) {
471
- cells.push(createStyledCell(' ', { fg: '', bg: COLOR.tabBg }));
472
- }
473
-
474
- return cells.slice(0, width);
475
- }
476
-
477
- // ── Status bar ───────────────────────────────────────────────────────────
478
-
479
- private renderStatusBar(width: number, entry: DiffEntry | null): Line {
480
- const fileInfo = entry
481
- ? `${entry.filePath} [${this.selectedFile + 1}/${this.entries.length}]`
482
- : 'No file';
483
- const scroll = entry
484
- ? ` L${this.scrollOffset + 1}/${entry.lines.length} Tab: next file Up/Down: scroll`
485
- : '';
486
- const semantic = entry?.semanticSummary ? ` * ${entry.semanticSummary}` : '';
487
- const text = ` ${fileInfo}${scroll}${semantic}`;
488
- return renderText(width, text, COLOR.tabActive, COLOR.statusBar);
489
- }
490
-
491
- // ── Parsed line ──────────────────────────────────────────────────────────
492
-
493
- private renderParsedLine(pl: ParsedLine, width: number): Line {
494
- const left = pl.beforeNum !== null ? String(pl.beforeNum) : '';
495
- const right = pl.afterNum !== null ? String(pl.afterNum) : '';
496
-
497
- switch (pl.kind) {
498
- case 'addition':
499
- return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, '#001a0d', COLOR.lineNumAdd, true);
500
- case 'deletion':
501
- return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, '#1a0000', COLOR.lineNumDel, false);
502
- case 'hunk':
503
- return renderText(width, pl.text, COLOR.hunk, '#0a0a1a', false);
504
- case 'header':
505
- return renderText(width, pl.text, COLOR.header, '', false);
506
- case 'context':
507
- default:
508
- return makeLine(width, left, right, ` ${pl.text}`, COLOR.context, '', COLOR.lineNum, false);
509
- }
510
- }
511
- }
512
-
513
- // ---------------------------------------------------------------------------
514
- // Helpers
515
- // ---------------------------------------------------------------------------
516
-
517
- function basename(p: string): string {
518
- const parts = p.replace(/\\/g, '/').split('/');
519
- return parts[parts.length - 1] ?? p;
520
- }