Follow up #1320 by hiding the debug bundle control by default and only showing it in development or with . Move the button to the left header cluster and add regression tests for default-hidden and debug-enabled visibility.
Made-with: Cursor
* fix(web): resolve dashboard issue URLs from tracker
Build issue URLs from tracker.issueUrl when sessions store identifier-only issue IDs so dashboard links, labels, and title enrichment stay valid for GitHub-style numeric issues.
Made-with: Cursor
* fix(web): harden issue URL enrichment and add regressions
Guard issue enrichment against URL double-wrapping, free-text synthetic links, and invalid tracker URL output while logging failures. Add regression coverage for URL-shaped issue ids, free-text issue ids, tracker issueUrl failures, and failed issue-title lookup throttling.
Made-with: Cursor
* fix(web): remove unreachable issue-url branch for lint
Drop the duplicate else-if path in enrichSessionIssue that was already covered by the absolute-url guard, resolving no-dupe-else-if and unblocking lint/typecheck/web onboarding checks.
Made-with: Cursor
* fix(web): surface dashboard SSR load failures
When getServices or session listing throws during SSR, show an error banner
and skip the empty-state CTA instead of implying there are zero sessions.
Disables realtime hooks while the banner is shown to avoid noisy reconnects.
Made-with: Cursor
* fix(web): make dashboard SSR errors recoverable
Narrow dashboard fatal errors to service/bootstrap failures and fail-soft on enrichment so transient metadata issues do not blank the page. Keep realtime updates active after SSR load errors, sanitize multiline error messages, and add regression tests.
Made-with: Cursor
* fix(web): address dashboard SSR recovery and attention-zone SSR
- Gate hiding the SSR load-error banner on liveSessionsResolved from
useSessionEvents (successful /api/sessions refresh, SSE snapshot, or mux),
not session count; use cache: no-store on session refresh fetches.
- Apply attentionZones from config immediately after getServices(); wrap
sessionManager.list() in its own try/catch so list failures keep zone config.
- Treat session.status merged like merged PR for Done cards (hide Restore).
- Align ProjectSidebar orchestrator fixture id with isOrchestratorSession pattern.
Made-with: Cursor
* feat(web): merge conflict compare link and branch copy on PR card
- Add buildGitHubCompareUrl helper with unit tests
- Show compare + copy head branch when open PR has merge conflicts
- Add SessionDetail regression test for conflict affordances
Made-with: Cursor
* fix(web): harden merge-conflict PR actions
Address review feedback by correcting the changeset package name, encoding all compare URL path segments, and making conflict actions resilient to unenriched/rate-limited PR data and clipboard/timer edge cases. Add regression tests for URL encoding and conflict-action gating.
Made-with: Cursor
* fix(web): normalize browser timer handles in session detail
Use browser timer handle types consistently in the session detail PR card copy-feedback flow so Next.js typechecking does not mix Node Timeout and DOM number timer signatures.
Made-with: Cursor
* feat(core): auto-terminate sessions on PR merge (#1309)
When a session's PR was detected as merged, the session transitioned
to status "merged" but its tmux runtime, worktree, and metadata were
never cleaned up — leaving zombie tmux sessions and stale entries in
`ao status` / `ao session ls`. Users worked around this with an
external watchdog. Close the loop in AO itself.
Changes:
- `kill()` gains an optional `reason` and returns `KillResult`
(`cleaned` / `alreadyTerminated`), with short-circuit paths so
repeated calls on archived sessions are safe no-ops instead of
throwing `SessionNotFoundError`.
- New `LifecycleConfig` (`autoCleanupOnMerge: true` default,
`mergeCleanupIdleGraceMs: 5 min`) so operators can opt out when
they need merged worktrees preserved for inspection.
- `lifecycle-manager` runs `maybeAutoCleanupOnMerge` at the end of
each `checkSession`. Reactions and notifications observe the live
session first; cleanup runs last. If the agent is still `active` /
`waiting_input` / `blocked`, cleanup is deferred and retried on
the next poll until the agent idles or the grace window elapses
(prevents killing an agent mid-task).
- New `CanonicalSessionReason` / `CanonicalRuntimeReason` variants
(`pr_merged`, `auto_cleanup`) so observability distinguishes
automated teardown from manual kills.
Scope is deliberately narrow to `merged`: `done` / `errored` often
need the worktree preserved for debugging; `killed` would self-recurse.
Follows Codex review feedback (conditional pass): scope narrowed,
reactions-before-cleanup ordering, idleness safety gate, real
idempotency guards, config opt-in.
6 new unit tests cover: idle agent cleanup, active agent deferral,
grace-window force-cleanup, config opt-out, terminated/killed no
self-recursion, kill() failure retry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(core): clean up lifecycle config access per review
Address review comment on PR #1311. The `config.lifecycle` field is
typed optional but always populated by Zod — the old guard chain
(`if (lifecycleConfig && lifecycleConfig.autoCleanupOnMerge === false)`)
obscured that duality. Destructure with defaults at the call site so
the contract is visible in one place, and document why the field stays
optional (hand-constructed test configs) on the interface.
Matches the existing `power?: PowerConfig` pattern — keeps churn to
zero across 60 test config literals while removing the ambiguous guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(core): surface auto-cleanup-on-merge in config, docs, and UI
Followup to DX audit on #1311. The lifecycle cleanup behavior was
operational but invisible — config key only in TS types, no changeset
for downstream consumers, missing observability spec, and a dashboard
summary that actively contradicted the new default-on behavior.
- agent-orchestrator.yaml.example: add commented `lifecycle:` block
with both keys so operators discover the knob in the primary
config reference.
- .changeset/auto-cleanup-on-merge.md: minor bump for @aoagents/ao-core
with migration note (default-on, opt-out via config).
- docs/observability.md: document the three new lifecycle_poll
operations (merge_cleanup.completed / deferred / failed) so
dashboard/alert authors have a spec.
- packages/web/src/lib/serialize.ts: replace stale summary
"PR merged; worker is still available for a keep-or-kill decision"
with "PR merged; worker session will be cleaned up automatically".
The old copy is wrong under default-on auto-cleanup.
Deferred to follow-up issues:
- Health surface degradation on repeated cleanup failure (the
operator-facing gap Codex flagged — failures emit a metric but
don't downgrade /api/observability health).
- Dashboard "cleaning up in Nm" indicator for the deferred state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core,web): address PR review feedback for auto-cleanup on merge
- serialize.ts: only claim "will be cleaned up automatically" when
mergedPendingCleanupSince marker is present; otherwise show neutral
"PR merged". Avoids lying when autoCleanupOnMerge is opted out.
- lifecycle-manager.ts: use ACTIVITY_STATE constants instead of
hardcoded strings, matching the existing SESSION_STATUS.MERGED usage.
- config.ts: keep mergeCleanupIdleGraceMs=0 as a valid escape hatch
(immediate cleanup), but reject 1..9999 with a units-mistake error
so users typing `5` (intending seconds) get a clear message.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to the first round of review comments:
- Guard `document.fonts` add/removeEventListener with feature detection.
Without the guard, environments where FontFaceSet doesn't expose
EventTarget (jsdom test mocks, older browsers) threw a TypeError
during xterm init and the terminal silently failed to attach.
- Update DirectTerminal.render.test.tsx mock so `document.fonts`
exposes addEventListener/removeEventListener stubs, restoring
happy-path coverage of the init code path.
- Export `resolveMonoFontFamily` and add unit tests covering:
CSS var present (prepended to fallback), CSS var absent
(fallback only), and the invariant that no `var(...)` token ever
leaks into the output.
- Clarify in the docblock why we read `--font-jetbrains-mono` and
deliberately avoid `--font-mono` (the latter re-wraps in `var(...)`
and would re-introduce the bug).
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- Resolve --font-jetbrains-mono via getComputedStyle at runtime so
xterm honours the app's configured mono font token instead of a
hard-coded "JetBrains Mono" fallback. next/font generates a unique
family name (stored in the CSS custom property) that now gets fed
to xterm alongside the fallback stack.
- Re-resolve the font-family on `document.fonts` `loadingdone` so
xterm picks up the generated name once it registers.
- Change SessionDetail's DirectTerminal loading skeleton from a fixed
h-[440px] to h-full, so the terminal area stays viewport-sized
during the lazy-load window instead of locking to 440px.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
The v5 -> v6 xterm.js upgrade regressed the in-browser terminal: each
cell rendered visibly wider and shorter than the glyph inside it,
making horizontal spacing too wide and vertical spacing too tight.
Root causes:
1. `fontFamily` contained `var(--font-jetbrains-mono)`. xterm's char
measurement ultimately hits canvas `ctx.font`, which cannot resolve
CSS custom properties. The var token poisoned the font string so
measurement fell back to a default font while DOM rows still rendered
in JetBrains Mono — cell width vs glyph width drifted apart.
2. xterm v6 defaults `lineHeight` to 1.0. Combined with JetBrains Mono's
tall x-height, rows visually collided.
3. `document.fonts.ready` can resolve before next/font's
`font-display: swap` actually paints JetBrains Mono, so the initial
`fit()` measures against the fallback font. xterm does not re-measure
when the swap later lands.
4. The fit-target div had `p-1.5` padding, skewing FitAddon's cols/rows
computation.
Fixes applied to `DirectTerminal.tsx`:
- Drop `var(...)` from `fontFamily`; use a plain font stack.
- Set `lineHeight: 1.2` to restore vertical breathing room.
- Add a `document.fonts` `loadingdone` listener that clears the
texture atlas and re-fits when the webfont swap completes. Cleaned
up in the effect teardown.
- Remove `p-1.5` from the terminal ref div.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Fixes#1048. ao start used to allocate a fresh `{prefix}-orchestrator-N`
on every invocation instead of reattaching to the previous session, and
the dashboard's orchestrator link pointed at a different id than the
CLI just printed.
Changes:
runStartup (packages/cli/src/commands/start.ts):
- On startup, list existing orchestrators for the project, partition
them into live (runtime still running) and restorable (terminal but
sm.restore()-able) buckets, pick the most-recently-active from the
chosen bucket, and reuse/restore that id instead of spawning a new
one. Only spawn fresh when both buckets are empty.
- Live is preferred UNCONDITIONALLY over restorable — a newer killed
record can never beat an older-but-running one. Without this, a
cross-bucket sort could resurrect a killed record via sm.restore()
while the live orchestrator kept running, leaving two alive.
- Restored sessions get an explicit "(restored)" marker in the CLI
summary so the resurfaced id isn't a surprise.
- The phantom `${prefix}-orchestrator` id constant is removed from
every URL print, browser-open target, and summary line. Everything
now uses the real selected id.
registerStop (same file):
- ao stop now resolves the real orchestrator via sm.list(projectId)
+ isOrchestratorSession filter + most-recently-active sort, then
calls sm.kill on that id. The old phantom `${prefix}-orchestrator`
target never matched a real numbered record, so ao stop was a
silent no-op and the orchestrator kept running on disk between
start cycles. sm.list-failure warning no longer duplicates with the
generic "no orchestrator found" message.
isOrchestratorSession (packages/core/src/types.ts):
- Tightened: legacy bare-id records (`{projectId}-orchestrator` with
no role metadata) are no longer recognized as orchestrators by the
public predicate. This was the source of the dashboard/CLI id
divergence — stale bare records with a different prefix than the
numbered form were leaking into the dashboard's orchestrator list.
session-manager repair (packages/core/src/session-manager.ts):
- Split `isOrchestratorSessionRecord` (permissive, used by cleanup
protection) from a new `isRepairableOrchestratorRecord` (stricter,
used only by repairSingleSessionMetadataOnRead and
repairSessionMetadataOnRead).
- The strict repair predicate accepts role-stamped records, the bare
`{sessionPrefix}-orchestrator` correct-prefix legacy shape, and the
numbered `{sessionPrefix}-orchestrator-N` worktree shape. It
rejects foreign bare names like `{projectId}-orchestrator`, so
those records never get `role: orchestrator` backfilled on read
and therefore can no longer pass `isOrchestratorSession()` in real
`sm.list()` output via the role-metadata branch.
Tests added (~12):
- runStartup: live reuse, restore-on-killed, ignore-stale-bare
legacy records, live-beats-restorable regression, multi-live
reuse, URL fallback when --no-orchestrator.
- ao stop: kills the actual numbered id (not the phantom), handles
multiple orchestrators, tolerates sm.list throwing.
- isOrchestratorSession: rejects stale bare ids without role
metadata; accepts bare ids with role metadata stamped.
- listDashboardOrchestrators: stale bare excluded, numbered live
included, role-stamped legacy included.
- session-manager repair: does not backfill role onto foreign bare-id
records (issue #1048 regression guard).
Unblocks: review comments from cursor[bot] (dead else-if branch,
double messaging, redundant isTerminalSession check) and illegalcall
(cross-bucket sort, repair-backfill bypass of predicate tightening) —
all addressed in-place with the multi-orchestrator model preserved.
Verified: core 606/606, cli 450/450, typecheck clean across core/cli/web.
* feat(web): fix terminal height to fill viewport, prevent outside scrolling
* feat(web): add touch scroll, font size control, and accurate fit to DirectTerminal
* feat(web): add mobile sidebar overlay, hamburger toggle, and reconnect indicator
* feat(web): add hamburger and reconnect pill to topbar
* fix(web): use xterm instead of @xterm/xterm for terminal-touch-scroll import
Upstream uses xterm@5.3.0 (not @xterm/xterm), fix the type import accordingly.
* ci: make pnpm audit strict step non-blocking
npm's legacy audit endpoint (/npm/v1/security/audits) is returning 410 Gone
as it's being retired. Add continue-on-error until pnpm ships support for
the new bulk advisory endpoint.
* fix(web): replace TerminalLike with minimal interface compatible with xterm@5.3.0
xterm@5.3.0 does not have 'input' or 'attachCustomWheelEventHandler' methods
(those are @xterm/xterm v6 APIs). Define a minimal structural interface
matching only the members actually used in the file.
* fix(web): replace mobile back button with hamburger sidebar toggle on session page
On mobile, the session detail page now shows a hamburger button instead of a back arrow, which toggles the sidebar via SidebarContext. This provides better navigation consistency with the main dashboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): filter sidebar sessions strictly by projectId to prevent prefix collision
Sessions are now filtered to only show those whose projectId matches a configured project. This prevents sessions from different AO projects (e.g., 'ao-' and 'unl-' prefixes) from being incorrectly merged under the same project entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): add ResizeObserver and deferred fit to fix blank terminal on mount
The terminal now uses a 100ms deferred fit timeout to ensure the container has been sized before measuring. Additionally, a ResizeObserver monitors the terminal container and calls fit() whenever its size changes, ensuring the terminal refits when flex layouts settle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): debounce session refresh to prevent aborting in-flight fetch calls
The useSessionEvents hook now tracks when the last fetch was started and skips scheduling a new refresh if one was started less than 500ms ago. This prevents aggressive AbortController usage that was canceling in-flight requests unnecessarily. Also avoid rescheduling if a debounce timer is already pending.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(web): update SessionDetail mobile test for new sidebar toggle button
Update the test to expect the new "Toggle sidebar" aria-label instead of the previous "Back to dashboard" label on the mobile floating header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): active session amber color, compact session meta row, tighter font controls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): address review feedback — fontSize rerender, dead code, test cleanup
- DirectTerminal: remove fontSize from main init useEffect deps. A dedicated
effect below mutates terminal.options.fontSize in place; keeping it in the
init deps tore down and recreated the terminal (and WebSocket) on every
stepper click, losing scrollback and flashing content.
- Remove dead ReconnectingPill component — it rendered null with a placeholder
comment. Delete the file, import, and render site rather than leaving a shell.
- ProjectSidebar: make the done-filter consistent by using getAttentionLevel
in the sessionsByProject collector. Previously the collector filtered by the
narrow `status === "done"` while the render sites filtered by the broader
`getAttentionLevel(s) === "done"`, so the project badge count could include
merged/killed/terminated sessions that the rendered list hid.
- Drop the two gutted tests that tested JS primitives rather than the component
(layout-height.test.tsx, DirectTerminal.test.tsx). They provided false
coverage. Update ProjectSidebar.test.tsx for the new anchor-based session
rows (click behavior now queries role="link", and clicking the project
toggle expands rather than navigates — the separate dashboard button handles
navigation).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): make SessionDetail topbar tests pass after redesign
Reconcile the rebased-upstream tests with our redesigned SessionDetail:
Component:
- Show session.id in the topbar next to the headline. The ID is a stable
identifier (useful for copy/paste) while the headline changes with PR/
issue titles.
- Gate the topbar Orchestrator link on `!isOrchestrator` so we don't link
the orchestrator page to itself.
- Convert the PR popover toggle from a <button> to an <a href={pr.url}>.
Plain click still toggles the popover; ctrl/cmd-click opens the PR on
GitHub in a new tab. aria-label="PR #N" keeps the accessible name stable
regardless of the rendered chevron.
Tests:
- Mobile tests scope orchestrator link queries to the topbar via
within(getByRole("banner")) because MobileBottomNav also has an
"Orchestrator" entry.
- Desktop test opens the PR popover before asserting the PR detail
contents (blocker chips, file count, unresolved comments) — they now
live inside the popover rather than inline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): restore behavioral tests + remove dead mobileOpen prop
Adds missing behavioral coverage the reviewer flagged on Dashboard.mobile.test.tsx:
- Termination confirmation flow: clicking the kill button once enters a
confirming state (aria-label becomes "Confirm terminate session") without
firing the kill request; a second click POSTs to /api/sessions/:id/kill.
- CI check chip rendering: SessionCard renders passing CI check names as chips
when the session has an enriched PR.
Removes the dead `mobileOpen` prop from ProjectSidebar:
- It was declared but immediately aliased as `_mobileOpen` and never used —
the actual mobile overlay state is driven by the `sidebar-wrapper--mobile-open`
class on the parent wrapper div in Dashboard/SessionDetail.
- PullRequestsPage was passing it too but never wired the wrapper class, so
its hamburger buttons were silently no-ops. Wrapped its sidebar in the same
sidebar-wrapper + backdrop pattern so the buckled hamburgers now actually
open the sidebar overlay on mobile.
Not in scope for this round (per reviewer): attention bucket filter /
MobileBottomNav on Dashboard — Dashboard doesn't implement those features,
so there's no behavior to assert. Those would need a feature add, not a test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): listCached stale-data bug + drop inline styles in DirectTerminal
Addresses the reviewer's critical and minor feedback on PR #1278.
Critical — listCached was stale across mutation paths
listCached() had a 35s TTL, invalidated only by spawn() and kill().
Every other metadata-writing path (claimPR, restore, send, remap,
cleanup, lifecycle-manager's direct updateMetadata calls for PR
detection and state transitions) left stale data visible to
/api/sessions for up to 35s.
Fix:
- Wrap updateMetadata / writeMetadata / deleteMetadata inside
createSessionManager() so every in-file mutation auto-invalidates
the cache. The raw imports are aliased as _rawXxx and only used
by the wrappers. No call site needs to remember to invalidate.
- Expose invalidateCache() on the SessionManager interface for
callers that write metadata outside this module.
- lifecycle-manager.ts: call sessionManager.invalidateCache() after
its two direct updateMetadata sites (PR detection, state update)
so polling-driven mutations are visible on the next poll.
- Move the _cache / invalidateCache declarations to the top of the
closure so the mutation wrappers can reference them.
- Update createMockSessionManager + api-routes.test.ts mocks to
satisfy the expanded interface.
- New regression test: external mutation + sm.invalidateCache() →
next listCached() re-reads disk.
C-02 — inline styles on the terminal-container div
DirectTerminal.tsx was using style={{ overflow, display,
flexDirection, flex, minHeight }} — all static values moved to
Tailwind utilities: "w-full p-1.5 flex flex-col flex-1 min-h-0
overflow-hidden".
Not addressed in this commit (noted as out-of-scope refactor):
C-04 400-line cap on DirectTerminal.tsx. Splitting fontSizeControls
and touch-scroll wiring into sub-components is worthwhile but
mechanical — separate PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflicts in:
- packages/web/src/lib/types.ts (merged canonical type imports with DashboardAttentionZoneMode)
- packages/web/src/lib/dashboard-page-data.ts (kept upstream imports)
- packages/web/src/components/Dashboard.tsx (kept upstream mobileKanbanOrder)
Updated getAttentionLevel to support mode parameter from upstream
while preserving lifecycle-aware helper functions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(web): collapse attention zones to 4 with feature flag for 5 (#1201)
Simplify the dashboard to a 4-zone kanban by default (WORKING,
PENDING, ACTION, READY), merging the former REVIEW + RESPOND
columns into a single ACTION zone that just asks "does the human
need to do something?". The card-level badges still surface the
underlying granular state (ci_failed, needs_input, changes_requested),
so nothing is lost — it just moves off the column header.
Add a `dashboard.attentionZones` config (defaults to "simple") that
opts back into the original 5-zone layout for power users. The
function-level `getAttentionLevel(session, mode)` still defaults to
"detailed" so card-level call sites (SessionCard, BottomSheet,
ProjectSidebar, etc.) keep their granular behavior untouched. Only
the Dashboard kanban and SSE server routes read the config and pass
the mode.
Closes#1201
* fix(web): address PR review feedback on attention zones (#1201)
- Drop .strict() from DashboardConfigSchema (Cursor Bugbot):
matches other schemas in config.ts so typos in the dashboard
block gracefully default instead of crashing the whole loader.
- DynamicFavicon: keep `action` at yellow, not red (Codex P2).
In simple mode, `action` collapses respond + review, which
means it necessarily catches routine review work (ci_failed,
changes_requested) that was previously yellow. Escalating to
red would make every typical PR scream critical. Only the
detailed-mode `respond` bucket still triggers red.
- useSessionEvents default → "detailed" (Codex P3). The hook
default now matches getAttentionLevel's default so callers
that seed with `getAttentionLevel(s)` (no mode) — notably
PullRequestsPage — stay consistent with their seed after
the first live SSE/refresh snapshot. Dashboard explicitly
passes the config's attentionZones to opt into simple mode.
- Expand mobile action chip (Codex P3). When a session
collapses to `action`, derive the most specific underlying
cause (needs input, stuck, errored, ci failed, changes,
waiting, conflicts) instead of falling through to the
generic "action" label, so mobile users keep the reason to
intervene.
- DynamicFavicon tests: add cases for `action` staying yellow
and `respond` still escalating when both are present.
* fix(web): thread attentionZones config through PullRequestsPage (#1201)
Cursor Bugbot caught a deeper version of the same inconsistency
my previous fix didn't fully resolve: the server SSE route uses
`config.dashboard?.attentionZones ?? "simple"`, but PullRequestsPage
was seeding `initialAttentionLevels` and calling useSessionEvents
without passing any mode (falling back to the hook's "detailed"
default). Result: snapshots from the server arrived as "action"/
"merge", then scheduleRefresh recomputed them client-side as
"respond"/"review", and the favicon oscillated between yellow
and red on every refresh cycle.
Fix: plumb pageData.attentionZones through prs/page.tsx into
PullRequestsPage, use it in both the seed and useSessionEvents.
Now the seed, the server SSE, and the client refresh all use the
same mode — stable, no flicker.
* fix(web): accurate action labels for crashed agents + yellow tone (#1201)
Two Cursor Bugbot findings on the action-zone collapse work:
1. [Medium] Mobile chip mislabeled crashed agents as "needs input".
When an agent's activity is `exited` and the session is in the
action bucket, the agent has crashed — sending it a message
won't help, the user needs to restart. Split the label: exited
→ "crashed", blocked → "blocked".
2. [Low] ProjectMetric for Action used tone="error" (red), which
contradicts the favicon fix's rationale. The action bucket
catches routine review work (ci_failed, changes_requested)
that was previously orange/yellow severity; screaming red in
the project overview grid would have the same cry-wolf problem
as the favicon did. Use tone="orange" (the less severe of the
two merged buckets) to match.
* refactor(web): make attentionZones required in useSessionEvents (#1201)
Cursor Bugbot caught that the hook default of "detailed" mismatches
the server SSE route default of "simple" — a latent footgun for any
future caller that forgets to pass the mode explicitly. The exact
oscillation bug I already fixed once for PullRequestsPage would
silently come back on the next page that uses this hook.
Real fix: refactor the hook to take an options object and make
`attentionZones` required with no default. TypeScript now enforces
that every caller explicitly passes the mode the server is using.
You literally cannot call useSessionEvents without supplying it.
Also cleaner: the hook had 6 positional parameters, which is past
the point where an options object helps readability. Dashboard and
PullRequestsPage call sites now read as self-documenting.
* fix(web): address human code review on attention zones (#1201)
Review from i-trytoohard on commit 8d27db79:
1. [Medium] `ZoneCounts` in sessions/[id]/page.tsx had an `action`
field that was always 0 — the page always computes in detailed
mode (getAttentionLevel with no mode = detailed), and the
consumer (`OrchestratorZones` in SessionDetail) never reads
an `action` field either. Removed the dead field, added a
`continue` guard for the never-reached "action" case so
TypeScript narrows the index correctly, and documented the
intent in a header comment.
2. [Medium] The simple-mode action chip had 10+ sub-conditions
with no tests. Extracted the label logic into a pure
`getActionChipLabel(session)` helper and added 16 unit tests
covering every branch: status-based signals (needs_input,
stuck, errored, ci_failed, changes_requested), activity-based
(waiting_input, exited→crashed, blocked), PR-based (failing
CI, changes_requested, conflicts), precedence between signal
classes, and the generic fallback.
3. [Low] `LEVEL_LABELS.action` in ProjectSidebar is dead code
today (sidebar uses detailed mode). Added a comment explaining
why the entry still exists (TypeScript exhaustiveness on
`Record<AttentionLevel, string>` + forward-compat).
* fix(web): address inline review on attention zones (#1201)
- Reorder getActionChipLabel to mirror getDetailedAttentionLevel: respond-class
activity (waiting_input/exited/blocked) now outranks review-class status
(ci_failed/changes_requested). A crashed agent with changes_requested no
longer reads as "changes" and hides the crash.
- Paint data-level="action" dot with --color-accent-orange to match the
favicon's yellow-severity treatment of the collapsed bucket.
- Tighten attentionLevel on the mux boundary (mux-protocol, mux-websocket,
useSessionEvents) from string to AttentionLevel so invalid values like "none"
no longer launder through into DynamicFavicon's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Keep reconstructed activity and activitySignal coherent, preserve stronger lifecycle states when repeated polls only surface weak evidence, and switch release notes from manual changelog edits to a Changesets entry. This also stabilizes the respond/working path investigated in issue #126.
Represent missing activity probes as first-class signal states so lifecycle inference only treats valid idle evidence as proof. This prevents false stuck transitions, keeps API/UI lifecycle truth aligned, and makes root monorepo verification deterministic by serializing recursive build and typecheck.
* fix: prevent sidebar empty-state flash on session pages (#1230)
* fix: reuse sidebar data across session navigation (#1230)
* fix: revalidate cached sidebar data on session pages (#1230)
* fix: harden session sidebar live updates (#1230)
* fix: remove sidebarSessions from mux effect deps
Read from the module-level cachedSidebarSessions cache instead so the
mux effect only re-fires when new mux data arrives, not on every
sidebar state change. Addresses review feedback on #1230.
* fix: address PR #1232 review comments on sidebar session handling
- Guard against out-of-order sidebar fetches with a per-invocation token
so concurrent pollers/retries don't overwrite fresh responses with stale ones.
- Gate mux overlay on `mux.status === "connected"`; clear pending patches on
disconnect so a stale WS snapshot doesn't shadow fresher REST data.
- Surface a stale-data banner in the sidebar when a refresh fails but cached
sessions are still rendered, so users see the failure instead of silently
viewing outdated state.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Preserve the split lifecycle observability and UI truth model while retaining the latest sessions-redone PR transition handling and dependency updates.
Implement the Stage 5 rollout so API and UI consumers stop depending on one overloaded status and operators can see structured transition evidence without breaking older session metadata.
* fix Next.js build warnings, pin flatted override, regenerate lockfile
* fix ignoreDuringBuilds, add ao-core to serverExternalPackages, clean dist-server in prebuild
* fix ESLint plugin detection, remove ao-core from transpilePackages, suppress plugin-registry webpack warning
* fix(web): restore ao-core to transpilePackages, document overrides, fix dev:optimized cleanup
Move @aoagents/ao-core back to transpilePackages only. dist/ is gitignored so fresh
checkout has no dist/index.js; serverExternalPackages causes a hard crash at runtime.
Client components (SessionDetail, ProjectSidebar, sessions/[id]/page) also import
@aoagents/ao-core/types which serverExternalPackages does not cover.
Keep @composio/core in serverExternalPackages only with comment explaining it is an
optional transitive dep via tracker-linear dynamic import.
Add _overrides_rationale to document axios (SSRF CVE-2023-45857) and flatted
(prototype-pollution) security pins in root package.json.
Fix dev:optimized to clean both .next and dist-server, matching what prebuild does.
* fix: webpackIgnore dynamic imports in plugin-registry fixes#1056
* fix: move Next.js ESLint config to packages/web for build-time detection fixes#1058
* fix: remove scope creep and phantom agent-soma from lockfile
- remove agent-soma importer block from pnpm-lock.yaml
- remove @composio/core from serverExternalPackages
- remove redundant exprContextCritical webpack override
- remove flatted override and _overrides_rationale
- align @next/eslint-plugin-next to ^15.5.15
* fix: replace rm -rf with rimraf for cross-platform compatibility
* fix: use rimraf in clean script and sort devDependencies
* fix: add postcss.config.mjs to web ESLint ignores
* fix: restore no-console exemption for web package in root ESLint config