mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
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:
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
+3
-1
@@ -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 &&
|
||||
|
||||
+46
@@ -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)) {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user