Merge branch 'master' into worktree/merge-compact-card

This commit is contained in:
Yichen Jiang
2026-08-08 14:45:55 +08:00
committed by GitHub
192 changed files with 2422 additions and 404 deletions

View File

@@ -83,6 +83,9 @@ export function contextProvenance(source: unknown): ContextProvenanceView {
return { role: 'inject', label: joined(collect(record, 'changes', 'path')) ?? kind }
case 'plugin':
return { role: 'inject', label: readString(record, 'plugin') ?? kind }
// A user-explicit skill invocation names the skill it injected.
case 'skill-invocation':
return { role: 'inject', label: readString(record, 'name') ?? kind }
// Documented default arm of the merge-extensible source map: an unknown
// producer still identifies itself by its own durable kind.
default:

View File

@@ -57,10 +57,11 @@ function materializeNode(
stepTimings: ReadonlyMap<string, AssistantStepMetadata>,
): ConversationNode {
switch (event.type) {
case 'user/message':
// Injected context (plugin/goal source) folds to a context node, not a
// user message; only a direct human prompt is a user node. A compaction
// checkpoint never reaches here (isCompactCheckpoint routes it away).
case 'user/message': {
// Injected context (plugin/goal/skill-invocation source) folds to a
// context node, not a user message; only a direct human prompt is a
// user node. A compaction checkpoint never reaches here
// (isCompactCheckpoint routes it away).
if (event.data.source.kind !== 'user') {
return {
kind: 'context', seq: event.seq, time: event.time,
@@ -80,6 +81,7 @@ function materializeNode(
kind: 'user', seq: event.seq, time: event.time,
content: event.data.content, source: event.data.source,
}
}
case 'assistant/message':
return {
kind: 'assistant', seq: event.seq, time: event.time,

View File

@@ -19,7 +19,7 @@ import type { Context } from 'cordis'
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
import type {
LocaleFace, OwnerOf, SlotEntryDef, SlotMap, SlotRenderer, SlotRendererHost,
SlotScope, SlotSpec, StoreDecl, StoredEntry, StoreInstanceLike,
SlotScope, SlotSpec, StoreDecl, StoreFactory, StoredEntry, StoreInstanceLike,
} from '@deepseek-ai/dsh-client-ui-slots'
declare module '@deepseek-ai/dsh-client-ui-slots' {
@@ -35,16 +35,11 @@ export interface RootOwnerProps { children?: never }
/** Instance key for root-scoped store records (session records key by session id, so the literal cannot collide). */
const ROOT_INSTANCE_KEY = 'root'
// FIXME(slot-parity): the engine's arbitrated persist extensions — create()
// takes the scope key (per-session localStorage suffix) and instances expose
// clearPersisted() — are not yet on ui-slots' StoreHandle/StoreInstanceLike;
// these local structural faces bridge until fw-slots lifts them.
/** Canonical type-erased store handle used by the runtime lifecycle map. */
type EngineStoreHandle = Exclude<StoreDecl, StoreFactory>
/** Store handle face as the engine actually ships it (scope-key-aware create). */
interface EngineStoreHandle { create(scopeKey?: string): EngineStoreInstance }
/** Engine instance face: the host-contract shape plus persisted-state cleanup. */
interface EngineStoreInstance extends StoreInstanceLike { clearPersisted(): void }
/** Canonical engine instance derived from the handle's create contract. */
type EngineStoreInstance = ReturnType<EngineStoreHandle['create']>
/** Store axis record: one per live handle, dropped when the last holding entry unloads. */
interface StoreAxisRecord {

View File

@@ -198,6 +198,7 @@ export class FakeApiClient implements IApiClient {
onSkillList: (payload: unknown) => Promise<RpcResponse<{ skills: SkillEntry[] }>>
= () => Promise.resolve(ok({ skills: [] }))
readonly commands: IApiClient['commands'] = {
list: (payload: unknown) => this.record('command.list', payload, this.onCommandList(payload)),
execute: (payload: unknown) => this.record('command.execute', payload, this.onCommandExecute(payload)),

View File

@@ -164,6 +164,28 @@ describe('TranscriptAdapter', () => {
expect(adapter.nodes().map(node => node.kind)).toEqual(['user', 'user', 'context'])
})
it('materializes a skill-invocation injection as a named instructions context', () => {
const adapter = new TranscriptAdapter()
adapter.reset([
at(0, { type: 'user/message', surfaceOp: 'append', data: createUserMessage({
content: [{ type: 'text', text: '/hidden-demo check the fixture' }],
source: { kind: 'user' },
}) }),
at(1, { type: 'user/message', surfaceOp: 'append', data: createUserMessage({
content: [{ type: 'text', text: '<skill_content name="hidden-demo">body</skill_content>' }],
source: { kind: 'skill-invocation', name: 'hidden-demo', form: 'instructions' } as never,
}) }),
])
const nodes = adapter.nodes()
// The gesture stays a user bubble; the injected body folds to a context
// row named after the skill, presented as instructions.
expect(nodes.map(node => node.kind)).toEqual(['user', 'context'])
expect(nodes[1]).toMatchObject({
provenance: { role: 'inject', label: 'hidden-demo' },
form: 'instructions',
})
})
it('skips events core does not call surface-eligible, marker or not', () => {
// The transcript is the append-origin surface, so log-only events (a chunk,
// a turn boundary, a compact/* provenance record) and a future type core