Commit Graph

62 Commits

Author SHA1 Message Date
Harsh Batheja b3ff0d9b85 feat(web): Project-scoped dashboard with sidebar navigation (#381)
* 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
2026-03-11 06:41:04 +00:00
Harsh Batheja f48c939d9b feat: lifecycle manager, backlog auto-claim, task decomposition, and verification gate (#365)
* 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>
2026-03-11 06:41:03 +00:00
Harsh Batheja ffae671884 fix: pause workers on model limits and stabilize session visibility (#367)
* 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)
2026-03-11 06:41:03 +00:00
Harsh Batheja 4e2144d99e
feat: OpenCode session lifecycle and CLI controls (#315)
* feat: refine OpenCode session reuse strategy and cleanup

* fix: harden OpenCode session selection and lint errors

* refactor: centralize OpenCode reuse resolution flow

* fix: return 404 for missing session in message route

* feat: replace force remap with terminal reload control

* fix: protect project path from session kill cleanup

* fix: preserve fullscreen alignment without reload action

* fix: harden OpenCode session id handling and title reuse selection

* fix: show OpenCode reload control and remap before restart

* fix: preserve title-only OpenCode reuse with fallback mapping persistence

* fix: resolve remaining PR315 Bugbot findings

* fix: keep OpenCode discovery title-based without timestamp sorting

* fix: avoid enrichment race fallout in session listing

* fix: guard OpenCode discovery parse with array check

* fix: stabilize Linear comment integration check

* fix: harden OpenCode discovery and prompt option flow

* ux: clarify OpenCode terminal restart action

* fix: remap OpenCode session fresh on each restart

* fix: validate remap session ids before reuse

* fix: clean archived metadata only after purge

* docs: align OpenCode remap selection with title-based behavior

* fix: harden opencode cleanup and ignore local sisyphus state

* test: add timeout cleanup coverage for session enrichment

* fix: harden Linear integration helper against transient non-JSON errors

* fix: make linear integration assertions resilient to eventual consistency

* fix: remove unused fs import after rebase

* fix: address remaining Bugbot blockers for opencode session handling

* fix: avoid stale metadata overwrite during restore post-launch

* fix: forward subagent in orchestrator flows and defer reuse lookup

* fix: apply configured subagent fallback for session spawn

* fix: scope archived cleanup by project and delay archive restore write

* fix: normalize orchestrator strategy aliases in start display logic

* fix: centralize orchestrator strategy normalization in core

* fix: derive orchestrator reuse display from spawn result

* fix: keep cleanup results consistent across project-id collisions

* fix: namespace cleanup results when session IDs collide

* fix: harden GitHub issue stateReason fallback

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: avoid false failing CI state mapping

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: add tmux command timeouts

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: bound session API enrichment latency

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: repair scm-github merge resolution

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: repair lifecycle-manager test merge

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: delay archive metadata recreation until restore passes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test: restore claim-pr session mocks

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: address review findings for OpenCode lifecycle PR

- Fix stripControlChars to preserve newlines for reload commands
- Add SessionNotFoundError and use instanceof checks in API routes
- Document orchestratorSessionStrategy in YAML example
- Validate existingSessionId with asValidOpenCodeSessionId()
- Extract inline Node script to buildSessionLookupScript helper
- Create OpenCodeSessionManager interface for remap capability
- Create OpenCodeAgentConfig type for agent-specific config
- Change default orchestratorSessionStrategy from delete to reuse

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: guard reused session display without metadata

* chore: add agent config files to .gitignore

Agent configuration files (CLAUDE.md, AGENTS.md, IMPROVEMENTS.md, etc.) are personal and project-specific. They should not be committed to the repository.

Changes:
- Remove CLAUDE.md from git tracking
- Add agent config files to .gitignore
- Create .gitignore-template for reference

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: update gitignore for agent config folder structure

Reorganized agent configuration files:
- CLAUDE.md and AGENTS.md stay in root (agents read them there)
- Tracking files move to .opencode/ (IMPROVEMENTS.md, etc.)
- Optional Claude files in .claude/

Updated .gitignore to ignore folders instead of individual files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: tighten opencode remap and session discovery safeguards

* fix: address Bugbot findings in session manager

* fix: scope opencode discovery to opencode sessions

* fix: restore concurrent listing and strict permission literals

* fix: throw SessionNotFoundError, parallelize list enrichment, fix permissions type

- Session manager now throws SessionNotFoundError instead of plain Error
  for missing sessions, so web API routes correctly return 404 (not 500)
- Parallelize session enrichment in list() — was sequential, causing O(N)
  latency for N sessions with subprocess enrichment
- Fix AgentLaunchConfig.permissions type to accept legacy "skip" value
  (AgentPermissionInput instead of AgentPermissionMode)
- Add happy-path and validation tests for /api/sessions/:id/message route
- Update all test mocks to use SessionNotFoundError

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: route ao send through session manager

* feat: add purge option to orchestrator stop

* fix: register opencode agent in web services

* test: align web API missing-session coverage

* fix: match notifier config by plugin name

* refactor: dedupe session lookup and tmux buffer send flow

* test: update send lifecycle wait expectation

* fix: harden send routing and cleanup purge controls

---------

Co-authored-by: Harsh <harsh@Ubuntu-24-Forrest.lan>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Prateek <karnalprateek@gmail.com>
2026-03-08 09:55:44 +05:30
Prateek 06011be6f0 test(web): cover direct terminal websocket URL selection 2026-03-08 04:21:41 +05:30
Prateek 40055eb0d1 fix(web): preserve host port for proxy websocket path 2026-03-08 04:16:04 +05:30
Prateek 309bfbbc19 fix: DirectTerminal WebSocket URL behind reverse proxy
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)
2026-03-07 20:28:00 +05:30
prateek e503caeaa3
fix: don't show 'Ready to merge' badge on merged/closed PRs (#274)
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>
2026-03-03 14:46:54 +05:30
suraj_markup d1bf5a5b03
Merge pull request #266 from suraj-markup/feat/issue-265
feat(mobile): React Native app for monitoring AO sessions
2026-03-03 05:21:20 +05:30
suraj-markup 53142119bd fix(mobile,web): expose orchestratorId in /api/sessions response
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>
2026-03-03 04:13:58 +05:30
suraj_markup 1d7e6e3c66
Merge pull request #240 from suraj-markup/feat/issue-237
fix(terminal): OSC 52 clipboard, resize frame cap, and hardcoded height
2026-03-02 02:27:39 +05:30
suraj_markup 2d785beac5
Merge pull request #242 from suraj-markup/feat/issue-238
fix(core): atomic metadata writes, restoredAt persistence, and session count/sorting bugs
2026-03-02 02:24:51 +05:30
suraj-markup 421e443d30 fix(terminal): remove DOM paste handler that could double-paste with xterm.js
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>
2026-03-02 01:56:02 +05:30
suraj-markup 3fc51b8882 fix(terminal): harden clipboard buffer and resize for production
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>
2026-03-02 01:17:16 +05:30
suraj-markup 09e2c2b540 fix(terminal): preserve selection highlight by buffering after selection exists
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>
2026-03-02 01:08:24 +05:30
suraj-markup fb5e0c9646 fix(terminal): remove write buffer that broke selection highlighting
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>
2026-03-02 01:04:11 +05:30
suraj-markup 2d80a1dd35 fix(terminal): preserve text selection while terminal receives output
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>
2026-03-02 00:58:49 +05:30
suraj-markup d8bbf860cd fix(terminal): auto-copy selection and use DOM paste event for clipboard
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>
2026-03-02 00:49:19 +05:30
suraj-markup 941a99e0c9 fix: resolve merge conflict with upstream WebSocket reconnection
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>
2026-03-02 00:41:13 +05:30
suraj-markup 54c0a075bc Revert "fix(terminal): preserve text selection while terminal receives output"
This reverts commit 3ac26ba6e8.
2026-03-02 00:35:55 +05:30
suraj-markup 3ac26ba6e8 fix(terminal): preserve text selection while terminal receives output
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>
2026-03-02 00:13:34 +05:30
suraj-markup 783b21dc93 feat(terminal): add Cmd+C/V and Ctrl+Shift+C/V clipboard shortcuts
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>
2026-03-02 00:03:14 +05:30
prateek 3cb03f6222
fix: WebSocket reconnection in DirectTerminal (#248)
* 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>
2026-03-01 20:18:07 +05:30
prateek 2108620ee9
feat(web): subscribe Dashboard to SSE for real-time session updates (#249)
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>
2026-03-01 20:04:57 +05:30
suraj-markup 899e47784f fix(web): rename stats label from "active" to "working" to match broadened filter
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>
2026-03-01 19:22:47 +05:30
suraj-markup b96eafeb31 fix(terminal): use height stabilisation instead of fixed threshold for resize
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>
2026-03-01 19:22:44 +05:30
suraj-markup b894b31a5a test: add tests for atomic writes, restoredAt, computeStats, and attention level fixes
- 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>
2026-03-01 19:10:57 +05:30
suraj-markup dc3b864a67 fix(terminal): UTF-8 safe OSC 52 decode, cancel RAF loop on cleanup
- 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>
2026-03-01 18:59:17 +05:30
suraj-markup 1f062201fb fix(core): atomic metadata writes, restoredAt persistence, and session count/sorting bugs
- 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>
2026-03-01 18:47:29 +05:30
suraj-markup 0814f9515c fix(terminal): wire OSC 52 clipboard, increase resize cap, fix hardcoded height
- 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>
2026-03-01 18:46:08 +05:30
prateek 40c1906d41
feat(web): redesign dashboard, session detail, and orchestrator terminal (#125)
* 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>
2026-02-20 18:43:57 +05:30
prateek 0e2ca70b0b
feat: session title fallback chain for PR-less sessions (#105)
* 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>
2026-02-19 19:02:02 +05:30
prateek 0e533840ba
fix: tab title followups — empty name guard, dedupe truncation (#121)
- 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>
2026-02-19 07:12:32 +05:30
prateek b605ee8ed4
feat: dynamic tab titles and health-aware favicons (#111)
* 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>
2026-02-19 04:21:49 +05:30
prateek 520010d5a2
feat: configurable terminal server ports for multi-dashboard support (#113)
* 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>
2026-02-19 04:00:19 +05:30
prateek 65fa811b3b
feat: implement session restore for crashed/exited agents (#104)
* 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>
2026-02-19 01:12:57 +05:30
prateek de6653e258
feat: first-class orchestrator session + file-based system prompt (#101)
* feat: first-class orchestrator session + file-based system prompt

Make the orchestrator a first-class managed session that flows through
the same SessionManager pipeline as worker sessions, and fix a blocking
bug where long system prompts get truncated by tmux/zsh.

Changes:
- Add OrchestratorSpawnConfig type and spawnOrchestrator() to
  SessionManager interface
- Implement spawnOrchestrator() in session-manager.ts: proper
  hash-based tmuxName, runtimeHandle, plugin lifecycle — no workspace
  creation (uses project.path directly)
- Refactor `ao start` to use SessionManager.spawnOrchestrator()
  instead of manual tmux calls + metadata writes (~80 lines removed)
- Refactor `ao stop` to use SessionManager.kill() instead of manual
  tmux kill + metadata delete
- Update `ao init` next steps: guide users to `ao start` before
  `ao spawn`
- Add systemPromptFile to AgentLaunchConfig for file-based system
  prompts (avoids tmux truncation of 2000+ char inline prompts)
- Update agent-claude-code, agent-codex, agent-aider plugins to use
  shell command substitution "$(cat '/path')" when systemPromptFile
  is set
- Update runtime-tmux create() to use load-buffer/paste-buffer for
  launch commands >200 chars
- Add 8 tests for spawnOrchestrator
- Fix SessionManager mock in 8 test files (add spawnOrchestrator)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use hash-based tmux name in orchestrator attach hint

The tmux attach hint after `ao start` printed the user-facing session
ID (e.g. app-orchestrator) instead of the hash-based tmux session name
(e.g. a3b4c5d6e7f8-app-orchestrator), causing "session not found"
errors. Now captures the runtimeHandle.id from spawnOrchestrator's
return value for the correct tmux target.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 19:32:35 +05:30
prateek 59c490a3af
fix: dashboard config discovery + CLI service layer refactoring (#70)
* fix: config discovery, activity detection, and metadata port storage

- findConfigFile() checks AO_CONFIG_PATH env var (resolved to absolute path)
- loadConfig() delegates to findConfigFile() for consistent validation
- Pure Node.js readLastJsonlEntry (no external tail binary), safe for
  multi-byte UTF-8 at chunk boundaries
- Added "ready" activity state to agent plugins
- Store dashboardPort, terminalWsPort, directTerminalWsPort in session
  metadata so ao stop targets the correct processes
- Zod schema port default aligned with TypeScript interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: dashboard config discovery + CLI service layer refactoring

- Config discovery via AO_CONFIG_PATH env var
- Auto port detection with PortManager
- Activity detection with ready state, pure Node.js readLastLine
- 5 CLI services: ConfigService, PortManager, DashboardManager, MetadataService, ProcessManager
- Store all service ports in metadata for ao stop
- Set NEXT_PUBLIC_ env vars for frontend terminal components
- Multi-byte UTF-8 safe readLastJsonlEntry
- Tests for all new services and utils

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address bugbot review comments (port fallback + systemPrompt)

1. Align port fallback to 3000 everywhere (matching Zod schema default):
   - start.ts: config.port ?? 3000
   - dashboard.ts: config.port ?? 3000
   - types.ts JSDoc: "defaults to 3000"
   - orchestrator-prompt.ts: already correct at 3000

2. Add --append-system-prompt to Claude Code plugin's getLaunchCommand
   so orchestrator context is actually passed to the Claude agent.
   Previously systemPrompt was generated but silently dropped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove dead ConfigService mock from status test

The vi.mock for ConfigService.js referenced a deleted module.
Config mocking is already handled by the @composio/ao-core mock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: extract shared buildDashboardEnv to eliminate duplication

Dashboard env construction (AO_CONFIG_PATH, PORT, NEXT_PUBLIC_*) was
duplicated between start.ts and dashboard.ts. Extracted into
buildDashboardEnv() in web-dir.ts (already shared by both commands).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 17:08:48 +05:30
prateek 73957182f7
fix: activity detection — fix path encoding bug, add ready state (#71)
* fix: activity detection — fix path encoding, use tail -1 for JSONL

Two tightly coupled infrastructure fixes:

- Fix toClaudeProjectPath(): leading `/` becomes `-` (not stripped),
  matching Claude Code's actual project directory naming convention.
- Replace manual 4KB buffer read in readLastJsonlEntry() with
  `tail -1` + JSON.parse — handles any file size, any line length,
  and eliminates the truncated-line edge case entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add "ready" state, return null when unknown, remove dead code

Behavioral changes to activity detection:

- Add "ready" to ActivityState — separates "alive at prompt" from
  "idle/stale". Configurable via readyThresholdMs (default 5 min).
- Agent plugins return null when they can't determine activity
  (no workspace, no JSONL, no per-session tracking). Session manager
  preserves existing activity instead of overwriting with a guess.
- Remove isProcessing() from Agent interface — zero callers in
  production code, fully superseded by getActivityState().
- Remove extractLastMessageType() from claude-code — the field it
  populated (lastMessageType) was only consumed by the old inline
  CLI mapping, which is now replaced by plugin delegation.
- CLI status delegates to agent.getActivityState() (single source
  of truth) with metadata fallback when plugin returns null.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: comprehensive activity detection coverage

- activity-detection.test.ts: 42+ tests covering path encoding,
  getActivityState edge cases (exited/null/fallback), real Claude Code
  JSONL types, agent interface spec types, staleness thresholds,
  JSONL file selection, and realistic session sequences.
- status.test.ts: plugin delegation tests — verifies CLI uses
  agent.getActivityState() as single source of truth, passes
  readyThresholdMs from config, falls back to metadata on null/throw.
- Integration tests: updated type expectations for null returns from
  codex, opencode, and aider; added "ready" to valid state lists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: flaky Linear integration test + missing ready label in SessionDetail

Linear API has eventual consistency — updateIssue state changes don't
propagate instantly. Poll with retries instead of asserting immediately.

Also adds "ready" entry to SessionDetail activityLabel map (was missing,
causing fallback to dim/unstyled rendering).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pure Node.js readLastJsonlEntry, use pollUntilEqual for Linear test

Replace `tail -1` with pure Node.js implementation that reads backwards
from end of file in 4KB chunks. No external binary dependency — works
on any platform.

Fix flaky Linear integration test by using the existing pollUntilEqual
helper instead of an inline retry loop. Linear API has eventual
consistency; pollUntilEqual retries for up to 5s with 500ms intervals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use tail -1 for readLastJsonlEntry, add real-data integration test

Replace over-engineered pure Node.js backward-reading implementation with
simple `tail -1` via execFile. The codebase already shells out to tmux,
git, and ps everywhere — tail is no different.

Add integration test that validates toClaudeProjectPath() and
readLastJsonlEntry() against real ~/.claude/projects/ data on disk.
No API key needed — just requires Claude to have been run once.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 03:48:19 +05:30
prateek dcfee04e1b
fix: terminal servers compatible with hash-based architecture (#87)
* fix: terminal servers compatible with hash-based architecture

The terminal WebSocket servers (direct-terminal-ws and terminal-websocket)
used config.dataDir to validate sessions, which no longer exists in the
hash-based architecture. Also fixed node-pty failing to find tmux via
posix_spawnp.

Changes:
- Remove config.dataDir dependency, validate via `tmux has-session` instead
- Add resolveTmuxSession() to map user-facing IDs (ao-15) to hash-prefixed
  tmux names (8474d6f29887-ao-15)
- Use explicit tmux path discovery (findTmux) since node-pty's posix_spawnp
  doesn't reliably inherit PATH
- Include /opt/homebrew/bin in fallback PATH for macOS ARM

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add session resolution to ttyd server and use exact tmux matching

- Add findTmux() and resolveTmuxSession() to terminal-websocket.ts
  (previously only in direct-terminal-ws.ts), fixing hash-prefixed
  session lookup for the ttyd-based terminal server
- Use tmux exact match prefix (=sessionId) in has-session checks
  to prevent ao-1 from matching ao-15 via prefix matching
- Add server compatibility tests that verify both servers handle
  hash-based architecture correctly (14 tests)
- Include server/ in tsconfig and vitest config for typecheck coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract tmux-utils and add proper unit tests

- Extract findTmux(), resolveTmuxSession(), validateSessionId() into
  shared server/tmux-utils.ts — eliminates duplication between
  direct-terminal-ws.ts and terminal-websocket.ts
- Add 20 real unit tests with injected mocks that test actual behavior:
  - findTmux: candidate priority, fallback to bare name
  - resolveTmuxSession: exact match, hash-prefix resolution,
    = prefix for preventing tmux prefix matching (ao-1 vs ao-15),
    null when no session found, tmux not running
  - validateSessionId: path traversal, shell injection, whitespace
- Slim down server-compatibility.test.ts to 10 structural checks
  (imports tmux-utils, no loadConfig, no config.dataDir, no existsSync)

Total: 30 tests — all pass on fix branch, 8 fail on main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add real integration tests for direct-terminal-ws

- Refactor direct-terminal-ws to export createDirectTerminalServer()
  factory so tests can control server lifecycle without side effects
- Add 10 integration tests that create real tmux sessions, start the
  real server, connect via WebSocket, and verify the full flow:
  - Health endpoint returns 200
  - Missing session parameter → close 1008
  - Path traversal in session ID → close 1008
  - Shell injection in session ID → close 1008
  - Nonexistent tmux session → close 1008
  - Real tmux session → connects and receives terminal output
  - Hash-prefixed session resolution works end-to-end
  - Can send input and receive echoed output
  - Resize messages don't crash the connection
  - Unknown HTTP path → 404
- Tests create/destroy tmux sessions in beforeAll/afterAll
- Server runs on random port (port 0) to avoid conflicts
- Total test suite: 40 tests (20 unit + 10 compatibility + 10 integration)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add web server tests to CI pipeline

The web package was explicitly excluded from CI test runs
(pnpm -r --filter '!@composio/ao-web' test). Add a test-web job
that installs tmux, starts the tmux server, and runs the web
package tests (unit + integration).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address CI failures and bugbot review comments

- Fix lint: replace require() with ESM import in integration test
- Fix base-path mismatch: ttyd now uses user-facing sessionId for
  --base-path/URL and actual tmux name for attach-session
- Fix bare "tmux" in ttyd spawn args: use TMUX constant (full path)
- Scope CI test-web job to server/__tests__/ to avoid pre-existing
  failures in src/__tests__/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: comprehensive unit and integration test coverage

Unit tests (77): validateSessionId covers all injection vectors (shell,
path traversal, command substitution, special chars, unicode, control
chars), findTmux covers all candidate paths and error types,
resolveTmuxSession covers exact match, hash-prefix resolution, suffix
matching precision, edge cases (single char, long lists, multiple
hyphens, different tmux paths).

Integration tests (45): health endpoint lifecycle (active count tracks
connections/disconnections), HTTP routing (404s for all non-health
paths), WebSocket validation (11 injection/traversal vectors), terminal
connection (resize, multi-resize, invalid JSON, non-resize JSON),
hash-prefixed resolution (suffix match, command passthrough, session key
tracking, cross-match prevention), terminal I/O (Ctrl-C, Tab, Enter,
empty messages, rapid keystrokes, multi-line), connection lifecycle
(cleanup, rapid connect/disconnect, error recovery), server creation
(independent instances).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: validate 12-char hex prefix in hash-prefixed session resolution

The previous endsWith("-{sessionId}") suffix match was ambiguous:
"hash-my-app-1" would falsely match a lookup for "app-1". Now validates
that the prefix matches the exact format generated by generateConfigHash
(12-char lowercase hex) before comparing the remainder.

Added unit tests for the ambiguity case and invalid prefix formats.
Updated integration test session names to use proper 12-char hex prefixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 03:28:55 +05:30
prateek 599710296d
fix: migrate to hash-based project isolation architecture
Complete migration to hash-based directory structure for project isolation. All bugbot issues resolved.
2026-02-18 00:19:55 +05:30
prateek eaea131af9
feat: seamless onboarding with enhanced documentation (#66)
* feat: implement seamless onboarding with enhanced documentation

- Add comprehensive README.md (18KB) with quick start, core concepts, and FAQ
- Add detailed SETUP.md (16.5KB) with prerequisites, integration guides, and troubleshooting
- Add examples/ directory with 5 ready-to-use config templates:
  - simple-github.yaml: Minimal GitHub setup
  - linear-team.yaml: Linear integration
  - multi-project.yaml: Multiple repos
  - auto-merge.yaml: Aggressive automation
  - codex-integration.yaml: Using Codex agent

- Add environment detection (git repo, remote, branch, auth status)
- Auto-fill prompts with smart defaults from detected environment
- Add prerequisite validation (git, tmux, gh CLI)
- Show actionable next steps and warnings
- Parse owner/repo from git remote automatically
- Detect LINEAR_API_KEY and SLACK_WEBHOOK_URL in environment
- Prompt for Linear team ID when Linear tracker selected

- Format all files with Prettier for consistency

Reduces onboarding time from 30+ minutes to ~5 minutes:
1. Install CLI: `npm install -g @composio/ao-cli`
2. Run init: `ao init` (auto-detects everything)
3. Spawn agent: `ao spawn my-project ISSUE-123`

Users no longer need to:
- Manually parse git remote URLs
- Look up current branch names
- Remember YAML syntax
- Search for Linear team IDs
- Debug missing prerequisites

-  pnpm build - All packages compile
-  pnpm typecheck - No TypeScript errors
-  pnpm lint - No new linting issues
-  pnpm format - All files formatted

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: update installation instructions to reflect npm not yet published

Package is not published to npm yet, so users must build from source.
Updated README.md and SETUP.md to:
- Make 'build from source' the primary installation method
- Add note that npm publishing is coming soon
- Include pnpm as a prerequisite

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: add ao init --auto --smart for zero-config setup

Implements intelligent config generation with project type detection.

## What's New

### ao init --auto
- Zero prompts - auto-generates config with smart defaults
- Detects: git repo, remote, branch, languages, frameworks, tools
- Generates project-specific agentRules based on detected tech stack

### Project Detection
- Languages: TypeScript, JavaScript, Python, Go, Rust
- Frameworks: React, Next.js, Vue, Express, FastAPI, Django, Flask
- Tools: pnpm workspaces, test frameworks
- Package managers: pnpm, yarn, npm

### Rule Templates
Created templates for:
- base.md - Universal best practices
- typescript.md - TS strict mode, ESM, type imports
- javascript.md - Modern ES6+ patterns
- react.md - Hooks, composition, best practices
- nextjs.md - App Router, Server Components
- python.md - Type hints, PEP 8
- go.md - Error handling, defer patterns
- pnpm-workspaces.md - Monorepo commands

### Example Output

```bash
ao init --auto

# Detects:
# ✓ TypeScript + pnpm workspaces
# ✓ React + Next.js
# ✓ Vitest

# Generates:
agentRules: |
  Always run tests before pushing.
  Use TypeScript strict mode.
  Use ESM modules with .js extensions.
  Use React best practices (hooks, composition).
  Before pushing: pnpm build && pnpm typecheck && pnpm lint && pnpm test
```

## Benefits

- **5 seconds** instead of 5 minutes
- **Zero config knowledge** required
- **Context-aware rules** tailored to your stack
- **Still customizable** - edit the generated config

## Future: --smart (AI-powered)

Flag added but not yet implemented. Will use Claude Code to:
- Analyze CLAUDE.md, CONTRIBUTING.md
- Read CI/CD config
- Generate custom rules based on project patterns

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: detect repo default branch instead of current branch

Fixes Bugbot issue: "Current branch wrongly suggested as default base branch"

## Problem

detectEnvironment was using `git branch --show-current` to suggest
defaultBranch in the config. If a user ran `ao init` while on a feature
branch like `feat/my-work`, the wizard would suggest that feature branch
as the default, causing agents to branch from the wrong base.

## Solution

Added detectDefaultBranch() function with 3 fallback methods:
1. git symbolic-ref refs/remotes/origin/HEAD (most reliable)
2. GitHub API via gh CLI (if ownerRepo known)
3. Check common branch names: main, master, next, develop

Now EnvironmentInfo tracks both:
- currentBranch: The checked-out branch (for display only)
- defaultBranch: The repo's base branch (for config)

## Testing

Tested on feat/seamless-onboarding branch:
- Current branch: feat/seamless-onboarding (displayed)
- Default branch: main (correctly detected for config)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: prevent duplicate framework detection in Python projects

Fixes Bugbot issue: "Duplicate frameworks when multiple Python config files exist"

## Problem

When both requirements.txt and pyproject.toml exist and mention the same
framework (e.g., FastAPI), the detection loop added it to the frameworks
array twice, causing duplicate rules in the generated config.

## Solution

Added addFramework() helper that checks if framework already exists before
adding to the array. Also prevents pytest from being set multiple times as
testFramework.

## Testing

Verified with test repo containing both files with FastAPI:
- Before: Would add 'fastapi' twice
- After: Only adds 'fastapi' once ✓

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address Bugbot review comments

- Remove redundant conditional in --smart flag (both branches were identical)
- Include templates directory in npm package files

* fix: add existence check for base.md template file

Add existsSync guard before reading base.md to handle missing templates gracefully, consistent with other template file reads.

* fix: use direct tool invocation instead of which command

Replace 'which' with direct tool invocation (tmux -V, gh --version)
for better portability on minimal Linux systems where 'which' may
not be installed.

* fix: address Bugbot review comments

- Simplify gh auth status check to rely on exit code instead of output string
- Remove async from synchronous functions (detectProjectType, generateRulesFromTemplates)

* feat: add setup script for one-command installation

Add scripts/setup.sh that:
- Installs pnpm if not present
- Installs dependencies
- Builds all packages
- Links CLI globally

Updated README with simplified setup instructions using the script.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: correct npm link command in setup script

Remove incorrect -g flag from npm link command. The correct syntax is to cd into the package directory and run npm link without flags.

* fix: address Bugbot review comments on init command

- Validate --smart flag requires --auto (prevents silent ignore)
- Fix path validation to check user-specified path (not CWD)

These fixes address medium and low severity issues found by Cursor Bugbot
in PR #66 review.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: add DirectTerminal troubleshooting and fix setup script

- Add TROUBLESHOOTING.md documenting node-pty posix_spawnp error
- Update setup.sh to rebuild node-pty from source (fixes DirectTerminal)
- Ensures seamless onboarding with working terminal out-of-the-box

Resolves DirectTerminal WebSocket failures from incompatible prebuilt binaries.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: resolve variable scope issue in init command validation

- Move path variable outside if block to fix TypeScript scope error
- Only validate path existence if projectId is provided
- Use inline tilde expansion instead of missing expandHome import

Fixes build error that prevented setup.sh from completing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: automate node-pty rebuild to eliminate terminal issues

- Add postinstall hook to automatically rebuild node-pty after pnpm install
- Create scripts/rebuild-node-pty.js for automatic rebuild with error handling
- Remove manual node-pty rebuild from setup.sh (now automatic)

This ensures DirectTerminal works correctly on every installation without
manual intervention. Fixes posix_spawnp errors from incompatible prebuilt
binaries across different systems and installations.

Resolves issue where users would encounter blank terminals after setup.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: update TROUBLESHOOTING with automatic node-pty rebuild

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: add comprehensive README with quick start guide

- 3-line magical setup: clone → setup → init → start
- Architecture overview with plugin slots table
- Usage examples and auto-reaction configuration
- Links to detailed docs (SETUP.md, TROUBLESHOOTING.md, examples/)
- Philosophy: push not pull, amplify judgment

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: resolve ESLint errors in rebuild-node-pty script

- Add scripts directory configuration to eslint.config.js
- Configure Node.js globals (console, process) for scripts
- Remove unused error variable from catch block

Fixes lint CI failure.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: warn when auto mode uses placeholder repo value

- Detect when 'owner/repo' placeholder is used in --auto mode
- Show warning: 'Could not detect GitHub repository'
- Update next steps to emphasize editing config when placeholder used
- Prevents silent failures when spawning agents with invalid repo

Addresses Bugbot review comment about silent placeholder values.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 22:22:13 +05:30
prateek cd9003a7db
fix: improve 'Ask Agent to Fix' button UX (#61)
* fix: improve "Ask Agent to Fix" button UX and remove outdated TODO

Replaced browser alerts with proper visual feedback:
- Added loading state ("Sending...") with disabled button during request
- Show success state ("Sent!") in green for 3 seconds
- Show error state ("Failed") in red for 3 seconds
- Prevent multiple clicks while processing

Also removed outdated TODO comment - the API endpoint is already implemented.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: prevent race condition in concurrent "Ask Agent to Fix" clicks

Changed state tracking from single values to Sets to properly handle
multiple concurrent button clicks:

- `sendingComments: Set<string>` - tracks all in-flight requests
- `sentComments: Set<string>` - tracks successful completions
- `errorComments: Set<string>` - tracks failures
- `timersRef: Map<string, Timeout>` - per-comment cleanup timers

This ensures that:
- Each button correctly shows its own loading/success/error state
- Buttons remain disabled only while their specific request is pending
- Callbacks from different requests don't overwrite each other's state

Fixes the race condition identified by Cursor Bugbot.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 20:29:51 +05:30
prateek 2fce2c70f0
fix: prevent merged PRs from showing 'Merge conflicts' status (#60)
* fix: prevent merged PRs from showing "Merge conflicts" status

GitHub returns `mergeable: null` for merged/closed PRs. The SCM plugin
was misinterpreting this as unknown/problematic status, leading to
false "Merge conflicts" warnings in the dashboard.

Fixed in two layers:
1. SCM plugin: Check PR state first in getMergeability(). Return clean
   result immediately for merged/closed PRs without querying mergeable.
2. Dashboard: Skip merge conflict display for non-open PRs in
   SessionDetail component.

SessionCard already had protection via early return in getAlerts().

Closes: bug described in task description
Tests: Added tests for merged/closed PR cases, all 54 tests passing

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: only skip mergeability checks for merged PRs, not closed PRs

Addresses review comment: A closed-but-unmerged PR should still have
its mergeability checked accurately. Only merged PRs should skip the
checks since they're already merged.

Changes:
- SCM plugin: Only return clean status for state === "merged"
- Dashboard: Show conflicts for closed PRs (pr.state !== "merged")
- Tests: Updated to verify closed PRs still get checked

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 20:24:43 +05:30
prateek 1cb52c122d
refactor: replace magic strings with constants for status enums (#64)
Replaces hardcoded string literals with typed constants for:
- SESSION_STATUS (needs_input, stuck, errored, etc.)
- PR_STATE (merged, closed, open)
- CI_STATUS (passing, failing, pending, none)

Benefits:
- Type safety - IDE catches typos at compile time
- Autocomplete - Better developer experience
- Single source of truth - Easy to add/remove states
- Easier refactoring - Change constant value in one place
- Better git grep - Search for SESSION_STATUS.STUCK vs all "stuck" strings

Files updated:
- packages/core/src/types.ts - Added constants
- packages/core/src/lifecycle-manager.ts - SessionStatus, PRState, CIStatus
- packages/core/src/session-manager.ts - PRState
- packages/web/src/lib/types.ts - SessionStatus, CIStatus
- packages/web/src/components/SessionCard.tsx - CIStatus
- packages/web/src/components/SessionDetail.tsx - CIStatus
- packages/web/src/components/Dashboard.tsx - CIStatus
- packages/plugins/scm-github/src/index.ts - CIStatus
- packages/plugins/notifier-slack/src/index.ts - CIStatus

Follows pattern established in PR #55 for ACTIVITY_STATE constants.

Resolves INT-1368

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 08:53:17 +05:30
prateek 77323cb309
feat: implement ao start command for unified orchestrator startup (#42)
* feat: implement ao start command for unified orchestrator startup

Adds `ao start` and `ao stop` commands to unify orchestrator and
dashboard startup. Key features:

- Generates CLAUDE.orchestrator.md with project-specific context
- Auto-imports orchestrator prompt via CLAUDE.local.md
- Creates orchestrator tmux session with agent
- Starts Next.js dashboard server
- Supports --no-dashboard, --no-orchestrator, --regenerate flags
- Idempotent operation (safe to run multiple times)
- Computes orchestrator ID from config (not session search)
- Dashboard button always visible for orchestrator terminal

Components:
- packages/core/src/orchestrator-prompt.ts: Prompt generator
- packages/cli/src/commands/start.ts: Start/stop commands
- Modified exports and dashboard UI for orchestrator support

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: add automatic metadata updates via Claude Code hooks

CRITICAL: This makes the dashboard work by auto-updating metadata when
agents run git/gh commands. Without this, PRs created by agents never
appear on the dashboard.

Changes:
- packages/core/src/claude-hooks.ts: Setup Claude hooks (settings.json + metadata-updater.sh)
- ao start: Automatically configures Claude hooks in project directory
- ao spawn: Sets AO_SESSION and AO_DATA_DIR env vars for hook script
- metadata-updater.sh: Detects gh pr create, git checkout -b, gh pr merge

How it works:
1. PostToolUse hook fires after every Bash command
2. metadata-updater.sh receives JSON with command and output
3. Pattern matches git/gh commands and updates flat metadata files
4. Dashboard reads metadata files to show PR/branch/status

The .claude directory is symlinked from main repo to worktrees so all
sessions share the same hook config.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: move hook setup to Agent plugin interface

ARCHITECTURE: Hooks setup must go through the Agent plugin interface,
not be hardcoded for Claude Code. This allows other agents (Codex,
Aider, OpenCode) to implement their own metadata update mechanisms.

Changes:
- Added Agent.setupWorkspaceHooks() method to types.ts
- Added WorkspaceHooksConfig interface
- Implemented setupWorkspaceHooks() in Claude Code plugin
- ao start: calls agent.setupWorkspaceHooks() instead of direct setup
- ao spawn: calls agent.setupWorkspaceHooks() for new worktrees
- Uses $CLAUDE_PROJECT_DIR variable for hook path (works with symlinked .claude)

Each agent plugin now implements its own hook mechanism:
- Claude Code: .claude/settings.json with PostToolUse hook
- Future: Codex, Aider, OpenCode with their own config formats

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address Cursor Bugbot review comments

Fixes 3 issues identified by Cursor Bugbot:

1. HIGH: ao start is now truly idempotent - if orchestrator session exists,
   it skips creating the session but still proceeds with dashboard startup
   and hook configuration. This allows `ao start` to recover from dashboard
   crashes without failing.

2. MEDIUM: Dashboard orchestrator button now finds the actual running
   orchestrator session instead of always using the first project. Fallback
   to first project ID if no orchestrator is running.

3. LOW: Deduplicated findWebDir() function by moving it to shared utility
   lib/web-dir.ts. Now used by both dashboard.ts and start.ts.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: resolve ESLint errors

Fixes 4 ESLint errors identified in CI:
- Added { cause: err } to Error constructors (preserve-caught-error rule)
- Prefixed unused parameter 'config' with underscore in setupWorkspaceHooks
- Prefixed unused variable 'projectId' with underscore in stop command

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address bugbot review comments - markdown escaping and unused module

- Fix markdown code fence escaping in orchestrator-prompt.ts (change
  `\\\`` to `\`` for proper markdown rendering)
- Remove unused claude-hooks.ts module (functionality moved to plugin)
- Clean up exports from core/src/index.ts

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address remaining bugbot issues - metadata path, duplication, summary

HIGH severity - Fix metadata path mismatch for worker sessions:
- Add AO_PROJECT_ID environment variable in spawn.ts
- Update metadata-updater hook script to construct correct path:
  * Worker sessions: $AO_DATA_DIR/${AO_PROJECT_ID}-sessions/$AO_SESSION
  * Orchestrator: $AO_DATA_DIR/$AO_SESSION (no project ID)
- Fixes silent hook failures where PRs/branches never appeared on dashboard

LOW severity - Remove code duplication in hook setup:
- Extract setupHookInWorkspace() helper function (90 lines)
- Refactor setupWorkspaceHooks() to use helper (from 80 lines to 4)
- Refactor postLaunchSetup() to use helper (from 82 lines to 6)
- Eliminates risk of methods drifting out of sync

LOW severity - Fix misleading summary output:
- Change "Orchestrator started" to "Startup complete" when components skipped
- Only show dashboard URL when --no-dashboard NOT used
- Only show session info when --no-orchestrator NOT used
- Show "already running" status when orchestrator exists

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address new bugbot issues - hook quoting, orchestrator link, pkill scope

HIGH severity - Fix Claude hook command quoting:
- Remove embedded double quotes from $CLAUDE_PROJECT_DIR path
- Change from '"$CLAUDE_PROJECT_DIR"/.claude/...' to '$CLAUDE_PROJECT_DIR/.claude/...'
- Prevents hook execution failures if runner treats command as literal path

MEDIUM severity - Fix orchestrator link pointing to nowhere:
- Only show "orchestrator terminal" link when session actually exists
- Remove fallback to computed/hardcoded "ao-orchestrator" ID
- Prevents 404s when user clicks link before starting orchestrator

MEDIUM severity - Fix stop command killing unrelated processes:
- Replace broad `pkill -f "next dev -p ${port}"` with targeted approach
- Use `lsof -ti :${port}` to find exact PID listening on port
- Only kill the specific process, not any process mentioning "next dev"
- Prevents accidentally killing unrelated Next.js dev servers

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: orchestrator metadata path and multi-pid dashboard stop

HIGH severity - Fix orchestrator AO_PROJECT_ID pollution:
- Orchestrator intentionally omits AO_PROJECT_ID (uses flat metadata path)
- agent.getEnvironment() adds AO_PROJECT_ID=project.name
- Object.assign() merged this in, breaking metadata hook path lookup
- Fix: delete environment.AO_PROJECT_ID after merge
- Ensures orchestrator metadata updates work correctly

MEDIUM severity - Fix stopDashboard with multiple PIDs:
- lsof -ti :PORT returns multiple PIDs (one per line) for parent+children
- Passing entire multi-line string to kill fails (can't parse newlines)
- Fix: split stdout by newlines, filter empty, pass PIDs as separate args
- Now correctly stops dashboard even when multiple Node processes exist

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: remove AO_PROJECT_ID from agent plugins to fix metadata path mismatch

The agent.getEnvironment() method was setting AO_PROJECT_ID to
config.projectConfig.name, but different callers have different metadata
path schemes:
- spawn.ts writes to project-specific directories (dataDir/{projectId}-sessions/)
- start.ts writes to flat directories for orchestrator (dataDir/)
- session-manager writes to flat directories (dataDir/)

Setting AO_PROJECT_ID in getEnvironment() caused the metadata updater hook
to look for files in the wrong location for orchestrator and session-manager
flows, breaking automatic metadata updates.

Fix: Remove AO_PROJECT_ID from all agent plugins' getEnvironment() methods
and make it the caller's responsibility to set when using project-specific
directories. Only spawn.ts sets it now.

Changes:
- Remove AO_PROJECT_ID assignment from getEnvironment() in all 4 agent plugins
  (claude-code, aider, codex, opencode)
- Update corresponding tests to expect AO_PROJECT_ID to be undefined
- Remove delete environment.AO_PROJECT_ID statement from start.ts (no longer needed)
- Add comments explaining the metadata path scheme contract

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: only run orchestrator setup when actually starting orchestrator

The orchestrator-specific setup steps (generating CLAUDE.orchestrator.md,
configuring CLAUDE.local.md, and setting up agent hooks) were executing
unconditionally, even when --no-orchestrator was passed. This caused
`ao start --no-dashboard` to fail if hook setup had errors, because the
hook setup was fatal and blocked the dashboard from starting.

Fix: Move all orchestrator setup steps inside the `if (opts?.orchestrator !== false)`
guard, and specifically inside the `else` branch (when session doesn't already exist).
Now these steps only run when we're actually creating a new orchestrator session.

This allows:
- `ao start --no-orchestrator` to start only the dashboard
- Skipping setup when orchestrator session already exists
- Setup to be non-blocking for dashboard-only mode

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: eliminate redundant getAgent call by hoisting agent declaration

The agent instance was being created twice in the orchestrator setup block:
- Once at line 237 inside the hook setup try block
- Again at line 253 for getting the launch command

This creates duplicate agent instances unnecessarily. Fixed by declaring
the agent variable before the hook setup try block, allowing it to be
reused for both hook setup and launch command generation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address resource leaks and undefined env variable

Fixed 4 Bugbot issues:

1. HIGH: Undefined $CLAUDE_PROJECT_DIR in hook script path
   - Changed setupWorkspaceHooks to use absolute path instead of undefined
     env variable
   - Matches approach used in postLaunchSetup for consistency

2. HIGH: Orchestrator session leaks when metadata write fails
   - Added try-catch around tmux session launch and metadata write
   - Kills tmux session if metadata write or agent launch fails
   - Prevents orphaned sessions from consuming resources

3. MEDIUM: Dashboard process leaks when orchestrator setup fails
   - Wrapped orchestrator setup in try-catch block
   - Kills dashboard child process if orchestrator setup fails
   - Prevents orphaned Next.js server from blocking future starts

4. LOW: Inconsistent indentation (fixed as side effect of restructuring)
   - Refactored error handling simplified indentation structure

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 04:58:39 +05:30
prateek bba1316527
feat: implement DirectTerminal with XDA clipboard support (#55)
* feat: implement DirectTerminal with XDA clipboard support

Fixes clipboard functionality in web terminal without requiring iTerm2 attachment.

## Problem
Browser clipboard (Cmd+C/Ctrl+C) only worked when an iTerm2 client was attached
to the tmux session. Users had to keep iTerm2 tabs open in the background for
clipboard to work in the web dashboard.

## Root Cause
- tmux uses XDA (Extended Device Attributes) queries to detect terminal capabilities
- xterm.js doesn't implement XDA (marked as TODO in their codebase)
- Without XDA response, tmux doesn't enable clipboard support (TTYC_MS)
- iTerm2 responds to XDA, enabling clipboard for the entire session

## Solution
Implemented DirectTerminal component with custom XDA handler:
- Registers CSI > q handler using xterm.js parser API
- Responds with XTerm identification: DCS > | XTerm(370) ST
- tmux detects "XTerm(" and enables clipboard capability
- OSC 52 sequences flow: tmux → WebSocket → xterm.js → navigator.clipboard

## Changes
- Add DirectTerminal component with XDA handler
- Add direct-terminal-ws WebSocket server using node-pty
- Replace Terminal with DirectTerminal in SessionDetail
- Add comprehensive test page at /dev/terminal-test documenting:
  - Root cause analysis
  - Implementation details
  - Node version requirements (requires Node 20.x due to node-pty)
  - Debugging journey and lessons learned
- Fix ttyd port recycling to prevent EADDRINUSE errors

## Testing
Visit http://localhost:3000/dev/terminal-test for side-by-side comparison
and complete documentation.

Investigation time: 12+ hours (Feb 15-16, 2026)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: lint and typecheck errors

- Remove unused expectedWidth variable in DirectTerminal
- Add test files to eslint ignores

* fix: wrap useSearchParams in Suspense boundary

- Add Suspense wrapper to /dev/terminal-test page
- Add Suspense wrapper to /test-direct page
- Fixes Next.js build prerender error

* fix: address Bugbot review comments

- Fix useEffect cleanup memory leak in DirectTerminal
- Remove accidentally committed build artifacts
- Remove test scripts from repository root
- Add build/ to .gitignore

* fix: address additional Bugbot comments

- Re-enable mouse mode in ttyd terminal (was temporarily disabled)
- Fix hardcoded macOS paths in direct-terminal-ws
- Use 'tmux' from PATH instead of hardcoded /opt/homebrew/bin/tmux
- Use os.userInfo() for username fallback instead of 'equinox'
- Use process.env.PATH for cross-platform compatibility

Note: Bugbot comment about XDA response is incorrect - terminal.write()
is the correct API for responding to XDA queries. The implementation works
as confirmed by user testing.

* perf: fix slow terminal scrolling

- Remove status and error from useEffect dependencies (was recreating terminal on every state change)
- Add scroll performance settings: scrollSensitivity, fastScrollModifier
- Terminal now only recreates when sessionId changes

* fix: pass startFullscreen prop to DirectTerminal in SessionDetail

The fullscreen query parameter was being read from URL but not passed
through to the DirectTerminal component. This caused the fullscreen=true
query param to be ignored when viewing session detail pages.

Fixes:
- Pass startFullscreen prop from SessionDetail to DirectTerminal
- Enables ?fullscreen=true to work on session detail pages

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: enable mouse mode for DirectTerminal scrolling

DirectTerminal was not scrollable because tmux mouse mode was not enabled
for the sessions. The ttyd implementation already had this, but the
DirectTerminal WebSocket server was missing it.

Changes:
- Add spawn import from node:child_process
- Enable tmux mouse mode on session connection
- Hide tmux status bar for cleaner appearance

This makes DirectTerminal scrolling work the same as ttyd terminals.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: reduce scroll speed in DirectTerminal for smoother experience

Reduced scroll sensitivity from 3 to 1 lines per wheel tick for more
natural scrolling behavior. Also reduced fast scroll (with Alt) from 5 to 3.

Changes:
- scrollSensitivity: 3 → 1 (normal scroll speed)
- fastScrollSensitivity: 5 → 3 (Alt+scroll speed)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: auto-select two different sessions for terminal test page

When no query params are provided, the test page now automatically fetches
available sessions and picks two different ones for side-by-side comparison.
This avoids port conflicts between ttyd and DirectTerminal by default.

Changes:
- Fetch sessions from /api/sessions on mount
- Use first two available sessions as defaults (or fall back to hardcoded)
- Query params still work for manual override
- Warning only shows when sessions are actually the same

Examples:
- /dev/terminal-test (auto-picks two sessions)
- /dev/terminal-test?old_session=X&new_session=Y (manual override)
- /dev/terminal-test?session=X (uses same session for both)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: filter out terminated sessions in terminal test auto-selection

The auto-selection was picking terminated/exited sessions, causing
WebSocket connection failures when trying to attach to non-existent
tmux sessions (PTY exit code 1).

Now filters to only use sessions with activity !== "exited" for
auto-selection, ensuring terminals can actually connect.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: add server-side active session filtering to API

Instead of filtering terminated sessions on the client, added proper
API support with ?active=true query parameter to filter server-side.

Changes:
- GET /api/sessions?active=true - Returns only non-exited sessions
- Updated terminal test page to use new API parameter
- Properly maintains session/dashboard alignment during filtering
- Removed client-side filtering logic

This is cleaner, more efficient, and follows proper API design patterns.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: replace magic strings with ACTIVITY_STATE constants

Added ACTIVITY_STATE constants to @composio/ao-core types and replaced
all hardcoded "exited", "waiting_input", "blocked" strings with proper
constants throughout the codebase.

Changes:
- Added ACTIVITY_STATE constant object to core/types.ts
- Replaced magic strings in API route (/api/sessions)
- Replaced magic strings in lib/types.ts (getAttentionLevel)
- Properly typed constants with satisfies Record<string, ActivityState>

This prevents typos, improves IDE autocomplete, and makes refactoring
easier by having a single source of truth for activity state values.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: move WebSocket servers outside src/ to fix webpack build error

Moved terminal-websocket.ts and direct-terminal-ws.ts from src/server/
to server/ (outside src/) to prevent Next.js webpack from trying to
bundle them with client code.

These are standalone Node.js WebSocket servers (not Next.js API routes)
that should not be part of the Next.js build. Webpack was failing when
encountering node:child_process imports.

Changes:
- Moved src/server/*.ts to server/*.ts
- Updated package.json scripts to point to new location

Fixes: Module build failed: UnhandledSchemeError with node:child_process

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: update server file path in terminal test page docs

Updated documentation to reflect new server file location after moving
files from src/server/ to server/.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: import from @composio/ao-core/types to avoid bundling server code

The main @composio/ao-core export includes tmux utilities that use
node:child_process, which webpack cannot bundle for the client.

The package already exports a /types entry point that only includes
types and constants (no server utilities).

Changes:
- Import from @composio/ao-core/types instead of @composio/ao-core
- This prevents webpack from trying to bundle tmux.js in client code

Fixes webpack error: UnhandledSchemeError with node:child_process

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: combine duplicate imports to resolve lint errors

* fix: update .env.local.example to match code defaults (port 3003)

Addresses bugbot comment: Documentation showed port 3002 but both
server (direct-terminal-ws.ts) and client (DirectTerminal.tsx) default
to 3003. This mismatch could cause connection failures if developers
set only DIRECT_TERMINAL_PORT without NEXT_PUBLIC_DIRECT_TERMINAL_PORT.

Updated documentation to reflect actual code defaults.

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 04:34:15 +05:30
prateek 2a3723be48
fix: prevent "Leave Site?" dialog on session pages (#47)
* fix: proxy ttyd through Next.js to fix "Leave Site?" dialog

Changes:
- Add Next.js rewrite to proxy /terminal-proxy/* to ttyd server
- Simplify Terminal component to use same-origin proxy URL
- Remove cross-origin fetch and error handling (no longer needed)

Why:
- Fixes "Leave Site?" confirmation dialog when navigating away from session pages
- Makes ttyd iframe same-origin with Next.js app (eliminates cross-origin restrictions)
- Simplifies architecture (no cross-origin complexity)

Note: This does NOT fix clipboard copying - that's a separate ttyd/xterm.js
limitation where clipboard operations don't work even on the raw ttyd page.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: add development workflow section to CLAUDE.md

Explains:
- Build packages before running dev server (web depends on built core/plugins)
- Config file requirement (agent-orchestrator.yaml)
- Worktree-specific setup steps

This prevents the "Module not found: @composio/ao-core" error when starting
the dev server without building packages first.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add sandbox attribute to terminal iframe to prevent "Leave Site?" dialog

Added sandbox="allow-scripts allow-same-origin allow-forms" to the terminal
iframe. This prevents the iframe's beforeunload handler from triggering a
"Leave Site?" confirmation dialog when navigating away from session pages.

The sandbox attribute restricts the iframe's ability to block navigation
while still allowing:
- Scripts (for xterm.js and WebSocket)
- Same-origin access (for terminal functionality)
- Forms (for terminal input)

Note: Clipboard copying behavior is inconsistent across sessions (works on
some but not others). This appears to be a ttyd/xterm.js limitation and is
tracked separately.

Fixes the "Leave Site?" popup reported in the issue.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* chore: remove unused proxy rewrite from next.config.js

The proxy approach was attempted but reverted in favor of the simpler
sandbox attribute solution.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add allow-popups to sandbox for clipboard support

Some clipboard operations in browsers use popups internally. Adding
allow-popups to the sandbox attribute may help with clipboard copying.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* chore: minor formatting cleanup in SessionDetail.tsx

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 19:39:53 +05:30
prateek 7ce8dc3480
fix: remove redundant attention level badge from session detail (#41)
* fix: remove redundant attention level badge from session detail page

The detail page was showing both activity ("Idle") and attention level
("Working") as badges, which looked contradictory. Attention level is
for the dashboard overview zones, not the detail page. Keep only the
activity badge.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: terminal button should link to session detail page, not raw xterm URL

The terminal button was fetching from the terminal-web plugin and opening
a raw xterm.js URL (localhost:7801). Changed to a simple link to the
session detail page which has an embedded terminal.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 13:36:16 +05:30
prateek de662dc042
fix: recognize terminated/done session states and hide terminal for dead sessions (#40)
* fix: recognize terminated/done session states and hide terminal for dead sessions

- Add "done" and "terminated" to VALID_STATUSES in session-manager so
  validateStatus() doesn't fall back to "spawning" for these states
- Hide terminal button for terminal-state sessions (no tmux to connect to)
- Hide "terminate session" button for already-terminated sessions
- Show "restore session" button for terminated/done sessions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: restore activity=exited check for crashed sessions

Bugbot caught that the refactor dropped the activity === "exited"
condition. When an agent crashes, status stays non-terminal (e.g.
"working") but activity becomes "exited" — these need the restore
button and should not show terminal/terminate buttons.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add terminated/done to backend RESTORABLE_STATUSES

Frontend shows restore button for terminated/done sessions but
the backend restore endpoint only accepted killed/cleanup, returning
409 "Session is not in a terminal state" for the new statuses.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add type annotations to fix implicit any errors in integration tests

Pre-existing issue from package rename — callback parameters in
.find() lost type inference. Add explicit type annotations.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: filter orchestrator session from SSR page

The API route filtered it but the SSR path in page.tsx did not,
causing the orchestrator to appear as a session card.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: make orchestrator session name dynamic using prefix convention

Use endsWith("-orchestrator") instead of hardcoded "orchestrator" to
support project-prefixed names like "ao-orchestrator". Pass orchestratorId
from SSR to Dashboard so the terminal button links to the correct session.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: update stale @agent-orchestrator/core imports to @composio/ao-core

Package was renamed in PR #32 but these two files were missed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 05:15:41 +05:30