fix(omp): preserve owner-rewritten terminal state markers

Keep state, identity and the opaque session label when native OMP titles
are rewritten for the launch owner. Reuse the native marker parser for
ASCII brands, stale-title clearing and idempotent owner normalization.

Fixes #17690. Secondary comparison with #17723 corroborates the separator
and identity repair; credit its author for that direction.

Co-authored-by: shahidbeig-a11y <258701601+shahidbeig-a11y@users.noreply.github.com>
This commit is contained in:
Neil
2026-09-14 04:26:24 -07:00
co-authored by shahidbeig-a11y
parent b87a6c0f23
commit fc92f75395
10 changed files with 160 additions and 3 deletions
@@ -0,0 +1,13 @@
{
"capturedAt": "2026-09-14T11:25:01.730Z",
"platform": "darwin",
"command": [
"bun",
"tests/tools/omp-native-title-capture.mjs",
"<read-only-omp-checkout>"
],
"cols": 100,
"rows": 30,
"note": "OMP source ne7546987ca526eac8f605fac19ef9805b8f01898 buildTerminalTitleWithState; explicit win32 argument on macOS PTY, synthetic state transitions, no model/account. Not a Windows runtime capture.",
"exitCode": 0
}
@@ -0,0 +1 @@
]0;π : Run a long task]0;π : release | π : note | OMP ! action required ✦]0;π > Run a long task]0;π > release | π : note | OMP ! action required ✦]0;π ! Run a long task]0;π ! release | π : note | OMP ! action required ✦
@@ -90,6 +90,26 @@ describe('buildTitleDerivedAgentRows', () => {
])
})
it.each([
[':', 'working'],
['>', 'idle'],
['!', 'waiting']
])('retains hook-less OMP rows for owner marker %s', (marker, state) => {
const title = `OMP ${marker} Run a long task`
const rows = buildWorktreeAgentRows({
tabs: [makeTab('tab-1', { launchAgent: 'omp' })],
entries: [],
retained: [],
runtimePaneTitlesByTabId: { 'tab-1': { 1: title } },
ptyIdsByTabId: { 'tab-1': ['pty-omp'] },
terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
now: 2000
})
expect(rows.map((row) => [row.agentType, row.state, row.entry.terminalTitle])).toEqual([
['omp', state, title]
])
})
it('keeps Pi-compatible title-derived rows as Pi for launched Pi sessions', () => {
const rows = buildWorktreeAgentRows({
tabs: [makeTab('tab-1', { launchAgent: 'pi' })],
@@ -1,3 +1,4 @@
import { readFileSync } from 'node:fs'
// Why: Phase 3 slice 1 of terminal-side-effect-authority.md runs a per-PTY
// title tracker in main alongside the renderer transport's byte parser. Both
// must derive IDENTICAL ordered title/status facts from the same bytes, or
@@ -103,6 +104,19 @@ describe('main title tracker parity with the renderer transport processor', () =
vi.useRealTimers()
})
it('agrees on captured OMP native frames before and after owner rebranding', () => {
const captured = readFileSync(
new URL('../../../../main/runtime/__fixtures__/omp-native-title-win32.txt', import.meta.url),
'utf8'
)
feedBoth(paths, captured)
expect(paths.main.events).toEqual(paths.renderer.events)
expect(paths.main.events.some((event) => event.kind === 'became-working')).toBe(true)
expect(paths.main.events.some((event) => event.kind === 'became-idle')).toBe(true)
feedBoth(paths, captured.replaceAll(']0;π', ']0;OMP'))
expect(paths.main.events).toEqual(paths.renderer.events)
})
it('derives identical facts from a coalesced spinner+idle chunk (issue #1083)', () => {
// One realistic node-pty batch: Pi's 80ms spinner frames plus agent_end's
// trailing idle title. A last-title reader sees only the idle title and
+5
View File
@@ -1,3 +1,4 @@
import { getPiStateTitleBrand } from './pi-state-title-marker'
import {
AGY_AGENT_NAME_RE,
CLAUDE_IDLE,
@@ -67,6 +68,10 @@ function computeAgentLabel(title: string): string | null {
) {
return 'Claude Code'
}
const piStateBrand = getPiStateTitleBrand(title)
if (piStateBrand) {
return piStateBrand
}
if (isGeminiTerminalTitle(title)) {
return 'Gemini CLI'
}
+5
View File
@@ -1,3 +1,4 @@
import { rebrandPiStateTitle } from './pi-state-title-marker'
import { detectAgentStatusFromTitle, getAgentLabel } from './agent-detection'
import type { AgentStatusEntry, AgentType } from './agent-status-types'
import {
@@ -157,6 +158,10 @@ export function normalizeCompatibleAgentTitleForOwner(
) {
return title
}
const stateTitle = rebrandPiStateTitle(title, ownerProfile.workingLabel)
if (stateTitle !== null) {
return stateTitle
}
// Why: a π-branded title is the agent's own semantic session title (`π > <session> - <cwd>`;
// Orca's injected extension writes the same shape). Swap only the BRAND for the owner's label
// so the pane still reads as its launch owner (#6689, #7633, #9077) without discarding the
+50
View File
@@ -0,0 +1,50 @@
import { getPiCompatibleTitleSeparatorStatus } from './pi-compatible-synthetic-title'
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { detectAgentStatusFromTitle, getAgentLabel } from './agent-detection'
import { normalizeCompatibleAgentTitleForOwner } from './agent-title-owner'
import { clearPiStateWorkingMarker } from './pi-state-title-marker'
const transcript = readFileSync(
new URL('../main/runtime/__fixtures__/omp-native-title-win32.txt', import.meta.url),
'utf8'
)
// oxlint-disable-next-line no-control-regex -- The fixture retains actual OSC control bytes.
const titles = [...transcript.matchAll(/\x1b\]0;([^\x07]+)\x07/g)].map((match) => match[1])
describe('owner-rewritten OMP titles from captured upstream output', () => {
it('contains the six upstream state frames', () => expect(titles).toHaveLength(6))
it.each(
titles.map((title, index) => ({
title,
state: index < 2 ? 'working' : index < 4 ? 'idle' : 'permission'
}))
)('preserves $state and label for $title', ({ title, state }) => {
for (const prefix of ['', 'zsh | ', 'tmux: ']) {
const wrapped = prefix + title
expect(detectAgentStatusFromTitle(wrapped)).toBe(state)
const owned = normalizeCompatibleAgentTitleForOwner(wrapped, 'omp', { ownerIsLaunch: true })
expect(owned).toBe(prefix + title.replace('π', 'OMP'))
expect(getAgentLabel(owned)).toBe('OMP')
expect(detectAgentStatusFromTitle(owned)).toBe(state)
expect(getPiCompatibleTitleSeparatorStatus(owned)).toBe(state)
expect(normalizeCompatibleAgentTitleForOwner(owned, 'omp')).toBe(owned)
expect(normalizeCompatibleAgentTitleForOwner(owned, 'pi')).toBe(
prefix + title.replace('π', 'Pi')
)
if (state === 'working') {
expect(detectAgentStatusFromTitle(clearPiStateWorkingMarker(owned) ?? '')).toBe('idle')
}
}
})
it.each([
'omp-harness ready',
'/tmp/OMP : file',
'lowercase omp : note',
'Pi: legacy',
'OMP ready'
])('does not rewrite neutral or legacy title %s as a working marker', (title) => {
expect(clearPiStateWorkingMarker(title)).toBeNull()
expect(detectAgentStatusFromTitle(title)).not.toBe('working')
})
})
@@ -1,3 +1,5 @@
import { getPiStateTitleStatus } from './pi-state-title-marker'
export type PiCompatibleSyntheticAgentLabel = 'Pi' | 'OMP'
export type PiCompatibleSyntheticAgentStatus = 'working' | 'permission' | 'idle'
@@ -71,6 +73,10 @@ export function isLegacyPiCompatibleTitle(title: string): boolean {
export function getPiCompatibleTitleSeparatorStatus(
title: string
): PiCompatibleSyntheticAgentStatus | null {
const nativeState = getPiStateTitleStatus(title)
if (nativeState) {
return nativeState
}
// Why: a spinner anywhere means the agent is working, and that outranks the separator —
// the frame is drawn over the idle separator position while a turn runs.
if (containsBrailleSpinner(title)) {
+28 -3
View File
@@ -27,15 +27,17 @@ function escapeForCharacterClass(marker: string): string {
return marker.replace(/[\\\]^-]/g, '\\$&')
}
// Why: `π` must sit at a token boundary so wrapper prefixes of any shape (`zsh | π : cwd`,
// Why: the brand must sit at a token boundary so wrapper prefixes (`zsh | OMP : cwd`,
// `tmux: π : cwd`) still expose the marker, and whitespace must separate the marker so the
// legacy no-space `π: cwd` disabled title keeps its historical idle classification.
const PI_STATE_TITLE_RE = new RegExp(
`(?:^|[\\s|])π[ \\t]+([${PI_STATE_MARKERS.map(escapeForCharacterClass).join('')}])(?=\\s|$)`,
`(?:^|[\\s|])(π|Pi|OMP)[ \\t]+([${PI_STATE_MARKERS.map(escapeForCharacterClass).join('')}])(?=\\s|$)`,
'u'
)
type PiStateTitleMatch = {
brand: string
brandIndex: number
marker: PiStateMarker
markerIndex: number
}
@@ -49,8 +51,14 @@ function matchPiStateTitle(title: string): PiStateTitleMatch | null {
if (!match) {
return null
}
const marker = match[2]
if (marker !== ':' && marker !== '!' && marker !== '>') {
return null
}
return {
marker: match[1] as PiStateMarker,
brand: match[1],
brandIndex: match.index + match[0].indexOf(match[1]),
marker,
markerIndex: match.index + match[0].length - 1
}
}
@@ -73,3 +81,20 @@ export function clearPiStateWorkingMarker(title: string): string | null {
}
return `${title.slice(0, match.markerIndex)}${PI_IDLE_MARKER}${title.slice(match.markerIndex + 1)}`
}
/** The state marker owns identity too; its label may mention another agent. */
export function getPiStateTitleBrand(title: string): 'Pi' | 'OMP' | null {
const match = matchPiStateTitle(title)
return match ? (match.brand === 'OMP' ? 'OMP' : 'Pi') : null
}
/** Rebrand only the protocol prefix, preserving wrappers and the opaque session label. */
export function rebrandPiStateTitle(title: string, brand: string): string | null {
const match = matchPiStateTitle(title)
if (!match) {
return null
}
return (
title.slice(0, match.brandIndex) + brand + title.slice(match.brandIndex + match.brand.length)
)
}
+18
View File
@@ -0,0 +1,18 @@
// Run under Bun through capture-agent-pty-transcript.mjs; sourceRoot is read-only.
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const sourceRoot = process.argv[2]
if (!sourceRoot) {
throw new Error('Expected path to the read-only oh-my-pi checkout')
}
const { buildTerminalTitleWithState } = await import(
pathToFileURL(resolve(sourceRoot, 'packages/coding-agent/src/utils/title-generator.ts')).href
)
for (const state of ['working', 'idle', 'attention']) {
for (const label of ['Run a long task', 'release | π : note | OMP ! action required ✦']) {
// Exercise upstream's explicit Windows argument, independently of the capture host OS.
const title = buildTerminalTitleWithState(label, state, 0, true, 'win32')
process.stdout.write(`\x1b]0;${title}\x07`)
}
}