@aiwg/cockpit 2026.6.3
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/README.md +337 -0
- package/bridge/package.json +13 -0
- package/bridge/src/public/index.html +395 -0
- package/bridge/src/server.mjs +1132 -0
- package/bridge/src/smoke.mjs +158 -0
- package/contrib/aiwg-core.json +15 -0
- package/contrib/contribution.schema.json +69 -0
- package/desktop/README.md +42 -0
- package/desktop/src-tauri/Cargo.toml +17 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/frontend/index.html +11 -0
- package/desktop/src-tauri/src/main.rs +55 -0
- package/desktop/src-tauri/tauri.conf.json +20 -0
- package/package.json +49 -0
- package/runtime-docs/README.md +38 -0
- package/shell-core/runtime.mjs +45 -0
- package/shell-core/smoke.mjs +36 -0
- package/vscode/README.md +25 -0
- package/vscode/extension.js +59 -0
- package/vscode/package.json +29 -0
- package/web/dist/assets/index-B5anpdS1.js +67 -0
- package/web/dist/assets/index-CP3BF6uZ.css +32 -0
- package/web/dist/index.html +14 -0
- package/web/index.html +13 -0
- package/web/package.json +31 -0
- package/web/src/App.test.tsx +237 -0
- package/web/src/App.tsx +255 -0
- package/web/src/api.ts +20 -0
- package/web/src/components/Actions.tsx +60 -0
- package/web/src/components/Approvals.tsx +62 -0
- package/web/src/components/CapabilitySearch.tsx +73 -0
- package/web/src/components/Explore.tsx +41 -0
- package/web/src/components/Inventory.tsx +124 -0
- package/web/src/components/LaunchInstanceModal.test.tsx +97 -0
- package/web/src/components/LaunchInstanceModal.tsx +236 -0
- package/web/src/components/Library.tsx +76 -0
- package/web/src/components/Running.tsx +60 -0
- package/web/src/components/Sessions.test.tsx +125 -0
- package/web/src/components/Sessions.tsx +189 -0
- package/web/src/components/StartSessionModal.test.tsx +86 -0
- package/web/src/components/StartSessionModal.tsx +168 -0
- package/web/src/components/Welcome.tsx +474 -0
- package/web/src/main.tsx +11 -0
- package/web/src/styles.css +254 -0
- package/web/src/types.ts +47 -0
- package/web/src/useDebounce.ts +10 -0
- package/web/src/useSession.ts +220 -0
- package/web/src/util.test.ts +30 -0
- package/web/src/util.ts +17 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { useEffect, useState, type CSSProperties } from 'react';
|
|
2
|
+
import { api } from '../api';
|
|
3
|
+
import { fmtId } from '../util';
|
|
4
|
+
import type { Instance, Approval, Cost, RunningTask } from '../types';
|
|
5
|
+
|
|
6
|
+
interface Status {
|
|
7
|
+
instances: Instance[];
|
|
8
|
+
running: RunningTask[];
|
|
9
|
+
approvals: Approval[];
|
|
10
|
+
cost: Cost | null;
|
|
11
|
+
executor: string;
|
|
12
|
+
connected: boolean;
|
|
13
|
+
inventoryWarning?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface OrbitNode {
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
meta: string;
|
|
20
|
+
family: 'host' | 'container' | 'vm' | 'mission' | 'approval' | 'cost' | 'capability' | 'library' | 'action' | 'session' | 'inventory';
|
|
21
|
+
tab: string;
|
|
22
|
+
state: 'running' | 'ready' | 'attention' | 'blocked' | 'idle';
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type OrbitStyle = CSSProperties & { '--x': string; '--y': string };
|
|
28
|
+
type WallReviewMode = 'topology' | 'handoff';
|
|
29
|
+
|
|
30
|
+
const ORBIT_POSITIONS = [
|
|
31
|
+
[50, 10], [72, 16], [88, 34], [84, 58], [68, 72], [50, 72],
|
|
32
|
+
[32, 72], [16, 58], [12, 34], [28, 16], [50, 30],
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
interface InventoryEnvelope {
|
|
36
|
+
instances: Instance[];
|
|
37
|
+
degraded_admin_inventory?: string;
|
|
38
|
+
admin_error?: { title?: string; detail?: string; code?: string; status?: number };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Welcome({ onStartSession, onLaunchInstance, goTo }: { onStartSession: () => void; onLaunchInstance: () => void; goTo: (t: string) => void }) {
|
|
42
|
+
const [st, setSt] = useState<Status | null>(null);
|
|
43
|
+
const [firstRun] = useState(() => !localStorage.getItem('cockpit.welcomed'));
|
|
44
|
+
const [selectedStackId, setSelectedStackId] = useState('');
|
|
45
|
+
const [selectedRuntime, setSelectedRuntime] = useState('auto');
|
|
46
|
+
const [selectedTask, setSelectedTask] = useState('observe');
|
|
47
|
+
const [wallMode, setWallMode] = useState<WallReviewMode>(() => initialWallMode());
|
|
48
|
+
|
|
49
|
+
useEffect(() => { localStorage.setItem('cockpit.welcomed', '1'); }, []);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
(async () => {
|
|
52
|
+
try {
|
|
53
|
+
// Inventory + health are load-bearing: they decide whether a stack is
|
|
54
|
+
// connected. Fetch them first and let a failure fall through to the
|
|
55
|
+
// disconnected state.
|
|
56
|
+
const [inv, health] = await Promise.all([
|
|
57
|
+
api<InventoryEnvelope>('/api/inventory'),
|
|
58
|
+
api<{ executor_url: string }>('/api/health'),
|
|
59
|
+
]);
|
|
60
|
+
// Running / approvals / cost are enrichment. A real executor may expose
|
|
61
|
+
// no such admin surface (#1638), so each degrades to empty on its own
|
|
62
|
+
// and must never blank the whole Home view.
|
|
63
|
+
const [run, apr, cost] = await Promise.all([
|
|
64
|
+
api<{ count: number; running: RunningTask[] }>('/api/running').catch(() => ({ running: [] as RunningTask[] })),
|
|
65
|
+
api<{ approvals: Approval[] }>('/api/approvals?status=pending').catch(() => ({ approvals: [] as Approval[] })),
|
|
66
|
+
api<Cost>('/api/cost').catch(() => null),
|
|
67
|
+
]);
|
|
68
|
+
setSt({
|
|
69
|
+
instances: inv.instances,
|
|
70
|
+
running: run.running ?? [],
|
|
71
|
+
approvals: apr.approvals ?? [],
|
|
72
|
+
cost,
|
|
73
|
+
executor: health.executor_url,
|
|
74
|
+
connected: inv.instances.length > 0,
|
|
75
|
+
inventoryWarning: inventoryWarning(inv),
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
setSt({ instances: [], running: [], approvals: [], cost: null, executor: '', connected: false });
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const runningInstances = st?.instances.filter((i) => i.state === 'running') ?? [];
|
|
84
|
+
const runtimeCoverage = {
|
|
85
|
+
host: st?.instances.some((i) => i.runtime_posture.kind === 'host') ?? false,
|
|
86
|
+
container: st?.instances.some((i) => i.runtime_posture.kind === 'container' || i.runtime_posture.kind === 'docker') ?? false,
|
|
87
|
+
vm: st?.instances.some((i) => i.runtime_posture.kind === 'vm') ?? false,
|
|
88
|
+
};
|
|
89
|
+
const copyStartCommand = async () => {
|
|
90
|
+
await navigator.clipboard?.writeText('aiwg cockpit');
|
|
91
|
+
};
|
|
92
|
+
const orbitNodes = buildOrbitNodes(st);
|
|
93
|
+
const attentionCount = (st?.approvals.length ?? 0) + (runtimeCoverage.host && runtimeCoverage.container && runtimeCoverage.vm ? 0 : 1);
|
|
94
|
+
const selectedStack = runningInstances.find((i) => i.id === selectedStackId) ?? runningInstances[0] ?? st?.instances[0];
|
|
95
|
+
const selectedStackCost = selectedStack ? st?.cost?.per_instance.find((c) => c.instance_id === selectedStack.id) : undefined;
|
|
96
|
+
const totalCost = st?.cost?.total.usd ?? 0;
|
|
97
|
+
const quotaUsed = st?.cost ? Math.min(100, Math.max(6, totalCost * 12)) : 0;
|
|
98
|
+
const startPlan = [
|
|
99
|
+
selectedStack ? selectedStack.loadout : 'no stack',
|
|
100
|
+
selectedRuntime === 'auto' ? 'auto runtime' : selectedRuntime,
|
|
101
|
+
selectedTask,
|
|
102
|
+
].join(' / ');
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="welcome operator-wall">
|
|
106
|
+
<section className={`wall-hero ${st?.connected ? 'wall-hero-compact' : ''}`} aria-labelledby="cockpit-wall-title">
|
|
107
|
+
<div>
|
|
108
|
+
<p className="eyebrow">Operator wall</p>
|
|
109
|
+
<h2 id="cockpit-wall-title">Work alongside your agents</h2>
|
|
110
|
+
<p className="lead">
|
|
111
|
+
Observe live stacks, take control when a session allows it, and keep approvals, cost,
|
|
112
|
+
runtime posture, and handoff context visible in one control plane.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="hero-actions" aria-label="Primary Cockpit actions">
|
|
116
|
+
<button className="cta" onClick={onLaunchInstance}>+ New instance + session</button>
|
|
117
|
+
<button className="cta" onClick={onStartSession}>▸ Start a session</button>
|
|
118
|
+
<button onClick={() => goTo('running')}>View board</button>
|
|
119
|
+
<button onClick={copyStartCommand}>Copy CLI</button>
|
|
120
|
+
{st?.connected && (
|
|
121
|
+
<div className="segmented" role="group" aria-label="Wall review mode">
|
|
122
|
+
<button aria-pressed={wallMode === 'topology'} onClick={() => setWallMode('topology')}>Topology</button>
|
|
123
|
+
<button aria-pressed={wallMode === 'handoff'} onClick={() => setWallMode('handoff')}>Handoff</button>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
{!st ? <p className="empty">Checking your stacks…</p>
|
|
130
|
+
: !st.connected ? (
|
|
131
|
+
<div className="card warn-card">
|
|
132
|
+
<h3>No stack connected</h3>
|
|
133
|
+
{st.executor ? (
|
|
134
|
+
<>
|
|
135
|
+
<p>Bridge is connected to a real executor at <code>{st.executor}</code>, but that executor is not reporting any registered instances or agents.</p>
|
|
136
|
+
{st.inventoryWarning && <p className="warn">Inventory note: {st.inventoryWarning}</p>}
|
|
137
|
+
<div className="cta-row">
|
|
138
|
+
<button className="cta" onClick={onLaunchInstance}>+ Launch instance</button>
|
|
139
|
+
<button onClick={() => goTo('inventory')}>Open inventory</button>
|
|
140
|
+
</div>
|
|
141
|
+
</>
|
|
142
|
+
) : (
|
|
143
|
+
<>
|
|
144
|
+
<p>Cockpit talks to a real agentic-sandbox executor. Start the executor, then point the Bridge at it:</p>
|
|
145
|
+
<pre className="terminal">AIWG_COCKPIT_EXECUTOR_URL=http://127.0.0.1:<executor-port> node apps/cockpit/bridge/src/server.mjs</pre>
|
|
146
|
+
</>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
) : (
|
|
150
|
+
<>
|
|
151
|
+
<section className="radial-wall" aria-labelledby="operator-wall-map-title">
|
|
152
|
+
<div className={`radial-map mode-${wallMode}`} aria-label="Operator wall topology">
|
|
153
|
+
<svg className="radial-links" viewBox="0 0 100 100" aria-hidden="true" focusable="false">
|
|
154
|
+
<circle className="link-ring" cx="50" cy="50" r="37" />
|
|
155
|
+
<circle className="link-ring link-ring-inner" cx="50" cy="50" r="22" />
|
|
156
|
+
{orbitNodes.map((node) => (
|
|
157
|
+
<line key={node.key} className={`link-line link-${node.state}`} x1="50" y1="50" x2={node.x} y2={node.y} />
|
|
158
|
+
))}
|
|
159
|
+
<path className="handoff-arc" d="M 17 58 C 29 19, 70 13, 87 42" />
|
|
160
|
+
</svg>
|
|
161
|
+
<button className="central-hub" onClick={onStartSession} aria-label="Start from Cockpit command hub">
|
|
162
|
+
<span className="hub-core" aria-hidden="true" />
|
|
163
|
+
<strong>Cockpit</strong>
|
|
164
|
+
<span>{runningInstances.length} running · {st.approvals.length} approvals</span>
|
|
165
|
+
</button>
|
|
166
|
+
{orbitNodes.map((node, index) => (
|
|
167
|
+
<button
|
|
168
|
+
key={node.key}
|
|
169
|
+
className={`orbit-node node-${node.family} node-${node.state}`}
|
|
170
|
+
style={orbitStyle(node)}
|
|
171
|
+
onClick={() => goTo(node.tab)}
|
|
172
|
+
aria-label={`${node.label}: ${node.meta}`}
|
|
173
|
+
>
|
|
174
|
+
<span className={`node-glyph glyph-${node.family}`} aria-hidden="true" />
|
|
175
|
+
<span className="node-copy">
|
|
176
|
+
<strong>{node.label}</strong>
|
|
177
|
+
<span>{node.meta}</span>
|
|
178
|
+
</span>
|
|
179
|
+
{index === 3 && <span className="handoff-marker" aria-hidden="true" />}
|
|
180
|
+
</button>
|
|
181
|
+
))}
|
|
182
|
+
{wallMode === 'handoff' && (
|
|
183
|
+
<>
|
|
184
|
+
<div className="handoff-callout handoff-source">
|
|
185
|
+
<span>Source</span>
|
|
186
|
+
<strong>{st.running[0] ? fmtId(st.running[0].instance_id) : 'ready'}</strong>
|
|
187
|
+
</div>
|
|
188
|
+
<div className="handoff-callout handoff-destination">
|
|
189
|
+
<span>Destination</span>
|
|
190
|
+
<strong>{runningInstances[1] ? fmtId(runningInstances[1].id) : 'next stack'}</strong>
|
|
191
|
+
</div>
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
<div className="radial-overlay" aria-labelledby="operator-wall-map-title">
|
|
195
|
+
<div className="radial-overlay-head">
|
|
196
|
+
<div>
|
|
197
|
+
<p className="eyebrow">{wallMode === 'handoff' ? 'Mission route' : 'Live topology'}</p>
|
|
198
|
+
<h3 id="operator-wall-map-title">{wallMode === 'handoff' ? 'Mission-handoff operator wall' : 'Eleven-stack operator wall'}</h3>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="mission-route" aria-label="Mission handoff state">
|
|
201
|
+
<span>Mission</span>
|
|
202
|
+
<strong>{st.running[0] ? fmtId(st.running[0].task_id) : 'ready'}</strong>
|
|
203
|
+
<span className={attentionCount > 0 ? 'route-attention' : 'route-ready'}>
|
|
204
|
+
{attentionCount > 0 ? `${attentionCount} gate(s)` : 'clear'}
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<dl className="wall-readout">
|
|
209
|
+
<div><dt>Coverage</dt><dd>{[runtimeCoverage.host, runtimeCoverage.container, runtimeCoverage.vm].filter(Boolean).length}/3</dd></div>
|
|
210
|
+
<div><dt>Executor</dt><dd>{st.executor || 'unknown'}</dd></div>
|
|
211
|
+
<div><dt>Control</dt><dd>{runningInstances.some((i) => i.session_backends.some((b) => b.available && b.drive)) ? 'drive available' : 'observe first'}</dd></div>
|
|
212
|
+
<div><dt>Quota</dt><dd>{st.cost ? `$${st.cost.total.usd.toFixed(2)}` : 'n/a'}</dd></div>
|
|
213
|
+
<div><dt>Gates</dt><dd>{attentionCount > 0 ? `${attentionCount} attention` : 'clear'}</dd></div>
|
|
214
|
+
</dl>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</section>
|
|
218
|
+
|
|
219
|
+
<section className="stack-board" aria-label="Running stack board">
|
|
220
|
+
{runningInstances.slice(0, 6).map((i) => {
|
|
221
|
+
const task = st.running.find((r) => r.instance_id === i.id);
|
|
222
|
+
const cost = st.cost?.per_instance.find((c) => c.instance_id === i.id);
|
|
223
|
+
const drive = i.session_backends.some((b) => b.available && b.drive);
|
|
224
|
+
return (
|
|
225
|
+
<article className={`stack-card accent-${accentFor(i.id)}`} key={i.id}>
|
|
226
|
+
<div className="stack-head">
|
|
227
|
+
<span className={`topology topology-${runtimeFamily(i)}`} aria-hidden="true" />
|
|
228
|
+
<div>
|
|
229
|
+
<h3>{i.loadout}</h3>
|
|
230
|
+
<p>{fmtId(i.id)} · {i.runtime_posture.label}</p>
|
|
231
|
+
</div>
|
|
232
|
+
<span className={`badge isolation-${i.runtime_posture.isolation}`}>{i.state}</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="stack-log" role="img" aria-label={`Recent activity for ${i.loadout}`}>
|
|
235
|
+
<span>{task ? `task ${fmtId(task.task_id)} · ${task.state}` : 'idle · ready for session attach'}</span>
|
|
236
|
+
<span>{i.transport.label} · {i.transport.mode}</span>
|
|
237
|
+
<span>{drive ? 'Drive enabled' : 'Observe only'}</span>
|
|
238
|
+
</div>
|
|
239
|
+
<div className="segment-progress" aria-label="Session progress">
|
|
240
|
+
<span style={{ width: task ? '68%' : '28%' }} />
|
|
241
|
+
</div>
|
|
242
|
+
<div className="transport-bar">
|
|
243
|
+
<button onClick={() => goTo('sessions')}>Attach</button>
|
|
244
|
+
<button onClick={() => goTo('running')}>Details</button>
|
|
245
|
+
<span>{cost ? `$${cost.usd.toFixed(2)}` : 'cost n/a'}</span>
|
|
246
|
+
</div>
|
|
247
|
+
</article>
|
|
248
|
+
);
|
|
249
|
+
})}
|
|
250
|
+
</section>
|
|
251
|
+
|
|
252
|
+
<section className="telemetry-strip" aria-label="Fleet telemetry">
|
|
253
|
+
<button onClick={() => goTo('inventory')}>
|
|
254
|
+
<strong>{st.instances.length}</strong>
|
|
255
|
+
<span>instances · {runningInstances.length} running</span>
|
|
256
|
+
</button>
|
|
257
|
+
<button onClick={() => goTo('running')}>
|
|
258
|
+
<strong>{st.running.length}</strong>
|
|
259
|
+
<span>active tasks</span>
|
|
260
|
+
</button>
|
|
261
|
+
<button onClick={() => goTo('approvals')}>
|
|
262
|
+
<strong>{st.approvals.length}</strong>
|
|
263
|
+
<span>pending approvals</span>
|
|
264
|
+
</button>
|
|
265
|
+
<button onClick={() => goTo('running')}>
|
|
266
|
+
<strong>{st.cost ? `$${st.cost.total.usd.toFixed(2)}` : '--'}</strong>
|
|
267
|
+
<span>cost · quota</span>
|
|
268
|
+
</button>
|
|
269
|
+
<button onClick={() => goTo('inventory')}>
|
|
270
|
+
<strong>{runtimeCoverage.host && runtimeCoverage.container && runtimeCoverage.vm ? '3/3' : `${[runtimeCoverage.host, runtimeCoverage.container, runtimeCoverage.vm].filter(Boolean).length}/3`}</strong>
|
|
271
|
+
<span>host · docker · vm</span>
|
|
272
|
+
</button>
|
|
273
|
+
</section>
|
|
274
|
+
|
|
275
|
+
<section className="control-planner" aria-label="Guided start and cost quota">
|
|
276
|
+
<div className="guided-start" aria-label="Guided start">
|
|
277
|
+
<div>
|
|
278
|
+
<p className="eyebrow">Guided start</p>
|
|
279
|
+
<h3>Pick stack, runtime, and task posture.</h3>
|
|
280
|
+
<p className="hint">Plan: {startPlan}</p>
|
|
281
|
+
</div>
|
|
282
|
+
<div className="planner-grid">
|
|
283
|
+
<label>
|
|
284
|
+
<span>Stack</span>
|
|
285
|
+
<select value={selectedStack?.id ?? ''} onChange={(event) => setSelectedStackId(event.target.value)}>
|
|
286
|
+
{runningInstances.map((i) => <option key={i.id} value={i.id}>{i.loadout} · {i.runtime_posture.kind}</option>)}
|
|
287
|
+
</select>
|
|
288
|
+
</label>
|
|
289
|
+
<label>
|
|
290
|
+
<span>Runtime</span>
|
|
291
|
+
<select value={selectedRuntime} onChange={(event) => setSelectedRuntime(event.target.value)}>
|
|
292
|
+
<option value="auto">Auto from selected stack</option>
|
|
293
|
+
<option value="host">Host</option>
|
|
294
|
+
<option value="container">Docker/container</option>
|
|
295
|
+
<option value="vm">VM</option>
|
|
296
|
+
</select>
|
|
297
|
+
</label>
|
|
298
|
+
<label>
|
|
299
|
+
<span>Task</span>
|
|
300
|
+
<select value={selectedTask} onChange={(event) => setSelectedTask(event.target.value)}>
|
|
301
|
+
<option value="observe">Observe first</option>
|
|
302
|
+
<option value="drive">Drive when granted</option>
|
|
303
|
+
<option value="handoff">Mission handoff</option>
|
|
304
|
+
</select>
|
|
305
|
+
</label>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="cta-row">
|
|
308
|
+
<button className="cta" onClick={onStartSession}>▸ Start planned session</button>
|
|
309
|
+
<button onClick={() => goTo('explore')}>Browse capabilities</button>
|
|
310
|
+
{st.approvals.length > 0 && <button onClick={() => goTo('approvals')}>Review {st.approvals.length} approval(s)</button>}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<aside className="quota-panel" aria-label="Cost and quota">
|
|
315
|
+
<p className="eyebrow">Cost & quota</p>
|
|
316
|
+
<div className="quota-donut" style={{ '--quota': `${quotaUsed}%` } as CSSProperties}>
|
|
317
|
+
<strong>{st.cost ? `$${totalCost.toFixed(2)}` : '--'}</strong>
|
|
318
|
+
<span>{st.cost ? `${st.cost.total.input_tokens + st.cost.total.output_tokens} tokens` : 'metrics unavailable'}</span>
|
|
319
|
+
</div>
|
|
320
|
+
<dl className="quota-readout">
|
|
321
|
+
<div><dt>Selected stack</dt><dd>{selectedStackCost ? `$${selectedStackCost.usd.toFixed(2)}` : 'n/a'}</dd></div>
|
|
322
|
+
<div><dt>Headroom</dt><dd>{st.cost ? (quotaUsed > 80 ? 'near limit' : 'clear') : 'unknown'}</dd></div>
|
|
323
|
+
<div><dt>Approval load</dt><dd>{st.approvals.length} pending</dd></div>
|
|
324
|
+
</dl>
|
|
325
|
+
</aside>
|
|
326
|
+
</section>
|
|
327
|
+
</>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{firstRun && (
|
|
331
|
+
<div className="card tour">
|
|
332
|
+
<h3>Three things to know</h3>
|
|
333
|
+
<ol>
|
|
334
|
+
<li><strong>Sessions</strong> are where work happens — attach to observe, take the wheel to drive. Output mirrors to every viewer.</li>
|
|
335
|
+
<li><strong>Actions</strong> and the <strong>+ capability picker</strong> drop a command into a session; the agent runs it. The Cockpit never runs the CLI.</li>
|
|
336
|
+
<li><strong>Explore</strong> browses the read-only AIWG catalog. Your own copied/imported assets live in your library — AIWG files are never overwritten.</li>
|
|
337
|
+
</ol>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function inventoryWarning(inv: InventoryEnvelope) {
|
|
345
|
+
const admin = inv.admin_error;
|
|
346
|
+
return [
|
|
347
|
+
inv.degraded_admin_inventory,
|
|
348
|
+
admin?.code,
|
|
349
|
+
admin?.title,
|
|
350
|
+
admin?.detail,
|
|
351
|
+
].filter(Boolean).join(' · ') || undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function runtimeFamily(instance: Instance): string {
|
|
355
|
+
if (instance.runtime_posture.kind === 'host') return 'terminal';
|
|
356
|
+
if (instance.runtime_posture.kind === 'vm' || instance.runtime_posture.kind === 'container' || instance.runtime_posture.kind === 'docker') return 'cube';
|
|
357
|
+
return 'window';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function accentFor(value: string): number {
|
|
361
|
+
let sum = 0;
|
|
362
|
+
for (const ch of value) sum += ch.charCodeAt(0);
|
|
363
|
+
return (sum % 4) + 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function orbitStyle(node: OrbitNode): OrbitStyle {
|
|
367
|
+
return { '--x': `${node.x}%`, '--y': `${node.y}%` };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function initialWallMode(): WallReviewMode {
|
|
371
|
+
const wall = new URLSearchParams(window.location.search).get('wall');
|
|
372
|
+
return wall === 'handoff' ? 'handoff' : 'topology';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildOrbitNodes(st: Status | null): OrbitNode[] {
|
|
376
|
+
const running = st?.instances.filter((i) => i.state === 'running') ?? [];
|
|
377
|
+
const host = st?.instances.find((i) => i.runtime_posture.kind === 'host');
|
|
378
|
+
const container = st?.instances.find((i) => i.runtime_posture.kind === 'container' || i.runtime_posture.kind === 'docker');
|
|
379
|
+
const vm = st?.instances.find((i) => i.runtime_posture.kind === 'vm');
|
|
380
|
+
const drive = st?.instances.some((i) => i.session_backends.some((b) => b.available && b.drive)) ?? false;
|
|
381
|
+
const approvals = st?.approvals.length ?? 0;
|
|
382
|
+
const cost = st?.cost?.total.usd;
|
|
383
|
+
const base: Omit<OrbitNode, 'x' | 'y'>[] = [
|
|
384
|
+
{
|
|
385
|
+
key: 'host',
|
|
386
|
+
label: host?.loadout || 'Host CLI',
|
|
387
|
+
meta: host ? `${fmtId(host.id)} · ${host.state}` : 'not connected',
|
|
388
|
+
family: 'host',
|
|
389
|
+
tab: 'inventory',
|
|
390
|
+
state: host ? 'running' : 'blocked',
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
key: 'vm',
|
|
394
|
+
label: vm?.loadout || 'VM sandbox',
|
|
395
|
+
meta: vm ? `${fmtId(vm.id)} · ${vm.state}` : 'not connected',
|
|
396
|
+
family: 'vm',
|
|
397
|
+
tab: 'inventory',
|
|
398
|
+
state: vm ? 'running' : 'blocked',
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
key: 'container',
|
|
402
|
+
label: container?.loadout || 'Docker runtime',
|
|
403
|
+
meta: container ? `${fmtId(container.id)} · ${container.state}` : 'not connected',
|
|
404
|
+
family: 'container',
|
|
405
|
+
tab: 'inventory',
|
|
406
|
+
state: container ? 'running' : 'blocked',
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
key: 'mission',
|
|
410
|
+
label: 'Mission handoff',
|
|
411
|
+
meta: st?.running[0] ? `${fmtId(st.running[0].task_id)} · ${st.running[0].state}` : 'ready',
|
|
412
|
+
family: 'mission',
|
|
413
|
+
tab: 'running',
|
|
414
|
+
state: st?.running.length ? 'running' : 'ready',
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
key: 'approvals',
|
|
418
|
+
label: 'Approvals',
|
|
419
|
+
meta: approvals ? `${approvals} pending` : 'clear',
|
|
420
|
+
family: 'approval',
|
|
421
|
+
tab: 'approvals',
|
|
422
|
+
state: approvals ? 'attention' : 'ready',
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
key: 'cost',
|
|
426
|
+
label: 'Cost & quota',
|
|
427
|
+
meta: cost === undefined ? 'metrics n/a' : `$${cost.toFixed(2)}`,
|
|
428
|
+
family: 'cost',
|
|
429
|
+
tab: 'running',
|
|
430
|
+
state: cost === undefined ? 'idle' : 'ready',
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
key: 'capabilities',
|
|
434
|
+
label: 'Capability search',
|
|
435
|
+
meta: 'catalog ready',
|
|
436
|
+
family: 'capability',
|
|
437
|
+
tab: 'explore',
|
|
438
|
+
state: 'ready',
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
key: 'library',
|
|
442
|
+
label: 'Library',
|
|
443
|
+
meta: 'workspace assets',
|
|
444
|
+
family: 'library',
|
|
445
|
+
tab: 'library',
|
|
446
|
+
state: 'ready',
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
key: 'actions',
|
|
450
|
+
label: 'Actions',
|
|
451
|
+
meta: drive ? 'drive available' : 'observe first',
|
|
452
|
+
family: 'action',
|
|
453
|
+
tab: 'actions',
|
|
454
|
+
state: drive ? 'ready' : 'idle',
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
key: 'sessions',
|
|
458
|
+
label: 'Sessions',
|
|
459
|
+
meta: running.length ? `${running.length} attachable` : 'none active',
|
|
460
|
+
family: 'session',
|
|
461
|
+
tab: 'sessions',
|
|
462
|
+
state: running.length ? 'running' : 'idle',
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
key: 'inventory',
|
|
466
|
+
label: 'Inventory',
|
|
467
|
+
meta: `${st?.instances.length ?? 0} registered`,
|
|
468
|
+
family: 'inventory',
|
|
469
|
+
tab: 'inventory',
|
|
470
|
+
state: st?.instances.length ? 'ready' : 'idle',
|
|
471
|
+
},
|
|
472
|
+
];
|
|
473
|
+
return base.map((node, index) => ({ ...node, x: ORBIT_POSITIONS[index][0], y: ORBIT_POSITIONS[index][1] }));
|
|
474
|
+
}
|
package/web/src/main.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import '@xterm/xterm/css/xterm.css';
|
|
4
|
+
import './styles.css';
|
|
5
|
+
import { App } from './App';
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<App />
|
|
10
|
+
</StrictMode>,
|
|
11
|
+
);
|