Files
orca/src/shared/structured-agent-session-tool-call-block.ts
T
Brennan Benson 520033b0e1 fix(native-chat): fold thoughts and tool calls between replies into one row (#26048)
* fix(native-chat): fold thoughts and tool calls between replies into one row

A model that thinks before every tool call drew a 'Thought for Ns' row between
each call, and each thought also stopped the calls from merging, so a turn read
as dozens of alternating rows. The transcript now draws an unbroken stretch of
tool calls and thoughts within a turn as one tool run; thoughts read in order
inside it when opened and are not counted in its label.

* fix(native-chat): keep work runs whole while a turn edits files

A live turn that had edited a file kept splitting its newest call or
thought off the run (the row carrying the turn's diff rollup could not
join), so a lone "Thought for Ns" row came back at the bottom.

- Run boundaries are now one pure pass in src/shared
  (nativeChatWorkRunSpans), applied over the built transcript slots.
  Section heads end runs by sitting between rows, and rows inside an
  expanded subagent section are grouped the same way.
- The row carrying the turn's diff rollup joins as the run's last
  member; the run takes the rollup from it and the bar from its head.
- A collapsed run reserves one tool row plus a lead's words, not every
  thought's text.
- Lone rows and runs draw through the same MessageRow, with one flat
  keyed line list, so a row becoming a run no longer remounts (a
  revealed diff card no longer re-fires its scroll).
- A run opens by default while the reader holds one of its thoughts
  open; the run's own choice still wins.
- A drawn thought with no visible text joins a run instead of
  splitting it.

* fix(native-chat): pair each row's result with its own call; keep open thoughts out of runs

- A structured journal row's call and result now share one id (the
  provider's call id, else the row's own id), so a diff or output can no
  longer pair with an earlier unanswered call in the same work run or
  folded message. A quiet command followed by edits now shows each diff
  card under its own call, and the rollup's reveal lands on it.
- A thought still being thought, while its turn or subagent works, keeps
  its own row after the run and joins once it ends.
- Removed the rule that opened a run because a thought inside it was
  open; a thought keeps its own open state inside its run.

* fix(native-chat): an open thought the live line lets go of joins its run

Only a subagent section keeps a still-running thought out of its run.
At the top level the live line owns the open thought; when it lets go
(a Stop in flight, a prompt waiting on the reader) the thought now joins
the run at once instead of flashing as its own row until the turn ends.

* test(native-chat): guard the run row memo; name the slot post-pass test after it

Settled work runs must not rebuild their lines and thoughts on every stream
frame of the live run. Renames the slot-level run test so it no longer
shares a name with the shared span test.
2026-10-07 03:35:08 -07:00

52 lines
2.2 KiB
TypeScript

// The one definition of a structured journal's tool actions: which rows the chat draws as a
// tool call, and the block it draws. The status line reads the same definition, so the sidebar
// names a tool exactly as the chat draws it.
import type {
AgentJournalDiffItem,
AgentJournalItemBody,
AgentJournalToolCallItem
} from './agent-session-journal-types'
import type { NativeChatToolCallBlock } from './native-chat-types'
export type StructuredAgentSessionToolAction = AgentJournalToolCallItem | AgentJournalDiffItem
// Codex rewrites an edit's `apply_patch` call into a diff once its changes exist.
export function isStructuredAgentSessionToolAction(
body: AgentJournalItemBody | undefined
): body is StructuredAgentSessionToolAction {
return body?.kind === 'tool-call' || body?.kind === 'diff'
}
/** A diff carries no lifecycle, so it reads as settled; otherwise a finished edit would stay the
* running call for the rest of its turn. */
export function isRunningStructuredAgentSessionToolAction(
action: StructuredAgentSessionToolAction
): boolean {
return action.kind === 'tool-call' && action.state === 'running'
}
/** `itemId`: the journal row's, the call's id when the provider gave none. A call and its result
* are one row, so the result names its call by it; without an id a result pairs with the oldest
* unanswered call, which in a run of several rows can be another row's. */
export function structuredAgentSessionToolCallBlock(
action: StructuredAgentSessionToolAction,
itemId: string
): NativeChatToolCallBlock {
if (action.kind === 'diff') {
return { type: 'tool-call', name: 'Diff', input: { path: action.path }, callId: itemId }
}
return {
type: 'tool-call',
name: action.name,
input: action.input,
state: action.state,
...(action.endedAs !== undefined ? { endedAs: action.endedAs } : {}),
callId: action.callId ?? itemId,
...(action.mcpIdentity !== undefined ? { mcpIdentity: action.mcpIdentity } : {}),
...(action.exitCode !== undefined ? { exitCode: action.exitCode } : {}),
...(action.durationMs !== undefined ? { durationMs: action.durationMs } : {}),
...(action.webSearchResults !== undefined ? { webSearchResults: action.webSearchResults } : {})
}
}