From e5c86df067f5b7acb02f9ec90c37b71f48e9fa0e Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 21 May 2026 18:48:06 -0400 Subject: [PATCH 1/3] ship: prepare lane for review --- docs/features/chat/README.md | 2 +- .../pty-and-processes.md | 13 +++++++- .../terminals-and-sessions/ui-surfaces.md | 31 ++++++++++++++++--- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/docs/features/chat/README.md b/docs/features/chat/README.md index 9ba2197d1..ebbac75ba 100644 --- a/docs/features/chat/README.md +++ b/docs/features/chat/README.md @@ -40,7 +40,7 @@ machinery layered on top. | `apps/desktop/src/shared/chatTranscript.ts` | Pure JSON-lines parser for `AgentChatEventEnvelope` values. Used by both the main process and the renderer. | | `apps/desktop/src/shared/chatSubagents.ts` | Cross-target subagent helpers: `buildSubagentPaneRows`, `selectedSubagentSnapshot`, `subagentIndexForPaneLine`, `subagentPaneSelectableLineOffsets`, `buildSubagentTranscriptEvents`, `isLifecycleEventForSnapshot`, plus the `latestPlan` derivation. Both the desktop `ChatSubagentsPanel` and the `apps/ade-cli/src/tuiClient/subagentPane.ts` / `chatInfo.ts` modules re-export from here so the desktop pane and the terminal TUI render the same roster, transcript filter, and plan summary. | | `apps/desktop/src/shared/types/chat.ts` | All chat types: `AgentChatSession`, `AgentChatEvent` union, `AgentChatEventHistorySnapshot` (with optional `sessionFound` for stale-session detection), permission modes, pending input, completion reports, `PARALLEL_CHAT_MAX_ATTACHMENTS`, and parallel launch state DTOs. | -| `apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` | Top-level renderer surface: state derivation, IPC wiring, composer mount, message-list mount, End/Delete chat controls in the header, parallel multi-model lane launch orchestration, transient-lane cleanup, and multi-lane deep-link navigation. Mounts `AgentQuestionModal` when the active pending input is a question/structured-question. Resolves the surface accent colour through `providerChatAccent(provider)` so Claude/Codex/Cursor stay visually consistent regardless of model variant. Visible Work grid tiles flush user/lifecycle/live events immediately and poll-recover active transcripts when IPC misses an event, even when the tile is not focused. Event-history snapshots with `sessionFound: false` clear stale locked-pane state instead of rendering a dead transcript. Draft chats scope their last-launch config by project/lane/surface/draft-kind and mark local model/reasoning/permission edits as touched so late lane-session hydration cannot overwrite the user's draft selection. During project transitions the pane blocks send/model/permission mutations and shows a "Project is switching..." composer placeholder so chat calls do not hit the wrong runtime binding. On macOS, polls `ade.iosSimulator.getStatus` and renders the iOS Simulator drawer toggle in the header when the platform is supported (see [iOS Simulator feature](../ios-simulator/README.md)); selecting elements inside the drawer flows back through the pane as `IosElementContextItem` chips on the composer. Polls `ade.appControl.getStatus` and exposes the App Control drawer toggle when the platform is supported, mounting `ChatAppControlPanel`; selections become `AppControlContextItem` chips + attachments on the composer. See [App Control](../computer-use/app-control.md). When mounted as a Work tile (`SessionSurface` passes `hideLaneToolDrawers={true}`) the iOS, App Control, and chat terminal drawer toggles are suppressed because the Work right-edge sidebar owns those lane-scoped drawers; hidden lane-tool mode also skips App Control status polling and terminal listing. The pane still listens on `ade:agent-chat:add-attachment` / `add-ios-context` / `add-app-control-context` / `add-builtin-browser-context` / `insert-draft` window events so selections from the sidebar flow into the active chat composer when the sidebar's `attachChatSessionId` matches. Work-tab CLI launches pass the active lane worktree into the shared launcher so the spawned CLI sees lane-aware Agent Skill roots. Proof remains chat-scoped and stays on the chat header. | +| `apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` | Top-level renderer surface: state derivation, IPC wiring, composer mount, message-list mount, End/Delete chat controls in the header, parallel multi-model lane launch orchestration, transient-lane cleanup, and multi-lane deep-link navigation. Mounts `AgentQuestionModal` when the active pending input is a question/structured-question. Resolves the surface accent colour through `providerChatAccent(provider)` so Claude/Codex/Cursor stay visually consistent regardless of model variant. Visible Work grid tiles flush user/lifecycle/live events immediately and poll-recover active transcripts when IPC misses an event, even when the tile is not focused. Event-history snapshots with `sessionFound: false` clear stale locked-pane state instead of rendering a dead transcript. Draft chats scope their last-launch config by project/lane/surface/draft-kind and mark local model/reasoning/permission edits as touched so late lane-session hydration cannot overwrite the user's draft selection. During project transitions the pane blocks send/model/permission mutations and shows a "Project is switching..." composer placeholder so chat calls do not hit the wrong runtime binding. On macOS, polls `ade.iosSimulator.getStatus` and renders the iOS Simulator drawer toggle in the header when the platform is supported (see [iOS Simulator feature](../ios-simulator/README.md)); selecting elements inside the drawer flows back through the pane as `IosElementContextItem` chips on the composer. Polls `ade.appControl.getStatus` and exposes the App Control drawer toggle when the platform is supported, mounting `ChatAppControlPanel`; selections become `AppControlContextItem` chips + attachments on the composer. See [App Control](../computer-use/app-control.md). When mounted as a Work tile (`SessionSurface` passes `hideLaneToolDrawers={true}`) the iOS, App Control, and chat terminal drawer toggles are suppressed because the Work right-edge sidebar owns those lane-scoped drawers; hidden lane-tool mode also skips App Control status polling and terminal listing. The pane still listens on `ade:agent-chat:add-attachment` / `add-ios-context` / `add-app-control-context` / `add-builtin-browser-context` / `insert-draft` window events so selections from the sidebar flow into the active chat composer when the sidebar's `attachChatSessionId` matches. Work-tab CLI launches pass the active lane worktree into the shared launcher so the spawned CLI sees lane-aware Agent Skill roots. Work CLI launches intentionally skip the direct-argv path: the pane drops `command` / `args` from the `onLaunchPtySession` payload and always sends `startupCommand` plus `WORK_CLI_STARTUP_DELAY_MS = 180` so the spawned shell can finish drawing its prompt before the CLI invocation is typed in (see [pty-and-processes.md](../terminals-and-sessions/pty-and-processes.md#create-flow-createargs) for how `ptyService.create` consumes the delay). Proof remains chat-scoped and stays on the chat header. | | `apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx` | Virtualized transcript renderer. Coalesces resize / measurement updates and, while sticky-to-bottom is active, follows height changes across multiple animation frames so streamed output and late row measurements do not leave the user above the newest message. Programmatic scroll writes are tracked by target scroll position, not a stale counter, so browser-coalesced scroll events do not swallow the next real user gesture. | | `apps/desktop/src/renderer/components/chat/ChatGitToolbar.tsx` | Git / PR quick-action toolbar above the composer. If the lane already has a linked PR, the PR button opens that PR; otherwise it routes to the PR workspace with a create-PR handoff (`create=1&sourceLaneId=&target=primary`). | | `apps/desktop/src/renderer/lib/visualContextFormatting.ts` | Serializes iOS, App Control, built-in browser, macOS VM, and attachment context into prompt text. Automatic macOS VM capability context is prompt-intent gated (`ADE VM`, `macOS VM`, Lume, isolated macOS GUI, etc.) so ordinary sends do not query or inject VM state. | diff --git a/docs/features/terminals-and-sessions/pty-and-processes.md b/docs/features/terminals-and-sessions/pty-and-processes.md index a5cd37513..bb91cac3f 100644 --- a/docs/features/terminals-and-sessions/pty-and-processes.md +++ b/docs/features/terminals-and-sessions/pty-and-processes.md @@ -202,7 +202,18 @@ Each live PTY has an entry in the `ptys` map keyed by `ptyId` with: 10. If the spawn ended up in a shell (no direct launch, or direct launch fell back), type `args.startupCommand` into the PTY so the shell executes the CLI. Direct launches that succeeded skip this — - they already received argv. Returns `{ ptyId, sessionId, pid }`. + they already received argv. The write can be deferred by + `args.startupDelayMs` (clamped 0–1000 ms via + `normalizeStartupCommandDelayMs`; non-numeric / negative / `NaN` + inputs collapse to `0`). When the delay is positive the write is + scheduled with `setTimeout(...).unref()` so the timer never blocks + process exit, and the scheduled callback bails out if the PTY was + disposed in the meantime. This is what the renderer Work CLI + launch path uses (`WORK_CLI_STARTUP_DELAY_MS = 180` in + `AgentChatPane`) to give the spawned shell a beat to finish + drawing its initial prompt before the CLI invocation is typed in, + avoiding a half-rendered command in the user's scrollback. Returns + `{ ptyId, sessionId, pid }`. The launch env is built layer by layer: `process.env`, the lane runtime env (from `getLaneRuntimeEnv`), the caller's `args.env`, then diff --git a/docs/features/terminals-and-sessions/ui-surfaces.md b/docs/features/terminals-and-sessions/ui-surfaces.md index 34ac9ccaa..758e62e46 100644 --- a/docs/features/terminals-and-sessions/ui-surfaces.md +++ b/docs/features/terminals-and-sessions/ui-surfaces.md @@ -554,7 +554,22 @@ up to `renameError` in `TerminalsPage`. A single hook that owns a lot of state: - session lists, deduped via `listSessionsCached()` with project-root + - lane + status keying + lane + status keying. When the IPC refresh returns a persisted row + that already has a pending optimistic session for the same id, the + hook calls `mergePendingOptimisticSession(persisted, optimistic)` to + keep the optimistic `ptyId` on the row until the persisted view + reflects it. The helper only merges when the persisted row is still + `running`, the optimistic session carries a non-empty `ptyId`, and + the persisted `ptyId` does not already match — that case returns the + persisted row untouched and drops the pending entry. When merged, + the row keeps the persisted fields but inherits the optimistic + `ptyId` (plus `toolType` / `runtimeState` as gap-fillers when the + persisted row hasn't backfilled them yet), and `keepPending: true` + leaves the pending entry in place so the next refresh can re-merge + if the persisted row still hasn't caught up. This closes a race + where the persisted row landed before its `ptyId` was written and + would otherwise clobber the optimistic attachment, leaving the new + `TerminalView` unable to subscribe to live PTY data - per-project work view state (open items, active/selected, view mode, draft kind, filters, organization, collapsed IDs, focus-hidden flag) - lane-scoped work view state keyed as `projectRoot::laneId` @@ -585,8 +600,9 @@ can subscribe to live PTY data before fast TUIs like Codex or Claude paint their first frame. Waiting on the refresh round-trip first used to lose the initial paint and leave the terminal blank. The `launchPtySession({ laneId, profile, command?, args?, startupCommand?, -env?, title?, tracked? })` helper (and its lane-scoped twin in -`useLaneWorkSessions`) builds a default launch payload with +startupDelayMs?, env?, title?, tracked? })` helper (and its +lane-scoped twin in `useLaneWorkSessions`) builds a default launch +payload with `buildTrackedCliLaunchCommand` when the caller didn't override `command`/`args`/`env`, so every entry point — chat composer launch button, TopBar work controls, lane Work pane — produces the same @@ -598,7 +614,14 @@ maps in `apps/desktop/src/shared/cliLaunch.ts`. The runtime strips leading `ENV=value` assignments before sniffing the provider, so continuation commands the OpenCode preamble emits (`OPENCODE_CONFIG_CONTENT=… opencode --session …`) round-trip -correctly. +correctly. `startupDelayMs` is forwarded into the `ade.pty.create` +payload only when the caller passes it (so non-Work callers don't +inherit a non-zero default); the Work CLI launch path in +`AgentChatPane` passes `WORK_CLI_STARTUP_DELAY_MS = 180` and +intentionally omits `command` / `args` so every Work CLI launch +goes through the shell + `startupCommand` path (see +[pty-and-processes.md](./pty-and-processes.md#create-flow-createargs) +for how the PTY service consumes the delay). `useLaneWorkSessions` (same file) wraps the same state but scopes to a single lane for the Lanes tab. From 31fd7a43ad34ac0303725118c5c37d99a148e83f Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 21 May 2026 19:16:21 -0400 Subject: [PATCH 2/3] ship: iteration 1 - address greptile startup timer review --- .../src/main/services/pty/ptyService.test.ts | 22 +++++++++++++++++++ .../src/main/services/pty/ptyService.ts | 15 +++++++++++-- .../pty-and-processes.md | 5 +++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/main/services/pty/ptyService.test.ts b/apps/desktop/src/main/services/pty/ptyService.test.ts index ac56aba9e..f993a0de7 100644 --- a/apps/desktop/src/main/services/pty/ptyService.test.ts +++ b/apps/desktop/src/main/services/pty/ptyService.test.ts @@ -1030,6 +1030,28 @@ describe("ptyService", () => { } }); + it("clears delayed startup command writes when disposed", async () => { + vi.useFakeTimers(); + try { + const { service, mockPty } = createHarness(); + const created = await service.create({ + laneId: "lane-1", + title: "Disposed delayed startup", + cols: 80, + rows: 24, + startupCommand: "echo disposed", + startupDelayMs: 180, + }); + + service.dispose({ ptyId: created.ptyId, sessionId: created.sessionId }); + await vi.advanceTimersByTimeAsync(180); + + expect(mockPty.write).not.toHaveBeenCalledWith("echo disposed\r"); + } finally { + vi.useRealTimers(); + } + }); + it("hydrates transcript reads from recent live output before the file stream flushes", async () => { const { service, mockPty, sessionService } = createHarness(); const created = await service.create({ diff --git a/apps/desktop/src/main/services/pty/ptyService.ts b/apps/desktop/src/main/services/pty/ptyService.ts index aa1236787..b074bfbe5 100644 --- a/apps/desktop/src/main/services/pty/ptyService.ts +++ b/apps/desktop/src/main/services/pty/ptyService.ts @@ -384,6 +384,7 @@ type PtyEntry = { recentOutputTail: string; /** Output-snippet title timer (skipped for interactive Claude/Codex; see CLI user-title path). */ aiTitleTimer: ReturnType | null; + startupTimer: ReturnType | null; cliUserTitleLineBuffer: string; cliUserTitleCommitted: boolean; }; @@ -2098,6 +2099,10 @@ export function createPtyService({ clearTimeout(entry.aiTitleTimer); entry.aiTitleTimer = null; } + if (entry.startupTimer) { + clearTimeout(entry.startupTimer); + entry.startupTimer = null; + } clearToolAutoCloseTimer(ptyId); cleanupEntryPaths(entry); flushPreview(entry); @@ -2717,6 +2722,7 @@ export function createPtyService({ terminalSnapshot: tracked ? createTerminalSnapshotMirror(cols, rows) : null, recentOutputTail: "", aiTitleTimer: null, + startupTimer: null, cliUserTitleLineBuffer: "", cliUserTitleCommitted: false, }; @@ -2828,6 +2834,7 @@ export function createPtyService({ // with CLIs that are only available through shell startup files. if (startupCommand && !launchedDirectCommand && selectedShell) { const writeStartupCommand = () => { + entry.startupTimer = null; if (entry.disposed) return; try { pty.write(`${startupCommand}\r`); @@ -2847,8 +2854,8 @@ export function createPtyService({ }; const startupDelayMs = normalizeStartupCommandDelayMs(args.startupDelayMs); if (startupDelayMs > 0) { - const startupTimer = setTimeout(writeStartupCommand, startupDelayMs); - startupTimer.unref?.(); + entry.startupTimer = setTimeout(writeStartupCommand, startupDelayMs); + entry.startupTimer.unref?.(); } else { writeStartupCommand(); } @@ -3587,6 +3594,10 @@ export function createPtyService({ clearTimeout(entry.aiTitleTimer); entry.aiTitleTimer = null; } + if (entry.startupTimer) { + clearTimeout(entry.startupTimer); + entry.startupTimer = null; + } clearToolAutoCloseTimer(ptyId); flushQueuedPtyData(entry, { ptyId, sessionId: entry.sessionId }); cleanupEntryPaths(entry); diff --git a/docs/features/terminals-and-sessions/pty-and-processes.md b/docs/features/terminals-and-sessions/pty-and-processes.md index bb91cac3f..5689fe26b 100644 --- a/docs/features/terminals-and-sessions/pty-and-processes.md +++ b/docs/features/terminals-and-sessions/pty-and-processes.md @@ -207,8 +207,9 @@ Each live PTY has an entry in the `ptys` map keyed by `ptyId` with: `normalizeStartupCommandDelayMs`; non-numeric / negative / `NaN` inputs collapse to `0`). When the delay is positive the write is scheduled with `setTimeout(...).unref()` so the timer never blocks - process exit, and the scheduled callback bails out if the PTY was - disposed in the meantime. This is what the renderer Work CLI + process exit; `closeEntry` / `dispose` clear the pending timer, and + the scheduled callback also bails out if the PTY was disposed in + the meantime. This is what the renderer Work CLI launch path uses (`WORK_CLI_STARTUP_DELAY_MS = 180` in `AgentChatPane`) to give the spawned shell a beat to finish drawing its initial prompt before the CLI invocation is typed in, From d127917b143930c175c5cc38429ba7df6ee31a1f Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 21 May 2026 19:39:55 -0400 Subject: [PATCH 3/3] ship: iteration 2 - address coderabbit naming review --- apps/desktop/src/renderer/components/chat/AgentChatPane.tsx | 4 ++-- docs/features/chat/README.md | 2 +- docs/features/terminals-and-sessions/pty-and-processes.md | 2 +- docs/features/terminals-and-sessions/ui-surfaces.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/renderer/components/chat/AgentChatPane.tsx b/apps/desktop/src/renderer/components/chat/AgentChatPane.tsx index c51eebf13..01bcad196 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatPane.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatPane.tsx @@ -152,7 +152,7 @@ const LAST_REASONING_KEY_PREFIX = "ade.chat.lastReasoningEffort"; const LAST_LAUNCH_CONFIG_KEY_PREFIX = "ade.chat.lastLaunchConfig.v1"; const SUBAGENT_AUTOOPEN_FIRED_KEY_PREFIX = "ade.chat.subagentAutoOpenFired"; const SUBAGENT_AUTOOPEN_FIRED_TTL_MS = 7 * 24 * 60 * 60 * 1000; -const WORK_CLI_STARTUP_DELAY_MS = 180; +const workCliStartupDelayMs = 180; export const DEFAULT_PARALLEL_ATTACHMENT_REQUEST = "Please review the attached files."; const chatToolbarActionBase = @@ -5542,7 +5542,7 @@ export function AgentChatPane({ profile: provider, title: workCliTitleFromPrompt(text || finalDisplayText || finalText, LAUNCH_PROFILE_TITLE[provider]), startupCommand: launch.startupCommand, - startupDelayMs: WORK_CLI_STARTUP_DELAY_MS, + startupDelayMs: workCliStartupDelayMs, ...(launch.env ? { env: launch.env } : {}), tracked: true, }); diff --git a/docs/features/chat/README.md b/docs/features/chat/README.md index ebbac75ba..18669e619 100644 --- a/docs/features/chat/README.md +++ b/docs/features/chat/README.md @@ -40,7 +40,7 @@ machinery layered on top. | `apps/desktop/src/shared/chatTranscript.ts` | Pure JSON-lines parser for `AgentChatEventEnvelope` values. Used by both the main process and the renderer. | | `apps/desktop/src/shared/chatSubagents.ts` | Cross-target subagent helpers: `buildSubagentPaneRows`, `selectedSubagentSnapshot`, `subagentIndexForPaneLine`, `subagentPaneSelectableLineOffsets`, `buildSubagentTranscriptEvents`, `isLifecycleEventForSnapshot`, plus the `latestPlan` derivation. Both the desktop `ChatSubagentsPanel` and the `apps/ade-cli/src/tuiClient/subagentPane.ts` / `chatInfo.ts` modules re-export from here so the desktop pane and the terminal TUI render the same roster, transcript filter, and plan summary. | | `apps/desktop/src/shared/types/chat.ts` | All chat types: `AgentChatSession`, `AgentChatEvent` union, `AgentChatEventHistorySnapshot` (with optional `sessionFound` for stale-session detection), permission modes, pending input, completion reports, `PARALLEL_CHAT_MAX_ATTACHMENTS`, and parallel launch state DTOs. | -| `apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` | Top-level renderer surface: state derivation, IPC wiring, composer mount, message-list mount, End/Delete chat controls in the header, parallel multi-model lane launch orchestration, transient-lane cleanup, and multi-lane deep-link navigation. Mounts `AgentQuestionModal` when the active pending input is a question/structured-question. Resolves the surface accent colour through `providerChatAccent(provider)` so Claude/Codex/Cursor stay visually consistent regardless of model variant. Visible Work grid tiles flush user/lifecycle/live events immediately and poll-recover active transcripts when IPC misses an event, even when the tile is not focused. Event-history snapshots with `sessionFound: false` clear stale locked-pane state instead of rendering a dead transcript. Draft chats scope their last-launch config by project/lane/surface/draft-kind and mark local model/reasoning/permission edits as touched so late lane-session hydration cannot overwrite the user's draft selection. During project transitions the pane blocks send/model/permission mutations and shows a "Project is switching..." composer placeholder so chat calls do not hit the wrong runtime binding. On macOS, polls `ade.iosSimulator.getStatus` and renders the iOS Simulator drawer toggle in the header when the platform is supported (see [iOS Simulator feature](../ios-simulator/README.md)); selecting elements inside the drawer flows back through the pane as `IosElementContextItem` chips on the composer. Polls `ade.appControl.getStatus` and exposes the App Control drawer toggle when the platform is supported, mounting `ChatAppControlPanel`; selections become `AppControlContextItem` chips + attachments on the composer. See [App Control](../computer-use/app-control.md). When mounted as a Work tile (`SessionSurface` passes `hideLaneToolDrawers={true}`) the iOS, App Control, and chat terminal drawer toggles are suppressed because the Work right-edge sidebar owns those lane-scoped drawers; hidden lane-tool mode also skips App Control status polling and terminal listing. The pane still listens on `ade:agent-chat:add-attachment` / `add-ios-context` / `add-app-control-context` / `add-builtin-browser-context` / `insert-draft` window events so selections from the sidebar flow into the active chat composer when the sidebar's `attachChatSessionId` matches. Work-tab CLI launches pass the active lane worktree into the shared launcher so the spawned CLI sees lane-aware Agent Skill roots. Work CLI launches intentionally skip the direct-argv path: the pane drops `command` / `args` from the `onLaunchPtySession` payload and always sends `startupCommand` plus `WORK_CLI_STARTUP_DELAY_MS = 180` so the spawned shell can finish drawing its prompt before the CLI invocation is typed in (see [pty-and-processes.md](../terminals-and-sessions/pty-and-processes.md#create-flow-createargs) for how `ptyService.create` consumes the delay). Proof remains chat-scoped and stays on the chat header. | +| `apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` | Top-level renderer surface: state derivation, IPC wiring, composer mount, message-list mount, End/Delete chat controls in the header, parallel multi-model lane launch orchestration, transient-lane cleanup, and multi-lane deep-link navigation. Mounts `AgentQuestionModal` when the active pending input is a question/structured-question. Resolves the surface accent colour through `providerChatAccent(provider)` so Claude/Codex/Cursor stay visually consistent regardless of model variant. Visible Work grid tiles flush user/lifecycle/live events immediately and poll-recover active transcripts when IPC misses an event, even when the tile is not focused. Event-history snapshots with `sessionFound: false` clear stale locked-pane state instead of rendering a dead transcript. Draft chats scope their last-launch config by project/lane/surface/draft-kind and mark local model/reasoning/permission edits as touched so late lane-session hydration cannot overwrite the user's draft selection. During project transitions the pane blocks send/model/permission mutations and shows a "Project is switching..." composer placeholder so chat calls do not hit the wrong runtime binding. On macOS, polls `ade.iosSimulator.getStatus` and renders the iOS Simulator drawer toggle in the header when the platform is supported (see [iOS Simulator feature](../ios-simulator/README.md)); selecting elements inside the drawer flows back through the pane as `IosElementContextItem` chips on the composer. Polls `ade.appControl.getStatus` and exposes the App Control drawer toggle when the platform is supported, mounting `ChatAppControlPanel`; selections become `AppControlContextItem` chips + attachments on the composer. See [App Control](../computer-use/app-control.md). When mounted as a Work tile (`SessionSurface` passes `hideLaneToolDrawers={true}`) the iOS, App Control, and chat terminal drawer toggles are suppressed because the Work right-edge sidebar owns those lane-scoped drawers; hidden lane-tool mode also skips App Control status polling and terminal listing. The pane still listens on `ade:agent-chat:add-attachment` / `add-ios-context` / `add-app-control-context` / `add-builtin-browser-context` / `insert-draft` window events so selections from the sidebar flow into the active chat composer when the sidebar's `attachChatSessionId` matches. Work-tab CLI launches pass the active lane worktree into the shared launcher so the spawned CLI sees lane-aware Agent Skill roots. Work CLI launches intentionally skip the direct-argv path: the pane drops `command` / `args` from the `onLaunchPtySession` payload and always sends `startupCommand` plus `workCliStartupDelayMs = 180` so the spawned shell can finish drawing its prompt before the CLI invocation is typed in (see [pty-and-processes.md](../terminals-and-sessions/pty-and-processes.md#create-flow-createargs) for how `ptyService.create` consumes the delay). Proof remains chat-scoped and stays on the chat header. | | `apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx` | Virtualized transcript renderer. Coalesces resize / measurement updates and, while sticky-to-bottom is active, follows height changes across multiple animation frames so streamed output and late row measurements do not leave the user above the newest message. Programmatic scroll writes are tracked by target scroll position, not a stale counter, so browser-coalesced scroll events do not swallow the next real user gesture. | | `apps/desktop/src/renderer/components/chat/ChatGitToolbar.tsx` | Git / PR quick-action toolbar above the composer. If the lane already has a linked PR, the PR button opens that PR; otherwise it routes to the PR workspace with a create-PR handoff (`create=1&sourceLaneId=&target=primary`). | | `apps/desktop/src/renderer/lib/visualContextFormatting.ts` | Serializes iOS, App Control, built-in browser, macOS VM, and attachment context into prompt text. Automatic macOS VM capability context is prompt-intent gated (`ADE VM`, `macOS VM`, Lume, isolated macOS GUI, etc.) so ordinary sends do not query or inject VM state. | diff --git a/docs/features/terminals-and-sessions/pty-and-processes.md b/docs/features/terminals-and-sessions/pty-and-processes.md index 5689fe26b..56d683448 100644 --- a/docs/features/terminals-and-sessions/pty-and-processes.md +++ b/docs/features/terminals-and-sessions/pty-and-processes.md @@ -210,7 +210,7 @@ Each live PTY has an entry in the `ptys` map keyed by `ptyId` with: process exit; `closeEntry` / `dispose` clear the pending timer, and the scheduled callback also bails out if the PTY was disposed in the meantime. This is what the renderer Work CLI - launch path uses (`WORK_CLI_STARTUP_DELAY_MS = 180` in + launch path uses (`workCliStartupDelayMs = 180` in `AgentChatPane`) to give the spawned shell a beat to finish drawing its initial prompt before the CLI invocation is typed in, avoiding a half-rendered command in the user's scrollback. Returns diff --git a/docs/features/terminals-and-sessions/ui-surfaces.md b/docs/features/terminals-and-sessions/ui-surfaces.md index 758e62e46..f63d30a9c 100644 --- a/docs/features/terminals-and-sessions/ui-surfaces.md +++ b/docs/features/terminals-and-sessions/ui-surfaces.md @@ -617,7 +617,7 @@ provider, so continuation commands the OpenCode preamble emits correctly. `startupDelayMs` is forwarded into the `ade.pty.create` payload only when the caller passes it (so non-Work callers don't inherit a non-zero default); the Work CLI launch path in -`AgentChatPane` passes `WORK_CLI_STARTUP_DELAY_MS = 180` and +`AgentChatPane` passes `workCliStartupDelayMs = 180` and intentionally omits `command` / `args` so every Work CLI launch goes through the shell + `startupCommand` path (see [pty-and-processes.md](./pty-and-processes.md#create-flow-createargs)