feat(ultracode): master-detail tab for Workflow/ultracode run visualization

Opt-in (showUltracodeAgents, default OFF) panel that visualizes ultracode /
Workflow-tool runs like Claude Code's "working agents" TUI: LEFT = runs + phases
(selectable tasks), RIGHT = each run's agents with model, live state, tokens
burned, and tool calls.

Standalone — ZERO edits to subagent-watcher.ts. A new workflow-run-watcher.ts
singleton globs the run-state tree (~/.claude/projects/*/*/workflows/wf_*.json,
disjoint from the transcript tree), strips the heavy script/scriptPath/result/logs
fields (174KB -> ~25KB/run), and emits workflow:run_* SSE events. The LEFT list
ships lightweight summaries (getLightState replay + SSE); the RIGHT pane fetches
the full run (with agents[]) via GET /api/workflows/:runId on selection.

Backend: workflow-run-watcher.ts, types/workflow-run.ts, config/workflow-config.ts,
3 SSE events, getLightState workflowRuns replay, GET /api/workflows[/:runId],
showUltracodeAgents schema key + boot-gate (default OFF) + live toggleService.
Frontend: ultracode-panel.js (debounced master-detail render, run/phase select),
header launcher (btn-ultracode-agents--hidden marker -> mobile-guard-exempt),
App Settings toggle (SYNCED, deliberately not in displayKeys).

Agent states on disk are start|progress|done (start=queued; done has
durationMs/resultPreview). Tests: workflow-run-watcher (9), workflow-routes (3).
Verified: tsc/lint/prettier/frontend-syntax/public-assets/mobile-header-guard
clean; full test:ci green (2986 passed); live server + Playwright e2e against 25
real runs (28-agent grid, phase filter, OFF hides launcher).

