@pellux/goodvibes-tui 1.1.0 → 1.7.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 +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import { BasePanel } from './base-panel.ts';
|
|
4
|
-
import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
5
|
-
import { buildSandboxReview, listSandboxPresets, listSandboxProfiles } from '@/runtime/index.ts';
|
|
6
|
-
import type { SandboxProfile, SandboxReview, SandboxSession, SandboxSessionRegistry } from '@/runtime/index.ts';
|
|
7
|
-
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
8
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
|
-
import {
|
|
10
|
-
buildAlignedRow,
|
|
11
|
-
buildBodyText,
|
|
12
|
-
buildEmptyState,
|
|
13
|
-
buildGuidanceLine,
|
|
14
|
-
buildKeyboardHints,
|
|
15
|
-
buildKeyValueLine,
|
|
16
|
-
buildPanelLine,
|
|
17
|
-
buildPanelWorkspace,
|
|
18
|
-
resolveStackedScrollableSections,
|
|
19
|
-
DEFAULT_PANEL_PALETTE,
|
|
20
|
-
type PanelPalette,
|
|
21
|
-
type PanelWorkspaceSection,
|
|
22
|
-
} from './polish.ts';
|
|
23
|
-
|
|
24
|
-
// Base chrome only — title band comes straight from DEFAULT_PANEL_PALETTE
|
|
25
|
-
// (WO-002).
|
|
26
|
-
const C = DEFAULT_PANEL_PALETTE;
|
|
27
|
-
|
|
28
|
-
/** Poll cadence for live session-state refresh; SandboxSessionRegistry has no event subscription. */
|
|
29
|
-
const POLL_INTERVAL_MS = 3_000;
|
|
30
|
-
|
|
31
|
-
type Selectable =
|
|
32
|
-
| { readonly kind: 'profile'; readonly id: SandboxProfile['id'] }
|
|
33
|
-
| { readonly kind: 'session'; readonly id: string };
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* One contextual next-step line, derived from the current review/session
|
|
37
|
-
* state instead of the previous nine-line guidance wall. Picks the single
|
|
38
|
-
* most relevant action given host warnings, QEMU config gaps, and whether
|
|
39
|
-
* any session is running yet.
|
|
40
|
-
*/
|
|
41
|
-
function buildContextualGuidance(width: number, review: SandboxReview, sessionCount: number, palette: PanelPalette): Line {
|
|
42
|
-
if (review.host.warnings.length > 0) {
|
|
43
|
-
return buildGuidanceLine(width, '/sandbox review', `resolve ${review.host.warnings.length} host warning(s) before enabling QEMU-backed execution`, palette);
|
|
44
|
-
}
|
|
45
|
-
if (review.config.vmBackend === 'qemu' && !review.config.qemuImagePath) {
|
|
46
|
-
return buildGuidanceLine(width, '/sandbox set-qemu-image <path>', 'set the guest image path before enabling QEMU-backed session execution', palette);
|
|
47
|
-
}
|
|
48
|
-
if (review.config.vmBackend === 'qemu' && !review.config.qemuExecWrapper) {
|
|
49
|
-
return buildGuidanceLine(width, '/sandbox set-qemu-wrapper <path>', 'configure the host bridge that actually executes commands inside the QEMU guest', palette);
|
|
50
|
-
}
|
|
51
|
-
if (sessionCount === 0) {
|
|
52
|
-
return buildGuidanceLine(width, 's', 'select a profile below and press s to start a sandbox session', palette);
|
|
53
|
-
}
|
|
54
|
-
return buildGuidanceLine(width, '/sandbox session run <id> <command> [args...]', 'run a custom command against a running session from the command line', palette);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const SANDBOX_HINTS = [
|
|
58
|
-
{ keys: '↑/↓', label: 'select' },
|
|
59
|
-
{ keys: 'Home/End', label: 'first profile/session' },
|
|
60
|
-
{ keys: 's', label: 'start' },
|
|
61
|
-
{ keys: 'x', label: 'stop' },
|
|
62
|
-
{ keys: 'e', label: 'execute probe' },
|
|
63
|
-
] as const;
|
|
64
|
-
|
|
65
|
-
export class SandboxPanel extends BasePanel {
|
|
66
|
-
private selectedIndex = 0;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The profile/session row under the cursor. This panel owns its own
|
|
70
|
-
* selection state (`selectedIndex` navigates the combined `_selectable()`
|
|
71
|
-
* list directly), so every selected-row read routes through this one
|
|
72
|
-
* accessor — indexing the `_selectable()` list by the cursor directly is
|
|
73
|
-
* banned by the no-raw-selectedindex-read architecture rule.
|
|
74
|
-
*/
|
|
75
|
-
private selectedSelectable(): Selectable | undefined {
|
|
76
|
-
return this._selectable().at(this.selectedIndex);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private sessionsScrollOffset = 0;
|
|
80
|
-
private profilesScrollOffset = 0;
|
|
81
|
-
private confirm: ConfirmState<string> | null = null;
|
|
82
|
-
private readonly config: ConfigManager;
|
|
83
|
-
private readonly sessions: SandboxSessionRegistry;
|
|
84
|
-
private readonly requestRender: () => void;
|
|
85
|
-
|
|
86
|
-
public constructor(
|
|
87
|
-
config: ConfigManager,
|
|
88
|
-
sessions: SandboxSessionRegistry,
|
|
89
|
-
requestRender: () => void = () => {},
|
|
90
|
-
) {
|
|
91
|
-
super('sandbox', 'Sandbox', '▪', 'security-policy');
|
|
92
|
-
this.config = config;
|
|
93
|
-
this.sessions = sessions;
|
|
94
|
-
this.requestRender = requestRender;
|
|
95
|
-
// Live state: SandboxSessionRegistry has no event subscription, so poll
|
|
96
|
-
// for session state changes (start/stop/execute results) while the
|
|
97
|
-
// panel is registered.
|
|
98
|
-
this.registerTimer(setInterval(() => {
|
|
99
|
-
this.markDirty();
|
|
100
|
-
this.requestRender();
|
|
101
|
-
}, POLL_INTERVAL_MS));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private _selectable(): Selectable[] {
|
|
105
|
-
const profiles = listSandboxProfiles(this.config);
|
|
106
|
-
const sessions = this.sessions.list();
|
|
107
|
-
return [
|
|
108
|
-
...profiles.map((profile): Selectable => ({ kind: 'profile', id: profile.id })),
|
|
109
|
-
...sessions.map((session): Selectable => ({ kind: 'session', id: session.id })),
|
|
110
|
-
];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
public handleInput(key: string): boolean {
|
|
114
|
-
if (this.lastError !== null) this.clearError();
|
|
115
|
-
|
|
116
|
-
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
117
|
-
if (confirmResult === 'confirmed') {
|
|
118
|
-
const sessionId = this.confirm!.subject;
|
|
119
|
-
this.confirm = null;
|
|
120
|
-
this.sessions.stop(sessionId);
|
|
121
|
-
this.markDirty();
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
if (confirmResult === 'cancelled') {
|
|
125
|
-
this.confirm = null;
|
|
126
|
-
this.markDirty();
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
if (confirmResult === 'absorbed') return true;
|
|
130
|
-
|
|
131
|
-
const selectable = this._selectable();
|
|
132
|
-
const profileCount = selectable.filter((entry) => entry.kind === 'profile').length;
|
|
133
|
-
const sessionCount = selectable.length - profileCount;
|
|
134
|
-
const selected = this.selectedSelectable();
|
|
135
|
-
|
|
136
|
-
if (key === 's' && selected?.kind === 'profile') {
|
|
137
|
-
void this._startSession(selected.id);
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
if (key === 'x' && selected?.kind === 'session') {
|
|
141
|
-
this.confirm = { subject: selected.id, label: `sandbox session ${selected.id}`, verb: 'Stop' };
|
|
142
|
-
this.markDirty();
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
if (key === 'e' && selected?.kind === 'session') {
|
|
146
|
-
void this._executeProbe(selected.id);
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const itemCount = selectable.length;
|
|
151
|
-
if (itemCount === 0) return false;
|
|
152
|
-
if (key === 'up' || key === 'k') {
|
|
153
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
154
|
-
this.markDirty();
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
if (key === 'down' || key === 'j') {
|
|
158
|
-
this.selectedIndex = Math.min(itemCount - 1, this.selectedIndex + 1);
|
|
159
|
-
this.markDirty();
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
if (key === 'home') {
|
|
163
|
-
this.selectedIndex = 0;
|
|
164
|
-
this.markDirty();
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
if (key === 'end') {
|
|
168
|
-
this.selectedIndex = sessionCount > 0 ? profileCount : Math.max(0, itemCount - 1);
|
|
169
|
-
this.markDirty();
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Start a sandbox session for `profileId` via the shared registry, then repaint. */
|
|
176
|
-
private async _startSession(profileId: SandboxProfile['id']): Promise<void> {
|
|
177
|
-
try {
|
|
178
|
-
await this.sessions.start(profileId, undefined, this.config);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
this.setError(summarizeError(err));
|
|
181
|
-
} finally {
|
|
182
|
-
this.markDirty();
|
|
183
|
-
this.requestRender();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Execute a lightweight liveness probe against the selected session so its
|
|
189
|
-
* live state (last run, exit status, stdout/stderr preview) is genuinely
|
|
190
|
-
* populated from `SandboxSessionRegistry.execute()`. Panels have no
|
|
191
|
-
* free-text input widget today, so this proves the execute verb is wired
|
|
192
|
-
* without inventing one; `/sandbox session run <id> <command> [args...]`
|
|
193
|
-
* remains available for arbitrary commands.
|
|
194
|
-
*/
|
|
195
|
-
private async _executeProbe(sessionId: string): Promise<void> {
|
|
196
|
-
try {
|
|
197
|
-
this.sessions.execute(sessionId, process.execPath, ['-e', "console.log('sandbox panel probe ok')"], this.config, { timeoutMs: 5_000 });
|
|
198
|
-
} catch (err) {
|
|
199
|
-
this.setError(summarizeError(err));
|
|
200
|
-
} finally {
|
|
201
|
-
this.markDirty();
|
|
202
|
-
this.requestRender();
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
public render(width: number, height: number): Line[] {
|
|
207
|
-
this.needsRender = false;
|
|
208
|
-
|
|
209
|
-
if (this.confirm) {
|
|
210
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
211
|
-
title: 'Sandbox Control Room',
|
|
212
|
-
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
213
|
-
palette: C,
|
|
214
|
-
});
|
|
215
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
216
|
-
return lines.slice(0, height);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const review = buildSandboxReview(this.config);
|
|
220
|
-
const profiles = listSandboxProfiles(this.config);
|
|
221
|
-
const presets = listSandboxPresets();
|
|
222
|
-
const sessions = this.sessions.list();
|
|
223
|
-
const selectable = this._selectable();
|
|
224
|
-
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, selectable.length - 1));
|
|
225
|
-
const selected = this.selectedSelectable() ?? null;
|
|
226
|
-
const selectedProfile = selected?.kind === 'profile'
|
|
227
|
-
? profiles.find((profile) => profile.id === selected.id) ?? null
|
|
228
|
-
: null;
|
|
229
|
-
const selectedSession = selected?.kind === 'session'
|
|
230
|
-
? sessions.find((session) => session.id === selected.id) ?? null
|
|
231
|
-
: null;
|
|
232
|
-
|
|
233
|
-
const intro = 'Sandbox posture for local execution, optional QEMU isolation, profiles, sessions, and Windows host requirements.';
|
|
234
|
-
const overviewLines: Line[] = [
|
|
235
|
-
buildKeyValueLine(width, [
|
|
236
|
-
{ label: 'host', value: review.host.platform, valueColor: C.value },
|
|
237
|
-
{ label: 'backend', value: review.config.vmBackend, valueColor: C.info },
|
|
238
|
-
{ label: 'windows mode', value: review.config.windowsMode, valueColor: review.host.secureSandboxReady || !review.host.windows ? C.good : C.warn },
|
|
239
|
-
], C),
|
|
240
|
-
buildKeyValueLine(width, [
|
|
241
|
-
{ label: 'repl isolation', value: review.config.replIsolation, valueColor: C.value },
|
|
242
|
-
{ label: 'mcp isolation', value: review.config.mcpIsolation, valueColor: review.config.mcpIsolation === 'disabled' ? C.warn : C.good },
|
|
243
|
-
], C),
|
|
244
|
-
buildKeyValueLine(width, [
|
|
245
|
-
{ label: 'virtualization', value: review.config.vmBackend === 'local' ? 'disabled' : (review.host.secureSandboxReady ? 'ready' : 'host blocked'), valueColor: review.config.vmBackend === 'local' ? C.dim : (review.host.secureSandboxReady ? C.good : C.warn) },
|
|
246
|
-
{ label: 'warnings', value: String(review.host.warnings.length), valueColor: review.host.warnings.length > 0 ? C.warn : C.dim },
|
|
247
|
-
{ label: 'sessions', value: String(sessions.length), valueColor: sessions.length > 0 ? C.info : C.dim },
|
|
248
|
-
], C),
|
|
249
|
-
buildKeyValueLine(width, [
|
|
250
|
-
{ label: 'qemu binary', value: review.config.qemuBinary || '(default)', valueColor: C.value },
|
|
251
|
-
{ label: 'qemu image', value: review.config.qemuImagePath || '(not configured)', valueColor: review.config.qemuImagePath ? C.info : C.warn },
|
|
252
|
-
], C),
|
|
253
|
-
buildKeyValueLine(width, [
|
|
254
|
-
{ label: 'qemu wrapper', value: review.config.qemuExecWrapper || '(not configured)', valueColor: review.config.qemuExecWrapper ? C.info : C.warn },
|
|
255
|
-
{ label: 'guest host', value: review.config.qemuGuestHost || '(not configured)', valueColor: review.config.qemuGuestHost ? C.info : C.warn },
|
|
256
|
-
], C),
|
|
257
|
-
buildKeyValueLine(width, [
|
|
258
|
-
{ label: 'guest port', value: String(review.config.qemuGuestPort), valueColor: C.value },
|
|
259
|
-
{ label: 'guest user', value: review.config.qemuGuestUser || '(not configured)', valueColor: review.config.qemuGuestUser ? C.info : C.warn },
|
|
260
|
-
{ label: 'session mode', value: review.config.qemuSessionMode, valueColor: review.config.qemuSessionMode === 'attach' ? C.dim : C.info },
|
|
261
|
-
], C),
|
|
262
|
-
buildKeyValueLine(width, [
|
|
263
|
-
{ label: 'guest workspace', value: review.config.qemuWorkspacePath || '(not configured)', valueColor: review.config.qemuWorkspacePath ? C.value : C.warn },
|
|
264
|
-
], C),
|
|
265
|
-
// Single contextual next-step line (replaces the former nine-line
|
|
266
|
-
// guidance wall) — the only buildGuidanceLine call in this panel.
|
|
267
|
-
buildContextualGuidance(width, review, sessions.length, C),
|
|
268
|
-
];
|
|
269
|
-
|
|
270
|
-
const selectionLines: Line[] = [];
|
|
271
|
-
if (selectedProfile) {
|
|
272
|
-
selectionLines.push(buildKeyValueLine(width, [
|
|
273
|
-
{ label: 'id', value: selectedProfile.id, valueColor: C.info },
|
|
274
|
-
{ label: 'kind', value: selectedProfile.kind, valueColor: C.value },
|
|
275
|
-
{ label: 'isolation', value: selectedProfile.isolation, valueColor: C.value },
|
|
276
|
-
{ label: 'vm', value: selectedProfile.requiresVm ? 'required' : 'optional', valueColor: selectedProfile.requiresVm ? C.good : C.warn },
|
|
277
|
-
], C));
|
|
278
|
-
selectionLines.push(...buildBodyText(width, selectedProfile.notes.join(' | '), C, C.dim));
|
|
279
|
-
} else if (selectedSession) {
|
|
280
|
-
selectionLines.push(buildKeyValueLine(width, [
|
|
281
|
-
{ label: 'profile', value: selectedSession.profileId, valueColor: C.info },
|
|
282
|
-
{ label: 'state', value: selectedSession.state, valueColor: selectedSession.state === 'running' ? C.good : selectedSession.state === 'failed' ? C.bad : C.warn },
|
|
283
|
-
{ label: 'backend', value: selectedSession.resolvedBackend ?? selectedSession.backend, valueColor: C.value },
|
|
284
|
-
{ label: 'sharing', value: selectedSession.shared ? 'shared' : 'dedicated', valueColor: C.value },
|
|
285
|
-
], C));
|
|
286
|
-
if (selectedSession.startupStatus) {
|
|
287
|
-
selectionLines.push(buildKeyValueLine(width, [
|
|
288
|
-
{ label: 'startup', value: selectedSession.startupStatus, valueColor: selectedSession.startupStatus === 'verified' ? C.good : selectedSession.startupStatus === 'failed' ? C.bad : C.warn },
|
|
289
|
-
], C));
|
|
290
|
-
}
|
|
291
|
-
if (selectedSession.launchPlan) {
|
|
292
|
-
selectionLines.push(...buildBodyText(width, `Launch plan: ${selectedSession.launchPlan.summary}`, C, C.dim));
|
|
293
|
-
}
|
|
294
|
-
if (selectedSession.startupDetail) {
|
|
295
|
-
selectionLines.push(...buildBodyText(width, `Startup: ${selectedSession.startupDetail}`, C, selectedSession.startupStatus === 'failed' ? C.bad : C.dim));
|
|
296
|
-
}
|
|
297
|
-
if (selectedSession.lastCommandSummary) {
|
|
298
|
-
selectionLines.push(...buildBodyText(width, `Last command: ${selectedSession.lastCommandSummary}`, C, C.dim));
|
|
299
|
-
}
|
|
300
|
-
if (selectedSession.lastRunAt !== undefined) {
|
|
301
|
-
selectionLines.push(buildKeyValueLine(width, [
|
|
302
|
-
{ label: 'runs', value: String(selectedSession.executionCount ?? 0), valueColor: C.info },
|
|
303
|
-
{ label: 'last exit', value: String(selectedSession.lastExitStatus ?? 'n/a'), valueColor: selectedSession.lastExitStatus === 0 ? C.good : C.warn },
|
|
304
|
-
{ label: 'last run', value: new Date(selectedSession.lastRunAt).toISOString(), valueColor: C.dim },
|
|
305
|
-
], C));
|
|
306
|
-
}
|
|
307
|
-
if (selectedSession.lastStdoutPreview) {
|
|
308
|
-
selectionLines.push(...buildBodyText(width, `stdout: ${selectedSession.lastStdoutPreview}`, C, C.dim));
|
|
309
|
-
}
|
|
310
|
-
if (selectedSession.lastStderrPreview) {
|
|
311
|
-
selectionLines.push(...buildBodyText(width, `stderr: ${selectedSession.lastStderrPreview}`, C, C.bad));
|
|
312
|
-
}
|
|
313
|
-
selectionLines.push(...buildBodyText(width, selectedSession.notes.join(' | '), C, C.dim));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const sessionLines: Line[] = [];
|
|
317
|
-
if (sessions.length === 0) {
|
|
318
|
-
sessionLines.push(...buildEmptyState(
|
|
319
|
-
width,
|
|
320
|
-
' No active sandbox sessions.',
|
|
321
|
-
'Select a profile above and press s to start a sandbox session.',
|
|
322
|
-
[],
|
|
323
|
-
C,
|
|
324
|
-
));
|
|
325
|
-
} else {
|
|
326
|
-
for (const session of sessions) {
|
|
327
|
-
const selectedRow = selectedSession?.id === session.id;
|
|
328
|
-
sessionLines.push(buildAlignedRow(
|
|
329
|
-
width,
|
|
330
|
-
[
|
|
331
|
-
{ text: session.profileId, fg: C.info },
|
|
332
|
-
{ text: session.state, fg: session.state === 'running' ? C.good : session.state === 'failed' ? C.bad : C.warn },
|
|
333
|
-
{ text: session.shared ? 'shared' : 'dedicated', fg: C.value },
|
|
334
|
-
{ text: String(session.resolvedBackend ?? session.backend), fg: C.dim },
|
|
335
|
-
{ text: session.startupStatus ?? 'n/a', fg: session.startupStatus === 'verified' ? C.good : session.startupStatus === 'failed' ? C.bad : C.warn },
|
|
336
|
-
{ text: `${session.executionCount ?? 0}x`, fg: C.info },
|
|
337
|
-
{ text: session.id, fg: C.dim },
|
|
338
|
-
],
|
|
339
|
-
[
|
|
340
|
-
{ width: 15 },
|
|
341
|
-
{ width: 10 },
|
|
342
|
-
{ width: 12 },
|
|
343
|
-
{ width: 8 },
|
|
344
|
-
{ width: 9 },
|
|
345
|
-
{ width: 5, align: 'right' },
|
|
346
|
-
{ width: Math.max(8, width - 72) },
|
|
347
|
-
],
|
|
348
|
-
{ selected: selectedRow, selectedBg: C.headerBg },
|
|
349
|
-
));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const presetLines: Line[] = [];
|
|
354
|
-
for (const preset of presets.slice(0, 2)) {
|
|
355
|
-
presetLines.push(buildAlignedRow(
|
|
356
|
-
width,
|
|
357
|
-
[
|
|
358
|
-
{ text: preset.id, fg: C.info },
|
|
359
|
-
{ text: preset.config.replIsolation, fg: C.value },
|
|
360
|
-
{ text: preset.config.mcpIsolation, fg: C.dim },
|
|
361
|
-
{ text: preset.config.windowsMode, fg: C.warn },
|
|
362
|
-
],
|
|
363
|
-
[
|
|
364
|
-
{ width: 18 },
|
|
365
|
-
{ width: 16 },
|
|
366
|
-
{ width: 16 },
|
|
367
|
-
{ width: Math.max(8, width - 62) },
|
|
368
|
-
],
|
|
369
|
-
{},
|
|
370
|
-
));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const profileLines: Line[] = [];
|
|
374
|
-
for (const profile of profiles) {
|
|
375
|
-
const selectedRow = selectedProfile?.id === profile.id;
|
|
376
|
-
profileLines.push(buildAlignedRow(
|
|
377
|
-
width,
|
|
378
|
-
[
|
|
379
|
-
{ text: profile.id, fg: C.info },
|
|
380
|
-
{ text: profile.isolation, fg: C.value },
|
|
381
|
-
{ text: profile.kind, fg: C.dim },
|
|
382
|
-
{ text: `vm=${profile.requiresVm ? 'yes' : 'no'}`, fg: profile.requiresVm ? C.good : C.warn },
|
|
383
|
-
],
|
|
384
|
-
[
|
|
385
|
-
{ width: 15 },
|
|
386
|
-
{ width: 14 },
|
|
387
|
-
{ width: 12 },
|
|
388
|
-
{ width: Math.max(6, width - 53) },
|
|
389
|
-
],
|
|
390
|
-
{ selected: selectedRow, selectedBg: C.headerBg },
|
|
391
|
-
));
|
|
392
|
-
}
|
|
393
|
-
const postureSection: PanelWorkspaceSection = { title: 'Sandbox posture', lines: overviewLines };
|
|
394
|
-
const selectedSection: PanelWorkspaceSection = { title: selectedProfile ? 'Selected Profile' : 'Selected Session', lines: selectionLines };
|
|
395
|
-
const presetsSection: PanelWorkspaceSection = { title: 'Presets', lines: presetLines };
|
|
396
|
-
const [sessionsSection, profilesSection] = resolveStackedScrollableSections(width, height, {
|
|
397
|
-
intro,
|
|
398
|
-
footerLines: [buildKeyboardHints(width, SANDBOX_HINTS, C)],
|
|
399
|
-
palette: C,
|
|
400
|
-
beforeSections: [
|
|
401
|
-
postureSection,
|
|
402
|
-
...(selectionLines.length > 0 ? [selectedSection] : []),
|
|
403
|
-
],
|
|
404
|
-
sections: [
|
|
405
|
-
{
|
|
406
|
-
title: 'Sessions',
|
|
407
|
-
scrollableLines: sessionLines,
|
|
408
|
-
selectedIndex: selectedSession ? Math.max(0, sessions.findIndex((session) => session.id === selectedSession.id)) : undefined,
|
|
409
|
-
scrollOffset: this.sessionsScrollOffset,
|
|
410
|
-
minRows: 2,
|
|
411
|
-
weight: 1,
|
|
412
|
-
appendWindowSummary: sessions.length > 0 ? {
|
|
413
|
-
dimColor: C.dim,
|
|
414
|
-
formatter: (window) => buildPanelLine(width, [[` [${window.start + 1}-${window.end} of ${sessions.length}]`, C.dim]]),
|
|
415
|
-
} : undefined,
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
title: 'Profiles',
|
|
419
|
-
scrollableLines: profileLines,
|
|
420
|
-
selectedIndex: selectedProfile ? Math.max(0, profiles.findIndex((profile) => profile.id === selectedProfile.id)) : undefined,
|
|
421
|
-
scrollOffset: this.profilesScrollOffset,
|
|
422
|
-
minRows: 2,
|
|
423
|
-
weight: 1,
|
|
424
|
-
appendWindowSummary: profiles.length > 0 ? {
|
|
425
|
-
dimColor: C.dim,
|
|
426
|
-
formatter: (window) => buildPanelLine(width, [[` [${window.start + 1}-${window.end} of ${profiles.length}]`, C.dim]]),
|
|
427
|
-
} : undefined,
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
afterSections: [presetsSection],
|
|
431
|
-
});
|
|
432
|
-
this.sessionsScrollOffset = sessionsSection?.scrollOffset ?? this.sessionsScrollOffset;
|
|
433
|
-
this.profilesScrollOffset = profilesSection?.scrollOffset ?? this.profilesScrollOffset;
|
|
434
|
-
|
|
435
|
-
const sections: PanelWorkspaceSection[] = [
|
|
436
|
-
postureSection,
|
|
437
|
-
...(selectionLines.length > 0 ? [selectedSection] : []),
|
|
438
|
-
sessionsSection?.section ?? { title: 'Sessions', lines: sessionLines },
|
|
439
|
-
presetsSection,
|
|
440
|
-
profilesSection?.section ?? { title: 'Profiles', lines: profileLines },
|
|
441
|
-
];
|
|
442
|
-
|
|
443
|
-
const errorLine = this.renderErrorLine(width);
|
|
444
|
-
if (errorLine) sections.push({ title: 'Error', lines: [errorLine] });
|
|
445
|
-
|
|
446
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
447
|
-
title: 'Sandbox Control Room',
|
|
448
|
-
intro,
|
|
449
|
-
sections,
|
|
450
|
-
footerLines: [buildKeyboardHints(width, SANDBOX_HINTS, C)],
|
|
451
|
-
palette: C,
|
|
452
|
-
});
|
|
453
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
454
|
-
return lines.slice(0, height);
|
|
455
|
-
}
|
|
456
|
-
}
|