Skip to content

Refine Work session lifecycle and sidebar - #973

Merged
arul28 merged 2 commits into
mainfrom
t3code/refine-ade-chat-sidebar
Jul 31, 2026
Merged

Refine Work session lifecycle and sidebar#973
arul28 merged 2 commits into
mainfrom
t3code/refine-ade-chat-sidebar

Conversation

@arul28

@arul28 arul28 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • modernize the Work sidebar selection, lane headers, full-bleed session rows, grouped menus, and delayed hover details
  • align settle/snooze/status behavior across desktop, Attention, CLI/TUI, push Live Activities, and iOS
  • add cross-machine routing safeguards, native parity, internal docs, and focused regression coverage

Validation

  • /quality dual-review: clean after fixes
  • desktop affected tests: 761 passed
  • ADE CLI/TUI affected tests: 696 passed
  • desktop shard 1/8: 1,334 passed
  • desktop + ADE CLI typechecks passed
  • iOS changed Swift files parse cleanly
  • internal docs validation passed

Summary by CodeRabbit

  • New Features
    • Search recent threads directly from the command palette, including remote sessions and project-aware navigation.
    • Added session lifecycle banners with wake and unsettle actions.
    • Added dynamic snooze choices, including “next week,” with clear wake-time details.
    • Redesigned session rows with hover details, status actions, lane context menus, and pull request badges.
    • Added reusable lane actions, improved lane coloring, and quiet session shelves.
  • Behavior Updates
    • Agents can no longer settle or unsettle sessions; completion is reported with notes.
    • User-stopped CLI runs no longer appear as failures.
    • Status language now consistently uses “Working,” “Done,” and “Stale.”

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jul 31, 2026 12:16am

@arul28 arul28 changed the title Primary -> main Refine Work session lifecycle and sidebar Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@arul28, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 34bf1e38-a172-49f8-a25a-082e5d3f4769

📥 Commits

Reviewing files that changed from the base of the PR and between 1e57088 and fd32664.

📒 Files selected for processing (21)
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/desktop/resources/ade-cli-help.txt
  • apps/desktop/src/main/services/lanes/laneService.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/renderer/components/app/CommandPalette.test.tsx
  • apps/desktop/src/renderer/components/app/commandPaletteThreads.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatSurfaceShell.tsx
  • apps/desktop/src/renderer/components/settings/ChatAppearancePreview.tsx
  • apps/desktop/src/renderer/components/terminals/LaneActionsSubmenu.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionHoverCard.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.tsx
  • apps/desktop/src/renderer/components/ui/MenuSubmenu.tsx
  • apps/desktop/src/shared/laneColorPalette.ts
  • apps/ios/ADE/Views/AttentionDrawer/AttentionDrawerModel.swift
  • apps/ios/ADETests/AttentionDrawerModelTests.swift
  • apps/ios/ADEWidgets/ADEAgentActivityWidget.swift
📝 Walkthrough

Walkthrough

Changes

Session lifecycle and authorization

Layer / File(s) Summary
Settlement authorization and CLI removal
apps/ade-cli/..., apps/desktop/src/main/services/adeActions/..., apps/desktop/src/shared/...
Agent self-settlement commands and actions were removed; user or CTO-gated settlement remains supported.
Dynamic snoozing and stopped-run handling
apps/ade-cli/src/tuiClient/..., apps/ade-cli/src/services/push/..., apps/desktop/src/renderer/lib/sessionSnooze.ts
Snooze choices now resolve by time, and user-stopped CLI exits are excluded from completion and failure notifications.

Command palette threads

Layer / File(s) Summary
Thread indexing and palette integration
apps/desktop/src/renderer/components/app/...
Recent local and remote threads are indexed, ranked, rendered before other results, deduplicated, and routed through project bindings.

Desktop work surface

Layer / File(s) Summary
Canonical attention and lifecycle presentation
apps/desktop/src/renderer/components/attention/..., apps/desktop/src/renderer/components/chat/..., apps/desktop/src/renderer/lib/...
Desktop status presentation distinguishes done, blocked, stale, snoozed, settled, and needs-you states, with lifecycle banners in chat.
Shared lane menus and colors
apps/desktop/src/renderer/components/lanes/..., apps/desktop/src/renderer/components/terminals/LaneActionsSubmenu.tsx
Lane actions, submenus, color allocation, Primary-lane accents, and copy/navigation actions are centralized.
Session rows, menus, and shelves
apps/desktop/src/renderer/components/terminals/...
Session cards, hover cards, status slots, context menus, singleton lanes, quiet shelves, PR metadata, and cross-machine routing were expanded or redesigned.

iOS presentation

Layer / File(s) Summary
Unified status and snooze semantics
apps/ios/ADE/..., apps/ios/ADEWidgets/..., apps/ios/ADETests/...
iOS adopts Working, Done, Stale, and Blocked terminology, revised prominence and tint rules, and shared dynamic snooze options.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Possibly related PRs

  • arul28/ADE#886: Introduced the agent self-settlement actions removed by this change.
  • arul28/ADE#909: Overlaps the session settlement authorization and settleSession/unsettleSession surfaces.
  • arul28/ADE#923: Overlaps session lifecycle UI, multi-machine routing, and terminal components.

Suggested labels: desktop, ios

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the pull request’s primary focus on refining Work session lifecycle behavior and the sidebar UI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/refine-ade-chat-sidebar

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/src/main/services/lanes/laneService.ts (1)

2258-2291: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Enforce the reserved primary color in updateAppearance.

This backfill/insert makes Primary purple, but updateAppearance can later set its color to any value. Reject or normalize primary-lane color updates so the stated cross-surface invariant persists.

Proposed fix
 const normalizedColor = color === undefined ? lane.color : color;
