@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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
2
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
4
|
import type { TokenAuditResult } from '@pellux/goodvibes-sdk/platform/security';
|
|
4
5
|
import type { UiReadModel, UiSecuritySnapshot } from '../runtime/ui-read-models.ts';
|
|
5
6
|
import {
|
|
7
|
+
buildAlignedRow,
|
|
6
8
|
buildEmptyState,
|
|
7
9
|
buildGuidanceLine,
|
|
8
10
|
buildPanelLine,
|
|
@@ -63,6 +65,8 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
63
65
|
public constructor(private readonly readModel: UiReadModel<UiSecuritySnapshot>) {
|
|
64
66
|
super('security', 'Security', 'U', 'monitoring');
|
|
65
67
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
68
|
+
this.filterEnabled = true;
|
|
69
|
+
this.filterLabel = 'Filter tokens';
|
|
66
70
|
this.unsub = this.readModel.subscribe(() => this.markDirty());
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -85,24 +89,38 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
protected renderItem(result: TokenAuditResult, index: number, selected: boolean, width: number): Line {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
[
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
return buildAlignedRow(
|
|
93
|
+
width,
|
|
94
|
+
[
|
|
95
|
+
{ text: result.label, fg: C.value },
|
|
96
|
+
{ text: result.tokenId, fg: C.info },
|
|
97
|
+
{ text: result.scope.policyId, fg: C.label },
|
|
98
|
+
{ text: resultSummary(result), fg: resultColor(result) },
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
{ width: 22 },
|
|
102
|
+
{ width: 12 },
|
|
103
|
+
{ width: 10 },
|
|
104
|
+
{ width: Math.max(8, width - 50) },
|
|
105
|
+
],
|
|
106
|
+
{ selected, selectedBg: C.selectBg },
|
|
107
|
+
);
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
public handleInput(key: string): boolean {
|
|
99
|
-
if (key === 'r') {
|
|
111
|
+
if (!this.filterActive && key === 'r') {
|
|
100
112
|
this.markDirty();
|
|
101
113
|
return true;
|
|
102
114
|
}
|
|
103
115
|
return super.handleInput(key);
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
protected override filterMatches(result: TokenAuditResult, q: string): boolean {
|
|
119
|
+
return result.label.toLowerCase().includes(q)
|
|
120
|
+
|| result.scope.policyId.toLowerCase().includes(q)
|
|
121
|
+
|| result.tokenId.toLowerCase().includes(q);
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
public render(width: number, height: number): Line[] {
|
|
107
125
|
this.clampSelection();
|
|
108
126
|
const snapshot = this.readModel.getSnapshot();
|
|
@@ -168,7 +186,7 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
168
186
|
[' critical ', C.label],
|
|
169
187
|
...buildStatusPill(attackPathReview.incoherentFindings > 0 ? 'warn' : 'good', String(attackPathReview.incoherentFindings)),
|
|
170
188
|
[' review ', C.label],
|
|
171
|
-
[attackPathReview.summary
|
|
189
|
+
[truncateDisplay(attackPathReview.summary, Math.max(0, width - 36)), C.dim],
|
|
172
190
|
]),
|
|
173
191
|
];
|
|
174
192
|
|
|
@@ -205,20 +223,41 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
205
223
|
attackPathLines.push(buildPanelLine(width, [[' MCP attack-path review', C.label]]));
|
|
206
224
|
for (const finding of attackPathReview.findings.slice(0, 3)) {
|
|
207
225
|
attackPathLines.push(buildPanelLine(width, [[
|
|
208
|
-
` ${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}
|
|
226
|
+
truncateDisplay(` ${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}`, width),
|
|
209
227
|
severityColor(finding.severity),
|
|
210
228
|
]]));
|
|
211
229
|
attackPathLines.push(buildPanelLine(width, [[
|
|
212
|
-
` ${finding.reason}
|
|
230
|
+
truncateDisplay(` ${finding.reason}`, width),
|
|
213
231
|
C.dim,
|
|
214
232
|
]]));
|
|
215
233
|
attackPathLines.push(buildPanelLine(width, [[
|
|
216
|
-
` evidence: ${finding.evidence.join(' | ')}
|
|
234
|
+
truncateDisplay(` evidence: ${finding.evidence.join(' | ')}`, width),
|
|
217
235
|
C.dim,
|
|
218
236
|
]]));
|
|
219
237
|
}
|
|
220
238
|
}
|
|
221
239
|
|
|
240
|
+
// Column header for the token audit list so the aligned columns are legible.
|
|
241
|
+
const listHeader: Line[] = [
|
|
242
|
+
...governanceLines,
|
|
243
|
+
buildAlignedRow(
|
|
244
|
+
width,
|
|
245
|
+
[
|
|
246
|
+
{ text: 'TOKEN LABEL', fg: C.label, bold: true },
|
|
247
|
+
{ text: 'TOKEN ID', fg: C.label, bold: true },
|
|
248
|
+
{ text: 'POLICY', fg: C.label, bold: true },
|
|
249
|
+
{ text: `STATUS (${view.results.length} audited)`, fg: C.label, bold: true },
|
|
250
|
+
],
|
|
251
|
+
[
|
|
252
|
+
{ width: 22 },
|
|
253
|
+
{ width: 12 },
|
|
254
|
+
{ width: 10 },
|
|
255
|
+
{ width: Math.max(8, width - 50) },
|
|
256
|
+
],
|
|
257
|
+
{},
|
|
258
|
+
),
|
|
259
|
+
];
|
|
260
|
+
|
|
222
261
|
const selected = view.results[this.selectedIndex];
|
|
223
262
|
const detailLines: Line[] = [];
|
|
224
263
|
if (selected) {
|
|
@@ -234,7 +273,7 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
234
273
|
[' Scope: ', C.label],
|
|
235
274
|
[selected.scope.outcome, selected.scope.outcome === 'violation' ? C.error : C.ok],
|
|
236
275
|
[' Excess: ', C.label],
|
|
237
|
-
[(selected.scope.excessScopes.length > 0 ? selected.scope.excessScopes.join(', ') : 'none'
|
|
276
|
+
[truncateDisplay(selected.scope.excessScopes.length > 0 ? selected.scope.excessScopes.join(', ') : 'none', Math.max(0, width - 27)), selected.scope.excessScopes.length > 0 ? C.error : C.dim],
|
|
238
277
|
]));
|
|
239
278
|
detailLines.push(buildPanelLine(width, [
|
|
240
279
|
[' Rotation: ', C.label],
|
|
@@ -250,46 +289,59 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
250
289
|
]]));
|
|
251
290
|
if (preflightStatus !== 'n/a') {
|
|
252
291
|
detailLines.push(buildPanelLine(width, [[
|
|
253
|
-
`Policy preflight: ${preflightStatus} (${preflightIssueCount} issue${preflightIssueCount === 1 ? '' : 's'})
|
|
292
|
+
truncateDisplay(`Policy preflight: ${preflightStatus} (${preflightIssueCount} issue${preflightIssueCount === 1 ? '' : 's'})`, width),
|
|
254
293
|
preflightStatus === 'block' ? C.error : preflightStatus === 'warn' ? C.warn : C.dim,
|
|
255
294
|
]]));
|
|
256
295
|
}
|
|
257
296
|
if (quarantinedMcp.length > 0) {
|
|
258
297
|
const server = quarantinedMcp[0]!;
|
|
259
298
|
detailLines.push(buildPanelLine(width, [[
|
|
260
|
-
`MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}
|
|
299
|
+
truncateDisplay(`MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}`, width),
|
|
261
300
|
C.error,
|
|
262
301
|
]]));
|
|
263
302
|
}
|
|
264
303
|
if (quarantinedPlugins.length > 0) {
|
|
265
304
|
const plugin = quarantinedPlugins[0]!;
|
|
266
305
|
detailLines.push(buildPanelLine(width, [[
|
|
267
|
-
`Plugin quarantine: ${plugin.name} (${plugin.trustTier})
|
|
306
|
+
truncateDisplay(`Plugin quarantine: ${plugin.name} (${plugin.trustTier})`, width),
|
|
268
307
|
C.error,
|
|
269
308
|
]]));
|
|
270
309
|
} else if (untrustedPlugins.length > 0) {
|
|
271
310
|
const plugin = untrustedPlugins[0]!;
|
|
272
311
|
detailLines.push(buildPanelLine(width, [[
|
|
273
|
-
`Plugin trust warning: ${plugin.name} remains untrusted
|
|
312
|
+
truncateDisplay(`Plugin trust warning: ${plugin.name} remains untrusted.`, width),
|
|
274
313
|
C.warn,
|
|
275
314
|
]]));
|
|
276
315
|
}
|
|
277
316
|
if (latestIncident) {
|
|
278
317
|
detailLines.push(buildPanelLine(width, [[
|
|
279
|
-
`Latest incident: ${latestIncident.classification} - ${latestIncident.summary}
|
|
318
|
+
truncateDisplay(`Latest incident: ${latestIncident.classification} - ${latestIncident.summary}`, width),
|
|
280
319
|
C.warn,
|
|
281
320
|
]]));
|
|
282
321
|
}
|
|
283
322
|
}
|
|
284
323
|
|
|
324
|
+
const hints = this.filterActive
|
|
325
|
+
? [
|
|
326
|
+
{ keys: 'type', label: 'filter tokens' },
|
|
327
|
+
{ keys: 'Enter', label: 'apply' },
|
|
328
|
+
{ keys: 'Esc', label: 'clear' },
|
|
329
|
+
]
|
|
330
|
+
: [
|
|
331
|
+
{ keys: '↑/↓', label: 'select token' },
|
|
332
|
+
{ keys: '/', label: 'filter' },
|
|
333
|
+
{ keys: 'r', label: 'refresh audit' },
|
|
334
|
+
];
|
|
335
|
+
|
|
285
336
|
return this.renderList(width, height, {
|
|
286
337
|
title: 'Security Control Room',
|
|
287
|
-
header:
|
|
338
|
+
header: listHeader,
|
|
288
339
|
footer: [
|
|
289
340
|
...detailLines,
|
|
290
341
|
...attackPathLines,
|
|
291
342
|
footerLine,
|
|
292
343
|
],
|
|
344
|
+
hints,
|
|
293
345
|
});
|
|
294
346
|
}
|
|
295
347
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
5
|
import {
|
|
5
6
|
type ServiceConfig,
|
|
@@ -8,7 +9,9 @@ import {
|
|
|
8
9
|
} from '@pellux/goodvibes-sdk/platform/config';
|
|
9
10
|
import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
+
buildDetailBlock,
|
|
13
|
+
buildKeyValueLine,
|
|
14
|
+
buildKeyboardHints,
|
|
12
15
|
buildPanelLine,
|
|
13
16
|
buildPanelWorkspace,
|
|
14
17
|
buildStatusPill,
|
|
@@ -80,11 +83,18 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
80
83
|
) {
|
|
81
84
|
super('services', 'Services', 'V', 'monitoring');
|
|
82
85
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
86
|
+
this.filterEnabled = true;
|
|
87
|
+
this.filterLabel = 'Filter services';
|
|
83
88
|
this.registry = registry;
|
|
84
89
|
this.subscriptionManager = subscriptionManager;
|
|
85
90
|
void this.refresh();
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
protected override filterMatches(entry: ServicePanelEntry, q: string): boolean {
|
|
94
|
+
return entry.name.toLowerCase().includes(q)
|
|
95
|
+
|| (entry.inspection.config.baseUrl ?? '').toLowerCase().includes(q);
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
public override onActivate(): void {
|
|
89
99
|
super.onActivate();
|
|
90
100
|
if (this.entries.length === 0 && !this.loading) {
|
|
@@ -112,18 +122,20 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
112
122
|
[entry.name.padEnd(16), C.value, bg],
|
|
113
123
|
[` ${statusLabel(entry).padEnd(12)}`, statusColor(entry), bg],
|
|
114
124
|
[` ${authSummary(entry.inspection.config, this.subscriptionManager).padEnd(18)}`, C.info, bg],
|
|
115
|
-
[` ${entry.inspection.config.baseUrl ?? '(no baseUrl)'}`, C.dim, bg],
|
|
125
|
+
[` ${truncateDisplay(entry.inspection.config.baseUrl ?? '(no baseUrl)', Math.max(0, width - 48))}`, C.dim, bg],
|
|
116
126
|
]);
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
public handleInput(key: string): boolean {
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
if (!this.filterActive) {
|
|
131
|
+
if (key === 'r') {
|
|
132
|
+
void this.refresh();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (key === 't') {
|
|
136
|
+
void this.testSelected();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
127
139
|
}
|
|
128
140
|
return super.handleInput(key);
|
|
129
141
|
}
|
|
@@ -176,11 +188,29 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
176
188
|
return lines;
|
|
177
189
|
}
|
|
178
190
|
|
|
191
|
+
// Health-summary header — surface the most important counts first.
|
|
192
|
+
const counts = { healthy: 0, error: 0, configured: 0, unconfigured: 0 };
|
|
193
|
+
for (const entry of this.entries) {
|
|
194
|
+
const label = statusLabel(entry);
|
|
195
|
+
if (label === 'HEALTHY') counts.healthy++;
|
|
196
|
+
else if (label === 'ERROR') counts.error++;
|
|
197
|
+
else if (label === 'CONFIGURED') counts.configured++;
|
|
198
|
+
else counts.unconfigured++;
|
|
199
|
+
}
|
|
200
|
+
const headerLines: Line[] = [
|
|
201
|
+
buildKeyValueLine(width, [
|
|
202
|
+
{ label: 'services', value: String(this.entries.length), valueColor: this.entries.length > 0 ? C.info : C.dim },
|
|
203
|
+
{ label: 'healthy', value: String(counts.healthy), valueColor: counts.healthy > 0 ? C.ok : C.dim },
|
|
204
|
+
{ label: 'errors', value: String(counts.error), valueColor: counts.error > 0 ? C.error : C.dim },
|
|
205
|
+
{ label: 'unconfigured', value: String(counts.unconfigured), valueColor: counts.unconfigured > 0 ? C.warn : C.dim },
|
|
206
|
+
], C),
|
|
207
|
+
];
|
|
208
|
+
|
|
179
209
|
const selected = this.entries[this.selectedIndex];
|
|
180
|
-
const
|
|
210
|
+
const detailRows: Line[] = [];
|
|
181
211
|
if (selected) {
|
|
182
212
|
const inspect = selected.inspection;
|
|
183
|
-
|
|
213
|
+
detailRows.push(buildPanelLine(width, [
|
|
184
214
|
[' Service: ', C.label],
|
|
185
215
|
[selected.name, C.value],
|
|
186
216
|
[' State: ', C.label],
|
|
@@ -188,7 +218,11 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
188
218
|
[' Auth: ', C.label],
|
|
189
219
|
[authSummary(inspect.config, this.subscriptionManager), C.info],
|
|
190
220
|
]));
|
|
191
|
-
|
|
221
|
+
detailRows.push(buildPanelLine(width, [
|
|
222
|
+
[' Base URL: ', C.label],
|
|
223
|
+
[truncateDisplay(inspect.config.baseUrl ?? '(no baseUrl)', Math.max(0, width - 13)), C.dim],
|
|
224
|
+
]));
|
|
225
|
+
detailRows.push(buildPanelLine(width, [
|
|
192
226
|
[' Primary credential: ', C.label],
|
|
193
227
|
...buildStatusPill(inspect.hasPrimaryCredential ? 'good' : 'bad', inspect.hasPrimaryCredential ? 'present' : 'missing'),
|
|
194
228
|
[' Webhook URL: ', C.label],
|
|
@@ -199,32 +233,44 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
199
233
|
...buildStatusPill(inspect.hasAppToken ? 'good' : 'info', inspect.hasAppToken ? 'present' : 'missing'),
|
|
200
234
|
]));
|
|
201
235
|
if (selected.lastTest) {
|
|
202
|
-
|
|
236
|
+
detailRows.push(buildPanelLine(width, [
|
|
203
237
|
[' Last test: ', C.label],
|
|
204
238
|
...buildStatusPill(selected.lastTest.ok ? 'good' : 'bad', selected.lastTest.ok ? 'ok' : 'failed'),
|
|
205
239
|
[' Status: ', C.label],
|
|
206
240
|
[selected.lastTest.status != null ? String(selected.lastTest.status) : 'n/a', C.value],
|
|
207
241
|
[' URL: ', C.label],
|
|
208
|
-
[(selected.lastTest.testedUrl ?? 'n/a'
|
|
242
|
+
[truncateDisplay(selected.lastTest.testedUrl ?? 'n/a', Math.max(0, width - 34)), C.dim],
|
|
209
243
|
]));
|
|
210
244
|
if (selected.lastTest.error) {
|
|
211
|
-
|
|
245
|
+
detailRows.push(buildPanelLine(width, [
|
|
212
246
|
[' Error: ', C.label],
|
|
213
|
-
[selected.lastTest.error
|
|
247
|
+
[truncateDisplay(selected.lastTest.error, Math.max(0, width - 10)), C.error],
|
|
214
248
|
]));
|
|
215
249
|
}
|
|
216
250
|
} else {
|
|
217
|
-
|
|
251
|
+
detailRows.push(buildPanelLine(width, [[' Not tested yet — press t to run a live connection check.', C.dim]]));
|
|
218
252
|
}
|
|
219
|
-
detailLines.push(buildPanelLine(width, [[' Services resolve credentials through hierarchy-aware secure storage, plaintext fallback policy, and project-local config.', C.dim]]));
|
|
220
253
|
}
|
|
221
254
|
|
|
255
|
+
// Context-aware hints: filter mode swaps to filter-specific keys; the test
|
|
256
|
+
// key only makes sense when there is a service to test.
|
|
257
|
+
const hints = this.filterActive
|
|
258
|
+
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
259
|
+
: [
|
|
260
|
+
{ keys: 'Up/Down', label: 'move' },
|
|
261
|
+
...(selected ? [{ keys: 't', label: 'test selected' }] : []),
|
|
262
|
+
{ keys: 'r', label: 'refresh' },
|
|
263
|
+
{ keys: '/', label: 'filter' },
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
const footer: Line[] = selected
|
|
267
|
+
? [...buildDetailBlock(width, `Service · ${selected.name}`, detailRows, C), buildKeyboardHints(width, hints, C)]
|
|
268
|
+
: [buildKeyboardHints(width, hints, C)];
|
|
269
|
+
|
|
222
270
|
return this.renderList(width, height, {
|
|
223
271
|
title: 'Service Control Room',
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
buildPanelLine(width, [[' Up/Down move t test selected service r refresh inspections', C.dim]]),
|
|
227
|
-
],
|
|
272
|
+
header: headerLines,
|
|
273
|
+
footer,
|
|
228
274
|
emptyMessage: intro,
|
|
229
275
|
});
|
|
230
276
|
}
|
|
@@ -9,15 +9,17 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
9
9
|
import type { SessionBrowserQuery } from '../runtime/ui-service-queries.ts';
|
|
10
10
|
import {
|
|
11
11
|
buildEmptyState,
|
|
12
|
+
buildKeyboardHints,
|
|
12
13
|
buildPanelLine,
|
|
13
14
|
buildSearchInputLine,
|
|
14
15
|
buildStyledPanelLine,
|
|
15
16
|
buildPanelWorkspace,
|
|
16
17
|
resolveScrollablePanelSection,
|
|
17
18
|
DEFAULT_PANEL_PALETTE,
|
|
19
|
+
extendPalette,
|
|
18
20
|
type PanelWorkspaceSection,
|
|
19
21
|
} from './polish.ts';
|
|
20
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
22
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
21
23
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
22
24
|
import {
|
|
23
25
|
getPanelSearchFocusTransition,
|
|
@@ -28,24 +30,15 @@ import {
|
|
|
28
30
|
} from './search-focus.ts';
|
|
29
31
|
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
label: '#8888bb',
|
|
41
|
-
value: '#ccccdd',
|
|
42
|
-
dateFg: '#6699aa',
|
|
43
|
-
modelFg: '#99aacc',
|
|
44
|
-
countFg: '#88bbcc',
|
|
45
|
-
warnFg: '#ffcc44',
|
|
46
|
-
errorFg: '#ff6666',
|
|
47
|
-
separator: '#333355',
|
|
48
|
-
} as const;
|
|
33
|
+
// Panel-specific extras only; shared tones come from DEFAULT_PANEL_PALETTE so
|
|
34
|
+
// theme changes propagate. `selected` is a deliberate bright-cyan cursor accent
|
|
35
|
+
// and the date/model/count column tints have no clean shared equivalent.
|
|
36
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
37
|
+
selected: '#00ffff',
|
|
38
|
+
dateFg: '#6699aa',
|
|
39
|
+
modelFg: '#99aacc',
|
|
40
|
+
countFg: '#88bbcc',
|
|
41
|
+
});
|
|
49
42
|
|
|
50
43
|
function shortDate(ts: number): string {
|
|
51
44
|
const d = new Date(ts);
|
|
@@ -92,6 +85,7 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
92
85
|
private confirm: ConfirmState<string> | null = null;
|
|
93
86
|
private deleteError = '';
|
|
94
87
|
private loadError = '';
|
|
88
|
+
private hasLoaded = false;
|
|
95
89
|
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
96
90
|
|
|
97
91
|
constructor(
|
|
@@ -177,8 +171,15 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
177
171
|
if (height <= 0 || width <= 0) return [];
|
|
178
172
|
const intro = 'Browse, search, resume, and prune saved conversations.';
|
|
179
173
|
|
|
174
|
+
// Lazily load on first render so the panel works even when rendered without
|
|
175
|
+
// onActivate(). The session list is a synchronous source, so this resolves
|
|
176
|
+
// immediately rather than flashing a loading screen.
|
|
177
|
+
if (!this.hasLoaded) this._load();
|
|
178
|
+
|
|
180
179
|
const count = this.filtered.length;
|
|
181
180
|
const total = this.sessions.length;
|
|
181
|
+
const hasSelection = this.filtered.length > 0;
|
|
182
|
+
// Search/status line — surfaces the active query or any error.
|
|
182
183
|
const searchLine = this.searching
|
|
183
184
|
? ` Search: ${this.searchQuery}_`
|
|
184
185
|
: this.loadError
|
|
@@ -186,11 +187,21 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
186
187
|
: this.deleteError
|
|
187
188
|
? ` Error: ${this.deleteError}`
|
|
188
189
|
: this.searchQuery
|
|
189
|
-
? ` Filter: ${this.searchQuery}
|
|
190
|
-
: `
|
|
190
|
+
? ` Filter: ${this.searchQuery}`
|
|
191
|
+
: ` (no filter)`;
|
|
191
192
|
const statusFg = this.loadError || this.deleteError ? DEFAULT_PANEL_PALETTE.bad : this.searching ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim;
|
|
193
|
+
// Context-aware hints: only advertise keys that work in the current state.
|
|
194
|
+
const hints: Array<{ keys: string; label: string }> = [];
|
|
195
|
+
if (this.searching) {
|
|
196
|
+
hints.push({ keys: 'type', label: 'filter' }, { keys: 'Esc/Enter', label: 'apply' });
|
|
197
|
+
} else {
|
|
198
|
+
hints.push({ keys: '/', label: this.searchQuery ? 'edit search' : 'search' });
|
|
199
|
+
if (hasSelection) hints.push({ keys: 'Enter', label: 'resume' }, { keys: 'd', label: 'delete' });
|
|
200
|
+
hints.push({ keys: 'r', label: 'refresh' });
|
|
201
|
+
}
|
|
192
202
|
const footerLines = [
|
|
193
203
|
buildSearchInputLine(width, '', searchLine.trimStart(), DEFAULT_PANEL_PALETTE, { active: this.searching, valueColor: statusFg }),
|
|
204
|
+
buildKeyboardHints(width, hints, DEFAULT_PANEL_PALETTE),
|
|
194
205
|
];
|
|
195
206
|
|
|
196
207
|
if (this.confirm) {
|
|
@@ -216,12 +227,15 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
216
227
|
const emptyBody = this.searchQuery
|
|
217
228
|
? 'Clear or change the current filter to surface saved conversations again.'
|
|
218
229
|
: 'Conversations are saved automatically. Once you have saved sessions, they appear here for review and resume.';
|
|
230
|
+
const emptyActions = this.searchQuery
|
|
231
|
+
? [{ command: 'Esc', summary: 'clear the filter and show all sessions' }]
|
|
232
|
+
: [{ command: '/session save', summary: 'name and persist the current conversation' }];
|
|
219
233
|
return buildPanelWorkspace(width, height, {
|
|
220
234
|
title: ` Sessions [${count}/${total}]`,
|
|
221
235
|
intro,
|
|
222
236
|
sections: [
|
|
223
237
|
{
|
|
224
|
-
lines: buildEmptyState(width, emptyTitle, emptyBody,
|
|
238
|
+
lines: buildEmptyState(width, emptyTitle, emptyBody, emptyActions, DEFAULT_PANEL_PALETTE),
|
|
225
239
|
},
|
|
226
240
|
],
|
|
227
241
|
footerLines,
|
|
@@ -308,19 +322,19 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
308
322
|
}
|
|
309
323
|
|
|
310
324
|
private _renderSession(width: number, sess: SessionInfo, isCursor: boolean): Line {
|
|
311
|
-
const bg = isCursor ? C.
|
|
325
|
+
const bg = isCursor ? C.selectBg : '';
|
|
312
326
|
const date = shortDate(sess.timestamp);
|
|
313
327
|
const cnt = String(sess.messageCount).padStart(3) + 'm ';
|
|
314
|
-
const model = (sess.model || 'unknown'
|
|
328
|
+
const model = fitDisplay(sess.model || 'unknown', 18) + ' ';
|
|
315
329
|
const prefixLength = 1 + 16 + 1 + 4 + 19;
|
|
316
330
|
const title = truncateDisplay(sess.title || sess.name || '(untitled)', Math.max(0, width - prefixLength));
|
|
317
331
|
return buildStyledPanelLine(width, [
|
|
318
332
|
{ text: isCursor ? '▸' : ' ', fg: C.selected, bg, bold: isCursor },
|
|
319
333
|
{ text: date, fg: C.dateFg, bg },
|
|
320
|
-
{ text: ' ', fg: C.
|
|
334
|
+
{ text: ' ', fg: C.value, bg },
|
|
321
335
|
{ text: cnt, fg: C.countFg, bg },
|
|
322
336
|
{ text: model, fg: C.modelFg, bg },
|
|
323
|
-
{ text: title, fg: isCursor ? C.selected : C.
|
|
337
|
+
{ text: title, fg: isCursor ? C.selected : C.value, bg, bold: isCursor },
|
|
324
338
|
]);
|
|
325
339
|
}
|
|
326
340
|
|
|
@@ -329,10 +343,12 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
329
343
|
this.sessions = this.sessionManager.list();
|
|
330
344
|
this._filter();
|
|
331
345
|
this.loadError = '';
|
|
346
|
+
this.hasLoaded = true;
|
|
332
347
|
this.markDirty();
|
|
333
348
|
} catch (e) {
|
|
334
349
|
logger.debug('SessionBrowserPanel._load failed', { error: summarizeError(e) });
|
|
335
350
|
this.loadError = 'Failed to load sessions';
|
|
351
|
+
this.hasLoaded = true;
|
|
336
352
|
this.markDirty();
|
|
337
353
|
}
|
|
338
354
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* that passes configManager has a coherent call site.
|
|
11
11
|
*/
|
|
12
12
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
13
|
+
import { computeContextUsage } from '../core/context-usage.ts';
|
|
13
14
|
|
|
14
15
|
export type PanelGuidanceMode = 'off' | 'minimal' | 'guided';
|
|
15
16
|
export type PanelSessionMaintenanceLevel = 'stable' | 'watch' | 'suggest-compact' | 'compacting' | 'needs-repair' | 'unknown';
|
|
@@ -76,8 +77,7 @@ export function evaluateSessionMaintenance(input: PanelSessionMaintenanceInput):
|
|
|
76
77
|
const thresholdPct = Math.max(0, Number.isFinite(rawThreshold) ? rawThreshold : 0);
|
|
77
78
|
const autoCompactEnabled = thresholdPct > 0;
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
-
const remainingTokens = Math.max(0, input.contextWindow - input.currentTokens);
|
|
80
|
+
const { pct: usagePct, remaining: remainingTokens } = computeContextUsage(input.currentTokens, input.contextWindow);
|
|
81
81
|
const sessionMemoryCount = Math.max(0, input.sessionMemoryCount ?? 0);
|
|
82
82
|
const compactionCount = Math.max(0, input.session?.lineage?.filter((entry) => entry.branchReason === 'compaction').length ?? 0);
|
|
83
83
|
const lastCompactedAt = input.session?.lastCompactedAt;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
2
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
4
|
import {
|
|
4
5
|
buildDetailBlock,
|
|
5
6
|
buildGuidanceLine,
|
|
7
|
+
buildKeyboardHints,
|
|
6
8
|
buildPanelListRow,
|
|
7
9
|
buildPanelLine,
|
|
8
10
|
buildStatusPill,
|
|
@@ -28,6 +30,14 @@ export class SettingsSyncPanel extends ScrollableListPanel<ResolvedEntry> {
|
|
|
28
30
|
public constructor(private readonly configManager: ConfigManager) {
|
|
29
31
|
super('settings-sync', 'Settings Sync', 'S', 'monitoring');
|
|
30
32
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
33
|
+
this.filterEnabled = true;
|
|
34
|
+
this.filterLabel = 'Filter settings';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected override filterMatches(entry: ResolvedEntry, q: string): boolean {
|
|
38
|
+
return entry.key.toLowerCase().includes(q)
|
|
39
|
+
|| String(entry.effectiveSource).toLowerCase().includes(q)
|
|
40
|
+
|| String(entry.effectiveValue).toLowerCase().includes(q);
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
protected override getPalette(): PanelPalette {
|
|
@@ -40,9 +50,9 @@ export class SettingsSyncPanel extends ScrollableListPanel<ResolvedEntry> {
|
|
|
40
50
|
|
|
41
51
|
protected renderItem(entry: ResolvedEntry, _index: number, selected: boolean, width: number): Line {
|
|
42
52
|
return buildPanelListRow(width, [
|
|
43
|
-
{ text: entry.key.padEnd(32), fg: C.value },
|
|
53
|
+
{ text: fitDisplay(entry.key, 32).padEnd(32), fg: C.value },
|
|
44
54
|
{ text: ` ${entry.effectiveSource}`.padEnd(11), fg: entry.effectiveSource === 'managed' ? C.warn : entry.effectiveSource === 'synced' ? C.ok : entry.effectiveSource === 'local' ? C.info : C.dim },
|
|
45
|
-
{ text: `${String(entry.effectiveValue)}
|
|
55
|
+
{ text: truncateDisplay(`${String(entry.effectiveValue)}`, Math.max(0, width - 47)), fg: entry.locked ? C.warn : C.dim },
|
|
46
56
|
], C, { selected });
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -69,28 +79,28 @@ export class SettingsSyncPanel extends ScrollableListPanel<ResolvedEntry> {
|
|
|
69
79
|
...(snapshot.stagedManagedBundle
|
|
70
80
|
? [
|
|
71
81
|
buildPanelLine(width, [[' profile ', C.label], [snapshot.stagedManagedBundle.profileName, C.value], [' risk ', C.label], [snapshot.stagedManagedBundle.risk, snapshot.stagedManagedBundle.risk === 'high' ? C.error : snapshot.stagedManagedBundle.risk === 'medium' ? C.warn : C.ok], [' changes ', C.label], [String(snapshot.stagedManagedBundle.changeCount), C.info]]),
|
|
72
|
-
buildPanelLine(width, [[' path ', C.label], [snapshot.stagedManagedBundle.path
|
|
82
|
+
buildPanelLine(width, [[' path ', C.label], [truncateDisplay(snapshot.stagedManagedBundle.path, Math.max(0, width - 9)), C.dim]]),
|
|
73
83
|
]
|
|
74
84
|
: [buildPanelLine(width, [[' No staged managed settings bundle.', C.dim]])]),
|
|
75
85
|
// Recent Events
|
|
76
86
|
...(snapshot.recentEvents.length > 0
|
|
77
|
-
? snapshot.recentEvents.map((event) => buildPanelLine(width, [[` ${event.surface}/${event.direction}
|
|
87
|
+
? snapshot.recentEvents.map((event) => buildPanelLine(width, [[fitDisplay(` ${event.surface}/${event.direction}`, 18).padEnd(18), C.info], [truncateDisplay(` ${event.detail}`, Math.max(0, width - 20)), C.dim]]))
|
|
78
88
|
: [buildPanelLine(width, [[' No sync or managed-setting events recorded yet.', C.dim]])]),
|
|
79
89
|
// Managed Locks
|
|
80
90
|
...(snapshot.managedLocks.length > 0
|
|
81
|
-
? snapshot.managedLocks.slice(0, 10).map((lock) => buildPanelLine(width, [[` ${lock.key}
|
|
91
|
+
? snapshot.managedLocks.slice(0, 10).map((lock) => buildPanelLine(width, [[fitDisplay(` ${lock.key}`, 30).padEnd(30), C.value], [fitDisplay(` source=${lock.source}`, 24).padEnd(24), C.info], [truncateDisplay(` ${lock.reason}`, Math.max(0, width - 56)), C.dim]]))
|
|
82
92
|
: [buildPanelLine(width, [[' No managed locks are currently active.', C.dim]])]),
|
|
83
93
|
// Failures
|
|
84
94
|
...(snapshot.recentFailures.length > 0
|
|
85
|
-
? snapshot.recentFailures.map((failure) => buildPanelLine(width, [[` ${failure.surface}`.padEnd(10), C.error], [` ${failure.message}
|
|
95
|
+
? snapshot.recentFailures.map((failure) => buildPanelLine(width, [[` ${failure.surface}`.padEnd(10), C.error], [truncateDisplay(` ${failure.message}`, Math.max(0, width - 12)), C.dim]]))
|
|
86
96
|
: [buildPanelLine(width, [[' No recent sync or managed-setting failures.', C.dim]])]),
|
|
87
97
|
// Conflicts
|
|
88
98
|
...(snapshot.conflicts.length > 0
|
|
89
|
-
? snapshot.conflicts.map((conflict) => buildPanelLine(width, [[` ${conflict.key}
|
|
99
|
+
? snapshot.conflicts.map((conflict) => buildPanelLine(width, [[fitDisplay(` ${conflict.key}`, 30).padEnd(30), C.value], [fitDisplay(` ${conflict.source}`, 10).padEnd(10), C.warn], [truncateDisplay(` resolve: /settings-sync resolve ${conflict.key} local|synced`, Math.max(0, width - 42)), C.dim]]))
|
|
90
100
|
: [buildPanelLine(width, [[' No settings conflicts detected.', C.dim]])]),
|
|
91
101
|
// Rollback History
|
|
92
102
|
...(snapshot.rollbackHistory.length > 0
|
|
93
|
-
? snapshot.rollbackHistory.map((entry) => buildPanelLine(width, [[` ${entry.token}
|
|
103
|
+
? snapshot.rollbackHistory.map((entry) => buildPanelLine(width, [[fitDisplay(` ${entry.token}`, 18).padEnd(18), C.info], [fitDisplay(` ${entry.profileName}`, 18).padEnd(18), C.value], [` restored=${String(entry.restoredKeys.length).padEnd(4)}`, C.warn], [truncateDisplay(` ${new Date(entry.appliedAt).toLocaleString()}`, Math.max(0, width - 46)), C.dim]]))
|
|
94
104
|
: [buildPanelLine(width, [[' No managed rollback records yet.', C.dim]])]),
|
|
95
105
|
];
|
|
96
106
|
|
|
@@ -101,14 +111,19 @@ export class SettingsSyncPanel extends ScrollableListPanel<ResolvedEntry> {
|
|
|
101
111
|
? buildDetailBlock(width, 'Selected setting', [
|
|
102
112
|
buildPanelLine(width, [[' key ', C.label], [selectedEntry.key, C.value], [' category ', C.label], [selectedEntry.category, C.info]]),
|
|
103
113
|
buildPanelLine(width, [[' effective ', C.label], [selectedEntry.effectiveSource, selectedEntry.effectiveSource === 'managed' ? C.warn : selectedEntry.effectiveSource === 'synced' ? C.ok : selectedEntry.effectiveSource === 'local' ? C.info : C.dim], [' locked ', C.label], [selectedEntry.locked ? 'yes' : 'no', selectedEntry.locked ? C.warn : C.dim], [' conflict ', C.label], [selectedEntry.conflict ? 'yes' : 'no', selectedEntry.conflict ? C.error : C.good]]),
|
|
104
|
-
buildPanelLine(width, [[' source ', C.label], [(selectedEntry.sourceLabel ?? 'local/default'
|
|
105
|
-
buildPanelLine(width, [[' overrides ', C.label], [(selectedEntry.overriddenSources.length > 0 ? selectedEntry.overriddenSources.join(', ') : 'none'
|
|
106
|
-
buildPanelLine(width, [[' local ', C.label], [String(selectedEntry.localValue)
|
|
107
|
-
buildPanelLine(width, [[' synced ', C.label], [String(selectedEntry.syncedValue ?? '(unset)')
|
|
108
|
-
buildPanelLine(width, [[' managed ', C.label], [String(selectedEntry.managedValue ?? '(unset)')
|
|
114
|
+
buildPanelLine(width, [[' source ', C.label], [truncateDisplay(selectedEntry.sourceLabel ?? 'local/default', Math.max(0, width - 10)), C.dim]]),
|
|
115
|
+
buildPanelLine(width, [[' overrides ', C.label], [truncateDisplay(selectedEntry.overriddenSources.length > 0 ? selectedEntry.overriddenSources.join(', ') : 'none', Math.max(0, width - 13)), C.dim]]),
|
|
116
|
+
buildPanelLine(width, [[' local ', C.label], [truncateDisplay(String(selectedEntry.localValue), Math.max(0, width - 9)), C.dim]]),
|
|
117
|
+
buildPanelLine(width, [[' synced ', C.label], [truncateDisplay(String(selectedEntry.syncedValue ?? '(unset)'), Math.max(0, width - 10)), C.ok]]),
|
|
118
|
+
buildPanelLine(width, [[' managed ', C.label], [truncateDisplay(String(selectedEntry.managedValue ?? '(unset)'), Math.max(0, width - 11)), C.warn]]),
|
|
109
119
|
], C)
|
|
110
|
-
: []),
|
|
111
|
-
|
|
120
|
+
: [buildPanelLine(width, [[' Select a setting above to inspect its effective value, source, and overrides.', C.dim]])]),
|
|
121
|
+
buildKeyboardHints(width, [
|
|
122
|
+
{ keys: '↑/↓', label: 'browse' },
|
|
123
|
+
{ keys: '/', label: 'filter' },
|
|
124
|
+
{ keys: '/settings-sync show <key>', label: 'inspect' },
|
|
125
|
+
{ keys: '/settings-sync resolve <key> <local|synced>', label: 'resolve conflict' },
|
|
126
|
+
], C),
|
|
112
127
|
];
|
|
113
128
|
|
|
114
129
|
return this.renderList(width, height, {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getPanelSearchFocusTransition,
|
|
17
17
|
isPanelSearchCancel,
|
|
18
18
|
} from './search-focus.ts';
|
|
19
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
19
20
|
|
|
20
21
|
const C = {
|
|
21
22
|
...DEFAULT_PANEL_PALETTE,
|
|
@@ -259,7 +260,7 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
|
259
260
|
this.invalidateFilter();
|
|
260
261
|
});
|
|
261
262
|
} catch (err) {
|
|
262
|
-
this.setError(
|
|
263
|
+
this.setError(summarizeError(err));
|
|
263
264
|
}
|
|
264
265
|
this.markDirty();
|
|
265
266
|
})();
|