Files
orca/src/shared/native-chat-work-run.test.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

55 lines
1.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
nativeChatWorkRunSpans,
type NativeChatWorkRunMember,
type NativeChatWorkRunRow
} from './native-chat-work-run'
/** `t` thought, `c` call, `l` lead, `x` a drawn row that ends runs, `-` a row that draws nothing;
* `|` starts the next scope. */
function rowsOf(pattern: string): NativeChatWorkRunRow[] {
const members: Record<string, NativeChatWorkRunMember> = { t: 'thought', c: 'tool', l: 'lead' }
let scope = 0
return [...pattern].flatMap((char) => {
if (char === '|') {
scope += 1
return []
}
return [{ member: members[char] ?? null, draws: char !== '-', scope: String(scope) }]
})
}
const spans = (pattern: string): number[][] => nativeChatWorkRunSpans(rowsOf(pattern))
describe('work run spans', () => {
it('runs thoughts and calls together until something else draws', () => {
expect(spans('tctctxct')).toEqual([
[0, 1, 2, 3, 4],
[6, 7]
])
})
it('needs two members and a call', () => {
expect(spans('c')).toEqual([])
expect(spans('tt')).toEqual([])
expect(spans('txc')).toEqual([])
})
it('lets a lead head a run but never join one', () => {
expect(spans('ltclt')).toEqual([
[0, 1, 2],
[3, 4]
])
expect(spans('cl')).toEqual([])
})
it('stays open across a row that draws nothing', () => {
expect(spans('c-t-c')).toEqual([[0, 2, 4]])
})
it('never crosses a scope, even through a row that draws nothing', () => {
expect(spans('ct|c')).toEqual([[0, 1]])
expect(spans('c|-|c')).toEqual([])
})
})