aicodeman 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/config/workflow-config.d.ts +24 -0
  2. package/dist/config/workflow-config.d.ts.map +1 -0
  3. package/dist/config/workflow-config.js +24 -0
  4. package/dist/config/workflow-config.js.map +1 -0
  5. package/dist/subagent-watcher.d.ts +34 -0
  6. package/dist/subagent-watcher.d.ts.map +1 -1
  7. package/dist/subagent-watcher.js +147 -4
  8. package/dist/subagent-watcher.js.map +1 -1
  9. package/dist/types/index.d.ts +1 -0
  10. package/dist/types/index.d.ts.map +1 -1
  11. package/dist/types/index.js +1 -0
  12. package/dist/types/index.js.map +1 -1
  13. package/dist/types/workflow-run.d.ts +130 -0
  14. package/dist/types/workflow-run.d.ts.map +1 -0
  15. package/dist/types/workflow-run.js +20 -0
  16. package/dist/types/workflow-run.js.map +1 -0
  17. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  18. package/dist/web/public/{app.92f49a9d.js → app.6b133aaf.js} +6 -6
  19. package/dist/web/public/app.6b133aaf.js.br +0 -0
  20. package/dist/web/public/app.6b133aaf.js.gz +0 -0
  21. package/dist/web/public/{constants.59faac65.js → constants.1c779517.js} +5 -0
  22. package/dist/web/public/constants.1c779517.js.br +0 -0
  23. package/dist/web/public/constants.1c779517.js.gz +0 -0
  24. package/dist/web/public/image-input.0ea86695.js.gz +0 -0
  25. package/dist/web/public/index.html +48 -8
  26. package/dist/web/public/index.html.br +0 -0
  27. package/dist/web/public/index.html.gz +0 -0
  28. package/dist/web/public/input-cjk.b8686b5e.js.gz +0 -0
  29. package/dist/web/public/keyboard-accessory.bc753cc7.js.gz +0 -0
  30. package/dist/web/public/mobile-handlers.db3dc3c8.js.gz +0 -0
  31. package/dist/web/public/mobile.06b38d3a.css.gz +0 -0
  32. package/dist/web/public/notification-manager.9c984ac2.js.gz +0 -0
  33. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  34. package/dist/web/public/{panels-ui.2f467969.js → panels-ui.f3f08e26.js} +48 -48
  35. package/dist/web/public/panels-ui.f3f08e26.js.br +0 -0
  36. package/dist/web/public/panels-ui.f3f08e26.js.gz +0 -0
  37. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  38. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  39. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  40. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  41. package/dist/web/public/session-ui.1463b824.js.gz +0 -0
  42. package/dist/web/public/settings-ui.08f7708b.js +55 -0
  43. package/dist/web/public/settings-ui.08f7708b.js.br +0 -0
  44. package/dist/web/public/settings-ui.08f7708b.js.gz +0 -0
  45. package/dist/web/public/{styles.8e1ea0c6.css → styles.379f31e0.css} +1 -1
  46. package/dist/web/public/styles.379f31e0.css.br +0 -0
  47. package/dist/web/public/styles.379f31e0.css.gz +0 -0
  48. package/dist/web/public/{subagent-windows.a366a4ad.js → subagent-windows.07e139f2.js} +9 -0
  49. package/dist/web/public/subagent-windows.07e139f2.js.br +0 -0
  50. package/dist/web/public/subagent-windows.07e139f2.js.gz +0 -0
  51. package/dist/web/public/sw.js.gz +0 -0
  52. package/dist/web/public/terminal-ui.a7e046da.js.gz +0 -0
  53. package/dist/web/public/ultracode-panel.js +314 -0
  54. package/dist/web/public/ultracode-panel.js.br +0 -0
  55. package/dist/web/public/ultracode-panel.js.gz +0 -0
  56. package/dist/web/public/ultracode-windows.js +382 -0
  57. package/dist/web/public/ultracode-windows.js.br +0 -0
  58. package/dist/web/public/ultracode-windows.js.gz +0 -0
  59. package/dist/web/public/upload.html.gz +0 -0
  60. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  61. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  62. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  63. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  64. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  65. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  66. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  67. package/dist/web/public/vendor/xterm.css.gz +0 -0
  68. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  69. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  70. package/dist/web/routes/file-routes.d.ts.map +1 -1
  71. package/dist/web/routes/file-routes.js +80 -24
  72. package/dist/web/routes/file-routes.js.map +1 -1
  73. package/dist/web/routes/system-routes.d.ts.map +1 -1
  74. package/dist/web/routes/system-routes.js +23 -0
  75. package/dist/web/routes/system-routes.js.map +1 -1
  76. package/dist/web/schemas.d.ts +2 -0
  77. package/dist/web/schemas.d.ts.map +1 -1
  78. package/dist/web/schemas.js +4 -0
  79. package/dist/web/schemas.js.map +1 -1
  80. package/dist/web/server.d.ts +14 -0
  81. package/dist/web/server.d.ts.map +1 -1
  82. package/dist/web/server.js +57 -0
  83. package/dist/web/server.js.map +1 -1
  84. package/dist/web/sse-events.d.ts +10 -0
  85. package/dist/web/sse-events.d.ts.map +1 -1
  86. package/dist/web/sse-events.js +12 -0
  87. package/dist/web/sse-events.js.map +1 -1
  88. package/dist/workflow-run-watcher.d.ts +76 -0
  89. package/dist/workflow-run-watcher.d.ts.map +1 -0
  90. package/dist/workflow-run-watcher.js +327 -0
  91. package/dist/workflow-run-watcher.js.map +1 -0
  92. package/package.json +1 -1
  93. package/dist/web/public/app.92f49a9d.js.br +0 -0
  94. package/dist/web/public/app.92f49a9d.js.gz +0 -0
  95. package/dist/web/public/constants.59faac65.js.br +0 -0
  96. package/dist/web/public/constants.59faac65.js.gz +0 -0
  97. package/dist/web/public/panels-ui.2f467969.js.br +0 -0
  98. package/dist/web/public/panels-ui.2f467969.js.gz +0 -0
  99. package/dist/web/public/settings-ui.44b99ce0.js +0 -55
  100. package/dist/web/public/settings-ui.44b99ce0.js.br +0 -0
  101. package/dist/web/public/settings-ui.44b99ce0.js.gz +0 -0
  102. package/dist/web/public/styles.8e1ea0c6.css.br +0 -0
  103. package/dist/web/public/styles.8e1ea0c6.css.gz +0 -0
  104. package/dist/web/public/subagent-windows.a366a4ad.js.br +0 -0
  105. package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
