@pellux/goodvibes-tui 0.28.0 → 0.29.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 +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +203 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +309 -128
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
package/src/panels/qr-panel.ts
CHANGED
|
@@ -6,19 +6,27 @@ import {
|
|
|
6
6
|
buildPanelLine,
|
|
7
7
|
buildPanelTitle,
|
|
8
8
|
DEFAULT_PANEL_PALETTE,
|
|
9
|
+
extendPalette,
|
|
9
10
|
} from './polish.ts';
|
|
11
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
10
12
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
11
13
|
import { renderQrMatrix, generateQrMatrix } from '../renderer/qr-renderer.ts';
|
|
12
14
|
import { encodeConnectionPayload } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
15
|
+
import type { UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
16
|
+
import type { UiControlPlaneSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
// Fixed-width placeholder — deliberately NOT derived from the real secret's
|
|
19
|
+
// length, so masking doesn't leak how long the token/password is.
|
|
20
|
+
const SECRET_MASK = '••••••••••••';
|
|
21
|
+
|
|
22
|
+
// Domain accents only; base chrome (header/headerBg/label/info) comes from
|
|
23
|
+
// DEFAULT_PANEL_PALETTE. qrFg/qrBg stay pure black/white — scanner contrast
|
|
24
|
+
// requirements override theming.
|
|
25
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
17
26
|
token: '#a78bfa',
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} as const;
|
|
27
|
+
qrFg: '#000000',
|
|
28
|
+
qrBg: '#ffffff',
|
|
29
|
+
} as const);
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
32
|
* Connection info passed to the QR panel.
|
|
@@ -62,27 +70,82 @@ export class QrPanel extends BasePanel {
|
|
|
62
70
|
private connectionInfo: QrPanelConnectionInfo;
|
|
63
71
|
private readonly regenerateToken: RegenerateTokenFn | undefined;
|
|
64
72
|
private readonly copyToClipboard: CopyToClipboardFn | undefined;
|
|
73
|
+
private readonly controlPlaneReadModel: UiReadModel<UiControlPlaneSnapshot> | undefined;
|
|
74
|
+
private readonly localUserAuthManager: Pick<UserAuthManager, 'inspect'> | undefined;
|
|
75
|
+
private readonly unsub: (() => void) | null;
|
|
65
76
|
private lastStatus = '';
|
|
77
|
+
/** v toggles whether the token/password render in the clear. */
|
|
78
|
+
private revealed = false;
|
|
79
|
+
/** Set when r is pressed while a companion session is live — confirmed with y/Enter, cancelled with n/Esc. */
|
|
80
|
+
private confirmRegenerate: ConfirmState<null> | null = null;
|
|
66
81
|
|
|
67
82
|
public constructor(
|
|
68
83
|
connectionInfo: QrPanelConnectionInfo,
|
|
69
84
|
regenerateToken?: RegenerateTokenFn,
|
|
70
85
|
copyToClipboard?: CopyToClipboardFn,
|
|
86
|
+
controlPlaneReadModel?: UiReadModel<UiControlPlaneSnapshot>,
|
|
87
|
+
localUserAuthManager?: Pick<UserAuthManager, 'inspect'>,
|
|
71
88
|
) {
|
|
72
89
|
super('qr-code', 'QR Code', 'Q', 'session');
|
|
73
90
|
this.connectionInfo = connectionInfo;
|
|
74
91
|
this.regenerateToken = regenerateToken;
|
|
75
92
|
this.copyToClipboard = copyToClipboard;
|
|
93
|
+
this.controlPlaneReadModel = controlPlaneReadModel;
|
|
94
|
+
this.localUserAuthManager = localUserAuthManager;
|
|
95
|
+
this.unsub = controlPlaneReadModel ? controlPlaneReadModel.subscribe(() => this.markDirty()) : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public override onDestroy(): void {
|
|
99
|
+
this.unsub?.();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private hasLiveCompanionSession(): boolean {
|
|
103
|
+
return (this.localUserAuthManager?.inspect().sessionCount ?? 0) > 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private doRegenerate(): void {
|
|
107
|
+
if (!this.regenerateToken) return;
|
|
108
|
+
this.connectionInfo = this.regenerateToken();
|
|
109
|
+
this.lastStatus = 'Token regenerated.';
|
|
110
|
+
this.markDirty();
|
|
76
111
|
}
|
|
77
112
|
|
|
78
113
|
public handleInput(key: string): boolean {
|
|
114
|
+
if (this.confirmRegenerate) {
|
|
115
|
+
const result = handleConfirmInput(this.confirmRegenerate, key);
|
|
116
|
+
if (result === 'confirmed') {
|
|
117
|
+
this.confirmRegenerate = null;
|
|
118
|
+
this.doRegenerate();
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (result === 'cancelled') {
|
|
122
|
+
this.confirmRegenerate = null;
|
|
123
|
+
this.lastStatus = 'Regeneration cancelled.';
|
|
124
|
+
this.markDirty();
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return true; // absorbed
|
|
128
|
+
}
|
|
79
129
|
if (key === 'r') {
|
|
80
|
-
if (this.regenerateToken) {
|
|
81
|
-
this.connectionInfo = this.regenerateToken();
|
|
82
|
-
this.lastStatus = 'Token regenerated.';
|
|
83
|
-
} else {
|
|
130
|
+
if (!this.regenerateToken) {
|
|
84
131
|
this.lastStatus = 'Regeneration not available.';
|
|
132
|
+
this.markDirty();
|
|
133
|
+
return true;
|
|
85
134
|
}
|
|
135
|
+
if (this.hasLiveCompanionSession()) {
|
|
136
|
+
this.confirmRegenerate = {
|
|
137
|
+
subject: null,
|
|
138
|
+
label: 'pairing token — a companion session is live and will be disconnected',
|
|
139
|
+
verb: 'Regenerate',
|
|
140
|
+
};
|
|
141
|
+
this.markDirty();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
this.doRegenerate();
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (key === 'v') {
|
|
148
|
+
this.revealed = !this.revealed;
|
|
86
149
|
this.markDirty();
|
|
87
150
|
return true;
|
|
88
151
|
}
|
|
@@ -105,27 +168,32 @@ export class QrPanel extends BasePanel {
|
|
|
105
168
|
|
|
106
169
|
const { url, token, username, password } = this.connectionInfo;
|
|
107
170
|
const valueWidth = Math.max(0, width - 12);
|
|
171
|
+
const displayToken = this.revealed ? token : SECRET_MASK;
|
|
172
|
+
const displayPassword = password !== undefined ? (this.revealed ? password : SECRET_MASK) : undefined;
|
|
108
173
|
|
|
109
174
|
// ── Title + purpose: tell the operator exactly what this code is for ────
|
|
110
175
|
lines.push(buildPanelTitle(width, 'Companion Pairing', C));
|
|
111
176
|
lines.push(
|
|
112
177
|
buildPanelLine(width, [
|
|
113
|
-
[' Scan with the GoodVibes companion app to pair this session.', C.
|
|
178
|
+
[' Scan with the GoodVibes companion app to pair this session.', C.label],
|
|
114
179
|
]),
|
|
115
180
|
);
|
|
116
181
|
|
|
117
182
|
// ── Connection info header ─────────────────────────────────────────────
|
|
183
|
+
// Token and (if present) bootstrap password are masked by default — they
|
|
184
|
+
// are credentials, not display text — and only render in the clear while
|
|
185
|
+
// `revealed` is toggled on via v.
|
|
118
186
|
lines.push(createEmptyLine(width));
|
|
119
187
|
lines.push(
|
|
120
188
|
buildPanelLine(width, [
|
|
121
189
|
[' URL ', C.label],
|
|
122
|
-
[truncateDisplay(url, valueWidth), C.
|
|
190
|
+
[truncateDisplay(url, valueWidth), C.info],
|
|
123
191
|
]),
|
|
124
192
|
);
|
|
125
193
|
lines.push(
|
|
126
194
|
buildPanelLine(width, [
|
|
127
195
|
[' Token ', C.label],
|
|
128
|
-
[truncateDisplay(
|
|
196
|
+
[truncateDisplay(displayToken, valueWidth), C.token],
|
|
129
197
|
]),
|
|
130
198
|
);
|
|
131
199
|
lines.push(
|
|
@@ -134,11 +202,22 @@ export class QrPanel extends BasePanel {
|
|
|
134
202
|
[truncateDisplay(username, valueWidth), C.value],
|
|
135
203
|
]),
|
|
136
204
|
);
|
|
137
|
-
if (
|
|
205
|
+
if (displayPassword !== undefined) {
|
|
138
206
|
lines.push(
|
|
139
207
|
buildPanelLine(width, [
|
|
140
208
|
[' Password ', C.label],
|
|
141
|
-
[truncateDisplay(
|
|
209
|
+
[truncateDisplay(displayPassword, valueWidth), C.value],
|
|
210
|
+
]),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Connected companions ──────────────────────────────────────────────
|
|
215
|
+
if (this.controlPlaneReadModel) {
|
|
216
|
+
const connected = this.controlPlaneReadModel.getSnapshot().activeClientIds.length;
|
|
217
|
+
lines.push(
|
|
218
|
+
buildPanelLine(width, [
|
|
219
|
+
[' Companions ', C.label],
|
|
220
|
+
[`connected: ${connected}`, connected > 0 ? C.good : C.dim],
|
|
142
221
|
]),
|
|
143
222
|
);
|
|
144
223
|
}
|
|
@@ -161,11 +240,15 @@ export class QrPanel extends BasePanel {
|
|
|
161
240
|
|
|
162
241
|
lines.push(createEmptyLine(width));
|
|
163
242
|
|
|
164
|
-
// ──
|
|
165
|
-
|
|
166
|
-
|
|
243
|
+
// ── Footer: confirm overlay (regenerate while a companion is live) takes
|
|
244
|
+
// priority over the ephemeral status message ──────────────────────────
|
|
245
|
+
const footerLines: Line[] = [];
|
|
246
|
+
if (this.confirmRegenerate) {
|
|
247
|
+
footerLines.push(...renderConfirmLines(width, this.confirmRegenerate));
|
|
248
|
+
} else if (this.lastStatus) {
|
|
249
|
+
footerLines.push(
|
|
167
250
|
buildPanelLine(width, [
|
|
168
|
-
[` ${this.lastStatus} `, C.
|
|
251
|
+
[` ${this.lastStatus} `, C.label],
|
|
169
252
|
]),
|
|
170
253
|
);
|
|
171
254
|
}
|
|
@@ -174,21 +257,24 @@ export class QrPanel extends BasePanel {
|
|
|
174
257
|
const hints: Array<{ keys: string; label: string }> = [];
|
|
175
258
|
if (this.regenerateToken) hints.push({ keys: 'r', label: 'regenerate token' });
|
|
176
259
|
if (this.copyToClipboard) hints.push({ keys: 'c', label: 'copy token' });
|
|
260
|
+
hints.push({ keys: 'v', label: this.revealed ? 'hide token' : 'reveal token' });
|
|
177
261
|
const hintsLine = hints.length > 0
|
|
178
262
|
? buildKeyboardHints(width, hints, C)
|
|
179
263
|
: buildPanelLine(width, [[' Pairing is read-only in this surface.', C.dim]]);
|
|
264
|
+
footerLines.push(hintsLine);
|
|
180
265
|
|
|
181
|
-
// Push
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
for (let i = 0; i < fillCount; i++) {
|
|
187
|
-
lines.push(createEmptyLine(width));
|
|
188
|
-
}
|
|
266
|
+
// Push the footer at the bottom if we have room, otherwise append it
|
|
267
|
+
// directly after the QR code.
|
|
268
|
+
const remaining = height - lines.length - footerLines.length;
|
|
269
|
+
if (remaining > 0) {
|
|
270
|
+
for (let i = 0; i < remaining; i++) lines.push(createEmptyLine(width));
|
|
189
271
|
}
|
|
190
|
-
lines.push(
|
|
272
|
+
lines.push(...footerLines);
|
|
191
273
|
|
|
192
|
-
|
|
274
|
+
// Clamp to the exact requested height — the QR matrix's row count is
|
|
275
|
+
// driven by payload size, not by `height`, so it can legitimately
|
|
276
|
+
// overflow a small pane; never hand back more (or fewer) lines than asked.
|
|
277
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
278
|
+
return lines.slice(0, height);
|
|
193
279
|
}
|
|
194
280
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
3
|
import { BasePanel } from './base-panel.ts';
|
|
4
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
4
5
|
import type { UiReadModel, UiRemoteSnapshot } from '../runtime/ui-read-models.ts';
|
|
5
6
|
import {
|
|
6
7
|
buildDetailBlock,
|
|
7
8
|
buildEmptyState,
|
|
8
|
-
buildGuidanceLine,
|
|
9
9
|
buildPanelListRow,
|
|
10
10
|
buildPanelLine,
|
|
11
11
|
buildSummaryBlock,
|
|
@@ -17,28 +17,22 @@ import {
|
|
|
17
17
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
18
18
|
import { getTrackedVisibleWindow } from '../renderer/surface-layout.ts';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
headerBg: '#1e293b',
|
|
24
|
-
dim: '#475569',
|
|
25
|
-
ok: '#22c55e',
|
|
26
|
-
warn: '#eab308',
|
|
27
|
-
error: '#ef4444',
|
|
28
|
-
} as const;
|
|
20
|
+
// Base chrome only — title band, state colors, and text tokens all come
|
|
21
|
+
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
22
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
29
23
|
|
|
30
24
|
function stateColor(state: string): string {
|
|
31
25
|
switch (state) {
|
|
32
26
|
case 'connected':
|
|
33
27
|
case 'syncing':
|
|
34
|
-
return C.
|
|
28
|
+
return C.good;
|
|
35
29
|
case 'degraded':
|
|
36
30
|
case 'reconnecting':
|
|
37
31
|
case 'authenticating':
|
|
38
32
|
case 'initializing':
|
|
39
33
|
return C.warn;
|
|
40
34
|
case 'terminal_failure':
|
|
41
|
-
return C.
|
|
35
|
+
return C.bad;
|
|
42
36
|
default:
|
|
43
37
|
return C.dim;
|
|
44
38
|
}
|
|
@@ -52,15 +46,39 @@ function truncate(text: string, width: number): string {
|
|
|
52
46
|
return truncateDisplay(text, width);
|
|
53
47
|
}
|
|
54
48
|
|
|
49
|
+
// Splits a machine-generated slash-command string (e.g. `/remote show foo`)
|
|
50
|
+
// into the { name, args } shape ctx.executeCommand expects — the same
|
|
51
|
+
// leading-slash-stripping approach panel-integration-actions.ts uses for
|
|
52
|
+
// ApprovalPanel's dispatched command.
|
|
53
|
+
function parseCommand(command: string): { name: string; args: string[] } | null {
|
|
54
|
+
const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
|
|
55
|
+
const [name, ...args] = parts;
|
|
56
|
+
return name ? { name, args } : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Mirrors RemoteSupervisor's own stateIsDegraded()/degradedConnections
|
|
60
|
+
// definition (sdk/platform/runtime/remote/supervisor.js) so "degraded/stale"
|
|
61
|
+
// means the same thing here as it does in the posture summary above.
|
|
62
|
+
function isSupervisorEntryDegraded(entry: { transportState: string; heartbeat: { status: string } }): boolean {
|
|
63
|
+
const degradedTransport = entry.transportState === 'degraded'
|
|
64
|
+
|| entry.transportState === 'reconnecting'
|
|
65
|
+
|| entry.transportState === 'terminal_failure';
|
|
66
|
+
return degradedTransport || entry.heartbeat.status !== 'fresh';
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
export class RemotePanel extends BasePanel {
|
|
56
70
|
private readonly readModel?: UiReadModel<UiRemoteSnapshot>;
|
|
57
71
|
private readonly unsub: (() => void) | null;
|
|
58
72
|
private selectedIndex = 0;
|
|
59
73
|
private scrollOffset = 0;
|
|
60
74
|
private browseMode: 'connections' | 'contracts' = 'connections';
|
|
75
|
+
// Set by handleInput (enter/r) and consumed on the very next
|
|
76
|
+
// handlePanelIntegrationAction dispatch of that same key, mirroring the
|
|
77
|
+
// token-budget-panel pattern — handleInput has no ctx.executeCommand.
|
|
78
|
+
private pendingCommand: { name: string; args: string[] } | null = null;
|
|
61
79
|
|
|
62
80
|
public constructor(readModel?: UiReadModel<UiRemoteSnapshot>) {
|
|
63
|
-
super('remote', 'Remote', '
|
|
81
|
+
super('remote', 'Remote', '▰', 'providers');
|
|
64
82
|
this.readModel = readModel;
|
|
65
83
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
66
84
|
}
|
|
@@ -70,6 +88,29 @@ export class RemotePanel extends BasePanel {
|
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
public handleInput(key: string): boolean {
|
|
91
|
+
// r = dispatch /remote recover for whatever is currently selected (or
|
|
92
|
+
// the runtime's first non-fresh session when nothing is selected).
|
|
93
|
+
if (this.readModel && key === 'r') {
|
|
94
|
+
const runnerId = this.getSelectedRunnerId();
|
|
95
|
+
const parsed = parseCommand(runnerId ? `/remote recover ${runnerId}` : '/remote recover');
|
|
96
|
+
if (parsed) {
|
|
97
|
+
this.pendingCommand = parsed;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Enter on a degraded/stale supervisor selection dispatches its
|
|
102
|
+
// top recovery action (already machine-readable via .recovery[0].command)
|
|
103
|
+
// instead of requiring the operator to retype it.
|
|
104
|
+
if (this.readModel && (key === 'enter' || key === 'return')) {
|
|
105
|
+
const entry = this.getSelectedSupervisorEntry();
|
|
106
|
+
if (entry && isSupervisorEntryDegraded(entry) && entry.recovery.length > 0) {
|
|
107
|
+
const parsed = parseCommand(entry.recovery[0].command);
|
|
108
|
+
if (parsed) {
|
|
109
|
+
this.pendingCommand = parsed;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
73
114
|
const activeConnections = this.getActiveConnections();
|
|
74
115
|
const contracts = this.readModel?.getSnapshot().contracts ?? [];
|
|
75
116
|
const browseCount = this.browseMode === 'connections' && activeConnections.length > 0
|
|
@@ -109,6 +150,40 @@ export class RemotePanel extends BasePanel {
|
|
|
109
150
|
return this.readModel?.getSnapshot().acp.activeConnections ?? [];
|
|
110
151
|
}
|
|
111
152
|
|
|
153
|
+
// Runner id backing the currently browsed row, whichever browse mode is
|
|
154
|
+
// active — shared by the Enter/r dispatch logic and render()'s own
|
|
155
|
+
// selection lookups so they never drift apart.
|
|
156
|
+
private getSelectedRunnerId(): string | null {
|
|
157
|
+
const snapshot = this.readModel?.getSnapshot();
|
|
158
|
+
if (!snapshot) return null;
|
|
159
|
+
const activeConnections = this.getActiveConnections();
|
|
160
|
+
const contracts = snapshot.contracts;
|
|
161
|
+
const viewingConnections = this.browseMode === 'connections' && activeConnections.length > 0;
|
|
162
|
+
// selectedIndex indexes only the mode's active list (activeConnections when
|
|
163
|
+
// viewing connections, otherwise contracts); .at() keeps that read off any
|
|
164
|
+
// raw source array (no-raw-selectedindex-read rule).
|
|
165
|
+
if (viewingConnections) {
|
|
166
|
+
return activeConnections.at(this.selectedIndex)?.agentId ?? null;
|
|
167
|
+
}
|
|
168
|
+
return contracts.at(this.selectedIndex)?.runnerId ?? null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private getSelectedSupervisorEntry() {
|
|
172
|
+
const snapshot = this.readModel?.getSnapshot();
|
|
173
|
+
if (!snapshot) return null;
|
|
174
|
+
const runnerId = this.getSelectedRunnerId();
|
|
175
|
+
if (!runnerId) return null;
|
|
176
|
+
return snapshot.supervisor.sessions.find((entry) => entry.runnerId === runnerId) ?? null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
180
|
+
if (!this.pendingCommand) return false;
|
|
181
|
+
const { name, args } = this.pendingCommand;
|
|
182
|
+
this.pendingCommand = null;
|
|
183
|
+
void ctx.executeCommand?.(name, args);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
112
187
|
public render(width: number, height: number): Line[] {
|
|
113
188
|
this.needsRender = false;
|
|
114
189
|
const intro = 'Bridge, ACP, runner-contract, and artifact posture for self-hosted remote work.';
|
|
@@ -161,9 +236,9 @@ export class RemotePanel extends BasePanel {
|
|
|
161
236
|
[' daemon ', C.label],
|
|
162
237
|
[daemon.transportState.toUpperCase(), stateColor(daemon.transportState)],
|
|
163
238
|
[' running ', C.label],
|
|
164
|
-
[daemon.isRunning ? 'yes' : 'no', daemon.isRunning ? C.
|
|
239
|
+
[daemon.isRunning ? 'yes' : 'no', daemon.isRunning ? C.good : C.dim],
|
|
165
240
|
[' reconnects ', C.label],
|
|
166
|
-
[String(daemon.reconnectAttempts), daemon.reconnectAttempts > 0 ? C.warn : C.
|
|
241
|
+
[String(daemon.reconnectAttempts), daemon.reconnectAttempts > 0 ? C.warn : C.good],
|
|
167
242
|
[' jobs ', C.label],
|
|
168
243
|
[String(daemon.runningJobCount), daemon.runningJobCount > 0 ? C.info : C.dim],
|
|
169
244
|
]),
|
|
@@ -181,17 +256,17 @@ export class RemotePanel extends BasePanel {
|
|
|
181
256
|
[' pools ', C.label],
|
|
182
257
|
[String(pools.length), pools.length > 0 ? C.info : C.dim],
|
|
183
258
|
[' review artifacts ', C.label],
|
|
184
|
-
[String(artifactCount), artifactCount > 0 ? C.
|
|
259
|
+
[String(artifactCount), artifactCount > 0 ? C.good : C.dim],
|
|
185
260
|
]),
|
|
186
261
|
buildPanelLine(width, [
|
|
187
262
|
[' supervisor ', C.label],
|
|
188
263
|
[String(supervisor.sessions.length), C.info],
|
|
189
264
|
[' degraded ', C.label],
|
|
190
|
-
[String(supervisor.degradedConnections), supervisor.degradedConnections > 0 ? C.warn : C.
|
|
265
|
+
[String(supervisor.degradedConnections), supervisor.degradedConnections > 0 ? C.warn : C.good],
|
|
191
266
|
[' distributed peers ', C.label],
|
|
192
267
|
[String(distributed.peers?.total ?? 0), C.info],
|
|
193
268
|
[' connected ', C.label],
|
|
194
|
-
[String(distributed.peers?.connected ?? 0), (distributed.peers?.connected ?? 0) > 0 ? C.
|
|
269
|
+
[String(distributed.peers?.connected ?? 0), (distributed.peers?.connected ?? 0) > 0 ? C.good : C.dim],
|
|
195
270
|
[' queued work ', C.label],
|
|
196
271
|
[String(distributed.work?.queued ?? 0), (distributed.work?.queued ?? 0) > 0 ? C.info : C.dim],
|
|
197
272
|
]),
|
|
@@ -200,23 +275,17 @@ export class RemotePanel extends BasePanel {
|
|
|
200
275
|
if (daemon.lastError) {
|
|
201
276
|
postureLines.push(buildPanelLine(width, [
|
|
202
277
|
[' daemon error ', C.label],
|
|
203
|
-
[truncateDisplay(daemon.lastError, Math.max(0, width - 14)), C.
|
|
278
|
+
[truncateDisplay(daemon.lastError, Math.max(0, width - 14)), C.bad],
|
|
204
279
|
]));
|
|
205
280
|
}
|
|
206
|
-
postureLines.push(
|
|
207
|
-
buildGuidanceLine(width, '/remote recover', 'resume remote state with runner, capability, and disconnect recovery hints', C),
|
|
208
|
-
buildGuidanceLine(width, '/remote capabilities', 'inspect transport support before routing remote work or reattaching a session', C),
|
|
209
|
-
);
|
|
210
|
-
|
|
211
281
|
const canBrowse = activeConnections.length > 0 || contracts.length > 0;
|
|
212
282
|
const canSwitch = contracts.length > 0 && activeConnections.length > 0;
|
|
213
283
|
const navHint = !canBrowse
|
|
214
284
|
? ` focus=${this.browseMode} idle - no connections or contracts to browse`
|
|
215
285
|
: canSwitch
|
|
216
|
-
? ` focus=${this.browseMode} Up/Down move Tab switch connections/contracts`
|
|
217
|
-
: ` focus=${this.browseMode} Up/Down move`;
|
|
286
|
+
? ` focus=${this.browseMode} Up/Down move Tab switch connections/contracts r=recover Enter=recover selection`
|
|
287
|
+
: ` focus=${this.browseMode} Up/Down move r=recover Enter=recover selection`;
|
|
218
288
|
const footerLines = [
|
|
219
|
-
buildGuidanceLine(width, '/remote setup', 'review bridge, tunnel, env, and bootstrap flows for self-hosted remote work', C),
|
|
220
289
|
buildPanelLine(width, [[navHint, C.dim]]),
|
|
221
290
|
] as const;
|
|
222
291
|
|
|
@@ -252,8 +321,8 @@ export class RemotePanel extends BasePanel {
|
|
|
252
321
|
Math.max(0, (viewingConnections ? activeConnections.length : contracts.length) - 1),
|
|
253
322
|
);
|
|
254
323
|
const browseCount = viewingConnections ? activeConnections.length : contracts.length;
|
|
255
|
-
const selected = viewingConnections ? activeConnections
|
|
256
|
-
const selectedContract = !viewingConnections ? contracts
|
|
324
|
+
const selected = viewingConnections ? activeConnections.at(this.selectedIndex) ?? null : null;
|
|
325
|
+
const selectedContract = !viewingConnections ? contracts.at(this.selectedIndex) ?? null : null;
|
|
257
326
|
const detailRows: Line[] = [];
|
|
258
327
|
|
|
259
328
|
if (selected) {
|
|
@@ -276,7 +345,7 @@ export class RemotePanel extends BasePanel {
|
|
|
276
345
|
if (selected.lastError) {
|
|
277
346
|
detailRows.push(buildPanelLine(width, [
|
|
278
347
|
[' Last error: ', C.label],
|
|
279
|
-
[selected.lastError.slice(0, Math.max(0, width - 13)), C.
|
|
348
|
+
[selected.lastError.slice(0, Math.max(0, width - 13)), C.bad],
|
|
280
349
|
]));
|
|
281
350
|
}
|
|
282
351
|
|
|
@@ -319,13 +388,19 @@ export class RemotePanel extends BasePanel {
|
|
|
319
388
|
if (supervisorEntry) {
|
|
320
389
|
detailRows.push(buildPanelLine(width, [
|
|
321
390
|
[' Heartbeat: ', C.label],
|
|
322
|
-
[supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.
|
|
391
|
+
[supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.good : supervisorEntry.heartbeat.status === 'stale' ? C.warn : C.bad],
|
|
323
392
|
[' Protocol: ', C.label],
|
|
324
393
|
[supervisorEntry.negotiation.executionProtocol, C.value],
|
|
325
394
|
[' Review: ', C.label],
|
|
326
|
-
[supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.
|
|
395
|
+
[supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.good : C.dim],
|
|
327
396
|
]));
|
|
328
397
|
detailRows.push(buildPanelLine(width, [[truncateDisplay(` ${supervisorEntry.heartbeat.detail}`, width), C.dim]]));
|
|
398
|
+
if (isSupervisorEntryDegraded(supervisorEntry) && supervisorEntry.recovery.length > 0) {
|
|
399
|
+
detailRows.push(buildPanelLine(width, [
|
|
400
|
+
[' Enter recovers: ', C.label],
|
|
401
|
+
[supervisorEntry.recovery[0].reason, C.warn],
|
|
402
|
+
]));
|
|
403
|
+
}
|
|
329
404
|
}
|
|
330
405
|
|
|
331
406
|
const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selected.agentId);
|
|
@@ -377,19 +452,22 @@ export class RemotePanel extends BasePanel {
|
|
|
377
452
|
if (supervisorEntry) {
|
|
378
453
|
detailRows.push(buildPanelLine(width, [
|
|
379
454
|
[' Heartbeat: ', C.label],
|
|
380
|
-
[supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.
|
|
455
|
+
[supervisorEntry.heartbeat.status, supervisorEntry.heartbeat.status === 'fresh' ? C.good : supervisorEntry.heartbeat.status === 'stale' ? C.warn : C.bad],
|
|
381
456
|
[' Lane: ', C.label],
|
|
382
457
|
[supervisorEntry.negotiation.communicationLane, C.info],
|
|
383
458
|
]));
|
|
384
|
-
|
|
385
|
-
detailRows.push(
|
|
459
|
+
if (isSupervisorEntryDegraded(supervisorEntry) && supervisorEntry.recovery.length > 0) {
|
|
460
|
+
detailRows.push(buildPanelLine(width, [
|
|
461
|
+
[' Enter recovers: ', C.label],
|
|
462
|
+
[supervisorEntry.recovery[0].reason, C.warn],
|
|
463
|
+
]));
|
|
386
464
|
}
|
|
387
465
|
}
|
|
388
466
|
const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selectedContract.runnerId);
|
|
389
467
|
if (recentArtifact) {
|
|
390
468
|
detailRows.push(buildPanelLine(width, [
|
|
391
469
|
[' Recent artifact: ', C.label],
|
|
392
|
-
[recentArtifact.id, C.
|
|
470
|
+
[recentArtifact.id, C.good],
|
|
393
471
|
[' Status: ', C.label],
|
|
394
472
|
[recentArtifact.task.status, stateColor(recentArtifact.evidence.transportState)],
|
|
395
473
|
]));
|