Files
orca/src/main/runtime/runtime-worktree-agent-rows.ts
T
Kelvin Amoaba 7e5f9a48df fix(mobile): use desktop's sort, colour, count, agent-row and delete rules in the workspace list (#33995)
* fix(mobile): use desktop's sort, colour, count and agent-row rules in the workspace list

Recent ignores terminal output, repo colours fall back to desktop's neutral,
Home counts skip archived rows, folder workspaces trail each lane, and a stale
agent row on a live terminal reads "No update in …" like desktop.

* fix(mobile): keep Delete disabled on a primary checkout, as desktop does

* fix(mobile): keep the task text on a silent agent row

* fix(mobile): show a silent agent row's time since its last update

* test(mobile): drop agent-row tests that replay the shared verdict

* refactor(mobile): check host PTY liveness once and keep the delete guard on the row flag

* test(mobile): re-record the home catalog goldens for the list's active count
2026-10-27 04:25:59 +00:00

123 lines
4.8 KiB
TypeScript

import { isFreshNonDoneAgentStatus } from '../../shared/agent-status-types'
import { isAgentStatusFresh } from '../../shared/agent-status-freshness'
import type { RuntimeWorktreeAgentRow, RuntimeWorktreePsSummary } from '../../shared/runtime-types'
import { mergeWorktreeSummaryStatus } from './runtime-worktree-status-projection'
import {
resolveRuntimeWorktreeSidebarStatus,
terminalTitleEvidenceMatchesSource
} from './runtime-worktree-sidebar-status'
import type { RuntimeWorktreePsSummaryLookup } from './runtime-worktree-summary-paths'
import type { RuntimeTerminalTitleEvidence } from './runtime-worktree-ps-activity'
import type { RuntimeWorktreeAgentSource } from './runtime-worktree-agent-source'
export type { RuntimeAgentRowSnapshot } from './runtime-hook-agent-row-selection'
type OrchestrationDisplay = {
taskTitle?: string | null
displayName?: string | null
parentPaneKey?: string | null
}
export function attachRuntimeWorktreeAgentRows(args: {
summaries: Map<string, RuntimeWorktreePsSummary>
rowSources: ReadonlyMap<string, RuntimeWorktreeAgentSource>
terminalTitleEvidenceByWorktreeId: ReadonlyMap<string, readonly RuntimeTerminalTitleEvidence[]>
orchestrationByPaneKey: Record<string, OrchestrationDisplay> | null | undefined
getSummary: RuntimeWorktreePsSummaryLookup
}): void {
const { rowSources } = args
const now = Date.now()
const rowsByWorktree = new Map<string, RuntimeWorktreeAgentRow[]>()
for (const source of rowSources.values()) {
const { worktreeId } = source
if (!worktreeId) {
continue
}
const summary = args.getSummary(worktreeId)
if (!summary) {
continue
}
const orchestration = args.orchestrationByPaneKey?.[source.paneKey]
const row: RuntimeWorktreeAgentRow = {
paneKey: source.paneKey,
parentPaneKey: orchestration?.parentPaneKey ?? null,
state: source.state,
...(source.workingMode ? { workingMode: source.workingMode } : {}),
agentType: source.agentType,
prompt: source.prompt,
taskTitle: orchestration?.taskTitle ?? null,
displayName: orchestration?.displayName ?? null,
lastAssistantMessage: source.lastAssistantMessage,
toolName: source.toolName,
toolInput: source.toolInput,
interrupted: source.interrupted,
...(source.mainAgent ? { mainAgent: source.mainAgent } : {}),
stateStartedAt: source.stateStartedAt,
updatedAt: source.updatedAt,
...(source.children !== undefined ? { children: source.children } : {}),
...(source.subagents !== undefined ? { subagents: source.subagents } : {}),
...(source.structuredHost === 'owned' ? { structuredHostOwned: true as const } : {}),
...(source.hasLivePty ? { hasLivePty: true as const } : {}),
...(source.programStatus ? { programStatus: source.programStatus } : {})
}
if (source.children !== undefined || source.subagents !== undefined) {
row.subagentObservation = isAgentStatusFresh(row, now) ? 'live' : 'unverifiable'
}
const rows = rowsByWorktree.get(summary.worktreeId)
if (rows) {
rows.push(row)
} else {
rowsByWorktree.set(summary.worktreeId, [row])
}
}
for (const [worktreeId, rows] of rowsByWorktree) {
rows.sort((a, b) => a.stateStartedAt - b.stateStartedAt)
const summary = args.summaries.get(worktreeId)
if (!summary) {
continue
}
summary.agents = rows
let hasForegroundWorkingAgent = false
const monitoringSources: RuntimeWorktreeAgentSource[] = []
for (const row of rows) {
if (!isFreshNonDoneAgentStatus(row, now)) {
continue
}
summary.hasHostSidebarActivity = true
if (row.state === 'working') {
if (row.workingMode === 'monitoring') {
const source = rowSources.get(row.paneKey)
if (source) {
monitoringSources.push(source)
}
} else {
hasForegroundWorkingAgent = true
}
} else {
mergeWorktreeSummaryStatus(summary, 'permission')
}
}
if (hasForegroundWorkingAgent || monitoringSources.length > 0) {
const hasIndependentWorkingTerminal = (
args.terminalTitleEvidenceByWorktreeId.get(worktreeId) ?? []
).some(
(evidence) =>
evidence.status === 'working' &&
monitoringSources.every((source) => !terminalTitleEvidenceMatchesSource(evidence, source))
)
mergeWorktreeSummaryStatus(
summary,
'working',
hasForegroundWorkingAgent || hasIndependentWorkingTerminal ? undefined : 'monitoring'
)
}
}
for (const summary of args.summaries.values()) {
summary.sidebarStatus = resolveRuntimeWorktreeSidebarStatus(
summary,
now,
args.terminalTitleEvidenceByWorktreeId.get(summary.worktreeId) ?? [],
(paneKey) => rowSources.get(paneKey)
)
}
}