diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2c08744..442930fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/CLAUDE.md b/CLAUDE.md
index 7efcd263..fc8a0d92 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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
diff --git a/package-lock.json b/package-lock.json
index 752b5dcb..948f5871 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": [
diff --git a/package.json b/package.json
index ae74ff83..0a412e40 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/web/public/app.js b/src/web/public/app.js
index 30960fbe..63535778 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -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 {
${hasRunningTasks ? `${taskStats.running}` : ''}
${subagentBadge}
+ ${ultracodeBadge}
⚙⧉×
diff --git a/src/web/public/styles.css b/src/web/public/styles.css
index 998cab16..eeba9e9b 100644
--- a/src/web/public/styles.css
+++ b/src/web/public/styles.css
@@ -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
diff --git a/src/web/public/subagent-windows.js b/src/web/public/subagent-windows.js
index 3d21b292..c0bc622d 100644
--- a/src/web/public/subagent-windows.js
+++ b/src/web/public/subagent-windows.js
@@ -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);
+ }
},
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
diff --git a/src/web/public/ultracode-panel.js b/src/web/public/ultracode-panel.js
index ca7293f6..96d9c4a9 100644
--- a/src/web/public/ultracode-panel.js
+++ b/src/web/public/ultracode-panel.js
@@ -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-.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(
- `Workflow agent ${escapeHtml(agentId)} transcript` +
- `` +
- `
` +
diff --git a/src/web/public/ultracode-windows.js b/src/web/public/ultracode-windows.js
index 3901c659..b7ef4637 100644
--- a/src/web/public/ultracode-windows.js
+++ b/src/web/public/ultracode-windows.js
@@ -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 minimized to a tab
+ if (!this.minimizedUltracodeAgents) this.minimizedUltracodeAgents = new Map(); // sessionId -> Map
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, {
-
+
@@ -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(
+ `
`;
+ },
+
+ /** 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 =
`
${escapeHtml(title)}` +
`${this._fmtNum(tok)} tok Β· ${tools} tools
`;
- 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);
+ }
+ },
});