diff --git a/packages/client/runtime/README.i18n.yaml b/packages/client/runtime/README.i18n.yaml index 3b19ab54c4..8f9b6ba17c 100644 --- a/packages/client/runtime/README.i18n.yaml +++ b/packages/client/runtime/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/runtime/README.md -README.md: 63876e2f2c762c5eeff95e065338413017e0a333 -README.zh.md: efa5e841efad1e5ce0f8c3af63eba00bcae1a363 +README.md: fbb979ad410e520e01220519b57dd428bbda14f1 +README.zh.md: 29e3f0ef46e4b016679cf17dc58d8fe1a67fca6c diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index 63876e2f2c..fbb979ad41 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -26,6 +26,10 @@ SlotsService gives the renderer separate bare observables for `useSessions` and Because the projection is log-ordered, the node array is seq-monotonic by construction: log-only `command/run` / `command/done` nodes splice in by seq, `Session` merges interrupted frozen nodes by their fractional seqs, and a window whose checkpoint cites a shadowed range outside it renders the marker with nothing logged. The marker's summary text comes from the checkpoint's `compact/summary` provenance; a window cut that left the provenance outside makes the row non-expandable rather than empty, and a later page that supplies it resolves the text. Performance contract: one append materializes at most one node and copies the projection only when it adds that node; an event that changes no node keeps the previous array reference (a chunk storm costs nothing), and unchanged nodes keep their object identity. +## Request inspection + +`SessionHistoryInspection.requests` is one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. + ## Code Mode sub-dispatch index `ConversationSnapshot.codeDispatches` groups a `run_code` call's sub-dispatches under their parent callId, in start order, using the native call-block shapes: a `tool/code-dispatch-start` event lands the `RunningToolCall` form (rows derive the running ring from the shape) and its `tool/code-dispatch` settlement replaces it in place with the `ToolResultNode` form, `callTime` carrying the paired start's time. A settle whose start fell outside the replay window appends directly with `callTime: null` (duration unknown — never a fabricated zero). Live mux frames and history replay build the identical index; sub-calls never join the transcript `nodes` flow; per-parent array and map references are memo-stable across unrelated snapshot swaps. diff --git a/packages/client/runtime/README.zh.md b/packages/client/runtime/README.zh.md index efa5e841ef..29e3f0ef46 100644 --- a/packages/client/runtime/README.zh.md +++ b/packages/client/runtime/README.zh.md @@ -26,6 +26,10 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 由于投影按日志顺序,节点数组天然按 seq 单调:仅日志的 `command/run` / `command/done` 节点按 seq 插入,`Session` 按分数 seq 归并被打断的冻结节点,而检查点所引范围落在窗口之外的窗口会渲染出标记且不打印任何日志。标记的摘要文本来自检查点的 `compact/summary` 溯源;窗口切分把溯源留在窗口外时该行不可展开而非空白,后续补上溯源的分页会解析出文本。性能契约:一次追加最多物化一个节点,并且仅在加入该节点时复制投影;不改变任何节点的事件保持上一次的数组引用(分片风暴零成本),未变化的节点保持其对象标识。 +## 请求检查 + +`SessionHistoryInspection.requests` 是一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn` 与 `step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。 + ## Code Mode 子调用索引 `ConversationSnapshot.codeDispatches` 按父调用的 callId 和启动顺序,用原生调用块形状组织一个 `run_code` 调用的子调用:`tool/code-dispatch-start` 事件落成 `RunningToolCall` 形状(行组件从该形状推导运行中的转圈状态),其 `tool/code-dispatch` 完结事件原位替换为 `ToolResultNode` 形状,`callTime` 携带成对 start 事件的时间。start 落在回放窗口之外的完结事件则直接追加,`callTime: null`(耗时未知——绝不伪造零耗时)。live mux 帧与历史回放构建相同的索引;子调用永不进入对话记录 `nodes` 流;无关快照交换不会改变每个父调用对应的数组引用和映射引用,两者均保持 memo 稳定。 diff --git a/packages/client/runtime/src/client/sessions/request-inspection.ts b/packages/client/runtime/src/client/sessions/request-inspection.ts index e15ad55c8b..c74e6aca9d 100644 --- a/packages/client/runtime/src/client/sessions/request-inspection.ts +++ b/packages/client/runtime/src/client/sessions/request-inspection.ts @@ -35,40 +35,55 @@ export interface RequestPromptChange { previous?: ConversationPromptSnapshot } -/** One provider request reconstructed from durable request lifecycle events. */ -export interface RequestView { - /** Request category; compaction is a purpose, not a separate projection. */ - purpose: 'assistant' | 'compaction' +/** Lifecycle fields shared by ordinary generation and compaction requests. */ +interface RequestViewBase { /** Sequence that opened the operation represented by this request. */ startSeq: number - turn: number - /** Agent-loop step, or zero for a direct compaction request. */ - step: number startedAt: number completedAt: number | null status: 'running' | 'complete' | 'error' error?: string - /** Effective ordinary request input, inherited until a later header changes it. */ - prompt?: ConversationPromptSnapshot - /** Prompt change logged while preparing this request. */ - promptChange?: RequestPromptChange provenance?: AssistantProvenanceView requestConfig?: AssistantRequestConfig usage?: unknown /** Assistant message or compaction summary sequence produced by this request. */ resultSeq?: number +} + +/** One ordinary assistant generation reconstructed from durable request events. */ +interface AssistantRequestView extends RequestViewBase { + purpose: 'assistant' + turn: number + /** Agent-loop step that issued this request. */ + step: number + /** Effective ordinary request input, inherited until a later header changes it. */ + prompt?: ConversationPromptSnapshot + /** Prompt change logged while preparing this request. */ + promptChange?: RequestPromptChange + /** Retry ordinal scheduled after a failed ordinary request. */ + retry?: number + maxRetries?: number + retryDelayMs?: number +} + +/** One compaction provider request, either turn-owned or standalone between turns. */ +interface CompactionRequestView extends RequestViewBase { + purpose: 'compaction' + /** Owning turn, or `null` when manual compaction ran between turns. */ + turn: number | null + /** Direct compaction requests do not consume an agent-loop step. */ + step: 0 /** Compaction replacement message sequence, when one was committed. */ replacementSeq?: number /** Safe compaction summary projection. */ summary?: readonly ContentBlock[] /** Complete compaction provider output before the safe projection. */ rawOutput?: readonly ContentBlock[] - /** Retry ordinal scheduled after a failed ordinary request. */ - retry?: number - maxRetries?: number - retryDelayMs?: number } +/** One provider request reconstructed from durable request lifecycle events. */ +export type RequestView = AssistantRequestView | CompactionRequestView + /** Immutable request-centric projection derived from one history window. */ export interface RequestInspectionSnapshot { requests: readonly RequestView[] @@ -110,7 +125,7 @@ interface CompactionStartEvent { type: 'compact/start' seq: number time: number - data: { turn: number } + data: { turn: number | null } } interface CompactionSummaryEvent { @@ -131,7 +146,7 @@ interface CompactionEndEvent { type: 'compact/end' seq: number time: number - data: { turn: number; error?: string } + data: { turn: number | null; error?: string } } function requestKey(turn: number, step: number): string { @@ -228,10 +243,21 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] let activePrompt: ConversationPromptSnapshot | undefined let activeCompaction: number | undefined - const update = (index: number | undefined, change: Partial): void => { + const updateAssistant = ( + index: number | undefined, + change: Partial>, + ): void => { if (index === undefined) return const request = requests[index] - if (request !== undefined) requests[index] = { ...request, ...change } + if (request?.purpose === 'assistant') requests[index] = { ...request, ...change } + } + const updateCompaction = ( + index: number | undefined, + change: Partial>, + ): void => { + if (index === undefined) return + const request = requests[index] + if (request?.purpose === 'compaction') requests[index] = { ...request, ...change } } for (const sourceEvent of events) { @@ -263,7 +289,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] } const change = promptChange(activePrompt, prompt, sourceEvent) activePrompt = prompt - update(activeStep === undefined ? undefined : ordinaryByStep.get(activeStep), { + updateAssistant(activeStep === undefined ? undefined : ordinaryByStep.get(activeStep), { prompt, requestConfig: prompt.config, ...(change === undefined ? {} : { promptChange: change }), @@ -278,8 +304,11 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] requestKey(sourceEvent.data.turn, sourceEvent.data.step), ) const request = index === undefined ? undefined : requests[index] - update(index, { - usage: addTokenUsage(request?.usage, sourceEvent.data.chunk.usage), + updateAssistant(index, { + usage: addTokenUsage( + request?.purpose === 'assistant' ? request.usage : undefined, + sourceEvent.data.chunk.usage, + ), }) continue } @@ -288,7 +317,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] requestKey(sourceEvent.data.turn, sourceEvent.data.step), ) const request = index === undefined ? undefined : requests[index] - update(index, { + updateAssistant(index, { completedAt: sourceEvent.time, status: 'complete', resultSeq: sourceEvent.seq, @@ -296,7 +325,9 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] provider: sourceEvent.data.message.source.provider, model: sourceEvent.data.message.source.model, }, - ...(request?.usage !== undefined || sourceEvent.data.usage === undefined + ...(request?.purpose === 'assistant' + && request.usage !== undefined + || sourceEvent.data.usage === undefined ? {} : { usage: sourceEvent.data.usage }), }) @@ -306,8 +337,8 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] const key = requestKey(sourceEvent.data.turn, sourceEvent.data.step) const index = ordinaryByStep.get(key) const request = index === undefined ? undefined : requests[index] - if (request?.status === 'running') { - update(index, { + if (request?.purpose === 'assistant' && request.status === 'running') { + updateAssistant(index, { completedAt: sourceEvent.time, status: 'error', }) @@ -317,7 +348,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] } if ((sourceEvent.type as string) === 'llm/retry') { const event = sourceEvent as unknown as RetryEvent - update(ordinaryByStep.get(requestKey(event.data.turn, event.data.step)), { + updateAssistant(ordinaryByStep.get(requestKey(event.data.turn, event.data.step)), { status: 'error', error: event.data.failure.message, retry: event.data.retry, @@ -328,7 +359,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] } if (sourceEvent.type === 'turn/end' && sourceEvent.data.reason.kind === 'error') { const reason = sourceEvent.data.reason - update(ordinaryByStep.get(requestKey(sourceEvent.data.turn, reason.step)), { + updateAssistant(ordinaryByStep.get(requestKey(sourceEvent.data.turn, reason.step)), { status: 'error', error: 'failure' in reason ? reason.failure.message : reason.message, }) @@ -352,7 +383,7 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] } if (type === 'compact/summary' && activeCompaction !== undefined) { const event = sourceEvent as unknown as CompactionSummaryEvent - update(activeCompaction, { + updateCompaction(activeCompaction, { resultSeq: event.seq, summary: event.data.summary, ...(event.data.rawOutput === undefined ? {} : { rawOutput: event.data.rawOutput }), @@ -375,12 +406,12 @@ function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] && activeCompaction !== undefined && isCompactionSource(sourceEvent.data.source) ) { - update(activeCompaction, { replacementSeq: sourceEvent.seq }) + updateCompaction(activeCompaction, { replacementSeq: sourceEvent.seq }) continue } if (type !== 'compact/end' || activeCompaction === undefined) continue const event = sourceEvent as unknown as CompactionEndEvent - update(activeCompaction, { + updateCompaction(activeCompaction, { completedAt: event.time, status: event.data.error === undefined ? 'complete' : 'error', ...(event.data.error === undefined ? {} : { error: event.data.error }), diff --git a/packages/client/runtime/tests/request-inspection.spec.ts b/packages/client/runtime/tests/request-inspection.spec.ts index a76036c076..7ba9b50951 100644 --- a/packages/client/runtime/tests/request-inspection.spec.ts +++ b/packages/client/runtime/tests/request-inspection.spec.ts @@ -85,6 +85,37 @@ describe('inspectRequests', () => { expect(snapshot.callSchemas.get('call-1')?.name).toBe('read') }) + it('preserves a standalone compaction owner without widening assistant turns', () => { + const snapshot = inspectRequests(entriesOf([ + at(0, 'compact/start', { turn: null }), + at(1, 'compact/summary', { + summary: [{ type: 'text', text: 'standalone summary' }], + provider: 'fake', + model: 'compact-model', + }), + at(2, 'compact/end', { turn: null }), + at(3, 'step/start', { turn: 2, step: 1 }), + ])) + + const [compaction, assistant] = snapshot.requests + expect(compaction).toMatchObject({ + purpose: 'compaction', + turn: null, + step: 0, + status: 'complete', + }) + expect(assistant).toMatchObject({ + purpose: 'assistant', + turn: 2, + step: 1, + status: 'running', + }) + if (assistant?.purpose === 'assistant') { + const turn: number = assistant.turn + expect(turn).toBe(2) + } + }) + it('captures schemas for nested tool dispatches from the active request header', () => { const snapshot = inspectRequests(entriesOf([ at(0, 'request/header', { @@ -179,6 +210,7 @@ describe('inspectRequests', () => { ])) expect(snapshot.callSchemas).toEqual(new Map()) - expect(snapshot.requests[0]?.prompt?.tools).toEqual([]) + const [request] = snapshot.requests + expect(request?.purpose === 'assistant' ? request.prompt?.tools : undefined).toEqual([]) }) }) diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml index dcaa1020a7..ec322d26bf 100644 --- a/packages/client/ui-trajectory/README.i18n.yaml +++ b/packages/client/ui-trajectory/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: b9c8b849b3454fe46e1fc37713d9d3b9449734cf -README.zh.md: 19ae5050a4c4f7dfe80de0ab58e772e9d26e3f6a +README.md: a65c11aed9dd74f9b0b60795441f876c1d64b3ad +README.zh.md: 853d1be9468f1b136452a2bb72f6be314f45d7ee diff --git a/packages/client/ui-trajectory/README.md b/packages/client/ui-trajectory/README.md index b9c8b849b3..a65c11aed9 100644 --- a/packages/client/ui-trajectory/README.md +++ b/packages/client/ui-trajectory/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Trajectory renders a turn-aware event ledger with selectable User, Assistant, Tool, and nested Subtool records. Thick rules mark Turn boundaries, compact inline markers identify Steps, and the main ledger keeps only index, event, and content; selection opens a local inspector for token usage, duration, Input, Output, and Timing. A fixed Overview above the ledger projects real record start/duration timing from left to right; dragging an interval focuses the ledger on every record active at any point in that inclusive range, while clearing the selection restores the full branch. The runtime's independent history source supplies raw context lineage and projects cancellation-frozen Assistant and Tool records, so Trajectory neither reads nor changes the Chat conversation snapshot. The package remains a pure-consumer plugin (registers one view tab into the conversation's `'conversation.view'` slot ring, provides no service, declares no Context merge). Contract: api-contracts v3 §8. +Trajectory renders a turn-aware event ledger with selectable User, Assistant, Tool, and nested Subtool records. Thick rules mark Turn boundaries, compact inline markers identify Steps, and the main ledger keeps only index, event, and content; selection opens a local inspector for token usage, duration, Input, Output, and Timing. A standalone compaction request appears chronologically in its own `Between turns` section, while a numbered compaction remains inside its owning turn. A fixed Overview above the ledger projects real record start/duration timing from left to right; dragging an interval focuses the ledger on every record active at any point in that inclusive range, while clearing the selection restores the full branch. The runtime's independent history source supplies raw context lineage and projects cancellation-frozen Assistant and Tool records, so Trajectory neither reads nor changes the Chat conversation snapshot. The package remains a pure-consumer plugin (registers one view tab into the conversation's `'conversation.view'` slot ring, provides no service, declares no Context merge). Contract: api-contracts v3 §8. ## Model Experience diff --git a/packages/client/ui-trajectory/README.zh.md b/packages/client/ui-trajectory/README.zh.md index 19ae5050a4..853d1be946 100644 --- a/packages/client/ui-trajectory/README.zh.md +++ b/packages/client/ui-trajectory/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -Trajectory 渲染按轮次组织的事件记录表,其中可选择用户、助手、工具和嵌套子工具记录。较粗的分割线标示轮次边界,紧凑的行内标记标识步骤,主记录表仅保留索引、事件和内容;选择记录则会打开局部检查器,查看 token 用量、耗时、输入、输出和计时。固定在记录表上方的 Overview 区域从左到右投影记录的真实开始时间与耗时;拖选一个区间会将记录表聚焦到活动区间与该闭区间有重叠的所有记录,清除选择则恢复完整分支。runtime 的独立历史数据源提供原始上下文谱系,并投影因取消而冻结的助手和工具记录,因此 Trajectory 既不读取也不改变 Chat 会话快照。该包(package)保持为纯消费方插件(向会话的 `'conversation.view'` slot 环注册一个视图标签页,不提供服务,也不声明 Context 合并)。契约:api-contracts v3 §8。 +Trajectory 渲染按轮次组织的事件记录表,其中可选择用户、助手、工具和嵌套子工具记录。较粗的分割线标示轮次边界,紧凑的行内标记标识步骤,主记录表仅保留索引、事件和内容;选择记录则会打开局部检查器,查看 token 用量、耗时、输入、输出和计时。独立运行的压缩请求会按时间顺序显示在自己的 `Between turns` 区段中,而带数值所有者的压缩仍位于其所属轮次内。固定在记录表上方的 Overview 区域从左到右投影记录的真实开始时间与耗时;拖选一个区间会将记录表聚焦到活动区间与该闭区间有重叠的所有记录,清除选择则恢复完整分支。runtime 的独立历史数据源提供原始上下文谱系,并投影因取消而冻结的助手和工具记录,因此 Trajectory 既不读取也不改变 Chat 会话快照。该包(package)保持为纯消费方插件(向会话的 `'conversation.view'` slot 环注册一个视图标签页,不提供服务,也不声明 Context 合并)。契约:api-contracts v3 §8。 ## 模型体验 diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index 09ad60b427..36c81fc2ad 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -27,7 +27,8 @@ const KIND_LABEL: Record = { } interface TableRecord { - turn: number + turn: number | null + section: number group: string groupStart: boolean turnStart: boolean @@ -69,7 +70,7 @@ interface ToolCallTextParts { } interface SelectedRequest { - turn: number + turn: number | null number: number group: string } @@ -237,15 +238,12 @@ export interface TrajectoryTableProps { onToggleAssistant: (index: number) => void } -/** One request identity paired with its session-global number. */ -export interface TrajectoryRequestNumber { +/** Request-inspector fields shared by ordinary generation and compaction. */ +interface TrajectoryRequestNumberBase { /** Request anchor event sequence; absent for the currently streaming ordinary request. */ seq?: number - turn: number - step: number group: string number: number - purpose?: 'compaction' status?: 'complete' | 'running' | 'error' startedAt?: number completedAt?: number | null @@ -261,6 +259,20 @@ export interface TrajectoryRequestNumber { cumulativeUsage?: TrajectoryUsage } +/** One purpose-discriminated request identity paired with its session-global number. */ +export type TrajectoryRequestNumber = TrajectoryRequestNumberBase & ( + | { + purpose?: 'assistant' + turn: number + step: number + } + | { + purpose: 'compaction' + turn: number | null + step: 0 + } +) + /** Disjoint provider token buckets for one request or a session prefix. */ export interface TrajectoryUsage { input?: number @@ -271,17 +283,18 @@ export interface TrajectoryUsage { } function flattenRecords(turns: readonly TrajectoryTurnModel[]): TableRecord[] { - return turns.flatMap((turn) => { - let firstInTurn = true + return turns.flatMap((turn, section) => { + let firstInSection = true const records = turn.groups.flatMap((group) => { return group.cells.map((cell, index) => { - const turnStart = firstInTurn + const turnStart = firstInSection && cell.requestOnly !== true && cell.kind !== 'system' - && cell.kind !== 'compacted' - if (turnStart) firstInTurn = false + && (cell.kind !== 'compacted' || turn.turn === null) + if (turnStart) firstInSection = false return { turn: turn.turn, + section, group: group.title, groupStart: index === 0, turnStart, @@ -305,18 +318,18 @@ function filterRecords( record.cell.requestOnly !== true && matches.has(record.cell.index), ) .map(record => ({ ...record, groupStart: false, turnStart: false, turnEnd: false })) - const startedTurns = new Set() + const startedSections = new Set() for (const [index, record] of filtered.entries()) { const previous = filtered[index - 1] const next = filtered[index + 1] record.groupStart = previous === undefined - || previous.turn !== record.turn + || previous.section !== record.section || previous.group !== record.group - record.turnStart = !startedTurns.has(record.turn) + record.turnStart = !startedSections.has(record.section) && record.cell.kind !== 'system' - && record.cell.kind !== 'compacted' - if (record.turnStart) startedTurns.add(record.turn) - record.turnEnd = next === undefined || next.turn !== record.turn + && (record.cell.kind !== 'compacted' || record.turn === null) + if (record.turnStart) startedSections.add(record.section) + record.turnEnd = next === undefined || next.section !== record.section } return filtered } @@ -327,10 +340,14 @@ function requestStep(group: string): number | undefined { return Number.isInteger(value) && value > 0 ? value : undefined } -function requestKey(turn: number, group: string): string { +function requestKey(turn: number | null, group: string): string { return `${turn}\u0000${group}` } +function sectionLabel(turn: number | null): string { + return turn === null ? 'Between turns' : `Turn ${turn}` +} + function indexRequestNumbers( records: readonly TableRecord[], sessionNumbers: readonly TrajectoryRequestNumber[] | undefined, @@ -372,12 +389,13 @@ function collapseTurnRecords( if (collapsedTurns.size === 0) return [...records] const recordsByTurn = new Map() for (const record of records) { + if (record.turn === null) continue const turnRecords = recordsByTurn.get(record.turn) ?? [] turnRecords.push(record) recordsByTurn.set(record.turn, turnRecords) } return records.flatMap((record) => { - if (!collapsedTurns.has(record.turn)) return [record] + if (record.turn === null || !collapsedTurns.has(record.turn)) return [record] const turnRecords = recordsByTurn.get(record.turn) ?? [record] if (record.cell.requestOnly === true || record.cell.kind === 'system') return [record] const contentRecords = turnRecords.filter(candidate => @@ -1486,7 +1504,7 @@ export function TrajectoryTable({ const selectedRequestCumulativeUsage = selectedRequestInfo?.cumulativeUsage ?? selectedRequestUsage const selectedRequestOptions = selectedRequestInfo?.requestConfig - const activeTurn = selectedRequest?.turn ?? selected?.turn + const activeTurn = selectedRequest === null ? selected?.turn : selectedRequest.turn const selectedTabs = selectedRequest !== null ? REQUEST_TABS.filter(tab => tab.id !== 'options' || selectedRequestOptions !== undefined) : selected === undefined ? [] : detailTabs(selected) @@ -1554,7 +1572,7 @@ export function TrajectoryTable({ const openRecordSummary = (target: TableRecord) => { const targetAt = allRecords.findIndex(record => record.cell.index === target.cell.index) - if (collapsedTurns.has(target.turn)) onToggleTurn(target.turn) + if (target.turn !== null && collapsedTurns.has(target.turn)) onToggleTurn(target.turn) if (target.cell.kind === 'tool' || target.cell.kind === 'subtool') { for (let i = targetAt - 1; i >= 0; i--) { const candidate = allRecords[i] @@ -1600,7 +1618,7 @@ export function TrajectoryTable({ && record.cell.index === allRecords[0]?.cell.index const request = record.groupStart && !isCollapsedSummary - && !collapsedTurns.has(record.turn) + && (record.turn === null || !collapsedTurns.has(record.turn)) ? requestNumbers.get(requestKey(record.turn, record.group)) : undefined const requestInfo = request === undefined @@ -1641,13 +1659,14 @@ export function TrajectoryTable({ ? undefined : isCollapsedSummary ? () => { - if (record.collapsedSummaryKind === 'turn') onToggleTurn(record.turn) - else onToggleAssistant(record.cell.index) + if (record.collapsedSummaryKind === 'turn' && record.turn !== null) { + onToggleTurn(record.turn) + } else onToggleAssistant(record.cell.index) } : () => { selectRecord(record.cell.index) }} onDoubleClick={(event) => { if (isCollapsedSummary || isRequestOnly) return - if (collapsedTurns.has(record.turn)) { + if (record.turn !== null && collapsedTurns.has(record.turn)) { event.preventDefault() onToggleTurn(record.turn) return @@ -1661,6 +1680,7 @@ export function TrajectoryTable({ return } if (!record.turnStart) return + if (record.turn === null) return if (allRecords.filter(candidate => candidate.turn === record.turn && candidate.cell.requestOnly !== true @@ -1673,8 +1693,9 @@ export function TrajectoryTable({ if (event.key !== 'Enter' && event.key !== ' ') return event.preventDefault() if (isCollapsedSummary) { - if (record.collapsedSummaryKind === 'turn') onToggleTurn(record.turn) - else onToggleAssistant(record.cell.index) + if (record.collapsedSummaryKind === 'turn' && record.turn !== null) { + onToggleTurn(record.turn) + } else onToggleAssistant(record.cell.index) return } selectRecord(record.cell.index) @@ -1701,7 +1722,9 @@ export function TrajectoryTable({ onDoubleClick={(event) => { event.stopPropagation() }} /> )} - {activeTurn === record.turn && !isInitialSystem && ( + {record.turn !== null + && activeTurn === record.turn + && !isInitialSystem && ( )}
@@ -1895,8 +1918,8 @@ export function TrajectoryTable({ {selectedRequestInfo?.purpose === 'compaction' - ? `Compaction · Turn ${selectedRequest.turn}` - : `Turn ${selectedRequest.turn}`} + ? `Compaction · ${sectionLabel(selectedRequest.turn)}` + : sectionLabel(selectedRequest.turn)} ) @@ -1927,8 +1950,8 @@ export function TrajectoryTable({ {selected.cell.kind === 'compacted' - ? `Turn ${selected.turn}` - : `Turn ${selected.turn} · ${selected.group}`} + ? sectionLabel(selected.turn) + : `${sectionLabel(selected.turn)} · ${selected.group}`} )} diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTimeline.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTimeline.tsx index 8ad1afefc1..8e74833ae4 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTimeline.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTimeline.tsx @@ -315,9 +315,9 @@ export const TrajectoryTimeline = memo(function TrajectoryTimeline({ )}