@@ -0,0 +1,382 @@
1
+ /**
2
+ * @fileoverview Ultracode floating run windows — auto-popping draggable windows
3
+ * with a connector line to the originating session tab.
4
+ *
5
+ * This is the "floating thing" companion to the docked master-detail panel in
6
+ * `ultracode-panel.js` (the dock panel stays — these windows are ADDITIONAL).
7
+ * When the `showUltracodeAgents` setting is on, a small floating window pops up
8
+ * for each ACTIVE ultracode/Workflow run (status not completed/killed/failed),
9
+ * mirroring the live agent grid, and is connected by a glowing line to the
10
+ * Codeman tab whose `claudeSessionId` matches the run's `sessionUuid` — the same
11
+ * line idiom subagent windows use. The window auto-closes a few seconds after
12
+ * its run finishes; an explicitly-closed run is remembered and never re-pops.
13
+ *
14
+ * Reuses, rather than duplicates:
15
+ * - `makeWindowDraggable` + the shared `#connectionLines` SVG (subagent-windows.js)
16
+ * - `_workflowAgentCardHtml`, `_fmtNum`, `_workflowStatusClass`, `_fetchWorkflowRunDetail`,
17
+ * and the `workflowRuns` / `workflowRunDetails` maps (ultracode-panel.js)
18
+ *
19
+ * The connector-line draw is appended to the shared SVG from inside
20
+ * `_updateConnectionLinesImmediate` (subagent-windows.js calls
21
+ * `_appendUltracodeConnectionLines` at the end of its render pass), so both the
22
+ * subagent and ultracode lines live in one batched read→write reflow pass.
23
+ *
24
+ * @mixin Extends CodemanApp.prototype via Object.assign
25
+ * @dependency subagent-windows.js (makeWindowDraggable, updateConnectionLines, #connectionLines)
26
+ * @dependency ultracode-panel.js (workflowRuns/workflowRunDetails, _workflowAgentCardHtml, _fmtNum)
27
+ * @loadorder 15.5 (after subagent-windows.js — needs makeWindowDraggable at runtime)
28
+ */
29
+ /* global CodemanApp, escapeHtml */
30
+
31
+ Object.assign(CodemanApp.prototype, {
32
+ /** Lazily seed the floating-window state maps (constructor also seeds them). */
33
+ _ensureUltracodeWindowState() {
34
+ if (!this.ultracodeWindows) this.ultracodeWindows = new Map(); // runId -> { element, parentSessionId, dragListeners, collapsed }
35
+ if (!this.ultracodeWindowsClosed) this.ultracodeWindowsClosed = new Set(); // runIds the user dismissed
36
+ if (!this.ultracodeWindowCloseTimers) this.ultracodeWindowCloseTimers = new Map(); // runId -> setTimeout id
37
+ if (this.ultracodeWindowZIndex === undefined) this.ultracodeWindowZIndex = 1000;
38
+ },
39
+
40
+ /** Floating windows have their own opt-in (default OFF), independent of the dock panel. */
41
+ _ultracodeFloatingEnabled() {
42
+ const settings = this.loadAppSettingsFromStorage ? this.loadAppSettingsFromStorage() : {};
43
+ return !!(settings && settings.ultracodeFloatingWindows);
44
+ },
45
+
46
+ /** A run is "working" until it reaches a terminal status. Mid-run status is absent. */
47
+ _isWorkflowRunActive(run) {
48
+ const s = String((run && run.status) || '');
49
+ return !(s === 'completed' || s === 'killed' || s === 'failed');
50
+ },
51
+
52
+ /**
53
+ * Resolve which Codeman tab a run belongs to: the session whose
54
+ * `claudeSessionId` equals the run's `sessionUuid` (the path segment the watcher
55
+ * captured). Falls back to the active session so the line still lands somewhere.
56
+ */
57
+ _resolveUltracodeParentSession(run) {
58
+ const uuid = run && run.sessionUuid;
59
+ if (uuid && this.sessions) {
60
+ for (const [sessionId, session] of this.sessions) {
61
+ if (session && session.claudeSessionId === uuid) return sessionId;
62
+ }
63
+ }
64
+ if (this.activeSessionId && this.sessions && this.sessions.has(this.activeSessionId)) {
65
+ return this.activeSessionId;
66
+ }
67
+ return null;
68
+ },
69
+
70
+ /**
71
+ * Auto-pop driver — called for every run discovered/updated and on reconnect seed.
72
+ * Creates a floating window for active runs, refreshes existing ones, and schedules
73
+ * an auto-close once a run finishes.
74
+ */
75
+ _syncUltracodeFloatingWindow(run, opts) {
76
+ this._ensureUltracodeWindowState();
77
+ if (!run || !run.runId) return;
78
+ if (!this._ultracodeFloatingEnabled()) return;
79
+ const runId = run.runId;
80
+ if (this.ultracodeWindowsClosed.has(runId)) return; // respect explicit dismissal
81
+
82
+ const active = this._isWorkflowRunActive(run);
83
+ const existing = this.ultracodeWindows.get(runId);
84
+
85
+ if (active) {
86
+ // Run is alive — cancel any pending auto-close.
87
+ const pending = this.ultracodeWindowCloseTimers.get(runId);
88
+ if (pending) {
89
+ clearTimeout(pending);
90
+ this.ultracodeWindowCloseTimers.delete(runId);
91
+ }
92
+ if (existing) {
93
+ this.renderUltracodeWindowContent(runId);
94
+ this._fetchWorkflowRunDetail(runId); // refresh agents[]; re-renders window on land
95
+ } else {
96
+ // On a reconnect snapshot, only restore windows for genuinely recent runs so
97
+ // a backlog of stale undefined-status runs doesn't carpet the screen.
98
+ if (opts && opts.fromSeed) {
99
+ const FLOAT_SEED_MAX_AGE_MS = 5 * 60 * 1000;
100
+ const age = Date.now() - (run.lastActivityAt || 0);
101
+ if (!(run.lastActivityAt && age < FLOAT_SEED_MAX_AGE_MS)) return;
102
+ }
103
+ this.createUltracodeWindow(run);
104
+ }
105
+ } else if (existing) {
106
+ // Finished — refresh to the final state (status + final agent states), show it
107
+ // briefly, then retire the floating window.
108
+ this._fetchWorkflowRunDetail(runId);
109
+ this.renderUltracodeWindowContent(runId);
110
+ if (!this.ultracodeWindowCloseTimers.has(runId)) {
111
+ const FLOAT_FINISH_GRACE_MS = 8000;
112
+ const timer = setTimeout(() => {
113
+ this.ultracodeWindowCloseTimers.delete(runId);
114
+ this.closeUltracodeWindow(runId, false);
115
+ }, FLOAT_FINISH_GRACE_MS);
116
+ this.ultracodeWindowCloseTimers.set(runId, timer);
117
+ }
118
+ }
119
+ },
120
+
121
+ /** Build and mount a floating window for a run, positioned near its parent tab. */
122
+ createUltracodeWindow(run) {
123
+ this._ensureUltracodeWindowState();
124
+ const runId = run.runId;
125
+ if (this.ultracodeWindows.has(runId)) return;
126
+ const parentSessionId = this._resolveUltracodeParentSession(run);
127
+ const titleText = run.workflowName || run.summary || runId;
128
+
129
+ const win = document.createElement('div');
130
+ win.className = 'ultracode-window spawning';
131
+ win.id = `ultracode-window-${runId}`;
132
+ win.style.zIndex = ++this.ultracodeWindowZIndex;
133
+ win.innerHTML = `
134
+ <div class="ultracode-window-header">
135
+ <div class="ultracode-window-title" title="${escapeHtml(titleText)}">
136
+ <span class="icon">🧬</span>
137
+ <span class="uw-name">${escapeHtml(titleText)}</span>
138
+ <span class="uw-status"></span>
139
+ </div>
140
+ <div class="ultracode-window-actions">
141
+ <button class="uw-min" type="button" title="Collapse">─</button>
142
+ <button class="uw-close" type="button" title="Close">&times;</button>
143
+ </div>
144
+ </div>
145
+ <div class="ultracode-window-body" id="ultracode-window-body-${runId}">
146
+ <div class="subagent-empty">Loading agents…</div>
147
+ </div>
148
+ `;
149
+
150
+ // Position: spawn from the parent tab if we can find it, else cascade.
151
+ const parentTab = parentSessionId ? document.querySelector(`.session-tab[data-id="${parentSessionId}"]`) : null;
152
+ if (parentTab) {
153
+ const r = parentTab.getBoundingClientRect();
154
+ const left = Math.max(8, Math.min(r.left, window.innerWidth - 392));
155
+ win.style.left = `${left}px`;
156
+ win.style.top = `${r.bottom + 14}px`;
157
+ } else {
158
+ const n = this.ultracodeWindows.size;
159
+ win.style.left = `${24 + n * 26}px`;
160
+ win.style.top = `${96 + n * 26}px`;
161
+ }
162
+
163
+ document.body.appendChild(win);
164
+ // Drop the spawn class on the next frame so the transition runs.
165
+ requestAnimationFrame(() => win.classList.remove('spawning'));
166
+
167
+ const header = win.querySelector('.ultracode-window-header');
168
+ const dragListeners = this.makeWindowDraggable(win, header);
169
+
170
+ win.querySelector('.uw-min').addEventListener('click', (e) => {
171
+ e.stopPropagation();
172
+ this.toggleUltracodeWindowCollapse(runId);
173
+ });
174
+ win.querySelector('.uw-close').addEventListener('click', (e) => {
175
+ e.stopPropagation();
176
+ this.closeUltracodeWindow(runId, true);
177
+ });
178
+ const nameEl = win.querySelector('.uw-name');
179
+ if (parentSessionId) {
180
+ nameEl.style.cursor = 'pointer';
181
+ nameEl.title = 'Go to session';
182
+ nameEl.addEventListener('click', () => this.selectSession(parentSessionId));
183
+ }
184
+
185
+ this.ultracodeWindows.set(runId, { element: win, parentSessionId, dragListeners, collapsed: false });
186
+
187
+ this.renderUltracodeWindowContent(runId);
188
+ this._fetchWorkflowRunDetail(runId); // pull agents[] for the body
189
+ this.updateConnectionLines();
190
+ },
191
+
192
+ /** Collapse/expand the window to header-only (line stays connected). */
193
+ toggleUltracodeWindowCollapse(runId) {
194
+ const data = this.ultracodeWindows.get(runId);
195
+ if (!data) return;
196
+ data.collapsed = !data.collapsed;
197
+ data.element.classList.toggle('collapsed', data.collapsed);
198
+ this.updateConnectionLines();
199
+ },
200
+
201
+ /** Remove a floating window. `userInitiated` records a dismissal so it won't re-pop. */
202
+ closeUltracodeWindow(runId, userInitiated) {
203
+ this._ensureUltracodeWindowState();
204
+ const pending = this.ultracodeWindowCloseTimers.get(runId);
205
+ if (pending) {
206
+ clearTimeout(pending);
207
+ this.ultracodeWindowCloseTimers.delete(runId);
208
+ }
209
+ const data = this.ultracodeWindows.get(runId);
210
+ if (userInitiated) this.ultracodeWindowsClosed.add(runId);
211
+ if (!data) return;
212
+ this._teardownUltracodeDrag(data.dragListeners);
213
+ data.element.remove();
214
+ this.ultracodeWindows.delete(runId);
215
+ this.updateConnectionLines();
216
+ },
217
+
218
+ /** Detach the document-level drag listeners returned by makeWindowDraggable. */
219
+ _teardownUltracodeDrag(dl) {
220
+ if (!dl) return;
221
+ document.removeEventListener('mousemove', dl.move);
222
+ document.removeEventListener('mouseup', dl.up);
223
+ if (dl.touchMove) {
224
+ document.removeEventListener('touchmove', dl.touchMove);
225
+ document.removeEventListener('touchend', dl.up);
226
+ document.removeEventListener('touchcancel', dl.up);
227
+ }
228
+ if (dl.handle) {
229
+ dl.handle.removeEventListener('mousedown', dl.handleMouseDown);
230
+ dl.handle.removeEventListener('touchstart', dl.handleTouchStart);
231
+ }
232
+ },
233
+
234
+ /** Tear down every floating window (called on SSE reconnect; keeps user dismissals). */
235
+ removeAllUltracodeWindows() {
236
+ this._ensureUltracodeWindowState();
237
+ const had = this.ultracodeWindows.size > 0;
238
+ for (const [, data] of this.ultracodeWindows) {
239
+ this._teardownUltracodeDrag(data.dragListeners);
240
+ if (data.element) data.element.remove();
241
+ }
242
+ this.ultracodeWindows.clear();
243
+ for (const t of this.ultracodeWindowCloseTimers.values()) clearTimeout(t);
244
+ this.ultracodeWindowCloseTimers.clear();
245
+ // Redraw so the now-orphaned connector lines are cleared from the shared SVG.
246
+ if (had) this.updateConnectionLines();
247
+ },
248
+
249
+ /** When the feature is toggled on, pop windows for any currently-active runs. */
250
+ syncAllUltracodeFloatingWindows() {
251
+ this._ensureUltracodeWindowState();
252
+ if (!this._ultracodeFloatingEnabled()) {
253
+ this.removeAllUltracodeWindows();
254
+ return;
255
+ }
256
+ if (!this.workflowRuns) return;
257
+ for (const run of this.workflowRuns.values()) {
258
+ this._syncUltracodeFloatingWindow(run, { fromSeed: true });
259
+ }
260
+ },
261
+
262
+ /** Refresh a floating window's header + body from the latest summary/detail. */
263
+ renderUltracodeWindowContent(runId) {
264
+ const data = this.ultracodeWindows.get(runId);
265
+ if (!data) return;
266
+ const summary = this.workflowRuns && this.workflowRuns.get(runId);
267
+ const detail = this.workflowRunDetails && this.workflowRunDetails.get(runId);
268
+ // Summary is the freshest run-level info (every SSE tick); detail supplies agents[]
269
+ // but is fetched less often. Merge so a completed summary isn't masked by stale detail.
270
+ const run = summary && detail ? { ...detail, ...summary, agents: detail.agents } : detail || summary;
271
+ if (!run) return;
272
+
273
+ const nameEl = data.element.querySelector('.uw-name');
274
+ if (nameEl) nameEl.textContent = run.workflowName || run.summary || runId;
275
+
276
+ const statusEl = data.element.querySelector('.uw-status');
277
+ if (statusEl) {
278
+ const finished = !this._isWorkflowRunActive(run);
279
+ const label = run.status ? String(run.status) : finished ? '—' : 'running';
280
+ const clsKey = run.status ? run.status : finished ? '' : 'running';
281
+ statusEl.textContent = label;
282
+ statusEl.className = 'uw-status ultracode-status ' + this._workflowStatusClass(clsKey);
283
+ }
284
+
285
+ const body = data.element.querySelector('.ultracode-window-body');
286
+ if (body) body.innerHTML = this._ultracodeWindowBodyHtml(run);
287
+ },
288
+
289
+ /** Compact body: a stats line + agent cards grouped by phase (reuses panel helpers). */
290
+ _ultracodeWindowBodyHtml(run) {
291
+ const phases = Array.isArray(run.phases) ? run.phases : [];
292
+ const agents = Array.isArray(run.agents) ? run.agents : null;
293
+ const agentCount = run.agentCount ?? (agents ? agents.length : 0);
294
+ const head = `<div class="uw-summary">${this._fmtNum(run.totalTokens)} tok · ${run.totalToolCalls ?? 0} tools · ${agentCount} agents</div>`;
295
+
296
+ if (!agents) {
297
+ // Summary-only (detail not fetched yet): show phase chips as a teaser.
298
+ if (phases.length) {
299
+ const chips = phases
300
+ .map(
301
+ (p) =>
302
+ `<span class="ultracode-phase-chip" title="${escapeHtml(p.detail || '')}">${escapeHtml(p.title || '')}</span>`
303
+ )
304
+ .join('');
305
+ return (
306
+ head + `<div class="ultracode-phase-list">${chips}</div><div class="subagent-empty">Loading agents…</div>`
307
+ );
308
+ }
309
+ return head + '<div class="subagent-empty">Loading agents…</div>';
310
+ }
311
+ if (!agents.length) return head + '<div class="subagent-empty">No agents yet</div>';
312
+
313
+ const groups = new Map();
314
+ agents.forEach((a) => {
315
+ const key = a.phaseIndex || 0;
316
+ if (!groups.has(key)) groups.set(key, []);
317
+ groups.get(key).push(a);
318
+ });
319
+ const orderedKeys = Array.from(groups.keys()).sort((a, b) => a - b);
320
+ const grid = orderedKeys
321
+ .map((key) => {
322
+ const group = groups.get(key);
323
+ const title = (phases[key - 1] && phases[key - 1].title) || `Phase ${key}`;
324
+ const tok = group.reduce((s, a) => s + (a.tokens || 0), 0);
325
+ const tools = group.reduce((s, a) => s + (a.toolCalls || 0), 0);
326
+ const header =
327
+ `<div class="ultracode-phase-header"><span>${escapeHtml(title)}</span>` +
328
+ `<span class="ultracode-phase-sub">${this._fmtNum(tok)} tok · ${tools} tools</span></div>`;
329
+ return header + group.map((a) => this._workflowAgentCardHtml(a)).join('');
330
+ })
331
+ .join('');
332
+ return head + grid;
333
+ },
334
+
335
+ /**
336
+ * Append ultracode-window → parent-tab connector lines into the shared SVG.
337
+ * Invoked at the tail of `_updateConnectionLinesImmediate` (subagent-windows.js),
338
+ * so it shares that pass's batched read/write discipline. `rects` is the tab-rect
339
+ * cache already populated for subagent lines — reuse it, fill any gaps.
340
+ */
341
+ _appendUltracodeConnectionLines(svg, rects) {
342
+ this._ensureUltracodeWindowState();
343
+ if (!svg || !this.ultracodeWindows.size) return;
344
+ if (!rects) rects = new Map();
345
+
346
+ // PHASE 1: layout reads (resolve parents, batch getBoundingClientRect).
347
+ const winList = [];
348
+ for (const [runId, data] of this.ultracodeWindows) {
349
+ if (!data.element) continue;
350
+ if (!data.parentSessionId) {
351
+ const summary = this.workflowRuns && this.workflowRuns.get(runId);
352
+ if (summary) data.parentSessionId = this._resolveUltracodeParentSession(summary);
353
+ }
354
+ const parentSessionId = data.parentSessionId;
355
+ if (!parentSessionId) continue;
356
+ const tabKey = 'tab:' + parentSessionId;
357
+ if (!rects.has(tabKey)) {
358
+ const tab = document.querySelector(`.session-tab[data-id="${parentSessionId}"]`);
359
+ if (tab) rects.set(tabKey, tab.getBoundingClientRect());
360
+ }
361
+ winList.push({ runId, parentSessionId, winRect: data.element.getBoundingClientRect() });
362
+ }
363
+
364
+ // PHASE 2: writes (curve from tab bottom-center to window top-center).
365
+ for (const { runId, parentSessionId, winRect } of winList) {
366
+ const tabRect = rects.get('tab:' + parentSessionId);
367
+ if (!tabRect) continue;
368
+ const x1 = tabRect.left + tabRect.width / 2;
369
+ const y1 = tabRect.bottom;
370
+ const x2 = winRect.left + winRect.width / 2;
371
+ const y2 = winRect.top;
372
+ const midY = (y1 + y2) / 2;
373
+ const path = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
374
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
375
+ line.setAttribute('d', path);
376
+ line.setAttribute('class', 'connection-line ultracode-connection');
377
+ line.setAttribute('data-run-id', runId);
378
+ line.setAttribute('data-parent-tab', parentSessionId);
379
+ svg.appendChild(line);
380
+ }
381
+ },
382
+ });
Binary file
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"file-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/file-routes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAC;AAuB7D,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAoV5E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,IAAI,CA6pBxG"}
1
+ {"version":3,"file":"file-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/file-routes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAC;AAuB7D,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAoV5E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,IAAI,CAytBxG"}
@@ -392,48 +392,70 @@ export function registerFileRoutes(app, ctx) {
392
392
  const { resolvedPath } = validated;
393
393
  try {
394
394
  const stat = await fs.stat(resolvedPath);
395
- // Check if it's a binary/media file
395
+ // Classify by extension. Known media types render with a dedicated player;
396
+ // other known-binary types are flagged so the client offers a download
397
+ // affordance instead of trying to decode the bytes as text. Matches the
398
+ // breadth of formats the attachments viewer renders (image/audio/video/pdf)
399
+ // so the file viewer can open the same files.
396
400
  const ext = filePath.split('.').pop()?.toLowerCase() || '';
397
- const binaryExts = new Set([
398
- 'png',
399
- 'jpg',
400
- 'jpeg',
401
- 'gif',
402
- 'webp',
403
- 'ico',
404
- 'svg',
405
- 'bmp',
406
- 'mp4',
407
- 'webm',
408
- 'mov',
409
- 'avi',
410
- 'mp3',
411
- 'wav',
412
- 'ogg',
401
+ const imageExts = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico']);
402
+ const videoExts = new Set(['mp4', 'webm', 'mov', 'm4v', 'ogv']);
403
+ const audioExts = new Set(['mp3', 'wav', 'ogg', 'oga', 'm4a', 'aac', 'flac', 'opus']);
404
+ const otherBinaryExts = new Set([
413
405
  'pdf',
414
406
  'zip',
415
407
  'tar',
416
408
  'gz',
409
+ 'bz2',
410
+ 'xz',
411
+ '7z',
412
+ 'rar',
417
413
  'exe',
418
414
  'dll',
419
415
  'so',
416
+ 'dylib',
417
+ 'bin',
418
+ 'wasm',
419
+ 'class',
420
+ 'o',
421
+ 'a',
420
422
  'woff',
421
423
  'woff2',
422
424
  'ttf',
423
425
  'eot',
426
+ 'otf',
427
+ 'xlsx',
428
+ 'xls',
429
+ 'doc',
430
+ 'docx',
431
+ 'ppt',
432
+ 'pptx',
433
+ 'odt',
434
+ 'ods',
435
+ 'odp',
436
+ 'avi',
437
+ 'mkv',
438
+ 'wmv',
439
+ 'flv',
424
440
  ]);
425
- const imageExts = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico']);
426
- const videoExts = new Set(['mp4', 'webm', 'mov', 'avi']);
427
- if (raw === 'true' || binaryExts.has(ext)) {
428
- // Return metadata for binary files
441
+ const mediaType = imageExts.has(ext)
442
+ ? 'image'
443
+ : videoExts.has(ext)
444
+ ? 'video'
445
+ : audioExts.has(ext)
446
+ ? 'audio'
447
+ : null;
448
+ const fileRawUrl = `/api/sessions/${id}/file-raw?path=${encodeURIComponent(filePath)}`;
449
+ if (raw === 'true' || mediaType || otherBinaryExts.has(ext)) {
450
+ // Return metadata for media/binary files (no text body)
429
451
  return {
430
452
  success: true,
431
453
  data: {
432
454
  path: filePath,
433
455
  size: stat.size,
434
- type: imageExts.has(ext) ? 'image' : videoExts.has(ext) ? 'video' : 'binary',
456
+ type: mediaType ?? 'binary',
435
457
  extension: ext,
436
- url: `/api/sessions/${id}/file-raw?path=${encodeURIComponent(filePath)}`,
458
+ url: fileRawUrl,
437
459
  },
438
460
  };
439
461
  }
@@ -442,10 +464,37 @@ export function registerFileRoutes(app, ctx) {
442
464
  if (stat.size > MAX_TEXT_FILE_SIZE) {
443
465
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, `File too large (${Math.round(stat.size / 1024 / 1024)}MB > ${MAX_TEXT_FILE_SIZE / 1024 / 1024}MB limit)`);
444
466
  }
467
+ // Read as raw bytes so we can sniff for binary content before decoding. An
468
+ // unrecognized extension (none at all, or a format not listed above) that
469
+ // is actually binary would otherwise be dumped to the viewer as UTF-8
470
+ // mojibake; a NUL byte in the first 8KB is a reliable binary signal that
471
+ // (unlike a static extension list) catches arbitrary binary formats.
472
+ const fileBuffer = await fs.readFile(resolvedPath);
473
+ const buf = Buffer.isBuffer(fileBuffer) ? fileBuffer : Buffer.from(String(fileBuffer));
474
+ const sniffLength = Math.min(buf.length, 8192);
475
+ let looksBinary = false;
476
+ for (let i = 0; i < sniffLength; i++) {
477
+ if (buf[i] === 0) {
478
+ looksBinary = true;
479
+ break;
480
+ }
481
+ }
482
+ if (looksBinary) {
483
+ return {
484
+ success: true,
485
+ data: {
486
+ path: filePath,
487
+ size: stat.size,
488
+ type: 'binary',
489
+ extension: ext,
490
+ url: fileRawUrl,
491
+ },
492
+ };
493
+ }
445
494
  // Read text file with line limit (bounded to prevent DoS)
446
495
  const MAX_LINES_LIMIT = 10000;
447
496
  const maxLines = Math.min(parseInt(lines || '500', 10) || 500, MAX_LINES_LIMIT);
448
- const content = await fs.readFile(resolvedPath, 'utf-8');
497
+ const content = buf.toString('utf-8');
449
498
  const allLines = content.split('\n');
450
499
  const truncatedContent = allLines.length > maxLines;
451
500
  const displayContent = truncatedContent ? allLines.slice(0, maxLines).join('\n') : content;
@@ -503,9 +552,16 @@ export function registerFileRoutes(app, ctx) {
503
552
  mp4: 'video/mp4',
504
553
  webm: 'video/webm',
505
554
  mov: 'video/quicktime',
555
+ m4v: 'video/mp4',
556
+ ogv: 'video/ogg',
506
557
  mp3: 'audio/mpeg',
507
558
  wav: 'audio/wav',
508
559
  ogg: 'audio/ogg',
560
+ oga: 'audio/ogg',
561
+ opus: 'audio/ogg',
562
+ m4a: 'audio/mp4',
563
+ aac: 'audio/aac',
564
+ flac: 'audio/flac',
509
565
  pdf: 'application/pdf',
510
566
  json: 'application/json',
511
567
  };