From 3d481f439dfb8d65ee078bc918f12264bb08bc8b Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Sun, 26 Jul 2026 03:24:31 +0900 Subject: [PATCH 1/3] feat(agents): opt out of reopening workspaces with creation agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspaces created with an agent relaunched that agent on every empty sidebar reopen. Add Settings → Agents toggle (default on) so users can prefer a blank terminal instead, and label the launch as request_kind new rather than resume since no provider session is restored. Closes #10578 --- .../src/components/settings/AgentsPane.tsx | 3 + .../settings/CreatedAgentReopenSetting.tsx | 59 ++++++++ .../src/components/settings/agents-search.ts | 10 ++ .../settings/created-agent-reopen-copy.ts | 29 ++++ .../src/components/settings/setting-labels.ts | 3 +- .../worktree-activation-created-agent.test.ts | 137 ++++++++++++++---- src/renderer/src/lib/worktree-activation.ts | 72 ++++++++- src/shared/constants.ts | 2 + src/shared/types.ts | 6 + 9 files changed, 292 insertions(+), 29 deletions(-) create mode 100644 src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx create mode 100644 src/renderer/src/components/settings/created-agent-reopen-copy.ts 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..9c4298008ce9 --- /dev/null +++ b/src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx @@ -0,0 +1,59 @@ +import type { GlobalSettings } from '../../../../shared/types' +import { Label } from '../ui/label' +import { + getCreatedAgentReopenDescription, + getCreatedAgentReopenSearchKeywords, + getCreatedAgentReopenTitle +} from './created-agent-reopen-copy' +import { SearchableSetting } from './SearchableSetting' + +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 ( +
+ +
+
+ +

{description}

+
+ +
+
+
+ ) +} 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..1b3a5886fb36 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 creation agent' } 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 From e9c4b48820c1fbcf02ee8009f6152f8c494d29fb Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Sun, 26 Jul 2026 12:23:09 +0900 Subject: [PATCH 2/3] fix(settings): use shared SettingsSwitchRow for agent reopen toggle CodeRabbit: replace the bespoke switch markup with the settings form primitive so interaction and styling stay consistent. --- .../settings/CreatedAgentReopenSetting.tsx | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx b/src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx index 9c4298008ce9..5e85b9a1bf98 100644 --- a/src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx +++ b/src/renderer/src/components/settings/CreatedAgentReopenSetting.tsx @@ -1,11 +1,11 @@ import type { GlobalSettings } from '../../../../shared/types' -import { Label } from '../ui/label' import { getCreatedAgentReopenDescription, getCreatedAgentReopenSearchKeywords, getCreatedAgentReopenTitle } from './created-agent-reopen-copy' import { SearchableSetting } from './SearchableSetting' +import { SettingsSwitchRow } from './SettingsFormControls' type CreatedAgentReopenSettingProps = { settings: GlobalSettings @@ -27,32 +27,16 @@ export function CreatedAgentReopenSetting({ description={description} keywords={getCreatedAgentReopenSearchKeywords()} > -
-
- -

{description}

-
- -
+ + updateSettings({ + reopenWorkspacesWithCreatedAgent: !enabled + }) + } + /> ) From 59aafa54b3758619b5796092c413361ad645f32a Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Wed, 29 Jul 2026 12:05:15 +0900 Subject: [PATCH 3/3] fix(settings): align created-agent reopen label with UI title Greptile: SETTING_LABELS must match the SettingsSwitchRow title string. Co-Authored-By: Grok Companion --- src/renderer/src/components/settings/setting-labels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/components/settings/setting-labels.ts b/src/renderer/src/components/settings/setting-labels.ts index 1b3a5886fb36..9bdb70b5ab3c 100644 --- a/src/renderer/src/components/settings/setting-labels.ts +++ b/src/renderer/src/components/settings/setting-labels.ts @@ -25,5 +25,5 @@ export const SETTING_LABELS: Partial> = { terminalDividerColorLight: 'Divider Color (Light)', terminalInactivePaneOpacity: 'Inactive Pane Opacity', windowBackgroundBlur: 'Window Background Blur', - reopenWorkspacesWithCreatedAgent: 'Reopen workspaces with creation agent' + reopenWorkspacesWithCreatedAgent: 'Reopen workspaces with the agent they were created with' }