@pellux/goodvibes-tui 0.18.23 → 0.19.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 +34 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -3
- package/src/core/conversation-rendering.ts +8 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/input/commands/diff-runtime.ts +6 -5
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/shell-core.ts +3 -2
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/subscription-runtime.ts +4 -4
- package/src/input/handler.ts +8 -10
- package/src/input/panel-integration-actions.ts +2 -1
- package/src/input/settings-modal-types.ts +60 -0
- package/src/input/settings-modal.ts +83 -65
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +1 -0
- package/src/panels/automation-control-panel.ts +1 -0
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +1 -0
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +1 -0
- package/src/panels/diff-panel.ts +2 -0
- package/src/panels/file-explorer-panel.ts +51 -31
- package/src/panels/file-preview-panel.ts +57 -35
- package/src/panels/git-panel.ts +12 -13
- package/src/panels/hooks-panel.ts +3 -1
- package/src/panels/incident-review-panel.ts +4 -2
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +1 -0
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +3 -1
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +1 -0
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +3 -0
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +1 -0
- package/src/panels/polish.ts +51 -2
- package/src/panels/provider-accounts-panel.ts +1 -0
- package/src/panels/provider-health-panel.ts +6 -8
- package/src/panels/routes-panel.ts +3 -1
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +19 -2
- package/src/panels/security-panel.ts +17 -15
- package/src/panels/services-panel.ts +6 -4
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +3 -1
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +1 -0
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +1 -0
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +1 -0
- package/src/panels/worktree-panel.ts +1 -0
- package/src/panels/wrfc-panel.ts +2 -0
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/buffer.ts +12 -1
- package/src/renderer/help-overlay.ts +14 -3
- package/src/renderer/settings-modal-helpers.ts +27 -0
- package/src/renderer/settings-modal.ts +18 -1
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +4 -8
- package/src/renderer/tool-call.ts +4 -3
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap.ts +7 -8
- package/src/runtime/diagnostics/panels/policy.ts +2 -1
- package/src/shell/ui-openers.ts +1 -1
- package/src/version.ts +1 -1
package/src/panels/polish.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
|
3
3
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
4
4
|
import { getSurfaceContentRows, getTrackedVisibleWindow, getVisibleWindow, type VisibleWindow } from '../renderer/surface-layout.ts';
|
|
5
5
|
import { GLYPHS, UI_TONES } from '../renderer/ui-primitives.ts';
|
|
6
|
+
import { type StatusState, STATE_GLYPHS } from '../renderer/status-glyphs.ts';
|
|
6
7
|
|
|
7
8
|
export interface PanelPalette {
|
|
8
9
|
readonly label: string;
|
|
@@ -42,13 +43,36 @@ export const DEFAULT_PANEL_PALETTE: Readonly<Required<PanelPalette>> = {
|
|
|
42
43
|
selectBg: UI_TONES.bg.selected,
|
|
43
44
|
} as const;
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Extend the base panel palette with domain-specific colors.
|
|
48
|
+
*
|
|
49
|
+
* Convention: raw hex colors may only live inside a palette constant declared
|
|
50
|
+
* at the top of a panel file, not inline in render calls.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
55
|
+
* decision: '#38bdf8',
|
|
56
|
+
* incident: '#ef4444',
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function extendPalette<T extends Record<string, string>>(
|
|
61
|
+
base: typeof DEFAULT_PANEL_PALETTE,
|
|
62
|
+
extras: T,
|
|
63
|
+
): typeof DEFAULT_PANEL_PALETTE & T {
|
|
64
|
+
return { ...base, ...extras };
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
export function buildPanelLine(
|
|
46
68
|
width: number,
|
|
47
|
-
segments: Array<[string, string, string?]>,
|
|
69
|
+
segments: Array<StyledPanelSegment | [string, string, string?]>,
|
|
48
70
|
): Line {
|
|
49
71
|
return buildStyledPanelLine(
|
|
50
72
|
width,
|
|
51
|
-
segments.map((
|
|
73
|
+
segments.map((seg) =>
|
|
74
|
+
Array.isArray(seg) ? { text: seg[0], fg: seg[1], bg: seg[2] } : seg,
|
|
75
|
+
),
|
|
52
76
|
);
|
|
53
77
|
}
|
|
54
78
|
|
|
@@ -256,6 +280,31 @@ export function buildSearchInputLine(
|
|
|
256
280
|
], { fillBg: bg });
|
|
257
281
|
}
|
|
258
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Build a status pill segment (glyph + label) for use in buildPanelLine.
|
|
285
|
+
*
|
|
286
|
+
* Returns StyledPanelSegment[] — spread directly into a buildPanelLine segments
|
|
287
|
+
* array:
|
|
288
|
+
* buildPanelLine(width, [[' count ', C.label], ...buildStatusPill('bad', '3')])
|
|
289
|
+
*
|
|
290
|
+
* Convention: raw hex colors may only live inside a palette constant declared at
|
|
291
|
+
* the top of a panel file; buildStatusPill derives its color from the palette.
|
|
292
|
+
*/
|
|
293
|
+
export function buildStatusPill(
|
|
294
|
+
state: StatusState,
|
|
295
|
+
label: string,
|
|
296
|
+
opts?: { glyph?: string; bg?: string; count?: number },
|
|
297
|
+
): StyledPanelSegment[] {
|
|
298
|
+
const glyph = opts?.glyph ?? STATE_GLYPHS[state];
|
|
299
|
+
const color = state === 'good' ? DEFAULT_PANEL_PALETTE.good
|
|
300
|
+
: state === 'warn' ? DEFAULT_PANEL_PALETTE.warn
|
|
301
|
+
: state === 'bad' ? DEFAULT_PANEL_PALETTE.bad
|
|
302
|
+
: DEFAULT_PANEL_PALETTE.info;
|
|
303
|
+
const bg = opts?.bg;
|
|
304
|
+
const text = opts?.count !== undefined ? `${glyph} ${label} (${opts.count})` : `${glyph} ${label}`;
|
|
305
|
+
return [{ text, fg: color, bg }];
|
|
306
|
+
}
|
|
307
|
+
|
|
259
308
|
export function buildStatPill(
|
|
260
309
|
label: string,
|
|
261
310
|
value: string,
|
|
@@ -36,6 +36,7 @@ export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRe
|
|
|
36
36
|
|
|
37
37
|
public constructor(deps: ProviderAccountsPanelDeps) {
|
|
38
38
|
super('accounts', 'Accounts', 'Q', 'monitoring');
|
|
39
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
39
40
|
this.providerAccounts = deps.providerAccounts;
|
|
40
41
|
void this.refresh();
|
|
41
42
|
}
|
|
@@ -362,7 +362,7 @@ function buildProviderRuntimeRecord(
|
|
|
362
362
|
*/
|
|
363
363
|
export class ProviderHealthPanel extends BasePanel {
|
|
364
364
|
private _unsubs: Array<() => void> = [];
|
|
365
|
-
private
|
|
365
|
+
private _refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
366
366
|
private _selectedIndex = 0;
|
|
367
367
|
private _scrollOffset = 0;
|
|
368
368
|
private _accountRecords = new Map<string, ProviderRuntimeRecord>();
|
|
@@ -461,23 +461,21 @@ export class ProviderHealthPanel extends BasePanel {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
override onDestroy(): void {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
this._refreshTimer = null;
|
|
467
|
-
}
|
|
464
|
+
super.onDestroy();
|
|
465
|
+
this._refreshTimerId = null;
|
|
468
466
|
for (const unsub of this._unsubs) unsub();
|
|
469
467
|
this._unsubs = [];
|
|
470
468
|
}
|
|
471
469
|
|
|
472
470
|
private _ensureRefreshTimer(): void {
|
|
473
|
-
if (this.
|
|
474
|
-
this.
|
|
471
|
+
if (this._refreshTimerId !== null) return;
|
|
472
|
+
this._refreshTimerId = this.registerTimer(setInterval(() => {
|
|
475
473
|
if (Date.now() - this._accountRefreshAt > 30_000) {
|
|
476
474
|
void this._refreshAccountPosture();
|
|
477
475
|
}
|
|
478
476
|
this.markDirty();
|
|
479
477
|
this.requestRender();
|
|
480
|
-
}, 1_000);
|
|
478
|
+
}, 1_000));
|
|
481
479
|
}
|
|
482
480
|
|
|
483
481
|
handleInput(key: string): boolean {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
buildKeyValueLine,
|
|
10
10
|
buildPanelLine,
|
|
11
11
|
buildPanelWorkspace,
|
|
12
|
+
buildStatusPill,
|
|
12
13
|
DEFAULT_PANEL_PALETTE,
|
|
13
14
|
type PanelPalette,
|
|
14
15
|
} from './polish.ts';
|
|
@@ -37,6 +38,7 @@ export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
|
|
|
37
38
|
|
|
38
39
|
public constructor(readModel?: UiReadModel<UiRoutesSnapshot>) {
|
|
39
40
|
super('routes', 'Routes', 'R', 'monitoring');
|
|
41
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
40
42
|
this.readModel = readModel;
|
|
41
43
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
42
44
|
}
|
|
@@ -60,7 +62,7 @@ export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
|
|
|
60
62
|
[' ', C.label, bg],
|
|
61
63
|
[binding.surfaceKind.padEnd(9), C.info, bg],
|
|
62
64
|
[` ${truncateDisplay(binding.title ?? binding.externalId, 22).padEnd(22)}`, C.value, bg],
|
|
63
|
-
|
|
65
|
+
...buildStatusPill(binding.sessionId ? 'good' : 'warn', ` ${truncateDisplay(binding.sessionId ?? binding.runId ?? 'unbound', 18).padEnd(18)}`, { bg }),
|
|
64
66
|
[` ${truncateDisplay(formatTime(binding.lastSeenAt), Math.max(0, width - 54))}`, C.dim, bg],
|
|
65
67
|
]);
|
|
66
68
|
}
|
|
@@ -87,7 +87,7 @@ export class SchedulePanel extends BasePanel {
|
|
|
87
87
|
private items: ViewItem[] = [];
|
|
88
88
|
private selectedIndex = 0;
|
|
89
89
|
private scrollOffset = 0;
|
|
90
|
-
private
|
|
90
|
+
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
91
91
|
private readonly automationManager: ScheduleAutomationManager;
|
|
92
92
|
|
|
93
93
|
constructor(automationManager: ScheduleAutomationManager) {
|
|
@@ -106,21 +106,22 @@ export class SchedulePanel extends BasePanel {
|
|
|
106
106
|
this.markDirty();
|
|
107
107
|
});
|
|
108
108
|
this.rebuild();
|
|
109
|
-
this.
|
|
109
|
+
this.refreshTimerId = this.registerTimer(setInterval(() => {
|
|
110
110
|
this.rebuild();
|
|
111
111
|
this.markDirty();
|
|
112
|
-
}, 5_000);
|
|
112
|
+
}, 5_000));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
override onDeactivate(): void {
|
|
116
|
-
if (this.
|
|
117
|
-
|
|
118
|
-
this.
|
|
116
|
+
if (this.refreshTimerId !== null) {
|
|
117
|
+
this.clearTimer(this.refreshTimerId);
|
|
118
|
+
this.refreshTimerId = null;
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
override onDestroy(): void {
|
|
123
123
|
this.onDeactivate();
|
|
124
|
+
super.onDestroy();
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
// -------------------------------------------------------------------------
|
|
@@ -122,9 +122,26 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
122
122
|
// Navigation — consistent across ALL panels
|
|
123
123
|
// -------------------------------------------------------------------------
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Handle keyboard input for list navigation.
|
|
127
|
+
*
|
|
128
|
+
* **Auto-clearError contract**: At the top of this method, `lastError` is cleared if
|
|
129
|
+
* non-null. This means any transient error set via `setError()` is dismissed on the
|
|
130
|
+
* very next keystroke the user presses. Subclasses that override `handleInput()` should
|
|
131
|
+
* either:
|
|
132
|
+
* 1. Call `super.handleInput(key)` as a fallback (preferred), which will clear the
|
|
133
|
+
* error when navigation keys are pressed, or
|
|
134
|
+
* 2. Manually call `this.clearError()` at the top of their override to maintain
|
|
135
|
+
* the same contract for their handled keys.
|
|
136
|
+
*
|
|
137
|
+
* Returns `true` if the key was consumed, `false` to let the panel manager try another
|
|
138
|
+
* handler.
|
|
139
|
+
*/
|
|
125
140
|
handleInput(key: string): boolean {
|
|
126
|
-
// I2: auto-clear
|
|
127
|
-
|
|
141
|
+
// I2: auto-clear transient errors on the next keystroke so stale errors don't linger.
|
|
142
|
+
// Subclasses that override handleInput should call super.handleInput(key) OR manually
|
|
143
|
+
// call this.clearError() at the start of their handler.
|
|
144
|
+
if (this.lastError !== null) this.clearError();
|
|
128
145
|
|
|
129
146
|
const items = this.getItems();
|
|
130
147
|
const total = items.length;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildGuidanceLine,
|
|
8
8
|
buildPanelLine,
|
|
9
9
|
buildPanelWorkspace,
|
|
10
|
+
buildStatusPill,
|
|
10
11
|
DEFAULT_PANEL_PALETTE,
|
|
11
12
|
} from './polish.ts';
|
|
12
13
|
import { createEmptyLine } from '../types/grid.ts';
|
|
@@ -61,6 +62,7 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
61
62
|
|
|
62
63
|
public constructor(private readonly readModel: UiReadModel<UiSecuritySnapshot>) {
|
|
63
64
|
super('security', 'Security', 'U', 'monitoring');
|
|
65
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
64
66
|
this.unsub = this.readModel.subscribe(() => this.markDirty());
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -125,46 +127,46 @@ export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
|
125
127
|
[' tokens ', C.label],
|
|
126
128
|
[String(view.totalTokens), C.value],
|
|
127
129
|
[' blocked ', C.label],
|
|
128
|
-
|
|
130
|
+
...buildStatusPill(view.blocked.length > 0 ? 'bad' : 'good', String(view.blocked.length)),
|
|
129
131
|
[' scope violations ', C.label],
|
|
130
|
-
|
|
132
|
+
...buildStatusPill(view.scopeViolations.length > 0 ? 'bad' : 'good', String(view.scopeViolations.length)),
|
|
131
133
|
[' overdue ', C.label],
|
|
132
|
-
|
|
134
|
+
...buildStatusPill(view.rotationOverdue.length > 0 ? 'bad' : 'good', String(view.rotationOverdue.length)),
|
|
133
135
|
[' warnings ', C.label],
|
|
134
|
-
|
|
136
|
+
...buildStatusPill(view.rotationWarnings.length > 0 ? 'warn' : 'good', String(view.rotationWarnings.length)),
|
|
135
137
|
]),
|
|
136
138
|
buildPanelLine(width, [
|
|
137
139
|
[' preflight ', C.label],
|
|
138
|
-
|
|
140
|
+
...buildStatusPill(preflightStatus === 'block' ? 'bad' : preflightStatus === 'warn' ? 'warn' : preflightStatus === 'pass' ? 'good' : 'info', preflightStatus.toUpperCase()),
|
|
139
141
|
[' issues ', C.label],
|
|
140
|
-
|
|
142
|
+
...buildStatusPill(preflightIssueCount > 0 ? 'warn' : 'good', String(preflightIssueCount)),
|
|
141
143
|
[' lint ', C.label],
|
|
142
|
-
|
|
144
|
+
...buildStatusPill(lintFindingCount > 0 ? 'warn' : 'good', String(lintFindingCount)),
|
|
143
145
|
[' denied permissions ', C.label],
|
|
144
|
-
|
|
146
|
+
...buildStatusPill(snapshot.deniedPermissions > 0 ? 'warn' : 'good', String(snapshot.deniedPermissions)),
|
|
145
147
|
]),
|
|
146
148
|
buildPanelLine(width, [
|
|
147
149
|
[' quarantined MCP ', C.label],
|
|
148
|
-
|
|
150
|
+
...buildStatusPill(quarantinedMcp.length > 0 ? 'bad' : 'good', String(quarantinedMcp.length)),
|
|
149
151
|
[' elevated MCP ', C.label],
|
|
150
|
-
|
|
152
|
+
...buildStatusPill(elevatedMcp.length > 0 ? 'warn' : 'good', String(elevatedMcp.length)),
|
|
151
153
|
[' quarantined plugins ', C.label],
|
|
152
|
-
|
|
154
|
+
...buildStatusPill(quarantinedPlugins.length > 0 ? 'bad' : 'good', String(quarantinedPlugins.length)),
|
|
153
155
|
[' untrusted plugins ', C.label],
|
|
154
|
-
|
|
156
|
+
...buildStatusPill(untrustedPlugins.length > 0 ? 'warn' : 'good', String(untrustedPlugins.length)),
|
|
155
157
|
]),
|
|
156
158
|
buildPanelLine(width, [
|
|
157
159
|
[' incidents ', C.label],
|
|
158
|
-
|
|
160
|
+
...buildStatusPill(incidents.length > 0 ? 'warn' : 'good', String(incidents.length)),
|
|
159
161
|
]),
|
|
160
162
|
];
|
|
161
163
|
|
|
162
164
|
const attackPathLines: Line[] = [
|
|
163
165
|
buildPanelLine(width, [
|
|
164
166
|
[' attack paths ', C.label],
|
|
165
|
-
|
|
167
|
+
...buildStatusPill(attackPathReview.criticalFindings > 0 ? 'bad' : 'good', String(attackPathReview.criticalFindings)),
|
|
166
168
|
[' critical ', C.label],
|
|
167
|
-
|
|
169
|
+
...buildStatusPill(attackPathReview.incoherentFindings > 0 ? 'warn' : 'good', String(attackPathReview.incoherentFindings)),
|
|
168
170
|
[' review ', C.label],
|
|
169
171
|
[attackPathReview.summary.slice(0, Math.max(0, width - 36)), C.dim],
|
|
170
172
|
]),
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
buildEmptyState,
|
|
12
12
|
buildPanelLine,
|
|
13
13
|
buildPanelWorkspace,
|
|
14
|
+
buildStatusPill,
|
|
14
15
|
DEFAULT_PANEL_PALETTE,
|
|
15
16
|
} from './polish.ts';
|
|
16
17
|
|
|
@@ -78,6 +79,7 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
78
79
|
subscriptionManager: SubscriptionAccessQuery,
|
|
79
80
|
) {
|
|
80
81
|
super('services', 'Services', 'V', 'monitoring');
|
|
82
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
81
83
|
this.registry = registry;
|
|
82
84
|
this.subscriptionManager = subscriptionManager;
|
|
83
85
|
void this.refresh();
|
|
@@ -188,16 +190,16 @@ export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
|
188
190
|
]));
|
|
189
191
|
detailLines.push(buildPanelLine(width, [
|
|
190
192
|
[' Primary credential: ', C.label],
|
|
191
|
-
|
|
193
|
+
...buildStatusPill(inspect.hasPrimaryCredential ? 'good' : 'bad', inspect.hasPrimaryCredential ? 'present' : 'missing'),
|
|
192
194
|
[' Webhook URL: ', C.label],
|
|
193
|
-
|
|
195
|
+
...buildStatusPill(inspect.hasWebhookUrl ? 'good' : 'info', inspect.hasWebhookUrl ? 'present' : 'missing'),
|
|
194
196
|
[' Signing secret: ', C.label],
|
|
195
|
-
|
|
197
|
+
...buildStatusPill(inspect.hasSigningSecret ? 'good' : 'info', inspect.hasSigningSecret ? 'present' : 'missing'),
|
|
196
198
|
]));
|
|
197
199
|
if (selected.lastTest) {
|
|
198
200
|
detailLines.push(buildPanelLine(width, [
|
|
199
201
|
[' Last test: ', C.label],
|
|
200
|
-
|
|
202
|
+
...buildStatusPill(selected.lastTest.ok ? 'good' : 'bad', selected.lastTest.ok ? 'ok' : 'failed'),
|
|
201
203
|
[' Status: ', C.label],
|
|
202
204
|
[selected.lastTest.status != null ? String(selected.lastTest.status) : 'n/a', C.value],
|
|
203
205
|
[' URL: ', C.label],
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isPanelSearchCommit,
|
|
27
27
|
isPanelSearchPrintable,
|
|
28
28
|
} from './search-focus.ts';
|
|
29
|
+
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
29
30
|
|
|
30
31
|
const C = {
|
|
31
32
|
headerBg: '#1a1a2e',
|
|
@@ -79,7 +80,7 @@ function formatReturnContextLines(returnContext: SessionInfo['returnContext']):
|
|
|
79
80
|
// ---------------------------------------------------------------------------
|
|
80
81
|
// Confirmation state for deletion
|
|
81
82
|
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
+
// ConfirmState<string> — subject holds the session name to delete
|
|
83
84
|
|
|
84
85
|
export class SessionBrowserPanel extends BasePanel {
|
|
85
86
|
private sessions: SessionInfo[] = [];
|
|
@@ -88,10 +89,10 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
88
89
|
private searching = false; // true when user is actively typing a search
|
|
89
90
|
private cursorIndex = 0;
|
|
90
91
|
private scrollOffset = 0;
|
|
91
|
-
private confirm: ConfirmState = null;
|
|
92
|
+
private confirm: ConfirmState<string> | null = null;
|
|
92
93
|
private deleteError = '';
|
|
93
94
|
private loadError = '';
|
|
94
|
-
private
|
|
95
|
+
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
95
96
|
|
|
96
97
|
constructor(
|
|
97
98
|
private readonly sessionManager: SessionBrowserQuery,
|
|
@@ -103,29 +104,29 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
103
104
|
override onActivate(): void {
|
|
104
105
|
super.onActivate();
|
|
105
106
|
this._load();
|
|
106
|
-
this.
|
|
107
|
+
this.refreshTimerId = this.registerTimer(setInterval(() => { this._load(); }, 5000));
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
override onDeactivate(): void {
|
|
110
|
-
if (this.
|
|
111
|
+
if (this.refreshTimerId !== null) { this.clearTimer(this.refreshTimerId); this.refreshTimerId = null; }
|
|
111
112
|
this.searching = false;
|
|
112
113
|
this.confirm = null;
|
|
113
114
|
super.onDeactivate();
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
handleInput(key: string): boolean {
|
|
117
|
-
// Confirmation dialog
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
118
|
+
// Confirmation dialog — use shared handleConfirmInput for y/n/Esc UX
|
|
119
|
+
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
120
|
+
if (confirmResult === 'confirmed') {
|
|
121
|
+
this._deleteConfirmed();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (confirmResult === 'cancelled') {
|
|
125
|
+
this.confirm = null;
|
|
126
|
+
this.markDirty();
|
|
127
127
|
return true;
|
|
128
128
|
}
|
|
129
|
+
if (confirmResult === 'absorbed') return true;
|
|
129
130
|
|
|
130
131
|
// Search mode
|
|
131
132
|
if (this.searching) {
|
|
@@ -200,7 +201,7 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
200
201
|
{
|
|
201
202
|
title: 'Confirmation',
|
|
202
203
|
lines: [
|
|
203
|
-
buildPanelLine(width, [[` Delete "${this.confirm.
|
|
204
|
+
buildPanelLine(width, [[` Delete "${this.confirm.subject}"?`, DEFAULT_PANEL_PALETTE.warn]]),
|
|
204
205
|
buildPanelLine(width, [[' y', DEFAULT_PANEL_PALETTE.info], [' confirm delete', DEFAULT_PANEL_PALETTE.dim], [' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim]]),
|
|
205
206
|
],
|
|
206
207
|
},
|
|
@@ -378,13 +379,13 @@ export class SessionBrowserPanel extends BasePanel {
|
|
|
378
379
|
private _promptDelete(): void {
|
|
379
380
|
const sess = this.filtered[this.cursorIndex];
|
|
380
381
|
if (!sess) return;
|
|
381
|
-
this.confirm = {
|
|
382
|
+
this.confirm = { subject: sess.name, label: sess.name };
|
|
382
383
|
this.markDirty();
|
|
383
384
|
}
|
|
384
385
|
|
|
385
386
|
private _deleteConfirmed(): void {
|
|
386
387
|
if (!this.confirm) return;
|
|
387
|
-
const name = this.confirm.
|
|
388
|
+
const name = this.confirm.subject;
|
|
388
389
|
this.confirm = null;
|
|
389
390
|
try {
|
|
390
391
|
this.sessionManager.delete(name);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
buildGuidanceLine,
|
|
6
6
|
buildPanelListRow,
|
|
7
7
|
buildPanelLine,
|
|
8
|
+
buildStatusPill,
|
|
8
9
|
buildSummaryBlock,
|
|
9
10
|
DEFAULT_PANEL_PALETTE,
|
|
10
11
|
type PanelPalette,
|
|
@@ -26,6 +27,7 @@ type ResolvedEntry = ReturnType<typeof getSettingsControlPlaneSnapshot>['resolve
|
|
|
26
27
|
export class SettingsSyncPanel extends ScrollableListPanel<ResolvedEntry> {
|
|
27
28
|
public constructor(private readonly configManager: ConfigManager) {
|
|
28
29
|
super('settings-sync', 'Settings Sync', 'S', 'monitoring');
|
|
30
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
protected override getPalette(): PanelPalette {
|
|
@@ -52,7 +54,7 @@ export class SettingsSyncPanel extends ScrollableListPanel<ResolvedEntry> {
|
|
|
52
54
|
const snapshot = getSettingsControlPlaneSnapshot(this.configManager);
|
|
53
55
|
|
|
54
56
|
const postureLines: Line[] = [
|
|
55
|
-
buildPanelLine(width, [[' resolved keys ', C.label], [String(snapshot.resolvedEntries.length), C.value], [' conflicts ', C.label],
|
|
57
|
+
buildPanelLine(width, [[' resolved keys ', C.label], [String(snapshot.resolvedEntries.length), C.value], [' conflicts ', C.label], ...buildStatusPill(snapshot.conflicts.length > 0 ? 'bad' : 'good', String(snapshot.conflicts.length)), [' failures ', C.label], ...buildStatusPill(snapshot.recentFailures.length > 0 ? 'warn' : 'good', String(snapshot.recentFailures.length))]),
|
|
56
58
|
buildPanelLine(width, [[' managed locks ', C.label], [String(snapshot.managedLockCount), snapshot.managedLockCount > 0 ? C.warn : C.dim], [' staged bundle ', C.label], [snapshot.stagedManagedBundle ? snapshot.stagedManagedBundle.profileName : 'none', snapshot.stagedManagedBundle ? C.info : C.dim]]),
|
|
57
59
|
buildGuidanceLine(width, '/settingssync conflicts', 'review conflicting synced values before they silently shape effective configuration', C),
|
|
58
60
|
buildGuidanceLine(width, '/managed review', 'inspect staged managed changes, risk posture, and rollback records', C),
|