Merge finalized Web transcript parent

# Conflicts:
#	apps/cli/README.i18n.yaml
#	packages/client/runtime/README.i18n.yaml
#	packages/client/ui-trajectory/README.i18n.yaml
#	packages/client/ui-trajectory/README.zh.md
This commit is contained in:
Tianyi Cui
2026-07-31 13:42:47 +08:00
324 changed files with 10708 additions and 1388 deletions

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-trajectory/README.md
README.md: a65c11aed9dd74f9b0b60795441f876c1d64b3ad
README.zh.md: 853d1be9468f1b136452a2bb72f6be314f45d7ee
README.zh.md: 6e25d24c6b65673b3d003e624b6e0727be60c0e1

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
Trajectory 渲染按轮次组织的事件记录表,其中可选择用户、助手、工具和嵌套子工具记录。较粗的分割线标示轮次边界,紧凑的行内标记标识步骤,主记录表仅保留索引、事件和内容;选择记录则会打开局部检查器,查看 token 用量、耗时、输入、输出和计时。独立运行的压缩请求会按时间顺序显示在自己的 `Between turns` 区段中,而带数值所有者的压缩仍位于其所属轮次内。固定在记录表上方的 Overview 区域从左到右投影记录的真实开始时间与耗时;拖选一个区间会将记录表聚焦到活动区间与该闭区间有重叠的所有记录,清除选择则恢复完整分支。runtime 的独立历史数据源提供原始上下文谱系,并投影因取消而冻结的助手和工具记录,因此 Trajectory 既不读取也不改变 Chat 会话快照。该包package保持为纯消费方插件向会话的 `'conversation.view'` slot 环注册一个视图标签页,不提供服务,也不声明 Context 合并。契约api-contracts v3 §8。
Trajectory 渲染按轮次组织的事件记录表,其中可选择用户、助手、工具和嵌套子工具记录。较粗的分割线标示轮次边界,紧凑的行内标记标识步骤,主记录表仅保留索引、事件和内容;选择记录则会打开局部检查器,查看 token 用量、耗时、输入、输出和计时。独立运行的压缩compaction请求会按时间顺序显示在自己的 `Between turns` 区段中,而带数值所有者的压缩仍位于其所属轮次内。固定在记录表上方的 Overview 区域从左到右投影记录的真实开始时间与耗时;拖选一个区间会将记录表聚焦到活动区间与该闭区间有重叠的所有记录,清除选择则恢复完整分支。运行时的独立历史数据源提供原始上下文谱系,并投影因取消而冻结的助手和工具记录,因此 Trajectory 既不读取也不改变 Chat 会话快照。该包package保持为纯消费方插件向会话的 `'conversation.view'` slot 环注册一个视图标签页,不提供服务,也不声明 Context 合并。契约api-contracts v3 §8。
## 模型体验
@@ -10,7 +10,7 @@ Trajectory 渲染按轮次组织的事件记录表,其中可选择用户、助
#### KV Cache 影响
无;该包package既不组装也不发送提供方请求。
无;该包既不组装也不发送提供方请求。
## 已知限制与暂缓事项

View File

@@ -316,6 +316,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
}
/** Request-inspector fields shared by ordinary generation and compaction. */
@@ -1516,6 +1520,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)
@@ -1700,8 +1706,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] =
@@ -521,6 +521,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. */