@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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
|
+
* https://github.com/chjj/term.js
|
|
5
|
+
* @license MIT
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in
|
|
15
|
+
* all copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
* THE SOFTWARE.
|
|
24
|
+
*
|
|
25
|
+
* Originally forked from (with the author's permission):
|
|
26
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
27
|
+
* http://bellard.org/jslinux/
|
|
28
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
29
|
+
* The original design remains. The terminal itself
|
|
30
|
+
* has been extended to include xterm CSI codes, among
|
|
31
|
+
* other features.
|
|
32
|
+
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}:root{color-scheme:dark;--bg:#0b1220;--panel:#111827;--glass:rgba(15,23,42,.82);--fg:#f8fafc;--muted:#cbd5e1;--line:#334155;--ok:#86efac;--warn:#fde68a;--idle:#94a3b8;--accent:#5eead4;--accent2:#93c5fd;--accent3:#c4b5fd;--accent4:#86efac;--err:#fda4af}*{box-sizing:border-box}body{margin:0;font:15px/1.5 system-ui,sans-serif;background:var(--bg);color:var(--fg);overflow-x:hidden}header{padding:12px 22px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:14px;flex-wrap:wrap;background:var(--panel);position:sticky;top:0;z-index:5}header h1{font-size:18px;margin:0;font-weight:650}header .mark{color:var(--accent);font-size:20px}header .meta{margin-left:0;color:var(--muted);font-size:13px}.brand-lockup{display:flex;align-items:center;gap:12px}.top-status{margin-left:auto;display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:var(--muted);font-size:12px}.top-status span{border:1px solid var(--line);border-radius:999px;padding:3px 8px;background:#0b1220b8}.health-pill.ok{color:var(--ok);border-color:#86efaca6}.health-pill.warn{color:var(--warn);border-color:#fde68aa6}.executor-pill{max-width:34ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.matrix-mini{white-space:nowrap}[role=tablist]{display:flex;gap:4px;padding:0 22px;border-bottom:1px solid var(--line);flex-wrap:wrap}[role=tab]{background:none;border:0;border-bottom:2px solid transparent;color:var(--muted);padding:10px 14px;cursor:pointer;font:inherit}[role=tab][aria-selected=true]{color:var(--fg);border-bottom-color:var(--accent)}[role=tab]:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}main{padding:22px}button,select,input{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-radius:7px;padding:7px 12px;font:inherit}button{cursor:pointer}button:hover:not(:disabled){border-color:var(--accent)}button:disabled{opacity:.45;cursor:not-allowed}button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:2px}table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden}caption{text-align:left;color:var(--muted);padding:10px 14px;font-size:13px}th,td{text-align:left;padding:11px 14px;border-bottom:1px solid var(--line)}th{color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em}tr:last-child td{border-bottom:0}code{font:13px ui-monospace,monospace;color:var(--muted)}.badge{display:inline-block;padding:2px 8px;border-radius:999px;border:1px solid var(--line);font-size:12px}.badge.controller{border-color:var(--ok);color:var(--ok)}.badge.observer,.badge.high{border-color:var(--accent);color:var(--accent)}.badge.isolation-least,.badge.trust-compatibility,.badge.trust-degraded,.badge.daemon-permission_denied{border-color:var(--err);color:var(--err)}.badge.isolation-shared-kernel,.badge.isolation-opaque,.badge.trust-local,.badge.daemon-degraded,.badge.daemon-stopped,.badge.daemon-unknown{border-color:var(--warn);color:var(--warn)}.badge.isolation-strong,.badge.trust-secure,.badge.daemon-available,.badge.daemon-detected{border-color:var(--ok);color:var(--ok)}.badge.isolation-unknown,.badge.trust-unknown,.badge.daemon-unavailable{border-color:var(--idle);color:var(--muted)}.cell-note{color:var(--muted);font-size:12px;margin-top:3px;max-width:260px}.state{display:inline-flex;align-items:center;gap:7px}.dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:var(--idle)}.state.running .dot,.state.working .dot{background:var(--ok)}.state.provisioning .dot{background:var(--warn)}.empty{color:var(--muted);padding:22px}.err{color:var(--err);padding:12px 0}.controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px}.controls label{color:var(--muted);font-size:13px}.manage-actions{white-space:nowrap;display:flex;gap:8px;align-items:center}.section-toolbar{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:14px}.section-toolbar h2{margin:0;font-size:20px}.empty-state{display:grid;gap:10px;justify-items:start;border:1px solid var(--line);border-radius:8px;padding:18px;background:var(--panel)}.empty-state h2{margin:0;font-size:20px}.terminal{background:#0a0c10;border:1px solid var(--line);border-radius:10px;padding:0;height:min(56vh,calc(100vh - 310px));min-height:320px;overflow:hidden}.terminal .xterm{height:100%;padding:8px}.terminal .xterm-viewport,.terminal .xterm-screen{height:100%}.terminal .xterm-viewport{overflow-y:auto}.inputrow{display:flex;gap:8px;margin-top:12px}.inputrow input{flex:1;font-family:ui-monospace,monospace}.hint{color:var(--muted);font-size:13px;margin:6px 0 0}.response-needed{display:grid;gap:10px;margin:14px 0 18px}.response-needed h2{margin:0;font-size:16px}.response-item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start;border:1px solid var(--warn);border-radius:8px;background:#fde68a14;padding:12px 14px}.response-item p{margin:5px 0;color:var(--fg);white-space:pre-wrap;overflow-wrap:anywhere}.response-item span{color:var(--muted);font-size:12px}.grid2{display:grid;grid-template-columns:minmax(260px,1fr) 1.4fr;gap:16px;align-items:start}.act{text-align:left}.action-cluster{display:inline-flex;align-items:stretch;gap:4px}.action-cluster .act{border-radius:7px 0 0 7px}.copy-command{border-radius:0 7px 7px 0;color:var(--muted);font-size:12px;padding-inline:9px}.capgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px}.capcard{display:block;text-align:left;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:11px 13px;cursor:pointer}.capcard:hover{border-color:var(--accent)}.capcard-head{display:flex;align-items:center;gap:8px}.capcard-head strong{font-size:14px}.capcard-score{margin-left:auto;color:var(--muted);font-size:12px}.capcard-cap{color:var(--muted);font-size:13px;margin-top:5px;line-height:1.4}.capcard-tags{margin-top:8px;display:flex;gap:5px;flex-wrap:wrap}.tag{font-size:11px;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:1px 7px}.picker{background:#0a0c10;border:1px solid var(--line);border-radius:10px;padding:12px;margin-bottom:12px}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:20;display:grid;place-items:center;padding:24px;background:#020617b8;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.modal{width:min(780px,calc(100vw - 48px));max-height:calc(100vh - 48px);overflow:auto;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:0 24px 70px #0000007a;padding:20px}.modal h2{margin:0 0 14px;font-size:20px}.form-grid{display:grid;grid-template-columns:minmax(120px,.28fr) minmax(0,1fr);gap:12px;align-items:center}.form-grid label{color:var(--muted);font-size:13px}.form-grid select,.form-grid input,.form-grid textarea{min-width:0;width:100%}.form-grid textarea{min-height:88px;resize:vertical;background:var(--panel);color:var(--fg);border:1px solid var(--line);border-radius:7px;padding:7px 12px;font:inherit}.check-row{display:flex;align-items:center;gap:9px;color:var(--fg)!important}.check-row input{width:auto}.ro{min-width:0;color:var(--fg);overflow-wrap:anywhere}.warn{color:var(--warn)}.ok-text{color:var(--ok)}.modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:18px}.welcome{max-width:none}.welcome h2{font-size:clamp(26px,3vw,42px);margin:4px 0 10px;line-height:1.08}.welcome .lead{color:var(--muted);font-size:16px;line-height:1.6;margin:0;max-width:680px}.welcome .lead strong{color:var(--fg)}.eyebrow{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;font-size:12px;font-weight:700;margin:0 0 6px}.statgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:18px}.stat{display:flex;flex-direction:column;align-items:flex-start;gap:2px;text-align:left;padding:14px 16px}.stat-n{font-size:26px;font-weight:680;color:var(--fg)}.stat-l{color:var(--muted);font-size:13px}.cta-row{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:22px}.cta{background:var(--accent);color:#0b0d12;border-color:var(--accent);font-weight:620;padding:9px 18px}.cta:hover:not(:disabled){filter:brightness(1.08)}.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin-bottom:16px}.card h3{margin:0 0 8px;font-size:15px}.card ol{margin:0;padding-left:20px;color:var(--muted);line-height:1.7}.card ol strong{color:var(--fg)}.warn-card{border-color:var(--warn)}.warn-card pre{margin:10px 0 0}.wall-hero{display:grid;grid-template-columns:minmax(280px,1fr) auto;gap:18px;align-items:end;margin:4px 0 18px}.wall-hero-compact{margin:0 0 10px;align-items:center}.wall-hero-compact h2{font-size:clamp(24px,2.4vw,34px);margin-bottom:6px}.wall-hero-compact .lead{font-size:14px;line-height:1.45;max-width:620px}.wall-hero-compact .hero-actions{align-items:center}.hero-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.segmented{display:inline-flex;gap:0;border:1px solid var(--line);border-radius:8px;overflow:hidden}.segmented button{border:0;border-radius:0;color:var(--muted);background:#0f172ab8}.segmented button[aria-pressed=true]{color:#0b0d12;background:var(--accent);font-weight:650}.operator-wall{min-height:calc(100vh - 130px)}.radial-wall{display:grid;grid-template-columns:1fr;gap:18px;align-items:stretch;margin:10px calc(50% - 50vw) 18px}.radial-map{position:relative;min-height:clamp(640px,calc(100vh - 260px),780px);border-block:1px solid rgba(147,197,253,.25);overflow:hidden;background:radial-gradient(circle at 50% 47%,rgba(94,234,212,.22),transparent 17%),radial-gradient(circle at 18% 62%,rgba(59,130,246,.14),transparent 20%),radial-gradient(circle at 82% 38%,rgba(124,58,237,.18),transparent 18%),linear-gradient(180deg,#0f172af2,#020617c2);box-shadow:inset 0 0 70px #020617b8}.radial-links{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;filter:drop-shadow(0 0 14px rgba(94,234,212,.34))}.link-ring{fill:none;stroke:#93c5fd2e;stroke-width:.35;stroke-dasharray:1.2 1.4}.link-ring-inner{stroke:#5eead424;stroke-dasharray:.7 1.8}.link-line{stroke:#5eead48a;stroke-width:.42}.link-ready,.link-idle{stroke:#93c5fd61}.link-attention{stroke:#fde68ac7}.link-blocked{stroke:#fda4af80;stroke-dasharray:1.4 1.2}.handoff-arc{fill:none;stroke:#c4b5fdf0;stroke-width:1.25;stroke-linecap:round;filter:drop-shadow(0 0 5px rgba(196,181,253,.72))}.mode-handoff .link-line{stroke:#93c5fd3d}.mode-handoff .handoff-arc{stroke-width:2.35;filter:drop-shadow(0 0 11px rgba(196,181,253,.9))}.central-hub{position:absolute;left:50%;top:45%;transform:translate(-50%,-50%);width:214px;min-height:178px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;text-align:center;border-radius:8px;border-color:#5eead4e6;background:linear-gradient(180deg,#5eead457,#0f172ae6);box-shadow:0 0 64px #5eead46b,inset 0 0 38px #93c5fd33;z-index:3}.mode-handoff .central-hub{border-color:#c4b5fde6;box-shadow:0 0 72px #c4b5fd6b,inset 0 0 38px #5eead429}.central-hub strong{font-size:24px}.central-hub span:last-child{color:var(--muted);font-size:12px}.hub-core{width:82px;height:52px;border:1px solid rgba(248,250,252,.78);border-radius:7px;transform:skew(-12deg);background:linear-gradient(135deg,#f8fafcdb,#5eead475);box-shadow:0 10px 32px #5eead459}.orbit-node{position:absolute;left:var(--x);top:var(--y);transform:translate(-50%,-50%);width:104px;min-height:106px;display:grid;grid-template-rows:72px auto;gap:4px;place-items:center;text-align:center;padding:8px 7px;border-radius:8px;background:radial-gradient(circle at 50% 38%,#f8fafc1c,#0f172a8f 54%,#0206176b);border-color:#93c5fd2e;box-shadow:0 20px 38px #00000057,inset 0 -18px 26px #3b82f60f;z-index:2}.orbit-node:hover:not(:disabled){transform:translate(-50%,-50%) translateY(-2px)}.orbit-node:focus-visible{outline-offset:4px}.node-copy{min-width:0;display:flex;flex-direction:column;gap:2px}.node-copy strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:94px;font-size:11px}.node-copy span{color:var(--muted);font-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:94px}.orbit-node:focus-visible .node-copy span,.orbit-node:hover .node-copy span{font-size:10px}.node-glyph{width:72px;height:62px;display:inline-block;border:2px solid var(--node-accent,var(--accent));background:#ffffff0a;box-shadow:0 0 30px color-mix(in srgb,var(--node-accent,var(--accent)) 48%,transparent)}.node-host,.node-session{--node-accent:var(--accent)}.node-container,.node-inventory{--node-accent:var(--accent2)}.node-vm,.node-mission{--node-accent:var(--accent3)}.node-approval.node-attention,.node-blocked{--node-accent:var(--warn)}.node-cost,.node-capability,.node-library,.node-action{--node-accent:var(--accent4)}.node-blocked{border-color:#fda4af99}.node-attention{border-color:#fde68ab8}.node-running{border-color:#86efacb8}.glyph-host,.glyph-session{border-radius:6px;box-shadow:inset 0 -10px #ffffff0f,0 0 18px #5eead447}.glyph-container,.glyph-inventory{border-radius:7px;transform:skew(-8deg)}.glyph-vm{border-radius:4px;transform:rotate(45deg) scale(.82)}.glyph-mission{border-radius:50%;position:relative}.glyph-mission:before,.glyph-mission:after{content:"";position:absolute;width:10px;height:10px;border:2px solid var(--node-accent);border-radius:50%;background:#0f172ae6}.glyph-mission:before{left:-7px;top:12px}.glyph-mission:after{right:-7px;top:12px}.glyph-approval{border-radius:18px 18px 6px 6px}.glyph-cost{border-radius:50%;border-style:dashed}.glyph-capability{border-radius:8px;background:repeating-linear-gradient(45deg,rgba(255,255,255,.08) 0 4px,transparent 4px 8px)}.glyph-library{border-radius:4px;box-shadow:inset 9px 0 #ffffff14,inset 18px 0 #ffffff0d}.glyph-action{border-radius:10px;transform:rotate(45deg) scale(.8)}.handoff-marker{position:absolute;right:-8px;top:50%;width:16px;height:16px;margin-top:-8px;border-radius:50%;background:var(--accent3);box-shadow:0 0 22px #c4b5fdd1}.mode-handoff .node-mission{border-color:#c4b5fdf2;box-shadow:0 22px 48px #7c3aed5c,inset 0 -18px 26px #c4b5fd1a}.mode-handoff .handoff-marker{width:22px;height:22px;right:-12px;margin-top:-11px}.handoff-callout{position:absolute;z-index:4;display:grid;gap:3px;padding:9px 11px;border:1px solid rgba(196,181,253,.54);border-radius:8px;background:#0f172ac2;box-shadow:0 0 28px #c4b5fd2e}.handoff-callout span{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.06em}.handoff-callout strong{font-size:13px}.handoff-source{left:20%;top:60%}.handoff-destination{right:14%;top:39%}.radial-overlay{position:absolute;left:22px;right:22px;bottom:18px;border:1px solid rgba(203,213,225,.16);border-radius:8px;background:#0f172aa8;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);padding:13px;display:grid;grid-template-columns:minmax(280px,.62fr) 1fr;gap:12px;z-index:4}.radial-overlay-head{display:grid;grid-template-columns:minmax(160px,1fr) minmax(220px,.8fr);gap:12px;align-items:end}.radial-overlay h3{margin:0;font-size:18px}.mission-route{display:grid;grid-template-columns:1fr auto auto;gap:8px;align-items:center;padding:9px;border:1px solid rgba(203,213,225,.16);border-radius:8px;background:#02061757}.mission-route span{color:var(--muted);font-size:12px}.mission-route strong{font-size:14px}.route-attention{color:var(--warn)!important}.route-ready{color:var(--ok)!important}.wall-readout{display:grid;grid-template-columns:repeat(5,minmax(90px,1fr));gap:8px;margin:0}.wall-readout div{display:grid;gap:4px;padding:8px 10px;border:1px solid rgba(203,213,225,.12);border-radius:8px;background:#02061752}.wall-readout div:last-child{border-bottom:1px solid rgba(203,213,225,.12)}.wall-readout dt{color:var(--muted);font-size:12px}.wall-readout dd{margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.stack-board{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px;margin:16px 0}.stack-card{position:relative;min-height:236px;display:grid;grid-template-rows:auto 1fr auto auto;gap:12px;background:linear-gradient(160deg,var(--glass),rgba(15,23,42,.58));border:1px solid var(--line);border-radius:8px;padding:16px;overflow:hidden;box-shadow:0 18px 45px #00000038}.stack-card:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;border-top:3px solid var(--stack-accent,var(--accent));pointer-events:none}.stack-card.accent-1{--stack-accent:var(--accent)}.stack-card.accent-2{--stack-accent:var(--accent2)}.stack-card.accent-3{--stack-accent:var(--accent3)}.stack-card.accent-4{--stack-accent:var(--accent4)}.stack-head{display:grid;grid-template-columns:42px minmax(0,1fr) auto;gap:12px;align-items:start}.stack-head h3{margin:0;font-size:16px}.stack-head p{margin:3px 0 0;color:var(--muted);font-size:12px}.topology{width:38px;height:38px;border:2px solid var(--stack-accent,var(--accent));background:#ffffff0a;display:inline-block}.topology-terminal{border-radius:6px;box-shadow:inset 0 -10px #ffffff0f}.topology-cube{transform:skew(-8deg);border-radius:7px}.topology-window{border-radius:10px 10px 4px 4px}.stack-log{min-height:92px;border:1px solid rgba(203,213,225,.18);background:#02061770;border-radius:8px;padding:10px;display:flex;flex-direction:column;gap:8px;color:var(--muted);font:12px/1.35 ui-monospace,monospace}.stack-log span:first-child{color:var(--fg)}.segment-progress{height:8px;border-radius:999px;background:#94a3b82e;overflow:hidden}.segment-progress span{display:block;height:100%;background:var(--stack-accent,var(--accent));border-radius:inherit}.transport-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.transport-bar span{margin-left:auto;color:var(--muted);font-size:13px}.telemetry-strip{display:grid;grid-template-columns:repeat(5,minmax(120px,1fr));gap:10px;margin:16px 0}.telemetry-strip button{min-height:82px;display:flex;flex-direction:column;align-items:flex-start;justify-content:center;text-align:left;gap:3px;background:#111827c7;border-radius:8px}.telemetry-strip strong{font-size:24px;color:var(--fg)}.telemetry-strip span{color:var(--muted);font-size:12px}.control-planner{display:grid;grid-template-columns:minmax(320px,1fr) minmax(260px,.36fr);gap:14px;margin-top:16px;align-items:stretch}.guided-start{display:grid;grid-template-columns:1fr;align-items:start;gap:14px;border:1px solid var(--line);background:#0f172a94;border-radius:8px;padding:16px}.guided-start h3{margin:0 0 4px;font-size:18px}.planner-grid{display:grid;grid-template-columns:repeat(3,minmax(160px,1fr));gap:10px}.planner-grid label{display:flex;flex-direction:column;gap:6px;color:var(--muted);font-size:12px}.planner-grid select{width:100%;min-height:38px}.quota-panel{border:1px solid var(--line);border-radius:8px;background:#0f172ab8;padding:16px;display:grid;gap:12px;align-content:start}.quota-donut{--quota:0%;width:148px;aspect-ratio:1;border-radius:50%;justify-self:center;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;background:conic-gradient(var(--accent) var(--quota),rgba(148,163,184,.18) 0);position:relative;color:var(--fg)}.quota-donut:before{content:"";position:absolute;top:13px;right:13px;bottom:13px;left:13px;border-radius:inherit;background:var(--panel);border:1px solid rgba(203,213,225,.16)}.quota-donut strong,.quota-donut span{position:relative;z-index:1}.quota-donut strong{font-size:23px}.quota-donut span{color:var(--muted);font-size:11px;max-width:96px;text-align:center}.quota-readout{margin:0;display:grid;gap:7px}.quota-readout div{display:grid;grid-template-columns:1fr auto;gap:10px;border-bottom:1px solid rgba(203,213,225,.13);padding-bottom:7px}.quota-readout div:last-child{border-bottom:0;padding-bottom:0}.quota-readout dt{color:var(--muted);font-size:12px}.quota-readout dd{margin:0}@media(max-width:820px){header{position:static}.top-status{width:100%;margin-left:0}.wall-hero,.guided-start,.radial-wall,.control-planner{grid-template-columns:1fr;align-items:start}.hero-actions{justify-content:flex-start}.telemetry-strip{grid-template-columns:repeat(2,minmax(120px,1fr))}.planner-grid{grid-template-columns:1fr}.radial-wall{margin-inline:0}.radial-map{min-height:0;display:grid;grid-template-columns:1fr;gap:8px;padding:12px}.radial-links,.central-hub .hub-core{display:none}.central-hub,.orbit-node{position:static;transform:none;width:100%;min-height:72px}.central-hub{margin-bottom:4px}.orbit-node:hover:not(:disabled){transform:none}.node-copy span{font-size:10px}.handoff-callout{position:static}.radial-overlay{position:static;grid-template-columns:1fr}.radial-overlay-head,.wall-readout{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>AIWG Cockpit</title>
|
|
7
|
+
<!-- The Bridge injects window.__COCKPIT_TOKEN__ at serve time. -->
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-B5anpdS1.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CP3BF6uZ.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root">Loading AIWG Cockpit…</div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/web/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>AIWG Cockpit</title>
|
|
7
|
+
<!-- The Bridge injects window.__COCKPIT_TOKEN__ at serve time. -->
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root">Loading AIWG Cockpit…</div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/web/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aiwg/cockpit-web",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "AIWG Cockpit web UI — React + Vite. Built output is served by the Bridge (token-injected). Not shipped in base aiwg.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "vitest run"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
16
|
+
"@xterm/xterm": "^5.5.0",
|
|
17
|
+
"react": "^19.2.0",
|
|
18
|
+
"react-dom": "^19.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@testing-library/dom": "^10.4.0",
|
|
22
|
+
"@testing-library/react": "^16.1.0",
|
|
23
|
+
"@types/react": "^19.2.0",
|
|
24
|
+
"@types/react-dom": "^19.2.0",
|
|
25
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
26
|
+
"jsdom": "^25.0.1",
|
|
27
|
+
"typescript": "^5.6.3",
|
|
28
|
+
"vite": "^6.0.7",
|
|
29
|
+
"vitest": "^2.1.8"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, cleanup, waitFor, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { App } from './App';
|
|
4
|
+
|
|
5
|
+
// Rendered-DOM coverage (the a11y assertions deferred from T2, and a guard against the
|
|
6
|
+
// "blank render" class of bug). The Welcome tab fetches inventory/running/approvals on
|
|
7
|
+
// mount, so fetch is stubbed.
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
(window as unknown as { __COCKPIT_TOKEN__: string }).__COCKPIT_TOKEN__ = 'test-token';
|
|
10
|
+
window.history.replaceState({}, '', '/');
|
|
11
|
+
globalThis.fetch = vi.fn(() => new Promise<Response>(() => undefined)) as typeof fetch;
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => { cleanup(); vi.restoreAllMocks(); });
|
|
14
|
+
|
|
15
|
+
const TAB_LABELS = ['Home', 'Inventory', 'Running', 'Sessions', 'Approvals', 'Explore', 'Library', 'Actions'];
|
|
16
|
+
|
|
17
|
+
describe('App shell (rendered DOM)', () => {
|
|
18
|
+
it('renders an ARIA tablist with all eight tabs', () => {
|
|
19
|
+
render(<App />);
|
|
20
|
+
expect(screen.getByRole('tablist', { name: /cockpit views/i })).toBeTruthy();
|
|
21
|
+
expect(screen.getAllByRole('tab')).toHaveLength(TAB_LABELS.length);
|
|
22
|
+
for (const label of TAB_LABELS) expect(screen.getByRole('tab', { name: label })).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('marks exactly one tab selected and the rest unselected', () => {
|
|
26
|
+
render(<App />);
|
|
27
|
+
const selected = screen.getAllByRole('tab').filter((t) => t.getAttribute('aria-selected') === 'true');
|
|
28
|
+
expect(selected).toHaveLength(1);
|
|
29
|
+
expect(selected[0]).toHaveProperty('id', 'tab-welcome');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('exposes the Start a session primary verb', () => {
|
|
33
|
+
render(<App />);
|
|
34
|
+
expect(screen.getAllByRole('button', { name: /start a session/i }).length).toBeGreaterThanOrEqual(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('exposes additive host/docker/vm instance launch', () => {
|
|
38
|
+
render(<App />);
|
|
39
|
+
fireEvent.click(screen.getByRole('button', { name: /launch instance/i }));
|
|
40
|
+
expect(screen.getByRole('dialog', { name: /new instance/i })).toBeTruthy();
|
|
41
|
+
expect(screen.getByRole('option', { name: 'Host' })).toBeTruthy();
|
|
42
|
+
expect(screen.getByRole('option', { name: 'Docker container' })).toBeTruthy();
|
|
43
|
+
expect(screen.getByRole('option', { name: 'VM / QEMU' })).toBeTruthy();
|
|
44
|
+
expect(screen.getByText(/existing instances and sessions keep running/i)).toBeTruthy();
|
|
45
|
+
expect(screen.getByText(/start a session automatically/i)).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renders the welcome heading and its tabpanel (not blank)', () => {
|
|
49
|
+
render(<App />);
|
|
50
|
+
expect(screen.getByRole('heading', { name: /work alongside your agents/i })).toBeTruthy();
|
|
51
|
+
const panel = document.getElementById('panel-welcome');
|
|
52
|
+
expect(panel?.hidden).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('surfaces persistent bridge and executor status in global chrome', async () => {
|
|
56
|
+
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
57
|
+
const url = String(input);
|
|
58
|
+
if (url.includes('/api/health')) return jsonResponse({ executor_url: 'http://127.0.0.1:8122' });
|
|
59
|
+
if (url.includes('/api/inventory')) return jsonResponse({ instances: [instance('host-1', 'host', 'Codex host')] });
|
|
60
|
+
if (url.includes('/api/running')) return jsonResponse({ count: 2, running: [] });
|
|
61
|
+
if (url.includes('/api/approvals')) return jsonResponse({ approvals: [{ id: 'approval-1', instance_id: 'host-1', prompt: 'Allow?', risk: 'medium', status: 'pending' }] });
|
|
62
|
+
if (url.includes('/api/cost')) return jsonResponse({ total: { input_tokens: 0, output_tokens: 0, usd: 0 }, per_instance: [] });
|
|
63
|
+
return jsonResponse({});
|
|
64
|
+
}) as typeof fetch;
|
|
65
|
+
|
|
66
|
+
render(<App />);
|
|
67
|
+
|
|
68
|
+
expect(await screen.findByText('Bridge live')).toBeTruthy();
|
|
69
|
+
expect(screen.getAllByText('http://127.0.0.1:8122').length).toBeGreaterThanOrEqual(1);
|
|
70
|
+
expect(screen.getByText('1 stacks')).toBeTruthy();
|
|
71
|
+
expect(screen.getByText('2 running')).toBeTruthy();
|
|
72
|
+
expect(screen.getByText('1 responses needed')).toBeTruthy();
|
|
73
|
+
expect(screen.getByText(/host ✓ · docker - · vm -/)).toBeTruthy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('stays connected when the executor exposes no running/approvals admin surface (#1638)', async () => {
|
|
77
|
+
// Real agentic-sandbox has no /running or /approvals admin route, so those
|
|
78
|
+
// Bridge endpoints error. Inventory + health succeed. The header must stay
|
|
79
|
+
// "Bridge live" and Home must bind inventory — not collapse to the
|
|
80
|
+
// "No stack connected" / "Bridge checking" empty state.
|
|
81
|
+
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
82
|
+
const url = String(input);
|
|
83
|
+
if (url.includes('/api/health')) return jsonResponse({ executor_url: 'http://127.0.0.1:8122' });
|
|
84
|
+
if (url.includes('/api/inventory')) return jsonResponse({ instances: [instance('host-1', 'host', 'Codex host')] });
|
|
85
|
+
if (url.includes('/api/running')) return errorResponse(502);
|
|
86
|
+
if (url.includes('/api/approvals')) return errorResponse(404);
|
|
87
|
+
if (url.includes('/api/cost')) return errorResponse(404);
|
|
88
|
+
return jsonResponse({});
|
|
89
|
+
}) as typeof fetch;
|
|
90
|
+
|
|
91
|
+
render(<App />);
|
|
92
|
+
|
|
93
|
+
// Header chrome stays live (enrichment failures degrade independently).
|
|
94
|
+
expect(await screen.findByText('Bridge live')).toBeTruthy();
|
|
95
|
+
expect(screen.getByText('1 stacks')).toBeTruthy();
|
|
96
|
+
expect(screen.getByText('0 running')).toBeTruthy();
|
|
97
|
+
expect(screen.getByText('0 responses needed')).toBeTruthy();
|
|
98
|
+
// Home binds inventory and renders the operator wall, not the empty state.
|
|
99
|
+
expect(await screen.findByRole('heading', { name: /eleven-stack operator wall/i })).toBeTruthy();
|
|
100
|
+
expect(screen.queryByText(/no stack connected/i)).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('renders the radial operator-wall topology from live status data', async () => {
|
|
104
|
+
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
105
|
+
const url = String(input);
|
|
106
|
+
if (url.includes('/api/health')) return jsonResponse({ executor_url: 'http://127.0.0.1:8122' });
|
|
107
|
+
if (url.includes('/api/inventory')) return jsonResponse({
|
|
108
|
+
instances: [
|
|
109
|
+
instance('host-1', 'host', 'Codex host'),
|
|
110
|
+
instance('docker-1', 'container', 'Docker Codex'),
|
|
111
|
+
instance('vm-1', 'vm', 'QEMU Codex'),
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
if (url.includes('/api/running')) return jsonResponse({ count: 1, running: [{ instance_id: 'host-1', task_id: 'task-abc', state: 'working', tenant: 'local' }] });
|
|
115
|
+
if (url.includes('/api/approvals')) return jsonResponse({ approvals: [{ id: 'approval-1', instance_id: 'host-1', prompt: 'Allow?', risk: 'medium', status: 'pending' }] });
|
|
116
|
+
if (url.includes('/api/cost')) return jsonResponse({ total: { input_tokens: 10, output_tokens: 20, usd: 0.42 }, per_instance: [] });
|
|
117
|
+
return jsonResponse({});
|
|
118
|
+
}) as typeof fetch;
|
|
119
|
+
|
|
120
|
+
render(<App />);
|
|
121
|
+
|
|
122
|
+
expect(await screen.findByRole('heading', { name: /eleven-stack operator wall/i })).toBeTruthy();
|
|
123
|
+
expect(screen.getByRole('button', { name: /start from cockpit command hub/i })).toBeTruthy();
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(document.querySelectorAll('.orbit-node')).toHaveLength(11);
|
|
126
|
+
});
|
|
127
|
+
expect(screen.getByRole('button', { name: /codex host/i })).toBeTruthy();
|
|
128
|
+
expect(screen.getByRole('button', { name: /docker codex/i })).toBeTruthy();
|
|
129
|
+
expect(screen.getByRole('button', { name: /qemu codex/i })).toBeTruthy();
|
|
130
|
+
expect(screen.getAllByText('3/3').length).toBeGreaterThanOrEqual(1);
|
|
131
|
+
expect(screen.getByRole('group', { name: /wall review mode/i })).toBeTruthy();
|
|
132
|
+
expect(screen.getByRole('button', { name: 'Topology' }).getAttribute('aria-pressed')).toBe('true');
|
|
133
|
+
fireEvent.click(screen.getByRole('button', { name: 'Handoff' }));
|
|
134
|
+
expect(screen.getByRole('button', { name: 'Handoff' }).getAttribute('aria-pressed')).toBe('true');
|
|
135
|
+
expect(screen.getByRole('heading', { name: /mission-handoff operator wall/i })).toBeTruthy();
|
|
136
|
+
expect(screen.getByText('Source')).toBeTruthy();
|
|
137
|
+
expect(screen.getByText('Destination')).toBeTruthy();
|
|
138
|
+
expect(screen.getByRole('region', { name: /guided start and cost quota/i })).toBeTruthy();
|
|
139
|
+
expect(screen.getByLabelText('Cost and quota')).toBeTruthy();
|
|
140
|
+
expect(screen.getByText(/codex host \/ auto runtime \/ observe/i)).toBeTruthy();
|
|
141
|
+
expect(screen.getAllByText('$0.42').length).toBeGreaterThanOrEqual(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('exposes copy-command affordances beside contributed actions', async () => {
|
|
145
|
+
const writeText = vi.fn();
|
|
146
|
+
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
|
|
147
|
+
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
148
|
+
const url = String(input);
|
|
149
|
+
if (url.includes('/api/health')) return jsonResponse({ executor_url: 'http://127.0.0.1:8122' });
|
|
150
|
+
if (url.includes('/api/inventory')) return jsonResponse({ instances: [instance('host-1', 'host', 'Codex host')] });
|
|
151
|
+
if (url.includes('/api/running')) return jsonResponse({ count: 0, running: [] });
|
|
152
|
+
if (url.includes('/api/approvals')) return jsonResponse({ approvals: [] });
|
|
153
|
+
if (url.includes('/api/cost')) return jsonResponse({ total: { input_tokens: 0, output_tokens: 0, usd: 0 }, per_instance: [] });
|
|
154
|
+
if (url.includes('/api/contributions')) return jsonResponse({
|
|
155
|
+
actions: [{
|
|
156
|
+
id: 'doctor',
|
|
157
|
+
title: 'Run doctor',
|
|
158
|
+
source: 'test',
|
|
159
|
+
inject: { command: 'aiwg doctor' },
|
|
160
|
+
}],
|
|
161
|
+
});
|
|
162
|
+
return jsonResponse({});
|
|
163
|
+
}) as typeof fetch;
|
|
164
|
+
|
|
165
|
+
render(<App />);
|
|
166
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Actions' }));
|
|
167
|
+
fireEvent.click(await screen.findByRole('button', { name: /copy CLI command for run doctor/i }));
|
|
168
|
+
|
|
169
|
+
expect(writeText).toHaveBeenCalledWith('aiwg doctor');
|
|
170
|
+
await waitFor(() => expect(screen.getByText(/copied "aiwg doctor"/i)).toBeTruthy());
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('can open the mission-handoff review layout from a shareable URL', async () => {
|
|
174
|
+
window.history.replaceState({}, '', '/?wall=handoff');
|
|
175
|
+
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
176
|
+
const url = String(input);
|
|
177
|
+
if (url.includes('/api/health')) return jsonResponse({ executor_url: 'http://127.0.0.1:8122' });
|
|
178
|
+
if (url.includes('/api/inventory')) return jsonResponse({
|
|
179
|
+
instances: [
|
|
180
|
+
instance('host-1', 'host', 'Codex host'),
|
|
181
|
+
instance('docker-1', 'container', 'Docker Codex'),
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
if (url.includes('/api/running')) return jsonResponse({ count: 1, running: [{ instance_id: 'host-1', task_id: 'task-abc', state: 'working', tenant: 'local' }] });
|
|
185
|
+
if (url.includes('/api/approvals')) return jsonResponse({ approvals: [] });
|
|
186
|
+
if (url.includes('/api/cost')) return jsonResponse({ total: { input_tokens: 0, output_tokens: 0, usd: 0 }, per_instance: [] });
|
|
187
|
+
return jsonResponse({});
|
|
188
|
+
}) as typeof fetch;
|
|
189
|
+
|
|
190
|
+
render(<App />);
|
|
191
|
+
|
|
192
|
+
expect(await screen.findByRole('heading', { name: /mission-handoff operator wall/i })).toBeTruthy();
|
|
193
|
+
expect(screen.getByRole('button', { name: 'Handoff' }).getAttribute('aria-pressed')).toBe('true');
|
|
194
|
+
expect(screen.getByText('Source')).toBeTruthy();
|
|
195
|
+
expect(screen.getByText('Destination')).toBeTruthy();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('each tab has a matching labelled tabpanel (controls/labelledby pairing)', () => {
|
|
199
|
+
render(<App />);
|
|
200
|
+
for (const tab of screen.getAllByRole('tab')) {
|
|
201
|
+
const panelId = tab.getAttribute('aria-controls')!;
|
|
202
|
+
const panel = document.getElementById(panelId);
|
|
203
|
+
expect(panel, `panel ${panelId} exists`).toBeTruthy();
|
|
204
|
+
expect(panel!.getAttribute('aria-labelledby')).toBe(tab.id);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
function jsonResponse(body: unknown): Response {
|
|
210
|
+
return new Response(JSON.stringify(body), {
|
|
211
|
+
status: 200,
|
|
212
|
+
headers: { 'Content-Type': 'application/json' },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function errorResponse(status: number): Response {
|
|
217
|
+
return new Response(JSON.stringify({ error: 'unavailable' }), {
|
|
218
|
+
status,
|
|
219
|
+
headers: { 'Content-Type': 'application/json' },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function instance(id: string, kind: string, loadout: string) {
|
|
224
|
+
return {
|
|
225
|
+
id,
|
|
226
|
+
runtime: kind,
|
|
227
|
+
loadout,
|
|
228
|
+
state: 'running',
|
|
229
|
+
tenant: 'local',
|
|
230
|
+
card_url: '',
|
|
231
|
+
runtime_posture: { kind, isolation: kind === 'vm' ? 'strong' : 'shared-kernel', label: kind },
|
|
232
|
+
host_daemon: { status: 'available' },
|
|
233
|
+
transport: { mode: 'mtls', trust: 'secure', label: 'mTLS', source: 'test' },
|
|
234
|
+
launch_context: {},
|
|
235
|
+
session_backends: [{ mode: 'managed', backend: 'tmux', available: true, observe: true, drive: true }],
|
|
236
|
+
};
|
|
237
|
+
}
|
package/web/src/App.tsx
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
2
|
+
import { useSession } from './useSession';
|
|
3
|
+
import { api } from './api';
|
|
4
|
+
import type { Approval, Instance, ResponseNeeded } from './types';
|
|
5
|
+
import { Welcome } from './components/Welcome';
|
|
6
|
+
import { Inventory } from './components/Inventory';
|
|
7
|
+
import { Running } from './components/Running';
|
|
8
|
+
import { Sessions } from './components/Sessions';
|
|
9
|
+
import { Approvals } from './components/Approvals';
|
|
10
|
+
import { Explore } from './components/Explore';
|
|
11
|
+
import { Library } from './components/Library';
|
|
12
|
+
import { Actions } from './components/Actions';
|
|
13
|
+
import { StartSessionModal } from './components/StartSessionModal';
|
|
14
|
+
import { LaunchInstanceModal } from './components/LaunchInstanceModal';
|
|
15
|
+
|
|
16
|
+
const TABS = [
|
|
17
|
+
{ id: 'welcome', label: 'Home' },
|
|
18
|
+
{ id: 'inventory', label: 'Inventory' },
|
|
19
|
+
{ id: 'running', label: 'Running' },
|
|
20
|
+
{ id: 'sessions', label: 'Sessions' },
|
|
21
|
+
{ id: 'approvals', label: 'Approvals' },
|
|
22
|
+
{ id: 'explore', label: 'Explore' },
|
|
23
|
+
{ id: 'library', label: 'Library' },
|
|
24
|
+
{ id: 'actions', label: 'Actions' },
|
|
25
|
+
] as const;
|
|
26
|
+
type TabId = (typeof TABS)[number]['id'];
|
|
27
|
+
interface ChromeStatus {
|
|
28
|
+
executor: string;
|
|
29
|
+
instances: number;
|
|
30
|
+
running: number;
|
|
31
|
+
responses: number;
|
|
32
|
+
host: boolean;
|
|
33
|
+
container: boolean;
|
|
34
|
+
vm: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
38
|
+
|
|
39
|
+
export function App() {
|
|
40
|
+
const [tab, setTab] = useState<TabId>('welcome');
|
|
41
|
+
const session = useSession();
|
|
42
|
+
const [composer, setComposer] = useState('');
|
|
43
|
+
const [chrome, setChrome] = useState<ChromeStatus | null>(null);
|
|
44
|
+
const [startOpen, setStartOpen] = useState(false);
|
|
45
|
+
const [startInst, setStartInst] = useState<string | undefined>(undefined);
|
|
46
|
+
const [launchOpen, setLaunchOpen] = useState(false);
|
|
47
|
+
const [refreshTick, setRefreshTick] = useState(0);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
const load = async () => {
|
|
52
|
+
try {
|
|
53
|
+
// Health + inventory decide "Bridge live". Running + approvals are
|
|
54
|
+
// enrichment that a real executor may not expose (#1638) — degrade each
|
|
55
|
+
// independently so the header stays live instead of "Bridge checking".
|
|
56
|
+
const [health, inv] = await Promise.all([
|
|
57
|
+
api<{ executor_url: string }>('/api/health'),
|
|
58
|
+
api<{ instances: Instance[] }>('/api/inventory'),
|
|
59
|
+
]);
|
|
60
|
+
if (cancelled) return;
|
|
61
|
+
const [run, apr] = await Promise.all([
|
|
62
|
+
api<{ count: number }>('/api/running').catch(() => ({ count: 0 })),
|
|
63
|
+
api<{ approvals: Approval[] }>('/api/approvals?status=pending').catch(() => ({ approvals: [] as Approval[] })),
|
|
64
|
+
]);
|
|
65
|
+
if (cancelled) return;
|
|
66
|
+
const kinds = inv.instances.map((i) => i.runtime_posture.kind);
|
|
67
|
+
setChrome({
|
|
68
|
+
executor: health.executor_url,
|
|
69
|
+
instances: inv.instances.length,
|
|
70
|
+
running: run.count,
|
|
71
|
+
responses: apr.approvals.length + (session.responseNeeded.needed ? 1 : 0),
|
|
72
|
+
host: kinds.includes('host'),
|
|
73
|
+
container: kinds.includes('container') || kinds.includes('docker'),
|
|
74
|
+
vm: kinds.includes('vm'),
|
|
75
|
+
});
|
|
76
|
+
} catch {
|
|
77
|
+
if (!cancelled) setChrome(null);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
load();
|
|
81
|
+
const timer = window.setInterval(load, 15_000);
|
|
82
|
+
return () => { cancelled = true; window.clearInterval(timer); };
|
|
83
|
+
}, [session.responseNeeded.needed, refreshTick]);
|
|
84
|
+
|
|
85
|
+
// The onboarding primary verb: open the start-session picker (#1640/#1641). The picker
|
|
86
|
+
// is the single home for both this dashboard verb and the Sessions-tab Start button —
|
|
87
|
+
// neither launches blind with defaults, neither silently clobbers an attached session,
|
|
88
|
+
// and failures render inline in the picker instead of an alert().
|
|
89
|
+
const requestStart = (instanceId?: string) => { setStartInst(instanceId); setStartOpen(true); };
|
|
90
|
+
const handleLaunched = async (instanceId?: string, openSession?: boolean, operationId?: string) => {
|
|
91
|
+
setRefreshTick((t) => t + 1);
|
|
92
|
+
if (openSession) {
|
|
93
|
+
const inst = await waitForSessionReady(instanceId, operationId);
|
|
94
|
+
const backend = inst.session_backends.find((b) => b.available !== false && b.drive !== false)
|
|
95
|
+
?? inst.session_backends.find((b) => b.available !== false)
|
|
96
|
+
?? inst.session_backends[0];
|
|
97
|
+
if (!backend || backend.available === false) throw new Error(backend?.reason ?? 'No available session backend for the new instance.');
|
|
98
|
+
const qs = new URLSearchParams({ mode: backend.mode, backend: backend.backend });
|
|
99
|
+
const s = await api<{ id: string; attach_url: string }>(
|
|
100
|
+
`/api/instances/${encodeURIComponent(inst.id)}/sessions?${qs}`, { method: 'POST' },
|
|
101
|
+
);
|
|
102
|
+
session.attach(s.attach_url, false, backend.drive === false ? 'observer' : 'controller');
|
|
103
|
+
setTab('sessions');
|
|
104
|
+
} else {
|
|
105
|
+
setTab('inventory');
|
|
106
|
+
}
|
|
107
|
+
setRefreshTick((t) => t + 1);
|
|
108
|
+
};
|
|
109
|
+
const copyLaunchCommand = async () => {
|
|
110
|
+
await navigator.clipboard?.writeText('aiwg cockpit');
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<header>
|
|
116
|
+
<div className="brand-lockup">
|
|
117
|
+
<span className="mark" aria-hidden="true">◆</span>
|
|
118
|
+
<h1>AIWG Cockpit</h1>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="top-status" aria-label="Cockpit status">
|
|
121
|
+
<span className={`health-pill ${chrome ? 'ok' : 'warn'}`}>{chrome ? 'Bridge live' : 'Bridge checking'}</span>
|
|
122
|
+
{chrome && (
|
|
123
|
+
<>
|
|
124
|
+
<span className="executor-pill" title={chrome.executor}>{chrome.executor}</span>
|
|
125
|
+
<span>{chrome.instances} stacks</span>
|
|
126
|
+
<span>{chrome.running} running</span>
|
|
127
|
+
<span>{chrome.responses} responses needed</span>
|
|
128
|
+
<span className="matrix-mini" title="Runtime target coverage">host {chrome.host ? '✓' : '-'} · docker {chrome.container ? '✓' : '-'} · vm {chrome.vm ? '✓' : '-'}</span>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
<button className="meta" onClick={() => setLaunchOpen(true)}>+ Launch instance</button>
|
|
133
|
+
<button className="meta" onClick={() => requestStart()}>▸ Start a session</button>
|
|
134
|
+
<button className="meta" onClick={copyLaunchCommand}>Copy CLI</button>
|
|
135
|
+
</header>
|
|
136
|
+
<div role="tablist" aria-label="Cockpit views">
|
|
137
|
+
{TABS.map((t) => (
|
|
138
|
+
<button key={t.id} role="tab" id={`tab-${t.id}`} aria-controls={`panel-${t.id}`}
|
|
139
|
+
aria-selected={tab === t.id} tabIndex={tab === t.id ? 0 : -1} onClick={() => setTab(t.id)}>
|
|
140
|
+
{t.label}
|
|
141
|
+
</button>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
<main>
|
|
145
|
+
<Panel id="welcome" tab={tab}><Welcome onStartSession={() => requestStart()} onLaunchInstance={() => setLaunchOpen(true)} goTo={(t) => setTab(t as TabId)} /></Panel>
|
|
146
|
+
<Panel id="inventory" tab={tab}><Inventory onStartSession={requestStart} onLaunchInstance={() => setLaunchOpen(true)} /></Panel>
|
|
147
|
+
<Panel id="running" tab={tab}><Running /></Panel>
|
|
148
|
+
{/* Sessions stays mounted so the WebSocket survives tab switches */}
|
|
149
|
+
<section id="panel-sessions" role="tabpanel" aria-labelledby="tab-sessions" hidden={tab !== 'sessions'}>
|
|
150
|
+
<Sessions session={session} composer={composer} setComposer={setComposer} onRequestStart={requestStart} />
|
|
151
|
+
</section>
|
|
152
|
+
<Panel id="approvals" tab={tab}><Approvals responses={session.responseNeeded.needed ? [sessionResponse(session)] : []} goSessions={() => setTab('sessions')} /></Panel>
|
|
153
|
+
<Panel id="explore" tab={tab}><Explore /></Panel>
|
|
154
|
+
<Panel id="library" tab={tab}>
|
|
155
|
+
<Library session={session} setComposer={setComposer} goSessions={() => setTab('sessions')} />
|
|
156
|
+
</Panel>
|
|
157
|
+
<Panel id="actions" tab={tab}>
|
|
158
|
+
<Actions session={session} setComposer={setComposer} goSessions={() => setTab('sessions')} />
|
|
159
|
+
</Panel>
|
|
160
|
+
</main>
|
|
161
|
+
<StartSessionModal
|
|
162
|
+
open={startOpen}
|
|
163
|
+
onClose={() => setStartOpen(false)}
|
|
164
|
+
session={session}
|
|
165
|
+
onStarted={() => setTab('sessions')}
|
|
166
|
+
initialInstanceId={startInst}
|
|
167
|
+
/>
|
|
168
|
+
<LaunchInstanceModal
|
|
169
|
+
open={launchOpen}
|
|
170
|
+
onClose={() => setLaunchOpen(false)}
|
|
171
|
+
onLaunched={handleLaunched}
|
|
172
|
+
/>
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface OperationStatus {
|
|
178
|
+
id?: string;
|
|
179
|
+
state?: string;
|
|
180
|
+
result?: { instance_id?: string; instanceId?: string; container_id?: string; runtime?: string };
|
|
181
|
+
failure?: { message?: string; detail?: string; code?: string };
|
|
182
|
+
error?: { message?: string; detail?: string; code?: string } | string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function waitForSessionReady(instanceId?: string, operationId?: string) {
|
|
186
|
+
let last = '';
|
|
187
|
+
let operationDetail = '';
|
|
188
|
+
for (let i = 0; i < 45; i += 1) {
|
|
189
|
+
if (operationId) {
|
|
190
|
+
const op = await api<OperationStatus>(`/api/operations/${encodeURIComponent(operationId)}`);
|
|
191
|
+
const state = String(op.state ?? '').toLowerCase();
|
|
192
|
+
const opInstance = op.result?.instance_id ?? op.result?.instanceId;
|
|
193
|
+
if (opInstance && !instanceId) instanceId = opInstance;
|
|
194
|
+
if (state === 'failed') throw new Error(operationFailure(op));
|
|
195
|
+
if (state) {
|
|
196
|
+
operationDetail = operationSummary(op);
|
|
197
|
+
last = operationDetail;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const inv = await api<{ instances: Instance[] }>('/api/inventory');
|
|
201
|
+
const candidates = inv.instances.filter((inst) => String(inst.state).toLowerCase() === 'running');
|
|
202
|
+
const selected = instanceId
|
|
203
|
+
? candidates.find((inst) => inst.id === instanceId)
|
|
204
|
+
: candidates[0];
|
|
205
|
+
if (selected) {
|
|
206
|
+
const backend = selected.session_backends.find((b) => b.available !== false) ?? selected.session_backends[0];
|
|
207
|
+
if (backend && backend.available !== false) return selected;
|
|
208
|
+
last = backend?.reason ?? `instance ${selected.id} has no available backend yet`;
|
|
209
|
+
} else {
|
|
210
|
+
last = [
|
|
211
|
+
operationDetail,
|
|
212
|
+
instanceId ? `instance ${instanceId} not visible in inventory yet` : 'no running instance visible in inventory yet',
|
|
213
|
+
].filter(Boolean).join('; ');
|
|
214
|
+
}
|
|
215
|
+
await sleep(1_000);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Instance launched, but no session-ready agent appeared within 45s (${last}).`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function operationFailure(op: OperationStatus) {
|
|
221
|
+
if (typeof op.error === 'string') return op.error;
|
|
222
|
+
return op.failure?.message ?? op.failure?.detail ?? op.failure?.code
|
|
223
|
+
?? op.error?.message ?? op.error?.detail ?? op.error?.code
|
|
224
|
+
?? `operation ${op.id ?? ''} failed`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function operationSummary(op: OperationStatus) {
|
|
228
|
+
const parts = [`operation ${op.id ?? ''} is ${op.state ?? 'unknown'}`];
|
|
229
|
+
if (op.result?.runtime) parts.push(`runtime ${op.result.runtime}`);
|
|
230
|
+
if (op.result?.container_id) parts.push(`container ${op.result.container_id.slice(0, 12)}`);
|
|
231
|
+
if (op.result?.instance_id ?? op.result?.instanceId) parts.push(`instance ${op.result.instance_id ?? op.result.instanceId}`);
|
|
232
|
+
if (String(op.state ?? '').toLowerCase() === 'succeeded' && op.result?.container_id) {
|
|
233
|
+
parts.push('container was created, but no agent registered; see agentic-sandbox #501');
|
|
234
|
+
}
|
|
235
|
+
return parts.join('; ');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function sessionResponse(session: ReturnType<typeof useSession>): ResponseNeeded {
|
|
239
|
+
return {
|
|
240
|
+
id: `pty:${session.state.url ?? 'attached'}`,
|
|
241
|
+
instance_id: 'attached session',
|
|
242
|
+
prompt: session.responseNeeded.prompt,
|
|
243
|
+
source: session.responseNeeded.source,
|
|
244
|
+
status: 'response-needed',
|
|
245
|
+
attach_url: session.state.url,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function Panel({ id, tab, children }: { id: string; tab: string; children: ReactNode }) {
|
|
250
|
+
return (
|
|
251
|
+
<section id={`panel-${id}`} role="tabpanel" aria-labelledby={`tab-${id}`} hidden={tab !== id}>
|
|
252
|
+
{tab === id ? children : null}
|
|
253
|
+
</section>
|
|
254
|
+
);
|
|
255
|
+
}
|