mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
* fix(automations): deliver prompt to Hermes TUI via process-ready signal Hermes's prompt_toolkit TUI never emits the DECSET 2004 bracketed-paste handshake that the default draft-paste readiness waiter gates on, so the automation prompt was silently dropped and the agent sat idle (terminal opens, nothing entered). Add a 'process-ready' DraftPasteReadySignal that arms the quiet-window on first PTY output and pastes once the TUI settles, and assign it to the hermes agent config. Adds a Hermes unit test covering the no-handshake path. Design for the follow-up PR/issue review loop lives in docs/design/pr-issue-review-loop.md (not committed; gitignored). * fix(automations): skip broken process-name fallback for process-ready CodeRabbit: the process-ready fallback consulted waitForAgentReady, which compares the foreground process basename via isExpectedAgentProcess. Wrapped interpreter launches surface as 'python3 .../hermes', so the check can never confirm readiness and only drops the paste. process-ready readiness is the PTY-quiet window (handled by waitForAgentDraftInputReady); the process-name fallback is dead for this signal. Skip it and return false on timeout instead. * test(automations): assert single paste for Hermes quiet-window path Guard against a duplicate paste in the no-handshake Hermes path by asserting sendRuntimePtyInputVerified is called exactly once. Addresses the CodeRabbit nitpick on PR #7862. * fix(automations): skip broken process-name fallback on pty-bound paste path pasteDraftToAgentPtyWhenReady (quick-create/work-item route) still ran the python3-vs-hermes process-name fallback that can never match for process-ready agents, burning ~1s and dropping the paste. Mirror the process-ready guard from pasteDraftWhenAgentReady and surface onTimeout. Adds regression tests for both the quiet-window happy path and the fallback-skip on the pty-bound path. Co-authored-by: Orca <help@stably.ai> * fix(automations): defer submit Enter until the Hermes TUI is interactive Live-testing against Hermes v0.18.2 showed the fixed 50ms post-paste Enter is swallowed: the node ui-tui takes 15s+ to boot, the paste fires ~1.5s in (process-ready quiet window), and the cooked-mode line discipline turns the early \r into \n, which the editor treats as newline-insert. The prompt parked in the input box and the automation never executed. For process-ready agents, defer the Enter until the TUI signals interactivity (DECSET 2004 enable) or echoes the pasted content marker-free (legacy prompt_toolkit), with a 120s best-effort cap. PTY input is FIFO, so the deferred Enter always lands after the buffered paste text. The paste echo of the content itself (raw or caret-notation markers adjacent) is rejected so a cooked-mode echo can't release the Enter early. Verified end-to-end in the dev app: automation prompt pasted, deferred Enter released on tui-ready, Hermes submitted and ran the turn. Co-authored-by: Orca <help@stably.ai> * fix(automations): use Hermes native startup query Co-authored-by: Orca <help@stably.ai> * fix(automations): preserve quoted Hermes queries on Windows * fix(agents): preserve invalid argument error message Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Brandon Bennett <brandonbennett@macbookair.myfiosgateway.com> Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai> Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
131 lines
3.4 KiB
TypeScript
131 lines
3.4 KiB
TypeScript
import { tokenizeCustomCommandTemplate } from './commit-message-prompt'
|
|
|
|
export type AgentStartupShell = 'posix' | 'powershell' | 'cmd'
|
|
|
|
export type StartupCommandTokens = { ok: true; tokens: string[] } | { ok: false; error: string }
|
|
|
|
function tokenizeWindowsStartupCommand(
|
|
value: string,
|
|
shell: Exclude<AgentStartupShell, 'posix'>
|
|
): StartupCommandTokens {
|
|
const tokens: string[] = []
|
|
let token = ''
|
|
let quote: "'" | '"' | null = null
|
|
let tokenStarted = false
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
const char = value[index]
|
|
const escape = shell === 'cmd' ? '^' : '`'
|
|
if (char === escape && index + 1 < value.length) {
|
|
token += value[index + 1]
|
|
tokenStarted = true
|
|
index += 1
|
|
continue
|
|
}
|
|
if (quote) {
|
|
if (char === quote) {
|
|
if (shell === 'powershell' && quote === "'" && value[index + 1] === "'") {
|
|
token += "'"
|
|
index += 1
|
|
} else {
|
|
quote = null
|
|
}
|
|
} else {
|
|
token += char
|
|
}
|
|
tokenStarted = true
|
|
continue
|
|
}
|
|
if (char === "'" || char === '"') {
|
|
quote = char
|
|
tokenStarted = true
|
|
} else if (/\s/.test(char)) {
|
|
if (tokenStarted) {
|
|
tokens.push(token)
|
|
token = ''
|
|
tokenStarted = false
|
|
}
|
|
} else {
|
|
token += char
|
|
tokenStarted = true
|
|
}
|
|
}
|
|
if (quote) {
|
|
return { ok: false, error: 'Unclosed quote in command template.' }
|
|
}
|
|
if (tokenStarted) {
|
|
tokens.push(token)
|
|
}
|
|
return { ok: true, tokens }
|
|
}
|
|
|
|
export function tokenizeStartupCommand(
|
|
value: string,
|
|
shell: AgentStartupShell
|
|
): StartupCommandTokens {
|
|
return shell === 'posix'
|
|
? tokenizeCustomCommandTemplate(value)
|
|
: tokenizeWindowsStartupCommand(value, shell)
|
|
}
|
|
|
|
export function resolveStartupShell(
|
|
platform: NodeJS.Platform,
|
|
shell?: AgentStartupShell
|
|
): AgentStartupShell {
|
|
return shell ?? (platform === 'win32' ? 'powershell' : 'posix')
|
|
}
|
|
|
|
export function quoteStartupArg(value: string, shell: AgentStartupShell): string {
|
|
if (shell === 'powershell') {
|
|
return `'${value.replace(/'/g, "''")}'`
|
|
}
|
|
if (shell === 'cmd') {
|
|
return `"${value.replace(/([\^&|<>()%!"])/g, '^$1')}"`
|
|
}
|
|
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
}
|
|
|
|
export function buildShellCommandFromArgv(
|
|
args: readonly string[],
|
|
shell: AgentStartupShell
|
|
): string {
|
|
const command = args.map((arg) => quoteStartupArg(arg, shell)).join(' ')
|
|
if (shell === 'powershell' && command) {
|
|
return `& ${command}`
|
|
}
|
|
return command
|
|
}
|
|
|
|
export function clearEnvCommand(name: string, shell: AgentStartupShell): string {
|
|
if (shell === 'powershell') {
|
|
return `Remove-Item Env:${name} -ErrorAction SilentlyContinue`
|
|
}
|
|
if (shell === 'cmd') {
|
|
return `set "${name}="`
|
|
}
|
|
return `unset ${name}`
|
|
}
|
|
|
|
export function commandSeparator(shell: AgentStartupShell): string {
|
|
return shell === 'cmd' ? ' & ' : '; '
|
|
}
|
|
|
|
export type AgentCliArgsPlan = { ok: true; suffix: string } | { ok: false; error: string }
|
|
|
|
export function planAgentCliArgsSuffix(
|
|
agentArgs: string | null | undefined,
|
|
shell: AgentStartupShell
|
|
): AgentCliArgsPlan {
|
|
const trimmed = agentArgs?.trim()
|
|
if (!trimmed) {
|
|
return { ok: true, suffix: '' }
|
|
}
|
|
const tokenized = tokenizeStartupCommand(trimmed, shell)
|
|
if (!tokenized.ok) {
|
|
return { ok: false, error: `CLI arguments are invalid: ${tokenized.error}` }
|
|
}
|
|
return {
|
|
ok: true,
|
|
suffix: tokenized.tokens.map((token) => quoteStartupArg(token, shell)).join(' ')
|
|
}
|
|
}
|