fix(terminal): key agent-paste bracketing on live evidence, not shellForeground

Measured against a real pane: shellForeground is republished only at OSC 133
boundaries, so a shell without 133 integration leaves it latched true while an
agent owns the foreground. Vetoing on it silently reinstated the submit bug the
parent commit fixes - the parked draft was sent on paste with the gate in place.

Prefer process-confirmed agent identity when present, keep the restart-rehydrated
veto, and drop the shellForeground veto. Erring toward bracketing costs a literal
ESC[200~ in a non-2004 program; erring the other way sends the user's draft.

Refs STA-4294
This commit is contained in:
Brennan Benson
2026-08-13 22:44:18 -07:00
parent 0d8259e2af
commit 4eb92f2852
2 changed files with 19 additions and 16 deletions
@@ -76,16 +76,15 @@ describe('shouldForceBracketedMultilinePasteForPane', () => {
).toBe(false)
})
// Staleness: the agent row is retained on purpose and is not lifecycle-driven, so it
// can outlive the agent. Only evidence the pane is no longer the agent's may veto.
it('vetoes when OSC 133;D proved the foreground is back at the shell', () => {
it('brackets on a process-confirmed agent even with no status row', () => {
expect(
decide({
agentStatusByPaneKey: {},
paneForegroundAgentByPaneKey: {
[AGENT_PANE_KEY]: foregroundEntry({ shellForeground: true })
[AGENT_PANE_KEY]: foregroundEntry({ agent: 'codex', shellForeground: false })
}
})
).toBe(false)
).toBe(true)
})
it('vetoes a row rehydrated from disk across an app restart', () => {
@@ -111,11 +110,14 @@ describe('shouldForceBracketedMultilinePasteForPane', () => {
).toBe(true)
})
it('still brackets when foreground evidence exists but has not seen a shell', () => {
it('still brackets when shellForeground is latched true while an agent owns the pane', () => {
// Why: shellForeground is republished only at OSC 133 boundaries, so a shell with
// no 133 integration leaves it true while an agent runs. Measured live: vetoing on
// it reinstated the submit bug.
expect(
decide({
paneForegroundAgentByPaneKey: {
[AGENT_PANE_KEY]: foregroundEntry({ agent: 'codex', shellForeground: false })
[AGENT_PANE_KEY]: foregroundEntry({ shellForeground: true })
}
})
).toBe(true)
@@ -8,11 +8,6 @@ import type { PaneForegroundAgentEntry } from '../../store/slices/pane-foregroun
* ConPTY never forwards it and that ConPTY can be on a remote host — so the client's
* platform proves nothing about the PTY. Unbracketed, xterm rewrites the paste's
* newlines to CR and the agent submits whatever draft was parked in its composer.
*
* An agent row is retained on purpose (an idle agent sits at `done`), so neither the
* 30-minute freshness TTL nor `state !== 'done'` can gate this — both would strip
* bracketing from a live but quiet agent. Only evidence that the pane is *no longer*
* the agent's vetoes it.
*/
export function shouldForceBracketedMultilinePasteForPane({
isWindowsClient,
@@ -31,10 +26,9 @@ export function shouldForceBracketedMultilinePasteForPane({
return true
}
const paneKey = makePaneKey(tabId, leafId)
// Why: OSC 133;D proved the foreground is back at the shell — process-grade evidence
// that outranks an agent row nothing has cleared yet.
if (paneForegroundAgentByPaneKey[paneKey]?.shellForeground === true) {
return false
// Why: a process-table confirmed agent is the strongest evidence there is.
if (isTuiAgent(paneForegroundAgentByPaneKey[paneKey]?.agent)) {
return true
}
const entry = agentStatusByPaneKey[paneKey]
// Why: a row rehydrated from disk across a restart describes the previous process,
@@ -42,5 +36,12 @@ export function shouldForceBracketedMultilinePasteForPane({
if (entry?.restoredUnconfirmed === true) {
return false
}
// Why NOT vetoed on shellForeground: that flag is republished only at OSC 133
// boundaries, so a shell without 133 integration leaves it latched true while an
// agent owns the foreground. Vetoing on it silently reinstates the submit bug —
// measured live. An idle agent also sits at `done` past the 30-minute freshness
// TTL, so neither state nor TTL can gate this either. Erring toward bracketing
// costs a literal ESC[200~ in a non-2004 program; erring the other way sends the
// user's parked draft.
return isTuiAgent(entry?.agentType)
}