feat(web): declare the remaining context forms on every shipped producer

Four values complete the vocabulary, so the opaque body is reached only by
producers that genuinely promise no shape.

`snapshot` — current state a later snapshot supersedes. system-prompt now
exposes `renderContextSections()`, the named contributions
`renderContextSnapshot()` already joins for the model, so the body attributes
each part to the subsystem that produced it instead of re-splitting joined
prose. The runtime snapshot, time-context, and tmux-context declare it.

`notice` — a one-off account of what just happened, declared by tool-tasks,
goal state changes, tool-goal wrap-up, plan-mode switches, and
repeat-tool-guard. Its `summary` rides the COLLAPSED row: these five are the
majority of shipped producers and none of them needs expanding to be read.
The task summary bounds itself because its inputs are unbounded caller text.

`relay` — a message another agent addressed to this one; both subagent
sources declare it and the body names the sender above what it said.

`recall` — material lifted from another session's log. session-reference
needed no new field: its references already record retained and omitted
counts and the truncation flag, which the body shows first, because recalled
context is bounded on the way in.

`ContextFormed` is now discriminated by `form`, so a producer cannot declare
a shape without the facts that shape is presented from — a notice without its
summary, or a snapshot without its sections, fails to compile.

Only the two hook bridges stay opaque, by design: their content is whatever
an external program printed, so no shape can be promised for it. Unknown
kinds and unreadable records land there too.
This commit is contained in:
creatixchu
2026-08-05 16:07:04 +08:00
parent 5f34f782fc
commit b7034e4a26
132 changed files with 771 additions and 159 deletions

View File

@@ -562,6 +562,106 @@ describe('MessageItem arms', () => {
expect(fields).toEqual(['plugin', 'form'])
})
it('the snapshot form attributes each part to the subsystem that produced it', () => {
const view = render(
<MessageItem t={t} node={{
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'Current runtime context.\n\nsandbox\n\nworkspace' }],
source: {
kind: 'plugin',
plugin: '@deepseek-ai/dsh-system-prompt',
form: 'snapshot',
sections: [{ name: 'sandbox:policy', text: 'workspace-write' }, { name: 'workspace', text: '/repo' }],
},
provenance: { role: 'inject', label: '@deepseek-ai/dsh-system-prompt' },
form: 'snapshot',
} as never}
/>,
)
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*@deepseek-ai\/dsh-system-prompt$/ }))
const rows = [...view.container.querySelectorAll('[data-context-sections] div')].map(node => node.textContent)
expect(rows).toEqual(['sandbox:policyworkspace-write', 'workspace/repo'])
})
it('a notice puts its account on the collapsed row', () => {
// The whole point of the form: readable without expanding.
const view = render(
<MessageItem t={t} node={{
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'background task bash-1 finished.' }],
source: { kind: 'plugin', plugin: 'tool-tasks', form: 'notice', summary: 'bash pnpm test [status: completed]' },
provenance: { role: 'inject', label: 'tool-tasks' },
form: 'notice',
} as never}
/>,
)
expect(view.container.querySelector('[data-context-summary]')?.textContent)
.toBe('bash pnpm test [status: completed]')
expect(view.container.querySelector('[data-context-injection-body]')).toBeNull()
})
it('a notice without its account falls back to the opaque body', () => {
const view = render(
<MessageItem t={t} node={{
kind: 'context', seq: 3, content: [{ type: 'text', text: 'notice prose' }],
source: { kind: 'plugin', plugin: 'tool-tasks', form: 'notice' },
provenance: { role: 'inject', label: 'tool-tasks' },
form: 'notice',
} as never}
/>,
)
expect(view.container.querySelector('[data-context-summary]')).toBeNull()
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*tool-tasks$/ }))
expect(view.container.querySelector('[data-context-fields]')).not.toBeNull()
})
it('a relay names the agent that sent it above what it said', () => {
const view = render(
<MessageItem t={t} node={{
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'child report body' }],
source: { kind: 'subagent-report', form: 'relay', senderSessionId: 'child-7' },
provenance: { role: 'inject', label: 'subagent-report' },
form: 'relay',
} as never}
/>,
)
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*subagent-report$/ }))
expect(view.container.querySelector('[data-context-relay-sender]')?.textContent).toBe('来自会话 child-7')
expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('child report body')
})
it('a recall reports how much of each source session survived the read', () => {
// Recalled context is bounded on the way in, so hiding the omitted count
// would overstate what the model received.
const view = render(
<MessageItem t={t} node={{
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'recalled material' }],
source: {
kind: 'session-reference',
form: 'recall',
version: 1,
references: [
{ label: '重构 loader', retainedMessages: 18, omittedMessages: 42, truncated: true },
{ label: '修 CI', retainedMessages: 3, omittedMessages: 0, truncated: false },
],
},
provenance: { role: 'recall', label: '重构 loader, 修 CI' },
form: 'recall',
} as never}
/>,
)
fireEvent.click(view.getByRole('button', { name: /^跨会话召回\s*重构 loader, 修 CI$/ }))
const rows = [...view.container.querySelectorAll('[data-context-recalls] li')].map(node => node.textContent)
expect(rows).toEqual(['重构 loader保留 18 条 · 省略 42 条已截断', '修 CI保留 3 条 · 省略 0 条'])
expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('recalled material')
})
it('unknown nodes retain the generic JSON row', () => {
const unknownView = render(
<MessageItem t={t} node={{ kind: 'unknown', seq: 4, type: 'surface/next', data: { x: 1 } } as never} />,