Skip to content

Commit 20d67bc

Browse files
frenchie4111claude
andcommitted
Stop restored shell tabs from re-running their command
A shell tab created by an agent persists the command it was launched with, and XTerminal treated that field as an instruction: any mount without a live PTY spawned `zsh -ilc <command>`. Reopening Ness (or touching a tab whose command had already exited) therefore re-ran it unprompted. The command is only ever meant to run once, in createShell's eager spawn, so the renderer now always opens a plain interactive shell and the persisted command stays as a record of origin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7ce2e8b commit 20d67bc

4 files changed

Lines changed: 13 additions & 14 deletions

File tree

src/main/persistence-migrations.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export interface PersistedTab {
2424
sessionId?: string
2525
/** For browser tabs: last URL so we can restore the tab on reload. */
2626
url?: string
27-
/** For shell tabs: command passed via `zsh -ilc <command>` (agent-spawned). */
27+
/** For shell tabs: the command the tab was created with (agent-spawned).
28+
* Kept for display only — restored tabs do not re-run it. */
2829
command?: string
2930
/** For shell tabs: cwd (absolute or relative to worktree root). */
3031
cwd?: string

src/renderer/components/WorkspaceView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,6 @@ export function WorkspaceView({
634634
initialPrompt={tab.initialPrompt}
635635
teleportSessionId={tab.teleportSessionId}
636636
modelOverride={tab.type === 'agent' ? tab.model : undefined}
637-
shellCommand={tab.type === 'shell' ? tab.command : undefined}
638637
shellCwd={tab.type === 'shell' ? tab.cwd : undefined}
639638
onRestartAgent={
640639
tab.type === 'agent'

src/renderer/components/XTerminal.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,6 @@ interface XTerminalProps {
267267
initialPrompt?: string
268268
teleportSessionId?: string
269269
modelOverride?: string
270-
/** Shell tabs only: when set, spawn `<user-shell> -ilc <command>` instead
271-
* of an interactive login shell. Used for agent-spawned shells. */
272-
shellCommand?: string
273270
/** Shell tabs only: directory to spawn in. Relative paths resolve against
274271
* `cwd` (the worktree root); absolute paths are used as-is. */
275272
shellCwd?: string
@@ -292,7 +289,7 @@ interface XTerminalProps {
292289
onSwitchToChat?: () => void
293290
}
294291

295-
export function XTerminal({ terminalId, cwd, type, agentKind, visible, sessionName, sessionId, initialPrompt, teleportSessionId, modelOverride, shellCommand, shellCwd, backgroundVar, preamble, hideRestoreNotice, onRestartAgent, onSwitchToChat }: XTerminalProps): JSX.Element {
292+
export function XTerminal({ terminalId, cwd, type, agentKind, visible, sessionName, sessionId, initialPrompt, teleportSessionId, modelOverride, shellCwd, backgroundVar, preamble, hideRestoreNotice, onRestartAgent, onSwitchToChat }: XTerminalProps): JSX.Element {
296293
// Lazy font-cache init — fires once on first XTerminal mount. See
297294
// initFontCache() comment for why this is lazy rather than at module
298295
// top.
@@ -634,12 +631,12 @@ export function XTerminal({ terminalId, cwd, type, agentKind, visible, sessionNa
634631
const shell = ''
635632
const agentArg = type === 'agent' ? await buildAgentArg() : ''
636633
if (disposed) return
637-
const args =
638-
type === 'agent'
639-
? ['-ilc', agentArg]
640-
: shellCommand
641-
? ['-ilc', shellCommand]
642-
: ['-il']
634+
// Shell tabs always come up as a plain interactive shell, even when the
635+
// tab carries a `command`. Executing it belongs to the create_shell
636+
// path in main, which spawns eagerly at creation time; doing it here too
637+
// would re-run the command every time the PTY is gone but the tab isn't
638+
// — i.e. on every app restart, and after the command's shell exits.
639+
const args = type === 'agent' ? ['-ilc', agentArg] : ['-il']
643640
const spawnCwd = shellCwd
644641
? shellCwd.startsWith('/')
645642
? shellCwd

src/shared/state/terminals.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ export interface TerminalTab {
5858
teleportSessionId?: string
5959
/** For browser tabs: the URL currently loaded (restored on reload). */
6060
url?: string
61-
/** For shell tabs: command to run via `zsh -ilc <command>` instead of
62-
* spawning an interactive login shell. Set by agents via the shell MCP. */
61+
/** For shell tabs: the command the tab was created with (agents set this via
62+
* the shell MCP). A record of origin, not an instruction — it runs once, in
63+
* `createShell`'s eager spawn. Remounting the tab (app restart, or after the
64+
* command's shell exited) gives a plain interactive shell instead. */
6365
command?: string
6466
/** For shell tabs: directory to run in. Relative paths resolve against the
6567
* worktree root; absolute paths are used as-is. */

0 commit comments

Comments
 (0)