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({