Fix stale mobile worktree agent rows

This commit is contained in:
brennanb2025
2026-06-23 19:28:26 -07:00
parent e03a1cf769
commit 7f88fa07e8
3 changed files with 121 additions and 30 deletions
+92
View File
@@ -14800,6 +14800,62 @@ describe('OrcaRuntimeService', () => {
})
})
it('does not report stale persisted tabs or hydrated done hook rows as live mobile activity', async () => {
const leafId = '44444444-4444-4444-8444-444444444444'
const paneKey = `host-tab:${leafId}`
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
activeTabId: 'host-tab',
activeTabIdByWorktree: { [TEST_WORKTREE_ID]: 'host-tab' },
tabsByWorktree: {
[TEST_WORKTREE_ID]: [
{
id: 'host-tab',
ptyId: 'persisted-pty',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex Done',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
},
terminalLayoutsByTabId: {
'host-tab': makeHeadlessTerminalLayout({ [leafId]: 'persisted-pty' })
}
})
)
const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
worktreeId: TEST_WORKTREE_ID,
tabId: 'host-tab',
state: 'done',
prompt: 'old prompt',
agentType: 'codex',
lastAssistantMessage: 'old done',
connectionId: null,
receivedAt: 1000,
stateStartedAt: 900
}
]
})
const { worktrees } = await runtime.getWorktreePs()
const summary = worktrees.find((w) => w.worktreeId === TEST_WORKTREE_ID)
expect(await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)).toMatchObject({ terminals: [] })
expect(summary).toMatchObject({
worktreeId: TEST_WORKTREE_ID,
liveTerminalCount: 0,
hasAttachedPty: false,
status: 'inactive',
agents: []
})
})
it('falls back to the path-keyed GitHub cache entry', async () => {
const runtimeStore = {
...store,
@@ -14903,6 +14959,42 @@ describe('OrcaRuntimeService', () => {
])
})
it('keeps agent rows for connected PTYs without a mounted renderer leaf', async () => {
const runtime = new OrcaRuntimeService(store)
const leafId = '55555555-5555-4555-8555-555555555555'
const paneKey = `headless-tab:${leafId}`
runtime['recordPtyWorktree']('headless-pty', TEST_WORKTREE_ID, {
connected: true,
tabId: 'headless-tab',
paneKey
})
runtime.onPtyData(
'headless-pty',
'\x1b]9999;{"state":"working","prompt":"ship it","agentType":"codex","lastAssistantMessage":"on it"}\x07',
321
)
const { worktrees } = await runtime.getWorktreePs()
const summary = worktrees.find((w) => w.worktreeId === TEST_WORKTREE_ID)
expect(summary).toMatchObject({
liveTerminalCount: 1,
hasAttachedPty: true,
hasHostSidebarActivity: true,
status: 'active'
})
expect(summary?.agents).toEqual([
expect.objectContaining({
paneKey,
state: 'working',
agentType: 'codex',
prompt: 'ship it',
lastAssistantMessage: 'on it'
})
])
})
it('attaches inline agent rows from hook-reported status (not just OSC)', async () => {
// Why: agent status normally arrives via hooks, not OSC terminal output;
// worktree.ps reads the hook snapshot so mobile surfaces those agents too.
+27 -29
View File
@@ -8599,6 +8599,8 @@ export class OrcaRuntimeService {
}
const countedPtyIds = new Set<string>()
const liveAgentPaneKeys = new Set<string>()
const liveAgentPtyIds = new Set<string>()
for (const leaf of this.leaves.values()) {
const summary = this.getSummaryForRuntimeWorktreeId(
summaries,
@@ -8608,8 +8610,10 @@ export class OrcaRuntimeService {
if (!summary) {
continue
}
liveAgentPaneKeys.add(this.makeRuntimePaneKey(leaf))
if (leaf.ptyId) {
countedPtyIds.add(leaf.ptyId)
liveAgentPtyIds.add(leaf.ptyId)
}
if (leaf.ptyId && leaf.connected) {
summary.hasHostSidebarActivity = true
@@ -8642,9 +8646,14 @@ export class OrcaRuntimeService {
if (!summary) {
continue
}
if (pty.paneKey) {
liveAgentPaneKeys.add(pty.paneKey)
}
liveAgentPtyIds.add(pty.ptyId)
const previousLastOutputAt = summary.lastOutputAt
summary.liveTerminalCount += 1
summary.hasAttachedPty = true
summary.hasHostSidebarActivity = true
summary.lastOutputAt = maxTimestamp(summary.lastOutputAt, pty.lastOutputAt)
summary.status = mergeWorktreeStatus(summary.status, 'active')
if (
@@ -8655,30 +8664,9 @@ export class OrcaRuntimeService {
}
}
// Why: tabsByWorktree is restore state, not proof of a live terminal. Mobile
// activity must come from runtime leaves or connected PTY records only.
const session = this.store?.getWorkspaceSession?.()
for (const [worktreeId, tabs] of Object.entries(session?.tabsByWorktree ?? {})) {
if (tabs.length === 0) {
continue
}
const summary = this.getSummaryForRuntimeWorktreeId(summaries, resolvedWorktrees, worktreeId)
if (!summary) {
continue
}
// Why: desktop can show terminal tabs that are not mounted as renderer
// leaves and are not currently visible in the PTY provider list. Mobile
// still needs those worktrees to show as terminal-bearing entries.
summary.liveTerminalCount = Math.max(summary.liveTerminalCount, tabs.length)
summary.hasAttachedPty = summary.hasAttachedPty || tabs.some((tab) => tab.ptyId !== null)
if (tabs.some((tab) => tab.ptyId !== null && this.ptysById.get(tab.ptyId)?.connected)) {
summary.hasHostSidebarActivity = true
}
for (const tab of tabs) {
summary.status = mergeWorktreeStatus(
summary.status,
getSavedTabWorktreeStatus(tab.title, tab.ptyId !== null)
)
}
}
// Why: surface the desktop's focused worktree so mobile can scroll it into
// view and highlight it. Resolve through getSummaryForRuntimeWorktreeId so
@@ -8694,7 +8682,10 @@ export class OrcaRuntimeService {
}
}
this.attachAgentRowsToSummaries(summaries)
this.attachAgentRowsToSummaries(summaries, {
livePaneKeys: liveAgentPaneKeys,
livePtyIds: liveAgentPtyIds
})
const sorted = [...summaries.values()].sort(compareWorktreePs)
return {
@@ -8708,7 +8699,10 @@ export class OrcaRuntimeService {
// agent list, mirroring the desktop sidebar. Lineage parent is resolved from
// the orchestration db (paneKey-keyed), not the OSC payload, since spawn
// hierarchy is pane-level state tracked separately from terminal output.
private attachAgentRowsToSummaries(summaries: Map<string, RuntimeWorktreePsSummary>): void {
private attachAgentRowsToSummaries(
summaries: Map<string, RuntimeWorktreePsSummary>,
liveEvidence: { livePaneKeys: ReadonlySet<string>; livePtyIds: ReadonlySet<string> }
): void {
// Why: most agents report via hooks (agent-hooks/server), not OSC, so the
// hook snapshot is the primary source — same one the desktop sidebar reads.
// OSC-only entries (no hook) are merged in as a fallback, keyed by paneKey.
@@ -8716,6 +8710,7 @@ export class OrcaRuntimeService {
string,
{
paneKey: string
ptyId?: string
worktreeId?: string
state: ParsedAgentStatusPayload['state']
agentType: string | null
@@ -8732,6 +8727,7 @@ export class OrcaRuntimeService {
const { payload } = snapshot
rowSources.set(snapshot.paneKey, {
paneKey: snapshot.paneKey,
ptyId: snapshot.ptyId,
worktreeId: snapshot.worktreeId,
state: payload.state,
agentType: payload.agentType ?? null,
@@ -8769,6 +8765,12 @@ export class OrcaRuntimeService {
if (!worktreeId || !summaries.has(worktreeId)) {
continue
}
if (
!liveEvidence.livePaneKeys.has(src.paneKey) &&
(!src.ptyId || !liveEvidence.livePtyIds.has(src.ptyId))
) {
continue
}
const taskTitle = orchestrationByPaneKey?.[src.paneKey]?.taskTitle ?? null
const displayName = orchestrationByPaneKey?.[src.paneKey]?.displayName ?? null
const row: RuntimeWorktreeAgentRow = {
@@ -22400,10 +22402,6 @@ function getLatestAgentCandidateTitleInfo(
return latest
}
function getSavedTabWorktreeStatus(title: string, hasPty: boolean): RuntimeWorktreeStatus {
return getDetectedWorktreeStatus(detectAgentStatusFromTitle(title), hasPty)
}
function getDetectedWorktreeStatus(
detected: AgentStatus | null,
hasPty: boolean
+2 -1
View File
@@ -1102,7 +1102,8 @@
"importHostSetup": "导入",
"notePasteTooLarge": "笔记字段的粘贴内容过大。",
"reuseExistingBranch": "复用分支",
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。"
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。",
"createMultiple": "Create more"
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "创建工作区之前选择项目、工作区名称和 Agent。"