mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 00:02:29 +00:00
fix(opencode): keep wrapped run tasks positional
Recognize supported environment prefixes and PowerShell call operators without
mistaking prompt arguments for executables. Keep environment and run separators
separate, preserve the task text and exclude run commands from native submission.
Source-parent: 23fc08b4e9
Related-to: stablya/orca#17551
Credits: @coelho-doti (stablya/orca#13065)
This commit is contained in:
@@ -91,7 +91,7 @@ export async function prepareOpenCodePtyLaunch(options: {
|
||||
const last = parsed.ok ? parsed.tokens.length - 1 : -1
|
||||
if (
|
||||
parsed.ok &&
|
||||
!isOpenCodeRunCommand(parsed.tokens) &&
|
||||
!isOpenCodeRunCommand(parsed.tokens, shell) &&
|
||||
parsed.tokens.filter((token) => token === '--prompt').length === 1 &&
|
||||
parsed.tokens[last - 1] === '--prompt' &&
|
||||
parsed.tokens[last] === body &&
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SessionOptionValue } from './native-chat-session-options'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
||||
import { tokenizeStartupCommand, type AgentStartupShell } from './tui-agent-startup-shell'
|
||||
import { isOpenCodeRunCommand } from './opencode-headless-command'
|
||||
import { findOpenCodeRunCommand } from './opencode-headless-command'
|
||||
import { openCodeStartupPromptEnv } from './opencode-startup-prompt'
|
||||
|
||||
export function appliedSessionOptionProps(values: Record<string, SessionOptionValue>) {
|
||||
@@ -22,13 +22,13 @@ export function buildFlagPromptStartupPlan(args: {
|
||||
agentEnv: Record<string, string> | null | undefined
|
||||
}): AgentStartupPlan {
|
||||
const parsed = tokenizeStartupCommand(args.launchCommand, args.shell)
|
||||
const isOpenCodeRun =
|
||||
(args.agent === 'opencode' || args.agent === 'opencode2') &&
|
||||
parsed.ok &&
|
||||
isOpenCodeRunCommand(parsed.tokens)
|
||||
const openCodeRun =
|
||||
(args.agent === 'opencode' || args.agent === 'opencode2') && parsed.ok
|
||||
? findOpenCodeRunCommand(parsed.tokens, args.shell)
|
||||
: null
|
||||
// OpenCode run takes a positional message; --prompt belongs to its TUI.
|
||||
const promptSuffix = isOpenCodeRun
|
||||
? parsed.tokens.includes('--')
|
||||
const promptSuffix = openCodeRun
|
||||
? openCodeRun.messageSeparatorIndex !== null
|
||||
? ` ${args.quotedPrompt}`
|
||||
: ` -- ${args.quotedPrompt}`
|
||||
: ` --prompt ${args.quotedPrompt}`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { tokenizeStartupCommand } from './tui-agent-startup-shell'
|
||||
import { tokenizeCommandLine } from './agent-command-line-entrypoint'
|
||||
import { isOpenCodeRunCommand } from './opencode-headless-command'
|
||||
|
||||
@@ -24,3 +25,32 @@ describe('isOpenCodeRunCommand', () => {
|
||||
expect(matches('opencode --log-level run')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrapped OpenCode run command position', () => {
|
||||
it.each([
|
||||
'CUSTOM_CONFIG=private opencode --log-level debug run --standalone',
|
||||
'env CUSTOM_CONFIG=private opencode run --standalone',
|
||||
'CUSTOM_CONFIG=private /usr/bin/env -- EXTRA_CONFIG=kept opencode run --standalone'
|
||||
])('recognizes only the executable behind supported POSIX prefixes: %s', (command) => {
|
||||
expect(matches(command)).toBe(true)
|
||||
})
|
||||
|
||||
it('recognizes a PowerShell call operator before a quoted executable', () => {
|
||||
const parsed = tokenizeStartupCommand(
|
||||
'& "C:\\Program Files\\opencode\\opencode.exe" run --standalone',
|
||||
'powershell'
|
||||
)
|
||||
expect(parsed.ok).toBe(true)
|
||||
if (!parsed.ok) throw new Error(parsed.error)
|
||||
expect(isOpenCodeRunCommand(parsed.tokens, 'powershell')).toBe(true)
|
||||
expect(isOpenCodeRunCommand(parsed.tokens, 'cmd')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not find executable names inside other commands or prompt arguments', () => {
|
||||
expect(matches('env CUSTOM_CONFIG=private echo opencode run')).toBe(false)
|
||||
expect(matches('env CUSTOM_CONFIG=private echo run')).toBe(false)
|
||||
expect(matches('CUSTOM_CONFIG=private opencode --prompt "opencode run"')).toBe(false)
|
||||
expect(matches('env -- opencode serve --title run')).toBe(false)
|
||||
expect(matches('opencode attach http://host/run')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,82 @@
|
||||
// Why: `opencode run` answers one prompt and exits, so its process lifetime is its turn.
|
||||
// Not in agent-headless-command's table: that would also drop OpenCode 1 `run`'s identity,
|
||||
// whose in-process plugin reports it. Only `--log-level` takes a separate value before the
|
||||
// subcommand; any other valued option makes the value the first positional, which fails safe.
|
||||
export function isOpenCodeRunCommand(tokens: readonly string[]): boolean {
|
||||
for (let index = 1; index < tokens.length; index += 1) {
|
||||
import { extractLeadingEnvAssignments } from './command-environment'
|
||||
import { getCommandTokenPathBasename } from './command-token-scanner'
|
||||
import type { AgentStartupShell } from './tui-agent-startup-shell'
|
||||
|
||||
const RUN_VALUE_FLAGS = new Set([
|
||||
'--log-level',
|
||||
'--completions',
|
||||
'--server',
|
||||
'--session',
|
||||
'-s',
|
||||
'--model',
|
||||
'-m',
|
||||
'--agent',
|
||||
'--format',
|
||||
'--file',
|
||||
'-f',
|
||||
'--title'
|
||||
])
|
||||
|
||||
function openCodeCommandPosition(tokens: readonly string[], shell: AgentStartupShell): number {
|
||||
if (shell === 'powershell' && tokens[0] === '&') {
|
||||
return 1
|
||||
}
|
||||
if (shell !== 'posix') {
|
||||
return 0
|
||||
}
|
||||
let index = tokens.length - extractLeadingEnvAssignments(tokens.slice()).rest.length
|
||||
if (getCommandTokenPathBasename(tokens[index] ?? '') === 'env') {
|
||||
index += 1
|
||||
if (tokens[index] === '--') {
|
||||
index += 1
|
||||
}
|
||||
index = tokens.length - extractLeadingEnvAssignments(tokens.slice(index)).rest.length
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
export function findOpenCodeRunCommand(
|
||||
tokens: readonly string[],
|
||||
shell: AgentStartupShell = 'posix'
|
||||
): { runIndex: number; messageSeparatorIndex: number | null } | null {
|
||||
const commandPosition = openCodeCommandPosition(tokens, shell)
|
||||
if (commandPosition > 0) {
|
||||
const binary = getCommandTokenPathBasename(tokens[commandPosition] ?? '')
|
||||
.toLowerCase()
|
||||
.replace(/\.(?:exe|cmd)$/, '')
|
||||
if (binary !== 'opencode' && binary !== 'opencode2') {
|
||||
return null
|
||||
}
|
||||
}
|
||||
for (let index = commandPosition + 1; index < tokens.length; index += 1) {
|
||||
const token = tokens[index]
|
||||
if (!token.startsWith('-')) {
|
||||
return token === 'run'
|
||||
if (token !== 'run') {
|
||||
return null
|
||||
}
|
||||
for (let argumentIndex = index + 1; argumentIndex < tokens.length; argumentIndex += 1) {
|
||||
const argument = tokens[argumentIndex]
|
||||
if (argument === '--') {
|
||||
return { runIndex: index, messageSeparatorIndex: argumentIndex }
|
||||
}
|
||||
if (RUN_VALUE_FLAGS.has(argument)) {
|
||||
argumentIndex += 1
|
||||
}
|
||||
}
|
||||
return { runIndex: index, messageSeparatorIndex: null }
|
||||
}
|
||||
// Other valued global options fail closed at their first positional value.
|
||||
if (token === '--log-level') {
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
// OpenCode run's process lifetime is its turn; v1 still reports through its plugin.
|
||||
export function isOpenCodeRunCommand(
|
||||
tokens: readonly string[],
|
||||
shell: AgentStartupShell = 'posix'
|
||||
): boolean {
|
||||
return findOpenCodeRunCommand(tokens, shell) !== null
|
||||
}
|
||||
|
||||
@@ -105,3 +105,55 @@ describe('native OpenCode startup submission intent', () => {
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrapped OpenCode run startup', () => {
|
||||
it.each(['opencode', 'opencode2'] as const)(
|
||||
'keeps %s run flags and positional task behind POSIX prefixes',
|
||||
(agent) => {
|
||||
for (const command of [
|
||||
'CUSTOM_CONFIG=private opencode run --standalone',
|
||||
'env CUSTOM_CONFIG=private opencode run --standalone',
|
||||
'env -- CUSTOM_CONFIG=private opencode run --standalone',
|
||||
'env -- CUSTOM_CONFIG=private opencode run --standalone --',
|
||||
'env CUSTOM_CONFIG=private opencode run --title "--"'
|
||||
]) {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent,
|
||||
prompt: '--literal task',
|
||||
cmdOverrides: { [agent]: command },
|
||||
platform: 'linux',
|
||||
shell: 'posix',
|
||||
isRemote: true,
|
||||
agentEnv: { CUSTOM_CONFIG: 'kept' }
|
||||
})
|
||||
expect(plan?.launchCommand).toBe(
|
||||
command.endsWith(' --') ? `${command} '--literal task'` : `${command} -- '--literal task'`
|
||||
)
|
||||
expect(plan?.env).toEqual({ CUSTOM_CONFIG: 'kept' })
|
||||
expect(plan?.followupPrompt).toBeNull()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['opencode', 'opencode2'] as const)(
|
||||
'preserves %s PowerShell call syntax and its run separator',
|
||||
(agent) => {
|
||||
for (const separator of ['', ' --']) {
|
||||
const command =
|
||||
'& "C:\\Program Files\\opencode\\opencode.exe" --log-level debug run --standalone' +
|
||||
separator
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent,
|
||||
prompt: "--task's é",
|
||||
cmdOverrides: { [agent]: command },
|
||||
platform: 'win32',
|
||||
shell: 'powershell',
|
||||
agentEnv: { CUSTOM_CONFIG: 'kept' }
|
||||
})
|
||||
expect(plan?.launchCommand).toBe(command + (separator ? ' ' : ' -- ') + "'--task''s é'")
|
||||
expect(plan?.env).toEqual({ CUSTOM_CONFIG: 'kept' })
|
||||
expect(plan?.followupPrompt).toBeNull()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ export function openCodeStartupPromptEnv(
|
||||
if (
|
||||
(agent !== 'opencode' && agent !== 'opencode2') ||
|
||||
!parsed.ok ||
|
||||
isOpenCodeRunCommand(parsed.tokens)
|
||||
isOpenCodeRunCommand(parsed.tokens, shell)
|
||||
) {
|
||||
return env ? { env: { ...env } } : {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user