Default experimental native chat to structured chat (#22933)

* refactor(native-chat): make structured chat the only Chat UI runtime

Retire the experimentalStructuredNativeChat opt-in. Chat UI on is now the
one gate for structured chat: new agent tabs that default to Chat UI start a
structured session wherever the launch is supported, and unsupported
launches keep falling back to terminal-backed chat.

- Shared isNativeChatEnabled predicate replaces prefersStructuredNativeChatByDefault;
  host admission, renderer routing, orchestration mode, startup projection,
  tab sync and the resume offer all read it.
- The retired key is stripped on load and on save, dropped from the settings
  type, defaults, the published client-settings projection and telemetry.
- Settings no longer offer the structured toggle; resume-after-restart and
  shell-environment rows show under the Chat UI default view.
- Startup structured-tab projection moves beside its sync hook so both gates
  read the same predicate.

* docs(native-chat): describe structured chat as the Chat UI runtime

New local Claude and Codex chats no longer sit on a PTY, so the page stops saying the terminal is always the source of truth and scopes the chat/terminal toggle to terminal-backed chats.

* fix(native-chat): stop promising a Windows fallback in the Chat UI scope note

A host refusal after the route picks structured chat still ends as a failed launch.

* fix(native-chat): keep open structured chats reachable when Chat UI is off

Turning Chat UI off hid open Claude/Codex chat tabs while their provider
processes kept running, because the host and the desktop mirror gated
serving existing sessions on the same setting that decides how new
launches open.

The host now asks two separate questions. Creating a new structured
session (agentSession.create, createSupport, ensure) needs the negotiated
capability plus the Chat UI setting. Serving a session that already
exists (listing, reading, sending, resuming, closing, and the mobile
projection and restore) needs the capability only. The desktop mirror,
startup restore and resume-on-restart offer no longer read the setting,
and the toggle-off teardown and the mobile re-publish on setting change
are removed as dead.

* fix(native-chat): run the local structured tab sync only where chats may exist

The previous commit started the every-worktree session-tab census and
subscription, the startup restore, and the resume-offer host read for every
desktop user. Gate them on a derived fact instead: Chat UI is on, or the
local host holds at least one structured session record. With Chat UI off
nothing can create a session, so the answer can only go from yes to no.

The host answers from its installed record store, or from whether the store
file exists when the host is not installed, so a machine that never had a
structured chat pays one existence check and never starts the host. Chat UI
turning on starts the sync; turning it off never stops it.

* fix(native-chat): answer an empty restart offer without starting the chat host

With every Chat UI user now on structured chat, the launch-time resume offer read built the host even on machines with no stored chat, which on Linux scans every process environment. No store on disk means no offer, so the listing answers empty without installing the host.

* fix(native-chat): release the structured tab sync when it never subscribed

A failed capability probe reads as unsupported, so the sync returned without subscribing while still holding the one-sync slot. Nothing could start another, and chat tabs stopped live-updating until relaunch. The start now reports whether it subscribed, and a sync that did not releases the slot so the next Chat UI change retries.

* fix(native-chat): let the host answer the restart offer whatever Chat UI says

The renderer asked main whether a structured store existed before reading the restart offer, so it would not start the chat host for users without one. The host now answers an empty offer without starting itself, which made that probe an extra round trip. In paired web it was also wrong: the probe is hard-coded to no and the gate read the browser's own Chat UI setting, so a paired host's chats were never offered back once that was off.

* refactor(native-chat): drop the structured tab sync generation fence

The fence existed so a Chat UI toggle-off could retire in-flight inventory replies before it cleared the mirror. That teardown is gone: turning Chat UI off keeps open chats, and only a test reset still bumped the generation, so every production check compared equal. The sync's own disposal still fences its subscription, and the startup restore latch and publication cursors stay.

* fix(native-chat): retry the structured tab sync when the capability probe fails

A failed runtime status probe maps to an empty capability list, which the sync read as "unsupported" and gave up on. The release only let a later Chat UI change try again, so one transient startup failure left chat tabs without live updates for the whole session. The sync now tells an unknown answer apart from a host that answered without the surface, re-asks unknown answers on its existing subscribe backoff, and stops only on a definite no.

* refactor(native-chat): begin single-switch structured launch routing

* Make Chat UI the single structured launch default

* Remove retired chat settings copy from localization catalogs

* Keep locale checks aligned with the single Chat UI switch

* Keep explicit chats accessible and older client defaults terminal

* Align newly landed Settings tests with the Chat UI switch

* Expect terminal fallback on older paired structured hosts

* Correct terminal fallback launch expectation

* Update tests that still expected the retired chat settings

* Keep chat composer options off terminal launches and restore route coverage

Chat composer model and effort no longer become CLI flags on terminal
launches, since a new launch can no longer open in the terminal chat
view; structured launches seed their own options. Restores dropped
route coverage, including that an explicit resume-in-new-chat request
stays available while Chat UI is off.

* Replace the stale remembered-options test with terminal fallback coverage

The test expected chat composer model and effort on a codex launch with
Chat UI on, but its mocked host has no structured capability, so that
launch is the terminal fallback that must not carry them. It now checks
the fallback with Chat UI on and off, with shipped and stored arguments;
structured creates are covered where the seed is applied.

* Lower the terminal pane listener budget to the measured 15

The explicit-chat change retired the per-pane Chat UI setting read, so
each mounted pane opens one fewer store listener. Pin the budget to the
measured count and count that read among the saved listeners.
This commit is contained in:
Brennan Benson
2026-10-08 11:46:55 -07:00
committed by GitHub
parent 9ca9b4e36b
commit 22974c5809
161 changed files with 638 additions and 2115 deletions
@@ -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()
})
})
@@ -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: {
+2 -2
View File
@@ -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',
@@ -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<GlobalSettings, 'experimentalNativeChat' | 'openAgentTabsInChatByDefault'>
> | 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'
}
+1 -1
View File
@@ -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',
@@ -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<GlobalSettings> & RetiredGlobalSettings
void _legacyScrollbackBytes
@@ -82,6 +86,8 @@ export function stripRetiredGlobalSettings(
void _legacyShowAgentsSidebar
void _retiredManagedServersExperiment
void _legacyOpenCodeGoApiKey
void _legacyStructuredNativeChat
void _legacyChatDefaultView
return rest
}
@@ -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({
@@ -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),
@@ -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 = [
{
@@ -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' })
@@ -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<OrcaRuntimeService['getClientSettings']>
)
const scope = vi.spyOn(runtime, 'showTerminalWorkspaceLaunchScope')
@@ -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 () => {
@@ -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)
@@ -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 } : {}),
@@ -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 = {
@@ -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)
@@ -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({
@@ -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 {
@@ -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' }
)
@@ -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({
@@ -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 {
@@ -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' })
}
@@ -7,9 +7,7 @@ import {
const mode = decideWorkerStartMode({
params: { agent: 'claude' },
settings: {
experimentalNativeChat: true,
experimentalStructuredNativeChat: true,
openAgentTabsInChatByDefault: true
experimentalNativeChat: true
}
})
@@ -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
@@ -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
@@ -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,
@@ -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,
@@ -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<OrcaRuntimeService['getClientSettings']>)
vi.spyOn(runtime, 'getStructuredAgentSessionCreateSupport').mockResolvedValue({
supported: true
@@ -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<OrcaRuntimeService['getClientSettings']>
)
dispatcher = new RpcDispatcher({ runtime, methods: STRUCTURED_AGENT_SESSION_METHODS })
})
@@ -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
@@ -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<OrcaRuntimeService, 'getClientSettings'>
): boolean {
try {
return runtime.getClientSettings().experimentalStructuredNativeChat === true
return isNativeChatEnabled(runtime.getClientSettings())
} catch {
return false
}
@@ -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(),
@@ -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(),
@@ -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)
@@ -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<OrcaRuntimeService['getClientSettings']>
)
dispatcher = new RpcDispatcher({
runtime,
+6 -6
View File
@@ -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',
@@ -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']
@@ -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' })
})
@@ -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 {
@@ -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)
@@ -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
}
})
@@ -54,7 +54,6 @@ function settings(autoResume: boolean | undefined): void {
useAppStore.setState({
settings: {
...getDefaultSettings(''),
experimentalStructuredNativeChat: true,
...(autoResume === undefined ? {} : { nativeChatResumeWorkOnRestart: autoResume })
}
})
@@ -211,7 +211,7 @@ export function createResumeModalFixture(rpc: Mock<RestartRpc>, 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 }
@@ -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'
})
@@ -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
}
@@ -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()
})
})
@@ -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<string, SessionOptionValue> | undefined {
const viewMode = decideInitialAgentTabViewMode({
experimentalNativeChat: settings?.experimentalNativeChat,
openAgentTabsInChatByDefault: settings?.openAgentTabsInChatByDefault,
...args
})
return viewMode === 'chat'
? resolveNativeChatLaunchSessionOptions(settings?.nativeChatSessionOptions, args.agent)
: undefined
}
@@ -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,
@@ -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({
@@ -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
},
[]
)
@@ -42,9 +42,7 @@ function resumeInChat() {
activeWorkspaceId: PAIRED_WORKSPACE,
targetState: { folderWorkspaces: [], projectGroups: [], repos: [], worktreesByRepo: {} },
settings: {
experimentalNativeChat: true,
experimentalStructuredNativeChat: true,
openAgentTabsInChatByDefault: true
experimentalNativeChat: true
}
})
}
@@ -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
@@ -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',
@@ -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')
@@ -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)
@@ -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<string | null>('chat')
@@ -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<GlobalSettings>) => {
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(
<ActiveSettingsSectionProvider value="chat">
<ChatSettingsSection
settings={{ ...getDefaultSettings('/tmp'), experimentalStructuredNativeChat: false }}
settings={{ ...getDefaultSettings('/tmp'), experimentalNativeChat: false }}
updateSettings={vi.fn()}
writeSourceControlAiSettings={async () => {}}
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(
@@ -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')
@@ -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(
<ExperimentalPane settings={disabledSettings} updateSettings={vi.fn()} />
)
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<HTMLButtonElement>(
'#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<HTMLButtonElement>('[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<HTMLButtonElement>(
'#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(
<ExperimentalPane settings={disabledSettings} updateSettings={vi.fn()} />
)
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<HTMLButtonElement>('[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<HTMLButtonElement>('[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<HTMLButtonElement>(
'#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<HTMLButtonElement>('[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 = {
@@ -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()
@@ -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<GlobalSettings>) => 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 (
<SearchableSetting
title={translate('auto.components.settings.ExperimentalPane.nativeChat.title', 'Chat UI')}
description={translate(
'auto.components.settings.ExperimentalPane.nativeChat.description',
'Preview the desktop chat surface for supported agent terminal sessions.'
'Open supported new agents in Chat UI.'
)}
keywords={getExperimentalSearchEntry().nativeChat.keywords}
className="space-y-3 py-2"
@@ -51,7 +42,7 @@ export function NativeChatExperimentalSetting({
<p className="text-xs text-muted-foreground">
{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.'
)}
</p>
<NativeChatSupportedAgents />
@@ -69,97 +60,8 @@ export function NativeChatExperimentalSetting({
}
/>
</div>
{nativeChatEnabled ? (
{structuredChatActive ? (
<div className="ml-4 space-y-4 border-l border-border pl-4">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.nativeChat.defaultTitle',
'Default view'
)}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.ExperimentalPane.nativeChat.defaultCopy',
'Choose how new supported agent terminal tabs open.'
)}
</p>
</div>
<Select
value={defaultView}
onValueChange={(value: NativeChatDefaultView) => {
updateSettings({
openAgentTabsInChatByDefault: value === 'native-chat'
})
}}
>
<SelectTrigger
aria-label={translate(
'auto.components.settings.ExperimentalPane.nativeChat.defaultViewLabel',
'Default Chat UI view'
)}
className="w-36"
size="sm"
>
<SelectValue />
</SelectTrigger>
<SelectContent position="popper" side="bottom" sideOffset={4} avoidCollisions={false}>
<SelectItem value="terminal-chat">
{translate(
'auto.components.settings.ExperimentalPane.nativeChat.defaultViewTerminal',
'Terminal chat'
)}
</SelectItem>
<SelectItem value="native-chat">
{translate(
'auto.components.settings.ExperimentalPane.nativeChat.defaultViewNative',
'Chat UI'
)}
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 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' ? (
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>
{translate(
'auto.components.settings.ExperimentalPane.nativeChat.structuredTitle',
'Use updated structured native chat'
)}
</Label>
<p className="text-xs text-muted-foreground">
{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.'
)}
</p>
<p className="text-xs text-muted-foreground">
{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.'
)}
</p>
</div>
<SettingsSwitch
checked={structuredNativeChatEnabled}
ariaLabel={translate(
'auto.components.settings.ExperimentalPane.nativeChat.structuredToggleLabel',
'Toggle updated structured native chat'
)}
onChange={() =>
updateSettings({
experimentalStructuredNativeChat: !structuredNativeChatEnabled
})
}
/>
</div>
) : null}
{/* Only structured sessions have a resume cursor to continue from. */}
{structuredChatActive ? (
<div className="flex items-start justify-between gap-4">
@@ -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')
@@ -17,7 +17,7 @@ beforeEach(() => {
useAppStore.setState({
settings: {
...getDefaultSettings('/synthetic'),
experimentalStructuredNativeChat: true,
experimentalNativeChat: true,
sourceControlAi: normalizeSourceControlAiSettings({
...normalizeSourceControlAiSettings(undefined),
enabled: false,
@@ -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,
@@ -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<string, string> | undefined
agentArgs?: string | null
agentEnv?: Record<string, string>
sessionOptions?: Record<string, SessionOptionValue>
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,
@@ -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<Record<string, unknown>>()
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)
})
})
@@ -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<string, string> | undefined
agentArgs?: string | null
agentEnv?: Record<string, string>
sessionOptions?: Record<string, SessionOptionValue>
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 }
: {}),
@@ -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': [
{
@@ -161,7 +161,6 @@ function Strip({
resolvedGroupId: 'group-1',
generatedTabTitlesEnabled,
unifiedTabByVisibleId: new Map(),
nativeChatEnabled: false,
tabAgentTypesByTabId: {},
nativeChatTabWideFallbackUnsafeTabsById: {},
nativeChatTranscriptIsLocalReadable: false,
@@ -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,
@@ -27,12 +27,6 @@ const EMPTY_AGENT_STATUS_BY_PANE_KEY: Record<string, AgentStatusEntry> = Object.
const EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID: Record<string, TerminalLayoutSnapshot> = Object.freeze({})
const EMPTY_TAB_AGENT_TYPES_BY_TAB_ID: Record<string, AgentType> = Object.freeze({})
const EMPTY_UNSAFE_TABS_BY_ID: Record<string, true> = 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<T>(
previous: Record<string, T> | undefined,
next: Record<string, T>
@@ -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
}
@@ -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 (
@@ -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) =>
@@ -136,6 +136,7 @@ function useFixture(fixture: ReturnType<typeof makeFixture>, 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', () => {
@@ -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)
@@ -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,
@@ -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<void>
prepareQuickSubmit: (
resolution: PendingSmartGitHubSubmitResolution,
@@ -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,
@@ -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,
@@ -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<void> => {
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'
@@ -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,
@@ -143,8 +143,7 @@ export function useQuickSubmitAction(input: QuickSubmitActionInput) {
requestedAgent,
workspaceNameSeed,
workspaceRunContext,
repoId,
selectedRepo
repoId
)
} catch (error) {
if (isSubmissionCancelled()) {
@@ -118,7 +118,6 @@ export type TerminalCreateSurfacingStore = {
settings: {
terminalFontSize: number
experimentalNativeChat: boolean
openAgentTabsInChatByDefault: boolean
activeRuntimeEnvironmentId: string | undefined
}
}
@@ -104,7 +104,6 @@ export async function setupTerminalCreateSurfacing(
settings: {
terminalFontSize: 13,
experimentalNativeChat: false,
openAgentTabsInChatByDefault: false,
activeRuntimeEnvironmentId: undefined as string | undefined
}
}
@@ -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 } : {}),
@@ -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
@@ -7,7 +7,7 @@ export type SettingsNavigationBuildOptions = {
isLocalWindowsHost: boolean
isWindowsTerminalHost: boolean
isWebClient: boolean
experimentalStructuredNativeChat: boolean
nativeChatEnabled: boolean
managedBrowserCreationEnabled: boolean
mobileEmulatorCreationEnabled: boolean
isDev: boolean
@@ -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',
@@ -85,7 +85,6 @@ function storeStateWithRuntime(
settings: {
terminalFontSize: 13,
experimentalNativeChat: false,
openAgentTabsInChatByDefault: false,
activeRuntimeEnvironmentId: 'env-a'
}
})
@@ -75,8 +75,7 @@ describe('useIpcEvents updater integration', () => {
sshTargetLabels: new Map<string, string>([['conn-1', 'Remote']]),
settings: {
terminalFontSize: 13,
experimentalNativeChat: false,
openAgentTabsInChatByDefault: false
experimentalNativeChat: false
}
}
vi.doMock('react', async () => {
@@ -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()
@@ -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')
@@ -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,
+4 -1
View File
@@ -1704,7 +1704,10 @@
"d23ae13990": "worktree",
"f082788cfe": "links",
"fa72e71f05": "agents",
"fe5688b761": "sidebar"
"fe5688b761": "sidebar",
"nativeChat": {
"description": "Open supported new agents in Chat UI."
}
}
},
"general": {
+3 -12
View File
@@ -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",
-12
View File
@@ -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",
-12
View File
@@ -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",
-12
View File
@@ -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": "チャット",
-12
View File
@@ -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": "채팅",
-12
View File
@@ -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": "聊天",

Some files were not shown because too many files have changed in this diff Show More