From a0bf92231549b526533f6b8129813b74d6ce5cf3 Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Sun, 11 Jan 2026 15:21:08 +0000 Subject: [PATCH 1/5] beads(mobile-log-scroll): Create task for fixing mobile log viewer scrollability --- .beads/beads.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 395ac29cc..a9288e73b 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -50,3 +50,4 @@ {"id":"bd-docker-concurrency","title":"Add concurrency limit to Docker workflow","description":"Add GitHub Actions concurrency settings to .github/workflows/docker.yml to prevent resource contention from 4 concurrent jobs (build-base + 2 matrix builds + update-workspaces).\n\nCurrent state:\n- 4 Docker build jobs run concurrently\n- Compete for CPU, memory, and registry quota\n- Can cause builds to timeout or fail\n\nRequired changes:\nAdd to docker.yml after 'on:' section:\n```yaml\nconcurrency:\n group: docker-build\n cancel-in-progress: true\n```\n\nBenefit: Serializes Docker builds to prevent resource starvation while maintaining fast sequential execution.\n\nNote: This requires GitHub App to have 'workflows' OAuth scope. Currently blocked by missing scope.","priority":70,"status":"closed","created_at":"2026-01-09T22:45:00Z","closed_at":"2026-01-10T00:00:00Z","closed_reason":"Added concurrency settings to .github/workflows/docker.yml","tags":["ci","docker","optimization","workflows-permission-required"],"depends_on":[]} {"id":"bd-git-worktrees","title":"Set up git worktrees for parallel multi-branch agent work","description":"Implement git worktrees to improve coordination when multiple agents work on different branches simultaneously.\n\nProblem:\n- Current workflow: agents work on different branches but single worktree causes switching overhead\n- Branch switching adds 30+ seconds per context switch\n- No true parallel work on multiple branches\n- Coordination complexity when agents need different branches\n\nSolution: Git worktrees allow multiple branches checked out simultaneously\n- Each branch in separate directory (/workspace/lead, /workspace/auth, /workspace/test-fix)\n- No switching overhead\n- Agents can work in parallel without blocking each other\n- Cleaner coordination model\n\nImplementation:\n1. Create worktree structure in .git/worktrees/\n2. Document worktree setup in lead.md\n3. Update agent spawn process to use worktrees when multi-branch work needed\n4. Example: git worktree add ../workspace-lead fix/workspace-persistence\n\nBenefit:\nParallel multi-branch coordination: 3+ agents on different branches simultaneously without context switching overhead.","priority":60,"status":"open","created_at":"2026-01-09T21:30:00Z","tags":["infrastructure","git-workflow","coordination","optimization"],"depends_on":[]} {"id":"bd-trail-config","title":"Trail CLI should read relay config file independently","description":"Trail CLI currently only reads TRAJECTORIES_DATA_DIR env var set by daemon. Should independently read ~/.config/agent-relay/relay.json when env var not set. This makes Trail CLI work standalone without daemon pre-configuration.\n\nRequirements:\n1. Trail CLI should check for config file before relying on env var\n2. Read ~/.config/agent-relay/relay.json or AGENT_RELAY_CONFIG_DIR/relay.json\n3. Parse storeInRepo setting to determine storage location\n4. Fall back to default ~/.config/agent-relay/trajectories/ if no config exists\n5. Environment variables still take precedence (can override config)\n\nBenefits:\n- Trail CLI independent of daemon environment setup\n- Consistent behavior across contexts (manual cli, daemon spawn, etc)\n- Config-driven approach instead of env var leakage\n\nFiles to modify:\n- Trail CLI source (not in this repo - upstream project)\n- Update integration.ts documentation about expected Trail CLI behavior\n- Add note to trajectory/config.ts about Trail CLI expectations","priority":60,"status":"open","created_at":"2026-01-09T22:37:00Z","tags":["trajectory","infrastructure","enhancement"],"depends_on":[]} +{"id":"bd-mobile-log-scroll","title":"Fix mobile log viewer scrollability","description":"Enable scrolling for log viewers on mobile devices.\n\nProblem:\n- Log viewer on mobile is not scrollable due to overflow-hidden CSS\n- Desktop xterm.js works fine but mobile touch users cannot scroll\n\nSolution:\n- Add mobile-specific scroll handling to XTermLogViewer.tsx\n- Use Tailwind responsive classes (sm:, md:, lg:) for breakpoint handling\n- Keep desktop xterm.js scrolling experience unchanged\n- Ensure touch scrolling works smoothly on iOS and Android\n\nImplementation:\n- Modify terminal container overflow styles for mobile responsiveness\n- Test on mobile devices (iOS Safari, Android Chrome)\n- Ensure no console errors\n- Both inline and panel modes should work\n\nFiles:\n- src/dashboard/react-components/XTermLogViewer.tsx\n- src/dashboard/react-components/LogViewer.tsx (inline mode)\n- src/dashboard/react-components/LogViewerPanel.tsx (if needed)\n\nAcceptance Criteria:\n- Mobile users can scroll through logs with touch gestures\n- Desktop scrolling behavior unchanged\n- No console errors\n- Works on both iPhone and Android devices","priority":60,"status":"open","created_at":"2026-01-11T14:58:00Z","tags":["frontend","mobile","ui","bugfix"]} From c2efb3229aeb56efd16744cad312fd6886bb1c43 Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Sun, 11 Jan 2026 15:23:37 +0000 Subject: [PATCH 2/5] fix(dashboard): enable mobile touch scrolling in log viewers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - XTermLogViewer: Change overflow-hidden to overflow-auto on mobile, keeping overflow-hidden on md+ breakpoints where xterm.js handles internal scrolling - LogViewer inline mode: Add touch-pan-y for consistent touch behavior - Both components: Add WebkitOverflowScrolling: 'touch' for iOS momentum scrolling Fixes mobile users being unable to scroll through logs due to overflow-hidden blocking touch scroll gestures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/dashboard/react-components/LogViewer.tsx | 4 ++-- src/dashboard/react-components/XTermLogViewer.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dashboard/react-components/LogViewer.tsx b/src/dashboard/react-components/LogViewer.tsx index 989b25ad0..0a2c91d15 100644 --- a/src/dashboard/react-components/LogViewer.tsx +++ b/src/dashboard/react-components/LogViewer.tsx @@ -134,8 +134,8 @@ export function LogViewer({
diff --git a/src/dashboard/react-components/XTermLogViewer.tsx b/src/dashboard/react-components/XTermLogViewer.tsx index 666f81578..52bd10ee3 100644 --- a/src/dashboard/react-components/XTermLogViewer.tsx +++ b/src/dashboard/react-components/XTermLogViewer.tsx @@ -455,8 +455,8 @@ export function XTermLogViewer({ {/* Terminal container */}
{/* Footer status bar */} From 5df46ac4dbf8d888af60aac24d48a7e319a45897 Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Sun, 11 Jan 2026 15:49:03 +0000 Subject: [PATCH 3/5] fix(api): detect agents from daemon registry in docker deployments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api/spawned endpoint now returns agents from two sources: 1. Spawner's in-memory activeWorkers map (authoritative for spawned agents) 2. Daemon's agents.json registry (fallback for docker restarts) This fixes the issue where workspace.agents returned empty in docker deployments after container restarts. The spawner's in-memory state would be lost, but agents that reconnected to the daemon are tracked in agents.json with lastSeen timestamps. The fix only includes daemon-registered agents that are: - Not already tracked by spawner (to avoid duplicates) - Recently active (within 30s heartbeat window) Also added source debugging info to the response for troubleshooting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/dashboard-server/server.ts | 75 ++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/src/dashboard-server/server.ts b/src/dashboard-server/server.ts index c1e73445e..3c20cb4a0 100644 --- a/src/dashboard-server/server.ts +++ b/src/dashboard-server/server.ts @@ -3536,20 +3536,81 @@ Start by greeting the project leads and asking for status updates.`; /** * GET /api/spawned - List active spawned agents + * + * Returns agents from two sources: + * 1. Spawner's active workers (in-memory tracking) + * 2. Daemon's agents.json registry (persisted, survives restarts) + * + * This fallback ensures docker deployments show agents even after + * container restarts when spawner's in-memory state is lost but + * agents have reconnected to the daemon. */ app.get('/api/spawned', (req, res) => { - if (!spawner) { - return res.status(503).json({ - success: false, - error: 'Spawner not enabled', - agents: [], - }); + // Collect agents from all available sources + const agentsByName = new Map(); + + // Source 1: Spawner's active workers (authoritative for spawned agents) + if (spawner) { + for (const worker of spawner.getActiveWorkers()) { + agentsByName.set(worker.name, { + name: worker.name, + cli: worker.cli, + pid: worker.pid, + spawnedAt: worker.spawnedAt, + task: worker.task, + team: worker.team, + source: 'spawner', + }); + } } - const agents = spawner.getActiveWorkers(); + // Source 2: Daemon's agents.json registry (fallback for docker restarts) + // Only include agents not already tracked by spawner + const agentsPath = path.join(teamDir, 'agents.json'); + if (fs.existsSync(agentsPath)) { + try { + const data = JSON.parse(fs.readFileSync(agentsPath, 'utf-8')); + const registeredAgents = data.agents || []; + const thirtySecondsAgo = Date.now() - 30 * 1000; + + for (const agent of registeredAgents) { + // Skip if already tracked by spawner + if (agentsByName.has(agent.name)) continue; + + // Only include recently active agents (within 30s heartbeat window) + const lastSeen = agent.lastSeen ? new Date(agent.lastSeen).getTime() : 0; + if (lastSeen < thirtySecondsAgo) continue; + + agentsByName.set(agent.name, { + name: agent.name, + cli: agent.cli || 'unknown', + spawnedAt: agent.connectedAt ? new Date(agent.connectedAt).getTime() : undefined, + team: agent.team, + source: 'daemon', + }); + } + } catch (err) { + console.error('[api/spawned] Failed to read agents.json:', err); + } + } + + const agents = Array.from(agentsByName.values()); res.json({ success: true, agents, + // Include source info for debugging + sources: { + spawnerEnabled: !!spawner, + daemonAgentsFile: fs.existsSync(agentsPath), + }, }); }); From c0506b49b101e609da8fd8e3a43c212ab57d1df4 Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Sun, 11 Jan 2026 16:04:32 +0000 Subject: [PATCH 4/5] fix(dashboard): implement interrupt button with ESC sequence injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update InterruptIcon to show ESC key symbol instead of stop icon - Add /api/agents/by-name/:name/interrupt endpoint - Endpoint sends ESC ESC (0x1b 0x1b) to agent's PTY - Works for spawned agents via AgentSpawner The interrupt button now sends escape sequences to break agents out of stuck loops without terminating them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/dashboard-server/server.ts | 50 +++++++++++++++++++ .../react-components/LogViewerPanel.tsx | 10 ++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/dashboard-server/server.ts b/src/dashboard-server/server.ts index 3c20cb4a0..040760d64 100644 --- a/src/dashboard-server/server.ts +++ b/src/dashboard-server/server.ts @@ -3649,6 +3649,56 @@ Start by greeting the project leads and asking for status updates.`; } }); + /** + * POST /api/agents/by-name/:name/interrupt - Send ESC sequence to interrupt an agent + * + * Sends ESC ESC (0x1b 0x1b) to the agent's PTY to interrupt the current operation. + * This is useful for breaking agents out of stuck loops without terminating them. + */ + app.post('/api/agents/by-name/:name/interrupt', (req, res) => { + if (!spawner) { + return res.status(503).json({ + success: false, + error: 'Spawner not enabled', + }); + } + + const { name } = req.params; + + // Check if agent exists + if (!spawner.hasWorker(name)) { + return res.status(404).json({ + success: false, + error: `Agent ${name} not found or not spawned`, + }); + } + + try { + // Send ESC ESC sequence to interrupt the agent + // ESC = 0x1b in hexadecimal + const success = spawner.sendWorkerInput(name, '\x1b\x1b'); + + if (success) { + console.log(`[api] Sent interrupt (ESC ESC) to agent ${name}`); + res.json({ + success: true, + message: `Interrupt signal sent to ${name}`, + }); + } else { + res.status(500).json({ + success: false, + error: `Failed to send interrupt to ${name}`, + }); + } + } catch (err: any) { + console.error('[api] Interrupt error:', err); + res.status(500).json({ + success: false, + error: err.message, + }); + } + }); + /** * GET /api/trajectory - Get current trajectory status */ diff --git a/src/dashboard/react-components/LogViewerPanel.tsx b/src/dashboard/react-components/LogViewerPanel.tsx index 67f243d6a..023067e87 100644 --- a/src/dashboard/react-components/LogViewerPanel.tsx +++ b/src/dashboard/react-components/LogViewerPanel.tsx @@ -37,7 +37,6 @@ export function LogViewerPanel({ availableAgents = [], }: LogViewerPanelProps) { const colors = getAgentColor(agent.name); - const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(true); // Default to collapsed const [isInterrupting, setIsInterrupting] = useState(false); // Handle interrupt button click @@ -287,7 +286,7 @@ export function LogViewerPanel({
- {/* Interrupt button - break agent out of stuck loops */} + {/* Interrupt button - send ESC to break agent out of stuck loops */} @@ -444,8 +443,9 @@ function AgentSwitcher({ agents, currentAgent, onSelect }: AgentSwitcherProps) { function InterruptIcon() { return ( - {/* Stop/pause hand icon */} - + {/* ESC key icon - represents sending escape sequence */} + + ESC ); } From 5c3cb6d0f139e056d343a87dc67f00e48e808dca Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Sun, 11 Jan 2026 16:28:31 +0000 Subject: [PATCH 5/5] fix(dashboard): add missing isHeaderCollapsed state declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collapsible header feature was using isHeaderCollapsed state but the useState declaration was missing, causing a TypeScript error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/dashboard/react-components/LogViewerPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dashboard/react-components/LogViewerPanel.tsx b/src/dashboard/react-components/LogViewerPanel.tsx index 023067e87..1af482dc5 100644 --- a/src/dashboard/react-components/LogViewerPanel.tsx +++ b/src/dashboard/react-components/LogViewerPanel.tsx @@ -38,6 +38,7 @@ export function LogViewerPanel({ }: LogViewerPanelProps) { const colors = getAgentColor(agent.name); const [isInterrupting, setIsInterrupting] = useState(false); + const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(false); // Handle interrupt button click const handleInterrupt = useCallback(async () => {