@pellux/goodvibes-tui 0.27.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 +23 -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/eval.ts +4 -3
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +4 -0
- 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/main.ts +0 -1
- package/src/panels/agent-inspector-panel.ts +18 -13
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +22 -53
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/development.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 +25 -1
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +82 -54
- package/src/panels/debug-panel.ts +51 -16
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +19 -3
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +40 -41
- package/src/panels/file-preview-panel.ts +9 -2
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +58 -70
- 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 +31 -12
- package/src/panels/memory-panel.ts +5 -4
- 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 +22 -0
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +65 -23
- 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 +40 -17
- 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 +29 -5
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- 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 +34 -7
- package/src/panels/thinking-panel.ts +26 -25
- package/src/panels/token-budget-panel.ts +49 -38
- package/src/panels/tool-inspector-panel.ts +27 -21
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +125 -34
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +31 -127
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- 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/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/version.ts +1 -1
- 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,
|
|
@@ -84,15 +85,12 @@ interface TreeNode {
|
|
|
84
85
|
// ---------------------------------------------------------------------------
|
|
85
86
|
// Colour palette
|
|
86
87
|
// ---------------------------------------------------------------------------
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const CLR_SEARCH_FG = '#ff79c6';
|
|
94
|
-
const CLR_TOGGLE = '244'; // ▶/▼ toggle arrows
|
|
95
|
-
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
|
+
});
|
|
96
94
|
|
|
97
95
|
// ---------------------------------------------------------------------------
|
|
98
96
|
// Helpers
|
|
@@ -215,21 +213,29 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
215
213
|
? `Filter: ${this.searchQuery} (/ or up at top to edit)`
|
|
216
214
|
: `Root: ${relative(this.workingDirectory, this.rootPath) || '.'} (/ or up at top to search)`;
|
|
217
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
|
+
|
|
218
232
|
if (this.flat.length === 0) {
|
|
219
233
|
return buildPanelWorkspace(width, height, {
|
|
220
234
|
title: ' Explorer',
|
|
221
235
|
intro: 'Browse the project tree, expand directories, and search for paths.',
|
|
222
236
|
sections: [
|
|
223
237
|
{
|
|
224
|
-
lines: buildEmptyState(
|
|
225
|
-
width,
|
|
226
|
-
' No files found',
|
|
227
|
-
this.searchQuery
|
|
228
|
-
? 'No files or directories match the current search.'
|
|
229
|
-
: 'This root did not produce any visible files after the explorer filters were applied.',
|
|
230
|
-
[],
|
|
231
|
-
DEFAULT_PANEL_PALETTE,
|
|
232
|
-
),
|
|
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),
|
|
233
239
|
},
|
|
234
240
|
],
|
|
235
241
|
footerLines: [
|
|
@@ -237,7 +243,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
237
243
|
active: this.searchMode,
|
|
238
244
|
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
239
245
|
}),
|
|
240
|
-
|
|
246
|
+
hintsLine,
|
|
241
247
|
],
|
|
242
248
|
palette: DEFAULT_PANEL_PALETTE,
|
|
243
249
|
});
|
|
@@ -279,7 +285,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
279
285
|
active: this.searchMode,
|
|
280
286
|
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
281
287
|
}),
|
|
282
|
-
|
|
288
|
+
hintsLine,
|
|
283
289
|
],
|
|
284
290
|
palette: DEFAULT_PANEL_PALETTE,
|
|
285
291
|
beforeSections: [summarySection],
|
|
@@ -287,24 +293,17 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
287
293
|
title: 'Tree',
|
|
288
294
|
scrollableLines: this.flat.map((node, absoluteIdx) => {
|
|
289
295
|
const isCursor = absoluteIdx === this.cursor;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
node.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const sizeStr = ` ${formatSize(node.size)}`;
|
|
302
|
-
const contentWidth = getDisplayWidth(indent) + 2 + getDisplayWidth(node.name);
|
|
303
|
-
const gap = Math.max(1, width - contentWidth - getDisplayWidth(sizeStr));
|
|
304
|
-
segments.push({ text: ' '.repeat(gap), fg: baseFg });
|
|
305
|
-
segments.push({ text: sizeStr, fg: CLR_SIZE, dim: true });
|
|
306
|
-
}
|
|
307
|
-
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 });
|
|
308
307
|
}),
|
|
309
308
|
selectedIndex: this.cursor,
|
|
310
309
|
scrollOffset: this.scrollTop,
|
|
@@ -326,7 +325,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
326
325
|
active: this.searchMode,
|
|
327
326
|
valueColor: this.searchMode ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim,
|
|
328
327
|
}),
|
|
329
|
-
|
|
328
|
+
hintsLine,
|
|
330
329
|
],
|
|
331
330
|
palette: DEFAULT_PANEL_PALETTE,
|
|
332
331
|
});
|
|
@@ -8,6 +8,7 @@ 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,
|
|
@@ -273,7 +274,11 @@ export class FilePreviewPanel extends BasePanel {
|
|
|
273
274
|
],
|
|
274
275
|
} as const;
|
|
275
276
|
const footerLines = [
|
|
276
|
-
|
|
277
|
+
buildKeyboardHints(width, [
|
|
278
|
+
{ keys: '↑/↓', label: 'scroll' },
|
|
279
|
+
{ keys: 'PgUp/PgDn', label: 'page' },
|
|
280
|
+
{ keys: 'Home/End', label: 'top/bottom' },
|
|
281
|
+
], DEFAULT_PANEL_PALETTE),
|
|
277
282
|
];
|
|
278
283
|
const fullCode = this.fileLines.join('\n');
|
|
279
284
|
const hlLines = this.fenceTag
|
|
@@ -319,8 +324,10 @@ export class FilePreviewPanel extends BasePanel {
|
|
|
319
324
|
buildPanelLine(width, [
|
|
320
325
|
[' Lines ', DEFAULT_PANEL_PALETTE.label],
|
|
321
326
|
[String(this.fileLines.length), DEFAULT_PANEL_PALETTE.value],
|
|
322
|
-
['
|
|
327
|
+
[' Viewing ', DEFAULT_PANEL_PALETTE.label],
|
|
323
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],
|
|
324
331
|
]),
|
|
325
332
|
],
|
|
326
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,13 +1,16 @@
|
|
|
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,
|
|
13
16
|
extendPalette,
|
|
@@ -100,11 +103,16 @@ export class GitPanel extends BasePanel {
|
|
|
100
103
|
private loading = true;
|
|
101
104
|
private error: string | null = null;
|
|
102
105
|
|
|
103
|
-
constructor(workingDirectory: string) {
|
|
106
|
+
constructor(workingDirectory: string, private readonly requestRender: () => void = () => {}) {
|
|
104
107
|
super('git', 'Git', 'G', 'development');
|
|
105
108
|
this.workingDirectory = workingDirectory;
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
private _markDirtyAndRender(): void {
|
|
112
|
+
this.markDirty();
|
|
113
|
+
this.requestRender();
|
|
114
|
+
}
|
|
115
|
+
|
|
108
116
|
// ---------------------------------------------------------------------------
|
|
109
117
|
// Lifecycle
|
|
110
118
|
// ---------------------------------------------------------------------------
|
|
@@ -172,7 +180,7 @@ export class GitPanel extends BasePanel {
|
|
|
172
180
|
this.error = null;
|
|
173
181
|
this.rebuildItems();
|
|
174
182
|
// Do not clear expandedDiff during auto-refresh — only clear on explicit user action
|
|
175
|
-
this.
|
|
183
|
+
this._markDirtyAndRender();
|
|
176
184
|
} catch (err) {
|
|
177
185
|
const msg = summarizeError(err);
|
|
178
186
|
// If the failure is because this directory isn't a git repo, auto-initialise
|
|
@@ -196,7 +204,7 @@ export class GitPanel extends BasePanel {
|
|
|
196
204
|
}
|
|
197
205
|
this.loading = false;
|
|
198
206
|
logger.debug('GitPanel: refresh failed', { error: this.error });
|
|
199
|
-
this.
|
|
207
|
+
this._markDirtyAndRender();
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
210
|
|
|
@@ -383,77 +391,49 @@ export class GitPanel extends BasePanel {
|
|
|
383
391
|
}
|
|
384
392
|
|
|
385
393
|
private renderBranchLine(width: number): Line {
|
|
386
|
-
const line = createEmptyLine(width);
|
|
387
|
-
let x = 0;
|
|
388
|
-
|
|
389
|
-
const branchIcon = ' git: ';
|
|
390
|
-
x = this.paintText(line, branchIcon, x, width, C.sectionHeader);
|
|
391
|
-
x = this.paintText(line, this.data.branch, x, width, C.branch, { bold: true });
|
|
392
|
-
|
|
393
|
-
if (this.data.ahead > 0) {
|
|
394
|
-
x = this.paintText(line, ` +${this.data.ahead}`, x, width, C.good);
|
|
395
|
-
}
|
|
396
|
-
if (this.data.behind > 0) {
|
|
397
|
-
x = this.paintText(line, ` -${this.data.behind}`, x, width, C.bad);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
394
|
const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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);
|
|
406
412
|
}
|
|
407
413
|
|
|
408
414
|
private renderSectionHeader(label: string, width: number): Line {
|
|
409
|
-
return buildStyledPanelLine(width, [{ text:
|
|
415
|
+
return buildStyledPanelLine(width, [{ text: ` ◆ ${label}`, fg: C.sectionHeader, bold: true }]);
|
|
410
416
|
}
|
|
411
417
|
|
|
412
418
|
private renderFileRow(entry: GitFileEntry, selected: boolean, width: number): Line {
|
|
413
|
-
const line = createEmptyLine(width);
|
|
414
419
|
const fg = entry.staged ? C.good : C.bad;
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
let x = 0;
|
|
424
|
-
for (const ch of label) {
|
|
425
|
-
if (x >= width) break;
|
|
426
|
-
line[x++] = createStyledCell(ch, { fg, bg: C.selected, bold: true });
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
this.paintText(line, label, 0, width, fg);
|
|
430
|
-
}
|
|
431
|
-
return line;
|
|
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: '▸' });
|
|
432
427
|
}
|
|
433
428
|
|
|
434
429
|
private renderCommitRow(entry: CommitEntry, selected: boolean, width: number): Line {
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
const msgPart = entry.message
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
let x = 0;
|
|
444
|
-
for (const ch of hashPart) {
|
|
445
|
-
if (x >= width) break;
|
|
446
|
-
line[x++] = createStyledCell(ch, { fg: C.commitHash, bg: C.selected });
|
|
447
|
-
}
|
|
448
|
-
for (const ch of msgPart) {
|
|
449
|
-
if (x >= width) break;
|
|
450
|
-
line[x++] = createStyledCell(ch, { fg: C.selectedFg, bg: C.selected });
|
|
451
|
-
}
|
|
452
|
-
} else {
|
|
453
|
-
let x = this.paintText(line, hashPart, 0, width, C.commitHash);
|
|
454
|
-
this.paintText(line, msgPart, x, width, C.commit);
|
|
455
|
-
}
|
|
456
|
-
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: '▸' });
|
|
457
437
|
}
|
|
458
438
|
|
|
459
439
|
private renderList(width: number, height: number): Line[] {
|
|
@@ -528,7 +508,7 @@ export class GitPanel extends BasePanel {
|
|
|
528
508
|
const workspaceSection = resolveScrollablePanelSection(width, height, {
|
|
529
509
|
intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
|
|
530
510
|
footerLines: [
|
|
531
|
-
|
|
511
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Enter', label: 'open diff →' }, { keys: 'r', label: 'refresh' }], DEFAULT_PANEL_PALETTE),
|
|
532
512
|
],
|
|
533
513
|
palette: DEFAULT_PANEL_PALETTE,
|
|
534
514
|
beforeSections: [summarySection],
|
|
@@ -548,11 +528,11 @@ export class GitPanel extends BasePanel {
|
|
|
548
528
|
intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
|
|
549
529
|
sections: [
|
|
550
530
|
summarySection,
|
|
551
|
-
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) },
|
|
552
532
|
selectedSection,
|
|
553
533
|
],
|
|
554
534
|
footerLines: [
|
|
555
|
-
|
|
535
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Enter', label: 'open diff →' }, { keys: 'r', label: 'refresh' }], DEFAULT_PANEL_PALETTE),
|
|
556
536
|
],
|
|
557
537
|
palette: DEFAULT_PANEL_PALETTE,
|
|
558
538
|
});
|
|
@@ -562,7 +542,7 @@ export class GitPanel extends BasePanel {
|
|
|
562
542
|
intro: 'Review branch status, staged and unstaged files, and recent commits. Open a file row to inspect its diff.',
|
|
563
543
|
sections: [
|
|
564
544
|
{
|
|
565
|
-
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),
|
|
566
546
|
},
|
|
567
547
|
],
|
|
568
548
|
palette: DEFAULT_PANEL_PALETTE,
|
|
@@ -588,9 +568,17 @@ export class GitPanel extends BasePanel {
|
|
|
588
568
|
|
|
589
569
|
private renderDiff(width: number, height: number): Line[] {
|
|
590
570
|
const item = this.items[this.selectedIndex];
|
|
591
|
-
const title =
|
|
592
|
-
item?.kind === 'file' ? `Diff: ${item.entry.path}` : 'Diff';
|
|
593
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';
|
|
594
582
|
const renderedLines = diffLines.map((rawLine) => {
|
|
595
583
|
const dLine = createEmptyLine(width);
|
|
596
584
|
let fg: string;
|
|
@@ -607,7 +595,7 @@ export class GitPanel extends BasePanel {
|
|
|
607
595
|
return dLine;
|
|
608
596
|
});
|
|
609
597
|
const footerLines = [
|
|
610
|
-
|
|
598
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'scroll' }, { keys: 'Esc/q', label: 'back to files' }], DEFAULT_PANEL_PALETTE),
|
|
611
599
|
];
|
|
612
600
|
const diffSection = resolveScrollablePanelSection(width, height, {
|
|
613
601
|
palette: DEFAULT_PANEL_PALETTE,
|
|
@@ -9,6 +9,8 @@ import type { HookChain, HookDefinition } from '@pellux/goodvibes-sdk/platform/h
|
|
|
9
9
|
import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
10
10
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
11
11
|
import {
|
|
12
|
+
buildKeyValueLine,
|
|
13
|
+
buildKeyboardHints,
|
|
12
14
|
buildPanelLine,
|
|
13
15
|
buildStatusPill,
|
|
14
16
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -68,9 +70,17 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
68
70
|
) {
|
|
69
71
|
super('hooks', 'Hooks', 'H', 'monitoring');
|
|
70
72
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
73
|
+
this.filterEnabled = true;
|
|
74
|
+
this.filterLabel = 'Filter hooks';
|
|
71
75
|
this.dataSource = dataSource;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
protected override filterMatches(entry: HookEntry, q: string): boolean {
|
|
79
|
+
return (entry.hook.name ?? '').toLowerCase().includes(q)
|
|
80
|
+
|| entry.pattern.toLowerCase().includes(q)
|
|
81
|
+
|| entry.hook.type.toLowerCase().includes(q);
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
protected override getPalette() { return C; }
|
|
75
85
|
protected override getEmptyStateMessage() { return ' No hooks are currently registered.'; }
|
|
76
86
|
protected override getEmptyStateActions() {
|
|
@@ -96,7 +106,7 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
public handleInput(key: string): boolean {
|
|
99
|
-
if (key === 'r') {
|
|
109
|
+
if (!this.filterActive && key === 'r') {
|
|
100
110
|
this.markDirty();
|
|
101
111
|
return true;
|
|
102
112
|
}
|
|
@@ -224,15 +234,39 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
224
234
|
});
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
// Summary header — surface registration + activity counts first so the most
|
|
238
|
+
// important "what's firing" signal is visible without scrolling to the footer.
|
|
239
|
+
const denials = recentActivity.filter((r) => r.ok && r.decision === 'deny').length;
|
|
240
|
+
const errors = recentActivity.filter((r) => !r.ok).length;
|
|
241
|
+
const headerLines: Line[] = [
|
|
242
|
+
buildKeyValueLine(width, [
|
|
243
|
+
{ label: 'hooks', value: String(hooks.length), valueColor: C.info },
|
|
244
|
+
{ label: 'chains', value: String(chains.length), valueColor: C.value },
|
|
245
|
+
{ label: 'contracts', value: String(contracts.length), valueColor: C.value },
|
|
246
|
+
{ label: 'recent denials', value: String(denials), valueColor: denials > 0 ? C.warn : C.dim },
|
|
247
|
+
{ label: 'errors', value: String(errors), valueColor: errors > 0 ? C.error : C.dim },
|
|
248
|
+
], C),
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
const hints = this.filterActive
|
|
252
|
+
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
253
|
+
: [
|
|
254
|
+
{ keys: 'Up/Down', label: 'move' },
|
|
255
|
+
{ keys: 'r', label: 'refresh' },
|
|
256
|
+
{ keys: '/hooks', label: 'full listing' },
|
|
257
|
+
{ keys: '/', label: 'filter' },
|
|
258
|
+
];
|
|
259
|
+
|
|
227
260
|
return this.renderList(width, height, {
|
|
228
261
|
title: 'Hooks Control Room',
|
|
262
|
+
header: headerLines,
|
|
229
263
|
footer: [
|
|
230
264
|
...detailLines,
|
|
231
265
|
buildPanelLine(width, [[' Recent Activity', C.label]]),
|
|
232
266
|
...activityLines,
|
|
233
267
|
buildPanelLine(width, [[' Authoring', C.label]]),
|
|
234
268
|
...authoringLines,
|
|
235
|
-
|
|
269
|
+
buildKeyboardHints(width, hints, C),
|
|
236
270
|
],
|
|
237
271
|
});
|
|
238
272
|
}
|