@pellux/goodvibes-agent 0.1.1 → 0.1.3

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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +12 -1
  3. package/docs/README.md +2 -0
  4. package/docs/getting-started.md +19 -1
  5. package/docs/release-and-publishing.md +3 -1
  6. package/package.json +10 -1
  7. package/src/agent/persona-registry.ts +379 -0
  8. package/src/agent/skill-registry.ts +360 -0
  9. package/src/audio/spoken-turn-model-routing.ts +2 -1
  10. package/src/cli/agent-knowledge-command.ts +525 -0
  11. package/src/cli/help.ts +35 -0
  12. package/src/cli/management-commands.ts +3 -1
  13. package/src/cli/management.ts +33 -9
  14. package/src/cli/parser.ts +7 -0
  15. package/src/cli/types.ts +3 -0
  16. package/src/config/surface.ts +1 -0
  17. package/src/input/agent-workspace.ts +33 -3
  18. package/src/input/command-registry.ts +4 -1
  19. package/src/input/commands/agent-skills-runtime.ts +216 -0
  20. package/src/input/commands/delegation-runtime.ts +129 -0
  21. package/src/input/commands/knowledge.ts +18 -18
  22. package/src/input/commands/personas-runtime.ts +219 -0
  23. package/src/input/commands/shell-core.ts +9 -6
  24. package/src/input/commands/skills-runtime.ts +7 -2
  25. package/src/input/commands.ts +6 -0
  26. package/src/input/panel-integration-actions.ts +0 -52
  27. package/src/input/submission-router.ts +1 -1
  28. package/src/main.ts +2 -1
  29. package/src/panels/builtin/agent.ts +0 -14
  30. package/src/panels/builtin/session.ts +4 -3
  31. package/src/panels/index.ts +0 -5
  32. package/src/panels/orchestration-panel.ts +4 -5
  33. package/src/panels/qr-panel.ts +3 -2
  34. package/src/panels/tasks-panel.ts +4 -4
  35. package/src/renderer/agent-workspace.ts +2 -0
  36. package/src/runtime/bootstrap-command-context.ts +3 -0
  37. package/src/runtime/bootstrap-command-parts.ts +6 -2
  38. package/src/runtime/bootstrap-core.ts +8 -4
  39. package/src/runtime/bootstrap-shell.ts +5 -2
  40. package/src/runtime/bootstrap.ts +10 -2
  41. package/src/runtime/cloudflare-control-plane.ts +2 -1
  42. package/src/version.ts +1 -1
  43. package/src/daemon/cli.ts +0 -55
  44. package/src/daemon/safe-serve.ts +0 -61
  45. package/src/panels/diff-panel.ts +0 -520
  46. package/src/panels/file-explorer-panel.ts +0 -584
  47. package/src/panels/file-preview-panel.ts +0 -434
  48. package/src/panels/git-panel.ts +0 -638
  49. package/src/panels/sandbox-panel.ts +0 -283
  50. package/src/panels/symbol-outline-panel.ts +0 -486
  51. package/src/panels/worktree-panel.ts +0 -182
  52. package/src/panels/wrfc-panel.ts +0 -609
