@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
|
@@ -5,18 +5,31 @@ import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
|
5
5
|
import type { UiProvidersSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
6
6
|
import {
|
|
7
7
|
buildEmptyState,
|
|
8
|
+
buildKeyboardHints,
|
|
8
9
|
buildKeyValueLine,
|
|
10
|
+
buildPanelLine,
|
|
9
11
|
buildStyledPanelLine,
|
|
10
12
|
buildPanelWorkspace,
|
|
11
13
|
DEFAULT_PANEL_PALETTE,
|
|
14
|
+
extendPalette,
|
|
12
15
|
type PanelWorkspaceSection,
|
|
13
16
|
} from './polish.ts';
|
|
14
17
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
18
|
+
import { formatLatencyMs } from '../utils/format-duration.ts';
|
|
15
19
|
|
|
16
20
|
// ---------------------------------------------------------------------------
|
|
17
21
|
// Constants
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
19
23
|
|
|
24
|
+
// Domain-specific tones not covered by the base palette (latency/token greys).
|
|
25
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
26
|
+
muted2: '#6b7280', // secondary dim grey for unit labels
|
|
27
|
+
sep: '#374151', // inline separator grey
|
|
28
|
+
tokenDim: '#64748b', // token-count grey
|
|
29
|
+
p95: '#a78bfa', // p95 latency accent
|
|
30
|
+
latYellow: '#eab308', // mid-latency warning
|
|
31
|
+
});
|
|
32
|
+
|
|
20
33
|
const SPARKLINE_CHARS = '._-:=+*#';
|
|
21
34
|
const LATENCY_RING_SIZE = 20;
|
|
22
35
|
|
|
@@ -202,7 +215,10 @@ export class ProviderStatsPanel extends BasePanel {
|
|
|
202
215
|
width,
|
|
203
216
|
' No providers registered',
|
|
204
217
|
'Load or configure a provider to begin collecting per-provider latency and error metrics.',
|
|
205
|
-
[
|
|
218
|
+
[
|
|
219
|
+
{ command: '/provider', summary: 'review current provider and model selection' },
|
|
220
|
+
{ command: '/subscription', summary: 'sign in to a subscription-backed provider' },
|
|
221
|
+
],
|
|
206
222
|
DEFAULT_PANEL_PALETTE,
|
|
207
223
|
),
|
|
208
224
|
},
|
|
@@ -245,6 +261,18 @@ export class ProviderStatsPanel extends BasePanel {
|
|
|
245
261
|
title: ' Provider Stats',
|
|
246
262
|
intro: 'Per-provider request performance, latency distribution, error pressure, and session totals.',
|
|
247
263
|
sections: providerSections,
|
|
264
|
+
footerLines: [
|
|
265
|
+
buildPanelLine(width, [
|
|
266
|
+
[' sparkline ', C.muted2],
|
|
267
|
+
['low', C.good],
|
|
268
|
+
[' → ', C.muted2],
|
|
269
|
+
['high latency', C.bad],
|
|
270
|
+
]),
|
|
271
|
+
buildKeyboardHints(width, [
|
|
272
|
+
{ keys: '/provider', label: 'switch provider/model' },
|
|
273
|
+
{ keys: '/health', label: 'live provider health' },
|
|
274
|
+
], DEFAULT_PANEL_PALETTE),
|
|
275
|
+
],
|
|
248
276
|
palette: DEFAULT_PANEL_PALETTE,
|
|
249
277
|
});
|
|
250
278
|
}
|
|
@@ -262,7 +290,7 @@ export class ProviderStatsPanel extends BasePanel {
|
|
|
262
290
|
|
|
263
291
|
// Determine health
|
|
264
292
|
const hasErrors = rec !== undefined && rec.errors > 0;
|
|
265
|
-
const dotColor = hasErrors ?
|
|
293
|
+
const dotColor = hasErrors ? C.bad : C.good;
|
|
266
294
|
|
|
267
295
|
// Model ID (truncated)
|
|
268
296
|
const modelId = rec?.lastModelId ?? 'n/a';
|
|
@@ -271,18 +299,18 @@ export class ProviderStatsPanel extends BasePanel {
|
|
|
271
299
|
// Header row: * provider model
|
|
272
300
|
// Build as segments to avoid multi-byte char indexing issues
|
|
273
301
|
const headerLine = buildStyledPanelLine(width, [
|
|
274
|
-
{ text: ' ', fg:
|
|
302
|
+
{ text: ' ', fg: C.label },
|
|
275
303
|
{ text: '●', fg: dotColor },
|
|
276
|
-
{ text: ' ', fg:
|
|
277
|
-
{ text: `${truncateDisplay(provName, 14).padEnd(14)} `, fg:
|
|
278
|
-
{ text: modelDisplay, fg:
|
|
304
|
+
{ text: ' ', fg: C.label },
|
|
305
|
+
{ text: `${truncateDisplay(provName, 14).padEnd(14)} `, fg: C.value, bold: true },
|
|
306
|
+
{ text: modelDisplay, fg: C.accent },
|
|
279
307
|
]);
|
|
280
308
|
|
|
281
309
|
rows.push(headerLine);
|
|
282
310
|
|
|
283
311
|
if (rec === undefined || rec.requests === 0) {
|
|
284
312
|
rows.push(buildStyledPanelLine(width, [
|
|
285
|
-
{ text: ' No requests yet.', fg:
|
|
313
|
+
{ text: ' No requests yet.', fg: C.muted2 },
|
|
286
314
|
]));
|
|
287
315
|
} else {
|
|
288
316
|
const avgLatency = this._avg(rec.latencies);
|
|
@@ -291,24 +319,24 @@ export class ProviderStatsPanel extends BasePanel {
|
|
|
291
319
|
const sparkline = this._sparkline(rec.latencies);
|
|
292
320
|
|
|
293
321
|
const latFg = avgLatency < LATENCY_GREEN
|
|
294
|
-
?
|
|
322
|
+
? C.good
|
|
295
323
|
: avgLatency < LATENCY_YELLOW
|
|
296
|
-
?
|
|
297
|
-
:
|
|
324
|
+
? C.latYellow
|
|
325
|
+
: C.bad;
|
|
298
326
|
|
|
299
327
|
const segments = [
|
|
300
|
-
{ text: ' avg ', fg:
|
|
328
|
+
{ text: ' avg ', fg: C.muted2 },
|
|
301
329
|
{ text: this._fmtMs(avgLatency).padStart(6), fg: latFg, bold: true },
|
|
302
|
-
{ text: ' p95 ', fg:
|
|
303
|
-
{ text: this._fmtMs(p95Latency).padStart(6), fg:
|
|
304
|
-
{ text: ' ', fg:
|
|
330
|
+
{ text: ' p95 ', fg: C.muted2 },
|
|
331
|
+
{ text: this._fmtMs(p95Latency).padStart(6), fg: C.p95 },
|
|
332
|
+
{ text: ' ', fg: C.sep },
|
|
305
333
|
{ text: sparkline, fg: latFg },
|
|
306
|
-
{ text: ' err ', fg:
|
|
307
|
-
{ text: `${errRate.toFixed(0).padStart(3)}%`, fg: errRate > 0 ?
|
|
308
|
-
{ text: ` ${rec.requests.toString().padStart(4)}r`, fg:
|
|
334
|
+
{ text: ' err ', fg: C.muted2 },
|
|
335
|
+
{ text: `${errRate.toFixed(0).padStart(3)}%`, fg: errRate > 0 ? C.bad : C.good },
|
|
336
|
+
{ text: ` ${rec.requests.toString().padStart(4)}r`, fg: C.label },
|
|
309
337
|
] as const;
|
|
310
338
|
const tokenSegment = rec.totalTokens > 0
|
|
311
|
-
? [{ text: ` ${rec.totalTokens.toString().padStart(6)}tok`, fg:
|
|
339
|
+
? [{ text: ` ${rec.totalTokens.toString().padStart(6)}tok`, fg: C.tokenDim }]
|
|
312
340
|
: [];
|
|
313
341
|
rows.push(buildStyledPanelLine(width, [...segments, ...tokenSegment]));
|
|
314
342
|
}
|
|
@@ -351,10 +379,7 @@ export class ProviderStatsPanel extends BasePanel {
|
|
|
351
379
|
}
|
|
352
380
|
|
|
353
381
|
private _fmtMs(ms: number): string {
|
|
354
|
-
|
|
355
|
-
if (ms >= 10000) return `${(ms / 1000).toFixed(1)}s`;
|
|
356
|
-
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
|
357
|
-
return `${Math.round(ms)}ms`;
|
|
382
|
+
return formatLatencyMs(ms);
|
|
358
383
|
}
|
|
359
384
|
|
|
360
385
|
private _latencyColor(ms: number): string {
|
package/src/panels/qr-panel.ts
CHANGED
|
@@ -2,9 +2,12 @@ import type { Line } from '../types/grid.ts';
|
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
3
|
import { BasePanel } from './base-panel.ts';
|
|
4
4
|
import {
|
|
5
|
+
buildKeyboardHints,
|
|
5
6
|
buildPanelLine,
|
|
7
|
+
buildPanelTitle,
|
|
6
8
|
DEFAULT_PANEL_PALETTE,
|
|
7
9
|
} from './polish.ts';
|
|
10
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
8
11
|
import { renderQrMatrix, generateQrMatrix } from '../renderer/qr-renderer.ts';
|
|
9
12
|
import { encodeConnectionPayload } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
10
13
|
|
|
@@ -101,32 +104,41 @@ export class QrPanel extends BasePanel {
|
|
|
101
104
|
const lines: Line[] = [];
|
|
102
105
|
|
|
103
106
|
const { url, token, username, password } = this.connectionInfo;
|
|
107
|
+
const valueWidth = Math.max(0, width - 12);
|
|
108
|
+
|
|
109
|
+
// ── Title + purpose: tell the operator exactly what this code is for ────
|
|
110
|
+
lines.push(buildPanelTitle(width, 'Companion Pairing', C));
|
|
111
|
+
lines.push(
|
|
112
|
+
buildPanelLine(width, [
|
|
113
|
+
[' Scan with the GoodVibes companion app to pair this session.', C.hint],
|
|
114
|
+
]),
|
|
115
|
+
);
|
|
104
116
|
|
|
105
117
|
// ── Connection info header ─────────────────────────────────────────────
|
|
106
118
|
lines.push(createEmptyLine(width));
|
|
107
119
|
lines.push(
|
|
108
120
|
buildPanelLine(width, [
|
|
109
121
|
[' URL ', C.label],
|
|
110
|
-
[url
|
|
122
|
+
[truncateDisplay(url, valueWidth), C.url],
|
|
111
123
|
]),
|
|
112
124
|
);
|
|
113
125
|
lines.push(
|
|
114
126
|
buildPanelLine(width, [
|
|
115
127
|
[' Token ', C.label],
|
|
116
|
-
[token, C.token],
|
|
128
|
+
[truncateDisplay(token, valueWidth), C.token],
|
|
117
129
|
]),
|
|
118
130
|
);
|
|
119
131
|
lines.push(
|
|
120
132
|
buildPanelLine(width, [
|
|
121
133
|
[' Username ', C.label],
|
|
122
|
-
[username
|
|
134
|
+
[truncateDisplay(username, valueWidth), C.value],
|
|
123
135
|
]),
|
|
124
136
|
);
|
|
125
137
|
if (password !== undefined) {
|
|
126
138
|
lines.push(
|
|
127
139
|
buildPanelLine(width, [
|
|
128
140
|
[' Password ', C.label],
|
|
129
|
-
[password, C.value],
|
|
141
|
+
[truncateDisplay(password, valueWidth), C.value],
|
|
130
142
|
]),
|
|
131
143
|
);
|
|
132
144
|
}
|
|
@@ -158,13 +170,13 @@ export class QrPanel extends BasePanel {
|
|
|
158
170
|
);
|
|
159
171
|
}
|
|
160
172
|
|
|
161
|
-
// ── Hints
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
// ── Hints ── only advertise actions that are actually wired ─────────────
|
|
174
|
+
const hints: Array<{ keys: string; label: string }> = [];
|
|
175
|
+
if (this.regenerateToken) hints.push({ keys: 'r', label: 'regenerate token' });
|
|
176
|
+
if (this.copyToClipboard) hints.push({ keys: 'c', label: 'copy token' });
|
|
177
|
+
const hintsLine = hints.length > 0
|
|
178
|
+
? buildKeyboardHints(width, hints, C)
|
|
179
|
+
: buildPanelLine(width, [[' Pairing is read-only in this surface.', C.dim]]);
|
|
168
180
|
|
|
169
181
|
// Push hints at the bottom if we have room, otherwise append after QR
|
|
170
182
|
const remaining = height - lines.length;
|
|
@@ -200,7 +200,7 @@ export class RemotePanel extends BasePanel {
|
|
|
200
200
|
if (daemon.lastError) {
|
|
201
201
|
postureLines.push(buildPanelLine(width, [
|
|
202
202
|
[' daemon error ', C.label],
|
|
203
|
-
[daemon.lastError
|
|
203
|
+
[truncateDisplay(daemon.lastError, Math.max(0, width - 14)), C.error],
|
|
204
204
|
]));
|
|
205
205
|
}
|
|
206
206
|
postureLines.push(
|
|
@@ -208,9 +208,16 @@ export class RemotePanel extends BasePanel {
|
|
|
208
208
|
buildGuidanceLine(width, '/remote capabilities', 'inspect transport support before routing remote work or reattaching a session', C),
|
|
209
209
|
);
|
|
210
210
|
|
|
211
|
+
const canBrowse = activeConnections.length > 0 || contracts.length > 0;
|
|
212
|
+
const canSwitch = contracts.length > 0 && activeConnections.length > 0;
|
|
213
|
+
const navHint = !canBrowse
|
|
214
|
+
? ` focus=${this.browseMode} idle - no connections or contracts to browse`
|
|
215
|
+
: canSwitch
|
|
216
|
+
? ` focus=${this.browseMode} Up/Down move Tab switch connections/contracts`
|
|
217
|
+
: ` focus=${this.browseMode} Up/Down move`;
|
|
211
218
|
const footerLines = [
|
|
212
219
|
buildGuidanceLine(width, '/remote setup', 'review bridge, tunnel, env, and bootstrap flows for self-hosted remote work', C),
|
|
213
|
-
buildPanelLine(width, [[
|
|
220
|
+
buildPanelLine(width, [[navHint, C.dim]]),
|
|
214
221
|
] as const;
|
|
215
222
|
|
|
216
223
|
if (activeConnections.length === 0 && contracts.length === 0) {
|
|
@@ -318,7 +325,7 @@ export class RemotePanel extends BasePanel {
|
|
|
318
325
|
[' Review: ', C.label],
|
|
319
326
|
[supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.ok : C.dim],
|
|
320
327
|
]));
|
|
321
|
-
detailRows.push(buildPanelLine(width, [[` ${supervisorEntry.heartbeat.detail}
|
|
328
|
+
detailRows.push(buildPanelLine(width, [[truncateDisplay(` ${supervisorEntry.heartbeat.detail}`, width), C.dim]]));
|
|
322
329
|
}
|
|
323
330
|
|
|
324
331
|
const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selected.agentId);
|
|
@@ -400,7 +407,7 @@ export class RemotePanel extends BasePanel {
|
|
|
400
407
|
{ text: ` ${connection.transportState.padEnd(18)}`, fg: stateColor(connection.transportState) },
|
|
401
408
|
{ text: ` msgs=${String(connection.messageCount).padEnd(6)}`, fg: C.info },
|
|
402
409
|
{ text: ` errs=${String(connection.errorCount).padEnd(4)}`, fg: connection.errorCount > 0 ? C.warn : C.dim },
|
|
403
|
-
{ text: ` ${connection.label}
|
|
410
|
+
{ text: truncateDisplay(` ${connection.label}`, Math.max(0, width - 56)), fg: C.dim },
|
|
404
411
|
], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
|
|
405
412
|
})
|
|
406
413
|
: [
|
|
@@ -408,7 +415,7 @@ export class RemotePanel extends BasePanel {
|
|
|
408
415
|
return buildPanelListRow(width, [
|
|
409
416
|
{ text: contract.runnerId.padEnd(18), fg: C.value },
|
|
410
417
|
{ text: ` ${contract.transport.state.padEnd(18)}`, fg: stateColor(contract.transport.state) },
|
|
411
|
-
{ text: ` ${contract.template}
|
|
418
|
+
{ text: truncateDisplay(` ${contract.template}`, Math.max(0, width - 42)), fg: C.dim },
|
|
412
419
|
], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
|
|
413
420
|
}),
|
|
414
421
|
buildPanelLine(width, [[' No active connection is currently attached to these contracts.', C.dim]]),
|
|
@@ -4,9 +4,11 @@ import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
|
4
4
|
import type { UiReadModel, UiRoutesSnapshot } from '../runtime/ui-read-models.ts';
|
|
5
5
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
6
|
import {
|
|
7
|
+
buildDetailBlock,
|
|
7
8
|
buildEmptyState,
|
|
8
9
|
buildGuidanceLine,
|
|
9
10
|
buildKeyValueLine,
|
|
11
|
+
buildKeyboardHints,
|
|
10
12
|
buildPanelLine,
|
|
11
13
|
buildPanelWorkspace,
|
|
12
14
|
buildStatusPill,
|
|
@@ -39,10 +41,20 @@ export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
|
|
|
39
41
|
public constructor(readModel?: UiReadModel<UiRoutesSnapshot>) {
|
|
40
42
|
super('routes', 'Routes', 'R', 'monitoring');
|
|
41
43
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
44
|
+
this.filterEnabled = true;
|
|
45
|
+
this.filterLabel = 'Filter routes';
|
|
42
46
|
this.readModel = readModel;
|
|
43
47
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
protected override filterMatches(binding: RouteBinding, q: string): boolean {
|
|
51
|
+
return binding.surfaceKind.toLowerCase().includes(q)
|
|
52
|
+
|| (binding.title ?? '').toLowerCase().includes(q)
|
|
53
|
+
|| binding.externalId.toLowerCase().includes(q)
|
|
54
|
+
|| (binding.sessionId ?? '').toLowerCase().includes(q)
|
|
55
|
+
|| (binding.runId ?? '').toLowerCase().includes(q);
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
public override onDestroy(): void {
|
|
47
59
|
this.unsub?.();
|
|
48
60
|
}
|
|
@@ -127,7 +139,7 @@ export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
|
|
|
127
139
|
this.clampSelection();
|
|
128
140
|
const selected = bindings[this.selectedIndex]!;
|
|
129
141
|
|
|
130
|
-
const
|
|
142
|
+
const detailRows: Line[] = [
|
|
131
143
|
buildPanelLine(width, [
|
|
132
144
|
[' Binding: ', C.label],
|
|
133
145
|
[selected.id, C.value],
|
|
@@ -159,20 +171,30 @@ export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
|
|
|
159
171
|
];
|
|
160
172
|
|
|
161
173
|
if (surfaceEntries.length > 0) {
|
|
162
|
-
|
|
163
|
-
...surfaceEntries.slice(0,
|
|
174
|
+
detailRows.push(
|
|
175
|
+
...surfaceEntries.slice(0, 4).map(([surface, ids]) => buildPanelLine(width, [
|
|
164
176
|
[' ', C.label],
|
|
165
177
|
[surface.padEnd(10), C.info],
|
|
166
178
|
[` ${String(ids.length)} binding(s)`, C.value],
|
|
167
179
|
])),
|
|
168
180
|
);
|
|
169
181
|
}
|
|
170
|
-
|
|
182
|
+
|
|
183
|
+
const hints = this.filterActive
|
|
184
|
+
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
185
|
+
: [
|
|
186
|
+
{ keys: 'Up/Down', label: 'move' },
|
|
187
|
+
{ keys: '/communication', label: 'inspect flow' },
|
|
188
|
+
{ keys: '/', label: 'filter' },
|
|
189
|
+
];
|
|
171
190
|
|
|
172
191
|
return this.renderList(width, height, {
|
|
173
192
|
title: 'Route Bindings',
|
|
174
193
|
header: headerLines,
|
|
175
|
-
footer:
|
|
194
|
+
footer: [
|
|
195
|
+
...buildDetailBlock(width, `Binding · ${selected.surfaceKind}`, detailRows, C),
|
|
196
|
+
buildKeyboardHints(width, hints, C),
|
|
197
|
+
],
|
|
176
198
|
});
|
|
177
199
|
}
|
|
178
200
|
}
|
|
@@ -5,9 +5,11 @@ import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
5
5
|
import { buildSandboxReview, listSandboxPresets, listSandboxProfiles } from '@/runtime/index.ts';
|
|
6
6
|
import type { SandboxSessionRegistry } from '@/runtime/index.ts';
|
|
7
7
|
import {
|
|
8
|
+
buildAlignedRow,
|
|
8
9
|
buildBodyText,
|
|
9
10
|
buildEmptyState,
|
|
10
11
|
buildGuidanceLine,
|
|
12
|
+
buildKeyboardHints,
|
|
11
13
|
buildKeyValueLine,
|
|
12
14
|
buildPanelLine,
|
|
13
15
|
buildPanelWorkspace,
|
|
@@ -125,7 +127,10 @@ export class SandboxPanel extends BasePanel {
|
|
|
125
127
|
buildGuidanceLine(width, '/sandbox wrapper-test <profile>', 'validate the wrapper bridge contract before wiring a real guest transport', C),
|
|
126
128
|
buildGuidanceLine(width, '/sandbox session run <id> <command> [args...]', 'execute through a tracked sandbox session and capture runtime metadata on the session record', C),
|
|
127
129
|
buildGuidanceLine(width, 'GV_SANDBOX_WRAPPER_MODE=host-exec', 'validate the wrapper contract on the host before wiring a real guest transport', C),
|
|
128
|
-
|
|
130
|
+
buildKeyboardHints(width, [
|
|
131
|
+
{ keys: '↑/↓', label: 'select profile/session' },
|
|
132
|
+
{ keys: 'Home/End', label: 'jump to first profile / first session' },
|
|
133
|
+
], C),
|
|
129
134
|
];
|
|
130
135
|
|
|
131
136
|
const selectionLines: Line[] = [];
|
|
@@ -185,41 +190,71 @@ export class SandboxPanel extends BasePanel {
|
|
|
185
190
|
));
|
|
186
191
|
} else {
|
|
187
192
|
for (const session of sessions) {
|
|
188
|
-
const
|
|
189
|
-
sessionLines.push(
|
|
190
|
-
|
|
191
|
-
[
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
const selectedRow = selectedSession?.id === session.id;
|
|
194
|
+
sessionLines.push(buildAlignedRow(
|
|
195
|
+
width,
|
|
196
|
+
[
|
|
197
|
+
{ text: session.profileId, fg: C.info },
|
|
198
|
+
{ text: session.state, fg: session.state === 'running' ? C.good : session.state === 'failed' ? C.bad : C.warn },
|
|
199
|
+
{ text: session.shared ? 'shared' : 'dedicated', fg: C.value },
|
|
200
|
+
{ text: String(session.resolvedBackend ?? session.backend), fg: C.dim },
|
|
201
|
+
{ text: session.startupStatus ?? 'n/a', fg: session.startupStatus === 'verified' ? C.good : session.startupStatus === 'failed' ? C.bad : C.warn },
|
|
202
|
+
{ text: `${session.executionCount ?? 0}x`, fg: C.info },
|
|
203
|
+
{ text: session.id, fg: C.dim },
|
|
204
|
+
],
|
|
205
|
+
[
|
|
206
|
+
{ width: 15 },
|
|
207
|
+
{ width: 10 },
|
|
208
|
+
{ width: 12 },
|
|
209
|
+
{ width: 8 },
|
|
210
|
+
{ width: 9 },
|
|
211
|
+
{ width: 5, align: 'right' },
|
|
212
|
+
{ width: Math.max(8, width - 72) },
|
|
213
|
+
],
|
|
214
|
+
{ selected: selectedRow, selectedBg: C.headerBg },
|
|
215
|
+
));
|
|
199
216
|
}
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
const presetLines: Line[] = [];
|
|
203
220
|
for (const preset of presets.slice(0, 2)) {
|
|
204
|
-
presetLines.push(
|
|
205
|
-
|
|
206
|
-
[
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
221
|
+
presetLines.push(buildAlignedRow(
|
|
222
|
+
width,
|
|
223
|
+
[
|
|
224
|
+
{ text: preset.id, fg: C.info },
|
|
225
|
+
{ text: preset.config.replIsolation, fg: C.value },
|
|
226
|
+
{ text: preset.config.mcpIsolation, fg: C.dim },
|
|
227
|
+
{ text: preset.config.windowsMode, fg: C.warn },
|
|
228
|
+
],
|
|
229
|
+
[
|
|
230
|
+
{ width: 18 },
|
|
231
|
+
{ width: 16 },
|
|
232
|
+
{ width: 16 },
|
|
233
|
+
{ width: Math.max(8, width - 62) },
|
|
234
|
+
],
|
|
235
|
+
{},
|
|
236
|
+
));
|
|
211
237
|
}
|
|
212
238
|
|
|
213
239
|
const profileLines: Line[] = [];
|
|
214
240
|
for (const profile of profiles) {
|
|
215
|
-
const
|
|
216
|
-
profileLines.push(
|
|
217
|
-
|
|
218
|
-
[
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
241
|
+
const selectedRow = selectedProfile?.id === profile.id;
|
|
242
|
+
profileLines.push(buildAlignedRow(
|
|
243
|
+
width,
|
|
244
|
+
[
|
|
245
|
+
{ text: profile.id, fg: C.info },
|
|
246
|
+
{ text: profile.isolation, fg: C.value },
|
|
247
|
+
{ text: profile.kind, fg: C.dim },
|
|
248
|
+
{ text: `vm=${profile.requiresVm ? 'yes' : 'no'}`, fg: profile.requiresVm ? C.good : C.warn },
|
|
249
|
+
],
|
|
250
|
+
[
|
|
251
|
+
{ width: 15 },
|
|
252
|
+
{ width: 14 },
|
|
253
|
+
{ width: 12 },
|
|
254
|
+
{ width: Math.max(6, width - 53) },
|
|
255
|
+
],
|
|
256
|
+
{ selected: selectedRow, selectedBg: C.headerBg },
|
|
257
|
+
));
|
|
223
258
|
}
|
|
224
259
|
const postureSection: PanelWorkspaceSection = { title: 'Sandbox posture', lines: overviewLines };
|
|
225
260
|
const selectedSection: PanelWorkspaceSection = { title: selectedProfile ? 'Selected Profile' : 'Selected Session', lines: selectionLines };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { BasePanel } from './base-panel.ts';
|
|
2
2
|
import { type Line } from '../types/grid.ts';
|
|
3
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
|
|
4
5
|
import type { AutomationJob } from '@pellux/goodvibes-sdk/platform/automation';
|
|
5
6
|
import type { AutomationRun } from '@pellux/goodvibes-sdk/platform/automation';
|
|
6
7
|
import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platform/automation';
|
|
7
8
|
import {
|
|
8
9
|
buildEmptyState,
|
|
10
|
+
buildKeyboardHints,
|
|
9
11
|
buildPanelLine,
|
|
10
12
|
buildPanelWorkspace,
|
|
11
13
|
extendPalette,
|
|
@@ -87,11 +89,16 @@ export class SchedulePanel extends BasePanel {
|
|
|
87
89
|
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
88
90
|
private readonly automationManager: ScheduleAutomationManager;
|
|
89
91
|
|
|
90
|
-
constructor(automationManager: ScheduleAutomationManager) {
|
|
92
|
+
constructor(automationManager: ScheduleAutomationManager, private readonly requestRender: () => void = () => {}) {
|
|
91
93
|
super('schedule', 'Schedule', 'Z', 'agent');
|
|
92
94
|
this.automationManager = automationManager;
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
private _markDirtyAndRender(): void {
|
|
98
|
+
this.markDirty();
|
|
99
|
+
this.requestRender();
|
|
100
|
+
}
|
|
101
|
+
|
|
95
102
|
// -------------------------------------------------------------------------
|
|
96
103
|
// Lifecycle
|
|
97
104
|
// -------------------------------------------------------------------------
|
|
@@ -100,12 +107,12 @@ export class SchedulePanel extends BasePanel {
|
|
|
100
107
|
super.onActivate();
|
|
101
108
|
void this.automationManager.start().then(() => {
|
|
102
109
|
this.rebuild();
|
|
103
|
-
this.
|
|
110
|
+
this._markDirtyAndRender();
|
|
104
111
|
});
|
|
105
112
|
this.rebuild();
|
|
106
113
|
this.refreshTimerId = this.registerTimer(setInterval(() => {
|
|
107
114
|
this.rebuild();
|
|
108
|
-
this.
|
|
115
|
+
this._markDirtyAndRender();
|
|
109
116
|
}, 5_000));
|
|
110
117
|
}
|
|
111
118
|
|
|
@@ -221,14 +228,19 @@ export class SchedulePanel extends BasePanel {
|
|
|
221
228
|
lines: buildEmptyState(
|
|
222
229
|
width,
|
|
223
230
|
' No scheduled tasks',
|
|
224
|
-
'
|
|
225
|
-
[
|
|
231
|
+
'Create a recurring task and its next-run timing, run history, and enablement state will appear here.',
|
|
232
|
+
[
|
|
233
|
+
{ command: '/schedule add cron 0 * * * * repo sweep', summary: 'create a recurring cron task' },
|
|
234
|
+
{ command: '/schedule list', summary: 'inspect scheduled tasks from the shell' },
|
|
235
|
+
],
|
|
226
236
|
DEFAULT_PANEL_PALETTE,
|
|
227
237
|
),
|
|
228
238
|
},
|
|
229
239
|
],
|
|
230
240
|
footerLines: [
|
|
231
|
-
|
|
241
|
+
buildKeyboardHints(width, [
|
|
242
|
+
{ keys: '/schedule add', label: 'create a task' },
|
|
243
|
+
], DEFAULT_PANEL_PALETTE),
|
|
232
244
|
],
|
|
233
245
|
palette: DEFAULT_PANEL_PALETTE,
|
|
234
246
|
});
|
|
@@ -236,6 +248,7 @@ export class SchedulePanel extends BasePanel {
|
|
|
236
248
|
|
|
237
249
|
const taskItems = this.items.filter((item): item is Extract<ViewItem, { kind: 'task' }> => item.kind === 'task');
|
|
238
250
|
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, taskItems.length - 1));
|
|
251
|
+
const dueSoon = tasks.filter((task: AutomationJob) => typeof task.nextRunAt === 'number').length;
|
|
239
252
|
const summarySection: PanelWorkspaceSection = {
|
|
240
253
|
title: 'Summary',
|
|
241
254
|
lines: [
|
|
@@ -244,14 +257,28 @@ export class SchedulePanel extends BasePanel {
|
|
|
244
257
|
[String(tasks.length), DEFAULT_PANEL_PALETTE.value],
|
|
245
258
|
[' Enabled ', DEFAULT_PANEL_PALETTE.label],
|
|
246
259
|
[String(enabled), enabled > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
|
|
260
|
+
[' Paused ', DEFAULT_PANEL_PALETTE.label],
|
|
261
|
+
[String(tasks.length - enabled), tasks.length - enabled > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
|
|
262
|
+
[' Scheduled ', DEFAULT_PANEL_PALETTE.label],
|
|
263
|
+
[String(dueSoon), dueSoon > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
|
|
247
264
|
]),
|
|
248
265
|
],
|
|
249
266
|
};
|
|
267
|
+
const selectedTask = taskItems[this.selectedIndex]?.task;
|
|
268
|
+
const toggleLabel = selectedTask
|
|
269
|
+
? (selectedTask.enabled ? 'pause task' : 'enable task')
|
|
270
|
+
: 'toggle';
|
|
271
|
+
const footerLines = [
|
|
272
|
+
buildKeyboardHints(width, [
|
|
273
|
+
{ keys: 'Up/Down', label: 'navigate' },
|
|
274
|
+
{ keys: 'Space', label: toggleLabel },
|
|
275
|
+
{ keys: 'r', label: 'run now' },
|
|
276
|
+
{ keys: 'R', label: 'refresh' },
|
|
277
|
+
], DEFAULT_PANEL_PALETTE),
|
|
278
|
+
];
|
|
250
279
|
const scheduledTasksSection = resolveScrollablePanelSection(width, height, {
|
|
251
280
|
intro: 'Review recurring scheduled tasks, next run timing, recent history, and enablement state.',
|
|
252
|
-
footerLines
|
|
253
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Space', DEFAULT_PANEL_PALETTE.info], [' toggle', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' run now', DEFAULT_PANEL_PALETTE.dim], [' R', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
254
|
-
],
|
|
281
|
+
footerLines,
|
|
255
282
|
palette: DEFAULT_PANEL_PALETTE,
|
|
256
283
|
beforeSections: [summarySection],
|
|
257
284
|
section: {
|
|
@@ -272,9 +299,7 @@ export class SchedulePanel extends BasePanel {
|
|
|
272
299
|
title: ' Schedule',
|
|
273
300
|
intro: 'Review recurring scheduled tasks, next run timing, recent history, and enablement state.',
|
|
274
301
|
sections,
|
|
275
|
-
footerLines
|
|
276
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Space', DEFAULT_PANEL_PALETTE.info], [' toggle', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' run now', DEFAULT_PANEL_PALETTE.dim], [' R', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
277
|
-
],
|
|
302
|
+
footerLines,
|
|
278
303
|
palette: DEFAULT_PANEL_PALETTE,
|
|
279
304
|
});
|
|
280
305
|
}
|
|
@@ -291,11 +316,11 @@ export class SchedulePanel extends BasePanel {
|
|
|
291
316
|
|
|
292
317
|
const bullet = task.enabled ? '* ' : 'o ';
|
|
293
318
|
const bulletFg = task.enabled ? C.enabled : C.disabled;
|
|
294
|
-
const nameStr = task.name
|
|
319
|
+
const nameStr = fitDisplay(task.name, 28);
|
|
295
320
|
const scheduleText = formatSchedule(task.schedule);
|
|
296
321
|
const row1 = buildPanelLine(width, [
|
|
297
322
|
[bullet, bulletFg, bg],
|
|
298
|
-
[task.id
|
|
323
|
+
[fitDisplay(task.id, 12), fgBase ?? C.id, bg],
|
|
299
324
|
[' ', fgBase ?? C.prompt, bg],
|
|
300
325
|
[nameStr, fgBase ?? C.prompt, bg],
|
|
301
326
|
[' ', fgBase ?? C.prompt, bg],
|
|
@@ -318,9 +343,7 @@ export class SchedulePanel extends BasePanel {
|
|
|
318
343
|
|
|
319
344
|
const maxPromptLen = Math.max(20, width - indent.length - 30);
|
|
320
345
|
const prompt = task.execution.prompt ?? task.description ?? '';
|
|
321
|
-
const promptPreview = prompt
|
|
322
|
-
? prompt.slice(0, maxPromptLen - 1) + '\u2026'
|
|
323
|
-
: prompt;
|
|
346
|
+
const promptPreview = truncateDisplay(prompt, maxPromptLen);
|
|
324
347
|
|
|
325
348
|
// Show last 3 run statuses as colored dots
|
|
326
349
|
const recentRuns = history.slice(-3);
|