+if (
+  lane.lane_type === "primary"
+  && normalizedColor !== PRIMARY_LANE_COLOR
+) {
+  throw new Error("Primary lane color is fixed.");
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/lanes/laneService.ts` around lines 2258 -
2291, Update updateAppearance to enforce PRIMARY_LANE_COLOR whenever the target
lane is a primary lane: reject requested color changes or normalize them to the
reserved color before persisting. Preserve the existing appearance-update
behavior for non-primary lanes and ensure any primary update cannot store
another color.

Source: Coding guidelines

apps/ade-cli/src/cli.ts (1)

6959-7014: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

chat settle/chat unsettle fall through to a generic, unhelpful error instead of the documented guidance.

Unlike buildSessionPlan, which explicitly rejects removed subcommands with a clear CliUsageError (Line 6931: `Unknown session subcommand '${sub}'. Try: show, snooze, wake, clear-woke.`), buildChatPlan no longer has any case for "settle"/"unsettle". They fall through to the generic catch-all at Lines 7847-7851 and get dispatched as domain "chat", action "settle"/"unsettle" — actions that don't exist (settlement now lives only under the "session" domain). Users get an opaque "unknown action" failure instead of the help text you just wrote at Lines 1715-1717 ("'chat settle' / 'chat unsettle' were removed: ... report your outcome with 'chat note'").

🐛 Proposed fix: explicit rejection mirroring buildSessionPlan
+  if (sub === "settle" || sub === "unsettle") {
+    throw new CliUsageError(
+      "'chat settle' / 'chat unsettle' were removed: only the user (or a merged PR) settles a session — report your outcome with 'chat note'.",
+    );
+  }
   if (sub === "list" || sub === "ls") {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/cli.ts` around lines 6959 - 7014, Update buildChatPlan to
explicitly reject the removed "settle" and "unsettle" subcommands with a
CliUsageError containing the documented guidance to use "chat note". Place this
handling before the generic chat action fallback, while preserving existing
behavior for all supported subcommands.
🧹 Nitpick comments (10)
apps/desktop/src/shared/laneColorPalette.ts (1)

54-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Normalize both sides of the reserved-colour comparison.

The filter only excludes purple because PRIMARY_LANE_COLOR happens to be written lowercase; changing the constant's casing would silently return purple to the allocation pool and break the "purple always means Primary" invariant this module documents.

🛡️ Proposed hardening
-export const ALLOCATABLE_LANE_COLORS: readonly LaneColor[] = LANE_COLOR_PALETTE
-  .filter((entry) => entry.hex.toLowerCase() !== PRIMARY_LANE_COLOR);
+const PRIMARY_LANE_COLOR_KEY = PRIMARY_LANE_COLOR.toLowerCase();
+export const ALLOCATABLE_LANE_COLORS: readonly LaneColor[] = LANE_COLOR_PALETTE
+  .filter((entry) => entry.hex.toLowerCase() !== PRIMARY_LANE_COLOR_KEY);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/shared/laneColorPalette.ts` around lines 54 - 55, Update the
filter in ALLOCATABLE_LANE_COLORS to normalize PRIMARY_LANE_COLOR as well as
entry.hex before comparing them, preserving the exclusion of the reserved
primary color regardless of the constant’s casing.
apps/desktop/src/renderer/components/terminals/SessionHoverCard.tsx (1)

215-215: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Also release the module-level handoff entry on unmount.

Rows in this list unmount constantly (re-sort, filter, collapse). If a row unmounts while it owns activeHoverCard, the module keeps a closure over the dead hook plus a detached trigger node until the next hover.

♻️ Proposed cleanup
-  React.useEffect(() => clearTimers, [clearTimers]);
+  React.useEffect(() => () => {
+    clearTimers();
+    if (activeHoverCard?.rowId === rowId) activeHoverCard = null;
+  }, [clearTimers, rowId]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/terminals/SessionHoverCard.tsx` at line
215, Update the cleanup effect in the hover-card hook to release the
module-level activeHoverCard handoff when its owning row unmounts, but only if
that entry belongs to the current hook/trigger. Preserve the existing
clearTimers cleanup and avoid clearing a newer row’s active handoff.
apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx (1)

25-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Recorder accumulates across tests and cardPropsFor returns the first, not the latest, props.

sessionCardPropsForTest is only truncated inside two describes, and .find contradicts the doc comment ("most recent render pass") — a re-render in the same test makes the assertion read the stale prop set. Clearing it in the shared afterEach and using findLast removes the trap without touching any current expectation.

♻️ Proposed tightening
 function cardPropsFor(sessionId: string): Record<string, unknown> | undefined {
-  return sessionCardPropsForTest.find(
+  return sessionCardPropsForTest.findLast(
     (props) => (props.session as TerminalSessionSummary | undefined)?.id === sessionId,
   );
 }

Plus sessionCardPropsForTest.length = 0; in each afterEach.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx`
around lines 25 - 47, Update the shared test cleanup for sessionCardPropsForTest
so it is cleared in the common afterEach rather than only within individual
describe blocks. Change cardPropsFor to return the last matching session props
from the most recent render, using findLast or equivalent, while preserving its
existing session-id filtering behavior.
apps/desktop/src/renderer/components/terminals/SessionListPane.tsx (2)

961-980: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Re-playing a synthetic keydown to open the palette is a renderer-only workaround.

This depends on AppShell's listener being on window, in the bubble phase, un-swallowed by anything upstream, and on its matcher accepting a synthesized event — none of which this file can guarantee. Exposing an openCommandPalette action (store or context) alongside AppShell's key handler removes the coupling and keeps the shortcut chip logic here purely cosmetic.

As per coding guidelines, "prefer fixing the underlying service or shared type rather than adding renderer-only workarounds".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/terminals/SessionListPane.tsx` around
lines 961 - 980, Replace the synthetic keydown workaround in openCommandPalette
with the shared command-palette open action exposed by AppShell’s existing
state, store, or context alongside its keyboard handler. Update the search
button to invoke that action directly, and keep commandPaletteBinding only for
displaying the shortcut chip rather than parsing or dispatching keyboard events.

Source: Coding guidelines


152-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the keybinding combo parser for shortcut chips.

parsePrimaryCombo in SessionListPane.tsx re-implements the same Mod/platform resolution already in lib/keybindings, and it even still uses deprecated navigator.platform. Export a small combo resolver from lib/keybindings and use it here so chips and matching always come from one source of truth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/terminals/SessionListPane.tsx` around
lines 152 - 196, Replace the local parsePrimaryCombo implementation in
SessionListPane with a small exported combo resolver from lib/keybindings that
performs the existing Mod/platform resolution without deprecated
navigator.platform usage. Import and reuse that resolver in shortcutChipLabel,
preserving the current chip formatting while ensuring display and matching share
one source of truth.
apps/desktop/src/renderer/components/lanes/laneContextMenuItems.tsx (1)

493-500: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Swatch buttons are non-menuitem children of a role="menu" container.

LaneMenuGroups renders this custom node directly inside the menu (and inside the MenuSubmenu panel), so AT sees plain buttons as invalid menu children. Consider role="menuitemradio" with aria-checked for the swatches and role="menuitem" for the clear button.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/lanes/laneContextMenuItems.tsx` around
lines 493 - 500, Update the swatch buttons rendered by LaneMenuGroups to use
role="menuitemradio" with aria-checked reflecting isSelected, and update the
clear button to use role="menuitem", ensuring all direct children of the
role="menu" and MenuSubmenu panel have valid menu-item semantics.
apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx (1)

89-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the duplicated entry renderer.

The submenu and inline branches map entries with byte-identical logic; one helper keeps them from drifting.

♻️ Proposed refactor
+function renderEntry(entry: LaneMenuGroup["entries"][number]) {
+  if (entry.kind === "custom") {
+    return <React.Fragment key={entry.key}>{entry.node}</React.Fragment>;
+  }
+  return (
+    <HoverButton
+      key={entry.key}
+      style={menuItemStyle}
+      dataTour={entry.dataTour}
+      onClick={entry.onSelect}
+    >
+      {entry.label}
+    </HoverButton>
+  );
+}

Then use {group.entries.map(renderEntry)} in both branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx` around lines
89 - 120, Extract the duplicated entry-mapping JSX into a shared renderEntry
helper near the LaneContextMenu component, preserving the existing custom
Fragment and HoverButton behavior, keys, props, and handlers. Replace both
group.entries.map callbacks in the submenu and inline branches with
group.entries.map(renderEntry).
apps/desktop/src/renderer/components/ui/MenuSubmenu.tsx (1)

233-237: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Panel declares role="menu" but its children are plain <button>s.

ARIA requires a menu to contain menuitem/menuitemcheckbox/menuitemradio children; the session menu passes bare buttons (only LaneActionsSubmenu sets role="menuitem"). Screen readers then report a menu with zero items, and the arrow-key model implemented in moveFocus has no ARIA counterpart. Either apply the role in the panel (wrap children or document the requirement) or drop role="menu" and expose the panel as a plain group.

Also, focusableItems only matches button:not([disabled]), so any anchor or input a consumer places in a panel is skipped by arrow navigation.

Also applies to: 271-274

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/ui/MenuSubmenu.tsx` around lines 233 -
237, Update MenuSubmenu’s panel semantics and focus management: either ensure
every supported child receives an appropriate menuitem role while preserving the
existing menu keyboard model, or remove role="menu" and expose the panel as a
plain group. Expand focusableItems beyond enabled buttons so anchors and other
supported interactive descendants are included in moveFocus, while still
excluding disabled elements.
apps/desktop/src/renderer/components/terminals/TerminalsPage.test.tsx (1)

665-702: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Store vs. hook lane/focus mocks are the same function, so these two assertions can't tell the paths apart.

baseWork spreads ...fns, so workMocks.currentWork.selectLane === workMocks.fns.selectLane (same for focusSession). Lines 699-700 therefore pass whether the handler used selectLaneInStore/focusSessionInStore or work.selectLane/work.focusSession — which is exactly the distinction this test exists to pin down. Give the store selectors their own vi.fn()s.

♻️ Sketch
   const fns = {
-    selectLane: vi.fn(),
-    focusSession: vi.fn(),
+    selectLane: vi.fn(),        // hook (work.*)
+    focusSession: vi.fn(),
+    storeSelectLane: vi.fn(),   // app store
+    storeFocusSession: vi.fn(),

…and wire selectLane: workMocks.fns.storeSelectLane / focusSession: workMocks.fns.storeFocusSession into the useAppStore selector object, asserting on those in this test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/terminals/TerminalsPage.test.tsx` around
lines 665 - 702, Update the TerminalsPage test mocks so store selectors use
distinct vi.fn() instances from the hook functions: add dedicated
storeSelectLane and storeFocusSession mocks, wire them into the useAppStore
selector object, and assert the foreign-session handler calls those store mocks
while preserving the existing hook-call assertions.
apps/ade-cli/src/tuiClient/sessionLifecycle.ts (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the direct desktop renderer import for snooze presets.

resolveSnoozePresets is imported from ../../desktop/src/renderer/lib/sessionSnooze, and this already mixes renderer-only code into a CLI target. If the CLI needs these utils, move/reexport them from a shared package or keep an own copy instead of importing through apps/desktop/src/renderer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/tuiClient/sessionLifecycle.ts` at line 2, Remove the direct
renderer import of resolveSnoozePresets from the session lifecycle module.
Provide the CLI with this utility through a shared package or a local CLI-safe
implementation, ensuring no dependency on apps/desktop/src/renderer remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/adeRpcServer.test.ts`:
- Around line 1584-1591: Extend the denial matrix loop containing settleAttempt
cases to include the CTO-only bulk writer action unsettleSessions, using the
appropriate sessionIds argument for chat-1. Preserve the existing assertions and
coverage for the other settlement actions.

In `@apps/desktop/resources/ade-cli-help.txt`:
- Around line 51-53: Replace “File a session's lifecycle” with “Manage a
session's lifecycle” in all five generated command index entries:
apps/desktop/resources/ade-cli-help.txt lines 51-53, 188-190, 747-749, 947-949,
and 1362-1364.

In `@apps/desktop/src/renderer/components/app/commandPaletteThreads.tsx`:
- Around line 444-460: Remove the aria-hidden attribute from the
ThreadOverflowNote list item so assistive technologies can announce the “Showing
shown of total threads” status. Keep the existing non-focusable li structure,
styling, and conditional rendering unchanged.
- Around line 342-349: Update the contextParts construction near the
secondary-line logic to use the entry’s routing-bound project displayName for
foreign rows instead of the current tab’s projectName. Preserve the existing
project/branch/lane ordering and fallback behavior, while ensuring cross-machine
threads are labeled with their own project identity.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Around line 11786-11791: Update the lifecycleBanner conditional and
ChatLifecycleBanner sessionId prop to use the resolved composerSessionId instead
of selectedSessionId, matching the existing composer control targeting behavior
during chat switches.

In `@apps/desktop/src/renderer/components/chat/ChatSurfaceShell.tsx`:
- Around line 10-11: Move the h-8 height utility out of CHAT_SHELL_HEADER_CLASS
so the shell wrapper retains only horizontal padding and the existing spacing
behavior. Apply h-8 directly to the WorkSurfaceHeader element, while preserving
the wrapper’s space-y-1 (or equivalent) gap when the session tabs row is
rendered.

In `@apps/desktop/src/renderer/components/terminals/LaneActionsSubmenu.tsx`:
- Around line 69-77: Add role="menuitem" to the LaneActionsSubmenu MenuSubmenu
trigger, matching the role passed by LaneContextMenu and preserving the existing
fallback child behavior.
- Around line 53-66: Update useLaneMenuActions and the LaneMenuActions type to
expose an onAppearanceChanged refresh callback, include it in the hook’s
memoization dependencies, and pass it through LaneActionsSubmenu into
buildLaneMenuGroups so the color swatch receives the callback and refreshes
after selection.

In `@apps/desktop/src/renderer/components/terminals/SessionHoverCard.tsx`:
- Around line 204-213: Track the delayed-open state with a pending-open state
value: set it when the open timer is scheduled, and clear it in clearTimers and
open. Update the scroll/resize effect guard to depend on this state so listeners
are armed during the delay, while preserving cancellation for already-open
cards.
- Around line 318-344: Make SessionHoverCard actions keyboard reachable by
adding an assistive-technology-discoverable keyboard path from the session row,
rather than relying only on the hover-triggered tooltip. Update the relevant
session-row keyboard handler to invoke the same action used by row.onActivate,
and ensure the activated control is discoverable without relying on the inner
tabIndex={-1} button or pointer hover.

In `@apps/desktop/src/renderer/components/ui/MenuSubmenu.tsx`:
- Around line 195-221: Update closeNow to restore focus to the submenu trigger
whenever the closing panel contains document.activeElement, including
pointer-timeout closures after keyboard opening. Preserve existing close
behavior and avoid moving focus when the panel does not currently own focus; use
the trigger reference already associated with the submenu.

In `@apps/ios/ADE/Views/AttentionDrawer/AttentionDrawerModel.swift`:
- Around line 636-644: Update the WorkspaceSnapshot fallback classification near
the local snapshot handling to map phase == "blocked" to .blocked instead of
.running, while preserving .stale for disconnected hosts and existing
classifications for other phases. Add a regression test covering a local
WorkspaceSnapshot with a blocked phase and verify it produces the neutral
blocked presentation.

In `@apps/ios/ADEWidgets/ADEAgentActivityWidget.swift`:
- Around line 288-304: Update the headline logic near activeCount and the
primary-agent selection so completed runs are not counted as active work. Derive
the count from genuinely in-flight phases such as starting or running, or
preserve the host-reported active count separately, then ensure the zero-active
path at the headline generation logic produces the completed/result wording
instead of “1 agent working.”

---

Outside diff comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 6959-7014: Update buildChatPlan to explicitly reject the removed
"settle" and "unsettle" subcommands with a CliUsageError containing the
documented guidance to use "chat note". Place this handling before the generic
chat action fallback, while preserving existing behavior for all supported
subcommands.

In `@apps/desktop/src/main/services/lanes/laneService.ts`:
- Around line 2258-2291: Update updateAppearance to enforce PRIMARY_LANE_COLOR
whenever the target lane is a primary lane: reject requested color changes or
normalize them to the reserved color before persisting. Preserve the existing
appearance-update behavior for non-primary lanes and ensure any primary update
cannot store another color.

---

Nitpick comments:
In `@apps/ade-cli/src/tuiClient/sessionLifecycle.ts`:
- Line 2: Remove the direct renderer import of resolveSnoozePresets from the
session lifecycle module. Provide the CLI with this utility through a shared
package or a local CLI-safe implementation, ensuring no dependency on
apps/desktop/src/renderer remains.

In `@apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx`:
- Around line 89-120: Extract the duplicated entry-mapping JSX into a shared
renderEntry helper near the LaneContextMenu component, preserving the existing
custom Fragment and HoverButton behavior, keys, props, and handlers. Replace
both group.entries.map callbacks in the submenu and inline branches with
group.entries.map(renderEntry).

In `@apps/desktop/src/renderer/components/lanes/laneContextMenuItems.tsx`:
- Around line 493-500: Update the swatch buttons rendered by LaneMenuGroups to
use role="menuitemradio" with aria-checked reflecting isSelected, and update the
clear button to use role="menuitem", ensuring all direct children of the
role="menu" and MenuSubmenu panel have valid menu-item semantics.

In `@apps/desktop/src/renderer/components/terminals/SessionHoverCard.tsx`:
- Line 215: Update the cleanup effect in the hover-card hook to release the
module-level activeHoverCard handoff when its owning row unmounts, but only if
that entry belongs to the current hook/trigger. Preserve the existing
clearTimers cleanup and avoid clearing a newer row’s active handoff.

In `@apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx`:
- Around line 25-47: Update the shared test cleanup for sessionCardPropsForTest
so it is cleared in the common afterEach rather than only within individual
describe blocks. Change cardPropsFor to return the last matching session props
from the most recent render, using findLast or equivalent, while preserving its
existing session-id filtering behavior.

In `@apps/desktop/src/renderer/components/terminals/SessionListPane.tsx`:
- Around line 961-980: Replace the synthetic keydown workaround in
openCommandPalette with the shared command-palette open action exposed by
AppShell’s existing state, store, or context alongside its keyboard handler.
Update the search button to invoke that action directly, and keep
commandPaletteBinding only for displaying the shortcut chip rather than parsing
or dispatching keyboard events.
- Around line 152-196: Replace the local parsePrimaryCombo implementation in
SessionListPane with a small exported combo resolver from lib/keybindings that
performs the existing Mod/platform resolution without deprecated
navigator.platform usage. Import and reuse that resolver in shortcutChipLabel,
preserving the current chip formatting while ensuring display and matching share
one source of truth.

In `@apps/desktop/src/renderer/components/terminals/TerminalsPage.test.tsx`:
- Around line 665-702: Update the TerminalsPage test mocks so store selectors
use distinct vi.fn() instances from the hook functions: add dedicated
storeSelectLane and storeFocusSession mocks, wire them into the useAppStore
selector object, and assert the foreign-session handler calls those store mocks
while preserving the existing hook-call assertions.

In `@apps/desktop/src/renderer/components/ui/MenuSubmenu.tsx`:
- Around line 233-237: Update MenuSubmenu’s panel semantics and focus
management: either ensure every supported child receives an appropriate menuitem
role while preserving the existing menu keyboard model, or remove role="menu"
and expose the panel as a plain group. Expand focusableItems beyond enabled
buttons so anchors and other supported interactive descendants are included in
moveFocus, while still excluding disabled elements.

In `@apps/desktop/src/shared/laneColorPalette.ts`:
- Around line 54-55: Update the filter in ALLOCATABLE_LANE_COLORS to normalize
PRIMARY_LANE_COLOR as well as entry.hex before comparing them, preserving the
exclusion of the reserved primary color regardless of the constant’s casing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e10003a4-06d0-4b2b-927c-ea9eca0c64e7

📥 Commits

Reviewing files that changed from the base of the PR and between 1b087ef and 1e57088.

⛔ Files ignored due to path filters (9)
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/ade-code/README.md is excluded by !docs/**
  • docs/features/agents/README.md is excluded by !docs/**
  • docs/features/cto/README.md is excluded by !docs/**
  • docs/features/lanes/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/ui-surfaces.md is excluded by !docs/**
  • docs/features/web-client/README.md is excluded by !docs/**
📒 Files selected for processing (91)
  • apps/ade-cli/README.md
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/services/push/pushPublisherService.test.ts
  • apps/ade-cli/src/services/push/pushPublisherService.ts
  • apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/sessionLifecycle.test.tsx
  • apps/ade-cli/src/tuiClient/adeApi.ts
  • apps/ade-cli/src/tuiClient/app.tsx
  • apps/ade-cli/src/tuiClient/commands.ts
  • apps/ade-cli/src/tuiClient/sessionLifecycle.ts
  • apps/desktop/resources/ade-cli-help.txt
  • apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md
  • apps/desktop/src/main/services/adeActions/registry.test.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkSystemPrompt.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/sessions/sessionService.ts
  • apps/desktop/src/main/services/usage/usageStatsStore.ts
  • apps/desktop/src/main/services/usage/usageTrackingService.test.ts
  • apps/desktop/src/preload/preload.test.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/app/CommandPalette.test.tsx
  • apps/desktop/src/renderer/components/app/CommandPalette.tsx
  • apps/desktop/src/renderer/components/app/commandPaletteSearch.tsx
  • apps/desktop/src/renderer/components/app/commandPaletteThreads.tsx
  • apps/desktop/src/renderer/components/attention/AttentionCenter.css
  • apps/desktop/src/renderer/components/attention/AttentionCenter.tsx
  • apps/desktop/src/renderer/components/attention/HeaderAttentionControl.css
  • apps/desktop/src/renderer/components/attention/HeaderAttentionControl.test.tsx
  • apps/desktop/src/renderer/components/attention/attentionHeaderSummary.ts
  • apps/desktop/src/renderer/components/attention/attentionPresentation.test.ts
  • apps/desktop/src/renderer/components/attention/attentionPresentation.ts
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatLifecycleBanner.test.tsx
  • apps/desktop/src/renderer/components/chat/ChatLifecycleBanner.tsx
  • apps/desktop/src/renderer/components/chat/ChatSurfaceShell.tsx
  • apps/desktop/src/renderer/components/lanes/LaneContextMenu.test.tsx
  • apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx
  • apps/desktop/src/renderer/components/lanes/laneColorPalette.ts
  • apps/desktop/src/renderer/components/lanes/laneContextMenuItems.tsx
  • apps/desktop/src/renderer/components/lanes/laneDesignTokens.ts
  • apps/desktop/src/renderer/components/lanes/laneUtils.test.ts
  • apps/desktop/src/renderer/components/terminals/LaneActionsSubmenu.tsx
  • apps/desktop/src/renderer/components/terminals/LanePrBadge.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionContextMenu.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionContextMenu.tsx
  • apps/desktop/src/renderer/components/terminals/SessionHoverCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionInfoPopover.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.tsx
  • apps/desktop/src/renderer/components/terminals/SessionSnoozeControl.tsx
  • apps/desktop/src/renderer/components/terminals/SessionStatusSlot.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalsPage.test.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx
  • apps/desktop/src/renderer/components/terminals/sessionLifecycleActions.ts
  • apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.ts
  • apps/desktop/src/renderer/components/ui/MenuSubmenu.tsx
  • apps/desktop/src/renderer/components/work/WorkSurfaceHeader.test.tsx
  • apps/desktop/src/renderer/components/work/WorkSurfaceHeader.tsx
  • apps/desktop/src/renderer/index.css
  • apps/desktop/src/renderer/lib/sessionSnooze.test.ts
  • apps/desktop/src/renderer/lib/sessionSnooze.ts
  • apps/desktop/src/renderer/lib/terminalAttention.test.ts
  • apps/desktop/src/renderer/lib/terminalAttention.ts
  • apps/desktop/src/renderer/webclient/shell/sessionLifecycleChrome.ts
  • apps/desktop/src/shared/adeCliGuidance.test.ts
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/laneColorPalette.ts
  • apps/desktop/src/shared/sessionCanonicalState.ts
  • apps/desktop/src/shared/sessionStatusPresentation.ts
  • apps/ios/ADE/Shared/ADEAgentActivityAttributes.swift
  • apps/ios/ADE/Shared/ADESharedModels.swift
  • apps/ios/ADE/Shared/ADESharedTheme.swift
  • apps/ios/ADE/Views/AttentionDrawer/AttentionDrawerModel.swift
  • apps/ios/ADE/Views/AttentionDrawer/AttentionDrawerSheet.swift
  • apps/ios/ADE/Views/Work/WorkRootComponents.swift
  • apps/ios/ADE/Views/Work/WorkSessionCanonicalState.swift
  • apps/ios/ADE/Views/Work/WorkSessionGrouping.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/ios/ADETests/AttentionDrawerModelTests.swift
  • apps/ios/ADETests/WorkSessionCanonicalStateTests.swift
  • apps/ios/ADEWidgets/ADEAgentActivityWidget.swift
  • apps/ios/ADEWidgets/ADELockScreenWidget.swift
💤 Files with no reviewable changes (1)
  • apps/ade-cli/README.md

Comment thread apps/ade-cli/src/adeRpcServer.test.ts
Comment thread apps/desktop/resources/ade-cli-help.txt Outdated
Comment thread apps/desktop/src/renderer/components/app/commandPaletteThreads.tsx Outdated
Comment thread apps/desktop/src/renderer/components/app/commandPaletteThreads.tsx
Comment thread apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
Comment thread apps/desktop/src/renderer/components/terminals/SessionHoverCard.tsx Outdated
Comment thread apps/desktop/src/renderer/components/terminals/SessionHoverCard.tsx
Comment thread apps/desktop/src/renderer/components/ui/MenuSubmenu.tsx
Comment thread apps/ios/ADE/Views/AttentionDrawer/AttentionDrawerModel.swift
Comment thread apps/ios/ADEWidgets/ADEAgentActivityWidget.swift
@arul28

arul28 commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@arul28
arul28 merged commit d96563c into main Jul 31, 2026
3 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd3266484e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +808 to +811
// The inline input is gone (search now opens the command palette), but the
// query itself still drives every filter in this file and the setter stays in
// the contract so the parent can keep restoring/clearing it.
setQ: _setQ,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear the retired sidebar query before hiding its setter

When an upgrading user has a nonempty persisted WorkProjectViewState.search, useWorkSessions still applies that value to local sessions and this component also applies q to foreign rows, but the removed input was the only UI that called setQ. Opening the command palette does not change this separate persisted query, so matching sessions remain silently hidden—potentially behind a misleading “No sessions” state—with no way to clear the filter in Work. Clear/migrate the old value or retain an active-filter indicator and reset action.

Useful? React with 👍 / 👎.

Comment on lines +181 to +182
let hiddenActiveCount = max(0, safeState.activeCount - safeState.runs.count)
let activeCount = hiddenActiveCount + visibleInFlightCount

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve omitted working runs in the widget count

When the publisher has more than three runs and a recent completed/failed/waiting row occupies one of the capped runs slots, subtracting the entire visible roster from activeCount loses omitted working runs. For example, four working runs plus one newly completed run produce activeCount == 4 and a three-row roster containing the completion and two working runs; this computes only three working runs. The payload needs a working-count field or the hidden remainder must be derived from visible entries that actually contribute to the producer's count, rather than from runs.count.

Useful? React with 👍 / 👎.

@arul28
arul28 deleted the t3code/refine-ade-chat-sidebar branch August 6, 2026 22:40
arul28 added a commit that referenced this pull request Aug 10, 2026
Four independent fixes from the t3code competitor audit (P0 items 3 and 7).

Renderer crash recovery (main.ts). `render-process-gone` only logged, so a
dead renderer meant a permanent white screen while the agents behind it kept
running — recoverable only by restarting ADE. It now reloads the canonical
renderer URL after 500 ms. Renderer state rehydrates from the main process, so
a reload costs a repaint, not data. The retry budget (3 per rolling 60 s) lives
in a separate testable module because the failure it guards against and the
failure it could cause are the same shape: a renderer that dies during boot
would otherwise reload-loop forever. Recovery covers every reason except
`clean-exit`, including `killed`.

Work grid membership cap. Every grid tile renders a full live session surface
with `terminalVisible`, and membership was unbounded — N tiles meant N whole
AgentChatPanes against a ~4 GB renderer heap, which is what made a renderer OOM
reachable in normal use. Capped at 6, enforced in the membership helper, at the
drop layer (a full grid stops advertising its drop target, so no phantom
affordance), and in the persisted-state normalizer so a set saved by an older
build is trimmed on load rather than rebuilt.

PR-merge auto-settlement blast radius. A merged PR with no declared
`chatSessionIds` swept every non-settled chat in its lane, and that path
deliberately bypasses settlement blockers — so one merge could file chats
belonging to a different, still-open PR. PRs opened outside ADE legitimately
arrive with no links, so the sweep is kept but bounded: it is skipped entirely
when another PR in the lane is still live (ownership is genuinely ambiguous —
that PR's own merge should file its work), and it never touches a session
another PR explicitly claims. An explicit link still wins outright.

AI review-resolver diff context. The `prRefreshIssueInventory` handoff dropped
`diffHunk`, so the model addressed review comments having never seen the code
they point at. Restored per thread and capped at 2 KB, trimmed from the front:
a diff hunk ends at the commented line, so the tail is the part the comment is
about.

Also removes `getSettlementBlockers`. Its only consumer was the
`settleSelfSession` agent action, deleted in #973 on the explicit product
decision that agents must not file their own Work rows. With no caller it read
as "settlement consults blockers" when nothing does; that rationale now lives
where the types were.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 10, 2026
* Recover a crashed renderer; bound grid heap; fix two PR-path defects

Four independent fixes from the t3code competitor audit (P0 items 3 and 7).

Renderer crash recovery (main.ts). `render-process-gone` only logged, so a
dead renderer meant a permanent white screen while the agents behind it kept
running — recoverable only by restarting ADE. It now reloads the canonical
renderer URL after 500 ms. Renderer state rehydrates from the main process, so
a reload costs a repaint, not data. The retry budget (3 per rolling 60 s) lives
in a separate testable module because the failure it guards against and the
failure it could cause are the same shape: a renderer that dies during boot
would otherwise reload-loop forever. Recovery covers every reason except
`clean-exit`, including `killed`.

Work grid membership cap. Every grid tile renders a full live session surface
with `terminalVisible`, and membership was unbounded — N tiles meant N whole
AgentChatPanes against a ~4 GB renderer heap, which is what made a renderer OOM
reachable in normal use. Capped at 6, enforced in the membership helper, at the
drop layer (a full grid stops advertising its drop target, so no phantom
affordance), and in the persisted-state normalizer so a set saved by an older
build is trimmed on load rather than rebuilt.

PR-merge auto-settlement blast radius. A merged PR with no declared
`chatSessionIds` swept every non-settled chat in its lane, and that path
deliberately bypasses settlement blockers — so one merge could file chats
belonging to a different, still-open PR. PRs opened outside ADE legitimately
arrive with no links, so the sweep is kept but bounded: it is skipped entirely
when another PR in the lane is still live (ownership is genuinely ambiguous —
that PR's own merge should file its work), and it never touches a session
another PR explicitly claims. An explicit link still wins outright.

AI review-resolver diff context. The `prRefreshIssueInventory` handoff dropped
`diffHunk`, so the model addressed review comments having never seen the code
they point at. Restored per thread and capped at 2 KB, trimmed from the front:
a diff hunk ends at the commented line, so the tail is the part the comment is
about.

Also removes `getSettlementBlockers`. Its only consumer was the
`settleSelfSession` agent action, deleted in #973 on the explicit product
decision that agents must not file their own Work rows. With no caller it read
as "settlement consults blockers" when nothing does; that rationale now lives
where the types were.

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

* Put the mobile sync wire on a diet

Two changes that cut what a phone downloads, both measured against this
machine's live data (P0 items 1 and 2).

Chat events crossed the wire uncompacted. `commitChatEvent` built two
envelopes: the stored one went through `compactChatEventForStorage`, the live
one handed to `eventSubscribers` — which is what the sync host pushes to phones
and web — went out whole, transformed only by inline-image redaction. The same
event was therefore multi-megabyte on live push and small after reconnect
hydration, a size change users could observe.

The deeper defect was two compaction implementations with two cap tables, which
drifted the way duplicated policy does. `tool_result.structured` was added to
the event and to neither table. On a real 8 MB transcript it had grown to
4.53 MB — 56.6% of the entire file, and ten times larger than `result`, the
field that IS capped. So this collapses both into shared/chatEventCompaction,
where a new heavy field cannot be capped on one side and forgotten on the other.

`structured` is now bounded on disk and dropped from the wire outright, along
with `toolResultMeta`. Everything ADE reads from `structured` (grep totals, bash
timeout and cwd hints, subagent enrichment) is projected into typed fields on
the same event at construction time; past that point nothing consumes it.
`structured` is not a coding key in the iOS decoder at all, and `toolResultMeta`
is written once and never read on any surface — the phone was downloading and
parsing megabytes to produce something it discarded. Removing a field no client
decodes is backward-compatible by construction, so this needs no capability
gate; anything that added or reshaped a field would.

Measured on a real thread, replaying every event through the new wire path:
7.99 MB → 3.46 MB overall, and tool_result 5.10 MB → 0.57 MB across 356
records. That transcript is already storage-compacted, so the live-push saving
is larger than the 2.3x shown.

`pull_request_snapshots` no longer rides the mobile changeset pump. It was
10.65 MB of a 26.8 MB synced project database — 39.7%, 258 rows averaging 42 KB,
one `files_json` at 1.58 MB — for data the phone already fetched a second time
itself. iOS reads the table in exactly one SELECT, the per-PR detail query, and
reaches that data on demand through `prs.refresh`, which is in the REQUIRED
remote-command set, so no paired build loses PR detail. Lists and badges are
unaffected: slim `pull_requests` rows still sync, and though four iOS
projections name the table as an invalidation trigger, no projection query reads
a column from it. Already-paired devices keep the rows they have, so
previously-opened PRs still render offline. It also ends a scroll-driven write
path — the Lanes page's visible-lane refresh upserts here, so scrolling was
pushing changesets to every phone.

Note for the record: the research report said the `github_pr_*` siblings were
already excluded and this table was merely overlooked. They are not excluded;
the set had no PR tables at all.

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

* Get iOS off the main actor on the streaming path, and stop trusting dead sockets

Three iOS fixes from the t3code audit (P0 items 4, 5, 6).

Chat events decoded on the main actor. Every `chat_event` re-serialized
`[String: Any]` to Data and ran JSONDecoder on the main thread — and the two
gates that reject an event ran AFTER that decode, so every duplicate replayed
after a reconnect was fully decoded and then discarded. Both gates read the raw
dictionary now and run first; the decode moved to `Task.detached`, mirroring the
`changeset_batch` path that already did this correctly.

Ordering is preserved for the same reason it is there: receiveLoop awaits each
frame's handleIncoming before reading the next, so two chat events are never in
flight at once. Two details the reordering had to get right — the sequence
watermark still advances only after a successful decode, because advancing it
first would burn it on an event that never applied and lose that event on
re-subscribe; and the post-await connection-generation guard keeps a frame that
arrives across a teardown from mutating the new session. The `chat_subscribe`
snapshot decode (up to 256 KiB, landing exactly as a thread opens) gets the same
treatment.

Full transcript re-parsed per streaming tick. `makeWorkChatTranscript` ran over
the whole fallback entry array — a page of 240-600 KB — on the main actor on
every live delta, roughly 6-7 times a second while streaming. Nothing on that
path consumed it: `workChatShouldPreferFallbackTranscript` short-circuits on an
active turn before it ever reads the transcript, and the delta-append merge
branch never touches it. The parameter is an autoclosure now, with the cheap
status guards ordered ahead of it, and the call site memoizes so the two
branches that do need it still build it at most once.

Foreground resume trusted a socket iOS may have already suspended. Resumes are
classified by how long the app was backgrounded, which is the only evidence
available: under 10s the socket is probably real, so the existing liveness probe
runs alongside the refreshes instead of the refreshes going out unchecked; at or
above 10s the session is replaced outright, because mobile operating systems
commonly suspend sockets without delivering a close event and probing one only
buys a round trip we are about to spend anyway. Trusting it showed "Connected"
over a dead pipe for up to the ~35-42s the heartbeat took to notice.

The ladder now resets on every foreground, including from the terminal
unreachable state that previously needed a manual tap to escape and meanwhile
retried only on a 30-40s heartbeat. A resume also gets the manual button's
connection strength — stale in-flight attempt cancelled, full candidate sweep
rather than live-only, since the route that worked before a suspension is often
the one that died with it — but deliberately not its user-intent side effects,
so a deliberate "pause auto-reconnect" is still honored. NWPathMonitor racing
and cursor-based resume are untouched.

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

* Fix the defects /quality found in the P0 branch

A dual-track review of the three preceding commits, plus a re-review of these
fixes. Every finding either landed here or is named in the summary; nothing was
deferred.

The branch was red. `apps/desktop/src/main/services/sync/syncHostService.test.ts`
asserted the retired redaction wording; only the ade-cli copy of that expectation
had been updated. Two more tests asserted the old copy after it changed here.

Chat-event compaction was not idempotent, and two paths applied it twice.
Hydration and the replay ring compact events that already came off disk
compacted, and a second pass was not a no-op: the wrapper's newline-dense
preview re-serializes with JSON escaping and lands back over the cap, so each
pass made the payload BIGGER (16.7 KB → 17.1 KB → 18.0 KB) while overwriting
`originalBytes` with the previous pass's size and destroying the real one. That
inverted the whole point of the change on exactly the paths the lane was meant
to slim, and falsified the module's own claim that a live push and a reconnect
hydration agree byte for byte.

Compaction now recognizes its own output. By SHAPE, deliberately, not by a
marker key: the first attempt stamped `__adeCompacted` on the wrapper, and every
surface that shows an object tool result renders it as a JSON dump — the desktop
card and its collapsed preview, the TUI one-liner, the iOS Result block — so the
marker became the first line the user read. Shape detection also recognizes
wrappers already written to disk, which a marker never could, and it is bounded
by size so recognizing our own output cannot become a cap bypass.

Compaction copy is now user-neutral. The same text reaches phones, so it can no
longer talk about "stored chat history". The renderer's truncated-diff matcher
accepts both wordings — transcripts on disk carry the old one.

Also from the review: result byte-accounting is no longer restated when only
`structured` was capped (it was zeroing a real prior measurement); the grid drop
affordance gates on persisted membership, which is what the cap actually counts;
the iOS post-decode guards re-check the subscription, since an unsubscribe can
land while the decode runs off-actor; the foreground-resume decision moved into
SyncRecoveryPolicy.swift as a pure function beside the other 26 rules, which
deleted a test-only accessor and turned two fixture-heavy tests into one direct
assertion; lane-sweep scope resolution became a named discriminated union
instead of an always-run loop whose output was dead on the common path; and the
running-command-output cap stopped leaking out of the module built to own it.

New regression tests cover idempotence on every branch, the byte-accounting
rule, legacy wrapper recognition, the forged-wrapper bypass, the reorder of a
full grid, and both copy wordings.

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

* Cover the P0 branch: analytics, parity, and the defects CodeRabbit found

Report a lost renderer as a product fact. The branch introduced a new
product-level failure category and nothing recorded it, so `ade_renderer_recovered`
joins the closed taxonomy beside `ade_brain_recovered` — the existing precedent
for "a component died and we recovered it". It carries `crash_reason` and
whether the reload was still allowed, nothing else. `crash_reason` is its own
property key rather than the shared `reason`, which is pinned to the
auto-update abort set and would have been weakened by widening; its values are
Electron's closed enum, normalized to `unknown` so a future Electron string
cannot widen what crosses the boundary. Volume is bounded by the recovery budget
itself: a boot-crash loop stops trying rather than emitting forever. A boundary
test pins that the window URL and title — both in scope at the crash site —
cannot ride along.

Fixes from an independent CodeRabbit review of the committed branch:

- `reviewThreadDiffHunk` returned up to `MAX_CHARS + 4`, because the "...\n"
  marker was added after the budget instead of inside it. The cap is a promise
  about what reaches the prompt. My own test had asserted `<= cap + 4`, encoding
  the bug rather than catching it.
- The renderer recovery budget read `Date.now()`. It is a rolling time window,
  so a wall-clock correction mid-crash-storm would either free the budget early
  or freeze it; it now reads `performance.now()`.
- Declared chat sessions were found by filtering a 500-row lane listing, so a
  merged PR could silently fail to file a session it had explicitly named if a
  long-lived lane pushed it past that page. Declared sessions now resolve by id
  and keep an explicit lane check, which the lane-scoped listing had given for
  free. The sweep keeps the bounded listing — it is a guess, and a guess should
  stay bounded.

Mobile parity found a real consequence of the wire diet. Phones now receive
compacted `file_change` diffs on the live push, not only after hydration, and
iOS counted the wrapper's `----- BEGIN FIRST PREVIEW -----` separators as
deletions while deriving `+N / -N` from head-and-tail previews that omit the
middle — wrong twice over, rendered as exact. It now reports nothing, matching
desktop's `summarizeDiffStats`, and recognizes both the old and new notice
wording since transcripts on disk carry the old text. VoiceOver gets the reason
rather than a pair of zeros it would read as fact.

Docs follow the code: the compaction module and its idempotence invariant, the
wire-vs-storage agreement, the changeset-diet rule (a table needs an on-demand
path and a required remote command before it can leave the pump), merge
settlement scope, renderer crash recovery, the iOS resume classifier, and the
grid tile cap.

CLI and TUI parity: no changes required. The TUI reads chat events over the
local RPC socket, so it sees the stored form and never the wire form; both
compaction filter sites are mobile-peer-gated; and the removed settlement-blocker
symbols have no remaining references.

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

* Make the renderer-crash report survive the crash storm it reports

Ship-phase quality revalidation of the previous commit found the new
`ade_renderer_recovered` event structurally guaranteed to lose the occurrence
that matters most. The recovery budget allows 3 reloads per rolling 60 s and the
event's per-minute analytics budget was also 3, so the three successful reloads
always consumed it and the fourth occurrence — the one reporting that the window
stayed down — was dropped as rate-limited every time. The minute budget is now
one above the recovery budget, with a regression test that captures four in a
row and asserts the `recovered: false` one lands.

Two hand-maintained copies of Electron's reason enum already disagreed: the
normalizer listed seven values, the analytics allowlist six plus `unknown`. The
allowlist is now derived from the normalizer's exported set, so a value one
knows and the other does not can no longer ship an event stripped of its only
payload. The crash handler gates analytics on the exported
`isRecoverableRenderProcessGone` rather than re-testing `!== "clean-exit"`
inline, so the analytics gate and the recovery gate cannot drift apart either.

Smaller items from the same pass: the per-window recovery reporter is hoisted
once instead of duplicated at both `createWindow` sites, so a third site cannot
silently skip it; settlement scope selection is a named `switch` rather than a
three-level nested ternary; a test helper drops an `unknown` cast it did not
need; a Swift doc comment is reattached to the function it describes; and the
`pull_request_snapshots` measurement reads in one unit everywhere — 11.2 MB of
28.1 MB, 39.7% — instead of MB in the code comment and MiB in the docs.

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

* Fence untrusted review content before it reaches the resolver

Greptile flagged the diff hunk restored earlier in this branch as a prompt-
injection vector, and it is right. `prRefreshIssueInventory` hands review data to
an agent that also holds `prReplyToReviewThread` and `prResolveReviewThread`,
neither of which asks for confirmation, so instruction-shaped text written by
whoever opened the PR could steer real GitHub review-state mutations.

Comment bodies had the same exposure and predate this branch, so the whole class
is swept rather than just the field that drew the comment: review-thread diffs,
review comments, and issue comments are each wrapped in an explicit fence that
names them as data written by an outside contributor and says not to follow
instructions inside. The wrapping happens in code on every value rather than
being asked for in a prompt, the tool description states the same contract, and
any occurrence of the fence marker inside a payload is defanged so content
cannot close its own fence and speak as ADE.

The regression test plants a forged END marker followed by an instruction and
asserts exactly one BEGIN and one END survive per field.

* Fix the review findings from PR #1056

Greptile and CodeRabbit between them found eight things, all verified against
the code before changing anything.

The renderer-recovery event reported the attempt, not the outcome. It emitted
`recovered: true` the moment the budget allowed a reload, so a reload that was
attempted and then failed still went out as a recovery. It now reports after
`loadURL` settles — true on resolve, false on reject, and false when the budget
refuses outright — so the metric describes what the user got rather than what
ADE tried.

Compaction could be skipped entirely by a payload it could not measure. When
`JSON.stringify` throws (a BigInt anywhere in the object), the fallback measured
`String(value)` — "[object Object]", 15 bytes, under every cap — so the original
unbounded payload was stored and sent untouched, which is the one case a cap
exists for. Unmeasurable is now treated as must-compact. A circular reference
never reached this path; inline-image redaction breaks the cycle first.

Compaction also ran once per subscriber. `sendChatEvent` compacted inside the
per-peer loop, so one live event serialized and binary-searched its payload
again for every peer watching that session. It is memoized against the envelope
now — once per event, no matter how many peers.

The shortened-diff matcher was unanchored, on both desktop and iOS, so a real
diff whose own changed lines quoted the notice strings — editing the compactor,
for instance — was classified as compacted and reported as zero additions and
deletions. Both now require the header at the start.

iOS measured the suspension gap on the wall clock, the same defect already fixed
on the desktop budget: a device whose clock moves backward during a long
suspension would report a short or negative gap and go on to trust a socket iOS
had already suspended. It uses `systemUptime` now. And a shortened diff no longer
draws a `-0` deletion badge through the delete-kind branch, which contradicted
the VoiceOver label beside it.

One test weakness: `bytes(undefined)` is 0, so upper-bound assertions about
`structured` passed just as happily if compaction had deleted the field instead
of bounding it. Size claims about fields that must survive now go through a
helper that also asserts a lower bound.

Regression tests: reload-rejection reporting, the unserializable payload, the
anchored matcher on both platforms, and the fenced-content case from the earlier
commit.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant