mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(settings): paste bash-native skill setup under WSL PTY (#13710)
* fix(settings): paste bash-native skill setup under WSL PTY
WSL worktree setup terminals force wsl.exe even when shellOverride is
powershell.exe. Auto-pasting the PowerShell `& { wsl.exe ... }` wrapper
into bash fails with a leading-& syntax error (#13305). Rewrite that
wrapper to a bash login-shell script for setup-terminal paste only;
clipboard copy still keeps the PS host wrapper for manual use outside Orca.
* fix(settings): align skill paste with resolved PTY shell
* fix(onboarding): keep shell preparation out of render
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -21,12 +21,6 @@ export function CliSkillSetupTerminal(): React.JSX.Element {
|
||||
ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND,
|
||||
activeSkillRuntime.installDisabledReason ? undefined : activeSkillRuntime.agentRuntime
|
||||
)
|
||||
// The copied string stays as built; only what we execute is adapted.
|
||||
const setupTerminalCommand = buildSkillSetupTerminalCommand(
|
||||
skillCommand,
|
||||
activeSkillRuntime.terminalShellOverride
|
||||
)
|
||||
|
||||
const handleCopySkillCommand = async (): Promise<void> => {
|
||||
try {
|
||||
await window.api.ui.writeClipboardText(skillCommand)
|
||||
@@ -78,7 +72,8 @@ export function CliSkillSetupTerminal(): React.JSX.Element {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<OnboardingInlineCommandTerminal
|
||||
command={setupTerminalCommand}
|
||||
command={skillCommand}
|
||||
prepareCommandForShell={buildSkillSetupTerminalCommand}
|
||||
title={translate(
|
||||
'auto.components.feature.tips.CliSkillSetupTerminal.84e9576dac',
|
||||
'Skill setup'
|
||||
@@ -97,6 +92,7 @@ export function CliSkillSetupTerminal(): React.JSX.Element {
|
||||
autoScrollIntoView={false}
|
||||
worktreeId="feature-tip-cli-skills-terminal"
|
||||
shellOverride={activeSkillRuntime.terminalShellOverride}
|
||||
forceHostRuntime={Boolean(activeSkillRuntime.installDisabledReason)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,15 @@ const mocks = vi.hoisted(() => ({
|
||||
(command: string, runtime?: { runtime: 'host' | 'wsl' }) =>
|
||||
`${runtime?.runtime ?? 'host'}:${command}`
|
||||
),
|
||||
terminalProps: null as { command: string; shellOverride?: string } | null
|
||||
buildSetupCommand: vi.fn(
|
||||
(command: string, shellOverride?: string) => `${shellOverride ?? 'default'}:${command}`
|
||||
),
|
||||
terminalProps: null as {
|
||||
command: string
|
||||
forceHostRuntime?: boolean
|
||||
prepareCommandForShell?: (command: string, shellOverride?: string) => string
|
||||
shellOverride?: string
|
||||
} | null
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/useActiveProjectSkillRuntime', () => ({
|
||||
@@ -21,7 +29,8 @@ vi.mock('@/hooks/useActiveProjectSkillRuntime', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../settings/CliSkillRuntimeSetup', () => ({
|
||||
buildSkillCommandForRuntime: mocks.buildCommand
|
||||
buildSkillCommandForRuntime: mocks.buildCommand,
|
||||
buildSkillSetupTerminalCommand: mocks.buildSetupCommand
|
||||
}))
|
||||
|
||||
vi.mock('./OnboardingInlineCommandTerminal', () => ({
|
||||
@@ -57,8 +66,13 @@ describe('FeatureSetupInlineTerminal', () => {
|
||||
})
|
||||
expect(mocks.terminalProps).toMatchObject({
|
||||
command: 'wsl:npx skills add orchestration',
|
||||
forceHostRuntime: false,
|
||||
prepareCommandForShell: mocks.buildSetupCommand,
|
||||
shellOverride: 'powershell.exe'
|
||||
})
|
||||
expect(
|
||||
mocks.terminalProps?.prepareCommandForShell?.('wsl:npx skills add orchestration', 'wsl.exe')
|
||||
).toBe('wsl.exe:wsl:npx skills add orchestration')
|
||||
})
|
||||
|
||||
it('uses the host command builder when the WSL runtime needs repair', () => {
|
||||
@@ -71,6 +85,7 @@ describe('FeatureSetupInlineTerminal', () => {
|
||||
expect(mocks.buildCommand).toHaveBeenCalledWith('npx skills add orchestration', undefined)
|
||||
expect(mocks.terminalProps).toMatchObject({
|
||||
command: 'host:npx skills add orchestration',
|
||||
forceHostRuntime: true,
|
||||
shellOverride: 'powershell.exe'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,10 @@ import { useCallback, useMemo, useRef, type KeyboardEvent } from 'react'
|
||||
import { track } from '@/lib/telemetry'
|
||||
import { notifyInstalledAgentSkillsChanged } from '@/hooks/useInstalledAgentSkills'
|
||||
import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime'
|
||||
import { buildSkillCommandForRuntime } from '../settings/CliSkillRuntimeSetup'
|
||||
import {
|
||||
buildSkillCommandForRuntime,
|
||||
buildSkillSetupTerminalCommand
|
||||
} from '../settings/CliSkillRuntimeSetup'
|
||||
import { OnboardingInlineCommandTerminal } from './OnboardingInlineCommandTerminal'
|
||||
import {
|
||||
getOnboardingFeatureSetupAgentRuntime,
|
||||
@@ -71,7 +74,9 @@ export function FeatureSetupInlineTerminal({
|
||||
return (
|
||||
<OnboardingInlineCommandTerminal
|
||||
command={runtimeCommand}
|
||||
prepareCommandForShell={buildSkillSetupTerminalCommand}
|
||||
shellOverride={setupRuntime.terminalShellOverride}
|
||||
forceHostRuntime={Boolean(setupRuntime.installDisabledReason)}
|
||||
title={translate(
|
||||
'auto.components.onboarding.FeatureSetupInlineTerminal.c767ab7061',
|
||||
'Skill setup'
|
||||
|
||||
+83
-2
@@ -8,9 +8,10 @@ import {
|
||||
ORCA_TERMINAL_COMMAND_FINISHED_EVENT,
|
||||
type TerminalCommandFinishedEventDetail
|
||||
} from '@/hooks/terminal-command-finished-event'
|
||||
import { PASTE_TERMINAL_TEXT_EVENT, type PasteTerminalTextDetail } from '@/constants/terminal'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
createTab: vi.fn(() => ({ id: 'tab-1' })),
|
||||
createTab: vi.fn(() => ({ id: 'tab-1', shellOverride: 'wsl.exe' })),
|
||||
closeTab: vi.fn(),
|
||||
setActiveTabForWorktree: vi.fn(),
|
||||
setTabCustomTitle: vi.fn()
|
||||
@@ -27,7 +28,12 @@ vi.mock('@/store', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/components/terminal-pane/TerminalPane', () => ({
|
||||
default: () => <div data-testid="terminal-pane" />
|
||||
default: (props: { tabId: string }) => (
|
||||
<div data-testid="terminal-pane" data-terminal-tab-id={props.tabId}>
|
||||
<div data-pty-id="pty-1" />
|
||||
<div className="xterm-rows">$</div>
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/focus-terminal-tab-surface', () => ({
|
||||
@@ -69,6 +75,81 @@ describe('OnboardingInlineCommandTerminal command-finished forwarding', () => {
|
||||
container?.remove()
|
||||
container = null
|
||||
Reflect.deleteProperty(window, 'api')
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('prepares auto-paste again when the resolved tab shell changes', async () => {
|
||||
vi.useFakeTimers()
|
||||
mocks.createTab
|
||||
.mockReturnValueOnce({ id: 'tab-1', shellOverride: 'wsl.exe' })
|
||||
.mockReturnValueOnce({ id: 'tab-2', shellOverride: 'powershell.exe' })
|
||||
const prepareCommandForShell = vi.fn(
|
||||
(command: string, shellOverride: string | undefined) => `${shellOverride}:${command}`
|
||||
)
|
||||
const pasted: PasteTerminalTextDetail[] = []
|
||||
const handlePaste = (event: Event): void => {
|
||||
pasted.push((event as CustomEvent<PasteTerminalTextDetail>).detail)
|
||||
}
|
||||
window.addEventListener(PASTE_TERMINAL_TEXT_EVENT, handlePaste)
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<OnboardingInlineCommandTerminal
|
||||
command="npx skills add orchestration"
|
||||
prepareCommandForShell={prepareCommandForShell}
|
||||
shellOverride="powershell.exe"
|
||||
title="Skill setup"
|
||||
ariaLabel="Skill setup terminal"
|
||||
/>
|
||||
)
|
||||
})
|
||||
await act(async () => {})
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(250)
|
||||
})
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
<OnboardingInlineCommandTerminal
|
||||
command="npx skills add orchestration"
|
||||
forceHostRuntime
|
||||
prepareCommandForShell={prepareCommandForShell}
|
||||
shellOverride="powershell.exe"
|
||||
title="Skill setup"
|
||||
ariaLabel="Skill setup terminal"
|
||||
/>
|
||||
)
|
||||
})
|
||||
await act(async () => {})
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(250)
|
||||
})
|
||||
|
||||
expect(prepareCommandForShell).toHaveBeenCalledWith('npx skills add orchestration', 'wsl.exe')
|
||||
expect(prepareCommandForShell).toHaveBeenCalledWith(
|
||||
'npx skills add orchestration',
|
||||
'powershell.exe'
|
||||
)
|
||||
expect(mocks.createTab).toHaveBeenCalledWith(
|
||||
'ephemeral-setup-terminal:onboarding-inline-terminal',
|
||||
undefined,
|
||||
'powershell.exe',
|
||||
expect.objectContaining({ forceHostRuntime: true })
|
||||
)
|
||||
expect(pasted).toContainEqual({
|
||||
tabId: 'tab-1',
|
||||
text: 'wsl.exe:npx skills add orchestration'
|
||||
})
|
||||
expect(pasted).toContainEqual({
|
||||
tabId: 'tab-2',
|
||||
text: 'powershell.exe:npx skills add orchestration'
|
||||
})
|
||||
} finally {
|
||||
window.removeEventListener(PASTE_TERMINAL_TEXT_EVENT, handlePaste)
|
||||
}
|
||||
})
|
||||
|
||||
it('forwards exit codes only for its own branded worktree id', async () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ const PTY_TEXT_FALLBACK_MS = 750
|
||||
|
||||
type OnboardingInlineCommandTerminalProps = {
|
||||
command: string
|
||||
prepareCommandForShell?: (command: string, shellOverride: string | undefined) => string
|
||||
title: string
|
||||
description?: string
|
||||
ariaLabel: string
|
||||
@@ -30,6 +31,7 @@ type OnboardingInlineCommandTerminalProps = {
|
||||
autoScrollIntoView?: boolean
|
||||
worktreeId?: string
|
||||
shellOverride?: string
|
||||
forceHostRuntime?: boolean
|
||||
onOpened?: () => void
|
||||
onInteracted?: (method: 'keyboard' | 'pointer', event?: KeyboardEvent<HTMLElement>) => void
|
||||
onTerminalExit?: () => void
|
||||
@@ -43,6 +45,7 @@ type OnboardingInlineCommandTerminalProps = {
|
||||
*/
|
||||
export function OnboardingInlineCommandTerminal({
|
||||
command,
|
||||
prepareCommandForShell,
|
||||
title,
|
||||
description,
|
||||
ariaLabel,
|
||||
@@ -52,6 +55,7 @@ export function OnboardingInlineCommandTerminal({
|
||||
autoScrollIntoView = true,
|
||||
worktreeId: worktreeIdProp = ONBOARDING_INLINE_TERMINAL_WORKTREE_ID,
|
||||
shellOverride,
|
||||
forceHostRuntime = false,
|
||||
onOpened,
|
||||
onInteracted,
|
||||
onTerminalExit,
|
||||
@@ -75,13 +79,17 @@ export function OnboardingInlineCommandTerminal({
|
||||
[]
|
||||
)
|
||||
const [cwd, setCwd] = useState<string | null>(null)
|
||||
const [tabId, setTabId] = useState<string | null>(null)
|
||||
const [createdTab, setCreatedTab] = useState<{
|
||||
id: string
|
||||
shellOverride: string | undefined
|
||||
} | null>(null)
|
||||
const tabId = createdTab?.id ?? null
|
||||
// Why: starts at `prefersReducedMotion` so users opted out of motion never
|
||||
// see the slide-in frame; otherwise we flip to true after first paint so the
|
||||
// CSS transition has a starting state to interpolate from.
|
||||
const [entered, setEntered] = useState(prefersReducedMotion)
|
||||
const terminalSectionRef = useRef<HTMLElement>(null)
|
||||
const autoInsertedRef = useRef<string | null>(null)
|
||||
const autoInsertedRef = useRef<{ tabId: string; command: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
onOpened?.()
|
||||
@@ -120,11 +128,12 @@ export function OnboardingInlineCommandTerminal({
|
||||
useEffect(() => {
|
||||
const tab = createTab(worktreeId, undefined, shellOverride, {
|
||||
activate: false,
|
||||
recordInteraction: false
|
||||
recordInteraction: false,
|
||||
forceHostRuntime
|
||||
})
|
||||
setActiveTabForWorktree(worktreeId, tab.id)
|
||||
setTabCustomTitle(tab.id, title, { recordInteraction: false })
|
||||
setTabId(tab.id)
|
||||
setCreatedTab({ id: tab.id, shellOverride: tab.shellOverride })
|
||||
return () => {
|
||||
// Why: inline setup panels can disappear after detection succeeds; close
|
||||
// the backing tab so installer shells do not keep running invisibly.
|
||||
@@ -133,6 +142,7 @@ export function OnboardingInlineCommandTerminal({
|
||||
}, [
|
||||
closeTab,
|
||||
createTab,
|
||||
forceHostRuntime,
|
||||
setActiveTabForWorktree,
|
||||
setTabCustomTitle,
|
||||
shellOverride,
|
||||
@@ -201,9 +211,17 @@ export function OnboardingInlineCommandTerminal({
|
||||
}, [autoScrollIntoView, entered, prefersReducedMotion])
|
||||
|
||||
const insertCommand = useCallback(() => {
|
||||
if (!tabId) {
|
||||
if (!createdTab) {
|
||||
return
|
||||
}
|
||||
const terminalCommand = prepareCommandForShell?.(command, createdTab.shellOverride) ?? command
|
||||
if (
|
||||
autoInsertedRef.current?.tabId === createdTab.id &&
|
||||
autoInsertedRef.current.command === terminalCommand
|
||||
) {
|
||||
return
|
||||
}
|
||||
autoInsertedRef.current = { tabId: createdTab.id, command: terminalCommand }
|
||||
if (autoScrollIntoView) {
|
||||
terminalSectionRef.current?.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
@@ -213,16 +231,16 @@ export function OnboardingInlineCommandTerminal({
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<PasteTerminalTextDetail>(PASTE_TERMINAL_TEXT_EVENT, {
|
||||
detail: {
|
||||
tabId,
|
||||
text: command.trim()
|
||||
tabId: createdTab.id,
|
||||
text: terminalCommand.trim()
|
||||
}
|
||||
})
|
||||
)
|
||||
focusTerminalTabSurface(tabId)
|
||||
}, [autoScrollIntoView, command, tabId])
|
||||
focusTerminalTabSurface(createdTab.id)
|
||||
}, [autoScrollIntoView, command, createdTab, prepareCommandForShell])
|
||||
|
||||
useEffect(() => {
|
||||
if (!tabId || !cwd || autoInsertedRef.current === command) {
|
||||
if (!tabId || !cwd) {
|
||||
return
|
||||
}
|
||||
let canceled = false
|
||||
@@ -236,7 +254,6 @@ export function OnboardingInlineCommandTerminal({
|
||||
}
|
||||
insertionTimer = window.setTimeout(() => {
|
||||
if (!canceled) {
|
||||
autoInsertedRef.current = command
|
||||
insertCommand()
|
||||
}
|
||||
}, AUTO_INSERT_DELAY_MS)
|
||||
@@ -280,7 +297,7 @@ export function OnboardingInlineCommandTerminal({
|
||||
window.clearTimeout(insertionTimer)
|
||||
}
|
||||
}
|
||||
}, [command, cwd, insertCommand, tabId])
|
||||
}, [cwd, insertCommand, tabId])
|
||||
|
||||
// Why: grid 0fr → 1fr animates to the child's natural height without a
|
||||
// hardcoded max-height, so we don't leave dead space if the terminal
|
||||
|
||||
@@ -387,11 +387,11 @@ export function AgentSkillSetupPanel({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* The copied string above stays as built; only what we run is adapted. */}
|
||||
<OnboardingInlineCommandTerminal
|
||||
key={terminalAttempt}
|
||||
worktreeId={terminalWorktreeId}
|
||||
command={buildSkillSetupTerminalCommand(openTerminalCommand, terminalShellOverride)}
|
||||
command={openTerminalCommand}
|
||||
prepareCommandForShell={buildSkillSetupTerminalCommand}
|
||||
title={terminalTitle}
|
||||
description={translate(
|
||||
'auto.components.settings.AgentSkillSetupPanel.runCommandDescription',
|
||||
|
||||
@@ -314,19 +314,38 @@ describe('CliSkillRuntimeSetup runtime helpers', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves WSL and non-Windows setup terminal commands untouched', () => {
|
||||
it('rewrites WSL PowerShell wrappers to bash for setup-terminal auto-paste', () => {
|
||||
const skillCommand = 'npx skills add orchestration --global'
|
||||
const wslCommand = buildSkillCommandForRuntime(
|
||||
'npx skills add orchestration --global',
|
||||
skillCommand,
|
||||
{ runtime: 'wsl', wslDistro: 'Ubuntu', label: 'WSL Ubuntu' },
|
||||
'win32'
|
||||
)
|
||||
|
||||
expect(wslCommand.startsWith('& {')).toBe(true)
|
||||
expect(buildSkillSetupTerminalCommand(wslCommand, 'powershell.exe', 'win32')).toBe(wslCommand)
|
||||
const setupCommand = buildSkillSetupTerminalCommand(wslCommand, 'wsl.exe', 'win32')
|
||||
expect(setupCommand.startsWith('&')).toBe(false)
|
||||
expect(setupCommand).toBe(buildWslLoginShellCommand(skillCommand))
|
||||
expect(setupCommand).toContain('npx skills add orchestration --global')
|
||||
expect(
|
||||
buildSkillSetupTerminalCommand('npx skills add orchestration --global', undefined, 'linux')
|
||||
).toBe('npx skills add orchestration --global')
|
||||
})
|
||||
|
||||
it('preserves the exact WSL script when adapting setup-terminal auto-paste', () => {
|
||||
const skillCommand = "printf 'héllo\n# Runs: unchanged'"
|
||||
const copiedCommand = buildSkillCommandForRuntime(skillCommand, {
|
||||
runtime: 'wsl',
|
||||
wslDistro: 'Ubuntu',
|
||||
label: 'WSL Ubuntu'
|
||||
})
|
||||
|
||||
expect(buildSkillSetupTerminalCommand(copiedCommand, 'wsl.exe', 'win32')).toBe(
|
||||
buildWslLoginShellCommand(skillCommand)
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the bare reinstall rewrite for POSIX-family Windows skill updates', () => {
|
||||
const installCommand = buildAgentFeatureSkillInstallCommand(['orchestration'])
|
||||
const previous = useAppStore.getState()
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
quotePowerShellNativeArgument
|
||||
} from '../../../../shared/powershell-native-argument'
|
||||
import { buildWslLoginShellCommand } from '../../../../shared/wsl-login-shell-command'
|
||||
import { isWslShellName } from '../../../../shared/local-windows-terminal-runtime'
|
||||
import { resolveWindowsShellStartupFamily } from '../../../../shared/windows-terminal-shell'
|
||||
import { getProjectAgentSkillTerminalShellOverride } from '@/lib/project-skill-runtime'
|
||||
import { useAppStore } from '@/store'
|
||||
@@ -138,16 +139,23 @@ function normalizeWindowsSkillUpdateCommand(
|
||||
type SkillCommandTarget = 'copied-command' | 'orca-setup-terminal'
|
||||
|
||||
/**
|
||||
* Re-adds the npx preflight for Orca's own setup terminal, which
|
||||
* `getAgentSkillTerminalShellOverride` forces onto powershell.exe. The copied
|
||||
* string stays bare for POSIX-family shells; only the executed one is wrapped.
|
||||
* Adapts a copied skill command for Orca's inline setup terminal auto-paste.
|
||||
* Host Windows installs may gain an npx preflight; WSL-targeted PowerShell wrappers
|
||||
* must become bash-native because the daemon forces wsl.exe for WSL worktrees.
|
||||
*/
|
||||
export function buildSkillSetupTerminalCommand(
|
||||
copiedCommand: string,
|
||||
terminalShellOverride: string | undefined,
|
||||
effectiveShell: string | undefined,
|
||||
currentPlatform = getSkillCommandPlatform()
|
||||
): string {
|
||||
if (!isSetupTerminalForcedToPowerShell(terminalShellOverride)) {
|
||||
// Why: the created tab is authoritative when project runtime replaces the requested shell.
|
||||
const wslNative = isWslShellName(effectiveShell)
|
||||
? decodeWslSetupTerminalCommand(copiedCommand)
|
||||
: null
|
||||
if (wslNative) {
|
||||
return wslNative
|
||||
}
|
||||
if (!isSetupTerminalForcedToPowerShell(effectiveShell)) {
|
||||
return copiedCommand
|
||||
}
|
||||
return wrapWindowsSkillCommandWithNpxPrerequisite(
|
||||
@@ -157,6 +165,30 @@ export function buildSkillSetupTerminalCommand(
|
||||
)
|
||||
}
|
||||
|
||||
function decodeWslSetupTerminalCommand(command: string): string | null {
|
||||
if (
|
||||
!command.startsWith("& { $PSNativeCommandArgumentPassing = 'Legacy'; wsl.exe") ||
|
||||
!command.includes(' } # Runs: ')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const encoded = /-- sh -c 'eval \\"`printf %s ([A-Za-z0-9+/=]+) \| base64 -d`\\"'/.exec(
|
||||
command
|
||||
)?.[1]
|
||||
if (!encoded) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const binary = atob(encoded)
|
||||
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0))
|
||||
return new TextDecoder().decode(bytes)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function isSetupTerminalForcedToPowerShell(terminalShellOverride: string | undefined): boolean {
|
||||
const trimmedOverride = terminalShellOverride?.trim()
|
||||
return (
|
||||
|
||||
@@ -170,11 +170,11 @@ describe('AgentSkillSetupPanel installed-command call sites', () => {
|
||||
)
|
||||
|
||||
expect(source).toContain('buildSkillCommandForRuntime(')
|
||||
// The copied string stays bare for POSIX-family shells; the forced-PowerShell
|
||||
// setup terminal keeps the npx preflight.
|
||||
// Clipboard and auto-paste share the source command until the created tab
|
||||
// resolves the shell that prepares the executable form.
|
||||
expect(source).toContain('writeClipboardText(skillCommand)')
|
||||
expect(source).toContain('buildSkillSetupTerminalCommand(')
|
||||
expect(source).toContain('command={setupTerminalCommand}')
|
||||
expect(source).toContain('command={skillCommand}')
|
||||
expect(source).toContain('prepareCommandForShell={buildSkillSetupTerminalCommand}')
|
||||
expect(source).toContain('shellOverride={activeSkillRuntime.terminalShellOverride}')
|
||||
expect(source).not.toContain('command={ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND}')
|
||||
// This terminal auto-pastes with no install gate, so a repair-required runtime
|
||||
|
||||
@@ -153,6 +153,7 @@ type StoreState = {
|
||||
title?: string
|
||||
launchAgent?: string
|
||||
shellOverride?: string
|
||||
forceHostRuntime?: boolean
|
||||
generation?: number
|
||||
}[]
|
||||
>
|
||||
@@ -2316,6 +2317,27 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps an explicit host fallback out of the project runtime', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
transportFactoryQueue.push(transport)
|
||||
mockStoreState = {
|
||||
...mockStoreState,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: null, forceHostRuntime: true }]
|
||||
},
|
||||
settings: {
|
||||
...mockStoreState.settings,
|
||||
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
|
||||
}
|
||||
}
|
||||
|
||||
connectPanePty(createPane(1) as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks()
|
||||
|
||||
expect(createdTransportOptions[0]?.projectRuntime).toBeUndefined()
|
||||
})
|
||||
|
||||
it('observes live terminal GitHub PR URLs before agent completion', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
|
||||
@@ -3646,7 +3646,7 @@ export function connectPanePty(
|
||||
? getCachedWindowsTerminalCapabilities()
|
||||
: null
|
||||
const projectRuntime =
|
||||
!connectionId && runtimeEnvironmentId === null
|
||||
!tab?.forceHostRuntime && !connectionId && runtimeEnvironmentId === null
|
||||
? getLocalProjectExecutionRuntimeContext(state, deps.worktreeId, undefined, {
|
||||
wslAvailable: localWindowsTerminalCapabilities?.wslAvailable,
|
||||
availableWslDistros: localWindowsTerminalCapabilities?.wslDistros ?? null
|
||||
|
||||
@@ -1658,6 +1658,14 @@ describe('setActiveWorktree', () => {
|
||||
|
||||
const terminal = store.getState().createTab(wt, undefined, 'cmd.exe')
|
||||
expect(terminal.shellOverride).toBe('wsl.exe')
|
||||
|
||||
const hostTerminal = store
|
||||
.getState()
|
||||
.createTab(wt, undefined, 'powershell.exe', { forceHostRuntime: true })
|
||||
expect(hostTerminal).toMatchObject({
|
||||
shellOverride: 'powershell.exe',
|
||||
forceHostRuntime: true
|
||||
})
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
value: originalNavigator,
|
||||
|
||||
@@ -670,6 +670,7 @@ export type TerminalSlice = {
|
||||
/** Initial native-chat view mode; agent launches pass 'chat' when openAgentTabsInChatByDefault is on, else omitted for the 'terminal' default. */
|
||||
viewMode?: Tab['viewMode']
|
||||
startupCwd?: string
|
||||
forceHostRuntime?: boolean
|
||||
}
|
||||
) => TerminalTab
|
||||
openNewTerminalTabInActiveWorkspace: (groupId: string) => Promise<void>
|
||||
@@ -1379,7 +1380,9 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
|
||||
: null,
|
||||
// Why: new terminals enter the worktree's repo-scoped WSL distro even when the global Windows shell is PowerShell/cmd.exe.
|
||||
isWslWorktree,
|
||||
isRemoteWorktree ? undefined : getLocalProjectExecutionRuntimeContext(s, worktreeId)
|
||||
isRemoteWorktree || options?.forceHostRuntime
|
||||
? undefined
|
||||
: getLocalProjectExecutionRuntimeContext(s, worktreeId)
|
||||
)
|
||||
tab = {
|
||||
id,
|
||||
@@ -1396,6 +1399,7 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
|
||||
createdAt: Date.now(),
|
||||
...(createdShellOverride !== undefined ? { shellOverride: createdShellOverride } : {}),
|
||||
...(startupCwd && startupCwd.length > 0 ? { startupCwd } : {}),
|
||||
...(options?.forceHostRuntime ? { forceHostRuntime: true } : {}),
|
||||
...(options?.launchAgent ? { launchAgent: options.launchAgent } : {}),
|
||||
// Why: mark click-caused (not work-caused) spawns so updateTabPtyId skips the activity/sortEpoch bump that would reorder Recent/Smart on click.
|
||||
...(options?.pendingActivationSpawn ? { pendingActivationSpawn: true } : {})
|
||||
|
||||
@@ -903,6 +903,8 @@ export type TerminalTab = {
|
||||
* PTY and tab icon stay stable even if the default shell setting changes
|
||||
* later. Older persisted tabs may omit this field. */
|
||||
shellOverride?: string
|
||||
/** Keeps an ephemeral host fallback out of the active project's runtime. */
|
||||
forceHostRuntime?: boolean
|
||||
/** Why: explorer-created terminals can start below the workspace root while
|
||||
* still belonging to that workspace for tab/session ownership. */
|
||||
startupCwd?: string
|
||||
|
||||
Reference in New Issue
Block a user