@pellux/goodvibes-tui 0.27.0 → 0.29.0
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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
package/src/panels/diff-panel.ts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Line } from '../types/grid.ts';
|
|
6
6
|
import { createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
7
|
+
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
7
8
|
import { BasePanel } from './base-panel.ts';
|
|
9
|
+
import { UI_TONES } from '../renderer/ui-primitives.ts';
|
|
10
|
+
import { FilePreviewPanel } from './file-preview-panel.ts';
|
|
11
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
12
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
8
13
|
import {
|
|
9
14
|
buildBodyText,
|
|
10
15
|
buildEmptyState,
|
|
@@ -13,27 +18,63 @@ import {
|
|
|
13
18
|
buildStyledPanelLine,
|
|
14
19
|
type PanelWorkspaceSection,
|
|
15
20
|
DEFAULT_PANEL_PALETTE,
|
|
21
|
+
extendPalette,
|
|
16
22
|
} from './polish.ts';
|
|
17
23
|
|
|
18
24
|
// ---------------------------------------------------------------------------
|
|
19
|
-
// Colour palette
|
|
25
|
+
// Colour palette — dedicated diff-viewer scheme (like a mini syntax
|
|
26
|
+
// highlighter). Each hex literal is named once and reused for every role
|
|
27
|
+
// that shares it, including the workspace-chrome aliases (info/dim/value/
|
|
28
|
+
// empty) so the raw-hex count never grows. The title band itself is NOT
|
|
29
|
+
// overridden — buildPanelWorkspace always falls back to the canonical
|
|
30
|
+
// DEFAULT_PANEL_PALETTE.headerBg (WO-002: one title band everywhere).
|
|
20
31
|
// ---------------------------------------------------------------------------
|
|
21
32
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const HUNK_BLUE = '#88aaff';
|
|
34
|
+
// Context rows and line-number gutter use the shared theme's muted/dim
|
|
35
|
+
// foreground tones rather than dedicated gray hex literals.
|
|
36
|
+
const CONTEXT_GRAY = UI_TONES.fg.muted;
|
|
37
|
+
const FILENAME_WHITE = '#ffffff';
|
|
38
|
+
const ADD_GREEN = '#00ff88';
|
|
39
|
+
const ADD_BG = '#001a0d';
|
|
40
|
+
const DEL_RED = '#ff4444';
|
|
41
|
+
const DEL_BG = '#1a0000';
|
|
42
|
+
const HUNK_BG = '#0a0a1a';
|
|
43
|
+
const MARKER_GRAY = '#aaaaaa';
|
|
44
|
+
const LINE_NUM_GRAY = UI_TONES.fg.dim;
|
|
45
|
+
const LINE_NUM_ADD = '#00aa55';
|
|
46
|
+
const LINE_NUM_DEL = '#aa2222';
|
|
47
|
+
const TAB_ACTIVE_BG = '#333333';
|
|
48
|
+
const TAB_INACTIVE_GRAY = '#666666';
|
|
49
|
+
const TAB_BG = '#222222';
|
|
50
|
+
const STATUS_BAR_BG = '#444444';
|
|
51
|
+
|
|
52
|
+
const COLOR = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
53
|
+
// Workspace-chrome aliases (title band excluded — no headerBg override)
|
|
54
|
+
info: HUNK_BLUE,
|
|
55
|
+
dim: CONTEXT_GRAY,
|
|
56
|
+
value: FILENAME_WHITE,
|
|
57
|
+
empty: CONTEXT_GRAY,
|
|
58
|
+
|
|
59
|
+
// Domain accents
|
|
60
|
+
addition: ADD_GREEN,
|
|
61
|
+
additionBg: ADD_BG,
|
|
62
|
+
deletion: DEL_RED,
|
|
63
|
+
deletionBg: DEL_BG,
|
|
64
|
+
hunk: HUNK_BLUE,
|
|
65
|
+
hunkBg: HUNK_BG,
|
|
66
|
+
markerText: MARKER_GRAY,
|
|
67
|
+
lineNum: LINE_NUM_GRAY,
|
|
68
|
+
lineNumAdd: LINE_NUM_ADD,
|
|
69
|
+
lineNumDel: LINE_NUM_DEL,
|
|
70
|
+
filename: FILENAME_WHITE,
|
|
71
|
+
tabActive: FILENAME_WHITE,
|
|
72
|
+
tabActiveBg: TAB_ACTIVE_BG,
|
|
73
|
+
tabInactive: TAB_INACTIVE_GRAY,
|
|
74
|
+
tabBg: TAB_BG,
|
|
75
|
+
context: CONTEXT_GRAY,
|
|
76
|
+
statusBar: STATUS_BAR_BG,
|
|
77
|
+
} as const);
|
|
37
78
|
|
|
38
79
|
// ---------------------------------------------------------------------------
|
|
39
80
|
// Types
|
|
@@ -47,6 +88,17 @@ interface DiffEntry {
|
|
|
47
88
|
semanticSummary?: string;
|
|
48
89
|
}
|
|
49
90
|
|
|
91
|
+
/** +added / -removed line counts for a parsed diff entry. */
|
|
92
|
+
function diffStat(entry: DiffEntry): { added: number; removed: number } {
|
|
93
|
+
let added = 0;
|
|
94
|
+
let removed = 0;
|
|
95
|
+
for (const line of entry.lines) {
|
|
96
|
+
if (line.kind === 'addition') added++;
|
|
97
|
+
else if (line.kind === 'deletion') removed++;
|
|
98
|
+
}
|
|
99
|
+
return { added, removed };
|
|
100
|
+
}
|
|
101
|
+
|
|
50
102
|
type LineKind = 'addition' | 'deletion' | 'context' | 'hunk' | 'header';
|
|
51
103
|
|
|
52
104
|
interface ParsedLine {
|
|
@@ -147,9 +199,7 @@ function makeLine(
|
|
|
147
199
|
const LEFT_W = 5;
|
|
148
200
|
const usedForNums = LEFT_W + 1 + LEFT_W + 1 + 2; // 14
|
|
149
201
|
const contentWidth = Math.max(0, width - usedForNums);
|
|
150
|
-
const truncated = content
|
|
151
|
-
? content.slice(0, contentWidth)
|
|
152
|
-
: content;
|
|
202
|
+
const truncated = truncateDisplay(content, contentWidth);
|
|
153
203
|
return buildStyledPanelLine(width, [
|
|
154
204
|
{ text: leftNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
|
|
155
205
|
{ text: ' ', fg: '', bg },
|
|
@@ -161,8 +211,7 @@ function makeLine(
|
|
|
161
211
|
}
|
|
162
212
|
|
|
163
213
|
function renderText(width: number, text: string, fg: string, bg: string, bold = false): Line {
|
|
164
|
-
|
|
165
|
-
return buildStyledPanelLine(width, [{ text: truncated, fg, bg, bold }]);
|
|
214
|
+
return buildStyledPanelLine(width, [{ text: truncateDisplay(text, width), fg, bg, bold }]);
|
|
166
215
|
}
|
|
167
216
|
|
|
168
217
|
// ---------------------------------------------------------------------------
|
|
@@ -177,6 +226,9 @@ export class DiffPanel extends BasePanel {
|
|
|
177
226
|
private selectedFile = 0;
|
|
178
227
|
private scrollOffset = 0;
|
|
179
228
|
|
|
229
|
+
/** Set by the 'o' key; consumed by handlePanelIntegrationAction on the next dispatch. */
|
|
230
|
+
private pendingOpenPreview = false;
|
|
231
|
+
|
|
180
232
|
constructor(workingDirectory: string) {
|
|
181
233
|
super('diff', 'Diff', 'D', 'development');
|
|
182
234
|
this.workingDirectory = workingDirectory;
|
|
@@ -220,9 +272,14 @@ export class DiffPanel extends BasePanel {
|
|
|
220
272
|
const raw = await new Response(proc.stdout).text();
|
|
221
273
|
await proc.exited;
|
|
222
274
|
this.loadRawDiff(raw);
|
|
275
|
+
this.enrichSemanticDiffs(files, ref ?? 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
|
|
223
276
|
}
|
|
224
277
|
|
|
225
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* Run `git diff` and populate all changed files. Self-load entry point for
|
|
280
|
+
* the 'w' (working tree, no ref) and 'h' (vs HEAD) keys. Automatically
|
|
281
|
+
* enriches every loaded file with a semantic-diff summary in the background.
|
|
282
|
+
*/
|
|
226
283
|
async showGitDiff(ref?: string): Promise<void> {
|
|
227
284
|
const args = ['diff', ...(ref ? [ref] : [])];
|
|
228
285
|
const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
|
|
@@ -253,6 +310,73 @@ export class DiffPanel extends BasePanel {
|
|
|
253
310
|
this.selectedFile = Math.min(this.selectedFile, Math.max(0, this.entries.length - 1));
|
|
254
311
|
this.scrollOffset = 0;
|
|
255
312
|
this.markDirty();
|
|
313
|
+
this.enrichSemanticDiffs(newEntries.map((e) => e.filePath), 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Run `git diff --cached` and populate the staged changes. Self-load entry
|
|
318
|
+
* point for the 's' key — its own diff plumbing, independent of the
|
|
319
|
+
* `/diff staged` command handler.
|
|
320
|
+
*/
|
|
321
|
+
async showStagedDiff(): Promise<void> {
|
|
322
|
+
const proc = Bun.spawn(['/bin/sh', '-c', 'git diff --cached'], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
|
|
323
|
+
const [raw, errText] = await Promise.all([
|
|
324
|
+
new Response(proc.stdout).text(),
|
|
325
|
+
new Response(proc.stderr).text(),
|
|
326
|
+
]);
|
|
327
|
+
const exitCode = await proc.exited;
|
|
328
|
+
if (exitCode !== 0) {
|
|
329
|
+
const errorText = errText.trim() || 'git diff --cached failed';
|
|
330
|
+
this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!raw.trim()) {
|
|
334
|
+
this.showDiff('(no staged changes)', '@@ -0,0 +0,0 @@\n No staged changes.');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.loadRawDiff(raw);
|
|
338
|
+
this.enrichSemanticDiffs(this.entries.map((e) => e.filePath), 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Best-effort semantic-diff enrichment for a set of files, run against the
|
|
343
|
+
* before-content at `ref` and the current on-disk after-content. Self-contained
|
|
344
|
+
* diff plumbing — does not depend on the `/diff` command handler.
|
|
345
|
+
*/
|
|
346
|
+
private async enrichSemanticDiffs(files: string[], ref: string): Promise<void> {
|
|
347
|
+
if (files.length === 0) return;
|
|
348
|
+
const { computeSemanticDiff, formatSemanticDiffSummary } = await import('../renderer/semantic-diff.ts');
|
|
349
|
+
const { join, relative: pathRelative } = await import('path');
|
|
350
|
+
const repoRootProc = Bun.spawn(['git', 'rev-parse', '--show-toplevel'], { stdout: 'pipe', cwd: this.workingDirectory });
|
|
351
|
+
await repoRootProc.exited;
|
|
352
|
+
const repoRoot = (await new Response(repoRootProc.stdout).text()).trim() || this.workingDirectory;
|
|
353
|
+
await Promise.allSettled(
|
|
354
|
+
files.map(async (filePath) => {
|
|
355
|
+
try {
|
|
356
|
+
const absPath = filePath.startsWith('/') ? filePath : join(this.workingDirectory, filePath);
|
|
357
|
+
const repoRelPath = filePath.startsWith('/') ? pathRelative(repoRoot, filePath) : filePath;
|
|
358
|
+
const [beforeResult, afterResult] = await Promise.allSettled([
|
|
359
|
+
(async () => {
|
|
360
|
+
const proc = Bun.spawn(
|
|
361
|
+
['git', 'show', `${ref}:${repoRelPath}`],
|
|
362
|
+
{ stdout: 'pipe', stderr: 'pipe', cwd: repoRoot },
|
|
363
|
+
);
|
|
364
|
+
const [text, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
365
|
+
if (exitCode !== 0) throw new Error(`git show failed for ${repoRelPath}`);
|
|
366
|
+
return text;
|
|
367
|
+
})(),
|
|
368
|
+
Bun.file(absPath).text(),
|
|
369
|
+
]);
|
|
370
|
+
if (beforeResult.status !== 'fulfilled' || afterResult.status !== 'fulfilled') return;
|
|
371
|
+
const semanticDiff = await computeSemanticDiff(filePath, beforeResult.value, afterResult.value);
|
|
372
|
+
if (!semanticDiff) return;
|
|
373
|
+
const summary = formatSemanticDiffSummary(semanticDiff);
|
|
374
|
+
if (summary) this.setSemanticSummary(filePath, summary);
|
|
375
|
+
} catch {
|
|
376
|
+
// best-effort only
|
|
377
|
+
}
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
256
380
|
}
|
|
257
381
|
|
|
258
382
|
/**
|
|
@@ -293,12 +417,62 @@ export class DiffPanel extends BasePanel {
|
|
|
293
417
|
case 'up': this.scrollUp(); return true;
|
|
294
418
|
case 'down': this.scrollDown(); return true;
|
|
295
419
|
case 'tab': this.nextFile(); return true;
|
|
420
|
+
// Shift+Tab: most terminals send the bare CSI-Z ("backtab") escape
|
|
421
|
+
// sequence rather than a Tab keypress with a shift modifier, and the
|
|
422
|
+
// input tokenizer passes that sequence through unchanged as the
|
|
423
|
+
// logical key name — so it (and the friendlier aliases some terminals/
|
|
424
|
+
// future tokenizer versions may emit) are matched explicitly here.
|
|
425
|
+
case '\x1b[Z':
|
|
426
|
+
case 'shift-tab':
|
|
427
|
+
case 'backtab': this.prevFile(); return true;
|
|
296
428
|
case 'pageup': this.scrollPageUp(); return true;
|
|
297
429
|
case 'pagedown': this.scrollPageDown(); return true;
|
|
430
|
+
case 'w': void this.showGitDiff(); return true;
|
|
431
|
+
case 'h': void this.showGitDiff('HEAD'); return true;
|
|
432
|
+
case 's': void this.showStagedDiff(); return true;
|
|
433
|
+
case 'o': {
|
|
434
|
+
if (!this.currentEntry()) return false;
|
|
435
|
+
this.pendingOpenPreview = true;
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
298
438
|
default: return false;
|
|
299
439
|
}
|
|
300
440
|
}
|
|
301
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Cross-panel integration hook — 'o' opens the currently selected file in
|
|
444
|
+
* the preview panel via the same open/focus bridge FileExplorerPanel uses
|
|
445
|
+
* (src/input/panel-integration-actions.ts), without this panel needing to
|
|
446
|
+
* know about PanelManager pane/focus mechanics beyond what ctx exposes.
|
|
447
|
+
*/
|
|
448
|
+
handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
449
|
+
if (!this.pendingOpenPreview) return false;
|
|
450
|
+
this.pendingOpenPreview = false;
|
|
451
|
+
const entry = this.currentEntry();
|
|
452
|
+
if (!entry) return false;
|
|
453
|
+
|
|
454
|
+
const pm = ctx.panelManager;
|
|
455
|
+
let previewPanel = pm.getPanel('preview');
|
|
456
|
+
if (previewPanel instanceof FilePreviewPanel) {
|
|
457
|
+
const pane = pm.getPaneOf('preview');
|
|
458
|
+
pm.activateById('preview');
|
|
459
|
+
if (pane) pm.focusPane(pane);
|
|
460
|
+
} else {
|
|
461
|
+
const targetPane: 'top' | 'bottom' = pm.isBottomPaneVisible()
|
|
462
|
+
? (pm.getFocusedPane() === 'top' ? 'bottom' : 'top')
|
|
463
|
+
: 'bottom';
|
|
464
|
+
const opened = pm.open('preview', targetPane);
|
|
465
|
+
pm.show();
|
|
466
|
+
pm.focusPane(targetPane);
|
|
467
|
+
previewPanel = opened instanceof FilePreviewPanel ? opened : null;
|
|
468
|
+
}
|
|
469
|
+
if (previewPanel instanceof FilePreviewPanel) {
|
|
470
|
+
previewPanel.openFile(entry.filePath);
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
302
476
|
private scrollUp(): void {
|
|
303
477
|
if (this.scrollOffset > 0) {
|
|
304
478
|
this.scrollOffset--;
|
|
@@ -336,6 +510,13 @@ export class DiffPanel extends BasePanel {
|
|
|
336
510
|
this.markDirty();
|
|
337
511
|
}
|
|
338
512
|
|
|
513
|
+
private prevFile(): void {
|
|
514
|
+
if (this.entries.length === 0) return;
|
|
515
|
+
this.selectedFile = (this.selectedFile - 1 + this.entries.length) % this.entries.length;
|
|
516
|
+
this.scrollOffset = 0;
|
|
517
|
+
this.markDirty();
|
|
518
|
+
}
|
|
519
|
+
|
|
339
520
|
private currentEntry(): DiffEntry | null {
|
|
340
521
|
return this.entries[this.selectedFile] ?? null;
|
|
341
522
|
}
|
|
@@ -351,26 +532,20 @@ export class DiffPanel extends BasePanel {
|
|
|
351
532
|
if (this.entries.length === 0) {
|
|
352
533
|
return buildPanelWorkspace(width, height, {
|
|
353
534
|
title: 'Diff Workspace',
|
|
354
|
-
palette:
|
|
355
|
-
...DEFAULT_PANEL_PALETTE,
|
|
356
|
-
info: COLOR.hunk,
|
|
357
|
-
dim: COLOR.context,
|
|
358
|
-
value: COLOR.filename,
|
|
359
|
-
},
|
|
535
|
+
palette: COLOR,
|
|
360
536
|
sections: [{
|
|
361
537
|
title: 'Diff',
|
|
362
538
|
lines: buildEmptyState(
|
|
363
539
|
width,
|
|
364
540
|
' No diff to display.',
|
|
365
|
-
'Load a git diff or select a changed file to populate the workspace.',
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
},
|
|
541
|
+
'Load a git diff or select a changed file to populate the workspace. w=working h=HEAD s=staged self-load right here.',
|
|
542
|
+
// WO-160: corrected — bare /diff loads files changed *this
|
|
543
|
+
// session* (falling back to the full HEAD diff when none are
|
|
544
|
+
// tracked yet), not the working-tree diff the w key already
|
|
545
|
+
// loads in-panel; the old summary text described the wrong
|
|
546
|
+
// command.
|
|
547
|
+
[{ command: '/diff', summary: 'load files changed this session into the diff workspace (falls back to the HEAD diff if none yet)' }],
|
|
548
|
+
COLOR,
|
|
374
549
|
),
|
|
375
550
|
}],
|
|
376
551
|
});
|
|
@@ -383,20 +558,10 @@ export class DiffPanel extends BasePanel {
|
|
|
383
558
|
|
|
384
559
|
const compact = height <= 12;
|
|
385
560
|
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)
|
|
561
|
+
? buildBodyText(width, `Semantic summary: ${entry.semanticSummary}`, COLOR, COLOR.context)
|
|
391
562
|
: [];
|
|
392
563
|
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
|
-
},
|
|
564
|
+
palette: COLOR,
|
|
400
565
|
footerLines: [this.renderStatusBar(width, entry)],
|
|
401
566
|
beforeSections: [
|
|
402
567
|
{
|
|
@@ -431,13 +596,7 @@ export class DiffPanel extends BasePanel {
|
|
|
431
596
|
];
|
|
432
597
|
return buildPanelWorkspace(width, height, {
|
|
433
598
|
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
|
-
},
|
|
599
|
+
palette: COLOR,
|
|
441
600
|
sections,
|
|
442
601
|
footerLines: [this.renderStatusBar(width, entry)],
|
|
443
602
|
});
|
|
@@ -448,44 +607,60 @@ export class DiffPanel extends BasePanel {
|
|
|
448
607
|
|
|
449
608
|
private renderTabBar(width: number): Line {
|
|
450
609
|
const cells: Line = [];
|
|
610
|
+
const push = (ch: string, fg: string, bg: string, bold = false): void => {
|
|
611
|
+
const cw = getDisplayWidth(ch);
|
|
612
|
+
if (cells.length + cw > width) return;
|
|
613
|
+
cells.push(createStyledCell(ch, { fg, bg, bold }));
|
|
614
|
+
if (cw === 2 && cells.length < width) cells.push(createStyledCell('', { fg, bg }));
|
|
615
|
+
};
|
|
451
616
|
|
|
452
617
|
for (let i = 0; i < this.entries.length; i++) {
|
|
453
618
|
const entry = this.entries[i]!;
|
|
454
619
|
const active = i === this.selectedFile;
|
|
455
|
-
const
|
|
620
|
+
const stat = diffStat(entry);
|
|
456
621
|
const fg = active ? COLOR.tabActive : COLOR.tabInactive;
|
|
457
|
-
const bg = active ?
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
622
|
+
const bg = active ? COLOR.tabActiveBg : COLOR.tabBg;
|
|
623
|
+
// Active file gets a leading marker; every tab shows +adds/-dels at a glance.
|
|
624
|
+
const marker = active ? '▸ ' : ' ';
|
|
625
|
+
const label = `${marker}${basename(entry.filePath)} `;
|
|
626
|
+
for (const ch of label) push(ch, fg, bg, active);
|
|
627
|
+
if (stat.added > 0) for (const ch of `+${stat.added}`) push(ch, COLOR.addition, bg, active);
|
|
628
|
+
if (stat.added > 0 && stat.removed > 0) push(' ', fg, bg);
|
|
629
|
+
if (stat.removed > 0) for (const ch of `-${stat.removed}`) push(ch, COLOR.deletion, bg, active);
|
|
630
|
+
push(' ', fg, bg);
|
|
631
|
+
push('│', COLOR.lineNum, COLOR.tabBg);
|
|
467
632
|
}
|
|
468
633
|
|
|
469
|
-
// Fill remaining
|
|
470
634
|
while (cells.length < width) {
|
|
471
635
|
cells.push(createStyledCell(' ', { fg: '', bg: COLOR.tabBg }));
|
|
472
636
|
}
|
|
473
|
-
|
|
474
637
|
return cells.slice(0, width);
|
|
475
638
|
}
|
|
476
639
|
|
|
477
640
|
// ── Status bar ───────────────────────────────────────────────────────────
|
|
478
641
|
|
|
479
642
|
private renderStatusBar(width: number, entry: DiffEntry | null): Line {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
const text
|
|
488
|
-
|
|
643
|
+
if (!entry) {
|
|
644
|
+
return buildStyledPanelLine(width, [{ text: ' No file', fg: COLOR.tabInactive, bg: COLOR.statusBar }], { fillBg: COLOR.statusBar });
|
|
645
|
+
}
|
|
646
|
+
const stat = diffStat(entry);
|
|
647
|
+
// Keep the file path display-width-aware so a long/wide path can't overflow.
|
|
648
|
+
const pathBudget = Math.max(8, Math.floor(width / 2));
|
|
649
|
+
const fileInfo = truncateDisplay(entry.filePath, pathBudget);
|
|
650
|
+
const segments: Array<{ text: string; fg: string; bg?: string; bold?: boolean }> = [
|
|
651
|
+
{ text: ` ${fileInfo} `, fg: COLOR.filename, bg: COLOR.statusBar, bold: true },
|
|
652
|
+
{ text: `[${this.selectedFile + 1}/${this.entries.length}]`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
653
|
+
{ text: ' +', fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
654
|
+
{ text: String(stat.added), fg: COLOR.addition, bg: COLOR.statusBar },
|
|
655
|
+
{ text: ' -', fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
656
|
+
{ text: String(stat.removed), fg: COLOR.deletion, bg: COLOR.statusBar },
|
|
657
|
+
{ text: ` L${this.scrollOffset + 1}/${entry.lines.length}`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
658
|
+
{ text: ' Tab/S-Tab file o open w/h/s load ↑/↓ scroll', fg: COLOR.context, bg: COLOR.statusBar },
|
|
659
|
+
];
|
|
660
|
+
if (entry.semanticSummary) {
|
|
661
|
+
segments.push({ text: ` ◈ ${entry.semanticSummary}`, fg: COLOR.hunk, bg: COLOR.statusBar });
|
|
662
|
+
}
|
|
663
|
+
return buildStyledPanelLine(width, segments, { fillBg: COLOR.statusBar });
|
|
489
664
|
}
|
|
490
665
|
|
|
491
666
|
// ── Parsed line ──────────────────────────────────────────────────────────
|
|
@@ -496,13 +671,13 @@ export class DiffPanel extends BasePanel {
|
|
|
496
671
|
|
|
497
672
|
switch (pl.kind) {
|
|
498
673
|
case 'addition':
|
|
499
|
-
return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition,
|
|
674
|
+
return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, COLOR.additionBg, COLOR.lineNumAdd, true);
|
|
500
675
|
case 'deletion':
|
|
501
|
-
return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion,
|
|
676
|
+
return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, COLOR.deletionBg, COLOR.lineNumDel, false);
|
|
502
677
|
case 'hunk':
|
|
503
|
-
return renderText(width, pl.text, COLOR.hunk,
|
|
678
|
+
return renderText(width, pl.text, COLOR.hunk, COLOR.hunkBg, false);
|
|
504
679
|
case 'header':
|
|
505
|
-
return renderText(width, pl.text, COLOR.
|
|
680
|
+
return renderText(width, pl.text, COLOR.markerText, '', false);
|
|
506
681
|
case 'context':
|
|
507
682
|
default:
|
|
508
683
|
return makeLine(width, left, right, ` ${pl.text}`, COLOR.context, '', COLOR.lineNum, false);
|