fix(jcode): keep the Jcode agent row and show its turn on Linux (#41103)

* fix(jcode): keep the agent row for Jcode panes and read its working title

On Linux the Jcode release runs jcode-linux-<arch>.bin behind a jcode wrapper, so
the foreground read never identified the pane, and Jcode's own title
(<emoji> jcode/<server> <Codename> · work ~2s) was not classified. Once Jcode
replaced its startup title, the title-derived row lost its only identity and
disappeared. Map the platform binary to jcode, accept the server-qualified
identity title, and read the work/last tail as working/idle.

* fix(jcode): read the bare title tail only in a Jcode pane

The activity tail on conversation and SSH titles is too generic to prove an
agent, so title-only readers must not treat it as one. Gate it on the pane's
agent, as Hermes markers are, keep identity titles self-describing, narrow the
server segment, and keep the title strip linear.

* fix(jcode): keep the turn tail out of title-only readers

Title-only readers (completion tracking) saw a turn from Jcode's identity title
and then idle when Jcode switched to a conversation title naming another agent.
Read the tail only through the pane-scoped reader, check it before generic
title status in the sidebar row, and stop treating a bare jcode/<dir> as Jcode.

* fix(jcode): escape the title separator so the orcad bundle stays ASCII
This commit is contained in:
Kelvin Amoaba
2026-11-11 19:12:07 +00:00
committed by GitHub
parent 769bef7930
commit 3082e4aa2b
10 changed files with 142 additions and 9 deletions
@@ -7,6 +7,7 @@ import { terminalTitleBlocksExplicitAgentStatus } from '../../workspaces/process
import type { ClaudeTerminalEvidence } from '../../../../shared/claude-terminal-interrupt'
import { agentStatusTakesTypedInput } from '../../orchestration/mailbox/routing/mailbox-owner'
import { hermesNativeTitleStatus } from '../../../../shared/agent-status/title/providers/hermes-terminal-title'
import { jcodePaneTitleStatus } from '../../../../shared/agent-status/title/providers/jcode-terminal-title'
export abstract class OrcaRuntimeWithApplyTrackedPtyTitle extends OrcaRuntimeWithGetUnpersistedTrackedTitleForPty {
protected abstract collectPaneKeysForPty(ptyId: string): Set<string>
@@ -53,9 +54,11 @@ export abstract class OrcaRuntimeWithApplyTrackedPtyTitle extends OrcaRuntimeWit
/** The PTY's agent status under this title: the program's own OSC 7501 activity while it
* reports one, else what the title glyph says. */
protected classifyPtyTitleStatus(ptyId: string, rawTitle: string): AgentStatus | null {
const agent = this.getPaneTerminalAgent(ptyId)
return (
this.ptyTitleTrackersByPtyId.get(ptyId)?.programActivity ??
hermesNativeTitleStatus(rawTitle, this.getPaneTerminalAgent(ptyId)) ??
hermesNativeTitleStatus(rawTitle, agent) ??
jcodePaneTitleStatus(rawTitle, agent) ??
detectAgentStatusFromTitle(rawTitle)
)
}
@@ -8,6 +8,7 @@ import {
import { detectAgentStatusFromTitle } from '../../../../../../shared/agent-status/title/agent-title-status'
import { getAgentLabel } from '../../../../../../shared/agent-status/title/agent-title-identity'
import { jcodePaneTitleStatus } from '../../../../../../shared/agent-status/title/providers/jcode-terminal-title'
import { tabHasLivePty } from '../../../../../../shared/tab-has-live-pty'
import type {
AgentStatusEntry,
@@ -182,7 +183,8 @@ function buildTitleDerivedAgentRow(args: {
return null
}
// Why: the title sets activity only; a plain title on a process-identified pane is idle.
const status = titleStatus ?? 'idle'
// Jcode's own tail outranks agent names its conversation title happens to mention.
const status = jcodePaneTitleStatus(title, agentType) ?? titleStatus ?? 'idle'
const rowLabel =
agentType === titleAgentType &&
label &&
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest'
import type { TerminalTab } from '../../../../../../shared/workspace-layout/terminal-tab-types'
import { makePaneKey } from '../../../../../../shared/workspace-layout/stable-pane-id'
import { buildTitleDerivedAgentRows } from './worktree-title-derived-agent-rows'
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
const PANE_KEY = makePaneKey('tab-1', LEAF_ID)
function jcodeRow(title: string) {
const tab: TerminalTab = {
id: 'tab-1',
ptyId: 'pty-1',
worktreeId: 'wt-1',
title,
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 0
}
const rows = buildTitleDerivedAgentRows({
tabs: [tab],
ptyIdsByTabId: { 'tab-1': ['pty-1'] },
terminalLayoutsByTabId: {
'tab-1': { root: null, activeLeafId: LEAF_ID, expandedLeafId: null }
},
// Linux reads jcode-linux-x86_64.bin; the foreground tracker publishes it as jcode.
paneForegroundAgentByPaneKey: {
[PANE_KEY]: { agent: 'jcode', agentEvidence: 'process-read', shellForeground: false }
},
seenPaneKeys: new Set(),
now: 0
})
return rows.map((row) => ({ agentType: row.agentType, state: row.state }))
}
describe('title-derived Jcode rows', () => {
it('keeps the row through Jcode titles and follows its turn tail', () => {
expect(jcodeRow('🐩 jcode/marsh Poodle')).toEqual([{ agentType: 'jcode', state: 'idle' }])
expect(jcodeRow('🦊 Fix codex hooks · +3 -1 · work ~12s')).toEqual([
{ agentType: 'jcode', state: 'working' }
])
expect(jcodeRow('🦊 Fix codex hooks · last ~2m05s')).toEqual([
{ agentType: 'jcode', state: 'idle' }
])
})
})
@@ -89,6 +89,10 @@ function agentForNormalizedProcess(normalized: string): TerminalAgent | undefine
if (normalized.startsWith('grok-')) {
return PROCESS_TO_AGENT.get('grok')
}
// Jcode's Linux release wraps `jcode` around jcode-linux-<arch>.bin; raw release binaries keep the platform name.
if (/^jcode-(?:linux|macos|freebsd|windows)-(?:x86_64|aarch64)(?:\.bin)?$/.test(normalized)) {
return PROCESS_TO_AGENT.get('jcode')
}
// Muse execs a versioned muse-bin binary; this prefix also covers truncated comm without matching unrelated muse-* tools.
if (normalized.startsWith('muse-bin-')) {
return PROCESS_TO_AGENT.get('muse')
@@ -189,7 +193,8 @@ export function isExpectedAgentProcess(
/^(?:qoderclicn|qodercn)(?:-\d.*)?$/.test(normalizedProcess)) ||
(['qoder', 'qodercli'].includes(normalizedExpected) &&
/^(?:qoder|qodercli(?:-\d.*)?)$/.test(normalizedProcess)) ||
(normalizedExpected === 'muse' && normalizedProcess.startsWith('muse-bin-'))
(normalizedExpected === 'muse' && normalizedProcess.startsWith('muse-bin-')) ||
(normalizedExpected === 'jcode' && agentForNormalizedProcess(normalizedProcess) === 'jcode')
)
}
@@ -30,6 +30,7 @@ import {
} from './providers/pi-compatible-synthetic-title'
import { isDshTerminalTitle } from './providers/dsh-terminal-title'
import { hasQoderTitleLabelPayload, isQoderTerminalTitle } from './providers/qoder-terminal-title'
import { isJcodeIdentityTerminalTitle } from './providers/jcode-terminal-title'
import {
SYNTHETIC_AGENT_TITLE_AGENTS,
SYNTHETIC_AGENT_TITLE_PROFILES
@@ -265,6 +266,7 @@ function collectObserverAnchors(title: string): TerminalAgent[] {
? 'dsb'
: null) ??
agentForNativeFrame(segment) ??
(isJcodeIdentityTerminalTitle(segment) ? 'jcode' : null) ??
agentForWholeTitle(segment) ??
agentForSyntheticTitle(segment) ??
(ANTIGRAVITY_MODEL_TITLE_RE.test(undecorated) ? 'antigravity' : null) ??
@@ -1,4 +1,5 @@
import { isQoderTerminalTitle } from './providers/qoder-terminal-title'
import { isJcodeIdentityTerminalTitle } from './providers/jcode-terminal-title'
import { getPiStateTitleBrand } from './providers/pi-state-title-marker'
import {
AGY_AGENT_NAME_RE,
@@ -110,7 +111,7 @@ function computeAgentLabel(title: string): string | null {
if (titleHasAgentName(title, 'devin')) {
return 'Devin'
}
if (titleHasAgentName(title, 'jcode')) {
if (titleHasAgentName(title, 'jcode') || isJcodeIdentityTerminalTitle(title)) {
return 'Jcode'
}
if (titleHasAgentName(title, 'antigravity') || AGY_AGENT_NAME_RE.test(title)) {
@@ -1,5 +1,6 @@
import { getDeepSeekBuildTitleStatus } from './providers/dsb-terminal-title'
import { qoderTitleStatus } from './providers/qoder-terminal-title'
import { isJcodeIdentityTerminalTitle } from './providers/jcode-terminal-title'
import { cursorTitleStatus } from './providers/cursor-terminal-title'
import {
AGY_AGENT_NAME_RE,
@@ -279,6 +280,10 @@ function computeAgentStatusFromTitle(title: string): AgentStatus | null {
if (title.startsWith(`${CLAUDE_IDLE} `) || title === CLAUDE_IDLE) {
return 'idle'
}
// Activity comes from jcodePaneTitleStatus; the identity alone says Jcode is at rest.
if (isJcodeIdentityTerminalTitle(title)) {
return 'idle'
}
// Read π ! as blocked before generic idle or OMP user waits disappear.
const piCompatibleSeparatorStatus = getPiCompatibleTitleSeparatorStatus(title)
if (piCompatibleSeparatorStatus) {
@@ -1,17 +1,43 @@
import { CLAUDE_IDLE } from '../agent-title-glyphs'
// jcode updates emoji/session/diff/duration titles about once a second (terminal_title.rs); generated codenames are identity, not conversation names.
// Captured provenance: docs/reference/jcode-hook-events.md.
// (?<!\s) starts each separator at its whitespace run so long space runs stay linear.
const JCODE_TITLE_STATUS_RE =
/^[\p{Extended_Pictographic}\u{FE0F}\u{200D}]+\s*|\s+\u00B7\s+(?:\+\d+\s+-\d+|(?:work|last)\s+~\S+)(?=\s+\u00B7\s+|$)/gu
/^[\p{Extended_Pictographic}\u{FE0F}\u{200D}]+\s*|(?<!\s)\s+\u00B7\s+(?:\+\d+\s+-\d+|(?:work|last)\s+~\S+|working)(?=\s+\u00B7\s+|$)/gu
/** The jcode title with its per-session emoji and live diff/duration segments removed. */
export function stripJcodeTitleStatus(title: string): string {
return title.replace(JCODE_TITLE_STATUS_RE, '').trim()
}
// jcode, jcode Puppy and self-dev jcode/creek Puppy are identity; user work labels contain more.
const JCODE_IDENTITY_TITLE_RE = /^jcode(?:\/[^\s\u00B7]+)?(?:\s+[^\s\u00B7]+)?$/iu
// jcode, jcode Puppy and jcode/marsh Puppy (server name) are identity; a bare jcode/<dir> is a shell path.
const JCODE_IDENTITY_TITLE_RE = /^jcode(?:(?:\/[\w-]+)?\s+[^\s\u00B7]+)?$/iu
/** True when a jcode title says only which jcode session this is, not what it is doing. */
export function isJcodeIdentityTerminalTitle(title: string | null | undefined): boolean {
return Boolean(title) && JCODE_IDENTITY_TITLE_RE.test(stripJcodeTitleStatus(title ?? ''))
// Claude's ✳ is also an emoji; `✳ Jcode hooks` is a Claude task title.
return (
Boolean(title) &&
/jcode/i.test(title ?? '') &&
!title?.trimStart().startsWith(CLAUDE_IDLE) &&
JCODE_IDENTITY_TITLE_RE.test(stripJcodeTitleStatus(title ?? ''))
)
}
// title_with_metrics: `· work ~2m05s` (or `· working`) mid-turn, `· last ~2m05s` after it.
const JCODE_ACTIVITY_TAIL_RE =
/\s\u00B7\s(?:(work|last)\s~(?:\d+h\d{2}m|\d+m\d{2}s|\d+s)|working)$/u
/** The tail is too generic to identify Jcode, and title-only readers must not see a turn that a
* later conversation title drops, so it counts only in a pane already known to run Jcode. */
export function jcodePaneTitleStatus(
title: string,
agent: string | null | undefined
): 'working' | 'idle' | null {
if (agent !== 'jcode') {
return null
}
const tail = JCODE_ACTIVITY_TAIL_RE.exec(title)
return tail ? (tail[1] === 'last' ? 'idle' : 'working') : null
}
@@ -1,5 +1,6 @@
import { collectExplicitAgentTitleEvidence } from './agent-title-evidence'
import { isQoderTerminalTitle } from './providers/qoder-terminal-title'
import { isJcodeIdentityTerminalTitle } from './providers/jcode-terminal-title'
import {
AGY_AGENT_NAME_RE,
DROID_AGENT_NAME_RE,
@@ -180,7 +181,7 @@ function computeAgentLabel(title: string): string | null {
if (titleHasAgentName(title, 'devin')) {
return 'Devin'
}
if (titleHasAgentName(title, 'jcode')) {
if (titleHasAgentName(title, 'jcode') || isJcodeIdentityTerminalTitle(title)) {
return 'Jcode'
}
if (titleHasAgentName(title, 'antigravity') || AGY_AGENT_NAME_RE.test(title)) {
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import {
isExpectedAgentProcess,
recognizeAgentProcessFromCommandLine
} from './agent-status/process/agent-process-recognition'
import { detectAgentStatusFromTitle } from './agent-status/title/agent-title-status'
import { resolveExplicitTerminalTitleAgentType } from './agent-status/title/terminal-title-agent-type'
import { jcodePaneTitleStatus } from './agent-status/title/providers/jcode-terminal-title'
describe('Jcode pane identity and title status', () => {
it('recognizes the platform binary the Linux release wrapper execs', () => {
for (const command of [
'/home/dev/.local/bin/jcode-versions/0.99.1/jcode-linux-x86_64.bin',
'/home/dev/.local/bin/jcode-versions/0.99.1/jcode-linux-aarch64.bin --resume fox'
]) {
expect(recognizeAgentProcessFromCommandLine(command)?.agent).toBe('jcode')
expect(isExpectedAgentProcess(command.split(' ')[0], 'jcode')).toBe(true)
}
expect(recognizeAgentProcessFromCommandLine('jcode-helper')).toBeNull()
})
it('reads the turn tail only in a Jcode pane', () => {
for (const title of [
'🌐 jcode/fountain Duck · work ~2s',
'🦊 Fix codex hooks · +12 -3 · work ~2m05s',
'🐩 jcode Poodle · working'
]) {
expect(jcodePaneTitleStatus(title, 'jcode')).toBe('working')
expect(jcodePaneTitleStatus(title, 'claude')).toBeNull()
}
expect(jcodePaneTitleStatus('jcode SSH devbox fox · last ~1h05m', 'jcode')).toBe('idle')
// Title-only readers see the identity at rest, never a turn a later title would drop.
expect(detectAgentStatusFromTitle('🌐 jcode/fountain Duck · work ~2s')).toBe('idle')
expect(detectAgentStatusFromTitle('🦊 Improve session tabs · work ~2m05s')).toBeNull()
})
it('identifies Jcode from its server-qualified session title', () => {
expect(resolveExplicitTerminalTitleAgentType('🌐 jcode/fountain Duck · work ~2s')).toBe('jcode')
expect(resolveExplicitTerminalTitleAgentType('✳ Jcode hooks')).not.toBe('jcode')
expect(resolveExplicitTerminalTitleAgentType('jcode/scripts')).not.toBe('jcode')
})
})