@seed-ship/mcp-ui-solid 6.6.0 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/dist/adapters/index.d.ts +2 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/macro-run.cjs +226 -0
- package/dist/adapters/macro-run.cjs.map +1 -0
- package/dist/adapters/macro-run.d.ts +65 -0
- package/dist/adapters/macro-run.d.ts.map +1 -0
- package/dist/adapters/macro-run.js +226 -0
- package/dist/adapters/macro-run.js.map +1 -0
- package/dist/adapters.cjs +3 -0
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.cts +2 -1
- package/dist/adapters.d.ts +2 -1
- package/dist/adapters.js +4 -1
- package/dist/adapters.js.map +1 -1
- package/dist/components/ActionGroupRenderer.cjs +12 -3
- package/dist/components/ActionGroupRenderer.cjs.map +1 -1
- package/dist/components/ActionGroupRenderer.d.ts.map +1 -1
- package/dist/components/ActionGroupRenderer.js +12 -3
- package/dist/components/ActionGroupRenderer.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +22 -15
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +22 -15
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/context/MCPActionContext.cjs +4 -1
- package/dist/context/MCPActionContext.cjs.map +1 -1
- package/dist/context/MCPActionContext.d.ts +13 -1
- package/dist/context/MCPActionContext.d.ts.map +1 -1
- package/dist/context/MCPActionContext.js +4 -1
- package/dist/context/MCPActionContext.js.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs +250 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.js +251 -2
- package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.cjs +2 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.cjs.map +1 -1
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js +2 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/index.ts +4 -5
- package/src/adapters/macro-run.test.ts +293 -0
- package/src/adapters/macro-run.ts +362 -0
- package/src/components/ActionGroupRenderer.test.tsx +1 -0
- package/src/components/ActionGroupRenderer.tsx +19 -4
- package/src/components/ActionSubmit.test.tsx +188 -0
- package/src/components/UIResourceRenderer.tsx +19 -6
- package/src/context/MCPActionContext.tsx +17 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-ship/mcp-ui-solid",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.0",
|
|
4
4
|
"description": "SolidJS components for rendering MCP-generated UI resources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@seed-ship/mcp-ui-spec": "^5.
|
|
158
|
+
"@seed-ship/mcp-ui-spec": "^5.3.0",
|
|
159
159
|
"@types/dompurify": "^3.0.5",
|
|
160
160
|
"dompurify": "^3.4.1",
|
|
161
161
|
"marked": "^16.3.0",
|
package/src/adapters/index.ts
CHANGED
|
@@ -14,11 +14,10 @@
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
export {
|
|
18
|
-
connectorResultToUILayout,
|
|
19
|
-
connectorActionsToActionGroup,
|
|
20
|
-
} from './connector'
|
|
17
|
+
export { connectorResultToUILayout, connectorActionsToActionGroup } from './connector';
|
|
21
18
|
export type {
|
|
22
19
|
ConnectorResultToUILayoutOptions,
|
|
23
20
|
ConnectorActionsToActionGroupOptions,
|
|
24
|
-
} from './connector'
|
|
21
|
+
} from './connector';
|
|
22
|
+
|
|
23
|
+
export { macroRunToScratchpadState, macroInterrogationToChatPromptConfig } from './macro-run';
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MacroRun adapter tests (v6.7.0 — MacroRun Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Covers `macroRunToScratchpadState` and `macroInterrogationToChatPromptConfig`
|
|
5
|
+
* as pure functions: status mappings, section production, the stepper /
|
|
6
|
+
* split_stepper branch, embedded vs standalone interrogations, and the
|
|
7
|
+
* results-absent path.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import type { MacroRunV1, MacroInterrogationV1, MacroStepV1 } from '@seed-ship/mcp-ui-spec';
|
|
12
|
+
import { macroRunToScratchpadState, macroInterrogationToChatPromptConfig } from './macro-run';
|
|
13
|
+
import type { ScratchpadSection } from '../types/chat-bus';
|
|
14
|
+
|
|
15
|
+
// ─── Fixture builders ────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function run(overrides: Partial<MacroRunV1> = {}): MacroRunV1 {
|
|
18
|
+
return {
|
|
19
|
+
schemaVersion: 'macro-run/v1',
|
|
20
|
+
runId: 'run_1',
|
|
21
|
+
macroId: 'demo-macro',
|
|
22
|
+
macroName: 'Demo macro',
|
|
23
|
+
status: 'running',
|
|
24
|
+
steps: [],
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const STEPS: MacroStepV1[] = [
|
|
30
|
+
{ id: 's1', label: 'Collect', status: 'done', durationMs: 1200 },
|
|
31
|
+
{ id: 's2', label: 'Summarize', status: 'active' },
|
|
32
|
+
{ id: 's3', label: 'Draft', status: 'pending' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function interrogation(overrides: Partial<MacroInterrogationV1> = {}): MacroInterrogationV1 {
|
|
36
|
+
return {
|
|
37
|
+
schemaVersion: 'macro-interrogation/v1',
|
|
38
|
+
interrogationId: 'int_1',
|
|
39
|
+
runId: 'run_1',
|
|
40
|
+
kind: 'choice',
|
|
41
|
+
title: 'Pick a direction',
|
|
42
|
+
options: [
|
|
43
|
+
{ value: 'a', label: 'Option A' },
|
|
44
|
+
{ value: 'b', label: 'Option B' },
|
|
45
|
+
],
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sectionTypes = (sections: ScratchpadSection[]) => sections.map((s) => s.type);
|
|
51
|
+
|
|
52
|
+
// ─── macroRunToScratchpadState — status mapping ──────────────
|
|
53
|
+
|
|
54
|
+
describe('macroRunToScratchpadState — run status → scratchpad status', () => {
|
|
55
|
+
const cases: Array<[MacroRunV1['status'], string]> = [
|
|
56
|
+
['pending', 'loading'],
|
|
57
|
+
['running', 'processing'],
|
|
58
|
+
['awaiting_input', 'waiting_human'],
|
|
59
|
+
['completed', 'complete'],
|
|
60
|
+
['failed', 'error'],
|
|
61
|
+
['aborted', 'error'],
|
|
62
|
+
];
|
|
63
|
+
for (const [runStatus, scratchpadStatus] of cases) {
|
|
64
|
+
it(`maps run '${runStatus}' → scratchpad '${scratchpadStatus}'`, () => {
|
|
65
|
+
expect(macroRunToScratchpadState(run({ status: runStatus })).status).toBe(scratchpadStatus);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ─── macroRunToScratchpadState — sections ────────────────────
|
|
71
|
+
|
|
72
|
+
describe('macroRunToScratchpadState — sections', () => {
|
|
73
|
+
it('running run with steps → agent_card + stepper', () => {
|
|
74
|
+
const state = macroRunToScratchpadState(run({ status: 'running', steps: STEPS }));
|
|
75
|
+
expect(state.status).toBe('processing');
|
|
76
|
+
expect(sectionTypes(state.sections)).toContain('agent_card');
|
|
77
|
+
expect(sectionTypes(state.sections)).toContain('stepper');
|
|
78
|
+
expect(sectionTypes(state.sections)).not.toContain('split_stepper');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('maps step `failed` → stepper `error`', () => {
|
|
82
|
+
const state = macroRunToScratchpadState(
|
|
83
|
+
run({ status: 'failed', steps: [{ id: 's1', label: 'X', status: 'failed' }] })
|
|
84
|
+
);
|
|
85
|
+
const stepper = state.sections.find((s) => s.type === 'stepper');
|
|
86
|
+
const content = stepper?.content as { steps: Array<{ status: string }> };
|
|
87
|
+
expect(content.steps[0].status).toBe('error');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('carries the agent card from run.agent when present', () => {
|
|
91
|
+
const state = macroRunToScratchpadState(
|
|
92
|
+
run({
|
|
93
|
+
status: 'running',
|
|
94
|
+
steps: STEPS,
|
|
95
|
+
agent: { id: 'researcher', name: 'Researcher', status: 'running' },
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
const card = state.sections.find((s) => s.type === 'agent_card');
|
|
99
|
+
expect((card?.content as { agentId: string }).agentId).toBe('researcher');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('derives an agent card from the macro identity when run.agent is absent', () => {
|
|
103
|
+
const state = macroRunToScratchpadState(run({ status: 'running', steps: STEPS }));
|
|
104
|
+
const card = state.sections.find((s) => s.type === 'agent_card');
|
|
105
|
+
expect((card?.content as { agentId: string; name: string }).agentId).toBe('demo-macro');
|
|
106
|
+
expect((card?.content as { name: string }).name).toBe('Demo macro');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('awaiting_input + pendingInterrogation → waiting_human + prompt section (choice)', () => {
|
|
110
|
+
const state = macroRunToScratchpadState(
|
|
111
|
+
run({
|
|
112
|
+
status: 'awaiting_input',
|
|
113
|
+
steps: STEPS,
|
|
114
|
+
pendingInterrogation: interrogation(),
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
expect(state.status).toBe('waiting_human');
|
|
118
|
+
const prompt = state.sections.find((s) => s.type === 'prompt');
|
|
119
|
+
expect(prompt).toBeDefined();
|
|
120
|
+
expect((prompt?.content as { type: string }).type).toBe('choice');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('completed + results UI → complete with one section per result, mapped by type', () => {
|
|
124
|
+
const state = macroRunToScratchpadState(
|
|
125
|
+
run({
|
|
126
|
+
status: 'completed',
|
|
127
|
+
steps: STEPS,
|
|
128
|
+
results: [
|
|
129
|
+
{ id: 'r1', type: 'chart', params: {} },
|
|
130
|
+
{ id: 'r2', type: 'map', params: {} },
|
|
131
|
+
{ id: 'r3', type: 'table', params: {} },
|
|
132
|
+
],
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
expect(state.status).toBe('complete');
|
|
136
|
+
const types = sectionTypes(state.sections);
|
|
137
|
+
expect(types).toContain('chart');
|
|
138
|
+
expect(types).toContain('map');
|
|
139
|
+
expect(types).toContain('data_preview');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('does not crash and emits no result section when results are absent', () => {
|
|
143
|
+
const state = macroRunToScratchpadState(run({ status: 'completed', steps: STEPS }));
|
|
144
|
+
expect(state.status).toBe('complete');
|
|
145
|
+
expect(state.sections.every((s) => !s.id.startsWith('macro-result-'))).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('parallel steps present → split_stepper instead of stepper', () => {
|
|
149
|
+
const state = macroRunToScratchpadState(
|
|
150
|
+
run({
|
|
151
|
+
status: 'running',
|
|
152
|
+
steps: [
|
|
153
|
+
{
|
|
154
|
+
id: 'batch',
|
|
155
|
+
label: 'Parallel batch',
|
|
156
|
+
status: 'active',
|
|
157
|
+
parallel: [
|
|
158
|
+
{ id: 'b1', label: 'Branch 1', status: 'done' },
|
|
159
|
+
{ id: 'b2', label: 'Branch 2', status: 'active' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
const types = sectionTypes(state.sections);
|
|
166
|
+
expect(types).toContain('split_stepper');
|
|
167
|
+
expect(types).not.toContain('stepper');
|
|
168
|
+
const split = state.sections.find((s) => s.type === 'split_stepper');
|
|
169
|
+
expect((split?.content as { agents: unknown[] }).agents).toHaveLength(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('failed run → status error with retryable error detail', () => {
|
|
173
|
+
const state = macroRunToScratchpadState(
|
|
174
|
+
run({
|
|
175
|
+
status: 'failed',
|
|
176
|
+
steps: STEPS,
|
|
177
|
+
error: { message: 'Tool timed out', code: 'TIMEOUT', retryable: true },
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
expect(state.status).toBe('error');
|
|
181
|
+
expect(state.error).toEqual({ message: 'Tool timed out', code: 'TIMEOUT', retryable: true });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('aborted run → status error, never retryable', () => {
|
|
185
|
+
const state = macroRunToScratchpadState(
|
|
186
|
+
run({
|
|
187
|
+
status: 'aborted',
|
|
188
|
+
steps: STEPS,
|
|
189
|
+
error: { message: 'User aborted', retryable: true },
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
expect(state.status).toBe('error');
|
|
193
|
+
expect(state.error?.retryable).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('produces a well-formed ScratchpadState (id, title, required fields)', () => {
|
|
197
|
+
const state = macroRunToScratchpadState(run({ status: 'running', steps: STEPS }));
|
|
198
|
+
expect(state.id).toBe('run_1');
|
|
199
|
+
expect(state.title).toBe('Demo macro');
|
|
200
|
+
expect(state.filters).toEqual({});
|
|
201
|
+
expect(state.agentMessages).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ─── macroInterrogationToChatPromptConfig ────────────────────
|
|
206
|
+
|
|
207
|
+
describe('macroInterrogationToChatPromptConfig', () => {
|
|
208
|
+
it('choice → ChatPromptConfig { type: "choice" } with options', () => {
|
|
209
|
+
const config = macroInterrogationToChatPromptConfig(interrogation({ kind: 'choice' }));
|
|
210
|
+
expect(config.type).toBe('choice');
|
|
211
|
+
expect((config.config as { options: unknown[] }).options).toHaveLength(2);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('confirm → ChatPromptConfig { type: "confirm" } with labels', () => {
|
|
215
|
+
const config = macroInterrogationToChatPromptConfig(
|
|
216
|
+
interrogation({
|
|
217
|
+
kind: 'confirm',
|
|
218
|
+
message: 'Proceed?',
|
|
219
|
+
confirm: { confirmLabel: 'Yes', cancelLabel: 'No', variant: 'danger' },
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
expect(config.type).toBe('confirm');
|
|
223
|
+
expect(config.config).toMatchObject({
|
|
224
|
+
message: 'Proceed?',
|
|
225
|
+
confirmLabel: 'Yes',
|
|
226
|
+
variant: 'danger',
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('form → ChatPromptConfig { type: "form" } passing fields through', () => {
|
|
231
|
+
const fields = [{ name: 'tone', label: 'Tone', type: 'text' }];
|
|
232
|
+
const config = macroInterrogationToChatPromptConfig(interrogation({ kind: 'form', fields }));
|
|
233
|
+
expect(config.type).toBe('form');
|
|
234
|
+
expect((config.config as { fields: unknown[] }).fields).toEqual(fields);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('elicitation → routed through elicitationToPromptConfig (single boolean → confirm)', () => {
|
|
238
|
+
const config = macroInterrogationToChatPromptConfig(
|
|
239
|
+
interrogation({
|
|
240
|
+
kind: 'elicitation',
|
|
241
|
+
message: 'Confirm export?',
|
|
242
|
+
elicitationSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: { confirmed: { type: 'boolean', description: 'Export now' } },
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
// Single-boolean schema → elicitationToPromptConfig produces a confirm.
|
|
249
|
+
expect(config.type).toBe('confirm');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('elicitation → form for a multi-property schema', () => {
|
|
253
|
+
const config = macroInterrogationToChatPromptConfig(
|
|
254
|
+
interrogation({
|
|
255
|
+
kind: 'elicitation',
|
|
256
|
+
message: 'Report options',
|
|
257
|
+
elicitationSchema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
tone: { type: 'string' },
|
|
261
|
+
maxPages: { type: 'integer' },
|
|
262
|
+
},
|
|
263
|
+
required: ['tone'],
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
expect(config.type).toBe('form');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('elicitation with no usable schema → degrades to confirm, never throws', () => {
|
|
271
|
+
const config = macroInterrogationToChatPromptConfig(
|
|
272
|
+
interrogation({ kind: 'elicitation', elicitationSchema: undefined })
|
|
273
|
+
);
|
|
274
|
+
expect(config.type).toBe('confirm');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('choice with no options → empty options array, does not crash', () => {
|
|
278
|
+
const config = macroInterrogationToChatPromptConfig(
|
|
279
|
+
interrogation({ kind: 'choice', options: undefined })
|
|
280
|
+
);
|
|
281
|
+
expect(config.type).toBe('choice');
|
|
282
|
+
expect((config.config as { options: unknown[] }).options).toEqual([]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('is usable standalone and produces the same prompt as the embedded path', () => {
|
|
286
|
+
const q = interrogation();
|
|
287
|
+
const standalone = macroInterrogationToChatPromptConfig(q);
|
|
288
|
+
const embedded = macroRunToScratchpadState(
|
|
289
|
+
run({ status: 'awaiting_input', pendingInterrogation: q })
|
|
290
|
+
).sections.find((s) => s.type === 'prompt')?.content;
|
|
291
|
+
expect(embedded).toEqual(standalone);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MacroRun adapters — `MacroRunV1` / `MacroInterrogationV1` → MCP-UI render
|
|
3
|
+
* primitives.
|
|
4
|
+
*
|
|
5
|
+
* @since v6.7.0 (MacroRun Phase 2 — contract consolidated in deposium_MCPs
|
|
6
|
+
* `docs/2026/briefs/2026-05-22-macro-run-runtime-contract-consolidation.md`)
|
|
7
|
+
*
|
|
8
|
+
* ## Opt-in, pure
|
|
9
|
+
*
|
|
10
|
+
* Published under the dedicated subpath `@seed-ship/mcp-ui-solid/adapters` —
|
|
11
|
+
* never imported by the core renderer path. Every function here is a **pure
|
|
12
|
+
* function**: same input → same output, no `fetch`, no SSE listener, no
|
|
13
|
+
* persistence, no global state, no clock, no randomness.
|
|
14
|
+
*
|
|
15
|
+
* ## Scope boundary
|
|
16
|
+
*
|
|
17
|
+
* These adapters only translate the agnostic `MacroRunV1` contract (defined
|
|
18
|
+
* in `@seed-ship/mcp-ui-spec`) into existing MCP-UI primitives — a
|
|
19
|
+
* `ScratchpadState` and a `ChatPromptConfig`. They do NOT:
|
|
20
|
+
*
|
|
21
|
+
* - emit or consume any SSE event (a `macro_run_snapshot` producer is a
|
|
22
|
+
* separate, later goal on the producing runtime repo);
|
|
23
|
+
* - perform any fetch, persistence or resume;
|
|
24
|
+
* - know anything about a specific runtime, host, corpus or domain;
|
|
25
|
+
* - touch the `action:'submit'` executors, nor mix MacroRun with the
|
|
26
|
+
* existing tool-call action path.
|
|
27
|
+
*
|
|
28
|
+
* The host owns all of the above — it feeds a `MacroRunV1` snapshot in and
|
|
29
|
+
* decides where to render the result.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { MacroRunV1, MacroStepV1, MacroInterrogationV1 } from '@seed-ship/mcp-ui-spec';
|
|
33
|
+
import type {
|
|
34
|
+
ScratchpadState,
|
|
35
|
+
ScratchpadSection,
|
|
36
|
+
ChatPromptConfig,
|
|
37
|
+
ChoiceOption,
|
|
38
|
+
ConfirmPromptConfig,
|
|
39
|
+
AgentCardContent,
|
|
40
|
+
SplitStepperContent,
|
|
41
|
+
ElicitationRequestedSchema,
|
|
42
|
+
FormPromptConfig,
|
|
43
|
+
} from '../types/chat-bus';
|
|
44
|
+
import { elicitationToPromptConfig } from '../services/chat-bus';
|
|
45
|
+
|
|
46
|
+
// ─── Status mappings ─────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run status → top-level `ScratchpadState` status. `aborted` and `failed`
|
|
50
|
+
* both surface as `error` (an aborted run is additionally non-retryable —
|
|
51
|
+
* see `buildError`).
|
|
52
|
+
*/
|
|
53
|
+
const RUN_STATUS_TO_SCRATCHPAD: Record<MacroRunV1['status'], ScratchpadState['status']> = {
|
|
54
|
+
pending: 'loading',
|
|
55
|
+
running: 'processing',
|
|
56
|
+
awaiting_input: 'waiting_human',
|
|
57
|
+
completed: 'complete',
|
|
58
|
+
failed: 'error',
|
|
59
|
+
aborted: 'error',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Run status → `AgentCardContent` status, used when the run carries no agent. */
|
|
63
|
+
function runStatusToAgentStatus(status: MacroRunV1['status']): AgentCardContent['status'] {
|
|
64
|
+
switch (status) {
|
|
65
|
+
case 'pending':
|
|
66
|
+
return 'idle';
|
|
67
|
+
case 'running':
|
|
68
|
+
return 'running';
|
|
69
|
+
case 'awaiting_input':
|
|
70
|
+
return 'waiting';
|
|
71
|
+
case 'completed':
|
|
72
|
+
return 'done';
|
|
73
|
+
case 'failed':
|
|
74
|
+
case 'aborted':
|
|
75
|
+
return 'error';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Step status → stepper status. The MCP-UI stepper primitive renders
|
|
81
|
+
* `failed` as `error`; every other value passes through unchanged.
|
|
82
|
+
*/
|
|
83
|
+
function stepToStepperStatus(
|
|
84
|
+
status: MacroStepV1['status']
|
|
85
|
+
): 'pending' | 'active' | 'done' | 'skipped' | 'error' {
|
|
86
|
+
return status === 'failed' ? 'error' : status;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Section builders ────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
interface StepperSectionContent {
|
|
92
|
+
steps: Array<{
|
|
93
|
+
id: string;
|
|
94
|
+
label: string;
|
|
95
|
+
status: 'pending' | 'active' | 'done' | 'skipped' | 'error';
|
|
96
|
+
summary?: string;
|
|
97
|
+
duration_ms?: number;
|
|
98
|
+
}>;
|
|
99
|
+
orientation: 'horizontal' | 'vertical';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildAgentCard(run: MacroRunV1): AgentCardContent {
|
|
103
|
+
const agent = run.agent;
|
|
104
|
+
const card: AgentCardContent = {
|
|
105
|
+
agentId: agent?.id ?? run.macroId,
|
|
106
|
+
name: agent?.name ?? run.macroName ?? run.macroId,
|
|
107
|
+
status: agent?.status ?? runStatusToAgentStatus(run.status),
|
|
108
|
+
};
|
|
109
|
+
if (agent?.avatar) card.avatar = agent.avatar;
|
|
110
|
+
if (agent?.capabilities) card.capabilities = agent.capabilities;
|
|
111
|
+
if (agent?.currentStep) card.currentStep = agent.currentStep;
|
|
112
|
+
return card;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildStepperContent(steps: MacroStepV1[]): StepperSectionContent {
|
|
116
|
+
return {
|
|
117
|
+
orientation: 'horizontal',
|
|
118
|
+
steps: steps.map((step) => {
|
|
119
|
+
const out: StepperSectionContent['steps'][number] = {
|
|
120
|
+
id: step.id,
|
|
121
|
+
label: step.label,
|
|
122
|
+
status: stepToStepperStatus(step.status),
|
|
123
|
+
};
|
|
124
|
+
if (step.summary) out.summary = step.summary;
|
|
125
|
+
if (typeof step.durationMs === 'number') out.duration_ms = step.durationMs;
|
|
126
|
+
return out;
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Collapse a set of sub-step statuses into a single parallel-lane status. */
|
|
132
|
+
function laneStatus(subSteps: MacroStepV1[]): 'done' | 'active' | 'pending' | 'error' {
|
|
133
|
+
if (subSteps.some((s) => s.status === 'failed')) return 'error';
|
|
134
|
+
if (subSteps.some((s) => s.status === 'active')) return 'active';
|
|
135
|
+
if (subSteps.length > 0 && subSteps.every((s) => s.status === 'done' || s.status === 'skipped'))
|
|
136
|
+
return 'done';
|
|
137
|
+
return 'pending';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build a `split_stepper` content from steps that carry `parallel` branches.
|
|
142
|
+
* Each top-level step becomes a lane: a step with `parallel` sub-steps shows
|
|
143
|
+
* those sub-steps; a step without falls back to a single-step lane.
|
|
144
|
+
*/
|
|
145
|
+
function buildSplitStepperContent(steps: MacroStepV1[]): SplitStepperContent {
|
|
146
|
+
return {
|
|
147
|
+
agents: steps.map((step) => {
|
|
148
|
+
const subSteps = step.parallel && step.parallel.length > 0 ? step.parallel : [step];
|
|
149
|
+
return {
|
|
150
|
+
id: step.id,
|
|
151
|
+
name: step.label,
|
|
152
|
+
status: laneStatus(subSteps),
|
|
153
|
+
steps: subSteps.map((sub) => ({
|
|
154
|
+
id: sub.id,
|
|
155
|
+
label: sub.label,
|
|
156
|
+
status: stepToStepperStatus(sub.status),
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Component `type` → the closest `ScratchpadSection` type for a result. */
|
|
164
|
+
function resultSectionType(componentType: unknown): ScratchpadSection['type'] {
|
|
165
|
+
switch (componentType) {
|
|
166
|
+
case 'chart':
|
|
167
|
+
return 'chart';
|
|
168
|
+
case 'map':
|
|
169
|
+
return 'map';
|
|
170
|
+
case 'table':
|
|
171
|
+
return 'data_preview';
|
|
172
|
+
default:
|
|
173
|
+
return 'data';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildError(run: MacroRunV1): ScratchpadState['error'] | undefined {
|
|
178
|
+
if (run.status !== 'failed' && run.status !== 'aborted') return undefined;
|
|
179
|
+
const aborted = run.status === 'aborted';
|
|
180
|
+
if (run.error) {
|
|
181
|
+
const err: NonNullable<ScratchpadState['error']> = {
|
|
182
|
+
message: run.error.message,
|
|
183
|
+
// An aborted run is never retryable, regardless of the producer flag.
|
|
184
|
+
retryable: aborted ? false : (run.error.retryable ?? false),
|
|
185
|
+
};
|
|
186
|
+
if (run.error.code) err.code = run.error.code;
|
|
187
|
+
return err;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
message: aborted ? 'Macro run aborted.' : 'Macro run failed.',
|
|
191
|
+
retryable: false,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── macroRunToScratchpadState ───────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convert a `MacroRunV1` snapshot into a `ScratchpadState`.
|
|
199
|
+
*
|
|
200
|
+
* Sections produced, in order:
|
|
201
|
+
* 1. `agent_card` — always (derived from `run.agent`, or from the macro
|
|
202
|
+
* identity when the run carries no agent, e.g. a non-interactive macro).
|
|
203
|
+
* 2. `stepper` — when the run has steps and none carry `parallel` branches.
|
|
204
|
+
* `split_stepper` instead when any step carries `parallel` (future model).
|
|
205
|
+
* 3. `prompt` — when `run.pendingInterrogation` is set; its content is the
|
|
206
|
+
* `ChatPromptConfig` produced by {@link macroInterrogationToChatPromptConfig}.
|
|
207
|
+
* 4. one section per `run.results` entry (`chart` / `map` / `data_preview` /
|
|
208
|
+
* `data`, by component type). Optional — the adapter works without results.
|
|
209
|
+
*
|
|
210
|
+
* Pure: no fetch, no SSE, no persistence. The host owns all wiring.
|
|
211
|
+
*/
|
|
212
|
+
export function macroRunToScratchpadState(run: MacroRunV1): ScratchpadState {
|
|
213
|
+
const sections: ScratchpadSection[] = [];
|
|
214
|
+
|
|
215
|
+
// 1. Agent card — always present.
|
|
216
|
+
sections.push({
|
|
217
|
+
id: 'macro-agent',
|
|
218
|
+
title: 'Agent',
|
|
219
|
+
type: 'agent_card',
|
|
220
|
+
content: buildAgentCard(run),
|
|
221
|
+
editable: false,
|
|
222
|
+
source: 'agent',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// 2. Stepper / split_stepper — when there are steps.
|
|
226
|
+
if (run.steps.length > 0) {
|
|
227
|
+
const hasParallel = run.steps.some((s) => Array.isArray(s.parallel) && s.parallel.length > 0);
|
|
228
|
+
sections.push(
|
|
229
|
+
hasParallel
|
|
230
|
+
? {
|
|
231
|
+
id: 'macro-split-stepper',
|
|
232
|
+
title: 'Progress',
|
|
233
|
+
type: 'split_stepper',
|
|
234
|
+
content: buildSplitStepperContent(run.steps),
|
|
235
|
+
editable: false,
|
|
236
|
+
source: 'agent',
|
|
237
|
+
}
|
|
238
|
+
: {
|
|
239
|
+
id: 'macro-stepper',
|
|
240
|
+
title: 'Progress',
|
|
241
|
+
type: 'stepper',
|
|
242
|
+
content: buildStepperContent(run.steps),
|
|
243
|
+
editable: false,
|
|
244
|
+
source: 'agent',
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 3. Pending interrogation → a `prompt` section.
|
|
250
|
+
if (run.pendingInterrogation) {
|
|
251
|
+
sections.push({
|
|
252
|
+
id: 'macro-prompt',
|
|
253
|
+
title: run.pendingInterrogation.title,
|
|
254
|
+
type: 'prompt',
|
|
255
|
+
content: macroInterrogationToChatPromptConfig(run.pendingInterrogation),
|
|
256
|
+
editable: false,
|
|
257
|
+
source: 'agent',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 4. Result components → one section each (results are optional). A result
|
|
262
|
+
// is a `UIComponent`-shaped object — passthrough, read loosely (the spec
|
|
263
|
+
// validates the run envelope, the renderer validates each component).
|
|
264
|
+
const results = run.results ?? [];
|
|
265
|
+
results.forEach((component, index) => {
|
|
266
|
+
sections.push({
|
|
267
|
+
id: `macro-result-${String(component?.id ?? index)}`,
|
|
268
|
+
title: 'Result',
|
|
269
|
+
type: resultSectionType(component?.type),
|
|
270
|
+
content: component,
|
|
271
|
+
editable: false,
|
|
272
|
+
source: 'agent',
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const state: ScratchpadState = {
|
|
277
|
+
id: run.runId,
|
|
278
|
+
title: run.title ?? run.macroName ?? run.macroId,
|
|
279
|
+
sections,
|
|
280
|
+
filters: {},
|
|
281
|
+
agentMessages: [],
|
|
282
|
+
status: RUN_STATUS_TO_SCRATCHPAD[run.status],
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const error = buildError(run);
|
|
286
|
+
if (error) state.error = error;
|
|
287
|
+
|
|
288
|
+
return state;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── macroInterrogationToChatPromptConfig ────────────────────
|
|
292
|
+
|
|
293
|
+
function isElicitationSchema(value: unknown): value is ElicitationRequestedSchema {
|
|
294
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
295
|
+
const v = value as { type?: unknown; properties?: unknown };
|
|
296
|
+
return v.type === 'object' && typeof v.properties === 'object' && v.properties !== null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Convert a `MacroInterrogationV1` into a `ChatPromptConfig`.
|
|
301
|
+
*
|
|
302
|
+
* - `choice` → `{ type: 'choice' }` (vertical layout).
|
|
303
|
+
* - `confirm` → `{ type: 'confirm' }`.
|
|
304
|
+
* - `form` → `{ type: 'form' }` (the opaque `fields` are passed through).
|
|
305
|
+
* - `elicitation` → routed through the existing `elicitationToPromptConfig()`
|
|
306
|
+
* helper. If the interrogation carries no usable elicitation JSON Schema,
|
|
307
|
+
* it degrades to a `confirm` prompt rather than throwing.
|
|
308
|
+
*
|
|
309
|
+
* Always returns a `ChatPromptConfig` — never an `ElicitationEvent` — so the
|
|
310
|
+
* host has a single entry point. Usable standalone or via
|
|
311
|
+
* {@link macroRunToScratchpadState} (which calls it for an embedded
|
|
312
|
+
* `pendingInterrogation`).
|
|
313
|
+
*/
|
|
314
|
+
export function macroInterrogationToChatPromptConfig(q: MacroInterrogationV1): ChatPromptConfig {
|
|
315
|
+
switch (q.kind) {
|
|
316
|
+
case 'choice': {
|
|
317
|
+
const options: ChoiceOption[] = (q.options ?? []).map((o) => {
|
|
318
|
+
const opt: ChoiceOption = { value: o.value, label: o.label };
|
|
319
|
+
if (o.icon) opt.icon = o.icon;
|
|
320
|
+
if (o.description) opt.description = o.description;
|
|
321
|
+
if (o.metadata) opt.metadata = o.metadata;
|
|
322
|
+
return opt;
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
type: 'choice',
|
|
326
|
+
title: q.title,
|
|
327
|
+
config: { options, layout: 'vertical' },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
case 'confirm': {
|
|
332
|
+
const config: ConfirmPromptConfig = {};
|
|
333
|
+
if (q.message) config.message = q.message;
|
|
334
|
+
if (q.confirm?.confirmLabel) config.confirmLabel = q.confirm.confirmLabel;
|
|
335
|
+
if (q.confirm?.cancelLabel) config.cancelLabel = q.confirm.cancelLabel;
|
|
336
|
+
if (q.confirm?.variant) config.variant = q.confirm.variant;
|
|
337
|
+
return { type: 'confirm', title: q.title, config };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case 'form':
|
|
341
|
+
return {
|
|
342
|
+
type: 'form',
|
|
343
|
+
title: q.title,
|
|
344
|
+
config: { fields: (q.fields ?? []) as FormPromptConfig['fields'] },
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
case 'elicitation': {
|
|
348
|
+
if (isElicitationSchema(q.elicitationSchema)) {
|
|
349
|
+
return elicitationToPromptConfig({
|
|
350
|
+
message: q.message ?? q.title,
|
|
351
|
+
requestedSchema: q.elicitationSchema,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// No usable schema — degrade gracefully instead of crashing.
|
|
355
|
+
return {
|
|
356
|
+
type: 'confirm',
|
|
357
|
+
title: q.title,
|
|
358
|
+
config: { message: q.message ?? 'Please confirm to continue.' },
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -12,6 +12,7 @@ import type { UIComponent, ActionGroupParams, ActionComponentParams } from '../t
|
|
|
12
12
|
vi.mock('../hooks/useAction', () => ({
|
|
13
13
|
useAction: () => ({
|
|
14
14
|
execute: vi.fn().mockResolvedValue({ success: true }),
|
|
15
|
+
executeAction: vi.fn().mockResolvedValue({ success: true }),
|
|
15
16
|
isExecuting: () => false,
|
|
16
17
|
lastResult: () => undefined,
|
|
17
18
|
lastError: () => undefined,
|