aicodeman 1.4.0 → 1.5.0

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.
Files changed (223) hide show
  1. package/README.md +36 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +168 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config/multiuser.d.ts +38 -0
  6. package/dist/config/multiuser.d.ts.map +1 -0
  7. package/dist/config/multiuser.js +58 -0
  8. package/dist/config/multiuser.js.map +1 -0
  9. package/dist/cron/cron-service.d.ts +1 -1
  10. package/dist/cron/cron-service.d.ts.map +1 -1
  11. package/dist/cron/cron-service.js +31 -4
  12. package/dist/cron/cron-service.js.map +1 -1
  13. package/dist/docker-hosts.d.ts +85 -5
  14. package/dist/docker-hosts.d.ts.map +1 -1
  15. package/dist/docker-hosts.js +260 -22
  16. package/dist/docker-hosts.js.map +1 -1
  17. package/dist/mux-interface.d.ts +6 -0
  18. package/dist/mux-interface.d.ts.map +1 -1
  19. package/dist/plan-orchestrator.d.ts +8 -1
  20. package/dist/plan-orchestrator.d.ts.map +1 -1
  21. package/dist/plan-orchestrator.js +20 -1
  22. package/dist/plan-orchestrator.js.map +1 -1
  23. package/dist/push-store.d.ts +17 -5
  24. package/dist/push-store.d.ts.map +1 -1
  25. package/dist/push-store.js +3 -1
  26. package/dist/push-store.js.map +1 -1
  27. package/dist/session-cli-builder.d.ts +1 -1
  28. package/dist/session-cli-builder.d.ts.map +1 -1
  29. package/dist/session-cli-builder.js +7 -2
  30. package/dist/session-cli-builder.js.map +1 -1
  31. package/dist/session.d.ts +9 -0
  32. package/dist/session.d.ts.map +1 -1
  33. package/dist/session.js +19 -2
  34. package/dist/session.js.map +1 -1
  35. package/dist/tmux-manager.d.ts +24 -2
  36. package/dist/tmux-manager.d.ts.map +1 -1
  37. package/dist/tmux-manager.js +60 -12
  38. package/dist/tmux-manager.js.map +1 -1
  39. package/dist/tunnel-manager.d.ts +17 -0
  40. package/dist/tunnel-manager.d.ts.map +1 -1
  41. package/dist/tunnel-manager.js +48 -8
  42. package/dist/tunnel-manager.js.map +1 -1
  43. package/dist/types/api.d.ts +10 -0
  44. package/dist/types/api.d.ts.map +1 -1
  45. package/dist/types/api.js +20 -0
  46. package/dist/types/api.js.map +1 -1
  47. package/dist/types/cron.d.ts +2 -0
  48. package/dist/types/cron.d.ts.map +1 -1
  49. package/dist/types/index.d.ts +1 -0
  50. package/dist/types/index.d.ts.map +1 -1
  51. package/dist/types/index.js +1 -0
  52. package/dist/types/index.js.map +1 -1
  53. package/dist/types/session.d.ts +8 -1
  54. package/dist/types/session.d.ts.map +1 -1
  55. package/dist/types/session.js.map +1 -1
  56. package/dist/types/user.d.ts +59 -0
  57. package/dist/types/user.d.ts.map +1 -0
  58. package/dist/types/user.js +12 -0
  59. package/dist/types/user.js.map +1 -0
  60. package/dist/user-store.d.ts +139 -0
  61. package/dist/user-store.d.ts.map +1 -0
  62. package/dist/user-store.js +433 -0
  63. package/dist/user-store.js.map +1 -0
  64. package/dist/web/admin-audit.d.ts +17 -0
  65. package/dist/web/admin-audit.d.ts.map +1 -0
  66. package/dist/web/admin-audit.js +19 -0
  67. package/dist/web/admin-audit.js.map +1 -0
  68. package/dist/web/middleware/auth.d.ts +8 -0
  69. package/dist/web/middleware/auth.d.ts.map +1 -1
  70. package/dist/web/middleware/auth.js +250 -51
  71. package/dist/web/middleware/auth.js.map +1 -1
  72. package/dist/web/ports/auth-port.d.ts +13 -0
  73. package/dist/web/ports/auth-port.d.ts.map +1 -1
  74. package/dist/web/ports/config-port.d.ts +4 -1
  75. package/dist/web/ports/config-port.d.ts.map +1 -1
  76. package/dist/web/ports/infra-port.d.ts +4 -1
  77. package/dist/web/ports/infra-port.d.ts.map +1 -1
  78. package/dist/web/public/admin-ui.js +260 -0
  79. package/dist/web/public/admin-ui.js.br +0 -0
  80. package/dist/web/public/admin-ui.js.gz +0 -0
  81. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  82. package/dist/web/public/{app.07e61fcf.js → app.6c4b9893.js} +1 -1
  83. package/dist/web/public/app.6c4b9893.js.br +0 -0
  84. package/dist/web/public/app.6c4b9893.js.gz +0 -0
  85. package/dist/web/public/{constants.503109fa.js → constants.5ce97313.js} +7 -0
  86. package/dist/web/public/constants.5ce97313.js.br +0 -0
  87. package/dist/web/public/{constants.503109fa.js.gz → constants.5ce97313.js.gz} +0 -0
  88. package/dist/web/public/cron-ui.js.gz +0 -0
  89. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  90. package/dist/web/public/index.html +24 -14
  91. package/dist/web/public/index.html.br +0 -0
  92. package/dist/web/public/index.html.gz +0 -0
  93. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  94. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  95. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  96. package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
  97. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  98. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  99. package/dist/web/public/{panels-ui.ca03853f.js → panels-ui.3c9b0465.js} +53 -53
  100. package/dist/web/public/panels-ui.3c9b0465.js.br +0 -0
  101. package/dist/web/public/panels-ui.3c9b0465.js.gz +0 -0
  102. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  103. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  104. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  105. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  106. package/dist/web/public/session-ui.4851c61a.js +58 -0
  107. package/dist/web/public/session-ui.4851c61a.js.br +0 -0
  108. package/dist/web/public/session-ui.4851c61a.js.gz +0 -0
  109. package/dist/web/public/settings-ui.29214cf1.js +67 -0
  110. package/dist/web/public/settings-ui.29214cf1.js.br +0 -0
  111. package/dist/web/public/settings-ui.29214cf1.js.gz +0 -0
  112. package/dist/web/public/styles.9e6cdb9d.css +1 -0
  113. package/dist/web/public/styles.9e6cdb9d.css.br +0 -0
  114. package/dist/web/public/styles.9e6cdb9d.css.gz +0 -0
  115. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  116. package/dist/web/public/sw.js.gz +0 -0
  117. package/dist/web/public/terminal-ui.efda6083.js.gz +0 -0
  118. package/dist/web/public/ultracode-panel.js.gz +0 -0
  119. package/dist/web/public/ultracode-windows.js.gz +0 -0
  120. package/dist/web/public/upload.html.gz +0 -0
  121. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  122. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  123. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  124. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  125. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  126. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  127. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  128. package/dist/web/public/vendor/xterm.css.gz +0 -0
  129. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  130. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  131. package/dist/web/route-helpers.d.ts +78 -1
  132. package/dist/web/route-helpers.d.ts.map +1 -1
  133. package/dist/web/route-helpers.js +172 -3
  134. package/dist/web/route-helpers.js.map +1 -1
  135. package/dist/web/routes/admin-routes.d.ts +20 -0
  136. package/dist/web/routes/admin-routes.d.ts.map +1 -0
  137. package/dist/web/routes/admin-routes.js +193 -0
  138. package/dist/web/routes/admin-routes.js.map +1 -0
  139. package/dist/web/routes/case-routes.d.ts.map +1 -1
  140. package/dist/web/routes/case-routes.js +202 -74
  141. package/dist/web/routes/case-routes.js.map +1 -1
  142. package/dist/web/routes/clipboard-routes.d.ts +2 -2
  143. package/dist/web/routes/clipboard-routes.d.ts.map +1 -1
  144. package/dist/web/routes/clipboard-routes.js +10 -0
  145. package/dist/web/routes/clipboard-routes.js.map +1 -1
  146. package/dist/web/routes/cron-routes.d.ts.map +1 -1
  147. package/dist/web/routes/cron-routes.js +57 -9
  148. package/dist/web/routes/cron-routes.js.map +1 -1
  149. package/dist/web/routes/file-routes.d.ts.map +1 -1
  150. package/dist/web/routes/file-routes.js +26 -20
  151. package/dist/web/routes/file-routes.js.map +1 -1
  152. package/dist/web/routes/index.d.ts +2 -0
  153. package/dist/web/routes/index.d.ts.map +1 -1
  154. package/dist/web/routes/index.js +2 -0
  155. package/dist/web/routes/index.js.map +1 -1
  156. package/dist/web/routes/me-routes.d.ts +16 -0
  157. package/dist/web/routes/me-routes.d.ts.map +1 -0
  158. package/dist/web/routes/me-routes.js +73 -0
  159. package/dist/web/routes/me-routes.js.map +1 -0
  160. package/dist/web/routes/mux-routes.d.ts.map +1 -1
  161. package/dist/web/routes/mux-routes.js +23 -5
  162. package/dist/web/routes/mux-routes.js.map +1 -1
  163. package/dist/web/routes/orchestrator-routes.d.ts.map +1 -1
  164. package/dist/web/routes/orchestrator-routes.js +43 -11
  165. package/dist/web/routes/orchestrator-routes.js.map +1 -1
  166. package/dist/web/routes/plan-routes.d.ts.map +1 -1
  167. package/dist/web/routes/plan-routes.js +28 -9
  168. package/dist/web/routes/plan-routes.js.map +1 -1
  169. package/dist/web/routes/push-routes.d.ts.map +1 -1
  170. package/dist/web/routes/push-routes.js +4 -0
  171. package/dist/web/routes/push-routes.js.map +1 -1
  172. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  173. package/dist/web/routes/ralph-routes.js +21 -17
  174. package/dist/web/routes/ralph-routes.js.map +1 -1
  175. package/dist/web/routes/respawn-routes.d.ts.map +1 -1
  176. package/dist/web/routes/respawn-routes.js +23 -14
  177. package/dist/web/routes/respawn-routes.js.map +1 -1
  178. package/dist/web/routes/scheduled-routes.d.ts.map +1 -1
  179. package/dist/web/routes/scheduled-routes.js +22 -6
  180. package/dist/web/routes/scheduled-routes.js.map +1 -1
  181. package/dist/web/routes/search-routes.d.ts.map +1 -1
  182. package/dist/web/routes/search-routes.js +9 -3
  183. package/dist/web/routes/search-routes.js.map +1 -1
  184. package/dist/web/routes/session-routes.d.ts.map +1 -1
  185. package/dist/web/routes/session-routes.js +209 -65
  186. package/dist/web/routes/session-routes.js.map +1 -1
  187. package/dist/web/routes/system-routes.d.ts.map +1 -1
  188. package/dist/web/routes/system-routes.js +108 -23
  189. package/dist/web/routes/system-routes.js.map +1 -1
  190. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  191. package/dist/web/routes/ws-routes.js +11 -0
  192. package/dist/web/routes/ws-routes.js.map +1 -1
  193. package/dist/web/schemas.d.ts +1 -0
  194. package/dist/web/schemas.d.ts.map +1 -1
  195. package/dist/web/schemas.js +3 -0
  196. package/dist/web/schemas.js.map +1 -1
  197. package/dist/web/server.d.ts +12 -0
  198. package/dist/web/server.d.ts.map +1 -1
  199. package/dist/web/server.js +187 -11
  200. package/dist/web/server.js.map +1 -1
  201. package/dist/web/sse-events.d.ts +18 -0
  202. package/dist/web/sse-events.d.ts.map +1 -1
  203. package/dist/web/sse-events.js +19 -0
  204. package/dist/web/sse-events.js.map +1 -1
  205. package/dist/web/sse-stream-manager.d.ts +32 -2
  206. package/dist/web/sse-stream-manager.d.ts.map +1 -1
  207. package/dist/web/sse-stream-manager.js +51 -4
  208. package/dist/web/sse-stream-manager.js.map +1 -1
  209. package/package.json +1 -1
  210. package/dist/web/public/app.07e61fcf.js.br +0 -0
  211. package/dist/web/public/app.07e61fcf.js.gz +0 -0
  212. package/dist/web/public/constants.503109fa.js.br +0 -0
  213. package/dist/web/public/panels-ui.ca03853f.js.br +0 -0
  214. package/dist/web/public/panels-ui.ca03853f.js.gz +0 -0
  215. package/dist/web/public/session-ui.9da1b71f.js +0 -58
  216. package/dist/web/public/session-ui.9da1b71f.js.br +0 -0
  217. package/dist/web/public/session-ui.9da1b71f.js.gz +0 -0
  218. package/dist/web/public/settings-ui.4acbe789.js +0 -67
  219. package/dist/web/public/settings-ui.4acbe789.js.br +0 -0
  220. package/dist/web/public/settings-ui.4acbe789.js.gz +0 -0
  221. package/dist/web/public/styles.546ddae4.css +0 -1
  222. package/dist/web/public/styles.546ddae4.css.br +0 -0
  223. package/dist/web/public/styles.546ddae4.css.gz +0 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * @fileoverview Multi-user frontend: identity boot, admin Users panel, and the
