Skip to content

Add Codex chat integration to desktop app - #6

Closed
arul28 wants to merge 1 commit into
mainfrom
codex/add-codex-chat-integration
Closed

Add Codex chat integration to desktop app#6
arul28 wants to merge 1 commit into
mainfrom
codex/add-codex-chat-integration

Conversation

@arul28

@arul28 arul28 commented Feb 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • add the new CodexChatPage, its supporting state slice, renderer UI elements, and startup path wiring so the desktop app can host the Codex chat surface
  • introduce shared IPC/types and preload hooks plus a main-process codex service stack (JSON-RPC parser, lane thread store, codex app server service) that materializes threads and exposes the required endpoints
  • cover the new services/state with targeted unit tests so the integration logic stays verifiable as it evolves

Testing

  • Not run (not requested)

Summary by CodeRabbit

  • New Features

    • Integrated Codex chat with thread management, turn-based conversations, and model/reasoning effort selection.
    • Added ChatGPT and API key authentication for Codex accounts.
    • Added approval workflow UI for command execution and file change requests.
    • Lane-thread binding persistence for chat continuity.
  • UI/UX Enhancements

    • New Codex tab in main navigation.
    • Codex Chat button in terminal view for inline and full-page chat modes.
    • Lane picker dialog to select lanes for inline chat.
    • Connection state management and retry controls.
  • Performance

    • Optimized Vite caching and development configuration.
    • Improved startup page initialization with staged data loading.

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive Codex app-server integration into the desktop application. It adds a new backend service that spawns and manages the Codex process via JSON-RPC over stdio, implements lane/thread lifecycle management with persistent storage, exposes IPC handlers for all Codex operations, creates a new CodexChatPage UI component for chat interactions, and integrates Codex chat capabilities into existing views.

Changes

Cohort / File(s) Summary
Core Codex Service
apps/desktop/src/main/services/codex/codexAppServerService.ts, codexAppServerService.test.ts
Implements complete Codex app-server orchestration with process spawning, JSON-RPC over stdio protocol, request/response tracking, approval workflows, lane/thread management, and lifecycle controls; comprehensive test suite covers request/response correlation, approval flows, turn operations, retry logic, and error handling.
JSON-RPC Protocol
apps/desktop/src/main/services/codex/jsonRpcLineParser.ts, jsonRpcLineParser.test.ts
Defines JSON-RPC message types and line-based parser for newline-delimited JSON communication; handles buffering, partial messages, and error resilience with unit tests for multi-chunk parsing and invalid JSON handling.
Lane/Thread Persistence
apps/desktop/src/main/services/codex/laneThreadStore.ts, laneThreadStore.test.ts
Implements per-lane thread binding storage with recent thread history, default thread tracking, and reverse lookup; includes sanitization and persistent backing via AdeDb.
Main Process Integration
apps/desktop/src/main/main.ts
Wires codexAppServerService into AppContext, initializes with db/logger/laneService/clientVersion, exposes via IPC, and ensures proper disposal on context close.
IPC & Bridge
apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/preload/preload.ts, apps/desktop/src/preload/global.d.ts
Adds 18 new IPC handlers for Codex operations (connection, threads, turns, accounts, models, approvals); exposes codex API surface on window.ade via preload; includes type imports for all Codex types.
Shared IPC Channels
apps/desktop/src/shared/ipc.ts
Adds 19 IPC channel constants under ade.codex.* namespace for all Codex operations.
Type Definitions
apps/desktop/src/shared/types.ts
Defines comprehensive TypeScript types for Codex concepts: connection state, accounts, rate limits, models, threads, turns, approvals, authentication, and event payloads; all exported for type safety across main/renderer.
Chat UI Component
apps/desktop/src/renderer/components/codex/CodexChatPage.tsx
Introduces full-featured Codex chat UI with state management, thread lifecycle handling, model/effort selection, prompt submission, turn rendering, and approval workflows; supports both embedded and full-page modes.
Chat State Management
apps/desktop/src/renderer/state/codexChatState.ts, codexChatState.test.ts
Implements reducer-based state container for chat with hydration from threads, turn/item management, streaming delta handling, and approval tracking; includes type definitions and selector utilities.
UI Integration
apps/desktop/src/renderer/components/app/App.tsx, TabNav.tsx, LaneWorkPane.tsx, TerminalsPage.tsx, StartupAuthPage.tsx
Registers /codex route, adds Codex tab navigation, integrates CodexChatPage into lane work pane with embedded mode, adds inline Codex chat to terminals view with lane picker dialog, refactors startup flow for cached snapshot handling.
Build Configuration
apps/desktop/package.json, apps/desktop/vite.config.ts
Updates dev script with --force flag for Vite; adds stable cache directory and optimizeDeps configuration to exclude UI/motion libraries from pre-bundling.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as Renderer Process
    participant IPC as IPC Bridge
    participant Service as CodexAppServerService
    participant ChildProcess as Codex App Server
    
    Renderer->>IPC: codexThreadStart({ laneId, model })
    IPC->>Service: threadStart(args)
    Service->>ChildProcess: thread/start (JSON-RPC)
    ChildProcess-->>Service: thread/start response
    Service->>Service: Store lane-thread binding
    Service-->>IPC: Return CodexThread
    IPC-->>Renderer: Promise<CodexThread>
    
    Renderer->>IPC: codexTurnStart({ threadId, prompt, effort })
    IPC->>Service: turnStart(args)
    Service->>ChildProcess: turn/start (JSON-RPC)
    ChildProcess-->>Service: turn/start response
    ChildProcess-->>Service: item/commandExecution (notification)
    Service-->>Renderer: codexEvent (via IPC)
    Renderer->>Renderer: Update chat UI with turn
    
    ChildProcess-->>Service: item/fileChange (approval request)
    Service->>Service: Store pending approval
    Service-->>Renderer: codexEvent (approval request)
    Renderer->>Renderer: Render approval card
    
    Renderer->>IPC: codexRespondApproval({ requestId, decision })
    IPC->>Service: respondToApprovalRequest(requestId, decision)
    Service->>ChildProcess: approval/response (JSON-RPC)
    ChildProcess-->>Service: acknowledgement
    Service->>Service: Clear pending approval
Loading
sequenceDiagram
    participant User as User
    participant CodexChatUI as CodexChatPage Component
    participant ChatState as Chat State (Reducer)
    participant API as Codex API (IPC)
    
    User->>CodexChatUI: Select lane, write prompt
    CodexChatUI->>API: getLaneBinding(laneId)
    API-->>CodexChatUI: CodexLaneThreadBinding
    alt Thread exists
        CodexChatUI->>API: threadResume({ laneId, threadId })
    else No thread
        CodexChatUI->>API: threadStart({ laneId, model })
    end
    API-->>CodexChatUI: CodexThread
    
    CodexChatUI->>ChatState: hydrate-thread action
    ChatState-->>CodexChatUI: Updated state with turns
    CodexChatUI->>CodexChatUI: Render thread turns
    
    User->>CodexChatUI: Submit prompt with model/effort
    CodexChatUI->>API: turnStart({ threadId, prompt, model, effort })
    CodexChatUI->>API: onEvent(callback)
    API-->>CodexChatUI: Turn initiated
    
    API-->>CodexChatUI: codexEvent (turn-started, item notifications, completion)
    CodexChatUI->>ChatState: notification action (streaming deltas)
    ChatState-->>CodexChatUI: Updated with turn items
    CodexChatUI->>CodexChatUI: Re-render active turn
    
    alt Item requires approval
        API-->>CodexChatUI: codexEvent (approval request)
        CodexChatUI->>CodexChatUI: Render ApprovalCard
        User->>CodexChatUI: Accept/decline
        CodexChatUI->>API: codexRespondApproval({ requestId, decision })
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

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

@arul28 arul28 closed this Feb 23, 2026
@arul28
arul28 deleted the codex/add-codex-chat-integration branch March 14, 2026 05:00
arul28 added a commit that referenced this pull request Apr 16, 2026
Ports items #6, #8, and #10 from .factory/library/mobile-chat-port-plan.md.

#6 — Streaming shimmer on the active assistant bubble:
- New ADEStreamingShimmer view modifier (Views/Components/): gentle
  one-direction gradient sweep + accent glow + stroke, gated on isActive
  and respecting Reduce Motion (becomes a no-op).
