fix: normalize wrapper-prefixed OMP/Pi terminal titles (#8033)

Multiplexer/session wrappers prefix pane titles as "prefix | pane-title", pushing the Pi/OMP identity after " | ". Inspect each " | " suffix and prefer a re-ownable compatible identity over the wrapper text, so wrapped OMP/Pi titles normalize to the owner instead of flickering. Includes guard tests for braille-inner labels and no-identity wrappers.
This commit is contained in:
Vitus 2026-07-11 09:42:48 +08:00 committed by GitHub
parent 7b6eb3cc06
commit ebeb947dee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 48 additions and 11 deletions

View File

@ -118,13 +118,26 @@ describe('Pi-compatible title detection', () => {
['π: tmp', 'omp', 'OMP ready'],
['\u280b π: tmp', 'omp', '\u280b OMP'],
['\u280b π - tmp', 'omp', '\u280b OMP'],
['\u280b OMP', 'pi', '\u280b Pi']
['\u280b OMP', 'pi', '\u280b Pi'],
['lucky-echidna | \u283c π - Diagnose Orca terminal title flicker - test', 'omp', '\u280b OMP'],
['lucky-echidna | Pi ready', 'omp', 'OMP ready'],
['Codex | Pi ready', 'omp', 'OMP ready'],
// Why: the wrapped whole reads as a braille Claude title, but the re-ownable
// synthetic pane suffix must still win.
['lucky-echidna | ⠋ OMP', 'omp', '⠋ OMP'],
['lucky-echidna | \u283c π - Diagnose | test', 'omp', '\u280b OMP']
] as const)('normalizes %s to the authoritative %s owner', (title, owner, expectedTitle) => {
expect(normalizeCompatibleAgentTitleForOwner(title, owner)).toBe(expectedTitle)
})
it('preserves Pi-compatible custom titles and unrelated owners', () => {
expect(normalizeCompatibleAgentTitleForOwner('Fix pi bugs', 'omp')).toBe('Fix pi bugs')
// Why: a wrapped title with no re-ownable identity in any " | " segment
// passes through untouched instead of collapsing to the owner label.
expect(normalizeCompatibleAgentTitleForOwner('lucky-echidna | Fix pi bugs', 'omp')).toBe(
'lucky-echidna | Fix pi bugs'
)
expect(normalizeCompatibleAgentTitleForOwner('xxPi ready', 'omp')).toBe('xxPi ready')
expect(normalizeCompatibleAgentTitleForOwner('\u280b Pi', 'codex')).toBe('\u280b Pi')
})

View File

@ -9,14 +9,17 @@ import { isLegacyPiCompatibleTitle } from './pi-compatible-synthetic-title'
type TitleProfileMatch = {
profile: SyntheticAgentTitleProfile
sourceTitle: string
}
type TitleLabelProfileMatch = Pick<TitleProfileMatch, 'profile'>
const COMPATIBLE_IDLE_TITLE_RE = /(?<![\w./\\-])(?:ready|idle|done)(?![\w-])/i
/**
* Resolves the synthetic title profile matching a given agent label.
*/
function getProfileForTitleLabel(label: string | null): TitleProfileMatch | null {
function getProfileForTitleLabel(label: string | null): TitleLabelProfileMatch | null {
if (!label) {
return null
}
@ -33,14 +36,35 @@ function getProfileForTitleLabel(label: string | null): TitleProfileMatch | null
* Resolves the synthetic title profile matching a given terminal title.
*/
function getProfileForTitle(title: string): TitleProfileMatch | null {
const labelProfile = getProfileForTitleLabel(getAgentLabel(title))
if (labelProfile) {
return labelProfile
// Multiplexers/session wrappers prefix dynamic titles with ` | `, so inspect
// each suffix to preserve the inner compatible agent identity.
const candidates = [title]
let wrapperSeparatorIndex = title.indexOf(' | ')
while (wrapperSeparatorIndex >= 0) {
const wrappedPaneTitle = title.slice(wrapperSeparatorIndex + 3).trim()
if (wrappedPaneTitle && !candidates.includes(wrappedPaneTitle)) {
candidates.push(wrappedPaneTitle)
}
wrapperSeparatorIndex = title.indexOf(' | ', wrapperSeparatorIndex + 3)
}
if (isLegacyPiCompatibleTitle(title)) {
return getProfileForTitleLabel('Pi')
let fallback: TitleProfileMatch | null = null
for (const candidate of candidates) {
const labelProfile = getProfileForTitleLabel(getAgentLabel(candidate))
const legacyProfile = isLegacyPiCompatibleTitle(candidate)
? getProfileForTitleLabel('Pi')
: null
const candidateProfile = labelProfile ?? legacyProfile
if (!candidateProfile) {
continue
}
const match = { ...candidateProfile, sourceTitle: candidate }
if (candidateProfile.profile.titleIdentityGroup) {
return match
}
fallback ??= match
}
return null
return fallback
}
/**
@ -132,7 +156,7 @@ export function normalizeCompatibleAgentTitleForOwner(
) {
return title
}
const sourceStatus = getSourceTitleStatus(title)
const sourceStatus = getSourceTitleStatus(source.sourceTitle)
if (sourceStatus === 'working') {
return `\u280b ${ownerProfile.workingLabel}`
}
@ -142,10 +166,10 @@ export function normalizeCompatibleAgentTitleForOwner(
if (sourceStatus === 'idle') {
return ownerProfile.idleLabel
}
if (hasPermissionSuffix(title, source.profile)) {
if (hasPermissionSuffix(source.sourceTitle, source.profile)) {
return ownerProfile.permissionLabel
}
if (hasIdleSuffix(title, source.profile)) {
if (hasIdleSuffix(source.sourceTitle, source.profile)) {
return ownerProfile.idleLabel
}
return ownerProfile.workingLabel