Skip to content

Tui And Desktop App Upgrades#291

Merged
arul28 merged 4 commits into
mainfrom
ade/tui-and-desktop-app-upgrades-1209107e
May 13, 2026
Merged

Tui And Desktop App Upgrades#291
arul28 merged 4 commits into
mainfrom
ade/tui-and-desktop-app-upgrades-1209107e

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 13, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.


Note

Medium Risk
Medium risk: introduces new TUI rendering/aggregation and LAN discovery code paths plus stricter release-asset validation, which could affect runtime connectivity and release pipelines if edge cases were missed.

Overview
Delivers a major TUI UX upgrade: chat rendering now aggregates events into higher-level blocks (work/tool groups, plans, compaction, memory, queued steers) and hides low-signal noise, with new helpers for scrolling, transcript copy/export, subagent transcript viewing, and richer palettes/header/footer behavior.

Adds new runtime-backed surfaces for lane context (bulk diff line stats, per-lane PR summaries, and “worktree missing” handling), plus steer-message lifecycle support via new chat domain actions (steer/edit/cancel/dispatch).

Improves sync discovery reliability by adding a native dns-sd publisher on macOS Electron with fallback/retry behavior and explicit LAN refresh forcing, and tightens release automation by validating the publish asset manifest (including runtime tar contents) before drafting GitHub releases. Also adds a new /plan skill spec document for three-round locked planning.

Reviewed by Cursor Bugbot for commit f11519d. Configure here.

Summary by CodeRabbit

  • New Features

    • Lane diff stats and per-lane PR summaries.
    • Staged steer chat controls (/steer send/edit/cancel/etc.).
    • Richer drawer, right‑pane subagent/teammate browsing, and enhanced lane visuals.
    • Session pinning and lane-specific tab reordering.
    • Usage threshold notifications and in‑app usage toasts for Claude/Codex.
    • Improved chat UI: aggregated blocks, markdown tables, token bar/footer, improved command/mention palettes, and clipboard image attachments.
  • Bug Fixes

    • Improved phone sync labeling and macOS LAN discovery.
    • Stricter resume-target validation for CLI resume commands.

Greptile Summary

This PR delivers a large-scale TUI and desktop app upgrade: a new aggregateChatBlocks rendering model, staged steer chat controls (steer/editSteer/cancelSteer/dispatchSteer), new API endpoints for lane diff stats and per-lane PR summaries, native macOS Electron LAN discovery via dns-sd with fallback/recovery logic, usage threshold event emission, and a release asset validation step in CI.

  • TUI aggregate rendering (aggregate.ts): introduces AggregatedBlock union type and aggregateChatBlocks to group tools/edits/plan/memory/compaction/queued-steers into high-level blocks, with broad test coverage.
  • Native LAN discovery (syncHostService.ts): on macOS Electron, spawns dns-sd -R instead of Bonjour; on failure sets a 30-second Bonjour fallback, then retries native after the window expires.
  • CI release validation (release-core.yml): adds a pre-publish manifest check that validates expected DMG, ZIP, blockmap, YAML, runtime binary, and native tarball artifacts before drafting the GitHub release.

Confidence Score: 5/5

Safe to merge; no blocking defects found. All new paths have tests and graceful error handling.

The LAN discovery fallback/recovery logic is carefully guarded (disposed flag, port checks, 30-second Bonjour window), the aggregated block rendering is well-tested, and the new API endpoints follow existing patterns. The desktop test mock for node:child_process is narrower than the CLI counterpart but does not cause current failures. The linkedAt value passed to the Linear card callback is technically redundant rather than wrong. CI artifact validation is a net improvement even with the minor diagnostic gap in the tar check.

.github/workflows/release-core.yml tar validation and apps/desktop/src/main/services/sync/syncHostService.test.ts mock completeness are worth a second look.

Important Files Changed

Filename Overview
apps/ade-cli/src/services/sync/syncHostService.ts Adds native dns-sd LAN discovery with fallback/recovery; cleanup and guard logic looks correct.
apps/ade-cli/src/tuiClient/aggregate.ts New file implementing AggregatedBlock model and aggregateChatBlocks logic; clean implementation with comprehensive type coverage.
.github/workflows/release-core.yml Adds pre-publish artifact manifest validation; tar archive integrity check uses a misleading error message that conflates corruption with missing content.
apps/desktop/src/main/services/lanes/laneService.ts Adds onLinearIssueLinked callback with proper async error handling; notifyLinearIssueLinked fires only at lane creation, which is the intended scope.
apps/desktop/src/main/services/usage/usageTrackingService.ts Adds threshold crossing detection with file-persisted state; weekly-over-monthly window preference is correct.
apps/desktop/src/main/services/opencode/openCodeRuntime.ts Intentionally allows the task subagent tool in ADE plan profile so child sessions surface in the TUI subagents pane; trade-off documented in comment.
apps/desktop/src/main/services/sync/syncHostService.test.ts Desktop test mocks node:child_process as a complete module replacement with only execFile and spawn; CLI tests properly spread the real module.
apps/desktop/src/main/services/prs/prService.ts Adds listPrsByLane using getDisplayRowForCurrentLaneBranch; null rows are properly filtered, state mapping is conservative.
apps/desktop/src/main/services/diffs/diffService.ts Adds getLaneDiffStats and listLaneDiffStats using git shortstat; properly guards against invalid baseRef, handles failures gracefully.
apps/desktop/src/main/services/cto/linearLaneCardService.ts New file for building and publishing Linear lane card attachments; robust fallback chain for linkedAt, proper URL hash construction.

Sequence Diagram

sequenceDiagram
    participant SHS as SyncHostService
    participant dnsd as dns-sd (native)
    participant Bonjour as Bonjour (fallback)

    SHS->>SHS: publishLanDiscovery(port)
    SHS->>SHS: shouldUseNativeLanDiscovery()?
    alt macOS + Electron + fallback window expired
        SHS->>dnsd: spawn("dns-sd", ["-R", name, ...txt])
        dnsd-->>SHS: child.unref()
        note over SHS,dnsd: Running indefinitely
        dnsd--xSHS: exit/error event
        SHS->>SHS: "nativeLanDiscoveryFallbackUntilMs = now+30s"
        SHS->>SHS: scheduleNativeLanDiscoveryRecovery(port) [1s timer]
        SHS->>SHS: [1s later] publishLanDiscovery(port)
        note over SHS: shouldUseNativeLanDiscovery() = false (within 30s)
        SHS->>Bonjour: bonjourInstance.publish(...)
    else Linux/Windows or fallback active
        SHS->>Bonjour: bonjourInstance.publish(...)
    end

    SHS->>SHS: dispose()
    SHS->>dnsd: child.kill("SIGTERM")
    SHS->>Bonjour: bonjourAnnouncement.stop()
Loading

Comments Outside Diff (2)

  1. apps/desktop/src/main/services/ai/apiKeyStore.ts, line 84-88 (link)

    P2 __setSafeStorageForTests misses missingCredentialProviders reset

    This PR adds missingCredentialProviders as module-level state (line 82), and initApiKeyStore correctly clears it on re-init. However, __setSafeStorageForTests — used by tests that swap the safeStorage mock between cases — does not reset missingCredentialProviders. If a test that exercises the credential-store path fills missingCredentialProviders and a subsequent test calls only __setSafeStorageForTests, the stale "missing" entries will cause getApiKey to skip the credentialStore read and return null for those providers.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/ai/apiKeyStore.ts
    Line: 84-88
    
    Comment:
    **`__setSafeStorageForTests` misses `missingCredentialProviders` reset**
    
    This PR adds `missingCredentialProviders` as module-level state (line 82), and `initApiKeyStore` correctly clears it on re-init. However, `__setSafeStorageForTests` — used by tests that swap the `safeStorage` mock between cases — does not reset `missingCredentialProviders`. If a test that exercises the credential-store path fills `missingCredentialProviders` and a subsequent test calls only `__setSafeStorageForTests`, the stale "missing" entries will cause `getApiKey` to skip the `credentialStore` read and return `null` for those providers.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/desktop/src/main/services/usage/usageTrackingService.ts, line 629-631 (link)

    P2 Codex log entries with empty IDs are never deduplicated

    When both messageId and requestId are empty strings, dedupeKey is ":". The guard if (dedupeKey !== ":" && seen.has(dedupeKey)) continue bypasses deduplication entirely for these entries, potentially inflating the 30-day cost estimate. Compare with scanClaudeLogs which unconditionally deduplicates.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/usage/usageTrackingService.ts
    Line: 629-631
    
    Comment:
    **Codex log entries with empty IDs are never deduplicated**
    
    When both `messageId` and `requestId` are empty strings, `dedupeKey` is `":"`. The guard `if (dedupeKey !== ":" && seen.has(dedupeKey)) continue` bypasses deduplication entirely for these entries, potentially inflating the 30-day cost estimate. Compare with `scanClaudeLogs` which unconditionally deduplicates.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src/main/services/sync/syncHostService.test.ts:13-17
**Incomplete module mock may hide future regressions**

The desktop `syncHostService.test.ts` replaces the entire `node:child_process` module with only `{ execFile, spawn }`. If the service ever imports additional exports (e.g. `spawnSync`, `execSync`, `fork`) those would be `undefined` in tests and produce confusing failures. The CLI variant of this test (in `apps/ade-cli`) correctly preserves the real module by spreading `importOriginal`. Using the same pattern here would make the mock more resilient.

### Issue 2 of 3
apps/desktop/src/main/services/lanes/laneService.ts:722
**`linkedAt` is set to lane creation time rather than the actual link instant**

`lane.createdAt` comes from the database row's `created_at` column, which is written slightly before `notifyLinearIssueLinked` is called. `buildLinearLaneCardAttachment` already falls back to `lane.createdAt` when no `linkedAt` is supplied, so passing `lane.createdAt` explicitly is redundant. Using the wall-clock time at notification would more accurately record when the attachment was published and avoids confusion if this call path is later extended (e.g., to fire from `updateLane`).

```suggestion
      const result = onLinearIssueLinked({ lane, issue, linkedAt: new Date().toISOString() });
```

### Issue 3 of 3
.github/workflows/release-core.yml:414-417
**Ambiguous error message when archive is corrupted**

With `set -euo pipefail`, if `tar -tzf` itself fails (e.g. a truncated or corrupted archive), the pipeline exits non-zero and the `||` block fires — but the message says "missing node_modules" rather than indicating an integrity failure. Separating the extraction check from the content check produces an actionable CI error.

```suggestion
            if ! tar -tzf "release-assets/runtime/ade-$target.native.tar.gz" > /tmp/ade_tar_list 2>&1; then
              echo "::error::ADE native dependency archive for $target is corrupted or unreadable."
              exit 1
            fi
            grep -q '^\./node_modules/' /tmp/ade_tar_list || {
              echo "::error::ADE native dependency archive for $target is missing node_modules."
              exit 1
            }
```

Reviews (6): Last reviewed commit: "ship: iteration 5 - settle cursor follow..." | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview May 13, 2026 11:00am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

Adds a plan-mode skill spec and broad feature work across CLI/TUI, desktop, and iOS: a block-based TUI rendering/aggregation pipeline, provider/chat-service enhancements (Claude/Codex/Cursor), Linear issue-attachment and lane-card publishing, usage threshold detection + UI toasts, resume-target sanitization and Codex resume improvements, credential-store migrations, lane diff/PR summaries, many tests, and release artifact validation.

Changes

TUI rendering, aggregation, and related types