- WorkChatMessageBubble gains an `isLive: Bool = false` prop; the timeline
  call site passes `isLatestAssistantMessageLive(message)` so only the
  newest assistant bubble of an active session shimmers.
- Liveness derivation lives in WorkChatSessionView+MessageLiveness.swift
  (scans visibleTimeline tail for the most recent assistant message id).

#8 — Tool-result truncation, preview, and show-all toggle:
- WorkToolCardView now shows a first-line preview in the collapsed header.
- Long results (> 500 chars) render truncated with a "Show all (N chars)"
  toggle; copy affordance is already provided by WorkOutputBlockHeader.
- Helpers: workToolResultPreview, workToolResultTruncate,
  workToolResultByteLabel.

#10 — Context-compact divider:
- WorkEventCardView special-cases kind == "contextCompact" and renders
  the new WorkContextCompactDivider: a horizontal hairline flanking a
  warning-tinted chip with the rectangle.compress.vertical icon, tokens
  freed (when parseable) and an AUTO/MANUAL trigger tag.
- Parsing handled by WorkContextCompactSummary.

Tests (appended to the end of ADETests.swift per coordination rule):
8 new unit tests covering the truncation boundary, preview extraction,
byte-label formatting, and context-compact summary parsing.
arul28 added a commit that referenced this pull request Apr 17, 2026
* Add sticky commit bar to lane detail

* WIP: lanes tab, sync pairing, and settings refactor

Staging in-progress work on mobile lanes tab, desktop sync pairing
(PIN store), iOS design system/haptics, and connection settings
screen before merging in work tab branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* mobile work tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fixing review agent comments

* Sync active lane presence across devices

* Restore the green iOS baseline validation path

Make PR list hydration tolerate older/local test schemas, restore lane list ordering, and fix the manifest desktop test command so baseline validation can run again.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Promote shared iOS glass primitives and cache code highlighting

* Polish cached Lanes state and gating

Keep cached lane context visible while making offline, hydrating, and syncing states explicit so live git actions never fail silently on iPhone.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Restore cached lane diff inspection offline

* Synthesize foundation scrutiny rerun

Record the passing foundation scrutiny rerun after restoring the iOS-only hard gate and verifying the offline lane diff regression fix.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Extend Work sync parity metadata

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Prioritize mobile Work session triage and chat creation

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Split mobile Work session detail views

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Synthesize work scrutiny findings

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Fix Work follow-up parity gaps

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Synthesize work scrutiny rerun

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Add read-only Files mobile cache contracts

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Rebuild the iOS Files mobile browser

Split the Files tab into focused browser/detail slices so workspace switching, breadcrumbs, root-state messaging, and search flows stay readable and explicit on iPhone.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* Fix Work crash on duplicate transcript merge keys

The cached base transcript can occasionally hold two envelopes with the
same merge key (hosts replay activity events during resume), and the
previous Dictionary(uniqueKeysWithValues:) init fatal-errors on that.
Replace it with an in-place dedupe that keeps the later envelope and
harden laneById against duplicate lane ids with uniquingKeysWith.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rework Files detail around content-first layout

Replaces the five-stacked-glass-cards surface with a pinned thin header
(icon + name + language/size/read-only chips), an inline mode and diff
picker, and a content-hero region that lets the code/diff/image fill
the screen instead of competing with metadata chrome. Metadata and
history move into a Details sheet (info.circle in the toolbar) so they
stay one tap away without dominating the read flow.

Compact banners replace ADENoticeCard for disconnected / load-failure
states, and the binary and image-pending fallbacks share a single
centered FilesContentFallback. Transition IDs (files-container /
-icon / -title) stay stable so the zoom-push from FilesDirectoryScreen
keeps working. All read-only framing is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Reshape Settings into a mobile shell with grouped sections

Split the 1156-line ConnectionSettingsView.swift into focused sub-files
under Views/Settings/ (all < 500 lines), and reorganize the screen into
a proper settings shell: connection status header with inline reconnect/
disconnect quick action, pairing section (Discover/Scan/Manual), theme
tiles, and a diagnostics/about section with app version and paired-host
details. Preserves Bonjour discovery, QR scanning, manual entry, and
PIN pairing flows end-to-end via the existing SyncService API — no new
service methods.

- ConnectionSettingsView.swift: top-level shell + aurora background
- SettingsSupportTypes.swift: PinPreset, PairSheetRoute, status tint helpers
- SettingsConnectionHeader.swift: live indicator + host + Reconnect action
- SettingsPairingSection.swift: pair rows + Discover/QR/Manual sheets
- SettingsPinSheet.swift: PIN entry + digit box + keypad
- SettingsAppearanceSection.swift: theme tiles
- SettingsDiagnosticsSection.swift: version, paired host, last sync

* Cache Work activity transcripts and flatten filter chips

- Memoize parseWorkChatTranscript per session + buffer fingerprint so
  activityFeed stops re-parsing every localStateRevision tick
