@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,254 @@
|
|
|
1
|
+
: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; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin:0; font:15px/1.5 system-ui, sans-serif; background:var(--bg); color:var(--fg); overflow-x:hidden; }
|
|
4
|
+
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; }
|
|
5
|
+
header h1 { font-size:18px; margin:0; font-weight:650; }
|
|
6
|
+
header .mark { color:var(--accent); font-size:20px; }
|
|
7
|
+
header .meta { margin-left:0; color:var(--muted); font-size:13px; }
|
|
8
|
+
.brand-lockup { display:flex; align-items:center; gap:12px; }
|
|
9
|
+
.top-status { margin-left:auto; display:flex; align-items:center; gap:8px; flex-wrap:wrap; color:var(--muted); font-size:12px; }
|
|
10
|
+
.top-status span { border:1px solid var(--line); border-radius:999px; padding:3px 8px; background:rgba(11,18,32,.72); }
|
|
11
|
+
.health-pill.ok { color:var(--ok); border-color:rgba(134,239,172,.65); }
|
|
12
|
+
.health-pill.warn { color:var(--warn); border-color:rgba(253,230,138,.65); }
|
|
13
|
+
.executor-pill { max-width:34ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
14
|
+
.matrix-mini { white-space:nowrap; }
|
|
15
|
+
[role=tablist] { display:flex; gap:4px; padding:0 22px; border-bottom:1px solid var(--line); flex-wrap:wrap; }
|
|
16
|
+
[role=tab] { background:none; border:0; border-bottom:2px solid transparent; color:var(--muted); padding:10px 14px; cursor:pointer; font:inherit; }
|
|
17
|
+
[role=tab][aria-selected=true] { color:var(--fg); border-bottom-color:var(--accent); }
|
|
18
|
+
[role=tab]:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
|
|
19
|
+
main { padding:22px; }
|
|
20
|
+
button, select, input { background:var(--panel); color:var(--fg); border:1px solid var(--line); border-radius:7px; padding:7px 12px; font:inherit; }
|
|
21
|
+
button { cursor:pointer; }
|
|
22
|
+
button:hover:not(:disabled) { border-color:var(--accent); }
|
|
23
|
+
button:disabled { opacity:.45; cursor:not-allowed; }
|
|
24
|
+
button:focus-visible, select:focus-visible, input:focus-visible, textarea:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
|
25
|
+
table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--line); border-radius:10px; overflow:hidden; }
|
|
26
|
+
caption { text-align:left; color:var(--muted); padding:10px 14px; font-size:13px; }
|
|
27
|
+
th, td { text-align:left; padding:11px 14px; border-bottom:1px solid var(--line); }
|
|
28
|
+
th { color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
|
|
29
|
+
tr:last-child td { border-bottom:0; }
|
|
30
|
+
code { font:13px ui-monospace, monospace; color:var(--muted); }
|
|
31
|
+
.badge { display:inline-block; padding:2px 8px; border-radius:999px; border:1px solid var(--line); font-size:12px; }
|
|
32
|
+
.badge.controller { border-color:var(--ok); color:var(--ok); }
|
|
33
|
+
.badge.observer { border-color:var(--accent); color:var(--accent); }
|
|
34
|
+
.badge.high { border-color:var(--accent); color:var(--accent); }
|
|
35
|
+
.badge.isolation-least, .badge.trust-compatibility, .badge.trust-degraded, .badge.daemon-permission_denied { border-color:var(--err); color:var(--err); }
|
|
36
|
+
.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); }
|
|
37
|
+
.badge.isolation-strong, .badge.trust-secure, .badge.daemon-available, .badge.daemon-detected { border-color:var(--ok); color:var(--ok); }
|
|
38
|
+
.badge.isolation-unknown, .badge.trust-unknown, .badge.daemon-unavailable { border-color:var(--idle); color:var(--muted); }
|
|
39
|
+
.cell-note { color:var(--muted); font-size:12px; margin-top:3px; max-width:260px; }
|
|
40
|
+
.state { display:inline-flex; align-items:center; gap:7px; }
|
|
41
|
+
.dot { width:8px; height:8px; border-radius:50%; flex:0 0 auto; background:var(--idle); }
|
|
42
|
+
.state.running .dot, .state.working .dot { background:var(--ok); } .state.provisioning .dot { background:var(--warn); }
|
|
43
|
+
.empty { color:var(--muted); padding:22px; } .err { color:var(--err); padding:12px 0; }
|
|
44
|
+
.controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:14px; }
|
|
45
|
+
.controls label { color:var(--muted); font-size:13px; }
|
|
46
|
+
.manage-actions { white-space:nowrap; display:flex; gap:8px; align-items:center; }
|
|
47
|
+
.section-toolbar { display:flex; align-items:flex-end; justify-content:space-between; gap:14px; flex-wrap:wrap; margin-bottom:14px; }
|
|
48
|
+
.section-toolbar h2 { margin:0; font-size:20px; }
|
|
49
|
+
.empty-state { display:grid; gap:10px; justify-items:start; border:1px solid var(--line); border-radius:8px; padding:18px; background:var(--panel); }
|
|
50
|
+
.empty-state h2 { margin:0; font-size:20px; }
|
|
51
|
+
/* Host for the xterm.js terminal — fixed height so FitAddon can size the PTY; xterm owns
|
|
52
|
+
its own scrollback and wrapping, so no overflow/white-space rules belong here. */
|
|
53
|
+
.terminal { background:#0a0c10; border:1px solid var(--line); border-radius:10px; padding:0; height:min(56vh, calc(100vh - 310px)); min-height:320px; overflow:hidden; }
|
|
54
|
+
.terminal .xterm { height:100%; padding:8px; }
|
|
55
|
+
.terminal .xterm-viewport, .terminal .xterm-screen { height:100%; }
|
|
56
|
+
.terminal .xterm-viewport { overflow-y:auto; }
|
|
57
|
+
.inputrow { display:flex; gap:8px; margin-top:12px; }
|
|
58
|
+
.inputrow input { flex:1; font-family:ui-monospace, monospace; }
|
|
59
|
+
.hint { color:var(--muted); font-size:13px; margin:6px 0 0; }
|
|
60
|
+
.response-needed { display:grid; gap:10px; margin:14px 0 18px; }
|
|
61
|
+
.response-needed h2 { margin:0; font-size:16px; }
|
|
62
|
+
.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:rgba(253,230,138,.08); padding:12px 14px; }
|
|
63
|
+
.response-item p { margin:5px 0; color:var(--fg); white-space:pre-wrap; overflow-wrap:anywhere; }
|
|
64
|
+
.response-item span { color:var(--muted); font-size:12px; }
|
|
65
|
+
.grid2 { display:grid; grid-template-columns:minmax(260px,1fr) 1.4fr; gap:16px; align-items:start; }
|
|
66
|
+
.act { text-align:left; }
|
|
67
|
+
.action-cluster { display:inline-flex; align-items:stretch; gap:4px; }
|
|
68
|
+
.action-cluster .act { border-radius:7px 0 0 7px; }
|
|
69
|
+
.copy-command { border-radius:0 7px 7px 0; color:var(--muted); font-size:12px; padding-inline:9px; }
|
|
70
|
+
/* Tenor-style capability grid (modeled on fortemi-react SearchResults) */
|
|
71
|
+
.capgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:10px; }
|
|
72
|
+
.capcard { display:block; text-align:left; background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:11px 13px; cursor:pointer; }
|
|
73
|
+
.capcard:hover { border-color:var(--accent); }
|
|
74
|
+
.capcard-head { display:flex; align-items:center; gap:8px; }
|
|
75
|
+
.capcard-head strong { font-size:14px; }
|
|
76
|
+
.capcard-score { margin-left:auto; color:var(--muted); font-size:12px; }
|
|
77
|
+
.capcard-cap { color:var(--muted); font-size:13px; margin-top:5px; line-height:1.4; }
|
|
78
|
+
.capcard-tags { margin-top:8px; display:flex; gap:5px; flex-wrap:wrap; }
|
|
79
|
+
.tag { font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:1px 7px; }
|
|
80
|
+
.picker { background:#0a0c10; border:1px solid var(--line); border-radius:10px; padding:12px; margin-bottom:12px; }
|
|
81
|
+
.modal-backdrop { position:fixed; inset:0; z-index:20; display:grid; place-items:center; padding:24px; background:rgba(2,6,23,.72); backdrop-filter:blur(5px); }
|
|
82
|
+
.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 rgba(0,0,0,.48); padding:20px; }
|
|
83
|
+
.modal h2 { margin:0 0 14px; font-size:20px; }
|
|
84
|
+
.form-grid { display:grid; grid-template-columns:minmax(120px,.28fr) minmax(0,1fr); gap:12px; align-items:center; }
|
|
85
|
+
.form-grid label { color:var(--muted); font-size:13px; }
|
|
86
|
+
.form-grid select, .form-grid input, .form-grid textarea { min-width:0; width:100%; }
|
|
87
|
+
.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; }
|
|
88
|
+
.check-row { display:flex; align-items:center; gap:9px; color:var(--fg) !important; }
|
|
89
|
+
.check-row input { width:auto; }
|
|
90
|
+
.ro { min-width:0; color:var(--fg); overflow-wrap:anywhere; }
|
|
91
|
+
.warn { color:var(--warn); }
|
|
92
|
+
.ok-text { color:var(--ok); }
|
|
93
|
+
.modal-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:18px; }
|
|
94
|
+
/* Welcome / onboarding */
|
|
95
|
+
.welcome { max-width:none; }
|
|
96
|
+
.welcome h2 { font-size:clamp(26px, 3vw, 42px); margin:4px 0 10px; line-height:1.08; }
|
|
97
|
+
.welcome .lead { color:var(--muted); font-size:16px; line-height:1.6; margin:0; max-width:680px; }
|
|
98
|
+
.welcome .lead strong { color:var(--fg); }
|
|
99
|
+
.eyebrow { color:var(--accent); text-transform:uppercase; letter-spacing:.08em; font-size:12px; font-weight:700; margin:0 0 6px; }
|
|
100
|
+
.statgrid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin-bottom:18px; }
|
|
101
|
+
.stat { display:flex; flex-direction:column; align-items:flex-start; gap:2px; text-align:left; padding:14px 16px; }
|
|
102
|
+
.stat-n { font-size:26px; font-weight:680; color:var(--fg); }
|
|
103
|
+
.stat-l { color:var(--muted); font-size:13px; }
|
|
104
|
+
.cta-row { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:22px; }
|
|
105
|
+
.cta { background:var(--accent); color:#0b0d12; border-color:var(--accent); font-weight:620; padding:9px 18px; }
|
|
106
|
+
.cta:hover:not(:disabled) { filter:brightness(1.08); }
|
|
107
|
+
.card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:16px 18px; margin-bottom:16px; }
|
|
108
|
+
.card h3 { margin:0 0 8px; font-size:15px; }
|
|
109
|
+
.card ol { margin:0; padding-left:20px; color:var(--muted); line-height:1.7; }
|
|
110
|
+
.card ol strong { color:var(--fg); }
|
|
111
|
+
.warn-card { border-color:var(--warn); }
|
|
112
|
+
.warn-card pre { margin:10px 0 0; }
|
|
113
|
+
.wall-hero { display:grid; grid-template-columns:minmax(280px,1fr) auto; gap:18px; align-items:end; margin:4px 0 18px; }
|
|
114
|
+
.wall-hero-compact { margin:0 0 10px; align-items:center; }
|
|
115
|
+
.wall-hero-compact h2 { font-size:clamp(24px, 2.4vw, 34px); margin-bottom:6px; }
|
|
116
|
+
.wall-hero-compact .lead { font-size:14px; line-height:1.45; max-width:620px; }
|
|
117
|
+
.wall-hero-compact .hero-actions { align-items:center; }
|
|
118
|
+
.hero-actions { display:flex; gap:10px; flex-wrap:wrap; justify-content:flex-end; }
|
|
119
|
+
.segmented { display:inline-flex; gap:0; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
|
|
120
|
+
.segmented button { border:0; border-radius:0; color:var(--muted); background:rgba(15,23,42,.72); }
|
|
121
|
+
.segmented button[aria-pressed=true] { color:#0b0d12; background:var(--accent); font-weight:650; }
|
|
122
|
+
.operator-wall { min-height:calc(100vh - 130px); }
|
|
123
|
+
.radial-wall { display:grid; grid-template-columns:1fr; gap:18px; align-items:stretch; margin:10px calc(50% - 50vw) 18px; }
|
|
124
|
+
.radial-map { position:relative; min-height:clamp(640px, calc(100vh - 260px), 780px); border-block:1px solid rgba(147,197,253,.25); overflow:hidden; background:
|
|
125
|
+
radial-gradient(circle at 50% 47%, rgba(94,234,212,.22), transparent 17%),
|
|
126
|
+
radial-gradient(circle at 18% 62%, rgba(59,130,246,.14), transparent 20%),
|
|
127
|
+
radial-gradient(circle at 82% 38%, rgba(124,58,237,.18), transparent 18%),
|
|
128
|
+
linear-gradient(180deg, rgba(15,23,42,.95), rgba(2,6,23,.76)); box-shadow:inset 0 0 70px rgba(2,6,23,.72); }
|
|
129
|
+
.radial-links { position:absolute; inset:0; width:100%; height:100%; filter:drop-shadow(0 0 14px rgba(94,234,212,.34)); }
|
|
130
|
+
.link-ring { fill:none; stroke:rgba(147,197,253,.18); stroke-width:.35; stroke-dasharray:1.2 1.4; }
|
|
131
|
+
.link-ring-inner { stroke:rgba(94,234,212,.14); stroke-dasharray:.7 1.8; }
|
|
132
|
+
.link-line { stroke:rgba(94,234,212,.54); stroke-width:.42; }
|
|
133
|
+
.link-ready, .link-idle { stroke:rgba(147,197,253,.38); }
|
|
134
|
+
.link-attention { stroke:rgba(253,230,138,.78); }
|
|
135
|
+
.link-blocked { stroke:rgba(253,164,175,.5); stroke-dasharray:1.4 1.2; }
|
|
136
|
+
.handoff-arc { fill:none; stroke:rgba(196,181,253,.94); stroke-width:1.25; stroke-linecap:round; filter:drop-shadow(0 0 5px rgba(196,181,253,.72)); }
|
|
137
|
+
.mode-handoff .link-line { stroke:rgba(147,197,253,.24); }
|
|
138
|
+
.mode-handoff .handoff-arc { stroke-width:2.35; filter:drop-shadow(0 0 11px rgba(196,181,253,.9)); }
|
|
139
|
+
.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:rgba(94,234,212,.9); background:linear-gradient(180deg, rgba(94,234,212,.34), rgba(15,23,42,.9)); box-shadow:0 0 64px rgba(94,234,212,.42), inset 0 0 38px rgba(147,197,253,.2); z-index:3; }
|
|
140
|
+
.mode-handoff .central-hub { border-color:rgba(196,181,253,.9); box-shadow:0 0 72px rgba(196,181,253,.42), inset 0 0 38px rgba(94,234,212,.16); }
|
|
141
|
+
.central-hub strong { font-size:24px; }
|
|
142
|
+
.central-hub span:last-child { color:var(--muted); font-size:12px; }
|
|
143
|
+
.hub-core { width:82px; height:52px; border:1px solid rgba(248,250,252,.78); border-radius:7px; transform:skew(-12deg); background:linear-gradient(135deg, rgba(248,250,252,.86), rgba(94,234,212,.46)); box-shadow:0 10px 32px rgba(94,234,212,.35); }
|
|
144
|
+
.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%, rgba(248,250,252,.11), rgba(15,23,42,.56) 54%, rgba(2,6,23,.42)); border-color:rgba(147,197,253,.18); box-shadow:0 20px 38px rgba(0,0,0,.34), inset 0 -18px 26px rgba(59,130,246,.06); z-index:2; }
|
|
145
|
+
.orbit-node:hover:not(:disabled) { transform:translate(-50%,-50%) translateY(-2px); }
|
|
146
|
+
.orbit-node:focus-visible { outline-offset:4px; }
|
|
147
|
+
.node-copy { min-width:0; display:flex; flex-direction:column; gap:2px; }
|
|
148
|
+
.node-copy strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:94px; font-size:11px; }
|
|
149
|
+
.node-copy span { color:var(--muted); font-size:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:94px; }
|
|
150
|
+
.orbit-node:focus-visible .node-copy span, .orbit-node:hover .node-copy span { font-size:10px; }
|
|
151
|
+
.node-glyph { width:72px; height:62px; display:inline-block; border:2px solid var(--node-accent,var(--accent)); background:rgba(255,255,255,.04); box-shadow:0 0 30px color-mix(in srgb, var(--node-accent,var(--accent)) 48%, transparent); }
|
|
152
|
+
.node-host, .node-session { --node-accent:var(--accent); }
|
|
153
|
+
.node-container, .node-inventory { --node-accent:var(--accent2); }
|
|
154
|
+
.node-vm, .node-mission { --node-accent:var(--accent3); }
|
|
155
|
+
.node-approval.node-attention, .node-blocked { --node-accent:var(--warn); }
|
|
156
|
+
.node-cost, .node-capability, .node-library, .node-action { --node-accent:var(--accent4); }
|
|
157
|
+
.node-blocked { border-color:rgba(253,164,175,.6); }
|
|
158
|
+
.node-attention { border-color:rgba(253,230,138,.72); }
|
|
159
|
+
.node-running { border-color:rgba(134,239,172,.72); }
|
|
160
|
+
.glyph-host, .glyph-session { border-radius:6px; box-shadow:inset 0 -10px 0 rgba(255,255,255,.06), 0 0 18px rgba(94,234,212,.28); }
|
|
161
|
+
.glyph-container, .glyph-inventory { border-radius:7px; transform:skew(-8deg); }
|
|
162
|
+
.glyph-vm { border-radius:4px; transform:rotate(45deg) scale(.82); }
|
|
163
|
+
.glyph-mission { border-radius:50%; position:relative; }
|
|
164
|
+
.glyph-mission::before, .glyph-mission::after { content:""; position:absolute; width:10px; height:10px; border:2px solid var(--node-accent); border-radius:50%; background:rgba(15,23,42,.9); }
|
|
165
|
+
.glyph-mission::before { left:-7px; top:12px; }
|
|
166
|
+
.glyph-mission::after { right:-7px; top:12px; }
|
|
167
|
+
.glyph-approval { border-radius:18px 18px 6px 6px; }
|
|
168
|
+
.glyph-cost { border-radius:50%; border-style:dashed; }
|
|
169
|
+
.glyph-capability { border-radius:8px; background:repeating-linear-gradient(45deg, rgba(255,255,255,.08) 0 4px, transparent 4px 8px); }
|
|
170
|
+
.glyph-library { border-radius:4px; box-shadow:inset 9px 0 0 rgba(255,255,255,.08), inset 18px 0 0 rgba(255,255,255,.05); }
|
|
171
|
+
.glyph-action { border-radius:10px; transform:rotate(45deg) scale(.8); }
|
|
172
|
+
.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 rgba(196,181,253,.82); }
|
|
173
|
+
.mode-handoff .node-mission { border-color:rgba(196,181,253,.95); box-shadow:0 22px 48px rgba(124,58,237,.36), inset 0 -18px 26px rgba(196,181,253,.1); }
|
|
174
|
+
.mode-handoff .handoff-marker { width:22px; height:22px; right:-12px; margin-top:-11px; }
|
|
175
|
+
.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:rgba(15,23,42,.76); box-shadow:0 0 28px rgba(196,181,253,.18); }
|
|
176
|
+
.handoff-callout span { color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.06em; }
|
|
177
|
+
.handoff-callout strong { font-size:13px; }
|
|
178
|
+
.handoff-source { left:20%; top:60%; }
|
|
179
|
+
.handoff-destination { right:14%; top:39%; }
|
|
180
|
+
.radial-overlay { position:absolute; left:22px; right:22px; bottom:18px; border:1px solid rgba(203,213,225,.16); border-radius:8px; background:rgba(15,23,42,.66); backdrop-filter:blur(8px); padding:13px; display:grid; grid-template-columns:minmax(280px,.62fr) 1fr; gap:12px; z-index:4; }
|
|
181
|
+
.radial-overlay-head { display:grid; grid-template-columns:minmax(160px,1fr) minmax(220px,.8fr); gap:12px; align-items:end; }
|
|
182
|
+
.radial-overlay h3 { margin:0; font-size:18px; }
|
|
183
|
+
.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:rgba(2,6,23,.34); }
|
|
184
|
+
.mission-route span { color:var(--muted); font-size:12px; }
|
|
185
|
+
.mission-route strong { font-size:14px; }
|
|
186
|
+
.route-attention { color:var(--warn) !important; }
|
|
187
|
+
.route-ready { color:var(--ok) !important; }
|
|
188
|
+
.wall-readout { display:grid; grid-template-columns:repeat(5,minmax(90px,1fr)); gap:8px; margin:0; }
|
|
189
|
+
.wall-readout div { display:grid; gap:4px; padding:8px 10px; border:1px solid rgba(203,213,225,.12); border-radius:8px; background:rgba(2,6,23,.32); }
|
|
190
|
+
.wall-readout div:last-child { border-bottom:1px solid rgba(203,213,225,.12); }
|
|
191
|
+
.wall-readout dt { color:var(--muted); font-size:12px; }
|
|
192
|
+
.wall-readout dd { margin:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
193
|
+
.stack-board { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:14px; margin:16px 0; }
|
|
194
|
+
.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 rgba(0,0,0,.22); }
|
|
195
|
+
.stack-card::before { content:""; position:absolute; inset:0; border-top:3px solid var(--stack-accent,var(--accent)); pointer-events:none; }
|
|
196
|
+
.stack-card.accent-1 { --stack-accent:var(--accent); }
|
|
197
|
+
.stack-card.accent-2 { --stack-accent:var(--accent2); }
|
|
198
|
+
.stack-card.accent-3 { --stack-accent:var(--accent3); }
|
|
199
|
+
.stack-card.accent-4 { --stack-accent:var(--accent4); }
|
|
200
|
+
.stack-head { display:grid; grid-template-columns:42px minmax(0,1fr) auto; gap:12px; align-items:start; }
|
|
201
|
+
.stack-head h3 { margin:0; font-size:16px; }
|
|
202
|
+
.stack-head p { margin:3px 0 0; color:var(--muted); font-size:12px; }
|
|
203
|
+
.topology { width:38px; height:38px; border:2px solid var(--stack-accent,var(--accent)); background:rgba(255,255,255,.04); display:inline-block; }
|
|
204
|
+
.topology-terminal { border-radius:6px; box-shadow:inset 0 -10px 0 rgba(255,255,255,.06); }
|
|
205
|
+
.topology-cube { transform:skew(-8deg); border-radius:7px; }
|
|
206
|
+
.topology-window { border-radius:10px 10px 4px 4px; }
|
|
207
|
+
.stack-log { min-height:92px; border:1px solid rgba(203,213,225,.18); background:rgba(2,6,23,.44); border-radius:8px; padding:10px; display:flex; flex-direction:column; gap:8px; color:var(--muted); font:12px/1.35 ui-monospace, monospace; }
|
|
208
|
+
.stack-log span:first-child { color:var(--fg); }
|
|
209
|
+
.segment-progress { height:8px; border-radius:999px; background:rgba(148,163,184,.18); overflow:hidden; }
|
|
210
|
+
.segment-progress span { display:block; height:100%; background:var(--stack-accent,var(--accent)); border-radius:inherit; }
|
|
211
|
+
.transport-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
212
|
+
.transport-bar span { margin-left:auto; color:var(--muted); font-size:13px; }
|
|
213
|
+
.telemetry-strip { display:grid; grid-template-columns:repeat(5,minmax(120px,1fr)); gap:10px; margin:16px 0; }
|
|
214
|
+
.telemetry-strip button { min-height:82px; display:flex; flex-direction:column; align-items:flex-start; justify-content:center; text-align:left; gap:3px; background:rgba(17,24,39,.78); border-radius:8px; }
|
|
215
|
+
.telemetry-strip strong { font-size:24px; color:var(--fg); }
|
|
216
|
+
.telemetry-strip span { color:var(--muted); font-size:12px; }
|
|
217
|
+
.control-planner { display:grid; grid-template-columns:minmax(320px,1fr) minmax(260px,.36fr); gap:14px; margin-top:16px; align-items:stretch; }
|
|
218
|
+
.guided-start { display:grid; grid-template-columns:1fr; align-items:start; gap:14px; border:1px solid var(--line); background:rgba(15,23,42,.58); border-radius:8px; padding:16px; }
|
|
219
|
+
.guided-start h3 { margin:0 0 4px; font-size:18px; }
|
|
220
|
+
.planner-grid { display:grid; grid-template-columns:repeat(3,minmax(160px,1fr)); gap:10px; }
|
|
221
|
+
.planner-grid label { display:flex; flex-direction:column; gap:6px; color:var(--muted); font-size:12px; }
|
|
222
|
+
.planner-grid select { width:100%; min-height:38px; }
|
|
223
|
+
.quota-panel { border:1px solid var(--line); border-radius:8px; background:rgba(15,23,42,.72); padding:16px; display:grid; gap:12px; align-content:start; }
|
|
224
|
+
.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); }
|
|
225
|
+
.quota-donut::before { content:""; position:absolute; inset:13px; border-radius:inherit; background:var(--panel); border:1px solid rgba(203,213,225,.16); }
|
|
226
|
+
.quota-donut strong, .quota-donut span { position:relative; z-index:1; }
|
|
227
|
+
.quota-donut strong { font-size:23px; }
|
|
228
|
+
.quota-donut span { color:var(--muted); font-size:11px; max-width:96px; text-align:center; }
|
|
229
|
+
.quota-readout { margin:0; display:grid; gap:7px; }
|
|
230
|
+
.quota-readout div { display:grid; grid-template-columns:1fr auto; gap:10px; border-bottom:1px solid rgba(203,213,225,.13); padding-bottom:7px; }
|
|
231
|
+
.quota-readout div:last-child { border-bottom:0; padding-bottom:0; }
|
|
232
|
+
.quota-readout dt { color:var(--muted); font-size:12px; }
|
|
233
|
+
.quota-readout dd { margin:0; }
|
|
234
|
+
@media (max-width: 820px) {
|
|
235
|
+
header { position:static; }
|
|
236
|
+
.top-status { width:100%; margin-left:0; }
|
|
237
|
+
.wall-hero, .guided-start, .radial-wall, .control-planner { grid-template-columns:1fr; align-items:start; }
|
|
238
|
+
.hero-actions { justify-content:flex-start; }
|
|
239
|
+
.telemetry-strip { grid-template-columns:repeat(2,minmax(120px,1fr)); }
|
|
240
|
+
.planner-grid { grid-template-columns:1fr; }
|
|
241
|
+
.radial-wall { margin-inline:0; }
|
|
242
|
+
.radial-map { min-height:0; display:grid; grid-template-columns:1fr; gap:8px; padding:12px; }
|
|
243
|
+
.radial-links, .central-hub .hub-core { display:none; }
|
|
244
|
+
.central-hub, .orbit-node { position:static; transform:none; width:100%; min-height:72px; }
|
|
245
|
+
.central-hub { margin-bottom:4px; }
|
|
246
|
+
.orbit-node:hover:not(:disabled) { transform:none; }
|
|
247
|
+
.node-copy span { font-size:10px; }
|
|
248
|
+
.handoff-callout { position:static; }
|
|
249
|
+
.radial-overlay { position:static; grid-template-columns:1fr; }
|
|
250
|
+
.radial-overlay-head, .wall-readout { grid-template-columns:1fr; }
|
|
251
|
+
}
|
|
252
|
+
@media (prefers-reduced-motion: reduce) {
|
|
253
|
+
*, *::before, *::after { scroll-behavior:auto !important; transition:none !important; animation:none !important; }
|
|
254
|
+
}
|
package/web/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface RuntimePosture {
|
|
2
|
+
kind: string;
|
|
3
|
+
isolation: 'least' | 'shared-kernel' | 'strong' | 'opaque' | 'unknown';
|
|
4
|
+
label: string;
|
|
5
|
+
warning?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface HostDaemonStatus {
|
|
8
|
+
status: 'detected' | 'available' | 'unavailable' | 'permission_denied' | 'degraded' | 'stopped' | 'unknown';
|
|
9
|
+
detail?: string;
|
|
10
|
+
operator_command?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TransportPosture {
|
|
13
|
+
mode: string;
|
|
14
|
+
trust: 'secure' | 'local' | 'compatibility' | 'degraded' | 'unknown';
|
|
15
|
+
label: string;
|
|
16
|
+
source: string;
|
|
17
|
+
evidence?: string;
|
|
18
|
+
stale?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface LaunchContext { cwd?: string; loadout?: string; runtime_kind?: string; host?: string; selected_tier?: string; name?: string; image_ref?: string; source?: string }
|
|
21
|
+
export interface SessionBackend { mode: 'direct' | 'managed'; backend: string; replay?: boolean; keyframe?: boolean; drive?: boolean; observe?: boolean; available?: boolean; reason?: string }
|
|
22
|
+
export interface Instance {
|
|
23
|
+
id: string;
|
|
24
|
+
runtime: string;
|
|
25
|
+
loadout: string;
|
|
26
|
+
state: string;
|
|
27
|
+
tenant: string;
|
|
28
|
+
card_url: string;
|
|
29
|
+
runtime_posture: RuntimePosture;
|
|
30
|
+
host_daemon: HostDaemonStatus;
|
|
31
|
+
transport: TransportPosture;
|
|
32
|
+
launch_context: LaunchContext;
|
|
33
|
+
agent_ready?: boolean;
|
|
34
|
+
registered_agent_id?: string;
|
|
35
|
+
session_backends: SessionBackend[];
|
|
36
|
+
}
|
|
37
|
+
export interface RunningTask { instance_id: string; task_id: string; state: string; tenant: string; runtime_posture?: RuntimePosture; transport?: TransportPosture }
|
|
38
|
+
export interface SessionInfo { id: string; instance_id: string; seq?: number; members?: number; has_controller?: boolean; attach_url: string; mode?: 'direct' | 'managed'; backend?: string; session_name?: string; sessionName?: string; session_class?: string; session_backend?: string; role_policy?: string; replay?: boolean; keyframe?: boolean; controllers?: number; observers?: number }
|
|
39
|
+
export interface Approval { id: string; instance_id: string; prompt: string; risk: string; status: string }
|
|
40
|
+
export interface ResponseNeeded { id: string; instance_id: string; prompt: string; source: string; status: string; attach_url?: string | null }
|
|
41
|
+
export interface InstanceCost { instance_id: string; tenant: string; input_tokens: number; output_tokens: number; usd: number }
|
|
42
|
+
export interface Cost { total: { input_tokens: number; output_tokens: number; usd: number }; per_instance: InstanceCost[] }
|
|
43
|
+
export interface Loadout { id: string; label: string; description?: string; runtimes?: string[] }
|
|
44
|
+
export interface CapabilityResult { path: string; type: string; title?: string; capability?: string; score?: number; name: string; triggers?: string[] }
|
|
45
|
+
export interface ContribAction { id: string; title: string; icon?: string; group?: string; source: string; inject: { command: string; target?: string; needs_args?: boolean; args_hint?: string } }
|
|
46
|
+
export type Role = 'controller' | 'observer' | null;
|
|
47
|
+
export interface LibraryAsset { name: string; type: string; kind: 'dir' | 'file'; origin: string; source_path?: string }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useDebounced<T>(value: T, ms = 300): T {
|
|
4
|
+
const [v, setV] = useState(value);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const t = setTimeout(() => setV(value), ms);
|
|
7
|
+
return () => clearTimeout(t);
|
|
8
|
+
}, [value, ms]);
|
|
9
|
+
return v;
|
|
10
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Terminal } from '@xterm/xterm';
|
|
3
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
4
|
+
import type { Role } from './types';
|
|
5
|
+
|
|
6
|
+
// The pty session connection, lifted to App so the Sessions tab renders it and the
|
|
7
|
+
// Actions tab can inject into it. Data plane is browser→executor (the attach_url the
|
|
8
|
+
// Bridge issues); control plane stays on the Bridge.
|
|
9
|
+
//
|
|
10
|
+
// Output is rendered through an xterm.js terminal so ANSI/VT/OSC sequences — colors,
|
|
11
|
+
// tmux redraws, window titles, bracketed paste, shell-integration markers — are
|
|
12
|
+
// *interpreted*, not dumped as raw escape bytes (the bug a plain <pre>{output} showed).
|
|
13
|
+
// This mirrors the agentic-sandbox dashboard's pty-ws.v1 client: write decoded bytes to
|
|
14
|
+
// the terminal, forward keystrokes as pty.session_input, and keep the PTY (tmux) sized
|
|
15
|
+
// to the terminal via pty.session_resize.
|
|
16
|
+
type WsMsg = { op: string; seq?: number; payload?: { role?: Role; data?: string; code?: string; frames?: { seq: number; payload: { data: string } }[] } };
|
|
17
|
+
|
|
18
|
+
export interface SessionState { attached: boolean; role: Role; url: string | null }
|
|
19
|
+
export interface ResponseNeededState { needed: boolean; prompt: string; since: string | null; source: string }
|
|
20
|
+
|
|
21
|
+
// The executor rejects resizes below this floor (management/src/ws/connection.rs).
|
|
22
|
+
const RESIZE_FLOOR_COLS = 20;
|
|
23
|
+
const RESIZE_FLOOR_ROWS = 5;
|
|
24
|
+
|
|
25
|
+
const textEnc = new TextEncoder();
|
|
26
|
+
const textDec = new TextDecoder();
|
|
27
|
+
|
|
28
|
+
// base64 → raw bytes. xterm does its own UTF-8 decoding and escape-sequence parsing, so
|
|
29
|
+
// it must receive bytes (Uint8Array) — handing it a Latin-1 string is exactly what made
|
|
30
|
+
// the escape sequences render as literal text.
|
|
31
|
+
const b64ToBytes = (b64: string): Uint8Array => {
|
|
32
|
+
try {
|
|
33
|
+
const bin = atob(b64);
|
|
34
|
+
const out = new Uint8Array(bin.length);
|
|
35
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
36
|
+
return out;
|
|
37
|
+
} catch { return new Uint8Array(0); }
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// string → base64 of its UTF-8 bytes (so non-ASCII keystrokes/paste survive the round-trip).
|
|
41
|
+
const toB64 = (s: string): string => {
|
|
42
|
+
const bytes = textEnc.encode(s);
|
|
43
|
+
let bin = '';
|
|
44
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
45
|
+
return btoa(bin);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function stripAnsi(text: string): string {
|
|
49
|
+
return text
|
|
50
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
51
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '')
|
|
52
|
+
.replace(/\x1b[()][A-Za-z0-9]/g, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function interactivePromptFrom(output: string): string {
|
|
56
|
+
const clean = stripAnsi(output).replace(/\r/g, '\n');
|
|
57
|
+
const lines = clean.split('\n').map((line) => line.trim()).filter(Boolean).slice(-24);
|
|
58
|
+
const text = lines.join('\n');
|
|
59
|
+
const promptPatterns = [
|
|
60
|
+
/Enter to select\b/i,
|
|
61
|
+
/(?:↑|up)\/(?:↓|down)|arrow keys|navigate/i,
|
|
62
|
+
/\bEsc to cancel\b/i,
|
|
63
|
+
/\b(?:y\/n|Y\/n|y\/N|\[y\/N\]|\[Y\/n\])\b/,
|
|
64
|
+
/\b(?:choose|select|pick) (?:one|an option|a number)\b/i,
|
|
65
|
+
/\?$/,
|
|
66
|
+
];
|
|
67
|
+
if (!promptPatterns.some((re) => re.test(text))) return '';
|
|
68
|
+
return lines.slice(-10).join('\n').slice(0, 900);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function useSession() {
|
|
72
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
73
|
+
const lastSeq = useRef(0);
|
|
74
|
+
const termRef = useRef<Terminal | null>(null);
|
|
75
|
+
const fitRef = useRef<FitAddon | null>(null);
|
|
76
|
+
const roRef = useRef<ResizeObserver | null>(null);
|
|
77
|
+
const roleRef = useRef<Role>(null); // current role, read by term.onData without re-subscribing
|
|
78
|
+
const outputTailRef = useRef('');
|
|
79
|
+
const [state, setState] = useState<SessionState>({ attached: false, role: null, url: null });
|
|
80
|
+
const [responseNeeded, setResponseNeeded] = useState<ResponseNeededState>({ needed: false, prompt: '', since: null, source: 'pty' });
|
|
81
|
+
|
|
82
|
+
const fit = () => { try { fitRef.current?.fit(); } catch { /* container hidden / zero-sized */ } };
|
|
83
|
+
const noteOutput = (bytes: Uint8Array) => {
|
|
84
|
+
const text = textDec.decode(bytes, { stream: true });
|
|
85
|
+
if (!text) return;
|
|
86
|
+
outputTailRef.current = (outputTailRef.current + text).slice(-6000);
|
|
87
|
+
const prompt = interactivePromptFrom(outputTailRef.current);
|
|
88
|
+
if (prompt) {
|
|
89
|
+
setResponseNeeded((prev) => (
|
|
90
|
+
prev.needed && prev.prompt === prompt
|
|
91
|
+
? prev
|
|
92
|
+
: { needed: true, prompt, since: new Date().toISOString(), source: 'pty' }
|
|
93
|
+
));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const write = (bytes: Uint8Array) => {
|
|
97
|
+
noteOutput(bytes);
|
|
98
|
+
try { termRef.current?.write(bytes); } catch { /* term not open */ }
|
|
99
|
+
};
|
|
100
|
+
const sendOp = (op: string, payload?: unknown) => { try { wsRef.current?.send(JSON.stringify(payload === undefined ? { op } : { op, payload })); } catch { /* socket closed */ } };
|
|
101
|
+
const clearResponseNeeded = () => setResponseNeeded({ needed: false, prompt: '', since: null, source: 'pty' });
|
|
102
|
+
|
|
103
|
+
// Mount the terminal into the host element (ref callback from the Sessions tab).
|
|
104
|
+
// Idempotent: the Terminal is created once and reused across attaches. A ResizeObserver
|
|
105
|
+
// re-fits when the host gains size (the tab starts hidden/zero-sized, then becomes
|
|
106
|
+
// visible) and on any later layout change, keeping the PTY dimensions honest.
|
|
107
|
+
const openTerminal = useCallback((el: HTMLDivElement | null) => {
|
|
108
|
+
if (!el) return;
|
|
109
|
+
if (!termRef.current) {
|
|
110
|
+
const term = new Terminal({
|
|
111
|
+
convertEol: false, // the PTY/tmux emits its own CR/LF
|
|
112
|
+
scrollback: 2000,
|
|
113
|
+
cursorBlink: false,
|
|
114
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
115
|
+
fontSize: 13,
|
|
116
|
+
theme: { background: '#0a0c10', foreground: '#cdd3de' },
|
|
117
|
+
});
|
|
118
|
+
const fitAddon = new FitAddon();
|
|
119
|
+
term.loadAddon(fitAddon);
|
|
120
|
+
termRef.current = term;
|
|
121
|
+
fitRef.current = fitAddon;
|
|
122
|
+
// Forward keystrokes to the PTY only while driving.
|
|
123
|
+
term.onData((data) => {
|
|
124
|
+
if (roleRef.current !== 'controller') return;
|
|
125
|
+
clearResponseNeeded();
|
|
126
|
+
sendOp('pty.session_input', { data: toB64(data) });
|
|
127
|
+
});
|
|
128
|
+
// Keep tmux sized to the terminal so redraws don't wrap/overflow.
|
|
129
|
+
term.onResize(({ cols, rows }) => {
|
|
130
|
+
if (cols < RESIZE_FLOOR_COLS || rows < RESIZE_FLOOR_ROWS) return;
|
|
131
|
+
sendOp('pty.session_resize', { cols, rows });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if (!el.querySelector('.xterm')) termRef.current.open(el);
|
|
136
|
+
} catch { /* jsdom / detached node */ }
|
|
137
|
+
if (!roRef.current && typeof ResizeObserver !== 'undefined') {
|
|
138
|
+
roRef.current = new ResizeObserver(() => fit());
|
|
139
|
+
roRef.current.observe(el);
|
|
140
|
+
}
|
|
141
|
+
fit();
|
|
142
|
+
requestAnimationFrame(() => fit());
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
// Refit on window resize (ResizeObserver covers container changes; this covers the rest).
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const onResize = () => fit();
|
|
148
|
+
window.addEventListener('resize', onResize);
|
|
149
|
+
return () => window.removeEventListener('resize', onResize);
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
// Dispose on unmount.
|
|
153
|
+
useEffect(() => () => {
|
|
154
|
+
try { roRef.current?.disconnect(); } catch { /* */ }
|
|
155
|
+
try { termRef.current?.dispose(); } catch { /* */ }
|
|
156
|
+
wsRef.current?.close();
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
const attach = useCallback((url: string, replay = false, requestedRole: Exclude<Role, null> = 'observer') => {
|
|
160
|
+
wsRef.current?.close();
|
|
161
|
+
if (!replay) lastSeq.current = 0;
|
|
162
|
+
roleRef.current = null;
|
|
163
|
+
clearResponseNeeded();
|
|
164
|
+
outputTailRef.current = '';
|
|
165
|
+
setState({ attached: false, role: null, url });
|
|
166
|
+
if (!replay) { try { termRef.current?.reset(); } catch { /* */ } }
|
|
167
|
+
const ws = new WebSocket(replay ? `${url}?replay_from=${lastSeq.current}` : url);
|
|
168
|
+
wsRef.current = ws;
|
|
169
|
+
ws.addEventListener('open', () => setState((s) => ({ ...s, attached: true, url })));
|
|
170
|
+
ws.addEventListener('close', () => { roleRef.current = null; setState((s) => ({ ...s, attached: false, role: null })); });
|
|
171
|
+
ws.addEventListener('error', () => write(textEnc.encode('\r\n[connection error]\r\n')));
|
|
172
|
+
ws.addEventListener('message', (ev) => {
|
|
173
|
+
let m: WsMsg;
|
|
174
|
+
try { m = JSON.parse(ev.data as string); } catch { return; }
|
|
175
|
+
switch (m.op) {
|
|
176
|
+
case 'binding_hello': {
|
|
177
|
+
sendOp('pty.join_session', { role: requestedRole });
|
|
178
|
+
// Tell the PTY our current dimensions up front so the first tmux redraw fits.
|
|
179
|
+
const t = termRef.current;
|
|
180
|
+
if (t && t.cols >= RESIZE_FLOOR_COLS && t.rows >= RESIZE_FLOOR_ROWS) sendOp('pty.session_resize', { cols: t.cols, rows: t.rows });
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 'role_assigned':
|
|
184
|
+
roleRef.current = m.payload?.role ?? null;
|
|
185
|
+
setState((s) => ({ ...s, role: m.payload?.role ?? null }));
|
|
186
|
+
requestAnimationFrame(() => fit());
|
|
187
|
+
break;
|
|
188
|
+
case 'output':
|
|
189
|
+
if (m.seq) lastSeq.current = Math.max(lastSeq.current, m.seq);
|
|
190
|
+
write(b64ToBytes(m.payload?.data ?? ''));
|
|
191
|
+
break;
|
|
192
|
+
case 'keyframe':
|
|
193
|
+
for (const f of m.payload?.frames ?? []) { if (f.seq) lastSeq.current = Math.max(lastSeq.current, f.seq); write(b64ToBytes(f.payload.data)); }
|
|
194
|
+
break;
|
|
195
|
+
case 'error':
|
|
196
|
+
write(textEnc.encode(`\r\n[${m.payload?.code ?? 'error'}]\r\n`));
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
const detach = useCallback(() => { wsRef.current?.close(); wsRef.current = null; }, []);
|
|
203
|
+
const replay = useCallback((url: string, requestedRole?: Exclude<Role, null>) => {
|
|
204
|
+
const role = requestedRole ?? roleRef.current ?? 'observer';
|
|
205
|
+
detach();
|
|
206
|
+
setTimeout(() => attach(url, true, role), 50);
|
|
207
|
+
}, [attach, detach]);
|
|
208
|
+
const requestKeyframe = useCallback(() => sendOp('pty.request_keyframe'), []);
|
|
209
|
+
// Composer line-input (the input row + Actions inject). Raw keystrokes go via term.onData.
|
|
210
|
+
const sendInput = useCallback((text: string): boolean => {
|
|
211
|
+
if (!wsRef.current || roleRef.current !== 'controller' || !text) return false;
|
|
212
|
+
clearResponseNeeded();
|
|
213
|
+
sendOp('pty.session_input', { data: toB64(text + '\r\n') });
|
|
214
|
+
return true;
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
return { state, responseNeeded, attach, detach, replay, requestKeyframe, sendInput, openTerminal, isController: state.role === 'controller' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type SessionApi = ReturnType<typeof useSession>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { capRef, fmtId } from './util';
|
|
3
|
+
|
|
4
|
+
describe('capRef', () => {
|
|
5
|
+
it('prefixes agents with @', () => {
|
|
6
|
+
expect(capRef('agent', 'aiwg-steward')).toBe('@aiwg-steward');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('injects skills as the bare name (no slash) — discover-first resolves them (#1642)', () => {
|
|
10
|
+
expect(capRef('skill', 'intake-wizard')).toBe('intake-wizard');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('keeps the slash for commands (the real slash-command surface)', () => {
|
|
14
|
+
expect(capRef('command', 'discover')).toBe('/discover');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('falls back to the bare name for unknown types', () => {
|
|
18
|
+
expect(capRef('rule', 'no-attribution')).toBe('no-attribution');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('fmtId', () => {
|
|
23
|
+
it('truncates long ids with an ellipsis', () => {
|
|
24
|
+
expect(fmtId('0123456789abcdef')).toBe('01234567…');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('leaves short ids untouched', () => {
|
|
28
|
+
expect(fmtId('short')).toBe('short');
|
|
29
|
+
});
|
|
30
|
+
});
|
package/web/src/util.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const fmtId = (id: string): string => (id && id.length > 12 ? id.slice(0, 8) + '…' : id);
|
|
2
|
+
|
|
3
|
+
// How a picked capability is referenced when inserted into a session composer:
|
|
4
|
+
// - agents → @name (stable convention across hosts)
|
|
5
|
+
// - skills → the bare skill name. AIWG's discover-first protocol resolves a skill
|
|
6
|
+
// by plain name or lookup phrase, NOT a slash-command. A "/" prefix is wrong here:
|
|
7
|
+
// most hosts don't expose AIWG skills as real slash-commands, and the leading "/"
|
|
8
|
+
// is interpreted differently per platform (see #1642).
|
|
9
|
+
// - commands → /name, only because commands ARE the slash-command surface on hosts
|
|
10
|
+
// that have one. (If this ever needs to be platform-aware, thread the host in here.)
|
|
11
|
+
// - anything else → bare name.
|
|
12
|
+
export function capRef(type: string, name: string): string {
|
|
13
|
+
if (type === 'agent') return `@${name}`;
|
|
14
|
+
if (type === 'command') return `/${name}`;
|
|
15
|
+
return name;
|
|
16
|
+
}
|
|
17
|
+
|