diff --git a/src/renderer/src/components/settings/terminal-search.ts b/src/renderer/src/components/settings/terminal-search.ts
index 0d34440a8b9..3ed4fba8747 100644
--- a/src/renderer/src/components/settings/terminal-search.ts
+++ b/src/renderer/src/components/settings/terminal-search.ts
@@ -89,6 +89,26 @@ export const TERMINAL_ADVANCED_SEARCH_ENTRIES: SettingsSearchEntry[] = [
}
]
+export const TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES: SettingsSearchEntry[] = [
+ {
+ title: 'Setup Script Location',
+ description:
+ "Where the repository setup script runs when a new workspace is created: a vertical split (default), a horizontal split, or a background tab titled 'Setup'.",
+ keywords: [
+ 'setup',
+ 'script',
+ 'workspace',
+ 'split',
+ 'horizontal',
+ 'vertical',
+ 'tab',
+ 'new',
+ 'location',
+ 'launch'
+ ]
+ }
+]
+
export const TERMINAL_WINDOWS_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Right-click to paste',
@@ -111,6 +131,7 @@ export function getTerminalPaneSearchEntries(isWindows: boolean): SettingsSearch
...(isWindows ? TERMINAL_WINDOWS_SEARCH_ENTRIES : []),
...TERMINAL_DARK_THEME_SEARCH_ENTRIES,
...TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
+ ...TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES,
...TERMINAL_ADVANCED_SEARCH_ENTRIES
]
}
diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
index c8d4ba7e979..65295a90e52 100644
--- a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
+++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
@@ -10,7 +10,11 @@ import {
handleOscLink
} from './terminal-link-handlers'
import type { LinkHandlerDeps } from './terminal-link-handlers'
-import type { GlobalSettings, TerminalLayoutSnapshot } from '../../../../shared/types'
+import type {
+ GlobalSettings,
+ SetupSplitDirection,
+ TerminalLayoutSnapshot
+} from '../../../../shared/types'
import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts'
import {
buildFontFamily,
@@ -30,9 +34,14 @@ type UseTerminalPaneLifecycleDeps = {
worktreeId: string
cwd?: string
startup?: { command: string; env?: Record } | null
- /** When present, the initial pane boots clean and a right-side split pane is
- * created to run the setup command — keeping the main terminal interactive. */
- setupSplit?: { command: string; env?: Record } | null
+ /** When present, the initial pane boots clean and a split pane is created
+ * (vertical or horizontal per the user setting) to run the setup command —
+ * keeping the main terminal interactive. */
+ setupSplit?: {
+ command: string
+ env?: Record
+ direction: SetupSplitDirection
+ } | null
/** When present, a split pane is created to run the repo's configured
* issue-automation command with the linked issue number interpolated. */
issueCommandSplit?: { command: string; env?: Record } | null
@@ -466,11 +475,11 @@ export function useTerminalPaneLifecycle({
const setupPane = splitPaneWithOneShotStartup(
ptyDeps,
{ command: setupSplit.command, env: setupSplit.env },
- () => manager.splitPane(initialPane.id, 'vertical')
+ () => manager.splitPane(initialPane.id, setupSplit.direction)
)
issueAutomationAnchorPaneId = setupPane?.id ?? null
- // Restore focus to the main (left) pane so the user's terminal
- // receives keyboard input — the setup pane runs unattended.
+ // Restore focus to the main pane so the user's terminal receives
+ // keyboard input — the setup pane runs unattended.
manager.setActivePane(initialPane.id, { focus: isActive })
}
}
diff --git a/src/renderer/src/lib/worktree-activation.test.ts b/src/renderer/src/lib/worktree-activation.test.ts
index 9eb6b3be480..bcc8efd2626 100644
--- a/src/renderer/src/lib/worktree-activation.test.ts
+++ b/src/renderer/src/lib/worktree-activation.test.ts
@@ -1,11 +1,28 @@
-import { describe, expect, it, vi } from 'vitest'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import type { SetupScriptLaunchMode } from '../../../shared/types'
import { ensureWorktreeHasInitialTerminal } from './worktree-activation'
+import { useAppStore } from '@/store'
+
+function setSetupScriptLaunchMode(mode: SetupScriptLaunchMode | null): void {
+ useAppStore.setState((state) => ({
+ settings: state.settings
+ ? { ...state.settings, setupScriptLaunchMode: mode ?? 'split-vertical' }
+ : mode !== null
+ ? ({ setupScriptLaunchMode: mode } as unknown as typeof state.settings)
+ : state.settings
+ }))
+}
+
+afterEach(() => {
+ setSetupScriptLaunchMode('split-vertical')
+})
function createMockStore(overrides: Record = {}) {
return {
tabsByWorktree: {} as Record,
createTab: vi.fn(() => ({ id: 'tab-1' })),
setActiveTab: vi.fn(),
+ setTabCustomTitle: vi.fn(),
reconcileWorktreeTabModel: vi.fn(() => ({ renderableTabCount: 0 })),
queueTabStartupCommand: vi.fn(),
queueTabSetupSplit: vi.fn(),
@@ -34,7 +51,8 @@ describe('ensureWorktreeHasInitialTerminal', () => {
env: {
ORCA_ROOT_PATH: '/tmp/repo',
ORCA_WORKTREE_PATH: '/tmp/worktrees/wt-1'
- }
+ },
+ direction: 'vertical'
})
})
@@ -141,7 +159,8 @@ describe('ensureWorktreeHasInitialTerminal', () => {
expect(store.queueTabStartupCommand).not.toHaveBeenCalled()
expect(store.queueTabSetupSplit).toHaveBeenCalledWith('tab-1', {
command: 'bash /tmp/repo/.git/orca/setup-runner.sh',
- env: { ORCA_ROOT_PATH: '/tmp/repo' }
+ env: { ORCA_ROOT_PATH: '/tmp/repo' },
+ direction: 'vertical'
})
expect(store.queueTabIssueCommandSplit).toHaveBeenCalledWith('tab-1', {
command: 'bash /tmp/repo/.git/orca/issue-command-runner.sh',
@@ -157,4 +176,44 @@ describe('ensureWorktreeHasInitialTerminal', () => {
expect(store.queueTabStartupCommand).not.toHaveBeenCalled()
expect(store.queueTabIssueCommandSplit).not.toHaveBeenCalled()
})
+
+ it('queues a horizontal setup split when setupScriptLaunchMode is split-horizontal', () => {
+ setSetupScriptLaunchMode('split-horizontal')
+ const store = createMockStore()
+
+ ensureWorktreeHasInitialTerminal(store, 'wt-1', undefined, {
+ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh',
+ envVars: { ORCA_ROOT_PATH: '/tmp/repo' }
+ })
+
+ expect(store.queueTabSetupSplit).toHaveBeenCalledWith('tab-1', {
+ command: 'bash /tmp/repo/.git/orca/setup-runner.sh',
+ env: { ORCA_ROOT_PATH: '/tmp/repo' },
+ direction: 'horizontal'
+ })
+ })
+
+ it('creates a background Setup tab when setupScriptLaunchMode is new-tab', () => {
+ setSetupScriptLaunchMode('new-tab')
+ let createdIndex = 0
+ const createTab = vi.fn(() => ({ id: `tab-${++createdIndex}` }))
+ const store = createMockStore({ createTab })
+
+ ensureWorktreeHasInitialTerminal(store, 'wt-1', undefined, {
+ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh',
+ envVars: { ORCA_ROOT_PATH: '/tmp/repo' }
+ })
+
+ expect(createTab).toHaveBeenCalledTimes(2)
+ // Main tab is activated first (new terminal), then setup tab is created,
+ // and the helper re-activates the main tab so focus stays on tab-1.
+ expect(store.setActiveTab).toHaveBeenNthCalledWith(1, 'tab-1')
+ expect(store.setActiveTab).toHaveBeenLastCalledWith('tab-1')
+ expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup')
+ expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', {
+ command: 'bash /tmp/repo/.git/orca/setup-runner.sh',
+ env: { ORCA_ROOT_PATH: '/tmp/repo' }
+ })
+ expect(store.queueTabSetupSplit).not.toHaveBeenCalled()
+ })
})
diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts
index 2ab8bfc6863..55dbc523aeb 100644
--- a/src/renderer/src/lib/worktree-activation.ts
+++ b/src/renderer/src/lib/worktree-activation.ts
@@ -1,4 +1,4 @@
-import type { WorktreeSetupLaunch } from '../../../shared/types'
+import type { SetupSplitDirection, WorktreeSetupLaunch } from '../../../shared/types'
import { shouldAutoCreateInitialTerminal } from '@/components/terminal/initial-terminal'
import { buildSetupRunnerCommand } from './setup-runner'
import { useAppStore } from '@/store'
@@ -17,6 +17,7 @@ type WorktreeActivationStore = {
tabsByWorktree: Record
createTab: (worktreeId: string) => { id: string }
setActiveTab: (tabId: string) => void
+ setTabCustomTitle: (tabId: string, title: string | null) => void
reconcileWorktreeTabModel: (worktreeId: string) => { renderableTabCount: number }
queueTabStartupCommand: (
tabId: string,
@@ -24,7 +25,7 @@ type WorktreeActivationStore = {
) => void
queueTabSetupSplit: (
tabId: string,
- startup: { command: string; env?: Record }
+ startup: { command: string; env?: Record; direction: SetupSplitDirection }
) => void
queueTabIssueCommandSplit: (
tabId: string,
@@ -122,15 +123,34 @@ export function ensureWorktreeHasInitialTerminal(
store.queueTabStartupCommand(terminalTab.id, startup)
}
- // Why: run the setup script in a split pane to the right so the main
- // terminal stays immediately interactive. The TerminalPane reads this
- // signal on mount, creates the initial pane clean, then splits right
- // and injects the setup command into the new pane's PTY.
+ // Why: the setup script launch location is user-configurable. The default
+ // 'split-vertical' preserves the historical behavior (right-side split so
+ // the main terminal stays immediately interactive); 'split-horizontal'
+ // swaps the split orientation; 'new-tab' creates a separate background
+ // tab titled "Setup" without stealing focus from the main terminal.
if (setup) {
- store.queueTabSetupSplit(terminalTab.id, {
+ const mode = useAppStore.getState().settings?.setupScriptLaunchMode ?? 'split-vertical'
+ const setupCommand = {
command: buildSetupRunnerCommand(setup.runnerScriptPath),
env: setup.envVars
- })
+ }
+ if (mode === 'new-tab') {
+ const setupTab = store.createTab(worktreeId)
+ // Why: createTab auto-activates the new tab. Revert activation so the
+ // user's focus stays on the primary terminal — per the design, the
+ // Setup tab runs unattended in the background.
+ store.setActiveTab(terminalTab.id)
+ // Why: customTitle wins over the auto-generated "Terminal N" label
+ // everywhere the tab is rendered (tab bar, switcher, session snapshots),
+ // so labeling via customTitle is the single authoritative source.
+ store.setTabCustomTitle(setupTab.id, 'Setup')
+ store.queueTabStartupCommand(setupTab.id, setupCommand)
+ } else {
+ store.queueTabSetupSplit(terminalTab.id, {
+ ...setupCommand,
+ direction: mode === 'split-horizontal' ? 'horizontal' : 'vertical'
+ })
+ }
}
// Why: when the user links a GitHub issue and opts into that repo's
diff --git a/src/renderer/src/store/slices/terminals.ts b/src/renderer/src/store/slices/terminals.ts
index 87f44ffae18..8bf57b9dbb9 100644
--- a/src/renderer/src/store/slices/terminals.ts
+++ b/src/renderer/src/store/slices/terminals.ts
@@ -2,6 +2,7 @@
import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import type {
+ SetupSplitDirection,
TerminalLayoutSnapshot,
TerminalTab,
WorkspaceSessionState
@@ -63,9 +64,13 @@ export type TerminalSlice = {
terminalLayoutsByTabId: Record
pendingStartupByTabId: Record }>
/** Queued setup-split requests — when present, TerminalPane creates the
- * initial pane clean, then splits right and runs the command in the new pane
- * so the main terminal stays immediately interactive. */
- pendingSetupSplitByTabId: Record }>
+ * initial pane clean, then splits (vertical or horizontal per user setting)
+ * and runs the command in the new pane so the main terminal stays
+ * immediately interactive. */
+ pendingSetupSplitByTabId: Record<
+ string,
+ { command: string; env?: Record; direction: SetupSplitDirection }
+ >
/** Queued issue-command-split requests — similar to setup splits but triggered
* when an issue is linked during worktree creation and the repo's issue
* automation command is enabled. */
@@ -107,9 +112,11 @@ export type TerminalSlice = {
) => { command: string; env?: Record } | null
queueTabSetupSplit: (
tabId: string,
- startup: { command: string; env?: Record }
+ startup: { command: string; env?: Record; direction: SetupSplitDirection }
) => void
- consumeTabSetupSplit: (tabId: string) => { command: string; env?: Record } | null
+ consumeTabSetupSplit: (
+ tabId: string
+ ) => { command: string; env?: Record; direction: SetupSplitDirection } | null
queueTabIssueCommandSplit: (
tabId: string,
issueCommand: { command: string; env?: Record }
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index c6c093e6f53..a0d21dc0d96 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -107,6 +107,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
// { ...defaults.settings, ...parsed.settings } merge, so enabling
// focus-follows-mouse never happens unexpectedly.
terminalFocusFollowsMouse: false,
+ setupScriptLaunchMode: 'split-vertical',
terminalScrollbackBytes: 10_000_000,
openLinksInApp: true,
rightSidebarOpenByDefault: true,
diff --git a/src/shared/types.ts b/src/shared/types.ts
index 522643c195b..9c4ddac0d6b 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -546,6 +546,15 @@ export type TuiAgent =
export type TaskViewPresetId = 'all' | 'issues' | 'review' | 'my-issues' | 'my-prs' | 'prs'
+/** Where the repo setup script runs when a worktree is created.
+ * - 'split-vertical': split the initial terminal pane with a vertical divider (default).
+ * - 'split-horizontal': split the initial terminal pane with a horizontal divider.
+ * - 'new-tab': open a background tab titled "Setup" and leave focus on the first tab. */
+export type SetupScriptLaunchMode = 'split-vertical' | 'split-horizontal' | 'new-tab'
+
+/** Direction used when the setup script launch mode is a split. */
+export type SetupSplitDirection = 'vertical' | 'horizontal'
+
export type GlobalSettings = {
workspaceDir: string
nestWorkspaces: boolean
@@ -574,6 +583,9 @@ export type GlobalSettings = {
* menu behavior and users can still reach the menu with Ctrl+right-click. */
terminalRightClickToPaste: boolean
terminalFocusFollowsMouse: boolean
+ /** Where the repo setup script runs on workspace create. Defaults to a
+ * vertical split so the user's main terminal stays immediately usable. */
+ setupScriptLaunchMode: SetupScriptLaunchMode
terminalScrollbackBytes: number
/** Why: opening arbitrary links inside Orca uses an isolated guest browser surface.
* The setting stays opt-in so existing workflows continue to use the system browser