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.
- package/dist/config/workflow-config.d.ts +24 -0
- package/dist/config/workflow-config.d.ts.map +1 -0
- package/dist/config/workflow-config.js +24 -0
- package/dist/config/workflow-config.js.map +1 -0
- package/dist/subagent-watcher.d.ts +34 -0
- package/dist/subagent-watcher.d.ts.map +1 -1
- package/dist/subagent-watcher.js +147 -4
- package/dist/subagent-watcher.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/workflow-run.d.ts +130 -0
- package/dist/types/workflow-run.d.ts.map +1 -0
- package/dist/types/workflow-run.js +20 -0
- package/dist/types/workflow-run.js.map +1 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.92f49a9d.js → app.6b133aaf.js} +6 -6
- package/dist/web/public/app.6b133aaf.js.br +0 -0
- package/dist/web/public/app.6b133aaf.js.gz +0 -0
- package/dist/web/public/{constants.59faac65.js → constants.1c779517.js} +5 -0
- package/dist/web/public/constants.1c779517.js.br +0 -0
- package/dist/web/public/constants.1c779517.js.gz +0 -0
- package/dist/web/public/image-input.0ea86695.js.gz +0 -0
- package/dist/web/public/index.html +48 -8
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.b8686b5e.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.bc753cc7.js.gz +0 -0
- package/dist/web/public/mobile-handlers.db3dc3c8.js.gz +0 -0
- package/dist/web/public/mobile.06b38d3a.css.gz +0 -0
- package/dist/web/public/notification-manager.9c984ac2.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.2f467969.js → panels-ui.f3f08e26.js} +48 -48
- package/dist/web/public/panels-ui.f3f08e26.js.br +0 -0
- package/dist/web/public/panels-ui.f3f08e26.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-ui.1463b824.js.gz +0 -0
- package/dist/web/public/settings-ui.08f7708b.js +55 -0
- package/dist/web/public/settings-ui.08f7708b.js.br +0 -0
- package/dist/web/public/settings-ui.08f7708b.js.gz +0 -0
- package/dist/web/public/{styles.8e1ea0c6.css → styles.379f31e0.css} +1 -1
- package/dist/web/public/styles.379f31e0.css.br +0 -0
- package/dist/web/public/styles.379f31e0.css.gz +0 -0
- package/dist/web/public/{subagent-windows.a366a4ad.js → subagent-windows.07e139f2.js} +9 -0
- package/dist/web/public/subagent-windows.07e139f2.js.br +0 -0
- package/dist/web/public/subagent-windows.07e139f2.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.a7e046da.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js +314 -0
- package/dist/web/public/ultracode-panel.js.br +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js +382 -0
- package/dist/web/public/ultracode-windows.js.br +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +80 -24
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +23 -0
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +2 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +4 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +14 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +57 -0
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +10 -0
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +12 -0
- package/dist/web/sse-events.js.map +1 -1
- package/dist/workflow-run-watcher.d.ts +76 -0
- package/dist/workflow-run-watcher.d.ts.map +1 -0
- package/dist/workflow-run-watcher.js +327 -0
- package/dist/workflow-run-watcher.js.map +1 -0
- package/package.json +1 -1
- package/dist/web/public/app.92f49a9d.js.br +0 -0
- package/dist/web/public/app.92f49a9d.js.gz +0 -0
- package/dist/web/public/constants.59faac65.js.br +0 -0
- package/dist/web/public/constants.59faac65.js.gz +0 -0
- package/dist/web/public/panels-ui.2f467969.js.br +0 -0
- package/dist/web/public/panels-ui.2f467969.js.gz +0 -0
- package/dist/web/public/settings-ui.44b99ce0.js +0 -55
- package/dist/web/public/settings-ui.44b99ce0.js.br +0 -0
- package/dist/web/public/settings-ui.44b99ce0.js.gz +0 -0
- package/dist/web/public/styles.8e1ea0c6.css.br +0 -0
- package/dist/web/public/styles.8e1ea0c6.css.gz +0 -0
- package/dist/web/public/subagent-windows.a366a4ad.js.br +0 -0
- 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">×</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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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:
|
|
456
|
+
type: mediaType ?? 'binary',
|
|
435
457
|
extension: ext,
|
|
436
|
-
url:
|
|
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 =
|
|
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
|
};
|