@pellux/goodvibes-tui 0.27.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- 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 +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- 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 +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- 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 +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- 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/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- 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/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- 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 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- 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
|
@@ -2,20 +2,31 @@ import type { Line } from '../types/grid.ts';
|
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
3
|
import { BasePanel } from './base-panel.ts';
|
|
4
4
|
import {
|
|
5
|
+
buildKeyboardHints,
|
|
5
6
|
buildPanelLine,
|
|
7
|
+
buildPanelTitle,
|
|
6
8
|
DEFAULT_PANEL_PALETTE,
|
|
9
|
+
extendPalette,
|
|
7
10
|
} from './polish.ts';
|
|
11
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
12
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
8
13
|
import { renderQrMatrix, generateQrMatrix } from '../renderer/qr-renderer.ts';
|
|
9
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';
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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, {
|
|
14
26
|
token: '#a78bfa',
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} as const;
|
|
27
|
+
qrFg: '#000000',
|
|
28
|
+
qrBg: '#ffffff',
|
|
29
|
+
} as const);
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
32
|
* Connection info passed to the QR panel.
|
|
@@ -59,27 +70,82 @@ export class QrPanel extends BasePanel {
|
|
|
59
70
|
private connectionInfo: QrPanelConnectionInfo;
|
|
60
71
|
private readonly regenerateToken: RegenerateTokenFn | undefined;
|
|
61
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;
|
|
62
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;
|
|
63
81
|
|
|
64
82
|
public constructor(
|
|
65
83
|
connectionInfo: QrPanelConnectionInfo,
|
|
66
84
|
regenerateToken?: RegenerateTokenFn,
|
|
67
85
|
copyToClipboard?: CopyToClipboardFn,
|
|
86
|
+
controlPlaneReadModel?: UiReadModel<UiControlPlaneSnapshot>,
|
|
87
|
+
localUserAuthManager?: Pick<UserAuthManager, 'inspect'>,
|
|
68
88
|
) {
|
|
69
89
|
super('qr-code', 'QR Code', 'Q', 'session');
|
|
70
90
|
this.connectionInfo = connectionInfo;
|
|
71
91
|
this.regenerateToken = regenerateToken;
|
|
72
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();
|
|
73
111
|
}
|
|
74
112
|
|
|
75
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
|
+
}
|
|
76
129
|
if (key === 'r') {
|
|
77
|
-
if (this.regenerateToken) {
|
|
78
|
-
this.connectionInfo = this.regenerateToken();
|
|
79
|
-
this.lastStatus = 'Token regenerated.';
|
|
80
|
-
} else {
|
|
130
|
+
if (!this.regenerateToken) {
|
|
81
131
|
this.lastStatus = 'Regeneration not available.';
|
|
132
|
+
this.markDirty();
|
|
133
|
+
return true;
|
|
82
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;
|
|
83
149
|
this.markDirty();
|
|
84
150
|
return true;
|
|
85
151
|
}
|
|
@@ -101,32 +167,57 @@ export class QrPanel extends BasePanel {
|
|
|
101
167
|
const lines: Line[] = [];
|
|
102
168
|
|
|
103
169
|
const { url, token, username, password } = this.connectionInfo;
|
|
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;
|
|
173
|
+
|
|
174
|
+
// ── Title + purpose: tell the operator exactly what this code is for ────
|
|
175
|
+
lines.push(buildPanelTitle(width, 'Companion Pairing', C));
|
|
176
|
+
lines.push(
|
|
177
|
+
buildPanelLine(width, [
|
|
178
|
+
[' Scan with the GoodVibes companion app to pair this session.', C.label],
|
|
179
|
+
]),
|
|
180
|
+
);
|
|
104
181
|
|
|
105
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.
|
|
106
186
|
lines.push(createEmptyLine(width));
|
|
107
187
|
lines.push(
|
|
108
188
|
buildPanelLine(width, [
|
|
109
189
|
[' URL ', C.label],
|
|
110
|
-
[url
|
|
190
|
+
[truncateDisplay(url, valueWidth), C.info],
|
|
111
191
|
]),
|
|
112
192
|
);
|
|
113
193
|
lines.push(
|
|
114
194
|
buildPanelLine(width, [
|
|
115
195
|
[' Token ', C.label],
|
|
116
|
-
[
|
|
196
|
+
[truncateDisplay(displayToken, valueWidth), C.token],
|
|
117
197
|
]),
|
|
118
198
|
);
|
|
119
199
|
lines.push(
|
|
120
200
|
buildPanelLine(width, [
|
|
121
201
|
[' Username ', C.label],
|
|
122
|
-
[username
|
|
202
|
+
[truncateDisplay(username, valueWidth), C.value],
|
|
123
203
|
]),
|
|
124
204
|
);
|
|
125
|
-
if (
|
|
205
|
+
if (displayPassword !== undefined) {
|
|
126
206
|
lines.push(
|
|
127
207
|
buildPanelLine(width, [
|
|
128
208
|
[' Password ', C.label],
|
|
129
|
-
[
|
|
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],
|
|
130
221
|
]),
|
|
131
222
|
);
|
|
132
223
|
}
|
|
@@ -149,34 +240,41 @@ export class QrPanel extends BasePanel {
|
|
|
149
240
|
|
|
150
241
|
lines.push(createEmptyLine(width));
|
|
151
242
|
|
|
152
|
-
// ──
|
|
153
|
-
|
|
154
|
-
|
|
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(
|
|
155
250
|
buildPanelLine(width, [
|
|
156
|
-
[` ${this.lastStatus} `, C.
|
|
251
|
+
[` ${this.lastStatus} `, C.label],
|
|
157
252
|
]),
|
|
158
253
|
);
|
|
159
254
|
}
|
|
160
255
|
|
|
161
|
-
// ── Hints
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
256
|
+
// ── Hints ── only advertise actions that are actually wired ─────────────
|
|
257
|
+
const hints: Array<{ keys: string; label: string }> = [];
|
|
258
|
+
if (this.regenerateToken) hints.push({ keys: 'r', label: 'regenerate token' });
|
|
259
|
+
if (this.copyToClipboard) hints.push({ keys: 'c', label: 'copy token' });
|
|
260
|
+
hints.push({ keys: 'v', label: this.revealed ? 'hide token' : 'reveal token' });
|
|
261
|
+
const hintsLine = hints.length > 0
|
|
262
|
+
? buildKeyboardHints(width, hints, C)
|
|
263
|
+
: buildPanelLine(width, [[' Pairing is read-only in this surface.', C.dim]]);
|
|
264
|
+
footerLines.push(hintsLine);
|
|
265
|
+
|
|
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));
|
|
177
271
|
}
|
|
178
|
-
lines.push(
|
|
272
|
+
lines.push(...footerLines);
|
|
179
273
|
|
|
180
|
-
|
|
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);
|
|
181
279
|
}
|
|
182
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,17 +275,18 @@ export class RemotePanel extends BasePanel {
|
|
|
200
275
|
if (daemon.lastError) {
|
|
201
276
|
postureLines.push(buildPanelLine(width, [
|
|
202
277
|
[' daemon error ', C.label],
|
|
203
|
-
[daemon.lastError
|
|
278
|
+
[truncateDisplay(daemon.lastError, Math.max(0, width - 14)), C.bad],
|
|
204
279
|
]));
|
|
205
280
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
281
|
+
const canBrowse = activeConnections.length > 0 || contracts.length > 0;
|
|
282
|
+
const canSwitch = contracts.length > 0 && activeConnections.length > 0;
|
|
283
|
+
const navHint = !canBrowse
|
|
284
|
+
? ` focus=${this.browseMode} idle - no connections or contracts to browse`
|
|
285
|
+
: canSwitch
|
|
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`;
|
|
211
288
|
const footerLines = [
|
|
212
|
-
|
|
213
|
-
buildPanelLine(width, [[` focus=${this.browseMode} Up/Down move Tab switch connections/contracts`, C.dim]]),
|
|
289
|
+
buildPanelLine(width, [[navHint, C.dim]]),
|
|
214
290
|
] as const;
|
|
215
291
|
|
|
216
292
|
if (activeConnections.length === 0 && contracts.length === 0) {
|
|
@@ -245,8 +321,8 @@ export class RemotePanel extends BasePanel {
|
|
|
245
321
|
Math.max(0, (viewingConnections ? activeConnections.length : contracts.length) - 1),
|
|
246
322
|
);
|
|
247
323
|
const browseCount = viewingConnections ? activeConnections.length : contracts.length;
|
|
248
|
-
const selected = viewingConnections ? activeConnections
|
|
249
|
-
const selectedContract = !viewingConnections ? contracts
|
|
324
|
+
const selected = viewingConnections ? activeConnections.at(this.selectedIndex) ?? null : null;
|
|
325
|
+
const selectedContract = !viewingConnections ? contracts.at(this.selectedIndex) ?? null : null;
|
|
250
326
|
const detailRows: Line[] = [];
|
|
251
327
|
|
|
252
328
|
if (selected) {
|
|
@@ -269,7 +345,7 @@ export class RemotePanel extends BasePanel {
|
|
|
269
345
|
if (selected.lastError) {
|
|
270
346
|
detailRows.push(buildPanelLine(width, [
|
|
271
347
|
[' Last error: ', C.label],
|
|
272
|
-
[selected.lastError.slice(0, Math.max(0, width - 13)), C.
|
|
348
|
+
[selected.lastError.slice(0, Math.max(0, width - 13)), C.bad],
|
|
273
349
|
]));
|
|
274
350
|
}
|
|
275
351
|
|
|
@@ -312,13 +388,19 @@ export class RemotePanel extends BasePanel {
|
|
|
312
388
|
if (supervisorEntry) {
|
|
313
389
|
detailRows.push(buildPanelLine(width, [
|
|
314
390
|
[' Heartbeat: ', C.label],
|
|
315
|
-
[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],
|
|
316
392
|
[' Protocol: ', C.label],
|
|
317
393
|
[supervisorEntry.negotiation.executionProtocol, C.value],
|
|
318
394
|
[' Review: ', C.label],
|
|
319
|
-
[supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.
|
|
395
|
+
[supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.good : C.dim],
|
|
320
396
|
]));
|
|
321
|
-
detailRows.push(buildPanelLine(width, [[` ${supervisorEntry.heartbeat.detail}
|
|
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
|
+
}
|
|
322
404
|
}
|
|
323
405
|
|
|
324
406
|
const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selected.agentId);
|
|
@@ -370,19 +452,22 @@ export class RemotePanel extends BasePanel {
|
|
|
370
452
|
if (supervisorEntry) {
|
|
371
453
|
detailRows.push(buildPanelLine(width, [
|
|
372
454
|
[' Heartbeat: ', C.label],
|
|
373
|
-
[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],
|
|
374
456
|
[' Lane: ', C.label],
|
|
375
457
|
[supervisorEntry.negotiation.communicationLane, C.info],
|
|
376
458
|
]));
|
|
377
|
-
|
|
378
|
-
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
|
+
]));
|
|
379
464
|
}
|
|
380
465
|
}
|
|
381
466
|
const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selectedContract.runnerId);
|
|
382
467
|
if (recentArtifact) {
|
|
383
468
|
detailRows.push(buildPanelLine(width, [
|
|
384
469
|
[' Recent artifact: ', C.label],
|
|
385
|
-
[recentArtifact.id, C.
|
|
470
|
+
[recentArtifact.id, C.good],
|
|
386
471
|
[' Status: ', C.label],
|
|
387
472
|
[recentArtifact.task.status, stateColor(recentArtifact.evidence.transportState)],
|
|
388
473
|
]));
|
|
@@ -400,7 +485,7 @@ export class RemotePanel extends BasePanel {
|
|
|
400
485
|
{ text: ` ${connection.transportState.padEnd(18)}`, fg: stateColor(connection.transportState) },
|
|
401
486
|
{ text: ` msgs=${String(connection.messageCount).padEnd(6)}`, fg: C.info },
|
|
402
487
|
{ text: ` errs=${String(connection.errorCount).padEnd(4)}`, fg: connection.errorCount > 0 ? C.warn : C.dim },
|
|
403
|
-
{ text: ` ${connection.label}
|
|
488
|
+
{ text: truncateDisplay(` ${connection.label}`, Math.max(0, width - 56)), fg: C.dim },
|
|
404
489
|
], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
|
|
405
490
|
})
|
|
406
491
|
: [
|
|
@@ -408,7 +493,7 @@ export class RemotePanel extends BasePanel {
|
|
|
408
493
|
return buildPanelListRow(width, [
|
|
409
494
|
{ text: contract.runnerId.padEnd(18), fg: C.value },
|
|
410
495
|
{ text: ` ${contract.transport.state.padEnd(18)}`, fg: stateColor(contract.transport.state) },
|
|
411
|
-
{ text: ` ${contract.template}
|
|
496
|
+
{ text: truncateDisplay(` ${contract.template}`, Math.max(0, width - 42)), fg: C.dim },
|
|
412
497
|
], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
|
|
413
498
|
}),
|
|
414
499
|
buildPanelLine(width, [[' No active connection is currently attached to these contracts.', C.dim]]),
|