diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx
index 21f50bede40c..db30fa65e18b 100644
--- a/src/renderer/src/components/settings/AgentsPane.tsx
+++ b/src/renderer/src/components/settings/AgentsPane.tsx
@@ -19,6 +19,7 @@ import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { cn } from '@/lib/utils'
import { AgentAwakeSetting } from './AgentAwakeSetting'
+import { CreatedAgentReopenSetting } from './CreatedAgentReopenSetting'
import { AgentCacheTimerSection } from './AgentCacheTimerSection'
import { AgentRuntimeSetting } from './AgentRuntimeSetting'
import {
@@ -869,6 +870,8 @@ export function AgentsPane({
+
+
diff --git a/src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx b/src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx
new file mode 100644
index 000000000000..5e85b9a1bf98
--- /dev/null
+++ b/src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx
@@ -0,0 +1,43 @@
+import type { GlobalSettings } from '../../../../shared/types'
+import {
+ getCreatedAgentReopenDescription,
+ getCreatedAgentReopenSearchKeywords,
+ getCreatedAgentReopenTitle
+} from './created-agent-reopen-copy'
+import { SearchableSetting } from './SearchableSetting'
+import { SettingsSwitchRow } from './SettingsFormControls'
+
+type CreatedAgentReopenSettingProps = {
+ settings: GlobalSettings
+ updateSettings: (updates: Partial) => void
+}
+
+export function CreatedAgentReopenSetting({
+ settings,
+ updateSettings
+}: CreatedAgentReopenSettingProps): React.JSX.Element {
+ const title = getCreatedAgentReopenTitle()
+ const description = getCreatedAgentReopenDescription()
+ const enabled = settings.reopenWorkspacesWithCreatedAgent !== false
+
+ return (
+
+
+
+ updateSettings({
+ reopenWorkspacesWithCreatedAgent: !enabled
+ })
+ }
+ />
+
+
+ )
+}
diff --git a/src/renderer/src/components/settings/agents-search.ts b/src/renderer/src/components/settings/agents-search.ts
index 5e378d732d14..a69e631eaaad 100644
--- a/src/renderer/src/components/settings/agents-search.ts
+++ b/src/renderer/src/components/settings/agents-search.ts
@@ -4,6 +4,11 @@ import {
getAgentAwakeSearchKeywords,
getAgentAwakeTitle
} from './agent-awake-copy'
+import {
+ getCreatedAgentReopenDescription,
+ getCreatedAgentReopenSearchKeywords,
+ getCreatedAgentReopenTitle
+} from './created-agent-reopen-copy'
import {
getAgentGeneratedTabTitlesDescription,
getAgentGeneratedTabTitlesSearchKeywords,
@@ -116,6 +121,11 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
description: getAgentAwakeDescription(),
keywords: getAgentAwakeSearchKeywords()
},
+ {
+ title: getCreatedAgentReopenTitle(),
+ description: getCreatedAgentReopenDescription(),
+ keywords: getCreatedAgentReopenSearchKeywords()
+ },
{
title: translate(
'auto.components.settings.agents.search.agentPermissions',
diff --git a/src/renderer/src/components/settings/created-agent-reopen-copy.ts b/src/renderer/src/components/settings/created-agent-reopen-copy.ts
new file mode 100644
index 000000000000..a29e4e5c8f2c
--- /dev/null
+++ b/src/renderer/src/components/settings/created-agent-reopen-copy.ts
@@ -0,0 +1,29 @@
+import { translate } from '@/i18n/i18n'
+import { searchKeywords } from './settings-search-keywords'
+
+export function getCreatedAgentReopenTitle(): string {
+ return translate(
+ 'auto.components.settings.created-agent-reopen-copy.title',
+ 'Reopen workspaces with the agent they were created with'
+ )
+}
+
+export function getCreatedAgentReopenDescription(): string {
+ return translate(
+ 'auto.components.settings.created-agent-reopen-copy.description',
+ 'When a workspace was created with an agent and has no open tabs, reopening it from the sidebar launches that agent again. Turn this off to open a blank terminal instead.'
+ )
+}
+
+export function getCreatedAgentReopenSearchKeywords(): string[] {
+ return searchKeywords([
+ { key: 'auto.components.settings.agents.search.96ba2373b6', fallback: 'agent' },
+ { key: 'auto.components.settings.agents.search.reopen', fallback: 'reopen' },
+ { key: 'auto.components.settings.agents.search.created', fallback: 'created' },
+ { key: 'auto.components.settings.agents.search.workspace', fallback: 'workspace' },
+ { key: 'auto.components.settings.agents.search.sidebar', fallback: 'sidebar' },
+ { key: 'auto.components.settings.agents.search.blank', fallback: 'blank' },
+ { key: 'auto.components.settings.agents.search.resume', fallback: 'resume' },
+ { key: 'auto.components.settings.agents.search.relaunch', fallback: 'relaunch' }
+ ])
+}
diff --git a/src/renderer/src/components/settings/setting-labels.ts b/src/renderer/src/components/settings/setting-labels.ts
index 95eb3a26c7c2..9bdb70b5ab3c 100644
--- a/src/renderer/src/components/settings/setting-labels.ts
+++ b/src/renderer/src/components/settings/setting-labels.ts
@@ -24,5 +24,6 @@ export const SETTING_LABELS: Partial> = {
terminalDividerColorDark: 'Divider Color (Dark)',
terminalDividerColorLight: 'Divider Color (Light)',
terminalInactivePaneOpacity: 'Inactive Pane Opacity',
- windowBackgroundBlur: 'Window Background Blur'
+ windowBackgroundBlur: 'Window Background Blur',
+ reopenWorkspacesWithCreatedAgent: 'Reopen workspaces with the agent they were created with'
}
diff --git a/src/renderer/src/lib/worktree-activation-created-agent.test.ts b/src/renderer/src/lib/worktree-activation-created-agent.test.ts
index cc2af6e7d7ce..67dcd7ae991e 100644
--- a/src/renderer/src/lib/worktree-activation-created-agent.test.ts
+++ b/src/renderer/src/lib/worktree-activation-created-agent.test.ts
@@ -71,7 +71,7 @@ describe('activateAndRevealWorktree', () => {
expect(recordWorktreeVisit).toHaveBeenCalledWith(worktree.id)
})
- it('does not relaunch the creation-time agent when reopening an empty worktree', () => {
+ it('reopens an empty worktree with the agent selected at creation time', () => {
const worktree = makeWorktree()
const { revealWorktreeInSidebar } = seedEmptyActivatableWorktree(worktree)
@@ -79,45 +79,110 @@ describe('activateAndRevealWorktree', () => {
const state = useAppStore.getState()
const reopenedTab = state.tabsByWorktree[worktree.id]?.[0]
- // A focusable surface still appears — it is just a plain shell, with no queued agent launch.
expect(result).toEqual({ primaryTabId: reopenedTab?.id })
expect(reopenedTab).toBeDefined()
- expect(state.pendingStartupByTabId[reopenedTab!.id]).toBeUndefined()
+ expect(state.pendingStartupByTabId[reopenedTab!.id]).toEqual({
+ command: "codex '--dangerously-bypass-approvals-and-sandbox'",
+ env: {},
+ launchAgent: 'codex',
+ launchConfig: {
+ agentCommand: "codex '--dangerously-bypass-approvals-and-sandbox'",
+ agentArgs: '--dangerously-bypass-approvals-and-sandbox',
+ agentEnv: {}
+ },
+ launchToken: expect.any(String),
+ sessionOptions: undefined,
+ telemetry: {
+ agent_kind: 'codex',
+ launch_source: 'sidebar',
+ request_kind: 'new'
+ }
+ })
expect(revealWorktreeInSidebar).toHaveBeenCalledWith(worktree.id)
})
- it('does not relaunch on repeated activate/close cycles', () => {
+ it('does not relaunch the creation agent when the reopen setting is off', () => {
const worktree = makeWorktree()
- seedEmptyActivatableWorktree(worktree)
+ const { revealWorktreeInSidebar } = seedEmptyActivatableWorktree(worktree)
+ useAppStore.setState({
+ settings: {
+ ...useAppStore.getState().settings,
+ reopenWorkspacesWithCreatedAgent: false
+ } as ReturnType['settings']
+ })
- for (let cycle = 0; cycle < 3; cycle += 1) {
- activateAndExpectNoRelaunch(worktree.id)
+ const result = activateAndRevealWorktree(worktree.id)
+ const state = useAppStore.getState()
+ const reopenedTab = state.tabsByWorktree[worktree.id]?.[0]
- // Return to zero tabs, the state that used to re-arm the relaunch. Sets state
- // directly rather than via closeTab — the sleeping-record purge is covered in
- // worktree-reactivation-tab-forkbomb.test.ts.
- useAppStore.setState({ tabsByWorktree: {}, activeTabIdByWorktree: {} })
- }
+ expect(result).toEqual({ primaryTabId: reopenedTab?.id })
+ expect(reopenedTab).toBeDefined()
+ // Blank shell — no agent startup payload from createdWithAgent.
+ expect(state.pendingStartupByTabId[reopenedTab!.id]).toBeUndefined()
+ expect(revealWorktreeInSidebar).toHaveBeenCalledWith(worktree.id)
})
- it('does not relaunch when activating a sibling worktree the user never opened', () => {
- const sibling = makeWorktree()
- const target = { ...makeWorktree(), id: 'wt-handoff', displayName: 'handoff' }
- seedEmptyActivatableWorktree(target, { extraWorktrees: [sibling] })
+ it('uses WSL launch quoting when reopening a Windows-path WSL project agent', () => {
+ const worktree = {
+ ...makeWorktree(),
+ path: 'C:\\Users\\jinwo\\repo\\feature'
+ }
- // The shape post-delete focus handoff produces. That caller passes no opts at all —
- // asserted directly in active-worktree-focus-after-delete.test.ts.
- activateAndExpectNoRelaunch(target.id)
- })
+ useAppStore.setState({
+ projects: [
+ {
+ id: 'repo-1',
+ displayName: 'repo',
+ badgeColor: '#000000',
+ sourceRepoIds: ['repo-1'],
+ createdAt: 0,
+ updatedAt: 0,
+ localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
+ }
+ ],
+ repos: [
+ {
+ id: 'repo-1',
+ path: 'C:\\Users\\jinwo\\repo',
+ displayName: 'repo',
+ badgeColor: '#000000',
+ addedAt: 0
+ }
+ ],
+ worktreesByRepo: { 'repo-1': [worktree] },
+ activeRepoId: 'repo-1',
+ activeView: 'terminal',
+ tabsByWorktree: {},
+ unifiedTabsByWorktree: {},
+ groupsByWorktree: {},
+ layoutByWorktree: {},
+ activeGroupIdByWorktree: {},
+ openFiles: [],
+ browserTabsByWorktree: {},
+ activeFileIdByWorktree: {},
+ activeBrowserTabIdByWorktree: {},
+ activeTabTypeByWorktree: {},
+ activeTabIdByWorktree: {},
+ tabBarOrderByWorktree: {},
+ pendingStartupByTabId: {},
+ settings: {
+ agentCmdOverrides: {},
+ agentDefaultArgs: { codex: '--profile "don\'t"' },
+ setupScriptLaunchMode: 'new-tab'
+ } as unknown as ReturnType['settings'],
+ markWorktreeVisited: vi.fn(),
+ recordWorktreeVisit: vi.fn(),
+ refreshGitHubForWorktreeIfStale: vi.fn(),
+ revealWorktreeInSidebar: vi.fn()
+ })
- it('does not relaunch when activation opts carry no startup payload', () => {
- const worktree = makeWorktree()
- seedEmptyActivatableWorktree(worktree)
+ const result = activateAndRevealWorktree(worktree.id)
+ const state = useAppStore.getState()
+ const reopenedTab = state.tabsByWorktree[worktree.id]?.[0]
- // The opts shape CLI/relay navigation and notification clicks arrive with; those
- // callers are asserted in useIpcEvents.test.ts. The host's `didSpawnStartup` leg is
- // a main-process concern and is not reachable from here.
- activateAndExpectNoRelaunch(worktree.id, { notifyHostRuntime: false })
+ expect(result).toEqual({ primaryTabId: reopenedTab?.id })
+ expect(state.pendingStartupByTabId[reopenedTab!.id]?.command).toContain("'don'\\''t'")
+ expect(state.pendingStartupByTabId[reopenedTab!.id]?.command).not.toContain("'don''t'")
})
it('still queues an explicit startup supplied by the caller', () => {
@@ -136,6 +201,24 @@ describe('activateAndRevealWorktree', () => {
)
})
+ it('does not relaunch when the reopen setting is off across activate/close cycles', () => {
+ const worktree = makeWorktree()
+ seedEmptyActivatableWorktree(worktree)
+ useAppStore.setState({
+ settings: {
+ ...useAppStore.getState().settings,
+ reopenWorkspacesWithCreatedAgent: false
+ } as ReturnType['settings']
+ })
+
+ for (let cycle = 0; cycle < 3; cycle += 1) {
+ activateAndExpectNoRelaunch(worktree.id)
+
+ // Return to zero tabs — opt-out must stay blank even after repeated empty reopens.
+ useAppStore.setState({ tabsByWorktree: {}, activeTabIdByWorktree: {} })
+ }
+ })
+
it('does not duplicate a sleeping agent session owned by a preserved slept pane', () => {
const worktree = makeWorktree()
const revealWorktreeInSidebar = vi.fn()
diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts
index ea06052d9929..155a950c44b6 100644
--- a/src/renderer/src/lib/worktree-activation.ts
+++ b/src/renderer/src/lib/worktree-activation.ts
@@ -5,6 +5,7 @@ import type {
SetupSplitDirection,
Tab,
TuiAgent,
+ Worktree,
WorktreeDefaultTabsLaunch,
WorktreeSetupLaunch
} from '../../../shared/types'
@@ -18,6 +19,10 @@ import { shouldAutoCreateInitialTerminal } from '@/components/terminal/initial-t
import { buildSetupRunnerCommand } from './setup-runner'
import { createSequencedSetupAgentCommands } from '../../../shared/setup-agent-sequencing'
import { getSetupRunnerCommandPlatformForPath } from '../../../shared/setup-runner-command'
+import { buildAgentStartupPlan } from './tui-agent-startup'
+import { getAgentLaunchPlatformForRepo } from '@/lib/agent-launch-platform'
+import { CLIENT_PLATFORM } from './new-workspace'
+import { tuiAgentToAgentKind } from './telemetry'
import { agentKindToTuiAgent } from '../../../shared/agent-kind'
import { useAppStore } from '@/store'
import type { PendingSidebarWorktreeReveal } from '@/store/slices/ui'
@@ -37,8 +42,15 @@ import {
setWorktreeNavActivator,
setWorktreeNavViewActivator
} from '@/store/slices/worktree-nav-history'
+import {
+ resolveTuiAgentLaunchArgs,
+ resolveTuiAgentLaunchEnv
+} from '../../../shared/tui-agent-launch-defaults'
+import { isTuiAgent } from '../../../shared/tui-agent-config'
+import { repoIsRemote } from '../../../shared/agent-launch-remote'
import { resumeSleepingAgentSessionsForWorktree } from '@/lib/resume-sleeping-agent-session'
import { queueHookCommandsForFirstWorktreeTab } from '@/lib/hook-command-delayed-delivery'
+import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
import {
getRuntimeEnvironmentIdForWorktree,
type WorktreeRuntimeOwnerState
@@ -55,6 +67,7 @@ import { getConnectionId } from '@/lib/connection-context'
import { isDetachedHeadWorkspace } from '@/components/sidebar/visible-worktrees'
import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability'
import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache'
+import { resolveNativeChatSessionOptionDefaults } from '../../../shared/native-chat-session-option-defaults'
import type { SessionOptionValue } from '../../../shared/native-chat-session-options'
/** Telemetry threaded from the launch site to `pty:spawn`; main fires `agent_started`
@@ -216,6 +229,63 @@ export function activateAndRevealFolderWorkspace(
return { primaryTabId }
}
+function buildCreatedAgentReopenStartup(worktree: Worktree): WorktreeStartupPayload | undefined {
+ const agent = worktree.createdWithAgent
+ if (!isTuiAgent(agent)) {
+ return undefined
+ }
+
+ const state = useAppStore.getState()
+ // Why: creation agent is remembered for reopen, but users can opt out when a
+ // deliberate exit should not resurrect a fresh empty-prompt session (#10578).
+ if (state.settings?.reopenWorkspacesWithCreatedAgent === false) {
+ return undefined
+ }
+ const repo = state.repos.find((entry) => entry.id === worktree.repoId)
+ const launchPlatform = repo
+ ? getAgentLaunchPlatformForRepo(
+ repo,
+ repo.connectionId ? undefined : getLocalProjectExecutionRuntimeContext(state, worktree.id)
+ )
+ : CLIENT_PLATFORM
+
+ const startupPlan = buildAgentStartupPlan({
+ agent,
+ prompt: '',
+ cmdOverrides: state.settings?.agentCmdOverrides ?? {},
+ agentArgs: resolveTuiAgentLaunchArgs(agent, state.settings?.agentDefaultArgs),
+ agentEnv: resolveTuiAgentLaunchEnv(agent, state.settings?.agentDefaultEnv),
+ sessionOptions: resolveNativeChatSessionOptionDefaults(
+ state.settings?.nativeChatSessionOptions,
+ agent
+ ),
+ platform: launchPlatform,
+ isRemote: repo ? repoIsRemote(repo) : false,
+ allowEmptyPromptLaunch: true
+ })
+ if (!startupPlan) {
+ return undefined
+ }
+
+ return {
+ command: startupPlan.launchCommand,
+ ...(startupPlan.env ? { env: startupPlan.env } : {}),
+ launchConfig: startupPlan.launchConfig,
+ launchAgent: agent,
+ ...(startupPlan.sessionOptions ? { sessionOptions: startupPlan.sessionOptions } : {}),
+ ...(startupPlan.startupCommandDelivery
+ ? { startupCommandDelivery: startupPlan.startupCommandDelivery }
+ : {}),
+ telemetry: {
+ agent_kind: tuiAgentToAgentKind(agent),
+ launch_source: 'sidebar',
+ // Why: this path launches a brand-new empty-prompt session from
+ // createdWithAgent — it is not provider resume (#10578).
+ request_kind: 'new'
+ }
+ }
+}
+
export function activateAndRevealWorktree(
worktreeId: string,
opts?: {
@@ -283,7 +353,7 @@ export function activateAndRevealWorktree(
const primaryTabId = ensureWorktreeHasInitialTerminal(
useAppStore.getState(),
worktreeId,
- opts?.startup,
+ opts?.startup ?? buildCreatedAgentReopenStartup(wt),
opts?.setup,
opts?.issueCommand,
opts?.defaultTabs
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index d017a0cdec5f..d13bbee385ad 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -304,6 +304,8 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
terminalHiddenDeliveryGate: true,
terminalModelQueryAuthority: true,
defaultTuiAgent: null,
+ // Why: preserve #1814 reopen-with-creation-agent; users can opt out in Agents settings (#10578).
+ reopenWorkspacesWithCreatedAgent: true,
disabledTuiAgents: [...DEFAULT_DISABLED_TUI_AGENTS],
pluginSystemEnabled: false,
disabledPlugins: [],
diff --git a/src/shared/types.ts b/src/shared/types.ts
index b98f3d1ea1bf..06e3511f48c9 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -2866,6 +2866,12 @@ export type GlobalSettings = {
* - 'blank': blank terminal (no agent launched)
* - TuiAgent: a specific agent id */
defaultTuiAgent: TuiAgent | 'blank' | null
+ /**
+ * When true (default), reopening an empty workspace relaunches the agent
+ * selected at create time (`Worktree.createdWithAgent`). When false, reopen
+ * falls back to a blank terminal instead (#10578).
+ */
+ reopenWorkspacesWithCreatedAgent: boolean
/** Agents hidden from picker/auto-launch; detection stays a raw PATH snapshot. */
disabledTuiAgents: TuiAgent[]
/** Master switch for the experimental plugin system. Off by default: no