mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
fix(web): display known prompts in headerless history windows
Give appended Chat system messages their own cards and suppress the following initial or same-step update header card. Preserve replacement/header ownership and conservative unknown surface ordering. Trajectory uses prompt-only presentation when no request header is loaded, never fabricated provider, model, or tools; prepend deduplicates against real request changes. Test-first regressions showed missing Chat cards and missing trajectory prompt data; 148 focused tests and affected client project typechecks pass.
This commit is contained in:
@@ -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-chat/README.md
|
||||
README.md: 5995c8cdc2a96919780f3be7ebe67ea12584e17e
|
||||
README.zh.md: 23ba6084742e22684aa30b68adef75cbc5f4710d
|
||||
README.md: 1b60d08014ccbe7aa3f4e746556a250a2df27ec7
|
||||
README.zh.md: f80d5afe3ac4ff83ee257d0b52c98dcd7794e5fa
|
||||
|
||||
@@ -25,7 +25,7 @@ The browser Chat target for Conversation assembly. It registers Chat event defin
|
||||
<a id="system-prompt-row"></a>
|
||||
## System prompt row
|
||||
|
||||
Chat shows a collapsed `System prompt` row for a non-empty initial request, explicit message-series start, or `system/message` surface node replacement whose text differs, reading the last nonempty surviving system node in surface order at the `request/header`; a non-initial request whose preceding header is outside the loaded history window also shows one. A resume repeats the row even when its system text is unchanged, including after pagination supplies the preceding header and system node; same-series config-only or tool-only changes, tool steps, and retries create no repetition, and a `system/message` event is never rendered as a transcript message. The row appears before that request's user messages, matching the provider envelope, and expands to the exact model-visible text with its original line breaks. A request whose system node is empty or outside the loaded window creates no row until the page holding the node arrives.
|
||||
Each nonempty appended `system/message` owns a collapsed prompt row, including a complete prompt at the start of a headerless window; the same-step header does not duplicate it. Chat also shows a collapsed `System prompt` row for a non-empty initial request, explicit message-series start, or `system/message` surface node replacement whose text differs, reading the last nonempty surviving system node in surface order at the `request/header`; a non-initial request whose preceding header is outside the loaded history window also shows one. A resume repeats the row even when its system text is unchanged, including after pagination supplies the preceding header and system node; same-series config-only or tool-only changes, tool steps, and retries create no repetition, and a `system/message` event is never rendered as a transcript message. The row appears before that request's user messages, matching the provider envelope, and expands to the exact model-visible text with its original line breaks. A request whose system node is empty or outside the loaded window creates no row until the page holding the node arrives.
|
||||
|
||||
<a id="turn-token-usage"></a>
|
||||
## Turn token usage
|
||||
|
||||
@@ -25,7 +25,7 @@ Conversation 组装的浏览器 Chat target。本包注册 Chat event definition
|
||||
<a id="system-prompt-row"></a>
|
||||
## 系统提示词行
|
||||
|
||||
Chat 会为非空的初始请求、显式消息序列起点、文本发生变化的 `system/message` surface 节点替换(文本读取自 `request/header` 处 surface 顺序中最后一个非空存活系统节点),或前序 header 尚未进入已加载历史窗口的非初始请求显示一行默认折叠的`系统提示词`。即使系统文本未变,resume 也会重复该行,包括分页补齐前序 header 和系统节点后;同一序列内仅配置或仅工具变化、工具步骤与重试不会重复,且 `system/message` 事件绝不会渲染为对话消息。该行位于请求的用户消息之前,与提供方 envelope 顺序一致;展开后显示保留原始换行的精确模型可见文本。系统节点为空或位于已加载窗口之外的请求不创建该行,直到包含该节点的分页到达。
|
||||
每个非空追加的 `system/message` 都拥有一行折叠提示词,包括无 header 窗口起点的完整提示词;同一步骤的 header 不会重复它。Chat 也会为非空的初始请求、显式消息序列起点、文本发生变化的 `system/message` surface 节点替换(文本读取自 `request/header` 处 surface 顺序中最后一个非空存活系统节点),或前序 header 尚未进入已加载历史窗口的非初始请求显示一行默认折叠的`系统提示词`。即使系统文本未变,resume 也会重复该行,包括分页补齐前序 header 和系统节点后;同一序列内仅配置或仅工具变化、工具步骤与重试不会重复,且 `system/message` 事件绝不会渲染为对话消息。该行位于请求的用户消息之前,与提供方 envelope 顺序一致;展开后显示保留原始换行的精确模型可见文本。系统节点为空或位于已加载窗口之外的请求不创建该行,直到包含该节点的分页到达。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -51,12 +51,10 @@ function stableRequestPromptAnchor(
|
||||
/**
|
||||
* System-prompt surface node Definition for the Chat target. It owns every
|
||||
* `system/message` event on the Chat target so the unknown-surface fallback
|
||||
* never renders the prompt as a transcript row. A node that introduces or
|
||||
* replaces the prompt materializes no Node: the request-prompt Definition
|
||||
* reads its State through `reader.previous` and presents the prompt as the
|
||||
* request's `system-prompt` card. An in-history update — a prompt appended
|
||||
* after an earlier loaded system node — is the model-visible change at that
|
||||
* position, so it presents its own `system-prompt` card there. Positional
|
||||
* never renders the prompt as a transcript row. Each nonempty append owns a
|
||||
* prompt card, even without a loaded request header. Initial cards precede
|
||||
* their step's input; in-history updates stay at their own positions. The
|
||||
* request-prompt Definition owns replacement and later-series cards. Positional
|
||||
* replacements advance the effective prompt without changing historical cards.
|
||||
* @param inspect - Pure surface interpretation supplied by uiConversation.
|
||||
* @returns The Chat system-prompt Definition.
|
||||
@@ -75,8 +73,10 @@ export function systemMessageDefinition(inspect: SystemPromptInspector): Convers
|
||||
update: context => context.state,
|
||||
buildViewNode: (context) => {
|
||||
const state = context.state?.introduced
|
||||
if (state === undefined || !state.update || state.text === '') return null
|
||||
return chatNode(context, 'system-prompt', state.seq, { text: state.text, update: true })
|
||||
if (state === undefined || state.text === ''
|
||||
|| context.start?.event.type !== 'system/message' || context.start.event.surfaceOp !== 'append') return null
|
||||
const anchor = state.update ? state.seq : requestPromptAnchor(context.start, undefined, true)
|
||||
return chatNode(context, 'system-prompt', anchor, { text: state.text, ...state.update ? { update: true } : {} })
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -100,15 +100,18 @@ export function requestPromptDefinition(inspect: RequestPromptInspector): Conver
|
||||
throw new Error('request-prompt start requires request/header')
|
||||
}
|
||||
const previous = reader.previous<RequestPromptState>('request-prompt')?.state
|
||||
const system = reader.previous<SystemPromptState>('system-message')?.state.effective
|
||||
const systemContext = reader.previous<SystemPromptState>('system-message')
|
||||
const system = systemContext?.state.effective
|
||||
const location = match.location.kind === 'step'
|
||||
? { turn: match.location.turn.turn, step: match.location.step.step }
|
||||
: {}
|
||||
const inspection = inspect(previous?.prompt, match.event, system)
|
||||
const change = inspection.change?.kind
|
||||
// An in-history update committed in this same step already shows the
|
||||
// prompt at its own position.
|
||||
const shownByUpdate = system?.update === true
|
||||
// Appended prompts own their cards; a same-step header must not repeat them.
|
||||
const systemEvent = systemContext?.matches[0]?.event
|
||||
const shownByUpdate = system !== undefined
|
||||
&& systemEvent?.type === 'system/message' && systemEvent.surfaceOp === 'append'
|
||||
&& (system.update || previous === undefined)
|
||||
&& system.turn === location.turn
|
||||
&& system.step === location.step
|
||||
return {
|
||||
|
||||
@@ -1475,7 +1475,7 @@ describe('built-in conversation node Definitions', () => {
|
||||
const prompts = current.nodes.values()
|
||||
.filter(candidate => candidate.kind === 'system-prompt')
|
||||
expect(prompts.map(prompt => ({ anchorSeq: prompt.anchorSeq, data: prompt.data }))).toEqual([
|
||||
{ anchorSeq: 2, data: { text: '# Initial' } },
|
||||
{ anchorSeq: 1, data: { text: '# Initial' } },
|
||||
{ anchorSeq: 5, data: { text: '# Initial' } },
|
||||
{ anchorSeq: 6, data: { text: '# Initial' } },
|
||||
{ anchorSeq: 8, data: { text: '# Updated' } },
|
||||
@@ -1483,6 +1483,25 @@ describe('built-in conversation node Definitions', () => {
|
||||
expect(current.nodes.values().filter(candidate => candidate.kind === 'unknown')).toEqual([])
|
||||
})
|
||||
|
||||
it('shows a complete appended prompt at the start of a headerless window', () => {
|
||||
const value = assembler([
|
||||
systemUpdateAt(10, '# Known prompt', 2, 1),
|
||||
at(11, 'user/message', textMessage('window-user', 'continue'), { surfaceOp: 'append' }),
|
||||
], true)
|
||||
const current = snapshot(value)
|
||||
expect(current.nodes.values().filter(candidate => candidate.kind === 'system-prompt')
|
||||
.map(candidate => candidate.data)).toEqual([{ text: '# Known prompt' }])
|
||||
expect(current.nodes.values().filter(candidate => candidate.kind === 'unknown')).toEqual([])
|
||||
value.prepend([
|
||||
systemAt(1, '# Original'),
|
||||
at(2, 'request/header', { reason: 'initial', header: { config: { provider: 'fake', model: 'fake' } } }),
|
||||
], false)
|
||||
value.flush()
|
||||
const restored = snapshot(value)
|
||||
expect(restored.order.map(key => restored.nodes.get(key)).filter(candidate => candidate?.kind === 'system-prompt')
|
||||
.map(candidate => candidate?.data)).toEqual([{ text: '# Original' }, { text: '# Known prompt', update: true }])
|
||||
})
|
||||
|
||||
it('withholds windowed replacement prompts until prepend resolves their positions', () => {
|
||||
const windowed = assembler([
|
||||
systemAt(10, '# Resumed prompt', 5),
|
||||
@@ -1584,7 +1603,8 @@ describe('built-in conversation node Definitions', () => {
|
||||
if (mode === 'partial') {
|
||||
value.replaceWindow(history.slice(7), true)
|
||||
value.flush()
|
||||
expect(snapshot(value).nodes.values().filter(candidate => candidate.kind === 'system-prompt')).toEqual([])
|
||||
expect(snapshot(value).nodes.values().filter(candidate => candidate.kind === 'system-prompt').map(candidate => candidate.data))
|
||||
.toEqual([{ text: 'B' }])
|
||||
value.prepend(history.slice(0, 7), false)
|
||||
value.flush()
|
||||
}
|
||||
@@ -1594,8 +1614,9 @@ describe('built-in conversation node Definitions', () => {
|
||||
value.flush()
|
||||
}
|
||||
}
|
||||
expect(snapshot(value).nodes.values().filter(candidate => candidate.kind === 'system-prompt')
|
||||
.map(candidate => candidate.data)).toEqual([
|
||||
const current = snapshot(value)
|
||||
expect(current.order.map(key => current.nodes.get(key)).filter(candidate => candidate?.kind === 'system-prompt')
|
||||
.map(candidate => candidate?.data)).toEqual([
|
||||
{ text: 'A' }, { text: 'B', update: true }, { text: 'A' },
|
||||
])
|
||||
})
|
||||
@@ -1606,10 +1627,12 @@ describe('built-in conversation node Definitions', () => {
|
||||
at(8, 'request/header', { reason: 'resume', header: { config: { provider: 'test', model: 'test' } } }),
|
||||
], true)
|
||||
expect(node(snapshot(value), 'system-prompt')).toBeUndefined()
|
||||
const uncertain = assembler([systemAt(6, 'C', 3), systemUpdateAt(7, 'Known but unordered', 1, 2)], true)
|
||||
expect(node(snapshot(uncertain), 'system-prompt')).toBeUndefined()
|
||||
value.prepend([systemAt(1, 'A'), systemAt(3, 'B'), systemAt(5, 'A2', 1)], false)
|
||||
value.flush()
|
||||
expect(snapshot(value).nodes.values().filter(candidate => candidate.kind === 'system-prompt')
|
||||
.map(candidate => candidate.data)).toEqual([{ text: 'B', update: true }, { text: 'C' }])
|
||||
.map(candidate => candidate.data)).toEqual([{ text: 'A' }, { text: 'B', update: true }, { text: 'C' }])
|
||||
})
|
||||
|
||||
it('never renders a system/message as a transcript bubble', () => {
|
||||
@@ -1621,12 +1644,12 @@ describe('built-in conversation node Definitions', () => {
|
||||
])
|
||||
|
||||
const current = snapshot(value)
|
||||
expect(current.order.map(key => current.nodes.get(key)?.kind)).toEqual(['user'])
|
||||
expect(current.order.map(key => current.nodes.get(key)?.kind)).toEqual(['system-prompt', 'user'])
|
||||
|
||||
value.append(systemAt(5, '# Replaced', 3))
|
||||
value.flush()
|
||||
const replaced = snapshot(value)
|
||||
expect(replaced.order.map(key => replaced.nodes.get(key)?.kind)).toEqual(['user'])
|
||||
expect(replaced.order.map(key => replaced.nodes.get(key)?.kind)).toEqual(['system-prompt', 'user'])
|
||||
})
|
||||
|
||||
it('presents an in-history prompt update as its own card and lets no same-step header repeat it', () => {
|
||||
|
||||
@@ -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: 2dd236ecec41137e39b017b79bfbec84d536bebc
|
||||
README.zh.md: a7a521bfe0a0ce2b76633e3145e67d87b344c20f
|
||||
README.md: ea7e90221317dd62beb259773a8d9872e7b01ddb
|
||||
README.zh.md: 1e875c48d66bbd80057ccaecb86219f8de6540e8
|
||||
|
||||
@@ -45,7 +45,7 @@ A fixed Overview above the ledger projects real record start/duration timing fro
|
||||
|
||||
The view is a pure projection: Trajectory-owned Definitions assemble business records from the shared Session window — including durable cancellation-finalized prefixes, chunk-only interruption fallbacks, and interrupted Tool records — so Trajectory neither reads nor changes the Chat conversation snapshot. Its steering classifier retains only next-step Inbox IDs through persistent splice state and shares each current claimed batch across later Contexts.
|
||||
|
||||
In-history system prompt changes compare against the most recent request state, including earlier prompt updates without a new request header. Each request retains the prompt and change that applied at its own position. Surface replacements, including compaction, restore the last nonempty surviving system prompt even without a new system event; an unloaded prompt remains unavailable until its page arrives.
|
||||
A complete appended prompt without a loaded request header appears as a standalone system row; only its known text is available, with no inferred request options or tool catalog. Prepending its request history replaces that standalone presentation without duplicating the prompt. In-history system prompt changes compare against the most recent request state, including earlier prompt updates without a new request header. Each request retains the prompt and change that applied at its own position. Surface replacements, including compaction, restore the last nonempty surviving system prompt even without a new system event; an unloaded prompt remains unavailable until its page arrives.
|
||||
|
||||
### Virtual rows
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ kind: "package-reference"
|
||||
|
||||
视图是纯投影:Trajectory 自有的 Definition 从共享 Session 窗口组装业务记录——包括持久化的取消定稿前缀、只能从分片恢复的打断前缀与被打断的工具记录——因此 Trajectory 既不读取也不改变 Chat 会话快照。其 steering 分类器通过持久 splice state 只保留 next-step Inbox ID,并让后续 Context 共享当前 claimed batch。
|
||||
|
||||
历史中的系统提示词变更与最近的请求状态比较,包括没有新请求头的先前提示词更新。每个请求保留其所在位置生效的提示词与变更。包括压缩在内的 surface 替换会恢复最后一个非空的存活系统提示词,即使没有新的系统事件;未加载的提示词在对应分页到达前仍不可用。
|
||||
完整的追加提示词在请求头未加载时显示为独立系统行;仅提供已知文本,不推断请求选项或工具目录。补入其请求历史后,该独立展示被替代而不重复提示词。历史中的系统提示词变更与最近的请求状态比较,包括没有新请求头的先前提示词更新。每个请求保留其所在位置生效的提示词与变更。包括压缩在内的 surface 替换会恢复最后一个非空的存活系统提示词,即使没有新的系统事件;未加载的提示词在对应分页到达前仍不可用。
|
||||
|
||||
### 虚拟行
|
||||
|
||||
|
||||
@@ -954,6 +954,9 @@ function markdownSource(record: TableRecord): string | undefined {
|
||||
|
||||
function detailTabs(record: TableRecord): readonly DetailTabItem[] {
|
||||
if (record.cell.kind === 'system') {
|
||||
if (record.cell.promptDetail === undefined && record.cell.systemPromptDetail !== undefined) {
|
||||
return SYSTEM_PROMPT_TABS.filter(tab => tab.id === 'system-prompt')
|
||||
}
|
||||
return record.cell.previousPromptDetail === undefined
|
||||
? SYSTEM_PROMPT_TABS
|
||||
: SYSTEM_UPDATE_TABS
|
||||
@@ -1961,7 +1964,8 @@ export function TrajectoryTable({
|
||||
const selectedPreviousPrompt = selected?.cell.kind === 'system'
|
||||
? selected.cell.previousPromptDetail
|
||||
: undefined
|
||||
const promptSelected = selectedPrompt !== undefined
|
||||
const selectedSystemPrompt = selectedPrompt?.system ?? selected?.cell.systemPromptDetail
|
||||
const promptSelected = selectedSystemPrompt !== undefined
|
||||
const selectedState = selected === undefined ? undefined : stateOf(selected)
|
||||
const selectedRequestInfo = selectedRequest === null
|
||||
? undefined
|
||||
@@ -2947,7 +2951,7 @@ export function TrajectoryTable({
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{promptSelected
|
||||
{selectedPrompt !== undefined
|
||||
&& selectedPreviousPrompt !== undefined
|
||||
&& activeTab === 'diff' && (
|
||||
<SystemPromptDiff
|
||||
@@ -2957,15 +2961,15 @@ export function TrajectoryTable({
|
||||
/>
|
||||
)}
|
||||
{promptSelected && activeTab === 'system-prompt' && (
|
||||
selectedPrompt.system === ''
|
||||
selectedSystemPrompt === ''
|
||||
? <p className={css.noPayload}>{t('record.systemPromptMissing')}</p>
|
||||
: (
|
||||
<div className={`${css.markdownPayload} ${css.systemPrompt}`}>
|
||||
<MarkdownText text={selectedPrompt.system} labels={markdownLabels(t)} />
|
||||
<MarkdownText text={selectedSystemPrompt} labels={markdownLabels(t)} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{promptSelected && activeTab === 'tools' && (
|
||||
{selectedPrompt !== undefined && activeTab === 'tools' && (
|
||||
<ToolCatalog tools={selectedPrompt.tools} t={t} />
|
||||
)}
|
||||
{!promptSelected
|
||||
|
||||
@@ -319,12 +319,13 @@ export function TrajectoryView({
|
||||
: { turn: partialTurn, step: partialStep, blocks: [] },
|
||||
runningCalls,
|
||||
requests,
|
||||
systemPrompts: inspection.systemPrompts,
|
||||
callSchemas,
|
||||
}, t)
|
||||
return { turns, lastIndex: lastCellIndex(turns) }
|
||||
}, [
|
||||
nodes, eventLocations, partialTurn, partialStep,
|
||||
runningCalls, requests, callSchemas, t,
|
||||
runningCalls, requests, inspection.systemPrompts, callSchemas, t,
|
||||
])
|
||||
const timelinePartialSignature = partialStructureSignature(partial)
|
||||
const timelinePartial = useMemo<TrajectorySnapshot['partial']>(() => partial === null
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface TrajectoryTurnModel {
|
||||
|
||||
/** Snapshot slice the trajectory view folds. */
|
||||
export interface TrajectoryLayoutInput {
|
||||
systemPrompts?: TrajectorySnapshot['systemPrompts']
|
||||
nodes: TrajectorySnapshot['eventNodes']
|
||||
eventLocations?: ReadonlyMap<number, ConversationLocation>
|
||||
partial: TrajectorySnapshot['partial']
|
||||
@@ -94,7 +95,8 @@ type OrderedLayoutEntry =
|
||||
| {
|
||||
kind: 'system'
|
||||
seq: number
|
||||
request: AssistantRequestView
|
||||
request?: AssistantRequestView
|
||||
systemPrompt?: string
|
||||
change: RequestPromptChange
|
||||
}
|
||||
| {
|
||||
@@ -234,6 +236,10 @@ export function deriveTrajectoryLayout(
|
||||
}
|
||||
|
||||
const entries: OrderedLayoutEntry[] = [
|
||||
...(input.systemPrompts ?? []).map(prompt => ({
|
||||
kind: 'system' as const, seq: prompt.seq, systemPrompt: prompt.text,
|
||||
change: { seq: prompt.seq, time: prompt.time, kind: prompt.update ? 'system' as const : 'initial' as const },
|
||||
})),
|
||||
...nodes.map((node, nodeIndex) => ({
|
||||
kind: 'node' as const,
|
||||
seq: node.seq,
|
||||
@@ -306,7 +312,8 @@ export function deriveTrajectoryLayout(
|
||||
kind: 'system',
|
||||
text: promptChangeLabel(change, t),
|
||||
sourceSeq: change.seq,
|
||||
...(request.prompt === undefined ? {} : { promptDetail: request.prompt }),
|
||||
...(request?.prompt === undefined ? {} : { promptDetail: request.prompt }),
|
||||
...(entry.systemPrompt === undefined ? {} : { systemPromptDetail: entry.systemPrompt }),
|
||||
...(change.previous === undefined
|
||||
? {}
|
||||
: { previousPromptDetail: change.previous }),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
AssistantMessageNode, ConversationLocation, ConversationNode, ConversationPromptSnapshot,
|
||||
ConversationViewNode, MessageImagesOwnerProps, PartialAssistant, RequestPromptChange,
|
||||
RequestView, RunningToolCall, ToolCallBlock,
|
||||
RequestView, RunningToolCall, SystemPromptNode, ToolCallBlock,
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface TrajectoryRequestHeaderState {
|
||||
|
||||
/** One independently assembled contribution to the legacy Trajectory ledger. */
|
||||
export type TrajectoryContribution =
|
||||
| { readonly kind: 'system-prompt'; readonly prompt: SystemPromptNode }
|
||||
| {
|
||||
readonly kind: 'node'
|
||||
readonly node: ConversationNode
|
||||
@@ -61,6 +62,8 @@ export interface TrajectoryConversationViewNode extends ConversationViewNode {
|
||||
|
||||
/** Stage-oriented Trajectory data assembled from registered business Contexts. */
|
||||
export interface TrajectorySnapshot {
|
||||
/** Complete loaded prompt text whose request header is outside the window. */
|
||||
readonly systemPrompts?: readonly SystemPromptNode[]
|
||||
readonly eventNodes: readonly ConversationNode[]
|
||||
readonly eventLocations: ReadonlyMap<number, ConversationLocation>
|
||||
readonly requests: readonly RequestView[]
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface TrajectoryCellProps extends HTMLAttributes<HTMLDivElement> {
|
||||
inputDetail?: string
|
||||
/** Complete system-prompt/tool-catalog state introduced by a SYSTEM record. */
|
||||
promptDetail?: ConversationPromptSnapshot
|
||||
/** Known prompt text without a loaded request config or tool catalog. */
|
||||
systemPromptDetail?: string
|
||||
/** System-prompt/tool-catalog state replaced by a SYSTEM update. */
|
||||
previousPromptDetail?: ConversationPromptSnapshot
|
||||
/** Full assistant/tool result content for the details panel. */
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface TrajectorySystemMessageState extends SystemPromptState {
|
||||
* Definition retaining each `system/message` surface node for the Trajectory
|
||||
* request-header Definition, which reads it through `reader.previous` and
|
||||
* presents the prompt through the request's `system` cell. A node that
|
||||
* introduces or replaces the prompt contributes nothing itself; an in-history
|
||||
* lacks a loaded header contributes known text without request config or tools; an in-history
|
||||
* update contributes a request-header fact at its own position, since no
|
||||
* `request/header` follows a prompt change that keeps the cached history.
|
||||
* Surface replacements also contribute prompt changes when they remove the
|
||||
@@ -74,13 +74,17 @@ function trajectorySystemMessageDefinition(inspect: SystemPromptInspector): Conv
|
||||
}
|
||||
},
|
||||
update: context => context.state,
|
||||
buildViewNode: context => context.state?.header === undefined
|
||||
|| context.state.header.seq !== context.start?.event.seq
|
||||
? null
|
||||
: trajectoryNode(context, context.state.header.seq, {
|
||||
kind: 'request-header',
|
||||
header: context.state.header,
|
||||
}),
|
||||
buildViewNode: (context) => {
|
||||
const state = context.state
|
||||
if (state?.header !== undefined && state.header.seq === context.start?.event.seq) {
|
||||
return trajectoryNode(context, state.header.seq, { kind: 'request-header', header: state.header })
|
||||
}
|
||||
const prompt = state?.introduced
|
||||
return prompt !== undefined && prompt.text !== ''
|
||||
&& context.start?.event.type === 'system/message' && context.start.event.surfaceOp === 'append'
|
||||
? trajectoryNode(context, prompt.seq, { kind: 'system-prompt', prompt })
|
||||
: null
|
||||
},
|
||||
}
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
@@ -199,6 +199,9 @@ export class TrajectorySnapshotBuilder implements ConversationViewBuilder<
|
||||
: previous?.change === undefined ? {} : { change: previous.change }),
|
||||
})
|
||||
}
|
||||
const representedPrompts = new Set(this.contributions.flatMap(node =>
|
||||
node.data.kind === 'request-header' && node.data.header.change !== undefined ? [node.data.header.change.seq] : []))
|
||||
const systemPrompts: NonNullable<TrajectorySnapshot['systemPrompts']>[number][] = []
|
||||
const finalized: ConversationNode[] = []
|
||||
const eventLocations = new Map<number, TrajectoryConversationViewNode['location']>()
|
||||
const requests: RequestView[] = []
|
||||
@@ -218,6 +221,10 @@ export class TrajectorySnapshotBuilder implements ConversationViewBuilder<
|
||||
|
||||
for (const contribution of this.contributions) {
|
||||
const data = contribution.data
|
||||
if (data.kind === 'system-prompt') {
|
||||
if (!representedPrompts.has(data.prompt.seq)) systemPrompts.push(data.prompt)
|
||||
continue
|
||||
}
|
||||
if (data.kind === 'request-header') {
|
||||
previousHeader = data.header
|
||||
previousTools = indexTools(data.header.prompt.tools)
|
||||
@@ -274,6 +281,7 @@ export class TrajectorySnapshotBuilder implements ConversationViewBuilder<
|
||||
const eventNodes = finalized
|
||||
return {
|
||||
eventNodes,
|
||||
...systemPrompts.length > 0 ? { systemPrompts } : {},
|
||||
eventLocations,
|
||||
requests,
|
||||
callSchemas,
|
||||
|
||||
@@ -637,6 +637,31 @@ describe('Trajectory conversation Definitions', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('shows a complete appended prompt at the start of a headerless window', () => {
|
||||
const value = assembler([
|
||||
at(10, 'system/message', {
|
||||
turn: 2, step: 1, message: systemMessage('known prompt'),
|
||||
}, { surfaceOp: 'append' }),
|
||||
at(11, 'step/end', { turn: 2, step: 1 }),
|
||||
at(12, 'step/start', { turn: 2, step: 2 }),
|
||||
at(13, 'assistant/message', {
|
||||
turn: 2, step: 2, message: assistantMessage('window-assistant', 'answer'),
|
||||
}),
|
||||
], true)
|
||||
const request = snapshot(value).requests.find(request => request.purpose === 'assistant')
|
||||
expect(request?.purpose === 'assistant' && request.prompt).toBeUndefined()
|
||||
expect(request?.requestConfig).toBeUndefined()
|
||||
expect(snapshot(value).systemPrompts).toMatchObject([{ seq: 10, text: 'known prompt', update: false }])
|
||||
value.prepend([
|
||||
at(1, 'system/message', { turn: 1, step: 1, message: systemMessage('original') }, { surfaceOp: 'append' }),
|
||||
at(2, 'request/header', { reason: 'initial', header: { config: { provider: 'test', model: 'test' } } }),
|
||||
], false)
|
||||
value.flush()
|
||||
expect(snapshot(value).systemPrompts).toBeUndefined()
|
||||
expect(snapshot(value).requests.find(request => request.purpose === 'assistant'))
|
||||
.toMatchObject({ prompt: { system: 'known prompt', config: { provider: 'test', model: 'test' } } })
|
||||
})
|
||||
|
||||
it('carries an in-history prompt update into later requests as a system change at its own position', () => {
|
||||
const value = assembler([
|
||||
at(1, 'turn/start', { turn: 1 }),
|
||||
@@ -804,6 +829,8 @@ describe('Trajectory conversation Definitions', () => {
|
||||
at(10, 'assistant/message', { turn: 1, step: 1, message: assistantMessage('reply', 'reply') }),
|
||||
])
|
||||
expect(snapshot(value).requests.at(-1)).toMatchObject({ prompt: { system: '' } })
|
||||
const uncertain = assembler([system(6, 'C', 3), system(7, 'Known but unordered')], true)
|
||||
expect(snapshot(uncertain).systemPrompts).toBeUndefined()
|
||||
value.prepend([system(1, 'A'), system(3, 'B'), system(5, 'A2', 1)], false)
|
||||
value.flush()
|
||||
expect(snapshot(value).requests.at(-1)).toMatchObject({ prompt: { system: 'C' } })
|
||||
|
||||
@@ -6,7 +6,7 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { RenderMessageImages } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { TrajectoryTable as LocalizedTrajectoryTable } from '../src/client/TrajectoryTable.tsx'
|
||||
import type { TrajectoryTurnModel } from '../src/client/layout.ts'
|
||||
import { deriveTrajectoryLayout, type TrajectoryTurnModel } from '../src/client/layout.ts'
|
||||
import { trajectoryRecordId } from '../src/client/trajectory-record.ts'
|
||||
import { t, tZh } from './locale.client.ts'
|
||||
|
||||
@@ -124,6 +124,20 @@ const FOLD_PROPS = {
|
||||
}
|
||||
|
||||
describe('TrajectoryTable', () => {
|
||||
it('shows known standalone prompt text without a fabricated tool catalog or request options', () => {
|
||||
const turns = deriveTrajectoryLayout({
|
||||
nodes: [], partial: null, runningCalls: [],
|
||||
systemPrompts: [{ seq: 10, time: 10, turn: 2, step: 1, text: '# Known instructions', update: false }],
|
||||
}, t)
|
||||
expect(turns.flatMap(turn => turn.groups.flatMap(group => group.cells))).toMatchObject([
|
||||
{ kind: 'system', text: 'Initial System Prompt', systemPromptDetail: '# Known instructions' },
|
||||
])
|
||||
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
|
||||
fireEvent.click(screen.getByRole('row', { name: /SYSTEM/ }))
|
||||
expect(screen.getByRole('heading', { name: 'Known instructions' })).toBeTruthy()
|
||||
expect(screen.getAllByRole('tab').map(tab => tab.textContent)).toEqual(['System Prompt'])
|
||||
})
|
||||
|
||||
it('shows a muted placeholder for an assistant response containing only tool calls', () => {
|
||||
const turns: readonly TrajectoryTurnModel[] = [{
|
||||
turn: 1,
|
||||
|
||||
@@ -1271,7 +1271,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.trajectory.images\', () => ctx.slots.register(\n { name: \'conversation.trajectory.images\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-trajectory/src/client/trajectory-contract.ts:95',
|
||||
source: 'packages/client/ui-trajectory/src/client/trajectory-contract.ts:98',
|
||||
},
|
||||
{
|
||||
key: 'conversation.view',
|
||||
|
||||
Reference in New Issue
Block a user