diff --git a/src/main/agent-launch/agent-launch-executor.test.ts b/src/main/agent-launch/agent-launch-executor.test.ts index c27d84cf037..b917dff661b 100644 --- a/src/main/agent-launch/agent-launch-executor.test.ts +++ b/src/main/agent-launch/agent-launch-executor.test.ts @@ -15,9 +15,7 @@ import type { AgentLaunchIntent, AgentLaunchResult } from '../../shared/agent-la import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' const STRUCTURED_PREFERENCE = { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } function harness(options: { @@ -732,7 +730,7 @@ describe('the surface is published as the launch stands, before its prompt is de describe('a new local worktree whose startup terminal did not come up', () => { it('opens its agent in the view a local workspace allows, as an existing one would', async () => { const h = harness({ - settings: { experimentalNativeChat: true, openAgentTabsInChatByDefault: true } + settings: { experimentalNativeChat: true } }) h.createWorktree.mockImplementationOnce(async () => ({ worktreeId: 'wt-new', @@ -742,8 +740,7 @@ describe('a new local worktree whose startup terminal did not come up', () => { await h.run({ ...CREATE_INTENT, agent: 'opencode' }) - expect(h.createTerminalAgent).toHaveBeenCalledWith( - expect.objectContaining({ viewMode: 'chat' }) - ) + expect(h.createStructuredSession).toHaveBeenCalled() + expect(h.createTerminalAgent).not.toHaveBeenCalled() }) }) diff --git a/src/main/agent-launch/agent-launch-executor.ts b/src/main/agent-launch/agent-launch-executor.ts index 642e5991414..f33e85b92b0 100644 --- a/src/main/agent-launch/agent-launch-executor.ts +++ b/src/main/agent-launch/agent-launch-executor.ts @@ -47,7 +47,6 @@ import { type WorkspaceLaunchKind } from '../../shared/workspace-launch-kind' import type { OrcaRuntimeService } from '../runtime/orca-runtime' -import { deriveAgentLaunchTerminalViewMode } from './agent-launch-view-mode' import { isDefinitiveAgentSessionCreateRefusal } from '../../shared/agent-session-definitive-refusal' import { decideAgentLaunchMode, @@ -353,12 +352,7 @@ async function createTerminalSurface( agent: intent.agent, ...(startupPrompt ? { startupPrompt } : {}), ...terminalLaunchInputs(intent), - viewMode: deriveAgentLaunchTerminalViewMode({ - settings: readAgentLaunchModeSettings(execution.runtime), - agent: intent.agent, - ...(intent.prompt ? { prompt: intent.prompt } : {}), - connectionId: workspace.connectionId - }) + viewMode: 'terminal' }) return { outcome: { diff --git a/src/main/agent-launch/agent-launch-mode.ts b/src/main/agent-launch/agent-launch-mode.ts index be3837f97b6..523ad1fe0e8 100644 --- a/src/main/agent-launch/agent-launch-mode.ts +++ b/src/main/agent-launch/agent-launch-mode.ts @@ -26,7 +26,7 @@ import type { import { RUNTIME_CAPABILITIES } from '../../shared/protocol-version' import { STRUCTURED_AGENT_RUNTIME_REGISTRATIONS } from '../runtime/structured-agent-runtime-registrations' import { - prefersStructuredNativeChatByDefault, + isNativeChatEnabled, resolveStructuredNativeChatSupport, type NativeChatDefaultSettings, type StructuredNativeChatBlocker @@ -137,7 +137,7 @@ export function decideAgentLaunchMode(args: { }): AgentLaunchModeReceipt { const { placement, settings } = args const vocabulary = args.vocabulary ?? DEFAULT_LAUNCH_VOCABULARY - if (!prefersStructuredNativeChatByDefault(settings)) { + if (!isNativeChatEnabled(settings)) { return { mode: 'terminal', preferred: 'terminal', diff --git a/src/main/agent-launch/agent-launch-view-mode.ts b/src/main/agent-launch/agent-launch-view-mode.ts deleted file mode 100644 index f7e6d565b38..00000000000 --- a/src/main/agent-launch/agent-launch-view-mode.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Which view a terminal agent tab opens in — the terminal itself or its chat view — decided on the - * host by the rule the window applies to a tab it creates, so a launch from the phone, the CLI or the - * window opens the same way. Never a caller field. - */ - -import type { AgentLaunchPrompt } from '../../shared/agent-launch-intent' -import type { GlobalSettings } from '../../shared/global-settings-types' -import { decideInitialAgentTabViewMode } from '../../shared/native-chat-initial-view-mode' -import { isNativeChatTranscriptLocalReadable } from '../../shared/native-chat-transcript-readability' -import type { TuiAgent } from '../../shared/tui-agent' - -export type AgentLaunchTerminalViewMode = 'terminal' | 'chat' - -/** Always explicit: an absent view lets the window re-derive it without the prompt's delivery. */ -export function deriveAgentLaunchTerminalViewMode(args: { - settings: Partial< - Pick - > | null - agent: TuiAgent - prompt?: AgentLaunchPrompt - /** The workspace's SSH connection; `null` is local. */ - connectionId: string | null | undefined -}): AgentLaunchTerminalViewMode { - const viewMode = decideInitialAgentTabViewMode({ - experimentalNativeChat: args.settings?.experimentalNativeChat, - openAgentTabsInChatByDefault: args.settings?.openAgentTabsInChatByDefault, - agent: args.agent, - ...(args.prompt - ? { - promptDelivery: args.prompt.delivery === 'draft' ? 'draft' : 'auto-submit', - launchDraftText: args.prompt.text - } - : {}), - nativeChatTranscriptIsLocalReadable: - args.connectionId !== undefined && isNativeChatTranscriptLocalReadable(args.connectionId) - }) - return viewMode === 'chat' ? 'chat' : 'terminal' -} diff --git a/src/main/ipc/runtime.test.ts b/src/main/ipc/runtime.test.ts index ece4a272f9b..8820e7c93c4 100644 --- a/src/main/ipc/runtime.test.ts +++ b/src/main/ipc/runtime.test.ts @@ -172,7 +172,7 @@ describe('registerRuntimeHandlers', () => { } const runtime = { getRuntimeId: vi.fn().mockReturnValue('runtime-1'), - getClientSettings: vi.fn(() => ({ experimentalStructuredNativeChat: true })), + getClientSettings: vi.fn(() => ({ experimentalNativeChat: true })), restoreStructuredAgentSessionTabs: vi.fn(async () => undefined), listMobileSessionTabs: vi.fn(async () => ({ worktree: 'workspace-1', diff --git a/src/main/persistence/applying-settings/terminal-settings-migrations.ts b/src/main/persistence/applying-settings/terminal-settings-migrations.ts index 8f2cc0dc5ca..3ed0b25f84b 100644 --- a/src/main/persistence/applying-settings/terminal-settings-migrations.ts +++ b/src/main/persistence/applying-settings/terminal-settings-migrations.ts @@ -64,6 +64,8 @@ type RetiredGlobalSettings = { experimentalManagedServers?: unknown // Why: #22551 kept this key in settings; it now lives in a main-owned store and must never ride along. opencodeGoApiKey?: unknown + experimentalStructuredNativeChat?: unknown + openAgentTabsInChatByDefault?: unknown } export function stripRetiredGlobalSettings( @@ -75,6 +77,8 @@ export function stripRetiredGlobalSettings( showAgentsSidebar: _legacyShowAgentsSidebar, experimentalManagedServers: _retiredManagedServersExperiment, opencodeGoApiKey: _legacyOpenCodeGoApiKey, + experimentalStructuredNativeChat: _legacyStructuredNativeChat, + openAgentTabsInChatByDefault: _legacyChatDefaultView, ...rest } = (settings ?? {}) as Partial & RetiredGlobalSettings void _legacyScrollbackBytes @@ -82,6 +86,8 @@ export function stripRetiredGlobalSettings( void _legacyShowAgentsSidebar void _retiredManagedServersExperiment void _legacyOpenCodeGoApiKey + void _legacyStructuredNativeChat + void _legacyChatDefaultView return rest } diff --git a/src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts b/src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts index 7cbce85fe9b..0db64a75fb8 100644 --- a/src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts +++ b/src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts @@ -45,6 +45,19 @@ describe('retired managed servers experiment', () => { }) }) +describe('retired chat default selectors', () => { + it('keeps Chat UI on while dropping both older keys from a saved profile', () => { + const normalized = normalizeLegacyProfile({ + experimentalNativeChat: true, + experimentalStructuredNativeChat: false, + openAgentTabsInChatByDefault: false + }) + expect(normalized.experimentalNativeChat).toBe(true) + expect(normalized).not.toHaveProperty('experimentalStructuredNativeChat') + expect(normalized).not.toHaveProperty('openAgentTabsInChatByDefault') + }) +}) + describe('structured chat shell environment settings', () => { it('keeps a valid saved list and an explicit opt-out', () => { const normalized = normalizeLegacyProfile({ diff --git a/src/main/runtime/claude-structured-session-integration.test.ts b/src/main/runtime/claude-structured-session-integration.test.ts index 7dc6629a566..b3acbb0548d 100644 --- a/src/main/runtime/claude-structured-session-integration.test.ts +++ b/src/main/runtime/claude-structured-session-integration.test.ts @@ -227,7 +227,7 @@ beforeEach(async () => { hookServer = new AgentHookServer() const runtime = { getRuntimeId: () => 'runtime-1', - getClientSettings: () => ({ experimentalStructuredNativeChat: true }), + getClientSettings: () => ({ experimentalNativeChat: true }), getStructuredAgentSessionCreateSupport: async () => ({ supported: true }), resolveStructuredAgentSessionCreateIntent: async (input: { envelope: unknown }) => ({ ...ensureParams(1), diff --git a/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts b/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts index a823e7d4591..9f9909c7ec4 100644 --- a/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts +++ b/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts @@ -6,8 +6,21 @@ import { electronMocks } from '../orca-runtime-test-mocks.spec' import { deferred, store } from '../orca-runtime-test-fixtures.spec' +import { RuntimeClientSettingsController } from '../runtime-client-settings' describe('OrcaRuntimeService', () => { + it.each([true, false])('projects the single Chat UI switch to older clients (%s)', (enabled) => { + const controller = new RuntimeClientSettingsController({ + getSettings: () => ({ ...store.getSettings(), experimentalNativeChat: enabled }), + updateSettings: () => undefined + }) + expect(controller.get()).toMatchObject({ + experimentalNativeChat: enabled, + experimentalStructuredNativeChat: enabled, + openAgentTabsInChatByDefault: false + }) + }) + it('projects runtime-backed settings to paired clients', () => { const terminalQuickCommands = [ { diff --git a/src/main/runtime/rpc/methods/agent-launch-caller-renders.test.ts b/src/main/runtime/rpc/methods/agent-launch-caller-renders.test.ts index 9ad8705529b..cfc81c0d4ab 100644 --- a/src/main/runtime/rpc/methods/agent-launch-caller-renders.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch-caller-renders.test.ts @@ -91,7 +91,7 @@ describe('a launch the caller cannot show as a chat', () => { describe('Grok with the structured-chat setting off', () => { it('opens as a terminal for every caller, as Claude and Codex do', async () => { - const off = { ...STRUCTURED_PREFERENCE, experimentalStructuredNativeChat: false } + const off = { ...STRUCTURED_PREFERENCE, experimentalNativeChat: false } for (const agent of ['grok', 'claude']) { const { result, runtime } = await launchInto(agent, {}, off) expect(result.outcome).toMatchObject({ kind: 'terminal' }) diff --git a/src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts b/src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts index f51e53369ea..b235bac0e08 100644 --- a/src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts @@ -91,7 +91,7 @@ describe('agent.launch with the real floating workspace resolver', () => { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the launch reads only these preferences and optional agentCmdOverrides; no other settings consumer runs because terminal creation is stubbed. { ...STRUCTURED_PREFERENCE, - openAgentTabsInChatByDefault: structuredPreference + experimentalNativeChat: structuredPreference } as ReturnType ) const scope = vi.spyOn(runtime, 'showTerminalWorkspaceLaunchScope') diff --git a/src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts b/src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts index 75a801d7598..17e9ffdaef2 100644 --- a/src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts @@ -780,25 +780,34 @@ describe('whose view moves', () => { }) describe('the view the tab opens in', () => { - const CHAT_VIEW = { experimentalNativeChat: true, openAgentTabsInChatByDefault: true } + const TERMINAL_VIEW = { experimentalNativeChat: false } - it('is derived on the host, the same for the shown tab and the spawn', async () => { - const runtime = hostWithWindow({ settings: CHAT_VIEW }) + it('keeps a terminal launch in the same view for the shown tab and spawn', async () => { + const runtime = hostWithWindow({ settings: TERMINAL_VIEW }) await replayLaunch(runtime, {}, CLI) - expect(runtime.published[0]?.viewMode).toBe('chat') - expect(terminalOptions(runtime)).toMatchObject({ viewMode: 'chat' }) + expect(runtime.published[0]?.viewMode).toBe('terminal') + expect(terminalOptions(runtime)).toMatchObject({ viewMode: 'terminal' }) }) - it('stays the terminal for a draft the chat view cannot mirror', async () => { - const runtime = hostWithWindow({ settings: CHAT_VIEW }) + it('keeps a terminal draft in terminal view regardless of mirrorability', async () => { + const runtime = hostWithWindow({ settings: TERMINAL_VIEW }) await replayLaunch(runtime, { prompt: { text: ' ', delivery: 'draft' } }, CLI) expect(runtime.published[0]?.viewMode).toBe('terminal') expect(terminalOptions(runtime)).toMatchObject({ viewMode: 'terminal' }) }) + + it('publishes a terminal fallback in terminal view while Chat UI is on', async () => { + const runtime = hostWithWindow({ settings: { experimentalNativeChat: true } }) + + await replayLaunch(runtime, { agent: 'gemini' }, CLI) + + expect(runtime.published[0]?.viewMode).toBe('terminal') + expect(terminalOptions(runtime)).toMatchObject({ viewMode: 'terminal' }) + }) }) it('hands the window the prompt, so a pane whose agent did not start can offer to copy it', async () => { diff --git a/src/main/runtime/rpc/methods/agent-launch-replay.test.ts b/src/main/runtime/rpc/methods/agent-launch-replay.test.ts index 7d18b2d2f7c..712b55adb3f 100644 --- a/src/main/runtime/rpc/methods/agent-launch-replay.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch-replay.test.ts @@ -267,9 +267,7 @@ describe('a replay answers from the record', () => { // now say the user prefers a terminal; the recorded one still says what actually ran. const movedSettings = runtimeStub({ settings: { - experimentalNativeChat: false, - experimentalStructuredNativeChat: false, - openAgentTabsInChatByDefault: false + experimentalNativeChat: false } }) const replayed = await launch(params, movedSettings, PAIRED_CLIENT) diff --git a/src/main/runtime/rpc/methods/agent-launch-tab-publication.ts b/src/main/runtime/rpc/methods/agent-launch-tab-publication.ts index fd4d29bd547..ba48fada691 100644 --- a/src/main/runtime/rpc/methods/agent-launch-tab-publication.ts +++ b/src/main/runtime/rpc/methods/agent-launch-tab-publication.ts @@ -43,7 +43,6 @@ import { decideAgentLaunchMode, readAgentLaunchModeSettings } from '../../../agent-launch/agent-launch-mode' -import { deriveAgentLaunchTerminalViewMode } from '../../../agent-launch/agent-launch-view-mode' import type { RpcContext } from '../core' import type { AgentLaunchParams } from './agent-launch-schemas' import { agentLaunchOperationCallerKey } from './agent-launch-replay' @@ -199,12 +198,7 @@ export async function publishAgentLaunchTabEarly( tabId: pane.tabId, leafId: pane.leafId, launchAgent: params.agent, - viewMode: deriveAgentLaunchTerminalViewMode({ - settings, - agent: params.agent, - ...(params.prompt ? { prompt: params.prompt } : {}), - connectionId: workspace.connectionId - }), + viewMode: 'terminal', ...(params.placement ? { placement: params.placement } : {}), viewer: agentLaunchTabViewerRule(context, params.presentation), ...(params.prompt?.text ? { prompt: params.prompt.text } : {}), diff --git a/src/main/runtime/rpc/methods/agent-launch.test-fixture.ts b/src/main/runtime/rpc/methods/agent-launch.test-fixture.ts index d41f250ad96..348cfed86d9 100644 --- a/src/main/runtime/rpc/methods/agent-launch.test-fixture.ts +++ b/src/main/runtime/rpc/methods/agent-launch.test-fixture.ts @@ -17,9 +17,7 @@ import type { } from '../../../../shared/agent-launch-tab-publication' export const STRUCTURED_PREFERENCE = { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } export type AgentLaunchRuntimeStubOptions = { diff --git a/src/main/runtime/rpc/methods/agent-launch.test.ts b/src/main/runtime/rpc/methods/agent-launch.test.ts index 581a8611560..5bc9b3c98ab 100644 --- a/src/main/runtime/rpc/methods/agent-launch.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch.test.ts @@ -548,8 +548,8 @@ describe('the terminal factory', () => { expect(runtime.createTerminal).toHaveBeenCalledWith('id:wt-new', { startupAgent: 'claude', - // The host derives the tab's first view by the window's rule; chat view is on by default here. - viewMode: 'chat', + // A refused structured session opens its fallback tab as a terminal. + viewMode: 'terminal', // A paired device's launch never moves the desktop window. surfaceOwner: false, onPtySpawnDispatched: expect.any(Function) diff --git a/src/main/runtime/rpc/methods/client-ui.test.ts b/src/main/runtime/rpc/methods/client-ui.test.ts index cf47bc56454..0894fb7d7de 100644 --- a/src/main/runtime/rpc/methods/client-ui.test.ts +++ b/src/main/runtime/rpc/methods/client-ui.test.ts @@ -32,7 +32,6 @@ describe('client UI RPC methods', () => { visibleTaskProviders: ['github', 'gitlab'], defaultRepoSelection: ['repo-1'], defaultLinearTeamSelection: ['team-1'], - experimentalStructuredNativeChat: true, compactWorktreeCards: true, minimaxGroupId: 'group-42', minimaxUsageModels: 'general,abab6.5', @@ -71,7 +70,7 @@ describe('client UI RPC methods', () => { const dispatcher = new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS }) const response = await dispatcher.dispatch( - makeRequest('settings.update', { experimentalStructuredNativeChat: true }) + makeRequest('settings.update', { experimentalNativeChat: true }) ) expect(response).toMatchObject({ diff --git a/src/main/runtime/rpc/methods/orchestration-worker-mode-opacity.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-mode-opacity.test.ts index 7852626c0a1..6724ce93e22 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-mode-opacity.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-mode-opacity.test.ts @@ -60,13 +60,11 @@ vi.mock('./orchestration-structured-worker-session', async (importOriginal) => ( const STRUCTURED_DEFAULT = { experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true, agentCmdOverrides: {}, agentDefaultArgs: {}, agentDefaultEnv: {} } -const TERMINAL_DEFAULT = { ...STRUCTURED_DEFAULT, experimentalStructuredNativeChat: false } +const TERMINAL_DEFAULT = { ...STRUCTURED_DEFAULT, experimentalNativeChat: false } /** A coordinator that IS a structured session: registry identity plus a live durable record. */ function installStructuredCoordinator(handle: string, sessionId: string): string { diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-mode-selection.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-mode-selection.test.ts index 271071d5970..ecdf276d2dc 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-mode-selection.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-mode-selection.test.ts @@ -40,8 +40,6 @@ vi.mock('./orchestration-structured-worker-session', async (importOriginal) => ( const STRUCTURED_DEFAULT = { experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true, agentCmdOverrides: {}, agentDefaultArgs: {}, agentDefaultEnv: {} @@ -171,10 +169,7 @@ describe('worker-start honours the settings default', () => { }) it('starts a terminal agent worker when it is not', async () => { - const result = await startWorker({ - ...STRUCTURED_DEFAULT, - experimentalStructuredNativeChat: false - }) + const result = await startWorker({ experimentalNativeChat: false }) expect(result).toMatchObject({ state: 'ready', @@ -253,7 +248,7 @@ describe('worker-start honours the settings default', () => { const created = mockWorktreeCreation() const result = await startWorker( - { ...STRUCTURED_DEFAULT, experimentalStructuredNativeChat: false }, + { ...STRUCTURED_DEFAULT, experimentalNativeChat: false }, { worktree: 'new-child', name: 'worker-child' } ) diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts index aee3bfefb6d..e11bcb25758 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts @@ -14,9 +14,7 @@ import { } from './orchestration-worker-start-mode' const STRUCTURED_DEFAULT = { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true + experimentalNativeChat: true } function decide( @@ -42,8 +40,8 @@ describe('worker start mode from the user default', () => { it.each([ ['native chat off', { ...STRUCTURED_DEFAULT, experimentalNativeChat: false }], - ['chat-by-default off', { ...STRUCTURED_DEFAULT, openAgentTabsInChatByDefault: false }], - ['structured off', { ...STRUCTURED_DEFAULT, experimentalStructuredNativeChat: false }], + ['chat-by-default off', { ...STRUCTURED_DEFAULT, experimentalNativeChat: false }], + ['structured off', { ...STRUCTURED_DEFAULT, experimentalNativeChat: false }], ['no settings at all', null] ])('starts a terminal worker when %s', (_name, settings) => { expect(decide({ settings })).toMatchObject({ diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts index 4648880586b..b950ad51eab 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts @@ -21,9 +21,7 @@ import { } from './orchestration-worker-start-mode' const STRUCTURED_PREFERENCE = { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } as const function structuredReceipt(): WorkerStartModeReceipt { diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts index 175f650c7a1..9d24f3dad1e 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts @@ -11,9 +11,7 @@ import { import { createStructuredWorkerSessionForWorktree } from './orchestration/worker/worker-topology' const STRUCTURED_PREFERENCE = { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } as const describe('a Grok worker and the structured-chat setting', () => { @@ -56,7 +54,7 @@ describe('a Grok worker and the structured-chat setting', () => { expect( decideWorkerStartMode({ params: { agent }, - settings: { ...STRUCTURED_PREFERENCE, experimentalStructuredNativeChat: false } + settings: { experimentalNativeChat: false } }) ).toMatchObject({ mode: 'terminal', preferred: 'terminal', reason: 'user_default' }) } diff --git a/src/main/runtime/rpc/methods/orchestration-worker-support-unknown.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-support-unknown.test.ts index 1adca072f20..7bcd483e8c0 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-support-unknown.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-support-unknown.test.ts @@ -7,9 +7,7 @@ import { const mode = decideWorkerStartMode({ params: { agent: 'claude' }, settings: { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) diff --git a/src/main/runtime/rpc/methods/session-tab-agent-capability-mutations.test.ts b/src/main/runtime/rpc/methods/session-tab-agent-capability-mutations.test.ts index f0792e20a47..1996789733d 100644 --- a/src/main/runtime/rpc/methods/session-tab-agent-capability-mutations.test.ts +++ b/src/main/runtime/rpc/methods/session-tab-agent-capability-mutations.test.ts @@ -128,9 +128,10 @@ describe('session tab structured capability mutations', () => { async (method) => { const snapshot = agentSnapshot() const closeMobileSessionTab = vi.fn().mockResolvedValue({ closed: true }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Dispatcher uses only the methods supplied by this test double. const runtime = { getRuntimeId: () => 'test-runtime', - getClientSettings: vi.fn(() => ({ experimentalStructuredNativeChat: true })), + getClientSettings: vi.fn(() => ({ experimentalNativeChat: true })), listMobileSessionTabs: vi.fn().mockResolvedValue(snapshot), closeMobileSessionTab } as unknown as OrcaRuntimeService @@ -177,13 +178,14 @@ function createFixture( moveMobileSessionTab: vi.fn().mockResolvedValue({ moved: true }), setMobileSessionTabProps: vi.fn().mockResolvedValue({ updated: true }) } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Dispatcher uses only the methods supplied by this test double. const runtime = { getRuntimeId: () => 'test-runtime', listMobileSessionTabs: vi.fn().mockResolvedValue(snapshot), getClientSettings: () => ({ // Why: defaults on, so a fixture that says nothing about the setting exercises capability // gating alone; callers opt into the off case explicitly. - experimentalStructuredNativeChat: options.structuredNativeChatEnabled !== false + experimentalNativeChat: options.structuredNativeChatEnabled !== false }), ...calls } as unknown as OrcaRuntimeService diff --git a/src/main/runtime/rpc/methods/session-tabs-structured-restore.test.ts b/src/main/runtime/rpc/methods/session-tabs-structured-restore.test.ts index 0bfc94f3663..d28a76b74cf 100644 --- a/src/main/runtime/rpc/methods/session-tabs-structured-restore.test.ts +++ b/src/main/runtime/rpc/methods/session-tabs-structured-restore.test.ts @@ -17,7 +17,7 @@ function makeRuntime(): OrcaRuntimeService { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the runtime members these RPCs read are staged. return { getRuntimeId: () => 'test-runtime', - getClientSettings: vi.fn(() => ({ experimentalStructuredNativeChat: false })), + getClientSettings: vi.fn(() => ({ experimentalNativeChat: false })), restoreStructuredAgentSessionTabs: vi.fn(), listMobileSessionTabs: vi.fn().mockResolvedValue(visibleSnapshot()) } as unknown as OrcaRuntimeService diff --git a/src/main/runtime/rpc/methods/session-tabs-subscribe-admission.test.ts b/src/main/runtime/rpc/methods/session-tabs-subscribe-admission.test.ts index 6f874b22633..f683d5219d4 100644 --- a/src/main/runtime/rpc/methods/session-tabs-subscribe-admission.test.ts +++ b/src/main/runtime/rpc/methods/session-tabs-subscribe-admission.test.ts @@ -36,7 +36,7 @@ function makeHost(options: { structuredChat?: boolean } = {}) { const runtime = { getRuntimeId: () => 'test-runtime', getClientSettings: () => ({ - experimentalStructuredNativeChat: options.structuredChat === true + experimentalNativeChat: options.structuredChat === true }), restoreStructuredAgentSessionTabs: restore, listMobileSessionTabs, diff --git a/src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts index dc6fd761b1c..94f936b8050 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts @@ -31,8 +31,8 @@ afterEach(() => { clearStructuredHostStub() }) -const SETTING_OFF = { getClientSettings: () => ({ experimentalStructuredNativeChat: false }) } -const SETTING_ON = { getClientSettings: () => ({ experimentalStructuredNativeChat: true }) } +const SETTING_OFF = { getClientSettings: () => ({ experimentalNativeChat: false }) } +const SETTING_ON = { getClientSettings: () => ({ experimentalNativeChat: true }) } // A client that picks each launch's mode itself, as the desktop does. const MODE_CHOOSING_CLIENT = { ...STRUCTURED_CLIENT, diff --git a/src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts index db540d3bbd7..4a82cb90475 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts @@ -144,7 +144,6 @@ describe('committed adopting create RPC replay', () => { const runtime = new OrcaRuntimeService( { getSettings: () => ({ - experimentalStructuredNativeChat: true, agentDefaultEnv: { codex: {} } }) } as never, @@ -153,8 +152,9 @@ describe('committed adopting create RPC replay', () => { ) // The structured surface is settings-gated for every caller, not just mobile; this test // probes durable-identity replay, which only runs once the gate admits the call. + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The RPC gate reads only this setting from the test double. vi.spyOn(runtime, 'getClientSettings').mockReturnValue({ - experimentalStructuredNativeChat: true + experimentalNativeChat: true } as ReturnType) vi.spyOn(runtime, 'getStructuredAgentSessionCreateSupport').mockResolvedValue({ supported: true diff --git a/src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts index 1e5bb7e6bc3..436e7020d0d 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts @@ -81,9 +81,7 @@ beforeEach(async () => { vi.spyOn(runtime, 'getClientSettings').mockImplementation( () => // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the RPC gate reads only this one setting. - ({ experimentalStructuredNativeChat: true }) as ReturnType< - OrcaRuntimeService['getClientSettings'] - > + ({ experimentalNativeChat: true }) as ReturnType ) dispatcher = new RpcDispatcher({ runtime, methods: STRUCTURED_AGENT_SESSION_METHODS }) }) diff --git a/src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts index 549b0a92dd2..25467e57248 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts @@ -32,6 +32,8 @@ import { openTestJournalHostDatabase } from '../../../native-chat/agent-session- import { createStructuredAgentSessionLogger } from '../../../native-chat/agent-session-wire/structured-agent-session-logger' import { codexProviderHandle } from '../../../../shared/agent-session-provider-handle-encoding' import { NO_STRUCTURED_AGENTS } from '../../../native-chat/agent-session-wire/structured-agent-session-adapter-router-test-support' +import { getDefaultPersistedState } from '../../../../shared/constants' +import { RuntimeClientSettingsController } from '../../runtime-client-settings' const CONNECTION = 'connection-1' const CLIENT = { @@ -109,12 +111,14 @@ beforeEach(async () => { setStructuredAgentSessionHost(host) runtime = new OrcaRuntimeService() // The structured surface is settings-gated for every caller, in-process included. - vi.spyOn(runtime, 'getClientSettings').mockImplementation( - () => - ({ experimentalStructuredNativeChat: structuredNativeChatEnabled }) as ReturnType< - OrcaRuntimeService['getClientSettings'] - > - ) + const clientSettings = new RuntimeClientSettingsController({ + getSettings: () => ({ + ...getDefaultPersistedState(root).settings, + experimentalNativeChat: structuredNativeChatEnabled + }), + updateSettings: () => undefined + }).get() + vi.spyOn(runtime, 'getClientSettings').mockImplementation(() => clientSettings) dispatcher = new RpcDispatcher({ runtime, methods: STRUCTURED_AGENT_SESSION_METHODS }) expect(await host.attach({ callerKey: 'client-1' }, hostTestAttachParams(null))).toMatchObject({ ok: true diff --git a/src/main/runtime/rpc/methods/structured-agent-session-policy.ts b/src/main/runtime/rpc/methods/structured-agent-session-policy.ts index 4295dee842f..a920a2c4192 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-policy.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-policy.ts @@ -6,12 +6,13 @@ import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY, type RuntimeCapability } from '../../../../shared/protocol-version' +import { isNativeChatEnabled } from '../../../../shared/structured-native-chat-launch-route' import type { OrcaRuntimeService } from '../../orca-runtime' import type { RpcContext } from '../core' /** * One rule for every caller: can this client read structured sessions? The host's own - * `experimentalStructuredNativeChat` is not consulted. It is the host user's launch preference, + * The host's Chat UI setting is not consulted. It is the host user's launch preference, * and whether a new agent is a chat is decided by whoever launches it, so a paired client's * sessions stay reachable whatever the host's setting says. The negotiated capability is a wire * term, asked of remote clients only: in-process callers are the host's own build. @@ -91,7 +92,7 @@ export function isStructuredNativeChatEnabled( runtime: Pick ): boolean { try { - return runtime.getClientSettings().experimentalStructuredNativeChat === true + return isNativeChatEnabled(runtime.getClientSettings()) } catch { return false } diff --git a/src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts index 970374824d1..e837a7efdb0 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts @@ -79,7 +79,7 @@ async function create( getRuntimeId: () => 'runtime-1', // The structured surface is settings-gated for every caller; these fixtures probe the // pre-commit boundary, which only runs once the gate admits the call. - getClientSettings: () => ({ experimentalStructuredNativeChat: true }), + getClientSettings: () => ({ experimentalNativeChat: true }), registerSubscriptionCleanup: vi.fn(), cleanupSubscription: vi.fn(), cleanupSubscriptionsByPrefix: vi.fn(), diff --git a/src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts b/src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts index c4b3f62700b..a24d21e6938 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts @@ -248,7 +248,7 @@ export function dispatcher( }) const runtime = { getRuntimeId: () => 'runtime-1', - getClientSettings: () => ({ experimentalStructuredNativeChat: true }), + getClientSettings: () => ({ experimentalNativeChat: true }), registerSubscriptionCleanup: vi.fn(), cleanupSubscription: vi.fn(), cleanupSubscriptionsByPrefix: vi.fn(), diff --git a/src/main/runtime/rpc/methods/structured-agent-session.test.ts b/src/main/runtime/rpc/methods/structured-agent-session.test.ts index 4bbd415cd02..e287472bb6c 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session.test.ts @@ -312,7 +312,7 @@ describe('capability gating', () => { it('serves a capable mobile client whatever the host structured-chat setting says', async () => { const response = await call('agentSession.send', sendParams(), STRUCTURED_MOBILE_CLIENT, { - getClientSettings: () => ({ experimentalStructuredNativeChat: false }) + getClientSettings: () => ({ experimentalNativeChat: false }) }) expect(response).toMatchObject({ ok: true }) expect(hostCalls.send).toHaveBeenCalledTimes(1) diff --git a/src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts b/src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts index 7c8eb63c32e..97feae48ff0 100644 --- a/src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts +++ b/src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts @@ -187,9 +187,7 @@ beforeEach(async () => { runtime = new OrcaRuntimeService() vi.spyOn(runtime, 'getClientSettings').mockReturnValue( // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the structured-chat policy reads only this one setting on these paths. - { experimentalStructuredNativeChat: true } as ReturnType< - OrcaRuntimeService['getClientSettings'] - > + { experimentalNativeChat: true } as ReturnType ) dispatcher = new RpcDispatcher({ runtime, diff --git a/src/main/runtime/runtime-client-settings.ts b/src/main/runtime/runtime-client-settings.ts index a312fc66dbe..5ca04d127e3 100644 --- a/src/main/runtime/runtime-client-settings.ts +++ b/src/main/runtime/runtime-client-settings.ts @@ -42,8 +42,6 @@ export type RuntimeClientSettings = Pick< | 'githubProjects' | 'experimentalNewWorktreeCardStyle' | 'experimentalNativeChat' - | 'openAgentTabsInChatByDefault' - | 'experimentalStructuredNativeChat' | 'compactWorktreeCards' | 'minimaxGroupId' | 'minimaxUsageModels' @@ -55,6 +53,9 @@ export type RuntimeClientSettings = Pick< | 'agentSkillSharingEnabled' | 'machineName' > & { + /** Older clients still read these; neither is persisted or accepted as an update. */ + openAgentTabsInChatByDefault?: boolean + experimentalStructuredNativeChat?: boolean hostSettingOverrides: RuntimeHostDisplayLabelOverrides sourceControlAi: RuntimeClientSourceControlAi } @@ -125,11 +126,10 @@ export class RuntimeClientSettingsController { : null, githubProjects: settings.githubProjects, experimentalNewWorktreeCardStyle: settings.experimentalNewWorktreeCardStyle === true, - // The three that decide whether a new agent tab -- and so an orchestration worker -- is a - // structured chat session rather than a terminal agent. experimentalNativeChat: settings.experimentalNativeChat === true, - openAgentTabsInChatByDefault: settings.openAgentTabsInChatByDefault === true, - experimentalStructuredNativeChat: settings.experimentalStructuredNativeChat === true, + // Older clients use this key to fall back to terminal-backed chat. Keep their default terminal. + openAgentTabsInChatByDefault: false, + experimentalStructuredNativeChat: settings.experimentalNativeChat === true, compactWorktreeCards: settings.compactWorktreeCards === true, minimaxGroupId: settings.minimaxGroupId ?? '', minimaxUsageModels: settings.minimaxUsageModels ?? 'general', diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index b25168334c6..0c5c24f4cb2 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -96,8 +96,6 @@ export type RuntimeStore = { agentStatusHooksEnabled?: GlobalSettings['agentStatusHooksEnabled'] terminalCopyTrimsGutter?: GlobalSettings['terminalCopyTrimsGutter'] experimentalNativeChat?: GlobalSettings['experimentalNativeChat'] - openAgentTabsInChatByDefault?: GlobalSettings['openAgentTabsInChatByDefault'] - experimentalStructuredNativeChat?: GlobalSettings['experimentalStructuredNativeChat'] defaultTaskSource?: GlobalSettings['defaultTaskSource'] defaultTaskViewPreset?: GlobalSettings['defaultTaskViewPreset'] visibleTaskProviders?: GlobalSettings['visibleTaskProviders'] diff --git a/src/main/runtime/structured-agent-runtime-registrations-acp.test.ts b/src/main/runtime/structured-agent-runtime-registrations-acp.test.ts index 43b11bdee1c..c3892d5e655 100644 --- a/src/main/runtime/structured-agent-runtime-registrations-acp.test.ts +++ b/src/main/runtime/structured-agent-runtime-registrations-acp.test.ts @@ -55,19 +55,17 @@ describe('ACP agents in the runtime registrations', () => { expect(registration?.supportsLaunch).toBeTypeOf('function') }) - it('opens OMP as a structured chat only with the updated structured chat setting on', () => { + it('opens OMP as a structured chat with Chat UI on', () => { const settings = { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true + experimentalNativeChat: true } const placement = { agent: 'omp', workspaceKind: 'git-worktree' } as const expect(decideAgentLaunchMode({ placement, settings }).mode).toBe('structured') - // Off: OMP keeps the terminal-backed chat it has always had. + // Off: new OMP launches use the terminal. expect( decideAgentLaunchMode({ placement, - settings: { ...settings, experimentalStructuredNativeChat: false } + settings: { experimentalNativeChat: false } }) ).toMatchObject({ mode: 'terminal', reason: 'user_default' }) }) diff --git a/src/main/runtime/structured-agent-session-integration.test.ts b/src/main/runtime/structured-agent-session-integration.test.ts index 92510aa3241..2842c2b280f 100644 --- a/src/main/runtime/structured-agent-session-integration.test.ts +++ b/src/main/runtime/structured-agent-session-integration.test.ts @@ -327,7 +327,7 @@ beforeEach(async () => { shellEnvironmentPolicy = { inheritAll: true, names: [] } const runtime = { getRuntimeId: () => 'runtime-1', - getClientSettings: () => ({ experimentalStructuredNativeChat: true }), + getClientSettings: () => ({ experimentalNativeChat: true }), getStructuredAgentSessionCreateSupport: async () => ({ supported: true }), resolveStructuredAgentSessionCreateIntent: async () => { const { diff --git a/src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx b/src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx index 1d778c69746..eed135ccce0 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx +++ b/src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx @@ -48,7 +48,7 @@ beforeEach(() => { consumeNativeChatResumeOnRestartDialogRequest() useAppStore.setState(useAppStore.getInitialState(), true) useAppStore.setState({ - settings: { ...getDefaultSettings(''), experimentalStructuredNativeChat: true } + settings: { ...getDefaultSettings(''), experimentalNativeChat: true } }) container = document.createElement('div') document.body.append(container) diff --git a/src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx b/src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx index 5c419087020..5cae482fa95 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx +++ b/src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx @@ -317,7 +317,7 @@ it('resumes and continues once when the launch begins opted in', async () => { useAppStore.setState({ settings: { ...getDefaultSettings(''), - experimentalStructuredNativeChat: true, + experimentalNativeChat: true, nativeChatResumeWorkOnRestart: true } }) @@ -333,12 +333,8 @@ it('resumes and continues once when the launch begins opted in', async () => { await act(async () => useAppStore.getState().updateSettings({ nativeChatResumeWorkOnRestart: true }) ) - await act(async () => - useAppStore.getState().updateSettings({ experimentalStructuredNativeChat: false }) - ) - await act(async () => - useAppStore.getState().updateSettings({ experimentalStructuredNativeChat: true }) - ) + await act(async () => useAppStore.getState().updateSettings({ experimentalNativeChat: false })) + await act(async () => useAppStore.getState().updateSettings({ experimentalNativeChat: true })) // One bulk action, with its remaining list in the reply. expect(calls()).toEqual([ ['agentSession.restartResumable', undefined], @@ -355,7 +351,7 @@ it('shows an opted-in launch resume in the status bar while it runs', async () = useAppStore.setState({ settings: { ...getDefaultSettings(''), - experimentalStructuredNativeChat: true, + experimentalNativeChat: true, nativeChatResumeWorkOnRestart: true } }) @@ -376,7 +372,7 @@ it('reports chats an opted-in launch could not carry on in one toast and the sta useAppStore.setState({ settings: { ...getDefaultSettings(''), - experimentalStructuredNativeChat: true, + experimentalNativeChat: true, nativeChatResumeWorkOnRestart: true } }) diff --git a/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx b/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx index 51ef0de7a6d..a6c506fd292 100644 --- a/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx +++ b/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx @@ -54,7 +54,6 @@ function settings(autoResume: boolean | undefined): void { useAppStore.setState({ settings: { ...getDefaultSettings(''), - experimentalStructuredNativeChat: true, ...(autoResume === undefined ? {} : { nativeChatResumeWorkOnRestart: autoResume }) } }) diff --git a/src/renderer/src/components/native-chat-resume-modal.test-support.tsx b/src/renderer/src/components/native-chat-resume-modal.test-support.tsx index 0bbc9dcdf31..3fc6d9f7021 100644 --- a/src/renderer/src/components/native-chat-resume-modal.test-support.tsx +++ b/src/renderer/src/components/native-chat-resume-modal.test-support.tsx @@ -211,7 +211,7 @@ export function createResumeModalFixture(rpc: Mock, statusStream: Re vi.mocked(toast).mockClear() useAppStore.setState(useAppStore.getInitialState(), true) useAppStore.setState({ - settings: { ...getDefaultSettings(''), experimentalStructuredNativeChat: true }, + settings: { ...getDefaultSettings(''), experimentalNativeChat: true }, updateSettings: async (changes) => { useAppStore.setState((state) => ({ settings: { ...getDefaultSettings(''), ...state.settings, ...changes } diff --git a/src/renderer/src/components/native-chat/native-chat-availability.test.ts b/src/renderer/src/components/native-chat/native-chat-availability.test.ts index 737d7d4a077..d26c1bac03e 100644 --- a/src/renderer/src/components/native-chat/native-chat-availability.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-availability.test.ts @@ -6,7 +6,6 @@ describe('canToggleNativeChat', () => { it('allows a terminal launched with a supported coding agent', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'claude' }) @@ -16,7 +15,6 @@ describe('canToggleNativeChat', () => { it('allows a terminal with a live detected supported agent but no launchAgent', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: null, detectedAgent: 'codex' @@ -27,7 +25,6 @@ describe('canToggleNativeChat', () => { it('allows a terminal with a resolved title/foreground supported agent before hooks arrive', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: null, resolvedAgent: 'claude' @@ -38,7 +35,6 @@ describe('canToggleNativeChat', () => { it('allows the OpenClaude variant', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'openclaude' }) @@ -48,7 +44,6 @@ describe('canToggleNativeChat', () => { it('allows an existing chat view to toggle back after live signals are gone', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: null, isChatViewMode: true @@ -59,7 +54,6 @@ describe('canToggleNativeChat', () => { it('accepts local Grok once native chat can parse its transcript', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'grok', nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable(null) @@ -70,7 +64,6 @@ describe('canToggleNativeChat', () => { it('accepts runtime-owned Grok because Model B reads the transcript locally', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'grok', nativeChatTranscriptIsLocalReadable: @@ -82,7 +75,6 @@ describe('canToggleNativeChat', () => { it('rejects Model-A SSH Grok when its transcript is remote-only', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'grok', nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable('ssh-target-1') @@ -95,7 +87,6 @@ describe('canToggleNativeChat', () => { it('rejects Model-A SSH omp but accepts it local and runtime-owned', () => { const forConnection = (connectionId: string | null): boolean => canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'omp', nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable(connectionId) @@ -108,7 +99,6 @@ describe('canToggleNativeChat', () => { it('lets an existing Model-A SSH Grok chat toggle back to terminal', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'grok', nativeChatTranscriptIsLocalReadable: false, @@ -120,7 +110,6 @@ describe('canToggleNativeChat', () => { it.each(['gemini'] as const)('rejects unsupported agent %s detected live', (agent) => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: null, detectedAgent: agent @@ -131,7 +120,6 @@ describe('canToggleNativeChat', () => { it('accepts Grok when resolved from the title', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: null, resolvedAgent: 'grok', @@ -143,7 +131,6 @@ describe('canToggleNativeChat', () => { it('rejects a stale supported title when live detection found an unsupported agent', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: null, detectedAgent: 'gemini', @@ -155,7 +142,6 @@ describe('canToggleNativeChat', () => { it('rejects stale launch metadata when live detection found an unsupported agent', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'codex', detectedAgent: 'gemini' @@ -166,7 +152,6 @@ describe('canToggleNativeChat', () => { it('rejects a stale supported title when launch metadata names an unsupported agent', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: 'gemini', resolvedAgent: 'claude' @@ -174,20 +159,18 @@ describe('canToggleNativeChat', () => { ).toBe(false) }) - it('rejects otherwise eligible terminals while the experimental flag is off', () => { + it('allows an explicit chat toggle even when Chat UI is off', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: false, contentType: 'terminal', launchAgent: 'claude' }) - ).toBe(false) + ).toBe(true) }) it('rejects a plain shell terminal with no agent', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'terminal', launchAgent: null, detectedAgent: null @@ -196,15 +179,12 @@ describe('canToggleNativeChat', () => { }) it('rejects a plain shell terminal with everything omitted', () => { - expect( - canToggleNativeChat({ experimentalNativeChatEnabled: true, contentType: 'terminal' }) - ).toBe(false) + expect(canToggleNativeChat({ contentType: 'terminal' })).toBe(false) }) it('rejects an editor tab even if a supported agent hint were somehow present', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'editor', launchAgent: 'codex', detectedAgent: 'codex' @@ -215,7 +195,6 @@ describe('canToggleNativeChat', () => { it('rejects a browser tab', () => { expect( canToggleNativeChat({ - experimentalNativeChatEnabled: true, contentType: 'browser', detectedAgent: 'claude' }) diff --git a/src/renderer/src/components/native-chat/native-chat-availability.ts b/src/renderer/src/components/native-chat/native-chat-availability.ts index 3cad7fc157f..40366dbdd75 100644 --- a/src/renderer/src/components/native-chat/native-chat-availability.ts +++ b/src/renderer/src/components/native-chat/native-chat-availability.ts @@ -13,8 +13,6 @@ export { isNativeChatSupportedAgent } * unit-testable; call sites resolve `launchAgent`/`detectedAgent` from the * terminal tab + agent-status before calling. */ export type NativeChatAvailabilityInput = { - /** Feature flag: hidden unless enabled from Settings > Experimental. */ - experimentalNativeChatEnabled?: boolean contentType: Tab['contentType'] /** The coding-agent Orca launched in this terminal, if any (from TerminalTab). */ launchAgent?: TuiAgent | null @@ -40,9 +38,6 @@ export type NativeChatAvailabilityInput = { * launch metadata is next, and title resolution only fills the pre-hook gap for * manually-started Claude/Codex/Grok sessions. */ export function canToggleNativeChat(input: NativeChatAvailabilityInput): boolean { - if (input.experimentalNativeChatEnabled !== true) { - return false - } if (input.contentType !== 'terminal') { return false } diff --git a/src/renderer/src/components/native-chat/native-chat-launch-session-options.test.ts b/src/renderer/src/components/native-chat/native-chat-launch-session-options.test.ts deleted file mode 100644 index 551be10ab0c..00000000000 --- a/src/renderer/src/components/native-chat/native-chat-launch-session-options.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { resolveInitialNativeChatSessionOptions } from './native-chat-launch-session-options' - -const settings = { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - nativeChatSessionOptions: { - codex: { - model: 'gpt-5.2-codex', - valuesByModel: { 'gpt-5.2-codex': { effort: 'medium' } } - } - } -} - -describe('resolveInitialNativeChatSessionOptions', () => { - it('omits native-chat preferences from terminal-default launches', () => { - expect( - resolveInitialNativeChatSessionOptions( - { ...settings, openAgentTabsInChatByDefault: false }, - { agent: 'codex' } - ) - ).toBeUndefined() - }) - - it('applies native-chat preferences when the launch resolves to chat', () => { - expect(resolveInitialNativeChatSessionOptions(settings, { agent: 'codex' })).toEqual({ - model: 'gpt-5.2-codex', - effort: 'medium' - }) - }) - - it('omits preferences when a draft forces the initial view back to terminal', () => { - expect( - resolveInitialNativeChatSessionOptions(settings, { - agent: 'codex', - promptDelivery: 'draft', - launchDraftText: 'one\u2028two' - }) - ).toBeUndefined() - }) - - it('omits preferences when a remote transcript forces the initial view to terminal', () => { - const grokSettings = { - ...settings, - nativeChatSessionOptions: { grok: { model: 'grok-4.5' } } - } - expect( - resolveInitialNativeChatSessionOptions(grokSettings, { - agent: 'grok', - nativeChatTranscriptIsLocalReadable: false - }) - ).toBeUndefined() - }) -}) diff --git a/src/renderer/src/components/native-chat/native-chat-launch-session-options.ts b/src/renderer/src/components/native-chat/native-chat-launch-session-options.ts deleted file mode 100644 index 2271d4bd1d8..00000000000 --- a/src/renderer/src/components/native-chat/native-chat-launch-session-options.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { GlobalSettings } from '../../../../shared/global-settings-types' -import type { TuiAgent } from '../../../../shared/tui-agent' -import type { SessionOptionValue } from '../../../../shared/native-chat-session-options' -import { - decideInitialAgentTabViewMode, - type NativeChatLaunchPromptDelivery -} from '@/lib/native-chat-initial-view-mode' -import { resolveNativeChatLaunchSessionOptions } from './native-chat-session-option-enrichment' - -type NativeChatLaunchSettings = Pick< - GlobalSettings, - 'experimentalNativeChat' | 'openAgentTabsInChatByDefault' | 'nativeChatSessionOptions' -> - -export type InitialNativeChatSessionOptionsArgs = { - agent: TuiAgent - promptDelivery?: NativeChatLaunchPromptDelivery - launchDraftText?: string - nativeChatTranscriptIsLocalReadable?: boolean -} - -export function resolveInitialNativeChatSessionOptions( - settings: NativeChatLaunchSettings | null | undefined, - args: InitialNativeChatSessionOptionsArgs -): Record | undefined { - const viewMode = decideInitialAgentTabViewMode({ - experimentalNativeChat: settings?.experimentalNativeChat, - openAgentTabsInChatByDefault: settings?.openAgentTabsInChatByDefault, - ...args - }) - return viewMode === 'chat' - ? resolveNativeChatLaunchSessionOptions(settings?.nativeChatSessionOptions, args.agent) - : undefined -} diff --git a/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts b/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts index 6cc94609361..bb1cff3ddcf 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts @@ -88,7 +88,6 @@ export function useNativeChatToggleShortcut(worktreeId: string, isWorktreeActive : null if ( !canToggleNativeChat({ - experimentalNativeChatEnabled: state.settings?.experimentalNativeChat === true, contentType: 'terminal', launchAgent: detectedAgent || !tabWideFallbackSafe ? null : terminalTab?.launchAgent, detectedAgent, diff --git a/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts b/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts index d57028e882c..23cd8c0fe2e 100644 --- a/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts +++ b/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts @@ -41,44 +41,26 @@ describe('buildOnboardingFolderAgentStartup', () => { expect(startup).toBeUndefined() }) - it('omits native-chat preferences from terminal-default folder launches', () => { - const startup = buildOnboardingFolderAgentStartup({ - ...getDefaultSettings('/tmp/orca-workspaces'), - defaultTuiAgent: 'codex', - experimentalNativeChat: true, - openAgentTabsInChatByDefault: false, - nativeChatSessionOptions: { - codex: { - model: 'gpt-5.2-codex', - valuesByModel: { 'gpt-5.2-codex': { effort: 'medium' } } + it.each([false, true])( + 'keeps Chat UI model preferences out of the terminal folder launch (Chat UI %s)', + (experimentalNativeChat) => { + const startup = buildOnboardingFolderAgentStartup({ + ...getDefaultSettings('/tmp/orca-workspaces'), + defaultTuiAgent: 'codex', + experimentalNativeChat, + nativeChatSessionOptions: { + codex: { + model: 'gpt-5.2-codex', + valuesByModel: { 'gpt-5.2-codex': { effort: 'medium' } } + } } - } - }) + }) - expect(startup?.command).not.toContain("'-m'") - expect(startup?.sessionOptions).toBeUndefined() - }) - - it('applies native-chat preferences to chat-default folder launches', () => { - const startup = buildOnboardingFolderAgentStartup({ - ...getDefaultSettings('/tmp/orca-workspaces'), - defaultTuiAgent: 'codex', - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - nativeChatSessionOptions: { - codex: { - model: 'gpt-5.2-codex', - valuesByModel: { 'gpt-5.2-codex': { effort: 'medium' } } - } - } - }) - - expect(startup?.command).toContain("'-m' 'gpt-5.2-codex'") - expect(startup?.sessionOptions).toEqual({ - model: 'gpt-5.2-codex', - effort: 'medium' - }) - }) + expect(startup?.command).not.toContain("'-m'") + expect(startup?.command).not.toContain('model_reasoning_effort=') + expect(startup).not.toHaveProperty('sessionOptions') + } + ) it('does not infer an agent from auto mode', () => { const startup = buildOnboardingFolderAgentStartup({ diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx index a5592bb9fc0..caafca96b00 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx @@ -234,9 +234,7 @@ describe('SourceControlAgentActionDialog', () => { resetStore( { ...settingsWithGlobalRecipe(null), - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true }, [repoWithSavedRecipe('--model saved', 'build-box', 'ssh:build-box')] ) @@ -264,9 +262,7 @@ describe('SourceControlAgentActionDialog', () => { resetStore( { ...settingsWithGlobalRecipe(recipe), - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true }, [] ) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts index 59276f03d92..d8387ddb90c 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts @@ -42,9 +42,7 @@ function resumeInChat() { activeWorkspaceId: PAIRED_WORKSPACE, targetState: { folderWorkspaces: [], projectGroups: [], repos: [], worktreesByRepo: {} }, settings: { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) } diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.ts index 6468ca5ef00..ec18262ed09 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.ts @@ -65,9 +65,9 @@ export function resolveAiVaultSessionResumeInChatEligibility(args: { > & { structuredSession?: AiVaultSession['structuredSession'] } targetWorkspaceId: string | null targetWorkspacePath: string | null - /** The route the same (workspace, agent) pair would take for a fresh chat. Reused rather than - * re-derived: it already encodes the settings flag, host capability, platform refusals and the - * WSL/repair refusal, and a second copy of those conditions would drift from it. */ + /** Whether the same (workspace, agent) pair can host a structured chat; the Chat UI switch does + * not apply. Reused rather than re-derived: it already encodes host capability, platform refusals + * and the WSL/repair refusal, and a second copy of those conditions would drift from it. */ structuredRouteAvailable: boolean }): AiVaultResumeInChatEligibility { const { session } = args diff --git a/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts b/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts index 44e844bc4d6..dfbc7b9c422 100644 --- a/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts @@ -39,7 +39,7 @@ describe('sourceControlLaunchAppliesAgentArgs', () => { it('keeps arguments for a chat-by-default user whose launch falls back to a terminal', () => { // A remote host, an agent without a structured session, or a floating workspace all land here. - mocks.resolveAgentSessionLaunchRoute.mockReturnValue('legacy-native-chat') + mocks.resolveAgentSessionLaunchRoute.mockReturnValue('terminal-tui') expect( sourceControlLaunchAppliesAgentArgs({ agent: 'codex', diff --git a/src/renderer/src/components/settings/AppearancePane.test.tsx b/src/renderer/src/components/settings/AppearancePane.test.tsx index 488b90472be..62152c3244b 100644 --- a/src/renderer/src/components/settings/AppearancePane.test.tsx +++ b/src/renderer/src/components/settings/AppearancePane.test.tsx @@ -247,8 +247,7 @@ describe('AppearancePane', () => { it('keeps chat appearance controls out of the Appearance pane', async () => { mocks.state.settingsSearchQuery = '' const container = await renderAppearancePane({ - ...getDefaultSettings('/tmp'), - experimentalStructuredNativeChat: true + ...getDefaultSettings('/tmp') }) expect(appearanceSectionToggle(container, 'chat')).toBeUndefined() expect(container.textContent).not.toContain('Reset chat appearance') diff --git a/src/renderer/src/components/settings/ChatSettingsSection.inline-visuals.test.tsx b/src/renderer/src/components/settings/ChatSettingsSection.inline-visuals.test.tsx index b7853aa2f25..0f023f1ae05 100644 --- a/src/renderer/src/components/settings/ChatSettingsSection.inline-visuals.test.tsx +++ b/src/renderer/src/components/settings/ChatSettingsSection.inline-visuals.test.tsx @@ -29,7 +29,7 @@ beforeEach(() => { function renderChat(nativeChatInlineVisuals: boolean | undefined) { const settings = { ...getDefaultSettings('/tmp'), - experimentalStructuredNativeChat: true, + experimentalNativeChat: true, nativeChatInlineVisuals } state.settings = settings @@ -81,7 +81,7 @@ describe('Inline visuals on the Chat settings page', () => { isMac: false, isWindows: false, isWebClient: false, - experimentalStructuredNativeChat: true, + nativeChatEnabled: true, repos: [] }) const results = buildCmdJSettingsResults(sections) diff --git a/src/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsx b/src/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsx index 4a065f0881b..a274675bc0a 100644 --- a/src/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsx +++ b/src/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsx @@ -34,12 +34,12 @@ function NavigationHarness({ targetSection?: string }) { const [activeSectionId, setActiveSectionId] = useState(initialSection) - const settings = { ...getDefaultSettings('/tmp'), experimentalStructuredNativeChat: enabled } + const settings = { ...getDefaultSettings('/tmp'), experimentalNativeChat: enabled } const sections = buildSettingsNavigationMetadata({ isMac: false, isWindows: false, isWebClient: false, - experimentalStructuredNativeChat: enabled, + nativeChatEnabled: enabled, repos: [] }) const pendingNavSectionRef = useRef('chat') diff --git a/src/renderer/src/components/settings/ChatSettingsSection.test.tsx b/src/renderer/src/components/settings/ChatSettingsSection.test.tsx index 9e571645252..701f06526db 100644 --- a/src/renderer/src/components/settings/ChatSettingsSection.test.tsx +++ b/src/renderer/src/components/settings/ChatSettingsSection.test.tsx @@ -34,7 +34,7 @@ function renderChat( showDesktopOnlySettings = true, hasUnsavedChatPromptChanges = false ) { - const settings = { ...getDefaultSettings('/tmp'), experimentalStructuredNativeChat: enabled } + const settings = { ...getDefaultSettings('/tmp'), experimentalNativeChat: enabled } state.settings = settings const updateSettings = vi.fn(async (updates: Partial) => { if (state.settings) { @@ -73,7 +73,7 @@ describe('Chat settings page', () => { isMac: false, isWindows: false, isWebClient: true, - experimentalStructuredNativeChat: true, + nativeChatEnabled: true, repos: [] }) const results = buildCmdJSettingsResults(sections).filter((entry) => entry.sectionId === 'chat') @@ -141,7 +141,7 @@ describe('Chat settings page', () => { rerender( {}} searchEntries={[]} @@ -166,7 +166,7 @@ describe('Chat settings page', () => { isMac: true, isWindows: false, isWebClient: false, - experimentalStructuredNativeChat: true, + nativeChatEnabled: true, repos: [] }) const results = buildCmdJSettingsResults(sections) @@ -191,7 +191,7 @@ describe('Chat settings page', () => { isMac: false, isWindows: true, isWebClient: false, - experimentalStructuredNativeChat: true, + nativeChatEnabled: true, repos: [] }) const result = buildCmdJSettingsResults(sections).find( @@ -231,7 +231,7 @@ describe('Chat settings page', () => { isMac: true, isWindows: false, isWebClient: false, - experimentalStructuredNativeChat: true, + nativeChatEnabled: true, repos: [] }) const result = buildCmdJSettingsResults(sections).find( @@ -264,7 +264,7 @@ describe('Chat settings page', () => { isMac: true, isWindows: false, isWebClient: false, - experimentalStructuredNativeChat: true, + nativeChatEnabled: true, repos: [] }) const result = buildCmdJSettingsResults(sections).find( diff --git a/src/renderer/src/components/settings/ChatSettingsSection.tsx b/src/renderer/src/components/settings/ChatSettingsSection.tsx index 62d83dafa05..0eb46c38a4c 100644 --- a/src/renderer/src/components/settings/ChatSettingsSection.tsx +++ b/src/renderer/src/components/settings/ChatSettingsSection.tsx @@ -34,7 +34,7 @@ export function ChatSettingsSection({ isMounted: boolean }): React.JSX.Element | null { const query = useAppStore((state) => state.settingsSearchQuery) - if (settings.experimentalStructuredNativeChat !== true) { + if (settings.experimentalNativeChat !== true) { return null } const title = translate('settings.appearance.chat.title', 'Chat') diff --git a/src/renderer/src/components/settings/ExperimentalPane.test.tsx b/src/renderer/src/components/settings/ExperimentalPane.test.tsx index 66b795329f1..93b2d2f2565 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.test.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.test.tsx @@ -223,56 +223,27 @@ describe('ExperimentalPane', () => { expect(markup).toContain('aria-checked="true"') }) - it('shows the structured-native-chat child setting only when Chat UI is the default view', async () => { + it('offers one Chat UI switch and no default-view selector', async () => { const updateSettings = vi.fn() - const disabledSettings = getDefaultSettings('/tmp') - const disabledMarkup = renderToStaticMarkup( - - ) - expect(disabledMarkup).toContain('Chat UI') - expect(disabledMarkup).not.toContain('Use updated structured native chat') - expect(disabledMarkup).not.toContain('Default view') - - const terminalDefault = { - ...getDefaultSettings('/tmp'), - experimentalNativeChat: true, - experimentalStructuredNativeChat: false, - openAgentTabsInChatByDefault: false - } - const terminalRender = await renderExperimentalPane({ - updateSettings, - settings: terminalDefault - }) - - // The default-view control is a sibling of the Chat UI toggle, never replaced by the opt-in. - expect(terminalRender.container.textContent).toContain('Default view') - expect( - terminalRender.container.querySelector('[data-slot="native-chat-default-view-select"]') - ).not.toBeNull() - // Structured chat has no entry path under Terminal chat, so its opt-in is not offered. - expect(terminalRender.container.textContent).not.toContain('Use updated structured native chat') - terminalRender.root.unmount() - const { root, container } = await renderExperimentalPane({ updateSettings, - settings: { ...terminalDefault, openAgentTabsInChatByDefault: true } + settings: getDefaultSettings('/tmp') }) - - expect(container.textContent).toContain('Use updated structured native chat') - // The one setting governs both providers, so its copy must not name only Codex. - expect(container.textContent).toContain('Open new agents as structured chats where supported.') - // The setting picks what new agents open as; existing chats are left alone. - expect(container.textContent).toContain('Chats that already exist stay as they are.') - // Paired Orca servers run structured chats too; only WSL and SSH stay on terminal chat. - expect(container.textContent).toContain( - 'Runs on this machine and on paired Orca servers running a version that supports it; older servers keep terminal chat. WSL and SSH hosts continue to use terminal chat.' + expect(container.textContent).toContain('Chat UI') + expect(container.textContent).not.toContain('Default view') + expect(container.textContent).not.toContain('Use updated structured native chat') + const switchControl = container.querySelector( + '#experimental-native-chat button[aria-label="Toggle Chat UI"]' ) - expect(container.textContent).toContain('Default view') + expect(switchControl).not.toBeNull() + await act(async () => { + switchControl?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + expect(updateSettings).toHaveBeenCalledWith({ experimentalNativeChat: true }) root.unmount() }) - // Those settings govern the chats this machine holds, which keep running with the setting off. - it('shows the structured chat settings while this machine holds chats, whatever the setting', async () => { + it('shows structured chat controls for chats this machine still holds while Chat UI is off', async () => { Object.defineProperty(window, 'api', { configurable: true, value: { @@ -285,12 +256,11 @@ describe('ExperimentalPane', () => { try { const { root, container } = await renderExperimentalPane({ updateSettings: vi.fn(), - settings: { ...getDefaultSettings('/tmp'), experimentalNativeChat: true } + settings: getDefaultSettings('/tmp') }) await act(async () => { await Promise.resolve() }) - expect(container.textContent).toContain('Resume working chats automatically after a restart') root.unmount() } finally { @@ -299,174 +269,6 @@ describe('ExperimentalPane', () => { } }) - it('hides the structured chat settings on a machine that holds none with the setting off', async () => { - const { root, container } = await renderExperimentalPane({ - updateSettings: vi.fn(), - settings: { ...getDefaultSettings('/tmp'), experimentalNativeChat: true } - }) - - expect(container.textContent).not.toContain( - 'Resume working chats automatically after a restart' - ) - root.unmount() - }) - - it('hides a stale structured opt-in under Terminal chat without clearing it', async () => { - const updateSettings = vi.fn() - const settings = { - ...getDefaultSettings('/tmp'), - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true - } - const { root, container } = await renderExperimentalPane({ updateSettings, settings }) - - expect(container.textContent).toContain('Use updated structured native chat') - - const terminalChatOption = Array.from( - container.querySelectorAll('[data-slot="select-item"]') - ).find((button) => button.getAttribute('data-value') === 'terminal-chat') - if (!terminalChatOption) { - throw new Error('Terminal chat default-view option was not rendered') - } - - await act(async () => { - terminalChatOption.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - // Switching the default view must not clobber the persisted opt-in — only hide its control. - expect(updateSettings).toHaveBeenCalledWith({ openAgentTabsInChatByDefault: false }) - expect(updateSettings).toHaveBeenCalledTimes(1) - root.unmount() - - const hidden = await renderExperimentalPane({ - updateSettings, - settings: { ...settings, openAgentTabsInChatByDefault: false } - }) - - expect(hidden.container.textContent).not.toContain('Use updated structured native chat') - hidden.root.unmount() - - // Returning to Chat UI restores the control still switched on. - const restored = await renderExperimentalPane({ updateSettings, settings }) - const structuredSwitch = restored.container.querySelector( - '#experimental-native-chat button[role="switch"][aria-label="Toggle updated structured native chat"]' - ) - expect(structuredSwitch?.getAttribute('aria-checked')).toBe('true') - restored.root.unmount() - }) - - it('shows Chat UI default-mode as a child setting only when Chat UI is enabled', async () => { - const updateSettings = vi.fn() - const disabledSettings = getDefaultSettings('/tmp') - const disabledMarkup = renderToStaticMarkup( - - ) - expect(disabledMarkup).toContain('Chat UI') - expect(disabledMarkup).not.toContain('Default view') - - const settings = { - ...getDefaultSettings('/tmp'), - experimentalNativeChat: true, - openAgentTabsInChatByDefault: false - } - const { root, container } = await renderExperimentalPane({ updateSettings, settings }) - - expect(container.textContent).toContain('Default view') - expect(container.textContent).toContain('Terminal chat') - expect(container.textContent).toContain('Chat UI') - expect( - container - .querySelector('[data-slot="native-chat-default-view-select"]') - ?.getAttribute('data-value') - ).toBe('terminal-chat') - - const nativeChatOption = Array.from( - container.querySelectorAll('[data-slot="select-item"]') - ).find((button) => button.getAttribute('data-value') === 'native-chat') - if (!nativeChatOption) { - throw new Error('Chat UI default-view option was not rendered') - } - - await act(async () => { - nativeChatOption.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(updateSettings).toHaveBeenCalledWith({ openAgentTabsInChatByDefault: true }) - - root.unmount() - - const nativeSettings = { - ...settings, - openAgentTabsInChatByDefault: true - } - const secondRender = await renderExperimentalPane({ - updateSettings, - settings: nativeSettings - }) - - expect( - secondRender.container - .querySelector('[data-slot="native-chat-default-view-select"]') - ?.getAttribute('data-value') - ).toBe('native-chat') - - const terminalChatOption = Array.from( - secondRender.container.querySelectorAll('[data-slot="select-item"]') - ).find((button) => button.getAttribute('data-value') === 'terminal-chat') - if (!terminalChatOption) { - throw new Error('Terminal chat default-view option was not rendered') - } - - await act(async () => { - terminalChatOption.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(updateSettings).toHaveBeenCalledWith({ openAgentTabsInChatByDefault: false }) - - secondRender.root.unmount() - }) - - // The two controls are nested, but each still writes only its own key. - it('never writes one Chat UI child setting while changing the other', async () => { - const updateSettings = vi.fn() - const settings = { - ...getDefaultSettings('/tmp'), - experimentalNativeChat: true, - experimentalStructuredNativeChat: false, - openAgentTabsInChatByDefault: true - } - const { root, container } = await renderExperimentalPane({ updateSettings, settings }) - - const structuredSwitch = container.querySelector( - '#experimental-native-chat button[role="switch"][aria-label="Toggle updated structured native chat"]' - ) - if (!structuredSwitch) { - throw new Error('Structured native chat switch was not rendered') - } - - await act(async () => { - structuredSwitch.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(updateSettings).toHaveBeenCalledWith({ experimentalStructuredNativeChat: true }) - - const terminalChatOption = Array.from( - container.querySelectorAll('[data-slot="select-item"]') - ).find((button) => button.getAttribute('data-value') === 'terminal-chat') - if (!terminalChatOption) { - throw new Error('Terminal chat default-view option was not rendered') - } - - await act(async () => { - terminalChatOption.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(updateSettings).toHaveBeenCalledWith({ openAgentTabsInChatByDefault: false }) - expect(updateSettings).toHaveBeenCalledTimes(2) - root.unmount() - }) - it('renders the agent sleep idle duration as configurable minutes', async () => { const updateSettings = vi.fn() const settings = { diff --git a/src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx b/src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx index 8e95b25a45c..6f26ae16d12 100644 --- a/src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx +++ b/src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx @@ -45,37 +45,19 @@ function listedNames(container: HTMLElement): string[] { } describe('NativeChatExperimentalSetting shell environment', () => { - it('shows only when Chat UI, the Chat UI default view, and structured chat are all on', () => { - for (const experimentalNativeChat of [false, true]) { - for (const openAgentTabsInChatByDefault of [false, true]) { - for (const experimentalStructuredNativeChat of [false, true]) { - const { container, unmount } = renderSetting({ - experimentalNativeChat, - openAgentTabsInChatByDefault, - experimentalStructuredNativeChat - }) - const expected = - experimentalNativeChat && - openAgentTabsInChatByDefault && - experimentalStructuredNativeChat - expect( - container.querySelector(SHELL_ENV_TOGGLE) !== null, - JSON.stringify({ - experimentalNativeChat, - openAgentTabsInChatByDefault, - experimentalStructuredNativeChat - }) - ).toBe(expected) - unmount() - } - } - } + it('shows structured controls when Chat UI is on', () => { + const enabled = renderSetting({ experimentalNativeChat: true }) + expect(enabled.container.querySelector(SHELL_ENV_TOGGLE)).not.toBeNull() + expect(enabled.container.textContent).not.toContain('Default view') + enabled.unmount() + + const disabled = renderSetting({ experimentalNativeChat: false }) + expect(disabled.container.querySelector(SHELL_ENV_TOGGLE)).toBeNull() + disabled.unmount() }) const structuredOn = { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true + experimentalNativeChat: true } const chooseNames = { ...structuredOn, nativeChatInheritShellEnvironment: false } @@ -215,9 +197,7 @@ describe('NativeChatExperimentalSetting shell environment', () => { describe('NativeChatExperimentalSetting queue follow-ups', () => { const QUEUE_TOGGLE = '[aria-label="Toggle queue follow-ups"]' const structuredOn = { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true + experimentalNativeChat: true } it('shows the switch, with copy naming the image exception, when the host queues messages', () => { @@ -249,12 +229,9 @@ describe('NativeChatExperimentalSetting queue follow-ups', () => { expect(container.querySelector(QUEUE_TOGGLE)).not.toBeNull() }) - it('stays hidden outside structured chat even when the host queues messages', () => { + it('hides the switch with Chat UI off when no structured chats are held', () => { setLocalRuntimeCapabilitiesForTests([AGENT_SESSION_QUEUED_MESSAGES_RUNTIME_CAPABILITY]) - const { container } = renderSetting({ - ...structuredOn, - experimentalStructuredNativeChat: false - }) + const { container } = renderSetting({ experimentalNativeChat: false }) expect(container.querySelector(QUEUE_TOGGLE)).toBeNull() }) }) @@ -262,9 +239,7 @@ describe('NativeChatExperimentalSetting queue follow-ups', () => { describe('NativeChatExperimentalSetting inline visuals', () => { it('leaves Inline visuals on the Chat page even while structured chat is enabled', () => { const { queryByRole, getByRole } = renderSetting({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true + experimentalNativeChat: true }) expect(getByRole('switch', { name: 'Toggle automatic resume after a restart' })).toBeTruthy() expect(queryByRole('switch', { name: 'Toggle inline visuals' })).toBeNull() diff --git a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx index fae3362eb74..34c5de50a65 100644 --- a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx +++ b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx @@ -2,7 +2,6 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types' import { useLocalStructuredAgentSessionsHeld } from '@/runtime/local-structured-chats' import { translate } from '@/i18n/i18n' import { Label } from '../ui/label' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { NativeChatQueueFollowUpsSetting } from './NativeChatQueueFollowUpsSetting' import { NativeChatShellEnvironmentSetting } from './NativeChatShellEnvironmentSetting' import { NativeChatSupportedAgents } from './NativeChatSupportedAgents' @@ -10,8 +9,6 @@ import { SearchableSetting } from './SearchableSetting' import { SettingsSwitch } from './SettingsFormControls' import { getExperimentalSearchEntry } from './experimental-search' -type NativeChatDefaultView = 'terminal-chat' | 'native-chat' - type NativeChatExperimentalSettingProps = { settings: GlobalSettings updateSettings: (updates: Partial) => void @@ -22,22 +19,16 @@ export function NativeChatExperimentalSetting({ updateSettings }: NativeChatExperimentalSettingProps): React.JSX.Element { const nativeChatEnabled = settings.experimentalNativeChat === true - const structuredNativeChatEnabled = settings.experimentalStructuredNativeChat === true const resumeOnRestartEnabled = settings.nativeChatResumeWorkOnRestart === true - const defaultView: NativeChatDefaultView = - settings.openAgentTabsInChatByDefault === true ? 'native-chat' : 'terminal-chat' - // Structured-only settings; terminal-backed chat never reads them. They govern the chats this - // machine holds too, which keep running whatever the setting says. const holdsStructuredChats = useLocalStructuredAgentSessionsHeld() - const structuredChatActive = - (defaultView === 'native-chat' && structuredNativeChatEnabled) || holdsStructuredChats + const structuredChatActive = nativeChatEnabled || holdsStructuredChats return ( {translate( 'auto.components.settings.ExperimentalPane.nativeChat.copy', - 'Enables the experimental Chat UI for newly created supported local sessions. Existing terminal sessions keep the terminal chat path while we tune transcript fidelity, streaming, and parity.' + 'Supported new agents open in structured chat. Other agents open in the terminal; existing chats stay available.' )}

@@ -69,97 +60,8 @@ export function NativeChatExperimentalSetting({ } /> - {nativeChatEnabled ? ( + {structuredChatActive ? (
-
-
- -

- {translate( - 'auto.components.settings.ExperimentalPane.nativeChat.defaultCopy', - 'Choose how new supported agent terminal tabs open.' - )} -

-
- -
- - {/* Structured chat rides the Chat UI default view; it has no entry path under Terminal - chat. Hidden only — the opt-in keeps its persisted value for when Chat UI returns. */} - {defaultView === 'native-chat' ? ( -
-
- -

- {translate( - 'auto.components.settings.ExperimentalPane.nativeChat.structuredCopy', - 'Open new agents as structured chats where supported. Off opens them in the terminal-backed chat. Chats that already exist stay as they are.' - )} -

-

- {translate( - 'auto.components.settings.ExperimentalPane.nativeChat.structuredScope', - 'Runs on this machine and on paired Orca servers running a version that supports it; older servers keep terminal chat. WSL and SSH hosts continue to use terminal chat.' - )} -

-
- - updateSettings({ - experimentalStructuredNativeChat: !structuredNativeChatEnabled - }) - } - /> -
- ) : null} - {/* Only structured sessions have a resume cursor to continue from. */} {structuredChatActive ? (
diff --git a/src/renderer/src/components/settings/SettingsSidebar.test.tsx b/src/renderer/src/components/settings/SettingsSidebar.test.tsx index 8d7a12ef555..122c641097f 100644 --- a/src/renderer/src/components/settings/SettingsSidebar.test.tsx +++ b/src/renderer/src/components/settings/SettingsSidebar.test.tsx @@ -135,12 +135,12 @@ describe('SettingsSidebar', () => { }) it.each([false, true])('renders Chat navigation only with the opt-in enabled (%s)', (enabled) => { - const settings = { ...getDefaultSettings('/tmp'), experimentalStructuredNativeChat: enabled } + const settings = { ...getDefaultSettings('/tmp'), experimentalNativeChat: enabled } const sections = buildSettingsNavigationMetadata({ isMac: false, isWindows: false, isWebClient: false, - experimentalStructuredNativeChat: enabled, + nativeChatEnabled: enabled, repos: [] }).filter((section) => section.group === 'interface') const container = document.createElement('div') diff --git a/src/renderer/src/components/settings/settings-persistence-test-fixture.ts b/src/renderer/src/components/settings/settings-persistence-test-fixture.ts index 181e8736172..d6cd5d8e266 100644 --- a/src/renderer/src/components/settings/settings-persistence-test-fixture.ts +++ b/src/renderer/src/components/settings/settings-persistence-test-fixture.ts @@ -17,7 +17,7 @@ beforeEach(() => { useAppStore.setState({ settings: { ...getDefaultSettings('/synthetic'), - experimentalStructuredNativeChat: true, + experimentalNativeChat: true, sourceControlAi: normalizeSourceControlAiSettings({ ...normalizeSourceControlAiSettings(undefined), enabled: false, diff --git a/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx b/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx index 024cab700f2..8ae03f120d8 100644 --- a/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx +++ b/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx @@ -14,7 +14,6 @@ import { resolveDismissedOnboardingFolderAgentLaunch, revealOnboardingFolderWithAgentLaunch } from '@/lib/onboarding-folder-agent-launch' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { markOnboardingProjectAdded } from '@/lib/onboarding-project-checklist' import { translate } from '@/i18n/i18n' import { upsertAddedRepoWithProjectHostSetup } from './add-repo-store-upsert' @@ -92,8 +91,7 @@ const NonGitFolderDialog = React.memo(function NonGitFolderDialog() { store: useAppStore.getState(), onboarding, hasExistingProject: hadProjectBeforeAdd, - executionHostId: ownerOptions.executionHostId ?? connectionId, - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable(connectionId) + executionHostId: ownerOptions.executionHostId ?? connectionId }) await revealOnboardingFolderWithAgentLaunch({ worktreeId: folderWorktree.id, diff --git a/src/renderer/src/components/sidebar/folder-workspace-agent-startup.ts b/src/renderer/src/components/sidebar/folder-workspace-agent-startup.ts index d3746a38e82..bffc6debf1f 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-agent-startup.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-agent-startup.ts @@ -9,7 +9,6 @@ import { isWindowsAbsolutePathLike } from '../../../../shared/cross-platform-pat import type { ProjectGroup } from '../../../../shared/project-group-types' import type { TuiAgent } from '../../../../shared/tui-agent' import type { AgentStartupShell } from '../../../../shared/tui-agent-startup-shell' -import type { SessionOptionValue } from '../../../../shared/native-chat-session-options' import { isWslUncPath } from '../../../../shared/wsl-paths' export function getFolderWorkspaceAgentLaunchPlatform( @@ -38,7 +37,6 @@ export function buildFolderWorkspaceLinkedStartupPlan(args: { agentCmdOverrides: Record | undefined agentArgs?: string | null agentEnv?: Record - sessionOptions?: Record platform: NodeJS.Platform shell?: AgentStartupShell isRemote: boolean @@ -51,7 +49,6 @@ export function buildFolderWorkspaceLinkedStartupPlan(args: { cmdOverrides: args.agentCmdOverrides ?? {}, agentArgs: args.agentArgs, agentEnv: args.agentEnv, - sessionOptions: args.sessionOptions, platform: args.platform, shell: args.shell, isRemote: args.isRemote @@ -64,7 +61,6 @@ export function buildFolderWorkspaceLinkedStartupPlan(args: { expectedProcess: draftLaunchPlan.expectedProcess, followupPrompt: null, launchConfig: draftLaunchPlan.launchConfig, - ...(draftLaunchPlan.sessionOptions ? { sessionOptions: draftLaunchPlan.sessionOptions } : {}), ...(draftLaunchPlan.startupCommandDelivery ? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery } : {}), @@ -79,7 +75,6 @@ export function buildFolderWorkspaceLinkedStartupPlan(args: { cmdOverrides: args.agentCmdOverrides ?? {}, agentArgs: args.agentArgs, agentEnv: args.agentEnv, - sessionOptions: args.sessionOptions, platform: args.platform, shell: args.shell, isRemote: args.isRemote, diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts index 6976647ed8b..2e09dee878e 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts @@ -11,8 +11,6 @@ const mocks = vi.hoisted(() => ({ ensureAgentStartupInTerminal: vi.fn() })) -// Why: importOriginal keeps the real resolveStartupLaunchDraftText, so the -// invariant test below exercises the shipped gate instead of a copy of it. vi.mock('@/lib/worktree-activation', async (importOriginal) => { const actual = await importOriginal>() return { ...actual, activateAndRevealFolderWorkspace: mocks.activateAndRevealFolderWorkspace } @@ -27,8 +25,6 @@ vi.mock('@/lib/new-workspace', async (importOriginal) => { }) import { useAppStore } from '@/store' -import { decideInitialAgentTabViewMode } from '@/lib/native-chat-initial-view-mode' -import { resolveStartupLaunchDraftText } from '@/lib/worktree-startup-payload' import { getFolderWorkspaceAgentLaunchPlatform, submitFolderWorkspaceCreate @@ -804,9 +800,8 @@ describe('folder-workspace draft: seeded set == chat-opening set', () => { vi.restoreAllMocks() }) - // Why: `claude` takes its draft on argv, so `startupPlan.draftPrompt` stays - // undefined; `codex` gets a startup paste and sets it. Both must reach the - // view-mode gate, and both must agree with what the composer actually holds. + // Claude takes its draft on argv; Codex receives a startup paste. Both keep + // the draft available for an explicit switch to terminal chat. it.each([ ['argv-prefill', 'claude' as const, '', true], ['argv-prefill multi-line', 'claude' as const, 'Reproduce on Windows first', true], @@ -828,20 +823,8 @@ describe('folder-workspace draft: seeded set == chat-opening set', () => { const startup = mocks.activateAndRevealFolderWorkspace.mock.calls[0]?.[1]?.startup const seeded = useAppStore.getState().nativeChatLaunchDraftByTabId['tab-1'] != null - const draftText = resolveStartupLaunchDraftText(startup) - const opensInChat = - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: quickAgent, - ...(draftText != null - ? { promptDelivery: 'draft' as const, launchDraftText: draftText } - : {}) - }) === 'chat' - // The draft always reaches the TUI, whichever way it is delivered. expect(`${startup?.command ?? ''}${startup?.draftPrompt ?? ''}`).toContain(ISSUE_URL) expect(seeded).toBe(expectMirrored) - expect(opensInChat).toBe(expectMirrored) }) }) diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts index 40d65b759e2..a2df33dd4bd 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts @@ -10,7 +10,6 @@ import type { ProjectGroup } from '../../../../shared/project-group-types' import type { TuiAgent } from '../../../../shared/tui-agent' import { resolveLocalWindowsAgentStartupShell } from '../../../../shared/windows-terminal-shell' import type { LaunchSource } from '../../../../shared/telemetry-events' -import type { SessionOptionValue } from '../../../../shared/native-chat-session-options' import type { TaskSourceContext } from '../../../../shared/task-source-context' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { @@ -56,7 +55,6 @@ type SubmitFolderWorkspaceCreateParams = { agentCmdOverrides: Record | undefined agentArgs?: string | null agentEnv?: Record - sessionOptions?: Record terminalWindowsShell?: string | null isRemote?: boolean launchSource?: LaunchSource @@ -77,7 +75,6 @@ export async function submitFolderWorkspaceCreate({ agentCmdOverrides, agentArgs, agentEnv, - sessionOptions, terminalWindowsShell, launchSource = 'sidebar', runtimeEnvironmentId = null, @@ -108,7 +105,6 @@ export async function submitFolderWorkspaceCreate({ agentCmdOverrides, agentArgs, agentEnv, - sessionOptions, platform: launchPlatform, shell: launchShell, isRemote: launchIsRemote @@ -120,7 +116,6 @@ export async function submitFolderWorkspaceCreate({ cmdOverrides: agentCmdOverrides ?? {}, agentArgs, agentEnv, - sessionOptions, platform: launchPlatform, shell: launchShell, isRemote: launchIsRemote, @@ -141,8 +136,7 @@ export async function submitFolderWorkspaceCreate({ executionHostId: getNewWorkspaceProjectGroupHostId(projectGroup) }, prompt: launchDraftPrompt ?? note, - promptDelivery: launchDraftPrompt ? 'draft' : 'auto-submit', - initialSessionOptions: startupPlan?.sessionOptions + promptDelivery: launchDraftPrompt ? 'draft' : 'auto-submit' }) : null const structuredLaunch = plan?.route === 'structured-native-chat' @@ -183,11 +177,7 @@ export async function submitFolderWorkspaceCreate({ launchConfig: startupPlan.launchConfig, ...(startupPlan.launchToken ? { launchToken: startupPlan.launchToken } : {}), launchAgent: quickAgent, - ...(startupPlan.sessionOptions ? { sessionOptions: startupPlan.sessionOptions } : {}), ...(startupPlan.draftPrompt ? { draftPrompt: startupPlan.draftPrompt } : {}), - // Why: view-mode only. The argv-prefill plan sets no draftPrompt, so - // without this the tab opens in chat with nothing mirrored into it. - ...(launchDraftPrompt ? { launchDraftText: launchDraftPrompt } : {}), ...(startupPlan.startupCommandDelivery ? { startupCommandDelivery: startupPlan.startupCommandDelivery } : {}), diff --git a/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx b/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx index fc5fefce150..108f51e17da 100644 --- a/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx +++ b/src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx @@ -72,7 +72,7 @@ describe('NativeChatResumeStatusSegment', () => { consumeNativeChatResumeOnRestartDialogRequest() useAppStore.setState({ ...useAppStore.getInitialState(), - settings: { ...getDefaultSettings(''), experimentalStructuredNativeChat: true } + settings: { ...getDefaultSettings(''), experimentalNativeChat: true } }) }) @@ -186,7 +186,7 @@ describe('NativeChatResumeStatusSegment', () => { it('hides when no chat exists and the setting is off, or the host offers nothing', async () => { rpc.mockResolvedValue({ sessions: candidates }) useAppStore.setState({ - settings: { ...getDefaultSettings(''), experimentalStructuredNativeChat: false } + settings: { ...getDefaultSettings(''), experimentalNativeChat: false } }) await mount() expect(screen.queryByRole('button')).toBeNull() @@ -196,7 +196,7 @@ describe('NativeChatResumeStatusSegment', () => { cleanup() rpc.mockResolvedValue({ sessions: [] }) useAppStore.setState({ - settings: { ...getDefaultSettings(''), experimentalStructuredNativeChat: true } + settings: { ...getDefaultSettings(''), experimentalNativeChat: true } }) await mount() expect(screen.queryByRole('button')).toBeNull() @@ -206,7 +206,7 @@ describe('NativeChatResumeStatusSegment', () => { it('offers to continue the chats this machine holds while the setting is off', async () => { rpc.mockResolvedValue({ sessions: candidates }) useAppStore.setState({ - settings: { ...getDefaultSettings(''), experimentalStructuredNativeChat: false } + settings: { ...getDefaultSettings(''), experimentalNativeChat: false } }) stageLocalHost(true) await mount() @@ -219,7 +219,7 @@ describe('NativeChatResumeStatusSegment', () => { it("does not ask this machine for an offer over a paired server's chats", async () => { rpc.mockResolvedValue({ sessions: candidates }) useAppStore.setState({ - settings: { ...getDefaultSettings(''), experimentalStructuredNativeChat: false }, + settings: { ...getDefaultSettings(''), experimentalNativeChat: false }, unifiedTabsByWorktree: { 'wt-1': [ { diff --git a/src/renderer/src/components/tab-bar/TabBarItemRow.test.tsx b/src/renderer/src/components/tab-bar/TabBarItemRow.test.tsx index a095d4e2e6f..90fbfc385f4 100644 --- a/src/renderer/src/components/tab-bar/TabBarItemRow.test.tsx +++ b/src/renderer/src/components/tab-bar/TabBarItemRow.test.tsx @@ -161,7 +161,6 @@ function Strip({ resolvedGroupId: 'group-1', generatedTabTitlesEnabled, unifiedTabByVisibleId: new Map(), - nativeChatEnabled: false, tabAgentTypesByTabId: {}, nativeChatTabWideFallbackUnsafeTabsById: {}, nativeChatTranscriptIsLocalReadable: false, diff --git a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts index 38d6f2e267f..6b6b72727f2 100644 --- a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts +++ b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts @@ -307,7 +307,7 @@ describe('selectTabAgentTypesByTabId', () => { expect(select({ settings: { experimentalNativeChat: true } })).toBe(first) }) - it('releases enabled inputs on disable and rescans them after re-enabling', () => { + it('keeps agent evidence available for explicit chat toggles while the default is off', () => { const onStatusEntryVisited = vi.fn() const onAgentTypeLayoutVisited = vi.fn() const onUnsafeLayoutVisited = vi.fn() @@ -323,16 +323,17 @@ describe('selectTabAgentTypesByTabId', () => { } const first = select(state) - select({ ...state, settings: { experimentalNativeChat: false } }) + const disabled = select({ ...state, settings: { experimentalNativeChat: false } }) const afterReenable = select(state) + expect(disabled.tabAgentTypesByTabId).toEqual({ 'tab-1': 'claude' }) expect(afterReenable).not.toBe(first) - expect(onStatusEntryVisited).toHaveBeenCalledTimes(2) - expect(onAgentTypeLayoutVisited).toHaveBeenCalledTimes(2) - expect(onUnsafeLayoutVisited).toHaveBeenCalledTimes(2) + expect(onStatusEntryVisited).toHaveBeenCalledTimes(1) + expect(onAgentTypeLayoutVisited).toHaveBeenCalledTimes(1) + expect(onUnsafeLayoutVisited).toHaveBeenCalledTimes(1) }) - it('production selector skips all map scans while native chat is disabled', () => { + it('production selector scans once and reuses agent evidence with Chat UI off', () => { let statusEnumerations = 0 let layoutEnumerations = 0 const statuses = new Proxy( @@ -363,8 +364,8 @@ describe('selectTabAgentTypesByTabId', () => { for (let consumer = 0; consumer < 100; consumer++) { expect(selectTabBarAgentProjections(disabledState)).toBe(disabled) } - expect(statusEnumerations).toBe(0) - expect(layoutEnumerations).toBe(0) + expect(statusEnumerations).toBe(1) + expect(layoutEnumerations).toBe(2) const enabledState = { ...disabledState, diff --git a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts index 420949992d8..cf4ae8cc07b 100644 --- a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts +++ b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts @@ -27,12 +27,6 @@ const EMPTY_AGENT_STATUS_BY_PANE_KEY: Record = Object. const EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID: Record = Object.freeze({}) const EMPTY_TAB_AGENT_TYPES_BY_TAB_ID: Record = Object.freeze({}) const EMPTY_UNSAFE_TABS_BY_ID: Record = Object.freeze({}) -const DISABLED_TAB_BAR_AGENT_PROJECTIONS: TabBarAgentProjections = Object.freeze({ - nativeChatEnabled: false, - tabAgentTypesByTabId: EMPTY_TAB_AGENT_TYPES_BY_TAB_ID, - nativeChatTabWideFallbackUnsafeTabsById: EMPTY_UNSAFE_TABS_BY_ID -}) - function reuseRecordIfEqual( previous: Record | undefined, next: Record @@ -142,18 +136,7 @@ export function createTabBarAgentProjectionSelector( let cachedEnabledResult: TabBarAgentProjections | null = null return (state) => { - if (state.settings?.experimentalNativeChat !== true) { - if (cachedEnabledResult) { - cachedAgentStatusByPaneKey = null - cachedAgentTypeLayoutsByTabId = null - cachedAgentTypesByTabId = EMPTY_TAB_AGENT_TYPES_BY_TAB_ID - cachedUnsafeLayoutsByTabId = null - cachedUnsafeTabsById = EMPTY_UNSAFE_TABS_BY_ID - cachedEnabledResult = null - } - return DISABLED_TAB_BAR_AGENT_PROJECTIONS - } - + const nativeChatEnabled = state.settings?.experimentalNativeChat === true const statuses = state.agentStatusByPaneKey ?? EMPTY_AGENT_STATUS_BY_PANE_KEY const layouts = state.terminalLayoutsByTabId ?? EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID if (statuses !== cachedAgentStatusByPaneKey || layouts !== cachedAgentTypeLayoutsByTabId) { @@ -172,13 +155,14 @@ export function createTabBarAgentProjectionSelector( cachedUnsafeLayoutsByTabId = layouts } if ( - cachedEnabledResult?.tabAgentTypesByTabId === cachedAgentTypesByTabId && + cachedEnabledResult?.nativeChatEnabled === nativeChatEnabled && + cachedEnabledResult.tabAgentTypesByTabId === cachedAgentTypesByTabId && cachedEnabledResult.nativeChatTabWideFallbackUnsafeTabsById === cachedUnsafeTabsById ) { return cachedEnabledResult } cachedEnabledResult = { - nativeChatEnabled: true, + nativeChatEnabled, tabAgentTypesByTabId: cachedAgentTypesByTabId, nativeChatTabWideFallbackUnsafeTabsById: cachedUnsafeTabsById } diff --git a/src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx b/src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx index 7cd3c69cd35..48d7f0421f5 100644 --- a/src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx +++ b/src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx @@ -28,7 +28,6 @@ export type TabBarItemSurfaceRuntime = Pick< | 'resolvedGroupId' | 'generatedTabTitlesEnabled' | 'unifiedTabByVisibleId' - | 'nativeChatEnabled' | 'tabAgentTypesByTabId' | 'nativeChatTabWideFallbackUnsafeTabsById' | 'nativeChatTranscriptIsLocalReadable' @@ -66,7 +65,6 @@ export function renderTabBarItems({ resolvedGroupId, generatedTabTitlesEnabled, unifiedTabByVisibleId, - nativeChatEnabled, tabAgentTypesByTabId, nativeChatTabWideFallbackUnsafeTabsById, nativeChatTranscriptIsLocalReadable, @@ -114,7 +112,6 @@ export function renderTabBarItems({ canToggleViewMode = unifiedTabForItem !== undefined && canToggleNativeChat({ - experimentalNativeChatEnabled: nativeChatEnabled, contentType: 'terminal', launchAgent: tabWideFallbackSafe ? terminalTab.launchAgent : null, detectedAgent, @@ -122,7 +119,7 @@ export function renderTabBarItems({ nativeChatTranscriptIsLocalReadable, isChatViewMode: unifiedTabForItem.viewMode === 'chat' }) - isChatView = nativeChatEnabled && unifiedTabForItem?.viewMode === 'chat' + isChatView = unifiedTabForItem?.viewMode === 'chat' viewModeTabId = unifiedTabForItem?.id } return ( diff --git a/src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts b/src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts index 3436b2200f2..792093937a6 100644 --- a/src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts +++ b/src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts @@ -249,7 +249,7 @@ export function useTabBarRuntimeModel({ ) // Why: tab-wide launch/title hints are safe only before split; gate the view-mode toggle to the active leaf's agent. const toggleTabViewMode = useAppStore((s) => s.toggleTabViewMode) - // Why: every retained TabBar observes the same hot maps; one feature-gated selector shares their projections. + // Why: every retained TabBar observes the same hot maps; one selector shares their projections. const { nativeChatEnabled, tabAgentTypesByTabId, nativeChatTabWideFallbackUnsafeTabsById } = useAppStore(useShallow(selectTabBarAgentProjections)) const nativeChatTranscriptIsLocalReadable = useAppStore((s) => diff --git a/src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx b/src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx index 350c27667fa..ae024a5886e 100644 --- a/src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx +++ b/src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx @@ -136,6 +136,7 @@ function useFixture(fixture: ReturnType, initialOwner: strin beforeEach(() => { mocks.state.settings.experimentalNativeChat = true + mocks.state.runtimePaneTitlesByTabId.tab = { 1: 'codex', 2: 'codex' } mocks.state.setTabViewMode('tab', 'chat') mocks.state.setTabLayout('tab', { root: { @@ -166,9 +167,10 @@ describe('terminal chat ownership lifecycle', () => { expect(mocks.state.terminalLayoutsByTabId.tab.chatLeafId).toBeUndefined() }) - it('preserves the saved owner when this client disables native chat', () => { + it('keeps a saved legacy chat and its explicit toggle available with Chat UI off', () => { const push = vi.fn() const fixture = makeFixture(undefined, { push }) + mocks.state.runtimePaneTitlesByTabId.tab = { 1: 'bash', 2: 'codex' } mocks.state.terminalLayoutsByTabId.tab.ptyIdsByLeafId = { [LEFT]: 'remote:host:terminal', [RIGHT]: 'remote:host:chat' @@ -177,14 +179,21 @@ describe('terminal chat ownership lifecycle', () => { mocks.state.settings.experimentalNativeChat = false hook.rerender() hook.result.current.persistLayoutSnapshot() - expect(hook.result.current.effectiveChatViewMode).toBe(false) + expect(hook.result.current.effectiveChatViewMode).toBe(true) expect(mocks.state.unifiedTabsByWorktree.wt[0].viewMode).toBe('chat') expect(mocks.state.terminalLayoutsByTabId.tab.chatLeafId).toBe(RIGHT) + expect(hook.result.current.canToggleChatForLeaf(RIGHT)).toBe(true) + expect(hook.result.current.canToggleChatForLeaf(LEFT)).toBe(false) expect(push).toHaveBeenLastCalledWith( expect.objectContaining({ layout: expect.objectContaining({ chatLeafId: RIGHT }) }) ) + act(() => hook.result.current.toggleNativeChatForLeaf(RIGHT)) + expect(mocks.state.unifiedTabsByWorktree.wt[0].viewMode).toBe('terminal') + expect(hook.result.current.canToggleChatForLeaf(RIGHT)).toBe(true) + act(() => hook.result.current.toggleNativeChatForLeaf(RIGHT)) + expect(mocks.state.unifiedTabsByWorktree.wt[0].viewMode).toBe('chat') }) it('clears ownership when the tab-bar turns chat off, then targets the active sibling', () => { diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx b/src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx index 9228ceaa5c7..741519aaab5 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx +++ b/src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx @@ -26,7 +26,7 @@ import { * visit per store publication for every retained tab in the app — read the doc * above before you do. */ -const TERMINAL_PANE_LISTENER_BUDGET = 16 +const TERMINAL_PANE_LISTENER_BUDGET = 15 /** What the same mount cost before the stable-action and unified-tab folds: one listener per * bound action (each new stable action raises this by one, never the budget) plus the folded reads. */ const PRE_FOLD_LISTENERS_PER_PANE = 50 @@ -105,8 +105,9 @@ describe('TerminalPane store subscription budget', () => { expect(perPane).toBe(TERMINAL_PANE_LISTENER_BUDGET) expect(perPane).toBeLessThan(PRE_FOLD_LISTENERS_PER_PANE) - // 29 stable actions, four duplicate unified-tab reads, one dead dispatch-status read. - expect(PRE_FOLD_LISTENERS_PER_PANE - perPane).toBe(TERMINAL_PANE_STORE_ACTION_KEYS.length + 5) + // 29 stable actions, four duplicate unified-tab reads, one dead dispatch-status read, + // one retired per-pane Chat UI setting read. + expect(PRE_FOLD_LISTENERS_PER_PANE - perPane).toBe(TERMINAL_PANE_STORE_ACTION_KEYS.length + 6) unmount() expect(listenerCount()).toBe(baseline) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts index 06c3b979f63..400397f14f4 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts @@ -47,8 +47,7 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController selectUnifiedTerminalTabFields(store.unifiedTabsByWorktree, worktreeId, tabId) ) ) - const nativeChatEnabled = useAppStore((store) => store.settings?.experimentalNativeChat === true) - const effectiveChatViewMode = nativeChatEnabled && isChatViewMode + const effectiveChatViewMode = isChatViewMode const runtimePaneTitlesByPaneId = useAppStore( useShallow((store) => store.runtimePaneTitlesByTabId[tabId] ?? {}) ) @@ -129,7 +128,6 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController leafIds: getNativeChatLeafIds() }) return canToggleNativeChat({ - experimentalNativeChatEnabled: nativeChatEnabled, contentType: 'terminal', launchAgent: detectedAgent ? null : launchAgent, detectedAgent, @@ -139,7 +137,6 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController }, [ tabAgentTypeByLeaf, - nativeChatEnabled, nativeChatTranscriptIsLocalReadable, terminalTab?.launchAgent, getNativeChatLeafIds, @@ -208,9 +205,9 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController (leafId: string | null): boolean => { // Scope the "always allow toggling back" rule to the leaf showing chat; must not make an unsupported sibling look eligible. const isChatViewForLeaf = effectiveChatViewMode && leafId !== null && chatLeafId === leafId - return (nativeChatEnabled && isChatViewForLeaf) || isChatEligibleForLeaf(leafId) + return isChatViewForLeaf || isChatEligibleForLeaf(leafId) }, - [chatLeafId, effectiveChatViewMode, isChatEligibleForLeaf, nativeChatEnabled] + [chatLeafId, effectiveChatViewMode, isChatEligibleForLeaf] ) const toggleNativeChatForLeaf = useCallback( (leafId: string) => { @@ -261,7 +258,6 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController clearCodexRestartNotice, unifiedTabId, isChatViewMode, - nativeChatEnabled, effectiveChatViewMode, unifiedTabLabel, isTabPinned, diff --git a/src/renderer/src/hooks/composer-state/composer-submit-model.ts b/src/renderer/src/hooks/composer-state/composer-submit-model.ts index 351e5a23c07..490c543de4b 100644 --- a/src/renderer/src/hooks/composer-state/composer-submit-model.ts +++ b/src/renderer/src/hooks/composer-state/composer-submit-model.ts @@ -1,7 +1,6 @@ import type { TuiAgent } from '../../../../shared/tui-agent' import type { GitPushTarget } from '../../../../shared/worktree/types' import type { SetupDecision } from '../../../../shared/worktree/create-types' -import type { Repo } from '../../../../shared/repo-types' import type { WorkspaceIntentName } from '../../../../shared/workspace-name' import type { LinkedWorkItemSummary } from '@/lib/new-workspace' import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' @@ -47,8 +46,7 @@ export type ComposerSubmitModel = { requestedAgent: TuiAgent | null, workspaceNameSeed: string, workspaceRunContext: WorktreeCreationRequest['workspaceRunContext'], - repoId: string, - selectedRepo: Repo + repoId: string ) => Promise prepareQuickSubmit: ( resolution: PendingSmartGitHubSubmitResolution, diff --git a/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts b/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts index d1f3d0cb9ed..ed9e512d664 100644 --- a/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts +++ b/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts @@ -21,7 +21,6 @@ export function useComposerSubmitOrchestration( disabledTuiAgents: target.workspaceIdentityState.disabledTuiAgents, folderCreateDisabled: source.composerNavigationActions.folderCreateDisabled, folderSourceRepos: target.runtimeTargetSelection.folderSourceRepos, - folderTargetConnectionId: target.runtimeTargetSelection.folderTargetConnectionId, folderTargetIsRemote: target.runtimeTargetSelection.folderTargetIsRemote, folderTargetRuntimeEnvironmentId: target.runtimeTargetSelection.folderTargetRuntimeEnvironmentId, diff --git a/src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts b/src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts index e35abc70b1f..036177a4a73 100644 --- a/src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts +++ b/src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts @@ -8,7 +8,6 @@ type FolderSubmitOrchestrationInput = Pick< | 'disabledTuiAgents' | 'folderCreateDisabled' | 'folderSourceRepos' - | 'folderTargetConnectionId' | 'folderTargetIsRemote' | 'folderTargetRuntimeEnvironmentId' | 'isSubmissionCancelled' @@ -31,16 +30,11 @@ import { useCallback } from 'react' import type { TuiAgent } from '../../../../shared/tui-agent' import { settleComposerSubmit } from '@/lib/composer-submit-cancellation' import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' -import { - resolveFolderWorkspaceLaunchDraft, - submitFolderWorkspaceCreate -} from '@/components/sidebar/folder-workspace-composer-submit' +import { submitFolderWorkspaceCreate } from '@/components/sidebar/folder-workspace-composer-submit' import { resolveTuiAgentLaunchArgs, resolveTuiAgentLaunchEnv } from '../../../../shared/tui-agent-launch-defaults' -import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { translate } from '@/i18n/i18n' import { formatWorkspaceCreateError, @@ -56,7 +50,6 @@ export function useFolderSubmitOrchestration(input: FolderSubmitOrchestrationInp disabledTuiAgents, folderCreateDisabled, folderSourceRepos, - folderTargetConnectionId, folderTargetIsRemote, folderTargetRuntimeEnvironmentId, isSubmissionCancelled, @@ -107,10 +100,6 @@ export function useFolderSubmitOrchestration(input: FolderSubmitOrchestrationInp if (isSubmissionCancelled()) { return } - const folderLaunchDraftText = - agent && submitLinkedWorkItem - ? resolveFolderWorkspaceLaunchDraft(submitLinkedWorkItem, note) - : null const folderWorkspaceCreated = await submitFolderWorkspaceCreate({ projectGroup: selectedProjectGroup, name: smartGitHubMetadata?.workspaceName ?? name, @@ -125,23 +114,6 @@ export function useFolderSubmitOrchestration(input: FolderSubmitOrchestrationInp ? resolveTuiAgentLaunchArgs(agent, settings?.agentDefaultArgs) : undefined, agentEnv: agent ? resolveTuiAgentLaunchEnv(agent, settings?.agentDefaultEnv) : undefined, - sessionOptions: agent - ? resolveInitialNativeChatSessionOptions( - { - experimentalNativeChat: settings?.experimentalNativeChat, - openAgentTabsInChatByDefault: settings?.openAgentTabsInChatByDefault, - nativeChatSessionOptions: settings?.nativeChatSessionOptions - }, - { - agent, - ...(folderLaunchDraftText - ? { promptDelivery: 'draft' as const, launchDraftText: folderLaunchDraftText } - : {}), - nativeChatTranscriptIsLocalReadable: - isNativeChatTranscriptLocalReadable(folderTargetConnectionId) - } - ) - : undefined, terminalWindowsShell: settings?.terminalWindowsShell, isRemote: folderTargetIsRemote, launchSource: telemetrySource === 'onboarding' ? 'onboarding' : 'new_workspace_composer', @@ -188,7 +160,6 @@ export function useFolderSubmitOrchestration(input: FolderSubmitOrchestrationInp canResolveFolderSmartGitHubSubmit, disabledTuiAgents, folderCreateDisabled, - folderTargetConnectionId, folderTargetIsRemote, folderTargetRuntimeEnvironmentId, folderSourceRepos.length, diff --git a/src/renderer/src/hooks/composer-state/quick-creation-execution.ts b/src/renderer/src/hooks/composer-state/quick-creation-execution.ts index 554571dc631..c2b5119c8bc 100644 --- a/src/renderer/src/hooks/composer-state/quick-creation-execution.ts +++ b/src/renderer/src/hooks/composer-state/quick-creation-execution.ts @@ -33,7 +33,6 @@ type QuickCreationExecutionInput = Pick< > import { useCallback } from 'react' -import type { Repo } from '../../../../shared/repo-types' import type { TuiAgent } from '../../../../shared/tui-agent' import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' import { useAppStore } from '@/store' @@ -85,8 +84,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) { requestedAgent: TuiAgent | null, workspaceNameSeed: string, workspaceRunContext: WorktreeCreationRequest['workspaceRunContext'], - repoId: string, - selectedRepo: Repo + repoId: string ): Promise => { const prepared = await prepareQuickSubmit( smartGitHubResolution, @@ -134,7 +132,6 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) { prompt: quickPrompt, draftPrompt: quickDraftPrompt, settings, - repoConnectionId: selectedRepo.connectionId, platform: selectedRepoAgentLaunchPlatform, shell: selectedRepoStartupShell, isRemote: selectedRepoIsRemote, @@ -188,8 +185,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) { : (workspaceRunContext?.hostId ?? selectedRepoExecutionHostId ?? undefined) }, prompt: quickDraftPrompt ?? quickPrompt, - promptDelivery, - initialSessionOptions: startupPlan?.sessionOptions + promptDelivery }) : 'terminal-tui' const structuredLaunch = agentLaunchRoute === 'structured-native-chat' diff --git a/src/renderer/src/hooks/composer-state/quick-startup-plan.ts b/src/renderer/src/hooks/composer-state/quick-startup-plan.ts index 48655019781..f1f3ab33576 100644 --- a/src/renderer/src/hooks/composer-state/quick-startup-plan.ts +++ b/src/renderer/src/hooks/composer-state/quick-startup-plan.ts @@ -9,8 +9,6 @@ import { resolveTuiAgentLaunchArgs, resolveTuiAgentLaunchEnv } from '../../../../shared/tui-agent-launch-defaults' -import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { tuiAgentToAgentKind } from '@/lib/telemetry' export type QuickComposerStartupInput = { @@ -18,7 +16,6 @@ export type QuickComposerStartupInput = { prompt: string draftPrompt: string | null | undefined settings: GlobalSettings | null | undefined - repoConnectionId: string | null | undefined platform: NodeJS.Platform shell: AgentStartupShell | null | undefined isRemote: boolean @@ -33,25 +30,6 @@ export type QuickComposerStartup = { export function buildQuickComposerStartup(input: QuickComposerStartupInput): QuickComposerStartup { const { agent, draftPrompt, prompt, settings } = input - const sessionOptions = - agent === null - ? undefined - : resolveInitialNativeChatSessionOptions( - { - experimentalNativeChat: settings?.experimentalNativeChat, - openAgentTabsInChatByDefault: settings?.openAgentTabsInChatByDefault, - nativeChatSessionOptions: settings?.nativeChatSessionOptions - }, - { - agent, - ...(draftPrompt - ? { promptDelivery: 'draft' as const, launchDraftText: draftPrompt } - : {}), - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - input.repoConnectionId - ) - } - ) const draftLaunchPlan = agent === null || !draftPrompt ? null @@ -61,7 +39,6 @@ export function buildQuickComposerStartup(input: QuickComposerStartupInput): Qui cmdOverrides: settings?.agentCmdOverrides ?? {}, agentArgs: resolveTuiAgentLaunchArgs(agent, settings?.agentDefaultArgs), agentEnv: resolveTuiAgentLaunchEnv(agent, settings?.agentDefaultEnv), - sessionOptions, platform: input.platform, shell: input.shell ?? undefined, isRemote: input.isRemote @@ -74,7 +51,6 @@ export function buildQuickComposerStartup(input: QuickComposerStartupInput): Qui expectedProcess: draftLaunchPlan.expectedProcess, followupPrompt: null, launchConfig: draftLaunchPlan.launchConfig, - ...(draftLaunchPlan.sessionOptions ? { sessionOptions: draftLaunchPlan.sessionOptions } : {}), ...(draftLaunchPlan.startupCommandDelivery ? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery } : {}), @@ -87,7 +63,6 @@ export function buildQuickComposerStartup(input: QuickComposerStartupInput): Qui cmdOverrides: settings?.agentCmdOverrides ?? {}, agentArgs: resolveTuiAgentLaunchArgs(agent, settings?.agentDefaultArgs), agentEnv: resolveTuiAgentLaunchEnv(agent, settings?.agentDefaultEnv), - sessionOptions, platform: input.platform, shell: input.shell ?? undefined, isRemote: input.isRemote, diff --git a/src/renderer/src/hooks/composer-state/quick-submit-action.ts b/src/renderer/src/hooks/composer-state/quick-submit-action.ts index 1ce90eb3f90..3bdce721bfd 100644 --- a/src/renderer/src/hooks/composer-state/quick-submit-action.ts +++ b/src/renderer/src/hooks/composer-state/quick-submit-action.ts @@ -143,8 +143,7 @@ export function useQuickSubmitAction(input: QuickSubmitActionInput) { requestedAgent, workspaceNameSeed, workspaceRunContext, - repoId, - selectedRepo + repoId ) } catch (error) { if (isSubmissionCancelled()) { diff --git a/src/renderer/src/hooks/ipc-events-terminal-create-scenario-types.ts b/src/renderer/src/hooks/ipc-events-terminal-create-scenario-types.ts index af3ef297368..12ec35eadc6 100644 --- a/src/renderer/src/hooks/ipc-events-terminal-create-scenario-types.ts +++ b/src/renderer/src/hooks/ipc-events-terminal-create-scenario-types.ts @@ -118,7 +118,6 @@ export type TerminalCreateSurfacingStore = { settings: { terminalFontSize: number experimentalNativeChat: boolean - openAgentTabsInChatByDefault: boolean activeRuntimeEnvironmentId: string | undefined } } diff --git a/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts b/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts index 0b44fbfbf5c..67e5931cfa9 100644 --- a/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts @@ -104,7 +104,6 @@ export async function setupTerminalCreateSurfacing( settings: { terminalFontSize: 13, experimentalNativeChat: false, - openAgentTabsInChatByDefault: false, activeRuntimeEnvironmentId: undefined as string | undefined } } diff --git a/src/renderer/src/hooks/ipc-events/terminal-presentation-ipc-bridge.ts b/src/renderer/src/hooks/ipc-events/terminal-presentation-ipc-bridge.ts index e978865a34e..09da5ff7ff7 100644 --- a/src/renderer/src/hooks/ipc-events/terminal-presentation-ipc-bridge.ts +++ b/src/renderer/src/hooks/ipc-events/terminal-presentation-ipc-bridge.ts @@ -7,9 +7,6 @@ import { SPLIT_TERMINAL_PANE_EVENT } from '@/constants/terminal' import type { SplitTerminalPaneDetail } from '@/constants/terminal' import { singlePaneLayoutSnapshot } from '@/store/slices/terminal-helpers' import { verifyTerminalRevealIdentity } from '@/lib/terminal-reveal-identity' -import { initialAgentTabViewModeProps } from '@/lib/native-chat-initial-view-mode' -import { getConnectionIdFromState } from '@/lib/connection-context' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { tryMakePaneKey } from './agent-status-routing' import { wasAgentLaunchPaneClosedByUser } from '@/lib/agent-launch-pane-closes' import { useAppStore } from '../../store' @@ -104,15 +101,7 @@ export function registerTerminalPresentationIpcBridge(unsubs: (() => void)[]): v ? { launchAgent, // Why: a paired client resolved explicit mode before PTY materialization; only omitted mode uses host defaults. - ...(viewMode - ? { viewMode } - : initialAgentTabViewModeProps(store.settings, { - agent: launchAgent, - nativeChatTranscriptIsLocalReadable: - isNativeChatTranscriptLocalReadable( - getConnectionIdFromState(store, worktreeId) - ) - })) + ...(viewMode ? { viewMode } : {}) } : {}), ...(cwd ? { startupCwd: cwd } : {}), diff --git a/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts b/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts index 6aa784fa746..35f9cdcc79a 100644 --- a/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts +++ b/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts @@ -1,7 +1,4 @@ import { requestBackgroundTerminalWorktreeMount } from '@/components/terminal/background-terminal-worktree-mount' -import { getConnectionIdFromState } from '@/lib/connection-context' -import { initialAgentTabViewModeProps } from '@/lib/native-chat-initial-view-mode' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { resolveTerminalWorktreeRoute } from '@/lib/terminal-worktree-route' import { insertUnifiedTabAfterAnchor } from '@/lib/unified-tab-anchor-insertion' import { translate } from '@/i18n/i18n' @@ -56,14 +53,7 @@ export function registerTerminalRequestIpcBridge(unsubs: (() => void)[]): void { ? { ...(shouldActivate ? {} : { activate: false, recordInteraction: false }), launchAgent: data.launchAgent, - ...(data.viewMode - ? { viewMode: data.viewMode } - : initialAgentTabViewModeProps(store.settings, { - agent: data.launchAgent, - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - getConnectionIdFromState(store, worktreeId) - ) - })), + ...(data.viewMode ? { viewMode: data.viewMode } : {}), ...(data.cwd ? { startupCwd: data.cwd } : {}) } : shouldActivate diff --git a/src/renderer/src/hooks/settings-navigation-build-options.ts b/src/renderer/src/hooks/settings-navigation-build-options.ts index 9ea40e53593..6e501d90ab6 100644 --- a/src/renderer/src/hooks/settings-navigation-build-options.ts +++ b/src/renderer/src/hooks/settings-navigation-build-options.ts @@ -7,7 +7,7 @@ export type SettingsNavigationBuildOptions = { isLocalWindowsHost: boolean isWindowsTerminalHost: boolean isWebClient: boolean - experimentalStructuredNativeChat: boolean + nativeChatEnabled: boolean managedBrowserCreationEnabled: boolean mobileEmulatorCreationEnabled: boolean isDev: boolean diff --git a/src/renderer/src/hooks/settings-navigation-interface-sections.ts b/src/renderer/src/hooks/settings-navigation-interface-sections.ts index 8adfb8b0c8f..a40064076d5 100644 --- a/src/renderer/src/hooks/settings-navigation-interface-sections.ts +++ b/src/renderer/src/hooks/settings-navigation-interface-sections.ts @@ -15,7 +15,7 @@ export function buildInterfaceSettingsSections({ isMac, isWindows, isWebClient, - experimentalStructuredNativeChat, + nativeChatEnabled, managedBrowserCreationEnabled, mobileEmulatorCreationEnabled }: SettingsNavigationBuildOptions): SettingsNavSection[] { @@ -36,7 +36,7 @@ export function buildInterfaceSettingsSections({ }), group: 'interface' }, - ...(experimentalStructuredNativeChat + ...(nativeChatEnabled ? [ { id: 'chat', diff --git a/src/renderer/src/hooks/useIpcEvents-browser-certificate-failure.test.ts b/src/renderer/src/hooks/useIpcEvents-browser-certificate-failure.test.ts index cc40c801169..ef9d571bf6e 100644 --- a/src/renderer/src/hooks/useIpcEvents-browser-certificate-failure.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-browser-certificate-failure.test.ts @@ -85,7 +85,6 @@ function storeStateWithRuntime( settings: { terminalFontSize: 13, experimentalNativeChat: false, - openAgentTabsInChatByDefault: false, activeRuntimeEnvironmentId: 'env-a' } }) diff --git a/src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts b/src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts index 83e8c1108f1..3a2f64e7ced 100644 --- a/src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts @@ -75,8 +75,7 @@ describe('useIpcEvents updater integration', () => { sshTargetLabels: new Map([['conn-1', 'Remote']]), settings: { terminalFontSize: 13, - experimentalNativeChat: false, - openAgentTabsInChatByDefault: false + experimentalNativeChat: false } } vi.doMock('react', async () => { diff --git a/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts b/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts index 6bc2373f83c..63c3c9122e5 100644 --- a/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts @@ -210,8 +210,7 @@ describe('useIpcEvents updater integration', () => { focusTerminalTabSurface.mockClear() storeState.settings = { ...storeState.settings, - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } requestTerminalCreateListenerRef.current({ requestId: 'req-renderer-backed', @@ -232,7 +231,6 @@ describe('useIpcEvents updater integration', () => { activate: false, recordInteraction: false, launchAgent: 'codex', - viewMode: 'chat', startupCwd: '/repo/packages/app' }) expect(setActiveView).not.toHaveBeenCalled() @@ -481,8 +479,7 @@ describe('useIpcEvents updater integration', () => { expect(createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, { initialPtyId: 'pty-bg', activate: false, - launchAgent: 'codex', - viewMode: 'chat' + launchAgent: 'codex' }) expect(setActiveView).not.toHaveBeenCalled() expect(setActiveWorktree).not.toHaveBeenCalled() @@ -517,7 +514,7 @@ describe('useIpcEvents updater integration', () => { }) createTab.mockClear() - storeState.settings.openAgentTabsInChatByDefault = false + storeState.settings.experimentalNativeChat = false createTerminalListenerRef.current({ worktreeId: 'wt-2', ptyId: 'pty-explicit-chat', @@ -530,7 +527,7 @@ describe('useIpcEvents updater integration', () => { launchAgent: 'codex', viewMode: 'chat' }) - storeState.settings.openAgentTabsInChatByDefault = true + storeState.settings.experimentalNativeChat = true createTab.mockClear() setActiveView.mockClear() diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts index 1f8df823650..842993d4906 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts @@ -40,7 +40,7 @@ describe('settings navigation metadata', () => { isMac: false, isWindows: false, isWebClient: false, - experimentalStructuredNativeChat: enabled, + nativeChatEnabled: enabled, repos: [] }) expect(sections.some((section) => section.id === 'chat')).toBe(false) @@ -60,7 +60,7 @@ describe('settings navigation metadata', () => { isMac: false, isWindows: false, isWebClient: false, - experimentalStructuredNativeChat: true, + nativeChatEnabled: true, repos: [] }) const appearanceIndex = sections.findIndex((section) => section.id === 'appearance') diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts index e7ae73e1b2a..bda7a5d407c 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts @@ -41,7 +41,7 @@ export function buildSettingsNavigationMetadata({ isLocalWindowsHost = isWindows, isWindowsTerminalHost = isWindows, isWebClient, - experimentalStructuredNativeChat = false, + nativeChatEnabled = false, managedBrowserCreationEnabled = !isWebClient, mobileEmulatorCreationEnabled = !isWebClient, isDev = import.meta.env.DEV, @@ -54,7 +54,7 @@ export function buildSettingsNavigationMetadata({ isLocalWindowsHost?: boolean isWindowsTerminalHost?: boolean isWebClient: boolean - experimentalStructuredNativeChat?: boolean + nativeChatEnabled?: boolean managedBrowserCreationEnabled?: boolean mobileEmulatorCreationEnabled?: boolean isDev?: boolean @@ -82,7 +82,7 @@ export function buildSettingsNavigationMetadata({ isLocalWindowsHost, isWindowsTerminalHost, isWebClient, - experimentalStructuredNativeChat, + nativeChatEnabled, managedBrowserCreationEnabled, mobileEmulatorCreationEnabled, isDev, @@ -122,7 +122,7 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] { ] as const }) ) - const experimentalStructuredNativeChat = settings?.experimentalStructuredNativeChat === true + const nativeChatEnabled = settings?.experimentalNativeChat === true const isMac = isMacUserAgent() const isWindows = isWindowsUserAgent() const isWebClient = isWebClientLocation() @@ -165,7 +165,7 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] { isLocalWindowsHost, isWindowsTerminalHost, isWebClient, - experimentalStructuredNativeChat, + nativeChatEnabled, managedBrowserCreationEnabled, mobileEmulatorCreationEnabled, isDev: import.meta.env.DEV, @@ -183,7 +183,7 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] { isLocalWindowsHost, isWindowsTerminalHost, isWebClient, - experimentalStructuredNativeChat, + nativeChatEnabled, managedBrowserCreationEnabled, mobileEmulatorCreationEnabled, isLinearConnected, diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 2332d1aa4af..f47ca0c96a2 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -1704,7 +1704,10 @@ "d23ae13990": "worktree", "f082788cfe": "links", "fa72e71f05": "agents", - "fe5688b761": "sidebar" + "fe5688b761": "sidebar", + "nativeChat": { + "description": "Open supported new agents in Chat UI." + } } }, "general": { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 09cf619f493..d56b5ad2b56 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -7419,18 +7419,9 @@ "0e89a574ae": "Floating animated pet in the bottom-right corner.", "nativeChat": { "title": "Chat UI", - "description": "Preview the desktop chat surface for supported agent terminal sessions.", - "copy": "Enables the experimental Chat UI for newly created supported local sessions. Existing terminal sessions keep the terminal chat path while we tune transcript fidelity, streaming, and parity.", + "description": "Open supported new agents in Chat UI.", + "copy": "Supported new agents open in structured chat. Other agents open in the terminal; existing chats stay available.", "toggleLabel": "Toggle Chat UI", - "defaultTitle": "Default view", - "defaultCopy": "Choose how new supported agent terminal tabs open.", - "defaultViewLabel": "Default Chat UI view", - "defaultViewTerminal": "Terminal chat", - "defaultViewNative": "Chat UI", - "structuredTitle": "Use updated structured native chat", - "structuredCopy": "Open new agents as structured chats where supported. Off opens them in the terminal-backed chat. Chats that already exist stay as they are.", - "structuredScope": "Runs on this machine and on paired Orca servers running a version that supports it; older servers keep terminal chat. WSL and SSH hosts continue to use terminal chat.", - "structuredToggleLabel": "Toggle updated structured native chat", "resumeTitle": "Resume working chats automatically after a restart", "resumeCopy": "When Orca quits or installs an update, chats that were working are automatically resumed when Orca is reopened.", "resumeToggleLabel": "Toggle automatic resume after a restart", @@ -9939,7 +9930,7 @@ "87d99e634b": "Pet", "nativeChat": { "title": "Chat UI", - "description": "Preview the desktop chat surface for supported agent terminal sessions.", + "description": "Open supported new agents in Chat UI.", "grok": "grok", "native": "native", "chat": "chat", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 9007cb4c45d..a914e04dbc2 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -7197,14 +7197,7 @@ }, "nativeChat": { "title": "Chat UI", - "description": "Previsualiza la interfaz de chat de escritorio para sesiones de terminal de agentes compatibles.", - "copy": "Añade una vista de Chat UI a la que puedes cambiar desde paneles de terminal de agentes compatibles. Es experimental mientras ajustamos la fidelidad de las transcripciones, la transmisión y la paridad con el terminal.", "toggleLabel": "Alternar Chat UI", - "defaultTitle": "Vista predeterminada", - "defaultCopy": "Elige cómo se abren las nuevas pestañas de terminal de agentes compatibles.", - "defaultViewLabel": "Vista predeterminada de Chat UI", - "defaultViewTerminal": "Chat en terminal", - "defaultViewNative": "Chat UI", "shellEnvNamesLabel": "Variables que se pasan desde tu shell", "shellEnvNamePlaceholder": "Nombre de la variable", "shellEnvNameAdd": "Agregar", @@ -7215,10 +7208,6 @@ "shellEnvCopy": "Los chats de Codex y Claude empiezan con todas las variables que exporta tu shell de inicio de sesión, igual que un terminal. Desactívalo para elegir cuáles reciben.", "shellEnvToggleLabel": "Alternar el uso del entorno de tu shell", "resumeCopy": "Cuando Orca se cierra o instala una actualización, los chats que estaban trabajando se reanudan automáticamente al volver a abrir Orca.", - "structuredTitle": "Utilice el chat nativo estructurado actualizado", - "structuredCopy": "Abre nuevos agentes como chats estructurados cuando sea posible. Si está desactivado, los abre en el chat respaldado por la terminal. Los chats que ya existen permanecen como están.", - "structuredScope": "Se ejecuta en esta máquina y en servidores Orca emparejados que ejecutan una versión que lo admita; los servidores más antiguos mantienen el chat de terminal. Los hosts WSL y SSH continúan usando el chat de terminal.", - "structuredToggleLabel": "Alternar chat nativo estructurado actualizado", "resumeTitle": "Reanudar los chats de trabajo automáticamente después de reiniciar", "resumeToggleLabel": "Alternar reanudación automática después de un reinicio" }, @@ -9666,7 +9655,6 @@ }, "nativeChat": { "title": "Chat UI", - "description": "Previsualiza la interfaz de chat de escritorio para sesiones de terminal de agentes compatibles.", "grok": "grok", "queue": "cola", "native": "nativo", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 76da9f0fdfe..7bf8eb683a5 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -7302,14 +7302,7 @@ "0e89a574ae": "Compagnon animé flottant dans le coin inférieur droit.", "nativeChat": { "title": "Chat UI", - "description": "Prévisualisez la surface de chat de bureau pour les sessions de terminal d'agents prises en charge.", - "copy": "Ajoute une vue Chat UI accessible depuis les volets de terminal d'agents pris en charge. Expérimental pendant que nous peaufinons la fidélité de la transcription, le streaming et la parité avec le terminal.", "toggleLabel": "Activer Chat UI", - "defaultTitle": "Vue par défaut", - "defaultCopy": "Choisissez comment s'ouvrent les nouveaux onglets de terminal d'agents pris en charge.", - "defaultViewLabel": "Vue Chat UI par défaut", - "defaultViewTerminal": "Chat dans le terminal", - "defaultViewNative": "Chat UI", "shellEnvNamesLabel": "Variables à transmettre depuis votre shell", "shellEnvNamePlaceholder": "Nom de la variable", "shellEnvNameAdd": "Ajouter", @@ -7319,10 +7312,6 @@ "shellEnvTitle": "Utiliser l'environnement de votre shell", "shellEnvCopy": "Les chats Codex et Claude démarrent avec toutes les variables exportées par votre shell de connexion, comme un terminal. Désactivez pour choisir celles qu'ils reçoivent.", "shellEnvToggleLabel": "Activer l'utilisation de l'environnement de votre shell", - "structuredTitle": "Utiliser le chat natif structuré mis à jour", - "structuredCopy": "Ouvre les nouveaux agents sous forme de discussions structurées lorsque c'est possible. Désactivé, ils s'ouvrent dans le chat basé sur le terminal. Les discussions existantes restent telles quelles.", - "structuredScope": "Fonctionne sur cette machine et sur les serveurs Orca appairés dont la version le prend en charge ; les serveurs plus anciens conservent le chat du terminal. WSL et les hôtes SSH continuent d'utiliser le chat du terminal.", - "structuredToggleLabel": "Basculer le chat natif structuré mis à jour", "resumeTitle": "Reprendre automatiquement les discussions de travail après un redémarrage", "resumeCopy": "Lorsqu'Orca quitte ou installe une mise à jour, les discussions en cours reprennent automatiquement à la réouverture d'Orca.", "resumeToggleLabel": "Activer la reprise automatique après un redémarrage" @@ -9813,7 +9802,6 @@ "87d99e634b": "Compagnon", "nativeChat": { "title": "Chat UI", - "description": "Prévisualisez la surface de chat de bureau pour les sessions de terminal d'agents prises en charge.", "grok": "grok", "native": "indigène", "chat": "chat", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 7a3b7b5fdc9..5096ff9c7f6 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -7182,14 +7182,7 @@ }, "nativeChat": { "title": "Chat UI", - "description": "対応 Agent のターミナルセッション用デスクトップチャット画面をプレビューします。", - "copy": "対応 Agent のターミナルペインから切り替えられる Chat UI ビューを追加します。トランスクリプトの忠実度、ストリーミング、ターミナルとの同等性を調整しているため、試験運用中です。", "toggleLabel": "Chat UI の切り替え", - "defaultTitle": "デフォルトのビュー", - "defaultCopy": "対応 Agent の新規ターミナルタブを開く方法を選択します。", - "defaultViewLabel": "デフォルトの Chat UI ビュー", - "defaultViewTerminal": "ターミナルチャット", - "defaultViewNative": "Chat UI", "shellEnvNamesLabel": "シェルから渡す変数", "shellEnvNamePlaceholder": "変数名", "shellEnvNameAdd": "追加", @@ -7199,10 +7192,6 @@ "shellEnvTitle": "シェル環境を使用する", "shellEnvCopy": "Codex と Claude のチャットは、ターミナルと同じく、ログインシェルがエクスポートするすべての変数を引き継いで開始します。オフにすると、渡す変数を選択できます。", "shellEnvToggleLabel": "シェル環境の使用の切り替え", - "structuredTitle": "更新された構造化ネイティブチャットを使用する", - "structuredCopy": "対応している場合、新規の Agent を構造化チャットとして開きます。オフにすると、ターミナルを使用したチャットで開きます。既存のチャットはそのまま残ります。", - "structuredScope": "このマシンと、対応するバージョンを実行しているペアリング済みの Orca サーバーで動作します。古いサーバーはターミナルチャットのままです。WSL および SSH ホストは引き続きターミナルチャットを使用します。", - "structuredToggleLabel": "更新された構造化ネイティブチャットの切り替え", "resumeTitle": "再起動後に作業中のチャットを自動的に再開する", "resumeCopy": "Orca が終了するかアップデートをインストールすると、Orca が再度開かれたときに作業中だったチャットが自動的に再開されます。", "resumeToggleLabel": "再起動後の自動再開の切り替え" @@ -9688,7 +9677,6 @@ }, "nativeChat": { "title": "Chat UI", - "description": "対応 Agent のターミナルセッション用デスクトップチャット画面をプレビューします。", "grok": "grok", "native": "ネイティブ", "chat": "チャット", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index db78d9b861b..3b8312655be 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -7182,14 +7182,7 @@ }, "nativeChat": { "title": "Chat UI", - "description": "지원되는 에이전트 터미널 세션의 데스크톱 채팅 화면을 미리 봅니다.", - "copy": "지원되는 에이전트 터미널 창에서 전환할 수 있는 Chat UI 보기를 추가합니다. 대화 기록 충실도, 스트리밍, 터미널 동작 일치를 조정하는 동안 실험적으로 제공됩니다.", "toggleLabel": "Chat UI 전환", - "defaultTitle": "기본 보기", - "defaultCopy": "지원되는 에이전트의 새 터미널 탭을 여는 방식을 선택합니다.", - "defaultViewLabel": "기본 Chat UI 보기", - "defaultViewTerminal": "Terminal 채팅", - "defaultViewNative": "Chat UI", "shellEnvNamesLabel": "셸에서 전달할 변수", "shellEnvNamePlaceholder": "변수 이름", "shellEnvNameAdd": "추가", @@ -7199,10 +7192,6 @@ "shellEnvTitle": "셸 환경 사용", "shellEnvCopy": "Codex 및 Claude 채팅은 터미널과 마찬가지로 로그인 셸이 내보내는 모든 변수를 가지고 시작합니다. 끄면 전달할 변수를 선택할 수 있습니다.", "shellEnvToggleLabel": "셸 환경 사용 전환", - "structuredTitle": "업데이트된 구조화된 기본 채팅 사용", - "structuredCopy": "지원되는 경우 새 에이전트를 구조화된 채팅으로 엽니다. 끄면 터미널 기반 채팅으로 엽니다. 이미 있는 채팅은 그대로 유지됩니다.", - "structuredScope": "이 컴퓨터와 이를 지원하는 버전을 실행하는 페어링된 Orca 서버에서 실행됩니다. 이전 서버는 터미널 채팅을 유지합니다. WSL 및 SSH 호스트는 계속해서 터미널 채팅을 사용합니다.", - "structuredToggleLabel": "업데이트된 구조화된 기본 채팅 전환", "resumeTitle": "다시 시작한 후 작업 중인 채팅을 자동으로 재개합니다.", "resumeCopy": "Orca가 종료되거나 업데이트를 설치하면 Orca가 다시 열릴 때 작업 중이던 채팅이 자동으로 재개됩니다.", "resumeToggleLabel": "다시 시작한 후 자동 재개 전환" @@ -9651,7 +9640,6 @@ }, "nativeChat": { "title": "Chat UI", - "description": "지원되는 에이전트 터미널 세션의 데스크톱 채팅 화면을 미리 봅니다.", "grok": "grok", "native": "네이티브", "chat": "채팅", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index d55c9d9cc9e..4df85264d1d 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -7182,14 +7182,7 @@ }, "nativeChat": { "title": "Chat UI", - "description": "预览受支持的智能体终端会话的桌面聊天界面。", - "copy": "添加可从受支持的智能体终端窗格切换的 Chat UI 视图。在我们调整记录保真度、流式传输和终端一致性期间,此功能仍处于实验阶段。", "toggleLabel": "切换 Chat UI", - "defaultTitle": "默认视图", - "defaultCopy": "选择如何打开受支持智能体的新终端标签页。", - "defaultViewLabel": "默认 Chat UI 视图", - "defaultViewTerminal": "终端聊天", - "defaultViewNative": "Chat UI", "shellEnvNamesLabel": "从 shell 传递的变量", "shellEnvNamePlaceholder": "变量名", "shellEnvNameAdd": "添加", @@ -7199,10 +7192,6 @@ "shellEnvTitle": "使用 shell 环境", "shellEnvCopy": "Codex 和 Claude 聊天启动时会带上登录 shell 导出的所有变量,与终端一致。关闭后可选择传递哪些变量。", "shellEnvToggleLabel": "切换使用 shell 环境", - "structuredTitle": "使用更新的结构化本机聊天", - "structuredCopy": "在支持的情况下,将新的智能体作为结构化聊天打开。关闭时会在基于终端的聊天中打开。已有的聊天保持不变。", - "structuredScope": "在本机和运行支持该功能版本的已配对 Orca 服务器上运行;较旧的服务器保留终端聊天。WSL 和 SSH 主机继续使用终端聊天。", - "structuredToggleLabel": "切换更新的结构化本机聊天", "resumeTitle": "重启后自动恢复工作聊天", "resumeCopy": "当 Orca 退出或安装更新时,重新打开 Orca 后,正在工作的聊天会自动恢复。", "resumeToggleLabel": "重启后切换自动恢复" @@ -9651,7 +9640,6 @@ }, "nativeChat": { "title": "Chat UI", - "description": "预览受支持的智能体终端会话的桌面聊天界面。", "grok": "grok", "native": "原生", "chat": "聊天", diff --git a/src/renderer/src/i18n/native-chat-locales.test.ts b/src/renderer/src/i18n/native-chat-locales.test.ts index fc1f5e71e04..02349874150 100644 --- a/src/renderer/src/i18n/native-chat-locales.test.ts +++ b/src/renderer/src/i18n/native-chat-locales.test.ts @@ -45,19 +45,17 @@ describe('native chat locale copy', () => { }) it.each(Object.entries(localizedCatalogs))( - '%s keeps provider-neutral copy localized', + '%s falls back to current Chat UI copy until it is translated', (_code, catalog) => { const setting = catalog.auto.components.settings.ExperimentalPane.nativeChat const search = catalog.auto.components.settings.experimental.search.nativeChat - for (const [localized, english] of [ - [setting.description, englishSetting.description], - [setting.copy, englishSetting.copy], - [setting.defaultCopy, englishSetting.defaultCopy], - [search.description, englishSearch.description] - ]) { - expect(localized.trim()).not.toBe('') - expect(localized).not.toBe(english) - } + expect(englishSetting.description).toContain('Chat UI') + expect(englishSetting.copy).toContain('structured chat') + expect(englishSearch.description).toContain('Chat UI') + expect(setting).not.toHaveProperty('description') + expect(setting).not.toHaveProperty('copy') + expect(setting).not.toHaveProperty('defaultCopy') + expect(search).not.toHaveProperty('description') expect(search.grok).toBe('grok') const composer = catalog.components['native-chat'].composer for (const key of [ diff --git a/src/renderer/src/lib/agent-launch-caller-permissions.test.ts b/src/renderer/src/lib/agent-launch-caller-permissions.test.ts index dfc416260f8..b6654a2b36a 100644 --- a/src/renderer/src/lib/agent-launch-caller-permissions.test.ts +++ b/src/renderer/src/lib/agent-launch-caller-permissions.test.ts @@ -243,49 +243,41 @@ describe('agent launch caller arguments and permission bypass', () => { }) }) - it('applies a remembered model and effort to the launch command', async () => { - store.settings = { - ...store.settings, - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - nativeChatSessionOptions: { - codex: { model: 'gpt-5.2-codex', valuesByModel: { 'gpt-5.2-codex': { effort: 'medium' } } } + // Why: chat composer picks seed only structured creates (launch-structured-agent-session.test.ts, + // orca-runtime-structured-agent-session-create-intent.test.ts); a terminal fallback never gets them. + it.each([ + [true, undefined, CODEX_BYPASS], + [false, undefined, CODEX_BYPASS], + [true, '--search', '--search'], + [false, '--search', '--search'] + ])( + 'keeps remembered model and effort off a terminal fallback (Chat UI %s, stored args %s)', + async (chatUi, storedArgs, expectedArgs) => { + store.settings = { + ...store.settings, + experimentalNativeChat: chatUi, + ...(storedArgs === undefined ? {} : { agentDefaultArgs: { codex: storedArgs } }), + nativeChatSessionOptions: { + codex: { + model: 'gpt-5.2-codex', + valuesByModel: { 'gpt-5.2-codex': { effort: 'medium' } } + } + } } + const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') + + const result = launchAgentInNewTab({ + requestId: 'request-9', + agent: 'codex', + worktreeId: 'wt-1' + }) + + // No local structured capability, so even with Chat UI on this is the terminal fallback. + expect(result?.startupPlan?.sessionOptions).toBeUndefined() + const command = queuedStartupCommand(store) + expect(command).not.toContain("'-m'") + expect(command).not.toContain('model_reasoning_effort') + expect(command).toBe(`codex '${expectedArgs}'`) } - const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') - - const result = launchAgentInNewTab({ - requestId: 'request-9', - agent: 'codex', - worktreeId: 'wt-1' - }) - - expect(result?.startupPlan?.sessionOptions).toEqual({ - model: 'gpt-5.2-codex', - effort: 'medium' - }) - expect(queuedStartupCommand(store)).toContain("'-m' 'gpt-5.2-codex'") - expect(queuedStartupCommand(store)).toContain("'-c' 'model_reasoning_effort=medium'") - // The remembered options ride beside the bypass default rather than replacing it. - expect(queuedStartupCommand(store)).toContain(CODEX_BYPASS) - }) - - it('keeps remembered session options out of a plain terminal launch', async () => { - store.settings = { - ...store.settings, - nativeChatSessionOptions: { - codex: { model: 'gpt-5.2-codex', valuesByModel: { 'gpt-5.2-codex': { effort: 'medium' } } } - } - } - const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') - - const result = launchAgentInNewTab({ - requestId: 'request-10', - agent: 'codex', - worktreeId: 'wt-1' - }) - - expect(result?.startupPlan?.sessionOptions).toBeUndefined() - expect(queuedStartupCommand(store)).not.toContain("'-m'") - }) + ) }) diff --git a/src/renderer/src/lib/agent-launch-caller-routing.test.ts b/src/renderer/src/lib/agent-launch-caller-routing.test.ts index fb79b9c746a..034c8a2e99a 100644 --- a/src/renderer/src/lib/agent-launch-caller-routing.test.ts +++ b/src/renderer/src/lib/agent-launch-caller-routing.test.ts @@ -78,9 +78,7 @@ function serverReports(capabilities: readonly string[] | null): void { } const CHAT_DEFAULT_SETTINGS = { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } const cases = callerProfileCases() diff --git a/src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts b/src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts index c87c594c069..7c8e5159126 100644 --- a/src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts +++ b/src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts @@ -6,7 +6,7 @@ import { planAgentSessionLaunch } from './agent-session-launch-plan' // Deliberately un-mocked: the defect this pins lives in the owner resolution itself, so a suite // that stages `getConnectionIdFromState` cannot catch it. Grok is the agent the answer routes on — // only agents whose hook discloses no transcript path read the local-readability input at all. -const NATIVE_CHAT_SETTINGS = { experimentalNativeChat: true, openAgentTabsInChatByDefault: true } +const NATIVE_CHAT_SETTINGS = { experimentalNativeChat: true } const WORKTREE_ID = 'repo-1::/repo/wt-1' const WORKSPACE = { kind: 'git-worktree', worktreeId: WORKTREE_ID, repoId: 'repo-1' } as const @@ -44,7 +44,7 @@ describe('launch route transcript readability', () => { expect( planAgentSessionLaunch(store, { requestId: 'request-1', agent: 'grok', workspace: WORKSPACE }) .route - ).toBe('legacy-native-chat') + ).toBe('terminal-tui') }) it('keeps a remote repo off native chat through the same fallback', () => { diff --git a/src/renderer/src/lib/agent-launch-route-input.test.ts b/src/renderer/src/lib/agent-launch-route-input.test.ts index 3903f814c75..e5e87f6f137 100644 --- a/src/renderer/src/lib/agent-launch-route-input.test.ts +++ b/src/renderer/src/lib/agent-launch-route-input.test.ts @@ -59,8 +59,6 @@ const structuredFeasibleFor = (appStore: AgentLaunchRouteStore, args: AgentLaunc const STRUCTURED_SETTINGS = { experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true, agentCmdOverrides: {}, agentDefaultArgs: {}, agentDefaultEnv: {} @@ -103,8 +101,7 @@ describe('buildAgentLaunchRouteInput', () => { agent: 'codex', workspace: { kind: 'git-worktree', worktreeId: 'wt-1' }, prompt: 'fix the flaky test', - promptDelivery: 'auto-submit', - initialSessionOptions: { model: 'gpt-5.4' } + promptDelivery: 'auto-submit' }) expect(input).toEqual({ agent: 'codex', @@ -116,8 +113,7 @@ describe('buildAgentLaunchRouteInput', () => { promptDelivery: 'auto-submit', launchText: 'fix the flaky test', nativeChatTranscriptIsLocalReadable: true, - startsOutsideWorkspaceRoot: false, - initialSessionOptions: { model: 'gpt-5.4' } + startsOutsideWorkspaceRoot: false }) expect(mocks.getExecutionHostIdForWorktree).toHaveBeenCalledWith(appStore, 'wt-1') expect(mocks.getLocalProjectExecutionRuntimeContext).toHaveBeenCalledWith(appStore, 'wt-1') @@ -127,7 +123,7 @@ describe('buildAgentLaunchRouteInput', () => { agent: 'codex', workspace: { kind: 'git-worktree', worktreeId: 'wt-1' } }) - ).toBe('legacy-native-chat') + ).toBe('terminal-tui') }) it('never consults the local project runtime for a worktree on an SSH connection', () => { @@ -174,7 +170,7 @@ describe('buildAgentLaunchRouteInput', () => { prompt: 'issue body', promptDelivery: 'draft' }) - ).toBe('legacy-native-chat') + ).toBe('terminal-tui') }) it.each([ @@ -301,7 +297,7 @@ describe('buildAgentLaunchRouteInput', () => { }) // Grok reads its transcript off local disk, so it is the agent the readability answer routes on. - const NATIVE_CHAT_SETTINGS = { experimentalNativeChat: true, openAgentTabsInChatByDefault: true } + const NATIVE_CHAT_SETTINGS = { experimentalNativeChat: true } const UNLANDED_WORKSPACE = { kind: 'git-worktree', worktreeId: 'repo-1::/repo/wt-1', @@ -322,7 +318,7 @@ describe('buildAgentLaunchRouteInput', () => { .nativeChatTranscriptIsLocalReadable ).toBe(true) expect(routeFor(appStore, { agent: 'grok', workspace: UNLANDED_WORKSPACE })).toBe( - 'legacy-native-chat' + 'terminal-tui' ) }) @@ -346,7 +342,7 @@ describe('buildAgentLaunchRouteInput', () => { worktreesByRepo: {} } as unknown as AgentLaunchRouteStore expect(routeFor(appStore, { agent: 'grok', workspace: UNLANDED_WORKSPACE })).toBe( - 'legacy-native-chat' + 'terminal-tui' ) }) @@ -393,14 +389,14 @@ describe('buildAgentLaunchRouteInput', () => { it('treats a server that has not reported its status as unknown', () => { expect(buildAgentLaunchRouteInput(store(), args).hostCapabilities).toBeNull() - expect(routeFor(store(), args)).toBe('legacy-native-chat') + expect(routeFor(store(), args)).toBe('terminal-tui') }) // A released server advertises structured sessions but admits them only with its own chat // setting on; a chat opened there could never start. it('keeps the terminal on a server that predates client-chosen launch modes', () => { expect(routeFor(pairedStore([STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY]), args)).toBe( - 'legacy-native-chat' + 'terminal-tui' ) }) @@ -409,7 +405,7 @@ describe('buildAgentLaunchRouteInput', () => { it('keeps the host terminal for a browser client', () => { mocks.isWebClientLocation.mockReturnValue(true) try { - expect(routeFor(pairedStore(CURRENT_SERVER), args)).toBe('legacy-native-chat') + expect(routeFor(pairedStore(CURRENT_SERVER), args)).toBe('terminal-tui') } finally { mocks.isWebClientLocation.mockReturnValue(false) } @@ -448,7 +444,7 @@ describe('buildAgentLaunchRouteInput', () => { agent: 'claude', workspace: { kind: 'git-worktree', worktreeId: 'wt-1' } }) - ).toBe('legacy-native-chat') + ).toBe('terminal-tui') }) }) diff --git a/src/renderer/src/lib/agent-launch-route-input.ts b/src/renderer/src/lib/agent-launch-route-input.ts index 6ac1121ae24..316d0f7aeb4 100644 --- a/src/renderer/src/lib/agent-launch-route-input.ts +++ b/src/renderer/src/lib/agent-launch-route-input.ts @@ -19,7 +19,7 @@ import { getLocalProjectExecutionRuntimeContext, getLocalRepoProjectExecutionRuntimeContext } from '@/lib/local-preflight-context' -import type { NativeChatLaunchPromptDelivery } from '@/lib/native-chat-initial-view-mode' +import type { NativeChatLaunchPromptDelivery } from '@/lib/native-chat-launch-prompt-delivery' import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import { readLocalRuntimeCapabilitiesOrUnknown } from '@/runtime/local-runtime-capabilities' @@ -63,7 +63,6 @@ export type AgentLaunchRouteArgs = { promptDelivery?: NativeChatLaunchPromptDelivery /** A working directory only a terminal can apply; a structured session runs in its workspace. */ tuiCustomization?: { cwd?: string | null } - initialSessionOptions?: Readonly> } export { workspaceKindForWorktreeId } @@ -172,7 +171,6 @@ export function buildAgentLaunchRouteInput( resolveFolderWorkspacePath: (folderWorkspaceId) => store.folderWorkspaces?.find((entry) => entry.id === folderWorkspaceId)?.folderPath }), - initialSessionOptions: args.initialSessionOptions, ...hostStructuredAgentsInput(store, executionHostId) } } diff --git a/src/renderer/src/lib/agent-launch-route-registered-agents.test.ts b/src/renderer/src/lib/agent-launch-route-registered-agents.test.ts index 0a8c0bf005d..8b319c2e9a6 100644 --- a/src/renderer/src/lib/agent-launch-route-registered-agents.test.ts +++ b/src/renderer/src/lib/agent-launch-route-registered-agents.test.ts @@ -47,9 +47,7 @@ const LISTS_GROK = { ] } const SETTINGS = { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true + experimentalNativeChat: true } const LOCAL: ProspectiveWorkspace = { kind: 'git-worktree', repoId: 'repo-1' } const PAIRED: ProspectiveWorkspace = { kind: 'git-worktree', runtimeEnvironmentId: 'env-1' } @@ -121,12 +119,12 @@ describe('structured launch of a host-registered agent', () => { expect(routeFor(store(), 'gemini')).not.toBe('structured-native-chat') }) - it('keeps the experimental chat setting authoritative', async () => { + it('keeps the Chat UI switch authoritative', async () => { await loadHostStructuredAgents('local', REGISTERED, null) - expect( - routeFor(store({ settings: { experimentalStructuredNativeChat: false } }), 'grok') - ).not.toBe('structured-native-chat') + expect(routeFor(store({ settings: { experimentalNativeChat: false } }), 'grok')).not.toBe( + 'structured-native-chat' + ) }) it("reads a paired host's list only from the runtime that gave it", async () => { @@ -172,9 +170,9 @@ describe('OpenCode with its Command and environment set (live QA run 5)', () => mocks.callRuntimeRpc.mockResolvedValue(LISTS_OPENCODE) // What run 5 saw: the terminal-backed "OpenCode" chat, and no version check on the host. - expect(routeFor(withOverrides(), 'opencode')).toBe('legacy-native-chat') + expect(routeFor(withOverrides(), 'opencode')).toBe('terminal-tui') expect(warn).toHaveBeenCalledWith( - '[agent-launch-route] opencode opens legacy-native-chat: agent-without-structured-session ' + + '[agent-launch-route] opencode opens terminal-tui: agent-without-structured-session ' + '(host local, host agents not learned, host capabilities known)' ) await vi.waitFor(() => expect(mocks.callRuntimeRpc).toHaveBeenCalledTimes(2)) @@ -184,12 +182,10 @@ describe('OpenCode with its Command and environment set (live QA run 5)', () => ) }) - it('names the settings default when new agent tabs do not open as chats', () => { - expect(routeFor(store({ settings: { openAgentTabsInChatByDefault: false } }), 'opencode')).toBe( + it('keeps new agent tabs in terminals while Chat UI is off', () => { + expect(routeFor(store({ settings: { experimentalNativeChat: false } }), 'opencode')).toBe( 'terminal-tui' ) - expect(warn).toHaveBeenCalledWith( - expect.stringContaining('opencode opens terminal-tui: new-tabs-default-to-terminal') - ) + expect(warn).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/lib/agent-launch-routing.test.ts b/src/renderer/src/lib/agent-launch-routing.test.ts index 985ff7a7a02..f2b42c96f8c 100644 --- a/src/renderer/src/lib/agent-launch-routing.test.ts +++ b/src/renderer/src/lib/agent-launch-routing.test.ts @@ -2,16 +2,13 @@ import { describe, expect, it } from 'vitest' import { RUNTIME_CAPABILITIES, STRUCTURED_AGENT_SESSION_CLIENT_LAUNCH_MODE_CAPABILITY, + STRUCTURED_AGENT_SESSION_REGISTERED_AGENTS_RUNTIME_CAPABILITY, STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../../shared/electron-remote-runtime-client-capabilities' import { resolveAgentLaunchRoute, structuredAgentLaunchSupported } from './agent-launch-routing' -const settings = { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true -} +const settings = { experimentalNativeChat: true } function route(overrides: Partial[0]> = {}) { return resolveAgentLaunchRoute({ @@ -20,130 +17,45 @@ function route(overrides: Partial[0]> executionHostId: 'local', hostCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY], workspaceKind: 'git-worktree', - nativeChatTranscriptIsLocalReadable: true, ...overrides }) } -describe('resolveAgentLaunchRoute', () => { - it.each(['claude', 'codex'] as const)( - 'routes a supported local %s launch to structured native chat', - (agent) => { - expect(route({ agent })).toBe('structured-native-chat') - expect(route({ agent, initialSessionOptions: { model: 'gpt-5.6-sol' } })).toBe( - 'structured-native-chat' - ) - expect( - route({ agent, launchText: 'explain this change', promptDelivery: 'auto-submit' }) - ).toBe('structured-native-chat') - } - ) - - /** Windows eligibility is no client-side platform guess for either provider: the route lets the - * launch through and the executing host settles it with agentSession.createSupport at create - * time. A stale caller still passing the removed `platform` input must not flip Codex off the - * structured route — the field is gone, not reinterpreted. */ - it.each(['claude', 'codex'] as const)( - 'routes %s to structured even when the caller claims a win32 client platform', - (agent) => { - expect(route({ agent, ...({ platform: 'win32' } as object) })).toBe('structured-native-chat') - } - ) - - it('routes editable drafts to the structured chat composer', () => { - expect(route({ launchText: 'reviewable context', promptDelivery: 'draft' })).toBe( +describe('new agent launch routing', () => { + it.each(['claude', 'codex'] as const)('opens supported local %s in structured chat', (agent) => { + expect(route({ agent })).toBe('structured-native-chat') + expect(route({ agent, launchText: 'draft', promptDelivery: 'draft' })).toBe( 'structured-native-chat' ) }) - // Why: the terminal mirror gate caps a draft at forty lines because a TUI cannot clear more; - // the structured composer has no such limit and must be chosen before that gate runs. + it('uses Chat UI alone even when an old selector is false or absent', () => { + const oldSelectorOff = { experimentalNativeChat: true, openAgentTabsInChatByDefault: false } + expect(route({ settings: oldSelectorOff })).toBe('structured-native-chat') + expect(route({ settings: { experimentalNativeChat: true } })).toBe('structured-native-chat') + }) + + // Why: a TUI caps a mirrored draft at forty lines; the structured composer has no such limit. it('routes a draft longer than the terminal mirror cap to structured chat', () => { const sixtyLineDraft = Array.from({ length: 60 }, (_, i) => `line ${i + 1}`).join('\n') expect(route({ launchText: sixtyLineDraft, promptDelivery: 'draft' })).toBe( 'structured-native-chat' ) - expect( - route({ - launchText: sixtyLineDraft, - promptDelivery: 'draft', - settings: { ...settings, experimentalStructuredNativeChat: false } - }) - ).toBe('terminal-tui') }) - it('preserves toggle-off and terminal-default behavior', () => { - expect(route({ settings: { ...settings, experimentalStructuredNativeChat: false } })).toBe( - 'legacy-native-chat' - ) - expect(route({ settings: { ...settings, openAgentTabsInChatByDefault: false } })).toBe( - 'terminal-tui' - ) - expect(route({ settings: { ...settings, experimentalNativeChat: false } })).toBe('terminal-tui') + it('opens the terminal when Chat UI is off', () => { + expect(route({ settings: { experimentalNativeChat: false } })).toBe('terminal-tui') + expect(route({ settings: null })).toBe('terminal-tui') }) - it('fails closed for missing capability, unsupported providers, and explicit TUI options', () => { - expect(route({ hostCapabilities: [] })).toBe('legacy-native-chat') - expect(route({ hostCapabilities: null })).toBe('legacy-native-chat') - // openclaude and grok render native chat but have no structured adapter. - expect(route({ agent: 'openclaude' })).toBe('legacy-native-chat') - expect(route({ agent: 'grok' })).toBe('legacy-native-chat') - expect(route({ startsOutsideWorkspaceRoot: true })).toBe('legacy-native-chat') - }) - - it('keeps an SSH workspace terminal-backed, since no Orca runtime runs there', () => { - expect(route({ executionHostId: 'ssh:host-a' })).toBe('legacy-native-chat') - }) - - // The lists the two sides really advertise, not hand-written ones: dropping the launch-mode - // capability from either would quietly turn every paired-server launch into a terminal. - it('opens a chat on a current paired server with the lists both sides advertise', () => { - expect( - route({ - executionHostId: 'runtime:environment-a', - hostCapabilities: RUNTIME_CAPABILITIES, - clientCapabilities: ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES - }) - ).toBe('structured-native-chat') - }) - - it('routes a paired server by its own capabilities', () => { - const structured = [ - STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY, - STRUCTURED_AGENT_SESSION_CLIENT_LAUNCH_MODE_CAPABILITY - ] - const server = { - executionHostId: 'runtime:environment-a', - hostCapabilities: structured, - clientCapabilities: structured - } - expect(route(server)).toBe('structured-native-chat') - // A client that never told the server it reads structured sessions keeps the host terminal. - expect(route({ ...server, clientCapabilities: [] })).toBe('legacy-native-chat') - expect(route({ ...server, clientCapabilities: undefined })).toBe('legacy-native-chat') - // A released server admits chats only with its own setting on, so it keeps the terminal. - expect( - route({ executionHostId: 'runtime:environment-a', clientCapabilities: structured }) - ).toBe('legacy-native-chat') - // The server has not answered yet, or answered without structured sessions. - expect(route({ executionHostId: 'runtime:environment-a', hostCapabilities: null })).toBe( - 'legacy-native-chat' - ) - expect(route({ executionHostId: 'runtime:environment-a', hostCapabilities: [] })).toBe( - 'legacy-native-chat' - ) - }) - - // Floating joined this list: its configured directory resolves like any other workspace, so a - // session can be filed under it. Workspace kind no longer downgrades a launch on its own. - it.each(['git-worktree', 'folder', 'floating'] as const)( - 'resolves a structured session for a local %s', - (workspaceKind) => { - expect(route({ workspaceKind })).toBe('structured-native-chat') - } - ) - - it('keeps WSL and repair-required launches terminal-backed', () => { + it('keeps unsupported and unverified launches in terminal UI', () => { + expect(route({ hostCapabilities: [] })).toBe('terminal-tui') + expect(route({ hostCapabilities: null })).toBe('terminal-tui') + expect(route({ agent: 'openclaude' })).toBe('terminal-tui') + // Grok has no built-in structured adapter; only a host agent list admits it. + expect(route({ agent: 'grok' })).toBe('terminal-tui') + expect(route({ startsOutsideWorkspaceRoot: true })).toBe('terminal-tui') + expect(route({ executionHostId: 'ssh:host-a' })).toBe('terminal-tui') expect( route({ projectRuntime: { @@ -158,7 +70,7 @@ describe('resolveAgentLaunchRoute', () => { } } }) - ).toBe('legacy-native-chat') + ).toBe('terminal-tui') expect( route({ projectRuntime: { @@ -172,31 +84,89 @@ describe('resolveAgentLaunchRoute', () => { } } }) - ).toBe('legacy-native-chat') + ).toBe('terminal-tui') + }) + + it('uses the host agent list for providers beyond Claude and Codex', () => { + expect( + route({ + agent: 'grok', + hostCapabilities: [ + STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY, + STRUCTURED_AGENT_SESSION_REGISTERED_AGENTS_RUNTIME_CAPABILITY + ], + hostStructuredAgents: ['grok'] + }) + ).toBe('structured-native-chat') + }) + + it.each(['git-worktree', 'folder', 'floating'] as const)( + 'allows a supported local %s workspace', + (workspaceKind) => expect(route({ workspaceKind })).toBe('structured-native-chat') + ) + + it('honors current paired-host capabilities and falls back on older hosts', () => { + expect( + route({ + executionHostId: 'runtime:environment-a', + hostCapabilities: RUNTIME_CAPABILITIES, + clientCapabilities: ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES + }) + ).toBe('structured-native-chat') + const negotiated = [ + STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY, + STRUCTURED_AGENT_SESSION_CLIENT_LAUNCH_MODE_CAPABILITY + ] + expect( + route({ + executionHostId: 'runtime:environment-a', + hostCapabilities: negotiated, + clientCapabilities: negotiated + }) + ).toBe('structured-native-chat') + expect( + route({ + executionHostId: 'runtime:environment-a', + hostCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY], + clientCapabilities: negotiated + }) + ).toBe('terminal-tui') + const server = { + executionHostId: 'runtime:environment-a', + hostCapabilities: negotiated, + clientCapabilities: negotiated + } + // A client that never told the server it reads structured sessions keeps the host terminal. + expect(route({ ...server, clientCapabilities: [] })).toBe('terminal-tui') + expect(route({ ...server, clientCapabilities: undefined })).toBe('terminal-tui') + // The server has not answered yet, or answered without structured sessions. + expect(route({ ...server, hostCapabilities: null })).toBe('terminal-tui') + expect(route({ ...server, hostCapabilities: [] })).toBe('terminal-tui') }) }) describe('explicit structured chat requests', () => { - it.each(['claude', 'codex'] as const)( - 'supports %s history resume when new tabs default to terminal', - (agent) => { - const input = { - agent, - settings: { ...settings, openAgentTabsInChatByDefault: false }, - executionHostId: 'local', - platform: 'darwin' as const, - hostCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY], - workspaceKind: 'folder' as const - } - expect(resolveAgentLaunchRoute(input)).toBe('terminal-tui') - expect(structuredAgentLaunchSupported(input)).toBe(true) - expect(structuredAgentLaunchSupported({ ...input, hostCapabilities: [] })).toBe(false) - expect( - structuredAgentLaunchSupported({ - ...input, - settings: { ...input.settings, experimentalStructuredNativeChat: false } - }) - ).toBe(false) + it.each(['claude', 'codex'] as const)('supports %s history resume on a capable host', (agent) => { + const input = { + agent, + settings, + executionHostId: 'local', + hostCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY], + workspaceKind: 'folder' as const } - ) + expect(structuredAgentLaunchSupported(input)).toBe(true) + expect(structuredAgentLaunchSupported({ ...input, hostCapabilities: [] })).toBe(false) + }) + + it('still permits an explicit structured history request while Chat UI is off', () => { + const input = { + agent: 'codex' as const, + settings: { experimentalNativeChat: false }, + executionHostId: 'local', + hostCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY] + } + expect(resolveAgentLaunchRoute(input)).toBe('terminal-tui') + expect(structuredAgentLaunchSupported(input)).toBe(true) + expect(structuredAgentLaunchSupported({ ...input, settings: null })).toBe(true) + }) }) diff --git a/src/renderer/src/lib/agent-launch-routing.ts b/src/renderer/src/lib/agent-launch-routing.ts index 93bc37ecb52..700b04dc424 100644 --- a/src/renderer/src/lib/agent-launch-routing.ts +++ b/src/renderer/src/lib/agent-launch-routing.ts @@ -1,30 +1,19 @@ import type { GlobalSettings } from '../../../shared/global-settings-types' import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime' import { - prefersStructuredNativeChatByDefault, + isNativeChatEnabled, resolveStructuredNativeChatSupport, type StructuredNativeChatBlocker } from '../../../shared/structured-native-chat-launch-route' import type { TuiAgent } from '../../../shared/tui-agent' import type { WorkspaceLaunchKind } from '../../../shared/workspace-launch-kind' -import { - decideInitialAgentTabViewMode, - type NativeChatLaunchPromptDelivery -} from '@/lib/native-chat-initial-view-mode' +import type { NativeChatLaunchPromptDelivery } from '@/lib/native-chat-launch-prompt-delivery' -export type AgentLaunchRoute = 'structured-native-chat' | 'legacy-native-chat' | 'terminal-tui' +export type AgentLaunchRoute = 'structured-native-chat' | 'terminal-tui' export type AgentLaunchRoutingInput = { agent: TuiAgent - settings: - | Pick< - GlobalSettings, - | 'experimentalNativeChat' - | 'experimentalStructuredNativeChat' - | 'openAgentTabsInChatByDefault' - > - | null - | undefined + settings: Pick | null | undefined executionHostId: string /** Capabilities of the target host; `null` = not yet established. */ hostCapabilities: readonly string[] | null @@ -36,43 +25,21 @@ export type AgentLaunchRoutingInput = { launchText?: string nativeChatTranscriptIsLocalReadable?: boolean startsOutsideWorkspaceRoot?: boolean - initialSessionOptions?: Readonly> /** The agents the target host listed as structured; absent until it has. */ hostStructuredAgents?: readonly string[] } export function resolveAgentLaunchRoute(input: AgentLaunchRoutingInput): AgentLaunchRoute { - // Why: structured eligibility is decided before the view-mode decider. That decider applies the - // terminal mirror gate (a TUI cannot clear more than forty lines of prefilled draft), which has - // no meaning for a session that seeds the composer store directly. Its other gates are already - // implied here: the structured resolver admits only agents the host runs as chats, and - // only hosts with an Orca runtime, and a structured session reads its journal over RPC rather - // than the transcript file, so local transcript readability does not apply either. - if ( - prefersStructuredNativeChatByDefault(input.settings) && - structuredAgentLaunchSupported(input) - ) { - return 'structured-native-chat' - } - const initialViewMode = decideInitialAgentTabViewMode({ - experimentalNativeChat: input.settings?.experimentalNativeChat, - openAgentTabsInChatByDefault: input.settings?.openAgentTabsInChatByDefault, - agent: input.agent, - promptDelivery: input.promptDelivery, - launchDraftText: input.launchText, - nativeChatTranscriptIsLocalReadable: input.nativeChatTranscriptIsLocalReadable - }) - return initialViewMode === 'chat' ? 'legacy-native-chat' : 'terminal-tui' + return isNativeChatEnabled(input.settings) && structuredAgentLaunchSupported(input) + ? 'structured-native-chat' + : 'terminal-tui' } -// Explicit chat requests do not depend on the default view mode for new tabs. +// Explicit chat requests (resume history in a new chat) do not depend on the Chat UI switch. export function structuredAgentLaunchSupported( input: Omit ): boolean { - return ( - input.settings?.experimentalStructuredNativeChat === true && - structuredAgentLaunchSupport(input).supported - ) + return structuredAgentLaunchSupport(input).supported } function structuredAgentLaunchSupport(input: Omit) { @@ -93,16 +60,10 @@ function structuredAgentLaunchSupport(input: Omit { }) it('opens nothing structured on a route that is not', async () => { - mocks.resolveAgentLaunchRoute.mockReturnValue('legacy-native-chat') + mocks.resolveAgentLaunchRoute.mockReturnValue('terminal-tui') const plan = planAgentSessionLaunch(store, { requestId: 'request-4b', agent: 'grok', @@ -186,7 +186,7 @@ describe('structuredAgentSessionLaunchFeasible', () => { it.each([true, false])('answers from feasibility alone (supported=%s)', (supported) => { mocks.structuredAgentLaunchSupported.mockReturnValue(supported) - const settings = { experimentalStructuredNativeChat: true } as never + const settings = { experimentalNativeChat: true } expect( structuredAgentSessionLaunchFeasible(store, { @@ -200,7 +200,7 @@ describe('structuredAgentSessionLaunchFeasible', () => { }) it('builds the input from the named settings, not the store copy', () => { - const settings = { experimentalStructuredNativeChat: true } as never + const settings = { experimentalNativeChat: true } structuredAgentSessionLaunchFeasible(store, { agent: 'codex', workspace: { kind: 'git-worktree', worktreeId: 'wt-1' }, diff --git a/src/renderer/src/lib/agent-session-launch-plan.ts b/src/renderer/src/lib/agent-session-launch-plan.ts index 25a247ca4e3..0df9b006f80 100644 --- a/src/renderer/src/lib/agent-session-launch-plan.ts +++ b/src/renderer/src/lib/agent-session-launch-plan.ts @@ -17,7 +17,7 @@ import { type AgentLaunchRoute, type AgentLaunchRoutingInput } from '@/lib/agent-launch-routing' -import type { NativeChatLaunchPromptDelivery } from '@/lib/native-chat-initial-view-mode' +import type { NativeChatLaunchPromptDelivery } from '@/lib/native-chat-launch-prompt-delivery' import { beginStructuredAgentLaunchSettlement, type StructuredAgentLaunchHandle, diff --git a/src/renderer/src/lib/empty-workspace-default-agent-chat.test.ts b/src/renderer/src/lib/empty-workspace-default-agent-chat.test.ts index 78ba58e4eaf..0ae25d2652a 100644 --- a/src/renderer/src/lib/empty-workspace-default-agent-chat.test.ts +++ b/src/renderer/src/lib/empty-workspace-default-agent-chat.test.ts @@ -34,7 +34,6 @@ function seedSettings(settings: Partial): void { settings: { ...getDefaultSettings('/tmp'), experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, defaultTuiAgent: 'codex', disabledTuiAgents: [], ...settings @@ -82,7 +81,7 @@ describe('openDefaultAgentChatInEmptyWorkspace', () => { }) it('does nothing unless new agent tabs open as chat', () => { - seedSettings({ openAgentTabsInChatByDefault: false }) + seedSettings({ experimentalNativeChat: false }) expect(openDefaultAgentChatInEmptyWorkspace('wt-1', mocks.seedShell)).toBeNull() expect(mocks.launchAgentInNewTab).not.toHaveBeenCalled() @@ -135,7 +134,7 @@ describe('agent detection for the default chat', () => { useAppStore.setState({ remoteDetectedAgentIds: { 'conn-1': [] } }) expect(emptyWorkspaceDefaultChatAwaitsDetection('wt-1')).toBe(false) - seedSettings({ openAgentTabsInChatByDefault: false }) + seedSettings({ experimentalNativeChat: false }) useAppStore.setState({ remoteDetectedAgentIds: {} }) expect(emptyWorkspaceDefaultChatAwaitsDetection('wt-1')).toBe(false) }) diff --git a/src/renderer/src/lib/empty-workspace-default-agent-chat.ts b/src/renderer/src/lib/empty-workspace-default-agent-chat.ts index 79612829b71..ad447daa480 100644 --- a/src/renderer/src/lib/empty-workspace-default-agent-chat.ts +++ b/src/renderer/src/lib/empty-workspace-default-agent-chat.ts @@ -1,4 +1,4 @@ -import { agentTabsDefaultToNativeChat } from '../../../shared/structured-native-chat-launch-route' +import { isNativeChatEnabled } from '../../../shared/structured-native-chat-launch-route' import { pickTuiAgent } from '../../../shared/tui-agent-selection' import type { TuiAgent } from '../../../shared/tui-agent' import { withTimeout } from '../../../shared/promise-timeout-fallback' @@ -26,10 +26,7 @@ function defaultChatDetectionTarget( worktreeId: string ): AgentDetectionTarget | undefined { // Why: a 'blank' default means the user wants workspaces to open without an agent. - if ( - !agentTabsDefaultToNativeChat(state.settings) || - state.settings?.defaultTuiAgent === 'blank' - ) { + if (!isNativeChatEnabled(state.settings) || state.settings?.defaultTuiAgent === 'blank') { return undefined } // Why: an unresolved owner is an unknown host, not the local machine. diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts index 9e23b70db3c..6616205f689 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts @@ -12,9 +12,7 @@ const REGISTERED: RuntimeCapability[] = [ STRUCTURED_AGENT_SESSION_REGISTERED_AGENTS_RUNTIME_CAPABILITY ] const SETTINGS = { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true + experimentalNativeChat: true } const mocks = vi.hoisted(() => { @@ -141,7 +139,7 @@ describe('a new tab whose chat route waits on the host agent list', () => { await expect(result?.structuredSettlement).resolves.toEqual({ kind: 'terminal' }) expect(relaunch.mock.calls[0]?.[0]).toMatchObject({ - agentSessionLaunchPlan: { route: 'legacy-native-chat' } + agentSessionLaunchPlan: { route: 'terminal-tui' } }) }) @@ -157,11 +155,11 @@ describe('a new tab whose chat route waits on the host agent list', () => { }) expect( routeNewTabLaunch( - storeWithSettings({ ...SETTINGS, experimentalStructuredNativeChat: false }), + storeWithSettings({ ...SETTINGS, experimentalNativeChat: false }), args, request ) - ).toMatchObject({ plan: { route: 'legacy-native-chat' } }) + ).toMatchObject({ plan: { route: 'terminal-tui' } }) }) }) @@ -237,7 +235,7 @@ describe("a new tab during startup, before this computer's runtime answered", () 'structured-native-chat' ) expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('claude opens legacy-native-chat: runtime-capability-unknown') + expect.stringContaining('claude opens terminal-tui: runtime-capability-unknown') ) }) }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts index a2c203ac25a..c150741b7ac 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts @@ -16,8 +16,6 @@ type PlacementSettings = { agentDefaultEnv: Record> activeRuntimeEnvironmentId: string | null experimentalNativeChat?: boolean - experimentalStructuredNativeChat?: boolean - openAgentTabsInChatByDefault?: boolean nativeChatSessionOptions?: Record< string, { model?: string; valuesByModel?: Record> } @@ -108,11 +106,9 @@ describe('launchAgentInNewTab terminal tab activation', () => { } ) - it('honours the chat default in a floating launch and scopes its surface to the floating workspace', async () => { + it('scopes a floating terminal fallback to the floating workspace with Chat UI on', async () => { store.settings = placementSettings({ experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true, nativeChatSessionOptions: { codex: { model: 'gpt-5.2-codex', @@ -134,18 +130,14 @@ describe('launchAgentInNewTab terminal tab activation', () => { undefined, { launchAgent: 'codex', - viewMode: 'chat' + quickCommandLabel: undefined } ) expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith( 'terminal', FLOATING_TERMINAL_WORKTREE_ID ) - // Why: the panel hosts the chat pane itself, so the launch carries the user's model/effort - // preferences the same way a main-window launch does. - expect(mockSeedNativeChatAppliedSessionOptions).toHaveBeenCalledWith('tab-1', 'codex', { - model: 'gpt-5.2-codex', - effort: 'medium' - }) + // Chat UI model preferences belong to structured chats, not a terminal fallback. + expect(mockQueueTabStartupCommand.mock.calls[0]?.[1]).not.toHaveProperty('sessionOptions') }) }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index e9ad2fba373..fa5ff2a1aec 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -34,8 +34,6 @@ const store = { activeRuntimeEnvironmentId: string | null terminalWindowsShell?: string experimentalNativeChat?: boolean - experimentalStructuredNativeChat?: boolean - openAgentTabsInChatByDefault?: boolean nativeChatSessionOptions?: Record< string, { model?: string; valuesByModel?: Record> } @@ -241,9 +239,7 @@ describe('launchAgentInNewTab', () => { agentDefaultArgs: {}, agentDefaultEnv: {}, activeRuntimeEnvironmentId: null, - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') @@ -257,7 +253,7 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'codex', - viewMode: 'chat' + quickCommandLabel: undefined }) expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', @@ -280,9 +276,7 @@ describe('launchAgentInNewTab', () => { agentDefaultArgs: {}, agentDefaultEnv: {}, activeRuntimeEnvironmentId: null, - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') @@ -296,8 +290,7 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'grok', - quickCommandLabel: undefined, - viewMode: 'chat' + quickCommandLabel: undefined }) expect(mockSeedNativeChatLaunchPrompt).toHaveBeenCalledWith({ tabId: 'tab-1', @@ -313,9 +306,7 @@ describe('launchAgentInNewTab', () => { agentDefaultArgs: {}, agentDefaultEnv: {}, activeRuntimeEnvironmentId: null, - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } store.repos = [{ id: 'repo-1', connectionId: 'ssh-target-1', path: '/repo' }] const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') @@ -334,9 +325,7 @@ describe('launchAgentInNewTab', () => { agentDefaultArgs: {}, agentDefaultEnv: {}, activeRuntimeEnvironmentId: null, - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') @@ -357,18 +346,16 @@ describe('launchAgentInNewTab', () => { text: 'https://github.com/o/r/issues/12' }) ) - expect(mockCreateTab.mock.calls[0]?.[3]).toHaveProperty('viewMode', 'chat') + expect(mockCreateTab.mock.calls[0]?.[3]).not.toHaveProperty('viewMode') }) - it('mirrors a multi-line draft into chat and opens the tab there', async () => { + it('mirrors a multi-line draft into chat without opening the tab there', async () => { store.settings = { agentCmdOverrides: {}, agentDefaultArgs: {}, agentDefaultEnv: {}, activeRuntimeEnvironmentId: null, - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') @@ -384,7 +371,7 @@ describe('launchAgentInNewTab', () => { expect(mockSeedNativeChatLaunchDraft).toHaveBeenCalledWith( expect.objectContaining({ tabId: 'tab-1', agent: 'claude', text: prompt }) ) - expect(mockCreateTab.mock.calls[0]?.[3]).toHaveProperty('viewMode', 'chat') + expect(mockCreateTab.mock.calls[0]?.[3]).not.toHaveProperty('viewMode') }) it('passes quick command labels only to locally-created agent tabs', async () => { @@ -409,9 +396,7 @@ describe('launchAgentInNewTab', () => { agentDefaultArgs: { codex: '--profile team' }, agentDefaultEnv: {}, activeRuntimeEnvironmentId: null, - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: false, + experimentalNativeChat: false, nativeChatSessionOptions: { codex: { model: 'gpt-5.2-codex', @@ -437,15 +422,13 @@ describe('launchAgentInNewTab', () => { expect(launch.sessionOptions).toBeUndefined() }) - it('applies native-chat model preferences to Quick Commands opened in chat', async () => { + it('keeps Chat UI model preferences out of a terminal fallback launch', async () => { store.settings = { agentCmdOverrides: {}, agentDefaultArgs: {}, agentDefaultEnv: {}, activeRuntimeEnvironmentId: null, experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true, nativeChatSessionOptions: { codex: { model: 'gpt-5.2-codex', @@ -465,19 +448,20 @@ describe('launchAgentInNewTab', () => { }) const launch = mockQueueTabStartupCommand.mock.calls[0]?.[1] - expect(launch.command).toContain("'-m' 'gpt-5.2-codex'") - expect(launch.command).toContain("'-c' 'model_reasoning_effort=medium'") - expect(launch.sessionOptions).toEqual({ model: 'gpt-5.2-codex', effort: 'medium' }) + expect(launch.command).not.toContain("'-m'") + expect(launch.command).not.toContain('model_reasoning_effort=') + expect(launch.sessionOptions).toBeUndefined() expect(mockCreateTab).toHaveBeenCalledWith( 'wt-1', undefined, undefined, - expect.objectContaining({ viewMode: 'chat' }) + expect.objectContaining({ launchAgent: 'codex' }) ) + expect(mockCreateTab.mock.calls[0]?.[3]).not.toHaveProperty('viewMode') expect(mockSetTabViewMode).not.toHaveBeenCalled() }) - it('preserves paired-host draft delivery and supported launch preferences', async () => { + it('preserves paired-host draft delivery without Chat UI launch preferences', async () => { mockIsWebRuntimeSessionActive.mockReturnValue(true) store.settings = { agentCmdOverrides: {}, @@ -485,8 +469,6 @@ describe('launchAgentInNewTab', () => { agentDefaultEnv: {}, activeRuntimeEnvironmentId: 'web-runtime', experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true, nativeChatSessionOptions: { claude: { model: 'opus', @@ -513,25 +495,25 @@ describe('launchAgentInNewTab', () => { prompt: 'review before sending', promptDelivery: 'draft', agentArgs: '--permission-mode plan', - launchPreferences: { model: 'opus', effort: 'high' }, agent: 'claude', launchDraft: 'review before sending' }) ) + expect( + mockCreateWebRuntimeAgentSessionTerminalWithLaunchDraft.mock.calls[0]?.[0] + ).not.toHaveProperty('launchPreferences') expect(mockCreateWebRuntimeSessionTerminal).not.toHaveBeenCalled() expect(mockCreateTab).not.toHaveBeenCalled() }) - it('propagates the default chat mode to paired web runtime launches', async () => { + it('propagates terminal UI to paired web runtime fallbacks', async () => { mockIsWebRuntimeSessionActive.mockReturnValue(true) store.settings = { agentCmdOverrides: {}, agentDefaultArgs: {}, agentDefaultEnv: {}, activeRuntimeEnvironmentId: 'web-runtime', - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') @@ -543,7 +525,7 @@ describe('launchAgentInNewTab', () => { environmentId: 'web-runtime', agentSessionKind: 'fresh', agent: 'codex', - viewMode: 'chat' + viewMode: 'terminal' }) ) }) @@ -555,9 +537,7 @@ describe('launchAgentInNewTab', () => { agentDefaultArgs: {}, agentDefaultEnv: {}, activeRuntimeEnvironmentId: 'web-runtime', - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: false + experimentalNativeChat: true } const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.ts b/src/renderer/src/lib/launch-agent-in-new-tab.ts index 97e5b1071e9..16047919930 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -9,8 +9,6 @@ import { launchNewTabPromptThroughHost, newTabPromptLaunchesThroughHost } from '@/lib/launch-agent-new-tab-host-route' -import { initialAgentTabViewModeProps } from '@/lib/native-chat-initial-view-mode' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session' import { launchAgentInWebHostTab } from '@/lib/launch-agent-web-host-tab' @@ -22,8 +20,6 @@ import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import type { TuiAgent } from '../../../shared/tui-agent' import type { LaunchSource } from '../../../shared/telemetry-events' import { resolveAgentLaunchExecutionContext } from '@/lib/launch-agent-execution-context' -import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' -import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache' import { launchStructuredAgentFromNewTab } from '@/lib/launch-agent-in-new-tab-structured-route' import type { StructuredAgentLaunchSettlement } from '@/lib/structured-agent-launch-settlement' import type { StructuredLaunchTerminal } from '@/lib/structured-agent-session-launch-admission' @@ -134,11 +130,13 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent activate } = args const store = useAppStore.getState() - const { worktreeSshConnectionId, resolvedLaunchPlatform, isRemote, queuedShell } = - resolveAgentLaunchExecutionContext(store, { + const { resolvedLaunchPlatform, isRemote, queuedShell } = resolveAgentLaunchExecutionContext( + store, + { worktreeId, ...(launchPlatform ? { launchPlatform } : {}) - }) + } + ) const cmdOverrides = store.settings?.agentCmdOverrides ?? {} const effectiveAgentArgs = agentArgs !== undefined @@ -149,17 +147,9 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent const hasPrompt = trimmedPrompt.length > 0 const isFollowupPath = TUI_AGENT_CONFIG[agent].promptInjectionMode === 'stdin-after-start' const workspaceKind = workspaceKindForWorktreeId(worktreeId) - // Why: the remote host can't infer this client's draft/default view choice, so decide it here for paired tabs too. - const viewModePromptDelivery = + // Why: a followup-path agent gets its prompt pasted unsubmitted after start, so route it as a draft. + const routePromptDelivery = hasPrompt && isFollowupPath && promptDelivery === 'auto-submit' ? 'draft' : promptDelivery - const initialViewModeOptions = { - agent, - promptDelivery: viewModePromptDelivery, - launchDraftText: trimmedPrompt, - nativeChatTranscriptIsLocalReadable: - isNativeChatTranscriptLocalReadable(worktreeSshConnectionId) - } - const initialViewModeProps = initialAgentTabViewModeProps(store.settings, initialViewModeOptions) const startupPlanBase = { agent, cmdOverrides, @@ -167,8 +157,7 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent shell: queuedShell, isRemote, agentArgs: effectiveAgentArgs, - agentEnv, - sessionOptions: resolveInitialNativeChatSessionOptions(store.settings, initialViewModeOptions) + agentEnv } const { startupPlan, pasteDraftAfterLaunch, submitPastedPrompt } = planLaunchAgentStartupPrompt({ base: startupPlanBase, @@ -188,9 +177,8 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent agent, workspace: { kind: workspaceKind, worktreeId }, prompt: trimmedPrompt, - promptDelivery: viewModePromptDelivery, + promptDelivery: routePromptDelivery, tuiCustomization: { cwd: initialCwd }, - initialSessionOptions: startupPlan.sessionOptions, onPromptDelivered, ...(args.promptKeptByCaller ? { promptKeptByCaller: true as const } : {}) }) @@ -236,7 +224,7 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent agentArgs, // Why: omission means terminal locally, but would let a paired host apply // its own default; send the client's resolved terminal choice explicitly. - viewMode: initialViewModeProps.viewMode ?? 'terminal', + viewMode: 'terminal', onPromptDelivered }) return { @@ -263,12 +251,10 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent prompt: trimmedPrompt, ...(agentArgs !== undefined ? { agentArgs } : {}), ...(initialCwd?.trim() ? { cwd: initialCwd } : {}), - ...(startupPlan.sessionOptions ? { sessionOptions: startupPlan.sessionOptions } : {}), // The same source main's window stamps on its own launches. launchSource: launchSource ?? 'tab_bar_quick_launch', quickCommandLabel, ...(pendingActivationSpawn ? { pendingActivationSpawn: true } : {}), - ...(initialViewModeProps.viewMode ? { viewMode: initialViewModeProps.viewMode } : {}), pasteContent: pasteDraftAfterLaunch, submit: submitPastedPrompt, ...(onPromptDelivered ? { onPromptDelivered } : {}), @@ -282,15 +268,12 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent } } // Why: queue startup BEFORE TerminalPane mounts — it snapshots pendingStartupByTabId in useState on first render. - // Why: followup path pastes an unsubmitted draft, so gate the initial chat view like a draft launch, not auto-submit. const tab = store.createTab(worktreeId, groupId, undefined, { launchAgent: agent, quickCommandLabel, ...(pendingActivationSpawn ? { pendingActivationSpawn: true } : {}), - ...(activate === false ? { activate: false } : {}), - ...initialViewModeProps + ...(activate === false ? { activate: false } : {}) }) - seedNativeChatAppliedSessionOptions(tab.id, agent, startupPlan.sessionOptions) if (initialCwd?.trim()) { // Why: queue before mount so local, WSL, and SSH continuations preserve their subdirectory. store.queueTabInitialCwd(tab.id, initialCwd) @@ -301,7 +284,6 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent launchConfig: startupPlan.launchConfig, launchAgent: agent, ...(agentArgs !== undefined ? { agentArgsOverride: agentArgs } : {}), - ...(startupPlan.sessionOptions ? { sessionOptions: startupPlan.sessionOptions } : {}), ...(startupPlan.startupCommandDelivery ? { startupCommandDelivery: startupPlan.startupCommandDelivery } : {}), diff --git a/src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts b/src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts index 2d909918b44..c1fcc5b5b07 100644 --- a/src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts +++ b/src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts @@ -82,8 +82,6 @@ const store = { agentDefaultEnv: {}, activeRuntimeEnvironmentId: null, experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true, nativeChatSessionOptions: undefined as | Record< string, @@ -178,9 +176,8 @@ vi.mock('@/runtime/structured-agent-session-client', () => ({ : mockCallStructuredAgentSession(target, method, params) })) -/** Structured adoption creates the tab in terminal mode and flips it to chat once - * Codex is ready; the bridge stamps `viewMode: 'chat'` on the tab up front. That - * difference is the only observable signal that the availability guard ran. */ +/** Structured adoption creates a terminal tab and flips it to chat once Codex is ready. + * The absence of an initial chat mode shows that the availability guard ran. */ describe('structured chat adoption guard on the launch path', () => { beforeEach(() => { vi.clearAllMocks() @@ -247,7 +244,7 @@ describe('structured chat adoption guard on the launch path', () => { mockCreateSupport.mockResolvedValue({ supported: true }) mockToastError.mockReset() hostCapabilities = STRUCTURED_HOST_CAPABILITIES - store.settings.openAgentTabsInChatByDefault = true + store.settings.experimentalNativeChat = true store.settings.nativeChatSessionOptions = undefined }) @@ -409,7 +406,7 @@ describe('structured chat adoption guard on the launch path', () => { /** The toggle is hidden under Terminal chat but its persisted value survives, so the launch * path must re-check the default view rather than trust a stale opt-in. */ it('ignores a stale structured opt-in while the default view is Terminal chat', async () => { - store.settings.openAgentTabsInChatByDefault = false + store.settings.experimentalNativeChat = false const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') const result = launchAgentInNewTab({ @@ -675,10 +672,13 @@ describe('structured chat adoption guard on the launch path', () => { launchAgentInNewTab({ requestId: 'request-18', agent: 'codex', worktreeId: 'wt-1' }) - expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - launchAgent: 'codex', - viewMode: 'chat' - }) + expect(mockCreateTab).toHaveBeenCalledWith( + 'wt-1', + undefined, + undefined, + expect.objectContaining({ launchAgent: 'codex' }) + ) + expect(mockCreateTab.mock.calls[0]?.[3]).not.toHaveProperty('viewMode') expect(mockWaitForAgentReady).not.toHaveBeenCalled() }) @@ -688,10 +688,13 @@ describe('structured chat adoption guard on the launch path', () => { launchAgentInNewTab({ requestId: 'request-19', agent: 'codex', worktreeId: 'wt-1' }) - expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - launchAgent: 'codex', - viewMode: 'chat' - }) + expect(mockCreateTab).toHaveBeenCalledWith( + 'wt-1', + undefined, + undefined, + expect.objectContaining({ launchAgent: 'codex' }) + ) + expect(mockCreateTab.mock.calls[0]?.[3]).not.toHaveProperty('viewMode') expect(mockWaitForAgentReady).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts b/src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts index f92407a8fa8..8cb614a293d 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts @@ -72,7 +72,7 @@ describe('beginDirectWorkItemStructuredLaunch', () => { beginDirectWorkItemStructuredLaunch({ plan: adoptAgentSessionLaunchVerdict({ ...structuredPlan, - route: 'legacy-native-chat' + route: 'terminal-tui' }), primaryTabId: null, beforeOpen: vi.fn() diff --git a/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts b/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts index b1a0ac294fc..40e04c61e5c 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts @@ -4,7 +4,6 @@ import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import { isTuiAgentEnabled, pickTuiAgent } from '../../../shared/tui-agent-selection' import { buildDirectWorkItemAgentStartupPlan } from '@/lib/launch-work-item-direct-agent' import type { AgentSessionLaunchPlan } from '@/lib/agent-session-launch-plan' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { resolveSourceControlLaunchPlatform } from '@/lib/source-control-launch-platform' import { beginStructuredAgentSessionProvisionalLaunch } from '@/lib/structured-agent-session-provisional-tab' import type { DeclinedStructuredLaunchTerminalOptions } from '@/lib/structured-agent-session-launch-admission' @@ -34,9 +33,6 @@ export function buildDirectWorkItemStartup(args: { promptDelivery: args.promptDelivery, settings: args.settings, launchPlatform, - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - args.launchConnectionId - ), // Why: SSH hosts run the plain `orca` shim, so the Linux-only `orca-ide` rename is not applied. isRemote: typeof args.launchConnectionId === 'string' }) diff --git a/src/renderer/src/lib/launch-work-item-direct-agent.test.ts b/src/renderer/src/lib/launch-work-item-direct-agent.test.ts index b5358ec6b3a..3b9fd614635 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent.test.ts @@ -46,9 +46,7 @@ describe('buildDirectWorkItemStartupOpts', () => { }) }) - it('carries launchDraftText for a natively-prefilled draft launch', () => { - // Why: the draft is already inside launchCommand, so draftPrompt stays unset - // and launchDraftText is the only signal the view-mode gate can read. + it('keeps a natively-prefilled draft in the terminal command', () => { const plan: AgentStartupPlan = { agent: 'claude', launchCommand: "claude --prefill 'https://github.com/o/r/issues/12'", @@ -57,15 +55,10 @@ describe('buildDirectWorkItemStartupOpts', () => { launchConfig: { agentArgs: '', agentEnv: {} } } - const opts = buildDirectWorkItemStartupOpts( - 'claude', - plan, - 'task_page', - 'https://github.com/o/r/issues/12' - ) + const opts = buildDirectWorkItemStartupOpts('claude', plan, 'task_page') expect(opts.startup?.draftPrompt).toBeUndefined() - expect(opts.startup?.launchDraftText).toBe('https://github.com/o/r/issues/12') + expect(opts.startup?.command).toContain('https://github.com/o/r/issues/12') }) }) @@ -83,36 +76,19 @@ const settings = { } describe('buildDirectWorkItemAgentStartupPlan', () => { - it('omits native-chat preferences when the new workspace opens in terminal mode', () => { + it('keeps Chat UI model preferences out of the terminal startup', () => { const result = buildDirectWorkItemAgentStartupPlan({ agent: 'codex', draftContent: 'Review issue 42', promptDelivery: 'draft', - settings: { ...settings, openAgentTabsInChatByDefault: false }, - launchPlatform: 'darwin', - nativeChatTranscriptIsLocalReadable: true + settings, + launchPlatform: 'darwin' }) expect(result.startupPlan?.launchCommand).not.toContain("'-m'") + expect(result.startupPlan?.launchCommand).not.toContain('model_reasoning_effort=') expect(result.startupPlan?.sessionOptions).toBeUndefined() }) - - it('applies native-chat preferences when the new workspace opens in chat', () => { - const result = buildDirectWorkItemAgentStartupPlan({ - agent: 'codex', - draftContent: 'Review issue 42', - promptDelivery: 'draft', - settings: { ...settings, openAgentTabsInChatByDefault: true }, - launchPlatform: 'darwin', - nativeChatTranscriptIsLocalReadable: true - }) - - expect(result.startupPlan?.launchCommand).toContain("'-m' 'gpt-5.2-codex'") - expect(result.startupPlan?.sessionOptions).toEqual({ - model: 'gpt-5.2-codex', - effort: 'medium' - }) - }) }) describe('notifyDirectWorkItemAgentStartTimeout', () => { @@ -141,7 +117,6 @@ describe('notifyDirectWorkItemAgentStartTimeout', () => { describe('buildDirectWorkItemAgentStartupPlan global arguments fallback', () => { const withGlobalArgs = { ...settings, - openAgentTabsInChatByDefault: false, agentDefaultArgs: { codex: '--sandbox danger-full-access' } } @@ -151,8 +126,7 @@ describe('buildDirectWorkItemAgentStartupPlan global arguments fallback', () => draftContent: 'Fix the broken checks', promptDelivery: 'draft', settings: withGlobalArgs, - launchPlatform: 'darwin', - nativeChatTranscriptIsLocalReadable: true + launchPlatform: 'darwin' }) expect(result.startupPlan?.launchCommand).toContain("'--sandbox' 'danger-full-access'") @@ -165,8 +139,7 @@ describe('buildDirectWorkItemAgentStartupPlan global arguments fallback', () => draftContent: 'Fix the broken checks', promptDelivery: 'draft', settings: withGlobalArgs, - launchPlatform: 'darwin', - nativeChatTranscriptIsLocalReadable: true + launchPlatform: 'darwin' }) expect(result.startupPlan?.launchCommand).toContain("'--model' 'gpt-5'") diff --git a/src/renderer/src/lib/launch-work-item-direct-agent.ts b/src/renderer/src/lib/launch-work-item-direct-agent.ts index e5595c0674b..b9175034e7b 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent.ts @@ -15,8 +15,6 @@ import { resolveTuiAgentLaunchEnv } from '../../../shared/tui-agent-launch-defaults' import { translate } from '@/i18n/i18n' -import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' -import type { PersistedNativeChatSessionOptions } from '../../../shared/native-chat-session-options' export function buildDirectWorkItemAgentStartupPlan(args: { agent: TuiAgent | null @@ -28,14 +26,10 @@ export function buildDirectWorkItemAgentStartupPlan(args: { agentCmdOverrides?: Partial> agentDefaultArgs?: Partial> agentDefaultEnv?: Partial>> - experimentalNativeChat?: boolean - openAgentTabsInChatByDefault?: boolean - nativeChatSessionOptions?: PersistedNativeChatSessionOptions } | null | undefined launchPlatform: NodeJS.Platform - nativeChatTranscriptIsLocalReadable?: boolean /** Why: SSH remotes deploy the CLI shim as plain `orca`, so the Linux-only * `orca-ide` rename must not be applied for remote launches. */ isRemote?: boolean @@ -53,13 +47,6 @@ export function buildDirectWorkItemAgentStartupPlan(args: { ? resolveTuiAgentLaunchArgs(args.agent, args.settings?.agentDefaultArgs) : args.agentArgs const effectiveAgentEnv = resolveTuiAgentLaunchEnv(args.agent, args.settings?.agentDefaultEnv) - const sessionOptions = resolveInitialNativeChatSessionOptions(args.settings, { - agent: args.agent, - ...(args.promptDelivery === 'draft' - ? { promptDelivery: 'draft' as const, launchDraftText: args.draftContent } - : {}), - nativeChatTranscriptIsLocalReadable: args.nativeChatTranscriptIsLocalReadable - }) const draftLaunchPlan = args.promptDelivery === 'submit-after-ready' ? null @@ -70,8 +57,7 @@ export function buildDirectWorkItemAgentStartupPlan(args: { platform: args.launchPlatform, isRemote: args.isRemote, agentArgs: effectiveAgentArgs, - agentEnv: effectiveAgentEnv, - sessionOptions + agentEnv: effectiveAgentEnv }) if (draftLaunchPlan) { @@ -82,9 +68,6 @@ export function buildDirectWorkItemAgentStartupPlan(args: { expectedProcess: draftLaunchPlan.expectedProcess, followupPrompt: null, launchConfig: draftLaunchPlan.launchConfig, - ...(draftLaunchPlan.sessionOptions - ? { sessionOptions: draftLaunchPlan.sessionOptions } - : {}), ...(draftLaunchPlan.startupCommandDelivery ? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery } : {}), @@ -103,7 +86,6 @@ export function buildDirectWorkItemAgentStartupPlan(args: { isRemote: args.isRemote, agentArgs: effectiveAgentArgs, agentEnv: effectiveAgentEnv, - sessionOptions, allowEmptyPromptLaunch: true }) if (startupPlan && args.promptDelivery === 'draft') { @@ -119,10 +101,7 @@ export function buildDirectWorkItemAgentStartupPlan(args: { export function buildDirectWorkItemStartupOpts( agent: TuiAgent | null, plan: AgentStartupPlan | null, - launchSource: LaunchSource, - /** Unsent launch context, for the view-mode decision only. Set it for every - * draft launch — a natively-prefilled plan carries no `draftPrompt`. */ - launchDraftText?: string + launchSource: LaunchSource ): { startup?: { command: string @@ -130,7 +109,6 @@ export function buildDirectWorkItemStartupOpts( launchConfig?: SleepingAgentLaunchConfig launchAgent?: TuiAgent draftPrompt?: string - launchDraftText?: string sessionOptions?: AgentStartupPlan['sessionOptions'] startupCommandDelivery?: StartupCommandDelivery telemetry?: AgentStartedTelemetry @@ -151,7 +129,6 @@ export function buildDirectWorkItemStartupOpts( ...(plan.sessionOptions ? { sessionOptions: plan.sessionOptions } : {}), ...(agent ? { launchAgent: agent } : {}), ...(plan.draftPrompt ? { draftPrompt: plan.draftPrompt } : {}), - ...(launchDraftText ? { launchDraftText } : {}), ...(plan.startupCommandDelivery ? { startupCommandDelivery: plan.startupCommandDelivery } : {}), diff --git a/src/renderer/src/lib/launch-work-item-direct-route-preparation.ts b/src/renderer/src/lib/launch-work-item-direct-route-preparation.ts index 3dddd4276b4..a57c1318062 100644 --- a/src/renderer/src/lib/launch-work-item-direct-route-preparation.ts +++ b/src/renderer/src/lib/launch-work-item-direct-route-preparation.ts @@ -100,8 +100,7 @@ export async function prepareDirectWorkItemAgentLaunch(args: { agent: effectiveAgent, workspace: { kind: 'git-worktree', worktreeId: args.worktreeId, repoId: args.repoId }, prompt: args.draftContent, - promptDelivery: args.promptDelivery, - initialSessionOptions: startupPlan?.sessionOptions + promptDelivery: args.promptDelivery }) const structuredLaunch = plan?.route === 'structured-native-chat' diff --git a/src/renderer/src/lib/launch-work-item-direct.test.ts b/src/renderer/src/lib/launch-work-item-direct.test.ts index 92e625db529..333c4ea3b4f 100644 --- a/src/renderer/src/lib/launch-work-item-direct.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct.test.ts @@ -410,7 +410,6 @@ describe('launchWorkItemDirect', () => { cmdOverrides: {}, agentArgs: '--dangerously-skip-permissions', agentEnv: {}, - sessionOptions: undefined, platform: 'win32', isRemote: false }) @@ -465,11 +464,8 @@ describe('launchWorkItemDirect', () => { }) expect(mocks.seedNativeChatLaunchPrompt).not.toHaveBeenCalled() // Why: the draft is inside `--prefill`, so the plan sets no draftPrompt. - // launchDraftText is the only thing that lets the view-mode gate see a - // draft here — without it this tab opens in chat unconditionally. const startup = mocks.activateAndRevealWorktree.mock.calls.at(-1)?.[1]?.startup expect(startup?.draftPrompt).toBeUndefined() - expect(startup?.launchDraftText).toBe('https://github.com/acme/repo/issues/12') }) it('seeds the chat-composer launch draft for a multi-line Linear draft launch', async () => { @@ -672,7 +668,6 @@ describe('launchWorkItemDirect', () => { cmdOverrides: {}, agentArgs: '--yolo', agentEnv: {}, - sessionOptions: undefined, platform: 'linux', isRemote: true }) @@ -682,7 +677,6 @@ describe('launchWorkItemDirect', () => { cmdOverrides: {}, agentArgs: '--yolo', agentEnv: {}, - sessionOptions: undefined, platform: 'linux', isRemote: true, allowEmptyPromptLaunch: true diff --git a/src/renderer/src/lib/launch-work-item-direct.ts b/src/renderer/src/lib/launch-work-item-direct.ts index dba04c50c1f..debf385c3be 100644 --- a/src/renderer/src/lib/launch-work-item-direct.ts +++ b/src/renderer/src/lib/launch-work-item-direct.ts @@ -242,12 +242,7 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom defaultTabs: result.defaultTabs, ...(launchPreparation.structuredLaunch ? { providesInitialSurface: true } - : buildDirectWorkItemStartupOpts( - effectiveAgent, - startupPlan, - launchSource, - promptDelivery === 'draft' ? draftContent : undefined - )) + : buildDirectWorkItemStartupOpts(effectiveAgent, startupPlan, launchSource)) }) return activationHolder.value !== false } diff --git a/src/renderer/src/lib/native-chat-initial-view-mode.test.ts b/src/renderer/src/lib/native-chat-initial-view-mode.test.ts deleted file mode 100644 index 52385894c03..00000000000 --- a/src/renderer/src/lib/native-chat-initial-view-mode.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { describe, it, expect } from 'vitest' -import type { Tab } from '../../../shared/tab-types' -import { - decideInitialAgentTabViewMode, - initialAgentTabViewModeProps -} from './native-chat-initial-view-mode' -import { isNativeChatTranscriptLocalReadable } from './native-chat-transcript-readability' - -describe('decideInitialAgentTabViewMode', () => { - it("returns 'chat' when native chat and the opt-in default setting are on", () => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'codex' - }) - ).toBe('chat') - }) - - it('returns undefined when native chat is disabled', () => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: false, - openAgentTabsInChatByDefault: true, - agent: 'codex' - }) - ).toBeUndefined() - }) - - it('returns undefined when the default-chat setting is off', () => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: false, - agent: 'codex' - }) - ).toBeUndefined() - }) - - it('returns undefined when the setting is missing (legacy settings)', () => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: undefined, - agent: 'codex' - }) - ).toBeUndefined() - }) - - it.each(['gemini'] as const)('keeps unsupported agent %s in terminal view', (agent) => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent - }) - ).toBeUndefined() - }) - - it.each([ - ['local', null], - ['runtime-owned', 'runtime-ssh-env-1'] - ] as const)('opens %s Grok in chat when configured', (_host, connectionId) => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'grok', - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable(connectionId) - }) - ).toBe('chat') - }) - - it('keeps Model-A SSH omp in the terminal view but opens it locally', () => { - const forConnection = (connectionId: string | null): Tab['viewMode'] => - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'omp', - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable(connectionId) - }) - expect(forConnection('ssh-target-1')).toBeUndefined() - expect(forConnection(null)).toBe('chat') - }) - - it('keeps Model-A SSH Grok in the terminal view', () => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'grok', - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable('ssh-target-1') - }) - ).toBeUndefined() - expect( - initialAgentTabViewModeProps( - { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true - }, - { agent: 'grok', nativeChatTranscriptIsLocalReadable: false } - ) - ).toEqual({}) - }) - - it('opens a mirrorable draft launch in chat', () => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'claude', - promptDelivery: 'draft', - launchDraftText: 'https://github.com/o/r/issues/12' - }) - ).toBe('chat') - }) - - it.each([ - ['multi-line', 'Reproduce first\n\nhttps://github.com/o/r/issues/12'], - ['trailing-newline', 'https://github.com/o/r/issues/12\n'] - ])('opens a %s draft in chat with its mirrored composer text', (_label, launchDraftText) => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'claude', - promptDelivery: 'draft', - launchDraftText - }) - ).toBe('chat') - }) - - it.each([ - ['Unicode-line-separator', 'one\u2028two'], - ['blank', ' '], - ['absent', undefined] - ])('keeps a %s draft in the terminal, where its text actually is', (_label, launchDraftText) => { - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'claude', - promptDelivery: 'draft', - ...(launchDraftText === undefined ? {} : { launchDraftText }) - }) - ).toBeUndefined() - }) - - it('returns tab creation props only when chat should be the initial mode', () => { - expect( - initialAgentTabViewModeProps( - { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true - }, - { agent: 'claude' } - ) - ).toEqual({ viewMode: 'chat' }) - expect( - initialAgentTabViewModeProps( - { - experimentalNativeChat: false, - openAgentTabsInChatByDefault: true - }, - { agent: 'claude' } - ) - ).toEqual({}) - }) -}) diff --git a/src/renderer/src/lib/native-chat-initial-view-mode.ts b/src/renderer/src/lib/native-chat-initial-view-mode.ts deleted file mode 100644 index abc631dc6fa..00000000000 --- a/src/renderer/src/lib/native-chat-initial-view-mode.ts +++ /dev/null @@ -1,6 +0,0 @@ -// The host derives an agent.launch tab's view by this same rule, so it lives in shared. -export { - decideInitialAgentTabViewMode, - initialAgentTabViewModeProps, - type NativeChatLaunchPromptDelivery -} from '../../../shared/native-chat-initial-view-mode' diff --git a/src/renderer/src/lib/native-chat-launch-draft-mirrorability.test.ts b/src/renderer/src/lib/native-chat-launch-draft-mirrorability.test.ts index be8b0906170..279cadacd67 100644 --- a/src/renderer/src/lib/native-chat-launch-draft-mirrorability.test.ts +++ b/src/renderer/src/lib/native-chat-launch-draft-mirrorability.test.ts @@ -12,7 +12,6 @@ vi.mock('@/store', () => ({ import { seedNativeChatLaunchDraftForAgentTab } from './agent-launch-prompt-delivery' import { canMirrorLaunchDraftToNativeChat } from './native-chat-launch-draft-mirrorability' -import { decideInitialAgentTabViewMode } from './native-chat-initial-view-mode' import { AGENT_TUI_CLEAR_MAX_LINES } from '../../../shared/agent-tui-input-clear' const maxLineDraft = Array.from({ length: AGENT_TUI_CLEAR_MAX_LINES }, () => 'line').join('\n') @@ -39,18 +38,6 @@ const DRAFT_TEXTS = [ '\n' ] -function opensInChat(text: string): boolean { - return ( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'claude', - promptDelivery: 'draft', - launchDraftText: text - }) === 'chat' - ) -} - function seedsTheComposer(text: string): boolean { mocks.seedNativeChatLaunchDraft.mockClear() seedNativeChatLaunchDraftForAgentTab({ tabId: 'tab-1', agent: 'claude', text }) @@ -62,10 +49,9 @@ describe('launch draft mirrorability', () => { vi.clearAllMocks() }) - it.each(DRAFT_TEXTS)('both sides follow the predicate: %j', (text) => { + it.each(DRAFT_TEXTS)('seeds only mirrorable explicit chat drafts: %j', (text) => { const expected = canMirrorLaunchDraftToNativeChat(text) expect(seedsTheComposer(text)).toBe(expected) - expect(opensInChat(text)).toBe(expected) }) it('accepts CR/LF drafts and rejects unsupported Unicode line separators', () => { @@ -84,8 +70,6 @@ describe('launch draft mirrorability', () => { }) it('withholds the mirror from agents without a native-chat renderer', () => { - // The view mode already returns undefined for these, so the sets still - // agree — but only the seeding side enforces it. expect(seedsTheComposer('https://github.com/o/r/issues/12')).toBe(true) mocks.seedNativeChatLaunchDraft.mockClear() seedNativeChatLaunchDraftForAgentTab({ @@ -94,14 +78,5 @@ describe('launch draft mirrorability', () => { text: 'https://github.com/o/r/issues/12' }) expect(mocks.seedNativeChatLaunchDraft).not.toHaveBeenCalled() - expect( - decideInitialAgentTabViewMode({ - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - agent: 'gemini', - promptDelivery: 'draft', - launchDraftText: 'https://github.com/o/r/issues/12' - }) - ).toBeUndefined() }) }) diff --git a/src/renderer/src/lib/native-chat-launch-prompt-delivery.ts b/src/renderer/src/lib/native-chat-launch-prompt-delivery.ts new file mode 100644 index 00000000000..9650c5d0620 --- /dev/null +++ b/src/renderer/src/lib/native-chat-launch-prompt-delivery.ts @@ -0,0 +1 @@ +export type NativeChatLaunchPromptDelivery = 'auto-submit' | 'draft' | 'submit-after-ready' diff --git a/src/renderer/src/lib/onboarding-folder-agent-launch.ts b/src/renderer/src/lib/onboarding-folder-agent-launch.ts index 3ae37582360..7f18ee3444e 100644 --- a/src/renderer/src/lib/onboarding-folder-agent-launch.ts +++ b/src/renderer/src/lib/onboarding-folder-agent-launch.ts @@ -28,13 +28,11 @@ export function resolveDismissedOnboardingFolderAgentLaunch(args: { onboarding: OnboardingState | null hasExistingProject: boolean executionHostId: string - nativeChatTranscriptIsLocalReadable?: boolean }): OnboardingFolderAgentLaunch { const startup = buildDismissedOnboardingFolderAgentStartup( args.store.settings ?? null, args.onboarding, - args.hasExistingProject, - args.nativeChatTranscriptIsLocalReadable + args.hasExistingProject ) const agent = startup?.launchAgent ?? null if (!startup || !agent) { @@ -44,8 +42,7 @@ export function resolveDismissedOnboardingFolderAgentLaunch(args: { const plan = planAgentSessionLaunch(args.store, { requestId: newAgentLaunchRequestId(), agent, - workspace: { kind: 'folder', executionHostId: args.executionHostId }, - initialSessionOptions: startup.sessionOptions + workspace: { kind: 'folder', executionHostId: args.executionHostId } }) return { agent, diff --git a/src/renderer/src/lib/onboarding-folder-agent-startup.ts b/src/renderer/src/lib/onboarding-folder-agent-startup.ts index 5dbc893ecbd..6c2e1d8bea5 100644 --- a/src/renderer/src/lib/onboarding-folder-agent-startup.ts +++ b/src/renderer/src/lib/onboarding-folder-agent-startup.ts @@ -11,8 +11,6 @@ import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-re import type { GlobalSettings } from '../../../shared/global-settings-types' import type { OnboardingState } from '../../../shared/onboarding-state-types' import type { TuiAgent } from '../../../shared/tui-agent' -import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' -import type { SessionOptionValue } from '../../../shared/native-chat-session-options' export type OnboardingFolderAgentStartup = { command: string @@ -20,7 +18,6 @@ export type OnboardingFolderAgentStartup = { launchConfig?: SleepingAgentLaunchConfig launchAgent?: TuiAgent startupCommandDelivery?: StartupCommandDelivery - sessionOptions?: Record telemetry: AgentStartedTelemetry } @@ -32,8 +29,7 @@ function getClientPlatform(): NodeJS.Platform { } export function buildOnboardingFolderAgentStartup( - settings: GlobalSettings | null, - nativeChatTranscriptIsLocalReadable = true + settings: GlobalSettings | null ): OnboardingFolderAgentStartup | undefined { const agent = settings?.defaultTuiAgent if ( @@ -51,10 +47,6 @@ export function buildOnboardingFolderAgentStartup( cmdOverrides: settings.agentCmdOverrides ?? {}, agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs), agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv), - sessionOptions: resolveInitialNativeChatSessionOptions(settings, { - agent, - nativeChatTranscriptIsLocalReadable - }), platform: getClientPlatform(), allowEmptyPromptLaunch: true }) @@ -67,7 +59,6 @@ export function buildOnboardingFolderAgentStartup( ...(startupPlan.env ? { env: startupPlan.env } : {}), launchConfig: startupPlan.launchConfig, launchAgent: agent, - ...(startupPlan.sessionOptions ? { sessionOptions: startupPlan.sessionOptions } : {}), ...(startupPlan.startupCommandDelivery ? { startupCommandDelivery: startupPlan.startupCommandDelivery } : {}), @@ -94,11 +85,10 @@ export function shouldSeedFolderAgentAfterDismissedOnboarding( export function buildDismissedOnboardingFolderAgentStartup( settings: GlobalSettings | null, onboarding: OnboardingState | null, - hasExistingProject: boolean, - nativeChatTranscriptIsLocalReadable = true + hasExistingProject: boolean ): OnboardingFolderAgentStartup | undefined { if (!shouldSeedFolderAgentAfterDismissedOnboarding(onboarding, hasExistingProject)) { return undefined } - return buildOnboardingFolderAgentStartup(settings, nativeChatTranscriptIsLocalReadable) + return buildOnboardingFolderAgentStartup(settings) } diff --git a/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts b/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts index c577e143d63..eb2db4c8938 100644 --- a/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts +++ b/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts @@ -11,15 +11,8 @@ import { endWebRuntimeWakeTerminalRespawn } from '@/runtime/web-runtime-wake-terminal-respawn' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' -import { - draftViewModeProps, - resolveStartupLaunchDraftText, - type WorktreeStartupPayload -} from '@/lib/worktree-startup-payload' +import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload' import type { TuiAgent } from '../../../shared/tui-agent' -import { initialAgentTabViewModeProps } from '@/lib/native-chat-initial-view-mode' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' -import { getConnectionId } from '@/lib/connection-context' import { toast } from 'sonner' import { shouldAutoCreateInitialTerminal } from '@/components/terminal/initial-terminal' @@ -94,20 +87,10 @@ export function ensureWebRuntimeWorktreeTerminalAfterWake( } const startup = opts?.startup - const viewModeProps = launchAgent - ? initialAgentTabViewModeProps(state.settings, { - agent: launchAgent, - ...draftViewModeProps(resolveStartupLaunchDraftText(startup)), - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - getConnectionId(worktreeId) - ) - }) - : {} // Why: sleep keeps tab rows but terminal.stop clears host PTYs, while a failed create receipt leaves a selected agent with no host surface. void createWebRuntimeSessionTerminal({ worktreeId, environmentId: runtimeEnvironmentId, - ...viewModeProps, ...(startup ? { command: startup.command, diff --git a/src/renderer/src/lib/worktree-activation-agent-startup.test.ts b/src/renderer/src/lib/worktree-activation-agent-startup.test.ts index 5f450b99978..ca6595d0f43 100644 --- a/src/renderer/src/lib/worktree-activation-agent-startup.test.ts +++ b/src/renderer/src/lib/worktree-activation-agent-startup.test.ts @@ -33,8 +33,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { it('opens new agent workspace terminals in native chat when configured', () => { const store = createMockStore({ settings: { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) @@ -51,8 +50,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { pendingActivationSpawn: true, - launchAgent: 'claude', - viewMode: 'chat' + launchAgent: 'claude' }) expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { command: 'claude', @@ -61,14 +59,13 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) it.each([ - ['mirrorable', 'https://github.com/o/r/issues/12', { viewMode: 'chat' }], - ['multi-line', 'Review this\n\nhttps://github.com/o/r/issues/12', { viewMode: 'chat' }], + ['mirrorable', 'https://github.com/o/r/issues/12', {}], + ['multi-line', 'Review this\n\nhttps://github.com/o/r/issues/12', {}], ['unsupported-separator', 'Review this\u2028https://github.com/o/r/issues/12', {}] ])('opens a %s draft startup payload accordingly', (_label, draftPrompt, expectedViewMode) => { const store = createMockStore({ settings: { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) @@ -91,20 +88,17 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) }) - // An argv-prefill launch carries the draft inside `command` and sets NO - // draftPrompt, so gating on draftPrompt alone lets it open in chat with - // nothing mirrored — an empty composer beside a filled TUI input. + // An argv-prefill launch carries its draft inside the terminal command. it.each([ - ['mirrorable', 'https://github.com/o/r/issues/12', { viewMode: 'chat' }], - ['multi-line', 'Review this\n\nhttps://github.com/o/r/issues/12', { viewMode: 'chat' }], + ['mirrorable', 'https://github.com/o/r/issues/12', {}], + ['multi-line', 'Review this\n\nhttps://github.com/o/r/issues/12', {}], ['unsupported-separator', 'Review this\u2028https://github.com/o/r/issues/12', {}] ])( - 'gates a %s argv-prefill draft on launchDraftText alone', + 'keeps a %s argv-prefill draft in the terminal', (_label, launchDraftText, expectedViewMode) => { const store = createMockStore({ settings: { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) @@ -113,8 +107,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { 'wt-1', { command: `claude --prefill '${launchDraftText}'`, - launchAgent: 'claude', - launchDraftText + launchAgent: 'claude' }, undefined, undefined @@ -128,14 +121,13 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - it('opens the startup default tab in native chat when configured', () => { + it('opens the startup default tab in the terminal', () => { let createdIndex = 0 const createTab = vi.fn(() => ({ id: `tab-${++createdIndex}` })) const store = createMockStore({ createTab, settings: { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) @@ -151,14 +143,13 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { pendingActivationSpawn: true, recordInteraction: false, - launchAgent: 'claude', - viewMode: 'chat' + launchAgent: 'claude' }) }) it.each([ - ['mirrorable', 'https://github.com/o/r/issues/12', { viewMode: 'chat' }], - ['multi-line', 'Review this\n\nhttps://github.com/o/r/issues/12', { viewMode: 'chat' }], + ['mirrorable', 'https://github.com/o/r/issues/12', {}], + ['multi-line', 'Review this\n\nhttps://github.com/o/r/issues/12', {}], ['unsupported-separator', 'Review this\u2028https://github.com/o/r/issues/12', {}] ])( 'opens a %s draft startup default tab accordingly', @@ -168,8 +159,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { const store = createMockStore({ createTab, settings: { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) diff --git a/src/renderer/src/lib/worktree-activation-store-contract.ts b/src/renderer/src/lib/worktree-activation-store-contract.ts index 66de89e85e3..8d1f3aa9151 100644 --- a/src/renderer/src/lib/worktree-activation-store-contract.ts +++ b/src/renderer/src/lib/worktree-activation-store-contract.ts @@ -59,7 +59,7 @@ export type WorktreeActivationStore = Partial & { startup: { command: string; env?: Record } ) => void queueTabInitialCwd: (tabId: string, cwd: string) => void - settings?: Pick | null + settings?: Pick | null } export type InitialTerminalOptions = { diff --git a/src/renderer/src/lib/worktree-creation-agent-seeds.test.ts b/src/renderer/src/lib/worktree-creation-agent-seeds.test.ts index e052b75f7d4..ab2bd9e780d 100644 --- a/src/renderer/src/lib/worktree-creation-agent-seeds.test.ts +++ b/src/renderer/src/lib/worktree-creation-agent-seeds.test.ts @@ -91,8 +91,7 @@ beforeEach(() => { useAppStore.setState({ settings: { ...initialSettings, - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) }) @@ -131,9 +130,8 @@ describe('seedAgentTabStateAfterWorktreeCreate', () => { ) }) - it('moves a backend-spawned non-mirrorable draft out of an inherited chat view', () => { + it('preserves an explicit chat view while seeding a backend draft', () => { setTabs([{ id: 'agent-tab', launchAgent: 'claude', viewMode: 'chat' }]) - seedAgentTabStateAfterWorktreeCreate({ request: { ...request, launchDraftPrompt: 'note\u2028https://github.com/o/r/issues/12' }, worktreeId: 'wt-1', @@ -141,83 +139,23 @@ describe('seedAgentTabStateAfterWorktreeCreate', () => { startupTerminalTabId: 'agent-tab', backendSpawned: true }) - - expect(tabViewMode('agent-tab')).toBe('terminal') + expect(tabViewMode('agent-tab')).toBe('chat') }) - it('opens a backend-spawned mirrorable draft in chat after host reconciliation', async () => { + it('does not turn a backend terminal draft into terminal-backed chat', () => { setTabs([{ id: 'agent-tab', launchAgent: 'claude', viewMode: 'terminal' }]) - const runtimeCall = vi.fn(async ({ method }: { method: string }) => { - if (method === 'agentSession.handoffStatus') { - return { - id: 'status', - ok: true, - result: { owner: 'native', direction: null, phase: 'idle' }, - _meta: { runtimeId: 'runtime-1' } - } - } - throw new Error(`Unexpected runtime method: ${method}`) - }) - const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window') - Object.defineProperty(globalThis, 'window', { - configurable: true, - value: { api: { runtime: { call: runtimeCall } } } - }) - useAppStore.setState({ - terminalLayoutsByTabId: { - 'agent-tab': { - activeLeafId: 'leaf-1', - ptyIdsByLeafId: { 'leaf-1': 'pty-1' } - } as never - }, - agentStatusByPaneKey: { - 'agent-tab:leaf-1': { - agentType: 'claude', - providerSession: { id: 'claude-session-1' } - } as never - } - }) - - try { - seedAgentTabStateAfterWorktreeCreate({ - request, - worktreeId: 'wt-1', - primaryTabId: 'agent-tab', - startupTerminalTabId: 'agent-tab', - backendSpawned: true - }) - - await vi.waitFor(() => expect(tabViewMode('agent-tab')).toBe('chat')) - } finally { - if (previousWindow) { - Object.defineProperty(globalThis, 'window', previousWindow) - } else { - Reflect.deleteProperty(globalThis, 'window') - } - } - }) - - it('still opens a local omp draft in chat, despite the local-transcript gate', () => { - // Why: omp discloses no hook transcript path, so it joins Grok in requiring a - // locally readable sessions root. This call site must therefore SUPPLY that - // readability flag for omp too — gating on Grok alone left it undefined and - // parked every omp draft in the terminal view, local workspace or not. - setTabs([{ id: 'agent-tab', launchAgent: 'omp', viewMode: 'terminal' }]) - seedAgentTabStateAfterWorktreeCreate({ - request: { ...request, agent: 'omp' as const }, + request, worktreeId: 'wt-1', primaryTabId: 'agent-tab', startupTerminalTabId: 'agent-tab', backendSpawned: true }) - - expect(tabViewMode('agent-tab')).toBe('chat') + expect(tabViewMode('agent-tab')).toBe('terminal') }) - it('keys a raw backend tab id and updates the host before its tab mirror lands', async () => { + it('seeds a raw backend tab id without setting a default view on the host', () => { setTabs([], 'runtime-1') - seedAgentTabStateAfterWorktreeCreate({ request, worktreeId: 'wt-1', @@ -225,20 +163,8 @@ describe('seedAgentTabStateAfterWorktreeCreate', () => { startupTerminalTabId: 'host-agent-tab', backendSpawned: true }) - - expect(seededTabIds()).toEqual(['web-terminal-host-agent-tab']) - await vi.waitFor(() => - expect(mocks.setWebRuntimeTabProps).toHaveBeenCalledWith({ - worktreeId: 'wt-1', - tabId: 'web-terminal-host-agent-tab', - viewMode: 'chat' - }) - ) - setTabs( - [{ id: 'web-terminal-host-agent-tab', launchAgent: 'claude', viewMode: 'chat' }], - 'runtime-1' - ) expect(seededTabIds()).toEqual(['web-terminal-host-agent-tab']) + expect(mocks.setWebRuntimeTabProps).not.toHaveBeenCalled() }) it('seeds the launchAgent-stamped tab when the renderer owns startup', () => { diff --git a/src/renderer/src/lib/worktree-creation-agent-seeds.ts b/src/renderer/src/lib/worktree-creation-agent-seeds.ts index 6abf478bd28..e884cb38d7a 100644 --- a/src/renderer/src/lib/worktree-creation-agent-seeds.ts +++ b/src/renderer/src/lib/worktree-creation-agent-seeds.ts @@ -2,10 +2,6 @@ import { useAppStore } from '@/store' import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache' import { seedNativeChatLaunchDraftForAgentTab } from '@/lib/agent-launch-prompt-delivery' import { queueHookCommandsForFirstWorktreeTab } from '@/lib/hook-command-delayed-delivery' -import { decideInitialAgentTabViewMode } from '@/lib/native-chat-initial-view-mode' -import { getConnectionIdFromState } from '@/lib/connection-context' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' -import { nativeChatRequiresLocalTranscript } from '@/lib/native-chat-supported-agent' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import { toWebTerminalSurfaceTabId } from '@/runtime/web-terminal-surface-id' import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' @@ -45,46 +41,6 @@ function resolveLaunchAgentTabId( return stamped ?? args.primaryTabId ?? args.startupTerminalTabId ?? null } -function applyBackendSpawnedDraftViewMode(args: { - state: AppStoreSnapshot - request: SeedRequest - agent: TuiAgent - tabId: string - worktreeId: string - backendSpawned: boolean -}): void { - const { state, request, agent, tabId, worktreeId, backendSpawned } = args - if (!backendSpawned || !request.launchDraftPrompt) { - return - } - const desiredViewMode = - decideInitialAgentTabViewMode({ - experimentalNativeChat: state.settings?.experimentalNativeChat, - openAgentTabsInChatByDefault: state.settings?.openAgentTabsInChatByDefault, - agent, - promptDelivery: 'draft', - launchDraftText: request.launchDraftPrompt, - ...(nativeChatRequiresLocalTranscript(agent) - ? { - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - getConnectionIdFromState(state, worktreeId) - ) - } - : {}) - }) ?? 'terminal' - const tab = state.unifiedTabsByWorktree?.[worktreeId]?.find((tab) => tab.id === tabId) - if (!tab && getRuntimeEnvironmentIdForWorktree(state, worktreeId)) { - void import('@/runtime/web-runtime-session').then(({ setWebRuntimeTabProps }) => - setWebRuntimeTabProps({ worktreeId, tabId, viewMode: desiredViewMode }) - ) - return - } - const currentViewMode = tab?.viewMode ?? 'terminal' - if (currentViewMode !== desiredViewMode) { - state.setTabViewMode(tabId, desiredViewMode) - } -} - function applyAgentTabSeeds(args: { state: AppStoreSnapshot request: SeedRequest @@ -94,7 +50,6 @@ function applyAgentTabSeeds(args: { backendSpawned: boolean }): void { const { request, agent, tabId } = args - applyBackendSpawnedDraftViewMode(args) seedNativeChatAppliedSessionOptions(tabId, agent, request.startupPlan?.sessionOptions) // Why: draft launch context reaches only the TUI input; seed the // chat-composer copy so it isn't invisible in the chat view. diff --git a/src/renderer/src/lib/worktree-creation-flow-startup.ts b/src/renderer/src/lib/worktree-creation-flow-startup.ts index 4418cb21ed3..539c566a800 100644 --- a/src/renderer/src/lib/worktree-creation-flow-startup.ts +++ b/src/renderer/src/lib/worktree-creation-flow-startup.ts @@ -25,7 +25,6 @@ export function buildWorktreeCreationStartupOpt( ...(plan.draftPrompt ? { draftPrompt: plan.draftPrompt } : {}), // Why: view-mode only. An argv-prefill plan sets no draftPrompt, so this is // the sole signal that this launch starts with unsent context in the TUI. - ...(request.launchDraftPrompt ? { launchDraftText: request.launchDraftPrompt } : {}), ...(plan.startupCommandDelivery ? { startupCommandDelivery: plan.startupCommandDelivery } : {}), // Why: command-code shows its prompt in the tab status before the first // hook fires, so the prompt is threaded through here. diff --git a/src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts b/src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts index 7a678434a4e..70df0b29920 100644 --- a/src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts +++ b/src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts @@ -17,8 +17,7 @@ type TestActiveView = 'terminal' | 'tasks' const store = { settings: { activeRuntimeEnvironmentId: null as string | null, - experimentalNativeChat: undefined as boolean | undefined, - openAgentTabsInChatByDefault: undefined as boolean | undefined + experimentalNativeChat: undefined }, activeView: 'terminal' as TestActiveView, activePendingCreationId: 'creation-1' as string | null, diff --git a/src/renderer/src/lib/worktree-creation-flow.test.ts b/src/renderer/src/lib/worktree-creation-flow.test.ts index 92dea7e3991..f719025dd5b 100644 --- a/src/renderer/src/lib/worktree-creation-flow.test.ts +++ b/src/renderer/src/lib/worktree-creation-flow.test.ts @@ -13,8 +13,8 @@ type TestActiveView = 'terminal' | 'tasks' const store = { settings: { activeRuntimeEnvironmentId: null as string | null, - experimentalNativeChat: undefined as boolean | undefined, - openAgentTabsInChatByDefault: undefined as boolean | undefined + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture mutates this setting across test cases. + experimentalNativeChat: undefined as boolean | undefined }, activeView: 'terminal' as TestActiveView, activePendingCreationId: 'creation-1' as string | null, @@ -100,7 +100,6 @@ beforeEach(() => { vi.clearAllMocks() store.settings.activeRuntimeEnvironmentId = null store.settings.experimentalNativeChat = undefined - store.settings.openAgentTabsInChatByDefault = undefined store.activeView = 'terminal' store.activePendingCreationId = 'creation-1' store.repos = [] @@ -682,11 +681,10 @@ describe('staged background worktree creation', () => { }) it.each([ - ['mirrorable local Grok', 'grok', 'https://github.com/o/r/issues/12', 'chat'], - ['multi-line Claude', 'claude', 'note\nhttps://github.com/o/r/issues/12', 'chat'] + ['mirrorable local Grok', 'grok', 'https://github.com/o/r/issues/12', 'terminal'], + ['multi-line Claude', 'claude', 'note\nhttps://github.com/o/r/issues/12', 'terminal'] ] as const)('passes %s draft mode to backend startup', async (_label, agent, draft, viewMode) => { store.settings.experimentalNativeChat = true - store.settings.openAgentTabsInChatByDefault = true store.repos = [{ id: 'repo-1', connectionId: null }] continueBackgroundWorktreeCreation( 'creation-1', @@ -713,10 +711,7 @@ describe('staged background worktree creation', () => { }) }) - it('carries launchDraftText into activation for an argv-prefill launch', async () => { - // Why: the draft rides inside `launchCommand` here, so the plan sets no - // draftPrompt — without launchDraftText the initial view-mode decision - // never sees a draft and opens chat on an unmirrorable one. + it('keeps an argv-prefill draft inside the terminal command', async () => { store.activeView = 'terminal' store.activePendingCreationId = 'creation-1' store.createWorktree.mockResolvedValueOnce({ @@ -742,7 +737,7 @@ describe('staged background worktree creation', () => { await vi.waitFor(() => expect(activateAndRevealWorktree).toHaveBeenCalled()) const startup = vi.mocked(activateAndRevealWorktree).mock.calls[0]?.[1]?.startup expect(startup?.draftPrompt).toBeUndefined() - expect(startup?.launchDraftText).toBe('https://github.com/o/r/issues/12') + expect(startup?.command).toContain('https://github.com/o/r/issues/12') }) it('does not seed a launch draft without draft launch context', async () => { diff --git a/src/renderer/src/lib/worktree-default-terminal-tabs.ts b/src/renderer/src/lib/worktree-default-terminal-tabs.ts index 5a9d48723f0..7104318ff01 100644 --- a/src/renderer/src/lib/worktree-default-terminal-tabs.ts +++ b/src/renderer/src/lib/worktree-default-terminal-tabs.ts @@ -3,19 +3,12 @@ import type { WorktreeSetupLaunch } from '../../../shared/worktree/launch-types' import { agentKindToTuiAgent } from '../../../shared/agent-kind' -import { initialAgentTabViewModeProps } from './native-chat-initial-view-mode' -import { getConnectionId } from '@/lib/connection-context' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache' import type { InitialTerminalOptions, WorktreeActivationStore } from '@/lib/worktree-activation-store-contract' -import { - draftViewModeProps, - resolveStartupLaunchDraftText, - type WorktreeStartupPayload -} from '@/lib/worktree-startup-payload' +import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload' import { queueSetupAndIssueCommands, type IssueCommandLaunch @@ -51,20 +44,7 @@ export function applyDefaultTerminalTabs( const tab = store.createTab(worktreeId, undefined, undefined, { pendingActivationSpawn: true, recordInteraction: false, - ...(launchAgent - ? { - launchAgent, - ...initialAgentTabViewModeProps(store.settings ?? null, { - agent: launchAgent, - ...draftViewModeProps( - isStartupTab ? resolveStartupLaunchDraftText(startup) : undefined - ), - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - getConnectionId(worktreeId) - ) - }) - } - : {}), + ...(launchAgent ? { launchAgent } : {}), ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) }) if (index === 0) { diff --git a/src/renderer/src/lib/worktree-draft-startup-view-mode.test.ts b/src/renderer/src/lib/worktree-draft-startup-view-mode.test.ts index 71d895c80af..fcccaff9496 100644 --- a/src/renderer/src/lib/worktree-draft-startup-view-mode.test.ts +++ b/src/renderer/src/lib/worktree-draft-startup-view-mode.test.ts @@ -30,8 +30,7 @@ beforeEach(() => { useAppStore.setState({ settings: { ...initialSettings, - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true + experimentalNativeChat: true } }) }) @@ -41,13 +40,9 @@ afterEach(() => { }) describe('resolveBackendDraftStartup', () => { - // Why: omp discloses no hook transcript path, so it joins Grok in requiring a - // locally readable sessions root. This call site must SUPPLY that flag for omp - // too — gating on Grok alone left it undefined and parked every omp draft in - // the terminal view, local workspace or not. - it('opens a local omp draft in chat', () => { + it('keeps a local omp terminal startup in terminal view', () => { setRepoConnection(null) - expect(viewModeFor('omp')).toBe('chat') + expect(viewModeFor('omp')).toBe('terminal') }) it('keeps a Model-A SSH omp draft in the terminal view', () => { @@ -55,8 +50,8 @@ describe('resolveBackendDraftStartup', () => { expect(viewModeFor('omp')).toBe('terminal') }) - it('opens a runtime-owned SSH omp draft in chat, which reads the transcript locally', () => { + it('keeps a runtime-owned SSH omp terminal startup in terminal view', () => { setRepoConnection('runtime-ssh-env-1') - expect(viewModeFor('omp')).toBe('chat') + expect(viewModeFor('omp')).toBe('terminal') }) }) diff --git a/src/renderer/src/lib/worktree-draft-startup-view-mode.ts b/src/renderer/src/lib/worktree-draft-startup-view-mode.ts index 0c11ea02c80..a04d0f66858 100644 --- a/src/renderer/src/lib/worktree-draft-startup-view-mode.ts +++ b/src/renderer/src/lib/worktree-draft-startup-view-mode.ts @@ -1,7 +1,3 @@ -import { useAppStore } from '@/store' -import { decideInitialAgentTabViewMode } from '@/lib/native-chat-initial-view-mode' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' -import { nativeChatRequiresLocalTranscript } from '@/lib/native-chat-supported-agent' import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' export function resolveBackendDraftStartup( @@ -10,21 +6,5 @@ export function resolveBackendDraftStartup( if (!request.startup || !request.agent || !request.launchDraftPrompt) { return request.startup } - const state = useAppStore.getState() - const repo = state.repos.find((entry) => entry.id === request.repoId) - const connectionId = repo ? (repo.connectionId ?? null) : undefined - const viewMode = - decideInitialAgentTabViewMode({ - experimentalNativeChat: state.settings?.experimentalNativeChat, - openAgentTabsInChatByDefault: state.settings?.openAgentTabsInChatByDefault, - agent: request.agent, - promptDelivery: 'draft', - launchDraftText: request.launchDraftPrompt, - ...(nativeChatRequiresLocalTranscript(request.agent) - ? { - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable(connectionId) - } - : {}) - }) ?? 'terminal' - return { ...request.startup, viewMode } + return { ...request.startup, viewMode: 'terminal' } } diff --git a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts index 3bb5a28c420..026fbb58f5c 100644 --- a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts +++ b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts @@ -11,19 +11,12 @@ import { agentKindToTuiAgent } from '../../../shared/agent-kind' import { useAppStore } from '@/store' import { queueHookCommandsForFirstWorktreeTab } from '@/lib/hook-command-delayed-delivery' import { resolveWorkspaceTerminalHostAuthority } from '@/lib/workspace-terminal-host-authority' -import { initialAgentTabViewModeProps } from './native-chat-initial-view-mode' -import { getConnectionId } from '@/lib/connection-context' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache' import type { InitialTerminalOptions, WorktreeActivationStore } from '@/lib/worktree-activation-store-contract' -import { - draftViewModeProps, - resolveStartupLaunchDraftText, - type WorktreeStartupPayload -} from '@/lib/worktree-startup-payload' +import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload' import { queueSetupAndIssueCommands, type IssueCommandLaunch @@ -259,20 +252,7 @@ export function ensureWorktreeHasInitialTerminal( : undefined) const terminalTab = store.createTab(worktreeId, undefined, undefined, { pendingActivationSpawn: true, - ...(launchAgent - ? { - launchAgent, - ...initialAgentTabViewModeProps(store.settings ?? null, { - agent: launchAgent, - // Why: argv-prefill launches carry the draft in `command` and set no - // draftPrompt, so gating on draftPrompt alone misses them entirely. - ...draftViewModeProps(resolveStartupLaunchDraftText(sequencedStartup)), - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - getConnectionId(worktreeId) - ) - }) - } - : {}), + ...(launchAgent ? { launchAgent } : {}), ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) }) if (opts?.activateCreatedTabs !== false) { diff --git a/src/renderer/src/lib/worktree-startup-payload.ts b/src/renderer/src/lib/worktree-startup-payload.ts index afb29b980dd..77084b0a91f 100644 --- a/src/renderer/src/lib/worktree-startup-payload.ts +++ b/src/renderer/src/lib/worktree-startup-payload.ts @@ -20,37 +20,8 @@ export type WorktreeStartupPayload = { launchToken?: string launchAgent?: TuiAgent draftPrompt?: string - /** - * The unsent launch context, for the initial view-mode decision ONLY. - * - * Deliberately separate from `draftPrompt`, which drives the bracketed paste - * in pty-connection: an argv-prefill launch already carries the draft inside - * `command`, so reusing `draftPrompt` here would paste it a second time. - * Set this on every draft launch; set `draftPrompt` only for paste delivery. - */ - launchDraftText?: string startupCommandDelivery?: StartupCommandDelivery initialAgentStatus?: { agent: TuiAgent; prompt: string } sessionOptions?: Record telemetry?: AgentStartedTelemetry } - -/** - * The unsent launch context a startup payload carries, whichever way the agent - * receives it: argv prefill sets only `launchDraftText`, post-ready paste sets - * `draftPrompt`. Gating on `draftPrompt` alone silently misses every - * argv-prefill launch. - */ -export function resolveStartupLaunchDraftText( - startup: Pick | undefined -): string | undefined { - return startup?.draftPrompt ?? startup?.launchDraftText -} - -/** Shared by both tab-creation sites so the draft gate can't drift between them. */ -export function draftViewModeProps(draftText: string | undefined): { - promptDelivery?: 'draft' - launchDraftText?: string -} { - return draftText == null ? {} : { promptDelivery: 'draft', launchDraftText: draftText } -} diff --git a/src/renderer/src/runtime/host-structured-agents-sync.ts b/src/renderer/src/runtime/host-structured-agents-sync.ts index 79db61d7e5a..c30394980d6 100644 --- a/src/renderer/src/runtime/host-structured-agents-sync.ts +++ b/src/renderer/src/runtime/host-structured-agents-sync.ts @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { LOCAL_EXECUTION_HOST_ID, toRuntimeExecutionHostId } from '../../../shared/execution-host' +import { isNativeChatEnabled } from '../../../shared/structured-native-chat-launch-route' import { lastVerifiedRuntimeStatus } from '../../../shared/runtime-host-status' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' @@ -17,7 +18,7 @@ import { localStructuredChatsInUse } from './local-structured-chats' // Why gated: a host older than this build installs its structured host to answer, which a profile // that never uses structured chat must not pay for; only with chats in use can a launch route to one. function structuredChatSettingOn(state: Pick): boolean { - return state.settings?.experimentalStructuredNativeChat === true + return isNativeChatEnabled(state.settings) } async function syncLocalHost(settings: AppState['settings']): Promise { diff --git a/src/renderer/src/runtime/host-structured-agents.test.ts b/src/renderer/src/runtime/host-structured-agents.test.ts index 63ec2c2dd88..4ef1092f2c3 100644 --- a/src/renderer/src/runtime/host-structured-agents.test.ts +++ b/src/renderer/src/runtime/host-structured-agents.test.ts @@ -10,10 +10,10 @@ import type { RuntimeCapability } from '../../../shared/protocol-version' type Statuses = Map type StoreState = { runtimeStatusByEnvironmentId: Statuses - settings: { experimentalStructuredNativeChat: boolean } | null + settings: { experimentalNativeChat: boolean } | null } -const ON = { experimentalStructuredNativeChat: true } +const ON = { experimentalNativeChat: true } const mocks = vi.hoisted(() => { const state: StoreState = { runtimeStatusByEnvironmentId: new Map(), settings: null } @@ -36,7 +36,7 @@ vi.mock('./local-runtime-capabilities', () => ({ })) vi.mock('./local-structured-chats', () => ({ localStructuredChatsInUse: async (settings: StoreState['settings']) => - settings?.experimentalStructuredNativeChat === true + settings?.experimentalNativeChat === true })) vi.mock('@/store', () => ({ useAppStore: { @@ -168,7 +168,7 @@ describe('host structured agents', () => { // Asking installs a host's structured store; a profile with structured chat off never pays it. it('asks no host until structured chat is turned on', async () => { - mocks.state = { ...mocks.state, settings: { experimentalStructuredNativeChat: false } } + mocks.state = { ...mocks.state, settings: { experimentalNativeChat: false } } uninstall = installHostStructuredAgentsSync() setStatuses(new Map([['env-1', pairedStatus('rt-1', REGISTERED)]])) await Promise.resolve() diff --git a/src/renderer/src/runtime/local-structured-chats.test.ts b/src/renderer/src/runtime/local-structured-chats.test.ts index 24a2a481c45..35212d551e7 100644 --- a/src/renderer/src/runtime/local-structured-chats.test.ts +++ b/src/renderer/src/runtime/local-structured-chats.test.ts @@ -13,8 +13,8 @@ import { restoreLocalStructuredChatsAtStartup } from './local-structured-chats' -const SETTING_OFF = { experimentalStructuredNativeChat: false } -const SETTING_ON = { experimentalStructuredNativeChat: true } +const SETTING_OFF = { experimentalNativeChat: false } +const SETTING_ON = { experimentalNativeChat: true } beforeEach(() => { mocks.held = false diff --git a/src/renderer/src/runtime/local-structured-chats.ts b/src/renderer/src/runtime/local-structured-chats.ts index dad01c24361..54aec040aeb 100644 --- a/src/renderer/src/runtime/local-structured-chats.ts +++ b/src/renderer/src/runtime/local-structured-chats.ts @@ -1,5 +1,6 @@ import { useSyncExternalStore } from 'react' import type { GlobalSettings } from '../../../shared/global-settings-types' +import { isNativeChatEnabled } from '../../../shared/structured-native-chat-launch-route' import { isWebClientLocation } from '@/lib/web-client-location' import { useAppStore } from '@/store' import { restoreLocalStructuredSessionTabsOnce } from './local-structured-session-tabs-sync/inventory-refresh' @@ -70,17 +71,17 @@ export function useLocalStructuredAgentSessionsHeld(): boolean { ) } -type StructuredChatSettings = Pick | null +type StructuredChatSettings = Pick | null function chatsInUse(settings: StructuredChatSettings | undefined, holds: boolean): boolean { - return !isWebClientLocation() && (settings?.experimentalStructuredNativeChat === true || holds) + return !isWebClientLocation() && (isNativeChatEnabled(settings) || holds) } /** Structured chats can exist on this machine: the setting launches them, or the runtime holds some. */ export function useLocalStructuredChatsInUse(): boolean { const holds = useLocalStructuredAgentSessionsHeld() - const setting = useAppStore((state) => state.settings?.experimentalStructuredNativeChat === true) - return chatsInUse({ experimentalStructuredNativeChat: setting }, holds) + const setting = useAppStore((state) => isNativeChatEnabled(state.settings)) + return chatsInUse({ experimentalNativeChat: setting }, holds) } /** The same answer for one-shot startup work, asked of the host rather than read from the renderer. */ diff --git a/src/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.ts b/src/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.ts index 454012c23d6..dca9dabc1af 100644 --- a/src/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.ts +++ b/src/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.ts @@ -21,7 +21,7 @@ import { resetLocalStructuredChatsForTests } from './local-structured-chats' function setStructuredChat(enabled: boolean): void { useAppStore.setState({ - settings: { ...useAppStore.getState().settings!, experimentalStructuredNativeChat: enabled } + settings: { ...useAppStore.getState().settings!, experimentalNativeChat: enabled } }) } diff --git a/src/renderer/src/store/repos/repo-add-actions.ts b/src/renderer/src/store/repos/repo-add-actions.ts index a4afef1890c..95d81bcb86e 100644 --- a/src/renderer/src/store/repos/repo-add-actions.ts +++ b/src/renderer/src/store/repos/repo-add-actions.ts @@ -5,7 +5,6 @@ import type { Repo } from '../../../../shared/repo-types' import { isGitRepoKind } from '../../../../shared/repo-kind' import { getRepoHostIdentity } from '../slices/repo-host-identity' import { callRuntimeRpc, getActiveRuntimeTarget } from '../../runtime/runtime-rpc-client' -import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' import { markOnboardingProjectAdded } from '@/lib/onboarding-project-checklist' import { translate } from '@/i18n/i18n' import { @@ -194,10 +193,7 @@ export function createRepoAddActions( store: get(), onboarding, hasExistingProject: hadProjectBeforeAdd, - executionHostId: executionHostId ?? LOCAL_EXECUTION_HOST_ID, - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - repo.connectionId - ) + executionHostId: executionHostId ?? LOCAL_EXECUTION_HOST_ID }) await revealOnboardingFolderWithAgentLaunch({ worktreeId: folderWorktree.id, diff --git a/src/shared/default-global-settings.ts b/src/shared/default-global-settings.ts index be9ee19ab21..39e42cfdc76 100644 --- a/src/shared/default-global-settings.ts +++ b/src/shared/default-global-settings.ts @@ -132,9 +132,7 @@ export function buildDefaultSettings(args: { terminalLinkActionPopoverEnabled: true, terminalLinkClickBehavior: 'actions', terminalUrlMiddleClickBehavior: 'open', - openAgentTabsInChatByDefault: false, experimentalNativeChat: false, - experimentalStructuredNativeChat: false, nativeChatResumeWorkOnRestart: false, nativeChatQueueFollowUps: true, nativeChatInlineVisuals: true, diff --git a/src/shared/global-settings-types.ts b/src/shared/global-settings-types.ts index 433e1f928ec..a456af45b83 100644 --- a/src/shared/global-settings-types.ts +++ b/src/shared/global-settings-types.ts @@ -216,12 +216,8 @@ export type GlobalSettings = NativeChatGlobalSettings & { terminalLinkClickBehavior?: 'actions' | 'open' | 'none' /** Middle mouse URL behavior; defaults to opening the primary routed destination. */ terminalUrlMiddleClickBehavior?: 'open' | 'actions' | 'none' - /** Opt-in: open new coding-agent tabs in native chat instead of the raw terminal; optional for legacy settings. */ - openAgentTabsInChatByDefault?: boolean - /** Experimental native chat surface for Claude/Codex sessions; off by default. */ + /** New supported agent launches use structured Chat UI; off defaults to terminal UI. */ experimentalNativeChat?: boolean - /** Opt-in updated structured runtime; off keeps the existing PTY-backed native chat path. */ - experimentalStructuredNativeChat?: boolean /** Opt-in: resume working structured chats automatically on the next launch. Off still offers * the list, so the user sees exactly what would run before anything spends tokens. */ nativeChatResumeWorkOnRestart?: boolean diff --git a/src/shared/native-chat-initial-view-mode.ts b/src/shared/native-chat-initial-view-mode.ts deleted file mode 100644 index 42cdb4e44b8..00000000000 --- a/src/shared/native-chat-initial-view-mode.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { GlobalSettings } from './global-settings-types' -import { agentTabsDefaultToNativeChat } from './structured-native-chat-launch-route' -import type { Tab } from './tab-types' -import type { TuiAgent } from './tui-agent' -import { canMirrorLaunchDraftToNativeChat } from './native-chat-launch-draft-mirrorability' -import { - isNativeChatSupportedAgent, - nativeChatRequiresLocalTranscript -} from './native-chat-agent-support' - -export type NativeChatLaunchPromptDelivery = 'auto-submit' | 'draft' | 'submit-after-ready' - -/** - * Decide the initial `viewMode` for a newly launched agent tab from the - * opt-in `openAgentTabsInChatByDefault` setting. - * - * Returns `'chat'` only when the setting is explicitly on and the launched - * agent has a native-chat renderer. A draft launch opens in chat only when its - * unsent context can be mirrored into the composer — gated on the same - * predicate as seeding so the view never opens empty beside a filled TUI input. - */ -export function decideInitialAgentTabViewMode(args: { - experimentalNativeChat?: boolean - openAgentTabsInChatByDefault?: boolean - agent?: TuiAgent | null - promptDelivery?: NativeChatLaunchPromptDelivery - /** The unsent launch context, when `promptDelivery` is `'draft'`. */ - launchDraftText?: string - nativeChatTranscriptIsLocalReadable?: boolean -}): Tab['viewMode'] { - if (!agentTabsDefaultToNativeChat(args)) { - return undefined - } - if (!isNativeChatSupportedAgent(args.agent)) { - return undefined - } - if ( - nativeChatRequiresLocalTranscript(args.agent) && - args.nativeChatTranscriptIsLocalReadable !== true - ) { - return undefined - } - if ( - args.promptDelivery === 'draft' && - !canMirrorLaunchDraftToNativeChat(args.launchDraftText ?? '') - ) { - return undefined - } - return 'chat' -} - -export function initialAgentTabViewModeProps( - settings: - | Pick - | null - | undefined, - options: { - agent?: TuiAgent | null - promptDelivery?: NativeChatLaunchPromptDelivery - launchDraftText?: string - nativeChatTranscriptIsLocalReadable?: boolean - } = {} -): { viewMode?: Tab['viewMode'] } { - const viewMode = decideInitialAgentTabViewMode({ - experimentalNativeChat: settings?.experimentalNativeChat, - openAgentTabsInChatByDefault: settings?.openAgentTabsInChatByDefault, - agent: options.agent, - promptDelivery: options.promptDelivery, - launchDraftText: options.launchDraftText, - nativeChatTranscriptIsLocalReadable: options.nativeChatTranscriptIsLocalReadable - }) - return viewMode ? { viewMode } : {} -} diff --git a/src/shared/native-chat-launch-draft-mirrorability.ts b/src/shared/native-chat-launch-draft-mirrorability.ts index a66584cd846..d27d51f5ff2 100644 --- a/src/shared/native-chat-launch-draft-mirrorability.ts +++ b/src/shared/native-chat-launch-draft-mirrorability.ts @@ -4,10 +4,8 @@ import { AGENT_TUI_CLEAR_MAX_LINES, countAgentTuiInputLines } from './agent-tui- * Single source of truth for whether unsent launch context can be mirrored from * the agent's TUI input into the native-chat composer. * - * Both the seeding path (`seedNativeChatLaunchDraftForAgentTab`) and the - * initial-view-mode decision (`decideInitialAgentTabViewMode`) gate on this one - * predicate, so a draft launch can never open in chat with a composer that - * chat then refuses to fill. + * Explicit terminal chat view can mirror a launch draft only when the TUI input + * can be cleared within the same bound. * * CR/LF drafts are safe within the bounded TUI-clear budget. Unicode line * separators and drafts beyond that budget remain terminal-only. diff --git a/src/shared/structured-native-chat-launch-route.test.ts b/src/shared/structured-native-chat-launch-route.test.ts index 2f7d01017f1..c0c00e39c15 100644 --- a/src/shared/structured-native-chat-launch-route.test.ts +++ b/src/shared/structured-native-chat-launch-route.test.ts @@ -9,17 +9,12 @@ import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from './protocol-version' import { - agentTabsDefaultToNativeChat, - prefersStructuredNativeChatByDefault, + isNativeChatEnabled, resolveStructuredNativeChatSupport, type StructuredNativeChatSupportInput } from './structured-native-chat-launch-route' -const ON = { - experimentalNativeChat: true, - openAgentTabsInChatByDefault: true, - experimentalStructuredNativeChat: true -} +const ON = { experimentalNativeChat: true } function support(overrides: Partial = {}) { return resolveStructuredNativeChatSupport({ @@ -31,26 +26,16 @@ function support(overrides: Partial = {}) { }) } -describe('the settings default', () => { - it('needs all three toggles for structured, and the first two for native chat', () => { - expect(prefersStructuredNativeChatByDefault(ON)).toBe(true) - expect(prefersStructuredNativeChatByDefault({ ...ON, experimentalNativeChat: false })).toBe( - false - ) - expect( - prefersStructuredNativeChatByDefault({ ...ON, openAgentTabsInChatByDefault: false }) - ).toBe(false) - expect( - prefersStructuredNativeChatByDefault({ ...ON, experimentalStructuredNativeChat: false }) - ).toBe(false) - expect(agentTabsDefaultToNativeChat({ ...ON, experimentalStructuredNativeChat: false })).toBe( - true - ) +describe('the Chat UI switch', () => { + it('selects structured chat from the one persisted setting', () => { + const oldSelectorOff = { experimentalNativeChat: true, openAgentTabsInChatByDefault: false } + expect(isNativeChatEnabled(ON)).toBe(true) + expect(isNativeChatEnabled({ experimentalNativeChat: false })).toBe(false) + expect(isNativeChatEnabled(oldSelectorOff)).toBe(true) }) it.each([null, undefined, {}])('reads %s as no preference', (settings) => { - expect(prefersStructuredNativeChatByDefault(settings)).toBe(false) - expect(agentTabsDefaultToNativeChat(settings)).toBe(false) + expect(isNativeChatEnabled(settings)).toBe(false) }) }) diff --git a/src/shared/structured-native-chat-launch-route.ts b/src/shared/structured-native-chat-launch-route.ts index f92bac17e84..284737c987c 100644 --- a/src/shared/structured-native-chat-launch-route.ts +++ b/src/shared/structured-native-chat-launch-route.ts @@ -20,10 +20,7 @@ import { import type { TuiAgent } from './tui-agent' import type { WorkspaceLaunchKind } from './workspace-launch-kind' -export type NativeChatDefaultSettings = Pick< - GlobalSettings, - 'experimentalNativeChat' | 'experimentalStructuredNativeChat' | 'openAgentTabsInChatByDefault' -> +export type NativeChatDefaultSettings = Pick /** Why a launch that the user's default asked to be structured cannot be. */ export type StructuredNativeChatBlocker = @@ -64,22 +61,11 @@ export type StructuredNativeChatSupportInput = { hostStructuredAgents?: readonly string[] } -/** The user's default for a new agent tab: native chat rather than the raw TUI. */ -export function agentTabsDefaultToNativeChat( +/** The single Chat UI switch selects structured chat for supported new launches. */ +export function isNativeChatEnabled( settings: Partial | null | undefined ): boolean { - return ( - settings?.experimentalNativeChat === true && settings?.openAgentTabsInChatByDefault === true - ) -} - -/** ...and specifically a structured native chat session rather than a terminal rendered as chat. */ -export function prefersStructuredNativeChatByDefault( - settings: Partial | null | undefined -): boolean { - return ( - agentTabsDefaultToNativeChat(settings) && settings?.experimentalStructuredNativeChat === true - ) + return settings?.experimentalNativeChat === true } function clientChoosesStructuredLaunches(capabilities: readonly string[] | undefined): boolean { diff --git a/src/shared/telemetry-events.test.ts b/src/shared/telemetry-events.test.ts index 13dca1c08d1..053f79dcb27 100644 --- a/src/shared/telemetry-events.test.ts +++ b/src/shared/telemetry-events.test.ts @@ -537,15 +537,25 @@ describe('workspace_create_failed schema', () => { }) describe('settings_changed schema', () => { - it('accepts structured native chat as a boolean adoption signal', () => { + it('accepts the Chat UI switch as a boolean adoption signal', () => { expect( eventSchemas.settings_changed.safeParse({ - setting_key: 'experimentalStructuredNativeChat', + setting_key: 'experimentalNativeChat', value_kind: 'bool' }).success ).toBe(true) }) + it.each(['experimentalStructuredNativeChat', 'openAgentTabsInChatByDefault'])( + 'rejects the retired %s key', + (settingKey) => { + expect( + eventSchemas.settings_changed.safeParse({ setting_key: settingKey, value_kind: 'bool' }) + .success + ).toBe(false) + } + ) + it('rejects non-whitelisted setting keys', () => { const parsed = eventSchemas.settings_changed.safeParse({ setting_key: 'telemetryOptIn', // deliberately excluded from the whitelist diff --git a/src/shared/telemetry-property-schemas.ts b/src/shared/telemetry-property-schemas.ts index 1b91ead78c3..754c141b214 100644 --- a/src/shared/telemetry-property-schemas.ts +++ b/src/shared/telemetry-property-schemas.ts @@ -204,14 +204,12 @@ export const SETTINGS_CHANGED_WHITELIST = [ 'experimentalMobile', 'experimentalPet', 'experimentalNativeChat', - 'experimentalStructuredNativeChat', 'experimentalActivity', 'experimentalAgentDashboardPopout', 'experimentalTerminalAttention', 'experimentalAgentHibernation', 'experimentalEphemeralVms', - 'geminiCliOAuthEnabled', - 'openAgentTabsInChatByDefault' + 'geminiCliOAuthEnabled' ] as const satisfies readonly BooleanGlobalSettingsKey[] export const settingsChangedKeySchema = z.enum(SETTINGS_CHANGED_WHITELIST) export type SettingsChangedKey = z.infer diff --git a/tests/e2e/cross-version-wire/cross-version-paired-structured-launch.unit.test.ts b/tests/e2e/cross-version-wire/cross-version-paired-structured-launch.unit.test.ts index 545d79c80de..4bc014a9886 100644 --- a/tests/e2e/cross-version-wire/cross-version-paired-structured-launch.unit.test.ts +++ b/tests/e2e/cross-version-wire/cross-version-paired-structured-launch.unit.test.ts @@ -13,11 +13,7 @@ import { importReleaseCheckoutModule, materializeReleaseCheckout } from './relea // string would follow along in unit tests but silently change what this desktop does with that server. // This release advertises structured chat but predates client-chosen launch modes. const LEGACY_PAIRED_STRUCTURED_LAUNCH_RELEASE_REF = 'v1.4.219' -const SETTINGS = { - experimentalNativeChat: true, - experimentalStructuredNativeChat: true, - openAgentTabsInChatByDefault: true -} +const SETTINGS = { experimentalNativeChat: true } let legacyHostCapabilities: readonly string[] @@ -56,7 +52,7 @@ describe(`a current desktop paired with a ${LEGACY_PAIRED_STRUCTURED_LAUNCH_RELE supported: false, blocker: 'runtime-capability' }) - expect(resolveAgentLaunchRoute({ ...legacy, settings: SETTINGS })).toBe('legacy-native-chat') + expect(resolveAgentLaunchRoute({ ...legacy, settings: SETTINGS })).toBe('terminal-tui') // The same handshake against today's server: the refusal above is the old server's doing. expect( resolveAgentLaunchRoute({ diff --git a/tests/e2e/structured-native-chat-routing-authority.unit.test.ts b/tests/e2e/structured-native-chat-routing-authority.unit.test.ts index dd2211d08cd..304ff3492bf 100644 --- a/tests/e2e/structured-native-chat-routing-authority.unit.test.ts +++ b/tests/e2e/structured-native-chat-routing-authority.unit.test.ts @@ -118,8 +118,7 @@ describe('shared feasibility owns every caller decision', () => { promptDelivery, hostCapabilities: RUNTIME_CAPABILITIES, startsOutsideWorkspaceRoot: true, - workspaceKind: 'folder', - initialSessionOptions: { model: 'model-1', effort: 'high' } + workspaceKind: 'folder' } predicate.mockReturnValue({ supported: true }) expect(resolveAgentLaunchRoute(input)).toBe('structured-native-chat')