@pellux/goodvibes-agent 0.1.2 → 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 +6 -0
- package/LICENSE +21 -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/management-commands.ts +3 -1
- package/src/config/surface.ts +1 -0
- package/src/input/agent-workspace.ts +32 -2
- package/src/input/command-registry.ts +4 -1
- package/src/input/commands/agent-skills-runtime.ts +216 -0
- package/src/input/commands/knowledge.ts +18 -18
- package/src/input/commands/personas-runtime.ts +219 -0
- package/src/input/commands/skills-runtime.ts +7 -2
- package/src/input/commands.ts +4 -0
- package/src/input/panel-integration-actions.ts +0 -52
- package/src/main.ts +2 -1
- 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 +3 -1
- 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/git-panel.ts
DELETED
|
@@ -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
|
-
}
|