Merge latest master into Web transcript

This commit is contained in:
Tianyi Cui
2026-07-31 11:39:49 +08:00
213 changed files with 5435 additions and 912 deletions

View File

@@ -314,6 +314,10 @@ export interface TrajectoryTableProps {
collapsedAssistants: ReadonlySet<number>
/** Toggle tool calls under one assistant record. */
onToggleAssistant: (index: number) => void
/** One-shot cross-view inspect: open and scroll to this call's record. */
inspectCallId?: string | null
/** Acknowledge a consumed (or unresolvable) inspect request. */
onInspectApplied?: (() => void) | undefined
}
/** One request identity paired with its session-global number. */
@@ -1497,6 +1501,8 @@ export function TrajectoryTable({
onToggleTurn,
collapsedAssistants,
onToggleAssistant,
inspectCallId = null,
onInspectApplied,
}: TrajectoryTableProps) {
const [selectedIndex, setSelectedIndex] = useState<number | null>(null)
const [selectedRequest, setSelectedRequest] = useState<SelectedRequest | null>(null)
@@ -1678,8 +1684,37 @@ export function TrajectoryTable({
if (target !== undefined) openRecordSummary(target)
}
// Cross-view inspect handoff: resolve the requested call to its record,
// open its summary, and remember the row to scroll once the un-collapsed
// ledger has rendered. Not-found leaves the request pending (`turns` in the
// deps retries as history pages in); the ack clears the store field.
const rootRef = useRef<HTMLDivElement>(null)
const pendingScrollIndex = useRef<number | null>(null)
const openRecordSummaryRef = useRef(openRecordSummary)
openRecordSummaryRef.current = openRecordSummary
useEffect(() => {
if (inspectCallId === null) return
const target = flattenRecords(turns).find(record => record.cell.callId === inspectCallId)
if (target === undefined) return
openRecordSummaryRef.current(target)
pendingScrollIndex.current = target.cell.index
onInspectApplied?.()
}, [inspectCallId, turns, onInspectApplied])
useEffect(() => {
const index = pendingScrollIndex.current
if (index === null) return
const row = rootRef.current
?.querySelector<HTMLElement>(`tr[data-record-index="${index}"]`)
if (row === undefined || row === null) return
pendingScrollIndex.current = null
/* v8 ignore next -- jsdom lacks scrollIntoView; browsers always have it. */
if (typeof row.scrollIntoView === 'function') {
row.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
return (
<div className={css.split} style={splitStyle}>
<div ref={rootRef} className={css.split} style={splitStyle}>
<div
className={css.tablePane}
onClick={(event) => {

View File

@@ -134,7 +134,7 @@ function searchMatches(
}
export function TrajectoryView({
useHistory, loadAllHistory,
useHistory, loadAllHistory, inspect, onInspectDone,
}: ConvViewProps & InjectFace<TrajectoryViewInjected>) {
const [collapsedTurns, setCollapsedTurns] = useState<ReadonlySet<number>>(EMPTY_IDS)
const [collapsedAssistants, setCollapsedAssistants] =
@@ -519,6 +519,8 @@ export function TrajectoryView({
onToggleTurn={toggleTurn}
collapsedAssistants={collapsedAssistants}
onToggleAssistant={toggleAssistant}
inspectCallId={inspect?.callId ?? null}
onInspectApplied={onInspectDone}
/>
</div>
</div>

View File

@@ -257,4 +257,50 @@ describe('TrajectoryTable', () => {
expect(screen.getByRole('row', { name: /ASSISTANT/ })).toBeTruthy()
expect(screen.getByRole('row', { name: /Collapsed turn summary/ })).toBeTruthy()
})
const CALL_TURNS: readonly TrajectoryTurnModel[] = [{
turn: 1,
groups: [{
title: 'Step 1',
cells: [{
index: 1,
kind: 'tool',
text: 'bash · {"command":"pwd"}',
inputDetail: '{"command":"pwd"}',
callId: 'call-1',
timeSeconds: 0.1,
}],
}],
}]
it('an inspect target opens the matching record and acknowledges once', () => {
const onInspectApplied = vi.fn()
render(
<TrajectoryTable
turns={CALL_TURNS}
{...FOLD_PROPS}
inspectCallId="call-1"
onInspectApplied={onInspectApplied}
/>,
)
expect(screen.getByRole('row', { name: /TOOL/ }).getAttribute('aria-selected')).toBe('true')
expect(screen.getByRole('complementary', { name: 'Event details' })).toBeTruthy()
expect(onInspectApplied).toHaveBeenCalledOnce()
})
it('an unmatched inspect target stays pending without acknowledgement', () => {
const onInspectApplied = vi.fn()
render(
<TrajectoryTable
turns={CALL_TURNS}
{...FOLD_PROPS}
inspectCallId="call-missing"
onInspectApplied={onInspectApplied}
/>,
)
expect(screen.getByRole('row', { name: /TOOL/ }).getAttribute('aria-selected')).toBe('false')
expect(onInspectApplied).not.toHaveBeenCalled()
})
})

View File

@@ -44,9 +44,7 @@ afterEach(cleanup)
// The chat store persists under its declared key; clear so one case's active
// view cannot rehydrate into the next.
beforeEach(() => {
// Node 22+ exposes an experimental localStorage global that is undefined
// without --localstorage-file; only clear when a real Storage is present.
if (typeof localStorage !== 'undefined') localStorage.clear()
localStorage.clear()
})
/** Node fixture: user prologue, two turns, one tool result inside turn 1. */