@pellux/goodvibes-tui 0.26.0 → 0.28.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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -9,14 +9,15 @@ import { createEmptyLine } from '../types/grid.ts';
|
|
|
9
9
|
import { BasePanel } from './base-panel.ts';
|
|
10
10
|
import {
|
|
11
11
|
buildEmptyState,
|
|
12
|
+
buildKeyboardHints,
|
|
12
13
|
buildPanelLine,
|
|
13
14
|
buildSearchInputLine,
|
|
14
|
-
|
|
15
|
+
buildTreeRow,
|
|
15
16
|
buildPanelWorkspace,
|
|
16
17
|
resolveScrollablePanelSection,
|
|
17
18
|
DEFAULT_PANEL_PALETTE,
|
|
19
|
+
extendPalette,
|
|
18
20
|
} from './polish.ts';
|
|
19
|
-
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
20
21
|
import {
|
|
21
22
|
getPanelSearchFocusTransition,
|
|
22
23
|
isPanelSearchBackspace,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
isPanelSearchCommit,
|
|
25
26
|
isPanelSearchPrintable,
|
|
26
27
|
} from './search-focus.ts';
|
|
28
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
27
29
|
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
29
31
|
// Constants
|
|
@@ -83,15 +85,12 @@ interface TreeNode {
|
|
|
83
85
|
// ---------------------------------------------------------------------------
|
|
84
86
|
// Colour palette
|
|
85
87
|
// ---------------------------------------------------------------------------
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const CLR_SEARCH_FG = '#ff79c6';
|
|
93
|
-
const CLR_TOGGLE = '244'; // ▶/▼ toggle arrows
|
|
94
|
-
const CLR_ICON = '244'; // file type icon
|
|
88
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
89
|
+
dirColor: '#00ffff', // cyan — directories
|
|
90
|
+
fileColor: '#e0e0e0', // near-white — files
|
|
91
|
+
sizeColor: '244', // dim grey — sizes
|
|
92
|
+
cursorBg: '#1a2a3a', // cursor background
|
|
93
|
+
});
|
|
95
94
|
|
|
96
95
|
// ---------------------------------------------------------------------------
|
|
97
96
|
// Helpers
|
|
@@ -214,21 +213,29 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
214
213
|
? `Filter: ${this.searchQuery} (/ or up at top to edit)`
|
|
215
214
|
: `Root: ${relative(this.workingDirectory, this.rootPath) || '.'} (/ or up at top to search)`;
|
|
216
215
|
|
|
216
|
+
// Context-aware hints: only surface keys that act in the current mode.
|
|
217
|
+
const hintsLine = this.searchMode
|
|
218
|
+
? buildKeyboardHints(width, [
|
|
219
|
+
{ keys: 'type', label: 'filter' },
|
|
220
|
+
{ keys: '↑/↓', label: 'move' },
|
|
221
|
+
{ keys: 'Enter', label: 'keep results' },
|
|
222
|
+
{ keys: 'Esc', label: 'clear' },
|
|
223
|
+
], C)
|
|
224
|
+
: buildKeyboardHints(width, [
|
|
225
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
226
|
+
{ keys: 'Enter/→', label: 'expand' },
|
|
227
|
+
{ keys: '←', label: 'collapse' },
|
|
228
|
+
{ keys: '/', label: 'search' },
|
|
229
|
+
{ keys: 'r', label: 'refresh' },
|
|
230
|
+
], C);
|
|
231
|
+
|
|
217
232
|
if (this.flat.length === 0) {
|
|
218
233
|
return buildPanelWorkspace(width, height, {
|
|
219
234
|
title: ' Explorer',
|
|
220
235
|
intro: 'Browse the project tree, expand directories, and search for paths.',
|
|
221
236
|
sections: [
|
|
222
237
|
{
|
|
223
|
-
lines: buildEmptyState(
|
|
224
|
-
width,
|
|
225
|
-
' No files found',
|
|
226
|
-
this.searchQuery
|
|
227
|
-
? 'No files or directories match the current search.'
|
|
228
|
-
: 'This root did not produce any visible files after the explorer filters were applied.',
|
|
229
|
-
[],
|
|
230
|
-
DEFAULT_PANEL_PALETTE,
|
|
231
|
-
),
|
|
238
|
+
lines: buildEmptyState(width, ' No files found', this.searchQuery ? 'No files or directories match the current search.' : 'This root did not produce any visible files after the explorer filters were applied.', this.searchQuery ? [{ command: 'Esc', summary: 'clear the filter to show the full tree again' }] : [{ command: 'r', summary: 'rescan this root from disk' }], C),
|
|
232
239
|
},
|
|
233
240
|
],
|
|
234
241
|
footerLines: [
|
|
@@ -236,7 +243,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
236
243
|
active: this.searchMode,
|
|
237
244
|
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
238
245
|
}),
|
|
239
|
-
|
|
246
|
+
hintsLine,
|
|
240
247
|
],
|
|
241
248
|
palette: DEFAULT_PANEL_PALETTE,
|
|
242
249
|
});
|
|
@@ -278,7 +285,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
278
285
|
active: this.searchMode,
|
|
279
286
|
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
280
287
|
}),
|
|
281
|
-
|
|
288
|
+
hintsLine,
|
|
282
289
|
],
|
|
283
290
|
palette: DEFAULT_PANEL_PALETTE,
|
|
284
291
|
beforeSections: [summarySection],
|
|
@@ -286,24 +293,17 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
286
293
|
title: 'Tree',
|
|
287
294
|
scrollableLines: this.flat.map((node, absoluteIdx) => {
|
|
288
295
|
const isCursor = absoluteIdx === this.cursor;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
node.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const sizeStr = ` ${formatSize(node.size)}`;
|
|
301
|
-
const contentWidth = getDisplayWidth(indent) + 2 + getDisplayWidth(node.name);
|
|
302
|
-
const gap = Math.max(1, width - contentWidth - getDisplayWidth(sizeStr));
|
|
303
|
-
segments.push({ text: ' '.repeat(gap), fg: baseFg });
|
|
304
|
-
segments.push({ text: sizeStr, fg: CLR_SIZE, dim: true });
|
|
305
|
-
}
|
|
306
|
-
return buildSelectablePanelLine(width, segments, { selected: isCursor, selectedBg: baseBg, fillFg: baseFg });
|
|
296
|
+
return buildTreeRow(width, {
|
|
297
|
+
depth: node.depth,
|
|
298
|
+
label: node.name,
|
|
299
|
+
icon: node.isDir ? undefined : fileIcon(node.name),
|
|
300
|
+
expandable: node.isDir,
|
|
301
|
+
expanded: node.expanded,
|
|
302
|
+
labelColor: node.isDir ? C.dirColor : C.fileColor,
|
|
303
|
+
metadata: (!node.isDir && node.size > 0)
|
|
304
|
+
? [{ text: formatSize(node.size), fg: C.sizeColor }]
|
|
305
|
+
: undefined,
|
|
306
|
+
}, C, { selected: isCursor, selectedBg: C.cursorBg });
|
|
307
307
|
}),
|
|
308
308
|
selectedIndex: this.cursor,
|
|
309
309
|
scrollOffset: this.scrollTop,
|
|
@@ -325,7 +325,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
325
325
|
active: this.searchMode,
|
|
326
326
|
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
327
327
|
}),
|
|
328
|
-
|
|
328
|
+
hintsLine,
|
|
329
329
|
],
|
|
330
330
|
palette: DEFAULT_PANEL_PALETTE,
|
|
331
331
|
});
|
|
@@ -342,7 +342,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
342
342
|
this.cacheValid = true;
|
|
343
343
|
});
|
|
344
344
|
} catch (err) {
|
|
345
|
-
this.setError(
|
|
345
|
+
this.setError(summarizeError(err));
|
|
346
346
|
}
|
|
347
347
|
this.markDirty();
|
|
348
348
|
})();
|
|
@@ -8,11 +8,13 @@ import { SyntaxHighlighter, type SyntaxToken } from '../renderer/syntax-highligh
|
|
|
8
8
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
9
9
|
import {
|
|
10
10
|
buildEmptyState,
|
|
11
|
+
buildKeyboardHints,
|
|
11
12
|
buildPanelLine,
|
|
12
13
|
buildPanelWorkspace,
|
|
13
14
|
resolveScrollablePanelSection,
|
|
14
15
|
DEFAULT_PANEL_PALETTE,
|
|
15
16
|
} from './polish.ts';
|
|
17
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
16
18
|
|
|
17
19
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
18
20
|
|
|
@@ -144,7 +146,7 @@ export class FilePreviewPanel extends BasePanel {
|
|
|
144
146
|
this.clampScroll(0);
|
|
145
147
|
});
|
|
146
148
|
} catch (err) {
|
|
147
|
-
this.setError(
|
|
149
|
+
this.setError(summarizeError(err));
|
|
148
150
|
}
|
|
149
151
|
this.markDirty();
|
|
150
152
|
}
|
|
@@ -272,7 +274,11 @@ export class FilePreviewPanel extends BasePanel {
|
|
|
272
274
|
],
|
|
273
275
|
} as const;
|
|
274
276
|
const footerLines = [
|
|
275
|
-
|
|
277
|
+
buildKeyboardHints(width, [
|
|
278
|
+
{ keys: '↑/↓', label: 'scroll' },
|
|
279
|
+
{ keys: 'PgUp/PgDn', label: 'page' },
|
|
280
|
+
{ keys: 'Home/End', label: 'top/bottom' },
|
|
281
|
+
], DEFAULT_PANEL_PALETTE),
|
|
276
282
|
];
|
|
277
283
|
const fullCode = this.fileLines.join('\n');
|
|
278
284
|
const hlLines = this.fenceTag
|
|
@@ -318,8 +324,10 @@ export class FilePreviewPanel extends BasePanel {
|
|
|
318
324
|
buildPanelLine(width, [
|
|
319
325
|
[' Lines ', DEFAULT_PANEL_PALETTE.label],
|
|
320
326
|
[String(this.fileLines.length), DEFAULT_PANEL_PALETTE.value],
|
|
321
|
-
['
|
|
327
|
+
[' Viewing ', DEFAULT_PANEL_PALETTE.label],
|
|
322
328
|
[`${window.start + 1}-${window.end}`, DEFAULT_PANEL_PALETTE.info],
|
|
329
|
+
[' Lang ', DEFAULT_PANEL_PALETTE.label],
|
|
330
|
+
[this.fenceTag || 'text', this.fenceTag ? DEFAULT_PANEL_PALETTE.value : DEFAULT_PANEL_PALETTE.dim],
|
|
323
331
|
]),
|
|
324
332
|
],
|
|
325
333
|
},
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Open via /forensics or the panel picker.
|
|
8
8
|
*/
|
|
9
9
|
import type { Line } from '../types/grid.ts';
|
|
10
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
10
11
|
import type { ForensicsRegistry } from '@/runtime/index.ts';
|
|
11
12
|
import type { FailureReport, CausalChainEntry, PhaseTimingEntry } from '@/runtime/index.ts';
|
|
12
13
|
import { ForensicsDataPanel } from '@/runtime/index.ts';
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
DEFAULT_PANEL_PALETTE,
|
|
21
22
|
type PanelWorkspaceSection,
|
|
22
23
|
} from './polish.ts';
|
|
24
|
+
import { formatShortDuration } from '../utils/format-duration.ts';
|
|
23
25
|
|
|
24
26
|
// ── Colour palette ────────────────────────────────────────────────────────────
|
|
25
27
|
const C = {
|
|
@@ -59,9 +61,7 @@ function fmtTime(ts: number): string {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function fmtDuration(ms: number | undefined): string {
|
|
62
|
-
|
|
63
|
-
if (ms < 1000) return `${ms}ms`;
|
|
64
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
64
|
+
return formatShortDuration(ms);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function classificationColor(cls: FailureReport['classification']): string {
|
|
@@ -195,7 +195,21 @@ export class ForensicsPanel extends BasePanel {
|
|
|
195
195
|
// ── List view ──────────────────────────────────────────────────────────────
|
|
196
196
|
|
|
197
197
|
private _renderList(lines: Line[], reports: FailureReport[], width: number, height: number, intro: string): void {
|
|
198
|
+
// Posture summary: how many reports and how many are hard errors, so the
|
|
199
|
+
// most important signal (recent failures) is visible before scrolling.
|
|
200
|
+
const errorCount = reports.filter((r) => classificationColor(r.classification) === C.classError).length;
|
|
201
|
+
const newest = reports[0];
|
|
202
|
+
const summaryLine = buildPanelLine(width, [
|
|
203
|
+
[' reports ', C.label],
|
|
204
|
+
[String(reports.length), C.value],
|
|
205
|
+
[' errors ', C.label],
|
|
206
|
+
[String(errorCount), errorCount > 0 ? C.classError : C.classOk],
|
|
207
|
+
[' newest ', C.label],
|
|
208
|
+
[newest ? fmtTime(newest.generatedAt) : 'n/a', C.timestamp],
|
|
209
|
+
]);
|
|
210
|
+
|
|
198
211
|
const reportRows: Line[] = [
|
|
212
|
+
summaryLine,
|
|
199
213
|
buildPanelLine(width, [[' ID TIME CLASS SUMMARY', C.label]]),
|
|
200
214
|
];
|
|
201
215
|
|
|
@@ -204,12 +218,12 @@ export class ForensicsPanel extends BasePanel {
|
|
|
204
218
|
const isSelected = i === this._selectedIndex;
|
|
205
219
|
const bg = isSelected ? C.selectBg : undefined;
|
|
206
220
|
|
|
207
|
-
const idStr = report.id
|
|
221
|
+
const idStr = fitDisplay(report.id, 8);
|
|
208
222
|
const timeStr = fmtTime(report.generatedAt);
|
|
209
|
-
const cls = report.classification
|
|
223
|
+
const cls = fitDisplay(report.classification, 20);
|
|
210
224
|
const clsColor = classificationColor(report.classification);
|
|
211
225
|
const summaryMax = Math.max(0, width - 42);
|
|
212
|
-
const summaryStr = report.summary
|
|
226
|
+
const summaryStr = truncateDisplay(report.summary, summaryMax);
|
|
213
227
|
|
|
214
228
|
const segs: Array<[string, string, string?]> = [
|
|
215
229
|
[isSelected ? '▸' : ' ', C.jumpLink, bg],
|
|
@@ -226,12 +240,12 @@ export class ForensicsPanel extends BasePanel {
|
|
|
226
240
|
section: {
|
|
227
241
|
title: 'Reports',
|
|
228
242
|
scrollableLines: reportRows,
|
|
229
|
-
selectedIndex: this._selectedIndex +
|
|
243
|
+
selectedIndex: this._selectedIndex + 2,
|
|
230
244
|
scrollOffset: this._scrollOffset,
|
|
231
245
|
minRows: 4,
|
|
232
246
|
appendWindowSummary: {
|
|
233
247
|
dimColor: C.label,
|
|
234
|
-
formatter: () => buildPanelLine(width, [[` [${this._selectedIndex + 1}/${reports.length}]
|
|
248
|
+
formatter: () => buildPanelLine(width, [[` [${this._selectedIndex + 1}/${reports.length}] ↑/↓ navigate Enter expand report`, C.label]]),
|
|
235
249
|
},
|
|
236
250
|
},
|
|
237
251
|
});
|
|
@@ -262,13 +276,13 @@ export class ForensicsPanel extends BasePanel {
|
|
|
262
276
|
]));
|
|
263
277
|
detailLines.push(buildPanelLine(width, [
|
|
264
278
|
[' Summary: ', C.label],
|
|
265
|
-
[report.summary.
|
|
279
|
+
[truncateDisplay(report.summary, Math.max(0, width - 11)), C.summaryText],
|
|
266
280
|
]));
|
|
267
281
|
|
|
268
282
|
if (report.errorMessage) {
|
|
269
283
|
detailLines.push(buildPanelLine(width, [
|
|
270
284
|
[' Error: ', C.label],
|
|
271
|
-
[report.errorMessage.
|
|
285
|
+
[truncateDisplay(report.errorMessage, Math.max(0, width - 11)), C.classError],
|
|
272
286
|
]));
|
|
273
287
|
}
|
|
274
288
|
if (report.stopReason) {
|
|
@@ -339,8 +353,8 @@ export class ForensicsPanel extends BasePanel {
|
|
|
339
353
|
const statusChar = pt.success ? '✓' : '✕';
|
|
340
354
|
const statusColor = pt.success ? C.phaseOk : C.phaseFail;
|
|
341
355
|
const dur = fmtDuration(pt.durationMs);
|
|
342
|
-
const phaseLabel = pt.phase
|
|
343
|
-
const errPart = pt.error ? ` ${pt.error
|
|
356
|
+
const phaseLabel = fitDisplay(pt.phase, 14);
|
|
357
|
+
const errPart = pt.error ? ` ${truncateDisplay(pt.error, Math.max(0, width - 32))}` : '';
|
|
344
358
|
lines.push(buildPanelLine(width, [
|
|
345
359
|
[' ', C.dim],
|
|
346
360
|
[statusChar + ' ', statusColor],
|
|
@@ -358,7 +372,7 @@ export class ForensicsPanel extends BasePanel {
|
|
|
358
372
|
lines.push(buildPanelLine(width, [
|
|
359
373
|
[prefix, color],
|
|
360
374
|
[`${timeStr} `, C.timestamp],
|
|
361
|
-
[entry.description
|
|
375
|
+
[truncateDisplay(entry.description, descMax), color],
|
|
362
376
|
]));
|
|
363
377
|
}
|
|
364
378
|
}
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { BasePanel } from './base-panel.ts';
|
|
2
2
|
import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
3
4
|
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
4
5
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
6
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
7
|
import {
|
|
7
8
|
buildEmptyState,
|
|
9
|
+
buildKeyboardHints,
|
|
8
10
|
buildPanelLine,
|
|
9
11
|
buildPanelWorkspace,
|
|
10
12
|
resolveScrollablePanelSection,
|
|
13
|
+
buildSelectablePanelLine,
|
|
11
14
|
buildStyledPanelLine,
|
|
12
15
|
DEFAULT_PANEL_PALETTE,
|
|
16
|
+
extendPalette,
|
|
13
17
|
} from './polish.ts';
|
|
14
18
|
|
|
15
19
|
// ---------------------------------------------------------------------------
|
|
@@ -56,25 +60,17 @@ const MIN_VISIBLE_DIFF_LINES = 5;
|
|
|
56
60
|
// Colors
|
|
57
61
|
// ---------------------------------------------------------------------------
|
|
58
62
|
|
|
59
|
-
const C = {
|
|
63
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
60
64
|
branch: '#00d7ff',
|
|
61
|
-
clean: '#5fd700',
|
|
62
|
-
dirty: '#ffaf00',
|
|
63
|
-
ahead: '#5fd700',
|
|
64
|
-
behind: '#ff5f5f',
|
|
65
65
|
sectionHeader: '244',
|
|
66
|
-
staged: '#5fd700',
|
|
67
|
-
unstaged: '#ff5f5f',
|
|
68
66
|
commit: '250',
|
|
69
67
|
commitHash: '238',
|
|
70
68
|
commitAuthor: '244',
|
|
71
69
|
selected: '#1c1c1c',
|
|
72
70
|
selectedFg: '#ffffff',
|
|
73
|
-
diffAdd: '#5fd700',
|
|
74
|
-
diffRemove: '#ff5f5f',
|
|
75
71
|
diffMeta: '#5f87ff',
|
|
76
72
|
diffNeutral: '250',
|
|
77
|
-
}
|
|
73
|
+
});
|
|
78
74
|
|
|
79
75
|
// ---------------------------------------------------------------------------
|
|
80
76
|
// GitPanel
|
|
@@ -107,11 +103,16 @@ export class GitPanel extends BasePanel {
|
|
|
107
103
|
private loading = true;
|
|
108
104
|
private error: string | null = null;
|
|
109
105
|
|
|
110
|
-
constructor(workingDirectory: string) {
|
|
106
|
+
constructor(workingDirectory: string, private readonly requestRender: () => void = () => {}) {
|
|
111
107
|
super('git', 'Git', 'G', 'development');
|
|
112
108
|
this.workingDirectory = workingDirectory;
|
|
113
109
|
}
|
|
114
110
|
|
|
111
|
+
private _markDirtyAndRender(): void {
|
|
112
|
+
this.markDirty();
|
|
113
|
+
this.requestRender();
|
|
114
|
+
}
|
|
115
|
+
|
|
115
116
|
// ---------------------------------------------------------------------------
|
|
116
117
|
// Lifecycle
|
|
117
118
|
// ---------------------------------------------------------------------------
|
|
@@ -179,7 +180,7 @@ export class GitPanel extends BasePanel {
|
|
|
179
180
|
this.error = null;
|
|
180
181
|
this.rebuildItems();
|
|
181
182
|
// Do not clear expandedDiff during auto-refresh — only clear on explicit user action
|
|
182
|
-
this.
|
|
183
|
+
this._markDirtyAndRender();
|
|
183
184
|
} catch (err) {
|
|
184
185
|
const msg = summarizeError(err);
|
|
185
186
|
// If the failure is because this directory isn't a git repo, auto-initialise
|
|
@@ -203,7 +204,7 @@ export class GitPanel extends BasePanel {
|
|
|
203
204
|
}
|
|
204
205
|
this.loading = false;
|
|
205
206
|
logger.debug('GitPanel: refresh failed', { error: this.error });
|
|
206
|
-
this.
|
|
207
|
+
this._markDirtyAndRender();
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
|
|
@@ -352,7 +353,7 @@ export class GitPanel extends BasePanel {
|
|
|
352
353
|
return this.renderMessage(width, height, 'Loading git status...', C.branch);
|
|
353
354
|
}
|
|
354
355
|
if (this.error) {
|
|
355
|
-
return this.renderMessage(width, height, `Git error: ${this.error}`, C.
|
|
356
|
+
return this.renderMessage(width, height, `Git error: ${this.error}`, C.bad);
|
|
356
357
|
}
|
|
357
358
|
// I3: spinner during openDiff() async fetch
|
|
358
359
|
if (this.loadingState === 'loading') {
|
|
@@ -390,77 +391,49 @@ export class GitPanel extends BasePanel {
|
|
|
390
391
|
}
|
|
391
392
|
|
|
392
393
|
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
394
|
const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
395
|
+
const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
|
|
396
|
+
{ text: ' ⎇ ', fg: C.sectionHeader },
|
|
397
|
+
{ text: this.data.branch, fg: C.branch, bold: true },
|
|
398
|
+
];
|
|
399
|
+
if (this.data.ahead > 0) segments.push({ text: ` ↑${this.data.ahead}`, fg: C.good });
|
|
400
|
+
if (this.data.behind > 0) segments.push({ text: ` ↓${this.data.behind}`, fg: C.bad });
|
|
401
|
+
if (this.data.ahead === 0 && this.data.behind === 0) {
|
|
402
|
+
segments.push({ text: ' ≡ up to date', fg: C.dim });
|
|
403
|
+
}
|
|
404
|
+
segments.push({
|
|
405
|
+
text: isDirty ? ' ● dirty' : ' ✓ clean',
|
|
406
|
+
fg: isDirty ? C.warn : C.good,
|
|
407
|
+
bold: true,
|
|
408
|
+
});
|
|
409
|
+
segments.push({ text: ` ${this.data.stagedFiles.length} staged`, fg: this.data.stagedFiles.length > 0 ? C.good : C.dim });
|
|
410
|
+
segments.push({ text: ` · ${this.data.unstagedFiles.length} unstaged`, fg: this.data.unstagedFiles.length > 0 ? C.warn : C.dim });
|
|
411
|
+
return buildStyledPanelLine(width, segments);
|
|
413
412
|
}
|
|
414
413
|
|
|
415
414
|
private renderSectionHeader(label: string, width: number): Line {
|
|
416
|
-
return buildStyledPanelLine(width, [{ text:
|
|
415
|
+
return buildStyledPanelLine(width, [{ text: ` ◆ ${label}`, fg: C.sectionHeader, bold: true }]);
|
|
417
416
|
}
|
|
418
417
|
|
|
419
418
|
private renderFileRow(entry: GitFileEntry, selected: boolean, width: number): Line {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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;
|
|
419
|
+
const fg = entry.staged ? C.good : C.bad;
|
|
420
|
+
// Single-char status marker: + staged (ready to commit), M modified/unstaged.
|
|
421
|
+
const statusGlyph = entry.staged ? '+' : 'M';
|
|
422
|
+
const path = truncateDisplay(entry.path, Math.max(0, width - 6));
|
|
423
|
+
return buildSelectablePanelLine(width, [
|
|
424
|
+
{ text: ` ${statusGlyph} `, fg, bg: selected ? C.selected : undefined, bold: true },
|
|
425
|
+
{ text: path, fg: selected ? C.selectedFg : fg, bg: selected ? C.selected : undefined, bold: selected },
|
|
426
|
+
], { selected, selectedBg: C.selected, fillFg: selected ? C.selectedFg : '', leadingMarker: '▸' });
|
|
439
427
|
}
|
|
440
428
|
|
|
441
429
|
private renderCommitRow(entry: CommitEntry, selected: boolean, width: number): Line {
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
const msgPart = entry.message
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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;
|
|
430
|
+
const hashPart = ` ${entry.hash} `;
|
|
431
|
+
const msgBudget = Math.max(0, width - getDisplayWidth(hashPart) - 3);
|
|
432
|
+
const msgPart = truncateDisplay(entry.message, msgBudget);
|
|
433
|
+
return buildSelectablePanelLine(width, [
|
|
434
|
+
{ text: hashPart, fg: selected ? C.selectedFg : C.commitHash, bg: selected ? C.selected : undefined },
|
|
435
|
+
{ text: msgPart, fg: selected ? C.selectedFg : C.commit, bg: selected ? C.selected : undefined, bold: selected },
|
|
436
|
+
], { selected, selectedBg: C.selected, fillFg: selected ? C.selectedFg : '', leadingMarker: '▸' });
|
|
464
437
|
}
|
|
465
438
|
|
|
466
439
|
private renderList(width: number, height: number): Line[] {
|
|
@@ -535,7 +508,7 @@ export class GitPanel extends BasePanel {
|
|
|
535
508
|
const workspaceSection = resolveScrollablePanelSection(width, height, {
|
|
536
509
|
intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
|
|
537
510
|
footerLines: [
|
|
538
|
-
|
|
511
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Enter', label: 'open diff →' }, { keys: 'r', label: 'refresh' }], DEFAULT_PANEL_PALETTE),
|
|
539
512
|
],
|
|
540
513
|
palette: DEFAULT_PANEL_PALETTE,
|
|
541
514
|
beforeSections: [summarySection],
|
|
@@ -555,11 +528,11 @@ export class GitPanel extends BasePanel {
|
|
|
555
528
|
intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
|
|
556
529
|
sections: [
|
|
557
530
|
summarySection,
|
|
558
|
-
workspaceSection.section.lines.length > 0 ? workspaceSection.section : { title: 'Workspace', lines: buildEmptyState(width, ' No git rows', 'This repository has no
|
|
531
|
+
workspaceSection.section.lines.length > 0 ? workspaceSection.section : { title: 'Workspace', lines: buildEmptyState(width, ' No git rows', 'This repository has no staged or unstaged changes and no commits to display yet.', [{ command: '/git status', summary: 'refresh working-tree status, or stage changes to populate this view' }], DEFAULT_PANEL_PALETTE) },
|
|
559
532
|
selectedSection,
|
|
560
533
|
],
|
|
561
534
|
footerLines: [
|
|
562
|
-
|
|
535
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Enter', label: 'open diff →' }, { keys: 'r', label: 'refresh' }], DEFAULT_PANEL_PALETTE),
|
|
563
536
|
],
|
|
564
537
|
palette: DEFAULT_PANEL_PALETTE,
|
|
565
538
|
});
|
|
@@ -569,7 +542,7 @@ export class GitPanel extends BasePanel {
|
|
|
569
542
|
intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
|
|
570
543
|
sections: [
|
|
571
544
|
{
|
|
572
|
-
lines: buildEmptyState(width, ' No git rows', 'This repository has no
|
|
545
|
+
lines: buildEmptyState(width, ' No git rows', 'This repository has no staged or unstaged changes and no commits to display yet.', [{ command: '/git status', summary: 'refresh working-tree status, or stage changes to populate this view' }], DEFAULT_PANEL_PALETTE),
|
|
573
546
|
},
|
|
574
547
|
],
|
|
575
548
|
palette: DEFAULT_PANEL_PALETTE,
|
|
@@ -595,16 +568,24 @@ export class GitPanel extends BasePanel {
|
|
|
595
568
|
|
|
596
569
|
private renderDiff(width: number, height: number): Line[] {
|
|
597
570
|
const item = this.items[this.selectedIndex];
|
|
598
|
-
const title =
|
|
599
|
-
item?.kind === 'file' ? `Diff: ${item.entry.path}` : 'Diff';
|
|
600
571
|
const diffLines = this.expandedDiff ?? [];
|
|
572
|
+
let added = 0;
|
|
573
|
+
let removed = 0;
|
|
574
|
+
for (const l of diffLines) {
|
|
575
|
+
if (l.startsWith('+') && !l.startsWith('+++')) added++;
|
|
576
|
+
else if (l.startsWith('-') && !l.startsWith('---')) removed++;
|
|
577
|
+
}
|
|
578
|
+
const title =
|
|
579
|
+
item?.kind === 'file'
|
|
580
|
+
? `Diff: ${item.entry.path} +${added} -${removed}`
|
|
581
|
+
: 'Diff';
|
|
601
582
|
const renderedLines = diffLines.map((rawLine) => {
|
|
602
583
|
const dLine = createEmptyLine(width);
|
|
603
584
|
let fg: string;
|
|
604
585
|
if (rawLine.startsWith('+') && !rawLine.startsWith('+++')) {
|
|
605
|
-
fg = C.
|
|
586
|
+
fg = C.good;
|
|
606
587
|
} else if (rawLine.startsWith('-') && !rawLine.startsWith('---')) {
|
|
607
|
-
fg = C.
|
|
588
|
+
fg = C.bad;
|
|
608
589
|
} else if (rawLine.startsWith('@@') || rawLine.startsWith('diff') || rawLine.startsWith('index')) {
|
|
609
590
|
fg = C.diffMeta;
|
|
610
591
|
} else {
|
|
@@ -614,7 +595,7 @@ export class GitPanel extends BasePanel {
|
|
|
614
595
|
return dLine;
|
|
615
596
|
});
|
|
616
597
|
const footerLines = [
|
|
617
|
-
|
|
598
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'scroll' }, { keys: 'Esc/q', label: 'back to files' }], DEFAULT_PANEL_PALETTE),
|
|
618
599
|
];
|
|
619
600
|
const diffSection = resolveScrollablePanelSection(width, height, {
|
|
620
601
|
palette: DEFAULT_PANEL_PALETTE,
|