- Split WorkChatSessionView timeline switch into @ViewBuilder helpers
  in a new WorkChatSessionView+Timeline.swift (keeps parent under 500
  lines and lets the compiler skip card branches that haven't changed)
- Replace LaneMicroChip glass chips inside the filter card with a flat
  WorkFlatCountChip to avoid glass-on-glass nesting

* Add PR mobile snapshot sync contract for iOS parity

Introduce prs.getMobileSnapshot: a single viewer-allowed sync command
that returns stack metadata, create-PR eligibility, workflow cards
(queue/integration/rebase), and per-PR capability gates in one payload
so the iOS PRs rebuild can render its list/detail/workflow surfaces
without fanning out across several commands. Contract is additive —
existing desktop consumers of the PR service and sync registry are
untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Harden Dictionary merges and drop duplicate matched-geometry sources

Part A — replace Dictionary(uniqueKeysWithValues:) with coalescing
merges so sync reconciliation overlaps cannot fatal the app:
- Database.swift lane-row keying
- LaneTreeView.swift lane-by-id memo

Part B — remove the destination-side matchedGeometryEffect emissions
in LaneDetailHeaderCard and WorkSessionHeader. The container's
navigationTransition(.zoom(sourceID:)) interpolates child layouts
during the push, so having the detail header ALSO emit isSource=true
for lane-icon/title/status and work-icon/title/status groups is what
SwiftUI warns about ("Multiple inserted views ... have isSource: true,
results are undefined"). The list rows remain the sole source. Init
signatures are preserved for call-site compatibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Split PRsTabView.swift into per-file view modules

Pure mechanical file split of the 2480-line monolith into 14 focused
files under apps/ios/ADE/Views/PRs/ so the PRs mobile rebuild can
operate on clean per-file surfaces. No behavior, type, data-access, or
UI changes. Visibility narrowed from private to internal where types
are shared across the new files.

New files (all under 500 lines):
- PrModels.swift                — shared structs/enums
- PrHelpers.swift               — free functions + ISO date parsing
- PrListRowModifier.swift       — .prListRow() modifier
- PrFiltersCard.swift           — filters card + signal chip
- PrRowCard.swift               — list row card
- PrsRootScreen.swift           — PRsTabView root
- PrDetailScreen.swift          — PrDetailView root
- PrDetailOverviewTab.swift     — overview tab, header, section card, chip wrap, cleanup banner
- PrDetailFilesTab.swift        — files tab, file diff card, unified diff view
- PrDetailChecksTab.swift       — checks tab, check row
- PrDetailActivityTab.swift     — activity tab, timeline row
- PrWorkflowCards.swift         — integration/queue/rebase workflow cards
- PrStackSheet.swift            — stack members sheet
- CreatePrWizardView.swift      — wizard + step indicator + markdown renderer

PRsTabView.swift is emptied to a forwarding comment to preserve the
pbxproj reference without further edits.

* Wire PrMobileSnapshot into PRs root screen and unified workflow cards

Adopt the new prs.getMobileSnapshot contract (commit ad17c74) on the
root PRs surface. The snapshot replaces the three-fan-out fetch for
queue/integration/rebase state with a single unified `workflowCards`
array, and its per-PR capability map drives the row swipe actions so
Close/Reopen respect the host's actual gating instead of guessing from
the stored PR state. Create PR is now disabled when createCapabilities
reports canCreateAny=false, and the previously dead status notice for
disconnected / hydrating / failed phases finally renders at the top of
the list.

Adds a new PrMobileWorkflowCardView that dispatches on card.kind
(queue | integration | rebase) so one ForEach covers all three. Legacy
per-kind fetches remain as a fallback so the list still renders when a
paired host predates the mobile-snapshot command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Gate PR detail actions and Create wizard by host capabilities

Thread PrActionCapabilities through PrDetailScreen → PrOverviewTab so
merge/close/reopen/request-reviewers/rerun-checks/comment gates come
from the host snapshot when available, and fall back to the legacy
supportsRemoteAction probe + PrActionAvailability when the mobile
snapshot hasn't arrived (offline / pre-contract host). Surface
mergeBlockedReason under the merge button so users see the specific
blocker (draft, failing checks, closed) instead of a silent disabled
state.

Accept optional PrCreateCapabilities in CreatePrWizardView. When
present, the lane picker shows only eligible lanes, the target branch
defaults from the host, and blocked lanes are listed separately with
their blockedReason. Nil fallback keeps the existing LaneSummary flow
intact so offline create still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Forward createCapabilities from root into Create PR wizard

The wizard's new createCapabilities prop was landed with a nil default
so the root-screen wire-up could follow. Pass mobileSnapshot?.create
Capabilities through so the wizard actually filters to canCreate lanes,
fills the default base branch from host metadata, and surfaces each
lane's blockedReason. With a nil snapshot the wizard still falls back
to the raw lanes list unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Polish PR stack sheet, workflow cards, and create wizard

Stack sheet: join PrGroupMemberSummary with snapshot.stacks data so the
chain renders with role badges (BASE/BODY/HEAD), depth indentation
(capped at 4 levels to stay readable on iPhone), dirty-worktree
warnings per lane, and live PR state pills. Each member with a PR is
now a tap target that pushes PrDetailView inside the sheet's own
NavigationStack — users no longer have to dismiss + re-navigate from
the root to open a stack member. Falls back to position-derived depth
when the snapshot is unavailable so offline stacks still render.

Workflow cards:
- queue: position chip (3/5) now renders next to the title instead of
  at the bottom, so progress is visible before the action buttons
- integration: "Open linked PR" upgraded from ghost glass to prominent
  glass with a full-width label + icon so the escape hatch reads as a
  primary action
- rebase: the CONFLICT badge is a new PrConflictBadge with a solid red
  background and warning icon so a predicted conflict can't be glanced
  past alongside the other tinted status pills

Create wizard:
- PrStepIndicator: active step title shown beside the counter, segment
  labels align to segments and highlight up-to-current-step so users
  see where they are at a glance
- Blocked-lane list got a lock icon header, per-row minus-circle
  icons, and a subtle warning-tinted background so "not eligible"
  reads as a deliberate state, not greyed-out content

Tests: two new cases for buildStackRows covering snapshot-joined and
fallback paths. Build + targeted tests green on iPhone 17 Pro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add chat activity indicator pill + per-turn provider badge

Ports items #2 and #3 from .factory/library/mobile-chat-port-plan.md.

- WorkActivityIndicator.swift: new view that scans the transcript tail
  for the most recent running command / tool call / file change / named
  activity / web search / subagent event and renders a one-line pill
  ("Running: ls -la", "Editing .../foo.ts", "Searching", etc.) with a
  pulsing dot. Falls back to "Thinking" when nothing specific is
  streaming. Respects Reduce Motion. Drop-in for
  WorkChatSessionView.streamingStatusSection.

- WorkChatHeaderAndMessageViews.swift: WorkChatMessageBubble now picks
  up the active session's provider from a new `workChatProvider`
  environment value and renders a compact provider chip (icon + label
  tinted by providerTint) next to the "Assistant" role label. No change
  to call sites — ancestor views opt in via
  .environment(\.workChatProvider, chatSummary?.provider).

* Add reasoning card and smart-autoscroll jump-to-latest pill to Work chat

Ports items #1 and #5 from the desktop chat port plan. A new
WorkReasoningCard replaces the generic event-card rendering for
reasoning entries: brain icon pulses while a turn is streaming, the
body stays open, three staggered dots signal live thinking, and once
the turn settles the card auto-collapses behind a "Reasoning" label
so it stops competing with the final assistant message.

Smart autoscroll now tracks an unreadBelowCount instead of always
yanking the view: when the user scrolls up and new entries arrive, the
count accumulates and a floating WorkJumpToLatestPill appears at the
bottom-trailing edge showing "N new" with the accent tint. Tap it or
naturally scroll back to the end to clear the count. Reduce-motion is
honored throughout via ADEMotion + static-dot fallbacks.

Also lands .factory/library/mobile-chat-port-plan.md so future work
on the other eight port-plan items has the same inventory to work
from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Upgrade queued-steer strip with collapsible rows and haptic cancel

Chat composer plan item #7 — queued-steer strip. The mobile strip now
mirrors desktop's PendingSteerItem treatment:

- Header collapses by default to a compact "N queued · preview" row
  with a chevron; taps toggle the list. Strip auto-expands whenever a
  row enters edit mode so edits aren't hidden behind a collapse tap.
- Each row ribbon starts with a discreet "Queued" pill (accent tint)
  and a relative timestamp so the queued state reads at a glance
  without competing with the body text.
- Body text now clips at 2 lines with tail truncation. Long messages
  stay reachable through the Edit button's inline TextEditor.
- Cancel drives a light-weight .sensoryFeedback(.impact) so the strip
  confirms destructive actions through haptics, not just visuals.

File stays under the 500-line ceiling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rebuild Work sidebar to match desktop SessionListPane architecture

Replaces the phone-only filter card + hard-coded section stack with the
same information architecture the desktop uses: a compact toolbar
(Search + New Chat accent button + Filter funnel toggle), an
expandable Group/Lane filter panel, and a grouped session list driven
by the user's chosen organization — byLane / byStatus / byTime —
with collapsible section headers showing icon, label, and count
badge. Organization and per-section collapse state persist via
@AppStorage so the sidebar reopens the way it was left.

Iconography matches desktop: magnifyingglass for search,
line.3.horizontal.decrease.circle for the funnel, plus for New Chat,
arrow.triangle.branch for lane grouping, and colored status dots for
the byStatus sections. Removes the always-on status chip strip and
the standalone "0 live" chip in favor of the filter-panel-scoped
live/waiting count chips that only appear when the panel is open.

Session row logic extracted into a reusable WorkSessionListRow so the
new grouped loop can render any organization with consistent swipe
and context-menu actions. New WorkSessionGrouping.swift holds the
pure grouping helpers (byStatus / byLane / byTime bucketers plus the
AppStorage serialization for collapsed-section ids).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Wire WorkActivityIndicator + provider env into the chat session view

Drops the spinner-only streamingStatusSection in favor of the activity
indicator settings-worker shipped at 24db48b, so the tail of the
transcript always surfaces the current tool/command/file edit as a
"Label · detail" pill when a turn is streaming. Also injects
\.workChatProvider down the transcript ForEach so every assistant
bubble can render the per-turn provider chip without each row having
to resolve it independently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Chat polish: streaming shimmer, tool result truncation, compact divider

Ports items #6, #8, and #10 from .factory/library/mobile-chat-port-plan.md.

#6 — Streaming shimmer on the active assistant bubble:
- New ADEStreamingShimmer view modifier (Views/Components/): gentle
  one-direction gradient sweep + accent glow + stroke, gated on isActive
  and respecting Reduce Motion (becomes a no-op).
- WorkChatMessageBubble gains an `isLive: Bool = false` prop; the timeline
  call site passes `isLatestAssistantMessageLive(message)` so only the
  newest assistant bubble of an active session shimmers.
- Liveness derivation lives in WorkChatSessionView+MessageLiveness.swift
  (scans visibleTimeline tail for the most recent assistant message id).

#8 — Tool-result truncation, preview, and show-all toggle:
- WorkToolCardView now shows a first-line preview in the collapsed header.
- Long results (> 500 chars) render truncated with a "Show all (N chars)"
  toggle; copy affordance is already provided by WorkOutputBlockHeader.
- Helpers: workToolResultPreview, workToolResultTruncate,
  workToolResultByteLabel.

#10 — Context-compact divider:
- WorkEventCardView special-cases kind == "contextCompact" and renders
  the new WorkContextCompactDivider: a horizontal hairline flanking a
  warning-tinted chip with the rectangle.compress.vertical icon, tokens
  freed (when parseable) and an AUTO/MANUAL trigger tag.
- Parsing handled by WorkContextCompactSummary.

Tests (appended to the end of ADETests.swift per coordination rule):
8 new unit tests covering the truncation boundary, preview extraction,
byte-label formatting, and context-compact summary parsing.

* Use branded provider logos + fix Work sidebar row and chat header layout

User flagged three concrete problems in the mobile Work surface:
the session rows all shared a generic grey brain icon instead of the
branded model logos the desktop uses, timestamps leaked as raw ISO
strings (`2026-04-16T07:40:08.095Z`) because the parser did not
accept fractional seconds, and the chat detail header wrapped its
chip row off-screen while showing a useless "—" duration and a
truncated "Summa..." column on mobile.

Bundle the LobeHub static SVGs (claude-color, codex-color, cursor,
opencode + anthropic / openai family marks) into Assets.xcassets as
Provider* imagesets with preserves-vector-representation. A new
WorkProviderLogo view renders the branded asset when one exists and
falls back to the tinted SF Symbol we already had.

Rewrite WorkSessionRow around the desktop SessionListPane cadence:
one line for status-dot + title + relative time, an optional preview
line, and a horizontal-scrolling chip strip (status pill, lane, model,
device presence). Drop the standalone duration column that was only
ever rendering "—" on ended sessions.

Rewrite WorkSessionHeader to a single collapsed meta line
("3h ago · sonnet"), a horizontal chip ScrollView so "Claude /
sonnet / Primary" never clips past the edge again, and a summary
line that only renders when there is a real summary — no more empty
"Duration —" placeholder. Session starts now parse cleanly thanks to
workDateFormatterFractional, so "Started" reads as relative time
everywhere else too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Polish Work logo avatar container + row chip strip + ended-session control bar

Wrap the branded provider SVG in a tinted rounded-card container so the
mobile mark reads the way desktop's Claude.Avatar / Codex.Avatar do —
logo centered on a tinted squircle with a hairline border — instead of
a raw glyph floating on the background.

Drop the redundant ENDED / closed status pill from session rows whose
status matches the section they already live in. Needs-input, running,
idle, and archived rows keep their pill because those differ from the
group header.

Rewrite WorkSessionControlBar around the actual session states. On an
ENDED session the only sensible action is Resume, so the old "Close
session" filled button (closing an already-closed session) is gone and
Resume chat takes the whole row as the prominent CTA. On active turns
Interrupt is the prominent action; End is the small escape hatch. Idle
and awaiting-input get a Resume + End pair.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop the Turn finished card that dumped raw usage JSON into the transcript

The Turn status card already marks completion and the session usage
summary card at the top of the chat aggregates token spend. Rendering
a second "Turn finished" event card for every .done envelope just
dumped the host's raw summary string — usually a JSON usage blob —
into the transcript alongside the real messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add inline model picker to the chat composer

Composer chip strip now surfaces Model / Access / Profile as three
labeled pills with the branded provider logo on the model chip, and
tapping the model chip opens a WorkModelPickerSheet that mirrors the
desktop ProviderModelSelector mobile variant — grouped by provider,
branded logos inline, tier badge, one-line tagline, and a checkmark
for the currently applied model. Committing a selection calls
updateChatSession on the paired host and marks the change with a
light haptic.

Unknown models (e.g. a freshly released id the mobile catalog doesn't
know about yet) still render as the active entry so the picker never
hides the live host model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Replace New Chat modal with a pushed welcome screen

Desktop's "+ New Chat" flows straight into a dedicated welcome page:
big ADE word-mark, tagline, workspace pill, and a prominent composer
at the bottom. Typing and sending turns the page into the live chat
without bouncing back to a sidebar. Mobile currently opens a six-step
slide-up sheet for the same intent, which the user called out as
wrong for this flow.

Add WorkNewChatScreen and a WorkNewChatRoute hashable value. The "+"
toolbar button, the inline "New Chat" button in the sidebar toolbar,
and the empty-state CTA now push the route onto the NavigationStack
path instead of opening WorkNewChatSheet. Inside the screen the
composer defaults to claude-sonnet-4-6 but the same
WorkModelPickerSheet the chat composer uses is one tap away, so
users can still switch provider/model before sending. Submitting
creates the host chat session and replaces the path with the live
session route — Back goes to the sidebar, not to an empty form.

The old WorkNewChatSheet stays in the codebase for now so the sync
surfaces it still references keep compiling; it can be pulled once
nothing else presents it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Streamline Work chat composer, picker, and chrome

Drop the bulky two-line composer chips for a single-row strip of compact
desktop-style pills (access dot + runtime label, model logo + short name).
The access pill flips runtime modes through an inline menu instead of
routing users through a separate Chat Settings sheet, which is removed
from the UI entirely. Redesign the model picker to match the desktop
ProviderModelSelector — "Select Model" title, search field, provider
tab strip with count badges, and grouped rows with active/Ready chips.
Hide the tab bar once a session is pushed so the transcript and
composer get the full screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Collapse Work composer into a single desktop-style container

Fold the text field, pill strip, and send button into one rounded
container so the composer reads as one unit like the desktop
"Type to vibecode…" box. Drop the redundant ENDED/RUNNING status pill
row with its inline play/stop button — the session control bar above
the transcript already owns the Resume/Interrupt CTA and the
"Resume this chat before sending another message." feedback line below
communicates status in prose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Flow Work chat transcript like a document

Drop the heavy card chrome the timeline used on every message, turn
status, and system notice. User messages stay right-aligned with a
small tinted bubble; assistant messages shed the box entirely and
render as provider-chipped prose. Low-value event kinds
(status / activity / notice / todo / autoApproval / webSearch / promptSuggestion
/ toolUseSummary / pendingInputResolved) collapse into a single-line
ribbon instead of a full card, matching the desktop's document-style
feel. The session header drops its adeListCard chrome for a compact
status dot · lane · meta line, and the reasoning card now defaults to
collapsed with a slimmer background so it stops competing with the
final assistant turn for attention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tune composer typing, markdown pills, and model catalog

- Composer now accepts keystrokes while the session is ended so the
  user can draft before hitting Resume. The feedback line below still
  explains why send is blocked, and Send itself stays disabled via a
  new canSend gate.
- Give inline markdown code runs the desktop pill treatment — tinted
  accent background, monospaced font, accent foreground — so
  identifiers, branch names, and file paths pop out of prose like they
  do on desktop.
- Expand the mobile model catalog to mirror
  apps/desktop/src/shared/modelRegistry.ts: Claude Opus 4.6 / Opus 4.6
  1M / Sonnet 4.6 / Haiku 4.5; the seven shipping Codex CLI tiers;
  Cursor Auto/Sonnet-thinking/Sonnet/GPT-5/Codex; and a broad OpenCode
  row set (Anthropic / OpenAI / Google / xAI / DeepSeek / LM Studio /
  Ollama). Ids match the sync-contract shape the host accepts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Ship a properly defined composer card

Rewrite composerInset so the composer reads as one bold rounded
container rather than a faint tinted rectangle. The new card gets a
deeper recessedBackground fill, a 22pt radius, a visible border
stroke, and a subtle drop shadow so it sits cleanly over the chat
surface. The send button shows a visible disabled state (accent ring
outline + secondary glyph) instead of fading into the background, and
takes on an accent fill with a soft purple halo the moment typing
begins, mirroring the desktop "Send" affordance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Give composer desktop-shaped Send pill and full model name

The composer now renders the model pill with the desktop display
name ("Claude Sonnet 4.6" instead of just "Sonnet"), and replaces
the icon-only circle with a desktop-shaped Send pill — paperplane
glyph plus a "Send" label in a capsule that fills with accent and
halos purple the moment the draft has content. Wrap the card in
ultra-thin material over a darker tint so the composer sits cleanly
against the transcript background and reads as one container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop outer composer inset bubble

The composerInset used to paint a faint glass rectangle around the
whole bottom area (composer card + feedback text), which read as a
second "bubble" overlapping the composer card itself. Since the
composerCard now owns its own clearly-defined rounded surface we can
drop the outer background + glassEffect and let the feedback text sit
plainly below the card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Move composer feedback above the composer card

The "Resume this chat before sending another message." (and similar
status-blocked messages) now render above the composer card instead
of below, and the bottom padding is dropped to zero so the card sits
flush with the safe-area edge. Feedback text is also centered to
read as a banner hint rather than a footnote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Match desktop model picker organization and new chat composer

- Model picker now uses the desktop ModelCatalogPanel's 2-level
  hierarchy: CLAUDE / CODEX / CURSOR / OPENCODE group tab strip,
  then a provider badge row inside the active group. OpenCode
  exposes its full provider ladder (Anthropic, OpenAI, Google,
  xAI, DeepSeek, LM Studio, Ollama) as horizontal badges just like
  the desktop panel, with count chips on each. Search mode flattens
  results and renders them grouped by group header, mirroring
  desktop behavior.
- New chat welcome page composer rebuilt to share the same rounded
  glass card, model pill logic (full "Claude Sonnet 4.6" name), and
  desktop-shaped Send capsule as the in-session composer, dropping
  the prior ad-hoc chip + round-button layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* WIP: in-progress mobile droid work before merging main

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* WIP: iOS chat redesign + unified per-provider chat accent

iOS:
- New chatSurfaceAccent with per-provider table (Claude amber, Codex warm-white, etc.)
  mirroring the shared desktop mapping so each chat reads the same on both surfaces.
- Per-model reasoningTiers lookup matching MODEL_REGISTRY — effort pill only renders
  for models that advertise tiers (Opus: low/med/high/max, Sonnet: low/med/high,
  Haiku hides entirely, Codex tiers: low/med/high/xhigh, mini: med/high).
- Compact "…" nav toolbar menu (lane name + Go to lane) replacing the inline
  status row under the chat title.
- Stop button folded into the composer Send slot while the assistant streams;
  removed the full-width yellow WorkSessionControlBar.
- Turn-separator pill between user turns, assistant message card surface, dropped
  redundant model-badge chip above each response and inside the USAGE row, compact
  Thought pill, k/M-abbreviated usage numbers with separate Cache / New cache.
- Retired unused LaneChatSessionView and its pbxproj references.

Desktop:
- PROVIDER_CHAT_ACCENTS + providerChatAccent() in chatSurfaceTheme.ts; AgentChatPane's
  draftAccent routes through provider first so Claude/Codex chats share a single tone
  across model variants.
- resolveModelAlias now falls back to bySdkModelId so provider-model-id forms like
  "claude-sonnet-4-6" or "gpt-5.4-codex" resolve — fixes mid-turn model switches
  from iOS which sends that form.

Known build issue (pre-existing, not mine): apps/ios PRs area has uncommitted WIP
referencing PrSingleLineEditSheet / PrMultilineEditSheet / PrSubmitReviewSheet
(files don't exist) and ADEColor.surface (field doesn't exist — should be
surfaceBackground). Chat work compiles fine in isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Finalize pass: simplify sync/chat services, align iOS schema, docs

- Drop dead code across sync host/remote-command/pairing services, chat
  message builder, agent chat service, registerIpc, and PackedSessionGrid
- Mirror iOS files-cache schema (files_workspaces + 4 snapshot tables) into
  desktop kvDb migrations so generate-ios-bootstrap-sql stays in sync
- Stabilize CommandPalette test by making getDetail's default resolved value
  survive mockClear between tests
- Refresh internal docs for chat slash-command discovery, provider accents,
  PackedSessionGrid resize model, lane device presence, Linear dispatch
  replay, and sync pairing/pin stores

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix PR review and CI issues

* Retry buffered sync updates on transient failures

* Include project root in PTY broadcast tests

* Preserve local Claude commands and orchestrator compaction

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
arul28 added a commit that referenced this pull request Jul 3, 2026
Fixes remote/sync audit findings #1-#4, #6-#20, plus perf M6, bug-sweep #13, and perf L9.

Skips #5 per disposition because TUI app/component files are owned by concurrent perf work. Implements #6 as the requested partial version/capability validation only.
arul28 added a commit that referenced this pull request Jul 3, 2026
…ss (#696)

* ade code: per-provider Chat/CLI interface choice

Add an Interface: Chat | CLI row to the ADE Code (Ink TUI) new-chat and
/model setup panes, matching the desktop/iOS switcher. Chat creates an
SDK chat via chat.createSession (all providers, including Claude — a new
path); CLI starts a tracked provider CLI terminal via start_cli_session
(claude/codex/cursor/droid/opencode). Defaults to Chat; editable on a
draft, read-only once a session exists.

- Generalize the Claude-only terminal paths: startClaudeTerminalSession
  -> provider-generic startCliTerminalSession; listTerminalSessions and
  remoteLauncher.isTerminalSessionLaunchable now surface every tracked
  CLI provider (not just Claude); terminalSessionToChatSummary, the
  Ctrl+T control gates, TerminalPane status ("<PROVIDER> CONTROL"),
  FooterControls label, and grid control hint are provider-neutral.
- Submit-path branching: focused terminal -> pty send/resume (Claude
  keeps its double-enter, other providers use pty.sendToSession); draft
  Interface=CLI -> tracked CLI terminal; otherwise chat.createSession.
- Interface-aware Cursor model gating in the model picker (Chat disables
  CLI-only Cursor models and vice versa).
- Keep Claude-only chrome: closed-transcript stripping, naming hint, and
  /model + /effort writing into a running Claude terminal.
- Tests: provider-generic start payloads (5 providers), trackedCli
  provider resolution, listTerminalSessions/isTerminalSessionLaunchable
  inclusivity, interface-row state machine + defaulting, Cursor gating.
- Docs: ADE Code README chat-setup + terminal-control sections.

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

* ade cli: remote/sync reliability hardening (audit batch)

Fixes remote/sync audit findings #1-#4, #6-#20, plus perf M6, bug-sweep #13, and perf L9.

Skips #5 per disposition because TUI app/component files are owned by concurrent perf work. Implements #6 as the requested partial version/capability validation only.

* ade code: perf — single-pass rows, memoized children, debounced background refresh

Findings:
- H1: derive selectable transcript rows once per render and build copy text lazily.
- H2: reuse pending steers, gate full Chat Info scans, and collapse cheap latest scans.
- H3/M8: memoize hot TUI children and stabilize high-churn props without adding hover throttles.
- H4: debounce/coalesce background session refreshes with an in-flight guard.
- M5: LRU-cache assistant markdown parses by message text.
- M7: tighten terminal grid reads and avoid trailing blank cell/row rendering.
- L10: reconcile local Ink install to locked 7.1.0 and validate ADE CLI.

* ade code: fix tui bug and parity batch

* ade code + sync: review-wave fixups (R1-R5, T1-T2)

* Persist Work Chat/CLI interface choice on iOS (+ desktop verify)

The in-project New Chat screen and the all-projects hub composer both
defaulted the Chat/CLI switcher to .chat on every open, so the choice was
forgotten across app restarts and project switches. Add a shared per-project
store (WorkNewSessionModePreferences, keyed by project id in the app-group
UserDefaults) that mirrors desktop's per-project WorkProjectViewState.draftKind.

- Seed sessionMode from the store in each composer's init (not onAppear) so the
  sessionMode onChange never fires to reset runtimeMode to the provider default.
- Persist only on an explicit Chat/CLI switcher tap via a new onUserSelect
  callback; programmatic model-availability fallbacks never write the store.
- A stored CLI choice is honored only when the restored model can run in CLI;
  otherwise the session opens on chat without discarding the preference.

Desktop already persists this per project (WorkProjectViewState.draftKind in
localStorage ade.workViewState.v1, preserved across new-draft/close-tab/lane-
refresh/project-switch); no desktop change needed.

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

* ade code: interface persistence, closed-session browsing, /secrets, picker digit fix

* Hub composer: reload per-project Chat/CLI mode on project switch

The hub composer seeded sessionMode once in init and never reloaded it when
the destination project changed, so switching from a project saved as Chat to
one saved as CLI (or vice versa) left submit branching on the stale mode.

Centralize the availability-aware fallback in a pure
WorkNewSessionModePreferences.resolvedMode(stored:modelId:provider:) (reused by
both composers' init) and add HubComposerDrawer.reloadSessionMode(forProjectId:)
called wherever pickedProjectId changes — the projectRow tap and the
reconcileDestination fallback. Reload is read-only; explicit switcher taps
remain the only writes, and a stored CLI choice still drops to chat for the
session when the current model can't run in CLI without discarding the pref.

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

* ade code: quality synthesis — digit-picker fixes, app.tsx extractions, UI polish

* ade code: closed-session glyph — user-initiated closes are not failures

Re-review finding: exit 130/143 and runtimeState 'killed' are the daemon's
user-initiated close classification (ptyService.statusFromExit), so the
closed-session drawer was marking most intentionally closed CLI sessions
with the failed glyph. Only terminalStatus === 'failed' or a genuine
non-{0,130,143} exit code renders failed now.

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

* ade cli: parity with hardening branch

* docs: sync internal docs with ade-code hardening branch

* test: steward pass — prune, consolidate ade-code suite

* ship: iteration 1 — address 10 review comments (greptile P1, codex P1, coderabbit x8)

- remoteBridge + cli.ts transports: normalize JSON-RPC response ids (class sweep)
- connection.ts: unlink stale socket before daemon spawn retry; owner-aware spawn-lock cleanup
- eventBuffer: oversized skipped events mark replay gaps
- syncHostService: queued-message watchdog timeout + warning metadata
- TerminalPane: full-column wide-glyph scan
- displayWidth: splitByDisplayCells single-pass grapheme partition (+ new test file)
- modelState: ollama/lmstudio use OpenCode permission behavior (not Cursor)
- state.ts: final state write flushed + awaited on signal exit
- preload: gap polling no longer fires same-binding project refresh

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

* ship: iteration 2 — address 2 codex P3s (grid claudeChrome, CJK truncate overflow)

- MultiChatGrid passes claudeChrome by derived terminal provider so non-Claude
  grid tiles keep neutral closed-transcript cleanup
- truncateDisplayEnd uses a cluster-boundary-safe prefix so a leading wide
  grapheme can no longer overflow the allocated cell width (+ regression tests)

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

* ship: iteration 3 — quote-aware VISUAL/EDITOR parsing (codex P3)

splitEditorCommand now tokenizes with quote/escape support so editors like
'emacsclient -a ""' or app paths with spaces keep working after the
shell:false hardening; quoted empty strings survive as argv entries.

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

* ship: iteration 4 — provider-aware terminal hydrate + local-provider permission cycle (codex P1/P2)

- selecting a non-Claude tracked CLI session now hydrates model state from
  terminalSessionProvider instead of forcing claude (footer/model targets the
  right provider for subsequent changes and launches)
- cyclePermission + updateChatModel payload route ollama/lmstudio through the
  OpenCode permission branch via runtimeProviderForUiProvider (class sweep)

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 20, 2026
* chat: stop ADE overriding the user's Claude settings at flag tier

ADE passes its Claude settings to the Agent SDK at flag tier, which outranks
every settings.json the SDK reads. Three keys were being sent unconditionally,
so a value ADE invented always beat the user's own configuration:

- outputStyle: resolved from the lane's settings.local.json with a `?? "Default"`
  fallback. "Default" is a real style, so a style configured in ~/.claude never
  applied to any ADE chat. The resolver also wrote that substituted value back
  into the session cache and read the cache first on the next build, which
  pinned it permanently once a session had started.
- workflowSizeGuideline: hardcoded "medium", so the user's /config choice had
  no effect. ADE keeps supplying "medium" as its own default, but only while no
  settings file states one.
- The user-tier root ignored CLAUDE_CONFIG_DIR, unlike the six other ADE modules
  that read Claude config, so a relocated config dir was invisible here.

The rule: name a settings key only when ADE genuinely owns it, and otherwise
leave it absent so the SDK's own local > project > user precedence resolves it.
ADE already opts into that precedence via settingSources. enabledPlugins stays
unconditional — the CLI merges it per plugin key rather than replacing the map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* codex: stop forcing service tier to "default" over the user's config.toml

codexServiceTierArgs returned an explicit null whenever fast mode was not on,
which includes every session where the user never touched the fast toggle.

Verified against a live `codex app-server`, with service_tier = "priority" in
config.toml:

  omit  -> serviceTier = priority   (the user's value survives)
  null  -> serviceTier = default    (the user's value is erased)

and with no service_tier configured at all:

  omit  -> no tier    null -> "default"    fast -> "priority"

So null is a real downgrade rather than a neutral "no opinion", and ADE shows
no service tier anywhere for the user to notice or undo it. Fast-off cannot mean
"force default" either: fastMode is persisted only when true and rehydrated as
`persisted?.fastMode === true`, so false is indistinguishable from never-set.
Omitting is the only honest encoding of "ADE is not forcing a tier"; the
app-server re-resolves per request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* providers: read config from the directory each CLI actually uses

Every provider CLI has an env var that relocates its config directory, and ADE
ignored some of them — so ADE read one directory while the process it spawned
read another, inside a single session.

Confirmed with a sentinel custom model: with FACTORY_HOME_OVERRIDE set, `droid`
lists the override home's models while droidModelsDiscovery read the real home's.

The overrides do not share a shape, which is why this is a helper rather than a
find-and-replace:

- CLAUDE_CONFIG_DIR and CODEX_HOME name the config directory itself.
- FACTORY_HOME_OVERRIDE replaces the HOME that ".factory" is appended to
  (`join($R(), ".factory")` in the droid v0.70.0 binary, where $R() is
  `process.env.FACTORY_HOME_OVERRIDE || homedir()`).

Read paths only; no behavior changes for anyone without these vars set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* cursor: stop switching off a sandbox policy the user configured

ADE sent `sandboxOptions: { enabled: local.sandboxEnabled }` unconditionally.
In the vendored SDK an explicit `false` and an absent key are not equivalent:

    if (!1 === n?.enabled) return { defaultSandboxPolicy: { type: "insecure_none" } }
    const o = Q(r) ? r : (!0 === n?.enabled ? Y("workspace_readwrite", ...) : void 0)

`false` returns before `perUserSandboxPolicy` — the user's ~/.cursor/sandbox.json
— is ever read. ADE sent `false` for agent mode, so a user who wrote a Cursor
sandbox policy had it silently switched off. The SDK's own error text ("remove
~/.cursor/sandbox.json to run without sandboxing") shows that file is meant to
be authoritative.

A boolean cannot express this, so the policy layer now states a directive:

  enable  — ask/plan. ADE asks for a sandbox; a user policy still wins.
  disable — full access. No sandbox, including for a user who wrote a policy,
            because full access means full access. Also the retry after a
            ConfigurationError, where the environment cannot sandbox at all
            and the alternative is a hard failure.
  inherit — agent mode. ADE has no sandbox UI here, so it says nothing and the
            user's file decides.

The retry guard now keys off the error and the not-yet-downgraded flag rather
than off whether ADE asked for the sandbox, because with "inherit" the
unsupported-environment error can now surface through the user's policy instead
of ADE's request. The permission fingerprint tracks the directive, since
"disable" and "inherit" share a false boolean but produce different options.

Note the SDK only loads any sandbox policy when an apiKey is present, so this
affects users with a Cursor key configured in ADE or CURSOR_API_KEY set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* droid: inherit autonomy from the user's settings, and fix the dead model read-back

Two defects, found by driving the real @factory/droid-sdk against synthetic
FACTORY_HOME_OVERRIDE homes.

1. ADE always stated autonomyLevel and interactionMode, so the user's
   ~/.factory/settings.json never applied. Same call, two homes differing only in
   settings.json:

     home A (model + spec/high configured) -> gemini-3-flash-preview / spec / high
     home B (empty settings.json)          -> claude-opus-4-6 / auto / off

   Omission resolves from the user's file, per key. Any value ADE states
   outranks it. ADE's fallback was "auto-low", which permits file edits, while
   Droid's documented default is autonomyLevel "off" — read-only. So ADE was
   handing out write access the CLI would not, and then writing that invented
   value into the session record where it was read back first on the next launch
   and pinned, exactly as in the Claude output-style bug.

   The SDK path now says nothing when the user picked no mode, which is what the
   terminal path already did — droidSettingsJson omits sessionDefaultSettings
   when permissionMode is null. A chosen mode, plan, and orchestration leads all
   still state both keys.

   Keys are OMITTED, never nulled: an explicit null neither clears the key nor
   restores the default, it wedges the Droid RPC for 30 seconds.

2. buildReady read `initResult.currentModelId`, which does not exist — the SDK
   reports resolved settings under `initResult.settings`. It always evaluated to
   null, so applyDroidSdkReadyState's adoption branch had never fired and ADE
   could never learn what model Droid actually chose. Now reads
   initResult.settings.modelId.

Also fixes providerConfigHomes to resolve its base from the named `homedir`
import: test suites mock node:os by spreading the real module, so a default
import kept the real homedir and read the developer's own ~/.factory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* codex: keep reasoning effort on the thread, not on the app-server process

ADE pushed `-c model_reasoning_effort="..."` onto the app-server spawn args. Two
problems beyond the obvious one:

- `-c` is the highest config layer. Per the documented precedence it outranks
  the user's ~/.codex/config.toml AND their per-project .codex/config.toml.
- It is a process argument, so one chat's selection applied to every thread on
  that app-server.

It also defeated ADE's own per-thread overlay, which was already written
correctly — codexThreadConfigArgs omits model_reasoning_effort when nothing is
set. Dropping the spawn flag makes the composer's effort selector mean what it
says: this chat, this thread.

The resolved value is still computed for display.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* opencode: close the plan-mode write hole and stop overriding user config

PLAN MODE COULD WRITE FILES. ade-plan set edit:"deny" but deliberately left the
native `task` tool enabled so child sessions would appear in the subagents pane.
A spawned subagent runs under its OWN ruleset — OpenCode's `general` is
merge(base, {todowrite:"deny"}), and the base is {"*":"allow"}, so edit is
ALLOWED there. Plan blocked the direct write and permitted the indirect one.
Plan now denies `task`.

Also, since OPENCODE_CONFIG_CONTENT is merged LAST and per key — only managed/MDM
config outranks it — every key ADE names beats the user's own opencode.json:

- share and snapshot are no longer sent. Neither has ADE UI, and snapshot's
  documented default is true: forcing false silently disabled OpenCode's own
  /undo and /revert, which restore uncommitted in-turn state that git lanes do
  not cover.
- autoupdate moves to OPENCODE_DISABLE_AUTOUPDATE in the server env. ADE does
  pin the binary, but that does not need the top-precedence config slot.
- provider.ollama / provider.lmstudio were emitted for every session with ADE's
  default baseURL even when the user had never configured them, deep-merging
  over the endpoint in their own opencode.json and repointing a configured
  remote host back at localhost. They are now emitted only when the user typed
  an endpoint or ADE discovered models. lmstudio is in OpenCode's provider
  catalog with its own npm and baseURL, so only ollama states npm.

Two faithfulness fixes while here:
- ade-full-auto now states read:"allow". The base ruleset asks before reading
  *.env, so "full access" still prompted. external_directory stays "ask": that
  boundary is ADE's lane worktree, not a permission tier the user chose.
- ade-* agents are hidden. Without a mode they defaulted to "all" and appeared
  in the user's Tab-cycle and @-autocomplete.

Deprecated spellings replaced: the ade-plan `tools` map becomes explicit
permission entries (OpenCode desugars it to exactly those, and an explicit
permission block wins), and maxSteps becomes steps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* quality: fix the passthrough that was undone downstream, plus review findings

Three real defects found by the correctness track, and the first one silently
defeated half of the Droid change:

- droidSdkWorker rebuilt interactionMode unconditionally. buildDroidSdkSessionSettings
  correctly omitted it for a session with no chosen mode, and the worker then
  materialised DroidInteractionMode.Auto and sent it on createSession and every
  updateSettings — restating at the highest tier exactly what the omission
  existed to leave alone. toDroidInteractionMode now maps "auto" explicitly and
  returns undefined otherwise; both call sites spread conditionally.
- Isolated (orchestration-lead) OpenCode servers lost autoupdate suppression:
  buildIsolatedOpenCodeEnv strips every OPENCODE_* var and rebuilds from
  scratch, so it never saw the var set in buildUserOpenCodeEnv, and a lead's
  server would self-update the binary ADE pins.
- CLAUDE_CONFIG_DIR was outranked by the ancestor walk. A lane normally sits
  under $HOME, so the walk reached the real ~/.claude and ranked it as a project
  tier ABOVE the relocated user tier — the normal case, not an edge case. The
  stale home settings won and ADE passed them at flag tier, the exact class this
  branch exists to remove. Regression test included; it reports StaleHomeStyle
  without the fix. discoverClaudePlugins was reading the plugin registry from
  the same wrong directory.

Maintainability findings applied:

- Deleted the sandboxEnabled boolean. It survived only to keep one call site
  compiling, and that call site — providerTaskRunner — still emitted the
  explicit `false` this branch removed from the worker, so the user's
  ~/.cursor/sandbox.json was still being suppressed there. One field, one
  encoding, and the compiler found the straggler.
- Deleted resolveSessionDroidPermissionMode (one caller, applying a fallback its
  own caller had already applied) and the unreachable default branch that hid
  exhaustiveness from the compiler.
- buildDroidSdkSessionSettings computes one `stated` object instead of three
  overlapping booleans, so spec-mode fields cannot be emitted without the mode
  that justifies them.
- Removed the codex reasoning-effort spawn block: after the flag was dropped it
  only recomputed a value thread/start overwrites moments later.
- claudeOutputStyles uses the shared claudeConfigHome rather than the duplicate
  resolver this branch had added a few files away.
- Narrowed types that carried members which can no longer occur, and gave
  buildPermissionConfig a keyed union — the OpenCode SDK absorbs unknown
  permission keys through an index signature, so a typo would have compiled and
  silently failed to apply.
- Collapsed the rationale that had been restated in five adapters into one doc
  block in providerConfigHomes, and added the test that module never had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* quality: re-review pass — Windows path comparison and structural cleanups

The re-review's one behavioral finding: claudeRootsByPrecedence compared paths
with `===`. On Windows the same directory is reachable through more than one
spelling, so a hand-typed CLAUDE_CONFIG_DIR differing only in drive-letter or
user-name case would both fail to match the real home root and let one directory
enter the precedence list twice, shadowing the tier below it. Now routed through
pathsEqual/pathKey, which the repo already had for exactly this.

Structural findings applied:

- planRequested is now derived from resolveDroidSdkInteractionMode rather than
  restating its spec rule. The `stated?.interactionMode === "spec"` gate was only
  correct because the two happened to agree; editing the resolver would have
  silently stopped emitting spec-mode config with no type error and no failing
  test.
- The cursor log line called buildCursorSdkLocalRunOptions instead of
  reconstructing the directive from the options it had just built.
- `stated` is explicitly typed, which drops an `as const`, and spreads directly.
- Restored one trimmed sentence that was carrying a probed fact: when ADE does
  ask Cursor for a sandbox, a user policy still wins — the SDK only falls back to
  its own default when the user wrote none. That is stated nowhere else.
- Adapter comments now name services/shared/providerConfigHomes.ts, so the rule
  they follow is reachable by grep from the files that follow it.
- Dropped imports orphaned by the earlier fixes.

Verified, not changed: the reasoning fields are assigned as possibly-undefined
while autonomy/interaction are conditionally spread. These are identical on the
wire — settings cross a process boundary as JSON and JSON.stringify drops
undefined keys, which the live Droid probe confirmed (updateSettings with an
undefined key is a no-op, while null wedges the RPC for 30s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* opencode: pin the isolated-lead autoupdate suppression with a test

The fix had no coverage, and the failure mode is silent: buildIsolatedOpenCodeEnv
rebuilds its env from scratch and drops every OPENCODE_* var, so anything set on
the user path never reaches a lead. Without the flag the lead's server updates
the binary ADE resolves and pins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* droid: restore the only exit from Spec mode, and key one more path comparison

Blast radius of the omission fix, caught on re-review. The Droid SDK has no
exitSpecMode — `enterSpecMode` is one-way, and the unconditional
`DroidInteractionMode.Auto` that the omission fix removed was ADE's only way
back out. A session ADE had put into Spec, whose plan mode was later turned off
while no permission mode was chosen, states nothing on the next update and stays
read-only for the rest of its life with no UI indication.

applySettings now tracks whether ADE itself entered Spec, and states Auto once
on the way out before returning to saying nothing. That re-adds exactly one
statement, in one bounded case, rather than reinstating the blanket override.
The flag resets on init and teardown so a recycled worker cannot carry it.

Also keys the output-style source labels through pathsEqual: the precedence walk
now folds case, so a case-variant CLAUDE_CONFIG_DIR could put the canonical home
spelling in the root list while the raw comparison still expected the variant,
labelling user styles "project". Metadata only — nothing branches on it — but the
two comparisons should not disagree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* test: pin the Droid settings contract, and align plan mode across both paths

Writing the tests exposed that the Droid omission does not fire in practice.
ADE always carries a generic permissionMode, and legacyPermissionModeToDroid
PermissionMode maps it onto a Droid mode, so chosenMode is effectively never
null: a bare Droid session resolves to autonomyLevel "low", not to an omitted
key. That is the ADE-owned half of the rule working as intended — the composer
chip is real UI, so ADE's value should win — but it means the omission path is
reachable only from launches that carry no permission mode at all. The tests now
pin what actually happens rather than a claim that cannot be reached.

Also found by the same tests: ADE's two Droid paths disagreed about plan mode.
droidSettingsJson sends {interactionMode: spec, autonomyLevel: off} on the
terminal path, while the SDK path sent spec alongside whatever the permission
chip mapped to — "low" for a default session. Spec collapses Droid's compound
autonomyMode and reads back as level "off", so the extra claim was discarded and
behavior was unaffected, but the two paths should not state different things.
The SDK path now sends "off" with spec, and the spec-mode fields stay gated on
the stated interaction mode.

Three tests added to the existing agentChatService suite rather than a new file:
autonomy derived from ADE's chip, an explicitly chosen mode, and plan mapping
onto spec with autonomy off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* test: make the Droid interaction-mode mapping testable and pin it

droidSdkWorker.ts has zero exports and attaches a process.on("message") handler
at import — it is a fork() entrypoint, so nothing in it can be unit tested and
the re-materialisation bug it carried was unpinnable where it lived.

Moved the pure mapping to droidSdkProtocol.ts, which is importable and already
has a suite, and gave it the enum table rather than the SDK module so it stays a
pure function. Two tests now pin the contract that undefined maps to undefined —
the exact behavior whose absence let the worker restate a mode the service had
deliberately omitted — and that every stated mode still maps to its enum value.

No new test file: the assertions extend the existing droidSdkProtocol suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* docs+tui: record the provider config-ownership rule and honour CLAUDE_CONFIG_DIR

Docs now carry the rule itself rather than a list of what changed. agent-routing
gains "Provider config ownership" — the rule, plus a per-provider table of what
omitting a key does and what an explicit null/false does, each row verified
against a live runtime rather than read off a schema — and "Provider config
homes" for the three env overrides that do not share a shape. Source file maps
and the affected feature READMEs point at it, so provider #6 starts there
instead of reverse-engineering the rule from a Cursor comment.

TUI parity, both the same bug class this branch is about:

- claudeHomePath hardcoded ~/.claude, so the TUI read keybindings, statusLine,
  vim mode, and agents from a directory Claude Code is not using whenever
  CLAUDE_CONFIG_DIR moved it. Now goes through claudeConfigHome.
- formatOutputStyles keyed the active row off a session value that is now null
  until a settings file names one, so the listing would have highlighted
  nothing. It falls back to "Default" for display only, matching what the
  desktop /output-style handler shows, and never writes it back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* review: fix four findings from Codex, Cursor Bugbot and CodeRabbit

Three bots independently found that the Spec-mode escape flag only covered one
of the three ways a session can enter Spec. `sessionOptions` can start the SDK
directly in Spec, and the resume-failure fallback creates a session the same
way, so a session created in Spec had no recorded way back out — and the SDK has
no exitSpecMode. Both paths now record it. The cross-worker case is documented
rather than papered over: a session a PREVIOUS worker left in Spec cannot be
detected, because that state lives in Droid and the SDK exposes no way to read
it back, and assuming Spec on every resume would state a mode ADE does not own.

Cursor Bugbot and CodeRabbit both caught that listing `/output-style` with no
argument wrote the resolved name onto the session and persisted it. Every later
option build then treated that cache as a real selection, so ADE sent
outputStyle at flag tier and suppressed Claude's own resolution — reintroducing
the exact override this branch removes, through the display path. The listing no
longer mutates the session, and reads the settings files before the cache so a
newer selection wins.

Codex caught that the creation path still substituted "auto-low" for a Droid
session that requested no mode, which is why the resolver could never return
null. That is the same shape as the `?? "Default"` bug this branch fixes: the
substituted value is persisted and read back as a real choice. The desktop
composer always sends a mode, so this only changes launches that send nothing —
which is precisely the case that should inherit.

Codex also caught a regression in the local-provider trim: ollama is not in
OpenCode's catalog, so when discovery found models but the user had typed no
endpoint, the models were named with no address to reach them — worst for an
isolated lead, which inherits no user config at all. A user-typed endpoint still
wins; the default only fills the gap.

Regression tests added for the output-style listing and both ollama cases; each
was verified to fail without its fix. The listing test lives in its own suite
because adding it inside the existing block perturbed shared state two Cursor
recovery tests depend on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* droid: read the resumed session's mode instead of assuming it

CodeRabbit's remaining case was real and reachable: turn plan off while a Droid
chat is idle and its worker has been evicted, and the next message resumes a
session Droid still has in Spec while ADE now states nothing — leaving it
read-only with no way out, because the SDK has no exitSpecMode.

ADE cannot remember this on its own; the state lives in Droid. But the resumed
session hands its resolved settings back in initResult, so the worker can simply
read the live interactionMode and seed the escape flag from it. That is strictly
better than the alternative of assuming Spec on every resume, which would have
meant stating a mode ADE does not own — overriding a user who configured
interactionMode in their own settings.json.

Two Codex P1s on this push were re-anchored copies of findings already fixed in
07b88b6 (the "auto-low" creation fallback is gone; the Spec flag is set on both
create paths). Verified against the current code rather than re-fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* pty: honour CLAUDE_CONFIG_DIR for Claude session storage

CodeRabbit caught a genuine miss in my own sweep: this file was edited for
exactly this bug class, but only the Codex and Factory resolvers were replaced.
claudeProjectDirForCwd still built `<homedir>/.claude/projects`, so with
CLAUDE_CONFIG_DIR set, Claude storage backfill and runtime-title capture read a
directory the CLI is not writing to.

The fixtures in ptyService.test.ts now resolve through the same helper the
production path uses. The shared test setup already points CLAUDE_CONFIG_DIR at
a temp directory, so those cases only pass while the code honours it — verified
by reverting the fix, which fails three of them. That is the regression coverage
the review asked for, without a new test file.

All five provider-config call sites in this file now pass os.homedir()
explicitly, so the module's own node:os mock governs the fallback rather than
the shared helper's named import.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

* review: don't infer Spec ownership, and don't invent an endpoint over user config

Two follow-ups from CodeRabbit, both of which caught my previous fix trading one
override for another.

Reading the resumed session's interactionMode cannot tell a Spec that ADE
entered from one the user configured in ~/.factory/settings.json. Exiting the
latter would be precisely the override this branch removes, so the inference is
gone. applySettings still records Spec whenever ADE itself states it, which
covers every resume ADE drives; the residual — a session left in Spec by a
previous worker while ADE now states nothing — stays documented rather than
"fixed" by overriding a user setting. CodeRabbit suggested this inference in the
prior round and then flagged it here; the flag is right.

The ollama endpoint has the same shape. OPENCODE_CONFIG_CONTENT merges last, so
an ADE default can replace a remote host in the user's own opencode.json, which
ADE cannot read. But an isolated lead inherits no user config at all, so there
is nothing to clobber and nothing else to supply the address — which is the case
Codex's original finding was actually about. The fallback is now gated on
isolatedConfig, so a lead's discovered models stay runnable and an ordinary
session keeps deferring to the user's own file. Both directions are pinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nv4uczqGgnQBnrz1tNz5Lj

---------

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant