feat(client): render user skill invocations as dedicated transcript cards
A user/message carrying the skill-invocation source materializes as its own conversation node (name/args lifted off the source metadata, never re-parsed from the body) and renders as a right-aligned bubble: the /name chip plus the user's trailing text, with the injected <skill_content> collapsed behind a disclosure. A record with an unreadable name degrades to the injected-context row.
This commit is contained in:
8 files changed
+163
-4
No files matched your search
@@ -49,7 +49,7 @@ export type {
|
||||
AssistantBlock, AssistantMessageNode, AssistantProvenanceView, AssistantRequestConfig,
|
||||
AssistantTiming, CodeSubCall, CommandNode, CompactionSummaryNode, ComposerPhase,
|
||||
ContextMessageNode, ConversationNode, ConversationSnapshot, ModelRetryNode, QueuedMessage,
|
||||
RunningToolCall,
|
||||
RunningToolCall, SkillInvocationNode,
|
||||
SteeringMessageNode, TodoItem, ToolResultNode, TurnErrorNode, UnknownSurfaceNode, UserMessageNode,
|
||||
} from './sessions/conversation.ts'
|
||||
export type {
|
||||
|
||||
@@ -129,6 +129,25 @@ export interface ContextMessageNode {
|
||||
form: KnownContextForm | null
|
||||
}
|
||||
|
||||
/**
|
||||
* A user-explicit skill invocation: the host injected the rendered skill as a
|
||||
* user message carrying the `skill-invocation` source, so the card presents
|
||||
* `/name args` from source metadata and collapses the injected body.
|
||||
*/
|
||||
export interface SkillInvocationNode {
|
||||
kind: 'skill-invocation'
|
||||
seq: number
|
||||
/** Unix epoch ms from the source session event. */
|
||||
time: number
|
||||
/** Invoked skill name read off the message source. */
|
||||
name: string
|
||||
/** Trailing user text read off the message source, when recorded. */
|
||||
args?: string
|
||||
/** Full injected model-facing content (collapsed by default in the UI). */
|
||||
content: readonly ContentBlock[]
|
||||
source: unknown
|
||||
}
|
||||
|
||||
/** Durable notice that a closed failed step is waiting for a model-request retry. */
|
||||
export type ModelRetryNode = LlmRetryEventData & {
|
||||
kind: 'model-retry'
|
||||
@@ -245,6 +264,7 @@ export type ConversationNode =
|
||||
| AssistantMessageNode
|
||||
| SteeringMessageNode
|
||||
| ContextMessageNode
|
||||
| SkillInvocationNode
|
||||
| ModelRetryNode
|
||||
| TurnErrorNode
|
||||
| ToolResultNode
|
||||
|
||||
@@ -57,7 +57,20 @@ function materializeNode(
|
||||
stepTimings: ReadonlyMap<string, AssistantStepMetadata>,
|
||||
): ConversationNode {
|
||||
switch (event.type) {
|
||||
case 'user/message':
|
||||
case 'user/message': {
|
||||
// A user-explicit skill invocation carries its name (and optional args)
|
||||
// on the source; the dedicated node lets the card render `/name args`
|
||||
// from metadata instead of re-parsing the injected body. A record whose
|
||||
// name is unreadable degrades to injected context below.
|
||||
const source = event.data.source as { kind?: unknown; name?: unknown; args?: unknown }
|
||||
if (source.kind === 'skill-invocation' && typeof source.name === 'string') {
|
||||
return {
|
||||
kind: 'skill-invocation', seq: event.seq, time: event.time,
|
||||
name: source.name,
|
||||
...typeof source.args === 'string' ? { args: source.args } : {},
|
||||
content: event.data.content, source: event.data.source,
|
||||
}
|
||||
}
|
||||
// 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).
|
||||
@@ -80,6 +93,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,
|
||||
|
||||
Reference in New Issue
Block a user