mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Fix stale mobile worktree agent rows
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1102,7 +1102,8 @@
|
||||
"importHostSetup": "导入",
|
||||
"notePasteTooLarge": "笔记字段的粘贴内容过大。",
|
||||
"reuseExistingBranch": "复用分支",
|
||||
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。"
|
||||
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。",
|
||||
"createMultiple": "Create more"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"fa90f739a5": "创建工作区之前选择项目、工作区名称和 Agent。"
|
||||
|
||||
Reference in New Issue
Block a user