Layer / File(s) Summary
Aggregation model and block pipeline
apps/ade-cli/src/tuiClient/aggregate.ts, apps/ade-cli/src/tuiClient/workEventIds.ts
New AggregatedBlock model, helpers (diffStats, appendTool, derivePendingSteers), and aggregateChatBlocks/derivePendingSteers.
Inline runs, markdown tables, and line coalescing
apps/ade-cli/src/tuiClient/format.ts
Adds InlineRun/parseInlineRuns, table block in markdown parsing, pushLine dedupe, and revised system_notice tone/filtering.
Block-based ChatView & spinner ticks
apps/ade-cli/src/tuiClient/components/ChatView.tsx, apps/ade-cli/src/tuiClient/spinTick.tsx
Replaces line-based renderer with rows-for-blocks pipeline, inline-run-aware wrapping, table/hr support, block renderers, scrolling model, and spin/braille/dot pulse hooks.
TUI types/state selection
apps/ade-cli/src/tuiClient/types.ts, apps/ade-cli/src/tuiClient/state.ts, apps/ade-cli/src/tuiClient/project.ts, apps/ade-cli/src/tuiClient/drawerSelection.ts
Adds lastLaneId, expands RightPane/subagent types and lane/session selection helpers (chooseTuiLaunchLane, chooseMostRecentSessionLane, resolveTuiChatRefreshTarget), and tightens new-chat validation.
Drawer / RightPane / UI components & helpers
apps/ade-cli/src/tuiClient/components/Drawer.tsx, .../RightPane.tsx, .../FooterControls.tsx, .../Header.tsx, .../MentionPalette.tsx, .../SlashPalette.tsx, .../LaneTree.ts, .../LaneChip.tsx
Large refactors: stack-graph drawer, lane cards/PR pills/diff suffixes, themed RightPane (lane-details, model-setup, subagents), palette/ footer/ header UI changes, lane stack ordering and prefix helpers.
TUI tests
apps/ade-cli/src/tuiClient/__tests__/*
Extensive additions/rewrites to test suites to exercise aggregation/block rendering, scrolling, panes, palettes, image-clipboard, keybindings, subagent panes, and many components; ANSI-stripping helpers added.

Chat services, provider integrations, slash/steer APIs

Layer / File(s) Summary
Agent chat runtime: Codex/Claude/Cursor changes
apps/desktop/src/main/services/chat/agentChatService.ts, apps/desktop/src/main/services/ai/tools/systemPrompt.ts
Adds codex-app-server runtime support; refactors Codex collaboration-mode resolution and developer-instructions generation; strips automatic macOS VM context from follow-ups; wires Claude session disabled-plugins and plugin/option adjustments.
Cursor task lifecycle -> subagent events
apps/desktop/src/main/services/chat/cursorSdkEventMapper.ts
Adds stateful taskStatusMap, emits subagent_started/subagent_result on task lifecycle transitions, and normalizes terminal statuses.
Steer APIs + slash command routing
apps/ade-cli/src/tuiClient/adeApi.ts, apps/ade-cli/src/tuiClient/commands.ts, apps/desktop/src/shared/types/chat.ts
Adds steerChatMessage, editSteerMessage, cancelSteerMessage, dispatchSteerMessage; expands getSlashCommands args to accept session/lane/provider and updates CLI palette limits.
Slash command discovery in renderer
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx, apps/desktop/src/renderer/components/chat/ChatCommandMenu.tsx, apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
Fetches provider/lane-scoped slash commands for drafts; adds viewport-aware menu anchoring and anchor shape helpers; updates composer anchoring logic.
Service tests
apps/desktop/src/main/services/chat/*
Expanded tests for Codex/Claude follow-ups, plugin discovery, slash-command drafts, and steering helpers.

Linear issue attachments, lane publishing, diff/PR summaries

Layer / File(s) Summary
IssueTracker types & Linear attachment API
apps/desktop/src/main/services/cto/issueTracker.ts, apps/desktop/src/shared/types/prs.ts, apps/desktop/src/shared/types/git.ts
Adds types for issue-attachment payloads and createIssueAttachment method; adds DiffLineStats and PrLaneSummary types.
Linear client & lane-card publishing
apps/desktop/src/main/services/cto/linearClient.ts, apps/desktop/src/main/services/cto/linearLaneCardService.ts
Adds createIssueAttachment GraphQL call on Linear client; implements buildLinearLaneCardAttachment and publishLinearLaneCard.
Lane wiring & dispatcher
apps/desktop/src/main/main.ts, apps/desktop/src/main/services/lanes/laneService.ts, apps/desktop/src/main/services/cto/linearDispatcherService.ts
createLaneService accepts onLinearIssueLinked callback; main process stores linearIssueTrackerRef and publishes lane-card on linkage; dispatcher includes linearIssue shape in lane creation.
Diff/PR listing service APIs
apps/desktop/src/main/services/diffs/diffService.ts, apps/desktop/src/main/services/prs/prService.ts, apps/ade-cli/src/tuiClient/adeApi.ts
Adds getLaneDiffStats/listLaneDiffStats and listPrsByLane server-side; TUI API wrappers expose lane diff and PR-by-lane listing.

Usage tracking, threshold events, IPC and UI

Layer / File(s) Summary
Usage tracking & thresholds
apps/desktop/src/main/services/usage/usageTrackingService.ts, apps/desktop/src/main/services/usage/usageTrackingService.test.ts
Limits tracking to Claude/Codex, adds USAGE_THRESHOLDS, persisted threshold state + detectThresholdCrossings, bucketDaily7d and dailyUsage7d in snapshots, and onThresholdEvent callback wiring.
IPC / preload / renderer wiring for thresholds
apps/desktop/src/shared/ipc.ts, apps/desktop/src/preload/preload.ts, apps/desktop/src/preload/global.d.ts
Adds IPC.usageThresholdEvent and ade.usage.onThreshold subscription API exposed from preload.
Usage UI & toasts
apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx, apps/desktop/src/renderer/components/usage/UsageQuotaPanel.tsx, apps/desktop/src/renderer/components/app/AppShell.tsx
Header usage control refactor to provider chips; auto-refresh, provider-aware cards; AppShell subscribes to threshold events and renders capped auto-dismissing toasts with "Open usage" action.

Resume-target sanitization, Codex resume, and PTY/session changes

Layer / File(s) Summary
Resume-target sanitizers and CLI resume command
apps/desktop/src/main/utils/terminalSessionSignals.ts, apps/desktop/src/shared/cliLaunch.ts
Adds sanitizeResumeTargetId/sanitizeTrackedCliResumeTargetId, validates target IDs (reject control chars, leading -), used in parsing/building tracked resume commands.
PTY/Codex resume/backfill and session metadata
apps/desktop/src/main/services/pty/ptyService.ts, apps/desktop/src/main/services/sessions/sessionService.ts
Richer Codex storage resolution returning threadName, adopts runtime thread names into session metadata, schedules title capture, uses sanitization for backfill gating, emits idle runtime signals with preview.
IPC hydration gating
apps/desktop/src/main/services/ipc/registerIpc.ts
Uses sanitized resume-target validity to decide session resume-target hydration needs.
Tests
apps/desktop/src/main/.../ptyService.test.ts, .../terminalSessionSignals.test.ts
Updated/added tests to cover Codex resume backfill, thread-name adoption, and resume sanitization behavior.

Credential-store migration & API key handling

Layer / File(s) Summary
Credential store migration
apps/desktop/src/main/services/ai/apiKeyStore.ts, apps/desktop/src/main/main.ts
Adds EncryptedFileCredentialStore wiring for per-project API keys and Linear credentials; implements one-time migrations from legacy safeStorage/Keychain/project-scoped secrets into provided credential store; records migration markers.
Linear credential service machine store
apps/desktop/src/main/services/cto/linearCredentialService.ts
Optional credentialStore support for machine-scoped Linear tokens/oauth credentials, migration from legacy artifacts, and branch/fallback logic.
Tests
apps/desktop/src/main/services/ai/apiKeyStore.test.ts, apps/desktop/src/main/services/cto/linearAuth.test.ts
Adds migration tests for API keys and Linear credential migration and Linear createIssueAttachment behavior tests.

iOS sync, project catalog, timeouts, and database

Layer / File(s) Summary
SyncService project catalog & hydration
apps/ios/ADE/Services/SyncService.swift, apps/ios/ADE/Services/Database.swift
Dedup by normalized root, prefer remote selection, ensure active-project cache row for hydration, add upsertMobileProjectCache.
Connection load sampling & timeouts
apps/ios/ADE/Services/SyncService.swift, apps/ios/ADETests/ADETests.swift
Adds SyncConnectionLoadSample helper for poor/healthy classification and dedicated chat-send timeout/message.
PIN sheet / pairing help
apps/ios/ADE/Views/Settings/SettingsPinSheet.swift, apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
Updates pairing messaging and PIN submit discovery selection helpers to prefer latest discovered host and candidate address lists.

Release workflow & minor infra

Layer / File(s) Summary
Release artifact validation
.github/workflows/release-core.yml
Adds "Validate publish asset manifest" step that asserts presence/executability of required macOS/Windows/runtime artifacts and checks runtime archives contain node_modules.
Dependency and allowlist updates
apps/desktop/package.json, apps/desktop/src/main/services/adeActions/registry.ts
Adds mdast-util-find-and-replace dep; extends ADE action allowlist with diff/pr/github actions (getRemoteStatus, listLaneDiffStats, listPrsByLane).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

  • arul28/ADE#199: Overlaps PTY/ptyService resume/backfill and resume-target handling changes.
  • arul28/ADE#290: Related TUI system_notice/format rendering adjustments and tone determination.
  • arul28/ADE#276: Related usage subsystem work (thresholds, usage UI, and tracking).

Suggested labels

desktop, ios, ci

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/tui-and-desktop-app-upgrades-1209107e

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@cursor review

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@copilot review

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 13, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c10870a. Configure here.

Copy link
Copy Markdown
Contributor

Copilot AI commented May 13, 2026

@copilot review

Reviewed at commit c10870a. There are no new diff changes for this branch right now, so there’s nothing additional to flag or patch in this pass.

Copilot finished work on behalf of arul28 May 13, 2026 08:57
Comment thread apps/desktop/src/main/services/chat/claudeOutputStyles.ts Outdated
Comment thread apps/desktop/src/main/services/chat/agentChatService.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/components/usage/UsageQuotaPanel.tsx (1)

134-145: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent stale overwrite from concurrent initial fetches.

Line 135 (load) and Line 144 (manualRefresh) run in parallel; whichever resolves last wins. A slower getSnapshot() can overwrite freshly refreshed state.

Suggested fix
 useEffect(() => {
-  void load();
+  let cancelled = false;
+  void (async () => {
+    await load();
+    if (!cancelled) await manualRefresh();
+  })();
   if (!window.ade?.usage) return;
   const unsubscribe = window.ade.usage.onUpdate(applySnapshot);
-  return unsubscribe;
-}, [applySnapshot, load]);
-
-// Auto-refresh on open so the drawer always shows fresh data instead of
-// whatever the last background poll happened to leave behind.
-useEffect(() => {
-  void manualRefresh();
-}, [manualRefresh]);
+  return () => {
+    cancelled = true;
+    unsubscribe();
+  };
+}, [applySnapshot, load, manualRefresh]);

As per coding guidelines, Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices.

🤖 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/usage/UsageQuotaPanel.tsx` around lines
134 - 145, Both useEffect calls can race: load() and manualRefresh() may resolve
out of order and the slower one can overwrite newer state; update load and
manualRefresh to use a shared increasing requestId (or AbortController) stored
in component scope, increment before each async getSnapshot call, capture the
current id in the closure, and only call applySnapshot if the captured id equals
the latest id (i.e., request is still current); reference the existing symbols
load, manualRefresh, and applySnapshot when adding the guard so only the most
recent fetch result is applied.
🟡 Minor comments (13)
apps/desktop/package.json-91-91 (1)

91-91: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the unused mdast-util-find-and-replace dependency.

This dependency is not imported or referenced anywhere in the codebase. Adding unused packages bloats the Electron app bundle; it should be removed unless it's legitimately needed for this PR.

🤖 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/package.json` at line 91, Remove the unused dependency
"mdast-util-find-and-replace" from the package.json dependencies block (the
entry shown in the diff) and update the lockfile by running the package manager
install (e.g., npm/yarn/pnpm install) so the lockfile and node_modules reflect
the removal; verify no imports reference mdast-util-find-and-replace and then
rebuild the Electron app to ensure bundle size is reduced.
apps/desktop/src/main/services/sync/syncHostService.test.ts-15-21 (1)

15-21: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset spawnMock between tests to prevent cross-test leakage.

spawnMock is declared at module scope but never reset, causing call history and implementations to bleed into later tests. The implementation uses spawn() at line 1287 of apps/ade-cli/src/services/sync/syncHostService.ts, so this mock needs cleanup alongside execFileMock.

Proposed fix
 afterEach(async () => {
   while (activeDisposers.length > 0) {
     const dispose = activeDisposers.pop();
     if (dispose) await dispose();
   }
   execFileMock.mockReset();
+  spawnMock.mockReset();
 });
🤖 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/sync/syncHostService.test.ts` around lines 15
- 21, The module-scoped spawnMock retains calls/implementation across tests; add
cleanup for spawnMock alongside execFileMock in the test teardown (e.g.,
afterEach) by calling spawnMock.mockReset() or spawnMock.mockClear() (or use
vi.clearAllMocks()/vi.resetAllMocks()) so the mocked spawn used by spawn() does
not leak between tests; reference the existing spawnMock and execFileMock
symbols and ensure this cleanup runs afterEach test.
apps/desktop/src/main/services/chat/cursorSdkEventMapper.ts-195-217 (1)

195-217: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Track terminal-first task updates before emitting the result.

If the first "task" frame for a run_id already has a terminal status, this branch emits subagent_result without recording that state. A duplicated/replayed terminal frame for the same run will emit another result because prevStatus is still null.

🤖 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/chat/cursorSdkEventMapper.ts` around lines 195
- 217, When handling a first-seen task frame (prevStatus === null) that already
has a terminal status, record that terminal state in meta.taskStatusMap for
runId before emitting the result so future duplicate/replayed terminal frames
won't emit another result; specifically, in the branch inside the if (runId)
block where prevStatus === null && isCursorTaskTerminalStatus(status), call
meta.taskStatusMap.set(runId, status) prior to out.push(makeResultEvent(status))
(leave other branches using tagRuntime and non-terminal logic unchanged).
apps/desktop/src/main/services/chat/claudeOutputStyles.ts-104-106 (1)

104-106: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

pluginKeyName() drops scoped plugin names.

split("@", 1) returns "" for keys like @scope/plugin@1.2.3, so enabledPluginNames() drops them and local plugin discovery can never match the enabled plugin.

💡 Suggested fix
 function pluginKeyName(pluginKey: string): string {
-  return pluginKey.split("@", 1)[0]?.trim().toLowerCase() ?? "";
+  const trimmed = pluginKey.trim();
+  const versionSeparator = trimmed.lastIndexOf("@");
+  const base = versionSeparator > 0 ? trimmed.slice(0, versionSeparator) : trimmed;
+  return base.toLowerCase();
 }
🤖 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/chat/claudeOutputStyles.ts` around lines 104 -
106, pluginKeyName() currently uses split("@", 1) which returns an empty string
for scoped names like `@scope/plugin`@1.2.3 and causes enabledPluginNames() to
drop them; change pluginKeyName to strip only the version suffix by finding the
last "@" and returning the substring before it (but if the last "@" is at index
0 or not present, return the whole key trimmed/lowercased) so scoped names like
`@scope/plugin` are preserved and matching in enabledPluginNames() works
correctly.
apps/desktop/src/renderer/components/app/TopBar.tsx-92-99 (1)

92-99: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider deduplicating forced refreshes.

When force: true is passed, line 99 bypasses the in-flight request check. If multiple components or event handlers call fetchRecent({ force: true }) simultaneously (e.g., both the focus handler and the missing-project event), you'll create duplicate concurrent requests instead of sharing a single forced fetch.

🔒 Proposed fix to deduplicate forced refreshes
- if (!options?.force && recentProjectsInFlight) return recentProjectsInFlight;
+ if (recentProjectsInFlight && !options?.force) return recentProjectsInFlight;
+ // Even when forced, if a fetch is already in flight, wait for it rather than starting another
+ if (recentProjectsInFlight) return recentProjectsInFlight;

Alternatively, if you truly need force to always start a fresh request:

  if (
    !options?.force &&
    recentProjectsCache &&
    now - recentProjectsCache.fetchedAtMs < RECENT_PROJECTS_CACHE_TTL_MS
  ) {
    return Promise.resolve(recentProjectsCache.rows);
  }
- if (!options?.force && recentProjectsInFlight) return recentProjectsInFlight;
+ // Cancel the previous in-flight request when forced, or dedupe if not forced
+ if (recentProjectsInFlight) {
+   if (options?.force) {
+     // Mark the old promise stale; callers will get newer data from the next fetch
+   } else {
+     return recentProjectsInFlight;
+   }
+ }

The simpler fix is to always dedupe, even on force, since the cache is already short-lived (2.5s TTL).

🤖 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/app/TopBar.tsx` around lines 92 - 99,
The fetchRecent logic allows duplicate concurrent requests when options.force is
true because the in-flight check is skipped; modify fetchRecent so that
recentProjectsInFlight is always consulted before starting a new fetch (i.e.,
return recentProjectsInFlight if set regardless of options.force) and ensure
recentProjectsInFlight is set immediately when initiating the network call (and
cleared on completion/error) so forced refreshes are deduplicated; update
references in fetchRecent, recentProjectsInFlight, options.force,
recentProjectsCache, and RECENT_PROJECTS_CACHE_TTL_MS accordingly.
apps/desktop/src/main/services/cto/linearLaneCardService.ts-44-46 (1)

44-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an inline comment explaining the {linkedAt__since} Linear template variable.

Line 45 intentionally uses {linkedAt__since} as a Linear API template placeholder. The actual linkedAt timestamp is provided in metadata.linkedAt for Linear to process. Add a comment explaining that Linear replaces this placeholder with a "time since" formatted string at render time:

Suggested documentation
  return {
    issueId: args.issue.id,
    title: `ADE lane: ${truncate(args.lane.name, 64)}`,
+   // {linkedAt__since} is replaced by Linear with "linked X time ago" formatting
    subtitle: `${truncate(branch, 56)} - linked {linkedAt__since}`,
    url: buildCardUrl(args.issue, args.lane.id),
🤖 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/cto/linearLaneCardService.ts` around lines 44
- 46, Add an inline comment next to the subtitle template showing that
`{linkedAt__since}` is a Linear template placeholder which Linear will replace
with a “time since” string at render time using the actual timestamp provided in
metadata.linkedAt; update the file linearLaneCardService.ts near the subtitle
assignment (where subtitle: `${truncate(branch, 56)} - linked {linkedAt__since}`
is defined) to include a short comment referencing metadata.linkedAt so future
readers know the placeholder is intentionally left for Linear to process.
apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx-417-425 (1)

417-425: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard slash-command mock args before provider access

Line 418 dereferences args.provider without guarding args. If the call path passes undefined, this test throws a TypeError instead of asserting behavior.

Proposed fix
-    vi.mocked(window.ade.agentChat.slashCommands).mockImplementation(async (args) => {
-      if (args.provider === "claude") {
+    vi.mocked(window.ade.agentChat.slashCommands).mockImplementation(async (args) => {
+      if (args?.provider === "claude") {
         return [{
           name: "/agents",
           description: "Manage agent configurations.",
           source: "sdk",
         }];
       }
       return [];
     });
🤖 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/chat/AgentChatPane.submit.test.tsx`
around lines 417 - 425, The mock implementation for
window.ade.agentChat.slashCommands dereferences args.provider unguarded; update
the mock in AgentChatPane.submit.test.tsx so it safely handles undefined args
(e.g., check that args is defined before accessing args.provider or use a
default param) and only returns the Claude commands when the provider equals
"claude"; ensure the mock returns [] for any undefined or non-claude args to
avoid TypeError during tests.
apps/ade-cli/src/tuiClient/__tests__/ChatView.test.tsx-21-23 (1)

21-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

stripAnsi regex is missing the ESC character.

Real ANSI SGR sequences start with \x1B[ (or sometimes \x9B), but this regex matches only [<digits>m. As-is it will leave an orphaned \x1B byte in the output and would also (in principle) strip any literal [<digits>m substrings that happen to appear in legitimate content. Most of the suite gets away with this because the assertions use .toContain, but the helper is fragile and easy to misuse for negative assertions.

🧹 Proposed fix
 function stripAnsi(value: string): string {
-  return value.replace(/\[[0-9;]*m/g, "");
+  // eslint-disable-next-line no-control-regex
+  return value.replace(/\x1B\[[0-9;]*m/g, "");
 }
🤖 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/__tests__/ChatView.test.tsx` around lines 21 - 23,
The stripAnsi helper's regex is wrong because it doesn't include the ESC control
byte, so it may leave an orphaned ESC or wrongly strip plain text; update the
stripAnsi function to match real ANSI SGR sequences by including the ESC (and
optionally 0x9B) prefix — e.g., change the regex used in stripAnsi to match
either \x1B or \x9B followed by '[' and the parameter bytes (digits and
semicolons) and the final 'm' (for example use a pattern like
(?:\x1B|\x9B)\[[0-9;]*m with the global flag) so it only removes real ANSI
sequences.
apps/desktop/src/main/services/chat/agentChatService.test.ts-1220-1236 (1)

1220-1236: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a separator before userText in the VM context helper

userText is concatenated directly onto the last bullet line, which creates malformed synthetic prompt text and can reduce test fidelity for delimiter-sensitive stripping logic.

Suggested patch
 function automaticMacosVmContextText(userText: string): string {
   return [
@@
-    `- Keep VM-side edits inside the mounted guest lane path so the host lane and guest stay aligned.${userText}`,
+    "- Keep VM-side edits inside the mounted guest lane path so the host lane and guest stay aligned.",
+    userText,
   ].join("\n");
 }
🤖 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/chat/agentChatService.test.ts` around lines
1220 - 1236, automaticMacosVmContextText currently concatenates userText
directly onto the last bullet, corrupting delimiters; update
automaticMacosVmContextText to insert a clear separator before appending
userText (for example a newline or a distinct delimiter line such as "\n\n---\n"
or at minimum "\n") so userText begins on its own line and does not merge with
the preceding bullet.
apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx-711-712 (1)

711-712: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hide the lane/tab-switch header actions when their callbacks are absent.

onOpenSessionInTabsView and onGoToLane are typed as optional (lines 635-636), but the "Open in Tabs view" and "Go to lane" buttons — plus LaneChip's onClick={gotoLane} — render unconditionally. When the parent doesn't pass these props, the affordances are still presented as interactive but silently do nothing.

Gate rendering on the presence of the callback (and pass a real onClick only when bound):

🛠️ Proposed fix
-      const openInTabs = () => onOpenSessionInTabsView?.(session.id);
-      const gotoLane = () => onGoToLane?.(session.laneId);
+      const openInTabs = onOpenSessionInTabsView ? () => onOpenSessionInTabsView(session.id) : null;
+      const gotoLane = onGoToLane ? () => onGoToLane(session.laneId) : null;
       return [session.id, {
         ...
         headerActions: (
           <>
             <LaneChip
               laneName={session.laneName}
               laneColor={laneAccentColor}
               maxWidth={120}
-              onClick={gotoLane}
+              {...(gotoLane ? { onClick: gotoLane } : {})}
             />
             ...
-            <SmartTooltip content={{ label: "Open in Tabs view", ... }}>
-              <button ... onClick={openInTabs} ...>
-                <ArrowSquareOut size={11} />
-              </button>
-            </SmartTooltip>
-            <SmartTooltip content={{ label: "Go to lane", ... }}>
-              <button ... onClick={gotoLane} ...>
-                <Crosshair size={11} />
-              </button>
-            </SmartTooltip>
+            {openInTabs ? (
+              <SmartTooltip content={{ label: "Open in Tabs view", description: "Switch to the single-pane tab view focused on this session." }}>
+                <button type="button" onClick={openInTabs} ...><ArrowSquareOut size={11} /></button>
+              </SmartTooltip>
+            ) : null}
+            {gotoLane ? (
+              <SmartTooltip content={{ label: "Go to lane", description: "Switch the active lane to this session's lane." }}>
+                <button type="button" onClick={gotoLane} ...><Crosshair size={11} /></button>
+              </SmartTooltip>
+            ) : null}

Also applies to: 724-759

🤖 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/WorkViewArea.tsx` around lines
711 - 712, The "Open in Tabs view" and "Go to lane" affordances are rendered
even when their callbacks are undefined; update the rendering logic so these
buttons and LaneChip are only rendered when the corresponding props are present
and only pass onClick handlers when bound: check onOpenSessionInTabsView before
rendering the openInTabs button / creating openInTabs closure, and check
onGoToLane before rendering the go-to-lane button and before passing gotoLane
into LaneChip (i.e., render the elements conditionally and only supply
onClick={gotoLane} when onGoToLane exists).
apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx-982-1010 (1)

982-1010: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add role="tablist" to the ungrouped tab strip wrapper.

WorkTab renders an inner <button role="tab"> (line 507), but the ungrouped strip's wrapping <div className="ade-work-tab-strip-roomy ..."> has no role="tablist". The grouped path already sets role="tablist" on .ade-work-lane-band-tabs (line ~1148), so screen-reader semantics are inconsistent across tab modes and tab-group navigation breaks in the ungrouped path.

♿ Proposed fix
-            <div className="ade-work-tab-strip-roomy w-max min-w-0">
+            <div className="ade-work-tab-strip-roomy w-max min-w-0" role="tablist">
             {visibleSessions.map((session, index) => {
🤖 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/WorkViewArea.tsx` around lines
982 - 1010, The ungrouped tab strip wrapper in WorkViewArea lacks the tablist
role used by the grouped path; update the wrapping div that renders
visibleSessions (the element with className "ade-work-tab-strip-roomy") to
include role="tablist" so WorkTab's inner buttons (role="tab") are announced and
keyboard navigation works consistently with the grouped path.
apps/desktop/src/main/services/chat/agentChatService.ts-9891-9902 (1)

9891-9902: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Synthesize subagent_started on session.deleted for parity with session.updated.

The session.updated branch calls ensureSubagentStarted() first so the panel always has a row to update. The deleted branch skips that step, so if session.created/session.updated were missed (out-of-order delivery, late subscription, restart races) the renderer receives a subagent_result for a taskId it never observed starting. Mirror the defensive call here to avoid orphan result rows.

🛠️ Proposed fix
             } else {
               // session.deleted
+              // Synthesize started first if we missed the created/updated events
+              // so the panel has a row to mark complete.
+              ensureSubagentStarted();
               emitChatEvent(managed, {
                 type: "subagent_result",
                 taskId: childKey,
                 parentToolUseId: null,
                 status: "completed",
                 summary: formatSummary(),
                 turnId,
               });
               runtime.subagentSessionIds.delete(childKey);
             }
🤖 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/chat/agentChatService.ts` around lines 9891 -
9902, The session.deleted branch can emit a subagent_result for a taskId the
renderer never saw start; call ensureSubagentStarted(...) before emitting so the
panel always has a row to update. In the session.deleted branch (the block that
currently calls emitChatEvent(managed, { type: "subagent_result", taskId:
childKey, ... }) and runtime.subagentSessionIds.delete(childKey)), invoke the
same ensureSubagentStarted(managed, childKey, turnId) call used in the
session.updated branch (or its equivalent signature) immediately prior to
emitChatEvent, then proceed to emit the subagent_result and delete from
runtime.subagentSessionIds to maintain parity and avoid orphan result rows.
apps/desktop/src/main/services/chat/agentChatService.ts-1303-1307 (1)

1303-1307: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fragile coupling to producer wording — dedup silently regresses if upstream drifts.

stripAutomaticMacosVmContextPrefix deduplicates the macOS VM context block by exact-matching one of AUTOMATIC_MACOS_VM_CONTEXT_ENDINGS. These strings are produced by AgentChatPane.tsx (lines 434–436) and defined as static constants in agentChatService.ts (lines 1304–1306). If the renderer ever changes punctuation or wording in either ending, indexOf returns -1 on all iterations, the function returns null, and deduplication silently fails — the header reappears in subsequent messages.

Consider exporting the header and endings from the producer module to establish a single source of truth, or add a unit test asserting the producer's output is recognized by stripAutomaticMacosVmContextPrefix.

🤖 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/chat/agentChatService.ts` around lines 1303 -
1307, stripAutomaticMacosVmContextPrefix is fragile because it exact-matches
AUTOMATIC_MACOS_VM_CONTEXT_ENDINGS defined locally and duplicated in
AgentChatPane.tsx; if the renderer wording drifts dedupe will silently fail. Fix
by making the header and endings a single source of truth: move or export
AUTOMATIC_MACOS_VM_CONTEXT_HEADER and AUTOMATIC_MACOS_VM_CONTEXT_ENDINGS from
AgentChatPane.tsx (or a new shared module) and import them into
agentChatService.ts so stripAutomaticMacosVmContextPrefix uses the same
constants, and add a unit test that renders the producer output (AgentChatPane)
and asserts stripAutomaticMacosVmContextPrefix successfully
recognizes/deduplicates it.
🧹 Nitpick comments (20)
apps/desktop/src/main/services/opencode/openCodeRuntime.test.ts (1)

132-132: 💤 Low value

Consider testing the task tool allowance more directly.

The test now checks that code_search and web_search are disabled, which is correct. However, this doesn't directly verify the key change: that the task tool is now allowed. Consider adding an assertion that explicitly confirms task is not in the disabled tools list, or add a comment explaining why checking other disabled tools is sufficient.

Current approach is valid but less clear about the actual behavior change.

💡 Optional: Add explicit verification
          "ade-plan": expect.objectContaining({
            tools: expect.objectContaining({ code_search: false, web_search: false }),
          }),
+         // Verify task tool is not disabled (it should not be present in tools config)
+         "ade-plan": expect.objectContaining({
+           tools: expect.not.objectContaining({ task: expect.anything() }),
+         }),

Or simply add a comment:

          "ade-plan": expect.objectContaining({
+           // Verify code_search and web_search are disabled; task is intentionally omitted (allowed)
            tools: expect.objectContaining({ code_search: false, web_search: false }),
          }),
🤖 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/opencode/openCodeRuntime.test.ts` at line 132,
The test currently asserts tools: expect.objectContaining({ code_search: false,
web_search: false }) but doesn't explicitly verify that the "task" tool is
allowed; update the test in openCodeRuntime.test (the assertion block that sets
"tools") to also explicitly assert the task tool is permitted—either by adding
an expectation that "task" is true/allowed or by asserting that "task" is not
present in the disabled list (or adding a clarifying comment next to tools:
expect.objectContaining(...) explaining why checking other tools implies task is
allowed). Locate the expectation that mentions "tools" and add the additional
assertion referencing "task" so the key behavior change is directly tested.
apps/desktop/src/main/services/ai/aiIntegrationService.test.ts (1)

455-463: ⚡ Quick win

Consider adding positive assertions to strengthen test coverage.

The test correctly verifies that probeClaudeRuntimeHealth is not called during forced refresh. However, it could be more robust by also asserting that:

  1. The status is returned successfully
  2. Claude availability is still correctly reported in the result
  3. The force refresh still works for other aspects (e.g., invalidates caches)
💚 Suggested enhancement
 it("keeps forced status refresh non-interactive for Claude", async () => {
   const { service } = makeService({
     availability: { claude: true, codex: false, cursor: false, droid: false },
   });

-  await service.getStatus({ force: true });
+  const status = await service.getStatus({ force: true });

   expect(mockState.probeClaudeRuntimeHealth).not.toHaveBeenCalled();
+  expect(status.availableProviders.claude.binary.present).toBe(true);
+  expect(status.mode).toBe("subscription");
 });

This adds assertions confirming that the status is correctly populated even without the interactive probe.

🤖 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/ai/aiIntegrationService.test.ts` around lines
455 - 463, Add positive assertions to the "keeps forced status refresh
non-interactive for Claude" test: call service.getStatus({ force: true }) and
assert the returned status object is defined and indicates Claude is available
(use the field on the returned object that represents Claude availability), also
assert that forcing refresh invalidates/refreshes other cached aspects by
comparing a prior cached status vs the new result or by calling getStatus
without force then with force and verifying expected changes; reference
makeService, service.getStatus and mockState.probeClaudeRuntimeHealth to locate
where to add these checks.
apps/ade-cli/src/services/sync/syncHostService.ts (1)

1299-1309: ⚡ Quick win

Add a fallback path when native dns-sd publish fails.

On Line 1353, native mode becomes exclusive. On Line 1299 and Line 1305 failure/exit only logs, so LAN discovery can stay down indefinitely instead of falling back to Bonjour.

💡 Fallback sketch
+let nativeLanDiscoveryFailed = false;

 const shouldUseNativeLanDiscovery = (): boolean =>
-  process.platform === "darwin" && typeof process.versions.electron === "string";
+  process.platform === "darwin"
+  && typeof process.versions.electron === "string"
+  && !nativeLanDiscoveryFailed;

 const publishNativeLanDiscovery = (
   serviceName: string,
   port: number,
   txt: Record<string, string>,
 ): void => {
   stopNativeLanDiscovery();
   const child = spawn("dns-sd", [/* ... */], { stdio: "ignore" });
   nativeBonjourProcess = child;
   child.unref();
   child.once("error", (error) => {
     if (nativeBonjourProcess === child) nativeBonjourProcess = null;
+    nativeLanDiscoveryFailed = true;
+    publishLanDiscovery(port, { force: true });
     args.logger.warn("sync_host.discovery_native_publish_failed", {
       error: error instanceof Error ? error.message : String(error),
     });
   });
   child.once("exit", (code, signal) => {
     if (nativeBonjourProcess !== child) return;
     nativeBonjourProcess = null;
+    nativeLanDiscoveryFailed = true;
+    publishLanDiscovery(port, { force: true });
     args.logger.warn("sync_host.discovery_native_exited", { code, signal });
   });
 };

