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` + - `` + - `

Workflow agent ${escapeHtml(agentId)} (${entryCount} entries)

` + - `
${escapeHtml(formatted.join('\n'))}
` - ); - win.document.close(); + if (!formatted || !entryCount) return null; + return { formatted, entryCount }; }, // ----- Render (debounced) ----- @@ -262,13 +252,13 @@ 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, 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 ( `` + 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( + `
` + + `` + + `🧬` + + `${escapeHtml(trunc(name))}` + + `×` + + `
` + ); + } + } + // …then minimized agent transcripts (πŸ“„). + if (agentMap) { + for (const [agentId, entry] of agentMap) { + const name = (entry && entry.label) || agentId; + items.push( + `
` + + `` + + `πŸ“„` + + `${escapeHtml(trunc(name))}` + + `×` + + `
` + ); + } + } + const label = total === 1 ? 'ULTRA' : `ULTRA (${total})`; + return ( + `` + + `${label}` + + `
${items.join('')}
` + + `
` + ); + }, + + /** 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 = ` +
+
+ πŸ“„ + ${escapeHtml(label)} +
+
+ + +
+
+
+
Loading transcript…
+
+ `; + + // 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 = + '
No transcript available yet β€” the agent may be queued, aged out of tracking, or subagent tracking is disabled.
'; + return; + } + const text = escapeHtml(data.formatted.join('\n')); + body.innerHTML = `
${data.entryCount} entries
${text}
`; + }, + + /** 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); + } + }, });