aicodeman 0.5.2 → 0.5.4

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 (144) hide show
  1. package/dist/ai-checker-base.d.ts.map +1 -1
  2. package/dist/ai-checker-base.js +3 -2
  3. package/dist/ai-checker-base.js.map +1 -1
  4. package/dist/bash-tool-parser.d.ts +6 -0
  5. package/dist/bash-tool-parser.d.ts.map +1 -1
  6. package/dist/bash-tool-parser.js +87 -101
  7. package/dist/bash-tool-parser.js.map +1 -1
  8. package/dist/file-stream-manager.d.ts.map +1 -1
  9. package/dist/file-stream-manager.js +2 -1
  10. package/dist/file-stream-manager.js.map +1 -1
  11. package/dist/orchestrator-loop.d.ts +2 -0
  12. package/dist/orchestrator-loop.d.ts.map +1 -1
  13. package/dist/orchestrator-loop.js +27 -22
  14. package/dist/orchestrator-loop.js.map +1 -1
  15. package/dist/orchestrator-verifier.d.ts +1 -1
  16. package/dist/orchestrator-verifier.d.ts.map +1 -1
  17. package/dist/orchestrator-verifier.js +3 -2
  18. package/dist/orchestrator-verifier.js.map +1 -1
  19. package/dist/plan-orchestrator.d.ts +4 -1
  20. package/dist/plan-orchestrator.d.ts.map +1 -1
  21. package/dist/plan-orchestrator.js +66 -88
  22. package/dist/plan-orchestrator.js.map +1 -1
  23. package/dist/ralph-status-parser.d.ts +2 -0
  24. package/dist/ralph-status-parser.d.ts.map +1 -1
  25. package/dist/ralph-status-parser.js +98 -102
  26. package/dist/ralph-status-parser.js.map +1 -1
  27. package/dist/ralph-tracker.d.ts +9 -0
  28. package/dist/ralph-tracker.d.ts.map +1 -1
  29. package/dist/ralph-tracker.js +52 -60
  30. package/dist/ralph-tracker.js.map +1 -1
  31. package/dist/respawn-controller.d.ts +18 -1
  32. package/dist/respawn-controller.d.ts.map +1 -1
  33. package/dist/respawn-controller.js +215 -181
  34. package/dist/respawn-controller.js.map +1 -1
  35. package/dist/session-auto-ops.d.ts.map +1 -1
  36. package/dist/session-auto-ops.js +57 -55
  37. package/dist/session-auto-ops.js.map +1 -1
  38. package/dist/session.d.ts +5 -0
  39. package/dist/session.d.ts.map +1 -1
  40. package/dist/session.js +182 -218
  41. package/dist/session.js.map +1 -1
  42. package/dist/state-store.d.ts +6 -0
  43. package/dist/state-store.d.ts.map +1 -1
  44. package/dist/state-store.js +67 -79
  45. package/dist/state-store.js.map +1 -1
  46. package/dist/subagent-watcher.d.ts +24 -0
  47. package/dist/subagent-watcher.d.ts.map +1 -1
  48. package/dist/subagent-watcher.js +215 -220
  49. package/dist/subagent-watcher.js.map +1 -1
  50. package/dist/tmux-manager.d.ts +17 -0
  51. package/dist/tmux-manager.d.ts.map +1 -1
  52. package/dist/tmux-manager.js +57 -66
  53. package/dist/tmux-manager.js.map +1 -1
  54. package/dist/tunnel-manager.d.ts.map +1 -1
  55. package/dist/tunnel-manager.js +2 -1
  56. package/dist/tunnel-manager.js.map +1 -1
  57. package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
  58. package/dist/web/public/app.16290ae3.js +26 -0
  59. package/dist/web/public/app.16290ae3.js.br +0 -0
  60. package/dist/web/public/app.16290ae3.js.gz +0 -0
  61. package/dist/web/public/constants.64161167.js.gz +0 -0
  62. package/dist/web/public/index.html +7 -7
  63. package/dist/web/public/index.html.br +0 -0
  64. package/dist/web/public/index.html.gz +0 -0
  65. package/dist/web/public/input-cjk.88082175.js.gz +0 -0
  66. package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
  67. package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
  68. package/dist/web/public/mobile.0b213796.css.gz +0 -0
  69. package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
  70. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  71. package/dist/web/public/{panels-ui.8204db1e.js → panels-ui.2d5b9703.js} +1 -1
  72. package/dist/web/public/panels-ui.2d5b9703.js.br +0 -0
  73. package/dist/web/public/panels-ui.2d5b9703.js.gz +0 -0
  74. package/dist/web/public/{ralph-panel.a2733fd5.js → ralph-panel.61076370.js} +1 -1
  75. package/dist/web/public/ralph-panel.61076370.js.br +0 -0
  76. package/dist/web/public/ralph-panel.61076370.js.gz +0 -0
  77. package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
  78. package/dist/web/public/{respawn-ui.372c6ea7.js → respawn-ui.60be6ef5.js} +1 -1
  79. package/dist/web/public/respawn-ui.60be6ef5.js.br +0 -0
  80. package/dist/web/public/respawn-ui.60be6ef5.js.gz +0 -0
  81. package/dist/web/public/{session-ui.72f2f538.js → session-ui.554092ae.js} +1 -1
  82. package/dist/web/public/session-ui.554092ae.js.br +0 -0
  83. package/dist/web/public/session-ui.554092ae.js.gz +0 -0
  84. package/dist/web/public/{settings-ui.bd3eaadb.js → settings-ui.c58b0b9b.js} +7 -7
  85. package/dist/web/public/settings-ui.c58b0b9b.js.br +0 -0
  86. package/dist/web/public/settings-ui.c58b0b9b.js.gz +0 -0
  87. package/dist/web/public/styles.111ff326.css.gz +0 -0
  88. package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
  89. package/dist/web/public/sw.js.gz +0 -0
  90. package/dist/web/public/terminal-ui.474f79df.js.gz +0 -0
  91. package/dist/web/public/upload.html.gz +0 -0
  92. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  93. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  94. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  95. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  96. package/dist/web/public/vendor/xterm.css.gz +0 -0
  97. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  98. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  99. package/dist/web/respawn-event-wiring.d.ts +51 -0
  100. package/dist/web/respawn-event-wiring.d.ts.map +1 -0
  101. package/dist/web/respawn-event-wiring.js +280 -0
  102. package/dist/web/respawn-event-wiring.js.map +1 -0
  103. package/dist/web/route-helpers.d.ts +23 -0
  104. package/dist/web/route-helpers.d.ts.map +1 -1
  105. package/dist/web/route-helpers.js +53 -0
  106. package/dist/web/route-helpers.js.map +1 -1
  107. package/dist/web/routes/case-routes.d.ts.map +1 -1
  108. package/dist/web/routes/case-routes.js +2 -11
  109. package/dist/web/routes/case-routes.js.map +1 -1
  110. package/dist/web/routes/file-routes.d.ts.map +1 -1
  111. package/dist/web/routes/file-routes.js +8 -24
  112. package/dist/web/routes/file-routes.js.map +1 -1
  113. package/dist/web/routes/orchestrator-routes.d.ts.map +1 -1
  114. package/dist/web/routes/orchestrator-routes.js +23 -30
  115. package/dist/web/routes/orchestrator-routes.js.map +1 -1
  116. package/dist/web/routes/system-routes.d.ts.map +1 -1
  117. package/dist/web/routes/system-routes.js +17 -71
  118. package/dist/web/routes/system-routes.js.map +1 -1
  119. package/dist/web/server.d.ts +4 -51
  120. package/dist/web/server.d.ts.map +1 -1
  121. package/dist/web/server.js +98 -941
  122. package/dist/web/server.js.map +1 -1
  123. package/dist/web/session-listener-wiring.d.ts +89 -0
  124. package/dist/web/session-listener-wiring.d.ts.map +1 -0
  125. package/dist/web/session-listener-wiring.js +290 -0
  126. package/dist/web/session-listener-wiring.js.map +1 -0
  127. package/dist/web/sse-stream-manager.d.ts +91 -0
  128. package/dist/web/sse-stream-manager.d.ts.map +1 -0
  129. package/dist/web/sse-stream-manager.js +426 -0
  130. package/dist/web/sse-stream-manager.js.map +1 -0
  131. package/package.json +1 -1
  132. package/dist/web/public/app.e09fd4a6.js +0 -26
  133. package/dist/web/public/app.e09fd4a6.js.br +0 -0
  134. package/dist/web/public/app.e09fd4a6.js.gz +0 -0
  135. package/dist/web/public/panels-ui.8204db1e.js.br +0 -0
  136. package/dist/web/public/panels-ui.8204db1e.js.gz +0 -0
  137. package/dist/web/public/ralph-panel.a2733fd5.js.br +0 -0
  138. package/dist/web/public/ralph-panel.a2733fd5.js.gz +0 -0
  139. package/dist/web/public/respawn-ui.372c6ea7.js.br +0 -0
  140. package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
  141. package/dist/web/public/session-ui.72f2f538.js.br +0 -0
  142. package/dist/web/public/session-ui.72f2f538.js.gz +0 -0
  143. package/dist/web/public/settings-ui.bd3eaadb.js.br +0 -0
  144. package/dist/web/public/settings-ui.bd3eaadb.js.gz +0 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * @fileoverview SSE stream manager — owns all SSE client state, broadcasting, and event batching.