Also applies to: 1353-1359

🤖 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/services/sync/syncHostService.ts` around lines 1299 - 1309,
The error/exit handlers for the spawned native Bonjour process only log and
clear nativeBonjourProcess, leaving LAN discovery down; update both
child.once("error", ...) and child.once("exit", ...) to not only null out
nativeBonjourProcess but also trigger a fallback to the built-in Bonjour path
(e.g. call a function like startBonjourFallback() or enableBonjourMode())
whenever nativeBonjourProcess === child; also add the same fallback invocation
where native mode is made exclusive (the block that flips native mode exclusive
around the nativeBonjourProcess assignment) so that if native publish fails or
native mode is exclusive and unusable, the code falls back to the non-native
Bonjour implementation. Ensure the fallback function initializes the existing
Bonjour discovery flow used elsewhere.
apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts (1)

46-69: ⚡ Quick win

Consider adding explicit codex-cli plan-mode test coverage.

The plan-mode tests now only exercise codex-app-server, while the implementation treats both codex-cli and codex-app-server identically (line 155 in systemPrompt.ts). While testing one runtime is sufficient for current behavior, adding a parallel test for codex-cli would make the test suite more robust and catch regressions if the conditional logic changes.

🧪 Suggested test to add
it("keeps Codex CLI plan context aligned with native plan mode", () => {
  const result = buildCodingAgentSystemPrompt({
    cwd: "/x",
    permissionMode: "plan",
    runtime: "codex-cli",
  });

  expect(result).toContain("Native Codex Plan Mode controls planning and approval");
  expect(result).toContain("proposed-plan mechanism");
  expect(result).toContain("Do not use TodoWrite, update_plan, or exitPlanMode");
});
🤖 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/ai/tools/systemPrompt.test.ts` around lines 46
- 69, Add parallel tests exercising runtime "codex-cli" to mirror the existing
"codex-app-server" plan-mode cases: call buildCodingAgentSystemPrompt with
permissionMode: "plan" and runtime: "codex-cli" and assert it contains "Native
Codex Plan Mode controls planning and approval", "proposed-plan mechanism", and
"Do not use TodoWrite, update_plan, or exitPlanMode"; also add the
non-interactive variant (interactive: false) asserting it contains "make the
safest reasonable assumptions" and does not contain "use request_user_input".
Use the same test naming pattern as the existing "keeps Codex plan context
aligned..." and "does not tell non-interactive..." tests so the new tests
reference the same behavior paths.
apps/desktop/src/main/utils/terminalSessionSignals.ts (1)