3
+ * change-password flow. Self-contained (builds its own DOM) so it needs no
4
+ * index.html surgery beyond the script tag and integrates with the existing App
5
+ * Settings modal by injecting a "Users" tab (admins in multi-user mode only).
6
+ *
7
+ * @dependency app.js (window.app), settings-ui.js (App Settings modal + tab switch)
8
+ * @loadorder after settings-ui.js / ultracode-panel.js, before session-ui.js
9
+ *
10
+ * In single-user mode GET /api/me returns a synthetic admin with multiUser:false,
11
+ * so none of the admin UI is shown and behavior is unchanged.
12
+ */
13
+ (function () {
14
+ 'use strict';
15
+
16
+ const unwrap = (body) => (body && typeof body === 'object' && 'data' in body ? body.data : body);
17
+
18
+ async function apiGet(path) {
19
+ const res = await window.fetch(path, { headers: { Accept: 'application/json' } });
20
+ return unwrap(await res.json());
21
+ }
22
+ async function apiSend(method, path, body) {
23
+ const res = await window.fetch(path, {
24
+ method,
25
+ headers: body ? { 'Content-Type': 'application/json' } : {},
26
+ body: body ? JSON.stringify(body) : undefined,
27
+ });
28
+ let json = null;
29
+ try {
30
+ json = await res.json();
31
+ } catch {
32
+ /* empty body */
33
+ }
34
+ return { ok: res.ok, status: res.status, body: json, data: unwrap(json) };
35
+ }
36
+
37
+ // ── Change-password modal ─────────────────────────────────────────────────
38
+ let cpModal = null;
39
+ function buildChangePasswordModal() {
40
+ if (cpModal) return cpModal;
41
+ const el = document.createElement('div');
42
+ el.className = 'modal';
43
+ el.id = 'changePasswordModal';
44
+ el.style.zIndex = '3100';
45
+ el.innerHTML = `
46
+ <div class="modal-content" style="max-width:420px">
47
+ <div class="modal-header"><h2>Change Password</h2></div>
48
+ <div class="modal-body">
49
+ <p id="cpMustNote" class="form-hint" style="display:none;color:var(--warning,#c80)">
50
+ You must change your password before continuing.</p>
51
+ <div class="form-row"><label>Current password</label>
52
+ <input type="password" id="cpCurrent" class="form-input" autocomplete="current-password"></div>
53
+ <div class="form-row"><label>New password (min 8)</label>
54
+ <input type="password" id="cpNew" class="form-input" autocomplete="new-password"></div>
55
+ <div class="form-row"><label>Confirm new password</label>
56
+ <input type="password" id="cpConfirm" class="form-input" autocomplete="new-password"></div>
57
+ <p id="cpError" style="color:var(--error,#c33);min-height:1.2em"></p>
58
+ </div>
59
+ <div class="modal-footer">
60
+ <button class="btn" id="cpCancel">Cancel</button>
61
+ <button class="btn btn-primary" id="cpSubmit">Change password</button>
62
+ </div>
63
+ </div>`;
64
+ document.body.appendChild(el);
65
+ el.querySelector('#cpCancel').onclick = () => (el.style.display = 'none');
66
+ el.querySelector('#cpSubmit').onclick = async () => {
67
+ const current = el.querySelector('#cpCurrent').value;
68
+ const nw = el.querySelector('#cpNew').value;
69
+ const confirm = el.querySelector('#cpConfirm').value;
70
+ const err = el.querySelector('#cpError');
71
+ err.textContent = '';
72
+ if (nw.length < 8) return (err.textContent = 'New password must be at least 8 characters.');
73
+ if (nw !== confirm) return (err.textContent = 'Passwords do not match.');
74
+ const r = await apiSend('POST', '/api/me/password', { currentPassword: current, newPassword: nw });
75
+ if (!r.ok) return (err.textContent = (r.body && r.body.error) || 'Change failed.');
76
+ el.style.display = 'none';
77
+ if (window.app && window.app.showToast) window.app.showToast('Password changed');
78
+ };
79
+ cpModal = el;
80
+ return el;
81
+ }
82
+ function openChangePassword(forced) {
83
+ const el = buildChangePasswordModal();
84
+ el.querySelector('#cpMustNote').style.display = forced ? '' : 'none';
85
+ el.querySelector('#cpCancel').style.display = forced ? 'none' : '';
86
+ el.querySelector('#cpError').textContent = '';
87
+ el.style.display = 'flex';
88
+ }
89
+
90
+ // ── Fetch interceptor: surface PASSWORD_CHANGE_REQUIRED ───────────────────
91
+ function installInterceptor() {
92
+ const orig = window.fetch;
93
+ window.fetch = async function (...args) {
94
+ const res = await orig.apply(this, args);
95
+ if (res.status === 403) {
96
+ try {
97
+ const clone = res.clone();
98
+ const j = await clone.json();
99
+ if (j && j.errorCode === 'PASSWORD_CHANGE_REQUIRED') openChangePassword(true);
100
+ } catch {
101
+ /* not JSON */
102
+ }
103
+ }
104
+ return res;
105
+ };
106
+ }
107
+
108
+ // ── Admin Users panel (injected into the App Settings modal) ──────────────
109
+ function injectUsersTab() {
110
+ const modal = document.getElementById('appSettingsModal');
111
+ if (!modal || modal.querySelector('[data-tab="settings-users"]')) return;
112
+ const tabs = modal.querySelector('.modal-tabs');
113
+ const body = modal.querySelector('.modal-body');
114
+ if (!tabs || !body) return;
115
+ const btn = document.createElement('button');
116
+ btn.className = 'modal-tab-btn';
117
+ btn.dataset.tab = 'settings-users';
118
+ btn.textContent = 'Users';
119
+ tabs.appendChild(btn);
120
+ const content = document.createElement('div');
121
+ content.className = 'modal-tab-content hidden';
122
+ content.id = 'settings-users';
123
+ content.innerHTML = `
124
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
125
+ <strong>Users</strong>
126
+ <button class="btn btn-sm" id="adminAddUser">+ Add user</button>
127
+ </div>
128
+ <p class="form-hint">Users share the host account; this separates workspaces, it does not sandbox
129
+ users from each other. Pair with Docker cases for isolation.</p>
130
+ <div id="adminUsersTable"></div>
131
+ <p id="adminUsersMsg" style="min-height:1.2em;color:var(--muted,#888)"></p>`;
132
+ body.appendChild(content);
133
+ // Render whenever the tab is shown (the shared switchSettingsTab toggles it).
134
+ btn.addEventListener('click', renderUsers);
135
+ content.querySelector('#adminAddUser').onclick = addUserFlow;
136
+ }
137
+
138
+ function esc(s) {
139
+ return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
140
+ }
141
+
142
+ async function renderUsers() {
143
+ const table = document.getElementById('adminUsersTable');
144
+ if (!table) return;
145
+ table.innerHTML = 'Loading…';
146
+ let users;
147
+ try {
148
+ users = await apiGet('/api/admin/users');
149
+ } catch {
150
+ table.innerHTML = 'Failed to load users.';
151
+ return;
152
+ }
153
+ const rows = users
154
+ .map((u) => {
155
+ const flags = [
156
+ u.role === 'admin' ? 'admin' : 'user',
157
+ u.disabled ? 'disabled' : 'enabled',
158
+ u.canBypassPermissions ? 'can-bypass' : '',
159
+ u.mustChangePassword ? 'must-change-pw' : '',
160
+ ]
161
+ .filter(Boolean)
162
+ .join(', ');
163
+ const st = u.stats || {};
164
+ return `<tr data-u="${esc(u.username)}">
165
+ <td>${esc(u.username)}</td>
166
+ <td style="font-size:.85em;color:var(--muted,#888)">${esc(flags)}</td>
167
+ <td style="font-size:.85em">${st.liveSessions ?? 0} live · ${st.caseCount ?? 0} cases</td>
168
+ <td style="white-space:nowrap">
169
+ <button class="btn btn-xs" data-act="role">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>
170
+ <button class="btn btn-xs" data-act="disabled">${u.disabled ? 'Enable' : 'Disable'}</button>
171
+ <button class="btn btn-xs" data-act="bypass">${u.canBypassPermissions ? 'Revoke bypass' : 'Grant bypass'}</button>
172
+ <button class="btn btn-xs" data-act="reset">Reset pw</button>
173
+ <button class="btn btn-xs" data-act="delete">Delete</button>
174
+ </td></tr>`;
175
+ })
176
+ .join('');
177
+ table.innerHTML = `<table style="width:100%;border-collapse:collapse" class="admin-users">
178
+ <thead><tr><th align="left">User</th><th align="left">Flags</th><th align="left">Usage</th><th></th></tr></thead>
179
+ <tbody>${rows}</tbody></table>`;
180
+ table.querySelectorAll('button[data-act]').forEach((b) => {
181
+ b.onclick = () =>
182
+ userAction(
183
+ b.closest('tr').dataset.u,
184
+ b.dataset.act,
185
+ users.find((x) => x.username === b.closest('tr').dataset.u)
186
+ );
187
+ });
188
+ }
189
+
190
+ function setMsg(t) {
191
+ const m = document.getElementById('adminUsersMsg');
192
+ if (m) m.textContent = t || '';
193
+ }
194
+
195
+ async function userAction(username, act, u) {
196
+ if (act === 'role') {
197
+ const r = await apiSend('PATCH', `/api/admin/users/${encodeURIComponent(username)}`, {
198
+ role: u.role === 'admin' ? 'user' : 'admin',
199
+ });
200
+ setMsg(r.ok ? `Updated ${username}.` : (r.body && r.body.error) || 'Failed.');
201
+ } else if (act === 'disabled') {
202
+ const r = await apiSend('PATCH', `/api/admin/users/${encodeURIComponent(username)}`, { disabled: !u.disabled });
203
+ setMsg(r.ok ? `Updated ${username}.` : (r.body && r.body.error) || 'Failed.');
204
+ } else if (act === 'bypass') {
205
+ const r = await apiSend('PATCH', `/api/admin/users/${encodeURIComponent(username)}`, {
206
+ canBypassPermissions: !u.canBypassPermissions,
207
+ });
208
+ setMsg(r.ok ? `Updated ${username}.` : (r.body && r.body.error) || 'Failed.');
209
+ } else if (act === 'reset') {
210
+ if (!window.confirm(`Reset ${username}'s password? They must set a new one on next login.`)) return;
211
+ const r = await apiSend('POST', `/api/admin/users/${encodeURIComponent(username)}/reset-password`);
212
+ if (r.ok && r.data && r.data.oneTimePassword) {
213
+ window.prompt(`One-time password for ${username} (copy it now — shown once):`, r.data.oneTimePassword);
214
+ } else setMsg((r.body && r.body.error) || 'Reset failed.');
215
+ } else if (act === 'delete') {
216
+ const typed = window.prompt(`Type "${username}" to delete this user. Add " +space" to also delete their files.`);
217
+ if (typed !== username && typed !== `${username} +space`) return setMsg('Delete cancelled.');
218
+ const deleteSpace = typed.endsWith(' +space');
219
+ const r = await apiSend('DELETE', `/api/admin/users/${encodeURIComponent(username)}`, { deleteSpace });
220
+ setMsg(r.ok ? `Deleted ${username}.` : (r.body && r.body.error) || 'Delete failed.');
221
+ }
222
+ renderUsers();
223
+ }
224
+
225
+ async function addUserFlow() {
226
+ const username = window.prompt('New username (lowercase, 2-32 chars, [a-z0-9_-]):');
227
+ if (!username) return;
228
+ const admin = window.confirm('Make this user an admin? (OK = admin, Cancel = regular user)');
229
+ const r = await apiSend('POST', '/api/admin/users', { username: username.trim(), role: admin ? 'admin' : 'user' });
230
+ if (r.ok && r.data && r.data.oneTimePassword) {
231
+ window.prompt(`Created ${username}. One-time password (copy it now — shown once):`, r.data.oneTimePassword);
232
+ } else setMsg((r.body && r.body.error) || 'Create failed.');
233
+ renderUsers();
234
+ }
235
+
236
+ // ── Boot ──────────────────────────────────────────────────────────────────
237
+ async function boot() {
238
+ installInterceptor();
239
+ let me = null;
240
+ try {
241
+ me = await apiGet('/api/me');
242
+ } catch {
243
+ /* server may be pre-auth */
244
+ }
245
+ window.__codemanUser = me || { username: 'admin', role: 'admin', multiUser: false };
246
+ document.dispatchEvent(new CustomEvent('codeman:me', { detail: window.__codemanUser }));
247
+ if (window.__codemanUser.mustChangePassword) openChangePassword(true);
248
+ if (window.__codemanUser.multiUser && window.__codemanUser.role === 'admin') {
249
+ injectUsersTab();
250
+ }
251
+ }
252
+
253
+ if (document.readyState === 'loading') {
254
+ document.addEventListener('DOMContentLoaded', boot);
255
+ } else {
256
+ boot();
257
+ }
258
+
259
+ window.codemanAdmin = { openChangePassword, renderUsers };
260
+ })();
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.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"]];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)}})}_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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}_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.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"]];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_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)}})}_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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}_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(`
@@ -477,6 +477,13 @@ const SSE_EVENTS = {
477
477
  DOCKER_EXPORT_COMPLETE: 'docker:exportComplete',
478
478
  DOCKER_EXPORT_FAILED: 'docker:exportFailed',
479
479
  DOCKER_IMPORT_COMPLETE: 'docker:importComplete',
480
+ DOCKER_IMAGE_BUILD_STARTED: 'docker:imageBuildStarted',
481
+ DOCKER_IMAGE_BUILD_PROGRESS: 'docker:imageBuildProgress',
482
+ DOCKER_IMAGE_BUILD_COMPLETE: 'docker:imageBuildComplete',
483
+ DOCKER_IMAGE_BUILD_FAILED: 'docker:imageBuildFailed',
484
+ // Multi-user (admin-only / targeted)
485
+ ADMIN_USERS_CHANGED: 'admin:usersChanged',
486
+ AUTH_PASSWORD_CHANGE_REQUIRED: 'auth:passwordChangeRequired',
480
487
  };
481
488
 
482
489
  // ═══════════════════════════════════════════════════════════════
Binary file
@@ -16,7 +16,7 @@
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.546ddae4.css">
19
+ <link rel="stylesheet" href="styles.9e6cdb9d.css">
20
20
  <link rel="stylesheet" href="mobile.0644e3ac.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'">
@@ -24,8 +24,8 @@
24
24
  <!-- Preload critical resources — lets browser discover these during HTML parse
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
- <link rel="preload" href="constants.503109fa.js" as="script">
28
- <link rel="preload" href="app.07e61fcf.js" as="script">
27
+ <link rel="preload" href="constants.5ce97313.js" as="script">
28
+ <link rel="preload" href="app.6c4b9893.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>
@@ -124,6 +124,7 @@
124
124
  <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="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
125
125
  <span class="attachment-history-badge" id="attachmentHistoryBadge" style="display:none;">0</span>
126
126
  </button>
127
+ <button class="btn-icon-header btn-file-viewer btn-file-viewer--hidden" onclick="app.toggleFileBrowserButton()" title="File Viewer" aria-label="Open file viewer" aria-expanded="false"><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="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></button>
127
128
  <button class="btn-icon-header btn-multimonitor btn-multimonitor--hidden" onclick="app.launchMultiMonitor()" title="Open Codeman across all displays" aria-label="Open Codeman across all displays"><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"><rect x="2" y="4" width="13" height="9" rx="1.5"/><rect x="11" y="9" width="11" height="8" rx="1.5"/></svg></button>
128
129
  <button class="btn-icon-header btn-ultracode-agents btn-ultracode-agents--hidden" onclick="app.toggleUltracodeAgentsPanel()" title="Ultracode / Workflow agents" aria-label="Open ultracode workflow agents"><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"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><path d="M8.2 7.2 15.6 11M8.2 16.8 15.6 13"/></svg></button>
129
130
  <div class="header-plan-usage header-plan-usage--hidden" id="planUsageChip" title="Claude plan usage limits">—</div>
@@ -1268,6 +1269,13 @@
1268
1269
  <span class="slider"></span>
1269
1270
  </label>
1270
1271
  </div>
1272
+ <div class="settings-item" title="Show the file viewer button in header (opens the file browser panel for the active session)">
1273
+ <span class="settings-item-label">File Viewer</span>
1274
+ <label class="switch switch-sm">
1275
+ <input type="checkbox" id="appSettingsShowFileViewerButton">
1276
+ <span class="slider"></span>
1277
+ </label>
1278
+ </div>
1271
1279
  <div class="settings-item" title="Show the attachments button in header (opens the attachment history drawer)">
1272
1280
  <span class="settings-item-label">Attachments Button</span>
1273
1281
  <label class="switch switch-sm">
@@ -1441,6 +1449,7 @@
1441
1449
  <label>Startup Mode</label>
1442
1450
  <select id="appSettingsClaudeMode" class="form-select">
1443
1451
  <option value="dangerously-skip-permissions">Skip Permissions (default)</option>
1452
+ <option value="auto">Auto (classifier-guarded, low prompts)</option>
1444
1453
  <option value="normal">Normal (with prompts)</option>
1445
1454
  <option value="allowedTools">Allowed Tools Only</option>
1446
1455
  </select>
@@ -1864,12 +1873,12 @@
1864
1873
  <label>Description (optional)</label>
1865
1874
  <input type="text" id="newCaseDescription" placeholder="A brief description..." autocomplete="off">
1866
1875
  </div>
1867
- <div class="form-row">
1868
- <label class="checkbox-row"><input type="checkbox" id="newCaseDocker" onchange="app.toggleDockerQuickSettings()"> 🐳 Run in an isolated Docker container</label>
1869
- <span class="form-hint">One checkbox is enough — it creates the case folder AND a hardened container with default settings, then starts the session inside it. Click to expand for optional presets. Requires the base image (<code>node scripts/build-agent-image.mjs</code>).</span>
1876
+ <div class="form-row docker-quick-row">
1877
+ <label class="checkbox-row"><input type="checkbox" id="newCaseDocker"> 🐳 Run in an isolated Docker container</label>
1878
+ <span class="form-hint">Runs this case in a hardened, isolated container. The base image is built automatically on first use.</span>
1870
1879
  </div>
1871
- <details class="advanced-options" id="dockerQuickSettings" style="display:none;">
1872
- <summary>Container settings (optional sensible defaults)</summary>
1880
+ <details class="advanced-options docker-quick-settings" id="dockerQuickSettings">
1881
+ <summary>Container settings (optional, sensible defaults)</summary>
1873
1882
  <div class="advanced-options-content">
1874
1883
  <div class="form-row">
1875
1884
  <label>Template</label>
@@ -1880,7 +1889,7 @@
1880
1889
  <option value="gpu">GPU — 8 GB RAM, 4 CPU, all GPUs</option>
1881
1890
  <option value="custom">Custom</option>
1882
1891
  </select>
1883
- <span class="form-hint">Disk is elastic storage grows automatically as data flows in (no fixed cap).</span>
1892
+ <span class="form-hint">Disk is elastic: storage grows automatically as data flows in (no fixed cap).</span>
1884
1893
  </div>
1885
1894
  <div class="form-row">
1886
1895
  <label>Memory</label>
@@ -2469,7 +2478,7 @@
2469
2478
  <!-- Lines drawn dynamically -->
2470
2479
  </svg>
2471
2480
 
2472
- <script defer src="constants.503109fa.js"></script>
2481
+ <script defer src="constants.5ce97313.js"></script>
2473
2482
  <script defer src="mobile-handlers.b5a667ef.js"></script>
2474
2483
  <script defer src="voice-input.085e9e73.js"></script>
2475
2484
  <script defer src="notification-manager.ca9dc40d.js"></script>
@@ -2477,16 +2486,17 @@
2477
2486
  <script defer src="input-cjk.63794d0b.js"></script>
2478
2487
  <!-- Hardened markdown HTML sanitizer (wires DOMPurify). Must precede app.js. -->
2479
2488
  <script defer src="sanitize-html.bc7078d6.js"></script>
2480
- <script defer src="app.07e61fcf.js"></script>
2489
+ <script defer src="app.6c4b9893.js"></script>
2481
2490
  <script defer src="terminal-ui.efda6083.js"></script>
2482
2491
  <script defer src="respawn-ui.2d249da9.js"></script>
2483
2492
  <script defer src="ralph-panel.6de2d0f8.js"></script>
2484
2493
  <script defer src="orchestrator-panel.js"></script>
2485
2494
  <script defer src="cron-ui.js"></script>
2486
- <script defer src="settings-ui.4acbe789.js"></script>
2487
- <script defer src="panels-ui.ca03853f.js"></script>
2495
+ <script defer src="settings-ui.29214cf1.js"></script>
2496
+ <script defer src="panels-ui.3c9b0465.js"></script>
2488
2497
  <script defer src="ultracode-panel.js"></script>
2489
- <script defer src="session-ui.9da1b71f.js"></script>
2498
+ <script defer src="admin-ui.js"></script>
2499
+ <script defer src="session-ui.4851c61a.js"></script>
2490
2500
  <script defer src="ralph-wizard.13a1831e.js"></script>
2491
2501
  <script defer src="api-client.c9b1cddc.js"></script>
2492
2502
  <script defer src="subagent-windows.69b60c35.js"></script>
Binary file
Binary file