diff --git a/packages/client/runtime/src/client/index.ts b/packages/client/runtime/src/client/index.ts index 5ee2b3e492..c5fef9cc2e 100644 --- a/packages/client/runtime/src/client/index.ts +++ b/packages/client/runtime/src/client/index.ts @@ -29,7 +29,8 @@ export type { } from './contract/store.ts' export type { AssistantBlock, AssistantMessageNode, AssistantTiming, CodeSubCall, ComposerPhase, ContextMessageNode, - ConversationContext, ConversationContextOriginKind, ConversationNode, ConversationSnapshot, QueuedMessage, RunningToolCall, + ConversationContext, ConversationContextOriginKind, ConversationNode, ConversationPromptSnapshot, + ConversationSnapshot, QueuedMessage, RunningToolCall, SteeringMessageNode, ToolResultNode, UnknownSurfaceNode, UserMessageNode, } from './sessions/conversation.ts' export { PendingWait } from './sessions/pending.ts' diff --git a/packages/client/runtime/src/client/sessions/conversation.ts b/packages/client/runtime/src/client/sessions/conversation.ts index b888e8668f..bd75f83201 100644 --- a/packages/client/runtime/src/client/sessions/conversation.ts +++ b/packages/client/runtime/src/client/sessions/conversation.ts @@ -209,6 +209,14 @@ export type ComposerPhase = 'blank' | 'engaging' | 'active' /** Operation that started a new append-only model context. */ export type ConversationContextOriginKind = 'compaction' | 'rewind' | 'rewrite' +/** Latest complete model request header in force within one context generation. */ +export interface ConversationPromptSnapshot { + /** Rendered system prompt text; empty when the request had no system prompt. */ + system: string + /** Complete tool catalog sent with the request, including tools that were never called. */ + tools: readonly ToolSchema[] +} + /** One immutable model-context generation reconstructed from surface replacements. */ export interface ConversationContext { /** Zero-based generation within the session; stable across later appends. */ @@ -221,6 +229,8 @@ export interface ConversationContext { originSeq?: number /** Unix epoch ms of the replacement that created this generation. */ createdAt?: number + /** Latest request header observed in this generation, inherited until a later header replaces it. */ + prompt?: ConversationPromptSnapshot /** Final frozen nodes for historical generations, or current folded nodes for the tail. */ nodes: readonly ConversationNode[] } diff --git a/packages/client/runtime/src/client/sessions/fold-adapter.ts b/packages/client/runtime/src/client/sessions/fold-adapter.ts index ef715854f3..c7ac094017 100644 --- a/packages/client/runtime/src/client/sessions/fold-adapter.ts +++ b/packages/client/runtime/src/client/sessions/fold-adapter.ts @@ -13,6 +13,7 @@ import { import type { ToolCallView, ToolEventView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client' import type { AssistantTiming, ConversationContext, ConversationContextOriginKind, ConversationNode, + ConversationPromptSnapshot, } from './conversation.ts' import { toAssistantBlocks } from './conversation.ts' @@ -111,9 +112,12 @@ export class FoldAdapter { * reference-stability contract (§A.9.4) starts here. */ private rev = 0 private nodesResult: { rev: number; value: { nodes: ConversationNode[]; degraded: boolean } } | null = null - /** Revision of the model-visible surface only; log-only chunks do not rebuild context generations. */ - private surfaceRev = 0 + /** Revision of context structure or its request header; unrelated log-only events do not rebuild contexts. */ + private contextRev = 0 private contextsResult: { rev: number; value: readonly ConversationContext[] } | null = null + private contextGeneration = 0 + private activePrompt: ConversationPromptSnapshot | undefined + private promptsByContext = new Map() /** In-window tool/call index (Session uses it for runningCalls and result-card backfill). */ get callIndex(): ReadonlyMap { @@ -129,7 +133,7 @@ export class FoldAdapter { */ reset(events: readonly SessionEvent[], baseSeq: number, views?: readonly (ToolEventView | undefined)[]): void { this.rev++ - this.surfaceRev++ + this.contextRev++ this.baseSeq = baseSeq this.padded = [] for (let i = 0; i < baseSeq; i++) this.padded.push(paddingEvent(i)) @@ -139,10 +143,16 @@ export class FoldAdapter { this.degraded = false this.callIdx = new Map() this.resultViews.clear() + this.contextGeneration = 0 + this.activePrompt = undefined + this.promptsByContext = new Map() for (let i = 0; i < events.length; i++) { const event = events[i] /* v8 ignore next -- dense-array guard: i stays within events.length, so the undefined arm needs a sparse array no caller builds. */ - if (event !== undefined) this.indexCall(event, views?.[i]) + if (event !== undefined) { + this.indexCall(event, views?.[i]) + this.indexContextPrompt(event) + } } } @@ -154,9 +164,10 @@ export class FoldAdapter { */ append(event: SessionEvent, view?: ToolEventView): void { this.rev++ - if (isSurfaceEvent(event)) this.surfaceRev++ + if (isSurfaceEvent(event) || event.type === 'request/header') this.contextRev++ this.padded.push(event) this.indexCall(event, view) + this.indexContextPrompt(event) } /** @@ -207,13 +218,17 @@ export class FoldAdapter { * @returns Frozen historical contexts followed by the current context. */ contexts(): readonly ConversationContext[] { - if (this.contextsResult !== null && this.contextsResult.rev === this.surfaceRev) { + if (this.contextsResult !== null && this.contextsResult.rev === this.contextRev) { return this.contextsResult.value } const current = this.nodes() if (current.degraded) { - const value: readonly ConversationContext[] = [{ id: 0, nodes: current.nodes }] - this.contextsResult = { rev: this.surfaceRev, value } + const value: readonly ConversationContext[] = [{ + id: 0, + ...(this.activePrompt === undefined ? {} : { prompt: this.activePrompt }), + nodes: current.nodes, + }] + this.contextsResult = { rev: this.contextRev, value } return value } const value = this.surface.contexts.map((context): ConversationContext => { @@ -222,7 +237,14 @@ export class FoldAdapter { const node = this.materialize(seq) if (node !== undefined) nodes.push(node) } - if (context.origin === undefined) return { id: context.generation, nodes } + const prompt = this.promptsByContext.get(context.generation) + if (context.origin === undefined) { + return { + id: context.generation, + ...(prompt === undefined ? {} : { prompt }), + nodes, + } + } const originEvent = this.padded[context.origin.seq] return { id: context.generation, @@ -230,10 +252,11 @@ export class FoldAdapter { origin: contextOriginKind(originEvent), originSeq: context.origin.seq, ...(originEvent === undefined ? {} : { createdAt: originEvent.time }), + ...(prompt === undefined ? {} : { prompt }), nodes, } }) - this.contextsResult = { rev: this.surfaceRev, value } + this.contextsResult = { rev: this.contextRev, value } return value } @@ -303,6 +326,21 @@ export class FoldAdapter { // No backfill into already-materialized tool-result nodes for this callId // (window order puts the call before its result; cannot happen on the normal path). } + + private indexContextPrompt(event: SessionEvent): void { + if (isSurfaceEvent(event) && event.surfaceOp !== 'append') { + this.contextGeneration++ + if (this.activePrompt !== undefined) { + this.promptsByContext.set(this.contextGeneration, this.activePrompt) + } + } + if (event.type !== 'request/header') return + this.activePrompt = { + system: event.data.header.system ?? '', + tools: event.data.header.tools ?? [], + } + this.promptsByContext.set(this.contextGeneration, this.activePrompt) + } } function contextOriginKind(event: SessionEvent | undefined): ConversationContextOriginKind { @@ -310,10 +348,8 @@ function contextOriginKind(event: SessionEvent | undefined): ConversationContext const source = event.data.source if ( typeof source === 'object' - && source !== null && 'kind' in source && 'plugin' in source - && source.kind === 'plugin' ) { if (source.plugin === 'compact') return 'compaction' if (source.plugin === 'rewind') return 'rewind' diff --git a/packages/client/ui-primitives/src/JsonTree.module.css b/packages/client/ui-primitives/src/JsonTree.module.css index 34b690a0f0..ba6b206bfa 100644 --- a/packages/client/ui-primitives/src/JsonTree.module.css +++ b/packages/client/ui-primitives/src/JsonTree.module.css @@ -13,7 +13,8 @@ color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); font: 12px/16px var(--ds-font-family-code); - overscroll-behavior: contain; + overscroll-behavior-x: contain; + overscroll-behavior-y: auto; } :global(body[data-ds-dark-theme]) .root { @@ -78,8 +79,8 @@ position: absolute; z-index: 0; top: 0; - right: -100vw; - left: -100vw; + right: 0; + left: 0; height: 16px; background: var(--json-tree-hover); content: ''; @@ -134,7 +135,7 @@ } .copyAnchor { - position: absolute; + position: fixed; z-index: 3; display: inline-flex; } diff --git a/packages/client/ui-primitives/src/JsonTree.tsx b/packages/client/ui-primitives/src/JsonTree.tsx index 324c7abf1a..b7a31a61a1 100644 --- a/packages/client/ui-primitives/src/JsonTree.tsx +++ b/packages/client/ui-primitives/src/JsonTree.tsx @@ -235,14 +235,29 @@ export function JsonTree({ const rootRect = root.getBoundingClientRect() const rowRect = row.getBoundingClientRect() setCopyTarget({ - left: root.scrollLeft + root.clientWidth - 26, + left: rootRect.left + root.clientWidth - 26, path: target.path, side: rowRect.top - rootRect.top > root.clientHeight / 2 ? 'top' : 'bottom', - top: root.scrollTop + rowRect.top - rootRect.top, + top: rowRect.top, value: target.value, }) } + useEffect(() => { + const reposition = () => { + const row = activeRowRef.current + if (row === undefined) return + const resolved = resolveRow(data, row, expandTopLevel) + if (resolved !== undefined) positionCopyButton(row, resolved) + } + window.addEventListener('scroll', reposition, true) + window.addEventListener('resize', reposition) + return () => { + window.removeEventListener('scroll', reposition, true) + window.removeEventListener('resize', reposition) + } + }, [data, expandTopLevel]) + const clearCopyTarget = () => { setActiveRow(undefined) setCopyTarget(undefined) diff --git a/packages/client/ui-trajectory/src/client/TrajectoryCell.module.css b/packages/client/ui-trajectory/src/client/TrajectoryCell.module.css index 496a75c566..d92b53964b 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryCell.module.css +++ b/packages/client/ui-trajectory/src/client/TrajectoryCell.module.css @@ -52,13 +52,29 @@ } .tagContext { - color: var(--dsw-alias-label-secondary); - background: var(--dsw-alias-bg-layer-3); + color: color-mix( + in srgb, + var(--dsw-alias-state-success-primary) 68%, + var(--dsw-alias-label-secondary) + ); + background: var(--dsw-alias-state-success-tertiary); } .tagMessage { - color: var(--dsw-alias-brand-primary-new-colorprimary-new-color); - background: var(--dsw-specific-bubble); + color: color-mix( + in srgb, + var(--dsw-alias-brand-primary-new-colorprimary-new-color) 60%, + var(--dsw-alias-state-error-secondary) + ); + background: color-mix( + in srgb, + color-mix( + in srgb, + var(--dsw-alias-brand-primary-new-colorprimary-new-color) 55%, + var(--dsw-alias-state-error-secondary) + ) 15%, + var(--dsw-alias-bg-layer-1) + ); } .tagTool { @@ -69,8 +85,16 @@ /* run_code sub-dispatch cells: the business tint plus an indent so the nesting under the parent Tool cell reads at a glance. */ .tagSubtool { - color: var(--dsw-alias-state-business-primary); - background: var(--dsw-alias-state-business-tertiary); + color: color-mix( + in srgb, + var(--dsw-alias-state-warn-label) 62%, + var(--dsw-alias-label-tertiary) + ); + background: color-mix( + in srgb, + var(--dsw-alias-state-warn-tertiary) 58%, + var(--dsw-alias-bg-layer-1) + ); } .root[data-kind='subtool'] { diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css index f7098796a2..7ef67a9d38 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css @@ -79,7 +79,7 @@ transition: background-color 120ms var(--ds-ease-in-out); } -.table tbody tr:not([data-collapsed-summary]):hover { +.table tbody tr:not([data-collapsed-summary]):not([data-selected='true']):hover { background: var(--dsw-alias-interactive-bg-hover); } @@ -230,28 +230,53 @@ background: var(--dsw-alias-state-business-tertiary); } -.context { - border-color: var(--dsw-alias-border-l2); - color: var(--dsw-alias-label-secondary); - background: var(--dsw-alias-bg-layer-1); -} - -.message { - border-color: var(--dsw-alias-border-l2); +.systemNeutral { color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-module-platform); } -.tool { - border-color: var(--dsw-alias-border-l2); - color: var(--dsw-alias-label-primary); - background: var(--dsw-alias-bg-layer-1); +.contextGreen { + color: color-mix( + in srgb, + var(--dsw-alias-state-success-primary) 68%, + var(--dsw-alias-label-secondary) + ); + background: var(--dsw-alias-state-success-tertiary); } -.subtool { - border-color: var(--dsw-alias-border-l2); - color: var(--dsw-alias-state-business-primary); - background: transparent; +.assistantVioletBright { + color: color-mix( + in srgb, + var(--dsw-alias-brand-primary-new-colorprimary-new-color) 60%, + var(--dsw-alias-state-error-secondary) + ); + background: color-mix( + in srgb, + color-mix( + in srgb, + var(--dsw-alias-brand-primary-new-colorprimary-new-color) 55%, + var(--dsw-alias-state-error-secondary) + ) 15%, + var(--dsw-alias-bg-layer-1) + ); +} + +.toolAmber { + color: var(--dsw-alias-state-warn-label); + background: var(--dsw-alias-state-warn-tertiary); +} + +.subtoolAmber { + color: color-mix( + in srgb, + var(--dsw-alias-state-warn-label) 62%, + var(--dsw-alias-label-tertiary) + ); + background: color-mix( + in srgb, + var(--dsw-alias-state-warn-tertiary) 58%, + var(--dsw-alias-bg-layer-1) + ); } .contentText { @@ -326,6 +351,18 @@ white-space: nowrap; } +.toolCallNameTypeface { + color: var(--dsw-alias-label-primary); + font: 400 13px/18px Menlo, Consolas, 'Liberation Mono', 'PingFang SC', + 'Microsoft YaHei'; +} + +.toolCallPayload { + margin-left: 7px; + color: var(--dsw-alias-label-secondary); + font: 400 12px/18px var(--ds-font-family-code); +} + .table tbody tr[data-kind='tool'] .contentText, .table tbody tr[data-kind='subtool'] .contentText, .table tbody tr[data-kind='tool'] .resultPreview, @@ -475,7 +512,7 @@ height: 34px; padding: 0 8px; overflow-x: auto; - gap: 2px; + gap: 1px; border-bottom: 1px solid var(--dsw-alias-border-l2); scrollbar-width: none; } @@ -534,27 +571,32 @@ gap: 14px; } -.overviewHierarchyLink { +.overviewHierarchyNavLink { all: unset; display: inline-flex; align-items: center; - color: var(--dsw-alias-label-primary); + color: var(--dsw-alias-label-secondary); cursor: pointer; font: inherit; + gap: 1px; opacity: 1; - text-decoration-line: underline; - text-decoration-color: currentColor; - text-decoration-style: solid; - text-decoration-thickness: 1px; - text-underline-offset: 2px; } -.overviewHierarchyLink:hover { +.overviewHierarchyNavLink:hover { color: var(--dsw-alias-label-primary); - text-decoration-thickness: 2px; } -.overviewHierarchyLink:focus-visible { +.overviewHierarchyJumpIconTight { + flex: none; + color: var(--dsw-alias-label-caption); +} + +.overviewHierarchyNavLink:hover .overviewHierarchyJumpIconTight, +.overviewHierarchyNavLink:focus-visible .overviewHierarchyJumpIconTight { + color: var(--dsw-alias-label-primary); +} + +.overviewHierarchyNavLink:focus-visible { color: var(--dsw-alias-label-primary); outline: 1px solid var(--dsw-alias-state-business-primary); outline-offset: 2px; @@ -582,6 +624,16 @@ white-space: nowrap; } +.tokenEquation { + display: inline-flex; + align-items: baseline; +} + +.tokenOperator { + margin: 0 2px; + color: var(--dsw-alias-label-caption); +} + .overviewSections { border-top: 0; } @@ -651,8 +703,8 @@ padding-bottom: 0; } -.overviewPreview .jsonPreview > :last-child, -.overviewPreview .schemaTree > :last-child { +.overviewPreview .jsonPreview > :first-child, +.overviewPreview .schemaTree > :first-child { padding-bottom: 0; } @@ -1022,6 +1074,91 @@ white-space: nowrap; } +.systemPrompt { + box-sizing: border-box; +} + +.toolCatalog { + min-height: 100%; + padding: 8px 0 16px; +} + +.toolCatalogItem { + border-bottom: 1px solid var(--dsw-alias-border-l1); +} + +.toolCatalogSummary { + display: grid; + grid-template-columns: 12px 12px max-content minmax(0, 1fr); + align-items: center; + box-sizing: border-box; + min-height: 30px; + padding: 4px 12px; + gap: 5px; + cursor: pointer; + list-style: none; + user-select: none; +} + +.toolCatalogSummary::-webkit-details-marker { + display: none; +} + +.toolCatalogSummary:hover { + background: var(--dsw-alias-interactive-bg-hover); +} + +.toolCatalogSummary:focus-visible { + background: var(--dsw-alias-interactive-bg-hover); + outline: 1px solid var(--dsw-alias-state-business-primary); + outline-offset: -1px; +} + +.toolCatalogChevron, +.toolCatalogIcon { + color: var(--dsw-alias-label-caption); +} + +.toolCatalogChevron { + transition: transform 100ms var(--ds-ease-in-out); +} + +.toolCatalogItem[open] .toolCatalogChevron { + transform: rotate(90deg); +} + +.toolCatalogName { + color: var(--dsw-alias-label-primary); + font: 500 12px/18px var(--ds-font-family-code); +} + +.toolCatalogDescription { + min-width: 0; + overflow: hidden; + color: var(--dsw-alias-label-tertiary); + font: var(--dsw-font-xs-13); + text-overflow: ellipsis; + white-space: nowrap; +} + +.toolCatalogDefinition { + padding: 0 0 8px 29px; + background: var(--dsw-alias-bg-base); +} + +.toolCatalogFullDescription { + margin: 0; + padding: 7px 14px 4px 0; + color: var(--dsw-alias-label-secondary); + font: var(--dsw-font-xs-13); + white-space: pre-wrap; +} + +.toolCatalogTree { + margin-left: -14px; + margin-right: 6px; +} + .resultBlocks { display: flex; min-height: 100%; diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index db153276cd..2cd39ab5a8 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -5,6 +5,7 @@ import type { CSSProperties, ReactNode } from 'react' import { extractMarkdownPlainText, IconChevronRightOutline14, JsonTree, MarkdownText, } from '@deepseek-ai/dsh-client-ui-primitives' +import type { ConversationPromptSnapshot } from '@deepseek-ai/dsh-client-runtime/client' import type { AssistantMetricDetail, TrajectoryCellKind, TrajectoryCellProps, TrajectorySourceBlock, } from './trajectory-record.ts' @@ -31,7 +32,16 @@ interface TableRecord { collapsedSummaryKind?: 'turn' | 'assistant' } -type DetailTab = 'overview' | 'rendered' | 'source' | 'input' | 'output' | 'schema' | 'timing' +type DetailTab = + | 'system-prompt' + | 'tools' + | 'overview' + | 'rendered' + | 'source' + | 'input' + | 'output' + | 'schema' + | 'timing' type RecordState = 'complete' | 'running' | 'error' interface DetailTabItem { @@ -44,6 +54,11 @@ interface ParentRecords { tool?: TableRecord } +interface ToolCallTextParts { + name: string + args?: string +} + interface DetailsResizeDrag { pointerId: number startX: number @@ -61,6 +76,11 @@ const TOOL_REQUEST_MIN_WIDTH = 180 const TOOL_REQUEST_MAX_WIDTH = 480 const DEFAULT_TOOL_REQUEST_SHARE = 0.36 const DEFAULT_TOOL_REQUEST_OFFSET = 56 +const SYSTEM_PROMPT_INDEX = 0 +const SYSTEM_PROMPT_TABS: readonly DetailTabItem[] = [ + { id: 'system-prompt', label: 'System Prompt' }, + { id: 'tools', label: 'Tools' }, +] type TrajectorySplitStyle = CSSProperties & { '--trajectory-tool-request-width': string @@ -156,6 +176,8 @@ function AssistantTimingPanel({ metrics }: { metrics: AssistantMetricDetail }) { /** Props for the trajectory ledger. */ export interface TrajectoryTableProps { + /** Latest model request header in force for the selected context. */ + prompt?: ConversationPromptSnapshot /** Grouped records in display order. */ turns: readonly TrajectoryTurnModel[] /** Turn ids whose rows after the first are folded into a summary. */ @@ -326,9 +348,21 @@ function statusLabel(state: RecordState): string { return 'Completed' } -function tokenSummary(cell: TrajectoryCellProps): string { +function tokenSummary(cell: TrajectoryCellProps): ReactNode { if (cell.kind !== 'message') return '—' - return `${cell.input ?? '—'} / ${cell.output ?? '—'} / ${cell.think ?? '—'}` + if (cell.output === undefined) return '—' + if (cell.think === undefined) return String(cell.output) + return ( + + {cell.output} + = + + {Math.max(0, cell.output - cell.think)} + + + + {cell.think} + + ) } function isMarkdownRecord(record: TableRecord): boolean { @@ -409,6 +443,19 @@ function recordDisplayText(cell: TrajectoryCellProps): string { return plainText.replace(/\s+/g, ' ').trim() } +function toolCallTextParts( + kind: TrajectoryCellKind, + text: string, +): ToolCallTextParts | undefined { + if (kind !== 'tool' && kind !== 'subtool') return undefined + const separator = text.indexOf(' · ') + if (separator === -1) return { name: text } + return { + name: text.slice(0, separator), + args: text.slice(separator + 3), + } +} + function isToolCallOnly(cell: TrajectoryCellProps): boolean { return cell.kind === 'message' && !cell.outputDetail @@ -581,6 +628,55 @@ function AssistantToolCalls({ ) } +function ToolGlyph() { + return ( + + ) +} + +function ToolCatalog({ tools }: { tools: ConversationPromptSnapshot['tools'] }) { + if (tools.length === 0) return

No tools in this request

+ return ( +
+ {tools.map((tool, index) => ( +
+ + + + {tool.name} + {tool.description} + +
+ {tool.description !== '' && ( +

{tool.description}

+ )} + +
+
+ ))} +
+ ) +} + function ToolOutputBlocks({ blocks, preview, @@ -879,6 +975,7 @@ function OverviewSection({ * @returns The ledger and an optional local record inspector. */ export function TrajectoryTable({ + prompt, turns, collapsedTurns, onToggleTurn, @@ -895,9 +992,17 @@ export function TrajectoryTable({ const allRecords = flattenRecords(turns) const turnRecords = collapseTurnRecords(allRecords, collapsedTurns) const records = collapseAssistantRecords(turnRecords, collapsedAssistants) + const systemPromptPreview = prompt === undefined + ? 'Request header not recorded' + : prompt.system === '' + ? 'No system prompt' + : extractMarkdownPlainText(prompt.system).replace(/\s+/g, ' ').trim() + const promptSelected = selectedIndex === SYSTEM_PROMPT_INDEX const selected = allRecords.find(record => record.cell.index === selectedIndex) const selectedState = selected === undefined ? undefined : stateOf(selected) - const selectedTabs = selected === undefined ? [] : detailTabs(selected) + const selectedTabs = promptSelected + ? SYSTEM_PROMPT_TABS + : selected === undefined ? [] : detailTabs(selected) const selectedParents: ParentRecords = selected === undefined ? {} : parentRecords(allRecords, selected) @@ -924,6 +1029,11 @@ export function TrajectoryTable({ setActiveTab(recent ?? 'overview') } + const selectSystemPrompt = () => { + setSelectedIndex(SYSTEM_PROMPT_INDEX) + activateTab('system-prompt') + } + const openRecordSummary = (target: TableRecord) => { const targetAt = allRecords.findIndex(record => record.cell.index === target.cell.index) if (collapsedTurns.has(target.turn)) onToggleTurn(target.turn) @@ -954,8 +1064,42 @@ export function TrajectoryTable({ + { + if (event.key !== 'Enter' && event.key !== ' ') return + event.preventDefault() + selectSystemPrompt() + }} + > + + {promptSelected &&