* feat: add end-to-end observability across core, web, and terminal
Instrument lifecycle/API/websocket flows with correlation-aware metrics and operator health surfaces so the system can self-diagnose and escalate failures.
* fix: realign session restore set and unblock claim PR typecheck
* fix(web): restore project-filtered sessions route after main merge
* fix(core): remove unused orchestrator import to unblock lint
* fix(core): always record lifecycle poll failures and remove dead review branches
* fix(web): harden websocket metrics and reuse SSE observers
* fix(web): preserve primary session API errors when services bootstrap fails
* fix(web): use full orchestrator config type in SSE observer helper
* fix(web): restore project-scoped SSE and guard observability error paths
* fix(web): address remaining Bugbot review gaps
* fix(web): align SSE project attribution and share session project resolver
* fix(web): require request arg for SSE route and align tests
* fix(web): record websocket error disconnects as failures
* fix(web): use path alias for session project resolver import
* fix(web): include active connection count in disconnect metrics
Treat the all-projects dashboard as a project overview surface with per-project metrics and orchestrator access, while keeping the detailed kanban lanes for single-project workflows.
Scan every orchestrator for active pause metadata instead of stopping at the first control session, so pause banners stay visible even when metadata-role orchestrators appear before the paused orchestrator in session listings.
Use one project-matching rule across the sessions API and SSR dashboard paths, and keep global pause derived from the full session set even when the API request is scoped to a single project.
Keep the SSR dashboard page using unified project filtering while clearing orchestrator links and pause state whenever enrichment fails, so error paths cannot render stale state beside an empty session list.
Use the same project matching logic for dashboard orchestrators and workers, and switch the client detail page to the lightweight core types entry so the canonical orchestrator helper stays shared without pulling server-only modules into the browser bundle.
Carry legacy project ownership through recovery, drop the redundant dashboard worker filter, and remove the now-unused orchestrator helper so the PR branch stays aligned with the shared per-project orchestration contract.
Use the core orchestrator detector in web filtering paths so SSE snapshots, backlog polling, and pause state all treat metadata-role orchestrators consistently with the dashboard and sessions API.
Keep the orchestrator detail page keyed on stable project-scoped values so zone-count polling does not recreate its callback on every session fetch and hammer the sessions API.
Preserve the legacy project fallback in the shared session loader and keep the sessions route filtering by project after the clean branch cherry-pick, so the final PR branch matches the verified multi-project behavior.
Render every project's orchestrator explicitly and keep orchestrator detail pages scoped to their own project so multi-project dashboards show the right control session.
Expose orchestrator links per project and keep them out of worker session stats so dashboard consumers stop collapsing multi-project state into one global orchestrator.
* feat(core): add scm webhook contract
Defines a provider-agnostic SCM webhook contract in core types and
config so SCM plugins can verify and normalize inbound webhook events
without reshaping project config later.
* feat(scm): trigger lifecycle checks from github webhooks
Adds GitHub webhook verification and event parsing, exposes a web
webhook endpoint, and routes matching PR/branch events through the
existing lifecycle manager so CI and review reactions update immediately.
* fix(scm): verify github signatures with raw webhook bytes
Preserves the original webhook bytes alongside the decoded payload so
GitHub HMAC verification uses the exact request body while the route
continues to drive lifecycle checks through the existing manager.
* fix(web): wire scm webhook route into main branch services
Restores the main-branch service and route-test wiring while keeping the
new webhook route coverage and scoped lifecycle helper in place.
* fix(webhooks): use singleton lifecycle manager and fail closed on scm API errors
Reuses the existing services lifecycle manager for webhook-triggered checks
so reactions and state transitions don't replay from a fresh instance, and
restores fail-closed behavior for GitHub review comment fetch failures.
* fix(webhooks): tighten project matching and restore scm compatibility methods
Prevents repository-less webhook events from matching all projects, restores
GitHub SCM PR utility methods and CI status rollup fallback, and adds tests
covering the compatibility paths and safer project matching behavior.
* fix(webhooks): pre-check content length and continue on parse errors
Adds an early content-length guard against configured maxBodyBytes before
reading the body and changes candidate parse failures to fail-forward so
one malformed payload path does not abort other valid candidate handling.
* fix(scm-github): parse review-comment timestamps from comment payload
Use comment.updated_at/created_at for pull_request_review_comment webhook
timestamps so normalized events retain temporal data for comment events.
* fix(webhooks): apply early size guard only when all candidates are bounded
Uses the broadest candidate limit for pre-read content-length checks and
skips early rejection when any matching project has no configured limit,
while retaining per-candidate verification limits.
* fix(webhooks): normalize repo matching and skip terminal sessions
Match webhook repository names case-insensitively against configured project
repos and avoid lifecycle checks for terminal sessions when resolving
webhook-affected sessions.
* fix(webhooks): fail forward when lifecycle checks throw
* fix(scm-github): parse push webhook branch and sha
* refactor(scm-github): dedupe cli exec helper wrappers
* fix(scm-github): tighten exec helper type and comment timestamps
* fix(scm-github): prefer head_commit timestamp for push events
* fix(webhooks): tighten repository parsing and helper visibility
* fix(scm-github): ignore non-head refs for push branch
* chore: trigger bugbot rerun
* feat(scm-gitlab): add webhook verification and event parsing
* fix(webhooks): share parser utils and handle check_run branch
* fix(webhooks): ignore gitlab tag refs in ci branch mapping
* fix(scm-gitlab): harden token and tag ref handling
* chore(scm-gitlab): update webhook helpers around bugbot threads
* feat(web): add project-based dashboard architecture
- Add project query parameter to API routes
- Filter sessions by project in both SSR and SSE
- Update useSessionEvents hook to accept project param
- Update Dashboard and pass project to hook
- Add unit tests for project filtering
- Add architecture spec document
Implements project-based architecture as defined in docs/specs/project-based-dashboard-architecture.md:
- GET /api/sessions?project=X returns sessions for project X
- GET /api/events?project=X streams only sessions for project X
- Dashboard uses project filter from config
- SSE URL includes project param when provided
- Project matching uses projectId and sessionPrefix
- Full backward compatibility maintained (no param = all sessions)
* fix: remove duplicate export default in page.tsx
* fix(web): clean project-scoped dashboard verification
* fix(web): scope dashboard using project id
* fix(web): address Bugbot findings in project-scoped dashboard
- Consolidate triplicated matchesProject into shared lib/project-utils.ts
- Fix Dashboard to receive both projectId (for SSE filtering) and projectName (for display)
- Remove inline matchesProject definitions from page.tsx, sessions/route.ts, events/route.ts
* fix(web): resolve Bugbot issues in project-scoped dashboard
- Add ?project=all query param support in SSR page to show all sessions
(previously getPrimaryProjectId() always returned non-empty, making
else branches unreachable)
- Exclude orchestrator sessions from SSE stream to match SSR/API behavior
(orchestrator sessions get their own button, not a card)
- Add tests for SSE stream orchestrator exclusion and project filtering
Addresses Bugbot issues:
- #2903595986: Dead else branches when project filter always applied
- #2903595995: SSE stream includes orchestrator sessions unlike SSR/API
* feat(web): add project navigation sidebar
Add visible left sidebar with project navigation for multi-project setups:
- ProjectSidebar component with active state styling
- /api/projects endpoint to fetch configured projects
- getAllProjects() helper in project-name.ts
- Sidebar appears only when 2+ projects configured
- Click navigation updates ?project= query param
- Active project highlighted with accent color
Tests:
- ProjectSidebar component tests (8 tests)
- API routes tests with proper mocking
- All 386 web tests passing
Manual test steps:
1. Configure 2+ projects in agent-orchestrator.yaml
2. Start dashboard - sidebar should appear on left
3. Click different projects - URL updates, sessions filter
4. Click "All Projects" - shows all sessions across projects
5. Active project highlighted in sidebar
* fix(web): remove duplicate import in test file
* fix(web): consolidate ProjectInfo type to shared source
Remove duplicated ProjectInfo interface definitions from Dashboard.tsx and
ProjectSidebar.tsx. Both now import the type from @/lib/project-name where it is exported as the single source of truth.
This addresses Bugbot issue #2906835895: Triplicated ProjectInfo type instead of shared import.
* fix(web): integrate globalPause state from main
* fix(web): consolidate duplicate @/lib/types import
* feat(web): add project-based dashboard architecture
- Add project query parameter to API routes
- Filter sessions by project in both SSR and SSE
- Update useSessionEvents hook to accept project param
- Update Dashboard and pass project to hook
- Add unit tests for project filtering
- Add architecture spec document
Implements project-based architecture as defined in docs/specs/project-based-dashboard-architecture.md:
- GET /api/sessions?project=X returns sessions for project X
- GET /api/events?project=X streams only sessions for project X
- Dashboard uses project filter from config
- SSE URL includes project param when provided
- Project matching uses projectId and sessionPrefix
- Full backward compatibility maintained (no param = all sessions)
* fix: remove duplicate export default in page.tsx
* fix(web): clean project-scoped dashboard verification
* fix(web): scope dashboard using project id
* fix(web): address Bugbot findings in project-scoped dashboard
- Consolidate triplicated matchesProject into shared lib/project-utils.ts
- Fix Dashboard to receive both projectId (for SSE filtering) and projectName (for display)
- Remove inline matchesProject definitions from page.tsx, sessions/route.ts, events/route.ts
* fix(web): resolve Bugbot issues in project-scoped dashboard
- Add ?project=all query param support in SSR page to show all sessions
(previously getPrimaryProjectId() always returned non-empty, making
else branches unreachable)
- Exclude orchestrator sessions from SSE stream to match SSR/API behavior
(orchestrator sessions get their own button, not a card)
- Add tests for SSE stream orchestrator exclusion and project filtering
Addresses Bugbot issues:
- #2903595986: Dead else branches when project filter always applied
- #2903595995: SSE stream includes orchestrator sessions unlike SSR/API
* feat(web): add project navigation sidebar
Add visible left sidebar with project navigation for multi-project setups:
- ProjectSidebar component with active state styling
- /api/projects endpoint to fetch configured projects
- getAllProjects() helper in project-name.ts
- Sidebar appears only when 2+ projects configured
- Click navigation updates ?project= query param
- Active project highlighted with accent color
Tests:
- ProjectSidebar component tests (8 tests)
- API routes tests with proper mocking
- All 386 web tests passing
Manual test steps:
1. Configure 2+ projects in agent-orchestrator.yaml
2. Start dashboard - sidebar should appear on left
3. Click different projects - URL updates, sessions filter
4. Click "All Projects" - shows all sessions across projects
5. Active project highlighted in sidebar
* fix(web): remove duplicate import in test file
* fix(web): consolidate ProjectInfo type to shared source
Remove duplicated ProjectInfo interface definitions from Dashboard.tsx and
ProjectSidebar.tsx. Both now import the type from @/lib/project-name where it is exported as the single source of truth.
This addresses Bugbot issue #2906835895: Triplicated ProjectInfo type instead of shared import.
* fix(web): integrate globalPause state from main
* fix(web): consolidate duplicate @/lib/types import
* fix(web): restore global pause state and membership refresh
* fix(web): satisfy lint in project-scoped page defaults
* fix(web): remove dead global pause reducer action
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(web): restore global pause resume time
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* chore(web): retrigger bugbot after thread reset
* refactor(web): centralize project session filtering helpers
* fix(web): reset pause banner dismissal on new pause
* fix(web): remove useless orchestrator assignment
* fix(web): derive header stats from live session state
* fix(web): restore project name in dashboard header
* fix(web): restore backlog poller startup in events stream
* refactor(web): keep project-utils helpers internal
* chore: retrigger bugbot evaluation
* chore: retrigger stuck bugbot check
* fix: address latest bugbot findings for dashboard events
* test(web): add dashboard bugbot regression coverage
* refactor(web): simplify projectId selection in dashboard props
* fix(web): derive selected project name from project filter
* refactor(web): initialize dashboard defaults before service load
* fix(web): satisfy lint in dashboard page error path
* feat: wire lifecycle manager, backlog auto-claim, and dashboard overhaul
- Start LifecycleManager in dashboard server (30s polling) so reactions
actually fire: CI failures, review comments, merge conflicts are now
auto-forwarded to agents
- Add backlog auto-claim poller (60s interval) that watches for issues
labeled `agent:backlog` and auto-spawns agent sessions up to max
concurrent limit (5)
- Add tabbed dashboard UI: Board (kanban), Backlog (issue queue), PRs
- Add issue creation form in dashboard — creates GitHub issues with
`agent:backlog` label for immediate agent pickup
- Add API routes: /api/backlog, /api/issues, /api/setup-labels
- Pass notifier config through plugin registry (slack webhook fix)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add task decomposition layer (classify → decompose → recurse)
Adds LLM-driven recursive task decomposition upstream of session spawning.
Complex issues are broken into atomic subtasks before agents start working.
Each agent receives lineage context (where it fits in the hierarchy) and
sibling awareness (what parallel agents are doing).
Core changes:
- New decomposer module (core/src/decomposer.ts) — classify, decompose,
plan tree, lineage formatting, using Claude API
- Extended SessionSpawnConfig with lineage/siblings fields
- Prompt builder Layer 4: decomposition context (hierarchy + siblings)
- ProjectConfig.decomposer config section with Zod validation
- Tracker plugin: added removeLabels support for label management
CLI:
- `ao spawn <project> <issue> --decompose` flag
- `--max-depth <n>` option for decomposition depth
- Spawns multiple sessions with lineage context for composite tasks
Backlog poller:
- Respects project.decomposer.enabled for auto-decomposition
- Posts plan as issue comment when requireApproval=true
- Auto-spawns subtasks with lineage when requireApproval=false
Config example:
projects:
my-app:
decomposer:
enabled: true
maxDepth: 3
requireApproval: true
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add verification gate — issues stay open until human confirms fix
PR merge no longer auto-closes GitHub issues. Instead:
1. On PR merge: issue labeled `merged-unverified`, stays open
2. Human checks staging, then runs `ao verify <issue>` to close
3. Or `ao verify <issue> --fail` to flag verification failure
Changes:
- services.ts: labelIssuesForVerification() replaces closeIssuesForMergedSessions()
- New CLI command: `ao verify` (verify/fail/list modes)
- New API route: GET/POST /api/verify
- Dashboard: new Verify tab with one-click verify/fail buttons
- ao status: shows count of issues awaiting verification
- Idle session detection + auto-nudge reaction
- Use TERMINAL_STATUSES in batch-spawn dedup check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rename decomposerConfig to avoid variable shadowing
Addresses Bugbot medium severity issue where inner variable
shadowed outer from getServices().
* fix: update pnpm-lock.yaml for new @anthropic-ai/sdk dependency
* fix: resolve remaining merge conflicts and syntax errors
- Remove leftover conflict markers in types.ts
- Remove orphaned code in services.ts
- Fix semicolon to comma in config.ts
- Remove unused import in verify.ts
* fix: address final Bugbot issues
- requireApproval path now exits early with continue to prevent
fall-through to in-progress label and session spawned comment
- remove packages/core/package-lock.json (pnpm workspace should only
use root pnpm-lock.yaml)
* fix: idle sessions now transition back to working
When agent resumes activity after being idle, the status correctly
transitions to 'working' instead of remaining stuck in 'idle' state.
* fix(backlog): remove agent:backlog label when claiming issues
When claiming issues from the backlog, the poller now removes the
agent:backlog label in addition to adding agent:in-progress. This
prevents duplicate work if all spawned sessions reach terminal status
and the poller rediscovers the issue.
* fix(test): use Set for TERMINAL_STATUSES mock
The mock for TERMINAL_STATUSES was an array, but the real export is a
ReadonlySet. Changed to use a Set so tests with non-empty sessions won't
crash when calling .has().
* fix(web): resolve backlog/dashboard regressions after branch sync
* fix(web): align dashboard events hook and SSE test mocks
* fix(notifier-openclaw): apply exponential delay from retry index
* fix(integration-tests): align openclaw retry delay expectation
* fix(web): keep dashboard header stats in sync
* fix(openclaw): keep first retry at base delay
---------
Co-authored-by: Agent <agent@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Harsh <harsh@Ubuntu-24-Forrest.lan>
Co-authored-by: Harsh <harsh@example.com>
* fix: pause workers on model limits and stabilize session visibility
Detect model limit exhaustion, pause project worker operations until reset, and expose pause state in dashboard/API. Also harden killed-session cleanup and SSE session reconciliation so sessions do not appear ghost-active or disappear until reload.
* fix: satisfy lint in rate-limit pause probe
* fix: address bugbot feedback for pause handling
* fix(lifecycle): prevent infinite re-pause loop for duration-based rate limits
Duration-based rate limits (e.g., 'usage limit reached for N hours') were
causing infinite re-pause loops because they always calculate reset time as
Date.now() + duration, which extends the pause on every poll cycle if the
message remains in terminal output.
Now checks for existing active pause before setting a new one:
- Skips override if same session already has active pause
- Preserves longer pauses from other sessions
Fixes infinite loop described in PR #367 review comment.
* fix(core): export global pause constants to prevent duplication
Export GLOBAL_PAUSE_*_KEY constants and parsePauseUntil utility from
@composio/ao-core so web package can import them instead of hardcoding.
This prevents silent breakage if key values ever change - now there's
a single source of truth.
Addresses review comment on PR #367.
* fix(web): globalPause as first-class state in SSE event flow
globalPause is now part of the same reducer/event flow as sessions,
not derived from provider-specific output text in the UI.
Changes:
- useSessionEvents: manage globalPause alongside sessions in reducer state
- Dashboard: consume globalPause from hook instead of SSR-only prop
- types: re-export GlobalPauseState from shared lib (provider-agnostic contract)
- Tests: 15 new tests proving banner appears/disappears from state updates
alone, regardless of agent model/plugin (Claude Code, OpenCode, Codex)
Design requirements satisfied:
- First-class state in same reducer/event flow as sessions
- Key names sourced from shared core contract via export/import
- Provider-neutral: no Anthropic/OpenAI string coupling in control logic
- State-driven: banner visibility from reducer state updates via SSE
Addresses Bugbot finding: Dashboard pause banner never updates after
initial render (eba24a0b-9e4c-47e3-91c9-7d10be01e3cf)
* fix: remove unused import in test file
* fix(cli): consistent purge default for session kill and stop commands
The kill method's default changed from opt-in (=== true) to opt-out (!== false).
Both session kill and stop commands now use the same logic:
purgeOpenCode = opts.purgeSession === true ? true : opts.keepSession !== true
This ensures consistent behavior across all kill paths.
Adds --keep-session flag to both commands.
Adds regression tests for provider-agnostic behavior verified.
Addresses Bugbot finding: orchestrator start cleanup inconsistent
with new purge default (2bc2535f-b2d1-4f64-96d6-150f97ed8564)
When the AO dashboard is served behind a reverse proxy (e.g. Caddy ->
FastAPI -> Next.js), the browser cannot reach the direct terminal
WebSocket server on port 14801. The connection attempt to
wss://hostname:14801/ws times out, leaving terminals stuck at
'Connecting...'.
Fix: detect when running on a standard port (443/80, indicating a
reverse proxy) and use a path-based WebSocket endpoint
(/ao-terminal-ws) instead of the direct port. This allows the proxy
to forward WebSocket connections to the terminal server.
Supports three modes:
1. NEXT_PUBLIC_TERMINAL_WS_PATH env var (explicit path override)
2. Auto-detect reverse proxy (standard port -> /ao-terminal-ws)
3. Direct port access (dev mode, non-standard port -> :14801)
Extract isPRMergeReady() helper that checks pr.state === "open" before
evaluating merge criteria, preventing the badge from appearing alongside
the 'Merged' chip on already-merged PRs.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The /api/sessions endpoint was filtering out orchestrator sessions,
so the mobile app could never detect a running orchestrator. Now the
API finds the orchestrator session ID before filtering and includes
it in the response as orchestratorId.
- Add orchestratorId to /api/sessions JSON response
- Add orchestratorId to SessionsResponse type
- Thread orchestratorId through useSessions hook
- Use orchestratorId in OrchestratorScreen instead of searching sessions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
xterm.js handles paste natively via its internal textarea — our
container-level paste listener either double-pastes (if the event
bubbles) or is dead code (if xterm calls stopPropagation). Also remove
the dead isPaste key handler block that returned true (same as default).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes from code review:
1. Clear selection after Cmd+C so the terminal resumes receiving
output immediately instead of staying frozen up to 5 seconds.
2. Add 1MB byte cap on the write buffer to prevent OOM if a fast
process dumps output while the user has text selected.
3. Use ws.current instead of a stale captured websocket reference
in the resize effect, preventing throws if the WebSocket
reconnected during a fullscreen toggle.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Buffer incoming terminal.write() only after onSelectionChange reports
a non-empty selection — NOT during mousedown. This lets xterm.js
render the selection highlight normally, then prevents incoming data
from clearing it. The buffer flushes when the user clears the
selection (click/keypress) or after a 5s safety timeout.
Previous attempts broke because they buffered during mousedown which
prevented xterm.js from rendering the highlight in the first place.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The write buffer intercepted all terminal.write() during mousedown,
which prevented xterm.js from rendering the selection highlight.
Simplified to: auto-copy on selection change + DOM paste event +
keyboard shortcut fallback. The selection highlight may disappear
from incoming data, but the text is already in the clipboard.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Buffer incoming WebSocket data while the user has an active selection.
xterm.js clears the selection on every terminal.write(), so without
buffering the selection disappears instantly in a live terminal.
How it works:
- mousedown: start buffering (user is drag-selecting)
- mouseup: stop tracking mouse, but keep buffering if selection exists
- onSelectionChange(empty): selection cleared → flush buffer
- Safety timeout (5s): flush regardless to prevent unbounded buffering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues with the previous clipboard implementation:
1. Cmd+C failed because incoming terminal.write() clears the xterm.js
selection before the user can press the shortcut. Fixed by adding
terminal.onSelectionChange() that auto-copies to clipboard the
moment text is selected.
2. Cmd+V failed silently because navigator.clipboard.readText() requires
the "clipboard-read" browser permission which may not be granted.
Fixed by using the DOM "paste" event (ClipboardEvent.clipboardData)
which provides clipboard content without needing that permission.
Also simplified platform detection by checking metaKey/ctrlKey directly
instead of parsing navigator.platform.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep upstream's connectWebSocket() reconnection structure and add our
clipboard shortcuts (Cmd+C/V, Ctrl+Shift+C/V) and OSC 52 handler on
top. Clipboard paste now uses ws.current to match upstream's pattern
where the WebSocket reference can change across reconnects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Buffer incoming WebSocket data while the user is selecting text
(mousedown→mouseup). Without this, terminal.write() clears the
xterm.js selection mid-drag or immediately on mouseup. The buffer
is flushed 150ms after mouseup so the selection stays visible long
enough to copy with Cmd+C.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Intercept keyboard shortcuts in xterm.js so users can copy selected
text with Cmd+C (Mac) or Ctrl+Shift+C (Linux/Win), and paste from
system clipboard with Cmd+V / Ctrl+Shift+V. Without text selected,
Cmd+C still sends SIGINT as expected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add WebSocket reconnection with exponential backoff in DirectTerminal
A single network hiccup permanently killed the terminal with no retry.
Now transient disconnects trigger automatic reconnection (1s→2s→4s…15s max)
while permanent errors (4001 auth, 4004 not-found) still fail immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: reset reconnection state when sessionId changes
Reset permanentErrorRef and reconnectAttemptRef at the start of the
useEffect so switching sessions doesn't inherit stale error/retry state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Wire the Dashboard component to the existing /api/events SSE endpoint
so session status, activity, and lastActivityAt update in real-time
without requiring a full page refresh. SSE snapshots are lightweight
patches merged into the full server-rendered session objects.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The workingSessions stat now includes idle/ready sessions (not just
active), so the UI label should say "working" rather than "active"
to accurately reflect what's being counted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the hardcoded `expectedHeight < 700` heuristic with a
frame-to-frame height comparison. The loop now exits as soon as the
container height stops changing (within 1px tolerance), which works
correctly on all screen sizes. Previously, on viewports taller than
~1140px the non-fullscreen height exceeded 700px so the condition was
never satisfied and the loop exhausted all attempts, introducing a
~1s delay on large screens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BUG-28: Verify no .tmp files left behind after writeMetadata/updateMetadata,
and rapid sequential writes produce correct final state
- BUG-29: Verify restoredAt roundtrips through writeMetadata/readMetadata,
persists in key=value file, and can be set via updateMetadata
- BUG-12: Verify computeStats counts active/idle/ready as working,
excludes exited and null activity
- BUG-13: Verify errored/needs_input/stuck sessions with activity=active
land in respond zone, not working
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use TextDecoder to properly decode UTF-8 from base64 clipboard data
instead of raw atob() which only handles Latin-1
- Track requestAnimationFrame IDs and cancel them on effect cleanup
to prevent callbacks firing on disposed terminal/WebSocket
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BUG-28: Use atomic write-then-rename for metadata files to prevent
race condition data loss between concurrent TS and bash writers
- BUG-29: Read and write restoredAt field in metadata so it persists
across server restarts
- BUG-12: Count all non-exited sessions (idle, ready, active) as
"working" instead of only active ones
- BUG-13: Check status-based error conditions (errored, needs_input,
stuck) before activity-based checks in getAttentionLevel() so errored
sessions immediately appear in the Respond Kanban zone
Closes#238
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Register OSC 52 handler in DirectTerminal to decode base64 clipboard
data from tmux and write it to the system clipboard
- Increase resize maxAttempts from 10 to 60 so fullscreen toggle works
on slow machines with CSS transitions >166ms
- Replace hardcoded h-[600px] in Terminal.tsx with responsive
max(440px, calc(100vh - 440px)) to prevent overflow on small screens
Closes#237
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add design research artifacts — briefs, token reference, screenshots
Comprehensive design research package for the ao dashboard, session
detail page, and orchestrator terminal. Produced via competitive analysis
of 14 products (Linear, Vercel, Railway, Fly.io, Inngest, WandB, LangSmith,
Supabase, and more) + Playwright CSS extraction from live sites + full
codebase audit.
Artifacts:
- docs/design/design-brief.md Main design brief (v2, Playwright-updated)
- docs/design/session-detail-design-brief.md /sessions/[id] design spec
- docs/design/orchestrator-terminal-design-brief.md Orchestrator page spec
- docs/design/token-reference.css Drop-in CSS replacement for globals.css
- docs/design/competitive-analysis-raw.md Raw research notes, all 14 sites
- docs/design/design-brief-v1.md Original text-only brief (pre-Playwright)
- docs/design/README.md Index + research methods summary
- docs/design/screenshots/linear-homepage.png Playwright-captured screenshot
- docs/design/screenshots/railway-homepage.png Playwright-captured screenshot
Key findings:
- Linear CSS token values verified via Playwright (body bg #08090A, accent
#7070FF, Berkeley Mono monospace, type scale, radius, transitions)
- Recommended palette: #0C0C11 base (blue-cast dark vs current GitHub #0d1117)
- Highest-impact change: load Inter Variable via next/font/google
- Orchestrator terminal needs visual differentiation (violet accent, status strip)
- token-reference.css is ready to drop into packages/web/src/app/globals.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(web): redesign dashboard, session detail, and orchestrator terminal
Implements a cohesive dense dark-mode design system across all three main views.
- New color token palette: #0c0c11 base, #141419 surface, #1c1c25 elevated
- Accent blue #5b7ef8, status semantics (ready/error/attention/working/idle/done)
- Violet accent #a371f7 reserved for orchestrator
- Inter Variable + JetBrains Mono loaded via next/font with CSS variables
- activity-pulse keyframe for live agent dots
- AttentionZone header: dot + label + flex divider + count pill + chevron
- Sessions laid out in responsive 1→2→3 column grid
- Solid green merge button (translateY hover), no confirm() dialog
- Breadcrumb nav: ← Agent Orchestrator / {session-id} [orchestrator badge]
- CSS 8×8px activity dot with pulse animation replaces emoji labels
- Merge-ready state: green-bordered banner with checkmark icon
- Orchestrator sessions show zone counts strip (merge/respond/review counts)
- xterm.js dark theme (#0a0a0f bg, #d4d4d8 fg, full 16-color ANSI palette)
- variant prop: "agent" (blue cursor) vs "orchestrator" (violet cursor)
- Dynamic height prop instead of fixed 600px; fullscreen toggle with SVG icons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(web): strip rainbow stats, clean header, IBM Plex Sans typography
- Replace Inter with IBM Plex Sans (technical tool aesthetic, distinctive numerics)
- Replace 4-color big-number stats bar with a single compact inline status line
in the header: "35 sessions · 1 working · 9 PRs" — no decorative colors
- Remove the two-tone "Agent (blue) Orchestrator (white)" title — just "Orchestrator"
- Remove ClientTimestamp (useless) — replaced by orchestrator nav link
- Zone headers: colored dot only (semantic), neutral uppercase label, plain count
— removes the rainbow-colored label text that read as a widget template
- Add subtle radial gradient glow at top of page for depth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(web): kanban layout, amber accent, full-width, bigger stats
- Switch accent from blue (#5b7ef8) to amber/gold (#d18616) throughout
- Replace grid layout with horizontal Kanban columns for active zones
(merge, respond, review, pending, working), Done stays full-width below
- Remove max-w-[1100px] constraint — full viewport width
- Header stats numbers 20px bold (was 12px), orchestrator link is now a
visible bordered button
- AttentionZone gains variant="column" for Kanban mode (compact header
with count pill, vertical card stack)
- Update all hardcoded rgba(91,126,248,...) in SessionCard to amber
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): layout, alert sizing, column order, button feel
- Kanban column order: working→pending→review→respond→merge
(left = in progress, right = ready to ship)
- Columns use flex-1 min-w-[200px] to fill available width
instead of fixed 260px leaving half the page empty
- Alert badges: inline-flex wrapper prevents stretching to full
row width when wrapping
- Terminal button: add bg-subtle fill so it reads as a button
- PR number (#91): remove opaque pill background, now plain
amber text link — clearly a hyperlink
- Merge PR button: pt-0.5 spacer above the action area
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): don't cache rate-limited partial PR data
When GitHub rate limits fire, enrichSessionPR was caching the
bad partial data (0 additions, CI failing) for 60 seconds, causing
the dashboard to show incorrect data for the full TTL window.
- Skip cache write when majority of API calls failed
- Downgrade console.error → console.warn (this is handled/expected)
The next page refresh will retry live API calls, so data recovers
as soon as the rate limit window resets.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(web): graceful GitHub API rate limit handling in UI
When the GitHub plugin hits rate limits, the dashboard now:
- Shows a single amber banner: "GitHub API rate limited — PR data
(CI status, review state, sizes) may be stale. Will retry on
next refresh."
- Hides CI badge, review decision, and size pill on PR cards
(they'd show wrong values: +0 -0 XS, CI failing)
- Shows a subtle "⚠ PR data rate limited" note on affected cards
instead of misleading alert badges
- Skips CI/review/conflict-based attention zone classification
for rate-limited PRs (prevents sessions moving to Review due
to phantom "CI failing" from the fallback value)
- Doesn't cache partial rate-limited data so next refresh retries
live API calls as soon as the rate limit window resets
What still works when rate limited:
- Session ID, title, branch, PR number/link
- Session activity status (working/spawning/etc.)
- Merge button if mergeability was already cached
- Restore/terminate/send actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(web): dashboard redesign — glassmorphism, Kanban, rate-limit handling, perf fix
Design:
- Kanban layout: active zones as flex columns (working→pending→review→respond→merge),
Done as full-width grid below
- GitHub dark color palette (main's tokens) with glassmorphic card surfaces
(rgba bg + backdrop-blur) and subtle blue/violet body gradient
- Activity state shown as labeled pill (● active / ● idle etc.) instead of bare dot
- Session card: title on its own row, larger font, inline-flex alert badges (no stretch)
- PR number rendered as plain accent link, not a blue pill badge
- Terminal button has background fill to feel like a button
- Info circle icon replaces alarming warning triangle for rate-limit indicators
- "1 working" → "1 active" in header stats
- PR table constrained to max-w-[900px] and centered
- Orchestrator session no longer uses purple accent
Rate limiting:
- isPRRateLimited() helper; getAttentionLevel() skips PR classification when limited
- Rate-limited banner in Dashboard; suppressed CI/size/review badges in PRStatus
- SessionCard shows subtle "PR data rate limited" indicator; getAlerts() returns []
- serialize.ts: rate-limited enrichment results cached for 5 min (not 60s) to stop
retrying 168 failing API calls every minute
Performance:
- page.tsx: 4s hard timeout on PR enrichment — serves stale data fast instead of
blocking SSR for 75s under rate limiting
- cache.ts: TTLCache.set() accepts optional ttl override for per-entry control
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: suppress stale size/CI/review in PR table when rate limited
PRTableRow now shows "—" for size, CI, and review columns when GitHub
API is rate limited, matching the card view which already hides these.
Prevents misleading "+0 -0 XS" size and "needs review" labels from the
default fallback values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: 3D card effect with depth shadow and top-edge shine
Cards now clearly pop against the dark background:
- Solid gradient bg (rgba(28,36,47) → rgba(18,23,31)) instead of
near-invisible rgba(22,27,34,0.8) surface
- Layered box-shadow: contact shadow + diffuse depth + inset top highlight
that simulates light hitting the card's top edge (the "shine")
- Hover: card lifts 2px with deeper shadow
- Merge-ready: green-tinted bg with green ambient glow + stronger lift on hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: restore text legibility inside session cards
The darker solid card gradient made muted/secondary text nearly
invisible — #484f58 (text-muted) had only ~2:1 contrast on the
new card bg. Override the color tokens locally within .session-card
to GitHub's established dark-mode legibility values:
--color-text-muted: #484f58 → #656d76 (3.8:1 on card bg)
--color-text-secondary: #7d8590 → #8b949e (6.2:1 on card bg)
--color-text-tertiary: #484f58 → #656d76
Scoped to .session-card so the rest of the UI is unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address bugbot comments — fonts, review zone, ActivityDot, orchestrator btn
- layout.tsx: add IBM Plex Sans weight 700 (was missing, font-bold falling
back to 600)
- DirectTerminal.tsx: use "IBM Plex Mono" instead of unloaded "JetBrains Mono"
- SessionDetail.tsx: add review zone to OrchestratorStatusStrip (was omitted,
sessions with CI failures were invisible in the strip)
- ActivityDot.tsx: extract shared component, remove duplicate implementations
in SessionCard.tsx and SessionDetail.tsx
- Dashboard.tsx: redesign orchestrator button with 3D glass style matching
card aesthetic (blue-tinted bg, depth shadow, hover lift)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): lint — eqeqeq, duplicate import, unused var
- ActivityDot.tsx: != → !== (eqeqeq rule)
- PRStatus.tsx: merge duplicate @/lib/types imports into one
- SessionCard.tsx: remove unused activityIcon import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(web): elevate session detail + orchestrator page design
- Nav: glass backdrop-blur effect with chevron back link
- Header: detail-card 3D treatment with left-border accent keyed to activity color
- Meta chips: bordered pill style with subtle bg instead of flat text
- Status tag: pill badge for status instead of plain text
- PR card: detail-card 3D treatment, border-color reflects PR state
- PR merged badge: purple pill instead of gray text
- Unresolved count: red pill badge in section header
- Blockers section: renamed "Issues" → "Blockers"
- Terminal section: colored bar indicator instead of plain label
- Orchestrator status strip: total agent count + per-zone colored pills
- globals.css: add .nav-glass and .detail-card classes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): fetchZoneCounts parses body.sessions, delayed 2s to avoid contention
The /api/sessions endpoint returns `{ sessions: [...] }` not a bare array.
fetchZoneCounts was treating the whole response object as an array, so
zone counts were always zero on the orchestrator detail page.
Also delays the initial fetchZoneCounts call by 2s so it doesn't contend
with the session fetch on page load (both hit /api/sessions which is slow
when GitHub enrichment is running).
Also includes: Playwright kill-Chrome-for-Testing tip in CLAUDE.md,
toned-down detail-card shadow in globals.css.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf+test(web): cache-first PR enrichment, skip exited sessions, fix component tests
Performance improvements:
- enrichSessionPR() now accepts cacheOnly option and returns boolean
- /api/sessions/[id]: serve from cache immediately, only block on first load
- /api/sessions: skip PR enrichment for EXITED sessions (no longer changing)
- cache: increase default TTL from 60s to 5 minutes
Test fixes (match redesigned SessionCard + AttentionZone):
- "restore session" (header) → "restore"; expanded panel still shows "restore session"
- "merge PR #N" → "Merge PR #N" (capital M)
- "CI status unknown" → "CI unknown"
- "ask to fix CI" / "ask to fix CI" → "ask to fix"
- "terminate session" → "terminate"
- Zone labels: RESPOND/WORKING/DONE → Respond/Working/Done (CSS uppercase is visual only)
- "working" zone no longer collapsed by default; collapse tests now use "done" zone
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(core): ActivityDetection with timestamp propagation
- Add ActivityDetection interface { state, timestamp? } to types.ts
- Agent getActivityState() returns ActivityDetection | null instead of
ActivityState | null, allowing timestamp from JSONL mtime to propagate
- session-manager updates session.lastActivityAt when detected.timestamp
is more recent — fixes "active 22h ago" showing stale timestamps
- Update all agent plugins (claude-code, aider, codex, opencode) to
return ActivityDetection objects
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): dismissible rate limit banner + 60min rate-limit cache TTL
- Add X dismiss button to GitHub API rate limit banner in Dashboard.tsx
so it can be closed during demos
- Extend rate-limited PR cache TTL from 5min to 60min — GitHub GraphQL
rate limits reset hourly, no point retrying every 5 minutes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): address Cursor Bugbot review comments on PR #125
- Dashboard StatusLine: active sessions count now uses var(--color-status-working)
(blue) instead of neutral text color, matching the design system semantics
- SessionCard: isReadyToMerge now guards against rate-limited state — a card
with stale cached mergeability data won't show green merge-ready styling
- DirectTerminal: add `variant` to useEffect dependency array (was missing,
causing stale cursor/selection colors if variant changed after mount)
- agent-aider: include `timestamp: chatMtime` in all ActivityDetection returns,
matching the pattern used by agent-claude-code (enables accurate lastActivityAt)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): resolve lint, typecheck, and test failures
Lint:
- Remove unused parseJsonlFile function (superseded by parseJsonlFileTail)
- Remove dead lastLogModified stat() call in getSessionInfo (field was
removed from AgentSessionInfo but the filesystem read was left behind)
Typecheck + Tests (ActivityDetection):
- session-manager.test.ts: update mocks to return { state: "active" } /
{ state: "idle" } instead of bare strings — getActivityState() returns
ActivityDetection | null, not ActivityState | null
- integration tests (aider, claude-code, codex, opencode): update imports
from ActivityState → ActivityDetection, variable types, comparisons
(activityState?.state !== "exited"), and assertions (?.state).toBe()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: parseJsonlFileTail uses readFile for small files; enrich exited sessions with PRs
- parseJsonlFileTail now calls stat() then readFile() for files smaller than
maxBytes, falling back to open()/handle.read() only for large files. This
fixes the test infrastructure (which mocks readFile but not open) and also
fixes a scope bug where `offset` was declared inside an inner try block but
referenced outside both try blocks.
- Math.max(0, NaN) returns NaN not 0, so size must default to 0 when stat
returns a mock without a size field: `const { size = 0 } = await stat(...)`.
- Update activity-detection.test.ts: getActivityState() now returns
ActivityDetection objects, so tests use (await ...)?.state comparisons.
- Remove stale lastLogModified test (field was removed from AgentSessionInfo).
- Remove EXITED skip guard from api/sessions/route.ts: exited sessions can
still have open, merge-ready PRs that need enrichment on the dashboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: comprehensive code review fixes — tests, timestamps, UI correctness
Address gaps identified in code review of the ActivityDetection PR:
Core / Session Manager:
- Add `timestamp` to all `{ state: "exited" }` returns in all 4 agent plugins
(claude-code, aider, codex, opencode) using consistent `exitedAt = new Date()` pattern
- Add 2 new session-manager tests: timestamp propagation when detection timestamp
is newer, and no-downgrade when detection timestamp is older
- Fix `parseJsonlFileTail` lint error: remove useless `= 0` initializer (value was
always overwritten before use; catch block returns early)
Web package — tests:
- Fix 3 `api-routes.test.ts` failures: `sessionsGET()` needs a Request object since
the route reads `request.url` for `?active=true` query param
- Fix `serialize.test.ts` rate-limit test: spy on `console.warn` (what the code uses)
not `console.error`
- Add 5 `ActivityDot` component tests covering all activity states, unknown states,
null activity, and dotOnly mode
Web package — UI correctness:
- Fix `relativeTime()` in SessionDetail to guard against invalid/empty ISO strings
- Fix timer Map leak: add `timersRef.current.clear()` in cleanup effect after forEach
- Add `encodeURIComponent` to sessionId in message fetch URL
Server — race condition fix:
- Guard `activeSessions.delete` in pty.onExit, ws.on("close"), and ws.on("error")
against stale handlers deleting a newly-registered session with the same ID.
Fixes flaky integration test where afterEach's pty.kill() fired asynchronously
after the next test had already set up a new session with the same session ID.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): narrow PREnrichmentData types to eliminate unsafe casts in serialize
PREnrichmentData.ciStatus and .reviewDecision were typed as string,
requiring unsafe `as` casts when reading from cache into DashboardPR.
Narrow them to the same literal union types used by DashboardPR, making
the casts unnecessary. Also narrow ciChecks[].status to match CoreCICheck.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: session title fallback chain — PR title → summary → issue title → branch
Sessions without PRs now always show a meaningful title on the dashboard
instead of just the status text. The fallback chain is:
1. PR title (already worked)
2. Agent summary (now fetched from JSONL via getSessionInfo())
3. Issue title (now fetched via tracker.getIssue())
4. Humanized branch name (e.g., "feat/infer-project-id" → "Infer Project ID")
Key changes:
- Enrich agent summaries by calling getSessionInfo() for sessions
without summaries (local file I/O, not API calls)
- Enrich issue titles via tracker.getIssue() with 5-min TTL cache
- Add humanizeBranch() utility for last-resort branch name display
- Add issueTitle field to DashboardSession type
- Show issue title in expanded detail panel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: extract humanizeBranch to separate module to avoid client-side timer leaks
Moves humanizeBranch() from serialize.ts to format.ts — a pure utility
module with no side effects. This prevents the client bundle from pulling
in TTLCache instantiations (which create setInterval timers) when
SessionCard.tsx imports the function.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove dead re-export of humanizeBranch from serialize.ts
No consumer imports humanizeBranch from serialize — SessionCard imports
directly from format.ts. The re-export was unused surface area.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add missing first-project fallback in summary enrichment block
Matches the pattern used by all other enrichment blocks in page.tsx
(issue labels, issue titles, PR enrichment) which fall back to the
first configured project when projectId and sessionPrefix both miss.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smarter title heuristic — skip prompt excerpts, prefer issue titles
The agent summary fallback from extractSummary() often returns truncated
spawn prompts ("You are working on GitHub issue #42: Add auth...") which
make poor titles. The new heuristic detects these prompt excerpts and
prefers the issue title when available.
Updated fallback chain:
PR title → quality summary → issue title → any summary → humanized branch → status
Changes:
- Add looksLikePromptExcerpt() to detect spawn prompt patterns
- Add getSessionTitle() to encapsulate the smart fallback logic
- Expand humanizeBranch() with more prefix patterns (release, hotfix, etc.)
- SessionCard now uses getSessionTitle() instead of inline ?? chain
- Add 25 unit tests covering all functions and edge cases
- Fix missing issueTitle field in serialize.test.ts fixture
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract shared resolveProject() to eliminate duplication
Moves resolveProject() from route.ts into serialize.ts as a shared
export. Both page.tsx and route.ts now use the same function instead
of duplicating the 3-step project resolution logic (projectId →
sessionPrefix → first project fallback) inline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: replace looksLikePromptExcerpt heuristic with summaryIsFallback metadata
Instead of fragile string matching to detect truncated spawn prompts,
the agent plugin now sets summaryIsFallback: true when the summary is
a first-message fallback rather than a real agent-generated summary.
- Add summaryIsFallback to AgentSessionInfo (core/types.ts)
- extractSummary() returns { summary, isFallback } in claude-code plugin
- Add summaryIsFallback to DashboardSession, propagate in serialize.ts
- Replace looksLikePromptExcerpt() with !session.summaryIsFallback
- Fix .js extension in format.ts import (review feedback)
- Add thorough tests for all layers of propagation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add resolveProject and enrichSessionIssueTitle coverage
- resolveProject: 5 tests covering direct match, prefix fallback,
first-project fallback, empty projects, and priority ordering
- enrichSessionIssueTitle: 7 tests covering enrichment, # prefix
stripping, Linear-style labels, skip conditions, error handling,
and cross-call caching
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: extract shared enrichSessionsMetadata, fix session detail route
- Extract duplicated enrichment orchestration (issue labels, agent
summaries, issue titles) from page.tsx and route.ts into a single
enrichSessionsMetadata() function in serialize.ts
- Fix /api/sessions/[id] route: was missing agent summary and issue
title enrichment, and had hand-rolled project resolution instead of
using resolveProject() (also missing the first-project fallback)
- Optimize: resolve projects once per session instead of 3x
- Add 8 tests for enrichSessionsMetadata covering full pipeline, skip
conditions, missing plugins, no-tracker config, multiple sessions,
and default agent fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove dead getAgent and getTracker exports from services.ts
These helpers became unused when enrichSessionsMetadata was extracted
to serialize.ts with inline registry.get() calls (to avoid coupling
serialize.ts to services.ts and pulling plugin packages into webpack).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Guard against empty project name in getProjectName() and icon.tsx
(falls back to config key or "A" initial)
- Extract branch truncation into shared `truncate()` helper in
session detail page
- Addresses bugbot comments from PR #111
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: dynamic browser tab titles and health-aware favicons
Tabs now show contextual titles so multiple dashboard instances are
distinguishable at a glance. Favicons reflect system health (green/
yellow/red) and display the project initial for visual identification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review — dedupe project name, handle merge level, add activity emoji
- Extract getProjectName() to shared lib/project-name.ts (used by
layout, page, icon) — fixes bugbot duplication comment
- computeHealth now treats "merge" attention level as yellow (needs
human action) instead of silently mapping to green — fixes bugbot
merge-ignored comment
- Add activity status emoji to session tab titles (⚡🟢💤❓🚧💀)
that updates live as session state changes
- Special-case orchestrator sessions: "ao-orchestrator | Orchestrator Terminal"
- Extract activityIcon map to shared lib/activity-icons.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use absolute title to avoid layout template duplication
The layout template `%s | project` was wrapping the page title
`project | Agent Orchestrator`, producing `project | Agent Orchestrator | project`.
Use `title.absolute` to opt out of the template on the root page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use "ao | <project>" format for dashboard title
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: dedupe config reads with React.cache() on getProjectName
Wraps getProjectName with React.cache() so layout, page, and icon
share a single loadConfig() call per server render pass instead of
reading the YAML file three times.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: make terminal server ports configurable to fix multi-dashboard EADDRINUSE
When multiple ao dashboards run simultaneously (e.g., ao on port 3000,
integrator on port 3002), both try to start terminal WebSocket servers
on hardcoded ports 3001/3003, causing EADDRINUSE. Add terminalPort and
directTerminalPort to config schema so each instance can use unique ports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use optional() instead of default() for terminal port schema
Zod .default() always fills in the value, making config.terminalPort
never undefined and the env var fallback in buildDashboardEnv dead code.
Switch to .optional() so the priority chain works correctly:
config value > TERMINAL_PORT env var > hardcoded default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: move terminal server defaults from 3001/3003 to 14800/14801
The 3000-3009 range is the most contested in dev tooling (Next.js
auto-increments, BrowserSync, Grafana, Rails, Express all default to
3000+). Port 14800-14899 has zero IANA registrations, zero known dev
tool conflicts, and is safely below OS ephemeral ranges.
Updated all hardcoded fallbacks, .env.local.example, docker-compose
port mappings, and documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: auto-detect available terminal ports for zero-config multi-dashboard
When no terminal ports are configured (no config, no env vars),
buildDashboardEnv now probes for an available port pair starting at
14800. The second `ao start` automatically gets 14802/14803 (or the
next free pair), eliminating EADDRINUSE without any user configuration.
Port detection scans in steps of 2 to keep the pair consecutive.
Explicit config/env values bypass auto-detection entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update onboarding test to use new default terminal port (14801)
The onboarding integration test had port 3003 hardcoded for the
WebSocket health check. Updated to read from DIRECT_TERMINAL_PORT
env var with 14801 as the default, matching the new port defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: implement session restore for crashed/exited agents
Add true in-place session restore: same session ID, same worktree, same
metadata — optionally resuming the Claude Code conversation via --resume.
Core changes:
- Add TERMINAL_STATUSES, TERMINAL_ACTIVITIES, NON_RESTORABLE_STATUSES sets
and isTerminalSession/isRestorable helpers to types.ts
- Add SessionNotRestorableError and WorkspaceMissingError error classes
- Add restore() to SessionManager with 9-step flow: find metadata →
validate restorability → check/recreate workspace → get restore or
launch command → create runtime → update metadata
- Add restoredAt field to Session and SessionMetadata
Plugin extensions:
- workspace-worktree: exists() + restore() (git worktree prune + re-add)
- workspace-clone: exists() + restore() (git clone + checkout)
- scm-github: branchExists() via git rev-parse
- agent-claude-code: getRestoreCommand() finds latest JSONL session file
and builds claude --resume command
CLI + Web:
- Add `ao session restore <id>` subcommand
- Web restore API route uses sessionManager.restore() instead of spawn()
- SessionCard uses centralized TERMINAL_STATUSES/TERMINAL_ACTIVITIES
- Web types re-export core constants with sync tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add "merged" to TERMINAL_STATUSES
The old inline isTerminal check included "merged" but when refactored
to use the TERMINAL_STATUSES set, "merged" was omitted. This caused
merged sessions (whose activity is not "exited") to incorrectly show
the "terminal" link and "terminate session" button.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: enrich runtime state before restore check, remove dead branchExists
- Add enrichSessionWithRuntimeState() call before isRestorable() in
restore() so crashed sessions (status "working", agent exited) are
correctly detected as terminal and eligible for restore.
- Remove dead branchExists from SCM interface and scm-github plugin
(defined but never called anywhere in the codebase).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: allow restore of crashed working sessions
Remove "working" from NON_RESTORABLE_STATUSES. The isTerminalSession()
gate already prevents restoring truly active sessions (activity is not
"exited"). This fix allows crashed agents (status "working", activity
"exited") to be restored, aligning core behavior with the UI which
already shows the restore button for this case.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: distinguish missing branch from missing restore support
Split the compound condition so workspace restore gives an accurate
error message when branch metadata is null ("branch metadata is
missing") vs when the workspace plugin lacks a restore method.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>