25-31: ⚡ Quick win

Consider adding JSDoc documentation.

The validation logic is sound (control characters, leading dashes), but the function would benefit from JSDoc explaining:

  • What constitutes a valid resume target ID
  • Why control characters and leading "-" are rejected
  • Return value semantics (null for invalid/empty vs. string for valid)
📝 Suggested documentation
+/**
+ * Validates and sanitizes a resume target ID for CLI commands.
+ * Rejects empty strings, control characters (including ANSI escapes),
+ * and values starting with "-" to prevent command injection.
+ * `@returns` Sanitized target ID, or null if invalid/empty
+ */
 export function sanitizeResumeTargetId(value: string | null | undefined): string | null {
🤖 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/utils/terminalSessionSignals.ts` around lines 25 - 31,
Add JSDoc to sanitizeResumeTargetId describing what a valid resume target ID is
(non-empty string without ASCII control characters [\x00-\x1F\x7F] and not
starting with a hyphen), explain why control characters and leading "-" are
rejected (sanitization/security/CLI semantics), and document the return
semantics (returns null for empty or invalid inputs, otherwise the trimmed
string). Place the JSDoc immediately above the sanitizeResumeTargetId
declaration and reference the input type (string | null | undefined) and the
exact return type (string | null) so callers understand expected behavior.
apps/ade-cli/src/tuiClient/components/Header.tsx (1)

30-42: ⚡ Quick win

Consider documenting the project name suppression logic.

The logic that hides the project name when it appears in the branch or worktree name is subtle:

  • It checks if branchRef ends with the normalized project name (case-insensitive)
  • It checks if the worktree directory name exactly equals the normalized project name

This could be surprising behavior. For example, if your project is "auth" and your branch is "feature/oauth", the logic won't suppress "auth" even though "oauth" contains "auth" as a substring (because it uses endsWith, not includes).

📝 Suggested documentation or extraction

Add a comment explaining the suppression rules:

+ // Hide the project name in the header if:
+ // 1. The project is "ade" (default/meta project name), OR
+ // 2. The branch ref already ends with the project name (e.g., branch="feat/myapp", project="myapp"), OR
+ // 3. The worktree directory name matches the project name exactly
  const projectRepeatsBranch = Boolean(
    normalizedProject
    && lane
    && (
      branchRef.toLowerCase().endsWith(normalizedProject)
      || worktreeName === normalizedProject
    ),
  );

Or extract to a helper function for clarity:

function shouldSuppressProjectName(
  projectName: string,
  lane: LaneSummary | null,
): boolean {
  const normalized = projectName.trim().toLowerCase();
  if (!normalized || normalized === "ade") return true;
  if (!lane) return false;
  
  const branch = lane.branchRef?.trim().toLowerCase() ?? "";
  const worktree = lane.worktreePath?.split(/[\\/]/).filter(Boolean).pop()?.toLowerCase() ?? "";
  
  return branch.endsWith(normalized) || worktree === normalized;
}
🤖 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/components/Header.tsx` around lines 30 - 42, The
current suppression logic (variables normalizedProject, branchRef, worktreeName,
projectRepeatsBranch, showProject) is subtle and should be documented or
extracted; update Header.tsx by either adding a concise comment above the block
describing the rules (case-insensitive trim, branch must endWith the project
name, worktree must equal the project dir, and "ade" is always hidden) or
refactor the logic into a small helper function (e.g.
shouldSuppressProjectName(projectName, lane)) that returns the Boolean now
computed by projectRepeatsBranch/showProject and call that helper so the
behavior is explicit and easier to test.
apps/desktop/src/main/services/cto/linearCredentialService.ts (1)

270-308: ⚖️ Poor tradeoff

Consider extracting migration steps for readability.

The migrateLegacyProjectCredentialsIfNeeded function handles multiple migration paths in a single 38-line function with nested conditionals. While the logic appears correct, it's challenging to verify all branches and could be fragile during future changes.

♻️ Optional refactor to extract migration steps
const migrateLegacyProjectCredentialsIfNeeded = (): void => {
  if (!credentialStore) return;
  const projectRoot = path.resolve(path.dirname(args.adeDir));
  const migratedRoots = readMigratedProjectRoots();
  if (migratedRoots.has(projectRoot)) return;

  const hadEncryptedLegacyStore = fs.existsSync(tokenPath) || fs.existsSync(oauthClientPath);

  migrateTokenIfMissing();
  migrateOAuthClientIfMissing();

  if (!hadEncryptedLegacyStore || safeStorage.isEncryptionAvailable()) {
    markProjectMigrated();
  }
};

const migrateTokenIfMissing = (): void => {
  if (readMachineToken()) return;
  
  const legacyToken = readEncryptedToken();
  if (legacyToken) {
    persistMachineToken(legacyToken);
    return;
  }

  const legacyPath = path.join(args.adeDir, "local.secret.yaml");
  try {
    const raw = fs.readFileSync(legacyPath, "utf8");
    const token = extractLegacyToken(raw);
    if (token) persistMachineToken({ token, authMode: "manual" });
  } catch (error: unknown) {
    if (!isEnoentError(error)) {
      args.logger?.warn("linear_sync.token_import_failed", {
        legacyPath,
        error: getErrorMessage(error),
      });
    }
  }
};

const migrateOAuthClientIfMissing = (): void => {
  if (readMachineOAuthClientCredentials()) return;
  const legacyOAuth = readStoredOAuthClientCredentials() ?? readOAuthConfigFileCredentials();
  if (legacyOAuth) persistMachineOAuthClientCredentials(legacyOAuth);
};

This extraction makes the migration flow clearer: "migrate token, migrate OAuth client, mark as migrated."

🤖 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/cto/linearCredentialService.ts` around lines
270 - 308, The migrateLegacyProjectCredentialsIfNeeded function is doing
multiple migration steps in one long block which reduces readability; refactor
by extracting the token and OAuth client migration logic into two helper
functions (e.g., migrateTokenIfMissing and migrateOAuthClientIfMissing) and call
them from migrateLegacyProjectCredentialsIfNeeded while preserving existing
guards (credentialStore check), projectRoot/migratedRoots behavior,
hadEncryptedLegacyStore computation, the existing error handling/logging
(isEnoentError/getErrorMessage), and the final markProjectMigrated call so
behavior is unchanged. Ensure migrateTokenIfMissing replicates the
readMachineToken/readEncryptedToken/readFileSync/extractLegacyToken/persistMachineToken
flow and migrateOAuthClientIfMissing replicates the
readMachineOAuthClientCredentials/readStoredOAuthClientCredentials/readOAuthConfigFileCredentials/persistMachineOAuthClientCredentials
flow.
apps/ade-cli/src/tuiClient/components/FooterControls.tsx (1)

65-274: ⚡ Quick win

Consider decomposing FooterControls for clarity.

The component now handles multiple distinct concerns: provider/model display, token tracking, agents availability, scroll state, drawer/pane state, and steer staging. While the current implementation is functional, extracting sub-components (e.g., ModeBar, ActionHints) would improve testability and readability.

🤖 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/components/FooterControls.tsx` around lines 65 -
274, FooterControls is doing too many jobs (branding/model display,
token/context bar, agents toggle/chip, and action hint logic); refactor by
extracting smaller components: create ModeBar (renders
brand/modelDisplay/fastMode/reasoningEffort/permissionLabel/pendingSteerCount
and uses TokenBar for contextPercent/tokenSummary), ActionHints (encapsulates
the actionHint IIFE and uses Hint/TokenBar props like
chatScrollOffset/chatScrollMaxOffset/pendingSteerCount/drawerOpen/activePane/drawerMode),
and AgentsControls (renders showAgentsToggle, showAgentsChip and related
Toggle/Text bits). Replace the corresponding JSX in FooterControls with these
new components and pass only the needed props (e.g.,
provider/modelDisplay/fastMode/reasoningEffort/permissionLabel/contextPercent/tokenSummary/pendingSteerCount
to ModeBar;
chatScrollOffset/chatScrollMaxOffset/drawerOpen/activePane/drawerMode/pendingSteerCount
to ActionHints;
agentsAvailable/liveAgentCount/rightPaneShowsAgents/agentsOpen/agentsFocused to
AgentsControls) so FooterControls becomes a simple orchestrator.
apps/ade-cli/src/tuiClient/components/MentionPalette.tsx (1)

62-64: ⚡ Quick win

Enhance textWidth for unicode correctness.

The current implementation uses [...value].length which counts Unicode code points, but doesn't account for:

  • Combining marks (e.g., é as e + ´)
  • Zero-width characters
  • Emoji with modifiers or ZWJ sequences
  • Full-width vs half-width CJK characters

For terminal rendering, consider using a library like string-width (handles ANSI and Unicode width) or implementing grapheme cluster counting to ensure accurate column width calculations.

🤖 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/components/MentionPalette.tsx` around lines 62 -
64, The textWidth function currently returns [...value].length which
miscalculates terminal column width for combining marks, ZWJ/emoji sequences and
full-width CJK characters; replace its implementation in MentionPalette.tsx
(function textWidth) to use a proper width calculator such as the string-width
package (import stringWidth from 'string-width' and return stringWidth(value))
or, if avoiding dependencies, compute grapheme clusters and map them to display
widths (handle ANSI escapes, zero-width and fullwidth characters) so the
returned number reflects terminal column width accurately; update any
callers/tests that assume the old behavior.
apps/desktop/src/renderer/components/terminals/useWorkSessions.test.ts (2)

1302-1310: ⚡ Quick win

Consider expanding test coverage for reorderLaneSessionIdsForDisplay.

The single test case covers moving a pinned session, but the function likely handles additional scenarios that would benefit from explicit test coverage:

  • Moving unpinned sessions
  • Using edge: "before" instead of edge: "after"
  • Multiple pinned sessions in the base order
  • Edge cases (empty arrays, non-existent session IDs)

Broader coverage would increase confidence in the reordering logic, especially given the complexity of pinned vs unpinned session ordering described in the AI summary.

🤖 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/useWorkSessions.test.ts`
around lines 1302 - 1310, Add additional unit tests for
reorderLaneSessionIdsForDisplay to cover missing scenarios: create tests for
moving an unpinned session (movedSessionId unpinned), using edge: "before" (both
pinned and unpinned moves), a baseOrder with multiple pinnedSessionIds, and edge
cases such as empty arrays and non-existent session IDs; for each test call
reorderLaneSessionIdsForDisplay with appropriate baseOrder, pinnedSessionIds,
movedSessionId, targetSessionId and assert the expected array result so the
function's pinned vs unpinned ordering behaviour is fully exercised.

352-376: ⚡ Quick win

Consider explicitly verifying no refresh during the inactive period.

The test name suggests verification that no blocking refresh occurs while active: false, but the test only asserts that a refresh happens after re-entering. To make the test's intent clearer and more robust, consider adding an explicit assertion before line 370:

listSessionsCachedMock.mockClear();
rerender({ active: false });
await act(async () => {
  await new Promise((r) => setTimeout(r, 0));
});
// Explicitly verify no refresh during inactive period
expect(listSessionsCachedMock).not.toHaveBeenCalled();
rerender({ active: true });
🤖 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/useWorkSessions.test.ts`
around lines 352 - 376, The test for useWorkSessions should explicitly assert
that no refresh is triggered while active is false: after calling
listSessionsCachedMock.mockClear() and rerender({ active: false }) and awaiting
the microtask (the existing act/timeout), add an assertion that
listSessionsCachedMock.not.toHaveBeenCalled() before rerender({ active: true });
this ensures the test verifies the inactive-period behavior in addition to the
existing post-reentry assertions.
apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx (1)

1065-1065: ⚡ Quick win

Consider clearing commandMenuAnchor when commandMenuTrigger is set to null.

Several code paths set setCommandMenuTrigger(null) without clearing the anchor state. While this doesn't cause functional issues (since the menu won't render when trigger is null), it leaves stale positioning data in state. For better state hygiene and to prevent confusion in future maintenance, consider adding setCommandMenuAnchor(null) alongside setCommandMenuTrigger(null) at these locations.

Also applies to: 2253-2253, 2408-2408, 2420-2420, 2697-2697, 2708-2708, 2389-2389

🤖 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/chat/AgentChatComposer.tsx` at line
1065, Several code paths call setCommandMenuTrigger(null) but leave stale
positioning state in commandMenuAnchor; update each location (e.g., where
setCommandMenuTrigger(null) is used around lines noted) to also call
setCommandMenuAnchor(null) so both trigger and anchor are cleared together;
search for setCommandMenuTrigger(null) and add setCommandMenuAnchor(null)
alongside it (ensure you update all instances including the ones referenced) to
keep state consistent.
apps/ade-cli/src/tuiClient/components/Drawer.tsx (4)

446-453: 💤 Low value

Empty-chat fallback only triggers when selectedChatIndex !== 0.

When the lane has no sessions and selectedChatIndex === 0 (the very common case where the user just opened the chat block fresh), this branch is skipped and the rendered block shows only CHATS · 0 followed by + new chat — no “No chats in lane.” affordance at all. Either drop the selectedChatIndex !== 0 guard or document why a selected-state distinguishes the empty-state copy; right now the condition reads like an unintended coupling between selection focus and empty messaging.

🤖 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/components/Drawer.tsx` around lines 446 - 453, The
empty-chat fallback currently only renders when sessions.length === 0 &&
selectedChatIndex !== 0, which hides the “No chats in lane.” message for the
common case selectedChatIndex === 0; remove the selectedChatIndex !== 0 guard so
the conditional becomes simply sessions.length === 0 (in the Drawer component
where the Box/Text fallback is returned) so the “No chats in lane.” affordance
always appears for an empty lane.

534-535: ⚡ Quick win

Dead parameters in MiniDrawer.

focused and browsingLaneId are required props on MiniDrawer but are immediately discarded with void. Either drop them from the prop type and the call site (line 173, 168) to keep the contract honest, or actually wire them in (mini drawer currently has no visual "focused" highlight and ignores which lane the user is browsing, which seems inconsistent with the full drawer).

🤖 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/components/Drawer.tsx` around lines 534 - 535,
MiniDrawer currently discards the focused and browsingLaneId props (they're
voided) creating a broken contract; either remove these props from MiniDrawer's
prop type and stop passing them from its callers, or wire them up: remove the
two `void` statements, use `focused` to apply the focused/highlighted styling in
the MiniDrawer render logic, and use `browsingLaneId` to mark the active lane
(e.g., compare lane.id to browsingLaneId and apply active class/aria attributes)
so behavior matches the full drawer; update the prop type and call sites
accordingly (or delete the props at callers) and ensure TypeScript types and any
tests reflect the change.

326-335: 💤 Low value

branchMax can exceed the remaining meta width when space is tight.

metaWidth can collapse to a small or even negative value on very narrow drawers; branchMax = Math.max(3, metaWidth - …) then forces ≥3 chars for the branch even when no horizontal room remains, which can cause the line-2 text to overflow the lane card and visually wrap. Consider clamping branchMax to Math.max(0, metaWidth - …) (or skipping the branch entirely when metaWidth <= 0) so the layout degrades gracefully on small terminals.

🤖 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/components/Drawer.tsx` around lines 326 - 335, The
branch width calculation allows branchMax to be at least 3 even when metaWidth
is small/negative, causing overflow; update the logic around metaWidth/branchMax
in Drawer.tsx (the block that computes metaWidth, detailMax, branchMax,
truncBranch, truncDetail) to clamp branchMax to not exceed available space (use
Math.max(0, metaWidth - (detailMax ? detailMax + 3 : 0)) instead of Math.max(3,
...)) or skip rendering the branch when metaWidth <= 0, and ensure
truncate(truncBranch, branchMax) and related rendering handle branchMax === 0
safely to avoid visual wrapping on narrow terminals.

125-125: 💤 Low value

Default new Set<string>() allocated every render.

Each render of Drawer allocates a fresh Set for the unavailableLaneIds default, which becomes a new identity for any downstream memoization. Right now nothing here memoizes against it, but if you ever pass it into useMemo/useCallback deps it'll silently invalidate. A module-level frozen empty set is a cheap, safe alternative.

🧹 Proposed fix
+const EMPTY_LANE_ID_SET: ReadonlySet<string> = new Set<string>();
+
 export function Drawer({
   ...
-  unavailableLaneIds = new Set<string>(),
+  unavailableLaneIds = EMPTY_LANE_ID_SET,
 }: {
🤖 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/components/Drawer.tsx` at line 125, The default
parameter unavailableLaneIds = new Set<string>() in the Drawer component
allocates a new Set on every render; replace this with a module-level frozen
empty set (e.g. const EMPTY_UNAVAILABLE_LANE_IDS = Object.freeze(new
Set<string>())) and use that constant as the default for the unavailableLaneIds
parameter/prop in Drawer to preserve identity across renders and avoid
accidental memo invalidation.
apps/desktop/src/renderer/components/terminals/useWorkSessions.ts (2)

559-572: 💤 Low value

togglePinnedSession always replaces state, even when caller-provided id is unchanged.

Minor: each call produces a new pinnedSessionIds array (toggle either adds or removes), which is fine for a toggle but means rapid double-calls churn the store. If you ever batch this via an effect or external command, the new array identity will invalidate downstream useMemo/useCallback even when the logical set is unchanged after two toggles. Not blocking — flagging for awareness.

🤖 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/useWorkSessions.ts` around
lines 559 - 572, togglePinnedSession always creates a new pinnedSessionIds array
even when the resulting set is unchanged; update the callback in
togglePinnedSession to check current pinnedSessionIds (from
prev.pinnedSessionIds or empty array), compute whether any change is needed
(e.g., if sessionId already present and you intend to remove, or absent and
intend to add), and if there is no change return prev unchanged; otherwise
return the updated state (still adding/removing via pinnedSessionIds) so
downstream memoization won't be invalidated by redundant array identity changes.
Use the existing symbols togglePinnedSession and setProjectViewState to locate
and modify the logic.

521-557: 💤 Low value

reorderLaneSessions doesn't surface failures.

When the helper returns null (no movement / invalid args) the callback silently returns prev. That's fine for the happy path, but the callback also early-returns at line 523 on invalid args without any telemetry/log, and a stale laneSessionOrder[laneId] (e.g. left over from a previously-renamed or removed session id) is left in store untouched even though baseOrder was rebuilt against the current open set. Consider trimming laneSessionOrder[laneId] whenever it diverges from the live lane set — otherwise the persisted state grows monotonically as sessions are closed/removed.

🤖 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/useWorkSessions.ts` around
lines 521 - 557, reorderLaneSessions silently ignores invalid inputs and never
trims stale entries in laneSessionOrder; update reorderLaneSessions to (1) emit
a telemetry/log when called with invalid args (movedSessionId/targetSessionId
missing or equal) instead of silently returning, and (2) when computing
baseOrder compare it against prev.laneSessionOrder?.[laneId] and if they diverge
replace/trim prev.laneSessionOrder[laneId] with the rebuilt baseOrder (or the
filtered existing that matches sessionsInLane) before calling
reorderLaneSessionIdsForDisplay so the store doesn't accumulate removed/renamed
session ids; use the existing symbols reorderLaneSessions,
reorderLaneSessionIdsForDisplay, setProjectViewState, sessionsById and
laneSessionOrder to locate where to add logging and the trimming logic.
apps/ade-cli/src/tuiClient/__tests__/ChatView.test.tsx (2)

295-314: 💤 Low value

Inconsistent newline splitting.

Line 313 uses older.split("\n") while the rest of the suite (e.g. transcriptLines at line 46) uses /\r?\n/. If Ink ever emits CRLF on this path the last line will incorrectly contain a trailing \r, which could mask regressions. Align with the helper for consistency.

🧹 Proposed fix
-    expect(older.split("\n").at(-1)).toContain("↓ newer messages");
+    expect(transcriptLines(older).at(-1)).toContain("↓ newer messages");
🤖 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/__tests__/ChatView.test.tsx` around lines 295 -
314, The test uses older.split("\n").at(-1) which can leave a trailing '\r' on
CRLF outputs; update the assertion to split using the same helper regex as the
suite (use older.split(/\r?\n/).at(-1) or reuse the transcriptLines helper) so
the last-line check is CRLF-safe; modify the assertion in the "shows the bottom
viewport by default and older rows when scrolled" test that references
older.split("\n") accordingly.

562-573: 💤 Low value

transcriptBody flattens via JSON.stringify — fragile for content assertions.

Asserting transcriptBody.toContain("Subagent started: Explore renderer") against JSON.stringify(event) works only because the literal string happens to appear inside a stringified field. If any envelope’s event ever serializes that text with embedded escapes (e.g., quotes, newlines, unicode), these assertions can become false positives/negatives. Consider asserting directly on the relevant entry.event field (e.g., description, summary) rather than on a JSON dump.

🤖 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/__tests__/ChatView.test.tsx` around lines 562 -
573, The tests build a fragile transcriptBody by JSON.stringify(entry.event) and
assert substrings against that; instead, assert directly on the relevant event
properties from transcriptEvents (e.g., map transcriptEvents to
entry.event.description or entry.event.summary) or find the specific envelope
where entry.event.type === "subagent" and assert its description/summary
contains "Subagent started: Explore renderer", "found the renderer path", and
that no entry.event contains "unrelated agent result"; update the expectations
that currently use transcriptBody to use those direct property checks and keep
checks for renderEvents/frame (renderEvents, transcriptEvents, entry.event,
description, summary).

Comment thread apps/ade-cli/src/tuiClient/__tests__/Palettes.test.tsx
Comment on lines +287 to +295
if (subagentParentItemIds.size > 0) {
for (const envelope of args.events) {
const parentItemId = workEventParentItemId(envelope.event);
const itemId = workEventItemId(envelope.event);
if (parentItemId && itemId && subagentParentItemIds.has(parentItemId)) {
subagentChildItemIds.add(itemId);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter subagent work transitively, not just one level deep.

subagentChildItemIds is only populated for direct children of a subagent parent item. Any grandchild work item will miss both checks here, so nested command/file-change activity can leak into the main chat timeline instead of staying scoped to the subagent view.

Also applies to: 370-373

🤖 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/aggregate.ts` around lines 287 - 295, The current
loop only adds direct children to subagentChildItemIds, missing nested
descendants; build a transitive closure instead: construct a parent→children map
from args.events (using workEventParentItemId and workEventItemId), then perform
a BFS/DFS starting from subagentParentItemIds to collect all descendant item IDs
into subagentChildItemIds (or alternately iterate adding children until no new
IDs appear). Apply the same fix to the other occurrence where a similar
direct-child-only loop is used.

Comment on lines +367 to +369
if (event.type === "reasoning") {
continue;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

reasoning events are being dropped instead of aggregated into thinking blocks.

ChatView now has a dedicated thinking renderer, but this branch just skips every reasoning event. That removes thinking output from the transcript entirely and leaves the later turn-completion logic with nothing to finalize.

🤖 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/aggregate.ts` around lines 367 - 369, The branch
that checks if (event.type === "reasoning") should not drop those events;
instead reclassify or merge them into the existing "thinking" aggregation so the
ChatView thinking renderer and the turn-completion logic have content to
finalize. Locate the code handling event processing in aggregate.ts (the block
that contains if (event.type === "reasoning")) and replace the continue with
logic that either converts event.type = "thinking" or appends the reasoning
payload into the current thinking aggregation (e.g., push into the same
buffer/array used for "thinking" events or call the aggregator method that
collects thinking pieces), preserving timestamps/metadata so subsequent
turn-completion/finalize functions can operate normally.

Comment on lines +491 to +508
if (event.type === "done") {
const startMs = turnId ? turnStart.get(turnId) : undefined;
const durationMs = startMs !== undefined ? entry.timestamp - startMs : undefined;
for (const block of blocks) {
const blockTurn = (block as { turnId?: string | null }).turnId ?? null;
if (blockTurn !== turnId) continue;
if (block.kind === "work-block" || block.kind === "thinking" || block.kind === "plan" || block.kind === "compaction") {
block.live = false;
if (durationMs !== undefined && (block.kind === "work-block" || block.kind === "thinking")) {
block.durationMs = block.durationMs ?? durationMs;
}
}
}
continue;
}
if (AGGREGATED_TYPES.has(event.type)) {
// Already handled above or intentionally skipped (tokens, codex_*).
continue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

turn-footer never gets emitted, so duration/token/cost summaries won't render.

This code finalizes live blocks on done, but it never synthesizes the turn-footer block that ChatView knows how to display. The later aggregated-types skip path also swallows token-usage events, so footer metadata currently has no way to reach the UI.

Comment on lines +143 to +149
function commandAvailable(command: string): boolean {
const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
shell: process.platform !== "win32",
stdio: "ignore",
});
return result.status === 0;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify commandAvailable logic handles all platforms correctly

# Test the pattern used in commandAvailable
echo "Testing command availability check pattern..."

# Unix-style (should work on macOS/Linux)
if command -v node > /dev/null 2>&1; then
  echo "✓ 'command -v' works on $(uname -s)"
else
  echo "✗ 'command -v' failed on $(uname -s)"
fi

# Check if the shell parameter is necessary
node --eval "
const { spawnSync } = require('child_process');
// Test without shell
const r1 = spawnSync('command', ['-v', 'node'], { shell: false, stdio: 'ignore' });
console.log('Without shell:', r1.status === 0 ? 'pass' : 'fail');
// Test with shell
const r2 = spawnSync('command', ['-v', 'node'], { shell: true, stdio: 'ignore' });
console.log('With shell:', r2.status === 0 ? 'pass' : 'fail');
"

Repository: arul28/ADE

Length of output: 445


Address shell injection vulnerability when passing arguments with shell: true.

The commandAvailable function correctly uses shell: true on Unix platforms where command -v requires shell resolution. However, the Node.js deprecation warning reveals a security vulnerability: arguments passed to spawnSync with shell: true are not escaped and can lead to shell injection attacks if the command parameter contains special characters. Consider refactoring to avoid shell invocation (e.g., using which or type commands without shell spawning) or properly escaping the command argument.

🤖 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/imageTargets.ts` around lines 143 - 149, The
commandAvailable function is vulnerable because it calls spawnSync with shell:
true and unescaped user input; replace the shell invocation with a safe exec of
a non-shell binary on Unix (for example use spawnSync("which", [command],
{stdio:"ignore"}) instead of "command -v" with shell) while keeping Windows' use
of "where" (spawnSync("where", [command], ...)); update commandAvailable to call
the external binary (which/where) without shell:true so arguments are passed as
an argv array and not interpreted by a shell to eliminate injection risk.

Comment thread apps/desktop/src/main/services/ai/aiIntegrationService.ts Outdated
Comment on lines +379 to +391
function migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {
if (isCredentialLegacyKeychainMigrated()) return currentStore;
if (!isMacosKeychainAvailable()) return currentStore;

const index = readMacosKeychainProviderIndex();
const providerCandidates = new Set([
...index.providers,
...Object.keys(ENV_KEY_PROVIDERS),
]);
const keychainStore = readMacosKeychainStore(providerCandidates);
const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);
markCredentialLegacyKeychainMigrated();
return nextStore;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid marking keychain migration complete when keychain reads fail

Line 390 currently marks migration complete even when legacy keychain reads may have failed silently (e.g., readMacosKeychainSecret returned null with macosKeychainError). That can permanently disable fallback reads and strand existing keys.

Suggested fix
 function migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {
   if (isCredentialLegacyKeychainMigrated()) return currentStore;
   if (!isMacosKeychainAvailable()) return currentStore;
 
+  const errorBefore = macosKeychainError;
   const index = readMacosKeychainProviderIndex();
   const providerCandidates = new Set([
     ...index.providers,
     ...Object.keys(ENV_KEY_PROVIDERS),
   ]);
   const keychainStore = readMacosKeychainStore(providerCandidates);
   const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);
-  markCredentialLegacyKeychainMigrated();
+  const hadNewKeychainError = macosKeychainError != null && macosKeychainError !== errorBefore;
+  if (!hadNewKeychainError) {
+    markCredentialLegacyKeychainMigrated();
+  }
   return nextStore;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {
if (isCredentialLegacyKeychainMigrated()) return currentStore;
if (!isMacosKeychainAvailable()) return currentStore;
const index = readMacosKeychainProviderIndex();
const providerCandidates = new Set([
...index.providers,
...Object.keys(ENV_KEY_PROVIDERS),
]);
const keychainStore = readMacosKeychainStore(providerCandidates);
const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);
markCredentialLegacyKeychainMigrated();
return nextStore;
function migrateLegacyKeychainIntoCredentialStore(currentStore: StoredKeys): StoredKeys {
if (isCredentialLegacyKeychainMigrated()) return currentStore;
if (!isMacosKeychainAvailable()) return currentStore;
const errorBefore = macosKeychainError;
const index = readMacosKeychainProviderIndex();
const providerCandidates = new Set([
...index.providers,
...Object.keys(ENV_KEY_PROVIDERS),
]);
const keychainStore = readMacosKeychainStore(providerCandidates);
const nextStore = mergeLegacyValuesIntoCredentialStore(currentStore, keychainStore);
const hadNewKeychainError = macosKeychainError != null && macosKeychainError !== errorBefore;
if (!hadNewKeychainError) {
markCredentialLegacyKeychainMigrated();
}
return nextStore;
}
🤖 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/ai/apiKeyStore.ts` around lines 379 - 391, The
migration currently calls markCredentialLegacyKeychainMigrated()
unconditionally, which can disable future fallback reads if macOS keychain reads
failed silently; update migrateLegacyKeychainIntoCredentialStore to only mark
migration as complete when the macOS keychain read succeeded and produced usable
data. Concretely, after calling readMacosKeychainStore (and handling any
underlying readMacosKeychainSecret error indicators), check for success by
verifying there is no macosKeychainError and/or that keychainStore contains
entries (or that mergeLegacyValuesIntoCredentialStore actually changed
currentStore); if the read failed or returned no usable keys, return
currentStore without calling markCredentialLegacyKeychainMigrated(), otherwise
proceed to merge and then call markCredentialLegacyKeychainMigrated(). Ensure
references: migrateLegacyKeychainIntoCredentialStore,
readMacosKeychainProviderIndex, readMacosKeychainStore, readMacosKeychainSecret,
mergeLegacyValuesIntoCredentialStore, markCredentialLegacyKeychainMigrated,
isCredentialLegacyKeychainMigrated, isMacosKeychainAvailable.

Comment on lines +92 to +106
if (roomAbove >= MENU_HEIGHT || roomAbove >= roomBelow) {
return {
left,
width,
bottom: Math.max(VIEWPORT_GUTTER, viewportHeight - anchor.top + MENU_GAP),
maxHeight: Math.max(160, Math.min(MENU_HEIGHT, roomAbove - MENU_GAP)),
};
}

return {
left,
width,
top: Math.min(viewportHeight - VIEWPORT_GUTTER, anchorBottom + MENU_GAP),
maxHeight: Math.max(160, Math.min(MENU_HEIGHT, roomBelow - MENU_GAP)),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not force a 160px minimum maxHeight when viewport space is smaller

The current Math.max(160, ...) can push the menu off-screen in constrained layouts. Use the actual available space (bounded by a small lower floor only if needed for usability, not larger than available room).

Suggested fix
   if (roomAbove >= MENU_HEIGHT || roomAbove >= roomBelow) {
     return {
       left,
       width,
       bottom: Math.max(VIEWPORT_GUTTER, viewportHeight - anchor.top + MENU_GAP),
-      maxHeight: Math.max(160, Math.min(MENU_HEIGHT, roomAbove - MENU_GAP)),
+      maxHeight: Math.max(96, Math.min(MENU_HEIGHT, roomAbove - MENU_GAP)),
     };
   }

   return {
     left,
     width,
     top: Math.min(viewportHeight - VIEWPORT_GUTTER, anchorBottom + MENU_GAP),
-    maxHeight: Math.max(160, Math.min(MENU_HEIGHT, roomBelow - MENU_GAP)),
+    maxHeight: Math.max(96, Math.min(MENU_HEIGHT, roomBelow - MENU_GAP)),
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (roomAbove >= MENU_HEIGHT || roomAbove >= roomBelow) {
return {
left,
width,
bottom: Math.max(VIEWPORT_GUTTER, viewportHeight - anchor.top + MENU_GAP),
maxHeight: Math.max(160, Math.min(MENU_HEIGHT, roomAbove - MENU_GAP)),
};
}
return {
left,
width,
top: Math.min(viewportHeight - VIEWPORT_GUTTER, anchorBottom + MENU_GAP),
maxHeight: Math.max(160, Math.min(MENU_HEIGHT, roomBelow - MENU_GAP)),
};
if (roomAbove >= MENU_HEIGHT || roomAbove >= roomBelow) {
return {
left,
width,
bottom: Math.max(VIEWPORT_GUTTER, viewportHeight - anchor.top + MENU_GAP),
maxHeight: Math.max(96, Math.min(MENU_HEIGHT, roomAbove - MENU_GAP)),
};
}
return {
left,
width,
top: Math.min(viewportHeight - VIEWPORT_GUTTER, anchorBottom + MENU_GAP),
maxHeight: Math.max(96, Math.min(MENU_HEIGHT, roomBelow - MENU_GAP)),
};
🤖 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/chat/ChatCommandMenu.tsx` around lines
92 - 106, The code currently forces a 160px minimum via Math.max(160,
Math.min(MENU_HEIGHT, roomAbove - MENU_GAP)) which can push the menu off-screen;
change both branches to compute available = roomAbove - MENU_GAP (or roomBelow -
MENU_GAP) and set maxHeight to available if available < MIN_FLOOR else
Math.min(MENU_HEIGHT, available) where MIN_FLOOR is a small usability floor
(e.g. 48) — i.e. replace the Math.max(160, Math.min(...)) expressions with a
conditional: if (available < MIN_FLOOR) maxHeight = available else maxHeight =
Math.min(MENU_HEIGHT, available); apply this for the roomAbove and roomBelow
branches (references: MENU_HEIGHT, MENU_GAP, VIEWPORT_GUTTER, roomAbove,
roomBelow, anchor.top/anchorBottom).

Comment on lines 2736 to +2739
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 10%, transparent),
0 0 0 1px color-mix(in srgb, var(--color-fg) 12%, transparent);
inset 0 0 0 1px color-mix(in srgb, var(--awaiting-color) 70%, transparent),
0 0 0 2px color-mix(in srgb, var(--awaiting-color) 35%, transparent),
0 0 14px color-mix(in srgb, var(--awaiting-color) 22%, transparent);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix stylelint violation in awaiting-state block

Stylelint reports declaration-empty-line-before at Line 2736-2739. Add the required blank line before the declaration to keep CI lint green.

🧰 Tools
🪛 Stylelint (17.11.0)

[error] 2736-2739: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 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/index.css` around lines 2736 - 2739, The stylelint
error is caused by a missing blank line before the box-shadow declaration in the
awaiting-state block; open the CSS rule that contains the box-shadow property
(the declaration using color-mix with var(--awaiting-color)) and insert a single
empty line immediately before the "box-shadow:" declaration to satisfy
declaration-empty-line-before and restore CI linting.

Comment on lines +2844 to +2845
shouldCaptureLocalChanges = false
defer { shouldCaptureLocalChanges = true }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve previous capture state instead of forcing it to true

Line 2844 disables capture, but Line 2845 always restores to true. If this method runs under an outer scope that already set shouldCaptureLocalChanges = false, this will re-enable capture too early.

Proposed fix
-    shouldCaptureLocalChanges = false
-    defer { shouldCaptureLocalChanges = true }
+    let previousCaptureState = shouldCaptureLocalChanges
+    shouldCaptureLocalChanges = false
+    defer { shouldCaptureLocalChanges = previousCaptureState }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
shouldCaptureLocalChanges = false
defer { shouldCaptureLocalChanges = true }
let previousCaptureState = shouldCaptureLocalChanges
shouldCaptureLocalChanges = false
defer { shouldCaptureLocalChanges = previousCaptureState }
🤖 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/ios/ADE/Services/Database.swift` around lines 2844 - 2845, The defer
currently unconditionally sets shouldCaptureLocalChanges = true which can
incorrectly re-enable capturing when the outer scope had it disabled; change
this to preserve and restore the previous value by saving let previous =
shouldCaptureLocalChanges before mutating and then in defer set
shouldCaptureLocalChanges = previous so the method toggles capture idempotently
(refer to the shouldCaptureLocalChanges setter usage in this function).

@arul28 arul28 force-pushed the ade/tui-and-desktop-app-upgrades-1209107e branch from c10870a to 9968f21 Compare May 13, 2026 09:06
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@cursor review

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@copilot review

Comment thread apps/ade-cli/src/tuiClient/aggregate.ts Outdated
Comment thread apps/ade-cli/src/tuiClient/aggregate.ts Outdated
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@copilot review

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@cursor review

Comment thread apps/ade-cli/src/tuiClient/aggregate.ts
@arul28 arul28 force-pushed the ade/tui-and-desktop-app-upgrades-1209107e branch from 0f67cd1 to e520f7d Compare May 13, 2026 10:07
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@cursor review

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@copilot review

Comment thread apps/ade-cli/src/tuiClient/aggregate.ts
Comment thread apps/ade-cli/src/tuiClient/aggregate.ts
Comment thread apps/desktop/src/main/services/usage/usageTrackingService.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
apps/ade-cli/src/tuiClient/aggregate.ts (3)

284-292: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Filter subagent work transitively, not just one level deep.

subagentChildItemIds only captures direct children of subagent parent items. Nested subagent work (grandchildren and deeper) will pass both checks at lines 368-370, so nested command/file-change activity can leak into the main chat timeline instead of staying scoped to the subagent view.

🔄 Proposed fix to build transitive closure
  const subagentChildItemIds = new Set<string>();
  if (subagentParentItemIds.size > 0) {
+   // Build parent→children map
+   const childrenMap = new Map<string, Set<string>>();
    for (const envelope of args.events) {
      const parentItemId = workEventParentItemId(envelope.event);
      const itemId = workEventItemId(envelope.event);
-     if (parentItemId && itemId && subagentParentItemIds.has(parentItemId)) {
-       subagentChildItemIds.add(itemId);
-     }
+     if (parentItemId && itemId) {
+       if (!childrenMap.has(parentItemId)) {
+         childrenMap.set(parentItemId, new Set());
+       }
+       childrenMap.get(parentItemId)!.add(itemId);
+     }
+   }
+   // BFS to collect all transitive descendants
+   const queue = Array.from(subagentParentItemIds);
+   while (queue.length > 0) {
+     const current = queue.shift()!;
+     const children = childrenMap.get(current);
+     if (children) {
+       for (const child of children) {
+         if (!subagentChildItemIds.has(child)) {
+           subagentChildItemIds.add(child);
+           queue.push(child);
+         }
+       }
+     }
    }
  }
🤖 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/aggregate.ts` around lines 284 - 292, The current
loop only adds direct children to subagentChildItemIds; change it to compute the
transitive closure so grandchildren and deeper descendants are included: start
from the initial subagentParentItemIds (and include any newly discovered child
ids) and repeatedly scan args.events using workEventParentItemId(envelope.event)
and workEventItemId(envelope.event) to add children into subagentChildItemIds
until a full pass adds no new ids; replace the single-pass for-loop with this
iterative (while-changed) approach so nested subagent work is fully filtered.

364-366: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

reasoning events are dropped instead of being aggregated.

ChatView has a dedicated thinking renderer, but this branch skips every reasoning event, removing thinking output from the transcript entirely and leaving turn-completion logic with nothing to finalize.

💭 Proposed fix to preserve reasoning events

If reasoning should be treated as thinking blocks, replace the skip with aggregation logic:

  if (event.type === "reasoning") {
-   continue;
+   // Convert to thinking or aggregate into a thinking block
+   passthrough(id, "assistant-text");
+   continue;
  }

Alternatively, if a dedicated thinking block type exists or should be created, handle it similar to work-block or plan aggregation.

🤖 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/aggregate.ts` around lines 364 - 366, The code
currently drops events where event.type === "reasoning" (removing thinking
output); instead, preserve and aggregate them by treating "reasoning" like other
multi-event blocks: replace the continue with logic that appends/merges the
reasoning event into the current aggregation bucket (or invokes the same
aggregation/merge routine used for work-block/plan events), so the ChatView
thinking renderer still receives the combined thinking content for finalization;
locate the branch checking event.type === "reasoning" in aggregate.ts and route
those events into the existing aggregation flow rather than skipping them.

488-501: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

turn-footer block is never emitted, so duration/token/cost summaries won't render.

The turn-footer block type is defined at line 43 and ChatView presumably has a renderer for it, but the done event handler only finalizes existing blocks—it never synthesizes a turn-footer block. Token-usage events are also in AGGREGATED_TYPES (line 184-185) and skipped at line 503-506, so footer metadata has no path to the UI.

📊 Proposed fix to emit turn-footer
  if (event.type === "done") {
    const startMs = turnId ? turnStart.get(turnId) : undefined;
    const durationMs = startMs !== undefined ? entry.timestamp - startMs : undefined;
+   const tokens = event.usage ? event.usage.inputTokens + event.usage.outputTokens : undefined;
+   const cost = event.costUsd;
    for (const block of blocks) {
      const blockTurn = (block as { turnId?: string | null }).turnId ?? null;
      if (blockTurn !== turnId) continue;
      if (block.kind === "work-block" || block.kind === "plan" || block.kind === "compaction") {
        block.live = false;
        if (durationMs !== undefined && block.kind === "work-block") {
          block.durationMs = block.durationMs ?? durationMs;
        }
      }
    }
+   if (turnId) {
+     blocks.push({
+       kind: "turn-footer",
+       id,
+       turnId,
+       durationMs,
+       tokens,
+       cost,
+     });
+   }
    continue;
  }
🤖 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/aggregate.ts` around lines 488 - 501, When
handling event.type === "done" you finalize existing blocks but never emit the
synthesized "turn-footer" block, so duration/token/cost summaries never reach
the UI; update the done-event branch to create and push a new block of kind
"turn-footer" (including computed durationMs from turnStart.get(turnId),
aggregated token/cost data from AGGREGATED_TYPES events or your aggregation
state for that turn, and any metadata) only when turnId is present, then append
it to blocks before continuing; ensure you still skip raw AGGREGATED_TYPES
events later but use the aggregated values to populate the new "turn-footer" so
ChatView can render summaries.
🧹 Nitpick comments (2)
apps/desktop/src/main/services/prs/prService.ts (1)

6087-6088: ⚡ Quick win

Scope snapshot hydration to the selected PR IDs

listSnapshotRows() loads/deserializes snapshots for the whole project on every call, but this method only needs checks for the lane display rows you just selected. Fetching only those PR IDs will reduce avoidable DB/JSON overhead on a likely frequently-called path.

♻️ Proposed change
-      const checksByPrId = new Map(listSnapshotRows().map((snapshot) => [snapshot.prId, snapshot.checks] as const));
+      const checksByPrId = new Map<string, PrCheck[]>();
+      for (const row of rows) {
+        const snapshot = listSnapshotRows({ prId: row.id })[0];
+        checksByPrId.set(row.id, snapshot?.checks ?? []);
+      }
🤖 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/prs/prService.ts` around lines 6087 - 6088,
The code currently calls listSnapshotRows() for the whole project then builds
checksByPrId for every snapshot; instead scope snapshot hydration to only the PR
IDs present in rows: collect the selectedPrIds from rows (e.g., rows.map(r =>
r.id)), call a new or existing API that accepts an array of PR IDs (e.g.,
listSnapshotRowsForPrIds(selectedPrIds) or add a parameter to
listSnapshotRows(prIds)), build checksByPrId from that restricted snapshot
result, and pass checksByPrId.get(row.id) to rowToLanePrSummary (keeping
isActivePrState logic unchanged); update or add the targeted snapshot-loading
function to deserialize only those PR IDs to avoid full-project deserialization.
apps/ade-cli/src/services/sync/syncHostService.ts (1)

3300-3317: 💤 Low value

Reuse the new stopBonjourAnnouncement / stopNativeLanDiscovery helpers in dispose.

Now that unpublishLanDiscovery() already invokes stopNativeLanDiscovery() and stopBonjourAnnouncement(), the bonjourAnnouncement.stop?.() block below is redundant (it is always null by the time control reaches it). Folding the bonjourInstance.destroy() call into the existing teardown helpers keeps lifecycle logic in one place.

♻️ Proposed cleanup
       unpublishLanDiscovery();
       try {
         await unpublishTailnetDiscovery();
       } catch {
         // Never throw from dispose.
       }
       await new Promise<void>((resolve) => {
         // ...
       });
-      if (bonjourAnnouncement) {
-        try {
-          bonjourAnnouncement.stop?.();
-        } catch {
-          // ignore cleanup failures
-        }
-        bonjourAnnouncement = null;
-      }
-      bonjourPort = null;
-      bonjourSignature = null;
       if (bonjourInstance) {
         try {
           bonjourInstance.destroy();
         } catch {
           // ignore cleanup failures
         }
         bonjourInstance = null;
       }
🤖 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/services/sync/syncHostService.ts` around lines 3300 - 3317,
The dispose logic currently calls bonjourAnnouncement.stop and
bonjourInstance.destroy directly even though unpublishLanDiscovery already calls
stopNativeLanDiscovery() and stopBonjourAnnouncement(); remove the redundant
bonjourAnnouncement.stop?.() and the bonjourInstance.destroy() call from dispose
and instead invoke the existing helpers stopBonjourAnnouncement() and
stopNativeLanDiscovery() (or call unpublishLanDiscovery() if that consolidates
both) so teardown is centralized; ensure bonjourPort and bonjourSignature are
still nulled after using the helpers and that any try/catch behavior from the
helpers is preserved.
🤖 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/services/sync/syncHostService.ts`:
- Around line 1299-1310: The exit/error handlers for the native dns-sd child
(handlers on child.once("error") and child.once("exit")) currently only clear
nativeBonjourProcess and log, so they never re-spawn or fall back; update these
handlers to attempt auto-recovery by calling publishLanDiscovery(port, { force:
true }) (or trigger the Bonjour fallback) when the active child fails or exits,
but gate the recovery with a short debounce/throttle to avoid rapid respawns if
dns-sd is permanently broken; ensure you check nativeBonjourProcess === child
before scheduling recovery and clear any scheduled recovery when a new
nativeBonjourProcess is created to prevent fork-bombing.

In `@apps/ade-cli/src/tuiClient/workEventIds.ts`:
- Line 11: The function workEventParentItemId currently casts the event to {
parentItemId?: unknown } which hides that parentItemId exists only on
"tool_call" and "tool_result"; update workEventParentItemId(AgentChatEvent) to
explicitly narrow the event by checking event.type === "tool_call" || event.type
=== "tool_result" and return null for other types, then call
textField(event.parentItemId) without a cast; keep textField usage and apply the
same explicit type-narrowing pattern to any similar helper if needed.

In `@apps/desktop/src/renderer/components/usage/UsageQuotaPanel.tsx`:
- Around line 320-326: The current code treats "weekly" and "monthly" as
interchangeable by finding a single weeklyWindow that matches either type;
instead, split them into separate variables (weeklyWindow and monthlyWindow),
keep fiveHourWindow as is, and adjust otherWindows to exclude all three
(weeklyWindow, monthlyWindow, fiveHourWindow); where a single window is needed
for the sparkline use the weekly-first fallback weeklyWindow ?? monthlyWindow to
match selectPacingWindow and MissionHeader.tsx behavior and preserve priority
and labels.

---

Duplicate comments:
In `@apps/ade-cli/src/tuiClient/aggregate.ts`:
- Around line 284-292: The current loop only adds direct children to
subagentChildItemIds; change it to compute the transitive closure so
grandchildren and deeper descendants are included: start from the initial
subagentParentItemIds (and include any newly discovered child ids) and
repeatedly scan args.events using workEventParentItemId(envelope.event) and
workEventItemId(envelope.event) to add children into subagentChildItemIds until
a full pass adds no new ids; replace the single-pass for-loop with this
iterative (while-changed) approach so nested subagent work is fully filtered.
- Around line 364-366: The code currently drops events where event.type ===
"reasoning" (removing thinking output); instead, preserve and aggregate them by
treating "reasoning" like other multi-event blocks: replace the continue with
logic that appends/merges the reasoning event into the current aggregation
bucket (or invokes the same aggregation/merge routine used for work-block/plan
events), so the ChatView thinking renderer still receives the combined thinking
content for finalization; locate the branch checking event.type === "reasoning"
in aggregate.ts and route those events into the existing aggregation flow rather
than skipping them.
- Around line 488-501: When handling event.type === "done" you finalize existing
blocks but never emit the synthesized "turn-footer" block, so
duration/token/cost summaries never reach the UI; update the done-event branch
to create and push a new block of kind "turn-footer" (including computed
durationMs from turnStart.get(turnId), aggregated token/cost data from
AGGREGATED_TYPES events or your aggregation state for that turn, and any
metadata) only when turnId is present, then append it to blocks before
continuing; ensure you still skip raw AGGREGATED_TYPES events later but use the
aggregated values to populate the new "turn-footer" so ChatView can render
summaries.

---

Nitpick comments:
In `@apps/ade-cli/src/services/sync/syncHostService.ts`:
- Around line 3300-3317: The dispose logic currently calls
bonjourAnnouncement.stop and bonjourInstance.destroy directly even though
unpublishLanDiscovery already calls stopNativeLanDiscovery() and
stopBonjourAnnouncement(); remove the redundant bonjourAnnouncement.stop?.() and
the bonjourInstance.destroy() call from dispose and instead invoke the existing
helpers stopBonjourAnnouncement() and stopNativeLanDiscovery() (or call
unpublishLanDiscovery() if that consolidates both) so teardown is centralized;
ensure bonjourPort and bonjourSignature are still nulled after using the helpers
and that any try/catch behavior from the helpers is preserved.

In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 6087-6088: The code currently calls listSnapshotRows() for the
whole project then builds checksByPrId for every snapshot; instead scope
snapshot hydration to only the PR IDs present in rows: collect the selectedPrIds
from rows (e.g., rows.map(r => r.id)), call a new or existing API that accepts
an array of PR IDs (e.g., listSnapshotRowsForPrIds(selectedPrIds) or add a
parameter to listSnapshotRows(prIds)), build checksByPrId from that restricted
snapshot result, and pass checksByPrId.get(row.id) to rowToLanePrSummary
(keeping isActivePrState logic unchanged); update or add the targeted
snapshot-loading function to deserialize only those PR IDs to avoid full-project
deserialization.
🪄 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

Run ID: e69079da-0699-4d8d-8a29-af2469bb0d08

📥 Commits

Reviewing files that changed from the base of the PR and between c10870a and e520f7d.

⛔ Files ignored due to path filters (7)
  • apps/desktop/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • docs/features/ade-code/README.md is excluded by !docs/**
  • docs/features/cto/README.md is excluded by !docs/**
  • docs/features/linear-integration/README.md is excluded by !docs/**
  • docs/features/onboarding-and-settings/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
📒 Files selected for processing (137)
  • .claude/skills/plan/SKILL.md
  • .github/workflows/release-core.yml
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/ade-cli/src/services/sync/syncHostService.test.ts
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/ade-cli/src/services/sync/syncService.ts
  • apps/ade-cli/src/tuiClient/__tests__/ChatView.test.tsx
  • apps/ade-cli/src/tuiClient/__tests__/Drawer.test.tsx
  • apps/ade-cli/src/tuiClient/__tests__/HeaderFooter.test.tsx
  • apps/ade-cli/src/tuiClient/__tests__/Palettes.test.tsx
  • apps/ade-cli/src/tuiClient/__tests__/RightPane.test.tsx
  • apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/commands.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/connection.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/drawerSelection.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/eventDedup.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/format.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/heartbeat.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/imageTargets.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/jsonRpcClient.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/keybindings.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/linearCommands.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/project.test.ts
  • apps/ade-cli/src/tuiClient/__tests__/subagentPane.test.ts
  • apps/ade-cli/src/tuiClient/adeApi.ts
  • apps/ade-cli/src/tuiClient/aggregate.ts
  • apps/ade-cli/src/tuiClient/app.tsx
  • apps/ade-cli/src/tuiClient/commands.ts
  • apps/ade-cli/src/tuiClient/components/ApprovalPrompt.tsx
  • apps/ade-cli/src/tuiClient/components/ChatView.tsx
  • apps/ade-cli/src/tuiClient/components/Drawer.tsx
  • apps/ade-cli/src/tuiClient/components/FooterControls.tsx
  • apps/ade-cli/src/tuiClient/components/Header.tsx
  • apps/ade-cli/src/tuiClient/components/MentionPalette.tsx
  • apps/ade-cli/src/tuiClient/components/ModelStatus.tsx
  • apps/ade-cli/src/tuiClient/components/RightPane.tsx
  • apps/ade-cli/src/tuiClient/components/SlashPalette.tsx
  • apps/ade-cli/src/tuiClient/drawerSelection.ts
  • apps/ade-cli/src/tuiClient/format.ts
  • apps/ade-cli/src/tuiClient/imageTargets.ts
  • apps/ade-cli/src/tuiClient/keybindings/index.ts
  • apps/ade-cli/src/tuiClient/laneTree.ts
  • apps/ade-cli/src/tuiClient/project.ts
  • apps/ade-cli/src/tuiClient/spinTick.tsx
  • apps/ade-cli/src/tuiClient/state.ts
  • apps/ade-cli/src/tuiClient/subagentPane.ts
  • apps/ade-cli/src/tuiClient/theme.ts
  • apps/ade-cli/src/tuiClient/types.ts
  • apps/ade-cli/src/tuiClient/workEventIds.ts
  • apps/desktop/package.json
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/adeActions/registry.test.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.test.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.ts
  • apps/desktop/src/main/services/ai/apiKeyStore.test.ts
  • apps/desktop/src/main/services/ai/apiKeyStore.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/chat/claudeOutputStyles.test.ts
  • apps/desktop/src/main/services/chat/claudeOutputStyles.ts
  • apps/desktop/src/main/services/chat/cursorSdkEventMapper.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkEventMapper.ts
  • apps/desktop/src/main/services/cto/issueTracker.ts
  • apps/desktop/src/main/services/cto/linearAuth.test.ts
  • apps/desktop/src/main/services/cto/linearClient.ts
  • apps/desktop/src/main/services/cto/linearCredentialService.ts
  • apps/desktop/src/main/services/cto/linearDispatcherService.ts
  • apps/desktop/src/main/services/cto/linearIssueTracker.ts
  • apps/desktop/src/main/services/cto/linearLaneCardService.test.ts
  • apps/desktop/src/main/services/cto/linearLaneCardService.ts
  • apps/desktop/src/main/services/cto/linearSync.test.ts
  • apps/desktop/src/main/services/diffs/diffService.test.ts
  • apps/desktop/src/main/services/diffs/diffService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/lanes/laneListSnapshotService.test.ts
  • apps/desktop/src/main/services/lanes/laneListSnapshotService.ts
  • apps/desktop/src/main/services/lanes/laneService.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/opencode/openCodeRuntime.test.ts
  • apps/desktop/src/main/services/opencode/openCodeRuntime.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/main/services/pty/ptyService.test.ts
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/main/services/sessions/sessionService.ts
  • apps/desktop/src/main/services/sync/syncHostService.test.ts
  • apps/desktop/src/main/services/sync/syncService.test.ts
  • apps/desktop/src/main/services/usage/usageTrackingService.test.ts
  • apps/desktop/src/main/services/usage/usageTrackingService.ts
  • apps/desktop/src/main/utils/terminalSessionSignals.test.ts
  • apps/desktop/src/main/utils/terminalSessionSignals.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/app/AppShell.tsx
  • apps/desktop/src/renderer/components/app/TopBar.test.tsx
  • apps/desktop/src/renderer/components/app/TopBar.tsx
  • apps/desktop/src/renderer/components/automations/adeActionSchemas.ts
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatCommandMenu.tsx
  • apps/desktop/src/renderer/components/lanes/laneDesignTokens.ts
  • apps/desktop/src/renderer/components/lanes/useLaneWorkSessions.ts
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
  • apps/desktop/src/renderer/components/terminals/LaneChip.tsx
  • apps/desktop/src/renderer/components/terminals/ProviderChip.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionContextMenu.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx
  • apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.test.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.test.ts
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.ts
  • apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx
  • apps/desktop/src/renderer/components/usage/UsageQuotaPanel.test.tsx
  • apps/desktop/src/renderer/components/usage/UsageQuotaPanel.tsx
  • apps/desktop/src/renderer/index.css
  • apps/desktop/src/renderer/state/appStore.test.ts
  • apps/desktop/src/renderer/state/appStore.ts
  • apps/desktop/src/shared/cliLaunch.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/chat.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/desktop/src/shared/types/prs.ts
  • apps/desktop/src/shared/types/usage.ts
  • apps/ios/ADE/Services/Database.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
  • apps/ios/ADE/Views/Settings/SettingsPinSheet.swift
  • apps/ios/ADETests/ADETests.swift
💤 Files with no reviewable changes (4)
  • apps/ade-cli/src/tuiClient/tests/heartbeat.test.ts
  • apps/ade-cli/src/tuiClient/tests/jsonRpcClient.test.ts
  • apps/ade-cli/src/tuiClient/tests/linearCommands.test.ts
  • apps/ade-cli/src/tuiClient/tests/eventDedup.test.ts
✅ Files skipped from review due to trivial changes (7)
  • apps/desktop/src/shared/types/git.ts
  • apps/desktop/package.json
  • apps/desktop/src/main/services/cto/linearIssueTracker.ts
  • apps/desktop/src/main/services/opencode/openCodeRuntime.test.ts
  • .claude/skills/plan/SKILL.md
  • apps/desktop/src/main/services/cto/linearLaneCardService.test.ts
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
🚧 Files skipped from review as they are similar to previous changes (110)
  • apps/ade-cli/src/tuiClient/drawerSelection.ts
  • apps/desktop/src/main/utils/terminalSessionSignals.test.ts
  • apps/ade-cli/src/tuiClient/tests/drawerSelection.test.ts
  • apps/ade-cli/src/tuiClient/tests/keybindings.test.ts
  • apps/ade-cli/src/tuiClient/tests/appInput.test.ts
  • apps/ade-cli/src/tuiClient/keybindings/index.ts
  • apps/desktop/src/renderer/components/lanes/laneDesignTokens.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx
  • apps/desktop/src/main/services/ai/aiIntegrationService.test.ts
  • apps/ade-cli/src/tuiClient/state.ts
  • apps/desktop/src/shared/types/prs.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/desktop/src/renderer/components/terminals/LaneChip.tsx
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/main/services/cto/linearDispatcherService.ts
  • apps/desktop/src/main/services/cto/issueTracker.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/main/services/lanes/laneListSnapshotService.test.ts
  • apps/desktop/src/main/services/adeActions/registry.test.ts
  • apps/desktop/src/main/services/cto/linearSync.test.ts
  • apps/desktop/src/renderer/components/automations/adeActionSchemas.ts
  • apps/desktop/src/renderer/components/terminals/ProviderChip.tsx
  • apps/desktop/src/shared/cliLaunch.ts
  • apps/desktop/src/main/services/lanes/laneListSnapshotService.ts
  • apps/ade-cli/src/services/sync/syncService.ts
  • apps/desktop/src/shared/types/chat.ts
  • apps/ade-cli/src/tuiClient/tests/commands.test.ts
  • apps/desktop/src/renderer/components/usage/UsageQuotaPanel.test.tsx
  • apps/ade-cli/src/tuiClient/tests/Palettes.test.tsx
  • apps/desktop/src/main/services/sync/syncHostService.test.ts
  • apps/desktop/src/renderer/components/lanes/useLaneWorkSessions.ts
  • apps/desktop/src/main/services/diffs/diffService.test.ts
  • apps/desktop/src/renderer/state/appStore.test.ts
  • apps/ade-cli/src/tuiClient/tests/imageTargets.test.ts
  • apps/desktop/src/shared/types/usage.ts
  • apps/ade-cli/src/tuiClient/laneTree.ts
  • apps/desktop/src/main/utils/terminalSessionSignals.ts
  • apps/ade-cli/src/tuiClient/components/Header.tsx
  • apps/ade-cli/src/tuiClient/tests/HeaderFooter.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/ade-cli/src/tuiClient/components/ApprovalPrompt.tsx
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/ade-cli/src/tuiClient/components/ModelStatus.tsx
  • apps/ade-cli/src/tuiClient/tests/adeApi.test.ts
  • apps/ade-cli/src/tuiClient/tests/subagentPane.test.ts
  • apps/ios/ADE/Views/Settings/SettingsPinSheet.swift
  • apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx
  • apps/desktop/src/renderer/components/app/TopBar.test.tsx
  • apps/desktop/src/main/services/cto/linearAuth.test.ts
  • apps/desktop/src/main/services/cto/linearLaneCardService.ts
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.test.ts
  • apps/ade-cli/src/tuiClient/components/MentionPalette.tsx
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/sync/syncService.test.ts
  • apps/desktop/src/renderer/components/terminals/SessionContextMenu.tsx
  • apps/desktop/src/main/services/cto/linearCredentialService.ts
  • apps/desktop/src/renderer/components/chat/ChatCommandMenu.tsx
  • apps/ade-cli/src/tuiClient/tests/Drawer.test.tsx
  • apps/ade-cli/src/tuiClient/adeApi.ts
  • apps/desktop/src/renderer/components/app/AppShell.tsx
  • apps/desktop/src/preload/preload.ts
  • apps/ade-cli/src/tuiClient/project.ts
  • apps/desktop/src/main/services/pty/ptyService.test.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
  • apps/ade-cli/src/tuiClient/tests/connection.test.ts
  • apps/ade-cli/src/tuiClient/types.ts
  • apps/ade-cli/src/tuiClient/tests/project.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx
  • apps/ade-cli/src/tuiClient/components/FooterControls.tsx
  • apps/ade-cli/src/tuiClient/tests/format.test.ts
  • apps/ade-cli/src/tuiClient/commands.ts
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx
  • apps/desktop/src/main/services/chat/cursorSdkEventMapper.ts
  • apps/ade-cli/src/tuiClient/tests/RightPane.test.tsx
  • apps/desktop/src/main/services/chat/cursorSdkEventMapper.test.ts
  • apps/ade-cli/src/tuiClient/theme.ts
  • apps/ios/ADETests/ADETests.swift
  • apps/desktop/src/main/services/ai/apiKeyStore.test.ts
  • apps/ios/ADE/Services/Database.swift
  • apps/desktop/src/renderer/components/app/TopBar.tsx
  • .github/workflows/release-core.yml
  • apps/desktop/src/main/services/usage/usageTrackingService.test.ts
  • apps/desktop/src/renderer/index.css
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx
  • apps/desktop/src/main/services/sessions/sessionService.ts
  • apps/desktop/src/main/services/diffs/diffService.ts
  • apps/ade-cli/src/tuiClient/subagentPane.ts
  • apps/ade-cli/src/tuiClient/format.ts
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.ts
  • apps/ade-cli/src/tuiClient/components/Drawer.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.test.tsx
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/renderer/state/appStore.ts
  • apps/desktop/src/main/services/ai/apiKeyStore.ts
  • apps/desktop/src/main/services/usage/usageTrackingService.ts
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/ade-cli/src/tuiClient/components/RightPane.tsx
  • apps/desktop/src/main/services/chat/claudeOutputStyles.test.ts
  • apps/ade-cli/src/tuiClient/imageTargets.ts
  • apps/ade-cli/src/tuiClient/components/SlashPalette.tsx
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/ade-cli/src/tuiClient/components/ChatView.tsx
  • apps/ios/ADE/Services/SyncService.swift

Comment thread apps/ade-cli/src/services/sync/syncHostService.ts
Comment thread apps/ade-cli/src/tuiClient/workEventIds.ts Outdated
Comment thread apps/desktop/src/renderer/components/usage/UsageQuotaPanel.tsx Outdated
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@copilot review

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@cursor review

Comment thread apps/ade-cli/src/tuiClient/aggregate.ts Outdated
Comment thread apps/desktop/src/main/services/ai/apiKeyStore.ts
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@cursor review

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@copilot review

Comment thread apps/ade-cli/src/services/sync/syncHostService.ts
"codex_goal_updated",
"codex_goal_cleared",
"pending_input_resolved",
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AGGREGATED_TYPES missing system_notice and activity types

Low Severity

The AGGREGATED_TYPES set is used as a guard at line 510 to skip events already handled above. But system_notice, activity, context_compact, codex_context_compaction, and status are all handled by explicit if branches earlier in the loop yet are not listed in AGGREGATED_TYPES. When these event types reach the bottom fallthrough, the AGGREGATED_TYPES.has(event.type) check misses them. For system_notice events that aren't steer lifecycle or memory notices, and for context_compact/codex_context_compaction/status/activity, this doesn't cause a problem because they're handled before reaching line 510. But if new system_notice subtypes are added that don't match any existing branch, they'll fall through to the generic "notice" block instead of being silently skipped like other aggregated types.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c274574. Configure here.

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@cursor review

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 13, 2026

@copilot review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f11519d. Configure here.

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.

2 participants