@@ -1,434 +0,0 @@
1
- import type { Stats } from 'node:fs';
2
- import { promises as fsPromises, readFileSync, statSync } from 'node:fs';
3
- import * as path from 'node:path';
4
- import type { Line, Cell } from '../types/grid.ts';
5
- import { createStyledCell, createEmptyLine } from '../types/grid.ts';
6
- import { BasePanel } from './base-panel.ts';
7
- import { SyntaxHighlighter, type SyntaxToken } from '../renderer/syntax-highlighter.ts';
8
- import { getDisplayWidth } from '../utils/terminal-width.ts';
9
- import {
10
- buildEmptyState,
11
- buildPanelLine,
12
- buildPanelWorkspace,
13
- resolveScrollablePanelSection,
14
- DEFAULT_PANEL_PALETTE,
15
- } from './polish.ts';
16
-
17
- // ─── Constants ────────────────────────────────────────────────────────────────
18
-
19
- const MAX_FILE_SIZE = 100 * 1024; // 100 KB
20
- const BG = '#0d0d0d';
21
- const HEADER_BG = '#1e1e1e';
22
- const HEADER_FG = '#d4d4d4';
23
- const HEADER_ACCENT = '#4ec9b0';
24
- const LINE_NUM_FG = '238';
25
- const WARNING_FG = '#f44747';
26
- const EMPTY_FG = '244';
27
-
28
- // ─── Language Detection (from file extension) ─────────────────────────────────
29
-
30
- function extToFenceTag(filePath: string): string {
31
- const ext = path.extname(filePath).slice(1).toLowerCase();
32
- const map: Record<string, string> = {
33
- ts: 'ts', tsx: 'tsx', js: 'js', jsx: 'jsx',
34
- py: 'python', python: 'python',
35
- sh: 'bash', bash: 'bash', zsh: 'bash',
36
- json: 'json',
37
- yaml: 'yaml', yml: 'yaml',
38
- html: 'html', htm: 'html', xml: 'xml',
39
- css: 'css', scss: 'scss', less: 'less',
40
- rs: 'rust',
41
- go: 'go',
42
- c: 'c', cpp: 'cpp', cc: 'cpp', h: 'c',
43
- java: 'java',
44
- rb: 'ruby',
45
- md: 'markdown', mdx: 'markdown',
46
- toml: 'toml',
47
- lua: 'lua',
48
- };
49
- return map[ext] ?? '';
50
- }
51
-
52
- // ─── Panel ────────────────────────────────────────────────────────────────────
53
-
54
- export class FilePreviewPanel extends BasePanel {
55
- private readonly syntaxHighlighter = new SyntaxHighlighter();
56
- private filePath: string | null = null;
57
- private fileLines: string[] = [];
58
- private fenceTag: string = '';
59
- private scrollOffset: number = 0;
60
- private oversized: boolean = false;
61
-
62
- /** Per-file scroll position memory: path -> scrollOffset */
63
- private readonly scrollMemory = new Map<string, number>();
64
-
65
- constructor() {
66
- super('preview', 'Preview', 'P', 'development');
67
- }
68
-
69
- // ─── Public API ─────────────────────────────────────────────────────────────
70
-
71
- /**
72
- * Load a file into the preview. Reads asynchronously.
73
- * Files larger than 100 KB show a warning instead of content.
74
- */
75
- openFile(filePath: string): void {
76
- // Save scroll position for the current file before switching
77
- if (this.filePath !== null) {
78
- this.scrollMemory.set(this.filePath, this.scrollOffset);
79
- }
80
-
81
- this.filePath = filePath;
82
- this.oversized = false;
83
- this.fenceTag = extToFenceTag(filePath);
84
-
85
- // Restore scroll position for this file, or start at top
86
- this.scrollOffset = this.scrollMemory.get(filePath) ?? 0;
87
-
88
- // Synchronously pre-populate fileLines for small files so that callers
89
- // (e.g. syncSymbolOutlineFromPreview) can read getSource() immediately.
90
- try {
91
- const stat = statSync(filePath);
92
- if (stat.size <= MAX_FILE_SIZE) {
93
- const content = readFileSync(filePath, 'utf-8');
94
- this.fileLines = content.split('\n');
95
- } else {
96
- this.fileLines = [];
97
- this.oversized = true;
98
- }
99
- } catch {
100
- this.fileLines = [`(cannot open: ${filePath})`];
101
- }
102
-
103
- void this._loadFileAsync(filePath);
104
- }
105
-
106
- private async _loadFileAsync(filePath: string): Promise<void> {
107
- try {
108
- await this.withLoading('Loading…', async () => {
109
- let stat: Stats;
110
- try {
111
- stat = await fsPromises.stat(filePath);
112
- } catch {
113
- this.fileLines = [`(cannot open: ${filePath})`];
114
- return;
115
- }
116
-
117
- if (stat.size > MAX_FILE_SIZE) {
118
- this.oversized = true;
119
- return;
120
- }
121
-
122
- let content: string;
123
- try {
124
- content = await fsPromises.readFile(filePath, 'utf-8');
125
- } catch {
126
- this.fileLines = [`(read error: ${filePath})`];
127
- return;
128
- }
129
-
130
- this.fileLines = content.split('\n');
131
- // Strip trailing empty line from final newline
132
- if (this.fileLines.length > 0 && this.fileLines[this.fileLines.length - 1] === '') {
133
- this.fileLines.pop();
134
- }
135
-
136
- this.fenceTag = extToFenceTag(filePath);
137
-
138
- // Kick off async tree-sitter parse so subsequent renders get highlighting
139
- if (this.fenceTag) {
140
- this.syntaxHighlighter.highlight(content, this.fenceTag);
141
- }
142
-
143
- // Clamp scroll in case the new file is shorter
144
- this.clampScroll(0);
145
- });
146
- } catch (err) {
147
- this.setError(err instanceof Error ? err.message : String(err));
148
- }
149
- this.markDirty();
150
- }
151
-
152
- getCurrentFilePath(): string | null {
153
- return this.filePath;
154
- }
155
-
156
- getSource(): string | null {
157
- if (this.filePath === null || this.oversized) return null;
158
- return this.fileLines.join('\n');
159
- }
160
-
161
- goToLine(line: number): void {
162
- if (!Number.isFinite(line)) return;
163
- this.scrollOffset = Math.max(0, Math.min(Math.floor(line) - 1, Math.max(0, this.fileLines.length - 1)));
164
- this.markDirty();
165
- }
166
-
167
- getScrollOffset(): number {
168
- return this.scrollOffset;
169
- }
170
-
171
- // ─── Lifecycle ───────────────────────────────────────────────────────────────
172
-
173
- override onActivate(): void {
174
- super.onActivate();
175
- }
176
-
177
- override onDeactivate(): void {
178
- // Persist current scroll position
179
- if (this.filePath !== null) {
180
- this.scrollMemory.set(this.filePath, this.scrollOffset);
181
- }
182
- }
183
-
184
- // ─── Input handling ──────────────────────────────────────────────────────────
185
-
186
- handleInput(key: string): boolean {
187
- switch (key) {
188
- case 'up': return this.scroll(-1);
189
- case 'down': return this.scroll(1);
190
- case 'pageup': return this.scrollPage(-1);
191
- case 'pagedown': return this.scrollPage(1);
192
- case 'home': return this.scrollTo(0);
193
- case 'end': return this.scrollTo(Infinity);
194
- default: return false;
195
- }
196
- }
197
-
198
- // ─── Rendering ───────────────────────────────────────────────────────────────
199
-
200
- render(width: number, height: number): Line[] {
201
- const title = this.filePath === null
202
- ? ' Preview'
203
- : ` Preview / ${path.basename(this.filePath)}`;
204
- const intro = this.filePath
205
- ? `${this.filePath}${this.fenceTag ? ` [${this.fenceTag}]` : ''}`
206
- : 'Open a file to inspect its contents with line numbers and syntax highlighting.';
207
-
208
- if (this.filePath === null) {
209
- return buildPanelWorkspace(width, height, {
210
- title,
211
- intro,
212
- sections: [
213
- {
214
- lines: buildEmptyState(
215
- width,
216
- ' No file open',
217
- 'Use the explorer or a file-targeting command to load a file into the preview surface.',
218
- [],
219
- DEFAULT_PANEL_PALETTE,
220
- ),
221
- },
222
- ],
223
- palette: DEFAULT_PANEL_PALETTE,
224
- });
225
- }
226
-
227
- if (this.oversized) {
228
- return buildPanelWorkspace(width, height, {
229
- title,
230
- intro,
231
- sections: [
232
- {
233
- lines: buildEmptyState(
234
- width,
235
- ` File too large to preview`,
236
- `The selected file exceeds the 100 KB preview limit: ${path.basename(this.filePath)}.`,
237
- [],
238
- DEFAULT_PANEL_PALETTE,
239
- ),
240
- },
241
- ],
242
- palette: DEFAULT_PANEL_PALETTE,
243
- });
244
- }
245
-
246
- if (this.fileLines.length === 0) {
247
- return buildPanelWorkspace(width, height, {
248
- title,
249
- intro,
250
- sections: [
251
- {
252
- lines: buildEmptyState(
253
- width,
254
- ' Empty file',
255
- 'The selected file has no content.',
256
- [],
257
- DEFAULT_PANEL_PALETTE,
258
- ),
259
- },
260
- ],
261
- palette: DEFAULT_PANEL_PALETTE,
262
- });
263
- }
264
-
265
- const summarySection = {
266
- title: 'Summary',
267
- lines: [
268
- buildPanelLine(width, [
269
- [' Lines ', DEFAULT_PANEL_PALETTE.label],
270
- [String(this.fileLines.length), DEFAULT_PANEL_PALETTE.value],
271
- ]),
272
- ],
273
- } as const;
274
- const footerLines = [
275
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll', DEFAULT_PANEL_PALETTE.dim], [' PgUp/PgDn', DEFAULT_PANEL_PALETTE.info], [' page', DEFAULT_PANEL_PALETTE.dim], [' Home/End', DEFAULT_PANEL_PALETTE.info], [' bounds', DEFAULT_PANEL_PALETTE.dim]]),
276
- ];
277
- const fullCode = this.fileLines.join('\n');
278
- const hlLines = this.fenceTag
279
- ? this.syntaxHighlighter.highlight(fullCode, this.fenceTag)
280
- : null;
281
-
282
- const lineNumW = String(this.fileLines.length).length;
283
- const contentX = lineNumW + 2; // "NNN | "
284
- const previewLines: Line[] = [];
285
- for (let fileIdx = 0; fileIdx < this.fileLines.length; fileIdx++) {
286
-
287
- const rawLine = this.fileLines[fileIdx];
288
- const tokens: SyntaxToken[] =
289
- hlLines && fileIdx < hlLines.length && hlLines[fileIdx].length > 0
290
- ? (hlLines[fileIdx] as SyntaxToken[])
291
- : [{ text: rawLine, fg: '' }];
292
-
293
- previewLines.push(this.renderCodeLine(fileIdx, lineNumW, contentX, tokens, width));
294
- }
295
- const previewSection = resolveScrollablePanelSection(width, height, {
296
- intro,
297
- footerLines,
298
- palette: DEFAULT_PANEL_PALETTE,
299
- beforeSections: [summarySection],
300
- section: {
301
- title: 'Preview',
302
- scrollableLines: previewLines,
303
- scrollOffset: this.scrollOffset,
304
- minRows: 8,
305
- },
306
- });
307
- this.scrollOffset = previewSection.scrollOffset;
308
- const window = previewSection.window;
309
-
310
- this.needsRender = false;
311
- return buildPanelWorkspace(width, height, {
312
- title,
313
- intro,
314
- sections: [
315
- {
316
- title: 'Summary',
317
- lines: [
318
- buildPanelLine(width, [
319
- [' Lines ', DEFAULT_PANEL_PALETTE.label],
320
- [String(this.fileLines.length), DEFAULT_PANEL_PALETTE.value],
321
- [' Scroll ', DEFAULT_PANEL_PALETTE.label],
322
- [`${window.start + 1}-${window.end}`, DEFAULT_PANEL_PALETTE.info],
323
- ]),
324
- ],
325
- },
326
- {
327
- title: 'Preview',
328
- lines: previewSection.section.lines,
329
- },
330
- ],
331
- footerLines,
332
- palette: DEFAULT_PANEL_PALETTE,
333
- });
334
- }
335
-
336
- private renderCodeLine(
337
- fileIdx: number,
338
- lineNumW: number,
339
- contentX: number,
340
- tokens: SyntaxToken[],
341
- width: number,
342
- ): Line {
343
- const line: Cell[] = new Array(width).fill(null).map(() =>
344
- createStyledCell(' ', { bg: BG }),
345
- );
346
-
347
- // Line number gutter
348
- const lineNum = String(fileIdx + 1).padStart(lineNumW);
349
- let cx = 0;
350
- for (const ch of lineNum) {
351
- if (cx >= lineNumW) break;
352
- line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg: BG, dim: true });
353
- }
354
- // Separator " | "
355
- line[cx++] = createStyledCell(' ', { bg: BG });
356
- line[cx++] = createStyledCell('│', { fg: LINE_NUM_FG, bg: BG, dim: true });
357
- line[cx++] = createStyledCell(' ', { bg: BG });
358
-
359
- // Syntax tokens
360
- for (const token of tokens) {
361
- for (const ch of token.text) {
362
- if (cx >= width) break;
363
- const code = ch.charCodeAt(0);
364
- if (code < 32 || code === 127) { cx++; continue; }
365
- const cw = getDisplayWidth(ch);
366
- line[cx] = createStyledCell(ch, {
367
- fg: token.fg || '',
368
- bg: BG,
369
- bold: token.bold,
370
- italic: token.italic,
371
- });
372
- if (cw === 2 && cx + 1 < width) line[cx + 1] = { ...line[cx], char: '' };
373
- cx += cw;
374
- }
375
- }
376
-
377
- return line;
378
- }
379
-
380
- private renderEmpty(width: number, height: number, message: string): Line[] {
381
- const lines: Line[] = [];
382
- const msgLine = createEmptyLine(width);
383
- const isWarning = message.startsWith('File too large');
384
- const fg = isWarning ? WARNING_FG : EMPTY_FG;
385
- let cx = 2;
386
- for (const ch of message) {
387
- if (cx >= width - 1) break;
388
- msgLine[cx++] = createStyledCell(ch, { fg, bg: BG });
389
- }
390
- lines.push(msgLine);
391
- for (let i = 1; i < height; i++) {
392
- lines.push(this.renderBgLine(width));
393
- }
394
- return lines;
395
- }
396
-
397
- private renderBgLine(width: number): Line {
398
- const line = createEmptyLine(width);
399
- for (let x = 0; x < width; x++) {
400
- line[x] = createStyledCell(' ', { bg: BG });
401
- }
402
- return line;
403
- }
404
-
405
- private scroll(delta: number): boolean {
406
- const before = this.scrollOffset;
407
- this.scrollOffset = Math.max(0, this.scrollOffset + delta);
408
- if (this.scrollOffset !== before) this.markDirty();
409
- return true;
410
- }
411
-
412
- private scrollPage(direction: -1 | 1): boolean {
413
- // Page size is approximate — clamp happens in render()
414
- const pageSize = Math.max(1, this.fileLines.length > 0 ? 20 : 1);
415
- return this.scroll(direction * pageSize);
416
- }
417
-
418
- private scrollTo(target: number): boolean {
419
- const before = this.scrollOffset;
420
- this.scrollOffset = target === Infinity
421
- ? Math.max(0, this.fileLines.length - 1)
422
- : Math.max(0, target);
423
- if (this.scrollOffset !== before) this.markDirty();
424
- return true;
425
- }
426
-
427
- /** Clamp scrollOffset so content doesn't scroll past the last line. */
428
- private clampScroll(contentHeight: number): void {
429
- const maxScroll = Math.max(0, this.fileLines.length - Math.max(1, contentHeight));
430
- if (this.scrollOffset > maxScroll) {
431
- this.scrollOffset = maxScroll;
432
- }
433
- }
434
- }