3
+ *
4
+ * Extracted from server.ts for modularity. Handles:
5
+ * - SSE client connection tracking with subscription filtering
6
+ * - Backpressure-aware message delivery
7
+ * - Terminal data batching with adaptive intervals (16-50ms for 60fps)
8
+ * - Task update and session state batching
9
+ * - Dead client cleanup and keepalive
10
+ * - Cloudflare tunnel padding for proxy buffer flushing
11
+ *
12
+ * @dependencies CleanupManager (managed timers), config/server-timing (constants)
13
+ * @consumedby web/server.ts (WebServer delegates all SSE operations here)
14
+ *
15
+ * @module web/sse-stream-manager
16
+ */
17
+ import { StaleExpirationMap } from '../utils/index.js';
18
+ import { SseEvent } from './sse-events.js';
19
+ import { TERMINAL_BATCH_INTERVAL, TASK_UPDATE_BATCH_INTERVAL, STATE_UPDATE_DEBOUNCE_INTERVAL, BATCH_FLUSH_THRESHOLD, SSE_PADDING_SIZE, INACTIVITY_TIMEOUT_MS, } from '../config/server-timing.js';
20
+ // SSE padding for Cloudflare tunnel buffer flushing.
21
+ // Cloudflare quick tunnels buffer small SSE responses, causing lag for real-time events.
22
+ // Appending SSE comment padding (ignored by EventSource) forces the proxy to flush.
23
+ // Pre-computed once at startup to avoid repeated string allocation.
24
+ const SSE_PADDING = ':' + 'p'.repeat(SSE_PADDING_SIZE) + '\n';
25
+ export class SseStreamManager {
26
+ deps;
27
+ cleanup;
28
+ // ─── SSE Client Tracking ────────────────────────────────
29
+ /**
30
+ * SSE clients mapped to their session subscription filter.
31
+ * Value is a Set of session IDs the client wants events for,
32
+ * or `null` meaning "receive all events" (backwards-compatible default).
33
+ */
34
+ sseClients = new Map();
35
+ /** SSE clients connecting from non-localhost (i.e. through tunnel) */
36
+ remoteSseClients = new Set();
37
+ /** Clients with backpressure — skip writes until 'drain' fires */
38
+ backpressuredClients = new Set();
39
+ // ─── Tunnel State ───────────────────────────────────────
40
+ /** Cached tunnel active state — updated on TunnelStarted/TunnelStopped to avoid getUrl() on every broadcast */
41
+ _isTunnelActive = false;
42
+ // ─── Terminal Batching ──────────────────────────────────
43
+ terminalBatches = new Map();
44
+ terminalBatchSizes = new Map(); // Running total avoids O(n) reduce per push
45
+ terminalBatchTimers = new Map(); // Per-session timers (staggered flushes)
46
+ // Adaptive batching: track rapid events to extend batch window (per-session)
47
+ // StaleExpirationMap auto-cleans entries for sessions that stop generating output
48
+ lastTerminalEventTime;
49
+ // ─── Event Batching ─────────────────────────────────────
50
+ taskUpdateBatches = new Map();
51
+ taskUpdateBatchTimerId = null;
52
+ // State update batching (reduce expensive toDetailedState() serialization)
53
+ stateUpdatePending = new Set();
54
+ stateUpdateTimerId = null;
55
+ // ─── Lifecycle ──────────────────────────────────────────
56
+ _isStopping = false;
57
+ constructor(deps, cleanup) {
58
+ this.deps = deps;
59
+ this.cleanup = cleanup;
60
+ this.lastTerminalEventTime = new StaleExpirationMap({
61
+ ttlMs: INACTIVITY_TIMEOUT_MS, // 5 minutes - auto-expire stale session timing data
62
+ refreshOnGet: false, // Don't refresh on reads, only on explicit sets
63
+ });
64
+ }
65
+ // ========== SSE Connection Management ==========
66
+ get clientCount() {
67
+ return this.sseClients.size;
68
+ }
69
+ get remoteClientCount() {
70
+ return this.remoteSseClients.size;
71
+ }
72
+ get isTunnelActive() {
73
+ return this._isTunnelActive;
74
+ }
75
+ setTunnelActive(active) {
76
+ this._isTunnelActive = active;
77
+ }
78
+ addClient(reply, sessionFilter, isRemote) {
79
+ this.sseClients.set(reply, sessionFilter);
80
+ if (isRemote) {
81
+ this.remoteSseClients.add(reply);
82
+ }
83
+ }
84
+ removeClient(reply) {
85
+ this.sseClients.delete(reply);
86
+ this.remoteSseClients.delete(reply);
87
+ this.backpressuredClients.delete(reply);
88
+ }
89
+ /** Send a single SSE event to a specific client. */
90
+ sendSSE(reply, event, data) {
91
+ try {
92
+ reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
93
+ }
94
+ catch {
95
+ this.sseClients.delete(reply);
96
+ this.remoteSseClients.delete(reply);
97
+ }
98
+ }
99
+ /** Send pre-formatted tunnel padding to a specific client. */
100
+ sendPadding(reply) {
101
+ if (!this._isTunnelActive)
102
+ return;
103
+ try {
104
+ reply.raw.write(SSE_PADDING);
105
+ }
106
+ catch {
107
+ /* client gone */
108
+ }
109
+ }
110
+ // Optimized: send pre-formatted SSE message to a client
111
+ // Returns false if client is backpressured or dead
112
+ sendSSEPreformatted(reply, message) {
113
+ // Skip backpressured clients to prevent unbounded memory growth.
114
+ // Terminal data dropped here is recovered via session:needsRefresh on drain.
115
+ if (this.backpressuredClients.has(reply))
116
+ return;
117
+ try {
118
+ const ok = reply.raw.write(message);
119
+ if (!ok) {
120
+ // Buffer is full — mark as backpressured, resume on drain
121
+ this.backpressuredClients.add(reply);
122
+ reply.raw.once('drain', () => {
123
+ this.backpressuredClients.delete(reply);
124
+ // Client may have missed terminal data during backpressure.
125
+ // Tell it to reload the active session's buffer to recover.
126
+ try {
127
+ const drainPadding = this._isTunnelActive ? SSE_PADDING : '';
128
+ reply.raw.write(`event: ${SseEvent.SessionNeedsRefresh}\ndata: {}\n\n${drainPadding}`);
129
+ }
130
+ catch {
131
+ /* client gone */
132
+ }
133
+ });
134
+ }
135
+ }
136
+ catch {
137
+ this.sseClients.delete(reply);
138
+ this.remoteSseClients.delete(reply);
139
+ this.backpressuredClients.delete(reply);
140
+ }
141
+ }
142
+ // ========== Broadcasting ==========
143
+ broadcast(event, data) {
144
+ // Skip serialization entirely when no clients are listening
145
+ if (this.sseClients.size === 0)
146
+ return;
147
+ // Performance optimization: serialize JSON once for all clients.
148
+ // Only append Cloudflare tunnel padding for latency-sensitive events —
149
+ // Recovery events need immediate proxy flush; low-frequency metadata events
150
+ // (session:created, ralph:*, respawn:*, etc.) don't need padding.
151
+ // Note: session:terminal has its own padding in flushSessionTerminalBatch().
152
+ const needsPadding = this._isTunnelActive && event === SseEvent.SessionNeedsRefresh;
153
+ const padding = needsPadding ? SSE_PADDING : '';
154
+ let message;
155
+ try {
156
+ message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` + padding;
157
+ }
158
+ catch (err) {
159
+ // Handle circular references or non-serializable values
160
+ console.error(`[Server] Failed to serialize SSE event "${event}":`, err);
161
+ return;
162
+ }
163
+ // Extract sessionId from event data for subscription filtering.
164
+ const eventSessionId = this.extractSessionId(event, data);
165
+ for (const [client, filter] of this.sseClients) {
166
+ // No filter (null) = receive everything. Otherwise, skip if event is
167
+ // session-scoped and the session isn't in the client's subscription set.
168
+ if (filter && eventSessionId && !filter.has(eventSessionId))
169
+ continue;
170
+ this.sendSSEPreformatted(client, message);
171
+ }
172
+ }
173
+ /**
174
+ * Extract the session ID from an event's data payload for subscription filtering.
175
+ * Returns the sessionId string if the event is session-scoped, or null for global events.
176
+ */
177
+ extractSessionId(event, data) {
178
+ if (data == null || typeof data !== 'object')
179
+ return null;
180
+ const record = data;
181
+ // Most session-scoped events use `sessionId`
182
+ if (typeof record.sessionId === 'string')
183
+ return record.sessionId;
184
+ // Session lifecycle events (session:*) use `id` from the session state object
185
+ if (typeof record.id === 'string' && event.startsWith('session:'))
186
+ return record.id;
187
+ // No session ID found — treat as global event (sent to all clients)
188
+ return null;
189
+ }
190
+ // ========== Terminal Data Batching ==========
191
+ // Batch terminal data for better performance (60fps)
192
+ // Uses per-session timers with adaptive intervals to prevent thundering herd:
193
+ // each session flushes independently rather than all sessions flushing in one burst.
194
+ batchTerminalData(sessionId, data) {
195
+ // Skip if server is stopping
196
+ if (this._isStopping)
197
+ return;
198
+ let chunks = this.terminalBatches.get(sessionId);
199
+ if (!chunks) {
200
+ chunks = [];
201
+ this.terminalBatches.set(sessionId, chunks);
202
+ }
203
+ chunks.push(data);
204
+ const prevSize = this.terminalBatchSizes.get(sessionId) ?? 0;
205
+ const totalLength = prevSize + data.length;
206
+ this.terminalBatchSizes.set(sessionId, totalLength);
207
+ // Adaptive batching: detect rapid events and extend batch window (per-session)
208
+ const now = Date.now();
209
+ const lastEvent = this.lastTerminalEventTime.get(sessionId) ?? 0;
210
+ const eventGap = now - lastEvent;
211
+ this.lastTerminalEventTime.set(sessionId, now);
212
+ // Adjust batch interval based on event frequency (per-session)
213
+ // Rapid events (<10ms gap) = 50ms batch, moderate (<20ms) = 32ms, else 16ms
214
+ let sessionInterval;
215
+ if (eventGap > 0 && eventGap < 10) {
216
+ sessionInterval = 50;
217
+ }
218
+ else if (eventGap > 0 && eventGap < 20) {
219
+ sessionInterval = 32;
220
+ }
221
+ else {
222
+ sessionInterval = TERMINAL_BATCH_INTERVAL;
223
+ }
224
+ // Flush immediately if batch is large for responsiveness
225
+ if (totalLength > BATCH_FLUSH_THRESHOLD) {
226
+ const existingTimer = this.terminalBatchTimers.get(sessionId);
227
+ if (existingTimer) {
228
+ clearTimeout(existingTimer);
229
+ this.terminalBatchTimers.delete(sessionId);
230
+ }
231
+ this.flushSessionTerminalBatch(sessionId);
232
+ return;
233
+ }
234
+ // Start per-session batch timer if not already running
235
+ // Each session flushes independently — prevents one busy session from
236
+ // forcing all sessions to flush at its rate (thundering herd)
237
+ if (!this.terminalBatchTimers.has(sessionId)) {
238
+ this.terminalBatchTimers.set(sessionId, setTimeout(() => {
239
+ this.terminalBatchTimers.delete(sessionId);
240
+ this.flushSessionTerminalBatch(sessionId);
241
+ }, sessionInterval));
242
+ }
243
+ }
244
+ /** Flush a single session's batched terminal data */
245
+ flushSessionTerminalBatch(sessionId) {
246
+ if (this._isStopping) {
247
+ this.terminalBatches.delete(sessionId);
248
+ this.terminalBatchSizes.delete(sessionId);
249
+ return;
250
+ }
251
+ const chunks = this.terminalBatches.get(sessionId);
252
+ if (chunks && chunks.length > 0) {
253
+ // Join chunks only at flush time (avoids O(n^2) string concatenation in batchTerminalData)
254
+ const data = chunks.join('');
255
+ // Wrap batched output in DEC 2026 synchronized output markers so xterm.js
256
+ // renders the entire batch atomically. Ink spinner frames (cursor-up + redraw)
257
+ // do NOT emit their own 2026 markers, so without this wrapper each partial
258
+ // cursor update renders individually, causing visible flicker.
259
+ // xterm.js 6.0+ handles DEC 2026 natively: it buffers everything between
260
+ // 2026h/2026l and renders in one pass.
261
+ const syncData = '\x1b[?2026h' + data + '\x1b[?2026l';
262
+ // Fast path: build SSE message directly without JSON.stringify on wrapper object.
263
+ // Only the terminal data string needs escaping; sessionId is a UUID (safe to template).
264
+ const escapedData = JSON.stringify(syncData);
265
+ // Append tunnel padding for immediate Cloudflare proxy flush —
266
+ // terminal data is high-frequency and latency-sensitive.
267
+ const padding = this._isTunnelActive ? SSE_PADDING : '';
268
+ const message = `event: session:terminal\ndata: {"id":"${sessionId}","data":${escapedData}}\n\n` + padding;
269
+ for (const [client, filter] of this.sseClients) {
270
+ // Skip clients that have a session filter and aren't subscribed to this session
271
+ if (filter && !filter.has(sessionId))
272
+ continue;
273
+ this.sendSSEPreformatted(client, message);
274
+ }
275
+ }
276
+ this.terminalBatches.delete(sessionId);
277
+ this.terminalBatchSizes.delete(sessionId);
278
+ }
279
+ // ========== Task Update Batching ==========
280
+ // Batch task:updated events at 100ms - only send latest update per task
281
+ // Key is sessionId:taskId to avoid collisions when multiple tasks update concurrently
282
+ batchTaskUpdate(sessionId, task) {
283
+ // Skip if server is stopping
284
+ if (this._isStopping)
285
+ return;
286
+ // Use composite key to avoid losing updates when multiple tasks update in same batch window
287
+ const key = `${sessionId}:${task.id}`;
288
+ this.taskUpdateBatches.set(key, { sessionId, task });
289
+ if (!this.taskUpdateBatchTimerId) {
290
+ this.taskUpdateBatchTimerId = this.cleanup.setTimeout(() => {
291
+ this.taskUpdateBatchTimerId = null;
292
+ this.flushTaskUpdateBatches();
293
+ }, TASK_UPDATE_BATCH_INTERVAL, { description: 'task update batch flush' });
294
+ }
295
+ }
296
+ flushTaskUpdateBatches() {
297
+ // Skip if server is stopping (timer may have been queued before stop() was called)
298
+ if (this._isStopping) {
299
+ this.taskUpdateBatches.clear();
300
+ return;
301
+ }
302
+ for (const [, { sessionId, task }] of this.taskUpdateBatches) {
303
+ this.broadcast(SseEvent.TaskUpdated, { sessionId, task });
304
+ }
305
+ this.taskUpdateBatches.clear();
306
+ }
307
+ // ========== Session State Batching ==========
308
+ /**
309
+ * Debounce expensive session:updated broadcasts.
310
+ * Instead of calling toDetailedState() on every event, batch requests
311
+ * and only serialize once per STATE_UPDATE_DEBOUNCE_INTERVAL.
312
+ */
313
+ broadcastSessionStateDebounced(sessionId) {
314
+ // Skip if server is stopping
315
+ if (this._isStopping)
316
+ return;
317
+ this.stateUpdatePending.add(sessionId);
318
+ if (!this.stateUpdateTimerId) {
319
+ this.stateUpdateTimerId = this.cleanup.setTimeout(() => {
320
+ this.stateUpdateTimerId = null;
321
+ this.flushStateUpdates();
322
+ }, STATE_UPDATE_DEBOUNCE_INTERVAL, { description: 'state update debounce flush' });
323
+ }
324
+ }
325
+ flushStateUpdates() {
326
+ // Skip if server is stopping (timer may have been queued before stop() was called)
327
+ if (this._isStopping) {
328
+ this.stateUpdatePending.clear();
329
+ return;
330
+ }
331
+ for (const sessionId of this.stateUpdatePending) {
332
+ // Single expensive serialization per batch interval
333
+ const state = this.deps.getSessionStateWithRespawn(sessionId);
334
+ if (state) {
335
+ this.broadcast(SseEvent.SessionUpdated, state);
336
+ }
337
+ }
338
+ this.stateUpdatePending.clear();
339
+ }
340
+ // ========== Client Health ==========
341
+ /**
342
+ * Clean up dead SSE clients and send keep-alive comments.
343
+ * Keep-alive prevents proxy/load-balancer timeouts on idle connections.
344
+ * Dead client cleanup prevents memory leaks from abruptly terminated connections.
345
+ */
346
+ cleanupDeadClients() {
347
+ const deadClients = [];
348
+ for (const [client] of this.sseClients) {
349
+ try {
350
+ // Check if the underlying socket is still writable
351
+ const socket = client.raw.socket;
352
+ if (!socket || socket.destroyed || !socket.writable) {
353
+ deadClients.push(client);
354
+ }
355
+ else {
356
+ // Send SSE comment as keep-alive. Only add padding when tunnel is
357
+ // active — it flushes Cloudflare proxy buffers but wastes bandwidth
358
+ // for direct/Tailscale connections.
359
+ const ka = this._isTunnelActive ? ':keepalive\n' + SSE_PADDING : ':keepalive\n\n';
360
+ client.raw.write(ka);
361
+ }
362
+ }
363
+ catch {
364
+ // Error accessing socket means client is dead
365
+ deadClients.push(client);
366
+ }
367
+ }
368
+ // Remove dead clients
369
+ for (const client of deadClients) {
370
+ this.sseClients.delete(client);
371
+ this.remoteSseClients.delete(client);
372
+ this.backpressuredClients.delete(client);
373
+ }
374
+ if (deadClients.length > 0) {
375
+ console.log(`[Server] Cleaned up ${deadClients.length} dead SSE client(s)`);
376
+ }
377
+ }
378
+ // ========== Session Cleanup ==========
379
+ /** Clean up all batching state for a session (call on session exit or deletion). */
380
+ cleanupSessionBatches(sessionId) {
381
+ this.terminalBatches.delete(sessionId);
382
+ this.terminalBatchSizes.delete(sessionId);
383
+ const batchTimer = this.terminalBatchTimers.get(sessionId);
384
+ if (batchTimer) {
385
+ clearTimeout(batchTimer);
386
+ this.terminalBatchTimers.delete(sessionId);
387
+ }
388
+ this.taskUpdateBatches.delete(sessionId);
389
+ this.stateUpdatePending.delete(sessionId);
390
+ this.lastTerminalEventTime.delete(sessionId);
391
+ }
392
+ // ========== Lifecycle ==========
393
+ setStopping() {
394
+ this._isStopping = true;
395
+ }
396
+ /** Graceful shutdown: notify clients, close connections, clear all state. */
397
+ stop() {
398
+ this._isStopping = true;
399
+ // Gracefully close all SSE connections before clearing
400
+ for (const [client] of this.sseClients) {
401
+ try {
402
+ // Send a final event to notify clients of shutdown
403
+ this.sendSSE(client, 'server:shutdown', { reason: 'Server stopping' });
404
+ client.raw.end();
405
+ }
406
+ catch {
407
+ // Client may already be disconnected
408
+ }
409
+ }
410
+ this.sseClients.clear();
411
+ this.remoteSseClients.clear();
412
+ this.backpressuredClients.clear();
413
+ // Clear per-session batch timers
414
+ for (const timer of this.terminalBatchTimers.values()) {
415
+ clearTimeout(timer);
416
+ }
417
+ this.terminalBatchTimers.clear();
418
+ this.terminalBatches.clear();
419
+ this.terminalBatchSizes.clear();
420
+ this.taskUpdateBatches.clear();
421
+ this.stateUpdatePending.clear();
422
+ // Dispose StaleExpirationMap (stops internal cleanup timer)
423
+ this.lastTerminalEventTime.dispose();
424
+ }
425
+ }
426
+ //# sourceMappingURL=sse-stream-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-stream-manager.js","sourceRoot":"","sources":["../../src/web/sse-stream-manager.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicodeman",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,26 +0,0 @@
1
- "use strict";const _crashDiag={_entries:[],_maxEntries:50,log(l){const e=`${new Date().toISOString().slice(11,23)} ${l}`;this._entries.push(e),this._entries.length>this._maxEntries&&this._entries.shift();try{localStorage.setItem("codeman-crash-diag",this._entries.join(`
2
- `))}catch{}}};try{const l=localStorage.getItem("codeman-crash-diag");l&&console.log(`[CRASH-DIAG] Previous session breadcrumbs:
3
- `+l)}catch{}if(_crashDiag.log("PAGE LOAD"),setInterval(()=>{try{localStorage.setItem("codeman-crash-heartbeat",String(Date.now())),_crashDiag._entries.length>0&&navigator.sendBeacon("/api/crash-diag",JSON.stringify({data:_crashDiag._entries.join(`
4
- `)}))}catch{}},2e3),window.addEventListener("error",l=>{_crashDiag.log(`ERROR: ${l.message} at ${l.filename}:${l.lineno}`),console.error("[CRASH-DIAG] Uncaught error:",l.message,`
5
- File:`,l.filename,":",l.lineno,":",l.colno,`
6
- Stack:`,l.error?.stack)}),window.addEventListener("unhandledrejection",l=>{_crashDiag.log(`UNHANDLED: ${l.reason?.message||l.reason}`),console.error("[CRASH-DIAG] Unhandled promise rejection:",l.reason?.message||l.reason,`
7
- Stack:`,l.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(l,...e){const t=_origGetContext.call(this,l,...e);return(l==="webgl2"||l==="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_CLI_INFO,"_onSessionCliInfo"],[SSE_EVENTS.SCHEDULED_CREATED,"_onScheduledCreated"],[SSE_EVENTS.SCHEDULED_UPDATED,"_onScheduledUpdated"],[SSE_EVENTS.SCHEDULED_COMPLETED,"_onScheduledCompleted"],[SSE_EVENTS.SCHEDULED_STOPPED,"_onScheduledStopped"],[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.IMAGE_DETECTED,"_onImageDetected"],[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"]];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.terminal=null,this.fitAddon=null,this.activeSessionId=null,this._initGeneration=0,this._initFallbackTimer=null,this._selectGeneration=0,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.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.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.pendingWrites=[],this.writeFrameScheduled=!1,this._wasAtBottomBeforeWrite=!0,this.syncWaitTimeout=null,this._isLoadingBuffer=!1,this._loadBufferQueue=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._inputQueue=new Map,this._inputQueueMaxBytes=64*1024,this._connectionStatus="connected",this._inputSendChain=Promise.resolve(),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]}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,i=t/1e6*75;return s+i}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(),KeyboardHandler.init(),SwipeHandler.init(),VoiceInput.init(),KeyboardAccessoryBar.init(),this.applyHeaderVisibilitySettings(),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 e=fetch("/api/settings").then(t=>t.ok?t.json():null).catch(()=>null);if(this.loadQuickStartCases(null,e),this._initRunMode(),this.setupEventListeners(),MobileDetection.isTouchDevice()){const t=s=>{s&&s.addEventListener("touchstart",i=>{if(!KeyboardHandler.keyboardVisible)return;const n=i.target.closest("button");n&&(i.preventDefault(),n.click(),typeof app<"u"&&app.terminal&&app.terminal.focus())},{passive:!1})};t(document.querySelector(".toolbar")),t(document.querySelector(".welcome-overlay"))}this.setupOnlineDetection(),this.loadAppSettingsFromServer(e).then(()=>{this.applyHeaderVisibilitySettings(),this.applyTabWrapSettings(),this.applyMonitorVisibility()}),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"),this._webglAddon.dispose(),this._webglAddon=null}),this.terminal.loadAddon(this._webglAddon),console.log("[CRASH-DIAG] WebGL renderer enabled")}catch{}}setupEventListeners(){document.addEventListener("keydown",t=>{t.isComposing||t.keyCode===229||(t.key==="Escape"&&(this.closeAllPanels(),this.closeHelp()),(t.ctrlKey||t.metaKey)&&(t.key==="?"||t.key==="/")&&(t.preventDefault(),this.showHelp()),(t.ctrlKey||t.metaKey)&&t.key==="Enter"&&(t.preventDefault(),this.quickStart()),(t.ctrlKey||t.metaKey)&&t.key==="w"&&(t.preventDefault(),this.killActiveSession()),(t.ctrlKey||t.metaKey)&&t.key==="Tab"&&(t.preventDefault(),this.nextSession()),(t.ctrlKey||t.metaKey)&&t.key==="k"&&(t.preventDefault(),this.killAllSessions()),(t.ctrlKey||t.metaKey)&&t.key==="l"&&(t.preventDefault(),this.clearTerminal()),(t.ctrlKey||t.metaKey)&&t.shiftKey&&t.key==="R"&&(t.preventDefault(),this.restoreTerminalSize()),(t.ctrlKey||t.metaKey)&&(t.key==="="||t.key==="+")&&(t.preventDefault(),this.increaseFontSize()),(t.ctrlKey||t.metaKey)&&t.key==="-"&&(t.preventDefault(),this.decreaseFontSize()),(t.ctrlKey||t.metaKey)&&t.shiftKey&&t.key==="V"&&(t.preventDefault(),VoiceInput.toggle()))},!0);const e=this.$("headerTokens");e&&!e._statsHandlerAttached&&(e.classList.add("clickable"),e._statsHandlerAttached=!0,e.addEventListener("click",()=>this.openTokenStats())),this.setupColorPicker()}connectSSE(){if(!navigator.onLine){this.setConnectionStatus("offline");return}this.sseReconnectTimeout&&(clearTimeout(this.sseReconnectTimeout),this.sseReconnectTimeout=null),this._sseListenerCleanup&&(this._sseListenerCleanup(),this._sseListenerCleanup=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.reconnectAttempts===0?this.setConnectionStatus("connecting"):this.setConnectionStatus("reconnecting"),this.eventSource=new EventSource("/api/events");const e=[],t=(s,i)=>{this.eventSource.addEventListener(s,i),e.push({event:s,handler:i})};if(this._sseListenerCleanup=()=>{for(const{event:s,handler:i}of e)this.eventSource&&this.eventSource.removeEventListener(s,i);e.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.sseReconnectTimeout&&clearTimeout(this.sseReconnectTimeout);const s=this.reconnectAttempts<=1?200:Math.min(500*Math.pow(2,this.reconnectAttempts-2),3e4);this.sseReconnectTimeout=setTimeout(()=>this.connectSSE(),s)},!this._sseHandlerWrappers){this._sseHandlerWrappers=new Map;for(const[s,i]of _SSE_HANDLER_MAP){const n=this[i];this._sseHandlerWrappers.set(s,o=>{try{n.call(this,o.data?JSON.parse(o.data):{})}catch(r){console.error(`[SSE] Error handling ${s}:`,r)}})}}for(const[s]of _SSE_HANDLER_MAP)t(s,this._sseHandlerWrappers.get(s))}_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),i=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),i&&(this.recheckOrphanSubagents(),requestAnimationFrame(()=>{this.updateConnectionLines()}))}_onSessionDeleted(e){if(this._wsSessionId===e.id&&this._disconnectWs(),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,i)=>s+i.length,0)||0)+(this.flickerFilterBuffer?.length||0)>131072){this._clientDropRecoveryTimer||(this._clientDropRecoveryTimer=setTimeout(()=>{this._clientDropRecoveryTimer=null,this._onSessionNeedsRefresh()},2e3));return}this.batchTerminalWrite(e.data)}}async _onSessionNeedsRefresh(){if(!(!this.activeSessionId||!this.terminal)&&!this._isLoadingBuffer)try{const t=await(await fetch(`/api/sessions/${this.activeSessionId}/terminal?tail=${TERMINAL_TAIL_SIZE}`)).json();t.terminalBuffer&&(this.terminal.clear(),this.terminal.reset(),await this.chunkedTerminalWrite(t.terminalBuffer),this.terminal.scrollToBottom(),this._localEchoOverlay?.rerender(),this.activeSessionId&&this.sendResize(this.activeSessionId))}catch(e){console.error("needsRefresh reload failed:",e)}}async _onSessionClearTerminal(e){if(e.id===this.activeSessionId){if(this._isLoadingBuffer)return;try{const s=await(await fetch(`/api/sessions/${e.id}/terminal`)).json();if(this.terminal.clear(),this.terminal.reset(),s.terminalBuffer){const i=s.terminalBuffer.replace(DEC_SYNC_STRIP_RE,"");await this.chunkedTerminalWrite(i)}this.sendResize(e.id),this._localEchoOverlay?.rerender()}catch(t){console.error("clearTerminal refresh failed:",t)}}}_onSessionCompletion(e){this.totalCost+=e.cost||0,this.updateCost(),e.id===this.activeSessionId&&(this.terminal.writeln(""),this.terminal.writeln(`\x1B[1;32m Done (Cost: $${(e.cost||0).toFixed(4)})\x1B[0m`))}_onSessionError(e){e.id===this.activeSessionId&&this.terminal.writeln(`\x1B[1;31m Error: ${e.error}\x1B[0m`);const t=this.sessions.get(e.id);this.notificationManager?.notify({urgency:"critical",category:"session-error",sessionId:e.id,sessionName:t?.name||this.getShortId(e.id),title:"Session Error",message:e.error||"Unknown error"})}_onSessionExit(e){this._wsSessionId===e.id&&this._disconnectWs();const t=this.sessions.get(e.id);t&&(t.status="stopped",this.renderSessionTabs(),e.id===this.activeSessionId&&this._updateLocalEchoState()),e.code&&e.code!==0&&this.notificationManager?.notify({urgency:"critical",category:"session-crash",sessionId:e.id,sessionName:t?.name||this.getShortId(e.id),title:"Session Crashed",message:`Exited with code ${e.code}`})}_onSessionIdle(e){const t=this.sessions.get(e.id);if(t&&(t.status="idle",this.renderSessionTabs(),this.sendPendingCtrlL(e.id),e.id===this.activeSessionId&&this._updateLocalEchoState()),!this.respawnStatus[e.id]?.enabled){const s=this.notificationManager?.preferences?.stuckThresholdMs||6e5;clearTimeout(this.idleTimers.get(e.id)),this.idleTimers.set(e.id,setTimeout(()=>{const i=this.sessions.get(e.id);this.notificationManager?.notify({urgency:"warning",category:"session-stuck",sessionId:e.id,sessionName:i?.name||this.getShortId(e.id),title:"Session Idle",message:`Idle for ${Math.round(s/6e4)}+ minutes`}),this.idleTimers.delete(e.id)},s))}}_onSessionWorking(e){const t=this.sessions.get(e.id);t&&(t.status="busy",this.pendingHooks.has(e.id)||this.tabAlerts.delete(e.id),this.renderSessionTabs(),this.sendPendingCtrlL(e.id),e.id===this.activeSessionId&&this._updateLocalEchoState());const s=this.idleTimers.get(e.id);s&&(clearTimeout(s),this.idleTimers.delete(e.id))}_onSessionAutoClear(e){e.sessionId===this.activeSessionId&&(this.showToast(`Auto-cleared at ${e.tokens.toLocaleString()} tokens`,"info"),this.updateRespawnTokens(0));const t=this.sessions.get(e.sessionId);this.notificationManager?.notify({urgency:"info",category:"auto-clear",sessionId:e.sessionId,sessionName:t?.name||this.getShortId(e.sessionId),title:"Auto-Cleared",message:`Context reset at ${(e.tokens||0).toLocaleString()} tokens`})}_onSessionCliInfo(e){const t=this.sessions.get(e.sessionId);t&&(e.version&&(t.cliVersion=e.version),e.model&&(t.cliModel=e.model),e.accountType&&(t.cliAccountType=e.accountType),e.latestVersion&&(t.cliLatestVersion=e.latestVersion)),e.sessionId===this.activeSessionId&&this.updateCliInfoDisplay()}_onScheduledCreated(e){this.currentRun=e,this.showTimer()}_onScheduledUpdated(e){this.currentRun=e,this.updateTimer()}_onScheduledCompleted(e){this.currentRun=e,this.hideTimer(),this.showToast("Scheduled run completed!","success")}_onScheduledStopped(){this.currentRun=null,this.hideTimer()}setConnectionStatus(e){this._connectionStatus=e,this._updateConnectionIndicator(),e==="connected"&&this._inputQueue.size>0&&this._drainInputQueues()}_connectWs(e){this._disconnectWs();const s=`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/ws/sessions/${e}/terminal`,i=new WebSocket(s);this._ws=i,this._wsSessionId=e,i.onopen=()=>{this._ws===i&&(this._wsReady=!0,this._wsReconnectAttempts=0)},i.onmessage=n=>{if(this._ws===i)try{const o=JSON.parse(n.data);o.t==="o"?this._onSessionTerminal({id:e,data:o.d}):o.t==="c"?this._onSessionClearTerminal({id:e}):o.t==="r"&&this._onSessionNeedsRefresh({id:e})}catch{}},i.onclose=n=>{if(this._ws===i&&(this._ws=null,this._wsSessionId=null,this._wsReady=!1,n.code<4004&&this.activeSessionId===e)){const o=Math.min(1e3*Math.pow(2,this._wsReconnectAttempts||0),1e4);this._wsReconnectAttempts=(this._wsReconnectAttempts||0)+1,this._wsReconnectTimer=setTimeout(()=>{this._wsReconnectTimer=null,this.activeSessionId===e&&this._connectWs(e)},o)}},i.onerror=()=>{}}_disconnectWs(){this._wsReconnectTimer&&(clearTimeout(this._wsReconnectTimer),this._wsReconnectTimer=null),this._wsReconnectAttempts=0,this._ws&&(this._ws.onclose=null,this._ws.close(),this._ws=null,this._wsSessionId=null,this._wsReady=!1)}_sendInputAsync(e,t){if(!this.isOnline||this._connectionStatus==="disconnected"){this._enqueueInput(e,t);return}if(this._wsReady&&this._wsSessionId===e)try{this._ws.send(JSON.stringify({t:"i",d:t})),this.clearPendingHooks(e);return}catch{}this._inputSendChain=this._inputSendChain.then(()=>{fetch(`/api/sessions/${e}/input`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:t}),keepalive:t.length<65536}).then(i=>{i.ok?this.clearPendingHooks(e):this._enqueueInput(e,t)}).catch(()=>{this._enqueueInput(e,t)})})}_enqueueInput(e,t){let i=(this._inputQueue.get(e)||"")+t;i.length>this._inputQueueMaxBytes&&(i=i.slice(i.length-this._inputQueueMaxBytes)),this._inputQueue.set(e,i),this._updateConnectionIndicator()}async _drainInputQueues(){if(this._inputQueue.size===0)return;const e=new Map(this._inputQueue);this._inputQueue.clear(),this._updateConnectionIndicator();for(const[t,s]of e)(await this._apiPost(`/api/sessions/${t}/input`,{input:s}))?.ok||this._enqueueInput(t,s);this._updateConnectionIndicator()}_updateConnectionIndicator(){const e=this.$("connectionIndicator"),t=this.$("connectionDot"),s=this.$("connectionText");if(!e||!t||!s)return;let i=0;for(const a of this._inputQueue.values())i+=a.length;const n=this._connectionStatus,o=i>0;if((n==="connected"||n==="connecting")&&!o){e.style.display="none";return}e.style.display="flex",t.className="connection-dot";const r=a=>a<1024?`${a}B`:`${(a/1024).toFixed(1)}KB`;n==="connected"&&o?(t.classList.add("draining"),s.textContent=`Sending ${r(i)}...`):n==="reconnecting"?(t.classList.add("reconnecting"),s.textContent=o?`Reconnecting (${r(i)} queued)`:"Reconnecting..."):(t.classList.add("offline"),s.textContent=o?`Offline (${r(i)} queued)`:"Offline")}setupOnlineDetection(){window.addEventListener("online",()=>{this.isOnline=!0,this.reconnectAttempts=0,this.connectSSE()}),window.addEventListener("offline",()=>{this.isOnline=!1,this.setConnectionStatus("offline")})}_updateCjkInputState(){const e=document.getElementById("cjkInput");if(!e)return;const t=this.loadAppSettingsFromStorage(),s=this._serverCjkOverride||t.cjkInputEnabled||!1;e.style.display=s?"block":"none",s||(window.cjkActive=!1)}handleInit(e){this._initFallbackTimer&&(clearTimeout(this._initFallbackTimer),this._initFallbackTimer=null);const t=++this._initGeneration;if(this._serverCjkOverride=e.inputCjkForm||!1,this._updateCjkInputState(),e.version){const n=this.$("versionDisplay"),o=this.$("headerVersion");n&&(n.textContent=`v${e.version}`,n.title=`Codeman v${e.version}`),o&&(o.textContent=`v${e.version}`,o.title=`Codeman v${e.version}`)}VoiceInput.cleanup(),this.sessions.clear(),this.ralphStates.clear(),this.terminalBuffers.clear(),this.terminalBufferCache.clear(),this.projectInsights.clear(),this.teams.clear(),this.teamTasks.clear();for(const n of this.idleTimers.values())clearTimeout(n);if(this.idleTimers.clear(),this.flickerFilterTimeout&&(clearTimeout(this.flickerFilterTimeout),this.flickerFilterTimeout=null),this.flickerFilterBuffer="",this.flickerFilterActive=!1,this.syncWaitTimeout&&(clearTimeout(this.syncWaitTimeout),this.syncWaitTimeout=null),this.pendingWrites=[],this.writeFrameScheduled=!1,this._isLoadingBuffer=!1,this._loadBufferQueue=null,this._chunkedWriteGen=(this._chunkedWriteGen||0)+1,this._localEchoOverlay?.rerender(),this.pendingHooks.clear(),this._parentNameCache&&this._parentNameCache.clear(),this.subagentActivity.clear(),this.subagentToolResults.clear(),MobileDetection.cleanup(),KeyboardHandler.cleanup(),MobileDetection.init(),KeyboardHandler.init(),this.tabAlerts.clear(),this._shownCompletions&&this._shownCompletions.clear(),this.notificationManager?.titleFlashInterval&&(clearInterval(this.notificationManager.titleFlashInterval),this.notificationManager.titleFlashInterval=null),this.notificationManager?.groupingMap){for(const{timeout:n}of this.notificationManager.groupingMap.values())clearTimeout(n);this.notificationManager.groupingMap.clear()}this.terminalResizeObserver&&(this.terminalResizeObserver.disconnect(),this.terminalResizeObserver=null),this.planLoadingTimer&&(clearInterval(this.planLoadingTimer),this.planLoadingTimer=null),this.timerCountdownInterval&&(clearInterval(this.timerCountdownInterval),this.timerCountdownInterval=null),this.runSummaryAutoRefreshTimer&&(clearInterval(this.runSummaryAutoRefreshTimer),this.runSummaryAutoRefreshTimer=null),e.sessions.forEach(n=>{this.sessions.set(n.id,n),(n.ralphLoop||n.ralphTodos)&&!this.ralphClosedSessions.has(n.id)&&this.ralphStates.set(n.id,{loop:n.ralphLoop||null,todos:n.ralphTodos||[]})}),this._restoreEndedTabs(),this.syncSessionOrder(),e.respawnStatus?this.respawnStatus=e.respawnStatus:this.respawnStatus={},this.respawnTimers={},this.respawnCountdownTimers={},this.respawnActionLogs={},e.globalStats&&(this.globalStats=e.globalStats),this.totalCost=e.sessions.reduce((n,o)=>n+(o.totalCost||0),0),this.totalCost+=e.scheduledRuns.reduce((n,o)=>n+(o.totalCost||0),0);const s=e.scheduledRuns.find(n=>n.status==="running");if(s&&(this.currentRun=s,this.showTimer()),this.updateCost(),this.renderSessionTabs(),this.sessions.size>0?this.startSystemStatsPolling():this.stopSystemStatsPolling(),this.cleanupAllFloatingWindows(),e.subagents&&(this.subagents.clear(),this.subagentActivity.clear(),this.subagentToolResults.clear(),e.subagents.forEach(n=>{this.subagents.set(n.agentId,n)}),this.renderSubagentPanel(),this.subagentParentMap.clear(),this.loadSubagentParentMap().then(()=>{for(const[n,o]of this.subagentParentMap){const r=this.subagents.get(n);if(r&&this.sessions.has(o)){r.parentSessionId=o;const a=this.sessions.get(o);a&&(r.parentSessionName=this.getSessionName(a)),this.subagents.set(n,r)}}for(const[n]of this.subagents)this.subagentParentMap.has(n)||this.findParentSessionForSubagent(n);this.restoreSubagentWindowStates()})),t!==this._initGeneration)return;const i=this.activeSessionId;if(this.activeSessionId=null,this.sessionOrder.length>0){let n=i;if(!n||!this.sessions.has(n))try{n=localStorage.getItem("codeman-active-session")}catch{}n&&this.sessions.has(n)?this.selectSession(n):this.selectSession(this.sessionOrder[0])}}async loadState(){try{const t=await(await fetch("/api/status")).json();this.handleInit(t)}catch(e){console.error("Failed to load state:",e)}}_debouncedCall(e,t,s=100){this._debounceTimers[e]&&clearTimeout(this._debounceTimers[e]),this._debounceTimers[e]=setTimeout(()=>{this._debounceTimers[e]=null,t.call(this)},s)}renderSessionTabs(){this._debouncedCall("sessionTabs",this._renderSessionTabsImmediate)}_updateActiveTabImmediate(e){const t=this.$("sessionTabs");if(!t)return;const s=t.querySelectorAll(".session-tab[data-id]");for(const i of s)i.dataset.id===e?i.classList.add("active"):i.classList.remove("active")}_renderSessionTabsImmediate(){const e=this.$("sessionTabs"),t=e.querySelectorAll(".session-tab[data-id]"),s=new Set([...t].map(o=>o.dataset.id)),i=new Set(this.sessions.keys());if(s.size===i.size&&[...s].every(o=>i.has(o)))for(const[o,r]of this.sessions){const a=e.querySelector(`.session-tab[data-id="${o}"]`);if(!a)continue;const h=o===this.activeSessionId,f=r.status||"idle",c=this.getSessionName(r),p=r.taskStats||{running:0,total:0},d=p.running>0;h&&!a.classList.contains("active")?a.classList.add("active"):!h&&a.classList.contains("active")&&a.classList.remove("active");const u=this.tabAlerts.get(o),w=u==="action",S=u==="idle",m=a.classList.contains("tab-alert-action"),_=a.classList.contains("tab-alert-idle");w&&!m?(a.classList.add("tab-alert-action"),a.classList.remove("tab-alert-idle")):S&&!_?(a.classList.add("tab-alert-idle"),a.classList.remove("tab-alert-action")):!u&&(m||_)&&a.classList.remove("tab-alert-action","tab-alert-idle");const E=a.querySelector(".tab-status");E&&!E.classList.contains(f)&&(E.className=`tab-status ${f}`);const b=a.querySelector(".tab-name");b&&b.textContent!==c&&(b.textContent=c);const T=a.querySelector(".tab-badge");if(d)if(T)T.textContent!==String(p.running)&&(T.textContent=p.running);else{this._fullRenderSessionTabs();return}else if(T){this._fullRenderSessionTabs();return}const g=a.querySelector(".tab-subagent-badge"),A=this.minimizedSubagents.get(o),v=A?.size||0;if(v>0&&g){const C=g.querySelector(".subagent-label"),y=v===1?"AGENT":`AGENTS (${v})`;C&&C.textContent!==y&&(C.textContent=y);const R=g.querySelector(".subagent-dropdown");if(R){const N=this.renderSubagentTabBadge(o,A),O=document.createElement("div");O.innerHTML=N;const I=O.querySelector(".subagent-dropdown");I&&(R.innerHTML=I.innerHTML)}}else if(v>0&&!g){const C=this.renderSubagentTabBadge(o,A),y=a.querySelector(".tab-gear");y&&y.insertAdjacentHTML("beforebegin",C)}else v===0&&g&&g.remove()}else this._fullRenderSessionTabs()}_fullRenderSessionTabs(){const e=this.$("sessionTabs");document.querySelectorAll("body > .subagent-dropdown").forEach(i=>i.remove()),this.cancelHideSubagentDropdown();const t=[];let s=this.sessionOrder;MobileDetection.getDeviceType()==="mobile"&&this.activeSessionId&&(s=[this.activeSessionId,...this.sessionOrder.filter(i=>i!==this.activeSessionId)]);for(const i of s){const n=this.sessions.get(i);if(!n)continue;const o=i===this.activeSessionId,r=n.status||"idle",a=this.getSessionName(n),h=n.mode||"claude",f=n.color||"default",c=n.taskStats||{running:0,total:0},p=c.running>0,d=this.tabAlerts.get(i),u=d==="action"?" tab-alert-action":d==="idle"?" tab-alert-idle":"",w=this.minimizedSubagents.get(i),m=(w?.size||0)>0?this.renderSubagentTabBadge(i,w):"",_=n.workingDir&&n.workingDir.split("/").pop()||"",b=(this._tallTabsEnabled??!1)&&n.name&&_&&_!==a,T=n._ended?' data-ended="1"':"";t.push(`<div class="session-tab ${o?"active":""}${u}" data-id="${i}" data-color="${f}"${T} onclick="app.selectSession('${escapeHtml(i)}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${escapeHtml(i)}')" tabindex="0" role="tab" aria-selected="${o?"true":"false"}" aria-label="${escapeHtml(a)} session" ${n.workingDir?`title="${escapeHtml(n.workingDir)}"`:""}>
8
- <span class="tab-status ${r}" aria-hidden="true"></span>
9
- <span class="tab-info">
10
- <span class="tab-name-row">
11
- ${h==="shell"?'<span class="tab-mode shell" aria-hidden="true">sh</span>':h==="opencode"?'<span class="tab-mode opencode" aria-hidden="true">oc</span>':""}
12
- <span class="tab-name" data-session-id="${i}">${escapeHtml(a)}</span>
13
- </span>
14
- ${b?`<span class="tab-folder">\u{1F4C1} ${escapeHtml(_)}</span>`:""}
15
- </span>
16
- ${p?`<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()" aria-label="${c.running} running tasks">${c.running}</span>`:""}
17
- ${m}
18
- <span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${escapeHtml(i)}')" title="Session options" aria-label="Session options" tabindex="0">&#x2699;</span>
19
- <span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${escapeHtml(i)}')" title="Close session" aria-label="Close session" tabindex="0">&times;</span>
20
- </div>`)}e.innerHTML=t.join(""),this._saveTabMetadata(),this.setupTabDragHandlers(),this.setupTabKeyboardNavigation(e),this.updateConnectionLines()}setupTabKeyboardNavigation(e){this._tabKeydownHandler&&e.removeEventListener("keydown",this._tabKeydownHandler),this._tabKeydownHandler=t=>{if(!["ArrowLeft","ArrowRight","Home","End","Enter"," "].includes(t.key))return;const s=[...e.querySelectorAll(".session-tab")],i=s.indexOf(document.activeElement);if((t.key==="Enter"||t.key===" ")&&i>=0){t.preventDefault();const o=s[i].dataset.id;this.selectSession(o);return}if(i<0)return;let n;switch(t.key){case"ArrowLeft":n=i>0?i-1:s.length-1;break;case"ArrowRight":n=i<s.length-1?i+1:0;break;case"Home":n=0;break;case"End":n=s.length-1;break;default:return}t.preventDefault(),s[n]?.focus()},e.addEventListener("keydown",this._tabKeydownHandler)}syncSessionOrder(){const e=new Set(this.sessions.keys()),s=this.loadSessionOrder().filter(o=>e.has(o)),i=new Set(s),n=[...e].filter(o=>!i.has(o));this.sessionOrder=[...s,...n]}loadSessionOrder(){try{const e=localStorage.getItem("codeman-session-order");return e?JSON.parse(e):[]}catch{return[]}}saveSessionOrder(){try{localStorage.setItem("codeman-session-order",JSON.stringify(this.sessionOrder))}catch{}}_saveTabMetadata(){try{const e={};for(const[t,s]of this.sessions)s._ended||(e[t]={id:t,name:s.name||"",workingDir:s.workingDir||"",mode:s.mode||"claude",color:s.color||"default"});localStorage.setItem("codeman-tab-meta",JSON.stringify(e))}catch{}}_restoreEndedTabs(){try{const e=localStorage.getItem("codeman-tab-meta");if(!e)return;const t=JSON.parse(e);for(const[s,i]of Object.entries(t))this.sessions.has(s)||this.sessions.set(s,{id:s,name:i.name,workingDir:i.workingDir,mode:i.mode,color:i.color,status:"ended",_ended:!0})}catch{}}setupTabDragHandlers(){const e=this.$("sessionTabs");e.querySelectorAll(".session-tab[data-id]").forEach(s=>{s.setAttribute("draggable","true"),s.addEventListener("dragstart",i=>{this.draggedTabId=s.dataset.id,s.classList.add("dragging"),i.dataTransfer.effectAllowed="move",i.dataTransfer.setData("text/plain",s.dataset.id)}),s.addEventListener("dragend",()=>{s.classList.remove("dragging"),this.draggedTabId=null,e.querySelectorAll(".session-tab").forEach(i=>{i.classList.remove("drag-over-left","drag-over-right")})}),s.addEventListener("dragover",i=>{if(i.preventDefault(),!this.draggedTabId||this.draggedTabId===s.dataset.id)return;i.dataTransfer.dropEffect="move";const n=s.getBoundingClientRect(),o=n.left+n.width/2,r=i.clientX<o;s.classList.toggle("drag-over-left",r),s.classList.toggle("drag-over-right",!r)}),s.addEventListener("dragleave",()=>{s.classList.remove("drag-over-left","drag-over-right")}),s.addEventListener("drop",i=>{if(i.preventDefault(),s.classList.remove("drag-over-left","drag-over-right"),!this.draggedTabId||this.draggedTabId===s.dataset.id)return;const n=s.dataset.id,o=this.draggedTabId,r=s.getBoundingClientRect(),a=r.left+r.width/2,h=i.clientX<a,f=this.sessionOrder.indexOf(o);let c=this.sessionOrder.indexOf(n);f===-1||c===-1||(this.sessionOrder.splice(f,1),c=this.sessionOrder.indexOf(n),c!==-1&&(h?this.sessionOrder.splice(c,0,o):this.sessionOrder.splice(c+1,0,o),this.saveSessionOrder(),this._fullRenderSessionTabs()))})})}getShortId(e){if(!e)return"";let t=this._shortIdCache.get(e);return t||(t=e.slice(0,8),this._shortIdCache.set(e,t)),t}getSessionName(e){return e.name?e.name:e.workingDir?e.workingDir.split("/").pop()||e.workingDir:this.getShortId(e.id)}async selectSession(e){if(this.activeSessionId===e)return;this.terminal&&this.terminal.focus();const t=performance.now(),s=this.sessions.get(e)?.name||e.slice(0,8);_crashDiag.log(`SELECT: ${s}`),console.log(`[CRASH-DIAG] selectSession START: ${e.slice(0,8)}`);const i=++this._selectGeneration;if(i!==this._selectGeneration)return;this._disconnectWs();const n=document.getElementById("cjkInput");n&&(n.value=""),this.flickerFilterTimeout&&(clearTimeout(this.flickerFilterTimeout),this.flickerFilterTimeout=null),this.flickerFilterBuffer="",this.flickerFilterActive=!1,this._tabCompletionSessionId=null,this._tabCompletionRetries=0,this._tabCompletionBaseText=null,this._tabCompletionFallback&&(clearTimeout(this._tabCompletionFallback),this._tabCompletionFallback=null),this._clientDropRecoveryTimer&&(clearTimeout(this._clientDropRecoveryTimer),this._clientDropRecoveryTimer=null),this.syncWaitTimeout&&(clearTimeout(this.syncWaitTimeout),this.syncWaitTimeout=null),this.pendingWrites=[],this.writeFrameScheduled=!1,this._isLoadingBuffer=!1,this._loadBufferQueue=null,this._chunkedWriteGen=(this._chunkedWriteGen||0)+1;try{const a=this.terminal?._core?._compositionHelper;if(a?._isComposing){a._isComposing=!1;const h=this.terminal?.element?.querySelector(".xterm-helper-textarea");h&&h.dispatchEvent(new CompositionEvent("compositionend",{data:""}))}}catch{}if(this.activeSessionId){const a=this._localEchoOverlay?.pendingText||"",h=this._localEchoOverlay?.getFlushed()?.count||0,f=this._localEchoOverlay?.getFlushed()?.text||"";a&&this._sendInputAsync(this.activeSessionId,a);const c=h+a.length;c>0&&(this._flushedOffsets||(this._flushedOffsets=new Map),this._flushedTexts||(this._flushedTexts=new Map),this._flushedOffsets.set(this.activeSessionId,c),this._flushedTexts.set(this.activeSessionId,f+a))}this._localEchoOverlay?.clear(),this._localEchoOverlay&&!this._flushedOffsets?.has(e)&&this._localEchoOverlay.suppressBufferDetection(),this.activeSessionId=e;try{localStorage.setItem("codeman-active-session",e)}catch{}this.hideWelcome(),this.clearPendingHooks(e,"idle_prompt"),this._updateActiveTabImmediate(e),this.renderSessionTabs(),this._updateLocalEchoState(),this._flushedOffsets?.has(e)&&this._localEchoOverlay&&this._localEchoOverlay.setFlushed(this._flushedOffsets.get(e),this._flushedTexts?.get(e)||"",!1);const o=document.querySelector(`.session-tab.active[data-id="${e}"]`);o&&(o.classList.add("tab-glow"),o.addEventListener("animationend",()=>o.classList.remove("tab-glow"),{once:!0}));const r=this.sessions.get(e);if(r?._ended){this.terminal.clear(),this.terminal.write(`\r
21
- \x1B[2mSession ended. Close tab or click to reopen.\x1B[0m\r
22
- `);return}if(this.currentSessionWorkingDir=r?.workingDir||null,r&&r.pid===null&&r.status==="idle")try{const a=r.mode==="shell"?`/api/sessions/${e}/shell`:`/api/sessions/${e}/interactive`;await fetch(a,{method:"POST"}),r.status="busy"}catch(a){console.error("Failed to attach to restored session:",a)}this._restoringFlushedState=!0,this._isLoadingBuffer=!0,this._loadBufferQueue=[];try{this.fitAddon&&this.fitAddon.fit();const a=this.terminalBufferCache.get(e),h=r&&(r.status==="busy"||r.status==="working");if(a&&!h){if(_crashDiag.log(`CACHE_WRITE: ${(a.length/1024).toFixed(0)}KB`),this.terminal.clear(),this.terminal.reset(),await this.chunkedTerminalWrite(a),i!==this._selectGeneration){this._isLoadingBuffer&&this._finishBufferLoad(),this._restoringFlushedState=!1;return}this.terminal.scrollToBottom(),_crashDiag.log("CACHE_DONE")}else h&&(this.terminal.clear(),this.terminal.reset(),_crashDiag.log("CACHE_SKIP_BUSY"));_crashDiag.log("FETCH_START");const f=await fetch(`/api/sessions/${e}/terminal?tail=${TERMINAL_TAIL_SIZE}`);if(i!==this._selectGeneration){this._isLoadingBuffer&&this._finishBufferLoad(),this._restoringFlushedState=!1;return}const c=await f.json();if(_crashDiag.log(`FETCH_DONE: ${c.terminalBuffer?(c.terminalBuffer.length/1024).toFixed(0)+"KB":"empty"} truncated=${c.truncated}`),c.terminalBuffer){if(c.terminalBuffer!==a){if(_crashDiag.log(`REWRITE: ${(c.terminalBuffer.length/1024).toFixed(0)}KB`),this.terminal.clear(),this.terminal.reset(),c.truncated&&this.terminal.write(`\x1B[90m... (earlier output truncated for performance) ...\x1B[0m\r
23
- \r
24
- `),await this.chunkedTerminalWrite(c.terminalBuffer),i!==this._selectGeneration){this._isLoadingBuffer&&this._finishBufferLoad(),this._restoringFlushedState=!1;return}this.terminal.scrollToBottom()}if(this.terminalBufferCache.set(e,c.terminalBuffer),this.terminalBufferCache.size>20){const u=this.terminalBufferCache.keys().next().value;this.terminalBufferCache.delete(u)}}else a||(this.terminal.clear(),this.terminal.reset());if(this._isLoadingBuffer&&this._finishBufferLoad(),this._restoringFlushedState=!1,this._flushedOffsets?.has(e)&&this._localEchoOverlay){this._localEchoOverlay.setFlushed(this._flushedOffsets.get(e),this._flushedTexts?.get(e)||"",!1);const d=this._localEchoOverlay;this.terminal.write("",()=>{d.hasPending&&d.rerender()})}this.sendResize(e).then(()=>{i===this._selectGeneration&&fetch(`/api/sessions/${e}/input`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:"\f"})}).catch(()=>{})}),(typeof requestIdleCallback=="function"?requestIdleCallback:d=>setTimeout(d,16))(()=>{if(i!==this._selectGeneration)return;this.respawnStatus[e]?(this.showRespawnBanner(),this.updateRespawnBanner(this.respawnStatus[e].state),document.getElementById("respawnCycleCount").textContent=this.respawnStatus[e].cycleCount||0,this.updateCountdownTimerDisplay(),this.updateActionLogDisplay(),Object.keys(this.respawnCountdownTimers[e]||{}).length>0&&this.startCountdownInterval()):(this.hideRespawnBanner(),this.stopCountdownInterval());const d=document.getElementById("taskPanel");d&&d.classList.contains("open")&&this.renderTaskPanel();const u=this.sessions.get(e);if(u&&(u.ralphLoop||u.ralphTodos)&&this.updateRalphState(e,{loop:u.ralphLoop,todos:u.ralphTodos}),this.renderRalphStatePanel(),this.updateCliInfoDisplay(),this.renderProjectInsightsPanel(),this.updateSubagentWindowVisibility(),this.loadAppSettingsFromStorage().showFileBrowser){const S=this.$("fileBrowserPanel");if(S&&(S.classList.add("visible"),this.loadFileBrowser(e),!this.fileBrowserDragListeners)){const m=S.querySelector(".file-browser-header");if(m){const _=()=>{if(!S.style.left){const E=S.getBoundingClientRect();S.style.left=`${E.left}px`,S.style.top=`${E.top}px`,S.style.right="auto"}};m.addEventListener("mousedown",_),m.addEventListener("touchstart",_,{passive:!0}),this.fileBrowserDragListeners=this.makeWindowDraggable(S,m),this.fileBrowserDragListeners._onFirstDrag=_}}}}),this._connectWs(e),_crashDiag.log("FOCUS"),this.terminal.focus(),this.terminal.scrollToBottom(),_crashDiag.log(`SELECT_DONE: ${(performance.now()-t).toFixed(0)}ms`),console.log(`[CRASH-DIAG] selectSession DONE: ${e.slice(0,8)} in ${(performance.now()-t).toFixed(0)}ms`)}catch(a){this._isLoadingBuffer&&this._finishBufferLoad(),this._restoringFlushedState=!1,console.error("Failed to load session terminal:",a)}}_cleanupSessionData(e){this.sessions.delete(e);const t=this.sessionOrder.indexOf(e);t!==-1&&(this.sessionOrder.splice(t,1),this.saveSessionOrder()),this.terminalBuffers.delete(e),this.terminalBufferCache.delete(e),this._flushedOffsets?.delete(e),this._flushedTexts?.delete(e),this._inputQueue.delete(e),this.ralphStates.delete(e),this.ralphClosedSessions.delete(e),this.projectInsights.delete(e),this.pendingHooks.delete(e),this.tabAlerts.delete(e),this.clearCountdownTimers(e),this.closeSessionLogViewerWindows(e),this.closeSessionImagePopups(e),this.closeSessionSubagentWindows(e,!0);const s=this.idleTimers.get(e);s&&(clearTimeout(s),this.idleTimers.delete(e)),delete this.respawnStatus[e],delete this.respawnTimers[e],delete this.respawnCountdownTimers[e],delete this.respawnActionLogs[e]}async closeSession(e,t=!0){try{if(await this._apiDelete(`/api/sessions/${e}?killMux=${t}`),this._cleanupSessionData(e),this.activeSessionId===e){this.activeSessionId=null;try{localStorage.removeItem("codeman-active-session")}catch{}if(this.sessionOrder.length>0&&this.sessions.size>0){const s=this.sessionOrder[0];this.selectSession(s)}else this.terminal.clear(),this.showWelcome(),this.renderRalphStatePanel()}this.renderSessionTabs(),t?this.showToast("Session closed and tmux killed","success"):this.showToast("Tab hidden, tmux still running","info")}catch{this.showToast("Failed to close session","error")}}requestCloseSession(e){const t=this.sessions.get(e);if(!t)return;this.pendingCloseSessionId=e;const s=this.getSessionName(t),i=document.getElementById("closeConfirmSessionName");i.textContent=s;const n=document.getElementById("closeConfirmKillTitle");n&&(n.textContent=t.mode==="opencode"?"Kill Tmux & OpenCode":"Kill Tmux & Claude Code"),document.getElementById("closeConfirmModal").classList.add("active")}cancelCloseSession(){this.pendingCloseSessionId=null,document.getElementById("closeConfirmModal").classList.remove("active")}async confirmCloseSession(e=!0){const t=this.pendingCloseSessionId;this.cancelCloseSession(),t&&await this.closeSession(t,e)}nextSession(){if(this.sessionOrder.length<=1)return;const t=(this.sessionOrder.indexOf(this.activeSessionId)+1)%this.sessionOrder.length;this.selectSession(this.sessionOrder[t])}prevSession(){if(this.sessionOrder.length<=1)return;const t=(this.sessionOrder.indexOf(this.activeSessionId)-1+this.sessionOrder.length)%this.sessionOrder.length;this.selectSession(this.sessionOrder[t])}goHome(){this.activeSessionId=null;try{localStorage.removeItem("codeman-active-session")}catch{}this.terminal.clear(),this.showWelcome(),this.renderSessionTabs(),this.renderRalphStatePanel()}ralphWizardStep=1;ralphWizardConfig={taskDescription:"",completionPhrase:"COMPLETE",maxIterations:10,caseName:"testcase",enableRespawn:!1,generatedPlan:null,planGenerated:!1,skipPlanGeneration:!1,planDetailLevel:"detailed",existingPlan:null,useExistingPlan:!1};planLoadingTimer=null;planLoadingStartTime=null;async killActiveSession(){if(!this.activeSessionId){this.showToast("No active session","warning");return}await this.closeSession(this.activeSessionId)}async killAllSessions(){if(this.sessions.size!==0&&confirm(`Kill all ${this.sessions.size} session(s)?`))try{await this._apiDelete("/api/sessions"),this.sessions.clear(),this.terminalBuffers.clear(),this.terminalBufferCache.clear(),this.activeSessionId=null;try{localStorage.removeItem("codeman-active-session")}catch{}this.respawnStatus={},this.respawnCountdownTimers={},this.respawnActionLogs={},this.stopCountdownInterval(),this.hideRespawnBanner(),this.renderSessionTabs(),this.terminal.clear(),this.showWelcome(),this.showToast("All sessions killed","success")}catch{this.showToast("Failed to kill sessions","error")}}showTimer(){document.getElementById("timerBanner").style.display="flex",this.updateTimer(),this.timerInterval=setInterval(()=>this.updateTimer(),1e3)}hideTimer(){document.getElementById("timerBanner").style.display="none",this.timerInterval&&(clearInterval(this.timerInterval),this.timerInterval=null)}updateTimer(){if(!this.currentRun||this.currentRun.status!=="running")return;const e=Date.now(),t=Math.max(0,this.currentRun.endAt-e),s=this.currentRun.endAt-this.currentRun.startedAt,i=e-this.currentRun.startedAt,n=Math.min(100,i/s*100);document.getElementById("timerValue").textContent=this.formatTime(t),document.getElementById("timerProgress").style.width=`${n}%`,document.getElementById("timerMeta").textContent=`${this.currentRun.completedTasks} tasks | $${this.currentRun.totalCost.toFixed(2)}`}async stopCurrentRun(){if(this.currentRun)try{await fetch(`/api/scheduled/${this.currentRun.id}`,{method:"DELETE"})}catch{this.showToast("Failed to stop run","error")}}formatTime(e){const t=Math.floor(e/1e3),s=Math.floor(t/3600),i=Math.floor(t%3600/60),n=t%60;return`${s.toString().padStart(2,"0")}:${i.toString().padStart(2,"0")}:${n.toString().padStart(2,"0")}`}updateCost(){this.updateTokens()}updateTokens(){this._updateTokensTimeout&&clearTimeout(this._updateTokensTimeout),this._updateTokensTimeout=setTimeout(()=>{this._updateTokensTimeout=null,this._updateTokensImmediate()},200)}_updateTokensImmediate(){let e=0,t=0;this.globalStats?(e=this.globalStats.totalInputTokens||0,t=this.globalStats.totalOutputTokens||0):this.sessions.forEach(r=>{r.tokens&&(e+=r.tokens.input||0,t+=r.tokens.output||0)});const s=e+t;this.totalTokens=s;const i=this.formatTokens(s),n=this.estimateCost(e,t),o=this.$("headerTokens");if(o){const a=this.loadAppSettingsFromStorage().showCost??!1;o.textContent=s>0?a?`${i} tokens \xB7 $${n.toFixed(2)}`:`${i} tokens`:"0 tokens",o.title=this.globalStats?`Lifetime: ${this.globalStats.totalSessionsCreated} sessions created${a?`
25
- Estimated cost based on Claude Opus pricing`:""}`:`Token usage across active sessions${a?`
26
- Estimated cost based on Claude Opus pricing`:""}`}}}try{for(let l=0;l<localStorage.length;l++){const e=localStorage.key(l);if(e&&(e.startsWith("claudeman-")||e.startsWith("claudeman_"))){const t=e.replace(/^claudeman[-_]/,s=>"codeman"+s.charAt(s.length-1));localStorage.getItem(t)===null&&localStorage.setItem(t,localStorage.getItem(e))}}}catch{}let app;document.addEventListener("DOMContentLoaded",()=>{app=new CodemanApp,window.app=app}),window.MobileDetection=MobileDetection;
Binary file
Binary file