feat(ultracode): in-page agent transcript windows + minimize-to-tab (1.1.14)

Clicking an agent card opens its live transcript as an in-page connected
floating window instead of a detached browser popup. The "−" button on both
run and agent windows now minimizes into the originating session tab as a
restorable ULTRA badge (🧬 runs, 📄 transcripts). Removes the old
collapse-to-header behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-06-18 21:26:33 +02:00
parent cfcd9d288b
commit 5d59c1764d
9 changed files with 548 additions and 45 deletions
+9
View File
@@ -1,5 +1,14 @@
# aicodeman
## 1.1.14
### Patch Changes
- Ultracode (Workflow-tool) floating windows — agent transcripts in-page, and minimize-to-tab.
- **Agent transcripts open in-page, connected, instead of a detached browser popup.** Clicking an agent card (in a run window or the dock panel) now opens the agent's live transcript as its own draggable floating window, tied by a connector line to its parent run window (falling back to the run's session tab if that window has since closed) — the same line idiom the run windows use. Re-clicking a card focuses the existing window; closing it removes the window and its line. (Previously this spawned a separate `window.open` browser popup.)
- **The window "−" button now minimizes into the originating session tab**, mirroring the subagent-window idiom. The window genie-animates into its tab and is tracked there; the tab shows an `ULTRA` badge whose hover/click dropdown lists each minimized item (🧬 run windows, 📄 agent transcripts). Click an item to restore its floating window, or dismiss it with ×. A run minimized while still active keeps tracking in the background and its badge auto-clears shortly after the run finishes. Both run windows and agent-transcript windows minimize into the same merged badge.
- Removed the old collapse-to-header behavior that the "−" button previously triggered (now superseded by minimize-to-tab).
## 1.1.13
### Patch Changes
+1 -1
View File
@@ -56,7 +56,7 @@ When user says "COM":
CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed.
**Version**: 1.1.13 (must match `package.json`)
**Version**: 1.1.14 (must match `package.json`)
## Project Overview
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "aicodeman",
"version": "1.1.13",
"version": "1.1.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aicodeman",
"version": "1.1.13",
"version": "1.1.14",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aicodeman",
"version": "1.1.13",
"version": "1.1.14",
"description": "Mission control for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+5
View File
@@ -2773,6 +2773,10 @@ class CodemanApp {
const minimizedCount = minimizedAgents?.size || 0;
const subagentBadge = minimizedCount > 0 ? this.renderSubagentTabBadge(id, minimizedAgents) : '';
// Ultracode runs + agent transcripts minimized to this tab (ultracode-windows.js
// renders one merged ULTRA badge; returns '' when nothing is minimized).
const ultracodeBadge = this.renderUltracodeTabBadge ? this.renderUltracodeTabBadge(id) : '';
// Show folder name if session has a custom name AND tall tabs setting is enabled
const folderName = session.workingDir ? session.workingDir.split('/').pop() || '' : '';
const tallTabsEnabled = this._tallTabsEnabled ?? false;
@@ -2792,6 +2796,7 @@ class CodemanApp {
</span>
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()" aria-label="${taskStats.running} running tasks">${taskStats.running}</span>` : ''}
${subagentBadge}
${ultracodeBadge}
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${escapeHtml(id)}')" title="Session options" aria-label="Session options" tabindex="0">&#x2699;</span>
<span class="tab-detach" onclick="event.stopPropagation(); app.detachSession('${escapeHtml(id)}')" title="Open in a new window" aria-label="Open session in a new window" tabindex="0">&#x29C9;</span>
<span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${escapeHtml(id)}')" title="Close session" aria-label="Close session" tabindex="0">&times;</span>
+65 -7
View File
@@ -5573,6 +5573,45 @@ kbd {
letter-spacing: 0.03em;
}
/* Ultracode run minimized-to-tab badge — same chip shape as the subagent badge but a
distinct purple (matches the ultracode window accent), and a SEPARATE class so the
incremental tab-update path (which keys on .tab-subagent-badge) never touches it.
Reuses the shared .subagent-dropdown for its restore/dismiss list. */
.session-tab .tab-ultracode-badge {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
height: 16px;
padding: 0 5px;
border-radius: 8px;
font-size: 0.6rem;
cursor: pointer;
background: #a855f7;
color: white;
margin-left: 4px;
transition: transform 0.1s, background 0.15s;
}
@media (hover: hover) {
.session-tab .tab-ultracode-badge:hover {
background: #9333ea;
transform: scale(1.05);
}
}
.session-tab .tab-ultracode-badge .subagent-label {
font-size: 0.55rem;
font-weight: 600;
letter-spacing: 0.03em;
}
/* Per-item type glyph in the ULTRA dropdown (🧬 run vs 📄 transcript). */
.subagent-dropdown-item .ultracode-dd-icon {
font-size: 0.7rem;
line-height: 1;
flex-shrink: 0;
margin-right: 1px;
}
/* Subagent Dropdown - compact, hover-triggered */
.subagent-dropdown {
display: none;
@@ -8647,13 +8686,6 @@ kbd {
transform: scale(0.92);
opacity: 0;
}
.ultracode-window.collapsed {
height: auto !important;
resize: none;
}
.ultracode-window.collapsed .ultracode-window-body {
display: none;
}
.ultracode-window-header {
display: flex;
align-items: center;
@@ -8744,6 +8776,32 @@ kbd {
50% { opacity: 1; }
}
/* Agent-transcript window: spawned from an agent card (in a run window or the dock
panel), tied to its parent run window by its own connector line. Reuses the
.ultracode-window chrome; wider with a monospace transcript body. */
.ultracode-agent-window {
width: 470px;
height: 420px;
border-color: #38bdf8;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(56, 189, 248, 0.18);
}
.ultracode-agent-window .ultracode-window-body .uw-transcript {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-mono);
font-size: 0.68rem;
line-height: 1.45;
color: var(--text);
}
/* Distinguish the parent→agent line from the tab→run line: solid cyan, no dashes.
Must follow .ultracode-connection (equal specificity → source order wins). */
.connection-line.ultracode-agent-connection {
stroke: #38bdf8;
stroke-dasharray: none;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8)) drop-shadow(0 0 5px rgba(56, 189, 248, 0.85));
}
/* Plan-usage chip (App Settings → Display → "Plan Usage Limits"). Shows the
live 5-hour + weekly plan limits parsed from the Claude statusline. Hidden by
default via the marker class below; the server strips it at render when the
+4
View File
@@ -461,6 +461,10 @@ Object.assign(CodemanApp.prototype, {
if (typeof this._appendUltracodeConnectionLines === 'function') {
this._appendUltracodeConnectionLines(svg, rects);
}
// Agent-transcript windows → their run window / tab (ultracode-windows.js).
if (typeof this._appendUltracodeAgentConnectionLines === 'function') {
this._appendUltracodeAgentConnectionLines(svg, rects);
}
},
// ═══════════════════════════════════════════════════════════════
+12 -22
View File
@@ -131,12 +131,15 @@ Object.assign(CodemanApp.prototype, {
}
},
// Phase 4: open an agent's live transcript by agentId. The workflow agent's
// Phase 4: fetch an agent's live transcript by agentId. The workflow agent's
// agentId is byte-identical to the agent-<id>.jsonl stem already tracked by
// subagent-watcher, so we reuse the existing transcript route — no watcher edits.
// Graceful when the agent isn't tracked yet / aged out / tracking disabled.
async openWorkflowAgentTranscript(agentId) {
if (!agentId) return;
// Returns { formatted: string[], entryCount } or null when nothing is available
// (queued / aged out of tracking / tracking disabled). Rendering into a connected
// in-page floating window lives in ultracode-windows.js (openUltracodeAgentWindow) —
// we no longer spawn a detached browser popup.
async _fetchWorkflowAgentTranscript(agentId) {
if (!agentId) return null;
let data = null;
try {
const res = await fetch(`/api/subagents/${encodeURIComponent(agentId)}/transcript?format=formatted`);
@@ -147,21 +150,8 @@ Object.assign(CodemanApp.prototype, {
const ok = data && data.success && data.data;
const formatted = ok ? data.data.formatted : null;
const entryCount = ok ? data.data.entryCount || 0 : 0;
if (!formatted || !entryCount) {
alert(
'No transcript available for this agent yet — it may be queued, aged out of tracking, or subagent tracking is disabled.'
);
return;
}
const win = window.open('', '_blank', 'width=860,height=640');
if (!win) return; // popup blocked
win.document.write(
`<html><head><title>Workflow agent ${escapeHtml(agentId)} transcript</title>` +
`<style>body{background:#1a1a2e;color:#eee;font-family:monospace;padding:20px}pre{white-space:pre-wrap;word-wrap:break-word}</style>` +
`</head><body><h2>Workflow agent ${escapeHtml(agentId)} (${entryCount} entries)</h2>` +
`<pre>${escapeHtml(formatted.join('\n'))}</pre></body></html>`
);
win.document.close();
if (!formatted || !entryCount) return null;
return { formatted, entryCount };
},
// ----- Render (debounced) -----
@@ -262,13 +252,13 @@ Object.assign(CodemanApp.prototype, {
const header =
`<div class="ultracode-phase-header"><span>${escapeHtml(title)}</span>` +
`<span class="ultracode-phase-sub">${this._fmtNum(tok)} tok · ${tools} tools</span></div>`;
return header + group.map((a) => this._workflowAgentCardHtml(a)).join('');
return header + group.map((a) => this._workflowAgentCardHtml(a, runId)).join('');
})
.join('');
detail.innerHTML = html;
},
_workflowAgentCardHtml(a) {
_workflowAgentCardHtml(a, runId) {
const state = String(a.state || 'start');
const stateCls = this._workflowAgentStateClass(state);
const stateLabel = state === 'start' ? 'queued' : state === 'progress' ? 'running' : state;
@@ -289,7 +279,7 @@ Object.assign(CodemanApp.prototype, {
const cardStateCls = state === 'done' ? ' uw-state-done' : state === 'progress' ? ' uw-state-working' : '';
const cardAttrs = clickable
? ` class="ultracode-agent-card ultracode-agent-card--clickable${cardStateCls}" role="button" tabindex="0"` +
` title="View transcript" onclick="app.openWorkflowAgentTranscript('${escapeHtml(a.agentId)}')"`
` title="View transcript" onclick="app.openUltracodeAgentWindow('${escapeHtml(a.agentId)}','${escapeHtml(runId || '')}')"`
: ` class="ultracode-agent-card${cardStateCls}"`;
return (
`<div${cardAttrs}>` +
+449 -12
View File
@@ -33,9 +33,12 @@
Object.assign(CodemanApp.prototype, {
/** Lazily seed the floating-window state maps (constructor also seeds them). */
_ensureUltracodeWindowState() {
if (!this.ultracodeWindows) this.ultracodeWindows = new Map(); // runId -> { element, parentSessionId, dragListeners, collapsed }
if (!this.ultracodeWindows) this.ultracodeWindows = new Map(); // runId -> { element, parentSessionId, dragListeners }
if (!this.ultracodeWindowsClosed) this.ultracodeWindowsClosed = new Set(); // runIds the user dismissed
if (!this.ultracodeWindowCloseTimers) this.ultracodeWindowCloseTimers = new Map(); // runId -> setTimeout id
if (!this.ultracodeAgentWindows) this.ultracodeAgentWindows = new Map(); // agentId -> { element, runId, dragListeners }
if (!this.minimizedUltracodeRuns) this.minimizedUltracodeRuns = new Map(); // sessionId -> Set<runId> minimized to a tab
if (!this.minimizedUltracodeAgents) this.minimizedUltracodeAgents = new Map(); // sessionId -> Map<agentId,{runId,label}>
if (this.ultracodeWindowZIndex === undefined) this.ultracodeWindowZIndex = 1000;
},
@@ -86,6 +89,21 @@ Object.assign(CodemanApp.prototype, {
const active = this._isWorkflowRunActive(run);
// Minimized to a tab — keep it there (don't re-pop a window). Clear the tab badge a
// short while after the run finishes, mirroring the floating window's finish grace.
if (this._isUltracodeRunMinimized(runId)) {
if (!active && !this.ultracodeWindowCloseTimers.has(runId)) {
const timer = setTimeout(() => {
this.ultracodeWindowCloseTimers.delete(runId);
this._removeMinimizedUltracodeRun(runId);
this.renderSessionTabs();
this.updateConnectionLines();
}, 8000);
this.ultracodeWindowCloseTimers.set(runId, timer);
}
return;
}
if (active) {
// Run is alive — cancel any pending auto-close.
const pending = this.ultracodeWindowCloseTimers.get(runId);
@@ -134,11 +152,11 @@ Object.assign(CodemanApp.prototype, {
const run = this.workflowRuns && this.workflowRuns.get(runId);
if (!run) return;
this.ultracodeWindowsClosed.delete(runId); // an explicit open overrides a past dismissal
this._removeMinimizedUltracodeRun(runId); // …and a past minimize-to-tab
const existing = this.ultracodeWindows.get(runId);
if (existing) {
// Already open — bring to front, expand if collapsed, refresh.
// Already open — bring to front and refresh.
existing.element.style.zIndex = ++this.ultracodeWindowZIndex;
if (existing.collapsed) this.toggleUltracodeWindowCollapse(runId);
this.renderUltracodeWindowContent(runId);
this._fetchWorkflowRunDetail(runId);
this.updateConnectionLines();
@@ -167,7 +185,7 @@ Object.assign(CodemanApp.prototype, {
<span class="uw-status"></span>
</div>
<div class="ultracode-window-actions">
<button class="uw-min" type="button" title="Collapse">─</button>
<button class="uw-min" type="button" title="Minimize to tab">─</button>
<button class="uw-close" type="button" title="Close">&times;</button>
</div>
</div>
@@ -198,7 +216,7 @@ Object.assign(CodemanApp.prototype, {
win.querySelector('.uw-min').addEventListener('click', (e) => {
e.stopPropagation();
this.toggleUltracodeWindowCollapse(runId);
this.minimizeUltracodeWindowToTab(runId);
});
win.querySelector('.uw-close').addEventListener('click', (e) => {
e.stopPropagation();
@@ -211,19 +229,261 @@ Object.assign(CodemanApp.prototype, {
nameEl.addEventListener('click', () => this.selectSession(parentSessionId));
}
this.ultracodeWindows.set(runId, { element: win, parentSessionId, dragListeners, collapsed: false });
this.ultracodeWindows.set(runId, { element: win, parentSessionId, dragListeners });
this.renderUltracodeWindowContent(runId);
this._fetchWorkflowRunDetail(runId); // pull agents[] for the body
this.updateConnectionLines();
},
/** Collapse/expand the window to header-only (line stays connected). */
toggleUltracodeWindowCollapse(runId) {
// ── Minimize a run window into its originating tab (same idiom as subagent windows) ──
/** Is this run currently minimized to a tab (so auto-pop should leave it alone)? */
_isUltracodeRunMinimized(runId) {
if (!this.minimizedUltracodeRuns) return false;
for (const set of this.minimizedUltracodeRuns.values()) {
if (set.has(runId)) return true;
}
return false;
},
/** Drop a run from the minimized-to-tab tracking (all sessions, or a specific one). */
_removeMinimizedUltracodeRun(runId, sessionId) {
if (!this.minimizedUltracodeRuns) return;
if (sessionId) {
const set = this.minimizedUltracodeRuns.get(sessionId);
if (set) {
set.delete(runId);
if (!set.size) this.minimizedUltracodeRuns.delete(sessionId);
}
return;
}
for (const [sid, set] of this.minimizedUltracodeRuns) {
if (set.delete(runId) && !set.size) this.minimizedUltracodeRuns.delete(sid);
}
},
/**
* Minimize the floating run window into its originating session tab: record it as
* minimized (so a badge renders on the tab), genie-animate the window toward that
* tab, then remove the floating element. Restorable from the tab badge dropdown.
*/
minimizeUltracodeWindowToTab(runId) {
this._ensureUltracodeWindowState();
const data = this.ultracodeWindows.get(runId);
if (!data) return;
data.collapsed = !data.collapsed;
data.element.classList.toggle('collapsed', data.collapsed);
let parentSessionId = data.parentSessionId;
if (!parentSessionId) {
const summary = this.workflowRuns && this.workflowRuns.get(runId);
parentSessionId = summary ? this._resolveUltracodeParentSession(summary) : null;
}
// No tab to fly into → fall back to a plain close so the window isn't orphaned.
if (!parentSessionId) {
this.closeUltracodeWindow(runId, true);
return;
}
// Cancel any pending finish auto-close — the badge owns the run's lifecycle now.
const pending = this.ultracodeWindowCloseTimers.get(runId);
if (pending) {
clearTimeout(pending);
this.ultracodeWindowCloseTimers.delete(runId);
}
if (!this.minimizedUltracodeRuns.has(parentSessionId)) this.minimizedUltracodeRuns.set(parentSessionId, new Set());
this.minimizedUltracodeRuns.get(parentSessionId).add(runId);
const element = data.element;
const dragListeners = data.dragListeners;
this._animateUltracodeWindowToTab(element, parentSessionId, () => {
this._teardownUltracodeDrag(dragListeners);
if (element) element.remove();
this.ultracodeWindows.delete(runId);
// Full rebuild so the tab badge renders (the incremental path only knows subagent badges).
this._fullRenderSessionTabs();
this.updateConnectionLines();
});
},
/** Genie the window toward the center of its tab, then invoke `done` to tear it down. */
_animateUltracodeWindowToTab(element, sessionId, done) {
const tab = sessionId ? document.querySelector(`.session-tab[data-id="${sessionId}"]`) : null;
if (!tab || !element) {
done();
return;
}
const w = element.getBoundingClientRect();
const t = tab.getBoundingClientRect();
const dx = t.left + t.width / 2 - (w.left + w.width / 2);
const dy = t.top + t.height / 2 - (w.top + w.height / 2);
element.style.transformOrigin = 'center center';
element.style.transition = 'transform 0.26s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.26s ease';
element.style.pointerEvents = 'none';
requestAnimationFrame(() => {
element.style.transform = `translate(${dx}px, ${dy}px) scale(0.06)`;
element.style.opacity = '0';
});
let finished = false;
const finish = () => {
if (finished) return;
finished = true;
done();
};
element.addEventListener('transitionend', finish, { once: true });
setTimeout(finish, 320); // fallback in case transitionend doesn't fire
},
/** Tab badge (with restore/dismiss dropdown) for runs minimized to this session's tab. */
renderUltracodeTabBadge(sessionId) {
this._ensureUltracodeWindowState();
const runSet = this.minimizedUltracodeRuns.get(sessionId);
const agentMap = this.minimizedUltracodeAgents.get(sessionId);
const total = (runSet ? runSet.size : 0) + (agentMap ? agentMap.size : 0);
if (total === 0) return '';
const trunc = (s) => (s.length > 25 ? s.slice(0, 25) + '…' : s);
const items = [];
// Minimized run windows (🧬) first…
if (runSet) {
for (const runId of runSet) {
const run = this.workflowRuns && this.workflowRuns.get(runId);
const name = run ? run.workflowName || run.summary || runId : runId;
const statusCls = this._workflowStatusClass(run ? String(run.status || '') : '');
items.push(
`<div class="subagent-dropdown-item" onclick="event.stopPropagation(); app.restoreUltracodeRunFromTab('${escapeHtml(runId)}','${escapeHtml(sessionId)}')" title="Click to restore run">` +
`<span class="subagent-dropdown-status ${statusCls}"></span>` +
`<span class="ultracode-dd-icon">🧬</span>` +
`<span class="subagent-dropdown-name">${escapeHtml(trunc(name))}</span>` +
`<span class="subagent-dropdown-close" onclick="event.stopPropagation(); app.dismissMinimizedUltracodeRun('${escapeHtml(runId)}','${escapeHtml(sessionId)}')" title="Dismiss">&times;</span>` +
`</div>`
);
}
}
// …then minimized agent transcripts (📄).
if (agentMap) {
for (const [agentId, entry] of agentMap) {
const name = (entry && entry.label) || agentId;
items.push(
`<div class="subagent-dropdown-item" onclick="event.stopPropagation(); app.restoreUltracodeAgentFromTab('${escapeHtml(agentId)}','${escapeHtml(sessionId)}')" title="Click to restore transcript">` +
`<span class="subagent-dropdown-status"></span>` +
`<span class="ultracode-dd-icon">📄</span>` +
`<span class="subagent-dropdown-name">${escapeHtml(trunc(name))}</span>` +
`<span class="subagent-dropdown-close" onclick="event.stopPropagation(); app.dismissMinimizedUltracodeAgent('${escapeHtml(agentId)}','${escapeHtml(sessionId)}')" title="Dismiss">&times;</span>` +
`</div>`
);
}
}
const label = total === 1 ? 'ULTRA' : `ULTRA (${total})`;
return (
`<span class="tab-ultracode-badge" onmouseenter="app.showSubagentDropdown(this)" onmouseleave="app.scheduleHideSubagentDropdown(this)" onclick="event.stopPropagation(); app.pinSubagentDropdown(this);">` +
`<span class="subagent-label">${label}</span>` +
`<div class="subagent-dropdown" onmouseenter="app.cancelHideSubagentDropdown()" onmouseleave="app.scheduleHideSubagentDropdown(this.parentElement)">${items.join('')}</div>` +
`</span>`
);
},
/** Restore a minimized run from its tab badge: re-open the floating window. */
restoreUltracodeRunFromTab(runId, sessionId) {
this._ensureUltracodeWindowState();
this._removeMinimizedUltracodeRun(runId, sessionId);
this._fullRenderSessionTabs();
this.openUltracodeWindowForRun(runId);
},
/** Dismiss a minimized run from its tab badge (don't re-pop it). */
dismissMinimizedUltracodeRun(runId, sessionId) {
this._ensureUltracodeWindowState();
this._removeMinimizedUltracodeRun(runId, sessionId);
this.ultracodeWindowsClosed.add(runId);
this._fullRenderSessionTabs();
this.updateConnectionLines();
},
// ── Minimize an agent transcript window into its tab (same idiom as run windows) ──
/** Is this agent transcript currently minimized to a tab? */
_isUltracodeAgentMinimized(agentId) {
if (!this.minimizedUltracodeAgents) return false;
for (const map of this.minimizedUltracodeAgents.values()) {
if (map.has(agentId)) return true;
}
return false;
},
/** Look up a minimized agent's {runId,label} entry (across sessions). */
_getMinimizedUltracodeAgent(agentId) {
if (!this.minimizedUltracodeAgents) return null;
for (const map of this.minimizedUltracodeAgents.values()) {
if (map.has(agentId)) return map.get(agentId);
}
return null;
},
/** Drop an agent from minimized tracking (all sessions, or a specific one). */
_removeMinimizedUltracodeAgent(agentId, sessionId) {
if (!this.minimizedUltracodeAgents) return;
if (sessionId) {
const map = this.minimizedUltracodeAgents.get(sessionId);
if (map) {
map.delete(agentId);
if (!map.size) this.minimizedUltracodeAgents.delete(sessionId);
}
return;
}
for (const [sid, map] of this.minimizedUltracodeAgents) {
if (map.delete(agentId) && !map.size) this.minimizedUltracodeAgents.delete(sid);
}
},
/** Minimize an agent transcript window into the run's originating session tab. */
minimizeUltracodeAgentWindowToTab(agentId) {
this._ensureUltracodeWindowState();
const info = this.ultracodeAgentWindows.get(agentId);
if (!info) return;
const runId = info.runId;
const summary = runId && this.workflowRuns ? this.workflowRuns.get(runId) : null;
let parentSessionId = summary ? this._resolveUltracodeParentSession(summary) : null;
if (!parentSessionId && this.activeSessionId && this.sessions && this.sessions.has(this.activeSessionId)) {
parentSessionId = this.activeSessionId;
}
// No tab to fly into → plain close rather than orphan it.
if (!parentSessionId) {
this.closeUltracodeAgentWindow(agentId);
return;
}
const labelEl = info.element.querySelector('.uw-name');
const label = labelEl ? labelEl.textContent : agentId;
if (!this.minimizedUltracodeAgents.has(parentSessionId))
this.minimizedUltracodeAgents.set(parentSessionId, new Map());
this.minimizedUltracodeAgents.get(parentSessionId).set(agentId, { runId, label });
const element = info.element;
const dragListeners = info.dragListeners;
this._animateUltracodeWindowToTab(element, parentSessionId, () => {
this._teardownUltracodeDrag(dragListeners);
if (element) element.remove();
this.ultracodeAgentWindows.delete(agentId);
this._fullRenderSessionTabs();
this.updateConnectionLines();
});
},
/** Restore a minimized agent transcript from its tab badge: re-open its window. */
restoreUltracodeAgentFromTab(agentId, sessionId) {
this._ensureUltracodeWindowState();
const entry = this._getMinimizedUltracodeAgent(agentId);
const runId = entry ? entry.runId : null;
this._removeMinimizedUltracodeAgent(agentId, sessionId);
this._fullRenderSessionTabs();
this.openUltracodeAgentWindow(agentId, runId);
},
/** Dismiss a minimized agent transcript from its tab badge. */
dismissMinimizedUltracodeAgent(agentId, sessionId) {
this._ensureUltracodeWindowState();
this._removeMinimizedUltracodeAgent(agentId, sessionId);
this._fullRenderSessionTabs();
this.updateConnectionLines();
},
@@ -260,19 +520,149 @@ Object.assign(CodemanApp.prototype, {
}
},
// ── Agent-transcript windows ────────────────────────────────────────────────
// Clicking an agent card (in a run window OR the dock panel) opens the agent's
// live transcript as its OWN in-page floating window, line-tied to its parent run
// window (or the run's session tab when that window is closed). Replaces the old
// detached `window.open` browser popup so the transcript stays inside the same
// draggable, connector-line floating-window system as the run windows.
/** Open (or focus) the floating transcript window for a workflow agent. */
async openUltracodeAgentWindow(agentId, runId) {
this._ensureUltracodeWindowState();
if (!agentId) return;
this._removeMinimizedUltracodeAgent(agentId); // an explicit open overrides a past minimize
const existing = this.ultracodeAgentWindows.get(agentId);
if (existing && existing.element) {
// Already open — bring to front and refresh transcript.
existing.element.style.zIndex = ++this.ultracodeWindowZIndex;
this.updateConnectionLines();
} else if (!this.createUltracodeAgentWindow(agentId, runId)) {
return;
}
// Body shows a loading state until the fetch lands (re-fetch on focus too, so a
// still-running agent's transcript grows as you re-click).
const data = this._fetchWorkflowAgentTranscript ? await this._fetchWorkflowAgentTranscript(agentId) : null;
this.renderUltracodeAgentWindowContent(agentId, data);
},
/** Build and mount the floating agent-transcript window shell near its parent. */
createUltracodeAgentWindow(agentId, runId) {
this._ensureUltracodeWindowState();
if (this.ultracodeAgentWindows.has(agentId)) return this.ultracodeAgentWindows.get(agentId).element;
const label = this._ultracodeAgentLabel(agentId, runId) || agentId;
const win = document.createElement('div');
win.className = 'ultracode-window ultracode-agent-window spawning';
win.id = `ultracode-agent-window-${agentId}`;
win.style.zIndex = ++this.ultracodeWindowZIndex;
win.innerHTML = `
<div class="ultracode-window-header">
<div class="ultracode-window-title" title="${escapeHtml(label)} — transcript">
<span class="icon">📄</span>
<span class="uw-name">${escapeHtml(label)}</span>
</div>
<div class="ultracode-window-actions">
<button class="uw-min" type="button" title="Minimize to tab">─</button>
<button class="uw-close" type="button" title="Close">&times;</button>
</div>
</div>
<div class="ultracode-window-body">
<div class="subagent-empty">Loading transcript…</div>
</div>
`;
// Position: offset from the parent run window if it's open, else cascade.
const parentWin = runId ? this.ultracodeWindows.get(runId) : null;
if (parentWin && parentWin.element) {
const r = parentWin.element.getBoundingClientRect();
win.style.left = `${Math.max(8, Math.min(r.left + 40, window.innerWidth - 472))}px`;
win.style.top = `${Math.max(8, Math.min(r.top + 40, window.innerHeight - 160))}px`;
} else {
const n = this.ultracodeAgentWindows.size;
win.style.left = `${Math.min(140 + n * 28, Math.max(8, window.innerWidth - 472))}px`;
win.style.top = `${110 + n * 28}px`;
}
document.body.appendChild(win);
requestAnimationFrame(() => win.classList.remove('spawning'));
const header = win.querySelector('.ultracode-window-header');
const dragListeners = this.makeWindowDraggable(win, header);
win.querySelector('.uw-min').addEventListener('click', (e) => {
e.stopPropagation();
this.minimizeUltracodeAgentWindowToTab(agentId);
});
win.querySelector('.uw-close').addEventListener('click', (e) => {
e.stopPropagation();
this.closeUltracodeAgentWindow(agentId);
});
this.ultracodeAgentWindows.set(agentId, { element: win, runId, dragListeners });
this.updateConnectionLines();
return win;
},
/** Resolve a human label for an agent from the run's fetched detail.agents[]. */
_ultracodeAgentLabel(agentId, runId) {
const detail = this.workflowRunDetails && runId ? this.workflowRunDetails.get(runId) : null;
const agents = detail && Array.isArray(detail.agents) ? detail.agents : null;
if (agents) {
const found = agents.find((a) => a.agentId === agentId);
if (found && found.label) return found.label;
}
return null;
},
/** Fill an agent window's body with the fetched transcript (or a friendly empty state). */
renderUltracodeAgentWindowContent(agentId, data) {
const info = this.ultracodeAgentWindows.get(agentId);
if (!info || !info.element) return;
const body = info.element.querySelector('.ultracode-window-body');
if (!body) return;
if (!data || !data.formatted || !data.entryCount) {
body.innerHTML =
'<div class="subagent-empty">No transcript available yet — the agent may be queued, aged out of tracking, or subagent tracking is disabled.</div>';
return;
}
const text = escapeHtml(data.formatted.join('\n'));
body.innerHTML = `<div class="uw-summary">${data.entryCount} entries</div><pre class="uw-transcript">${text}</pre>`;
},
/** Close one floating agent-transcript window. */
closeUltracodeAgentWindow(agentId) {
this._ensureUltracodeWindowState();
const info = this.ultracodeAgentWindows.get(agentId);
if (!info) return;
this._teardownUltracodeDrag(info.dragListeners);
if (info.element) info.element.remove();
this.ultracodeAgentWindows.delete(agentId);
this.updateConnectionLines();
},
/** Tear down every floating window (called on SSE reconnect; keeps user dismissals). */
removeAllUltracodeWindows() {
this._ensureUltracodeWindowState();
const had = this.ultracodeWindows.size > 0;
const hadMinimized = this.minimizedUltracodeRuns.size > 0 || this.minimizedUltracodeAgents.size > 0;
const had = this.ultracodeWindows.size > 0 || this.ultracodeAgentWindows.size > 0;
for (const [, data] of this.ultracodeWindows) {
this._teardownUltracodeDrag(data.dragListeners);
if (data.element) data.element.remove();
}
this.ultracodeWindows.clear();
for (const [, info] of this.ultracodeAgentWindows) {
this._teardownUltracodeDrag(info.dragListeners);
if (info.element) info.element.remove();
}
this.ultracodeAgentWindows.clear();
for (const t of this.ultracodeWindowCloseTimers.values()) clearTimeout(t);
this.ultracodeWindowCloseTimers.clear();
this.minimizedUltracodeRuns.clear();
this.minimizedUltracodeAgents.clear();
// Redraw so the now-orphaned connector lines are cleared from the shared SVG.
if (had) this.updateConnectionLines();
// Drop any now-stale tab badges.
if (hadMinimized) this.renderSessionTabs();
},
/** When the feature is toggled on, pop windows for any currently-active runs. */
@@ -355,7 +745,7 @@ Object.assign(CodemanApp.prototype, {
const header =
`<div class="ultracode-phase-header"><span>${escapeHtml(title)}</span>` +
`<span class="ultracode-phase-sub">${this._fmtNum(tok)} tok · ${tools} tools</span></div>`;
return header + group.map((a) => this._workflowAgentCardHtml(a)).join('');
return header + group.map((a) => this._workflowAgentCardHtml(a, run.runId)).join('');
})
.join('');
return head + grid;
@@ -408,4 +798,51 @@ Object.assign(CodemanApp.prototype, {
svg.appendChild(line);
}
},
/**
* Append agent-window → parent connector lines into the shared SVG. Parent is the
* agent's run floating window when open, else the run's session tab. Called right
* after `_appendUltracodeConnectionLines` so it shares the same batched pass + the
* tab-rect cache.
*/
_appendUltracodeAgentConnectionLines(svg, rects) {
this._ensureUltracodeWindowState();
if (!svg || !this.ultracodeAgentWindows.size) return;
if (!rects) rects = new Map();
for (const [agentId, info] of this.ultracodeAgentWindows) {
if (!info.element) continue;
const winRect = info.element.getBoundingClientRect();
// Anchor: parent run window bottom-center if open, else the run's tab.
let px, py;
const runWin = info.runId ? this.ultracodeWindows.get(info.runId) : null;
if (runWin && runWin.element) {
const pr = runWin.element.getBoundingClientRect();
px = pr.left + pr.width / 2;
py = pr.bottom;
} else {
const summary = info.runId && this.workflowRuns ? this.workflowRuns.get(info.runId) : null;
const parentSessionId = summary ? this._resolveUltracodeParentSession(summary) : null;
if (!parentSessionId) continue;
const tabKey = 'tab:' + parentSessionId;
if (!rects.has(tabKey)) {
const tab = document.querySelector(`.session-tab[data-id="${parentSessionId}"]`);
if (tab) rects.set(tabKey, tab.getBoundingClientRect());
}
const tabRect = rects.get(tabKey);
if (!tabRect) continue;
px = tabRect.left + tabRect.width / 2;
py = tabRect.bottom;
}
const x2 = winRect.left + winRect.width / 2;
const y2 = winRect.top;
const midY = (py + y2) / 2;
const path = `M ${px} ${py} C ${px} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
line.setAttribute('d', path);
line.setAttribute('class', 'connection-line ultracode-connection ultracode-agent-connection');
line.setAttribute('data-agent-id', agentId);
svg.appendChild(line);
}
},
});