@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,638 +0,0 @@
1
- import { BasePanel } from './base-panel.ts';
2
- import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
3
- import { GitService } from '@pellux/goodvibes-sdk/platform/git';
4
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
5
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
6
- import {
7
- buildEmptyState,
8
- buildPanelLine,
9
- buildPanelWorkspace,
10
- resolveScrollablePanelSection,
11
- buildStyledPanelLine,
12
- DEFAULT_PANEL_PALETTE,
13
- } from './polish.ts';
14
-
15
- // ---------------------------------------------------------------------------
16
- // Types
17
- // ---------------------------------------------------------------------------
18
-
19
- interface GitFileEntry {
20
- path: string;
21
- staged: boolean;
22
- }
23
-
24
- interface CommitEntry {
25
- hash: string;
26
- message: string;
27
- author: string;
28
- date: string;
29
- }
30
-
31
- interface GitData {
32
- branch: string;
33
- ahead: number;
34
- behind: number;
35
- stagedFiles: GitFileEntry[];
36
- unstagedFiles: GitFileEntry[];
37
- recentCommits: CommitEntry[];
38
- }
39
-
40
- type ViewItem =
41
- | { kind: 'header' }
42
- | { kind: 'section'; label: string }
43
- | { kind: 'file'; entry: GitFileEntry }
44
- | { kind: 'commit'; entry: CommitEntry }
45
- | { kind: 'empty'; label: string }
46
- | { kind: 'diff-line'; text: string; diffType: 'add' | 'remove' | 'meta' | 'neutral' };
47
-
48
- // ---------------------------------------------------------------------------
49
- // Constants
50
- // ---------------------------------------------------------------------------
51
-
52
- /** Minimum number of diff lines kept visible when clamping scroll offset. */
53
- const MIN_VISIBLE_DIFF_LINES = 5;
54
-
55
- // ---------------------------------------------------------------------------
56
- // Colors
57
- // ---------------------------------------------------------------------------
58
-
59
- const C = {
60
- branch: '#00d7ff',
61
- clean: '#5fd700',
62
- dirty: '#ffaf00',
63
- ahead: '#5fd700',
64
- behind: '#ff5f5f',
65
- sectionHeader: '244',
66
- staged: '#5fd700',
67
- unstaged: '#ff5f5f',
68
- commit: '250',
69
- commitHash: '238',
70
- commitAuthor: '244',
71
- selected: '#1c1c1c',
72
- selectedFg: '#ffffff',
73
- diffAdd: '#5fd700',
74
- diffRemove: '#ff5f5f',
75
- diffMeta: '#5f87ff',
76
- diffNeutral: '250',
77
- } as const;
78
-
79
- // ---------------------------------------------------------------------------
80
- // GitPanel
81
- // ---------------------------------------------------------------------------
82
-
83
- export class GitPanel extends BasePanel {
84
- private readonly workingDirectory: string;
85
- private data: GitData = {
86
- branch: '...',
87
- ahead: 0,
88
- behind: 0,
89
- stagedFiles: [],
90
- unstagedFiles: [],
91
- recentCommits: [],
92
- };
93
-
94
- /** Flattened list of navigable rows (for arrow-key movement). */
95
- private items: ViewItem[] = [];
96
-
97
- /** Selected row index within `items`. */
98
- private selectedIndex = 0;
99
-
100
- /** When truthy, shows the diff for the selected file. */
101
- private expandedDiff: string[] | null = null;
102
-
103
- /** Scroll offset for both main view and diff view. */
104
- private scrollOffset = 0;
105
-
106
- private refreshTimerId: ReturnType<typeof setInterval> | null = null;
107
- private loading = true;
108
- private error: string | null = null;
109
-
110
- constructor(workingDirectory: string) {
111
- super('git', 'Git', 'G', 'development');
112
- this.workingDirectory = workingDirectory;
113
- }
114
-
115
- // ---------------------------------------------------------------------------
116
- // Lifecycle
117
- // ---------------------------------------------------------------------------
118
-
119
- override onActivate(): void {
120
- super.onActivate();
121
- void this.refresh();
122
- this.refreshTimerId = this.registerTimer(setInterval(() => {
123
- void this.refresh();
124
- }, 5_000));
125
- }
126
-
127
- override onDeactivate(): void {
128
- if (this.refreshTimerId !== null) {
129
- this.clearTimer(this.refreshTimerId);
130
- this.refreshTimerId = null;
131
- }
132
- }
133
-
134
- override onDestroy(): void {
135
- this.onDeactivate();
136
- super.onDestroy();
137
- }
138
-
139
- // ---------------------------------------------------------------------------
140
- // Data fetching
141
- // ---------------------------------------------------------------------------
142
-
143
- private async refresh(isRetry = false): Promise<void> {
144
- try {
145
- const git = new GitService(this.workingDirectory);
146
- const [statusResult, branchResult, logEntries] = await Promise.all([
147
- git.status(),
148
- git.branch(),
149
- git.log(10),
150
- ]);
151
-
152
- const stagedFiles: GitFileEntry[] = [
153
- ...statusResult.staged.map((p) => ({ path: p, staged: true })),
154
- ...statusResult.created.map((p) => ({ path: p, staged: true })),
155
- ];
156
-
157
- const unstagedFiles: GitFileEntry[] = [
158
- ...statusResult.modified.map((p) => ({ path: p, staged: false })),
159
- ...statusResult.deleted.map((p) => ({ path: p, staged: false })),
160
- ...statusResult.not_added.map((p) => ({ path: p, staged: false })),
161
- ...statusResult.conflicted.map((p) => ({ path: p, staged: false })),
162
- ];
163
-
164
- this.data = {
165
- branch: branchResult.current || 'HEAD',
166
- ahead: statusResult.ahead ?? 0,
167
- behind: statusResult.behind ?? 0,
168
- stagedFiles,
169
- unstagedFiles,
170
- recentCommits: logEntries.map((e) => ({
171
- hash: e.hash.slice(0, 7),
172
- message: e.message,
173
- author: e.author,
174
- date: e.date,
175
- })),
176
- };
177
-
178
- this.loading = false;
179
- this.error = null;
180
- this.rebuildItems();
181
- // Do not clear expandedDiff during auto-refresh — only clear on explicit user action
182
- this.markDirty();
183
- } catch (err) {
184
- const msg = summarizeError(err);
185
- // If the failure is because this directory isn't a git repo, auto-initialise
186
- // and retry once so the panel becomes functional immediately.
187
- if (/not a git\b/i.test(msg)) {
188
- const cwd = this.workingDirectory;
189
- const initResult = GitService.initRepo(cwd);
190
- if (initResult.success) {
191
- logger.debug('GitPanel: auto-initialised git repo', { cwd });
192
- if (!isRetry) {
193
- // Retry refresh now that the repo exists (once only)
194
- void this.refresh(true);
195
- return;
196
- }
197
- this.error = 'Not a git repository. Auto-init succeeded but refresh failed.';
198
- } else {
199
- this.error = `Not a git repository. Auto-init failed: ${initResult.error ?? 'unknown error'}`;
200
- }
201
- } else {
202
- this.error = msg;
203
- }
204
- this.loading = false;
205
- logger.debug('GitPanel: refresh failed', { error: this.error });
206
- this.markDirty();
207
- }
208
- }
209
-
210
- /** Rebuild the flat navigable item list from current data. */
211
- private rebuildItems(): void {
212
- const items: ViewItem[] = [];
213
-
214
- // Branch / status header row
215
- items.push({ kind: 'header' });
216
-
217
- // Staged files
218
- items.push({ kind: 'section', label: `Staged (${this.data.stagedFiles.length})` });
219
- if (this.data.stagedFiles.length === 0) {
220
- items.push({ kind: 'empty', label: ' (no staged files)' });
221
- } else {
222
- for (const entry of this.data.stagedFiles) {
223
- items.push({ kind: 'file', entry });
224
- }
225
- }
226
-
227
- // Unstaged files
228
- items.push({ kind: 'section', label: `Unstaged (${this.data.unstagedFiles.length})` });
229
- if (this.data.unstagedFiles.length === 0) {
230
- items.push({ kind: 'empty', label: ' (no unstaged files)' });
231
- } else {
232
- for (const entry of this.data.unstagedFiles) {
233
- items.push({ kind: 'file', entry });
234
- }
235
- }
236
-
237
- // Recent commits
238
- items.push({ kind: 'section', label: `Recent Commits (${this.data.recentCommits.length})` });
239
- if (this.data.recentCommits.length === 0) {
240
- items.push({ kind: 'empty', label: ' (no commits)' });
241
- } else {
242
- for (const entry of this.data.recentCommits) {
243
- items.push({ kind: 'commit', entry });
244
- }
245
- }
246
-
247
- this.items = items;
248
-
249
- // Keep selection in bounds
250
- if (this.selectedIndex >= this.items.length) {
251
- this.selectedIndex = Math.max(0, this.items.length - 1);
252
- }
253
- }
254
-
255
- // ---------------------------------------------------------------------------
256
- // Input handling
257
- // ---------------------------------------------------------------------------
258
-
259
- handleInput(key: string): boolean {
260
- if (this.expandedDiff !== null) {
261
- return this.handleDiffInput(key);
262
- }
263
- return this.handleListInput(key);
264
- }
265
-
266
- private handleListInput(key: string): boolean {
267
- switch (key) {
268
- case 'up':
269
- case 'k': {
270
- if (this.selectedIndex > 0) {
271
- this.selectedIndex--;
272
- this.markDirty();
273
- }
274
- return true;
275
- }
276
- case 'down':
277
- case 'j': {
278
- if (this.selectedIndex < this.items.length - 1) {
279
- this.selectedIndex++;
280
- this.markDirty();
281
- }
282
- return true;
283
- }
284
- case 'return': {
285
- void this.openDiff();
286
- return true;
287
- }
288
- case 'r': {
289
- void this.refresh();
290
- return true;
291
- }
292
- default:
293
- return false;
294
- }
295
- }
296
-
297
- private handleDiffInput(key: string): boolean {
298
- switch (key) {
299
- case 'up':
300
- case 'k': {
301
- if (this.scrollOffset > 0) {
302
- this.scrollOffset--;
303
- this.markDirty();
304
- }
305
- return true;
306
- }
307
- case 'down':
308
- case 'j': {
309
- const diffLen = this.expandedDiff?.length ?? 0;
310
- this.scrollOffset = Math.min(this.scrollOffset + 1, Math.max(0, diffLen - MIN_VISIBLE_DIFF_LINES));
311
- this.markDirty();
312
- return true;
313
- }
314
- case 'escape':
315
- case 'q': {
316
- this.expandedDiff = null;
317
- this.scrollOffset = 0;
318
- this.markDirty();
319
- return true;
320
- }
321
- default:
322
- return false;
323
- }
324
- }
325
-
326
- private async openDiff(): Promise<void> {
327
- const item = this.items[this.selectedIndex];
328
- if (!item || item.kind !== 'file') return;
329
-
330
- // I3: withLoading guarantees spinner is cleared even if diffFile throws
331
- try {
332
- const raw = await this.withLoading('Loading diff…', async () => {
333
- const git = new GitService(this.workingDirectory);
334
- return git.diffFile(item.entry.path, item.entry.staged);
335
- });
336
- this.expandedDiff = raw ? raw.split('\n') : ['(no diff available)'];
337
- this.scrollOffset = 0;
338
- this.markDirty();
339
- } catch (err) {
340
- this.expandedDiff = [`Error: ${summarizeError(err)}`];
341
- this.scrollOffset = 0;
342
- this.markDirty();
343
- }
344
- }
345
-
346
- // ---------------------------------------------------------------------------
347
- // Rendering
348
- // ---------------------------------------------------------------------------
349
-
350
- override render(width: number, height: number): Line[] {
351
- if (this.loading) {
352
- return this.renderMessage(width, height, 'Loading git status...', C.branch);
353
- }
354
- if (this.error) {
355
- return this.renderMessage(width, height, `Git error: ${this.error}`, C.unstaged);
356
- }
357
- // I3: spinner during openDiff() async fetch
358
- if (this.loadingState === 'loading') {
359
- return this.renderMessage(width, height, 'Loading diff...', C.branch);
360
- }
361
- if (this.expandedDiff !== null) {
362
- return this.renderDiff(width, height);
363
- }
364
- return this.renderList(width, height);
365
- }
366
-
367
- // -- Helpers -----------------------------------------------------------------
368
-
369
- private renderMessage(width: number, height: number, msg: string, fg: string): Line[] {
370
- const lines: Line[] = [buildStyledPanelLine(width, [{ text: msg, fg }])];
371
- while (lines.length < height) lines.push(createEmptyLine(width));
372
- return lines;
373
- }
374
-
375
- /** Paint a single text string into a new Line at x=startX. */
376
- private paintText(
377
- line: Line,
378
- text: string,
379
- startX: number,
380
- width: number,
381
- fg: string,
382
- opts: { bold?: boolean; dim?: boolean } = {},
383
- ): number {
384
- let x = startX;
385
- for (const ch of text) {
386
- if (x >= width) break;
387
- line[x++] = createStyledCell(ch, { fg, bold: opts.bold, dim: opts.dim });
388
- }
389
- return x;
390
- }
391
-
392
- private renderBranchLine(width: number): Line {
393
- const line = createEmptyLine(width);
394
- let x = 0;
395
-
396
- const branchIcon = ' git: ';
397
- x = this.paintText(line, branchIcon, x, width, C.sectionHeader);
398
- x = this.paintText(line, this.data.branch, x, width, C.branch, { bold: true });
399
-
400
- if (this.data.ahead > 0) {
401
- x = this.paintText(line, ` +${this.data.ahead}`, x, width, C.ahead);
402
- }
403
- if (this.data.behind > 0) {
404
- x = this.paintText(line, ` -${this.data.behind}`, x, width, C.behind);
405
- }
406
-
407
- const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
408
- const statusText = isDirty ? ' * dirty' : ' y clean';
409
- const statusFg = isDirty ? C.dirty : C.clean;
410
- this.paintText(line, statusText, x, width, statusFg);
411
-
412
- return line;
413
- }
414
-
415
- private renderSectionHeader(label: string, width: number): Line {
416
- return buildStyledPanelLine(width, [{ text: `-- ${label} `, fg: C.sectionHeader, dim: true }]);
417
- }
418
-
419
- private renderFileRow(entry: GitFileEntry, selected: boolean, width: number): Line {
420
- const line = createEmptyLine(width);
421
- const fg = entry.staged ? C.staged : C.unstaged;
422
- const prefix = ' ';
423
- const label = `${prefix}${entry.path}`;
424
-
425
- if (selected) {
426
- // Fill background highlight
427
- for (let i = 0; i < width; i++) {
428
- line[i] = createStyledCell(' ', { bg: C.selected, fg: C.selectedFg });
429
- }
430
- let x = 0;
431
- for (const ch of label) {
432
- if (x >= width) break;
433
- line[x++] = createStyledCell(ch, { fg, bg: C.selected, bold: true });
434
- }
435
- } else {
436
- this.paintText(line, label, 0, width, fg);
437
- }
438
- return line;
439
- }
440
-
441
- private renderCommitRow(entry: CommitEntry, selected: boolean, width: number): Line {
442
- const line = createEmptyLine(width);
443
- const hashPart = ` ${entry.hash} `;
444
- const msgPart = entry.message.length > 60 ? `${entry.message.slice(0, 57)}...` : entry.message;
445
-
446
- if (selected) {
447
- for (let i = 0; i < width; i++) {
448
- line[i] = createStyledCell(' ', { bg: C.selected, fg: C.selectedFg });
449
- }
450
- let x = 0;
451
- for (const ch of hashPart) {
452
- if (x >= width) break;
453
- line[x++] = createStyledCell(ch, { fg: C.commitHash, bg: C.selected });
454
- }
455
- for (const ch of msgPart) {
456
- if (x >= width) break;
457
- line[x++] = createStyledCell(ch, { fg: C.selectedFg, bg: C.selected });
458
- }
459
- } else {
460
- let x = this.paintText(line, hashPart, 0, width, C.commitHash);
461
- this.paintText(line, msgPart, x, width, C.commit);
462
- }
463
- return line;
464
- }
465
-
466
- private renderList(width: number, height: number): Line[] {
467
- const rows: Line[] = [];
468
- for (let i = 0; i < this.items.length; i++) {
469
- const item = this.items[i];
470
- const selected = i === this.selectedIndex;
471
- if (!item) continue;
472
-
473
- switch (item.kind) {
474
- case 'header':
475
- rows.push(this.renderBranchLine(width));
476
- rows.push(createEmptyLine(width)); // spacer
477
- break;
478
- case 'section':
479
- rows.push(this.renderSectionHeader(item.label, width));
480
- break;
481
- case 'file':
482
- rows.push(this.renderFileRow(item.entry, selected, width));
483
- break;
484
- case 'commit':
485
- rows.push(this.renderCommitRow(item.entry, selected, width));
486
- break;
487
- case 'empty': {
488
- rows.push(buildStyledPanelLine(width, [{ text: item.label, fg: C.sectionHeader, dim: true }]));
489
- break;
490
- }
491
- }
492
- }
493
-
494
- const selectedRowIndex = this.getRowIndexForItem(this.selectedIndex);
495
- if (selectedRowIndex >= 0) {
496
- const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
497
- const selectedItem = this.items[this.selectedIndex];
498
- const selectedLines: Line[] = [];
499
- if (selectedItem?.kind === 'file') {
500
- selectedLines.push(buildPanelLine(width, [
501
- [' File ', DEFAULT_PANEL_PALETTE.label],
502
- [selectedItem.entry.path, DEFAULT_PANEL_PALETTE.value],
503
- [' State ', DEFAULT_PANEL_PALETTE.label],
504
- [selectedItem.entry.staged ? 'staged' : 'unstaged', selectedItem.entry.staged ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.warn],
505
- ]));
506
- } else if (selectedItem?.kind === 'commit') {
507
- selectedLines.push(buildPanelLine(width, [
508
- [' Commit ', DEFAULT_PANEL_PALETTE.label],
509
- [selectedItem.entry.hash, DEFAULT_PANEL_PALETTE.info],
510
- [' Author ', DEFAULT_PANEL_PALETTE.label],
511
- [selectedItem.entry.author, DEFAULT_PANEL_PALETTE.value],
512
- ]));
513
- selectedLines.push(buildPanelLine(width, [
514
- [' Message ', DEFAULT_PANEL_PALETTE.label],
515
- [selectedItem.entry.message, DEFAULT_PANEL_PALETTE.value],
516
- ]));
517
- }
518
-
519
- const summarySection = {
520
- title: 'Summary',
521
- lines: [
522
- buildPanelLine(width, [
523
- [' Branch ', DEFAULT_PANEL_PALETTE.label],
524
- [this.data.branch, DEFAULT_PANEL_PALETTE.info],
525
- [' Ahead ', DEFAULT_PANEL_PALETTE.label],
526
- [String(this.data.ahead), this.data.ahead > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
527
- [' Behind ', DEFAULT_PANEL_PALETTE.label],
528
- [String(this.data.behind), this.data.behind > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
529
- [' Status ', DEFAULT_PANEL_PALETTE.label],
530
- [isDirty ? 'dirty' : 'clean', isDirty ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.good],
531
- ]),
532
- ],
533
- } as const;
534
- const selectedSection = { title: 'Selected', lines: selectedLines } as const;
535
- const workspaceSection = resolveScrollablePanelSection(width, height, {
536
- intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
537
- footerLines: [
538
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' diff', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
539
- ],
540
- palette: DEFAULT_PANEL_PALETTE,
541
- beforeSections: [summarySection],
542
- section: {
543
- title: 'Workspace',
544
- scrollableLines: rows,
545
- selectedIndex: selectedRowIndex,
546
- scrollOffset: this.scrollOffset,
547
- minRows: 8,
548
- },
549
- afterSections: [selectedSection],
550
- });
551
- this.scrollOffset = workspaceSection.scrollOffset;
552
-
553
- return buildPanelWorkspace(width, height, {
554
- title: ' Git',
555
- intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
556
- sections: [
557
- summarySection,
558
- workspaceSection.section.lines.length > 0 ? workspaceSection.section : { title: 'Workspace', lines: buildEmptyState(width, ' No git rows', 'This repository has no files or commits to display yet.', [], DEFAULT_PANEL_PALETTE) },
559
- selectedSection,
560
- ],
561
- footerLines: [
562
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' diff', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
563
- ],
564
- palette: DEFAULT_PANEL_PALETTE,
565
- });
566
- }
567
- return buildPanelWorkspace(width, height, {
568
- title: ' Git',
569
- intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
570
- sections: [
571
- {
572
- lines: buildEmptyState(width, ' No git rows', 'This repository has no files or commits to display yet.', [], DEFAULT_PANEL_PALETTE),
573
- },
574
- ],
575
- palette: DEFAULT_PANEL_PALETTE,
576
- });
577
- }
578
-
579
- /**
580
- * Map an item index in `this.items` to the row index in the rendered row list.
581
- * Header items expand to 2 rows (branch + spacer).
582
- */
583
- private getRowIndexForItem(itemIndex: number): number {
584
- let row = 0;
585
- for (let i = 0; i < itemIndex && i < this.items.length; i++) {
586
- const it = this.items[i];
587
- if (it?.kind === 'header') {
588
- row += 2;
589
- } else {
590
- row += 1;
591
- }
592
- }
593
- return row;
594
- }
595
-
596
- private renderDiff(width: number, height: number): Line[] {
597
- const item = this.items[this.selectedIndex];
598
- const title =
599
- item?.kind === 'file' ? `Diff: ${item.entry.path}` : 'Diff';
600
- const diffLines = this.expandedDiff ?? [];
601
- const renderedLines = diffLines.map((rawLine) => {
602
- const dLine = createEmptyLine(width);
603
- let fg: string;
604
- if (rawLine.startsWith('+') && !rawLine.startsWith('+++')) {
605
- fg = C.diffAdd;
606
- } else if (rawLine.startsWith('-') && !rawLine.startsWith('---')) {
607
- fg = C.diffRemove;
608
- } else if (rawLine.startsWith('@@') || rawLine.startsWith('diff') || rawLine.startsWith('index')) {
609
- fg = C.diffMeta;
610
- } else {
611
- fg = C.diffNeutral;
612
- }
613
- this.paintText(dLine, rawLine, 0, width, fg);
614
- return dLine;
615
- });
616
- const footerLines = [
617
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll', DEFAULT_PANEL_PALETTE.dim], [' Esc/q', DEFAULT_PANEL_PALETTE.info], [' close', DEFAULT_PANEL_PALETTE.dim]]),
618
- ];
619
- const diffSection = resolveScrollablePanelSection(width, height, {
620
- palette: DEFAULT_PANEL_PALETTE,
621
- footerLines,
622
- section: {
623
- title: 'Patch',
624
- scrollableLines: renderedLines,
625
- scrollOffset: this.scrollOffset,
626
- minRows: 1,
627
- },
628
- });
629
- this.scrollOffset = diffSection.scrollOffset;
630
-
631
- return buildPanelWorkspace(width, height, {
632
- title: ` ${title}`,
633
- sections: [diffSection.section],
634
- footerLines,
635
- palette: DEFAULT_PANEL_PALETTE,
636
- });
637
- }
638
- }