Files
orca/src/shared/native-chat-turn-activity.ts
T
Brennan Benson 3ab3c9239f fix(native-chat): the working line shows only what the agent is doing now (#24218)
* fix(native-chat): the working line shows only what the agent is doing now

A chat's live "Working…" line could show an old notice, such as "Claude hit a
temporary problem and is retrying.", long after the agent had moved on and was
running new commands. When the host had no live activity for the turn, the line
fell back to the newest status row in the turn, and any status row qualified:
retry warnings, "Context compacted", "Cancellation requested.", and notification
summaries. Those rows record the past and already appear in the transcript.

The line now reads only the host's live, per-turn activity, which is never saved
and is cleared at turn boundaries. Without it, the line says Thinking or
Working…. Desktop and mobile share the selector, so both change.

* test(codex): guard that a subagent's compaction never becomes the parent's live activity
2026-09-30 16:44:29 -07:00

101 lines
3.3 KiB
TypeScript

import { readAgentJournalTurn } from './agent-session-turn-record'
import type { AgentJournalRenderItem } from './agent-session-journal-types'
import type { AgentSessionTurnActivity } from './agent-session-wire'
import { isRootAgentJournalItem } from './agent-session-journal-producer'
import { normalizePromptField } from './agent-status-field-normalization'
import { describeActiveToolCall, formatActiveToolLabel } from './native-chat-tool-activity'
export type NativeChatTurnActivity = { kind: 'description'; text: string }
function activityLine(text: string): string | null {
let end = text.length
while (end > 0) {
const start = text.lastIndexOf('\n', end - 1) + 1
const latest = text.slice(start, end).trim()
if (latest) {
return normalizePromptField(latest) || null
}
if (start === 0) {
break
}
end = start - 1
}
return null
}
function recentToolActivityLabels(items: readonly AgentJournalRenderItem[]): Set<string> {
const labels = new Set<string>()
let foundRunning = false
let foundSettled = false
for (let index = items.length - 1; index >= 0 && (!foundRunning || !foundSettled); index -= 1) {
const body = items[index]?.body
if (body?.kind !== 'tool-call') {
continue
}
const isRunning = body.state === 'running'
if ((isRunning && foundRunning) || (!isRunning && foundSettled)) {
continue
}
const descriptor = describeActiveToolCall({
type: 'tool-call',
name: body.name,
input: body.input,
state: body.state
})
const candidates = [
formatActiveToolLabel(descriptor),
descriptor.preview,
descriptor.preview ? `${descriptor.toolName} ${descriptor.preview}` : descriptor.toolName
]
for (const candidate of candidates) {
const label = activityLine(candidate)?.toLowerCase()
if (label) {
labels.add(label)
}
}
foundRunning ||= isRunning
foundSettled ||= !isRunning
}
return labels
}
function repeatsRecentToolLabel(text: string, labels: ReadonlySet<string>): boolean {
const normalized = text.toLowerCase()
for (const label of labels) {
if (
normalized === label ||
normalized.startsWith(`${label} `) ||
normalized.endsWith(` ${label}`) ||
normalized.includes(` ${label} `)
) {
return true
}
}
return false
}
/** The live provider activity for this turn; callers provide the broad fallback.
* Journal rows are history, not the present, so none of them ever becomes this line. */
export function selectStructuredAgentTurnActivity(
items: readonly AgentJournalRenderItem[],
turnId: string | null,
providerActivity?: AgentSessionTurnActivity | null
): NativeChatTurnActivity | null {
if (!turnId || providerActivity?.turnId !== turnId) {
return null
}
const text = activityLine(providerActivity.text)
if (!text) {
return null
}
const turnStartIndex = items.findLastIndex((item) => {
const turn = readAgentJournalTurn(item.body)
return turn?.turnId === turnId && turn.state === 'running'
})
// Root-scoped: a subagent's tool label must not suppress the parent's own line.
const turnItems = items.slice(Math.max(0, turnStartIndex)).filter(isRootAgentJournalItem)
return repeatsRecentToolLabel(text, recentToolActivityLabels(turnItems))
? null
: { kind: 'description', text }
}