agent-office-cli 0.0.1 → 0.1.1
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/package.json +1 -1
- package/src/core/providers/codex.js +57 -11
- package/src/core/providers/codex.test.js +74 -0
- package/src/core/session-contract.js +3 -1
- package/src/core/session-contract.test.js +27 -0
- package/src/core/store/session-store.js +19 -16
- package/src/core/store/session-store.test.js +50 -0
- package/src/runtime/postinstall-path.test.js +26 -0
- package/src/runtime/pty-manager.js +16 -4
- package/src/server.js +17 -4
- package/src/web/index.js +0 -7
- package/src/web/public/app.js +0 -713
- package/src/web/public/dashboard.html +0 -245
- package/src/web/public/index.html +0 -84
- package/src/web/public/login.css +0 -833
- package/src/web/public/login.html +0 -28
- package/src/web/public/office.html +0 -22
- package/src/web/public/register.html +0 -316
- package/src/web/public/styles.css +0 -988
package/src/web/public/app.js
DELETED
|
@@ -1,713 +0,0 @@
|
|
|
1
|
-
(function () {
|
|
2
|
-
// Detect if served through a relay tunnel: /tunnel/:userId/app.js
|
|
3
|
-
const tunnelMatch = location.pathname.match(/^\/tunnel\/([^/]+)/);
|
|
4
|
-
const TUNNEL_PREFIX = tunnelMatch ? `/tunnel/${tunnelMatch[1]}` : "";
|
|
5
|
-
|
|
6
|
-
// Configurable API base — supports standalone deployment (CDN/Vercel) pointing to a relay
|
|
7
|
-
const API_BASE = window.AGENTOFFICE_API_BASE || TUNNEL_PREFIX;
|
|
8
|
-
const WS_BASE = window.AGENTOFFICE_WS_BASE || `${location.origin.replace(/^http/, "ws")}${TUNNEL_PREFIX}`;
|
|
9
|
-
|
|
10
|
-
const zones = [
|
|
11
|
-
{ id: "working-zone", title: "Office Floor", description: "Thinking, searching, editing, or running tools", color: "var(--working)" },
|
|
12
|
-
{ id: "approval-zone", title: "Approval Desk", description: "Waiting for permission, confirmation, or explicit approval", color: "var(--waiting)" },
|
|
13
|
-
{ id: "attention-zone", title: "Attention Desk", description: "Needs intervention or review", color: "var(--attention)" },
|
|
14
|
-
{ id: "idle-zone", title: "Idle", description: "No active task right now", color: "var(--idle)" }
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const state = {
|
|
18
|
-
sessions: [],
|
|
19
|
-
serverOnline: false,
|
|
20
|
-
navOpen: false,
|
|
21
|
-
terminal: null,
|
|
22
|
-
fitAddon: null,
|
|
23
|
-
terminalSocket: null,
|
|
24
|
-
terminalSessionId: null,
|
|
25
|
-
sessionMap: new Map(),
|
|
26
|
-
connectionStatus: "connecting"
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const app = document.querySelector("#app");
|
|
30
|
-
|
|
31
|
-
// --- WebSocket reconnection for /ws/events ---
|
|
32
|
-
|
|
33
|
-
let eventsSocket = null;
|
|
34
|
-
let eventsReconnectDelay = 1000;
|
|
35
|
-
const EVENTS_MAX_DELAY = 30000;
|
|
36
|
-
|
|
37
|
-
function connectEventsSocket() {
|
|
38
|
-
state.connectionStatus = "connecting";
|
|
39
|
-
let wsUrl = `${WS_BASE}/ws/events`;
|
|
40
|
-
if (TUNNEL_PREFIX) {
|
|
41
|
-
const jwt = getJwt();
|
|
42
|
-
if (jwt) wsUrl += `?token=${encodeURIComponent(jwt)}`;
|
|
43
|
-
}
|
|
44
|
-
eventsSocket = new WebSocket(wsUrl);
|
|
45
|
-
|
|
46
|
-
eventsSocket.addEventListener("open", () => {
|
|
47
|
-
state.connectionStatus = "connected";
|
|
48
|
-
state.serverOnline = true;
|
|
49
|
-
eventsReconnectDelay = 1000;
|
|
50
|
-
updateConnectionIndicator();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
eventsSocket.addEventListener("message", (event) => {
|
|
54
|
-
const payload = JSON.parse(event.data);
|
|
55
|
-
if (payload.type === "sessions:snapshot") {
|
|
56
|
-
state.sessions = payload.sessions.filter(isVisibleOfficeSession);
|
|
57
|
-
}
|
|
58
|
-
if (payload.type === "session:update") {
|
|
59
|
-
state.sessions = upsertSession(state.sessions, payload.session).filter(isVisibleOfficeSession);
|
|
60
|
-
}
|
|
61
|
-
state.sessionMap = new Map(state.sessions.map((session) => [session.sessionId, session]));
|
|
62
|
-
state.serverOnline = true;
|
|
63
|
-
if (route().name === "terminal") {
|
|
64
|
-
renderTerminalInfo(route().sessionId);
|
|
65
|
-
} else {
|
|
66
|
-
render();
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
eventsSocket.addEventListener("close", (event) => {
|
|
71
|
-
state.serverOnline = false;
|
|
72
|
-
state.connectionStatus = "reconnecting";
|
|
73
|
-
updateConnectionIndicator();
|
|
74
|
-
|
|
75
|
-
if (event.code === 4401 || event.reason === "unauthorized") {
|
|
76
|
-
handleUnauthorized();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
setTimeout(() => {
|
|
81
|
-
eventsReconnectDelay = Math.min(eventsReconnectDelay * 2, EVENTS_MAX_DELAY);
|
|
82
|
-
connectEventsSocket();
|
|
83
|
-
}, eventsReconnectDelay);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
eventsSocket.addEventListener("error", () => {
|
|
87
|
-
// Will trigger close event
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function reconnectEventsAndSync() {
|
|
92
|
-
if (eventsSocket) {
|
|
93
|
-
eventsSocket.close();
|
|
94
|
-
}
|
|
95
|
-
connectEventsSocket();
|
|
96
|
-
api(`/api/sessions`).then((data) => {
|
|
97
|
-
if (data && data.sessions) {
|
|
98
|
-
state.sessions = data.sessions.filter(isVisibleOfficeSession);
|
|
99
|
-
state.sessionMap = new Map(state.sessions.map((s) => [s.sessionId, s]));
|
|
100
|
-
render();
|
|
101
|
-
}
|
|
102
|
-
}).catch(() => {});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
connectEventsSocket();
|
|
106
|
-
|
|
107
|
-
// --- Connection status indicator ---
|
|
108
|
-
|
|
109
|
-
function updateConnectionIndicator() {
|
|
110
|
-
const pill = document.querySelector(".status-pill");
|
|
111
|
-
if (pill) {
|
|
112
|
-
const dot = pill.querySelector(".status-dot");
|
|
113
|
-
if (state.connectionStatus === "connected") {
|
|
114
|
-
pill.setAttribute("data-status", "connected");
|
|
115
|
-
if (dot) dot.style.background = "#78b07a";
|
|
116
|
-
const label = pill.querySelector(".status-label");
|
|
117
|
-
if (label) label.textContent = "online";
|
|
118
|
-
} else if (state.connectionStatus === "reconnecting") {
|
|
119
|
-
pill.setAttribute("data-status", "reconnecting");
|
|
120
|
-
if (dot) dot.style.background = "";
|
|
121
|
-
const label = pill.querySelector(".status-label");
|
|
122
|
-
if (label) label.textContent = "reconnecting\u2026";
|
|
123
|
-
} else {
|
|
124
|
-
pill.setAttribute("data-status", "connecting");
|
|
125
|
-
if (dot) dot.style.background = "";
|
|
126
|
-
const label = pill.querySelector(".status-label");
|
|
127
|
-
if (label) label.textContent = "connecting\u2026";
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
window.addEventListener("hashchange", render);
|
|
133
|
-
window.addEventListener("resize", () => {
|
|
134
|
-
if (!state.fitAddon || !state.terminalSocket || state.terminalSocket.readyState !== WebSocket.OPEN) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
state.fitAddon.fit();
|
|
138
|
-
state.terminalSocket.send(JSON.stringify({
|
|
139
|
-
type: "resize",
|
|
140
|
-
cols: state.terminal.cols,
|
|
141
|
-
rows: state.terminal.rows
|
|
142
|
-
}));
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
function isVisibleOfficeSession(session) {
|
|
146
|
-
return !["completed", "exited"].includes(session.status);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function upsertSession(items, next) {
|
|
150
|
-
const found = items.findIndex((item) => item.sessionId === next.sessionId);
|
|
151
|
-
if (found === -1) {
|
|
152
|
-
return [next, ...items];
|
|
153
|
-
}
|
|
154
|
-
const cloned = items.slice();
|
|
155
|
-
cloned[found] = next;
|
|
156
|
-
return cloned;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function route() {
|
|
160
|
-
const match = location.hash.match(/^#\/terminal\/([^/]+)$/);
|
|
161
|
-
if (match) {
|
|
162
|
-
return { name: "terminal", sessionId: decodeURIComponent(match[1]) };
|
|
163
|
-
}
|
|
164
|
-
return { name: "office" };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function getJwt() {
|
|
168
|
-
return localStorage.getItem("agentoffice_jwt");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function handleUnauthorized() {
|
|
172
|
-
if (TUNNEL_PREFIX) {
|
|
173
|
-
localStorage.removeItem("agentoffice_jwt");
|
|
174
|
-
localStorage.removeItem("agentoffice_user_id");
|
|
175
|
-
window.location.href = "/";
|
|
176
|
-
} else {
|
|
177
|
-
window.location.href = "/login.html";
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function getUserId() {
|
|
182
|
-
const storedUserId = localStorage.getItem("agentoffice_user_id");
|
|
183
|
-
if (storedUserId) {
|
|
184
|
-
return storedUserId;
|
|
185
|
-
}
|
|
186
|
-
return tunnelMatch ? decodeURIComponent(tunnelMatch[1]) : "";
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function renderNavDrawer(activeView) {
|
|
190
|
-
if (!TUNNEL_PREFIX) {
|
|
191
|
-
return "";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return `
|
|
195
|
-
<div class="nav-drawer ${state.navOpen ? "is-open" : ""}" aria-hidden="${state.navOpen ? "false" : "true"}">
|
|
196
|
-
<button class="nav-drawer-backdrop" type="button" aria-label="Close menu" data-nav-close></button>
|
|
197
|
-
<aside class="nav-drawer-panel">
|
|
198
|
-
<div class="nav-drawer-head">
|
|
199
|
-
<div>
|
|
200
|
-
<p class="eyebrow">Navigate</p>
|
|
201
|
-
<strong>AgentOffice</strong>
|
|
202
|
-
</div>
|
|
203
|
-
<button class="nav-close-button" type="button" aria-label="Close menu" data-nav-close>×</button>
|
|
204
|
-
</div>
|
|
205
|
-
<nav class="nav-drawer-links">
|
|
206
|
-
<a class="nav-link ${activeView === "office" ? "is-active" : ""}" href="${TUNNEL_PREFIX}/office.html">
|
|
207
|
-
<span class="nav-link-title">Office</span>
|
|
208
|
-
<span class="nav-link-copy">Live workers and shared terminals</span>
|
|
209
|
-
</a>
|
|
210
|
-
<a class="nav-link ${activeView === "api-keys" ? "is-active" : ""}" href="/dashboard.html">
|
|
211
|
-
<span class="nav-link-title">API Key</span>
|
|
212
|
-
<span class="nav-link-copy">Create and revoke hosted access keys</span>
|
|
213
|
-
</a>
|
|
214
|
-
</nav>
|
|
215
|
-
</aside>
|
|
216
|
-
</div>
|
|
217
|
-
`;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function bindNavDrawer() {
|
|
221
|
-
function syncNavDrawer() {
|
|
222
|
-
const drawer = document.querySelector(".nav-drawer");
|
|
223
|
-
if (!drawer) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
drawer.classList.toggle("is-open", state.navOpen);
|
|
227
|
-
drawer.setAttribute("aria-hidden", state.navOpen ? "false" : "true");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
document.querySelectorAll("[data-nav-toggle]").forEach((element) => {
|
|
231
|
-
element.addEventListener("click", () => {
|
|
232
|
-
state.navOpen = true;
|
|
233
|
-
syncNavDrawer();
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
document.querySelectorAll("[data-nav-close]").forEach((element) => {
|
|
238
|
-
element.addEventListener("click", () => {
|
|
239
|
-
state.navOpen = false;
|
|
240
|
-
syncNavDrawer();
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function api(urlPath, options) {
|
|
246
|
-
const headers = { "Content-Type": "application/json" };
|
|
247
|
-
// In tunnel mode, attach JWT as Bearer token
|
|
248
|
-
if (TUNNEL_PREFIX) {
|
|
249
|
-
const jwt = getJwt();
|
|
250
|
-
if (jwt) {
|
|
251
|
-
headers["Authorization"] = `Bearer ${jwt}`;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
const response = await fetch(`${API_BASE}${urlPath}`, {
|
|
255
|
-
headers,
|
|
256
|
-
...options
|
|
257
|
-
});
|
|
258
|
-
if (response.status === 401) {
|
|
259
|
-
handleUnauthorized();
|
|
260
|
-
throw new Error("unauthorized");
|
|
261
|
-
}
|
|
262
|
-
if (!response.ok) {
|
|
263
|
-
throw new Error(await response.text());
|
|
264
|
-
}
|
|
265
|
-
return response.json();
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async function handleQuickLaunch(provider) {
|
|
269
|
-
const names = {
|
|
270
|
-
claude: "Claude Session",
|
|
271
|
-
codex: "Codex Session"
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
await api("/api/sessions/launch", {
|
|
275
|
-
method: "POST",
|
|
276
|
-
body: JSON.stringify({
|
|
277
|
-
provider,
|
|
278
|
-
transport: "tmux",
|
|
279
|
-
title: names[provider] || `${provider} session`,
|
|
280
|
-
command: provider
|
|
281
|
-
})
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// --- Terminal WebSocket with reconnection ---
|
|
286
|
-
|
|
287
|
-
let terminalReconnectDelay = 1000;
|
|
288
|
-
const TERMINAL_MAX_DELAY = 30000;
|
|
289
|
-
let terminalReconnectTimer = null;
|
|
290
|
-
|
|
291
|
-
function connectTerminal(sessionId) {
|
|
292
|
-
if (state.terminalSessionId === sessionId && state.terminalSocket) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
cleanupTerminal();
|
|
296
|
-
if (state.terminalSocket) {
|
|
297
|
-
state.terminalSocket.close();
|
|
298
|
-
state.terminalSocket = null;
|
|
299
|
-
}
|
|
300
|
-
state.terminalSessionId = sessionId;
|
|
301
|
-
terminalReconnectDelay = 1000;
|
|
302
|
-
|
|
303
|
-
const host = document.querySelector("#terminal-host");
|
|
304
|
-
if (!host) {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
state.terminal = new window.Terminal({
|
|
309
|
-
cursorBlink: true,
|
|
310
|
-
fontSize: 13,
|
|
311
|
-
theme: {
|
|
312
|
-
background: "#151311",
|
|
313
|
-
foreground: "#f7f0df"
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
state.fitAddon = new window.FitAddon.FitAddon();
|
|
317
|
-
state.terminal.loadAddon(state.fitAddon);
|
|
318
|
-
state.terminal.open(host);
|
|
319
|
-
|
|
320
|
-
// Unicode11: fix CJK wide character alignment
|
|
321
|
-
if (window.Unicode11Addon) {
|
|
322
|
-
var unicode11 = new window.Unicode11Addon.Unicode11Addon();
|
|
323
|
-
state.terminal.loadAddon(unicode11);
|
|
324
|
-
state.terminal.unicode.activeVersion = "11";
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// WebGL renderer: sharper text, better performance on high-DPI screens
|
|
328
|
-
if (window.WebglAddon) {
|
|
329
|
-
try {
|
|
330
|
-
state.terminal.loadAddon(new window.WebglAddon.WebglAddon());
|
|
331
|
-
} catch (e) {
|
|
332
|
-
// WebGL not available, fall back to default canvas renderer
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
state.fitAddon.fit();
|
|
337
|
-
|
|
338
|
-
openTerminalSocket(sessionId);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function openTerminalSocket(sessionId) {
|
|
342
|
-
let wsUrl = `${WS_BASE}/ws/terminal/${encodeURIComponent(sessionId)}`;
|
|
343
|
-
if (TUNNEL_PREFIX) {
|
|
344
|
-
const jwt = getJwt();
|
|
345
|
-
if (jwt) wsUrl += `?token=${encodeURIComponent(jwt)}`;
|
|
346
|
-
}
|
|
347
|
-
const ws = new WebSocket(wsUrl);
|
|
348
|
-
state.terminalSocket = ws;
|
|
349
|
-
|
|
350
|
-
ws.addEventListener("open", () => {
|
|
351
|
-
terminalReconnectDelay = 1000;
|
|
352
|
-
updateTerminalWarning("");
|
|
353
|
-
ws.send(JSON.stringify({ type: "resize", cols: state.terminal.cols, rows: state.terminal.rows }));
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
ws.addEventListener("message", (event) => {
|
|
357
|
-
const payload = JSON.parse(event.data);
|
|
358
|
-
if (payload.type === "terminal:data") {
|
|
359
|
-
state.terminal.write(payload.data);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (payload.type === "terminal:unavailable") {
|
|
363
|
-
const warning = document.querySelector("#terminal-warning");
|
|
364
|
-
if (warning) {
|
|
365
|
-
warning.textContent = payload.reason;
|
|
366
|
-
}
|
|
367
|
-
if (state.terminal) {
|
|
368
|
-
state.terminal.write(`\r\n[terminal unavailable]\r\n${payload.reason}\r\n`);
|
|
369
|
-
}
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (payload.type === "terminal:exit") {
|
|
373
|
-
state.terminal.write(`\r\n\r\n[process exited: ${payload.exitCode}]\r\n`);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (payload.type === "session:update") {
|
|
377
|
-
state.sessions = upsertSession(state.sessions, payload.session).filter(isVisibleOfficeSession);
|
|
378
|
-
state.sessionMap = new Map(state.sessions.map((session) => [session.sessionId, session]));
|
|
379
|
-
renderTerminalInfo(sessionId);
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
ws.addEventListener("close", (event) => {
|
|
384
|
-
if (state.terminalSessionId !== sessionId) {
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (event.code === 4401 || event.reason === "unauthorized") {
|
|
389
|
-
handleUnauthorized();
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (route().name === "terminal" && route().sessionId === sessionId) {
|
|
394
|
-
updateTerminalWarning("Connection lost. Reconnecting...");
|
|
395
|
-
if (state.terminal) {
|
|
396
|
-
state.terminal.write("\r\n[connection lost, reconnecting...]\r\n");
|
|
397
|
-
}
|
|
398
|
-
terminalReconnectTimer = setTimeout(() => {
|
|
399
|
-
if (state.terminalSessionId === sessionId && route().name === "terminal") {
|
|
400
|
-
terminalReconnectDelay = Math.min(terminalReconnectDelay * 2, TERMINAL_MAX_DELAY);
|
|
401
|
-
openTerminalSocket(sessionId);
|
|
402
|
-
}
|
|
403
|
-
}, terminalReconnectDelay);
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
ws.addEventListener("error", () => {
|
|
408
|
-
// Will trigger close
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
state.terminal.onData((data) => {
|
|
412
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
413
|
-
ws.send(JSON.stringify({ type: "input", data }));
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function updateTerminalWarning(msg) {
|
|
419
|
-
const el = document.querySelector("#terminal-warning");
|
|
420
|
-
if (el) {
|
|
421
|
-
el.textContent = msg;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function cleanupTerminal() {
|
|
426
|
-
if (terminalReconnectTimer) {
|
|
427
|
-
clearTimeout(terminalReconnectTimer);
|
|
428
|
-
terminalReconnectTimer = null;
|
|
429
|
-
}
|
|
430
|
-
if (state.terminalSocket) {
|
|
431
|
-
state.terminalSocket.close();
|
|
432
|
-
state.terminalSocket = null;
|
|
433
|
-
}
|
|
434
|
-
if (state.terminal) {
|
|
435
|
-
state.terminal.dispose();
|
|
436
|
-
state.terminal = null;
|
|
437
|
-
}
|
|
438
|
-
state.fitAddon = null;
|
|
439
|
-
state.terminalSessionId = null;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function renderOffice() {
|
|
443
|
-
cleanupTerminal();
|
|
444
|
-
const sessionCount = state.sessions.length;
|
|
445
|
-
const statusLabel = state.connectionStatus === "connected" ? "online" :
|
|
446
|
-
state.connectionStatus === "reconnecting" ? "reconnecting\u2026" : "connecting\u2026";
|
|
447
|
-
const statusAttr = state.connectionStatus === "connected" ? "connected" : state.connectionStatus;
|
|
448
|
-
|
|
449
|
-
app.innerHTML = `
|
|
450
|
-
<div class="page-shell">
|
|
451
|
-
${renderNavDrawer("office")}
|
|
452
|
-
<header class="topbar">
|
|
453
|
-
<div class="topbar-main">
|
|
454
|
-
${TUNNEL_PREFIX ? `<button class="menu-button" type="button" data-nav-toggle>
|
|
455
|
-
<span class="menu-button-lines"></span>
|
|
456
|
-
<span class="menu-button-label">More</span>
|
|
457
|
-
</button>` : ""}
|
|
458
|
-
<div class="topbar-brand">
|
|
459
|
-
<div class="brand-mark">
|
|
460
|
-
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
|
461
|
-
</div>
|
|
462
|
-
<div class="topbar-text">
|
|
463
|
-
<p class="eyebrow">AgentOffice</p>
|
|
464
|
-
<h1>Office</h1>
|
|
465
|
-
</div>
|
|
466
|
-
</div>
|
|
467
|
-
</div>
|
|
468
|
-
<div class="topbar-pills">
|
|
469
|
-
<span class="pill">${sessionCount} active</span>
|
|
470
|
-
<span class="status-pill" data-status="${statusAttr}"><span class="status-dot"></span><span class="status-label">${statusLabel}</span></span>
|
|
471
|
-
</div>
|
|
472
|
-
</header>
|
|
473
|
-
|
|
474
|
-
<main class="layout">
|
|
475
|
-
<section class="panel">
|
|
476
|
-
<div class="section-head">
|
|
477
|
-
<div>
|
|
478
|
-
<h2>Office Floor</h2>
|
|
479
|
-
<p class="helper-text">Four states on top of provider adapters. Workers move between zones based on their activity.</p>
|
|
480
|
-
</div>
|
|
481
|
-
</div>
|
|
482
|
-
<div class="office-grid">
|
|
483
|
-
${zones.map(renderZone).join("")}
|
|
484
|
-
</div>
|
|
485
|
-
</section>
|
|
486
|
-
|
|
487
|
-
<aside class="panel">
|
|
488
|
-
<div class="section-head">
|
|
489
|
-
<div>
|
|
490
|
-
<h2>Quick Launch</h2>
|
|
491
|
-
<p class="helper-text">Start a tmux-backed worker in one click.</p>
|
|
492
|
-
</div>
|
|
493
|
-
</div>
|
|
494
|
-
<div class="quick-launch-actions">
|
|
495
|
-
<button class="primary-button" type="button" data-launch-provider="claude">Launch Claude</button>
|
|
496
|
-
<button class="primary-button" type="button" data-launch-provider="codex">Launch Codex</button>
|
|
497
|
-
<p class="helper-text">Web launch defaults to <code>tmux</code> transport. Use the CLI for custom title, working directory, or command.</p>
|
|
498
|
-
</div>
|
|
499
|
-
|
|
500
|
-
<div class="legend">
|
|
501
|
-
${zones.map((zone) => `
|
|
502
|
-
<div class="legend-item">
|
|
503
|
-
<span class="legend-color" style="background:${zone.color}"></span>
|
|
504
|
-
<div>
|
|
505
|
-
<strong>${zone.title}</strong>
|
|
506
|
-
<p class="helper-text">${zone.description}</p>
|
|
507
|
-
</div>
|
|
508
|
-
</div>
|
|
509
|
-
`).join("")}
|
|
510
|
-
</div>
|
|
511
|
-
</aside>
|
|
512
|
-
</main>
|
|
513
|
-
</div>
|
|
514
|
-
`;
|
|
515
|
-
|
|
516
|
-
document.querySelectorAll("[data-launch-provider]").forEach((element) => {
|
|
517
|
-
element.addEventListener("click", async () => {
|
|
518
|
-
element.disabled = true;
|
|
519
|
-
try {
|
|
520
|
-
await handleQuickLaunch(element.dataset.launchProvider);
|
|
521
|
-
} finally {
|
|
522
|
-
element.disabled = false;
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
bindNavDrawer();
|
|
527
|
-
document.querySelectorAll("[data-session-id]").forEach((element) => {
|
|
528
|
-
element.addEventListener("click", () => {
|
|
529
|
-
location.hash = `#/terminal/${element.dataset.sessionId}`;
|
|
530
|
-
});
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function renderZone(zone) {
|
|
535
|
-
const workers = state.sessions.filter((session) => session.displayZone === zone.id);
|
|
536
|
-
return `
|
|
537
|
-
<section class="zone" style="--zone-color:${zone.color}">
|
|
538
|
-
<div class="zone-head">
|
|
539
|
-
<div>
|
|
540
|
-
<h3>${zone.title}</h3>
|
|
541
|
-
<p>${zone.description}</p>
|
|
542
|
-
</div>
|
|
543
|
-
<span class="zone-count">${workers.length}</span>
|
|
544
|
-
</div>
|
|
545
|
-
<div class="worker-list">
|
|
546
|
-
${workers.length ? workers.map(renderWorkerCard).join("") : `<div class="empty-state">No workers here.</div>`}
|
|
547
|
-
</div>
|
|
548
|
-
</section>
|
|
549
|
-
`;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function renderWorkerCard(session) {
|
|
553
|
-
const providerClass = ["claude", "codex"].includes(session.provider) ? `provider-${session.provider}` : "provider-generic";
|
|
554
|
-
return `
|
|
555
|
-
<button class="worker-card" type="button" data-session-id="${session.sessionId}">
|
|
556
|
-
<div class="worker-icon ${providerClass}">
|
|
557
|
-
${providerIcon(session.provider)}
|
|
558
|
-
</div>
|
|
559
|
-
<div class="worker-card-text">
|
|
560
|
-
<h3>${escapeHtml(session.title)}</h3>
|
|
561
|
-
<p>${escapeHtml(session.provider)} · ${escapeHtml(session.displayState)}</p>
|
|
562
|
-
</div>
|
|
563
|
-
</button>
|
|
564
|
-
`;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function renderTerminal() {
|
|
568
|
-
const { sessionId } = route();
|
|
569
|
-
const session = state.sessionMap.get(sessionId);
|
|
570
|
-
|
|
571
|
-
app.innerHTML = `
|
|
572
|
-
<div class="terminal-shell">
|
|
573
|
-
${renderNavDrawer("office")}
|
|
574
|
-
<header class="terminal-topbar">
|
|
575
|
-
<div class="terminal-meta">
|
|
576
|
-
${TUNNEL_PREFIX ? `<button class="menu-button" type="button" data-nav-toggle>
|
|
577
|
-
<span class="menu-button-lines"></span>
|
|
578
|
-
<span class="menu-button-label">More</span>
|
|
579
|
-
</button>` : ""}
|
|
580
|
-
<button class="ghost-button" id="back-button" type="button">\u2190 Office</button>
|
|
581
|
-
<div class="terminal-info">
|
|
582
|
-
<p class="eyebrow">Terminal</p>
|
|
583
|
-
<h2 id="terminal-title">${escapeHtml(session ? session.title : sessionId)}</h2>
|
|
584
|
-
<p id="terminal-summary">${session ? `${session.provider} \u00b7 ${session.displayState}` : "Loading\u2026"}</p>
|
|
585
|
-
</div>
|
|
586
|
-
</div>
|
|
587
|
-
<div class="terminal-meta">
|
|
588
|
-
<span class="pill" id="terminal-state">${session ? session.displayState : "idle"}</span>
|
|
589
|
-
</div>
|
|
590
|
-
</header>
|
|
591
|
-
|
|
592
|
-
<div class="terminal-layout">
|
|
593
|
-
<section class="terminal-panel">
|
|
594
|
-
<div id="terminal-host" class="terminal-host"></div>
|
|
595
|
-
</section>
|
|
596
|
-
<aside class="terminal-sidebar" id="terminal-sidebar">
|
|
597
|
-
<div>
|
|
598
|
-
<p class="eyebrow">Session</p>
|
|
599
|
-
<dl id="terminal-metadata" class="meta-grid">
|
|
600
|
-
${renderTerminalMetadata(session)}
|
|
601
|
-
</dl>
|
|
602
|
-
</div>
|
|
603
|
-
<div>
|
|
604
|
-
<p class="eyebrow">Connection</p>
|
|
605
|
-
<p id="terminal-warning" class="terminal-warning"></p>
|
|
606
|
-
</div>
|
|
607
|
-
<div>
|
|
608
|
-
<p class="eyebrow">Recent Logs</p>
|
|
609
|
-
<div id="terminal-logs" class="log-box">${session && session.logs ? escapeHtml(session.logs.slice(-60).join("\n")) : "No logs captured yet."}</div>
|
|
610
|
-
</div>
|
|
611
|
-
</aside>
|
|
612
|
-
</div>
|
|
613
|
-
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
|
|
614
|
-
<button class="sidebar-toggle" id="sidebar-toggle" type="button" title="Toggle sidebar">\u2630</button>
|
|
615
|
-
</div>
|
|
616
|
-
`;
|
|
617
|
-
|
|
618
|
-
document.querySelector("#back-button").addEventListener("click", () => {
|
|
619
|
-
location.hash = "#/";
|
|
620
|
-
});
|
|
621
|
-
bindNavDrawer();
|
|
622
|
-
|
|
623
|
-
// Sidebar toggle for mobile
|
|
624
|
-
const sidebarToggle = document.querySelector("#sidebar-toggle");
|
|
625
|
-
const sidebar = document.querySelector("#terminal-sidebar");
|
|
626
|
-
const backdrop = document.querySelector("#sidebar-backdrop");
|
|
627
|
-
if (sidebarToggle && sidebar && backdrop) {
|
|
628
|
-
function toggleSidebar() {
|
|
629
|
-
const isOpen = sidebar.classList.toggle("open");
|
|
630
|
-
backdrop.classList.toggle("open", isOpen);
|
|
631
|
-
sidebarToggle.textContent = isOpen ? "\u2715" : "\u2630";
|
|
632
|
-
}
|
|
633
|
-
sidebarToggle.addEventListener("click", toggleSidebar);
|
|
634
|
-
backdrop.addEventListener("click", toggleSidebar);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
connectTerminal(sessionId);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function renderTerminalInfo(sessionId) {
|
|
641
|
-
const session = state.sessionMap.get(sessionId);
|
|
642
|
-
if (!session || route().name !== "terminal" || route().sessionId !== sessionId) {
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
const title = document.querySelector("#terminal-title");
|
|
646
|
-
const summary = document.querySelector("#terminal-summary");
|
|
647
|
-
const statePill = document.querySelector("#terminal-state");
|
|
648
|
-
const metadata = document.querySelector("#terminal-metadata");
|
|
649
|
-
const logs = document.querySelector("#terminal-logs");
|
|
650
|
-
if (title) title.textContent = session.title;
|
|
651
|
-
if (summary) summary.textContent = `${session.provider} \u00b7 ${session.displayState}`;
|
|
652
|
-
if (statePill) statePill.textContent = session.displayState;
|
|
653
|
-
if (metadata) metadata.innerHTML = renderTerminalMetadata(session);
|
|
654
|
-
if (logs) logs.textContent = (session.logs || []).slice(-60).join("\n") || "No logs captured yet.";
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function renderTerminalMetadata(session) {
|
|
658
|
-
if (!session) {
|
|
659
|
-
return `<dt>Status</dt><dd>Loading\u2026</dd>`;
|
|
660
|
-
}
|
|
661
|
-
let html = `
|
|
662
|
-
<dt>Provider</dt><dd>${escapeHtml(session.provider)}</dd>
|
|
663
|
-
<dt>Mode</dt><dd>${escapeHtml(session.mode)}</dd>
|
|
664
|
-
<dt>Transport</dt><dd>${escapeHtml(session.transport || "pty")}</dd>
|
|
665
|
-
<dt>Status</dt><dd>${escapeHtml(session.status)}</dd>
|
|
666
|
-
<dt>CWD</dt><dd><code>${escapeHtml(session.cwd)}</code></dd>
|
|
667
|
-
<dt>Command</dt><dd><code>${escapeHtml(session.command || "hooked session")}</code></dd>
|
|
668
|
-
<dt>PID</dt><dd>${session.pid || "\u2014"}</dd>
|
|
669
|
-
`;
|
|
670
|
-
if (session.meta && session.meta.tmuxSession) {
|
|
671
|
-
html += `<dt>tmux</dt><dd><code>${escapeHtml(session.meta.tmuxSession)}</code></dd>`;
|
|
672
|
-
}
|
|
673
|
-
if (session.meta && session.meta.localAttachCommand) {
|
|
674
|
-
html += `<dt>Attach</dt><dd><code>${escapeHtml(session.meta.localAttachCommand)}</code></dd>`;
|
|
675
|
-
}
|
|
676
|
-
return html;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function render() {
|
|
680
|
-
if (route().name === "terminal") {
|
|
681
|
-
renderTerminal();
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
renderOffice();
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function providerIcon(provider) {
|
|
688
|
-
if (provider === "claude") {
|
|
689
|
-
return `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>`;
|
|
690
|
-
}
|
|
691
|
-
if (provider === "codex") {
|
|
692
|
-
return `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0L19.2 12l-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>`;
|
|
693
|
-
}
|
|
694
|
-
return `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>`;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function colorByProvider(provider) {
|
|
698
|
-
if (provider === "claude") return "#7ea9d1";
|
|
699
|
-
if (provider === "codex") return "#d98f72";
|
|
700
|
-
return "#90b98b";
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function escapeHtml(value) {
|
|
704
|
-
return String(value || "")
|
|
705
|
-
.replaceAll("&", "&")
|
|
706
|
-
.replaceAll("<", "<")
|
|
707
|
-
.replaceAll(">", ">")
|
|
708
|
-
.replaceAll('"', """)
|
|
709
|
-
.replaceAll("'", "'");
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
render();
|
|
713
|
-
})();
|