Reliably deliver the issue prompt into opencode's composer (#6798)

This commit is contained in:
Brennan Benson 2026-06-30 01:16:55 -07:00 committed by GitHub
parent f6db7897d7
commit 973360cfce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 442 additions and 76 deletions

View File

@ -184,6 +184,7 @@ import {
resolveTuiAgentLaunchEnv
} from '../../shared/tui-agent-launch-defaults'
import { isTuiAgent, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config'
import { createDraftPasteReadyScanner } from '../../shared/draft-paste-ready-scanner'
import { detectInstalledAgentsWithShellPathHydration, detectRemoteAgents } from '../ipc/preflight'
import {
markCodexProjectTrusted,
@ -1072,8 +1073,6 @@ function getAgentLaunchPlatformForRepo(
const FOREGROUND_AGENT_WRAPPER_RETRY_INTERVAL_MS = 150
const FOREGROUND_AGENT_WRAPPER_RETRY_TIMEOUT_MS = 6_500
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
const CODEX_COMPOSER_PROMPT = ''
const BRACKETED_PASTE_BEGIN = '\x1b[200~'
const BRACKETED_PASTE_END = '\x1b[201~'
const BRACKETED_PASTE_QUIET_MS = 1500
@ -12303,9 +12302,7 @@ export class OrcaRuntimeService {
TUI_AGENT_CONFIG[agent].draftPasteReadySignal ?? 'render-quiet-after-bracketed-paste'
return new Promise<string | null>((resolve) => {
let settled = false
let recent = ''
let postHandshakeRecent = ''
let saw2004 = false
const scanner = createDraftPasteReadyScanner(readySignal)
let quietTimer: NodeJS.Timeout | null = null
let hardTimer: NodeJS.Timeout | null = null
let unsubscribe: (() => void) | null = null
@ -12333,36 +12330,12 @@ export class OrcaRuntimeService {
}
const observeData = (data: string): void => {
const combined = recent + data
recent = combined.slice(-512)
if (!saw2004) {
const markerIndex = combined.indexOf(DECSET_BRACKETED_PASTE)
if (markerIndex === -1) {
return
}
saw2004 = true
const postHandshakeChunk = combined.slice(markerIndex + DECSET_BRACKETED_PASTE.length)
if (readySignal === 'codex-composer-prompt') {
if (postHandshakeChunk.includes(CODEX_COMPOSER_PROMPT)) {
finish(ptyId)
return
}
postHandshakeRecent = postHandshakeChunk.slice(-512)
return
}
postHandshakeRecent = postHandshakeChunk.slice(-512)
} else {
if (
readySignal === 'codex-composer-prompt' &&
(data.includes(CODEX_COMPOSER_PROMPT) ||
(postHandshakeRecent + data).includes(CODEX_COMPOSER_PROMPT))
) {
finish(ptyId)
return
}
postHandshakeRecent = (postHandshakeRecent + data).slice(-512)
const { ready, armQuietTimer: shouldArm } = scanner.observe(data)
if (ready) {
finish(ptyId)
return
}
if (readySignal !== 'codex-composer-prompt' && saw2004) {
if (shouldArm) {
armQuietTimer()
}
}

View File

@ -3,9 +3,8 @@ import type { GlobalSettings } from '../../../shared/types'
import { subscribeToPtyData } from '@/components/terminal-pane/pty-data-sidecar-subscriptions'
import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection'
import { subscribeToRuntimeTerminalData } from '@/runtime/runtime-terminal-stream'
import { createDraftPasteReadyScanner } from '../../../shared/draft-paste-ready-scanner'
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
const CODEX_COMPOSER_PROMPT = ''
const BRACKETED_PASTE_QUIET_MS = 1500
/**
@ -26,9 +25,7 @@ export function waitForAgentDraftInputReady(
): Promise<boolean> {
return new Promise<boolean>((resolve) => {
let settled = false
let recent = ''
let postHandshakeRecent = ''
let saw2004 = false
const scanner = createDraftPasteReadyScanner(readySignal)
let quietTimer: number | null = null
let hardTimer: number | null = null
let unsubscribe: (() => void) | null = null
@ -56,41 +53,12 @@ export function waitForAgentDraftInputReady(
}
const observeData = (data: string): void => {
// Why: 512 bytes covers split escape sequences and Codex's styled prompt
// without retaining a large terminal scrollback copy.
const combined = recent + data
recent = combined.slice(-512)
if (!saw2004) {
const markerIndex = combined.indexOf(DECSET_BRACKETED_PASTE)
if (markerIndex === -1) {
return
}
saw2004 = true
const postHandshakeChunk = combined.slice(markerIndex + DECSET_BRACKETED_PASTE.length)
if (readySignal === 'codex-composer-prompt') {
if (postHandshakeChunk.includes(CODEX_COMPOSER_PROMPT)) {
finish(true)
return
}
postHandshakeRecent = postHandshakeChunk.slice(-512)
return
}
postHandshakeRecent = postHandshakeChunk.slice(-512)
} else {
if (
readySignal === 'codex-composer-prompt' &&
(data.includes(CODEX_COMPOSER_PROMPT) ||
(postHandshakeRecent + data).includes(CODEX_COMPOSER_PROMPT))
) {
finish(true)
return
}
postHandshakeRecent = (postHandshakeRecent + data).slice(-512)
}
if (readySignal === 'codex-composer-prompt') {
const { ready, armQuietTimer: shouldArm } = scanner.observe(data)
if (ready) {
finish(true)
return
}
if (saw2004) {
if (shouldArm) {
armQuietTimer()
}
}

View File

@ -52,6 +52,7 @@ vi.mock('@/runtime/runtime-terminal-stream', () => ({
}))
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
const SHOW_CURSOR = '\x1b[?25h'
const CODEX_COMPOSER_PROMPT_RENDER = '\x1b[1m\x1b[0m Ask Codex to do anything'
const ISSUE_URL = 'https://github.com/stablyai/orca/issues/123'
const PASTED_ISSUE_URL = `\x1b[200~${ISSUE_URL}\x1b[201~`
@ -146,7 +147,7 @@ describe('pasteDraftWhenAgentReady', () => {
const promise = pasteDraftWhenAgentReady({
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'opencode'
agent: 'gemini'
})
await flushMicrotasks()
@ -167,6 +168,156 @@ describe('pasteDraftWhenAgentReady', () => {
)
})
it('pastes into opencode as soon as show-cursor renders after bracketed paste is enabled', async () => {
const promise = pasteDraftWhenAgentReady({
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'opencode'
})
await flushMicrotasks()
testState.ptyObserver?.(DECSET_BRACKETED_PASTE)
await flushMicrotasks()
expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled()
testState.ptyObserver?.(SHOW_CURSOR)
await expect(promise).resolves.toBe(true)
expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith(
{},
'pty-1',
PASTED_ISSUE_URL
)
expect(vi.getTimerCount()).toBe(0)
})
it('detects opencode show-cursor inside a large first render chunk', async () => {
const promise = pasteDraftWhenAgentReady({
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'opencode'
})
await flushMicrotasks()
testState.ptyObserver?.(`${DECSET_BRACKETED_PASTE}${SHOW_CURSOR}${'x'.repeat(900)}`)
await expect(promise).resolves.toBe(true)
expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith(
{},
'pty-1',
PASTED_ISSUE_URL
)
})
it('detects opencode show-cursor split across a later chunk', async () => {
const promise = pasteDraftWhenAgentReady({
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'opencode'
})
await flushMicrotasks()
testState.ptyObserver?.(DECSET_BRACKETED_PASTE)
await flushMicrotasks()
testState.ptyObserver?.('render noise \x1b[?')
await flushMicrotasks()
expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled()
testState.ptyObserver?.('25h')
await expect(promise).resolves.toBe(true)
expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith(
{},
'pty-1',
PASTED_ISSUE_URL
)
})
it('rescues opencode delivery under never-settling output churn', async () => {
const promise = pasteDraftWhenAgentReady({
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'opencode'
})
await flushMicrotasks()
testState.ptyObserver?.(DECSET_BRACKETED_PASTE)
await flushMicrotasks()
for (let index = 0; index < 5; index += 1) {
await vi.advanceTimersByTimeAsync(1499)
testState.ptyObserver?.(`setup output ${index}`)
await flushMicrotasks()
expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled()
}
testState.ptyObserver?.(SHOW_CURSOR)
await expect(promise).resolves.toBe(true)
expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith(
{},
'pty-1',
PASTED_ISSUE_URL
)
expect(vi.getTimerCount()).toBe(0)
})
it('does not paste on the quiet window for opencode (it never arms one)', async () => {
// Why: opencode is silent for ~1.5-2s between enabling bracketed paste and
// mounting its composer. A quiet window would fire during that gap and paste
// before the composer exists (the original bug), so the cursor signal must
// not arm one. With process inspection failing, delivery times out instead.
testState.inspectRuntimeTerminalProcess.mockResolvedValue(null)
const onTimeout = vi.fn()
const promise = pasteDraftWhenAgentReady({
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'opencode',
onTimeout
})
await flushMicrotasks()
testState.ptyObserver?.(DECSET_BRACKETED_PASTE)
await flushMicrotasks()
// Quiet-window duration elapses with no show-cursor: must NOT paste.
await vi.advanceTimersByTimeAsync(1500)
await flushMicrotasks()
expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled()
// Only the hard timeout (and failed process check) resolves it — to false.
await vi.advanceTimersByTimeAsync(8000)
await flushMicrotasks(5)
await vi.advanceTimersByTimeAsync(1000)
await expect(promise).resolves.toBe(false)
expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled()
expect(onTimeout).toHaveBeenCalledTimes(1)
})
it('best-effort pastes for opencode at the hard timeout when its process is running', async () => {
// Why: with no quiet window, the hard-timeout process-ownership check is the
// backstop if show-cursor is somehow missed — same model as Codex.
testState.inspectRuntimeTerminalProcess.mockResolvedValue({
foregroundProcess: 'opencode',
hasChildProcesses: false
})
const promise = pasteDraftWhenAgentReady({
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'opencode'
})
await flushMicrotasks()
testState.ptyObserver?.(DECSET_BRACKETED_PASTE)
await vi.advanceTimersByTimeAsync(8000)
await expect(promise).resolves.toBe(true)
expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith(
{},
'pty-1',
PASTED_ISSUE_URL
)
})
it('does not paste for agents that already use native draft prefill', async () => {
await expect(
pasteDraftWhenAgentReady({

View File

@ -0,0 +1,138 @@
import { describe, expect, it } from 'vitest'
import { createDraftPasteReadyScanner } from './draft-paste-ready-scanner'
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
const SHOW_CURSOR = '\x1b[?25h'
const HIDE_CURSOR = '\x1b[?25l'
const CODEX_PROMPT = '\x1b[1m\x1b[0m Ask Codex to do anything'
describe('createDraftPasteReadyScanner', () => {
describe('render-cursor-after-bracketed-paste (opencode / mimo-code)', () => {
it('is ready when show-cursor renders after bracketed paste in one chunk', () => {
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
expect(scanner.observe(`${DECSET_BRACKETED_PASTE}${SHOW_CURSOR}`)).toEqual({
ready: true,
armQuietTimer: false
})
})
it('does not fire on bracketed paste alone, then fires once show-cursor arrives', () => {
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
// Why: opencode enables bracketed paste ~1.5-2s before its composer mounts
// and stays SILENT in between. The cursor gates delivery and must NOT arm
// the quiet window, which would otherwise fire during that silent gap and
// paste before the composer exists.
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({
ready: false,
armQuietTimer: false
})
expect(scanner.observe('startup banner output')).toEqual({
ready: false,
armQuietTimer: false
})
expect(scanner.observe(SHOW_CURSOR)).toEqual({ ready: true, armQuietTimer: false })
})
it('resolves from a single replayed buffer holding both markers (SSH/remote replay path)', () => {
// Why: the runtime waiter feeds recentPtyOutputById as one observe() call
// when the agent emitted 2004 + show-cursor before the subscription
// attached; a single combined buffer must still resolve.
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
expect(
scanner.observe(`banner\n${DECSET_BRACKETED_PASTE}composer\n${SHOW_CURSOR}rest`)
).toEqual({ ready: true, armQuietTimer: false })
})
it('detects a bracketed-paste handshake split across a chunk boundary', () => {
// Why: the pre-handshake `recent` ring must reassemble a \x1b[?2004h that
// straddles two PTY packets, or cursor-gated readiness breaks for
// fragmented startup output.
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
expect(scanner.observe('\x1b[?20')).toEqual({ ready: false, armQuietTimer: false })
expect(scanner.observe('04h')).toEqual({ ready: false, armQuietTimer: false })
expect(scanner.observe(SHOW_CURSOR)).toEqual({ ready: true, armQuietTimer: false })
})
it('detects show-cursor split across a later chunk boundary', () => {
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
scanner.observe(DECSET_BRACKETED_PASTE)
// The escape sequence is split mid-bytes across two separate chunks.
expect(scanner.observe('render noise \x1b[?')).toEqual({ ready: false, armQuietTimer: false })
expect(scanner.observe('25h')).toEqual({ ready: true, armQuietTimer: false })
})
it('never arms the quiet window during the silent pre-composer gap', () => {
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
scanner.observe(DECSET_BRACKETED_PASTE)
// Why: opencode is silent here; arming the quiet window would fire before
// the composer mounts and pre-empt the cursor signal (the original bug).
// Delivery waits for show-cursor, bounded by the caller's hard timeout.
for (let i = 0; i < 5; i += 1) {
expect(scanner.observe(`setup output ${i}`)).toEqual({ ready: false, armQuietTimer: false })
}
})
it('does not treat hide-cursor as the ready signal', () => {
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
// \x1b[?25l (hide) must not be mistaken for \x1b[?25h (show).
expect(scanner.observe(`${DECSET_BRACKETED_PASTE}${HIDE_CURSOR}`)).toEqual({
ready: false,
armQuietTimer: false
})
})
it('ignores show-cursor that appears before bracketed paste is enabled', () => {
const scanner = createDraftPasteReadyScanner('render-cursor-after-bracketed-paste')
// A pre-handshake cursor toggle must not trip readiness.
expect(scanner.observe(SHOW_CURSOR)).toEqual({ ready: false, armQuietTimer: false })
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({
ready: false,
armQuietTimer: false
})
})
})
describe('codex-composer-prompt (unchanged behavior)', () => {
it('is ready on the composer glyph after bracketed paste and never arms the quiet timer', () => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({
ready: false,
armQuietTimer: false
})
expect(scanner.observe(CODEX_PROMPT)).toEqual({ ready: true, armQuietTimer: false })
})
it('detects the composer glyph inside a large first render chunk', () => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
expect(scanner.observe(`${DECSET_BRACKETED_PASTE}${CODEX_PROMPT}${'x'.repeat(900)}`)).toEqual(
{ ready: true, armQuietTimer: false }
)
})
it('never arms the quiet-window fallback', () => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({
ready: false,
armQuietTimer: false
})
expect(scanner.observe('noise')).toEqual({ ready: false, armQuietTimer: false })
})
})
describe('render-quiet-after-bracketed-paste (default)', () => {
it('arms the quiet timer after bracketed paste and never reports a signal', () => {
const scanner = createDraftPasteReadyScanner('render-quiet-after-bracketed-paste')
expect(scanner.observe(DECSET_BRACKETED_PASTE)).toEqual({ ready: false, armQuietTimer: true })
// Show-cursor is not a signal for the default path; it just keeps arming.
expect(scanner.observe(SHOW_CURSOR)).toEqual({ ready: false, armQuietTimer: true })
})
it('does nothing until bracketed paste is enabled', () => {
const scanner = createDraftPasteReadyScanner('render-quiet-after-bracketed-paste')
expect(scanner.observe('pre-handshake output')).toEqual({
ready: false,
armQuietTimer: false
})
})
})
})

View File

@ -0,0 +1,94 @@
import type { DraftPasteReadySignal } from './tui-agent-config'
// Why: agents enable bracketed paste (DECSET 2004) before their composer is
// actually mounted/focused. These markers let the scanner detect the real
// "input is ready" moment per agent instead of guessing from output silence.
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
const CODEX_COMPOSER_PROMPT = ''
// Why: opencode emits the DECTCEM show-cursor only once the composer row is
// mounted and the text cursor is placed in it — a "composer ready" signal,
// analogous to Codex's prompt glyph. It fires ~2s after bracketed paste is
// enabled, so gating on it (instead of a quiet window) stops the paste from
// racing the composer mount under slow/noisy startup. mimo-code uses the same
// signal by parity; the quiet-window fallback covers any agent that differs.
const DECTCEM_SHOW_CURSOR = '\x1b[?25h'
export type DraftPasteReadyScanResult = {
/** The agent-specific ready signal fired — caller should deliver the paste now. */
ready: boolean
/** Caller should (re)arm the quiet-window fallback timer for this chunk. */
armQuietTimer: boolean
}
/**
* Pure, incremental scanner shared by the renderer and main-process draft-paste
* readiness waiters so the two delivery paths (desktop-local vs runtime/SSH/
* remote) cannot drift. It only parses the PTY byte stream; timers, the PTY
* subscription, and resolution stay with each caller because their transports
* and return types differ.
*
* Per agent signal:
* - `codex-composer-prompt`: ready when the `` glyph renders after DECSET
* 2004; never arms the quiet window (`armQuietTimer` stays false).
* - `render-cursor-after-bracketed-paste`: ready when DECTCEM show-cursor
* (`\x1b[?25h`) renders after DECSET 2004. Like Codex it does NOT arm the
* quiet window: opencode stays silent for ~1.5-2s between enabling
* bracketed paste and mounting its composer, so a quiet window would fire
* during that gap and pre-empt the marker. opencode re-emits show-cursor on
* every render frame once mounted, so the marker is effectively guaranteed;
* the caller's hard timeout is the backstop if it never appears.
* - `render-quiet-after-bracketed-paste` (default): no signal marker; arms the
* quiet window once DECSET 2004 is seen.
*
* A 512-byte ring (`recent` / `postHandshakeRecent`) covers escape sequences
* split across chunk boundaries without retaining terminal scrollback.
*/
export function createDraftPasteReadyScanner(readySignal: DraftPasteReadySignal): {
observe: (data: string) => DraftPasteReadyScanResult
} {
let recent = ''
let postHandshakeRecent = ''
let saw2004 = false
const signalMarker =
readySignal === 'codex-composer-prompt'
? CODEX_COMPOSER_PROMPT
: readySignal === 'render-cursor-after-bracketed-paste'
? DECTCEM_SHOW_CURSOR
: null
return {
observe(data: string): DraftPasteReadyScanResult {
const combined = recent + data
recent = combined.slice(-512)
if (!saw2004) {
const markerIndex = combined.indexOf(DECSET_BRACKETED_PASTE)
if (markerIndex === -1) {
return { ready: false, armQuietTimer: false }
}
saw2004 = true
const postHandshakeChunk = combined.slice(markerIndex + DECSET_BRACKETED_PASTE.length)
if (signalMarker !== null && postHandshakeChunk.includes(signalMarker)) {
return { ready: true, armQuietTimer: false }
}
postHandshakeRecent = postHandshakeChunk.slice(-512)
} else {
if (
signalMarker !== null &&
(data.includes(signalMarker) || (postHandshakeRecent + data).includes(signalMarker))
) {
return { ready: true, armQuietTimer: false }
}
postHandshakeRecent = (postHandshakeRecent + data).slice(-512)
}
// Why: marker-based signals (Codex glyph, opencode show-cursor) must NOT
// arm the quiet window. opencode goes silent for ~1.5-2s between enabling
// bracketed paste and mounting its composer, so a quiet window would fire
// during that gap — before the composer exists — and pre-empt the marker.
// These signals wait for their marker, bounded only by the caller's hard
// timeout (and the caller's best-effort process-ownership paste after it).
// Only the default signal, which has no marker, uses the quiet window.
return { ready: false, armQuietTimer: signalMarker === null && saw2004 }
}
}
}

View File

@ -8,7 +8,10 @@ export type AgentPromptInjectionMode =
| 'flag-interactive'
| 'stdin-after-start'
export type DraftPasteReadySignal = 'render-quiet-after-bracketed-paste' | 'codex-composer-prompt'
export type DraftPasteReadySignal =
| 'render-quiet-after-bracketed-paste'
| 'codex-composer-prompt'
| 'render-cursor-after-bracketed-paste'
export type TuiAgentConfig = {
detectCmd: string
@ -114,13 +117,20 @@ export const TUI_AGENT_CONFIG: Record<TuiAgent, TuiAgentConfig> = {
detectCmd: 'opencode',
launchCmd: 'opencode',
expectedProcess: 'opencode',
promptInjectionMode: 'flag-prompt'
promptInjectionMode: 'flag-prompt',
// Why: opencode enables bracketed paste before its composer mounts; wait
// for post-\x1b[?2004h show-cursor (\x1b[?25h) so paste hits mounted input.
draftPasteReadySignal: 'render-cursor-after-bracketed-paste'
},
'mimo-code': {
detectCmd: 'mimo',
launchCmd: 'mimo',
expectedProcess: 'mimo',
promptInjectionMode: 'flag-prompt'
promptInjectionMode: 'flag-prompt',
// Why: mimo-code shares opencode's flag-prompt paste route, so it gets the
// same cursor-gated signal by parity (its startup stream is not separately
// validated); the quiet-window fallback bounds the risk if it differs.
draftPasteReadySignal: 'render-cursor-after-bracketed-paste'
},
pi: {
detectCmd: 'pi',

View File

@ -5,6 +5,7 @@ import {
buildAgentStartupPlan,
buildShellCommandFromArgv
} from './tui-agent-startup'
import { TUI_AGENT_CONFIG } from './tui-agent-config'
import { normalizeTuiAgentArgsRecord, resolveTuiAgentLaunchArgs } from './tui-agent-launch-defaults'
describe('tui agent startup plans', () => {
@ -336,6 +337,37 @@ describe('tui agent startup plans', () => {
expect(plan?.launchCommand).toBe("opencode --prompt 'fix it'")
})
it('keeps opencode and mimo-code on the cursor-gated paste draft route', () => {
expect(TUI_AGENT_CONFIG.opencode.draftPasteReadySignal).toBe(
'render-cursor-after-bracketed-paste'
)
expect(TUI_AGENT_CONFIG.opencode.draftPromptFlag).toBeUndefined()
expect(TUI_AGENT_CONFIG.opencode.draftPromptEnvVar).toBeUndefined()
expect(TUI_AGENT_CONFIG['mimo-code'].draftPasteReadySignal).toBe(
'render-cursor-after-bracketed-paste'
)
expect(TUI_AGENT_CONFIG['mimo-code'].draftPromptFlag).toBeUndefined()
expect(TUI_AGENT_CONFIG['mimo-code'].draftPromptEnvVar).toBeUndefined()
// Why: no native draft launch plan means both agents fall through to the
// cursor-gated paste-after-ready route, where the new signal applies.
expect(
buildAgentDraftLaunchPlan({
agent: 'opencode',
draft: 'x',
cmdOverrides: {},
platform: 'darwin'
})
).toBeNull()
expect(
buildAgentDraftLaunchPlan({
agent: 'mimo-code',
draft: 'x',
cmdOverrides: {},
platform: 'darwin'
})
).toBeNull()
})
it('appends Kiro trust defaults to the chat subcommand that accepts them', () => {
const plan = buildAgentStartupPlan({
agent: 'kiro',