aicodeman 1.6.0 → 1.6.2
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 +48 -12
- package/README.zh-CN.md +68 -12
- package/dist/web/public/admin-ui.js +303 -6
- package/dist/web/public/admin-ui.js.br +0 -0
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.b78b0465.js → app.9367fd20.js} +1 -1
- package/dist/web/public/app.9367fd20.js.br +0 -0
- package/dist/web/public/{app.b78b0465.js.gz → app.9367fd20.js.gz} +0 -0
- package/dist/web/public/constants.21a0a83e.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +8 -4
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
- package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
- package/dist/web/public/mobile.6a53092b.css +1 -0
- package/dist/web/public/mobile.6a53092b.css.br +0 -0
- package/dist/web/public/mobile.6a53092b.css.gz +0 -0
- package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/panels-ui.58582d4c.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-ui.0cf86dd4.js.gz +0 -0
- package/dist/web/public/settings-ui.bc5f4792.js.gz +0 -0
- package/dist/web/public/styles.e9dc275f.css +1 -0
- package/dist/web/public/styles.e9dc275f.css.br +0 -0
- package/dist/web/public/{styles.7295e6f4.css.gz → styles.e9dc275f.css.gz} +0 -0
- package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.9ea677e9.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/routes/admin-routes.d.ts +2 -0
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +91 -2
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.b78b0465.js.br +0 -0
- package/dist/web/public/mobile.597136a6.css +0 -1
- package/dist/web/public/mobile.597136a6.css.br +0 -0
- package/dist/web/public/mobile.597136a6.css.gz +0 -0
- package/dist/web/public/styles.7295e6f4.css +0 -1
- package/dist/web/public/styles.7295e6f4.css.br +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Multi-user frontend: identity boot, admin Users panel,
|
|
3
|
-
* change-password flow
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* @fileoverview Multi-user frontend: identity boot, admin Users panel, the
|
|
3
|
+
* change-password flow, and the full Admin Panel modal (user CRUD, per-user
|
|
4
|
+
* permissions, case-folder management) opened by the header Admin Panel button
|
|
5
|
+
* (#adminPanelBtn, revealed for admins in multi-user mode). Self-contained
|
|
6
|
+
* (builds its own DOM) so it needs no index.html surgery beyond the script tag
|
|
7
|
+
* and button; integrates with the existing App Settings modal by injecting a
|
|
8
|
+
* "Users" tab (admins in multi-user mode only). Live-refreshes on the SSE
|
|
9
|
+
* admin:usersChanged event (wired in app.js → window.codemanAdmin.onUsersChanged).
|
|
6
10
|
*
|
|
7
11
|
* @dependency app.js (window.app), settings-ui.js (App Settings modal + tab switch)
|
|
8
12
|
* @loadorder after settings-ui.js / ultracode-panel.js, before session-ui.js
|
|
@@ -123,7 +127,10 @@
|
|
|
123
127
|
content.innerHTML = `
|
|
124
128
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
125
129
|
<strong>Users</strong>
|
|
126
|
-
<
|
|
130
|
+
<span>
|
|
131
|
+
<button class="btn btn-sm" id="adminOpenPanel">Open Admin Panel</button>
|
|
132
|
+
<button class="btn btn-sm" id="adminAddUser">+ Add user</button>
|
|
133
|
+
</span>
|
|
127
134
|
</div>
|
|
128
135
|
<p class="form-hint">Users share the host account; this separates workspaces, it does not sandbox
|
|
129
136
|
users from each other. Pair with Docker cases for isolation.</p>
|
|
@@ -133,6 +140,7 @@
|
|
|
133
140
|
// Render whenever the tab is shown (the shared switchSettingsTab toggles it).
|
|
134
141
|
btn.addEventListener('click', renderUsers);
|
|
135
142
|
content.querySelector('#adminAddUser').onclick = addUserFlow;
|
|
143
|
+
content.querySelector('#adminOpenPanel').onclick = openAdminPanel;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
function esc(s) {
|
|
@@ -233,6 +241,292 @@
|
|
|
233
241
|
renderUsers();
|
|
234
242
|
}
|
|
235
243
|
|
|
244
|
+
// ── Admin Panel (big header-button modal) ─────────────────────────────────
|
|
245
|
+
let apModal = null;
|
|
246
|
+
let apUsersCache = [];
|
|
247
|
+
const apOpenDrawers = new Set(); // usernames with an expanded case-folder drawer
|
|
248
|
+
|
|
249
|
+
function fmtDate(ts) {
|
|
250
|
+
return ts ? new Date(ts).toLocaleString() : 'never';
|
|
251
|
+
}
|
|
252
|
+
function cssEsc(s) {
|
|
253
|
+
return window.CSS && window.CSS.escape ? window.CSS.escape(s) : String(s).replace(/"/g, '\\"');
|
|
254
|
+
}
|
|
255
|
+
function apSetMsg(t) {
|
|
256
|
+
const m = document.getElementById('apMsg');
|
|
257
|
+
if (m) m.textContent = t || '';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildAdminPanel() {
|
|
261
|
+
if (apModal) return apModal;
|
|
262
|
+
const el = document.createElement('div');
|
|
263
|
+
el.className = 'modal';
|
|
264
|
+
el.id = 'adminPanelModal';
|
|
265
|
+
el.style.zIndex = '3000';
|
|
266
|
+
el.innerHTML = `
|
|
267
|
+
<div class="modal-content" style="max-width:940px;width:min(96vw,940px)">
|
|
268
|
+
<div class="modal-header" style="display:flex;justify-content:space-between;align-items:center;gap:12px">
|
|
269
|
+
<h2 style="display:flex;align-items:center;gap:8px;margin:0">
|
|
270
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
271
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
272
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
273
|
+
Admin Panel</h2>
|
|
274
|
+
<span id="apIdentity" style="color:var(--text-muted,#888);font-size:.85em"></span>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="modal-body" style="max-height:70vh;overflow-y:auto">
|
|
277
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
278
|
+
<strong>Users</strong>
|
|
279
|
+
<button class="btn btn-sm btn-primary" id="apAddToggle">+ Add user</button>
|
|
280
|
+
</div>
|
|
281
|
+
<div id="apAddForm" style="display:none;border:1px solid var(--border,#333);border-radius:8px;padding:10px;margin-bottom:10px">
|
|
282
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
|
|
283
|
+
<div class="form-row" style="margin:0"><label>Username</label>
|
|
284
|
+
<input id="apNewName" class="form-input" placeholder="lowercase a-z 0-9 _ -" style="width:170px"></div>
|
|
285
|
+
<div class="form-row" style="margin:0"><label>Role</label>
|
|
286
|
+
<select id="apNewRole" class="form-input" style="width:110px">
|
|
287
|
+
<option value="user">user</option>
|
|
288
|
+
<option value="admin">admin</option>
|
|
289
|
+
</select></div>
|
|
290
|
+
<div class="form-row" style="margin:0"><label>Password (optional)</label>
|
|
291
|
+
<input id="apNewPw" type="password" class="form-input" placeholder="blank = one-time pw"
|
|
292
|
+
style="width:170px" autocomplete="new-password"></div>
|
|
293
|
+
<label style="display:flex;align-items:center;gap:5px;white-space:nowrap;margin-bottom:6px">
|
|
294
|
+
<input type="checkbox" id="apNewBypass"> allow bypass permissions</label>
|
|
295
|
+
<button class="btn btn-sm btn-primary" id="apCreateUser" style="margin-bottom:2px">Create</button>
|
|
296
|
+
</div>
|
|
297
|
+
<p class="form-hint" style="margin:6px 0 0">Without a password a one-time password is generated and shown
|
|
298
|
+
once; the user must change it on first login. "Bypass" allows shell sessions, cron launch commands, and
|
|
299
|
+
skip-permissions agents.</p>
|
|
300
|
+
</div>
|
|
301
|
+
<div id="apOtp" style="display:none;border:1px solid var(--accent,#38b6f0);border-radius:8px;padding:10px;margin-bottom:10px"></div>
|
|
302
|
+
<div id="apTable">Loading…</div>
|
|
303
|
+
<p class="form-hint" style="margin-top:10px">Users share the host OS account: this separates workspaces, it
|
|
304
|
+
does not sandbox users from each other. Pair with Docker cases for isolation.</p>
|
|
305
|
+
<p id="apMsg" style="min-height:1.2em;color:var(--text-muted,#888)"></p>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="modal-footer">
|
|
308
|
+
<button class="btn" id="apClose">Close</button>
|
|
309
|
+
</div>
|
|
310
|
+
</div>`;
|
|
311
|
+
document.body.appendChild(el);
|
|
312
|
+
el.querySelector('#apClose').onclick = () => (el.style.display = 'none');
|
|
313
|
+
el.addEventListener('click', (e) => {
|
|
314
|
+
if (e.target === el) el.style.display = 'none';
|
|
315
|
+
});
|
|
316
|
+
el.querySelector('#apAddToggle').onclick = () => {
|
|
317
|
+
const f = el.querySelector('#apAddForm');
|
|
318
|
+
f.style.display = f.style.display === 'none' ? '' : 'none';
|
|
319
|
+
if (f.style.display === '') f.querySelector('#apNewName').focus();
|
|
320
|
+
};
|
|
321
|
+
el.querySelector('#apCreateUser').onclick = createUserFromForm;
|
|
322
|
+
apModal = el;
|
|
323
|
+
return el;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function showOneTimePassword(username, otp) {
|
|
327
|
+
const box = document.getElementById('apOtp');
|
|
328
|
+
if (!box) return;
|
|
329
|
+
box.style.display = '';
|
|
330
|
+
box.innerHTML = `One-time password for <strong>${esc(username)}</strong> (shown once, copy it now):
|
|
331
|
+
<code style="user-select:all;font-size:1.05em;margin:0 8px">${esc(otp)}</code>
|
|
332
|
+
<button class="btn btn-xs" id="apOtpCopy">Copy</button>
|
|
333
|
+
<button class="btn btn-xs" id="apOtpDismiss">Dismiss</button>`;
|
|
334
|
+
box.querySelector('#apOtpCopy').onclick = () => {
|
|
335
|
+
if (navigator.clipboard) {
|
|
336
|
+
navigator.clipboard.writeText(otp).then(() => apSetMsg('Password copied to clipboard.'));
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
box.querySelector('#apOtpDismiss').onclick = () => {
|
|
340
|
+
box.style.display = 'none';
|
|
341
|
+
box.innerHTML = '';
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function createUserFromForm() {
|
|
346
|
+
const name = (document.getElementById('apNewName').value || '').trim().toLowerCase();
|
|
347
|
+
const role = document.getElementById('apNewRole').value;
|
|
348
|
+
const pw = document.getElementById('apNewPw').value;
|
|
349
|
+
const bypass = document.getElementById('apNewBypass').checked;
|
|
350
|
+
if (!name) return apSetMsg('Enter a username.');
|
|
351
|
+
const body = { username: name, role };
|
|
352
|
+
if (pw) body.password = pw;
|
|
353
|
+
if (bypass) body.canBypassPermissions = true;
|
|
354
|
+
const r = await apiSend('POST', '/api/admin/users', body);
|
|
355
|
+
if (!r.ok) return apSetMsg((r.body && r.body.error) || 'Create failed.');
|
|
356
|
+
document.getElementById('apNewName').value = '';
|
|
357
|
+
document.getElementById('apNewPw').value = '';
|
|
358
|
+
document.getElementById('apNewBypass').checked = false;
|
|
359
|
+
apSetMsg(`Created ${name}.`);
|
|
360
|
+
if (r.data && r.data.oneTimePassword) showOneTimePassword(name, r.data.oneTimePassword);
|
|
361
|
+
renderPanel();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function renderPanel() {
|
|
365
|
+
const table = document.getElementById('apTable');
|
|
366
|
+
if (!table) return;
|
|
367
|
+
let users;
|
|
368
|
+
try {
|
|
369
|
+
users = await apiGet('/api/admin/users');
|
|
370
|
+
} catch {
|
|
371
|
+
table.innerHTML = 'Failed to load users.';
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
apUsersCache = users;
|
|
375
|
+
const meName = (window.__codemanUser || {}).username;
|
|
376
|
+
const rows = users
|
|
377
|
+
.map((u) => {
|
|
378
|
+
const st = u.stats || {};
|
|
379
|
+
const you = u.username === meName ? ' <span style="color:var(--accent,#38b6f0)">(you)</span>' : '';
|
|
380
|
+
const role = `<span style="font-weight:600;color:${
|
|
381
|
+
u.role === 'admin' ? 'var(--accent,#38b6f0)' : 'var(--text-muted,#888)'
|
|
382
|
+
}">${u.role}</span>`;
|
|
383
|
+
const status = u.disabled
|
|
384
|
+
? '<span style="color:var(--red,#c33)">disabled</span>'
|
|
385
|
+
: '<span style="color:var(--accent-soft,#4b9)">enabled</span>';
|
|
386
|
+
const pwFlag = u.mustChangePassword ? ' · must-change-pw' : '';
|
|
387
|
+
return `<tr data-u="${esc(u.username)}">
|
|
388
|
+
<td><strong>${esc(u.username)}</strong>${you}</td>
|
|
389
|
+
<td>${role}</td>
|
|
390
|
+
<td>${status}${pwFlag}</td>
|
|
391
|
+
<td>${u.canBypassPermissions ? 'yes' : 'no'}</td>
|
|
392
|
+
<td style="white-space:nowrap">${st.liveSessions ?? 0} live · ${st.activeSessions ?? 0} logins ·
|
|
393
|
+
<button class="btn btn-xs" data-act="cases">${st.caseCount ?? 0} cases</button></td>
|
|
394
|
+
<td style="font-size:.85em;color:var(--text-muted,#888)">${fmtDate(u.lastLoginAt)}</td>
|
|
395
|
+
<td style="white-space:nowrap">
|
|
396
|
+
<button class="btn btn-xs" data-act="role">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>
|
|
397
|
+
<button class="btn btn-xs" data-act="disabled">${u.disabled ? 'Enable' : 'Disable'}</button>
|
|
398
|
+
<button class="btn btn-xs" data-act="bypass">${u.canBypassPermissions ? 'Revoke bypass' : 'Grant bypass'}</button>
|
|
399
|
+
<button class="btn btn-xs" data-act="reset">Reset pw</button>
|
|
400
|
+
<button class="btn btn-xs" data-act="logout">Logout</button>
|
|
401
|
+
<button class="btn btn-xs" data-act="delete" style="color:var(--red,#c33)">Delete</button>
|
|
402
|
+
</td></tr>
|
|
403
|
+
<tr data-drawer="${esc(u.username)}" style="display:none"><td colspan="7"></td></tr>`;
|
|
404
|
+
})
|
|
405
|
+
.join('');
|
|
406
|
+
table.innerHTML = `<table style="width:100%;border-collapse:collapse" class="admin-users">
|
|
407
|
+
<thead><tr>
|
|
408
|
+
<th align="left">User</th><th align="left">Role</th><th align="left">Status</th>
|
|
409
|
+
<th align="left">Bypass</th><th align="left">Activity</th><th align="left">Last login</th><th></th>
|
|
410
|
+
</tr></thead><tbody>${rows}</tbody></table>`;
|
|
411
|
+
table.querySelectorAll('button[data-act]').forEach((b) => {
|
|
412
|
+
const username = b.closest('tr').dataset.u;
|
|
413
|
+
b.onclick = () => {
|
|
414
|
+
if (b.dataset.act === 'cases') return toggleCaseDrawer(username);
|
|
415
|
+
return panelAction(
|
|
416
|
+
username,
|
|
417
|
+
b.dataset.act,
|
|
418
|
+
apUsersCache.find((x) => x.username === username)
|
|
419
|
+
);
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
// Re-open drawers that were expanded before this refresh.
|
|
423
|
+
for (const name of [...apOpenDrawers]) {
|
|
424
|
+
if (users.some((u) => u.username === name)) void renderCaseDrawer(name);
|
|
425
|
+
else apOpenDrawers.delete(name);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function panelAction(username, act, u) {
|
|
430
|
+
const path = `/api/admin/users/${encodeURIComponent(username)}`;
|
|
431
|
+
if (act === 'role') {
|
|
432
|
+
const r = await apiSend('PATCH', path, { role: u.role === 'admin' ? 'user' : 'admin' });
|
|
433
|
+
apSetMsg(r.ok ? `Updated ${username}.` : (r.body && r.body.error) || 'Failed.');
|
|
434
|
+
} else if (act === 'disabled') {
|
|
435
|
+
const r = await apiSend('PATCH', path, { disabled: !u.disabled });
|
|
436
|
+
apSetMsg(r.ok ? `Updated ${username}.` : (r.body && r.body.error) || 'Failed.');
|
|
437
|
+
} else if (act === 'bypass') {
|
|
438
|
+
const r = await apiSend('PATCH', path, { canBypassPermissions: !u.canBypassPermissions });
|
|
439
|
+
apSetMsg(r.ok ? `Updated ${username}.` : (r.body && r.body.error) || 'Failed.');
|
|
440
|
+
} else if (act === 'reset') {
|
|
441
|
+
if (!window.confirm(`Reset ${username}'s password? They must set a new one on next login.`)) return;
|
|
442
|
+
const r = await apiSend('POST', `${path}/reset-password`);
|
|
443
|
+
if (r.ok && r.data && r.data.oneTimePassword) showOneTimePassword(username, r.data.oneTimePassword);
|
|
444
|
+
else if (!r.ok) apSetMsg((r.body && r.body.error) || 'Reset failed.');
|
|
445
|
+
} else if (act === 'logout') {
|
|
446
|
+
const r = await apiSend('POST', `${path}/logout`);
|
|
447
|
+
apSetMsg(r.ok ? `Revoked ${(r.data && r.data.revoked) || 0} login session(s) for ${username}.` : 'Failed.');
|
|
448
|
+
} else if (act === 'delete') {
|
|
449
|
+
if (!window.confirm(`Delete user "${username}"? Their live sessions are killed and logins revoked.`)) return;
|
|
450
|
+
const deleteSpace = window.confirm(
|
|
451
|
+
`Also delete ${username}'s files (their cases/workspace folder)?\nOK = delete files too, Cancel = keep files on disk.`
|
|
452
|
+
);
|
|
453
|
+
const r = await apiSend('DELETE', path, { deleteSpace });
|
|
454
|
+
apSetMsg(r.ok ? `Deleted ${username}.` : (r.body && r.body.error) || 'Delete failed.');
|
|
455
|
+
}
|
|
456
|
+
renderPanel();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function toggleCaseDrawer(username) {
|
|
460
|
+
if (apOpenDrawers.has(username)) {
|
|
461
|
+
apOpenDrawers.delete(username);
|
|
462
|
+
const row = apModal && apModal.querySelector(`tr[data-drawer="${cssEsc(username)}"]`);
|
|
463
|
+
if (row) row.style.display = 'none';
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
apOpenDrawers.add(username);
|
|
467
|
+
await renderCaseDrawer(username);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function renderCaseDrawer(username) {
|
|
471
|
+
const row = apModal && apModal.querySelector(`tr[data-drawer="${cssEsc(username)}"]`);
|
|
472
|
+
if (!row) return;
|
|
473
|
+
row.style.display = '';
|
|
474
|
+
const cell = row.firstElementChild;
|
|
475
|
+
cell.innerHTML = 'Loading folders…';
|
|
476
|
+
let data;
|
|
477
|
+
try {
|
|
478
|
+
data = await apiGet(`/api/admin/users/${encodeURIComponent(username)}/cases`);
|
|
479
|
+
} catch {
|
|
480
|
+
cell.innerHTML = 'Failed to load case folders.';
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const items = (data.cases || [])
|
|
484
|
+
.map(
|
|
485
|
+
(c) => `
|
|
486
|
+
<li style="display:flex;gap:10px;align-items:center;padding:2px 0">
|
|
487
|
+
<code>${esc(c.name)}</code>
|
|
488
|
+
<span style="color:var(--text-muted,#888);font-size:.85em">${fmtDate(c.modifiedAt)}</span>
|
|
489
|
+
${c.liveSessions ? `<span style="color:var(--yellow,#ca0)">${c.liveSessions} live session(s)</span>` : ''}
|
|
490
|
+
<button class="btn btn-xs" data-case="${esc(c.name)}"
|
|
491
|
+
${c.liveSessions ? 'disabled title="In use by a live session"' : ''}>Delete</button>
|
|
492
|
+
</li>`
|
|
493
|
+
)
|
|
494
|
+
.join('');
|
|
495
|
+
cell.innerHTML = `<div style="padding:6px 4px 6px 16px">
|
|
496
|
+
<div style="color:var(--text-muted,#888);font-size:.85em;margin-bottom:4px">${esc(data.dir || '')}</div>
|
|
497
|
+
${items ? `<ul style="list-style:none;margin:0;padding:0">${items}</ul>` : 'No case folders yet.'}
|
|
498
|
+
</div>`;
|
|
499
|
+
cell.querySelectorAll('button[data-case]').forEach((b) => {
|
|
500
|
+
b.onclick = async () => {
|
|
501
|
+
const name = b.dataset.case;
|
|
502
|
+
if (!window.confirm(`Permanently delete ${username}'s case folder "${name}" and ALL files in it?`)) return;
|
|
503
|
+
const r = await apiSend(
|
|
504
|
+
'DELETE',
|
|
505
|
+
`/api/admin/users/${encodeURIComponent(username)}/cases/${encodeURIComponent(name)}`
|
|
506
|
+
);
|
|
507
|
+
apSetMsg(r.ok ? `Deleted folder ${name}.` : (r.body && r.body.error) || 'Delete failed.');
|
|
508
|
+
renderPanel();
|
|
509
|
+
};
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function openAdminPanel() {
|
|
514
|
+
const me = window.__codemanUser || {};
|
|
515
|
+
if (!me.multiUser || me.role !== 'admin') return;
|
|
516
|
+
const el = buildAdminPanel();
|
|
517
|
+
el.querySelector('#apIdentity').textContent = `signed in as ${me.username} (admin)`;
|
|
518
|
+
apSetMsg('');
|
|
519
|
+
el.style.display = 'flex';
|
|
520
|
+
renderPanel();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** SSE admin:usersChanged: live-refresh whichever admin views are visible. */
|
|
524
|
+
function onUsersChanged() {
|
|
525
|
+
if (apModal && apModal.style.display === 'flex') renderPanel();
|
|
526
|
+
const tab = document.getElementById('settings-users');
|
|
527
|
+
if (tab && !tab.classList.contains('hidden')) renderUsers();
|
|
528
|
+
}
|
|
529
|
+
|
|
236
530
|
// ── Boot ──────────────────────────────────────────────────────────────────
|
|
237
531
|
async function boot() {
|
|
238
532
|
installInterceptor();
|
|
@@ -247,6 +541,9 @@
|
|
|
247
541
|
if (window.__codemanUser.mustChangePassword) openChangePassword(true);
|
|
248
542
|
if (window.__codemanUser.multiUser && window.__codemanUser.role === 'admin') {
|
|
249
543
|
injectUsersTab();
|
|
544
|
+
// Reveal the big header Admin Panel button (template ships it hidden).
|
|
545
|
+
const btn = document.getElementById('adminPanelBtn');
|
|
546
|
+
if (btn) btn.classList.remove('btn-admin-panel--hidden');
|
|
250
547
|
}
|
|
251
548
|
}
|
|
252
549
|
|
|
@@ -256,5 +553,5 @@
|
|
|
256
553
|
boot();
|
|
257
554
|
}
|
|
258
555
|
|
|
259
|
-
window.codemanAdmin = { openChangePassword, renderUsers };
|
|
556
|
+
window.codemanAdmin = { openChangePassword, renderUsers, openAdminPanel, onUsersChanged };
|
|
260
557
|
})();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
`),id:_crashDiag._pageId}))}catch{}}if(setInterval(()=>{try{localStorage.setItem("codeman-crash-heartbeat",String(Date.now()))}catch{}_crashDiagBeacon()},2e3),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&_crashDiagBeacon()}),window.addEventListener("error",d=>{_crashDiag.log(`ERROR: ${d.message} at ${d.filename}:${d.lineno}`),console.error("[CRASH-DIAG] Uncaught error:",d.message,`
|
|
5
5
|
File:`,d.filename,":",d.lineno,":",d.colno,`
|
|
6
6
|
Stack:`,d.error?.stack)}),window.addEventListener("unhandledrejection",d=>{_crashDiag.log(`UNHANDLED: ${d.reason?.message||d.reason}`),console.error("[CRASH-DIAG] Unhandled promise rejection:",d.reason?.message||d.reason,`
|
|
7
|
-
Stack:`,d.reason?.stack)}),typeof PerformanceObserver<"u")try{new PerformanceObserver(e=>{for(const t of e.getEntries())t.duration>200&&(_crashDiag.log(`LONG_TASK: ${t.duration.toFixed(0)}ms`),console.warn(`[CRASH-DIAG] Long task: ${t.duration.toFixed(0)}ms (type: ${t.entryType}, name: ${t.name})`))}).observe({type:"longtask",buffered:!0})}catch{}const _origGetContext=HTMLCanvasElement.prototype.getContext;HTMLCanvasElement.prototype.getContext=function(d,...e){const t=_origGetContext.call(this,d,...e);return(d==="webgl2"||d==="webgl")&&(this.addEventListener("webglcontextlost",s=>{_crashDiag.log(`WEBGL_LOST: ${this.width}x${this.height}`),console.error("[CRASH-DIAG] WebGL context LOST on canvas",this.width,"x",this.height,"\u2014 prevented:",s.defaultPrevented)}),this.addEventListener("webglcontextrestored",()=>{_crashDiag.log("WEBGL_RESTORED"),console.warn("[CRASH-DIAG] WebGL context restored")})),t};const _SSE_HANDLER_MAP=[[SSE_EVENTS.INIT,"_onInit"],[SSE_EVENTS.SESSION_CREATED,"_onSessionCreated"],[SSE_EVENTS.SESSION_UPDATED,"_onSessionUpdated"],[SSE_EVENTS.SESSION_DELETED,"_onSessionDeleted"],[SSE_EVENTS.SESSION_TERMINAL,"_onSSETerminal"],[SSE_EVENTS.SESSION_NEEDS_REFRESH,"_onSSENeedsRefresh"],[SSE_EVENTS.SESSION_CLEAR_TERMINAL,"_onSSEClearTerminal"],[SSE_EVENTS.SESSION_COMPLETION,"_onSessionCompletion"],[SSE_EVENTS.SESSION_ERROR,"_onSessionError"],[SSE_EVENTS.SESSION_EXIT,"_onSessionExit"],[SSE_EVENTS.SESSION_IDLE,"_onSessionIdle"],[SSE_EVENTS.SESSION_WORKING,"_onSessionWorking"],[SSE_EVENTS.SESSION_AUTO_CLEAR,"_onSessionAutoClear"],[SSE_EVENTS.SESSION_LIMIT_PAUSE_SCHEDULED,"_onSessionLimitPauseScheduled"],[SSE_EVENTS.SESSION_LIMIT_RESUME,"_onSessionLimitResume"],[SSE_EVENTS.SESSION_LIMIT_RESUME_CANCELLED,"_onSessionLimitResumeCancelled"],[SSE_EVENTS.SESSION_RESPAWN_BREAKER_TRIPPED,"_onSessionRespawnBreakerTripped"],[SSE_EVENTS.SESSION_CLI_INFO,"_onSessionCliInfo"],[SSE_EVENTS.SESSION_STATUS_TELEMETRY,"_onSessionStatusTelemetry"],[SSE_EVENTS.SCHEDULED_CREATED,"_onScheduledCreated"],[SSE_EVENTS.SCHEDULED_UPDATED,"_onScheduledUpdated"],[SSE_EVENTS.SCHEDULED_COMPLETED,"_onScheduledCompleted"],[SSE_EVENTS.SCHEDULED_STOPPED,"_onScheduledStopped"],[SSE_EVENTS.CRON_JOBS_CHANGED,"_onCronJobsChanged"],[SSE_EVENTS.CRON_JOB_DELETED,"_onCronJobsChanged"],[SSE_EVENTS.CRON_RUN_CREATED,"_onCronRunChanged"],[SSE_EVENTS.CRON_RUN_UPDATED,"_onCronRunChanged"],[SSE_EVENTS.RESPAWN_STARTED,"_onRespawnStarted"],[SSE_EVENTS.RESPAWN_STOPPED,"_onRespawnStopped"],[SSE_EVENTS.RESPAWN_STATE_CHANGED,"_onRespawnStateChanged"],[SSE_EVENTS.RESPAWN_CYCLE_STARTED,"_onRespawnCycleStarted"],[SSE_EVENTS.RESPAWN_BLOCKED,"_onRespawnBlocked"],[SSE_EVENTS.RESPAWN_AUTO_ACCEPT_SENT,"_onRespawnAutoAcceptSent"],[SSE_EVENTS.RESPAWN_DETECTION_UPDATE,"_onRespawnDetectionUpdate"],[SSE_EVENTS.RESPAWN_TIMER_STARTED,"_onRespawnTimerStarted"],[SSE_EVENTS.RESPAWN_TIMER_CANCELLED,"_onRespawnTimerCancelled"],[SSE_EVENTS.RESPAWN_TIMER_COMPLETED,"_onRespawnTimerCompleted"],[SSE_EVENTS.RESPAWN_ERROR,"_onRespawnError"],[SSE_EVENTS.RESPAWN_ACTION_LOG,"_onRespawnActionLog"],[SSE_EVENTS.TASK_CREATED,"_onTaskCreated"],[SSE_EVENTS.TASK_COMPLETED,"_onTaskCompleted"],[SSE_EVENTS.TASK_FAILED,"_onTaskFailed"],[SSE_EVENTS.TASK_UPDATED,"_onTaskUpdated"],[SSE_EVENTS.MUX_CREATED,"_onMuxCreated"],[SSE_EVENTS.MUX_KILLED,"_onMuxKilled"],[SSE_EVENTS.MUX_DIED,"_onMuxDied"],[SSE_EVENTS.MUX_STATS_UPDATED,"_onMuxStatsUpdated"],[SSE_EVENTS.REMOTE_SESSION_RECONNECTED,"_onRemoteSessionReconnected"],[SSE_EVENTS.REMOTE_RECONNECT_EXHAUSTED,"_onRemoteReconnectExhausted"],[SSE_EVENTS.SESSION_RALPH_LOOP_UPDATE,"_onRalphLoopUpdate"],[SSE_EVENTS.SESSION_RALPH_TODO_UPDATE,"_onRalphTodoUpdate"],[SSE_EVENTS.SESSION_RALPH_COMPLETION_DETECTED,"_onRalphCompletionDetected"],[SSE_EVENTS.SESSION_RALPH_STATUS_UPDATE,"_onRalphStatusUpdate"],[SSE_EVENTS.SESSION_CIRCUIT_BREAKER_UPDATE,"_onCircuitBreakerUpdate"],[SSE_EVENTS.SESSION_EXIT_GATE_MET,"_onExitGateMet"],[SSE_EVENTS.SESSION_BASH_TOOL_START,"_onBashToolStart"],[SSE_EVENTS.SESSION_BASH_TOOL_END,"_onBashToolEnd"],[SSE_EVENTS.SESSION_BASH_TOOLS_UPDATE,"_onBashToolsUpdate"],[SSE_EVENTS.HOOK_IDLE_PROMPT,"_onHookIdlePrompt"],[SSE_EVENTS.HOOK_PERMISSION_PROMPT,"_onHookPermissionPrompt"],[SSE_EVENTS.HOOK_ELICITATION_DIALOG,"_onHookElicitationDialog"],[SSE_EVENTS.HOOK_STOP,"_onHookStop"],[SSE_EVENTS.HOOK_TEAMMATE_IDLE,"_onHookTeammateIdle"],[SSE_EVENTS.HOOK_TASK_COMPLETED,"_onHookTaskCompleted"],[SSE_EVENTS.SUBAGENT_DISCOVERED,"_onSubagentDiscovered"],[SSE_EVENTS.SUBAGENT_UPDATED,"_onSubagentUpdated"],[SSE_EVENTS.SUBAGENT_TOOL_CALL,"_onSubagentToolCall"],[SSE_EVENTS.SUBAGENT_PROGRESS,"_onSubagentProgress"],[SSE_EVENTS.SUBAGENT_MESSAGE,"_onSubagentMessage"],[SSE_EVENTS.SUBAGENT_TOOL_RESULT,"_onSubagentToolResult"],[SSE_EVENTS.SUBAGENT_COMPLETED,"_onSubagentCompleted"],[SSE_EVENTS.WORKFLOW_RUN_DISCOVERED,"_onWorkflowRunDiscovered"],[SSE_EVENTS.WORKFLOW_RUN_UPDATED,"_onWorkflowRunUpdated"],[SSE_EVENTS.WORKFLOW_RUN_REMOVED,"_onWorkflowRunRemoved"],[SSE_EVENTS.IMAGE_DETECTED,"_onImageDetected"],[SSE_EVENTS.ATTACHMENT_DETECTED,"_onAttachmentDetected"],[SSE_EVENTS.TUNNEL_STARTED,"_onTunnelStarted"],[SSE_EVENTS.TUNNEL_STOPPED,"_onTunnelStopped"],[SSE_EVENTS.TUNNEL_PROGRESS,"_onTunnelProgress"],[SSE_EVENTS.TUNNEL_ERROR,"_onTunnelError"],[SSE_EVENTS.TUNNEL_QR_ROTATED,"_onTunnelQrRotated"],[SSE_EVENTS.TUNNEL_QR_REGENERATED,"_onTunnelQrRegenerated"],[SSE_EVENTS.TUNNEL_QR_AUTH_USED,"_onTunnelQrAuthUsed"],[SSE_EVENTS.PLAN_SUBAGENT,"_onPlanSubagent"],[SSE_EVENTS.PLAN_PROGRESS,"_onPlanProgress"],[SSE_EVENTS.PLAN_STARTED,"_onPlanStarted"],[SSE_EVENTS.PLAN_CANCELLED,"_onPlanCancelled"],[SSE_EVENTS.PLAN_COMPLETED,"_onPlanCompleted"],[SSE_EVENTS.ORCHESTRATOR_STATE_CHANGED,"_onOrchestratorStateChanged"],[SSE_EVENTS.ORCHESTRATOR_PLAN_PROGRESS,"_onOrchestratorPlanProgress"],[SSE_EVENTS.ORCHESTRATOR_PLAN_READY,"_onOrchestratorPlanReady"],[SSE_EVENTS.ORCHESTRATOR_PHASE_STARTED,"_onOrchestratorPhaseStarted"],[SSE_EVENTS.ORCHESTRATOR_PHASE_COMPLETED,"_onOrchestratorPhaseCompleted"],[SSE_EVENTS.ORCHESTRATOR_PHASE_FAILED,"_onOrchestratorPhaseFailed"],[SSE_EVENTS.ORCHESTRATOR_VERIFICATION,"_onOrchestratorVerification"],[SSE_EVENTS.ORCHESTRATOR_TASK_ASSIGNED,"_onOrchestratorTaskAssigned"],[SSE_EVENTS.ORCHESTRATOR_TASK_COMPLETED,"_onOrchestratorTaskCompleted"],[SSE_EVENTS.ORCHESTRATOR_TASK_FAILED,"_onOrchestratorTaskFailed"],[SSE_EVENTS.ORCHESTRATOR_COMPLETED,"_onOrchestratorCompleted"],[SSE_EVENTS.ORCHESTRATOR_ERROR,"_onOrchestratorError"],[SSE_EVENTS.CLIPBOARD_WRITE,"_onClipboardWrite"],[SSE_EVENTS.SESSION_ORDER_CHANGED,"_onSessionOrderChanged"]];function parseSessionPrefix(d){if(!d)return null;const e=d.match(/^(w\d+-[a-zA-Z0-9_-]+|s\d+-[a-zA-Z0-9_-]+)/);if(!e)return null;const t=e[1],s=d.slice(t.length);return s===""?{prefix:t,suffix:""}:s.startsWith(": ")?{prefix:t,suffix:s.slice(2)}:null}const DEFAULT_SHORTCUTS=[{id:"show-shortcuts",group:"Panels",label:"Show Shortcuts",bindings:[{modifiers:["ctrl"],key:"?",code:"Slash"},{modifiers:["ctrl","shift"],key:"?"},{modifiers:["alt"],key:"?",code:"Slash"}],action:"showShortcutOverlay"},{id:"close-session",group:"Session",label:"Close Session",bindings:[{modifiers:["ctrl"],key:"w"}],action:"killActiveSession"},{id:"next-session",group:"Session",label:"Next Session",bindings:[{modifiers:["ctrl"],key:"Tab"}],action:"nextSession"},{id:"clear-terminal",group:"Terminal",label:"Clear Terminal",bindings:[{modifiers:["ctrl"],key:"l"}],action:"clearTerminal"},{id:"increase-font",group:"Terminal",label:"Increase Font",bindings:[{modifiers:["ctrl"],key:"=",code:"Equal"},{modifiers:["ctrl"],key:"+",code:"Equal"}],action:"increaseFontSize"},{id:"decrease-font",group:"Terminal",label:"Decrease Font",bindings:[{modifiers:["ctrl"],key:"-",code:"Minus"}],action:"decreaseFontSize"},{id:"voice-input",group:"Terminal",label:"Voice Input",bindings:[{modifiers:["ctrl","shift"],key:"V"}],action:"toggleVoiceInput"},{id:"restore-terminal-size",group:"Terminal",label:"Restore Terminal Size",bindings:[{modifiers:["ctrl","shift"],key:"R"}],action:"restoreTerminalSize"},{id:"move-tab-left",group:"Tabs",label:"Move Active Tab Left",bindings:[{modifiers:["ctrl","shift"],key:"{",code:"BracketLeft"}],action:"moveActiveTabLeft"},{id:"move-tab-right",group:"Tabs",label:"Move Active Tab Right",bindings:[{modifiers:["ctrl","shift"],key:"}",code:"BracketRight"}],action:"moveActiveTabRight"},{id:"command-palette",group:"Session",label:"Find Open Session",bindings:[{modifiers:["ctrl"],key:"k",code:"KeyK"},{modifiers:["meta"],key:"k",code:"KeyK"},{modifiers:["alt"],key:"k",code:"KeyK"}],action:"openCommandPalette"},{id:"previous-next-session",group:"Session",label:"Previous / Next Session",displayBindings:["Alt/Option+[","Alt/Option+]"]},{id:"switch-tab-n",group:"Session",label:"Switch to Tab N",displayBindings:["Alt/Option+1-9"]},{id:"focus-tabs",group:"Tabs",label:"Focus Tabs",displayBindings:["ArrowLeft","ArrowRight","Home","End"]},{id:"activate-focused-tab",group:"Tabs",label:"Activate Focused Tab",displayBindings:["Enter","Space"]},{id:"insert-newline",group:"Terminal",label:"Insert Newline",displayBindings:["Shift+Enter","Ctrl+Enter"]},{id:"close-panels",group:"Panels",label:"Close Panels",displayBindings:["Escape"]}];class CodemanApp{constructor(){this.sessions=new Map,this._shortIdCache=new Map,this.sessionOrder=[],this.draggedTabId=null,this.cases=[],this.currentRun=null,this.totalTokens=0,this.globalStats=null,this.eventSource=null,this._clientId=typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"c-"+Math.random().toString(36).slice(2)+Date.now().toString(36),this._wsTabNonce=this._clientId,this.terminal=null,this.fitAddon=null,this.activeSessionId=null,this.soloSessionId=this._detectSoloSessionId(),this.isSoloWindow=!!this.soloSessionId,this.detachedSessions=new Set,this.detachedWindows=new Map,this._detachWatchTimers=new Map,this.windowChannel=null,this._redockGrace=new Map,this._detachPingPending=null,this._detachLivenessTimer=null,this._detachOrphanStrikes=new Map,this._initGeneration=0,this._initFallbackTimer=null,this._selectGeneration=0,this._initialFullBufferLoad=!0,this.terminalLoadStates=new Map,this.respawnStatus={},this.respawnTimers={},this.respawnCountdownTimers={},this.respawnActionLogs={},this.timerCountdownInterval=null,this.terminalBuffers=new Map,this.editingSessionId=null,this.pendingCloseSessionId=null,this.muxSessions=[],this.ralphStates=new Map,this.subagents=new Map,this.subagentActivity=new Map,this.subagentToolResults=new Map,this.activeSubagentId=null,this.subagentPanelVisible=!1,this.workflowRuns=new Map,this.workflowRunDetails=new Map,this.activeWorkflowRunId=null,this.activeWorkflowPhaseIndex=null,this.ultracodeWindows=new Map,this.ultracodeWindowsClosed=new Set,this.ultracodeWindowCloseTimers=new Map,this.ultracodeWindowZIndex=1e3,this.subagentWindows=new Map,this.subagentWindowZIndex=ZINDEX_SUBAGENT_BASE,this.minimizedSubagents=new Map,this._subagentHideTimeout=null,this.subagentParentMap=new Map,this.teams=new Map,this.teamTasks=new Map,this.teammateMap=new Map,this.teammatePanesByName=new Map,this.teammateTerminals=new Map,this.terminalBufferCache=new Map,this.ralphStatePanelCollapsed=!0,this.ralphClosedSessions=new Set,this.planSubagents=new Map,this.planSubagentWindowZIndex=ZINDEX_PLAN_SUBAGENT_BASE,this.planGenerationStopped=!1,this.planAgentsMinimized=!1,this.wizardDragState=null,this.wizardDragListeners=null,this.wizardPosition=null,this.projectInsights=new Map,this.logViewerWindows=new Map,this.logViewerWindowZIndex=ZINDEX_LOG_VIEWER_BASE,this.projectInsightsPanelVisible=!1,this.orchestratorState=null,this.orchestratorPanelVisible=!1,this.currentSessionWorkingDir=null,this.imagePopups=new Map,this.imagePopupZIndex=ZINDEX_IMAGE_POPUP_BASE,this.attachmentCards=new Map,this.attachmentCardStack=null,this.attachmentHistoryCounts=new Map,this.attachmentHistoryItems=[],this.attachmentHistoryDrawerOpen=!1,this.fileBrowserData=null,this.fileBrowserExpandedDirs=new Set,this.fileBrowserFilter="",this.fileBrowserAllExpanded=!1,this.fileBrowserDragListeners=null,this.filePreviewContent="",this._toastContainer=null,this._tunnelUrl=null,this.tabAlerts=new Map,this.pendingHooks=new Map,this._ws=null,this._wsSessionId=null,this._wsReady=!1,this._wsState="disconnected",this._wsLastRecvAt=0,this.pendingWrites=[],this.writeFrameScheduled=!1,this._wasAtBottomBeforeWrite=!0,this.syncWaitTimeout=null,this._isLoadingBuffer=!1,this._loadBufferQueue=null,this._bufferLoadSeq=0,this._bufferLoadOwner=null,this.flickerFilterBuffer="",this.flickerFilterActive=!1,this.flickerFilterTimeout=null,this._debounceTimers=Object.create(null),this.systemStatsInterval=null,this.sseReconnectTimeout=null,this._sseListenerCleanup=null,this.reconnectAttempts=0,this.maxReconnectAttempts=10,this.isOnline=navigator.onLine,this._connectionStatus="connected",this._clientId="",this._seqCounters=new Map,this._pendingDeliveries=new Map,this._lastIndicatorDescriptor=null,this._postDraining=new Set,this._persistReliableTimer=null,this._reliableAckTimeoutMs=4e3,this._reliableMaxBytes=256*1024,this._loadReliableState(),this._reliableSweepTimer=setInterval(()=>this._redeliverSweep(),2e3),window.addEventListener("pagehide",()=>this._persistReliableNow()),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this._persistReliableNow()}),this._localEchoOverlay=null,this._localEchoEnabled=!1,this._restoringFlushedState=!1,this.activeFocusTrap=null,this.notificationManager=new NotificationManager(this),this.idleTimers=new Map,this._elemCache={},this.init()}$(e){return this._elemCache[e]||(this._elemCache[e]=document.getElementById(e)),this._elemCache[e]}_clearTimer(e){this[e]&&(clearTimeout(this[e]),this[e]=null)}_isStaleSelect(e){return e!==this._selectGeneration?(this._isLoadingBuffer&&this._finishBufferLoad(e),this._restoringFlushedState=!1,!0):!1}formatTokens(e){if(e>=1e6){const t=e/1e6;return t>=10?`${t.toFixed(1)}m`:`${t.toFixed(2)}m`}else if(e>=1e3){const t=e/1e3;return t>=100?`${t.toFixed(0)}k`:`${t.toFixed(1)}k`}return String(e)}estimateCost(e,t){const s=e/1e6*15,n=t/1e6*75;return s+n}setPendingHook(e,t){this.pendingHooks.has(e)||this.pendingHooks.set(e,new Set),this.pendingHooks.get(e).add(t),this.updateTabAlertFromHooks(e)}clearPendingHooks(e,t=null){const s=this.pendingHooks.get(e);s&&(t?s.delete(t):s.clear(),s.size===0&&this.pendingHooks.delete(e),this.updateTabAlertFromHooks(e))}updateTabAlertFromHooks(e){const t=this.pendingHooks.get(e);!t||t.size===0?this.tabAlerts.delete(e):t.has("permission_prompt")||t.has("elicitation_dialog")?this.tabAlerts.set(e,"action"):t.has("idle_prompt")&&this.tabAlerts.set(e,"idle"),this.renderSessionTabs()}init(){MobileDetection.init(),this._initWindowChannel(),this.isSoloWindow&&document.body.classList.add("solo-mode"),KeyboardHandler.init(),SwipeHandler.init(),VoiceInput.init(),KeyboardAccessoryBar.init(),this.loadAppSettingsFromStorage().extendedKeyboardBar&&KeyboardAccessoryBar.setMode("extended"),this.applyHeaderVisibilitySettings(),this.restorePlanUsageChip(),this.applySkin(),this.applyTabWrapSettings(),this.applyMonitorVisibility(),document.documentElement.classList.remove("mobile-init"),requestAnimationFrame(()=>{this.initTerminal(),this.loadFontSize(),this.connectSSE(),this._initFallbackTimer=setTimeout(()=>{this._initGeneration===0&&this.loadState()},3e3)}),this.registerServiceWorker(),this.loadTunnelStatus();const t=fetch("/api/settings").then(s=>s.ok?s.json():null).then(s=>s?.data??null).catch(()=>null);if(this.loadQuickStartCases(null,t),this._initRunMode(),this.setupEventListeners(),MobileDetection.isTouchDevice()){const s=n=>{n&&n.addEventListener("touchstart",i=>{if(!KeyboardHandler.keyboardVisible)return;const o=i.target.closest("button");o&&(i.preventDefault(),o.click(),typeof app<"u"&&app.terminal&&app.terminal.focus())},{passive:!1})};s(document.querySelector(".toolbar")),s(document.querySelector(".welcome-overlay"))}this.setupOnlineDetection(),this.loadAppSettingsFromServer(t).then(()=>{this.applyHeaderVisibilitySettings(),this.applySkin(),this.applyTabWrapSettings(),this.applyMonitorVisibility(),typeof this.syncAllUltracodeFloatingWindows=="function"&&this.syncAllUltracodeFloatingWindows()}),document.body.classList.add("app-loaded")}_initWebGL(){if(!(typeof WebglAddon>"u"))try{this._webglAddon=new WebglAddon.WebglAddon,this._webglAddon.onContextLoss(()=>{console.error("[CRASH-DIAG] WebGL context LOST \u2014 falling back to canvas renderer"),_crashDiag.log("WEBGL_LOST"),this._disableWebGLSticky("context-lost"),this._disposeWebGLObserver(),this._webglAddon?.dispose(),this._webglAddon=null,this._scheduleTerminalRepaint()}),this.terminal.loadAddon(this._webglAddon),console.log("[CRASH-DIAG] WebGL renderer enabled"),this._installWebGLLongTaskGuard()}catch{}}_installWebGLLongTaskGuard(){if(typeof PerformanceObserver>"u"||this._webglLongTaskObserver)return;const e=performance.now(),t=[];try{this._webglLongTaskObserver=new PerformanceObserver(s=>{if(!this._webglAddon)return;const n=performance.now();n-e<WEBGL_FALLBACK.GRACE_MS||evaluateWebGLLongTaskTrip(t,s.getEntries(),n)&&(console.warn(`[CRASH-DIAG] WebGL long-task threshold (${t.length} stalls/${WEBGL_FALLBACK.WINDOW_MS}ms) \u2014 falling back to canvas renderer`),_crashDiag.log(`WEBGL_FALLBACK: ${t.length}`),this._disableWebGLSticky("long-tasks"),this._disposeWebGLObserver(),this._webglAddon?.dispose(),this._webglAddon=null,this._scheduleTerminalRepaint())}),this._webglLongTaskObserver.observe({type:"longtask",buffered:!1})}catch{}}_disposeWebGLObserver(){if(this._webglLongTaskObserver){try{this._webglLongTaskObserver.disconnect()}catch{}this._webglLongTaskObserver=null}}_scheduleTerminalRepaint(){if(this._terminalRepaintScheduled)return;this._terminalRepaintScheduled=!0,(typeof requestAnimationFrame=="function"?requestAnimationFrame:t=>setTimeout(t,0))(()=>{this._terminalRepaintScheduled=!1;try{this.terminal?.refresh(0,this.terminal.rows-1)}catch{}})}_disableWebGLSticky(e){try{localStorage.setItem("codeman-webgl-disabled",JSON.stringify({reason:e,at:Date.now()}))}catch{}}setupEventListeners(){const e={showShortcutOverlay:()=>this.showShortcutOverlay(),killActiveSession:()=>this.killActiveSession(),nextSession:()=>this.nextSession(),clearTerminal:()=>this.clearTerminal(),restoreTerminalSize:()=>this.restoreTerminalSize(),increaseFontSize:()=>this.increaseFontSize(),decreaseFontSize:()=>this.decreaseFontSize(),toggleVoiceInput:()=>VoiceInput.toggle(),moveActiveTabLeft:()=>this.moveActiveTabLeft(),moveActiveTabRight:()=>this.moveActiveTabRight()};document.addEventListener("keydown",s=>{if(!(s.isComposing||s.keyCode===229)){if(this.shouldOpenCommandPaletteFromShortcut?.(s)){s.preventDefault(),this.openCommandPalette();return}if(s.key==="Escape"&&(this.closeAllPanels(),this.closeHelp(),this.attachmentHistoryDrawerOpen&&this.closeAttachmentHistory(),this.closeSessionManager(),this.closeCommandPalette?.(),this.closeShortcutOverlay?.()),s.altKey&&!s.ctrlKey&&!s.shiftKey){const i=(s.code||"").match(/^Digit([1-9])$/);if(i){const o=parseInt(i[1],10)-1;o<this.sessionOrder.length&&(s.preventDefault(),this.selectSession(this.sessionOrder[o]));return}if(s.code==="BracketLeft"){s.preventDefault(),this.prevSession();return}if(s.code==="BracketRight"){s.preventDefault(),this.nextSession();return}}if(!(!s.ctrlKey&&!s.metaKey&&!s.altKey))for(const n of this.getShortcutRegistry()){if(n.disabled||!n.action)continue;const i=e[n.action];if(i&&this.matchesShortcutEvent(s,n)){s.preventDefault(),i();return}}}},!0);const t=this.$("headerTokens");t&&!t._statsHandlerAttached&&(t.classList.add("clickable"),t._statsHandlerAttached=!0,t.addEventListener("click",()=>this.openTokenStats())),this.setupColorPicker()}_updateSseSubscription(e){try{const t=JSON.stringify({clientId:this._clientId,sessions:e?[e]:null});fetch("/api/events/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:t,keepalive:!0}).catch(()=>{})}catch{}}_detectSoloSessionId(){try{if(typeof window<"u"&&typeof window.__CODEMAN_SOLO__=="string"&&window.__CODEMAN_SOLO__)return window.__CODEMAN_SOLO__;const e=location.pathname.match(/^\/session\/([^/]+)\/?$/);return e?decodeURIComponent(e[1]):null}catch{return null}}detachSession(e){if(this.isSoloWindow||!this.sessions.has(e)||this.detachedSessions.has(e)&&this._raiseDetached(e))return;const t="width=960,height=680,menubar=no,toolbar=no,location=no,status=no";let s=null;try{s=window.open("/session/"+encodeURIComponent(e),"codeman-session-"+e,t)}catch{}if(!s){this.showToast?.("Pop-out blocked \u2014 allow popups for this site to detach a session","error");return}this.detachedWindows.set(e,s),this._markDetached(e,!0),this._watchDetachedWindow(e,s),this._postWindowMessage({type:"detached",id:e});try{s.focus()}catch{}}_raiseDetached(e){const t=this.detachedWindows.get(e);if(t&&!t.closed){try{t.focus()}catch{}return!0}return t&&t.closed?(this._redock(e),!1):(this._postWindowMessage({type:"focus-request",id:e}),!0)}redockSession(e){const t=this.detachedWindows.get(e);if(t&&!t.closed)try{t.close()}catch{}this._postWindowMessage({type:"close-request",id:e}),this._redock(e)}_redock(e){const t=this._detachWatchTimers.get(e);t&&(clearInterval(t),this._detachWatchTimers.delete(e)),this._cancelPendingRedock(e),this._detachOrphanStrikes.delete(e),this.detachedWindows.delete(e),this._markDetached(e,!1)}_scheduleRedock(e){if(this._redockGrace.has(e))return;const t=setTimeout(()=>{this._redockGrace.delete(e),this._redock(e)},1500);this._redockGrace.set(e,t)}_cancelPendingRedock(e){const t=this._redockGrace.get(e);t&&(clearTimeout(t),this._redockGrace.delete(e))}_markDetached(e,t){t?this.detachedSessions.add(e):this.detachedSessions.delete(e);const s=this.$("sessionTabs"),n=s&&s.querySelector(`.session-tab[data-id="${e}"]`);n&&n.classList.toggle("detached",t)}_watchDetachedWindow(e,t){const s=this._detachWatchTimers.get(e);s&&clearInterval(s);const n=setInterval(()=>{(!t||t.closed)&&(clearInterval(n),this._detachWatchTimers.delete(e),this._redock(e))},800);this._detachWatchTimers.set(e,n)}_initWindowChannel(){if(!(typeof BroadcastChannel>"u")){try{this.windowChannel=new BroadcastChannel("codeman-windows")}catch{this.windowChannel=null;return}if(this.windowChannel.onmessage=e=>this._onWindowMessage(e.data),this.isSoloWindow){this._postWindowMessage({type:"detached",id:this.soloSessionId});const e=()=>this._postWindowMessage({type:"redocked",id:this.soloSessionId});window.addEventListener("pagehide",e),window.addEventListener("beforeunload",e)}else this._postWindowMessage({type:"roll-call"}),this._startDetachLiveness()}}_postWindowMessage(e){try{this.windowChannel&&this.windowChannel.postMessage(e)}catch{}}_onWindowMessage(e){if(!(!e||typeof e!="object")){if(this.isSoloWindow){if(e.type==="roll-call"){this._postWindowMessage({type:"detached",id:this.soloSessionId});return}if(e.id!==this.soloSessionId)return;if(e.type==="close-request")try{window.close()}catch{}else if(e.type==="focus-request")try{window.focus()}catch{}return}e.type==="detached"&&e.id?(this._cancelPendingRedock(e.id),this._detachPingPending?.delete(e.id),this._detachOrphanStrikes.delete(e.id),this._markDetached(e.id,!0)):e.type==="redocked"&&e.id?this._scheduleRedock(e.id):e.type==="detach-request"&&e.id&&this.detachSession(e.id)}}_startDetachLiveness(){this._detachLivenessTimer||(this._detachLivenessTimer=setInterval(()=>this._pingDetached(),5e3))}_pingDetached(){const e=[];for(const t of this.detachedSessions){const s=this.detachedWindows.get(t);s?s.closed&&this._redock(t):e.push(t)}e.length&&(this._detachPingPending=new Set(e),this._postWindowMessage({type:"roll-call"}),setTimeout(()=>{if(this._detachPingPending){for(const t of this._detachPingPending){const s=(this._detachOrphanStrikes.get(t)||0)+1;s>=2?(this._detachOrphanStrikes.delete(t),this._redock(t)):this._detachOrphanStrikes.set(t,s)}this._detachPingPending=null}},1200))}_applySoloMode(){document.body.classList.add("solo-mode");const e=this.sessions.get(this.soloSessionId);if(!e){this._showSoloSessionGone();return}this.activeSessionId=null,this.selectSession(this.soloSessionId);const t=this.getSessionName(e)||"Session",s=document.getElementById("soloSessionTitle");s&&(s.textContent=t,s.style.display="");const n=document.getElementById("soloRedockBtn");n&&(n.style.display=""),document.title=t+" \u2014 Codeman",this.notificationManager&&(this.notificationManager.originalTitle=document.title);const i=document.querySelector(".header-brand .logo");i&&(i.onclick=o=>{o.preventDefault()})}_showSoloSessionGone(){if(document.body.classList.add("solo-mode"),document.querySelector(".solo-gone-overlay"))return;const e=document.createElement("div");e.className="solo-gone-overlay",e.innerHTML='<h2>Session unavailable</h2><p>This session has ended or is no longer available.</p><button class="btn-primary" onclick="window.close()">Close window</button>',document.body.appendChild(e),document.title="Session ended \u2014 Codeman"}connectSSE(){if(!navigator.onLine){this.setConnectionStatus("offline");return}this._clearTimer("sseReconnectTimeout"),this._sseListenerCleanup&&(this._sseListenerCleanup(),this._sseListenerCleanup=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.reconnectAttempts===0?this.setConnectionStatus("connecting"):this.setConnectionStatus("reconnecting");const e=new URLSearchParams({clientId:this._clientId});this.activeSessionId&&e.set("sessions",this.activeSessionId),this.eventSource=new EventSource(`/api/events?${e.toString()}`);const t=[],s=(n,i)=>{this.eventSource.addEventListener(n,i),t.push({event:n,handler:i})};if(this._sseListenerCleanup=()=>{for(const{event:n,handler:i}of t)this.eventSource&&this.eventSource.removeEventListener(n,i);t.length=0},this.eventSource.onopen=()=>{this.reconnectAttempts=0,this.setConnectionStatus("connected")},this.eventSource.onerror=()=>{this.reconnectAttempts++,this.reconnectAttempts>=this.maxReconnectAttempts?this.setConnectionStatus("disconnected"):this.setConnectionStatus("reconnecting"),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this._clearTimer("sseReconnectTimeout");const n=this.reconnectAttempts<=1?200:Math.min(500*Math.pow(2,this.reconnectAttempts-2),3e4);this.sseReconnectTimeout=setTimeout(()=>this.connectSSE(),n)},!this._sseHandlerWrappers){this._sseHandlerWrappers=new Map;for(const[n,i]of _SSE_HANDLER_MAP){const o=this[i];this._sseHandlerWrappers.set(n,a=>{try{o.call(this,a.data?JSON.parse(a.data):{})}catch(r){console.error(`[SSE] Error handling ${n}:`,r)}})}}for(const[n]of _SSE_HANDLER_MAP)s(n,this._sseHandlerWrappers.get(n));for(const n of[SSE_EVENTS.SESSION_CREATED,SSE_EVENTS.SESSION_DELETED])s(n,()=>this._onSessionListMaybeChanged());s(SSE_EVENTS.DOCKER_EXPORT_COMPLETE,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Docker export ready: ${i.bundle} (${Math.round((i.sizeBytes||0)/1e6)} MB)`,"success"),this.refreshDockerExports?.()}catch(i){console.error("[SSE] docker export complete:",i)}}),s(SSE_EVENTS.DOCKER_EXPORT_FAILED,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Docker export failed: ${i.error||"unknown error"}`,"error")}catch(i){console.error("[SSE] docker export failed:",i)}}),s(SSE_EVENTS.DOCKER_IMPORT_COMPLETE,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Docker bundle imported as case "${i.name}"`,"success"),this.loadQuickStartCases?.(),this.refreshDockerExports?.()}catch(i){console.error("[SSE] docker import complete:",i)}}),s(SSE_EVENTS.DOCKER_CONTAINER_RECREATED,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Container for "${i.name}" removed \u2014 next launch recreates it with the new config`,"info")}catch(i){console.error("[SSE] docker container recreated:",i)}}),s(SSE_EVENTS.DOCKER_IMAGE_BUILD_STARTED,()=>{this.showToast("Building the Codeman agent image (first Docker case, a few minutes)...","info",{duration:8e3})}),s(SSE_EVENTS.DOCKER_IMAGE_BUILD_COMPLETE,n=>{try{const i=n.data?JSON.parse(n.data):{};i.error?this.showToast(`Agent image build failed: ${i.error}`,"error"):this.showToast("Agent image ready. Starting the container...","success")}catch(i){console.error("[SSE] docker image build complete:",i)}}),s(SSE_EVENTS.DOCKER_IMAGE_BUILD_FAILED,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Agent image build failed: ${i.error||"unknown error"}`,"error")}catch(i){console.error("[SSE] docker image build failed:",i)}}),s(SSE_EVENTS.SESSION_PINNED,n=>{let i=null;try{i=JSON.parse(n.data)}catch{}if(i&&i.id){const o=this.sessions.get(i.id);o&&(o.pinned=i.pinned===!0,o.pinnedAt=i.pinned?i.pinnedAt:void 0)}this._onSessionListMaybeChanged()})}_onInit(e){_crashDiag.log(`INIT: ${e.sessions?.length||0} sessions`),this.handleInit(e)}_onSessionCreated(e){this.sessions.set(e.id,e),this.sessionOrder.includes(e.id)||(this.sessionOrder.push(e.id),this.saveSessionOrder()),this.renderSessionTabs(),this.updateCost(),this.sessions.size===1&&this.startSystemStatsPolling()}_onSessionUpdated(e){const t=e.session||e,s=this.sessions.get(t.id),n=t.claudeSessionId&&(!s||!s.claudeSessionId);this.sessions.set(t.id,t),this.renderSessionTabs(),this.updateCost(),t.id===this.activeSessionId&&t.tokens&&this.updateRespawnTokens(t.tokens),this.updateSubagentParentNames(t.id),n&&(this.recheckOrphanSubagents(),requestAnimationFrame(()=>{this.updateConnectionLines()}))}_onSessionDeleted(e){if(this._wsSessionId===e.id&&this._disconnectWs(),this.isSoloWindow&&e.id===this.soloSessionId&&this._showSoloSessionGone(),this.detachedSessions.has(e.id)&&this._redock(e.id),this._cleanupSessionData(e.id),this.activeSessionId===e.id){this.activeSessionId=null;try{localStorage.removeItem("codeman-active-session")}catch{}this.terminal.clear(),this.showWelcome()}this.renderSessionTabs(),this.renderRalphStatePanel(),this.renderProjectInsightsPanel(),this.sessions.size===0&&this.stopSystemStatsPolling()}_onSSETerminal(e){this._wsReady&&this._wsSessionId===e.id||this._onSessionTerminal(e)}_onSSENeedsRefresh(e){this._wsReady&&this._wsSessionId===e?.id||this._onSessionNeedsRefresh(e)}_onSSEClearTerminal(e){this._wsReady&&this._wsSessionId===e?.id||this._onSessionClearTerminal(e)}_onSessionTerminal(e){if(e.id===this.activeSessionId){if(e.data.length>32768&&_crashDiag.log(`TERMINAL: ${(e.data.length/1024).toFixed(0)}KB`),(this.pendingWrites?.reduce((s,n)=>s+n.length,0)||0)+(this.flickerFilterBuffer?.length||0)>131072){this._clientDropRecoveryTimer||(this._clientDropRecoveryTimer=setTimeout(()=>{this._clientDropRecoveryTimer=null,this._onSessionNeedsRefresh()},2e3));return}this.batchTerminalWrite(e.data)}}_sanitizeHtml(e){return typeof window<"u"&&typeof window.sanitizeMarkdownHtml=="function"?window.sanitizeMarkdownHtml(e):String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}_cleanTerminalBuffer(e){const t=e.replace(/\x1b\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]/g,"").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g,"").replace(/\x1b[PX^_][^\x1b]*\x1b\\/g,"").replace(/\x1b[NO()][A-Z0-9]?/g,"").replace(/\x1b[>=<78cDEHM]/g,"").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g,"").replace(/\r\n/g,`
|
|
7
|
+
Stack:`,d.reason?.stack)}),typeof PerformanceObserver<"u")try{new PerformanceObserver(e=>{for(const t of e.getEntries())t.duration>200&&(_crashDiag.log(`LONG_TASK: ${t.duration.toFixed(0)}ms`),console.warn(`[CRASH-DIAG] Long task: ${t.duration.toFixed(0)}ms (type: ${t.entryType}, name: ${t.name})`))}).observe({type:"longtask",buffered:!0})}catch{}const _origGetContext=HTMLCanvasElement.prototype.getContext;HTMLCanvasElement.prototype.getContext=function(d,...e){const t=_origGetContext.call(this,d,...e);return(d==="webgl2"||d==="webgl")&&(this.addEventListener("webglcontextlost",s=>{_crashDiag.log(`WEBGL_LOST: ${this.width}x${this.height}`),console.error("[CRASH-DIAG] WebGL context LOST on canvas",this.width,"x",this.height,"\u2014 prevented:",s.defaultPrevented)}),this.addEventListener("webglcontextrestored",()=>{_crashDiag.log("WEBGL_RESTORED"),console.warn("[CRASH-DIAG] WebGL context restored")})),t};const _SSE_HANDLER_MAP=[[SSE_EVENTS.INIT,"_onInit"],[SSE_EVENTS.SESSION_CREATED,"_onSessionCreated"],[SSE_EVENTS.SESSION_UPDATED,"_onSessionUpdated"],[SSE_EVENTS.SESSION_DELETED,"_onSessionDeleted"],[SSE_EVENTS.SESSION_TERMINAL,"_onSSETerminal"],[SSE_EVENTS.SESSION_NEEDS_REFRESH,"_onSSENeedsRefresh"],[SSE_EVENTS.SESSION_CLEAR_TERMINAL,"_onSSEClearTerminal"],[SSE_EVENTS.SESSION_COMPLETION,"_onSessionCompletion"],[SSE_EVENTS.SESSION_ERROR,"_onSessionError"],[SSE_EVENTS.SESSION_EXIT,"_onSessionExit"],[SSE_EVENTS.SESSION_IDLE,"_onSessionIdle"],[SSE_EVENTS.SESSION_WORKING,"_onSessionWorking"],[SSE_EVENTS.SESSION_AUTO_CLEAR,"_onSessionAutoClear"],[SSE_EVENTS.SESSION_LIMIT_PAUSE_SCHEDULED,"_onSessionLimitPauseScheduled"],[SSE_EVENTS.SESSION_LIMIT_RESUME,"_onSessionLimitResume"],[SSE_EVENTS.SESSION_LIMIT_RESUME_CANCELLED,"_onSessionLimitResumeCancelled"],[SSE_EVENTS.SESSION_RESPAWN_BREAKER_TRIPPED,"_onSessionRespawnBreakerTripped"],[SSE_EVENTS.SESSION_CLI_INFO,"_onSessionCliInfo"],[SSE_EVENTS.SESSION_STATUS_TELEMETRY,"_onSessionStatusTelemetry"],[SSE_EVENTS.SCHEDULED_CREATED,"_onScheduledCreated"],[SSE_EVENTS.SCHEDULED_UPDATED,"_onScheduledUpdated"],[SSE_EVENTS.SCHEDULED_COMPLETED,"_onScheduledCompleted"],[SSE_EVENTS.SCHEDULED_STOPPED,"_onScheduledStopped"],[SSE_EVENTS.CRON_JOBS_CHANGED,"_onCronJobsChanged"],[SSE_EVENTS.CRON_JOB_DELETED,"_onCronJobsChanged"],[SSE_EVENTS.CRON_RUN_CREATED,"_onCronRunChanged"],[SSE_EVENTS.CRON_RUN_UPDATED,"_onCronRunChanged"],[SSE_EVENTS.RESPAWN_STARTED,"_onRespawnStarted"],[SSE_EVENTS.RESPAWN_STOPPED,"_onRespawnStopped"],[SSE_EVENTS.RESPAWN_STATE_CHANGED,"_onRespawnStateChanged"],[SSE_EVENTS.RESPAWN_CYCLE_STARTED,"_onRespawnCycleStarted"],[SSE_EVENTS.RESPAWN_BLOCKED,"_onRespawnBlocked"],[SSE_EVENTS.RESPAWN_AUTO_ACCEPT_SENT,"_onRespawnAutoAcceptSent"],[SSE_EVENTS.RESPAWN_DETECTION_UPDATE,"_onRespawnDetectionUpdate"],[SSE_EVENTS.RESPAWN_TIMER_STARTED,"_onRespawnTimerStarted"],[SSE_EVENTS.RESPAWN_TIMER_CANCELLED,"_onRespawnTimerCancelled"],[SSE_EVENTS.RESPAWN_TIMER_COMPLETED,"_onRespawnTimerCompleted"],[SSE_EVENTS.RESPAWN_ERROR,"_onRespawnError"],[SSE_EVENTS.RESPAWN_ACTION_LOG,"_onRespawnActionLog"],[SSE_EVENTS.TASK_CREATED,"_onTaskCreated"],[SSE_EVENTS.TASK_COMPLETED,"_onTaskCompleted"],[SSE_EVENTS.TASK_FAILED,"_onTaskFailed"],[SSE_EVENTS.TASK_UPDATED,"_onTaskUpdated"],[SSE_EVENTS.MUX_CREATED,"_onMuxCreated"],[SSE_EVENTS.MUX_KILLED,"_onMuxKilled"],[SSE_EVENTS.MUX_DIED,"_onMuxDied"],[SSE_EVENTS.MUX_STATS_UPDATED,"_onMuxStatsUpdated"],[SSE_EVENTS.REMOTE_SESSION_RECONNECTED,"_onRemoteSessionReconnected"],[SSE_EVENTS.REMOTE_RECONNECT_EXHAUSTED,"_onRemoteReconnectExhausted"],[SSE_EVENTS.SESSION_RALPH_LOOP_UPDATE,"_onRalphLoopUpdate"],[SSE_EVENTS.SESSION_RALPH_TODO_UPDATE,"_onRalphTodoUpdate"],[SSE_EVENTS.SESSION_RALPH_COMPLETION_DETECTED,"_onRalphCompletionDetected"],[SSE_EVENTS.SESSION_RALPH_STATUS_UPDATE,"_onRalphStatusUpdate"],[SSE_EVENTS.SESSION_CIRCUIT_BREAKER_UPDATE,"_onCircuitBreakerUpdate"],[SSE_EVENTS.SESSION_EXIT_GATE_MET,"_onExitGateMet"],[SSE_EVENTS.SESSION_BASH_TOOL_START,"_onBashToolStart"],[SSE_EVENTS.SESSION_BASH_TOOL_END,"_onBashToolEnd"],[SSE_EVENTS.SESSION_BASH_TOOLS_UPDATE,"_onBashToolsUpdate"],[SSE_EVENTS.HOOK_IDLE_PROMPT,"_onHookIdlePrompt"],[SSE_EVENTS.HOOK_PERMISSION_PROMPT,"_onHookPermissionPrompt"],[SSE_EVENTS.HOOK_ELICITATION_DIALOG,"_onHookElicitationDialog"],[SSE_EVENTS.HOOK_STOP,"_onHookStop"],[SSE_EVENTS.HOOK_TEAMMATE_IDLE,"_onHookTeammateIdle"],[SSE_EVENTS.HOOK_TASK_COMPLETED,"_onHookTaskCompleted"],[SSE_EVENTS.SUBAGENT_DISCOVERED,"_onSubagentDiscovered"],[SSE_EVENTS.SUBAGENT_UPDATED,"_onSubagentUpdated"],[SSE_EVENTS.SUBAGENT_TOOL_CALL,"_onSubagentToolCall"],[SSE_EVENTS.SUBAGENT_PROGRESS,"_onSubagentProgress"],[SSE_EVENTS.SUBAGENT_MESSAGE,"_onSubagentMessage"],[SSE_EVENTS.SUBAGENT_TOOL_RESULT,"_onSubagentToolResult"],[SSE_EVENTS.SUBAGENT_COMPLETED,"_onSubagentCompleted"],[SSE_EVENTS.WORKFLOW_RUN_DISCOVERED,"_onWorkflowRunDiscovered"],[SSE_EVENTS.WORKFLOW_RUN_UPDATED,"_onWorkflowRunUpdated"],[SSE_EVENTS.WORKFLOW_RUN_REMOVED,"_onWorkflowRunRemoved"],[SSE_EVENTS.IMAGE_DETECTED,"_onImageDetected"],[SSE_EVENTS.ATTACHMENT_DETECTED,"_onAttachmentDetected"],[SSE_EVENTS.TUNNEL_STARTED,"_onTunnelStarted"],[SSE_EVENTS.TUNNEL_STOPPED,"_onTunnelStopped"],[SSE_EVENTS.TUNNEL_PROGRESS,"_onTunnelProgress"],[SSE_EVENTS.TUNNEL_ERROR,"_onTunnelError"],[SSE_EVENTS.TUNNEL_QR_ROTATED,"_onTunnelQrRotated"],[SSE_EVENTS.TUNNEL_QR_REGENERATED,"_onTunnelQrRegenerated"],[SSE_EVENTS.TUNNEL_QR_AUTH_USED,"_onTunnelQrAuthUsed"],[SSE_EVENTS.PLAN_SUBAGENT,"_onPlanSubagent"],[SSE_EVENTS.PLAN_PROGRESS,"_onPlanProgress"],[SSE_EVENTS.PLAN_STARTED,"_onPlanStarted"],[SSE_EVENTS.PLAN_CANCELLED,"_onPlanCancelled"],[SSE_EVENTS.PLAN_COMPLETED,"_onPlanCompleted"],[SSE_EVENTS.ORCHESTRATOR_STATE_CHANGED,"_onOrchestratorStateChanged"],[SSE_EVENTS.ORCHESTRATOR_PLAN_PROGRESS,"_onOrchestratorPlanProgress"],[SSE_EVENTS.ORCHESTRATOR_PLAN_READY,"_onOrchestratorPlanReady"],[SSE_EVENTS.ORCHESTRATOR_PHASE_STARTED,"_onOrchestratorPhaseStarted"],[SSE_EVENTS.ORCHESTRATOR_PHASE_COMPLETED,"_onOrchestratorPhaseCompleted"],[SSE_EVENTS.ORCHESTRATOR_PHASE_FAILED,"_onOrchestratorPhaseFailed"],[SSE_EVENTS.ORCHESTRATOR_VERIFICATION,"_onOrchestratorVerification"],[SSE_EVENTS.ORCHESTRATOR_TASK_ASSIGNED,"_onOrchestratorTaskAssigned"],[SSE_EVENTS.ORCHESTRATOR_TASK_COMPLETED,"_onOrchestratorTaskCompleted"],[SSE_EVENTS.ORCHESTRATOR_TASK_FAILED,"_onOrchestratorTaskFailed"],[SSE_EVENTS.ORCHESTRATOR_COMPLETED,"_onOrchestratorCompleted"],[SSE_EVENTS.ORCHESTRATOR_ERROR,"_onOrchestratorError"],[SSE_EVENTS.CLIPBOARD_WRITE,"_onClipboardWrite"],[SSE_EVENTS.SESSION_ORDER_CHANGED,"_onSessionOrderChanged"]];function parseSessionPrefix(d){if(!d)return null;const e=d.match(/^(w\d+-[a-zA-Z0-9_-]+|s\d+-[a-zA-Z0-9_-]+)/);if(!e)return null;const t=e[1],s=d.slice(t.length);return s===""?{prefix:t,suffix:""}:s.startsWith(": ")?{prefix:t,suffix:s.slice(2)}:null}const DEFAULT_SHORTCUTS=[{id:"show-shortcuts",group:"Panels",label:"Show Shortcuts",bindings:[{modifiers:["ctrl"],key:"?",code:"Slash"},{modifiers:["ctrl","shift"],key:"?"},{modifiers:["alt"],key:"?",code:"Slash"}],action:"showShortcutOverlay"},{id:"close-session",group:"Session",label:"Close Session",bindings:[{modifiers:["ctrl"],key:"w"}],action:"killActiveSession"},{id:"next-session",group:"Session",label:"Next Session",bindings:[{modifiers:["ctrl"],key:"Tab"}],action:"nextSession"},{id:"clear-terminal",group:"Terminal",label:"Clear Terminal",bindings:[{modifiers:["ctrl"],key:"l"}],action:"clearTerminal"},{id:"increase-font",group:"Terminal",label:"Increase Font",bindings:[{modifiers:["ctrl"],key:"=",code:"Equal"},{modifiers:["ctrl"],key:"+",code:"Equal"}],action:"increaseFontSize"},{id:"decrease-font",group:"Terminal",label:"Decrease Font",bindings:[{modifiers:["ctrl"],key:"-",code:"Minus"}],action:"decreaseFontSize"},{id:"voice-input",group:"Terminal",label:"Voice Input",bindings:[{modifiers:["ctrl","shift"],key:"V"}],action:"toggleVoiceInput"},{id:"restore-terminal-size",group:"Terminal",label:"Restore Terminal Size",bindings:[{modifiers:["ctrl","shift"],key:"R"}],action:"restoreTerminalSize"},{id:"move-tab-left",group:"Tabs",label:"Move Active Tab Left",bindings:[{modifiers:["ctrl","shift"],key:"{",code:"BracketLeft"}],action:"moveActiveTabLeft"},{id:"move-tab-right",group:"Tabs",label:"Move Active Tab Right",bindings:[{modifiers:["ctrl","shift"],key:"}",code:"BracketRight"}],action:"moveActiveTabRight"},{id:"command-palette",group:"Session",label:"Find Open Session",bindings:[{modifiers:["ctrl"],key:"k",code:"KeyK"},{modifiers:["meta"],key:"k",code:"KeyK"},{modifiers:["alt"],key:"k",code:"KeyK"}],action:"openCommandPalette"},{id:"previous-next-session",group:"Session",label:"Previous / Next Session",displayBindings:["Alt/Option+[","Alt/Option+]"]},{id:"switch-tab-n",group:"Session",label:"Switch to Tab N",displayBindings:["Alt/Option+1-9"]},{id:"focus-tabs",group:"Tabs",label:"Focus Tabs",displayBindings:["ArrowLeft","ArrowRight","Home","End"]},{id:"activate-focused-tab",group:"Tabs",label:"Activate Focused Tab",displayBindings:["Enter","Space"]},{id:"insert-newline",group:"Terminal",label:"Insert Newline",displayBindings:["Shift+Enter","Ctrl+Enter"]},{id:"close-panels",group:"Panels",label:"Close Panels",displayBindings:["Escape"]}];class CodemanApp{constructor(){this.sessions=new Map,this._shortIdCache=new Map,this.sessionOrder=[],this.draggedTabId=null,this.cases=[],this.currentRun=null,this.totalTokens=0,this.globalStats=null,this.eventSource=null,this._clientId=typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"c-"+Math.random().toString(36).slice(2)+Date.now().toString(36),this._wsTabNonce=this._clientId,this.terminal=null,this.fitAddon=null,this.activeSessionId=null,this.soloSessionId=this._detectSoloSessionId(),this.isSoloWindow=!!this.soloSessionId,this.detachedSessions=new Set,this.detachedWindows=new Map,this._detachWatchTimers=new Map,this.windowChannel=null,this._redockGrace=new Map,this._detachPingPending=null,this._detachLivenessTimer=null,this._detachOrphanStrikes=new Map,this._initGeneration=0,this._initFallbackTimer=null,this._selectGeneration=0,this._initialFullBufferLoad=!0,this.terminalLoadStates=new Map,this.respawnStatus={},this.respawnTimers={},this.respawnCountdownTimers={},this.respawnActionLogs={},this.timerCountdownInterval=null,this.terminalBuffers=new Map,this.editingSessionId=null,this.pendingCloseSessionId=null,this.muxSessions=[],this.ralphStates=new Map,this.subagents=new Map,this.subagentActivity=new Map,this.subagentToolResults=new Map,this.activeSubagentId=null,this.subagentPanelVisible=!1,this.workflowRuns=new Map,this.workflowRunDetails=new Map,this.activeWorkflowRunId=null,this.activeWorkflowPhaseIndex=null,this.ultracodeWindows=new Map,this.ultracodeWindowsClosed=new Set,this.ultracodeWindowCloseTimers=new Map,this.ultracodeWindowZIndex=1e3,this.subagentWindows=new Map,this.subagentWindowZIndex=ZINDEX_SUBAGENT_BASE,this.minimizedSubagents=new Map,this._subagentHideTimeout=null,this.subagentParentMap=new Map,this.teams=new Map,this.teamTasks=new Map,this.teammateMap=new Map,this.teammatePanesByName=new Map,this.teammateTerminals=new Map,this.terminalBufferCache=new Map,this.ralphStatePanelCollapsed=!0,this.ralphClosedSessions=new Set,this.planSubagents=new Map,this.planSubagentWindowZIndex=ZINDEX_PLAN_SUBAGENT_BASE,this.planGenerationStopped=!1,this.planAgentsMinimized=!1,this.wizardDragState=null,this.wizardDragListeners=null,this.wizardPosition=null,this.projectInsights=new Map,this.logViewerWindows=new Map,this.logViewerWindowZIndex=ZINDEX_LOG_VIEWER_BASE,this.projectInsightsPanelVisible=!1,this.orchestratorState=null,this.orchestratorPanelVisible=!1,this.currentSessionWorkingDir=null,this.imagePopups=new Map,this.imagePopupZIndex=ZINDEX_IMAGE_POPUP_BASE,this.attachmentCards=new Map,this.attachmentCardStack=null,this.attachmentHistoryCounts=new Map,this.attachmentHistoryItems=[],this.attachmentHistoryDrawerOpen=!1,this.fileBrowserData=null,this.fileBrowserExpandedDirs=new Set,this.fileBrowserFilter="",this.fileBrowserAllExpanded=!1,this.fileBrowserDragListeners=null,this.filePreviewContent="",this._toastContainer=null,this._tunnelUrl=null,this.tabAlerts=new Map,this.pendingHooks=new Map,this._ws=null,this._wsSessionId=null,this._wsReady=!1,this._wsState="disconnected",this._wsLastRecvAt=0,this.pendingWrites=[],this.writeFrameScheduled=!1,this._wasAtBottomBeforeWrite=!0,this.syncWaitTimeout=null,this._isLoadingBuffer=!1,this._loadBufferQueue=null,this._bufferLoadSeq=0,this._bufferLoadOwner=null,this.flickerFilterBuffer="",this.flickerFilterActive=!1,this.flickerFilterTimeout=null,this._debounceTimers=Object.create(null),this.systemStatsInterval=null,this.sseReconnectTimeout=null,this._sseListenerCleanup=null,this.reconnectAttempts=0,this.maxReconnectAttempts=10,this.isOnline=navigator.onLine,this._connectionStatus="connected",this._clientId="",this._seqCounters=new Map,this._pendingDeliveries=new Map,this._lastIndicatorDescriptor=null,this._postDraining=new Set,this._persistReliableTimer=null,this._reliableAckTimeoutMs=4e3,this._reliableMaxBytes=256*1024,this._loadReliableState(),this._reliableSweepTimer=setInterval(()=>this._redeliverSweep(),2e3),window.addEventListener("pagehide",()=>this._persistReliableNow()),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this._persistReliableNow()}),this._localEchoOverlay=null,this._localEchoEnabled=!1,this._restoringFlushedState=!1,this.activeFocusTrap=null,this.notificationManager=new NotificationManager(this),this.idleTimers=new Map,this._elemCache={},this.init()}$(e){return this._elemCache[e]||(this._elemCache[e]=document.getElementById(e)),this._elemCache[e]}_clearTimer(e){this[e]&&(clearTimeout(this[e]),this[e]=null)}_isStaleSelect(e){return e!==this._selectGeneration?(this._isLoadingBuffer&&this._finishBufferLoad(e),this._restoringFlushedState=!1,!0):!1}formatTokens(e){if(e>=1e6){const t=e/1e6;return t>=10?`${t.toFixed(1)}m`:`${t.toFixed(2)}m`}else if(e>=1e3){const t=e/1e3;return t>=100?`${t.toFixed(0)}k`:`${t.toFixed(1)}k`}return String(e)}estimateCost(e,t){const s=e/1e6*15,n=t/1e6*75;return s+n}setPendingHook(e,t){this.pendingHooks.has(e)||this.pendingHooks.set(e,new Set),this.pendingHooks.get(e).add(t),this.updateTabAlertFromHooks(e)}clearPendingHooks(e,t=null){const s=this.pendingHooks.get(e);s&&(t?s.delete(t):s.clear(),s.size===0&&this.pendingHooks.delete(e),this.updateTabAlertFromHooks(e))}updateTabAlertFromHooks(e){const t=this.pendingHooks.get(e);!t||t.size===0?this.tabAlerts.delete(e):t.has("permission_prompt")||t.has("elicitation_dialog")?this.tabAlerts.set(e,"action"):t.has("idle_prompt")&&this.tabAlerts.set(e,"idle"),this.renderSessionTabs()}init(){MobileDetection.init(),this._initWindowChannel(),this.isSoloWindow&&document.body.classList.add("solo-mode"),KeyboardHandler.init(),SwipeHandler.init(),VoiceInput.init(),KeyboardAccessoryBar.init(),this.loadAppSettingsFromStorage().extendedKeyboardBar&&KeyboardAccessoryBar.setMode("extended"),this.applyHeaderVisibilitySettings(),this.restorePlanUsageChip(),this.applySkin(),this.applyTabWrapSettings(),this.applyMonitorVisibility(),document.documentElement.classList.remove("mobile-init"),requestAnimationFrame(()=>{this.initTerminal(),this.loadFontSize(),this.connectSSE(),this._initFallbackTimer=setTimeout(()=>{this._initGeneration===0&&this.loadState()},3e3)}),this.registerServiceWorker(),this.loadTunnelStatus();const t=fetch("/api/settings").then(s=>s.ok?s.json():null).then(s=>s?.data??null).catch(()=>null);if(this.loadQuickStartCases(null,t),this._initRunMode(),this.setupEventListeners(),MobileDetection.isTouchDevice()){const s=n=>{n&&n.addEventListener("touchstart",i=>{if(!KeyboardHandler.keyboardVisible)return;const o=i.target.closest("button");o&&(i.preventDefault(),o.click(),typeof app<"u"&&app.terminal&&app.terminal.focus())},{passive:!1})};s(document.querySelector(".toolbar")),s(document.querySelector(".welcome-overlay"))}this.setupOnlineDetection(),this.loadAppSettingsFromServer(t).then(()=>{this.applyHeaderVisibilitySettings(),this.applySkin(),this.applyTabWrapSettings(),this.applyMonitorVisibility(),typeof this.syncAllUltracodeFloatingWindows=="function"&&this.syncAllUltracodeFloatingWindows()}),document.body.classList.add("app-loaded")}_initWebGL(){if(!(typeof WebglAddon>"u"))try{this._webglAddon=new WebglAddon.WebglAddon,this._webglAddon.onContextLoss(()=>{console.error("[CRASH-DIAG] WebGL context LOST \u2014 falling back to canvas renderer"),_crashDiag.log("WEBGL_LOST"),this._disableWebGLSticky("context-lost"),this._disposeWebGLObserver(),this._webglAddon?.dispose(),this._webglAddon=null,this._scheduleTerminalRepaint()}),this.terminal.loadAddon(this._webglAddon),console.log("[CRASH-DIAG] WebGL renderer enabled"),this._installWebGLLongTaskGuard()}catch{}}_installWebGLLongTaskGuard(){if(typeof PerformanceObserver>"u"||this._webglLongTaskObserver)return;const e=performance.now(),t=[];try{this._webglLongTaskObserver=new PerformanceObserver(s=>{if(!this._webglAddon)return;const n=performance.now();n-e<WEBGL_FALLBACK.GRACE_MS||evaluateWebGLLongTaskTrip(t,s.getEntries(),n)&&(console.warn(`[CRASH-DIAG] WebGL long-task threshold (${t.length} stalls/${WEBGL_FALLBACK.WINDOW_MS}ms) \u2014 falling back to canvas renderer`),_crashDiag.log(`WEBGL_FALLBACK: ${t.length}`),this._disableWebGLSticky("long-tasks"),this._disposeWebGLObserver(),this._webglAddon?.dispose(),this._webglAddon=null,this._scheduleTerminalRepaint())}),this._webglLongTaskObserver.observe({type:"longtask",buffered:!1})}catch{}}_disposeWebGLObserver(){if(this._webglLongTaskObserver){try{this._webglLongTaskObserver.disconnect()}catch{}this._webglLongTaskObserver=null}}_scheduleTerminalRepaint(){if(this._terminalRepaintScheduled)return;this._terminalRepaintScheduled=!0,(typeof requestAnimationFrame=="function"?requestAnimationFrame:t=>setTimeout(t,0))(()=>{this._terminalRepaintScheduled=!1;try{this.terminal?.refresh(0,this.terminal.rows-1)}catch{}})}_disableWebGLSticky(e){try{localStorage.setItem("codeman-webgl-disabled",JSON.stringify({reason:e,at:Date.now()}))}catch{}}setupEventListeners(){const e={showShortcutOverlay:()=>this.showShortcutOverlay(),killActiveSession:()=>this.killActiveSession(),nextSession:()=>this.nextSession(),clearTerminal:()=>this.clearTerminal(),restoreTerminalSize:()=>this.restoreTerminalSize(),increaseFontSize:()=>this.increaseFontSize(),decreaseFontSize:()=>this.decreaseFontSize(),toggleVoiceInput:()=>VoiceInput.toggle(),moveActiveTabLeft:()=>this.moveActiveTabLeft(),moveActiveTabRight:()=>this.moveActiveTabRight()};document.addEventListener("keydown",s=>{if(!(s.isComposing||s.keyCode===229)){if(this.shouldOpenCommandPaletteFromShortcut?.(s)){s.preventDefault(),this.openCommandPalette();return}if(s.key==="Escape"&&(this.closeAllPanels(),this.closeHelp(),this.attachmentHistoryDrawerOpen&&this.closeAttachmentHistory(),this.closeSessionManager(),this.closeCommandPalette?.(),this.closeShortcutOverlay?.()),s.altKey&&!s.ctrlKey&&!s.shiftKey){const i=(s.code||"").match(/^Digit([1-9])$/);if(i){const o=parseInt(i[1],10)-1;o<this.sessionOrder.length&&(s.preventDefault(),this.selectSession(this.sessionOrder[o]));return}if(s.code==="BracketLeft"){s.preventDefault(),this.prevSession();return}if(s.code==="BracketRight"){s.preventDefault(),this.nextSession();return}}if(!(!s.ctrlKey&&!s.metaKey&&!s.altKey))for(const n of this.getShortcutRegistry()){if(n.disabled||!n.action)continue;const i=e[n.action];if(i&&this.matchesShortcutEvent(s,n)){s.preventDefault(),i();return}}}},!0);const t=this.$("headerTokens");t&&!t._statsHandlerAttached&&(t.classList.add("clickable"),t._statsHandlerAttached=!0,t.addEventListener("click",()=>this.openTokenStats())),this.setupColorPicker()}_updateSseSubscription(e){try{const t=JSON.stringify({clientId:this._clientId,sessions:e?[e]:null});fetch("/api/events/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:t,keepalive:!0}).catch(()=>{})}catch{}}_detectSoloSessionId(){try{if(typeof window<"u"&&typeof window.__CODEMAN_SOLO__=="string"&&window.__CODEMAN_SOLO__)return window.__CODEMAN_SOLO__;const e=location.pathname.match(/^\/session\/([^/]+)\/?$/);return e?decodeURIComponent(e[1]):null}catch{return null}}detachSession(e){if(this.isSoloWindow||!this.sessions.has(e)||this.detachedSessions.has(e)&&this._raiseDetached(e))return;const t="width=960,height=680,menubar=no,toolbar=no,location=no,status=no";let s=null;try{s=window.open("/session/"+encodeURIComponent(e),"codeman-session-"+e,t)}catch{}if(!s){this.showToast?.("Pop-out blocked \u2014 allow popups for this site to detach a session","error");return}this.detachedWindows.set(e,s),this._markDetached(e,!0),this._watchDetachedWindow(e,s),this._postWindowMessage({type:"detached",id:e});try{s.focus()}catch{}}_raiseDetached(e){const t=this.detachedWindows.get(e);if(t&&!t.closed){try{t.focus()}catch{}return!0}return t&&t.closed?(this._redock(e),!1):(this._postWindowMessage({type:"focus-request",id:e}),!0)}redockSession(e){const t=this.detachedWindows.get(e);if(t&&!t.closed)try{t.close()}catch{}this._postWindowMessage({type:"close-request",id:e}),this._redock(e)}_redock(e){const t=this._detachWatchTimers.get(e);t&&(clearInterval(t),this._detachWatchTimers.delete(e)),this._cancelPendingRedock(e),this._detachOrphanStrikes.delete(e),this.detachedWindows.delete(e),this._markDetached(e,!1)}_scheduleRedock(e){if(this._redockGrace.has(e))return;const t=setTimeout(()=>{this._redockGrace.delete(e),this._redock(e)},1500);this._redockGrace.set(e,t)}_cancelPendingRedock(e){const t=this._redockGrace.get(e);t&&(clearTimeout(t),this._redockGrace.delete(e))}_markDetached(e,t){t?this.detachedSessions.add(e):this.detachedSessions.delete(e);const s=this.$("sessionTabs"),n=s&&s.querySelector(`.session-tab[data-id="${e}"]`);n&&n.classList.toggle("detached",t)}_watchDetachedWindow(e,t){const s=this._detachWatchTimers.get(e);s&&clearInterval(s);const n=setInterval(()=>{(!t||t.closed)&&(clearInterval(n),this._detachWatchTimers.delete(e),this._redock(e))},800);this._detachWatchTimers.set(e,n)}_initWindowChannel(){if(!(typeof BroadcastChannel>"u")){try{this.windowChannel=new BroadcastChannel("codeman-windows")}catch{this.windowChannel=null;return}if(this.windowChannel.onmessage=e=>this._onWindowMessage(e.data),this.isSoloWindow){this._postWindowMessage({type:"detached",id:this.soloSessionId});const e=()=>this._postWindowMessage({type:"redocked",id:this.soloSessionId});window.addEventListener("pagehide",e),window.addEventListener("beforeunload",e)}else this._postWindowMessage({type:"roll-call"}),this._startDetachLiveness()}}_postWindowMessage(e){try{this.windowChannel&&this.windowChannel.postMessage(e)}catch{}}_onWindowMessage(e){if(!(!e||typeof e!="object")){if(this.isSoloWindow){if(e.type==="roll-call"){this._postWindowMessage({type:"detached",id:this.soloSessionId});return}if(e.id!==this.soloSessionId)return;if(e.type==="close-request")try{window.close()}catch{}else if(e.type==="focus-request")try{window.focus()}catch{}return}e.type==="detached"&&e.id?(this._cancelPendingRedock(e.id),this._detachPingPending?.delete(e.id),this._detachOrphanStrikes.delete(e.id),this._markDetached(e.id,!0)):e.type==="redocked"&&e.id?this._scheduleRedock(e.id):e.type==="detach-request"&&e.id&&this.detachSession(e.id)}}_startDetachLiveness(){this._detachLivenessTimer||(this._detachLivenessTimer=setInterval(()=>this._pingDetached(),5e3))}_pingDetached(){const e=[];for(const t of this.detachedSessions){const s=this.detachedWindows.get(t);s?s.closed&&this._redock(t):e.push(t)}e.length&&(this._detachPingPending=new Set(e),this._postWindowMessage({type:"roll-call"}),setTimeout(()=>{if(this._detachPingPending){for(const t of this._detachPingPending){const s=(this._detachOrphanStrikes.get(t)||0)+1;s>=2?(this._detachOrphanStrikes.delete(t),this._redock(t)):this._detachOrphanStrikes.set(t,s)}this._detachPingPending=null}},1200))}_applySoloMode(){document.body.classList.add("solo-mode");const e=this.sessions.get(this.soloSessionId);if(!e){this._showSoloSessionGone();return}this.activeSessionId=null,this.selectSession(this.soloSessionId);const t=this.getSessionName(e)||"Session",s=document.getElementById("soloSessionTitle");s&&(s.textContent=t,s.style.display="");const n=document.getElementById("soloRedockBtn");n&&(n.style.display=""),document.title=t+" \u2014 Codeman",this.notificationManager&&(this.notificationManager.originalTitle=document.title);const i=document.querySelector(".header-brand .logo");i&&(i.onclick=o=>{o.preventDefault()})}_showSoloSessionGone(){if(document.body.classList.add("solo-mode"),document.querySelector(".solo-gone-overlay"))return;const e=document.createElement("div");e.className="solo-gone-overlay",e.innerHTML='<h2>Session unavailable</h2><p>This session has ended or is no longer available.</p><button class="btn-primary" onclick="window.close()">Close window</button>',document.body.appendChild(e),document.title="Session ended \u2014 Codeman"}connectSSE(){if(!navigator.onLine){this.setConnectionStatus("offline");return}this._clearTimer("sseReconnectTimeout"),this._sseListenerCleanup&&(this._sseListenerCleanup(),this._sseListenerCleanup=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.reconnectAttempts===0?this.setConnectionStatus("connecting"):this.setConnectionStatus("reconnecting");const e=new URLSearchParams({clientId:this._clientId});this.activeSessionId&&e.set("sessions",this.activeSessionId),this.eventSource=new EventSource(`/api/events?${e.toString()}`);const t=[],s=(n,i)=>{this.eventSource.addEventListener(n,i),t.push({event:n,handler:i})};if(this._sseListenerCleanup=()=>{for(const{event:n,handler:i}of t)this.eventSource&&this.eventSource.removeEventListener(n,i);t.length=0},this.eventSource.onopen=()=>{this.reconnectAttempts=0,this.setConnectionStatus("connected")},this.eventSource.onerror=()=>{this.reconnectAttempts++,this.reconnectAttempts>=this.maxReconnectAttempts?this.setConnectionStatus("disconnected"):this.setConnectionStatus("reconnecting"),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this._clearTimer("sseReconnectTimeout");const n=this.reconnectAttempts<=1?200:Math.min(500*Math.pow(2,this.reconnectAttempts-2),3e4);this.sseReconnectTimeout=setTimeout(()=>this.connectSSE(),n)},!this._sseHandlerWrappers){this._sseHandlerWrappers=new Map;for(const[n,i]of _SSE_HANDLER_MAP){const o=this[i];this._sseHandlerWrappers.set(n,a=>{try{o.call(this,a.data?JSON.parse(a.data):{})}catch(r){console.error(`[SSE] Error handling ${n}:`,r)}})}}for(const[n]of _SSE_HANDLER_MAP)s(n,this._sseHandlerWrappers.get(n));for(const n of[SSE_EVENTS.SESSION_CREATED,SSE_EVENTS.SESSION_DELETED])s(n,()=>this._onSessionListMaybeChanged());s(SSE_EVENTS.DOCKER_EXPORT_COMPLETE,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Docker export ready: ${i.bundle} (${Math.round((i.sizeBytes||0)/1e6)} MB)`,"success"),this.refreshDockerExports?.()}catch(i){console.error("[SSE] docker export complete:",i)}}),s(SSE_EVENTS.DOCKER_EXPORT_FAILED,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Docker export failed: ${i.error||"unknown error"}`,"error")}catch(i){console.error("[SSE] docker export failed:",i)}}),s(SSE_EVENTS.DOCKER_IMPORT_COMPLETE,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Docker bundle imported as case "${i.name}"`,"success"),this.loadQuickStartCases?.(),this.refreshDockerExports?.()}catch(i){console.error("[SSE] docker import complete:",i)}}),s(SSE_EVENTS.DOCKER_CONTAINER_RECREATED,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Container for "${i.name}" removed \u2014 next launch recreates it with the new config`,"info")}catch(i){console.error("[SSE] docker container recreated:",i)}}),s(SSE_EVENTS.ADMIN_USERS_CHANGED,()=>{window.codemanAdmin?.onUsersChanged?.()}),s(SSE_EVENTS.DOCKER_IMAGE_BUILD_STARTED,()=>{this.showToast("Building the Codeman agent image (first Docker case, a few minutes)...","info",{duration:8e3})}),s(SSE_EVENTS.DOCKER_IMAGE_BUILD_COMPLETE,n=>{try{const i=n.data?JSON.parse(n.data):{};i.error?this.showToast(`Agent image build failed: ${i.error}`,"error"):this.showToast("Agent image ready. Starting the container...","success")}catch(i){console.error("[SSE] docker image build complete:",i)}}),s(SSE_EVENTS.DOCKER_IMAGE_BUILD_FAILED,n=>{try{const i=n.data?JSON.parse(n.data):{};this.showToast(`Agent image build failed: ${i.error||"unknown error"}`,"error")}catch(i){console.error("[SSE] docker image build failed:",i)}}),s(SSE_EVENTS.SESSION_PINNED,n=>{let i=null;try{i=JSON.parse(n.data)}catch{}if(i&&i.id){const o=this.sessions.get(i.id);o&&(o.pinned=i.pinned===!0,o.pinnedAt=i.pinned?i.pinnedAt:void 0)}this._onSessionListMaybeChanged()})}_onInit(e){_crashDiag.log(`INIT: ${e.sessions?.length||0} sessions`),this.handleInit(e)}_onSessionCreated(e){this.sessions.set(e.id,e),this.sessionOrder.includes(e.id)||(this.sessionOrder.push(e.id),this.saveSessionOrder()),this.renderSessionTabs(),this.updateCost(),this.sessions.size===1&&this.startSystemStatsPolling()}_onSessionUpdated(e){const t=e.session||e,s=this.sessions.get(t.id),n=t.claudeSessionId&&(!s||!s.claudeSessionId);this.sessions.set(t.id,t),this.renderSessionTabs(),this.updateCost(),t.id===this.activeSessionId&&t.tokens&&this.updateRespawnTokens(t.tokens),this.updateSubagentParentNames(t.id),n&&(this.recheckOrphanSubagents(),requestAnimationFrame(()=>{this.updateConnectionLines()}))}_onSessionDeleted(e){if(this._wsSessionId===e.id&&this._disconnectWs(),this.isSoloWindow&&e.id===this.soloSessionId&&this._showSoloSessionGone(),this.detachedSessions.has(e.id)&&this._redock(e.id),this._cleanupSessionData(e.id),this.activeSessionId===e.id){this.activeSessionId=null;try{localStorage.removeItem("codeman-active-session")}catch{}this.terminal.clear(),this.showWelcome()}this.renderSessionTabs(),this.renderRalphStatePanel(),this.renderProjectInsightsPanel(),this.sessions.size===0&&this.stopSystemStatsPolling()}_onSSETerminal(e){this._wsReady&&this._wsSessionId===e.id||this._onSessionTerminal(e)}_onSSENeedsRefresh(e){this._wsReady&&this._wsSessionId===e?.id||this._onSessionNeedsRefresh(e)}_onSSEClearTerminal(e){this._wsReady&&this._wsSessionId===e?.id||this._onSessionClearTerminal(e)}_onSessionTerminal(e){if(e.id===this.activeSessionId){if(e.data.length>32768&&_crashDiag.log(`TERMINAL: ${(e.data.length/1024).toFixed(0)}KB`),(this.pendingWrites?.reduce((s,n)=>s+n.length,0)||0)+(this.flickerFilterBuffer?.length||0)>131072){this._clientDropRecoveryTimer||(this._clientDropRecoveryTimer=setTimeout(()=>{this._clientDropRecoveryTimer=null,this._onSessionNeedsRefresh()},2e3));return}this.batchTerminalWrite(e.data)}}_sanitizeHtml(e){return typeof window<"u"&&typeof window.sanitizeMarkdownHtml=="function"?window.sanitizeMarkdownHtml(e):String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}_cleanTerminalBuffer(e){const t=e.replace(/\x1b\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]/g,"").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g,"").replace(/\x1b[PX^_][^\x1b]*\x1b\\/g,"").replace(/\x1b[NO()][A-Z0-9]?/g,"").replace(/\x1b[>=<78cDEHM]/g,"").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g,"").replace(/\r\n/g,`
|
|
8
8
|
`).replace(/\r/g,`
|
|
9
9
|
`),s=[/^\s*❯\s*/,/^\s*[⏵⏺⏸⏹]+\s*/,/^\s*✻\s*(Crunching|Crunched|Thinking)/i,/bypass permissions/i,/\bshift\+tab to cycle\b/i,/^\s*focus\s*$/,/^\s*new task\?/i,/\/clear to save/i,/^\s*─{5,}\s*$/,/\[(Opus|Sonnet|Haiku|GPT|Claude)[\s\S]*(tokens?|\$|¥|%|↑|↓)/i,/^\s*\[\d+[km]?\/\d+[km]?\]/i,/[█░▓▒]{3,}/,/^\s*\(.*\s*(tokens?|context).*\)\s*$/i];return t.split(`
|
|
10
10
|
`).filter(o=>o.trim()?!s.some(r=>r.test(o)):!0).join(`
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
<link rel="manifest" href="manifest.json">
|
|
17
17
|
<title>Codeman</title>
|
|
18
18
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2360a5fa'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='6' fill='%230a0a0a'/%3E%3Cpath d='M18 4L8 18h6l-2 10 10-14h-6z' fill='url(%23g)'/%3E%3C/svg%3E">
|
|
19
|
-
<link rel="stylesheet" href="styles.
|
|
20
|
-
<link rel="stylesheet" href="mobile.
|
|
19
|
+
<link rel="stylesheet" href="styles.e9dc275f.css">
|
|
20
|
+
<link rel="stylesheet" href="mobile.6a53092b.css" media="(max-width: 1023px)">
|
|
21
21
|
<!-- xterm.css loaded async — terminal won't display until xterm.js runs anyway -->
|
|
22
22
|
<link rel="preload" href="vendor/xterm.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
|
23
23
|
<noscript><link rel="stylesheet" href="vendor/xterm.css"></noscript>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
instead of waiting until <script> tags at bottom-of-body are reached. -->
|
|
26
26
|
<link rel="preload" href="vendor/xterm.min.js" as="script">
|
|
27
27
|
<link rel="preload" href="constants.21a0a83e.js" as="script">
|
|
28
|
-
<link rel="preload" href="app.
|
|
28
|
+
<link rel="preload" href="app.9367fd20.js" as="script">
|
|
29
29
|
<!-- Self-hosted xterm.js — eliminates CDN DNS/TLS latency (~100ms).
|
|
30
30
|
'defer' preserves execution order (xterm loads before fit addon). -->
|
|
31
31
|
<script defer src="vendor/xterm.min.js"></script>
|
|
@@ -87,6 +87,10 @@
|
|
|
87
87
|
<div class="solo-session-title" id="soloSessionTitle" style="display: none;" aria-live="polite"></div>
|
|
88
88
|
|
|
89
89
|
<div class="header-right" id="headerRight">
|
|
90
|
+
<button class="btn-admin-panel btn-admin-panel--hidden" id="adminPanelBtn" onclick="window.codemanAdmin.openAdminPanel()" title="Admin Panel (multi-user administration)" aria-label="Open admin panel">
|
|
91
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
92
|
+
<span>Admin Panel</span>
|
|
93
|
+
</button>
|
|
90
94
|
<button class="btn-icon-header btn-solo-redock" id="soloRedockBtn" style="display: none;" onclick="window.close()" title="Re-dock to dashboard (close window)" aria-label="Re-dock session to dashboard">⊞</button>
|
|
91
95
|
<button class="tunnel-indicator" id="tunnelIndicator" style="display: none;" onclick="app.toggleTunnelPanel()" title="Cloudflare Tunnel" aria-label="Tunnel status">
|
|
92
96
|
<span class="tunnel-dot"></span>
|
|
@@ -2505,7 +2509,7 @@
|
|
|
2505
2509
|
<script defer src="input-cjk.63794d0b.js"></script>
|
|
2506
2510
|
<!-- Hardened markdown HTML sanitizer (wires DOMPurify). Must precede app.js. -->
|
|
2507
2511
|
<script defer src="sanitize-html.bc7078d6.js"></script>
|
|
2508
|
-
<script defer src="app.
|
|
2512
|
+
<script defer src="app.9367fd20.js"></script>
|
|
2509
2513
|
<script defer src="terminal-ui.9ea677e9.js"></script>
|
|
2510
2514
|
<script defer src="respawn-ui.2d249da9.js"></script>
|
|
2511
2515
|
<script defer src="ralph-panel.6de2d0f8.js"></script>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|