@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.
- package/CHANGELOG.md +14 -0
- package/README.md +12 -1
- package/docs/README.md +2 -0
- package/docs/getting-started.md +19 -1
- package/docs/release-and-publishing.md +3 -1
- package/package.json +10 -1
- package/src/agent/persona-registry.ts +379 -0
- package/src/agent/skill-registry.ts +360 -0
- package/src/audio/spoken-turn-model-routing.ts +2 -1
- package/src/cli/agent-knowledge-command.ts +525 -0
- package/src/cli/help.ts +35 -0
- package/src/cli/management-commands.ts +3 -1
- package/src/cli/management.ts +33 -9
- package/src/cli/parser.ts +7 -0
- package/src/cli/types.ts +3 -0
- package/src/config/surface.ts +1 -0
- package/src/input/agent-workspace.ts +33 -3
- package/src/input/command-registry.ts +4 -1
- package/src/input/commands/agent-skills-runtime.ts +216 -0
- package/src/input/commands/delegation-runtime.ts +129 -0
- package/src/input/commands/knowledge.ts +18 -18
- package/src/input/commands/personas-runtime.ts +219 -0
- package/src/input/commands/shell-core.ts +9 -6
- package/src/input/commands/skills-runtime.ts +7 -2
- package/src/input/commands.ts +6 -0
- package/src/input/panel-integration-actions.ts +0 -52
- package/src/input/submission-router.ts +1 -1
- package/src/main.ts +2 -1
- package/src/panels/builtin/agent.ts +0 -14
- package/src/panels/builtin/session.ts +4 -3
- package/src/panels/index.ts +0 -5
- package/src/panels/orchestration-panel.ts +4 -5
- package/src/panels/qr-panel.ts +3 -2
- package/src/panels/tasks-panel.ts +4 -4
- package/src/renderer/agent-workspace.ts +2 -0
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +6 -2
- package/src/runtime/bootstrap-core.ts +8 -4
- package/src/runtime/bootstrap-shell.ts +5 -2
- package/src/runtime/bootstrap.ts +10 -2
- package/src/runtime/cloudflare-control-plane.ts +2 -1
- package/src/version.ts +1 -1
- package/src/daemon/cli.ts +0 -55
- package/src/daemon/safe-serve.ts +0 -61
- package/src/panels/diff-panel.ts +0 -520
- package/src/panels/file-explorer-panel.ts +0 -584
- package/src/panels/file-preview-panel.ts +0 -434
- package/src/panels/git-panel.ts +0 -638
- package/src/panels/sandbox-panel.ts +0 -283
- package/src/panels/symbol-outline-panel.ts +0 -486
- package/src/panels/worktree-panel.ts +0 -182
- package/src/panels/wrfc-panel.ts +0 -609
package/src/panels/diff-panel.ts
DELETED
|
@@ -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
|
-
}
|