From 314993c200f6f3e77be8f1e979520f2bdf7bff78 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sat, 10 Oct 2026 11:43:05 -0700 Subject: [PATCH] refactor(terminals): terminal input and inspection route by the PTY's owner (#27279) * refactor(terminals): terminal input and inspection route by the PTY's owner A remote PTY id already names the server that owns it. inspect, confirm, send, verified send and the data subscription took settings only to fall back to the focused server for an id without an owner segment, so an old ownerless id could reach a server that does not own it. They now route by the id alone: an ownerless remote id is never dialed and never confirmed locally. Dropping the parameter removes the settings plumbing the native chat, agent draft paste and follow-up paths carried only for this, including the raw focused-settings default in sendBracketedPasteToAgent, and deletes getSettingsForAgentTabRuntimeOwner. * test(e2e): native chat SSH delivery and remote completion follow the narrower terminal signatures --------- Co-authored-by: m4air --- config/focus-setting-read-baseline.txt | 28 +-- config/owner-routing-baseline.txt | 3 - .../NativeChatComposer.paste-remount.test.tsx | 1 - .../native-chat/NativeChatComposer.test.tsx | 15 +- .../native-chat/NativeChatComposer.tsx | 8 +- ...ResolvedView.prompt-card-composer.test.tsx | 12 +- .../native-chat/NativeChatResolvedView.tsx | 2 +- .../claude-model-switch-confirmation.test.ts | 7 - .../claude-model-switch-confirmation.ts | 7 +- ...ative-chat-composer-prompt-recall.test.tsx | 3 - .../native-chat-composer-target.ts | 2 - .../native-chat/native-chat-input-clear.ts | 21 +- .../native-chat-observed-send.test.ts | 24 +-- .../native-chat/native-chat-observed-send.ts | 8 +- .../native-chat-queue-send-confirm.test.tsx | 3 - .../native-chat-runtime-image-send.ts | 20 +- ...ive-chat-runtime-send-launch-draft.test.ts | 33 ++-- .../native-chat-runtime-send.test.ts | 184 ++++++------------ .../native-chat/native-chat-runtime-send.ts | 38 ++-- ...structured-send-composition-clear.test.tsx | 3 - ...-native-chat-composer-attachments.test.tsx | 3 +- .../use-native-chat-composer-interrupt.ts | 2 +- .../use-native-chat-interactive-send.test.tsx | 97 +++------ .../use-native-chat-interactive-send.ts | 42 ++-- ...tive-chat-picker-command-dispatch.test.tsx | 4 +- ...use-native-chat-picker-command-dispatch.ts | 4 +- ...use-native-chat-pty-composer-send.test.tsx | 6 +- .../use-native-chat-pty-composer-send.ts | 9 +- ...ative-chat-session-option-command.test.tsx | 8 +- .../use-native-chat-session-option-command.ts | 15 +- ...ion-coordinator-attention-dispatch.test.ts | 8 - ...oordinator-completion-replay-guard.test.ts | 8 - ...ompletion-coordinator-dispose-leak.test.ts | 1 - ...coordinator-hook-done-quiet-window.test.ts | 7 - ...-coordinator-hook-title-precedence.test.ts | 10 - ...on-coordinator-monitoring-turn-end.test.ts | 1 - ...ordinator-pending-title-inspection.test.ts | 8 - ...letion-coordinator-process-cadence.test.ts | 13 -- ...dinator-process-exit-turn-boundary.test.ts | 2 - ...dinator-queued-inspection-disposal.test.ts | 3 - ...-coordinator-stamped-turn-boundary.test.ts | 13 -- ...on-coordinator-stamped-turn-replay.test.ts | 12 -- .../agent-completion-coordinator-types.ts | 3 - ...ent-completion-no-evidence-cadence.test.ts | 1 - .../agent-completion-process-monitor.ts | 4 +- ...-completion-stale-evidence-backoff.test.ts | 1 - ...ent-completion-steady-state-opt-in.test.ts | 5 +- ...ane-foreground-process-exit-retire.test.ts | 1 - .../pending-pane-close-confirmation.test.ts | 3 +- .../pty-connection/pane-agent-identity.ts | 4 +- .../pty-connection/pane-serializer-settle.ts | 36 +--- .../pty-connection/terminal-keydown-fit.ts | 1 - .../terminal-paste-operation-order.test.ts | 2 - .../use-terminal-pane-close-actions.ts | 2 +- ...-running-work-probe-child-evidence.test.ts | 16 +- .../terminal/pty-running-work-probe.ts | 6 +- .../running-terminal-close-guard.test.ts | 6 +- .../terminal/running-terminal-close-guard.ts | 2 +- .../window-close-running-work.test.ts | 2 +- .../terminal/window-close-running-work.ts | 2 +- .../agent-hook-completion-notifications.ts | 1 - .../src/lib/agent-draft-paste-content.ts | 20 +- .../src/lib/agent-draft-readiness.test.ts | 8 +- src/renderer/src/lib/agent-draft-readiness.ts | 11 +- .../src/lib/agent-followup-delivery.test.ts | 16 +- .../src/lib/agent-followup-delivery.ts | 18 +- ...agent-paste-draft-readiness-budget.test.ts | 22 +-- .../agent-paste-draft-submit-retry.test.ts | 12 +- .../src/lib/agent-paste-draft.test.ts | 163 ++++------------ src/renderer/src/lib/agent-paste-draft.ts | 66 ++----- src/renderer/src/lib/agent-ready-wait.ts | 2 +- .../codex-session-restart-shell-flap.test.ts | 2 +- .../src/lib/codex-session-restart.test.ts | 8 +- src/renderer/src/lib/codex-session-restart.ts | 12 +- .../lib/launch-agent-background-session.ts | 1 - ...agent-new-tab-host-route-readiness.test.ts | 2 +- src/renderer/src/lib/new-workspace.test.ts | 6 - src/renderer/src/lib/new-workspace.ts | 12 +- .../src/lib/startup-draft-input-kind.test.ts | 1 - .../runtime-terminal-inspection.test.ts | 183 ++++------------- .../runtime/runtime-terminal-inspection.ts | 45 ++--- .../runtime/runtime-terminal-stream.test.ts | 13 +- .../src/runtime/runtime-terminal-stream.ts | 29 ++- .../runtime-terminal-verified-input.test.ts | 35 +++- .../runtime-terminal-verified-input.ts | 18 +- ...abs-agent-completion-notifications.test.ts | 4 - .../e2e/__mocks__/native-chat-ssh-delivery.ts | 7 +- .../e2e/native-chat-ssh-delivery.unit.test.ts | 35 ++-- ...te-agent-completion-authority.unit.test.ts | 7 - 89 files changed, 432 insertions(+), 1122 deletions(-) diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index b377f24e894..cabbe4f13aa 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -60,21 +60,12 @@ 1 src/renderer/src/components/linear-api-key-dialog.tsx 2 src/renderer/src/components/native-chat-resume-modal.test-support.tsx 1 src/renderer/src/components/native-chat-resume-tree.test-support.ts -2 src/renderer/src/components/native-chat/NativeChatComposer.tsx 1 src/renderer/src/components/native-chat/NativeChatResolvedView.tsx 1 src/renderer/src/components/native-chat/NativeChatVisualFrame.tsx -1 src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts 1 src/renderer/src/components/native-chat/native-chat-file-link.ts 1 src/renderer/src/components/native-chat/native-chat-http-link-source-owner.ts -1 src/renderer/src/components/native-chat/native-chat-observed-send.ts -2 src/renderer/src/components/native-chat/native-chat-runtime-image-send.ts 2 src/renderer/src/components/native-chat/native-chat-runtime-owner.ts -6 src/renderer/src/components/native-chat/native-chat-runtime-send.ts 2 src/renderer/src/components/native-chat/native-chat-session-option-discovery.ts -6 src/renderer/src/components/native-chat/use-native-chat-interactive-send.ts -2 src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.ts -4 src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.ts -2 src/renderer/src/components/native-chat/use-native-chat-session-option-command.ts 1 src/renderer/src/components/new-workspace/use-jira-source-connection.ts 2 src/renderer/src/components/new-workspace/use-linear-source-connection.ts 1 src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts @@ -234,9 +225,6 @@ 3 src/renderer/src/components/tab-group/useTabGroupActivationCommands.ts 5 src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts 1 src/renderer/src/components/task-page/TaskPage.tsx -2 src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts -1 src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts -1 src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts 1 src/renderer/src/components/terminal-pane/retire-unbound-runtime-terminal-pane.ts 2 src/renderer/src/components/terminal-pane/terminal-drop-runtime-owner.ts 1 src/renderer/src/components/terminal-pane/terminal-fit-restore.ts @@ -247,15 +235,11 @@ 1 src/renderer/src/components/terminal-pane/terminal-pane-mount-preparation.ts 1 src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts 1 src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts -1 src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts 1 src/renderer/src/components/terminal-pane/use-terminal-pane-controller.ts 1 src/renderer/src/components/terminal-pane/use-terminal-pane-mount-lifecycle.ts 1 src/renderer/src/components/terminal-pane/use-terminal-pane-paste-listeners.ts 1 src/renderer/src/components/terminal-workspace-model.ts -1 src/renderer/src/components/terminal/pty-running-work-probe.ts -1 src/renderer/src/components/terminal/running-terminal-close-guard.ts 1 src/renderer/src/components/terminal/terminal-tab-create.ts -1 src/renderer/src/components/terminal/window-close-running-work.ts 2 src/renderer/src/components/use-task-page-source-availability.ts 2 src/renderer/src/components/use-task-page-store-bindings.ts 2 src/renderer/src/components/use-terminal-activation-actions.ts @@ -301,18 +285,12 @@ 1 src/renderer/src/lib/active-agent-note-send.ts 1 src/renderer/src/lib/active-agent-note-target.ts 1 src/renderer/src/lib/agent-background-session-launch-host.ts -1 src/renderer/src/lib/agent-draft-paste-content.ts -1 src/renderer/src/lib/agent-draft-readiness.ts -2 src/renderer/src/lib/agent-followup-delivery.ts 1 src/renderer/src/lib/agent-hibernation-coordinator-test-fixture.ts 3 src/renderer/src/lib/agent-hibernation-coordinator.ts -5 src/renderer/src/lib/agent-paste-draft.ts -1 src/renderer/src/lib/agent-ready-wait.ts 1 src/renderer/src/lib/automation-session-observer.ts 1 src/renderer/src/lib/client-creation-action-error.ts 3 src/renderer/src/lib/client-creation-action-policy.ts 3 src/renderer/src/lib/codex-pane-selection-lane.ts -2 src/renderer/src/lib/codex-session-restart.ts 1 src/renderer/src/lib/default-creation-host.ts # the sanctioned reader of the default host 1 src/renderer/src/lib/doc-preview-grants.ts 3 src/renderer/src/lib/ensure-hooks-confirmed.ts @@ -324,7 +302,7 @@ 1 src/renderer/src/lib/host-mirrored-pane-liveness.ts 6 src/renderer/src/lib/http-link-destinations.ts 3 src/renderer/src/lib/http-link-routing.ts -2 src/renderer/src/lib/launch-agent-background-session.ts +1 src/renderer/src/lib/launch-agent-background-session.ts 1 src/renderer/src/lib/launch-agent-execution-context.ts 2 src/renderer/src/lib/launch-agent-in-new-tab.ts 1 src/renderer/src/lib/launch-agent-web-host-tab.ts @@ -333,7 +311,6 @@ 2 src/renderer/src/lib/launch-work-item-direct.ts 1 src/renderer/src/lib/launch-worktree-background-terminals.ts 2 src/renderer/src/lib/local-preflight-context.ts -1 src/renderer/src/lib/new-workspace.ts 1 src/renderer/src/lib/open-agent-message-sender.ts 2 src/renderer/src/lib/provider-runtime-context.ts 1 src/renderer/src/lib/remote-workspace-drop-test-fixtures.ts @@ -380,9 +357,6 @@ 5 src/renderer/src/runtime/runtime-linear-issue-mutations.ts 12 src/renderer/src/runtime/runtime-linear-project-client.ts 3 src/renderer/src/runtime/runtime-repo-client.ts -3 src/renderer/src/runtime/runtime-terminal-inspection.ts -1 src/renderer/src/runtime/runtime-terminal-stream.ts -1 src/renderer/src/runtime/runtime-terminal-verified-input.ts 5 src/renderer/src/runtime/use-runtime-session-mirror-environment-key.ts 2 src/renderer/src/runtime/web-runtime-browser-creation.ts 1 src/renderer/src/runtime/web-runtime-initial-terminal-bootstrap-dispatch.ts diff --git a/config/owner-routing-baseline.txt b/config/owner-routing-baseline.txt index 9cb19d92b22..b6a007bd176 100644 --- a/config/owner-routing-baseline.txt +++ b/config/owner-routing-baseline.txt @@ -40,9 +40,6 @@ 1 src/renderer/src/runtime/runtime-jira-target.ts 1 src/renderer/src/runtime/runtime-linear-client.ts 3 src/renderer/src/runtime/runtime-repo-client.ts -3 src/renderer/src/runtime/runtime-terminal-inspection.ts -1 src/renderer/src/runtime/runtime-terminal-stream.ts -1 src/renderer/src/runtime/runtime-terminal-verified-input.ts 1 src/renderer/src/store/folder-workspaces/folder-path-status-actions.ts 2 src/renderer/src/store/folder-workspaces/folder-workspace-catalog-actions.ts 3 src/renderer/src/store/folder-workspaces/folder-workspace-mutations.ts diff --git a/src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx b/src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx index 327397db8b0..6357b604916 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx @@ -54,7 +54,6 @@ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ isRemoteRuntimePtyId: () => false, sendRuntimePtyInput: vi.fn() })) -vi.mock('@/lib/agent-paste-draft', () => ({ getSettingsForAgentTabRuntimeOwner: () => ({}) })) vi.mock('@/lib/native-chat-telemetry', () => ({ emitNativeChatMessageSent: vi.fn(), emitNativeChatPickerItemAccepted: vi.fn(), diff --git a/src/renderer/src/components/native-chat/NativeChatComposer.test.tsx b/src/renderer/src/components/native-chat/NativeChatComposer.test.tsx index 85bfeff5573..cfcb0ea406c 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposer.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposer.test.tsx @@ -72,9 +72,6 @@ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ isRemoteRuntimePtyId: () => false, sendRuntimePtyInput: vi.fn() })) -vi.mock('@/lib/agent-paste-draft', () => ({ - getSettingsForAgentTabRuntimeOwner: () => ({}) -})) vi.mock('./native-chat-runtime-send', () => ({ sendNativeChatMessage: (...args: unknown[]) => mocks.sendNativeChatMessage(...args), sendNativeChatTypedCommand: (...args: unknown[]) => mocks.sendNativeChatTypedCommand(...args), @@ -450,7 +447,6 @@ describe('NativeChatComposer', () => { expect(mocks.sendNativeChatMessageWithImageAttachments).toHaveBeenCalledWith( 'codex', - {}, 'pty-1', 'hello', ['/tmp/pasted.png'], @@ -471,7 +467,7 @@ describe('NativeChatComposer', () => { act(() => mocks.fieldProps?.onSend?.()) - expect(mocks.sendNativeChatTypedCommand).toHaveBeenCalledWith({}, 'pty-1', '/status') + expect(mocks.sendNativeChatTypedCommand).toHaveBeenCalledWith('pty-1', '/status') expect(mocks.sendNativeChatMessage).not.toHaveBeenCalled() }) @@ -488,7 +484,7 @@ describe('NativeChatComposer', () => { act(() => mocks.fieldProps?.onSend?.()) - expect(mocks.sendNativeChatMessage).toHaveBeenCalledWith({}, 'pty-1', '$ref-oss', undefined) + expect(mocks.sendNativeChatMessage).toHaveBeenCalledWith('pty-1', '$ref-oss', undefined) expect(mocks.sendNativeChatTypedCommand).not.toHaveBeenCalled() }) @@ -505,7 +501,7 @@ describe('NativeChatComposer', () => { act(() => mocks.fieldProps?.onSend?.()) - expect(mocks.sendNativeChatMessage).toHaveBeenCalledWith({}, 'pty-1', '/clear', undefined) + expect(mocks.sendNativeChatMessage).toHaveBeenCalledWith('pty-1', '/clear', undefined) expect(mocks.sendNativeChatTypedCommand).not.toHaveBeenCalled() }) @@ -793,7 +789,6 @@ describe('NativeChatComposer', () => { }) expect(mocks.sendNativeChatMessageVerified).toHaveBeenCalledWith( - {}, 'pty-1', '/model opus', expect.any(AbortSignal) @@ -802,7 +797,6 @@ describe('NativeChatComposer', () => { expect(onOptimisticSend).not.toHaveBeenCalled() expect(mocks.createClaudeModelSwitchConfirmationObserver).toHaveBeenCalledWith({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Opus' }) expect(onSwitchToTerminal).not.toHaveBeenCalled() @@ -826,14 +820,12 @@ describe('NativeChatComposer', () => { }) expect(mocks.sendNativeChatMessageVerified).toHaveBeenCalledWith( - {}, 'pty-1', '/model fable', expect.any(AbortSignal) ) expect(mocks.createClaudeModelSwitchConfirmationObserver).toHaveBeenCalledWith({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Fable' }) expect(mocks.confirmationObserver?.arm).toHaveBeenCalledOnce() @@ -866,7 +858,6 @@ describe('NativeChatComposer', () => { }) expect(mocks.typeNativeChatCommand).toHaveBeenCalledWith( - {}, 'pty-1', '/model', expect.any(AbortSignal) diff --git a/src/renderer/src/components/native-chat/NativeChatComposer.tsx b/src/renderer/src/components/native-chat/NativeChatComposer.tsx index 62315c377ec..7bc1dba7d97 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposer.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposer.tsx @@ -3,7 +3,6 @@ import type { NativeChatComposerInput } from './native-chat-composer-input' import { forwardRef, useCallback, useState } from 'react' import { useNativeChatComposerInterrupt } from './use-native-chat-composer-interrupt' import { useNativeChatContextUsageSummary } from './use-native-chat-context-usage-summary' -import { getSettingsForAgentTabRuntimeOwner } from '@/lib/agent-paste-draft' import { useNativeChatMentionFiles } from './use-native-chat-mention-files' import { useNativeChatDraft } from './use-native-chat-draft' import { useNativeChatComposerRecall } from './use-native-chat-composer-recall' @@ -143,14 +142,13 @@ const NativeChatComposerPane = forwardRef { if (!targetPtyId) { return null } - return { ptyId: targetPtyId, settings: getSettingsForAgentTabRuntimeOwner(terminalTabId) } - }, [targetPtyId, terminalTabId]) + return { ptyId: targetPtyId } + }, [targetPtyId]) // Why inputOwnedByCard: the hidden field can still hold keyboard focus for a frame. const [hasPty, disabled] = structuredTransport diff --git a/src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx b/src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx index edca7b507ed..6de3a6d4e28 100644 --- a/src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx @@ -19,8 +19,8 @@ const MESSAGE = 'please also run lint' const retained = vi.hoisted((): { session: NativeChatLiveSession | null } => ({ session: null })) const composer = vi.hoisted(() => ({ mounts: 0, typed: vi.fn<(text: string) => boolean>() })) const pty = vi.hoisted(() => ({ - verified: vi.fn<(settings: unknown, id: string, data: string) => Promise>(), - raw: vi.fn<(settings: unknown, id: string, data: string) => boolean>() + verified: vi.fn<(id: string, data: string) => Promise>(), + raw: vi.fn<(id: string, data: string) => boolean>() })) vi.mock('./use-native-chat-retained-session', () => ({ useNativeChatRetainedSession: () => retained.session @@ -68,7 +68,7 @@ vi.mock('./NativeChatComposer', async () => { ) const send = (): void => { let pendingId: string | undefined - const handle = sendNativeChatMessage(null, props.targetPtyId ?? '', MESSAGE, { + const handle = sendNativeChatMessage(props.targetPtyId ?? '', MESSAGE, { onWriteRejected: () => { if (pendingId) { props.optimisticSendOutcome?.reject(pendingId) @@ -185,7 +185,7 @@ describe('a prompt card hides the composer without unmounting it', () => { await act(async () => { fireEvent.click(screen.getByTestId('composer-send')) }) - expect(pty.verified.mock.calls.map((call) => call[2]).join('')).toContain(MESSAGE) + expect(pty.verified.mock.calls.map((call) => call[1]).join('')).toContain(MESSAGE) // The approval arrives inside the body-to-Enter gap. await act(async () => { @@ -197,9 +197,9 @@ describe('a prompt card hides the composer without unmounting it', () => { }) expect(document.querySelector('[data-native-chat-approval-card="true"]')).not.toBeNull() - expect(pty.verified.mock.calls.map((call) => call[2])).not.toContain('\r') + expect(pty.verified.mock.calls.map((call) => call[1])).not.toContain('\r') // Only the pre-body clear: retiring the send types nothing under the card. - expect(pty.raw.mock.calls.map((call) => call[2])).toEqual(['\x15']) + expect(pty.raw.mock.calls.map((call) => call[1])).toEqual(['\x15']) expect(screen.getByTestId('composer-send').closest('[hidden]')).not.toBeNull() expect(screen.getAllByText(MESSAGE).length).toBeGreaterThan(0) expect(screen.getByText('Message not sent')).toBeInTheDocument() diff --git a/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx b/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx index e5a2f181ac2..cbdf7751baf 100644 --- a/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx +++ b/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx @@ -122,7 +122,7 @@ export function NativeChatResolvedView({ const canSend = useNativeChatCanSend(targetPtyId) // Reuse the verified composer send path for interactive cards and composer // stop (Stop sends ESC, the agent-TUI interrupt key). - const send = useNativeChatInteractiveSend(terminalTabId, paneKey, targetPtyId, agent) + const send = useNativeChatInteractiveSend(paneKey, targetPtyId, agent) // Every send this pane makes brings the latest into view, wherever the reader had scrolled. const { messageListRef, revealLatest } = useNativeChatRevealLatest() const interactiveSend = useNativeChatInteractiveSendReveal(send, targetPtyId, revealLatest) diff --git a/src/renderer/src/components/native-chat/claude-model-switch-confirmation.test.ts b/src/renderer/src/components/native-chat/claude-model-switch-confirmation.test.ts index 72e6c94762d..cfa90f03d20 100644 --- a/src/renderer/src/components/native-chat/claude-model-switch-confirmation.test.ts +++ b/src/renderer/src/components/native-chat/claude-model-switch-confirmation.test.ts @@ -21,7 +21,6 @@ describe('Claude model switch confirmation detection', () => { const unsubscribe = vi.fn(() => {}) const observer = createClaudeModelSwitchConfirmationObserver({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Fable 5', subscribeToData: (watcher) => { dataObserver.current = watcher @@ -47,7 +46,6 @@ describe('Claude model switch confirmation detection', () => { const dataObserver = { current: (_data: string): void => {} } const observer = createClaudeModelSwitchConfirmationObserver({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Opus (1M context)', subscribeToData: (watcher) => { dataObserver.current = watcher @@ -69,7 +67,6 @@ describe('Claude model switch confirmation detection', () => { const dataObserver = { current: (_data: string): void => {} } const observer = createClaudeModelSwitchConfirmationObserver({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Opus (1M context)', subscribeToData: (watcher) => { dataObserver.current = watcher @@ -95,7 +92,6 @@ describe('Claude model switch confirmation detection', () => { const submitConfirmation = vi.fn() const observer = createClaudeModelSwitchConfirmationObserver({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Fable 5', subscribeToData: (watcher) => { dataObserver.current = watcher @@ -121,7 +117,6 @@ describe('Claude model switch confirmation detection', () => { const dataObserver = { current: (_data: string): void => {} } const observer = createClaudeModelSwitchConfirmationObserver({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Haiku', subscribeToData: (watcher) => { dataObserver.current = watcher @@ -141,7 +136,6 @@ describe('Claude model switch confirmation detection', () => { it('reports unknown when the PTY observer cannot be established', async () => { const observer = createClaudeModelSwitchConfirmationObserver({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Fable 5', subscribeToData: () => Promise.reject(new Error('unavailable')), timeoutMs: 100 @@ -158,7 +152,6 @@ describe('Claude model switch confirmation detection', () => { let settled = false const observer = createClaudeModelSwitchConfirmationObserver({ ptyId: 'pty-1', - settings: {}, expectedModelLabel: 'Fable 5', subscribeToData: () => vi.fn(() => {}), timeoutMs: 100 diff --git a/src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts b/src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts index 334e4639add..4784348dc7d 100644 --- a/src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts +++ b/src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts @@ -1,4 +1,3 @@ -import type { GlobalSettings } from '../../../../shared/global-settings-types' import { subscribeToPtyData } from '../terminal-pane/pty-data-sidecar-subscriptions' import { isRemoteRuntimePtyId, sendRuntimePtyInput } from '@/runtime/runtime-terminal-inspection' import { subscribeToRuntimeTerminalData } from '@/runtime/runtime-terminal-stream' @@ -64,7 +63,6 @@ function hasClaudeModelSwitchRejection(buffer: string): boolean { function subscribeToClaudeModelSwitchData(args: { ptyId: string - settings: Pick | null | undefined subscribeToData?: SubscribeToData watcher: (data: string) => void }): Promise<() => void> | (() => void) { @@ -73,7 +71,6 @@ function subscribeToClaudeModelSwitchData(args: { } if (isRemoteRuntimePtyId(args.ptyId)) { return subscribeToRuntimeTerminalData( - args.settings, args.ptyId, `desktop:native-chat-model-switch:${args.ptyId}`, args.watcher, @@ -85,7 +82,6 @@ function subscribeToClaudeModelSwitchData(args: { export function createClaudeModelSwitchConfirmationObserver(args: { ptyId: string - settings: Pick | null | undefined expectedModelLabel: string | null subscribeToData?: SubscribeToData submitConfirmation?: () => boolean | void @@ -147,7 +143,7 @@ export function createClaudeModelSwitchConfirmationObserver(args: { // exact Claude warning defaults to “Yes” and needs only one Enter. const accepted = args.submitConfirmation ? args.submitConfirmation() !== false - : sendRuntimePtyInput(args.settings, args.ptyId, NATIVE_CHAT_SUBMIT, 'driving') + : sendRuntimePtyInput(args.ptyId, NATIVE_CHAT_SUBMIT, 'driving') if (!accepted) { finish('unknown') return @@ -162,7 +158,6 @@ export function createClaudeModelSwitchConfirmationObserver(args: { try { const subscription = subscribeToClaudeModelSwitchData({ ptyId: args.ptyId, - settings: args.settings, subscribeToData: args.subscribeToData, watcher: observeData }) diff --git a/src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx b/src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx index 33a6d6a6095..8e20ac067a9 100644 --- a/src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx +++ b/src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx @@ -33,9 +33,6 @@ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ isRemoteRuntimePtyId: () => false, sendRuntimePtyInput: vi.fn() })) -vi.mock('@/lib/agent-paste-draft', () => ({ - getSettingsForAgentTabRuntimeOwner: () => ({}) -})) vi.mock('./native-chat-runtime-send', () => ({ sendNativeChatMessage: vi.fn(), sendNativeChatTypedCommand: vi.fn(), diff --git a/src/renderer/src/components/native-chat/native-chat-composer-target.ts b/src/renderer/src/components/native-chat/native-chat-composer-target.ts index efa532c88a1..548fed952a6 100644 --- a/src/renderer/src/components/native-chat/native-chat-composer-target.ts +++ b/src/renderer/src/components/native-chat/native-chat-composer-target.ts @@ -1,11 +1,9 @@ import { translate } from '@/i18n/i18n' import { NATIVE_CHAT_TURN_STATUS_COPY } from '../../../../shared/native-chat-turn-status' import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection' -import type { getSettingsForAgentTabRuntimeOwner } from '@/lib/agent-paste-draft' export type NativeChatResolvedTarget = { ptyId: string - settings: ReturnType } /** Upper bound for clipboard text pulled into the composer via Cmd/Ctrl+V, so a diff --git a/src/renderer/src/components/native-chat/native-chat-input-clear.ts b/src/renderer/src/components/native-chat/native-chat-input-clear.ts index 03b6660dd36..3d8cbabe6e1 100644 --- a/src/renderer/src/components/native-chat/native-chat-input-clear.ts +++ b/src/renderer/src/components/native-chat/native-chat-input-clear.ts @@ -1,6 +1,5 @@ // Clearing the agent's unsubmitted input line before a chat send writes its body. import { sendRuntimePtyInput } from '@/runtime/runtime-terminal-inspection' -import type { getSettingsForAgentTabRuntimeOwner } from '@/lib/agent-paste-draft' import { AGENT_TUI_CLEAR_INPUT_MAX } from '../../../../shared/agent-tui-input-clear' // Why: agent TUI composers treat Ctrl+U as kill-to-start-of-line. Chat sends @@ -36,22 +35,11 @@ export type NativeChatSendOptions = { confirmCleared?: () => boolean } -type RuntimeSettings = ReturnType - -export function clearUnsubmittedAgentInput( - settings: RuntimeSettings, - ptyId: string, - options?: NativeChatSendOptions -): void { +export function clearUnsubmittedAgentInput(ptyId: string, options?: NativeChatSendOptions): void { if (options?.clearInput === '') { return } - sendRuntimePtyInput( - settings, - ptyId, - options?.clearInput ?? NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, - 'driving' - ) + sendRuntimePtyInput(ptyId, options?.clearInput ?? NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, 'driving') } /** @@ -63,13 +51,12 @@ export function clearUnsubmittedAgentInput( * the buffer holds, since the user can type into the TUI directly. */ export function clearThenWrite( - settings: RuntimeSettings, ptyId: string, options: NativeChatSendOptions | undefined, delay: (ms: number, fn: () => void) => void, writeBody: () => void ): void { - clearUnsubmittedAgentInput(settings, ptyId, options) + clearUnsubmittedAgentInput(ptyId, options) const confirmCleared = options?.confirmCleared if (!confirmCleared) { writeBody() @@ -83,7 +70,7 @@ export function clearThenWrite( // An unreadable terminal is unconfirmed; the maximal clear remains safe. } if (!cleared) { - sendRuntimePtyInput(settings, ptyId, AGENT_TUI_CLEAR_INPUT_MAX, 'driving') + sendRuntimePtyInput(ptyId, AGENT_TUI_CLEAR_INPUT_MAX, 'driving') } writeBody() }) diff --git a/src/renderer/src/components/native-chat/native-chat-observed-send.test.ts b/src/renderer/src/components/native-chat/native-chat-observed-send.test.ts index dd2836825f4..76563918496 100644 --- a/src/renderer/src/components/native-chat/native-chat-observed-send.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-observed-send.test.ts @@ -22,15 +22,15 @@ afterEach(() => { it('observes a refused write, skips Enter, and releases the queue for the next user action', async () => { const rejected = vi.fn() io.verified.mockResolvedValueOnce(false) - sendNativeChatMessage(null, 'pane', 'refused', { onWriteRejected: rejected }) + sendNativeChatMessage('pane', 'refused', { onWriteRejected: rejected }) await vi.advanceTimersByTimeAsync(1000) expect(rejected).toHaveBeenCalledOnce() - expect(io.verified.mock.calls.map((call) => call[2])).toEqual([ + expect(io.verified.mock.calls.map((call) => call[1])).toEqual([ buildNativeChatPasteBytes('refused') ]) - sendNativeChatMessage(null, 'pane', 'next', { onWriteRejected: rejected }) + sendNativeChatMessage('pane', 'next', { onWriteRejected: rejected }) await vi.advanceTimersByTimeAsync(1000) - expect(io.verified.mock.calls.map((call) => call[2])).toEqual([ + expect(io.verified.mock.calls.map((call) => call[1])).toEqual([ buildNativeChatPasteBytes('refused'), buildNativeChatPasteBytes('next'), NATIVE_CHAT_SUBMIT @@ -41,26 +41,26 @@ it('reports a lost acknowledgment once as unconfirmed, never as rejection, and s const unconfirmed = vi.fn() io.verified.mockRejectedValueOnce(new Error('lost acknowledgment')) io.verified.mockRejectedValueOnce(new Error('lost acknowledgment')) - sendNativeChatMessage(null, 'pane', 'uncertain', { + sendNativeChatMessage('pane', 'uncertain', { onWriteRejected: rejected, onWriteUnconfirmed: unconfirmed }) await vi.advanceTimersByTimeAsync(120000) expect(rejected).not.toHaveBeenCalled() expect(unconfirmed).toHaveBeenCalledOnce() - expect(io.verified.mock.calls.map((call) => call[2])).toEqual([ + expect(io.verified.mock.calls.map((call) => call[1])).toEqual([ buildNativeChatPasteBytes('uncertain'), NATIVE_CHAT_SUBMIT ]) }) it('serializes rapid sends through their acknowledged Enter and preserves the paste delay', async () => { const rejected = vi.fn() - sendNativeChatMessage(null, 'pane', 'one', { onWriteRejected: rejected }) - sendNativeChatMessage(null, 'pane', 'two', { onWriteRejected: rejected }) + sendNativeChatMessage('pane', 'one', { onWriteRejected: rejected }) + sendNativeChatMessage('pane', 'two', { onWriteRejected: rejected }) await vi.advanceTimersByTimeAsync(499) expect(io.verified).toHaveBeenCalledOnce() await vi.advanceTimersByTimeAsync(501) - expect(io.verified.mock.calls.map((call) => call[2])).toEqual([ + expect(io.verified.mock.calls.map((call) => call[1])).toEqual([ buildNativeChatPasteBytes('one'), NATIVE_CHAT_SUBMIT, buildNativeChatPasteBytes('two'), @@ -77,11 +77,11 @@ it.each(['accepted', 'refused', 'unknown'] as const)( } else if (outcome === 'unknown') { io.verified.mockRejectedValueOnce(new Error('lost acknowledgment')) } - sendNativeChatMessage(null, 'pane', 'answer', { onDeliverySettled: settled }) + sendNativeChatMessage('pane', 'answer', { onDeliverySettled: settled }) expect(settled).not.toHaveBeenCalled() await vi.advanceTimersByTimeAsync(1000) expect(settled).toHaveBeenCalledExactlyOnceWith(outcome === 'accepted') - expect(io.verified.mock.calls.map((call) => call[2])).toEqual( + expect(io.verified.mock.calls.map((call) => call[1])).toEqual( outcome === 'refused' ? [buildNativeChatPasteBytes('answer')] : [buildNativeChatPasteBytes('answer'), NATIVE_CHAT_SUBMIT] @@ -97,7 +97,7 @@ it('ignores a pasted answer acknowledgment after cancellation', async () => { }) ) const settled = vi.fn() - const handle = sendNativeChatMessage(null, 'pane', 'answer', { onDeliverySettled: settled }) + const handle = sendNativeChatMessage('pane', 'answer', { onDeliverySettled: settled }) handle.cancel() acknowledge(true) await vi.advanceTimersByTimeAsync(1000) diff --git a/src/renderer/src/components/native-chat/native-chat-observed-send.ts b/src/renderer/src/components/native-chat/native-chat-observed-send.ts index fe9c2b6244f..04692fdb682 100644 --- a/src/renderer/src/components/native-chat/native-chat-observed-send.ts +++ b/src/renderer/src/components/native-chat/native-chat-observed-send.ts @@ -1,5 +1,4 @@ import { sendRuntimePtyInputVerified } from '@/runtime/runtime-terminal-inspection' -import type { getSettingsForAgentTabRuntimeOwner } from '@/lib/agent-paste-draft' import { enqueueNativeChatPtySend } from './native-chat-pty-send-queue' import { clearConfirmDurationMs, @@ -10,7 +9,6 @@ import { /** Observe write refusals without mistaking transport success for provider acceptance. */ export function sendNativeChatObservedWrites( - settings: ReturnType, ptyId: string, writes: readonly { data: string; delayBeforeMs: number }[], options: NativeChatSendOptions & { stopOnUnconfirmed?: boolean; settleDelayMs?: number } @@ -48,7 +46,7 @@ export function sendNativeChatObservedWrites( if (isCancelled()) { return } - void sendRuntimePtyInputVerified(settings, ptyId, write.data, 'driving', writeOptions) + void sendRuntimePtyInputVerified(ptyId, write.data, 'driving', writeOptions) .then((accepted) => { if (isCancelled()) { return @@ -86,8 +84,8 @@ export function sendNativeChatObservedWrites( send() } } - clearThenWrite(settings, ptyId, options, delay, () => writeAt(0)) + clearThenWrite(ptyId, options, delay, () => writeAt(0)) }, - { onCancelUnsubmitted: () => clearUnsubmittedAgentInput(settings, ptyId, options) } + { onCancelUnsubmitted: () => clearUnsubmittedAgentInput(ptyId, options) } ) } diff --git a/src/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsx b/src/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsx index fa1d52d4e01..010906cd11f 100644 --- a/src/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsx +++ b/src/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsx @@ -46,9 +46,6 @@ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ isRemoteRuntimePtyId: () => false, sendRuntimePtyInput: vi.fn() })) -vi.mock('@/lib/agent-paste-draft', () => ({ - getSettingsForAgentTabRuntimeOwner: () => ({}) -})) vi.mock('./native-chat-runtime-send', () => ({ sendNativeChatMessage: vi.fn(), sendNativeChatTypedCommand: vi.fn(), diff --git a/src/renderer/src/components/native-chat/native-chat-runtime-image-send.ts b/src/renderer/src/components/native-chat/native-chat-runtime-image-send.ts index a4e79323317..d18ef1499ee 100644 --- a/src/renderer/src/components/native-chat/native-chat-runtime-image-send.ts +++ b/src/renderer/src/components/native-chat/native-chat-runtime-image-send.ts @@ -2,7 +2,6 @@ import { sendNativeChatObservedWrites } from './native-chat-observed-send' import { agentImagePasteWrites, formatAgentImagePath } from '../../../../shared/agent-image-paste' import type { AgentType } from '../../../../shared/agent-status-types' import { sendRuntimePtyInput } from '@/runtime/runtime-terminal-inspection' -import type { getSettingsForAgentTabRuntimeOwner } from '@/lib/agent-paste-draft' import { NATIVE_CHAT_SUBMIT_DELAY_MS } from '../../../../shared/native-chat-answer-stepping' import { buildNativeChatImagePasteBytes, @@ -22,8 +21,6 @@ import { sendNativeChatMessage, type NativeChatSendHandle } from './native-chat- export const NATIVE_CHAT_IMAGE_ATTACHMENT_SETTLE_MS = 300 -type RuntimeSettings = ReturnType - /** The path text an agent detects as an image. One with spaces or shell characters is escaped as a * terminal image drop is, which agent TUIs undo before checking the file exists. */ export function agentImagePastePath(agent: AgentType, path: string): string { @@ -37,14 +34,13 @@ export function agentImagePastePath(agent: AgentType, path: string): string { export function sendNativeChatMessageWithImageAttachments( agent: AgentType, - settings: RuntimeSettings, ptyId: string, text: string, imagePaths: readonly string[], options?: NativeChatSendOptions ): NativeChatSendHandle { if (imagePaths.length === 0) { - return sendNativeChatMessage(settings, ptyId, text, options) + return sendNativeChatMessage(ptyId, text, options) } const trimmedText = text.trim() if (options?.onWriteRejected) { @@ -60,7 +56,7 @@ export function sendNativeChatMessageWithImageAttachments( }) } writes.push({ data: NATIVE_CHAT_SUBMIT, delayBeforeMs: NATIVE_CHAT_SUBMIT_DELAY_MS }) - return sendNativeChatObservedWrites(settings, ptyId, writes, options) + return sendNativeChatObservedWrites(ptyId, writes, options) } const durationMs = (trimmedText.length > 0 @@ -73,7 +69,7 @@ export function sendNativeChatMessageWithImageAttachments( if (isCancelled()) { return } - clearThenWrite(settings, ptyId, options, delay, () => { + clearThenWrite(ptyId, options, delay, () => { if (isCancelled()) { return } @@ -84,26 +80,26 @@ export function sendNativeChatMessageWithImageAttachments( ), trimmedText.length > 0 )) { - sendRuntimePtyInput(settings, ptyId, payload, 'driving') + sendRuntimePtyInput(ptyId, payload, 'driving') } if (trimmedText.length > 0) { delay(NATIVE_CHAT_IMAGE_ATTACHMENT_SETTLE_MS, () => { - sendRuntimePtyInput(settings, ptyId, buildNativeChatPasteBytes(text), 'driving') + sendRuntimePtyInput(ptyId, buildNativeChatPasteBytes(text), 'driving') delay(NATIVE_CHAT_SUBMIT_DELAY_MS, () => { - sendRuntimePtyInput(settings, ptyId, NATIVE_CHAT_SUBMIT, 'driving') + sendRuntimePtyInput(ptyId, NATIVE_CHAT_SUBMIT, 'driving') markSubmitted() }) }) return } delay(NATIVE_CHAT_SUBMIT_DELAY_MS, () => { - sendRuntimePtyInput(settings, ptyId, NATIVE_CHAT_SUBMIT, 'driving') + sendRuntimePtyInput(ptyId, NATIVE_CHAT_SUBMIT, 'driving') markSubmitted() }) }) }, { - onCancelUnsubmitted: () => clearUnsubmittedAgentInput(settings, ptyId, options) + onCancelUnsubmitted: () => clearUnsubmittedAgentInput(ptyId, options) } ) } diff --git a/src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts b/src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts index a10bb87cd3b..5b3029847f7 100644 --- a/src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts @@ -29,11 +29,10 @@ import { buildAgentTuiClearInputForText } from '../../../../shared/agent-tui-input-clear' -const SETTINGS = {} as Parameters[0] const PTY = 'pty-launch-draft' const DRAFT = 'Linked Linear issue: ABC-123\nhttps://linear.app/x/issue/ABC-123' -const writes = (): string[] => sendRuntimePtyInput.mock.calls.map((call) => call[2] as string) +const writes = (): string[] => sendRuntimePtyInput.mock.calls.map((call) => String(call[1])) beforeEach(() => { vi.useFakeTimers() @@ -49,19 +48,19 @@ afterEach(() => { describe('sendNativeChatMessage with a parked multi-line draft', () => { it('leads with a clear sized to every line of the draft, not one Ctrl+U', () => { const clearInput = buildAgentTuiClearInputForText(DRAFT) - sendNativeChatMessage(SETTINGS, PTY, 'edited text', { clearInput }) + sendNativeChatMessage(PTY, 'edited text', { clearInput }) expect(writes()).toEqual([clearInput, buildNativeChatPasteBytes('edited text')]) expect(clearInput).not.toBe(NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT) }) it('still defaults to a single Ctrl+U when no draft is parked', () => { - sendNativeChatMessage(SETTINGS, PTY, 'plain') + sendNativeChatMessage(PTY, 'plain') expect(writes()[0]).toBe(NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT) }) it('holds the body until the clear is confirmed, then submits after the gap', () => { const clearInput = buildAgentTuiClearInputForText(DRAFT) - sendNativeChatMessage(SETTINGS, PTY, 'edited', { + sendNativeChatMessage(PTY, 'edited', { clearInput, confirmCleared: () => true }) @@ -76,15 +75,15 @@ describe('sendNativeChatMessage with a parked multi-line draft', () => { it('preserves the body-to-Enter gap when the renderer stalls past both nominal deadlines', async () => { vi.useRealTimers() const writeTimes = new Map() - sendRuntimePtyInput.mockImplementation((_settings, _pty, bytes: string) => { + sendRuntimePtyInput.mockImplementation((_pty, bytes: string) => { writeTimes.set(bytes, performance.now()) return true }) - sendNativeChatMessage(SETTINGS, PTY, 'edited', { + sendNativeChatMessage(PTY, 'edited', { clearInput: buildAgentTuiClearInputForText(DRAFT), confirmCleared: () => true }) - sendNativeChatMessage(SETTINGS, PTY, 'queued') + sendNativeChatMessage(PTY, 'queued') const blockedUntil = performance.now() + NATIVE_CHAT_CLEAR_CONFIRM_MS + NATIVE_CHAT_SUBMIT_DELAY_MS + 50 @@ -106,7 +105,7 @@ describe('sendNativeChatMessage with a parked multi-line draft', () => { it('widens to a maximal burst when the draft is still observed on the line', () => { const clearInput = buildAgentTuiClearInputForText(DRAFT) - sendNativeChatMessage(SETTINGS, PTY, 'edited', { + sendNativeChatMessage(PTY, 'edited', { clearInput, confirmCleared: () => false }) @@ -119,7 +118,7 @@ describe('sendNativeChatMessage with a parked multi-line draft', () => { }) it('re-clears before the body, never after it', () => { - sendNativeChatMessage(SETTINGS, PTY, 'edited', { + sendNativeChatMessage(PTY, 'edited', { clearInput: buildAgentTuiClearInputForText(DRAFT), confirmCleared: () => false }) @@ -131,7 +130,7 @@ describe('sendNativeChatMessage with a parked multi-line draft', () => { }) it('charges the confirm gap to the handle so the send card outlives the Enter', () => { - const withConfirm = sendNativeChatMessage(SETTINGS, PTY, 'a', { + const withConfirm = sendNativeChatMessage(PTY, 'a', { clearInput: '\x15', confirmCleared: () => true }) @@ -142,11 +141,11 @@ describe('sendNativeChatMessage with a parked multi-line draft', () => { it('submits before a queued send starts after clear confirmation', async () => { const clearInput = buildAgentTuiClearInputForText(DRAFT) - sendNativeChatMessage(SETTINGS, PTY, 'first', { + sendNativeChatMessage(PTY, 'first', { clearInput, confirmCleared: () => true }) - sendNativeChatMessage(SETTINGS, PTY, 'second') + sendNativeChatMessage(PTY, 'second') await vi.advanceTimersByTimeAsync(NATIVE_CHAT_CLEAR_CONFIRM_MS + NATIVE_CHAT_SUBMIT_DELAY_MS) @@ -163,7 +162,7 @@ describe('sendNativeChatMessage with a parked multi-line draft', () => { describe('image sends with a parked multi-line draft', () => { it('clears every draft line before pasting, so no line rides along with the image', () => { const clearInput = buildAgentTuiClearInputForText(DRAFT) - sendNativeChatMessageWithImageAttachments('claude', SETTINGS, PTY, 'caption', ['/tmp/a.png'], { + sendNativeChatMessageWithImageAttachments('claude', PTY, 'caption', ['/tmp/a.png'], { clearInput }) expect(writes()[0]).toBe(clearInput) @@ -171,7 +170,7 @@ describe('image sends with a parked multi-line draft', () => { it('clears exactly once — a second Ctrl+U would wipe the just-pasted image', () => { const clearInput = buildAgentTuiClearInputForText(DRAFT) - sendNativeChatMessageWithImageAttachments('claude', SETTINGS, PTY, 'caption', ['/tmp/a.png'], { + sendNativeChatMessageWithImageAttachments('claude', PTY, 'caption', ['/tmp/a.png'], { clearInput }) vi.advanceTimersByTime(10_000) @@ -180,11 +179,11 @@ describe('image sends with a parked multi-line draft', () => { it('submits the image send before a queued message starts', async () => { const clearInput = buildAgentTuiClearInputForText(DRAFT) - sendNativeChatMessageWithImageAttachments('claude', SETTINGS, PTY, 'caption', ['/tmp/a.png'], { + sendNativeChatMessageWithImageAttachments('claude', PTY, 'caption', ['/tmp/a.png'], { clearInput, confirmCleared: () => true }) - sendNativeChatMessage(SETTINGS, PTY, 'second') + sendNativeChatMessage(PTY, 'second') await vi.advanceTimersByTimeAsync( NATIVE_CHAT_CLEAR_CONFIRM_MS + diff --git a/src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts b/src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts index dd55ba3be2b..34f6872936a 100644 --- a/src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts @@ -28,11 +28,10 @@ import { } from './native-chat-runtime-image-send' import { buildNativeChatPasteBytes, NATIVE_CHAT_SUBMIT } from './native-chat-send' -const SETTINGS = {} as Parameters[0] const PTY = 'pty-1' function expectWriteOrder(calls: unknown[][], expected: string[]): void { - expect(calls.map((call) => call[2])).toEqual(expected) + expect(calls.map((call) => call[1])).toEqual(expected) } describe('sendNativeChatMessage', () => { @@ -48,7 +47,7 @@ describe('sendNativeChatMessage', () => { }) it('clears the TUI line, then writes the framed body, before the Enter', () => { - const handle = sendNativeChatMessage(SETTINGS, PTY, 'hello world') + const handle = sendNativeChatMessage(PTY, 'hello world') expectWriteOrder(sendRuntimePtyInput.mock.calls, [ NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, buildNativeChatPasteBytes('hello world') @@ -57,7 +56,7 @@ describe('sendNativeChatMessage', () => { }) it('does not fire Enter before the proven 500ms gap (busy-agent safety)', () => { - sendNativeChatMessage(SETTINGS, PTY, 'hi') + sendNativeChatMessage(PTY, 'hi') // A short gap would fire Enter while a busy Codex has not yet landed the // paste, submitting an empty box — so nothing must happen before 500ms. vi.advanceTimersByTime(NATIVE_CHAT_SUBMIT_DELAY_MS - 1) @@ -65,7 +64,7 @@ describe('sendNativeChatMessage', () => { }) it('writes the bare carriage-return Enter as a separate delayed write', () => { - sendNativeChatMessage(SETTINGS, PTY, 'hi') + sendNativeChatMessage(PTY, 'hi') vi.advanceTimersByTime(NATIVE_CHAT_SUBMIT_DELAY_MS) expectWriteOrder(sendRuntimePtyInput.mock.calls, [ NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, @@ -75,7 +74,7 @@ describe('sendNativeChatMessage', () => { }) it('cancels the delayed Enter and re-clears an unsubmitted body', () => { - const handle = sendNativeChatMessage(SETTINGS, PTY, 'hi') + const handle = sendNativeChatMessage(PTY, 'hi') handle.cancel() vi.advanceTimersByTime(NATIVE_CHAT_SUBMIT_DELAY_MS) @@ -88,15 +87,15 @@ describe('sendNativeChatMessage', () => { }) it('clears leftover unsubmitted body on cancel so the next send cannot glue', async () => { - const handle = sendNativeChatMessage(SETTINGS, PTY, 'tell me a joke') + const handle = sendNativeChatMessage(PTY, 'tell me a joke') handle.cancel() - sendNativeChatMessage(SETTINGS, PTY, 'continue') + sendNativeChatMessage(PTY, 'continue') // Queue release after cancel is promise-chained; flush so the next body runs. await Promise.resolve() await Promise.resolve() - expect(sendRuntimePtyInput.mock.calls.map((call) => call[2])).toEqual([ + expect(sendRuntimePtyInput.mock.calls.map((call) => call[1])).toEqual([ NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, buildNativeChatPasteBytes('tell me a joke'), NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, // cancel cleanup @@ -106,7 +105,7 @@ describe('sendNativeChatMessage', () => { }) it('does not clear the TUI input when cancel runs after Enter already fired', () => { - const handle = sendNativeChatMessage(SETTINGS, PTY, 'already submitted') + const handle = sendNativeChatMessage(PTY, 'already submitted') vi.advanceTimersByTime(NATIVE_CHAT_SUBMIT_DELAY_MS) sendRuntimePtyInput.mockClear() handle.cancel() @@ -115,8 +114,8 @@ describe('sendNativeChatMessage', () => { }) it('serializes rapid sends on the same PTY so bodies cannot glue before Enter', async () => { - sendNativeChatMessage(SETTINGS, PTY, 'tell me a joke') - sendNativeChatMessage(SETTINGS, PTY, 'continue') + sendNativeChatMessage(PTY, 'tell me a joke') + sendNativeChatMessage(PTY, 'continue') // First clear+body are immediate; second sequence waits for the first Enter. expectWriteOrder(sendRuntimePtyInput.mock.calls, [ @@ -134,19 +133,14 @@ describe('sendNativeChatMessage', () => { ]) await vi.advanceTimersByTimeAsync(NATIVE_CHAT_SUBMIT_DELAY_MS) - expect(sendRuntimePtyInput).toHaveBeenLastCalledWith( - SETTINGS, - PTY, - NATIVE_CHAT_SUBMIT, - 'driving' - ) + expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(PTY, NATIVE_CHAT_SUBMIT, 'driving') expect(sendRuntimePtyInput).toHaveBeenCalledTimes(6) }) it('does not let a canceled queued send stall the sends behind it', async () => { - sendNativeChatMessage(SETTINGS, PTY, 'first') - const canceled = sendNativeChatMessage(SETTINGS, PTY, 'canceled') - sendNativeChatMessage(SETTINGS, PTY, 'third') + sendNativeChatMessage(PTY, 'first') + const canceled = sendNativeChatMessage(PTY, 'canceled') + sendNativeChatMessage(PTY, 'third') canceled.cancel() await vi.advanceTimersByTimeAsync(NATIVE_CHAT_SUBMIT_DELAY_MS) @@ -161,16 +155,16 @@ describe('sendNativeChatMessage', () => { }) it('does not serialize sends across different PTYs', () => { - sendNativeChatMessage(SETTINGS, 'pty-a', 'one') - sendNativeChatMessage(SETTINGS, 'pty-b', 'two') + sendNativeChatMessage('pty-a', 'one') + sendNativeChatMessage('pty-b', 'two') expectWriteOrder(sendRuntimePtyInput.mock.calls, [ NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, buildNativeChatPasteBytes('one'), NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, buildNativeChatPasteBytes('two') ]) - expect(sendRuntimePtyInput.mock.calls[1]?.[1]).toBe('pty-a') - expect(sendRuntimePtyInput.mock.calls[3]?.[1]).toBe('pty-b') + expect(sendRuntimePtyInput.mock.calls[1]?.[0]).toBe('pty-a') + expect(sendRuntimePtyInput.mock.calls[3]?.[0]).toBe('pty-b') }) }) @@ -188,12 +182,11 @@ describe('sendNativeChatMessageVerified', () => { it('awaits body acceptance before the delayed Enter write (no pre-clear)', async () => { // Why: model-switch confirmation watches the PTY while this send runs; // verified option commands must not inject Ctrl+U noise. - const result = sendNativeChatMessageVerified(SETTINGS, PTY, '/model sonnet') + const result = sendNativeChatMessageVerified(PTY, '/model sonnet') await vi.waitFor(() => { expect(sendRuntimePtyInputVerified).toHaveBeenCalledTimes(1) }) expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith( - SETTINGS, PTY, buildNativeChatPasteBytes('/model sonnet'), 'driving' @@ -202,15 +195,10 @@ describe('sendNativeChatMessageVerified', () => { await vi.advanceTimersByTimeAsync(NATIVE_CHAT_SUBMIT_DELAY_MS) expect(await result).toBe(true) - expect(sendRuntimePtyInputVerified).toHaveBeenLastCalledWith( - SETTINGS, - PTY, - NATIVE_CHAT_SUBMIT, - 'driving' - ) + expect(sendRuntimePtyInputVerified).toHaveBeenLastCalledWith(PTY, NATIVE_CHAT_SUBMIT, 'driving') expect( sendRuntimePtyInputVerified.mock.calls.some( - (call) => call[2] === NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT + (call) => call[1] === NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT ) ).toBe(false) }) @@ -218,40 +206,35 @@ describe('sendNativeChatMessageVerified', () => { it('does not send Enter when the body is rejected', async () => { sendRuntimePtyInputVerified.mockResolvedValueOnce(false) - await expect(sendNativeChatMessageVerified(SETTINGS, PTY, '/model sonnet')).resolves.toBe(false) + await expect(sendNativeChatMessageVerified(PTY, '/model sonnet')).resolves.toBe(false) await vi.runAllTimersAsync() expect(sendRuntimePtyInputVerified).toHaveBeenCalledTimes(1) expect( - sendRuntimePtyInputVerified.mock.calls.some((call) => call[2] === NATIVE_CHAT_SUBMIT) + sendRuntimePtyInputVerified.mock.calls.some((call) => call[1] === NATIVE_CHAT_SUBMIT) ).toBe(false) }) it('cancels an in-flight chat Enter before delivering a verified option command', async () => { - sendNativeChatMessage(SETTINGS, PTY, 'hello') + sendNativeChatMessage(PTY, 'hello') expect(sendRuntimePtyInput).toHaveBeenCalled() - const result = sendNativeChatMessageVerified(SETTINGS, PTY, '/model haiku') + const result = sendNativeChatMessageVerified(PTY, '/model haiku') // Chat cancel may Ctrl+U the unsubmitted body; Enter from chat must not fire. await Promise.resolve() await Promise.resolve() await vi.advanceTimersByTimeAsync(NATIVE_CHAT_SUBMIT_DELAY_MS) expect(await result).toBe(true) - const submits = sendRuntimePtyInput.mock.calls.filter((call) => call[2] === NATIVE_CHAT_SUBMIT) + const submits = sendRuntimePtyInput.mock.calls.filter((call) => call[1] === NATIVE_CHAT_SUBMIT) // Only the verified path's Enter — chat's delayed Enter was cancelled. expect(submits).toHaveLength(0) - expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith( - SETTINGS, - PTY, - NATIVE_CHAT_SUBMIT, - 'driving' - ) + expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(PTY, NATIVE_CHAT_SUBMIT, 'driving') }) it('returns false when the delayed Enter wait is aborted', async () => { const controller = new AbortController() - const result = sendNativeChatMessageVerified(SETTINGS, PTY, '/model sonnet', controller.signal) + const result = sendNativeChatMessageVerified(PTY, '/model sonnet', controller.signal) await vi.waitFor(() => { expect(sendRuntimePtyInputVerified).toHaveBeenCalledTimes(1) }) @@ -259,7 +242,7 @@ describe('sendNativeChatMessageVerified', () => { expect(await result).toBe(false) expect( - sendRuntimePtyInputVerified.mock.calls.some((call) => call[2] === NATIVE_CHAT_SUBMIT) + sendRuntimePtyInputVerified.mock.calls.some((call) => call[1] === NATIVE_CHAT_SUBMIT) ).toBe(false) }) }) @@ -277,7 +260,7 @@ describe('typeNativeChatCommand', () => { }) it('writes the Codex picker command as keys instead of one pasted text write', async () => { - const result = typeNativeChatCommand(SETTINGS, PTY, '/model') + const result = typeNativeChatCommand(PTY, '/model') await vi.runAllTimersAsync() await expect(result).resolves.toBe(true) @@ -294,7 +277,7 @@ describe('typeNativeChatCommand', () => { }) it('queues composer commands as the same paced key sequence', async () => { - const handle = sendNativeChatTypedCommand(SETTINGS, PTY, '/status') + const handle = sendNativeChatTypedCommand(PTY, '/status') await vi.runAllTimersAsync() await handle.settled @@ -312,9 +295,9 @@ describe('typeNativeChatCommand', () => { }) it('does not clear into the next queued send after cancellation', async () => { - const command = sendNativeChatTypedCommand(SETTINGS, PTY, '/status') + const command = sendNativeChatTypedCommand(PTY, '/status') command.cancel() - const next = sendNativeChatMessage(SETTINGS, PTY, 'next') + const next = sendNativeChatMessage(PTY, 'next') await vi.runAllTimersAsync() await Promise.all([command.settled, next.settled]) @@ -339,7 +322,7 @@ describe('sendNativeChatMessageWithImageAttachments', () => { }) it('escapes a pasted image path with spaces, so an attachment agent sees one path', () => { - sendNativeChatMessageWithImageAttachments('codex', SETTINGS, PTY, '', [ + sendNativeChatMessageWithImageAttachments('codex', PTY, '', [ '/Users/me/Library/Application Support/orca/native-chat-pastes/orca-paste-1-ab.png' ]) expectWriteOrder(sendRuntimePtyInput.mock.calls, [ @@ -349,10 +332,7 @@ describe('sendNativeChatMessageWithImageAttachments', () => { }) it.each(['', 'describe'])('separates every OMP image reference before %j', (text) => { - sendNativeChatMessageWithImageAttachments('omp', SETTINGS, PTY, text, [ - '/tmp/a.png', - '/tmp/b.png' - ]) + sendNativeChatMessageWithImageAttachments('omp', PTY, text, ['/tmp/a.png', '/tmp/b.png']) expectWriteOrder(sendRuntimePtyInput.mock.calls, [ NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, '\x1b[200~@/tmp/a.png\x1b[201~ ', @@ -361,7 +341,7 @@ describe('sendNativeChatMessageWithImageAttachments', () => { }) it('sends OMP image paths as framed references, preserving spaces and delayed submit', () => { - sendNativeChatMessageWithImageAttachments('omp', SETTINGS, PTY, 'describe', [ + sendNativeChatMessageWithImageAttachments('omp', PTY, 'describe', [ 'C:\\Images\\screen shot.png' ]) expectWriteOrder(sendRuntimePtyInput.mock.calls, [ @@ -369,24 +349,15 @@ describe('sendNativeChatMessageWithImageAttachments', () => { '\x1b[200~@"C:\\Images\\screen shot.png"\x1b[201~ ' ]) vi.advanceTimersByTime(NATIVE_CHAT_IMAGE_ATTACHMENT_SETTLE_MS) - expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(SETTINGS, PTY, 'describe', 'driving') + expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(PTY, 'describe', 'driving') vi.advanceTimersByTime(NATIVE_CHAT_SUBMIT_DELAY_MS) - expect(sendRuntimePtyInput).toHaveBeenLastCalledWith( - SETTINGS, - PTY, - NATIVE_CHAT_SUBMIT, - 'driving' - ) + expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(PTY, NATIVE_CHAT_SUBMIT, 'driving') }) it('clears the line, then bracket-pastes image paths before prompt text', () => { - const handle = sendNativeChatMessageWithImageAttachments( - 'claude', - SETTINGS, - PTY, - 'what do you see?', - ['/tmp/orca-paste-image.png'] - ) + const handle = sendNativeChatMessageWithImageAttachments('claude', PTY, 'what do you see?', [ + '/tmp/orca-paste-image.png' + ]) expect(handle.settleAfterMs).toBe( NATIVE_CHAT_IMAGE_ATTACHMENT_SETTLE_MS + NATIVE_CHAT_SUBMIT_DELAY_MS @@ -399,25 +370,15 @@ describe('sendNativeChatMessageWithImageAttachments', () => { ]) vi.advanceTimersByTime(NATIVE_CHAT_IMAGE_ATTACHMENT_SETTLE_MS) - expect(sendRuntimePtyInput).toHaveBeenLastCalledWith( - SETTINGS, - PTY, - 'what do you see?', - 'driving' - ) + expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(PTY, 'what do you see?', 'driving') vi.advanceTimersByTime(NATIVE_CHAT_SUBMIT_DELAY_MS) - expect(sendRuntimePtyInput).toHaveBeenLastCalledWith( - SETTINGS, - PTY, - NATIVE_CHAT_SUBMIT, - 'driving' - ) + expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(PTY, NATIVE_CHAT_SUBMIT, 'driving') expect(sendRuntimePtyInput).toHaveBeenCalledTimes(4) }) it('does not append a trailing separator on an attachment-only send', () => { - const handle = sendNativeChatMessageWithImageAttachments('claude', SETTINGS, PTY, '', [ + const handle = sendNativeChatMessageWithImageAttachments('claude', PTY, '', [ '/tmp/orca-paste-image.png' ]) @@ -433,18 +394,11 @@ describe('sendNativeChatMessageWithImageAttachments', () => { vi.advanceTimersByTime(1) expect(sendRuntimePtyInput).toHaveBeenCalledTimes(3) - expect(sendRuntimePtyInput).toHaveBeenLastCalledWith( - SETTINGS, - PTY, - NATIVE_CHAT_SUBMIT, - 'driving' - ) + expect(sendRuntimePtyInput).toHaveBeenLastCalledWith(PTY, NATIVE_CHAT_SUBMIT, 'driving') }) it('treats whitespace-only prompt input as attachment-only', () => { - sendNativeChatMessageWithImageAttachments('claude', SETTINGS, PTY, ' ', [ - '/tmp/orca-paste-image.png' - ]) + sendNativeChatMessageWithImageAttachments('claude', PTY, ' ', ['/tmp/orca-paste-image.png']) expectWriteOrder(sendRuntimePtyInput.mock.calls, [ NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, @@ -453,10 +407,7 @@ describe('sendNativeChatMessageWithImageAttachments', () => { }) it('keeps multiple image frames bare and separates only the final frame from prompt text', () => { - sendNativeChatMessageWithImageAttachments('claude', SETTINGS, PTY, 'hello', [ - '/tmp/a.png', - '/tmp/b.png' - ]) + sendNativeChatMessageWithImageAttachments('claude', PTY, 'hello', ['/tmp/a.png', '/tmp/b.png']) expectWriteOrder(sendRuntimePtyInput.mock.calls, [ NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT, @@ -466,7 +417,7 @@ describe('sendNativeChatMessageWithImageAttachments', () => { }) it('cancels deferred prompt and Enter writes after the attachment path', () => { - const handle = sendNativeChatMessageWithImageAttachments('claude', SETTINGS, PTY, 'describe', [ + const handle = sendNativeChatMessageWithImageAttachments('claude', PTY, 'describe', [ '/tmp/orca-paste-image.png' ]) handle.cancel() @@ -478,7 +429,7 @@ describe('sendNativeChatMessageWithImageAttachments', () => { '\x1b[200~/tmp/orca-paste-image.png\x1b[201~ ', NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT ]) - expect(sendRuntimePtyInput.mock.calls.some((call) => call[2] === NATIVE_CHAT_SUBMIT)).toBe( + expect(sendRuntimePtyInput.mock.calls.some((call) => call[1] === NATIVE_CHAT_SUBMIT)).toBe( false ) }) @@ -490,9 +441,9 @@ describe('empty prompt submit', () => { }) it('submits an empty prompt with a bare Enter', () => { - submitNativeChatPrompt(SETTINGS, PTY) + submitNativeChatPrompt(PTY) expect(sendRuntimePtyInput).toHaveBeenCalledOnce() - expect(sendRuntimePtyInput).toHaveBeenCalledWith(SETTINGS, PTY, NATIVE_CHAT_SUBMIT, 'driving') + expect(sendRuntimePtyInput).toHaveBeenCalledWith(PTY, NATIVE_CHAT_SUBMIT, 'driving') }) }) @@ -510,14 +461,14 @@ describe('sendNativeChatAskAnswer', () => { }) it('returns a no-op handle for an empty key group list', () => { - const handle = sendNativeChatAskAnswer(SETTINGS, PTY, []) + const handle = sendNativeChatAskAnswer(PTY, []) expect(handle.settleAfterMs).toBe(0) handle.cancel() expect(sendRuntimePtyInput).not.toHaveBeenCalled() }) it('paces key groups so selector steps render before the next write', async () => { - const handle = sendNativeChatAskAnswer(SETTINGS, PTY, [ + const handle = sendNativeChatAskAnswer(PTY, [ { raw: '1' }, { raw: '2' }, { text: 'custom answer' } @@ -527,26 +478,13 @@ describe('sendNativeChatAskAnswer', () => { ) await vi.advanceTimersByTimeAsync(0) - expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith( - SETTINGS, - PTY, - '1', - 'driving', - undefined - ) + expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(PTY, '1', 'driving', undefined) await vi.advanceTimersByTimeAsync(NATIVE_CHAT_QUESTION_STEP_MS) - expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith( - SETTINGS, - PTY, - '2', - 'driving', - undefined - ) + expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(PTY, '2', 'driving', undefined) await vi.advanceTimersByTimeAsync(NATIVE_CHAT_QUESTION_STEP_MS) expect(sendRuntimePtyInputVerified).toHaveBeenLastCalledWith( - SETTINGS, PTY, buildNativeChatPasteBytes('custom answer'), 'driving', @@ -555,7 +493,7 @@ describe('sendNativeChatAskAnswer', () => { }) it('cancels remaining key group timers', async () => { - const handle = sendNativeChatAskAnswer(SETTINGS, PTY, [{ raw: '1' }, { raw: '2' }]) + const handle = sendNativeChatAskAnswer(PTY, [{ raw: '1' }, { raw: '2' }]) await vi.advanceTimersByTimeAsync(0) expect(sendRuntimePtyInputVerified).toHaveBeenCalledTimes(1) handle.cancel() @@ -566,14 +504,14 @@ describe('sendNativeChatAskAnswer', () => { it('reports verified delivery only after settling and suppresses it after cancellation', async () => { const onSettled = vi.fn() sendRuntimePtyInputVerified.mockResolvedValueOnce(true).mockResolvedValueOnce(false) - const handle = sendNativeChatAskAnswer(SETTINGS, PTY, [{ raw: '1' }, { raw: '\r' }], onSettled) + const handle = sendNativeChatAskAnswer(PTY, [{ raw: '1' }, { raw: '\r' }], onSettled) await vi.advanceTimersByTimeAsync(handle.settleAfterMs) expect(onSettled).toHaveBeenCalledExactlyOnceWith(false) expect(sendRuntimePtyInput).not.toHaveBeenCalled() const canceledSettled = vi.fn() - const canceled = sendNativeChatAskAnswer(SETTINGS, PTY, [{ raw: '1' }], canceledSettled) + const canceled = sendNativeChatAskAnswer(PTY, [{ raw: '1' }], canceledSettled) canceled.cancel() await vi.runAllTimersAsync() expect(canceledSettled).not.toHaveBeenCalled() @@ -588,10 +526,10 @@ describe('sendNativeChatAskAnswer', () => { }) ) - const handle = sendNativeChatAskAnswer(SETTINGS, PTY, [{ raw: '2' }], onSettled) + const handle = sendNativeChatAskAnswer(PTY, [{ raw: '2' }], onSettled) await vi.advanceTimersByTimeAsync(handle.settleAfterMs) - expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(SETTINGS, PTY, '2', 'driving', { + expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(PTY, '2', 'driving', { requireWriteSettlement: true }) expect(onSettled).not.toHaveBeenCalled() diff --git a/src/renderer/src/components/native-chat/native-chat-runtime-send.ts b/src/renderer/src/components/native-chat/native-chat-runtime-send.ts index 948485bc802..f210c30b35d 100644 --- a/src/renderer/src/components/native-chat/native-chat-runtime-send.ts +++ b/src/renderer/src/components/native-chat/native-chat-runtime-send.ts @@ -7,7 +7,6 @@ import { sendRuntimePtyInput, sendRuntimePtyInputVerified } from '@/runtime/runtime-terminal-inspection' -import type { getSettingsForAgentTabRuntimeOwner } from '@/lib/agent-paste-draft' import type { AskAnswerKeyGroup } from './native-chat-interactive-prompt' import { clearConfirmDurationMs, @@ -45,8 +44,6 @@ export type NativeChatSendHandle = { settled?: Promise } -type RuntimeSettings = ReturnType - /** * Chat message path: * 1. clear any unsubmitted TUI line @@ -56,14 +53,12 @@ type RuntimeSettings = ReturnType * Serialized per PTY so rapid sends cannot glue before Enter. */ export function sendNativeChatMessage( - settings: RuntimeSettings, ptyId: string, text: string, options?: NativeChatSendOptions ): NativeChatSendHandle { if (options?.onWriteRejected || options?.onDeliverySettled) { return sendNativeChatObservedWrites( - settings, ptyId, [ { data: buildNativeChatPasteBytes(text), delayBeforeMs: 0 }, @@ -79,21 +74,21 @@ export function sendNativeChatMessage( if (isCancelled()) { return } - clearThenWrite(settings, ptyId, options, delay, () => { + clearThenWrite(ptyId, options, delay, () => { if (isCancelled()) { return } - sendRuntimePtyInput(settings, ptyId, buildNativeChatPasteBytes(text), 'driving') + sendRuntimePtyInput(ptyId, buildNativeChatPasteBytes(text), 'driving') // Schedule from the actual body write: an overdue clear-confirm callback // must not collapse the required body-to-Enter gap after a renderer stall. delay(NATIVE_CHAT_SUBMIT_DELAY_MS, () => { - sendRuntimePtyInput(settings, ptyId, NATIVE_CHAT_SUBMIT, 'driving') + sendRuntimePtyInput(ptyId, NATIVE_CHAT_SUBMIT, 'driving') markSubmitted() }) }) }, { - onCancelUnsubmitted: () => clearUnsubmittedAgentInput(settings, ptyId, options) + onCancelUnsubmitted: () => clearUnsubmittedAgentInput(ptyId, options) } ) } @@ -127,7 +122,6 @@ function waitForNativeChatSubmit(signal?: AbortSignal): Promise { * chat Enter cannot dismiss Claude's "Switch model?" dialog. */ export async function sendNativeChatMessageVerified( - settings: RuntimeSettings, ptyId: string, text: string, signal?: AbortSignal @@ -144,7 +138,6 @@ export async function sendNativeChatMessageVerified( // Why: option commands await remote/SSH acceptance so the Enter cannot race // ahead of the body while a model-change observer is already armed. const bodyAccepted = await sendRuntimePtyInputVerified( - settings, ptyId, buildNativeChatPasteBytes(text), 'driving' @@ -152,12 +145,11 @@ export async function sendNativeChatMessageVerified( if (!bodyAccepted || signal?.aborted || !(await waitForNativeChatSubmit(signal))) { return false } - return sendRuntimePtyInputVerified(settings, ptyId, NATIVE_CHAT_SUBMIT, 'driving') + return sendRuntimePtyInputVerified(ptyId, NATIVE_CHAT_SUBMIT, 'driving') } /** Types a slash command as individual keys so Codex opens its command palette. */ export async function typeNativeChatCommand( - settings: RuntimeSettings, ptyId: string, command: string, signal?: AbortSignal @@ -168,17 +160,13 @@ export async function typeNativeChatCommand( command, signal, write: async (key) => - (await sendRuntimePtyInputVerified(settings, ptyId, key, 'driving')) ? 'accepted' : 'rejected' + (await sendRuntimePtyInputVerified(ptyId, key, 'driving')) ? 'accepted' : 'rejected' }) return outcome === 'accepted' } /** Queues a typed slash command with composer sends on the same PTY. */ -export function sendNativeChatTypedCommand( - settings: RuntimeSettings, - ptyId: string, - command: string -): NativeChatSendHandle { +export function sendNativeChatTypedCommand(ptyId: string, command: string): NativeChatSendHandle { const controller = new AbortController() return enqueueNativeChatPtySend( ptyId, @@ -186,7 +174,7 @@ export function sendNativeChatTypedCommand( ({ isCancelled, markSubmitted }) => { const finish = (outcome: 'accepted' | 'rejected' | 'unknown'): void => { if (!isCancelled() && outcome !== 'accepted') { - clearUnsubmittedAgentInput(settings, ptyId) + clearUnsubmittedAgentInput(ptyId) } markSubmitted() } @@ -197,7 +185,7 @@ export function sendNativeChatTypedCommand( if (isCancelled()) { return 'rejected' } - return (await sendRuntimePtyInputVerified(settings, ptyId, key, 'driving')) + return (await sendRuntimePtyInputVerified(ptyId, key, 'driving')) ? 'accepted' : 'rejected' } @@ -206,7 +194,7 @@ export function sendNativeChatTypedCommand( { onCancelUnsubmitted: () => { controller.abort() - clearUnsubmittedAgentInput(settings, ptyId) + clearUnsubmittedAgentInput(ptyId) } } ) @@ -214,8 +202,8 @@ export function sendNativeChatTypedCommand( /** Submit a TUI prompt with no body (Enter only) — e.g. a plain submit when the * composer is empty. */ -export function submitNativeChatPrompt(settings: RuntimeSettings, ptyId: string): void { - sendRuntimePtyInput(settings, ptyId, NATIVE_CHAT_SUBMIT, 'driving') +export function submitNativeChatPrompt(ptyId: string): void { + sendRuntimePtyInput(ptyId, NATIVE_CHAT_SUBMIT, 'driving') } /** @@ -224,7 +212,6 @@ export function submitNativeChatPrompt(settings: RuntimeSettings, ptyId: string) * step so the arrow-navigate selector applies each before the next. */ export function sendNativeChatAskAnswer( - settings: RuntimeSettings, ptyId: string, groups: AskAnswerKeyGroup[], onSettled?: (delivered: boolean) => void @@ -233,7 +220,6 @@ export function sendNativeChatAskAnswer( return { cancel: () => {}, settleAfterMs: 0 } } return sendNativeChatObservedWrites( - settings, ptyId, groups.map((group, index) => ({ data: 'raw' in group ? group.raw : buildNativeChatPasteBytes(group.text), diff --git a/src/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsx b/src/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsx index 78d62e8f2e4..88369c5157e 100644 --- a/src/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsx +++ b/src/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsx @@ -38,9 +38,6 @@ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ isRemoteRuntimePtyId: () => false, sendRuntimePtyInput: vi.fn() })) -vi.mock('@/lib/agent-paste-draft', () => ({ - getSettingsForAgentTabRuntimeOwner: () => ({}) -})) vi.mock('./native-chat-runtime-send', () => ({ sendNativeChatMessage: vi.fn(), sendNativeChatTypedCommand: vi.fn(), diff --git a/src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx index ddf55907789..2eb6c182a10 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx @@ -26,8 +26,7 @@ type AttachmentApi = ReturnType type ProbeApi = AttachmentApi & { adoptDraft: (draft: string) => void } const target: NativeChatResolvedTarget = { - ptyId: 'pty-1', - settings: { activeRuntimeEnvironmentId: null } + ptyId: 'pty-1' } function Probe({ diff --git a/src/renderer/src/components/native-chat/use-native-chat-composer-interrupt.ts b/src/renderer/src/components/native-chat/use-native-chat-composer-interrupt.ts index fbba5699834..3aa0f77f6b8 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-composer-interrupt.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-composer-interrupt.ts @@ -30,7 +30,7 @@ export function useNativeChatComposerInterrupt(args: { } const target = resolveTarget() if (target) { - sendRuntimePtyInput(target.settings, target.ptyId, ESC, 'driving') + sendRuntimePtyInput(target.ptyId, ESC, 'driving') } }, [inert, cancelPendingSends, isWorking, onStop, resolveTarget]) } diff --git a/src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx index bd09f0cf747..498f6ff0392 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx @@ -40,10 +40,6 @@ vi.mock('@/runtime/runtime-terminal-verified-input', () => ({ sendRuntimePtyInputVerified: (...args: unknown[]) => mocks.sendRuntimePtyInputVerified(...args) })) -vi.mock('@/lib/agent-paste-draft', () => ({ - getSettingsForAgentTabRuntimeOwner: (terminalTabId: string) => ({ terminalTabId }) -})) - vi.mock('./native-chat-runtime-send', () => ({ sendNativeChatAskAnswer: (...args: unknown[]) => mocks.sendNativeChatAskAnswer(...args), sendNativeChatMessage: (...args: unknown[]) => mocks.sendNativeChatMessage(...args) @@ -70,33 +66,25 @@ describe('useNativeChatInteractiveSend', () => { }) it('routes a non-selector answer through the pasted-text send path', () => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'grok') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'grok')) act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) // Grok commits a pasted answer: label text 'B', not option-number keystrokes. - expect(mocks.sendNativeChatMessage).toHaveBeenCalledWith( - { terminalTabId: 'tab-1' }, - 'pty-1', - 'B', - { onDeliverySettled: expect.any(Function) } - ) + expect(mocks.sendNativeChatMessage).toHaveBeenCalledWith('pty-1', 'B', { + onDeliverySettled: expect.any(Function) + }) expect(mocks.sendNativeChatAskAnswer).not.toHaveBeenCalled() }) it('routes a Codex answer through the option-number keystroke path', () => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'codex') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'codex')) act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) // Codex's request_user_input card ignores typed labels (STA-1860 shape): // the 2nd option is delivered as its digit '2', which selects AND commits. expect(mocks.sendNativeChatAskAnswer).toHaveBeenCalledWith( - { terminalTabId: 'tab-1' }, 'pty-1', [{ raw: '2' }], expect.any(Function) @@ -105,15 +93,12 @@ describe('useNativeChatInteractiveSend', () => { }) it('routes a Claude answer through the option-number keystroke path', () => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'claude')) act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) // The 2nd option is delivered as its number '2', not the label 'B' (STA-1860). expect(mocks.sendNativeChatAskAnswer).toHaveBeenCalledWith( - { terminalTabId: 'tab-1' }, 'pty-1', [{ raw: '2' }], expect.any(Function) @@ -123,21 +108,19 @@ describe('useNativeChatInteractiveSend', () => { it('infers OpenClaude answers through its Claude-compatible selector path', () => { const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'openclaude') + useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'openclaude') ) act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) - const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[3] + const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[2] expect(onSettled).toBeTypeOf('function') onSettled?.(true) expect(mocks.inferQuestionAnswered).toHaveBeenCalledOnce() }) it('does nothing when no option is answered', () => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'claude')) let resultValue: ReturnType | undefined act(() => { @@ -151,7 +134,7 @@ describe('useNativeChatInteractiveSend', () => { it('cancels delayed answer writes when the PTY target changes', () => { const { result, rerender } = renderHook( - ({ targetPtyId }) => useNativeChatInteractiveSend('tab-1', PANE_KEY, targetPtyId, 'codex'), + ({ targetPtyId }) => useNativeChatInteractiveSend(PANE_KEY, targetPtyId, 'codex'), { initialProps: { targetPtyId: 'pty-1' as string | null } } ) @@ -163,7 +146,7 @@ describe('useNativeChatInteractiveSend', () => { it('cancels delayed answer writes when the pane identity changes', () => { const { result, rerender } = renderHook( - ({ paneKey }) => useNativeChatInteractiveSend('tab-1', paneKey, 'pty-1', 'claude'), + ({ paneKey }) => useNativeChatInteractiveSend(paneKey, 'pty-1', 'claude'), { initialProps: { paneKey: PANE_KEY } } ) @@ -174,35 +157,27 @@ describe('useNativeChatInteractiveSend', () => { }) it('cancels delayed answer writes before interrupting the active PTY', () => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'claude')) act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) act(() => result.current.cancel()) expect(mocks.cancel).toHaveBeenCalledOnce() - expect(mocks.sendRuntimePtyInput).toHaveBeenCalledWith( - { terminalTabId: 'tab-1' }, - 'pty-1', - '\x1b', - 'driving' - ) + expect(mocks.sendRuntimePtyInput).toHaveBeenCalledWith('pty-1', '\x1b', 'driving') }) it.each(['opencode', 'opencode2'] as const)( 'paces two Escape writes for %s Stop and cancels them on rebind', (agent) => { const { result, rerender } = renderHook( - ({ ptyId }) => useNativeChatInteractiveSend('tab-1', PANE_KEY, ptyId, agent), + ({ ptyId }) => useNativeChatInteractiveSend(PANE_KEY, ptyId, agent), { initialProps: { ptyId: 'pty-1' } } ) act(() => result.current.cancel()) - expect(mocks.sendNativeChatAskAnswer).toHaveBeenCalledWith( - { terminalTabId: 'tab-1' }, - 'pty-1', - [{ raw: '\x1b' }, { raw: '\x1b' }] - ) + expect(mocks.sendNativeChatAskAnswer).toHaveBeenCalledWith('pty-1', [ + { raw: '\x1b' }, + { raw: '\x1b' } + ]) rerender({ ptyId: 'pty-2' }) expect(mocks.cancel).toHaveBeenCalledOnce() } @@ -211,12 +186,9 @@ describe('useNativeChatInteractiveSend', () => { it.each(['opencode', 'opencode2'] as const)( 'rejects a %s question with one acknowledged Escape and no delayed Stop', async (agent) => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', agent) - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', agent)) await expect(result.current.cancelAsk()).resolves.toBe(true) expect(mocks.sendRuntimePtyInputVerified).toHaveBeenCalledExactlyOnceWith( - { terminalTabId: 'tab-1' }, 'pty-1', '\x1b', 'driving', @@ -229,21 +201,16 @@ describe('useNativeChatInteractiveSend', () => { it('reports an Escape whose delivery is unknown as not delivered', async () => { mocks.sendRuntimePtyInputVerified.mockRejectedValueOnce(new Error('timeout')) - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'claude')) await expect(result.current.cancelAsk()).resolves.toBe(false) }) it.each(['opencode', 'opencode2'] as const)( 'delivers a non-default %s answer through selector keys', (agent) => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', agent) - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', agent)) act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) expect(mocks.sendNativeChatAskAnswer).toHaveBeenCalledWith( - { terminalTabId: 'tab-1' }, 'pty-1', [{ raw: '2' }], expect.any(Function) @@ -253,9 +220,7 @@ describe('useNativeChatInteractiveSend', () => { ) it('can cancel delayed writes without interrupting the replacement prompt', () => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'claude')) act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) act(() => result.current.cancelPending()) @@ -265,14 +230,12 @@ describe('useNativeChatInteractiveSend', () => { }) it('infers a Claude question answer only after every runtime write was delivered', () => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'claude')) act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) expect(mocks.inferQuestionAnswered).not.toHaveBeenCalled() - const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[3] + const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[2] expect(onSettled).toBeTypeOf('function') onSettled?.(false) expect(mocks.inferQuestionAnswered).not.toHaveBeenCalled() @@ -288,9 +251,7 @@ describe('useNativeChatInteractiveSend', () => { }) it('infers the answered question baseline, not a replacement that became current mid-send', () => { - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'claude')) // Answer question A while it is the current waiting question. act(() => result.current.sendAnswer(PROMPT, [{ indices: [1] }])) @@ -307,7 +268,7 @@ describe('useNativeChatInteractiveSend', () => { } } - const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[3] + const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[2] onSettled?.(true) // The baseline is question A's (captured before delivery), so the server can @@ -323,9 +284,7 @@ describe('useNativeChatInteractiveSend', () => { it('reports verified delivery settlement to the question card', () => { const onDeliverySettled = vi.fn() - const { result } = renderHook(() => - useNativeChatInteractiveSend('tab-1', PANE_KEY, 'pty-1', 'claude') - ) + const { result } = renderHook(() => useNativeChatInteractiveSend(PANE_KEY, 'pty-1', 'claude')) let sendResult: ReturnType | undefined act(() => { @@ -333,7 +292,7 @@ describe('useNativeChatInteractiveSend', () => { }) expect(sendResult).toEqual({ settleAfterMs: 500 }) - const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[3] + const onSettled = mocks.sendNativeChatAskAnswer.mock.calls[0]?.[2] onSettled?.(false) expect(onDeliverySettled).toHaveBeenCalledExactlyOnceWith(false) diff --git a/src/renderer/src/components/native-chat/use-native-chat-interactive-send.ts b/src/renderer/src/components/native-chat/use-native-chat-interactive-send.ts index 2c7a130bfbf..9ad2d4f53bb 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-interactive-send.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-interactive-send.ts @@ -2,7 +2,6 @@ import { useCallback, useLayoutEffect, useRef } from 'react' import { useAppStore } from '../../store' import { sendRuntimePtyInput } from '@/runtime/runtime-terminal-inspection' import { sendRuntimePtyInputVerified } from '@/runtime/runtime-terminal-verified-input' -import { getSettingsForAgentTabRuntimeOwner } from '@/lib/agent-paste-draft' import type { AgentType } from '../../../../shared/native-chat-types' import { resolveNativeChatTranscriptAgent, @@ -57,7 +56,6 @@ export type NativeChatInteractiveSend = { * `sendNativeChatMessage`. Control strings (option digits, ESC) are written raw. */ export function useNativeChatInteractiveSend( - terminalTabId: string, paneKey: string, targetPtyId: string | null, agent: AgentType @@ -72,38 +70,26 @@ export function useNativeChatInteractiveSend( }, []) // Why: a split can be rebound without unmounting this view. Cancel during // commit so no delayed answer write can race the replacement PTY. - useLayoutEffect( - () => cancelInFlight, - [agent, cancelInFlight, paneKey, targetPtyId, terminalTabId] - ) + useLayoutEffect(() => cancelInFlight, [agent, cancelInFlight, paneKey, targetPtyId]) const sendRaw = useCallback( (raw: string) => { if (!targetPtyId) { return } - sendRuntimePtyInput( - getSettingsForAgentTabRuntimeOwner(terminalTabId), - targetPtyId, - raw, - 'driving' - ) + sendRuntimePtyInput(targetPtyId, raw, 'driving') }, - [terminalTabId, targetPtyId] + [targetPtyId] ) const sendRawVerified = useCallback( (raw: string): Promise => targetPtyId - ? sendRuntimePtyInputVerified( - getSettingsForAgentTabRuntimeOwner(terminalTabId), - targetPtyId, - raw, - 'driving', - { requireWriteSettlement: true } - ).catch(() => false) + ? sendRuntimePtyInputVerified(targetPtyId, raw, 'driving', { + requireWriteSettlement: true + }).catch(() => false) : Promise.resolve(false), - [terminalTabId, targetPtyId] + [targetPtyId] ) const sendAnswer = useCallback( @@ -117,7 +103,6 @@ export function useNativeChatInteractiveSend( } // Cancel any prior in-flight answer before starting a new one. cancelInFlight() - const settings = getSettingsForAgentTabRuntimeOwner(terminalTabId) // Selector TUIs ignore pasted labels; Codex uses a different key sequence. const stepsAnswer = shouldStepNativeChatAskAnswer(agent) const buildsCodexAnswer = resolveNativeChatTranscriptAgent(agent) === 'codex' @@ -152,14 +137,13 @@ export function useNativeChatInteractiveSend( } const handle: NativeChatSendHandle = stepsAnswer ? sendNativeChatAskAnswer( - settings, targetPtyId, buildsCodexAnswer ? buildCodexAskAnswerKeys(prompt, selections) : buildAskAnswerKeys(prompt, selections), onSettled ) - : sendNativeChatMessage(settings, targetPtyId, formatAskAnswer(prompt, selections), { + : sendNativeChatMessage(targetPtyId, formatAskAnswer(prompt, selections), { onDeliverySettled: onSettled }) // Why: native-chat answer writes bypass xterm.onData. Infer only after @@ -171,7 +155,7 @@ export function useNativeChatInteractiveSend( settleAfterMs: handle.settleAfterMs } }, - [terminalTabId, paneKey, targetPtyId, agent, cancelInFlight] + [paneKey, targetPtyId, agent, cancelInFlight] ) const cancelAsk = useCallback(() => { @@ -183,15 +167,11 @@ export function useNativeChatInteractiveSend( cancelInFlight() if (resolveNativeChatTranscriptAgent(agent) === 'opencode' && targetPtyId) { // OpenCode confirms interruption with a second Escape; pace writes like mobile Stop. - inFlightRef.current = sendNativeChatAskAnswer( - getSettingsForAgentTabRuntimeOwner(terminalTabId), - targetPtyId, - [{ raw: ESC }, { raw: ESC }] - ) + inFlightRef.current = sendNativeChatAskAnswer(targetPtyId, [{ raw: ESC }, { raw: ESC }]) return } sendRaw(ESC) - }, [agent, cancelInFlight, sendRaw, targetPtyId, terminalTabId]) + }, [agent, cancelInFlight, sendRaw, targetPtyId]) return { sendAnswer, sendRaw, sendRawVerified, cancelPending: cancelInFlight, cancelAsk, cancel } } diff --git a/src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx index 53ef9eafd77..0fef6005cf6 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx @@ -60,7 +60,7 @@ describe('useNativeChatPickerCommandDispatch', () => { const hook = renderDispatch('codex', onSubmitted) act(() => hook.result.current(COMMAND)) - expect(sendNativeChatTypedCommand).toHaveBeenCalledWith({}, 'pty-1', '/status') + expect(sendNativeChatTypedCommand).toHaveBeenCalledWith('pty-1', '/status') expect(sendNativeChatMessage).not.toHaveBeenCalled() expect(onSubmitted).toHaveBeenCalledOnce() }) @@ -69,7 +69,7 @@ describe('useNativeChatPickerCommandDispatch', () => { const hook = renderDispatch(agent) act(() => hook.result.current(COMMAND)) - expect(sendNativeChatMessage).toHaveBeenCalledWith({}, 'pty-1', '/status') + expect(sendNativeChatMessage).toHaveBeenCalledWith('pty-1', '/status') expect(sendNativeChatTypedCommand).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.ts b/src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.ts index ec99002783b..81260bc2824 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.ts @@ -79,8 +79,8 @@ export function useNativeChatPickerCommandDispatch(args: { } trackPendingSend( agent === 'codex' - ? sendNativeChatTypedCommand(target.settings, target.ptyId, text) - : sendNativeChatMessage(target.settings, target.ptyId, text) + ? sendNativeChatTypedCommand(target.ptyId, text) + : sendNativeChatMessage(target.ptyId, text) ) emitNativeChatPickerItemAccepted({ agent, itemKind: 'command' }) // Why: picker dispatch is a catalog-verified command send; it must leave diff --git a/src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.test.tsx index 68565ace5d6..2a00a37f7d4 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.test.tsx @@ -62,7 +62,7 @@ beforeEach(() => { it('routes a Claude chat send outcome to its own pending echo', () => { const callbacks = send('claude', 'chat', 'hello') - const options = vi.mocked(sendNativeChatMessage).mock.calls[0]?.[3] + const options = vi.mocked(sendNativeChatMessage).mock.calls[0]?.[2] options?.onWriteRejected?.() options?.onWriteUnconfirmed?.() expect(callbacks.rejected).toHaveBeenCalledWith('pending-1') @@ -71,7 +71,7 @@ it('routes a Claude chat send outcome to its own pending echo', () => { it('routes a Claude image send outcome to its own pending echo', () => { const callbacks = send('claude', 'chat', 'look', ['/tmp/shot.png']) - vi.mocked(sendNativeChatMessageWithImageAttachments).mock.calls[0]?.[5]?.onWriteRejected?.() + vi.mocked(sendNativeChatMessageWithImageAttachments).mock.calls[0]?.[4]?.onWriteRejected?.() expect(callbacks.rejected).toHaveBeenCalledWith('pending-1') }) @@ -80,7 +80,7 @@ it.each([ ['claude', 'command', '/compact'] ] as const)('leaves a %s %s send on the unobserved write path', (agent, classification, draft) => { send(agent, classification, draft) - expect(vi.mocked(sendNativeChatMessage).mock.calls[0]?.[3]?.onWriteRejected).toBeUndefined() + expect(vi.mocked(sendNativeChatMessage).mock.calls[0]?.[2]?.onWriteRejected).toBeUndefined() }) // The pane brings the latest into view on this, whatever the send was classified as. diff --git a/src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.ts b/src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.ts index c1f979d4e84..d70d1d558fb 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.ts @@ -94,21 +94,20 @@ export function useNativeChatPtyComposerSend(args: { if (classification !== 'chat' && imagePaths.length === 0) { pendingHandle = args.agent === 'codex' && isSlashCommandDraft(text) - ? sendNativeChatTypedCommand(target.settings, target.ptyId, text) - : sendNativeChatMessage(target.settings, target.ptyId, text, sendOptions) + ? sendNativeChatTypedCommand(target.ptyId, text) + : sendNativeChatMessage(target.ptyId, text, sendOptions) } else if (imagePaths.length > 0) { pendingHandle = sendNativeChatMessageWithImageAttachments( args.agent, - target.settings, target.ptyId, text, imagePaths, sendOptions ) } else if (text.trim().length > 0) { - pendingHandle = sendNativeChatMessage(target.settings, target.ptyId, text, sendOptions) + pendingHandle = sendNativeChatMessage(target.ptyId, text, sendOptions) } else { - submitNativeChatPrompt(target.settings, target.ptyId) + submitNativeChatPrompt(target.ptyId) } if (classification !== 'chat') { if (pendingHandle) { diff --git a/src/renderer/src/components/native-chat/use-native-chat-session-option-command.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-session-option-command.test.tsx index f76a78dd732..0496c8dbaaa 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-session-option-command.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-session-option-command.test.tsx @@ -42,12 +42,7 @@ describe('useNativeChatSessionOptionCommand', () => { await act(() => hook.result.current.dispatch('/model')) expect(onSubmitted).toHaveBeenCalledOnce() - expect(typeNativeChatCommand).toHaveBeenCalledWith( - {}, - 'pty-1', - '/model', - expect.any(AbortSignal) - ) + expect(typeNativeChatCommand).toHaveBeenCalledWith('pty-1', '/model', expect.any(AbortSignal)) expect(sendNativeChatMessageVerified).not.toHaveBeenCalled() }) @@ -56,7 +51,6 @@ describe('useNativeChatSessionOptionCommand', () => { await act(() => hook.result.current.dispatch('/model sonnet', { delivery: 'type' })) expect(sendNativeChatMessageVerified).toHaveBeenCalledWith( - {}, 'pty-1', '/model sonnet', expect.any(AbortSignal) diff --git a/src/renderer/src/components/native-chat/use-native-chat-session-option-command.ts b/src/renderer/src/components/native-chat/use-native-chat-session-option-command.ts index c1b034eaf68..71beae47c83 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-session-option-command.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-session-option-command.ts @@ -74,7 +74,6 @@ export function useNativeChatSessionOptionCommand(args: { if (detectClaudeConfirmation) { observer = createClaudeModelSwitchConfirmationObserver({ ptyId: target.ptyId, - settings: target.settings, expectedModelLabel: options?.expectedChoiceLabel ?? null }) activeObserversRef.current.add(observer) @@ -88,18 +87,8 @@ export function useNativeChatSessionOptionCommand(args: { } const accepted = agent === 'codex' - ? await typeNativeChatCommand( - target.settings, - target.ptyId, - command, - sendController.signal - ) - : await sendNativeChatMessageVerified( - target.settings, - target.ptyId, - command, - sendController.signal - ) + ? await typeNativeChatCommand(target.ptyId, command, sendController.signal) + : await sendNativeChatMessageVerified(target.ptyId, command, sendController.signal) if (!accepted) { throw new Error('The terminal did not accept the command.') } diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-attention-dispatch.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-attention-dispatch.test.ts index 3ae6afa823f..9a3c283b0f4 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-attention-dispatch.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-attention-dispatch.test.ts @@ -16,7 +16,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, dispatchAttention, @@ -71,7 +70,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, dispatchAttention, @@ -113,7 +111,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, dispatchAttention, @@ -160,7 +157,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, dispatchAttention, @@ -201,7 +197,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion: vi.fn(), dispatchAttention, @@ -243,7 +238,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, dispatchAttention, @@ -272,7 +266,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foreground)), dispatchCompletion, dispatchAttention, @@ -321,7 +314,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion: vi.fn(), dispatchAttention, diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-completion-replay-guard.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-completion-replay-guard.test.ts index 75613bd4eeb..61bfc6e706b 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-completion-replay-guard.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-completion-replay-guard.test.ts @@ -13,7 +13,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -49,7 +48,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -73,7 +71,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -116,7 +113,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -163,7 +159,6 @@ describe('agent completion coordinator', () => { const firstCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -177,7 +172,6 @@ describe('agent completion coordinator', () => { const remountedCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -196,7 +190,6 @@ describe('agent completion coordinator', () => { const firstCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -221,7 +214,6 @@ describe('agent completion coordinator', () => { const remountedCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-dispose-leak.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-dispose-leak.test.ts index 5004c6d05d9..55b7282ac2c 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-dispose-leak.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-dispose-leak.test.ts @@ -28,7 +28,6 @@ function makeOptions(paneKey: string, live: { value: boolean }): AgentCompletion return { paneKey, getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion: vi.fn(), isLive: () => live.value diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-done-quiet-window.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-done-quiet-window.test.ts index 036ce1935fd..f0b1252158e 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-done-quiet-window.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-done-quiet-window.test.ts @@ -15,7 +15,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -71,7 +70,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -103,7 +101,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -134,7 +131,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -165,7 +161,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -190,7 +185,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foregroundProcess)), dispatchCompletion, isLive: () => true @@ -227,7 +221,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts index 918bdf5a562..dbb5df266b1 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts @@ -17,7 +17,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -55,7 +54,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -78,7 +76,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult('codex')), dispatchCompletion, isLive: () => true @@ -104,7 +101,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foregroundProcess)), dispatchCompletion, isLive: () => true @@ -132,7 +128,6 @@ describe('agent completion coordinator', () => { const hookCoordinator = createAgentCompletionCoordinator({ paneKey, getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(null)), dispatchCompletion, isLive: () => true @@ -157,7 +152,6 @@ describe('agent completion coordinator', () => { const processCoordinator = createAgentCompletionCoordinator({ paneKey, getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => result), dispatchCompletion, isLive: () => true @@ -189,7 +183,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(() => inspection.promise), dispatchCompletion, isLive: () => true @@ -219,7 +212,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -260,7 +252,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(() => inspection.promise), dispatchCompletion, isLive: () => true @@ -288,7 +279,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion, isLive: () => true diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-monitoring-turn-end.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-monitoring-turn-end.test.ts index 64162c8520d..70da53cf317 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-monitoring-turn-end.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-monitoring-turn-end.test.ts @@ -40,7 +40,6 @@ function createCoordinator() { const coordinator = createAgentCompletionCoordinator({ paneKey: PANE, getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, dispatchHookLifecycle, diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts index 3489bc0d701..9339f5f7570 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts @@ -27,7 +27,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult('zsh')), dispatchCompletion, isLive: () => true @@ -51,7 +50,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion, isLive: () => true @@ -86,7 +84,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion, isLive: () => true @@ -121,7 +118,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion, isLive: () => true @@ -151,7 +147,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(() => inspection.promise), dispatchCompletion, isLive: () => true @@ -171,7 +166,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(() => inspection.promise), dispatchCompletion, isLive: () => true @@ -192,7 +186,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult('codex')), dispatchCompletion, isLive: () => true @@ -211,7 +204,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'remote:terminal-1', - getSettings: () => ({ activeRuntimeEnvironmentId: 'env-1' }), inspectProcess: vi.fn(() => inspection.promise), dispatchCompletion, isLive: () => true diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts index eb5083ca2cb..852a2b326b1 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts @@ -16,7 +16,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true, @@ -41,7 +40,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true, @@ -63,7 +61,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true, @@ -84,7 +81,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true, @@ -117,7 +113,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foregroundProcess)), dispatchCompletion, isLive: () => true, @@ -150,7 +145,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foregroundProcess)), dispatchCompletion, isLive: () => true @@ -183,7 +177,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => result), dispatchCompletion, isLive: () => true @@ -226,7 +219,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => ({ foregroundProcess: null, hasChildProcesses: false as const, @@ -252,7 +244,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => ({ foregroundProcess: null, hasChildProcesses: false as const, @@ -295,7 +286,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => result), dispatchCompletion, isLive: () => true @@ -324,7 +314,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foregroundProcess)), dispatchCompletion, isLive: () => true @@ -349,7 +338,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foregroundProcess)), dispatchCompletion, shouldSuppressProcessReplacementCompletion: () => true, @@ -376,7 +364,6 @@ describe('agent completion coordinator', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foregroundProcess)), dispatchCompletion, shouldSuppressConfirmedProcessExitCompletion, diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts index aeedd4f133b..f4881ef510e 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts @@ -23,7 +23,6 @@ describe('agent completion coordinator process-exit turn boundary', () => { paneKey, statusLane: 'pty', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => result), dispatchCompletion: ptyDispatch, isLive: () => true @@ -48,7 +47,6 @@ describe('agent completion coordinator process-exit turn boundary', () => { paneKey, statusLane: 'hook', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(null)), dispatchCompletion, isLive: () => true diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts index 9d83b1d6a93..2e9a2afe42e 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts @@ -20,7 +20,6 @@ describe('agent completion coordinator queued inspections', () => { createAgentCompletionCoordinator({ paneKey: `tab-1:blocked-${index}`, getPtyId: () => `pty-blocked-${index}`, - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true @@ -40,7 +39,6 @@ describe('agent completion coordinator queued inspections', () => { createAgentCompletionCoordinator({ paneKey: `tab-1:stale-${index}`, getPtyId: () => `pty-stale-${index}`, - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true @@ -50,7 +48,6 @@ describe('agent completion coordinator queued inspections', () => { const liveCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:live', getPtyId: () => 'pty-live', - getSettings: () => null, inspectProcess: liveInspectProcess, dispatchCompletion: vi.fn(), isLive: () => true diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-boundary.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-boundary.test.ts index 11d3cd0aac7..2d97070b46c 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-boundary.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-boundary.test.ts @@ -13,7 +13,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -41,7 +40,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -63,7 +61,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const firstCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -81,7 +78,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const remounted = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -104,7 +100,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, dispatchHookLifecycle, @@ -167,7 +162,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const localCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -175,7 +169,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const hostCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -230,7 +223,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const localCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -238,7 +230,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const hostCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -287,7 +278,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const localCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -295,7 +285,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const hostCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -349,7 +338,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const localCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -357,7 +345,6 @@ describe('agent completion coordinator stamped turn boundary', () => { const hostCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-replay.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-replay.test.ts index 8e4fe3b45df..0d1586b15be 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-replay.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-replay.test.ts @@ -16,7 +16,6 @@ describe('agent completion coordinator stamped turn replay', () => { paneKey: 'tab-1:leaf-1', statusLane: 'pty', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -25,7 +24,6 @@ describe('agent completion coordinator stamped turn replay', () => { paneKey: 'tab-1:leaf-1', statusLane: 'hook', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -81,7 +79,6 @@ describe('agent completion coordinator stamped turn replay', () => { paneKey: 'tab-1:leaf-1', statusLane: 'pty', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -91,7 +88,6 @@ describe('agent completion coordinator stamped turn replay', () => { paneKey: 'tab-1:leaf-1', statusLane: 'hook', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -142,7 +138,6 @@ describe('agent completion coordinator stamped turn replay', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, dispatchHookLifecycle, @@ -196,7 +191,6 @@ describe('agent completion coordinator stamped turn replay', () => { const firstCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -221,7 +215,6 @@ describe('agent completion coordinator stamped turn replay', () => { const remounted = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -243,7 +236,6 @@ describe('agent completion coordinator stamped turn replay', () => { const firstCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -271,7 +263,6 @@ describe('agent completion coordinator stamped turn replay', () => { const remounted = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -293,7 +284,6 @@ describe('agent completion coordinator stamped turn replay', () => { const hookCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -301,7 +291,6 @@ describe('agent completion coordinator stamped turn replay', () => { const paneCoordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -350,7 +339,6 @@ describe('agent completion coordinator stamped turn replay', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts index c56e77b5bc7..8dafd227610 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts @@ -1,5 +1,4 @@ import type { ParsedAgentStatusPayload } from '../../../../shared/agent-status-types' -import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { RecognizedAgentProcess } from '../../../../shared/agent-process-recognition' import type { RuntimeTerminalProcessInspection } from '@/runtime/runtime-terminal-inspection' @@ -28,9 +27,7 @@ export type AgentCompletionCoordinatorOptions = { /** Remote authorities are event-triggered only; no periodic process polls. */ isRemotePtyId?: (ptyId: string) => boolean getExpectedIncarnationId?: () => string | null - getSettings: () => Pick | null | undefined inspectProcess: ( - settings: Pick | null | undefined, ptyId: string, options?: { expectedIncarnationId?: string; steadyState?: boolean } ) => Promise diff --git a/src/renderer/src/components/terminal-pane/agent-completion-no-evidence-cadence.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-no-evidence-cadence.test.ts index 4ed75cfd161..6b41c458c94 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-no-evidence-cadence.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-no-evidence-cadence.test.ts @@ -37,7 +37,6 @@ function createCoordinator( const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion, isLive: () => true, diff --git a/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts b/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts index fd4d1f448d2..2f8829bbea4 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts @@ -118,8 +118,8 @@ export function createAgentCompletionProcessMonitor({ : {}) } const result = await (Object.keys(inspectOptions).length > 0 - ? options.inspectProcess(options.getSettings(), ptyId, inspectOptions) - : options.inspectProcess(options.getSettings(), ptyId)) + ? options.inspectProcess(ptyId, inspectOptions) + : options.inspectProcess(ptyId)) if ( !state.disposed && generationAtRequest === state.inspectionGeneration && diff --git a/src/renderer/src/components/terminal-pane/agent-completion-stale-evidence-backoff.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-stale-evidence-backoff.test.ts index 37ab0ab4b8a..af3e426a3cb 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-stale-evidence-backoff.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-stale-evidence-backoff.test.ts @@ -91,7 +91,6 @@ function inspect(result: RuntimeTerminalProcessInspection, roundTripMs = 20): Pr options: { paneKey: 'tab-1:leaf-1', getPtyId: () => SSH_PTY_ID, - getSettings: () => null, isRemotePtyId: () => true, getExpectedIncarnationId: () => INCARNATION } as unknown as AgentCompletionCoordinatorOptions, diff --git a/src/renderer/src/components/terminal-pane/agent-completion-steady-state-opt-in.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-steady-state-opt-in.test.ts index 4f2286c7eb7..bc0fee2a3d1 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-steady-state-opt-in.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-steady-state-opt-in.test.ts @@ -12,14 +12,13 @@ import { describe('agent completion steadyState opt-in', () => { useAgentCompletionCoordinatorLifecycle() - const optionsOf = (call: unknown[]): unknown => call[2] + const optionsOf = (call: unknown[]): unknown => call[1] it('marks cadence polls on a local pane as steadyState', async () => { const inspectProcess = vi.fn(async () => processResult('codex')) const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true, @@ -41,7 +40,6 @@ describe('agent completion steadyState opt-in', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: 'tab-1:leaf-1', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true, @@ -64,7 +62,6 @@ describe('agent completion steadyState opt-in', () => { getPtyId: () => 'remote:pty-1', isRemotePtyId: () => true, getExpectedIncarnationId: () => 'inc-1', - getSettings: () => null, inspectProcess, dispatchCompletion: vi.fn(), isLive: () => true, diff --git a/src/renderer/src/components/terminal-pane/pane-foreground-process-exit-retire.test.ts b/src/renderer/src/components/terminal-pane/pane-foreground-process-exit-retire.test.ts index 5eafa29e07e..fe2a33b3cf2 100644 --- a/src/renderer/src/components/terminal-pane/pane-foreground-process-exit-retire.test.ts +++ b/src/renderer/src/components/terminal-pane/pane-foreground-process-exit-retire.test.ts @@ -36,7 +36,6 @@ function createUnmarkedPane(args: { visible: boolean }) { paneKey: 'tab-1:leaf-1', statusLane: 'pty', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(async () => processResult(foreground, hasChildren)), dispatchCompletion: vi.fn(), isLive: () => true, diff --git a/src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts b/src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts index 3fd342378bd..55784a507e1 100644 --- a/src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts +++ b/src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts @@ -44,7 +44,6 @@ it.each([false, true])('requires confirmation for pending live work, paired=%s', const p = await prepare(remote) p.actions.handleRequestClosePane(1) expect(p.probePtyRunningWork).toHaveBeenCalledWith( - expect.any(Object), [remote ? 'remote:env-1@@term_original' : 'pty-restored'], expect.any(Object) ) @@ -103,7 +102,7 @@ it('uses the direct SSH identity in the running-work probe', async () => { p.actions.handleRequestClosePane(1) p.verdict('live') await flushPtySideEffects() - expect(p.probePtyRunningWork).toHaveBeenCalledWith(expect.any(Object), [id], expect.any(Object)) + expect(p.probePtyRunningWork).toHaveBeenCalledWith([id], expect.any(Object)) expect(p.closed()).toBe(false) p.actions.handleConfirmClose(false) await p.settle() diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts b/src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts index 33b7998ef39..e6ececf03af 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts @@ -182,11 +182,11 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void { isTrackablePtyId: session.isForegroundTrackingAllowed, readForegroundProcess: (id, options) => isRemotePtyId(id) - ? inspectRuntimeTerminalProcess(useAppStore.getState().settings, id, options) + ? inspectRuntimeTerminalProcess(id, options) : window.api.pty.getForegroundProcess(id), confirmForegroundProcess: (id, options) => isRemotePtyId(id) - ? inspectRuntimeTerminalProcess(useAppStore.getState().settings, id, options) + ? inspectRuntimeTerminalProcess(id, options) : window.api.pty.confirmForegroundProcess(id), isRemotePtyId, getExpectedIncarnationId: () => session.remotePtyIncarnationId ?? null, diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts b/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts index a9eec4111a3..0d838771c97 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts @@ -1,10 +1,8 @@ import { isRemoteRuntimePtyId } from '../../../../../shared/remote-runtime-pty-id' -import { useAppStore } from '@/store' import type { SessionRestoredBannerReason } from '../session-restored-banner-pane-state' import { hasPtySerializer } from '../pty-buffer-serializer' import { inspectRuntimeTerminalProcess } from '@/runtime/runtime-terminal-inspection' import { waitForTerminalOutputParsed } from '@/lib/pane-manager/pane-terminal-output-scheduler' -import { getSettingsForWorktreeRuntimeOwner } from '@/lib/worktree-runtime-owner' import { isExpectedAgentProcess } from '../../../../../shared/agent-process-recognition' import { resolveDraftPasteReadyTimeoutMs } from '../../../../../shared/draft-paste-ready-timeout' import { createDraftPasteReadyScanner } from '../../../../../shared/draft-paste-ready-scanner' @@ -117,28 +115,18 @@ export function bindSettlePaneSerializer(session: ConnectPanePtySession): void { startupDraftPasteSettled = true session.startupDraftPasteAttempted = true session.cleanupStartupDraftPasteTimers() - const settings = getSettingsForWorktreeRuntimeOwner( - useAppStore.getState(), - session.deps.worktreeId - ) // Why: xterm focus reports share this transport queue. Bypassing it can // race CSI I against the draft on ConPTY and expose a literal `[I` prefix. - void sendAgentDraftPasteContent( - settings, - ptyId, - session.startupDraftPrompt, - 'launch', - async (data) => { - const accepted = await writeTerminalPastePtyInput(session.transport, data, 'launch') - if (accepted && !startupDraftInputRecorded) { - // Why: this transport write bypasses xterm's user-input signal; keep - // the composed draft from being discarded by later hibernation. - startupDraftInputRecorded = true - session.recordTerminalInputForHibernation() - } - return accepted + void sendAgentDraftPasteContent(ptyId, session.startupDraftPrompt, 'launch', async (data) => { + const accepted = await writeTerminalPastePtyInput(session.transport, data, 'launch') + if (accepted && !startupDraftInputRecorded) { + // Why: this transport write bypasses xterm's user-input signal; keep + // the composed draft from being discarded by later hibernation. + startupDraftInputRecorded = true + session.recordTerminalInputForHibernation() } - ) + return accepted + }) .catch(() => false) .finally(() => { startupDraftPasteInFlight = false @@ -152,12 +140,8 @@ export function bindSettlePaneSerializer(session: ConnectPanePtySession): void { if (!ptyId) { return } - const settings = getSettingsForWorktreeRuntimeOwner( - useAppStore.getState(), - session.deps.worktreeId - ) try { - const process = await inspectRuntimeTerminalProcess(settings, ptyId) + const process = await inspectRuntimeTerminalProcess(ptyId) const foreground = process.foregroundProcess?.toLowerCase() ?? '' if ( getStartupDraftPtyId() === ptyId && diff --git a/src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts b/src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts index 99accd330a8..2ba251c95ea 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts @@ -184,7 +184,6 @@ export function installTerminalKeydownFit(session: ConnectPanePtySession): void isRemotePtyId: (ptyId) => Boolean(isRemoteExecutionHostPtyId(ptyId) || isRemoteRuntimePtyId(ptyId)), getExpectedIncarnationId: () => session.remotePtyIncarnationId ?? null, - getSettings: () => useAppStore.getState().settings, inspectProcess: inspectRuntimeTerminalProcess, dispatchHookLifecycle: (payload) => dispatchAgentHookTerminalLifecycle(session.cacheKey, payload), diff --git a/src/renderer/src/components/terminal-pane/terminal-paste-operation-order.test.ts b/src/renderer/src/components/terminal-pane/terminal-paste-operation-order.test.ts index f08926b2c81..21cf0e902d8 100644 --- a/src/renderer/src/components/terminal-pane/terminal-paste-operation-order.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-paste-operation-order.test.ts @@ -55,7 +55,6 @@ describe('terminal paste operation ordering', () => { writePty, yieldToEventLoop: async () => { startupDraft ??= sendAgentDraftPasteContent( - null, 'pty-1', 'GENERATED_CONTEXT', 'launch', @@ -83,7 +82,6 @@ describe('terminal paste operation ordering', () => { startupOpened = resolve }) const startup = sendAgentDraftPasteContent( - null, 'pty-1', 'G'.repeat(64 * 1024 + 1), 'launch', diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts index efad698fca4..3cd0c696ab5 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts @@ -147,7 +147,7 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr ) // Why the shared probe rather than a direct inspect: this is the same question the tab-close // guard asks, and the two must not drift on what an unanswered host means. - void probePtyRunningWork(settings, [ptyId], { timeoutMs: RUNNING_CLOSE_PROBE_TIMEOUT_MS }) + void probePtyRunningWork([ptyId], { timeoutMs: RUNNING_CLOSE_PROBE_TIMEOUT_MS }) .then((probes) => { clearTimeout(probeTimeout) decide(() => { diff --git a/src/renderer/src/components/terminal/pty-running-work-probe-child-evidence.test.ts b/src/renderer/src/components/terminal/pty-running-work-probe-child-evidence.test.ts index 4d002ee0191..f673b4fe71b 100644 --- a/src/renderer/src/components/terminal/pty-running-work-probe-child-evidence.test.ts +++ b/src/renderer/src/components/terminal/pty-running-work-probe-child-evidence.test.ts @@ -14,8 +14,6 @@ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ import { probePtyRunningWork } from './pty-running-work-probe' -const SETTINGS = { activeRuntimeEnvironmentId: null } - describe('probePtyRunningWork child-process evidence', () => { beforeEach(() => { vi.clearAllMocks() @@ -28,9 +26,9 @@ describe('probePtyRunningWork child-process evidence', () => { childProcessEvidence: 'no-children' }) - await probePtyRunningWork(SETTINGS, ['pty-a'], { timeoutMs: 1000 }) + await probePtyRunningWork(['pty-a'], { timeoutMs: 1000 }) - expect(inspectRuntimeTerminalProcessMock).toHaveBeenCalledWith(SETTINGS, 'pty-a', { + expect(inspectRuntimeTerminalProcessMock).toHaveBeenCalledWith('pty-a', { scanChildProcesses: true }) }) @@ -42,7 +40,7 @@ describe('probePtyRunningWork child-process evidence', () => { childProcessEvidence: 'unverifiable' }) - const [probe] = await probePtyRunningWork(SETTINGS, ['pty-a'], { timeoutMs: 1000 }) + const [probe] = await probePtyRunningWork(['pty-a'], { timeoutMs: 1000 }) expect(probe).toMatchObject({ verdict: 'unverifiable', @@ -58,7 +56,7 @@ describe('probePtyRunningWork child-process evidence', () => { childProcessEvidence: 'no-children' }) - const [probe] = await probePtyRunningWork(SETTINGS, ['pty-a'], { timeoutMs: 1000 }) + const [probe] = await probePtyRunningWork(['pty-a'], { timeoutMs: 1000 }) expect(probe?.verdict).toBe('exited') }) @@ -70,7 +68,7 @@ describe('probePtyRunningWork child-process evidence', () => { childProcessEvidence: 'children' }) - const [probe] = await probePtyRunningWork(SETTINGS, ['pty-a'], { timeoutMs: 1000 }) + const [probe] = await probePtyRunningWork(['pty-a'], { timeoutMs: 1000 }) expect(probe?.verdict).toBe('live') }) @@ -85,8 +83,8 @@ describe('probePtyRunningWork child-process evidence', () => { hasChildProcesses: false }) - const [live] = await probePtyRunningWork(SETTINGS, ['pty-a'], { timeoutMs: 1000 }) - const [idle] = await probePtyRunningWork(SETTINGS, ['pty-b'], { timeoutMs: 1000 }) + const [live] = await probePtyRunningWork(['pty-a'], { timeoutMs: 1000 }) + const [idle] = await probePtyRunningWork(['pty-b'], { timeoutMs: 1000 }) expect(live?.verdict).toBe('live') expect(idle?.verdict).toBe('exited') diff --git a/src/renderer/src/components/terminal/pty-running-work-probe.ts b/src/renderer/src/components/terminal/pty-running-work-probe.ts index 9fe63c380f1..e97fda74939 100644 --- a/src/renderer/src/components/terminal/pty-running-work-probe.ts +++ b/src/renderer/src/components/terminal/pty-running-work-probe.ts @@ -1,4 +1,3 @@ -import type { GlobalSettings } from '../../../../shared/global-settings-types' import { inspectRuntimeTerminalProcess } from '@/runtime/runtime-terminal-inspection' import { probePtyRunningWorkWithInspection, @@ -10,15 +9,12 @@ export type { PtyRunningWorkVerdict } from '../../../../shared/pty-running-work-probe' -type ProbeSettings = Pick | null | undefined - export function probePtyRunningWork( - settings: ProbeSettings, ptyIds: readonly string[], options: { timeoutMs: number } ): Promise { return probePtyRunningWorkWithInspection(ptyIds, options, (ptyId) => // Close guards require host child-process evidence before acting. - inspectRuntimeTerminalProcess(settings, ptyId, { scanChildProcesses: true }) + inspectRuntimeTerminalProcess(ptyId, { scanChildProcesses: true }) ) } diff --git a/src/renderer/src/components/terminal/running-terminal-close-guard.test.ts b/src/renderer/src/components/terminal/running-terminal-close-guard.test.ts index 6b5b8343086..21d91ef21dc 100644 --- a/src/renderer/src/components/terminal/running-terminal-close-guard.test.ts +++ b/src/renderer/src/components/terminal/running-terminal-close-guard.test.ts @@ -111,7 +111,7 @@ describe('guardRunningTerminalClose', () => { guard(onClose) await settleProbe() - expect(inspectRuntimeTerminalProcessMock).toHaveBeenCalledWith(expect.anything(), 'pty-a', { + expect(inspectRuntimeTerminalProcessMock).toHaveBeenCalledWith('pty-a', { scanChildProcesses: true }) expect(onClose).not.toHaveBeenCalled() @@ -220,7 +220,7 @@ describe('guardRunningTerminalClose', () => { }, agentStatusByPaneKey: { [`tab-1:${LEAF_B}`]: { agentType: 'claude' } } }) - inspectRuntimeTerminalProcessMock.mockImplementation(async (_settings, ptyId: string) => ({ + inspectRuntimeTerminalProcessMock.mockImplementation(async (ptyId: string) => ({ foregroundProcess: ptyId === 'pty-b' ? 'claude' : 'zsh', hasChildProcesses: ptyId === 'pty-b' })) @@ -242,7 +242,7 @@ describe('guardRunningTerminalClose', () => { }, agentStatusByPaneKey: { [`tab-1:${LEAF_B}`]: { agentType: 'claude' } } }) - inspectRuntimeTerminalProcessMock.mockImplementation(async (_settings, ptyId: string) => ({ + inspectRuntimeTerminalProcessMock.mockImplementation(async (ptyId: string) => ({ foregroundProcess: ptyId === 'pty-a' ? 'npm' : 'zsh', hasChildProcesses: ptyId === 'pty-a' })) diff --git a/src/renderer/src/components/terminal/running-terminal-close-guard.ts b/src/renderer/src/components/terminal/running-terminal-close-guard.ts index 6bb9ff5a582..60082e0c667 100644 --- a/src/renderer/src/components/terminal/running-terminal-close-guard.ts +++ b/src/renderer/src/components/terminal/running-terminal-close-guard.ts @@ -111,7 +111,7 @@ export function guardRunningTerminalClose(params: { decided = true } - void probePtyRunningWork(settings, ptyIds, { timeoutMs: RUNNING_CLOSE_PROBE_TIMEOUT_MS }) + void probePtyRunningWork(ptyIds, { timeoutMs: RUNNING_CLOSE_PROBE_TIMEOUT_MS }) .then((probes) => { if (decided) { return diff --git a/src/renderer/src/components/terminal/window-close-running-work.test.ts b/src/renderer/src/components/terminal/window-close-running-work.test.ts index 77dc4ad745b..ba8f71ec0cd 100644 --- a/src/renderer/src/components/terminal/window-close-running-work.test.ts +++ b/src/renderer/src/components/terminal/window-close-running-work.test.ts @@ -49,7 +49,7 @@ function setState(ptyIds: string[]): void { /** Answers each pty id from `byPtyId`; anything unlisted never settles. */ function answerWith(byPtyId: Record): void { - inspectRuntimeTerminalProcessMock.mockImplementation((_settings: unknown, ptyId: string) => + inspectRuntimeTerminalProcessMock.mockImplementation((ptyId: string) => ptyId in byPtyId ? Promise.resolve(byPtyId[ptyId]) : new Promise(() => {}) ) } diff --git a/src/renderer/src/components/terminal/window-close-running-work.ts b/src/renderer/src/components/terminal/window-close-running-work.ts index 427ee72dae6..ccb9a34c593 100644 --- a/src/renderer/src/components/terminal/window-close-running-work.ts +++ b/src/renderer/src/components/terminal/window-close-running-work.ts @@ -57,7 +57,7 @@ export async function assessWindowCloseRunningWork(params: { return { kind: 'none' } } - const probes = await probePtyRunningWork(state.settings, candidatePtyIds, { + const probes = await probePtyRunningWork(candidatePtyIds, { timeoutMs: WINDOW_CLOSE_PROBE_TIMEOUT_MS }) if (probes.some((probe) => probe.verdict === 'live')) { diff --git a/src/renderer/src/hooks/agent-hook-completion-notifications.ts b/src/renderer/src/hooks/agent-hook-completion-notifications.ts index 8b43478818c..e90d2255383 100644 --- a/src/renderer/src/hooks/agent-hook-completion-notifications.ts +++ b/src/renderer/src/hooks/agent-hook-completion-notifications.ts @@ -236,7 +236,6 @@ function createCoordinator(paneKey: string, worktreeId: string): AgentCompletion paneKey, statusLane: 'hook', getPtyId: () => getPtyIdForPaneKey(paneKey), - getSettings: () => useAppStore.getState().settings, inspectProcess: async (): Promise => ({ foregroundProcess: null, hasChildProcesses: false diff --git a/src/renderer/src/lib/agent-draft-paste-content.ts b/src/renderer/src/lib/agent-draft-paste-content.ts index 19d5a30d275..7fa19bc1762 100644 --- a/src/renderer/src/lib/agent-draft-paste-content.ts +++ b/src/renderer/src/lib/agent-draft-paste-content.ts @@ -1,5 +1,4 @@ import { yieldToEventLoop } from '../../../shared/event-loop-yield' -import type { GlobalSettings } from '../../../shared/global-settings-types' import { getUtf8ByteLengthForCodePoint, readUtf8CodePointAt @@ -27,21 +26,19 @@ const AGENT_DRAFT_PASTE_INERT_ESCAPE = '\u241b' export type AgentDraftPtyInputWriter = (data: string) => boolean | Promise export async function sendAgentDraftPasteContent( - settings: Pick | null | undefined, ptyId: string, content: string, inputKind: TerminalInputKind, writePty?: AgentDraftPtyInputWriter ): Promise { return await runTerminalPtyInputTransaction(ptyId, () => - sendAgentDraftPasteContentNow(settings, ptyId, content, inputKind, writePty) + sendAgentDraftPasteContentNow(ptyId, content, inputKind, writePty) ) } // Why: callers that must keep extra PTY writes (e.g. submit Enter) inside the same // transaction take the lock themselves; taking it again here would deadlock. export async function sendAgentDraftPasteContentNow( - settings: Pick | null | undefined, ptyId: string, content: string, inputKind: TerminalInputKind, @@ -57,7 +54,6 @@ export async function sendAgentDraftPasteContentNow( }) if (!directMeasurement.exceededLimit) { return await writeAgentDraftPtyInput( - settings, ptyId, wrapTerminalBracketedPasteText(terminalContent), inputKind, @@ -75,16 +71,16 @@ export async function sendAgentDraftPasteContentNow( for (const chunk of iterateAgentDraftPasteContentChunks(terminalContent)) { let accepted = false try { - accepted = await writeAgentDraftPtyInput(settings, ptyId, chunk, inputKind, writePty) + accepted = await writeAgentDraftPtyInput(ptyId, chunk, inputKind, writePty) } catch { if (bracketedPasteOpen && chunk !== BRACKETED_PASTE_END) { - await closeAgentDraftBracketedPaste(settings, ptyId, inputKind, writePty) + await closeAgentDraftBracketedPaste(ptyId, inputKind, writePty) } return false } if (!accepted) { if (bracketedPasteOpen && chunk !== BRACKETED_PASTE_END) { - await closeAgentDraftBracketedPaste(settings, ptyId, inputKind, writePty) + await closeAgentDraftBracketedPaste(ptyId, inputKind, writePty) } return false } @@ -198,19 +194,15 @@ function getSanitizedUtf8ByteLengthForCodePoint(codePoint: number): number { } async function writeAgentDraftPtyInput( - settings: Pick | null | undefined, ptyId: string, data: string, inputKind: TerminalInputKind, writePty?: AgentDraftPtyInputWriter ): Promise { - return writePty - ? await writePty(data) - : await sendRuntimePtyInputVerified(settings, ptyId, data, inputKind) + return writePty ? await writePty(data) : await sendRuntimePtyInputVerified(ptyId, data, inputKind) } async function closeAgentDraftBracketedPaste( - settings: Pick | null | undefined, ptyId: string, inputKind: TerminalInputKind, writePty?: AgentDraftPtyInputWriter @@ -218,7 +210,7 @@ async function closeAgentDraftBracketedPaste( try { // Why: once the opener reached the PTY, a failed content chunk should not // leave the target TUI in bracketed-paste mode. - await writeAgentDraftPtyInput(settings, ptyId, BRACKETED_PASTE_END, inputKind, writePty) + await writeAgentDraftPtyInput(ptyId, BRACKETED_PASTE_END, inputKind, writePty) } catch { // The original write already failed; callers only need the paste to fail closed. } diff --git a/src/renderer/src/lib/agent-draft-readiness.test.ts b/src/renderer/src/lib/agent-draft-readiness.test.ts index bfc11476ca6..336f4a41ad9 100644 --- a/src/renderer/src/lib/agent-draft-readiness.test.ts +++ b/src/renderer/src/lib/agent-draft-readiness.test.ts @@ -45,7 +45,7 @@ describe('waitForAgentDraftInputReady', () => { const primary = vi.fn() await expect( - waitForAgentDraftInputReady(PTY_ID, 20_000, 'codex-composer-prompt', {}) + waitForAgentDraftInputReady(PTY_ID, 20_000, 'codex-composer-prompt') ).resolves.toBe(true) drainPreHandlerPtyData(PTY_ID, primary) @@ -61,7 +61,7 @@ describe('waitForAgentDraftInputReady', () => { vi.useFakeTimers() vi.stubGlobal('window', globalThis) let settled: boolean | null = null - void waitForAgentDraftInputReady(PTY_ID, 20_000, 'opencode-agent-row', {}).then( + void waitForAgentDraftInputReady(PTY_ID, 20_000, 'opencode-agent-row').then( (ready) => (settled = ready) ) testState.observer!(OPENCODE_BOX) @@ -78,7 +78,7 @@ describe('waitForAgentDraftInputReady', () => { vi.useFakeTimers() vi.stubGlobal('window', globalThis) let settled: boolean | null = null - void waitForAgentDraftInputReady(PTY_ID, 20_000, 'opencode-agent-row', {}).then( + void waitForAgentDraftInputReady(PTY_ID, 20_000, 'opencode-agent-row').then( (ready) => (settled = ready) ) await vi.advanceTimersByTimeAsync(19_000) @@ -94,7 +94,7 @@ describe('waitForAgentDraftInputReady', () => { vi.useFakeTimers() vi.stubGlobal('window', globalThis) let settled: boolean | null = null - void waitForAgentDraftInputReady(PTY_ID, 20_000, 'opencode-agent-row', {}).then( + void waitForAgentDraftInputReady(PTY_ID, 20_000, 'opencode-agent-row').then( (ready) => (settled = ready) ) testState.observer!(OPENCODE_BOX) diff --git a/src/renderer/src/lib/agent-draft-readiness.ts b/src/renderer/src/lib/agent-draft-readiness.ts index 22e9c2e8535..d96b01a06b6 100644 --- a/src/renderer/src/lib/agent-draft-readiness.ts +++ b/src/renderer/src/lib/agent-draft-readiness.ts @@ -1,5 +1,4 @@ import type { DraftPasteReadySignal } from '../../../shared/tui-agent-config' -import type { GlobalSettings } from '../../../shared/global-settings-types' import { subscribeToPtyData } from '@/components/terminal-pane/pty-data-sidecar-subscriptions' import { replayPreHandlerPtyData } from '@/components/terminal-pane/pty-pre-handler-buffer' import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection' @@ -21,8 +20,7 @@ const BRACKETED_PASTE_QUIET_MS = 1500 export function waitForAgentDraftInputReady( ptyId: string, timeoutMs: number, - readySignal: DraftPasteReadySignal, - settings: Pick | null | undefined + readySignal: DraftPasteReadySignal ): Promise { return new Promise((resolve) => { let settled = false @@ -75,12 +73,7 @@ export function waitForAgentDraftInputReady( } if (isRemoteRuntimePtyId(ptyId)) { - void subscribeToRuntimeTerminalData( - settings, - ptyId, - `desktop:paste-ready:${ptyId}`, - observeData - ) + void subscribeToRuntimeTerminalData(ptyId, `desktop:paste-ready:${ptyId}`, observeData) .then((remoteUnsubscribe) => { if (settled) { remoteUnsubscribe() diff --git a/src/renderer/src/lib/agent-followup-delivery.test.ts b/src/renderer/src/lib/agent-followup-delivery.test.ts index 06260adfb64..a2448637922 100644 --- a/src/renderer/src/lib/agent-followup-delivery.test.ts +++ b/src/renderer/src/lib/agent-followup-delivery.test.ts @@ -44,15 +44,14 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () = const delivery = sendFollowupPromptWhenAgentReady({ ptyId: 'pty-1', expectedProcess, - prompt: 'ship it', - settings: null + prompt: 'ship it' }) await vi.advanceTimersByTimeAsync(4 * 150) const delivered = await delivery expect(delivered).toBe(true) - expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(null, 'pty-1', 'ship it\r', 'launch') + expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', 'ship it\r', 'launch') }) it(`still refuses to type into a bare ${agent} shell foreground`, async () => { @@ -66,8 +65,7 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () = const delivery = sendFollowupPromptWhenAgentReady({ ptyId: 'pty-1', expectedProcess, - prompt: 'ship it', - settings: null + prompt: 'ship it' }) await vi.advanceTimersByTimeAsync(29 * 150) @@ -86,8 +84,7 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () = const delivery = sendFollowupPromptWhenAgentReady({ ptyId: 'pty-1', expectedProcess, - prompt: 'ship it', - settings: null + prompt: 'ship it' }) await vi.advanceTimersByTimeAsync(29 * 150) @@ -109,11 +106,10 @@ describe('sendFollowupPromptWhenAgentReady — interpreter-wrapped agents', () = const delivered = await sendFollowupPromptWhenAgentReady({ ptyId: 'pty-1', expectedProcess: 'aider', - prompt: 'ship it', - settings: null + prompt: 'ship it' }) expect(delivered).toBe(true) - expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith(null, 'pty-1', 'ship it\r', 'launch') + expect(sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', 'ship it\r', 'launch') }) }) diff --git a/src/renderer/src/lib/agent-followup-delivery.ts b/src/renderer/src/lib/agent-followup-delivery.ts index e520cb6df1f..efa2287913a 100644 --- a/src/renderer/src/lib/agent-followup-delivery.ts +++ b/src/renderer/src/lib/agent-followup-delivery.ts @@ -7,22 +7,18 @@ import { isExpectedAgentProcess } from '../../../shared/agent-process-recognition' import { isShellProcess } from '../../../shared/shell-process-detection' -import type { GlobalSettings } from '../../../shared/global-settings-types' - -type RuntimeOwnerSettings = Pick | null | undefined export async function sendFollowupPromptWhenAgentReady(args: { ptyId: string expectedProcess: string prompt: string - settings: RuntimeOwnerSettings }): Promise { - const { ptyId, expectedProcess, prompt, settings } = args - if (!(await waitForAgentForeground(ptyId, expectedProcess, settings))) { + const { ptyId, expectedProcess, prompt } = args + if (!(await waitForAgentForeground(ptyId, expectedProcess))) { return false } try { - return await sendRuntimePtyInputVerified(settings, ptyId, `${prompt}\r`, 'launch') + return await sendRuntimePtyInputVerified(ptyId, `${prompt}\r`, 'launch') } catch { return false } @@ -30,17 +26,13 @@ export async function sendFollowupPromptWhenAgentReady(args: { // Why: delayed follow-ups must not type into an arbitrary shell. Require a // positive readiness signal before writing user/task text to the PTY. -async function waitForAgentForeground( - ptyId: string, - expectedProcess: string, - settings: RuntimeOwnerSettings -): Promise { +async function waitForAgentForeground(ptyId: string, expectedProcess: string): Promise { for (let attempt = 0; attempt < 30; attempt += 1) { if (attempt > 0) { await new Promise((resolve) => globalThis.setTimeout(resolve, 150)) } try { - const process = await inspectRuntimeTerminalProcess(settings, ptyId) + const process = await inspectRuntimeTerminalProcess(ptyId) const foreground = process.foregroundProcess?.toLowerCase() ?? '' if (isExpectedAgentProcess(foreground, expectedProcess)) { return true diff --git a/src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts b/src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts index 32b0b88c753..9c4309b433d 100644 --- a/src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts +++ b/src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts @@ -53,7 +53,6 @@ describe('pty-bound agent draft readiness budget', () => { it('delivers when a cold Codex composer becomes ready after 8s', async () => { const promise = pasteDraftToAgentPtyWhenReady({ - tabId: 'tab-1', ptyId: 'pty-1', content: 'draft', agent: 'codex', @@ -63,19 +62,13 @@ describe('pty-bound agent draft readiness budget', () => { await vi.advanceTimersByTimeAsync(10_000) await expect(promise).resolves.toBe(true) - expect(testState.waitForReady).toHaveBeenCalledWith( - 'pty-1', - 20_000, - 'codex-composer-prompt', - {} - ) + expect(testState.waitForReady).toHaveBeenCalledWith('pty-1', 20_000, 'codex-composer-prompt') expect(testState.sendInput).toHaveBeenCalledTimes(1) }) it('gives a cold opencode composer the same headroom as Codex', async () => { const onUnconfirmedDelivery = vi.fn() const promise = pasteDraftToAgentPtyWhenReady({ - tabId: 'tab-1', ptyId: 'pty-1', content: 'draft', agent: 'opencode', @@ -89,8 +82,7 @@ describe('pty-bound agent draft readiness budget', () => { expect(testState.waitForReady).toHaveBeenCalledWith( 'pty-1', 20_000, - 'render-cursor-after-bracketed-paste', - {} + 'render-cursor-after-bracketed-paste' ) expect(testState.sendInput).toHaveBeenCalledTimes(1) expect(onUnconfirmedDelivery).not.toHaveBeenCalled() @@ -100,7 +92,6 @@ describe('pty-bound agent draft readiness budget', () => { 'waits for the %s agent row only when Enter follows the paste', async (agent) => { const promise = pasteDraftToAgentPtyWhenReady({ - tabId: 'tab-1', ptyId: 'pty-1', content: 'task', agent, @@ -111,7 +102,7 @@ describe('pty-bound agent draft readiness budget', () => { await vi.advanceTimersByTimeAsync(12_000) await expect(promise).resolves.toBe(true) - expect(testState.waitForReady).toHaveBeenCalledWith('pty-1', 20_000, 'opencode-agent-row', {}) + expect(testState.waitForReady).toHaveBeenCalledWith('pty-1', 20_000, 'opencode-agent-row') } ) @@ -132,7 +123,7 @@ describe('pty-bound agent draft readiness budget', () => { await vi.advanceTimersByTimeAsync(12_000) await expect(promise).resolves.toBe(true) - expect(testState.waitForReady).toHaveBeenCalledWith('pty-1', 20_000, signal, {}) + expect(testState.waitForReady).toHaveBeenCalledWith('pty-1', 20_000, signal) }) it('flags a blind paste when only the opencode process, not its composer, was seen', async () => { @@ -145,7 +136,6 @@ describe('pty-bound agent draft readiness budget', () => { }) const onUnconfirmedDelivery = vi.fn() const promise = pasteDraftToAgentPtyWhenReady({ - tabId: 'tab-1', ptyId: 'pty-1', content: 'draft', agent: 'opencode', @@ -163,7 +153,6 @@ describe('pty-bound agent draft readiness budget', () => { it('keeps the 8s readiness deadline for agents without an agent-specific budget', async () => { const onTimeout = vi.fn() const promise = pasteDraftToAgentPtyWhenReady({ - tabId: 'tab-1', ptyId: 'pty-1', content: 'draft', agent: 'gemini', @@ -177,8 +166,7 @@ describe('pty-bound agent draft readiness budget', () => { expect(testState.waitForReady).toHaveBeenCalledWith( 'pty-1', 8000, - 'render-quiet-after-bracketed-paste', - {} + 'render-quiet-after-bracketed-paste' ) expect(onTimeout).toHaveBeenCalledTimes(1) expect(testState.sendInput).not.toHaveBeenCalled() diff --git a/src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts b/src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts index 84ad14770d1..a8dee509d05 100644 --- a/src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts +++ b/src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts @@ -105,12 +105,7 @@ describe('post-paste submit retry Enter', () => { await expect(promise).resolves.toBe(true) expect(enterWrites()).toHaveLength(2) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenLastCalledWith( - {}, - 'pty-1', - '\r', - 'launch' - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenLastCalledWith('pty-1', '\r', 'launch') expect(vi.getTimerCount()).toBe(0) }) @@ -135,7 +130,7 @@ describe('post-paste submit retry Enter', () => { it('holds the PTY input transaction across the retry Enter', async () => { const writes: string[] = [] testState.sendRuntimePtyInputVerified.mockImplementation( - async (_settings: unknown, _ptyId: string, data: string) => { + async (_ptyId: string, data: string) => { writes.push(data) return true } @@ -146,7 +141,6 @@ describe('post-paste submit retry Enter', () => { await vi.advanceTimersByTimeAsync(POST_PASTE_SUBMIT_DELAY_MS) // Competing paste on the same PTY: it must not open a frame the retry can land in. const competing = sendAgentDraftPasteContent( - {}, 'pty-1', 'y'.repeat(AGENT_DRAFT_PASTE_DIRECT_MAX_BYTES + 1), 'driving' @@ -178,7 +172,7 @@ describe('post-paste submit retry Enter', () => { }) function enterWrites(): unknown[][] { - return testState.sendRuntimePtyInputVerified.mock.calls.filter((call) => call[2] === '\r') + return testState.sendRuntimePtyInputVerified.mock.calls.filter((call) => call[1] === '\r') } function startCodexSubmit(): Promise { diff --git a/src/renderer/src/lib/agent-paste-draft.test.ts b/src/renderer/src/lib/agent-paste-draft.test.ts index 2427c7a1e3b..3a6be024c3e 100644 --- a/src/renderer/src/lib/agent-paste-draft.test.ts +++ b/src/renderer/src/lib/agent-paste-draft.test.ts @@ -4,7 +4,6 @@ import { AGENT_DRAFT_PASTE_DIRECT_MAX_BYTES, AGENT_DRAFT_PASTE_MAX_BYTES, chunkAgentDraftPasteContent, - getSettingsForAgentTabRuntimeOwner, iterateAgentDraftPasteContentChunks, pasteDraftToAgentPtyWhenReady, pasteDraftWhenAgentReady, @@ -58,8 +57,8 @@ vi.mock('@/components/terminal-pane/pty-pre-handler-buffer', () => ({ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ isRemoteRuntimePtyId: testState.isRemoteRuntimePtyId, // Why drop the kind: this suite pins write shapes; startup-draft-input-kind.test.ts pins kinds. - sendRuntimePtyInputVerified: (settings: unknown, ptyId: string, data: string) => - testState.sendRuntimePtyInputVerified(settings, ptyId, data), + sendRuntimePtyInputVerified: (ptyId: string, data: string) => + testState.sendRuntimePtyInputVerified(ptyId, data), inspectRuntimeTerminalProcess: testState.inspectRuntimeTerminalProcess })) @@ -135,11 +134,7 @@ describe('pasteDraftWhenAgentReady', () => { testState.ptyObserver?.(CODEX_COMPOSER_PROMPT_RENDER) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) expect(vi.getTimerCount()).toBe(0) }) @@ -168,14 +163,10 @@ describe('pasteDraftWhenAgentReady', () => { expect(testState.replayPreHandlerPtyData).toHaveBeenCalledWith('pty-1', testState.ptyObserver) await flushMicrotasks() - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) await vi.advanceTimersByTimeAsync(POST_PASTE_SUBMIT_DELAY_MS + CODEX_SUBMIT_RETRY_DELAY_MS) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(2, {}, 'pty-1', '\r') + expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(2, 'pty-1', '\r') expect(testState.unsubscribe).toHaveBeenCalledTimes(1) expect(vi.getTimerCount()).toBe(0) }) @@ -198,11 +189,7 @@ describe('pasteDraftWhenAgentReady', () => { await vi.advanceTimersByTimeAsync(1) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) }) it('pastes into opencode as soon as show-cursor renders after bracketed paste is enabled', async () => { @@ -220,11 +207,7 @@ describe('pasteDraftWhenAgentReady', () => { testState.ptyObserver?.(SHOW_CURSOR) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) expect(vi.getTimerCount()).toBe(0) }) @@ -248,11 +231,7 @@ describe('pasteDraftWhenAgentReady', () => { testState.ptyObserver?.(SHOW_CURSOR) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) expect(vi.getTimerCount()).toBe(0) }) @@ -308,11 +287,7 @@ describe('pasteDraftWhenAgentReady', () => { await vi.advanceTimersByTimeAsync(20_000) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) // The composer was never observed; the caller must be able to hedge its success notice. expect(onUnconfirmedDelivery).toHaveBeenCalledTimes(1) }) @@ -336,11 +311,7 @@ describe('pasteDraftWhenAgentReady', () => { await flushMicrotasks(5) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) }) it('does not paste for agents that already use native draft prefill', async () => { @@ -371,16 +342,12 @@ describe('pasteDraftWhenAgentReady', () => { await flushMicrotasks() expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledTimes(1) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) await vi.advanceTimersByTimeAsync(49) expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledTimes(1) await vi.advanceTimersByTimeAsync(1) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(2, {}, 'pty-1', '\r') + expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(2, 'pty-1', '\r') }) it('does not submit when the verified paste write fails', async () => { @@ -446,11 +413,7 @@ describe('pasteDraftWhenAgentReady', () => { await vi.advanceTimersByTimeAsync(8000) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) }) it('waits past the default budget for a cold-boot Codex composer glyph (STA-3367)', async () => { @@ -474,11 +437,7 @@ describe('pasteDraftWhenAgentReady', () => { testState.ptyObserver?.(CODEX_COMPOSER_PROMPT_RENDER) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) expect(vi.getTimerCount()).toBe(0) }) @@ -532,11 +491,7 @@ describe('pasteDraftWhenAgentReady', () => { testState.ptyObserver?.(CODEX_COMPOSER_PROMPT_RENDER) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) }) it.each(['tab', 'pty'] as const)( @@ -568,7 +523,6 @@ describe('pasteDraftWhenAgentReady', () => { testState.inspectRuntimeTerminalProcess.mockReturnValue(new Promise(() => {})) const promise = pasteDraftToAgentPtyWhenReady({ - tabId: 'tab-1', ptyId: 'pty-1', content: ISSUE_URL, agent: 'codex', @@ -587,7 +541,7 @@ describe('pasteDraftWhenAgentReady', () => { expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled() }) - it('routes tab-owned paste writes through the worktree runtime owner', async () => { + it('pastes into a tab whose workspace a server owns', async () => { testState.appState.settings = { activeRuntimeEnvironmentId: 'focused-runtime' } testState.appState.tabsByWorktree = { 'wt-1': [{ id: 'tab-1' }] } testState.appState.repos = [ @@ -605,14 +559,10 @@ describe('pasteDraftWhenAgentReady', () => { testState.ptyObserver?.(`${DECSET_BRACKETED_PASTE}${CODEX_COMPOSER_PROMPT_RENDER}`) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - { activeRuntimeEnvironmentId: 'owner-runtime' }, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) }) - it('routes legacy remote PTY readiness subscription through the tab owner', async () => { + it('subscribes to a remote PTY by its id, never by the focused server', async () => { testState.appState.settings = { activeRuntimeEnvironmentId: 'focused-runtime' } testState.appState.ptyIdsByTabId = { 'tab-1': ['remote:terminal-handle'] } testState.appState.tabsByWorktree = { 'wt-1': [{ id: 'tab-1' }] } @@ -622,12 +572,7 @@ describe('pasteDraftWhenAgentReady', () => { testState.appState.worktreesByRepo = { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] } testState.isRemoteRuntimePtyId.mockReturnValue(true) testState.subscribeToRuntimeTerminalData.mockImplementation( - async ( - _settings: unknown, - _ptyId: string, - _clientId: string, - observer: (data: string) => void - ) => { + async (_ptyId: string, _clientId: string, observer: (data: string) => void) => { testState.ptyObserver = observer return testState.unsubscribe } @@ -644,7 +589,6 @@ describe('pasteDraftWhenAgentReady', () => { await expect(promise).resolves.toBe(true) expect(testState.subscribeToRuntimeTerminalData).toHaveBeenCalledWith( - { activeRuntimeEnvironmentId: 'owner-runtime' }, 'remote:terminal-handle', 'desktop:paste-ready:remote:terminal-handle', expect.any(Function) @@ -660,11 +604,7 @@ describe('pasteDraftWhenAgentReady', () => { expect(testState.subscribeToPtyData).not.toHaveBeenCalled() expect(testState.subscribeToRuntimeTerminalData).not.toHaveBeenCalled() expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledTimes(1) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, - 'pty-1', - PASTED_ISSUE_URL - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', PASTED_ISSUE_URL) await flushMicrotasks() await vi.advanceTimersByTimeAsync(49) @@ -672,13 +612,13 @@ describe('pasteDraftWhenAgentReady', () => { await vi.advanceTimersByTimeAsync(1) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(2, {}, 'pty-1', '\r') + expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(2, 'pty-1', '\r') }) it('holds the PTY transaction across the paste and its submit Enter', async () => { const writes: string[] = [] testState.sendRuntimePtyInputVerified.mockImplementation( - async (_settings: unknown, _ptyId: string, data: string) => { + async (_ptyId: string, data: string) => { writes.push(data) return true } @@ -688,7 +628,6 @@ describe('pasteDraftWhenAgentReady', () => { await flushMicrotasks() // Competing chunked paste on the same PTY: it must not open a frame the Enter can land in. const competing = sendAgentDraftPasteContent( - {}, 'pty-1', 'y'.repeat(AGENT_DRAFT_PASTE_DIRECT_MAX_BYTES + 1), 'driving' @@ -728,16 +667,10 @@ describe('pasteDraftWhenAgentReady', () => { await expect(promise).resolves.toBe(true) expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith( 1, - { activeRuntimeEnvironmentId: 'owner-runtime' }, 'pty-right', PASTED_ISSUE_URL ) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith( - 2, - { activeRuntimeEnvironmentId: 'owner-runtime' }, - 'pty-right', - '\r' - ) + expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(2, 'pty-right', '\r') }) it('streams large running-agent drafts as bounded bracketed chunks before submit', async () => { @@ -752,22 +685,22 @@ describe('pasteDraftWhenAgentReady', () => { await flushMicrotasks(20) const calls = testState.sendRuntimePtyInputVerified.mock.calls - expect(calls.at(0)).toEqual([{}, 'pty-1', '\x1b[200~']) - expect(calls.at(-1)?.[2]).toBe('\x1b[201~') + expect(calls.at(0)).toEqual(['pty-1', '\x1b[200~']) + expect(calls.at(-1)?.[1]).toBe('\x1b[201~') expect( calls .slice(1, -1) - .map((call) => call[2]) + .map((call) => call[1]) .join('') ).toBe(content) for (const call of calls.slice(1, -1)) { - expect((call[2] as string).length).toBeLessThanOrEqual(AGENT_DRAFT_PASTE_CHUNK_MAX_BYTES) + expect(String(call[1]).length).toBeLessThanOrEqual(AGENT_DRAFT_PASTE_CHUNK_MAX_BYTES) } await vi.advanceTimersByTimeAsync(50) await expect(promise).resolves.toBe(true) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenLastCalledWith({}, 'pty-1', '\r') + expect(testState.sendRuntimePtyInputVerified).toHaveBeenLastCalledWith('pty-1', '\r') }) it('normalizes multiline running-agent drafts like terminal paste', async () => { @@ -777,7 +710,6 @@ describe('pasteDraftWhenAgentReady', () => { }) expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, 'pty-1', '\x1b[200~line one\rline two\rline three\x1b[201~' ) @@ -802,9 +734,9 @@ describe('pasteDraftWhenAgentReady', () => { ).resolves.toBe(false) expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledTimes(3) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(1, {}, 'pty-1', '[200~') - expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(3, {}, 'pty-1', '[201~') - expect(testState.sendRuntimePtyInputVerified.mock.calls.some((call) => call[2] === '\r')).toBe( + expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(1, 'pty-1', '[200~') + expect(testState.sendRuntimePtyInputVerified).toHaveBeenNthCalledWith(3, 'pty-1', '[201~') + expect(testState.sendRuntimePtyInputVerified.mock.calls.some((call) => call[1] === '\r')).toBe( false ) }) @@ -859,7 +791,7 @@ describe('pasteDraftWhenAgentReady', () => { it('yields during large accepted-size preflight before writing agent draft chunks', async () => { const content = 'x'.repeat(AGENT_DRAFT_PASTE_DIRECT_MAX_BYTES + 300 * 1024) - const promise = sendAgentDraftPasteContent({}, 'pty-1', content, 'driving') + const promise = sendAgentDraftPasteContent('pty-1', content, 'driving') await flushMicrotasks(5) expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled() @@ -867,46 +799,19 @@ describe('pasteDraftWhenAgentReady', () => { await vi.runOnlyPendingTimersAsync() await flushMicrotasks(10) - expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith({}, 'pty-1', '\x1b[200~') + expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith('pty-1', '\x1b[200~') await expect(promise).resolves.toBe(true) }) it('rejects oversized agent drafts before any PTY write', async () => { const oversized = 'x'.repeat(AGENT_DRAFT_PASTE_MAX_BYTES + 1) - const result = sendAgentDraftPasteContent({}, 'pty-1', oversized, 'driving') + const result = sendAgentDraftPasteContent('pty-1', oversized, 'driving') await expect(result).resolves.toBe(false) expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled() }) }) -describe('getSettingsForAgentTabRuntimeOwner', () => { - beforeEach(() => { - testState.appState.settings = { activeRuntimeEnvironmentId: 'focused-runtime' } - testState.appState.tabsByWorktree = {} - testState.appState.repos = [] - testState.appState.worktreesByRepo = {} - }) - - it('falls back to focused settings when the tab is not mapped to a worktree', () => { - expect(getSettingsForAgentTabRuntimeOwner('missing-tab')).toEqual({ - activeRuntimeEnvironmentId: 'focused-runtime' - }) - }) - - it('uses the tab worktree owner when mapped', () => { - testState.appState.tabsByWorktree = { 'wt-1': [{ id: 'tab-1' }] } - testState.appState.repos = [ - { id: 'repo-1', connectionId: null, executionHostId: 'runtime:owner-runtime' } - ] - testState.appState.worktreesByRepo = { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] } - - expect(getSettingsForAgentTabRuntimeOwner('tab-1')).toEqual({ - activeRuntimeEnvironmentId: 'owner-runtime' - }) - }) -}) - async function flushMicrotasks(iterations = 2): Promise { for (let index = 0; index < iterations; index += 1) { await Promise.resolve() diff --git a/src/renderer/src/lib/agent-paste-draft.ts b/src/renderer/src/lib/agent-paste-draft.ts index 8a6fd962925..a5c829e8aca 100644 --- a/src/renderer/src/lib/agent-paste-draft.ts +++ b/src/renderer/src/lib/agent-paste-draft.ts @@ -1,4 +1,3 @@ -import type { GlobalSettings } from '../../../shared/global-settings-types' import type { TuiAgent } from '../../../shared/tui-agent' import type { TerminalInputKind } from '../../../shared/terminal-input-kind' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' @@ -16,7 +15,6 @@ import { } from '@/components/terminal-pane/terminal-bracketed-paste' import { runTerminalPtyInputTransaction } from '@/components/terminal-pane/terminal-pty-input-transaction' import { waitForAgentReady } from './agent-ready-wait' -import { getSettingsForWorktreeRuntimeOwner } from './worktree-runtime-owner' import { sendAgentDraftPasteContentNow } from './agent-draft-paste-content' import { agentDeliversDraftViaNativePrefill } from './agent-native-draft-prefill' import { waitForAgentDraftInputReady } from './agent-draft-readiness' @@ -45,20 +43,6 @@ export const POST_PASTE_SUBMIT_DELAY_MS = AGENT_PROMPT_POST_PASTE_SUBMIT_DELAY_M // also stops one slow step from spending the other's budget (STA-3367). const PTY_SPAWN_TIMEOUT_MS = 8000 -export function getSettingsForAgentTabRuntimeOwner( - tabId: string -): Pick | null | undefined { - const store = useAppStore.getState() - for (const [worktreeId, tabs] of Object.entries(store.tabsByWorktree ?? {})) { - if (tabs?.some((tab) => tab.id === tabId)) { - // Why: legacy remote PTY ids may not embed their runtime owner. The tab's - // worktree still identifies which host should receive readiness/send RPCs. - return getSettingsForWorktreeRuntimeOwner(store, worktreeId) - } - } - return store.settings -} - /** * Wait until the agent on `tabId` has rendered its input-accepting TUI, * then bracketed-paste `content` into its input buffer. By default the @@ -102,14 +86,12 @@ export async function pasteDraftWhenAgentReady(args: { } const readySignal = resolvePasteReadySignal(agentConfig, submit === true) - const settings = getSettingsForAgentTabRuntimeOwner(tabId) const readinessTimeoutMs = resolveDraftPasteReadyTimeoutMs(agent, timeoutMs) const readinessResult = waitForAgentDraftInputReadyOnTab({ tabId, spawnTimeoutMs: PTY_SPAWN_TIMEOUT_MS, readinessTimeoutMs, - readySignal, - settings + readySignal }) // Why: a closed gate is the caller's own outcome to report; the bounded wait above just lapses. if (args.sendGate && !(await args.sendGate)) { @@ -143,7 +125,6 @@ export async function pasteDraftWhenAgentReady(args: { } return await sendBracketedPasteToAgent({ - settings, ptyId, content, submit: submit === true, @@ -154,7 +135,6 @@ export async function pasteDraftWhenAgentReady(args: { } export async function pasteDraftToAgentPtyWhenReady(args: { - tabId: string ptyId: string content: string agent?: TuiAgent @@ -164,31 +144,21 @@ export async function pasteDraftToAgentPtyWhenReady(args: { onTimeout?: () => void onUnconfirmedDelivery?: () => void }): Promise { - const { - tabId, - ptyId, - content, - agent, - submit, - forcePaste, - timeoutMs, - onTimeout, - onUnconfirmedDelivery - } = args + const { ptyId, content, agent, submit, forcePaste, timeoutMs, onTimeout, onUnconfirmedDelivery } = + args const agentConfig = agent ? TUI_AGENT_CONFIG[agent] : null if (agentDeliversDraftViaNativePrefill(agent, forcePaste)) { return false } - const settings = getSettingsForAgentTabRuntimeOwner(tabId) const readySignal = resolvePasteReadySignal(agentConfig, submit === true) const budget = resolveDraftPasteReadyTimeoutMs(agent, timeoutMs) - const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal, settings) + const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal) if (!ready) { const fallbackReady = agentConfig && agent !== 'codex' - ? await waitForExpectedAgentOnPty(ptyId, agentConfig.expectedProcess, 1000, settings) + ? await waitForExpectedAgentOnPty(ptyId, agentConfig.expectedProcess, 1000) : false if (!fallbackReady) { onTimeout?.() @@ -198,7 +168,6 @@ export async function pasteDraftToAgentPtyWhenReady(args: { } return await sendBracketedPasteToAgent({ - settings, ptyId, content, submit: submit === true, @@ -214,7 +183,6 @@ export async function submitPromptToAgentPty(args: { content: string }): Promise { return await sendBracketedPasteToAgent({ - settings: getSettingsForAgentTabRuntimeOwner(args.tabId), ptyId: args.ptyId, content: args.content, submit: true, @@ -230,21 +198,20 @@ export async function sendBracketedPasteToRunningAgent(args: { } async function sendBracketedPasteToAgent(args: { - settings?: Pick | null ptyId: string content: string submit: boolean agent?: TuiAgent inputKind: TerminalInputKind }): Promise { - const { settings = useAppStore.getState().settings, ptyId, content, submit, agent } = args + const { ptyId, content, submit, agent } = args const { inputKind } = args const submitRetryDelayMs = agent ? TUI_AGENT_CONFIG[agent]?.submitRetryDelayMs : undefined try { // Why: paste + Enter (+ retry Enter) must be one transaction, or a concurrent // paste on this PTY can slip between them and submit a half-written prompt. return await runTerminalPtyInputTransaction(ptyId, async () => { - const pasted = await sendAgentDraftPasteContentNow(settings, ptyId, content, inputKind) + const pasted = await sendAgentDraftPasteContentNow(ptyId, content, inputKind) if (!pasted || !submit) { return pasted } @@ -253,14 +220,14 @@ async function sendBracketedPasteToAgent(args: { // Enter arrive in the same PTY write. Split the submit into the next turn so // the TUI processes bracketed-paste termination before handling Enter. await new Promise((resolve) => window.setTimeout(resolve, POST_PASTE_SUBMIT_DELAY_MS)) - const submitted = await sendRuntimePtyInputVerified(settings, ptyId, '\r', inputKind) + const submitted = await sendRuntimePtyInputVerified(ptyId, '\r', inputKind) if (submitRetryDelayMs !== undefined) { // Why: agents that render their composer before Enter is live silently eat // the first Enter; the retry is best-effort and never downgrades `submitted`. await new Promise((resolve) => window.setTimeout(resolve, submitRetryDelayMs)) try { - await sendRuntimePtyInputVerified(settings, ptyId, '\r', inputKind) + await sendRuntimePtyInputVerified(ptyId, '\r', inputKind) } catch { // Why: a rejected retry leaves the first Enter's verdict untouched. } @@ -278,7 +245,6 @@ function waitForAgentDraftInputReadyOnTab(args: { spawnTimeoutMs: number readinessTimeoutMs: number readySignal: Parameters[2] - settings: Pick | null | undefined }): Promise<{ ptyId: string; ready: boolean } | null> { return new Promise((resolve) => { let selectedPtyId: string | null = null @@ -308,12 +274,9 @@ function waitForAgentDraftInputReadyOnTab(args: { unsubscribeStore?.() // Why: Zustand subscribers run inside updateTabPtyId. Registering the // sidecar here precedes the transport's immediate pre-handler drain. - void waitForAgentDraftInputReady( - ptyId, - args.readinessTimeoutMs, - args.readySignal, - args.settings - ).then((ready) => finish({ ptyId, ready })) + void waitForAgentDraftInputReady(ptyId, args.readinessTimeoutMs, args.readySignal).then( + (ready) => finish({ ptyId, ready }) + ) } const bindFromState = (state: ReturnType): void => { const ptyId = state.ptyIdsByTabId[args.tabId]?.[0] @@ -331,14 +294,13 @@ function waitForAgentDraftInputReadyOnTab(args: { async function waitForExpectedAgentOnPty( ptyId: string, expectedProcess: string, - timeoutMs: number, - settings: Pick | null | undefined + timeoutMs: number ): Promise { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { try { const process = await withDeadline( - inspectRuntimeTerminalProcess(settings, ptyId), + inspectRuntimeTerminalProcess(ptyId), Math.max(0, deadline - Date.now()) ) if (!process) { diff --git a/src/renderer/src/lib/agent-ready-wait.ts b/src/renderer/src/lib/agent-ready-wait.ts index 8bd7cb8677b..627e3520ab6 100644 --- a/src/renderer/src/lib/agent-ready-wait.ts +++ b/src/renderer/src/lib/agent-ready-wait.ts @@ -91,7 +91,7 @@ export async function waitForAgentReady( } try { - const process = await inspectRuntimeTerminalProcess(useAppStore.getState().settings, ptyId) + const process = await inspectRuntimeTerminalProcess(ptyId) const foreground = process.foregroundProcess?.toLowerCase() ?? '' if (isExpectedAgentProcess(foreground, expectedProcess)) { return { ready: true, reason: 'foreground-match' } diff --git a/src/renderer/src/lib/codex-session-restart-shell-flap.test.ts b/src/renderer/src/lib/codex-session-restart-shell-flap.test.ts index 1fd13260028..031747c3ea4 100644 --- a/src/renderer/src/lib/codex-session-restart-shell-flap.test.ts +++ b/src/renderer/src/lib/codex-session-restart-shell-flap.test.ts @@ -183,7 +183,7 @@ describe('spurious shell readings on Codex-launched panes', () => { }) it('never routes a remote runtime pane through the local confirm bridge', async () => { - seedPane({ ptyId: 'remote:term-1', launchAgent: 'codex' }) + seedPane({ ptyId: 'remote:env-1@@term-1', launchAgent: 'codex' }) useAppStore.setState({ settings: { activeRuntimeEnvironmentId: 'env-1' } as never }) runtimeEnvironmentCall.mockResolvedValue({ id: 'rpc-1', diff --git a/src/renderer/src/lib/codex-session-restart.test.ts b/src/renderer/src/lib/codex-session-restart.test.ts index 67a418acffe..d30fade6450 100644 --- a/src/renderer/src/lib/codex-session-restart.test.ts +++ b/src/renderer/src/lib/codex-session-restart.test.ts @@ -388,14 +388,14 @@ describe('markLiveCodexSessionsForRestart', () => { }) }) - it('inspects remote runtime PTYs through the active runtime environment', async () => { + it('inspects remote runtime PTYs on the server their id names', async () => { useAppStore.setState({ settings: { activeRuntimeEnvironmentId: 'env-1' } as never, tabsByWorktree: { wt1: [ { id: 'tab-1', - ptyId: 'remote:term-1', + ptyId: 'remote:env-1@@term-1', worktreeId: 'wt1', title: 'orca-1', customTitle: null, @@ -406,7 +406,7 @@ describe('markLiveCodexSessionsForRestart', () => { ] }, ptyIdsByTabId: { - 'tab-1': ['remote:term-1'] + 'tab-1': ['remote:env-1@@term-1'] } }) runtimeEnvironmentCall.mockResolvedValue({ @@ -434,7 +434,7 @@ describe('markLiveCodexSessionsForRestart', () => { params: { terminal: 'term-1' }, timeoutMs: 15_000 }) - expect(useAppStore.getState().codexRestartNoticeByPtyId['remote:term-1']).toEqual({ + expect(useAppStore.getState().codexRestartNoticeByPtyId['remote:env-1@@term-1']).toEqual({ previousAccountLabel: ACCOUNT_A, nextAccountLabel: ACCOUNT_B }) diff --git a/src/renderer/src/lib/codex-session-restart.ts b/src/renderer/src/lib/codex-session-restart.ts index ab6a83ce0cf..e5d42d51ade 100644 --- a/src/renderer/src/lib/codex-session-restart.ts +++ b/src/renderer/src/lib/codex-session-restart.ts @@ -92,7 +92,6 @@ async function readRecordedCodexPaneLanes( * null (scan failed or unsupported) both keep today's ineligible outcome. */ async function isConfirmedCodexForegroundDespiteShellReading( - state: AppState, ptyId: string, launchAgent: TuiAgent | undefined, inspection: RuntimeTerminalProcessInspection @@ -105,7 +104,7 @@ async function isConfirmedCodexForegroundDespiteShellReading( ) { return false } - const confirmed = await confirmRuntimeTerminalForegroundProcess(state.settings, ptyId) + const confirmed = await confirmRuntimeTerminalForegroundProcess(ptyId) return isCodexForegroundProcess(confirmed) } @@ -159,7 +158,7 @@ async function scanCodexPanes( laneSource: lane.source } } - const inspection = await inspectRuntimeTerminalProcess(state.settings, ptyId).then( + const inspection = await inspectRuntimeTerminalProcess(ptyId).then( (result) => result, // Why: one stale remote pane must not hide restart notices for other confirmed Codex panes. () => null @@ -167,12 +166,7 @@ async function scanCodexPanes( const eligible = inspection !== null && (isCodexRestartEligiblePane({ inspection, launchAgent: tab.launchAgent }) || - (await isConfirmedCodexForegroundDespiteShellReading( - state, - ptyId, - tab.launchAgent, - inspection - ))) + (await isConfirmedCodexForegroundDespiteShellReading(ptyId, tab.launchAgent, inspection))) return { ptyId, eligible, diff --git a/src/renderer/src/lib/launch-agent-background-session.ts b/src/renderer/src/lib/launch-agent-background-session.ts index 3bca34e60f2..74c572da522 100644 --- a/src/renderer/src/lib/launch-agent-background-session.ts +++ b/src/renderer/src/lib/launch-agent-background-session.ts @@ -244,7 +244,6 @@ export async function launchAgentBackgroundSession( throw new Error('Runtime terminal id is invalid.') } unsubscribeData = await subscribeToRuntimeTerminalData( - store.settings, ptyId, `desktop:background:${tab.id}`, handleData diff --git a/src/renderer/src/lib/launch-agent-new-tab-host-route-readiness.test.ts b/src/renderer/src/lib/launch-agent-new-tab-host-route-readiness.test.ts index 23206c87435..d2ab492be0c 100644 --- a/src/renderer/src/lib/launch-agent-new-tab-host-route-readiness.test.ts +++ b/src/renderer/src/lib/launch-agent-new-tab-host-route-readiness.test.ts @@ -48,7 +48,7 @@ vi.mock('@/components/terminal-pane/pty-pre-handler-buffer', () => ({ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ isRemoteRuntimePtyId: () => false, inspectRuntimeTerminalProcess: vi.fn(async () => null), - sendRuntimePtyInputVerified: async (_settings: unknown, _ptyId: string, data: string) => { + sendRuntimePtyInputVerified: async (_ptyId: string, data: string) => { testState.writes.push({ at: Date.now(), data }) return true } diff --git a/src/renderer/src/lib/new-workspace.test.ts b/src/renderer/src/lib/new-workspace.test.ts index d3ca4fa01e6..4baee9bf9d4 100644 --- a/src/renderer/src/lib/new-workspace.test.ts +++ b/src/renderer/src/lib/new-workspace.test.ts @@ -78,7 +78,6 @@ vi.mock('@/runtime/runtime-terminal-inspection', () => ({ })) vi.mock('@/lib/agent-paste-draft', () => ({ - getSettingsForAgentTabRuntimeOwner: () => store.settings, pasteDraftToAgentPtyWhenReady: mockPasteDraftToAgentPtyWhenReady })) @@ -294,7 +293,6 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { }) expect(mockSendRuntimePtyInputVerified).toHaveBeenCalledWith( - {}, 'pty-1', 'fix the spinner\r', 'launch' @@ -411,7 +409,6 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { }) expect(mockPasteDraftToAgentPtyWhenReady).toHaveBeenCalledWith({ - tabId: 'tab-1', ptyId: 'pty-1', content: 'review this before sending', agent: 'claude', @@ -455,7 +452,6 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { }) expect(mockPasteDraftToAgentPtyWhenReady).toHaveBeenCalledWith({ - tabId: 'agent-tab', ptyId: 'agent-pty', content: 'Linear context draft', agent: 'codex', @@ -512,7 +508,6 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { expect(mockPasteDraftToAgentPtyWhenReady).toHaveBeenCalledTimes(1) expect(mockPasteDraftToAgentPtyWhenReady).toHaveBeenCalledWith({ - tabId: 'tab-1', ptyId: 'pty-delayed', content: 'https://github.com/stablyai/orca/pull/2051', agent: 'codex', @@ -594,7 +589,6 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => { expect(mockPasteDraftToAgentPtyWhenReady).toHaveBeenCalledTimes(1) expect(mockPasteDraftToAgentPtyWhenReady).toHaveBeenCalledWith({ - tabId: 'tab-1', ptyId: 'startup-pty', content: 'linked draft', agent: 'codex', diff --git a/src/renderer/src/lib/new-workspace.ts b/src/renderer/src/lib/new-workspace.ts index 6d5cf5ed39e..927e23b2ae1 100644 --- a/src/renderer/src/lib/new-workspace.ts +++ b/src/renderer/src/lib/new-workspace.ts @@ -1,8 +1,5 @@ import { useAppStore } from '@/store' -import { - getSettingsForAgentTabRuntimeOwner, - pasteDraftToAgentPtyWhenReady -} from '@/lib/agent-paste-draft' +import { pasteDraftToAgentPtyWhenReady } from '@/lib/agent-paste-draft' import { sendFollowupPromptWhenAgentReady } from '@/lib/agent-followup-delivery' import { showAutomationPromptNotSentToast } from '@/lib/agent-background-session-timeout-toast' import type { AgentStartupPlan } from '@/lib/tui-agent-startup' @@ -245,12 +242,11 @@ export async function ensureAgentStartupInTerminal(args: { } async function deliverAgentStartupToTerminal( - tabId: string, + _tabId: string, ptyId: string, startup: AgentStartupPlan ): Promise { const draftPrompt = startup.draftPrompt ?? null - const runtimeSettings = getSettingsForAgentTabRuntimeOwner(tabId) // Why: followupPrompt is the legacy path for stdin-after-start agents // (aider, goose, etc.) that need their initial prompt typed into the live // session and submitted. Wait until the agent owns the PTY before writing. @@ -258,8 +254,7 @@ async function deliverAgentStartupToTerminal( const delivered = await sendFollowupPromptWhenAgentReady({ ptyId, expectedProcess: startup.expectedProcess, - prompt: startup.followupPrompt, - settings: runtimeSettings + prompt: startup.followupPrompt }) // Why: a dropped follow-up is otherwise silent — surface the same toast the // draft path uses so the user knows to open the workspace and paste it. @@ -273,7 +268,6 @@ async function deliverAgentStartupToTerminal( // launch-work-item-direct flow so both behave identically. if (draftPrompt) { await pasteDraftToAgentPtyWhenReady({ - tabId, ptyId, content: draftPrompt, agent: startup.agent, diff --git a/src/renderer/src/lib/startup-draft-input-kind.test.ts b/src/renderer/src/lib/startup-draft-input-kind.test.ts index a69ea68a103..935a92909b7 100644 --- a/src/renderer/src/lib/startup-draft-input-kind.test.ts +++ b/src/renderer/src/lib/startup-draft-input-kind.test.ts @@ -86,7 +86,6 @@ async function paneRouteKinds(): Promise { async function helperRouteKinds(): Promise { const { kinds } = stubPtyApi() const pasted = pasteDraftToAgentPtyWhenReady({ - tabId: 'tab-1', ptyId: 'pty-1', content: DRAFT, agent: 'aider' diff --git a/src/renderer/src/runtime/runtime-terminal-inspection.test.ts b/src/renderer/src/runtime/runtime-terminal-inspection.test.ts index d1d4dcc9fed..c32f4231731 100644 --- a/src/renderer/src/runtime/runtime-terminal-inspection.test.ts +++ b/src/renderer/src/runtime/runtime-terminal-inspection.test.ts @@ -109,14 +109,7 @@ describe('runtime terminal owner routing', () => { }) it('sends input through the PTY owning environment instead of the active one', async () => { - expect( - sendRuntimePtyInput( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - 'x', - 'driving' - ) - ).toBe(true) + expect(sendRuntimePtyInput('remote:env-1@@terminal-1', 'x', 'driving')).toBe(true) await vi.waitFor(() => { expect(runtimeCall).toHaveBeenCalledWith({ @@ -134,12 +127,10 @@ describe('runtime terminal owner routing', () => { }) it('inspects the PTY owning environment instead of the active one', async () => { - await expect( - inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1' - ) - ).resolves.toMatchObject({ foregroundProcess: 'bash', hasChildProcesses: true }) + await expect(inspectRuntimeTerminalProcess('remote:env-1@@terminal-1')).resolves.toMatchObject({ + foregroundProcess: 'bash', + hasChildProcesses: true + }) expect(runtimeCall).toHaveBeenCalledWith({ selector: 'env-1', @@ -156,11 +147,7 @@ describe('runtime terminal owner routing', () => { // scan and answered `unverifiable`, which the guard reads as running work -- a confirmation // dialog on every idle close of a remote Windows pane. Found by review on #18591. it('forwards scanChildProcesses to the PTY owning environment', async () => { - await inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - { scanChildProcesses: true } - ) + await inspectRuntimeTerminalProcess('remote:env-1@@terminal-1', { scanChildProcesses: true }) expect(runtimeCall).toHaveBeenCalledWith({ selector: 'env-1', @@ -171,11 +158,10 @@ describe('runtime terminal owner routing', () => { }) it('forwards scanChildProcesses alongside the incarnation fence', async () => { - await inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - { expectedIncarnationId: 'incarnation-1', scanChildProcesses: true } - ) + await inspectRuntimeTerminalProcess('remote:env-1@@terminal-1', { + expectedIncarnationId: 'incarnation-1', + scanChildProcesses: true + }) expect(runtimeCall).toHaveBeenCalledWith({ selector: 'env-1', @@ -190,10 +176,7 @@ describe('runtime terminal owner routing', () => { }) it('omits scanChildProcesses when the caller is only polling', async () => { - await inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1' - ) + await inspectRuntimeTerminalProcess('remote:env-1@@terminal-1') expect(runtimeCall).toHaveBeenCalledWith({ selector: 'env-1', @@ -212,12 +195,7 @@ describe('runtime terminal owner routing', () => { _meta: { runtimeId: 'runtime-1' } }) - await expect( - inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1' - ) - ).resolves.toEqual({ + await expect(inspectRuntimeTerminalProcess('remote:env-1@@terminal-1')).resolves.toEqual({ foregroundProcess: null, hasChildProcesses: false, verdict: 'unverifiable', @@ -232,7 +210,7 @@ describe('runtime terminal owner routing', () => { foregroundProcessEvidence: liveEvidence('pty-1', 'codex') }) - await expect(inspectRuntimeTerminalProcess(null, 'ssh:host@@pty-1')).resolves.toMatchObject({ + await expect(inspectRuntimeTerminalProcess('ssh:host@@pty-1')).resolves.toMatchObject({ foregroundProcess: 'codex', hasChildProcesses: true }) @@ -247,7 +225,7 @@ describe('runtime terminal owner routing', () => { hasChildProcesses: true }) - await expect(inspectRuntimeTerminalProcess(null, 'ssh:host@@pty-1')).resolves.toEqual({ + await expect(inspectRuntimeTerminalProcess('ssh:host@@pty-1')).resolves.toEqual({ foregroundProcess: null, hasChildProcesses: false, verdict: 'unverifiable', @@ -263,12 +241,7 @@ describe('runtime terminal owner routing', () => { error: { code, message: code } }) - await expect( - inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-stale' - ) - ).resolves.toEqual({ + await expect(inspectRuntimeTerminalProcess('remote:env-1@@terminal-stale')).resolves.toEqual({ foregroundProcess: null, hasChildProcesses: false, verdict: 'unverifiable', @@ -280,10 +253,7 @@ describe('runtime terminal owner routing', () => { it('maps lost contact and timeout to client-only unverifiable results', async () => { runtimeCall.mockRejectedValueOnce(new Error('SSH connection lost, reconnecting...')) await expect( - inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-transport' - ) + inspectRuntimeTerminalProcess('remote:env-1@@terminal-transport') ).resolves.toEqual({ foregroundProcess: null, hasChildProcesses: false, @@ -293,10 +263,7 @@ describe('runtime terminal owner routing', () => { runtimeCall.mockRejectedValueOnce(new Error('Request timed out before completion')) await expect( - inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-timeout' - ) + inspectRuntimeTerminalProcess('remote:env-1@@terminal-timeout') ).resolves.toMatchObject({ verdict: 'unverifiable', reason: 'timeout' }) }) @@ -319,12 +286,9 @@ describe('runtime terminal owner routing', () => { it('still throws an unclassified programming error', async () => { runtimeCall.mockRejectedValueOnce(new Error('inspection invariant violated')) - await expect( - inspectRuntimeTerminalProcess( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-bug' - ) - ).rejects.toThrow('inspection invariant violated') + await expect(inspectRuntimeTerminalProcess('remote:env-1@@terminal-bug')).rejects.toThrow( + 'inspection invariant violated' + ) }) it('records accepted fire-and-forget runtime input against the owning pane key', async () => { @@ -345,14 +309,7 @@ describe('runtime terminal owner routing', () => { } }) - expect( - sendRuntimePtyInput( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - 'x', - 'driving' - ) - ).toBe(true) + expect(sendRuntimePtyInput('remote:env-1@@terminal-1', 'x', 'driving')).toBe(true) await vi.waitFor(() => { expect(useAppStore.getState().lastTerminalInputAtByPaneKey[PANE_KEY]).toEqual( @@ -381,14 +338,7 @@ describe('runtime terminal owner routing', () => { recordRuntimeTerminalInputForPtyId('remote:env-1@@terminal-1', 123) useAppStore.setState({ lastTerminalInputAtByPaneKey: {} }) - expect( - sendRuntimePtyInput( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - 'x', - 'driving' - ) - ).toBe(true) + expect(sendRuntimePtyInput('remote:env-1@@terminal-1', 'x', 'driving')).toBe(true) const nextLeafId = '22222222-2222-4222-8222-222222222222' useAppStore.setState({ terminalLayoutsByTabId: { @@ -431,14 +381,7 @@ describe('runtime terminal owner routing', () => { } }) - expect( - sendRuntimePtyInput( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - 'x', - 'driving' - ) - ).toBe(true) + expect(sendRuntimePtyInput('remote:env-1@@terminal-1', 'x', 'driving')).toBe(true) await vi.waitFor(() => { expect(runtimeCall).toHaveBeenCalled() @@ -453,12 +396,7 @@ describe('runtime terminal owner routing', () => { }) await expect( - sendRuntimePtyInputVerified( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-stale', - 'x', - 'driving' - ) + sendRuntimePtyInputVerified('remote:env-1@@terminal-stale', 'x', 'driving') ).resolves.toBe(false) }) @@ -470,12 +408,7 @@ describe('runtime terminal owner routing', () => { }) await expect( - sendRuntimePtyInputVerified( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - 'x', - 'driving' - ) + sendRuntimePtyInputVerified('remote:env-1@@terminal-1', 'x', 'driving') ).resolves.toBe(false) expect(runtimeCall).toHaveBeenCalledWith({ @@ -493,9 +426,7 @@ describe('runtime terminal owner routing', () => { it('uses accepted local writes for verified input', async () => { localWriteAccepted.mockResolvedValue(true) - await expect( - sendRuntimePtyInputVerified({ activeRuntimeEnvironmentId: null }, 'local-pty', 'x', 'driving') - ).resolves.toBe(true) + await expect(sendRuntimePtyInputVerified('local-pty', 'x', 'driving')).resolves.toBe(true) expect(localWriteAccepted).toHaveBeenCalledWith('local-pty', 'x', 'driving') expect(localWrite).not.toHaveBeenCalled() @@ -504,9 +435,7 @@ describe('runtime terminal owner routing', () => { it('rejects oversized fire-and-forget local input before IPC writes', () => { const text = 'x'.repeat(TERMINAL_INPUT_MAX_BYTES + 1) - expect( - sendRuntimePtyInput({ activeRuntimeEnvironmentId: null }, 'local-pty', text, 'driving') - ).toBe(false) + expect(sendRuntimePtyInput('local-pty', text, 'driving')).toBe(false) expect(localWrite).not.toHaveBeenCalled() expect(localWriteAccepted).not.toHaveBeenCalled() @@ -515,14 +444,7 @@ describe('runtime terminal owner routing', () => { it('rejects oversized fire-and-forget remote input before runtime RPC', () => { const text = 'x'.repeat(TERMINAL_INPUT_MAX_BYTES + 1) - expect( - sendRuntimePtyInput( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - text, - 'driving' - ) - ).toBe(false) + expect(sendRuntimePtyInput('remote:env-1@@terminal-1', text, 'driving')).toBe(false) expect(runtimeTransportCall).not.toHaveBeenCalled() expect(localWrite).not.toHaveBeenCalled() @@ -533,9 +455,7 @@ describe('runtime terminal owner routing', () => { try { const text = 'x'.repeat(CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS + 1) - expect( - sendRuntimePtyInput({ activeRuntimeEnvironmentId: null }, 'local-pty', text, 'driving') - ).toBe(true) + expect(sendRuntimePtyInput('local-pty', text, 'driving')).toBe(true) expect(localWrite).not.toHaveBeenCalled() await vi.advanceTimersByTimeAsync(0) @@ -552,9 +472,7 @@ describe('runtime terminal owner routing', () => { try { const text = makeByteOversizedTerminalInput() - expect( - sendRuntimePtyInput({ activeRuntimeEnvironmentId: null }, 'local-pty', text, 'driving') - ).toBe(true) + expect(sendRuntimePtyInput('local-pty', text, 'driving')).toBe(true) await vi.runAllTimersAsync() @@ -568,14 +486,7 @@ describe('runtime terminal owner routing', () => { it('rejects oversized verified input before fallback fire-and-forget writes', async () => { const text = 'x'.repeat(TERMINAL_INPUT_MAX_BYTES + 1) - await expect( - sendRuntimePtyInputVerified( - { activeRuntimeEnvironmentId: null }, - 'local-pty', - text, - 'driving' - ) - ).resolves.toBe(false) + await expect(sendRuntimePtyInputVerified('local-pty', text, 'driving')).resolves.toBe(false) expect(localWriteAccepted).not.toHaveBeenCalled() expect(localWrite).not.toHaveBeenCalled() @@ -586,12 +497,7 @@ describe('runtime terminal owner routing', () => { localWriteAccepted.mockResolvedValue(true) try { const text = 'x'.repeat(CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS + 1) - const accepted = sendRuntimePtyInputVerified( - { activeRuntimeEnvironmentId: null }, - 'local-pty', - text, - 'driving' - ) + const accepted = sendRuntimePtyInputVerified('local-pty', text, 'driving') expect(localWriteAccepted).not.toHaveBeenCalled() @@ -609,12 +515,7 @@ describe('runtime terminal owner routing', () => { vi.useFakeTimers() try { const text = makeByteOversizedTerminalInput() - const accepted = sendRuntimePtyInputVerified( - { activeRuntimeEnvironmentId: null }, - 'local-pty', - text, - 'driving' - ) + const accepted = sendRuntimePtyInputVerified('local-pty', text, 'driving') await vi.runAllTimersAsync() @@ -645,12 +546,7 @@ describe('runtime terminal owner routing', () => { }) await expect( - sendRuntimePtyInputVerified( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - 'x', - 'driving' - ) + sendRuntimePtyInputVerified('remote:env-1@@terminal-1', 'x', 'driving') ).resolves.toBe(true) expect(useAppStore.getState().lastTerminalInputAtByPaneKey[PANE_KEY]).toEqual( @@ -677,12 +573,7 @@ describe('runtime terminal owner routing', () => { }) await expect( - sendRuntimePtyInputVerified( - { activeRuntimeEnvironmentId: 'env-2' }, - 'remote:env-1@@terminal-1', - 'x', - 'driving' - ) + sendRuntimePtyInputVerified('remote:env-1@@terminal-1', 'x', 'driving') ).resolves.toBe(false) expect(useAppStore.getState().lastTerminalInputAtByPaneKey[PANE_KEY]).toBeUndefined() @@ -848,9 +739,7 @@ describe('runtime terminal owner routing', () => { it('reports success after fallback fire-and-forget writes when local acceptance cannot be verified', async () => { localWriteAccepted.mockResolvedValue(false) - await expect( - sendRuntimePtyInputVerified({ activeRuntimeEnvironmentId: null }, 'local-pty', 'x', 'driving') - ).resolves.toBe(true) + await expect(sendRuntimePtyInputVerified('local-pty', 'x', 'driving')).resolves.toBe(true) expect(localWriteAccepted).toHaveBeenCalledWith('local-pty', 'x', 'driving') expect(localWrite).toHaveBeenCalledWith('local-pty', 'x', 'driving') diff --git a/src/renderer/src/runtime/runtime-terminal-inspection.ts b/src/renderer/src/runtime/runtime-terminal-inspection.ts index 9592988aee1..c6435187fc2 100644 --- a/src/renderer/src/runtime/runtime-terminal-inspection.ts +++ b/src/renderer/src/runtime/runtime-terminal-inspection.ts @@ -1,14 +1,14 @@ -import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RuntimeTerminalSend } from '../../../shared/runtime-types' import { isTerminalInputTooLargeWithDeferredMeasurement } from '../../../shared/terminal-input' import { recordRuntimeTerminalInputForPtyId } from './runtime-terminal-input-recording' export { recordRuntimeTerminalInputForPtyId } from './runtime-terminal-input-recording' -import { callRuntimeRpc, getActiveRuntimeTarget } from './runtime-rpc-client' +import { callRuntimeRpc } from './runtime-rpc-client' import { getRemoteRuntimePtyEnvironmentId, - getRemoteRuntimeTerminalHandle + getRemoteRuntimePtyOwner } from './runtime-terminal-stream' import { parseAppSshPtyId } from '../../../shared/ssh-pty-id' +import { isRemoteRuntimePtyId } from '../../../shared/remote-runtime-pty-id' import { classifyTerminalProcessInspectionFailure, clientOnlyUnverifiableInspection, @@ -67,17 +67,12 @@ function normalizeInspectionResult( } export async function inspectRuntimeTerminalProcess( - settings: Pick | null | undefined, ptyId: string, options?: { expectedIncarnationId?: string; scanChildProcesses?: boolean; steadyState?: boolean } ): Promise { - const ownerEnvironmentId = getRemoteRuntimePtyEnvironmentId(ptyId) - const target = ownerEnvironmentId - ? ({ kind: 'environment', environmentId: ownerEnvironmentId } as const) - : getActiveRuntimeTarget(settings) - const terminal = getRemoteRuntimeTerminalHandle(ptyId) + const owner = getRemoteRuntimePtyOwner(ptyId) const remote = isRemoteInspectionPtyId(ptyId) - if (target.kind !== 'environment' || !terminal) { + if (!owner) { try { const result = await (options ? window.api.pty.inspectProcess(ptyId, options) @@ -94,10 +89,10 @@ export async function inspectRuntimeTerminalProcess( try { const result = await callRuntimeRpc<{ process: RuntimeTerminalProcessInspection }>( - target, + owner.target, 'terminal.inspectProcess', { - terminal, + terminal: owner.terminal, ...(options?.expectedIncarnationId ? { expectedIncarnationId: options.expectedIncarnationId } : {}), @@ -126,14 +121,10 @@ export async function inspectRuntimeTerminalProcess( * must read as "no new evidence", never as a shell confirmation. */ export async function confirmRuntimeTerminalForegroundProcess( - settings: Pick | null | undefined, ptyId: string ): Promise { - const ownerEnvironmentId = getRemoteRuntimePtyEnvironmentId(ptyId) - const target = ownerEnvironmentId - ? ({ kind: 'environment', environmentId: ownerEnvironmentId } as const) - : getActiveRuntimeTarget(settings) - if (target.kind === 'environment' && getRemoteRuntimeTerminalHandle(ptyId)) { + // Why any remote id: an id without an owner segment still runs off this host, never locally. + if (isRemoteRuntimePtyId(ptyId)) { return null } const confirmForegroundProcess = window.api.pty.confirmForegroundProcess @@ -145,7 +136,6 @@ export async function confirmRuntimeTerminalForegroundProcess( } export function sendRuntimePtyInput( - settings: Pick | null | undefined, ptyId: string, data: string, inputKind: TerminalInputKind @@ -160,38 +150,33 @@ export function sendRuntimePtyInput( void tooLarge .then((resolvedTooLarge) => { if (!resolvedTooLarge) { - sendRuntimePtyInputWithinLimit(settings, ptyId, data, inputKind) + sendRuntimePtyInputWithinLimit(ptyId, data, inputKind) } }) .catch(() => {}) return true } - return sendRuntimePtyInputWithinLimit(settings, ptyId, data, inputKind) + return sendRuntimePtyInputWithinLimit(ptyId, data, inputKind) } // Why the kind reaches only the local write: terminal.send has no launch kind, and its query-reply // kind is for mobile clients, so the host classifies a desktop's environment write by its bytes. function sendRuntimePtyInputWithinLimit( - settings: Pick | null | undefined, ptyId: string, data: string, inputKind: TerminalInputKind ): boolean { - const ownerEnvironmentId = getRemoteRuntimePtyEnvironmentId(ptyId) - const target = ownerEnvironmentId - ? ({ kind: 'environment', environmentId: ownerEnvironmentId } as const) - : getActiveRuntimeTarget(settings) - const terminal = getRemoteRuntimeTerminalHandle(ptyId) - if (target.kind !== 'environment' || !terminal) { + const owner = getRemoteRuntimePtyOwner(ptyId) + if (!owner) { window.api.pty.write(ptyId, data, inputKind) recordRuntimeTerminalInputForPtyId(ptyId) return true } void callRuntimeRpc<{ send: RuntimeTerminalSend }>( - target, + owner.target, 'terminal.send', - { terminal, text: data, client: DESKTOP_RUNTIME_CLIENT }, + { terminal: owner.terminal, text: data, client: DESKTOP_RUNTIME_CLIENT }, { timeoutMs: 15_000 } ) .then((result) => { diff --git a/src/renderer/src/runtime/runtime-terminal-stream.test.ts b/src/renderer/src/runtime/runtime-terminal-stream.test.ts index 0224964456d..ed3c21d96e7 100644 --- a/src/renderer/src/runtime/runtime-terminal-stream.test.ts +++ b/src/renderer/src/runtime/runtime-terminal-stream.test.ts @@ -82,7 +82,6 @@ describe('remote runtime terminal data subscriptions', () => { const watcher = vi.fn() const dispose = await subscribeToRuntimeTerminalData( - { activeRuntimeEnvironmentId: 'env-fallback' }, 'remote:env-1@@terminal-1', 'watcher-1', watcher @@ -151,7 +150,6 @@ describe('remote runtime terminal data subscriptions', () => { it('echoes the opaque stream generation in cumulative source-range ACKs', async () => { const dispose = await subscribeToRuntimeTerminalData( - { activeRuntimeEnvironmentId: 'env-fallback' }, 'remote:env-1@@terminal-1', 'watcher-1', vi.fn() @@ -195,7 +193,6 @@ describe('remote runtime terminal data subscriptions', () => { it('can start at the live tail without replaying the initial snapshot', async () => { const watcher = vi.fn() const subscription = subscribeToRuntimeTerminalData( - { activeRuntimeEnvironmentId: 'env-fallback' }, 'remote:env-1@@terminal-1', 'watcher-1', watcher, @@ -248,13 +245,11 @@ describe('remote runtime terminal data subscriptions', () => { it('keeps the shared terminal multiplexer until the last watcher closes', async () => { const firstDispose = await subscribeToRuntimeTerminalData( - { activeRuntimeEnvironmentId: 'env-fallback' }, 'remote:env-1@@terminal-1', 'watcher-1', vi.fn() ) const secondDispose = await subscribeToRuntimeTerminalData( - { activeRuntimeEnvironmentId: 'env-fallback' }, 'remote:env-1@@terminal-2', 'watcher-2', vi.fn() @@ -276,12 +271,7 @@ describe('remote runtime terminal data subscriptions', () => { runtimeSubscribe.mockRejectedValueOnce(new Error('offline')) await expect( - subscribeToRuntimeTerminalData( - { activeRuntimeEnvironmentId: 'env-fallback' }, - 'remote:env-1@@terminal-1', - 'watcher-1', - vi.fn() - ) + subscribeToRuntimeTerminalData('remote:env-1@@terminal-1', 'watcher-1', vi.fn()) ).rejects.toThrow('offline') expect(sendBinary).not.toHaveBeenCalled() @@ -302,7 +292,6 @@ describe('remote runtime terminal data subscriptions', () => { }) const subscriptionPromise = subscribeToRuntimeTerminalData( - { activeRuntimeEnvironmentId: 'env-fallback' }, 'remote:env-1@@terminal-1', 'watcher-1', vi.fn() diff --git a/src/renderer/src/runtime/runtime-terminal-stream.ts b/src/renderer/src/runtime/runtime-terminal-stream.ts index 0c9604b71d1..844cc160b5f 100644 --- a/src/renderer/src/runtime/runtime-terminal-stream.ts +++ b/src/renderer/src/runtime/runtime-terminal-stream.ts @@ -1,5 +1,4 @@ -import type { GlobalSettings } from '../../../shared/global-settings-types' -import { RuntimeRpcCallError, getActiveRuntimeTarget } from './runtime-rpc-client' +import { RuntimeRpcCallError } from './runtime-rpc-client' import { getRemoteRuntimeTerminalMultiplexer } from './remote-runtime-terminal-multiplexer' import { parseRemoteRuntimePtyId } from '../../../shared/remote-runtime-pty-id' @@ -27,6 +26,22 @@ export function getRemoteRuntimePtyEnvironmentId(ptyId: string): string | null { return parseRemoteRuntimePtyId(ptyId)?.environmentId ?? null } +/** + * The server a remote PTY belongs to, named by the id itself. An id without an owner segment names + * no server, so it is never dialed on whichever server happens to be focused. + */ +export function getRemoteRuntimePtyOwner( + ptyId: string +): { target: { kind: 'environment'; environmentId: string }; terminal: string } | null { + const parsed = parseRemoteRuntimePtyId(ptyId) + return parsed?.environmentId && parsed.handle + ? { + target: { kind: 'environment', environmentId: parsed.environmentId }, + terminal: parsed.handle + } + : null +} + export function runtimeTerminalErrorMessage(error: unknown): string { if (error instanceof RuntimeRpcCallError) { return error.message @@ -35,20 +50,16 @@ export function runtimeTerminalErrorMessage(error: unknown): string { } export async function subscribeToRuntimeTerminalData( - settings: Pick | null | undefined, ptyId: string, clientId: string, watcher: (data: string) => void, options?: RuntimeTerminalDataSubscriptionOptions ): Promise<() => void> { - const terminal = getRemoteRuntimeTerminalHandle(ptyId) - const ownerEnvironmentId = getRemoteRuntimePtyEnvironmentId(ptyId) - const target = ownerEnvironmentId - ? ({ kind: 'environment', environmentId: ownerEnvironmentId } as const) - : getActiveRuntimeTarget(settings) - if (target.kind !== 'environment' || !terminal) { + const owner = getRemoteRuntimePtyOwner(ptyId) + if (!owner) { return () => {} } + const { target, terminal } = owner let resolveLiveTail: (() => void) | null = null let rejectLiveTail: ((error: Error) => void) | null = null diff --git a/src/renderer/src/runtime/runtime-terminal-verified-input.test.ts b/src/renderer/src/runtime/runtime-terminal-verified-input.test.ts index b5f4232bf0f..43cb5bca5a3 100644 --- a/src/renderer/src/runtime/runtime-terminal-verified-input.test.ts +++ b/src/renderer/src/runtime/runtime-terminal-verified-input.test.ts @@ -45,7 +45,7 @@ describe('verified input that requires provider settlement', () => { ] as const)('reads the %s remote verdict', async (_label, send, accepted) => { hostReply(send) await expect( - sendRuntimePtyInputVerified(null, REMOTE_PTY, 'x', 'driving', { + sendRuntimePtyInputVerified(REMOTE_PTY, 'x', 'driving', { requireWriteSettlement: true }) ).resolves.toBe(accepted) @@ -67,7 +67,7 @@ describe('verified input that requires provider settlement', () => { } }) await expect( - sendRuntimePtyInputVerified(null, REMOTE_PTY, 'x', 'driving', { + sendRuntimePtyInputVerified(REMOTE_PTY, 'x', 'driving', { requireWriteSettlement: true }) ).rejects.toThrow('acknowledgment unavailable') @@ -79,9 +79,9 @@ describe('verified input that requires provider settlement', () => { describe('local verified input', () => { it('keeps the fire-and-forget fallback for ordinary input the provider cannot accept', async () => { localWriteAccepted.mockResolvedValue(false) - await expect( - sendRuntimePtyInputVerified(null, 'ssh:conn-1@@pty-1', 'x', 'driving') - ).resolves.toBe(true) + await expect(sendRuntimePtyInputVerified('ssh:conn-1@@pty-1', 'x', 'driving')).resolves.toBe( + true + ) expect(localWriteAccepted).toHaveBeenCalledWith('ssh:conn-1@@pty-1', 'x', 'driving') expect(localWrite).toHaveBeenCalledWith('ssh:conn-1@@pty-1', 'x', 'driving') }) @@ -90,17 +90,36 @@ describe('local verified input', () => { const settled = { requireWriteSettlement: true } as const localWriteAccepted.mockResolvedValueOnce(true) await expect( - sendRuntimePtyInputVerified(null, 'ssh:conn-1@@pty-1', 'answer', 'driving', settled) + sendRuntimePtyInputVerified('ssh:conn-1@@pty-1', 'answer', 'driving', settled) ).resolves.toBe(true) localWriteAccepted.mockResolvedValueOnce(false) await expect( - sendRuntimePtyInputVerified(null, 'ssh:conn-1@@pty-1', '1', 'driving', settled) + sendRuntimePtyInputVerified('ssh:conn-1@@pty-1', '1', 'driving', settled) ).resolves.toBe(false) localWriteAccepted.mockRejectedValueOnce(new Error('acknowledgment unavailable')) await expect( - sendRuntimePtyInputVerified(null, 'ssh:conn-1@@pty-1', '\r', 'driving', settled) + sendRuntimePtyInputVerified('ssh:conn-1@@pty-1', '\r', 'driving', settled) ).rejects.toThrow('acknowledgment unavailable') expect(localWriteAccepted).toHaveBeenCalledWith('ssh:conn-1@@pty-1', '\r', 'driving', settled) expect(localWrite).not.toHaveBeenCalled() }) }) + +describe('owner of the PTY', () => { + it('never dials a server for a remote id that names no owner', async () => { + localWriteAccepted.mockResolvedValue(false) + // Why: this id used to reach whichever server was focused, even when another one owned it. + await sendRuntimePtyInputVerified('remote:terminal-1', 'x', 'driving', { + requireWriteSettlement: true + }) + expect(runtimeSend).not.toHaveBeenCalled() + }) + + it('sends to the server the id names', async () => { + hostReply({ accepted: true }) + await sendRuntimePtyInputVerified(REMOTE_PTY, 'x', 'driving') + expect(runtimeSend).toHaveBeenCalledWith( + expect.objectContaining({ selector: 'env-1', method: 'terminal.send' }) + ) + }) +}) diff --git a/src/renderer/src/runtime/runtime-terminal-verified-input.ts b/src/renderer/src/runtime/runtime-terminal-verified-input.ts index 62b72b137c8..df433c26155 100644 --- a/src/renderer/src/runtime/runtime-terminal-verified-input.ts +++ b/src/renderer/src/runtime/runtime-terminal-verified-input.ts @@ -1,14 +1,10 @@ -import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RuntimeTerminalSend } from '../../../shared/runtime-types' import type { TerminalInputKind } from '../../../shared/terminal-input-kind' import { isTerminalInputTooLargeWithDeferredMeasurement } from '../../../shared/terminal-input' import { readTerminalSendAcknowledgment } from '../../../shared/terminal-send-acknowledgment' import { classifyTerminalProcessInspectionFailure } from '../../../shared/terminal-process-inspection' -import { callRuntimeRpc, getActiveRuntimeTarget } from './runtime-rpc-client' -import { - getRemoteRuntimePtyEnvironmentId, - getRemoteRuntimeTerminalHandle -} from './runtime-terminal-stream' +import { callRuntimeRpc } from './runtime-rpc-client' +import { getRemoteRuntimePtyOwner } from './runtime-terminal-stream' import { recordRuntimeTerminalInputForPtyId } from './runtime-terminal-input-recording' const DESKTOP_RUNTIME_CLIENT = { id: 'orca-desktop', type: 'desktop' } as const @@ -18,7 +14,6 @@ const DESKTOP_RUNTIME_CLIENT = { id: 'orca-desktop', type: 'desktop' } as const * current host for its acknowledgment; a lost one rejects. */ export async function sendRuntimePtyInputVerified( - settings: Pick | null | undefined, ptyId: string, data: string, inputKind: TerminalInputKind, @@ -28,12 +23,8 @@ export async function sendRuntimePtyInputVerified( if (typeof tooLarge === 'boolean' ? tooLarge : await tooLarge) { return false } - const ownerEnvironmentId = getRemoteRuntimePtyEnvironmentId(ptyId) - const target = ownerEnvironmentId - ? ({ kind: 'environment', environmentId: ownerEnvironmentId } as const) - : getActiveRuntimeTarget(settings) - const terminal = getRemoteRuntimeTerminalHandle(ptyId) - if (target.kind !== 'environment' || !terminal) { + const owner = getRemoteRuntimePtyOwner(ptyId) + if (!owner) { if (options?.requireWriteSettlement) { // Why: an answer that dismisses its card must not fall back to an unacknowledged write. const accepted = await window.api.pty.writeAccepted(ptyId, data, inputKind, options) @@ -54,6 +45,7 @@ export async function sendRuntimePtyInputVerified( return accepted } + const { target, terminal } = owner try { const result = await callRuntimeRpc<{ send: RuntimeTerminalSend }>( target, diff --git a/src/renderer/src/runtime/web-session-tabs-agent-completion-notifications.test.ts b/src/renderer/src/runtime/web-session-tabs-agent-completion-notifications.test.ts index 72b28dd9130..fedd7255b6d 100644 --- a/src/renderer/src/runtime/web-session-tabs-agent-completion-notifications.test.ts +++ b/src/renderer/src/runtime/web-session-tabs-agent-completion-notifications.test.ts @@ -131,7 +131,6 @@ describe('paired session-tab agent completion notifications', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: makePaneKey(toWebTerminalSurfaceTabId(HOST_TAB_ID), LEAF_ID), getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -169,7 +168,6 @@ describe('paired session-tab agent completion notifications', () => { const coordinator = createAgentCompletionCoordinator({ paneKey, getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -263,7 +261,6 @@ describe('paired session-tab agent completion notifications', () => { const coordinator = createAgentCompletionCoordinator({ paneKey: makePaneKey(toWebTerminalSurfaceTabId(HOST_TAB_ID), LEAF_ID), getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true @@ -285,7 +282,6 @@ describe('paired session-tab agent completion notifications', () => { paneKey: makePaneKey(toWebTerminalSurfaceTabId(HOST_TAB_ID), LEAF_ID), statusLane: 'hook', getPtyId: () => 'pty-1', - getSettings: () => null, inspectProcess: vi.fn(), dispatchCompletion, isLive: () => true diff --git a/tests/e2e/__mocks__/native-chat-ssh-delivery.ts b/tests/e2e/__mocks__/native-chat-ssh-delivery.ts index d0ad5595954..e4765d31937 100644 --- a/tests/e2e/__mocks__/native-chat-ssh-delivery.ts +++ b/tests/e2e/__mocks__/native-chat-ssh-delivery.ts @@ -36,12 +36,15 @@ vi.mock('../../../src/main/ipc/pty/provider/registry', async (importOriginal) => }) vi.mock('@/store', () => ({ useAppStore: { getState: () => ({ terminalLayoutsByTabId: {} }) } })) vi.mock('@/runtime/runtime-rpc-client', () => ({ - getActiveRuntimeTarget: () => ({ kind: 'local' }), callRuntimeRpc: (...args: unknown[]) => io.rpc(...args) })) vi.mock('@/runtime/runtime-terminal-stream', () => ({ getRemoteRuntimePtyEnvironmentId: (id: string) => (id.startsWith('remote:') ? 'owner' : null), - getRemoteRuntimeTerminalHandle: (id: string) => (id.startsWith('remote:') ? 'terminal' : null) + getRemoteRuntimeTerminalHandle: (id: string) => (id.startsWith('remote:') ? 'terminal' : null), + getRemoteRuntimePtyOwner: (id: string) => + id.startsWith('remote:') + ? { target: { kind: 'environment', environmentId: 'owner' }, terminal: 'terminal' } + : null })) export function createSshDelivery(mode: 'accepted' | 'lost' | 'slow' = 'accepted') { diff --git a/tests/e2e/native-chat-ssh-delivery.unit.test.ts b/tests/e2e/native-chat-ssh-delivery.unit.test.ts index 76566838e82..806007eaf67 100644 --- a/tests/e2e/native-chat-ssh-delivery.unit.test.ts +++ b/tests/e2e/native-chat-ssh-delivery.unit.test.ts @@ -40,7 +40,7 @@ describe('prompt delivery through production IPC, provider and paired host', () [{ indices: [], other: text }] ) const onSettled = vi.fn() - sendNativeChatAskAnswer(null, ssh.id, groups, onSettled) + sendNativeChatAskAnswer(ssh.id, groups, onSettled) await vi.advanceTimersByTimeAsync(2000) expect(ssh.bytes).not.toContain('\r') expect(onSettled).not.toHaveBeenCalled() @@ -60,7 +60,6 @@ describe('prompt delivery through production IPC, provider and paired host', () close = ssh.close const onSettled = vi.fn() const handle = sendNativeChatAskAnswer( - null, ssh.id, [{ text: `${'X'.repeat(32768)}TAIL` }, { raw: '\r' }], onSettled @@ -78,7 +77,7 @@ describe('prompt delivery through production IPC, provider and paired host', () const ssh = createSshDelivery() close = ssh.close const onDeliverySettled = vi.fn() - sendNativeChatMessage(null, ssh.id, 'body', { onDeliverySettled }) + sendNativeChatMessage(ssh.id, 'body', { onDeliverySettled }) await vi.advanceTimersByTimeAsync(600) expect(ssh.bytes.join('')).toBe('\x15body\r') expect(onDeliverySettled).toHaveBeenCalledExactlyOnceWith(true) @@ -103,15 +102,9 @@ describe('prompt delivery through production IPC, provider and paired host', () } return reply.result }) - const pending = sendRuntimePtyInputVerified( - null, - 'remote:owner@@terminal', - '\x1b', - 'driving', - { - requireWriteSettlement: true - } - ) + const pending = sendRuntimePtyInputVerified('remote:owner@@terminal', '\x1b', 'driving', { + requireWriteSettlement: true + }) await (mode === 'accepted' ? expect(pending).resolves.toBe(true) : expect(pending).rejects.toThrow('acknowledgment unavailable')) @@ -145,15 +138,15 @@ describe('prompt delivery through production IPC, provider and paired host', () return reply.result }) await expect( - sendRuntimePtyInputVerified(null, 'remote:owner@@terminal', 'answer', 'driving', { + sendRuntimePtyInputVerified('remote:owner@@terminal', 'answer', 'driving', { requireWriteSettlement: true }) ).resolves.toBe(true) + await expect(sendNativeChatMessageVerified('remote:owner@@terminal', 'body')).resolves.toBe( + true + ) await expect( - sendNativeChatMessageVerified(null, 'remote:owner@@terminal', 'body') - ).resolves.toBe(true) - await expect( - sendAgentDraftPasteContentNow(null, 'remote:owner@@terminal', 'launch', 'launch') + sendAgentDraftPasteContentNow('remote:owner@@terminal', 'launch', 'launch') ).resolves.toBe(true) expect(ssh.bytes).toEqual(['answer', 'body', '\r', '\x1b[200~launch\x1b[201~']) expect(ssh.settlement).not.toHaveBeenCalled() @@ -164,12 +157,12 @@ describe('prompt delivery through production IPC, provider and paired host', () io.rpc.mockResolvedValue({ send: { handle: 'terminal', accepted: true, bytesWritten: 1 } }) const onWriteUnconfirmed = vi.fn() const onWriteRejected = vi.fn() - sendNativeChatMessage(null, 'remote:owner@@terminal', 'hello', { + sendNativeChatMessage('remote:owner@@terminal', 'hello', { onWriteRejected, onWriteUnconfirmed }) const onDeliverySettled = vi.fn() - sendNativeChatMessage(null, 'remote:owner@@terminal', 'answer', { onDeliverySettled }) + sendNativeChatMessage('remote:owner@@terminal', 'answer', { onDeliverySettled }) await vi.advanceTimersByTimeAsync(2000) expect(onWriteUnconfirmed).not.toHaveBeenCalled() expect(onWriteRejected).not.toHaveBeenCalled() @@ -179,7 +172,7 @@ describe('prompt delivery through production IPC, provider and paired host', () it('sends ordinary paired input without asking the host for provider settlement', async () => { io.rpc.mockResolvedValue({ send: { handle: 'terminal', accepted: true, bytesWritten: 4 } }) await expect( - sendRuntimePtyInputVerified(null, 'remote:owner@@terminal', 'body', 'driving') + sendRuntimePtyInputVerified('remote:owner@@terminal', 'body', 'driving') ).resolves.toBe(true) expect(io.rpc).toHaveBeenCalledWith( expect.anything(), @@ -253,7 +246,7 @@ describe('prompt delivery through production IPC, provider and paired host', () [{ indices: [], other: 'custom answer' }] ) const onSettled = vi.fn() - sendNativeChatAskAnswer(null, 'remote:owner@@terminal', groups, onSettled) + sendNativeChatAskAnswer('remote:owner@@terminal', groups, onSettled) await vi.advanceTimersByTimeAsync(4000) expect(ssh.bytes).toEqual(groups.map((group) => ('raw' in group ? group.raw : group.text))) expect(ssh.settlement).not.toHaveBeenCalled() diff --git a/tests/e2e/remote-agent-completion-authority.unit.test.ts b/tests/e2e/remote-agent-completion-authority.unit.test.ts index 3b4fa8c436b..8949886ebd5 100644 --- a/tests/e2e/remote-agent-completion-authority.unit.test.ts +++ b/tests/e2e/remote-agent-completion-authority.unit.test.ts @@ -50,7 +50,6 @@ describe('remote agent completion authority', () => { getPtyId: () => REMOTE_PTY_ID, isRemotePtyId: () => true, getExpectedIncarnationId: () => 'inc-remote', - getSettings: () => ({ activeRuntimeEnvironmentId: 'remote-host' }), inspectProcess: inspectRuntimeTerminalProcess, dispatchCompletion, isLive: () => true @@ -71,7 +70,6 @@ describe('remote agent completion authority', () => { getPtyId: () => `${REMOTE_PTY_ID}-${index}`, isRemotePtyId: () => true, getExpectedIncarnationId: () => 'inc-remote', - getSettings: () => ({ activeRuntimeEnvironmentId: 'remote-host' }), inspectProcess: inspectRuntimeTerminalProcess, dispatchCompletion: vi.fn(), isLive: () => true @@ -92,7 +90,6 @@ describe('remote agent completion authority', () => { getPtyId: () => REMOTE_PTY_ID, isRemotePtyId: () => true, getExpectedIncarnationId: () => 'inc-remote', - getSettings: () => ({ activeRuntimeEnvironmentId: 'remote-host' }), inspectProcess: inspectRuntimeTerminalProcess, dispatchCompletion, isLive: () => true @@ -116,7 +113,6 @@ describe('remote agent completion authority', () => { getPtyId: () => REMOTE_PTY_ID, isRemotePtyId: () => true, getExpectedIncarnationId: () => 'inc-remote', - getSettings: () => ({ activeRuntimeEnvironmentId: 'remote-host' }), inspectProcess: inspectRuntimeTerminalProcess, dispatchCompletion, isLive: () => true @@ -143,7 +139,6 @@ describe('remote agent completion authority', () => { getPtyId: () => REMOTE_PTY_ID, isRemotePtyId: () => true, getExpectedIncarnationId: () => 'inc-remote', - getSettings: () => ({ activeRuntimeEnvironmentId: 'remote-host' }), inspectProcess: inspectRuntimeTerminalProcess, dispatchCompletion, isLive: () => true @@ -169,7 +164,6 @@ describe('remote agent completion authority', () => { getPtyId: () => REMOTE_PTY_ID, isRemotePtyId: () => true, getExpectedIncarnationId: () => 'inc-remote', - getSettings: () => ({ activeRuntimeEnvironmentId: 'remote-host' }), inspectProcess: inspectRuntimeTerminalProcess, dispatchCompletion, isLive: () => true @@ -211,7 +205,6 @@ describe('remote agent completion authority', () => { getPtyId: () => REMOTE_PTY_ID, isRemotePtyId: () => true, getExpectedIncarnationId: () => 'inc-remote', - getSettings: () => ({ activeRuntimeEnvironmentId: 'remote-host' }), inspectProcess: inspectRuntimeTerminalProcess, dispatchCompletion: (_title: string, meta?: AgentCompletionDispatchMeta) => { outcomes.push({