Design: docs/ultracode-agent-viz-plan.md (rev. 3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-06-15 08:40:05 +02:00
parent f6a30d7335
commit c15c19fab7
17 changed files with 1800 additions and 1 deletions
+63 -1
View File
@@ -44,7 +44,7 @@ import { dataPath } from '../config/instance.js';
import { getHookSecret } from '../config/hook-secret.js';
import { EventEmitter } from 'node:events';
import { Session, isExternalCliMode, type BackgroundTask } from '../session.js';
import type { ClaudeMode, SessionAttachmentHistoryItem, SessionState } from '../types.js';
import type { ClaudeMode, SessionAttachmentHistoryItem, SessionState, WorkflowRunInfo } from '../types.js';
import { RespawnController, RespawnConfig } from '../respawn-controller.js';
import type { TerminalMultiplexer } from '../mux-interface.js';
import { createMultiplexer } from '../mux-factory.js';
@@ -60,6 +60,7 @@ import {
type SubagentToolResult,
} from '../subagent-watcher.js';
import { imageWatcher } from '../image-watcher.js';
import { workflowRunWatcher, summarizeRun } from '../workflow-run-watcher.js';
import { attachmentRegistry, buildFileThumbnailRoute, registerExternalAttachment } from '../attachment-registry.js';
import {
buildDetectedAttachmentHistoryItem,
@@ -259,6 +260,11 @@ export class WebServer extends EventEmitter {
attachmentDetected: (event: AttachmentDetectedEvent) => void;
error: (error: Error, sessionId?: string) => void;
} | null = null;
private workflowRunWatcherHandlers: {
discovered: (info: WorkflowRunInfo) => void;
updated: (info: WorkflowRunInfo) => void;
removed: (data: { runId: string }) => void;
} | null = null;
private tunnelManager: TunnelManager = new TunnelManager();
private authSessions: StaleExpirationMap<string, import('./ports/auth-port.js').AuthSessionRecord> | null = null;
private authFailures: StaleExpirationMap<string, number> | null = null;
@@ -338,6 +344,7 @@ export class WebServer extends EventEmitter {
// Set up subagent watcher listeners
this.setupSubagentWatcherListeners();
this.setupWorkflowRunWatcherListeners();
// Set up image watcher listeners
this.setupImageWatcherListeners();
@@ -437,6 +444,31 @@ export class WebServer extends EventEmitter {
}
}
/**
* Bridge WorkflowRunWatcher events → SSE. Broadcasts run SUMMARIES (no agents[])
* to keep payloads small; the full agents[] is fetched per-run via
* GET /api/workflows/:runId when the user selects a run.
*/
private setupWorkflowRunWatcherListeners(): void {
this.workflowRunWatcherHandlers = {
discovered: (info: WorkflowRunInfo) => this.broadcast(SseEvent.WorkflowRunDiscovered, summarizeRun(info)),
updated: (info: WorkflowRunInfo) => this.broadcast(SseEvent.WorkflowRunUpdated, summarizeRun(info)),
removed: (data: { runId: string }) => this.broadcast(SseEvent.WorkflowRunRemoved, data),
};
workflowRunWatcher.on('run_discovered', this.workflowRunWatcherHandlers.discovered);
workflowRunWatcher.on('run_updated', this.workflowRunWatcherHandlers.updated);
workflowRunWatcher.on('run_removed', this.workflowRunWatcherHandlers.removed);
}
private cleanupWorkflowRunWatcherListeners(): void {
if (this.workflowRunWatcherHandlers) {
workflowRunWatcher.off('run_discovered', this.workflowRunWatcherHandlers.discovered);
workflowRunWatcher.off('run_updated', this.workflowRunWatcherHandlers.updated);
workflowRunWatcher.off('run_removed', this.workflowRunWatcherHandlers.removed);
this.workflowRunWatcherHandlers = null;
}
}
/**
* Set up event listeners for image watcher.
* Broadcasts image detection events to SSE clients for auto-popup.
@@ -1669,6 +1701,7 @@ export class WebServer extends EventEmitter {
respawnStatus,
globalStats: this.store.getAggregateStats(activeSessionTokens),
subagents: subagentWatcher.getRecentSubagents(15), // 15 min to avoid stale agents
workflowRuns: workflowRunWatcher.getAllRunSummaries(), // ultracode run summaries (no agents[]) for the LEFT list
timestamp: now,
inputCjkForm: process.env.INPUT_CJK_FORM?.toUpperCase() === 'ON',
planUsage: getLatestPlanUsage(), // last-known plan-usage telemetry, for the header chip on fresh load
@@ -1937,6 +1970,14 @@ export class WebServer extends EventEmitter {
console.log('Subagent watcher disabled by user settings');
}
// Start workflow run watcher for ultracode / Workflow run visualization (if enabled)
if (await this.isWorkflowAgentTrackingEnabled()) {
workflowRunWatcher.start();
console.log('Workflow run watcher started - monitoring ~/.claude/projects for ultracode run activity');
} else {
console.log('Workflow run watcher disabled by user settings (showUltracodeAgents off)');
}
// Start image watcher for auto-popup of screenshots (if enabled)
if (await this.isImageWatcherEnabled()) {
imageWatcher.start();
@@ -1983,6 +2024,23 @@ export class WebServer extends EventEmitter {
return true; // Default enabled
}
/**
* Check if ultracode/workflow run tracking is enabled in settings (default: FALSE — opt-in).
*/
private async isWorkflowAgentTrackingEnabled(): Promise<boolean> {
const settingsPath = dataPath('settings.json');
try {
const content = await fs.readFile(settingsPath, 'utf-8');
const settings = JSON.parse(content);
return settings.showUltracodeAgents ?? false;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
console.error('Failed to read showUltracodeAgents setting:', err);
}
}
return false; // Default disabled (opt-in)
}
/**
* Check if image watcher is enabled in settings (default: false)
*/
@@ -2337,12 +2395,16 @@ export class WebServer extends EventEmitter {
// Clean up watcher listeners to prevent memory leaks
this.cleanupSubagentWatcherListeners();
this.cleanupWorkflowRunWatcherListeners();
this.cleanupImageWatcherListeners();
this.cleanupTeamWatcherListeners();
// Stop subagent watcher
subagentWatcher.stop();
// Stop workflow run watcher
workflowRunWatcher.stop();
// Stop image watcher
imageWatcher.stop();