@seed-ship/mcp-ui-solid 5.1.0 → 5.2.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 +54 -0
- package/README.md +64 -13
- package/dist/components/FeedbackInline.cjs +57 -0
- package/dist/components/FeedbackInline.cjs.map +1 -0
- package/dist/components/FeedbackInline.d.ts +71 -0
- package/dist/components/FeedbackInline.d.ts.map +1 -0
- package/dist/components/FeedbackInline.js +57 -0
- package/dist/components/FeedbackInline.js.map +1 -0
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/services/chat-bus.cjs +71 -0
- package/dist/services/chat-bus.cjs.map +1 -1
- package/dist/services/chat-bus.d.ts +31 -1
- package/dist/services/chat-bus.d.ts.map +1 -1
- package/dist/services/chat-bus.js +71 -0
- package/dist/services/chat-bus.js.map +1 -1
- package/dist/services/chat-prompt-controller.cjs +83 -0
- package/dist/services/chat-prompt-controller.cjs.map +1 -0
- package/dist/services/chat-prompt-controller.d.ts +93 -0
- package/dist/services/chat-prompt-controller.d.ts.map +1 -0
- package/dist/services/chat-prompt-controller.js +83 -0
- package/dist/services/chat-prompt-controller.js.map +1 -0
- package/dist/stores/scratchpad-store.cjs +105 -77
- package/dist/stores/scratchpad-store.cjs.map +1 -1
- package/dist/stores/scratchpad-store.d.ts +88 -19
- package/dist/stores/scratchpad-store.d.ts.map +1 -1
- package/dist/stores/scratchpad-store.js +105 -77
- package/dist/stores/scratchpad-store.js.map +1 -1
- package/dist/types/chat-bus.d.ts +39 -0
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/FeedbackInline.test.tsx +117 -0
- package/src/components/FeedbackInline.tsx +143 -0
- package/src/index.ts +23 -1
- package/src/services/chat-bus.test.ts +154 -2
- package/src/services/chat-bus.ts +115 -0
- package/src/services/chat-prompt-controller.test.ts +144 -0
- package/src/services/chat-prompt-controller.ts +214 -0
- package/src/stores/scratchpad-store.test.tsx +140 -0
- package/src/stores/scratchpad-store.tsx +244 -0
- package/src/types/chat-bus.ts +40 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/src/stores/scratchpad-store.ts +0 -126
|
@@ -134,6 +134,76 @@ function mergeScratchpadSections(existing, incoming, mode = "replace") {
|
|
|
134
134
|
return incoming;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
function elicitationToPromptConfig(event) {
|
|
138
|
+
const propEntries = Object.entries(event.requestedSchema.properties);
|
|
139
|
+
if (propEntries.length === 1 && propEntries[0][1].type === "boolean") {
|
|
140
|
+
return {
|
|
141
|
+
type: "confirm",
|
|
142
|
+
title: event.message,
|
|
143
|
+
config: {
|
|
144
|
+
message: propEntries[0][1].description
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (propEntries.length === 1) {
|
|
149
|
+
const [, schema] = propEntries[0];
|
|
150
|
+
if (schema.enum && schema.enum.length > 0 && schema.enum.length <= 4) {
|
|
151
|
+
return {
|
|
152
|
+
type: "choice",
|
|
153
|
+
title: event.message,
|
|
154
|
+
config: {
|
|
155
|
+
options: schema.enum.map((val, idx) => {
|
|
156
|
+
var _a;
|
|
157
|
+
return {
|
|
158
|
+
value: String(val),
|
|
159
|
+
label: ((_a = schema.enumNames) == null ? void 0 : _a[idx]) ?? String(val)
|
|
160
|
+
};
|
|
161
|
+
}),
|
|
162
|
+
layout: "vertical"
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const required = new Set(event.requestedSchema.required ?? []);
|
|
168
|
+
const fields = propEntries.map(([name, schema]) => ({
|
|
169
|
+
name,
|
|
170
|
+
label: schema.title ?? name,
|
|
171
|
+
...schemaToFieldType(schema),
|
|
172
|
+
required: required.has(name),
|
|
173
|
+
helpText: schema.description,
|
|
174
|
+
...schema.default !== void 0 ? { placeholder: String(schema.default) } : {}
|
|
175
|
+
}));
|
|
176
|
+
return {
|
|
177
|
+
type: "form",
|
|
178
|
+
title: event.message,
|
|
179
|
+
config: { fields }
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function schemaToFieldType(schema) {
|
|
183
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
184
|
+
return {
|
|
185
|
+
type: "select",
|
|
186
|
+
options: schema.enum.map((val, idx) => {
|
|
187
|
+
var _a;
|
|
188
|
+
return {
|
|
189
|
+
label: ((_a = schema.enumNames) == null ? void 0 : _a[idx]) ?? String(val),
|
|
190
|
+
value: String(val)
|
|
191
|
+
};
|
|
192
|
+
})
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (schema.type === "boolean") return { type: "checkbox" };
|
|
196
|
+
if (schema.type === "number" || schema.type === "integer") return { type: "number" };
|
|
197
|
+
if (schema.type === "string") {
|
|
198
|
+
if (schema.format === "email") return { type: "email" };
|
|
199
|
+
if (schema.format === "date" || schema.format === "date-time") return { type: "date" };
|
|
200
|
+
return { type: "text" };
|
|
201
|
+
}
|
|
202
|
+
console.warn(
|
|
203
|
+
`[MCP-UI] elicitationToPromptConfig: unsupported schema type "${schema.type}", falling back to text.`
|
|
204
|
+
);
|
|
205
|
+
return { type: "text" };
|
|
206
|
+
}
|
|
137
207
|
function clarificationToPromptConfig(event) {
|
|
138
208
|
return {
|
|
139
209
|
type: "choice",
|
|
@@ -160,5 +230,6 @@ exports.clarificationToPromptConfig = clarificationToPromptConfig;
|
|
|
160
230
|
exports.createChatBus = createChatBus;
|
|
161
231
|
exports.createCommandHandler = createCommandHandler;
|
|
162
232
|
exports.createEventEmitter = createEventEmitter;
|
|
233
|
+
exports.elicitationToPromptConfig = elicitationToPromptConfig;
|
|
163
234
|
exports.mergeScratchpadSections = mergeScratchpadSections;
|
|
164
235
|
//# sourceMappingURL=chat-bus.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-bus.cjs","sources":["../../src/services/chat-bus.ts"],"sourcesContent":["/**\n * Chat Bus — Event Emitter + Command Handler\n * v2.4.0: Core primitives for the chat event/command bus\n *\n * @experimental — This API may change without major bump until v2.5.0.\n */\n\nimport type {\n ChatEvents,\n ChatCommands,\n ChatEventEmitter,\n ChatCommandHandler,\n ChatBus,\n EventSubscribeOptions,\n ScratchpadSection,\n ClarificationEvent,\n ChatPromptConfig,\n} from '../types/chat-bus'\n\n// ─── Event Emitter ───────────────────────────────────────────\n\ninterface Listener<F extends (...args: any[]) => any> {\n handler: F\n options?: EventSubscribeOptions\n throttledHandler?: F\n}\n\n/**\n * Create a typed event emitter with throttle and streamKey filtering support.\n *\n * @experimental\n *\n * @example\n * const emitter = createEventEmitter<ChatEvents>()\n * const unsub = emitter.on('onToken', (event) => console.log(event.token), { throttle: 100 })\n * emitter.emit('onToken', { streamKey: 'abc', token: 'hello' })\n * unsub()\n */\nexport function createEventEmitter(): ChatEventEmitter {\n const listeners = new Map<string, Set<Listener<any>>>()\n\n interface ThrottledFn<F> {\n fn: F\n cancel: () => void\n }\n\n function createThrottled<F extends (...args: any[]) => void>(fn: F, ms: number): ThrottledFn<F> {\n let lastCall = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n let lastArgs: any[] | null = null\n let cancelled = false\n\n const throttled = ((...args: any[]) => {\n if (cancelled) return\n lastArgs = args\n const now = Date.now()\n const remaining = ms - (now - lastCall)\n\n if (remaining <= 0) {\n if (timer) { clearTimeout(timer); timer = null }\n lastCall = now\n fn(...args)\n } else if (!timer) {\n timer = setTimeout(() => {\n lastCall = Date.now()\n timer = null\n if (lastArgs && !cancelled) {\n try { fn(...lastArgs) } catch (err) { console.error('[ChatBus] Error in throttled handler:', err) }\n }\n }, remaining)\n }\n }) as F\n\n return {\n fn: throttled,\n cancel: () => { cancelled = true; if (timer) { clearTimeout(timer); timer = null } },\n }\n }\n\n return {\n on(event, handler, options) {\n if (!listeners.has(event as string)) {\n listeners.set(event as string, new Set())\n }\n\n const listener: Listener<typeof handler> = { handler, options }\n\n // Apply throttle if requested\n let throttleHandle: ThrottledFn<typeof handler> | null = null\n if (options?.throttle && options.throttle > 0) {\n throttleHandle = createThrottled(handler, options.throttle)\n listener.throttledHandler = throttleHandle.fn\n }\n\n listeners.get(event as string)!.add(listener)\n\n // Return unsubscribe function — cancels pending throttle timers\n return () => {\n throttleHandle?.cancel()\n listeners.get(event as string)?.delete(listener)\n }\n },\n\n emit(event, ...args) {\n const set = listeners.get(event as string)\n if (!set) return\n\n for (const listener of set) {\n // StreamKey filtering: skip if listener wants a specific streamKey\n // For most events args[0] has streamKey; for onCustomEvent args[1] has it\n if (listener.options?.streamKey) {\n let streamKeyArg: unknown\n for (const arg of args) {\n if (arg && typeof arg === 'object' && 'streamKey' in (arg as any)) {\n streamKeyArg = (arg as any).streamKey\n break\n }\n }\n if (streamKeyArg !== undefined && streamKeyArg !== listener.options.streamKey) continue\n }\n\n const fn = listener.throttledHandler || listener.handler\n try {\n fn(...args)\n } catch (err) {\n console.error(`[ChatBus] Error in ${event as string} handler:`, err)\n }\n }\n },\n\n clear() {\n listeners.clear()\n },\n } as ChatEventEmitter\n}\n\n// ─── Command Handler ─────────────────────────────────────────\n\n/**\n * Create a typed command handler. The host app registers handlers,\n * agents execute commands.\n *\n * @experimental\n *\n * @example\n * const commands = createCommandHandler<ChatCommands>()\n * commands.handle('injectPrompt', (text) => setInputValue(text))\n * commands.exec('injectPrompt', 'Hello world')\n */\nexport function createCommandHandler(): ChatCommandHandler {\n const handlers = new Map<string, (...args: any[]) => any>()\n\n return {\n handle(command, handler) {\n handlers.set(command as string, handler)\n },\n\n exec(command, ...args) {\n const handler = handlers.get(command as string)\n if (!handler) {\n console.warn(`[ChatBus] No handler registered for command: ${command as string}`)\n return undefined as any\n }\n return handler(...args)\n },\n } as ChatCommandHandler\n}\n\n// ─── Chat Bus Factory ────────────────────────────────────────\n\n/**\n * Create a complete ChatBus with events + commands.\n *\n * @experimental\n *\n * @example\n * const bus = createChatBus()\n * bus.events.on('onStreamEnd', (event) => { ... })\n * bus.commands.handle('sendPrompt', (text) => { ... })\n */\nexport function createChatBus(): ChatBus {\n return {\n events: createEventEmitter(),\n commands: createCommandHandler(),\n }\n}\n\n// ─── Scratchpad Section Merge Helper ─────────────────────────\n\n/**\n * Merge sections from a ScratchpadEvent into existing state sections.\n * Handles replace/append/upsert modes.\n *\n * @example\n * const newSections = mergeScratchpadSections(\n * currentState.sections,\n * event.sections,\n * event.sectionMode\n * )\n */\nexport function mergeScratchpadSections(\n existing: ScratchpadSection[],\n incoming: ScratchpadSection[] | undefined,\n mode: 'replace' | 'append' | 'upsert' = 'replace'\n): ScratchpadSection[] {\n if (!incoming) return existing\n\n switch (mode) {\n case 'append':\n return [...existing, ...incoming]\n\n case 'upsert': {\n const result = [...existing]\n for (const section of incoming) {\n const idx = result.findIndex((s) => s.id === section.id)\n if (idx >= 0) {\n result[idx] = section\n } else {\n result.push(section)\n }\n }\n return result\n }\n\n case 'replace':\n default:\n return incoming\n }\n}\n\n// ─── Clarification → Prompt Helper (v4.3.9) ──────────────────\n\n/**\n * Convert a ClarificationEvent into a ChatPromptConfig.\n * Universal bridge for apps receiving clarification events via SSE.\n *\n * Legacy runtime `file_id` (removed from the type in v5.0.0) is still\n * transparently migrated into `metadata.file_id` when present, so payloads\n * from older servers continue to work without upgrade pressure.\n *\n * @experimental\n * @since v4.3.9\n * @example\n * bus.events.on('onClarificationNeeded', ({ clarification }) => {\n * bus.commands.exec('showChatPrompt', clarificationToPromptConfig(clarification))\n * })\n */\nexport function clarificationToPromptConfig(\n event: ClarificationEvent\n): ChatPromptConfig {\n return {\n type: 'choice',\n title: event.question,\n config: {\n options: event.options.map((opt) => {\n const merged: Record<string, unknown> = { ...(opt.metadata ?? {}) }\n // Runtime fallback for legacy payloads that still carry file_id at the top level.\n const legacyFileId = (opt as { file_id?: number }).file_id\n if (legacyFileId !== undefined && merged.file_id === undefined) {\n merged.file_id = legacyFileId\n }\n return {\n value: opt.value,\n label: opt.label,\n // Only include metadata if non-empty (keeps payloads clean)\n ...(Object.keys(merged).length > 0 ? { metadata: merged } : {}),\n }\n }),\n layout: 'vertical',\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-bus.cjs","sources":["../../src/services/chat-bus.ts"],"sourcesContent":["/**\n * Chat Bus — Event Emitter + Command Handler\n * v2.4.0: Core primitives for the chat event/command bus\n *\n * @experimental — This API may change without major bump until v2.5.0.\n */\n\nimport type {\n ChatEvents,\n ChatCommands,\n ChatEventEmitter,\n ChatCommandHandler,\n ChatBus,\n EventSubscribeOptions,\n ScratchpadSection,\n ClarificationEvent,\n ElicitationEvent,\n ElicitationPropertySchema,\n ChatPromptConfig,\n FormPromptConfig,\n} from '../types/chat-bus'\n\n// ─── Event Emitter ───────────────────────────────────────────\n\ninterface Listener<F extends (...args: any[]) => any> {\n handler: F\n options?: EventSubscribeOptions\n throttledHandler?: F\n}\n\n/**\n * Create a typed event emitter with throttle and streamKey filtering support.\n *\n * @experimental\n *\n * @example\n * const emitter = createEventEmitter<ChatEvents>()\n * const unsub = emitter.on('onToken', (event) => console.log(event.token), { throttle: 100 })\n * emitter.emit('onToken', { streamKey: 'abc', token: 'hello' })\n * unsub()\n */\nexport function createEventEmitter(): ChatEventEmitter {\n const listeners = new Map<string, Set<Listener<any>>>()\n\n interface ThrottledFn<F> {\n fn: F\n cancel: () => void\n }\n\n function createThrottled<F extends (...args: any[]) => void>(fn: F, ms: number): ThrottledFn<F> {\n let lastCall = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n let lastArgs: any[] | null = null\n let cancelled = false\n\n const throttled = ((...args: any[]) => {\n if (cancelled) return\n lastArgs = args\n const now = Date.now()\n const remaining = ms - (now - lastCall)\n\n if (remaining <= 0) {\n if (timer) { clearTimeout(timer); timer = null }\n lastCall = now\n fn(...args)\n } else if (!timer) {\n timer = setTimeout(() => {\n lastCall = Date.now()\n timer = null\n if (lastArgs && !cancelled) {\n try { fn(...lastArgs) } catch (err) { console.error('[ChatBus] Error in throttled handler:', err) }\n }\n }, remaining)\n }\n }) as F\n\n return {\n fn: throttled,\n cancel: () => { cancelled = true; if (timer) { clearTimeout(timer); timer = null } },\n }\n }\n\n return {\n on(event, handler, options) {\n if (!listeners.has(event as string)) {\n listeners.set(event as string, new Set())\n }\n\n const listener: Listener<typeof handler> = { handler, options }\n\n // Apply throttle if requested\n let throttleHandle: ThrottledFn<typeof handler> | null = null\n if (options?.throttle && options.throttle > 0) {\n throttleHandle = createThrottled(handler, options.throttle)\n listener.throttledHandler = throttleHandle.fn\n }\n\n listeners.get(event as string)!.add(listener)\n\n // Return unsubscribe function — cancels pending throttle timers\n return () => {\n throttleHandle?.cancel()\n listeners.get(event as string)?.delete(listener)\n }\n },\n\n emit(event, ...args) {\n const set = listeners.get(event as string)\n if (!set) return\n\n for (const listener of set) {\n // StreamKey filtering: skip if listener wants a specific streamKey\n // For most events args[0] has streamKey; for onCustomEvent args[1] has it\n if (listener.options?.streamKey) {\n let streamKeyArg: unknown\n for (const arg of args) {\n if (arg && typeof arg === 'object' && 'streamKey' in (arg as any)) {\n streamKeyArg = (arg as any).streamKey\n break\n }\n }\n if (streamKeyArg !== undefined && streamKeyArg !== listener.options.streamKey) continue\n }\n\n const fn = listener.throttledHandler || listener.handler\n try {\n fn(...args)\n } catch (err) {\n console.error(`[ChatBus] Error in ${event as string} handler:`, err)\n }\n }\n },\n\n clear() {\n listeners.clear()\n },\n } as ChatEventEmitter\n}\n\n// ─── Command Handler ─────────────────────────────────────────\n\n/**\n * Create a typed command handler. The host app registers handlers,\n * agents execute commands.\n *\n * @experimental\n *\n * @example\n * const commands = createCommandHandler<ChatCommands>()\n * commands.handle('injectPrompt', (text) => setInputValue(text))\n * commands.exec('injectPrompt', 'Hello world')\n */\nexport function createCommandHandler(): ChatCommandHandler {\n const handlers = new Map<string, (...args: any[]) => any>()\n\n return {\n handle(command, handler) {\n handlers.set(command as string, handler)\n },\n\n exec(command, ...args) {\n const handler = handlers.get(command as string)\n if (!handler) {\n console.warn(`[ChatBus] No handler registered for command: ${command as string}`)\n return undefined as any\n }\n return handler(...args)\n },\n } as ChatCommandHandler\n}\n\n// ─── Chat Bus Factory ────────────────────────────────────────\n\n/**\n * Create a complete ChatBus with events + commands.\n *\n * @experimental\n *\n * @example\n * const bus = createChatBus()\n * bus.events.on('onStreamEnd', (event) => { ... })\n * bus.commands.handle('sendPrompt', (text) => { ... })\n */\nexport function createChatBus(): ChatBus {\n return {\n events: createEventEmitter(),\n commands: createCommandHandler(),\n }\n}\n\n// ─── Scratchpad Section Merge Helper ─────────────────────────\n\n/**\n * Merge sections from a ScratchpadEvent into existing state sections.\n * Handles replace/append/upsert modes.\n *\n * @example\n * const newSections = mergeScratchpadSections(\n * currentState.sections,\n * event.sections,\n * event.sectionMode\n * )\n */\nexport function mergeScratchpadSections(\n existing: ScratchpadSection[],\n incoming: ScratchpadSection[] | undefined,\n mode: 'replace' | 'append' | 'upsert' = 'replace'\n): ScratchpadSection[] {\n if (!incoming) return existing\n\n switch (mode) {\n case 'append':\n return [...existing, ...incoming]\n\n case 'upsert': {\n const result = [...existing]\n for (const section of incoming) {\n const idx = result.findIndex((s) => s.id === section.id)\n if (idx >= 0) {\n result[idx] = section\n } else {\n result.push(section)\n }\n }\n return result\n }\n\n case 'replace':\n default:\n return incoming\n }\n}\n\n// ─── Clarification → Prompt Helper (v4.3.9) ──────────────────\n\n/**\n * Convert a ClarificationEvent into a ChatPromptConfig.\n * Universal bridge for apps receiving clarification events via SSE.\n *\n * Legacy runtime `file_id` (removed from the type in v5.0.0) is still\n * transparently migrated into `metadata.file_id` when present, so payloads\n * from older servers continue to work without upgrade pressure.\n *\n * @experimental\n * @since v4.3.9\n * @example\n * bus.events.on('onClarificationNeeded', ({ clarification }) => {\n * bus.commands.exec('showChatPrompt', clarificationToPromptConfig(clarification))\n * })\n */\n// ─── Elicitation → Prompt Helper (v5.2.0) ───────────────────\n\n/**\n * Convert an MCP `elicitation/create` payload into a `ChatPromptConfig`.\n *\n * Mapping rules :\n * - Single `boolean` property → `type: 'confirm'`\n * - Single property with `enum` of ≤4 values → `type: 'choice'` (one option per enum value)\n * - Anything else → `type: 'form'` with one field per schema property\n *\n * JSON Schema primitive types map to mcp-ui form field types :\n *\n * | JSON Schema | mcp-ui FormFieldType |\n * |---|---|\n * | `type: 'string'` | `'text'` |\n * | `type: 'string', format: 'email'` | `'email'` |\n * | `type: 'string', format: 'date'` or `'date-time'` | `'date'` |\n * | `type: 'string', enum: [...]` | `'select'` |\n * | `type: 'number' \\| 'integer'` | `'number'` |\n * | `type: 'boolean'` | `'checkbox'` |\n *\n * Unknown shapes fall through to plain text with a `helpText` warning.\n *\n * @experimental\n * @since v5.2.0\n *\n * @example\n * bus.events.on('onElicitation', ({ elicitation }) => {\n * bus.commands.exec('showChatPrompt', elicitationToPromptConfig(elicitation))\n * })\n */\nexport function elicitationToPromptConfig(event: ElicitationEvent): ChatPromptConfig {\n const propEntries = Object.entries(event.requestedSchema.properties)\n\n // Shortcut 1 : single boolean → confirm\n if (propEntries.length === 1 && propEntries[0][1].type === 'boolean') {\n return {\n type: 'confirm',\n title: event.message,\n config: {\n message: propEntries[0][1].description,\n },\n }\n }\n\n // Shortcut 2 : single enum property with ≤4 values → choice\n if (propEntries.length === 1) {\n const [, schema] = propEntries[0]\n if (schema.enum && schema.enum.length > 0 && schema.enum.length <= 4) {\n return {\n type: 'choice',\n title: event.message,\n config: {\n options: schema.enum.map((val, idx) => ({\n value: String(val),\n label: schema.enumNames?.[idx] ?? String(val),\n })),\n layout: 'vertical',\n },\n }\n }\n }\n\n // Default : full form\n const required = new Set(event.requestedSchema.required ?? [])\n const fields: FormPromptConfig['fields'] = propEntries.map(([name, schema]) => ({\n name,\n label: schema.title ?? name,\n ...schemaToFieldType(schema),\n required: required.has(name),\n helpText: schema.description,\n ...(schema.default !== undefined ? { placeholder: String(schema.default) } : {}),\n }))\n\n return {\n type: 'form',\n title: event.message,\n config: { fields },\n }\n}\n\nfunction schemaToFieldType(\n schema: ElicitationPropertySchema\n):\n | { type: FormPromptConfig['fields'][number]['type']; options?: Array<{ label: string; value: string }> }\n | { type: FormPromptConfig['fields'][number]['type']; helpText?: string } {\n // Enum → select\n if (schema.enum && schema.enum.length > 0) {\n return {\n type: 'select',\n options: schema.enum.map((val, idx) => ({\n label: schema.enumNames?.[idx] ?? String(val),\n value: String(val),\n })),\n }\n }\n\n if (schema.type === 'boolean') return { type: 'checkbox' }\n if (schema.type === 'number' || schema.type === 'integer') return { type: 'number' }\n if (schema.type === 'string') {\n if (schema.format === 'email') return { type: 'email' }\n if (schema.format === 'date' || schema.format === 'date-time') return { type: 'date' }\n return { type: 'text' }\n }\n\n // Unknown primitive — fall back to text with a warning\n console.warn(\n `[MCP-UI] elicitationToPromptConfig: unsupported schema type \"${(schema as { type?: string }).type}\", falling back to text.`\n )\n return { type: 'text' }\n}\n\nexport function clarificationToPromptConfig(\n event: ClarificationEvent\n): ChatPromptConfig {\n return {\n type: 'choice',\n title: event.question,\n config: {\n options: event.options.map((opt) => {\n const merged: Record<string, unknown> = { ...(opt.metadata ?? {}) }\n // Runtime fallback for legacy payloads that still carry file_id at the top level.\n const legacyFileId = (opt as { file_id?: number }).file_id\n if (legacyFileId !== undefined && merged.file_id === undefined) {\n merged.file_id = legacyFileId\n }\n return {\n value: opt.value,\n label: opt.label,\n // Only include metadata if non-empty (keeps payloads clean)\n ...(Object.keys(merged).length > 0 ? { metadata: merged } : {}),\n }\n }),\n layout: 'vertical',\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @experimental — This API may change without major bump until v2.5.0.
|
|
6
6
|
*/
|
|
7
|
-
import type { ChatEventEmitter, ChatCommandHandler, ChatBus, ScratchpadSection, ClarificationEvent, ChatPromptConfig } from '../types/chat-bus';
|
|
7
|
+
import type { ChatEventEmitter, ChatCommandHandler, ChatBus, ScratchpadSection, ClarificationEvent, ElicitationEvent, ChatPromptConfig } from '../types/chat-bus';
|
|
8
8
|
/**
|
|
9
9
|
* Create a typed event emitter with throttle and streamKey filtering support.
|
|
10
10
|
*
|
|
@@ -67,5 +67,35 @@ export declare function mergeScratchpadSections(existing: ScratchpadSection[], i
|
|
|
67
67
|
* bus.commands.exec('showChatPrompt', clarificationToPromptConfig(clarification))
|
|
68
68
|
* })
|
|
69
69
|
*/
|
|
70
|
+
/**
|
|
71
|
+
* Convert an MCP `elicitation/create` payload into a `ChatPromptConfig`.
|
|
72
|
+
*
|
|
73
|
+
* Mapping rules :
|
|
74
|
+
* - Single `boolean` property → `type: 'confirm'`
|
|
75
|
+
* - Single property with `enum` of ≤4 values → `type: 'choice'` (one option per enum value)
|
|
76
|
+
* - Anything else → `type: 'form'` with one field per schema property
|
|
77
|
+
*
|
|
78
|
+
* JSON Schema primitive types map to mcp-ui form field types :
|
|
79
|
+
*
|
|
80
|
+
* | JSON Schema | mcp-ui FormFieldType |
|
|
81
|
+
* |---|---|
|
|
82
|
+
* | `type: 'string'` | `'text'` |
|
|
83
|
+
* | `type: 'string', format: 'email'` | `'email'` |
|
|
84
|
+
* | `type: 'string', format: 'date'` or `'date-time'` | `'date'` |
|
|
85
|
+
* | `type: 'string', enum: [...]` | `'select'` |
|
|
86
|
+
* | `type: 'number' \| 'integer'` | `'number'` |
|
|
87
|
+
* | `type: 'boolean'` | `'checkbox'` |
|
|
88
|
+
*
|
|
89
|
+
* Unknown shapes fall through to plain text with a `helpText` warning.
|
|
90
|
+
*
|
|
91
|
+
* @experimental
|
|
92
|
+
* @since v5.2.0
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* bus.events.on('onElicitation', ({ elicitation }) => {
|
|
96
|
+
* bus.commands.exec('showChatPrompt', elicitationToPromptConfig(elicitation))
|
|
97
|
+
* })
|
|
98
|
+
*/
|
|
99
|
+
export declare function elicitationToPromptConfig(event: ElicitationEvent): ChatPromptConfig;
|
|
70
100
|
export declare function clarificationToPromptConfig(event: ClarificationEvent): ChatPromptConfig;
|
|
71
101
|
//# sourceMappingURL=chat-bus.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-bus.d.ts","sourceRoot":"","sources":["../../src/services/chat-bus.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAGV,gBAAgB,EAChB,kBAAkB,EAClB,OAAO,EAEP,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"chat-bus.d.ts","sourceRoot":"","sources":["../../src/services/chat-bus.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAGV,gBAAgB,EAChB,kBAAkB,EAClB,OAAO,EAEP,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAEhB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAA;AAU1B;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,IAAI,gBAAgB,CAgGrD;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,IAAI,kBAAkB,CAiBzD;AAID;;;;;;;;;GASG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,QAAQ,EAAE,iBAAiB,EAAE,GAAG,SAAS,EACzC,IAAI,GAAE,SAAS,GAAG,QAAQ,GAAG,QAAoB,GAChD,iBAAiB,EAAE,CAwBrB;AAID;;;;;;;;;;;;;;GAcG;AAGH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,CAgDnF;AAiCD,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,kBAAkB,GACxB,gBAAgB,CAsBlB"}
|
|
@@ -132,6 +132,76 @@ function mergeScratchpadSections(existing, incoming, mode = "replace") {
|
|
|
132
132
|
return incoming;
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
+
function elicitationToPromptConfig(event) {
|
|
136
|
+
const propEntries = Object.entries(event.requestedSchema.properties);
|
|
137
|
+
if (propEntries.length === 1 && propEntries[0][1].type === "boolean") {
|
|
138
|
+
return {
|
|
139
|
+
type: "confirm",
|
|
140
|
+
title: event.message,
|
|
141
|
+
config: {
|
|
142
|
+
message: propEntries[0][1].description
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (propEntries.length === 1) {
|
|
147
|
+
const [, schema] = propEntries[0];
|
|
148
|
+
if (schema.enum && schema.enum.length > 0 && schema.enum.length <= 4) {
|
|
149
|
+
return {
|
|
150
|
+
type: "choice",
|
|
151
|
+
title: event.message,
|
|
152
|
+
config: {
|
|
153
|
+
options: schema.enum.map((val, idx) => {
|
|
154
|
+
var _a;
|
|
155
|
+
return {
|
|
156
|
+
value: String(val),
|
|
157
|
+
label: ((_a = schema.enumNames) == null ? void 0 : _a[idx]) ?? String(val)
|
|
158
|
+
};
|
|
159
|
+
}),
|
|
160
|
+
layout: "vertical"
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const required = new Set(event.requestedSchema.required ?? []);
|
|
166
|
+
const fields = propEntries.map(([name, schema]) => ({
|
|
167
|
+
name,
|
|
168
|
+
label: schema.title ?? name,
|
|
169
|
+
...schemaToFieldType(schema),
|
|
170
|
+
required: required.has(name),
|
|
171
|
+
helpText: schema.description,
|
|
172
|
+
...schema.default !== void 0 ? { placeholder: String(schema.default) } : {}
|
|
173
|
+
}));
|
|
174
|
+
return {
|
|
175
|
+
type: "form",
|
|
176
|
+
title: event.message,
|
|
177
|
+
config: { fields }
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function schemaToFieldType(schema) {
|
|
181
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
182
|
+
return {
|
|
183
|
+
type: "select",
|
|
184
|
+
options: schema.enum.map((val, idx) => {
|
|
185
|
+
var _a;
|
|
186
|
+
return {
|
|
187
|
+
label: ((_a = schema.enumNames) == null ? void 0 : _a[idx]) ?? String(val),
|
|
188
|
+
value: String(val)
|
|
189
|
+
};
|
|
190
|
+
})
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (schema.type === "boolean") return { type: "checkbox" };
|
|
194
|
+
if (schema.type === "number" || schema.type === "integer") return { type: "number" };
|
|
195
|
+
if (schema.type === "string") {
|
|
196
|
+
if (schema.format === "email") return { type: "email" };
|
|
197
|
+
if (schema.format === "date" || schema.format === "date-time") return { type: "date" };
|
|
198
|
+
return { type: "text" };
|
|
199
|
+
}
|
|
200
|
+
console.warn(
|
|
201
|
+
`[MCP-UI] elicitationToPromptConfig: unsupported schema type "${schema.type}", falling back to text.`
|
|
202
|
+
);
|
|
203
|
+
return { type: "text" };
|
|
204
|
+
}
|
|
135
205
|
function clarificationToPromptConfig(event) {
|
|
136
206
|
return {
|
|
137
207
|
type: "choice",
|
|
@@ -159,6 +229,7 @@ export {
|
|
|
159
229
|
createChatBus,
|
|
160
230
|
createCommandHandler,
|
|
161
231
|
createEventEmitter,
|
|
232
|
+
elicitationToPromptConfig,
|
|
162
233
|
mergeScratchpadSections
|
|
163
234
|
};
|
|
164
235
|
//# sourceMappingURL=chat-bus.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-bus.js","sources":["../../src/services/chat-bus.ts"],"sourcesContent":["/**\n * Chat Bus — Event Emitter + Command Handler\n * v2.4.0: Core primitives for the chat event/command bus\n *\n * @experimental — This API may change without major bump until v2.5.0.\n */\n\nimport type {\n ChatEvents,\n ChatCommands,\n ChatEventEmitter,\n ChatCommandHandler,\n ChatBus,\n EventSubscribeOptions,\n ScratchpadSection,\n ClarificationEvent,\n ChatPromptConfig,\n} from '../types/chat-bus'\n\n// ─── Event Emitter ───────────────────────────────────────────\n\ninterface Listener<F extends (...args: any[]) => any> {\n handler: F\n options?: EventSubscribeOptions\n throttledHandler?: F\n}\n\n/**\n * Create a typed event emitter with throttle and streamKey filtering support.\n *\n * @experimental\n *\n * @example\n * const emitter = createEventEmitter<ChatEvents>()\n * const unsub = emitter.on('onToken', (event) => console.log(event.token), { throttle: 100 })\n * emitter.emit('onToken', { streamKey: 'abc', token: 'hello' })\n * unsub()\n */\nexport function createEventEmitter(): ChatEventEmitter {\n const listeners = new Map<string, Set<Listener<any>>>()\n\n interface ThrottledFn<F> {\n fn: F\n cancel: () => void\n }\n\n function createThrottled<F extends (...args: any[]) => void>(fn: F, ms: number): ThrottledFn<F> {\n let lastCall = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n let lastArgs: any[] | null = null\n let cancelled = false\n\n const throttled = ((...args: any[]) => {\n if (cancelled) return\n lastArgs = args\n const now = Date.now()\n const remaining = ms - (now - lastCall)\n\n if (remaining <= 0) {\n if (timer) { clearTimeout(timer); timer = null }\n lastCall = now\n fn(...args)\n } else if (!timer) {\n timer = setTimeout(() => {\n lastCall = Date.now()\n timer = null\n if (lastArgs && !cancelled) {\n try { fn(...lastArgs) } catch (err) { console.error('[ChatBus] Error in throttled handler:', err) }\n }\n }, remaining)\n }\n }) as F\n\n return {\n fn: throttled,\n cancel: () => { cancelled = true; if (timer) { clearTimeout(timer); timer = null } },\n }\n }\n\n return {\n on(event, handler, options) {\n if (!listeners.has(event as string)) {\n listeners.set(event as string, new Set())\n }\n\n const listener: Listener<typeof handler> = { handler, options }\n\n // Apply throttle if requested\n let throttleHandle: ThrottledFn<typeof handler> | null = null\n if (options?.throttle && options.throttle > 0) {\n throttleHandle = createThrottled(handler, options.throttle)\n listener.throttledHandler = throttleHandle.fn\n }\n\n listeners.get(event as string)!.add(listener)\n\n // Return unsubscribe function — cancels pending throttle timers\n return () => {\n throttleHandle?.cancel()\n listeners.get(event as string)?.delete(listener)\n }\n },\n\n emit(event, ...args) {\n const set = listeners.get(event as string)\n if (!set) return\n\n for (const listener of set) {\n // StreamKey filtering: skip if listener wants a specific streamKey\n // For most events args[0] has streamKey; for onCustomEvent args[1] has it\n if (listener.options?.streamKey) {\n let streamKeyArg: unknown\n for (const arg of args) {\n if (arg && typeof arg === 'object' && 'streamKey' in (arg as any)) {\n streamKeyArg = (arg as any).streamKey\n break\n }\n }\n if (streamKeyArg !== undefined && streamKeyArg !== listener.options.streamKey) continue\n }\n\n const fn = listener.throttledHandler || listener.handler\n try {\n fn(...args)\n } catch (err) {\n console.error(`[ChatBus] Error in ${event as string} handler:`, err)\n }\n }\n },\n\n clear() {\n listeners.clear()\n },\n } as ChatEventEmitter\n}\n\n// ─── Command Handler ─────────────────────────────────────────\n\n/**\n * Create a typed command handler. The host app registers handlers,\n * agents execute commands.\n *\n * @experimental\n *\n * @example\n * const commands = createCommandHandler<ChatCommands>()\n * commands.handle('injectPrompt', (text) => setInputValue(text))\n * commands.exec('injectPrompt', 'Hello world')\n */\nexport function createCommandHandler(): ChatCommandHandler {\n const handlers = new Map<string, (...args: any[]) => any>()\n\n return {\n handle(command, handler) {\n handlers.set(command as string, handler)\n },\n\n exec(command, ...args) {\n const handler = handlers.get(command as string)\n if (!handler) {\n console.warn(`[ChatBus] No handler registered for command: ${command as string}`)\n return undefined as any\n }\n return handler(...args)\n },\n } as ChatCommandHandler\n}\n\n// ─── Chat Bus Factory ────────────────────────────────────────\n\n/**\n * Create a complete ChatBus with events + commands.\n *\n * @experimental\n *\n * @example\n * const bus = createChatBus()\n * bus.events.on('onStreamEnd', (event) => { ... })\n * bus.commands.handle('sendPrompt', (text) => { ... })\n */\nexport function createChatBus(): ChatBus {\n return {\n events: createEventEmitter(),\n commands: createCommandHandler(),\n }\n}\n\n// ─── Scratchpad Section Merge Helper ─────────────────────────\n\n/**\n * Merge sections from a ScratchpadEvent into existing state sections.\n * Handles replace/append/upsert modes.\n *\n * @example\n * const newSections = mergeScratchpadSections(\n * currentState.sections,\n * event.sections,\n * event.sectionMode\n * )\n */\nexport function mergeScratchpadSections(\n existing: ScratchpadSection[],\n incoming: ScratchpadSection[] | undefined,\n mode: 'replace' | 'append' | 'upsert' = 'replace'\n): ScratchpadSection[] {\n if (!incoming) return existing\n\n switch (mode) {\n case 'append':\n return [...existing, ...incoming]\n\n case 'upsert': {\n const result = [...existing]\n for (const section of incoming) {\n const idx = result.findIndex((s) => s.id === section.id)\n if (idx >= 0) {\n result[idx] = section\n } else {\n result.push(section)\n }\n }\n return result\n }\n\n case 'replace':\n default:\n return incoming\n }\n}\n\n// ─── Clarification → Prompt Helper (v4.3.9) ──────────────────\n\n/**\n * Convert a ClarificationEvent into a ChatPromptConfig.\n * Universal bridge for apps receiving clarification events via SSE.\n *\n * Legacy runtime `file_id` (removed from the type in v5.0.0) is still\n * transparently migrated into `metadata.file_id` when present, so payloads\n * from older servers continue to work without upgrade pressure.\n *\n * @experimental\n * @since v4.3.9\n * @example\n * bus.events.on('onClarificationNeeded', ({ clarification }) => {\n * bus.commands.exec('showChatPrompt', clarificationToPromptConfig(clarification))\n * })\n */\nexport function clarificationToPromptConfig(\n event: ClarificationEvent\n): ChatPromptConfig {\n return {\n type: 'choice',\n title: event.question,\n config: {\n options: event.options.map((opt) => {\n const merged: Record<string, unknown> = { ...(opt.metadata ?? {}) }\n // Runtime fallback for legacy payloads that still carry file_id at the top level.\n const legacyFileId = (opt as { file_id?: number }).file_id\n if (legacyFileId !== undefined && merged.file_id === undefined) {\n merged.file_id = legacyFileId\n }\n return {\n value: opt.value,\n label: opt.label,\n // Only include metadata if non-empty (keeps payloads clean)\n ...(Object.keys(merged).length > 0 ? { metadata: merged } : {}),\n }\n }),\n layout: 'vertical',\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-bus.js","sources":["../../src/services/chat-bus.ts"],"sourcesContent":["/**\n * Chat Bus — Event Emitter + Command Handler\n * v2.4.0: Core primitives for the chat event/command bus\n *\n * @experimental — This API may change without major bump until v2.5.0.\n */\n\nimport type {\n ChatEvents,\n ChatCommands,\n ChatEventEmitter,\n ChatCommandHandler,\n ChatBus,\n EventSubscribeOptions,\n ScratchpadSection,\n ClarificationEvent,\n ElicitationEvent,\n ElicitationPropertySchema,\n ChatPromptConfig,\n FormPromptConfig,\n} from '../types/chat-bus'\n\n// ─── Event Emitter ───────────────────────────────────────────\n\ninterface Listener<F extends (...args: any[]) => any> {\n handler: F\n options?: EventSubscribeOptions\n throttledHandler?: F\n}\n\n/**\n * Create a typed event emitter with throttle and streamKey filtering support.\n *\n * @experimental\n *\n * @example\n * const emitter = createEventEmitter<ChatEvents>()\n * const unsub = emitter.on('onToken', (event) => console.log(event.token), { throttle: 100 })\n * emitter.emit('onToken', { streamKey: 'abc', token: 'hello' })\n * unsub()\n */\nexport function createEventEmitter(): ChatEventEmitter {\n const listeners = new Map<string, Set<Listener<any>>>()\n\n interface ThrottledFn<F> {\n fn: F\n cancel: () => void\n }\n\n function createThrottled<F extends (...args: any[]) => void>(fn: F, ms: number): ThrottledFn<F> {\n let lastCall = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n let lastArgs: any[] | null = null\n let cancelled = false\n\n const throttled = ((...args: any[]) => {\n if (cancelled) return\n lastArgs = args\n const now = Date.now()\n const remaining = ms - (now - lastCall)\n\n if (remaining <= 0) {\n if (timer) { clearTimeout(timer); timer = null }\n lastCall = now\n fn(...args)\n } else if (!timer) {\n timer = setTimeout(() => {\n lastCall = Date.now()\n timer = null\n if (lastArgs && !cancelled) {\n try { fn(...lastArgs) } catch (err) { console.error('[ChatBus] Error in throttled handler:', err) }\n }\n }, remaining)\n }\n }) as F\n\n return {\n fn: throttled,\n cancel: () => { cancelled = true; if (timer) { clearTimeout(timer); timer = null } },\n }\n }\n\n return {\n on(event, handler, options) {\n if (!listeners.has(event as string)) {\n listeners.set(event as string, new Set())\n }\n\n const listener: Listener<typeof handler> = { handler, options }\n\n // Apply throttle if requested\n let throttleHandle: ThrottledFn<typeof handler> | null = null\n if (options?.throttle && options.throttle > 0) {\n throttleHandle = createThrottled(handler, options.throttle)\n listener.throttledHandler = throttleHandle.fn\n }\n\n listeners.get(event as string)!.add(listener)\n\n // Return unsubscribe function — cancels pending throttle timers\n return () => {\n throttleHandle?.cancel()\n listeners.get(event as string)?.delete(listener)\n }\n },\n\n emit(event, ...args) {\n const set = listeners.get(event as string)\n if (!set) return\n\n for (const listener of set) {\n // StreamKey filtering: skip if listener wants a specific streamKey\n // For most events args[0] has streamKey; for onCustomEvent args[1] has it\n if (listener.options?.streamKey) {\n let streamKeyArg: unknown\n for (const arg of args) {\n if (arg && typeof arg === 'object' && 'streamKey' in (arg as any)) {\n streamKeyArg = (arg as any).streamKey\n break\n }\n }\n if (streamKeyArg !== undefined && streamKeyArg !== listener.options.streamKey) continue\n }\n\n const fn = listener.throttledHandler || listener.handler\n try {\n fn(...args)\n } catch (err) {\n console.error(`[ChatBus] Error in ${event as string} handler:`, err)\n }\n }\n },\n\n clear() {\n listeners.clear()\n },\n } as ChatEventEmitter\n}\n\n// ─── Command Handler ─────────────────────────────────────────\n\n/**\n * Create a typed command handler. The host app registers handlers,\n * agents execute commands.\n *\n * @experimental\n *\n * @example\n * const commands = createCommandHandler<ChatCommands>()\n * commands.handle('injectPrompt', (text) => setInputValue(text))\n * commands.exec('injectPrompt', 'Hello world')\n */\nexport function createCommandHandler(): ChatCommandHandler {\n const handlers = new Map<string, (...args: any[]) => any>()\n\n return {\n handle(command, handler) {\n handlers.set(command as string, handler)\n },\n\n exec(command, ...args) {\n const handler = handlers.get(command as string)\n if (!handler) {\n console.warn(`[ChatBus] No handler registered for command: ${command as string}`)\n return undefined as any\n }\n return handler(...args)\n },\n } as ChatCommandHandler\n}\n\n// ─── Chat Bus Factory ────────────────────────────────────────\n\n/**\n * Create a complete ChatBus with events + commands.\n *\n * @experimental\n *\n * @example\n * const bus = createChatBus()\n * bus.events.on('onStreamEnd', (event) => { ... })\n * bus.commands.handle('sendPrompt', (text) => { ... })\n */\nexport function createChatBus(): ChatBus {\n return {\n events: createEventEmitter(),\n commands: createCommandHandler(),\n }\n}\n\n// ─── Scratchpad Section Merge Helper ─────────────────────────\n\n/**\n * Merge sections from a ScratchpadEvent into existing state sections.\n * Handles replace/append/upsert modes.\n *\n * @example\n * const newSections = mergeScratchpadSections(\n * currentState.sections,\n * event.sections,\n * event.sectionMode\n * )\n */\nexport function mergeScratchpadSections(\n existing: ScratchpadSection[],\n incoming: ScratchpadSection[] | undefined,\n mode: 'replace' | 'append' | 'upsert' = 'replace'\n): ScratchpadSection[] {\n if (!incoming) return existing\n\n switch (mode) {\n case 'append':\n return [...existing, ...incoming]\n\n case 'upsert': {\n const result = [...existing]\n for (const section of incoming) {\n const idx = result.findIndex((s) => s.id === section.id)\n if (idx >= 0) {\n result[idx] = section\n } else {\n result.push(section)\n }\n }\n return result\n }\n\n case 'replace':\n default:\n return incoming\n }\n}\n\n// ─── Clarification → Prompt Helper (v4.3.9) ──────────────────\n\n/**\n * Convert a ClarificationEvent into a ChatPromptConfig.\n * Universal bridge for apps receiving clarification events via SSE.\n *\n * Legacy runtime `file_id` (removed from the type in v5.0.0) is still\n * transparently migrated into `metadata.file_id` when present, so payloads\n * from older servers continue to work without upgrade pressure.\n *\n * @experimental\n * @since v4.3.9\n * @example\n * bus.events.on('onClarificationNeeded', ({ clarification }) => {\n * bus.commands.exec('showChatPrompt', clarificationToPromptConfig(clarification))\n * })\n */\n// ─── Elicitation → Prompt Helper (v5.2.0) ───────────────────\n\n/**\n * Convert an MCP `elicitation/create` payload into a `ChatPromptConfig`.\n *\n * Mapping rules :\n * - Single `boolean` property → `type: 'confirm'`\n * - Single property with `enum` of ≤4 values → `type: 'choice'` (one option per enum value)\n * - Anything else → `type: 'form'` with one field per schema property\n *\n * JSON Schema primitive types map to mcp-ui form field types :\n *\n * | JSON Schema | mcp-ui FormFieldType |\n * |---|---|\n * | `type: 'string'` | `'text'` |\n * | `type: 'string', format: 'email'` | `'email'` |\n * | `type: 'string', format: 'date'` or `'date-time'` | `'date'` |\n * | `type: 'string', enum: [...]` | `'select'` |\n * | `type: 'number' \\| 'integer'` | `'number'` |\n * | `type: 'boolean'` | `'checkbox'` |\n *\n * Unknown shapes fall through to plain text with a `helpText` warning.\n *\n * @experimental\n * @since v5.2.0\n *\n * @example\n * bus.events.on('onElicitation', ({ elicitation }) => {\n * bus.commands.exec('showChatPrompt', elicitationToPromptConfig(elicitation))\n * })\n */\nexport function elicitationToPromptConfig(event: ElicitationEvent): ChatPromptConfig {\n const propEntries = Object.entries(event.requestedSchema.properties)\n\n // Shortcut 1 : single boolean → confirm\n if (propEntries.length === 1 && propEntries[0][1].type === 'boolean') {\n return {\n type: 'confirm',\n title: event.message,\n config: {\n message: propEntries[0][1].description,\n },\n }\n }\n\n // Shortcut 2 : single enum property with ≤4 values → choice\n if (propEntries.length === 1) {\n const [, schema] = propEntries[0]\n if (schema.enum && schema.enum.length > 0 && schema.enum.length <= 4) {\n return {\n type: 'choice',\n title: event.message,\n config: {\n options: schema.enum.map((val, idx) => ({\n value: String(val),\n label: schema.enumNames?.[idx] ?? String(val),\n })),\n layout: 'vertical',\n },\n }\n }\n }\n\n // Default : full form\n const required = new Set(event.requestedSchema.required ?? [])\n const fields: FormPromptConfig['fields'] = propEntries.map(([name, schema]) => ({\n name,\n label: schema.title ?? name,\n ...schemaToFieldType(schema),\n required: required.has(name),\n helpText: schema.description,\n ...(schema.default !== undefined ? { placeholder: String(schema.default) } : {}),\n }))\n\n return {\n type: 'form',\n title: event.message,\n config: { fields },\n }\n}\n\nfunction schemaToFieldType(\n schema: ElicitationPropertySchema\n):\n | { type: FormPromptConfig['fields'][number]['type']; options?: Array<{ label: string; value: string }> }\n | { type: FormPromptConfig['fields'][number]['type']; helpText?: string } {\n // Enum → select\n if (schema.enum && schema.enum.length > 0) {\n return {\n type: 'select',\n options: schema.enum.map((val, idx) => ({\n label: schema.enumNames?.[idx] ?? String(val),\n value: String(val),\n })),\n }\n }\n\n if (schema.type === 'boolean') return { type: 'checkbox' }\n if (schema.type === 'number' || schema.type === 'integer') return { type: 'number' }\n if (schema.type === 'string') {\n if (schema.format === 'email') return { type: 'email' }\n if (schema.format === 'date' || schema.format === 'date-time') return { type: 'date' }\n return { type: 'text' }\n }\n\n // Unknown primitive — fall back to text with a warning\n console.warn(\n `[MCP-UI] elicitationToPromptConfig: unsupported schema type \"${(schema as { type?: string }).type}\", falling back to text.`\n )\n return { type: 'text' }\n}\n\nexport function clarificationToPromptConfig(\n event: ClarificationEvent\n): ChatPromptConfig {\n return {\n type: 'choice',\n title: event.question,\n config: {\n options: event.options.map((opt) => {\n const merged: Record<string, unknown> = { ...(opt.metadata ?? {}) }\n // Runtime fallback for legacy payloads that still carry file_id at the top level.\n const legacyFileId = (opt as { file_id?: number }).file_id\n if (legacyFileId !== undefined && merged.file_id === undefined) {\n merged.file_id = legacyFileId\n }\n return {\n value: opt.value,\n label: opt.label,\n // Only include metadata if non-empty (keeps payloads clean)\n ...(Object.keys(merged).length > 0 ? { metadata: merged } : {}),\n }\n }),\n layout: 'vertical',\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
6
|
+
const solidJs = require("solid-js");
|
|
7
|
+
class PromptReplacedError extends Error {
|
|
8
|
+
constructor(message = "Prompt replaced by a newer one") {
|
|
9
|
+
super(message);
|
|
10
|
+
__publicField(this, "name", "PromptReplacedError");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function createChatPromptController() {
|
|
14
|
+
const [activePrompt, setActivePrompt] = solidJs.createSignal(null);
|
|
15
|
+
let pending = null;
|
|
16
|
+
function cleanupAbort(entry) {
|
|
17
|
+
if (entry.signal && entry.onAbort) {
|
|
18
|
+
entry.signal.removeEventListener("abort", entry.onAbort);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function handle(config, signal) {
|
|
22
|
+
if (pending) {
|
|
23
|
+
const previous = pending;
|
|
24
|
+
pending = null;
|
|
25
|
+
cleanupAbort(previous);
|
|
26
|
+
previous.reject(new PromptReplacedError());
|
|
27
|
+
}
|
|
28
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
29
|
+
setActivePrompt(null);
|
|
30
|
+
return Promise.reject(new DOMException("Prompt aborted", "AbortError"));
|
|
31
|
+
}
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const entry = { type: config.type, resolve, reject, signal };
|
|
34
|
+
if (signal) {
|
|
35
|
+
entry.onAbort = () => {
|
|
36
|
+
if (pending === entry) {
|
|
37
|
+
pending = null;
|
|
38
|
+
cleanupAbort(entry);
|
|
39
|
+
setActivePrompt(null);
|
|
40
|
+
reject(new DOMException("Prompt aborted", "AbortError"));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
signal.addEventListener("abort", entry.onAbort, { once: true });
|
|
44
|
+
}
|
|
45
|
+
pending = entry;
|
|
46
|
+
setActivePrompt(config);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function resolveActive(response) {
|
|
50
|
+
if (!pending) return;
|
|
51
|
+
const entry = pending;
|
|
52
|
+
pending = null;
|
|
53
|
+
cleanupAbort(entry);
|
|
54
|
+
setActivePrompt(null);
|
|
55
|
+
entry.resolve(response);
|
|
56
|
+
}
|
|
57
|
+
function dismissActive() {
|
|
58
|
+
if (!pending) return;
|
|
59
|
+
const entry = pending;
|
|
60
|
+
pending = null;
|
|
61
|
+
cleanupAbort(entry);
|
|
62
|
+
setActivePrompt(null);
|
|
63
|
+
entry.resolve({ type: entry.type, value: "", label: "", dismissed: true });
|
|
64
|
+
}
|
|
65
|
+
function abort(reason = "Prompt aborted") {
|
|
66
|
+
if (!pending) return;
|
|
67
|
+
const entry = pending;
|
|
68
|
+
pending = null;
|
|
69
|
+
cleanupAbort(entry);
|
|
70
|
+
setActivePrompt(null);
|
|
71
|
+
entry.reject(new DOMException(reason, "AbortError"));
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
handle,
|
|
75
|
+
activePrompt,
|
|
76
|
+
resolveActive,
|
|
77
|
+
dismissActive,
|
|
78
|
+
abort
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
exports.PromptReplacedError = PromptReplacedError;
|
|
82
|
+
exports.createChatPromptController = createChatPromptController;
|
|
83
|
+
//# sourceMappingURL=chat-prompt-controller.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-prompt-controller.cjs","sources":["../../src/services/chat-prompt-controller.ts"],"sourcesContent":["/**\n * createChatPromptController — centralised lifecycle for `showChatPrompt`\n *\n * @experimental\n * @since v5.2.0\n *\n * The controller owns the resolver closure, AbortSignal wiring, and\n * re-entrance policy in one primitive. Consumers go from ~20 LOC of manual\n * wiring per app to :\n *\n * ```ts\n * const ctrl = createChatPromptController()\n * bus.commands.handle('showChatPrompt', ctrl.handle)\n * // ...\n * <Show when={ctrl.activePrompt()}>\n * {(cfg) => (\n * <ChatPrompt\n * config={cfg()}\n * onSubmit={ctrl.resolveActive}\n * onDismiss={ctrl.dismissActive}\n * />\n * )}\n * </Show>\n * ```\n *\n * ## Re-entrance policy\n *\n * If a new `showChatPrompt` arrives while a previous Promise is still\n * pending, the previous Promise rejects **synchronously** with a\n * `PromptReplacedError` before the new prompt is installed. Callers that\n * care can branch on `err instanceof PromptReplacedError` or `err.name ===\n * 'PromptReplacedError'`.\n *\n * ## Abort semantics\n *\n * `handle(config, signal?)` honours `AbortSignal` :\n *\n * - If `signal.aborted === true` on entry → returns a rejected Promise with\n * `new DOMException('Prompt aborted', 'AbortError')`, does NOT set\n * `activePrompt`.\n * - Otherwise registers a once-only listener that rejects with the same\n * `DOMException` on abort, clearing the active state.\n *\n * `AbortError` is the Web Platform convention (matches `fetch()`,\n * `Response.body.cancel()`, etc.) — callers can branch on `err.name ===\n * 'AbortError'` without importing any mcp-ui type.\n */\n\nimport { createSignal, type Accessor } from 'solid-js'\nimport type { ChatPromptConfig, ChatPromptResponse } from '../types/chat-bus'\n\n// ─── Error class ─────────────────────────────────────────────\n\n/**\n * Thrown when an active `showChatPrompt` Promise is rejected because a new\n * prompt arrived before the previous one resolved. Consumers can use\n * `instanceof PromptReplacedError` or `err.name === 'PromptReplacedError'` to\n * branch (retry, bail, log).\n *\n * @experimental\n * @since v5.2.0\n */\nexport class PromptReplacedError extends Error {\n readonly name = 'PromptReplacedError' as const\n constructor(message = 'Prompt replaced by a newer one') {\n super(message)\n }\n}\n\n// ─── Controller shape ────────────────────────────────────────\n\nexport interface ChatPromptController {\n /**\n * Register as the bus handler :\n * `bus.commands.handle('showChatPrompt', ctrl.handle)`\n */\n handle: (config: ChatPromptConfig, signal?: AbortSignal) => Promise<ChatPromptResponse>\n\n /**\n * Reactive accessor for the currently active prompt config (null when no\n * prompt is pending). Use in JSX to drive `<ChatPrompt>` rendering.\n */\n activePrompt: Accessor<ChatPromptConfig | null>\n\n /** Call this from `<ChatPrompt>`'s `onSubmit` prop. */\n resolveActive: (response: ChatPromptResponse) => void\n\n /** Call this from `<ChatPrompt>`'s `onDismiss` prop. */\n dismissActive: () => void\n\n /**\n * Cancel the active prompt programmatically (e.g. on route change). Rejects\n * the pending Promise with the supplied reason or an `AbortError`.\n */\n abort: (reason?: string) => void\n}\n\n// ─── Factory ─────────────────────────────────────────────────\n\n/**\n * Create a stateful controller that owns the active prompt Promise, the\n * AbortSignal listener, and the re-entrance policy. See module JSDoc for\n * full usage.\n *\n * @experimental\n * @since v5.2.0\n */\nexport function createChatPromptController(): ChatPromptController {\n const [activePrompt, setActivePrompt] = createSignal<ChatPromptConfig | null>(null)\n\n interface PendingEntry {\n type: ChatPromptConfig['type']\n resolve: (r: ChatPromptResponse) => void\n reject: (err: unknown) => void\n signal?: AbortSignal\n onAbort?: () => void\n }\n\n let pending: PendingEntry | null = null\n\n function cleanupAbort(entry: PendingEntry): void {\n if (entry.signal && entry.onAbort) {\n entry.signal.removeEventListener('abort', entry.onAbort)\n }\n }\n\n function clearPending(): void {\n if (pending) {\n cleanupAbort(pending)\n pending = null\n }\n setActivePrompt(null)\n }\n\n function handle(\n config: ChatPromptConfig,\n signal?: AbortSignal\n ): Promise<ChatPromptResponse> {\n // Re-entrance : synchronously reject the previous Promise before\n // installing the new prompt. The caller's .catch sees the rejection\n // on the microtask boundary regardless.\n if (pending) {\n const previous = pending\n pending = null\n cleanupAbort(previous)\n previous.reject(new PromptReplacedError())\n }\n\n // Abort already tripped on entry : return a rejected Promise without\n // ever showing the UI.\n if (signal?.aborted) {\n setActivePrompt(null)\n return Promise.reject(new DOMException('Prompt aborted', 'AbortError'))\n }\n\n return new Promise<ChatPromptResponse>((resolve, reject) => {\n const entry: PendingEntry = { type: config.type, resolve, reject, signal }\n\n if (signal) {\n entry.onAbort = () => {\n // If this entry is still active, reject + clear. If a newer prompt\n // has since replaced it, the cleanup already ran — no-op.\n if (pending === entry) {\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n reject(new DOMException('Prompt aborted', 'AbortError'))\n }\n }\n signal.addEventListener('abort', entry.onAbort, { once: true })\n }\n\n pending = entry\n setActivePrompt(config)\n })\n }\n\n function resolveActive(response: ChatPromptResponse): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n entry.resolve(response)\n }\n\n function dismissActive(): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n // Surface as a resolved Promise with dismissed: true — matches existing\n // ChatPrompt onDismiss contract from v4.x.\n entry.resolve({ type: entry.type, value: '', label: '', dismissed: true })\n }\n\n function abort(reason = 'Prompt aborted'): void {\n if (!pending) return\n const entry = pending\n pending = null\n cleanupAbort(entry)\n setActivePrompt(null)\n entry.reject(new DOMException(reason, 'AbortError'))\n }\n\n return {\n handle,\n activePrompt,\n resolveActive,\n dismissActive,\n abort,\n }\n}\n"],"names":["createSignal"],"mappings":";;;;;;AA8DO,MAAM,4BAA4B,MAAM;AAAA,EAE7C,YAAY,UAAU,kCAAkC;AACtD,UAAM,OAAO;AAFN,gCAAO;AAAA,EAGhB;AACF;AAwCO,SAAS,6BAAmD;AACjE,QAAM,CAAC,cAAc,eAAe,IAAIA,QAAAA,aAAsC,IAAI;AAUlF,MAAI,UAA+B;AAEnC,WAAS,aAAa,OAA2B;AAC/C,QAAI,MAAM,UAAU,MAAM,SAAS;AACjC,YAAM,OAAO,oBAAoB,SAAS,MAAM,OAAO;AAAA,IACzD;AAAA,EACF;AAUA,WAAS,OACP,QACA,QAC6B;AAI7B,QAAI,SAAS;AACX,YAAM,WAAW;AACjB,gBAAU;AACV,mBAAa,QAAQ;AACrB,eAAS,OAAO,IAAI,qBAAqB;AAAA,IAC3C;AAIA,QAAI,iCAAQ,SAAS;AACnB,sBAAgB,IAAI;AACpB,aAAO,QAAQ,OAAO,IAAI,aAAa,kBAAkB,YAAY,CAAC;AAAA,IACxE;AAEA,WAAO,IAAI,QAA4B,CAAC,SAAS,WAAW;AAC1D,YAAM,QAAsB,EAAE,MAAM,OAAO,MAAM,SAAS,QAAQ,OAAA;AAElE,UAAI,QAAQ;AACV,cAAM,UAAU,MAAM;AAGpB,cAAI,YAAY,OAAO;AACrB,sBAAU;AACV,yBAAa,KAAK;AAClB,4BAAgB,IAAI;AACpB,mBAAO,IAAI,aAAa,kBAAkB,YAAY,CAAC;AAAA,UACzD;AAAA,QACF;AACA,eAAO,iBAAiB,SAAS,MAAM,SAAS,EAAE,MAAM,MAAM;AAAA,MAChE;AAEA,gBAAU;AACV,sBAAgB,MAAM;AAAA,IACxB,CAAC;AAAA,EACH;AAEA,WAAS,cAAc,UAAoC;AACzD,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AACpB,UAAM,QAAQ,QAAQ;AAAA,EACxB;AAEA,WAAS,gBAAsB;AAC7B,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AAGpB,UAAM,QAAQ,EAAE,MAAM,MAAM,MAAM,OAAO,IAAI,OAAO,IAAI,WAAW,KAAA,CAAM;AAAA,EAC3E;AAEA,WAAS,MAAM,SAAS,kBAAwB;AAC9C,QAAI,CAAC,QAAS;AACd,UAAM,QAAQ;AACd,cAAU;AACV,iBAAa,KAAK;AAClB,oBAAgB,IAAI;AACpB,UAAM,OAAO,IAAI,aAAa,QAAQ,YAAY,CAAC;AAAA,EACrD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;;;"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createChatPromptController — centralised lifecycle for `showChatPrompt`
|
|
3
|
+
*
|
|
4
|
+
* @experimental
|
|
5
|
+
* @since v5.2.0
|
|
6
|
+
*
|
|
7
|
+
* The controller owns the resolver closure, AbortSignal wiring, and
|
|
8
|
+
* re-entrance policy in one primitive. Consumers go from ~20 LOC of manual
|
|
9
|
+
* wiring per app to :
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const ctrl = createChatPromptController()
|
|
13
|
+
* bus.commands.handle('showChatPrompt', ctrl.handle)
|
|
14
|
+
* // ...
|
|
15
|
+
* <Show when={ctrl.activePrompt()}>
|
|
16
|
+
* {(cfg) => (
|
|
17
|
+
* <ChatPrompt
|
|
18
|
+
* config={cfg()}
|
|
19
|
+
* onSubmit={ctrl.resolveActive}
|
|
20
|
+
* onDismiss={ctrl.dismissActive}
|
|
21
|
+
* />
|
|
22
|
+
* )}
|
|
23
|
+
* </Show>
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ## Re-entrance policy
|
|
27
|
+
*
|
|
28
|
+
* If a new `showChatPrompt` arrives while a previous Promise is still
|
|
29
|
+
* pending, the previous Promise rejects **synchronously** with a
|
|
30
|
+
* `PromptReplacedError` before the new prompt is installed. Callers that
|
|
31
|
+
* care can branch on `err instanceof PromptReplacedError` or `err.name ===
|
|
32
|
+
* 'PromptReplacedError'`.
|
|
33
|
+
*
|
|
34
|
+
* ## Abort semantics
|
|
35
|
+
*
|
|
36
|
+
* `handle(config, signal?)` honours `AbortSignal` :
|
|
37
|
+
*
|
|
38
|
+
* - If `signal.aborted === true` on entry → returns a rejected Promise with
|
|
39
|
+
* `new DOMException('Prompt aborted', 'AbortError')`, does NOT set
|
|
40
|
+
* `activePrompt`.
|
|
41
|
+
* - Otherwise registers a once-only listener that rejects with the same
|
|
42
|
+
* `DOMException` on abort, clearing the active state.
|
|
43
|
+
*
|
|
44
|
+
* `AbortError` is the Web Platform convention (matches `fetch()`,
|
|
45
|
+
* `Response.body.cancel()`, etc.) — callers can branch on `err.name ===
|
|
46
|
+
* 'AbortError'` without importing any mcp-ui type.
|
|
47
|
+
*/
|
|
48
|
+
import { type Accessor } from 'solid-js';
|
|
49
|
+
import type { ChatPromptConfig, ChatPromptResponse } from '../types/chat-bus';
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when an active `showChatPrompt` Promise is rejected because a new
|
|
52
|
+
* prompt arrived before the previous one resolved. Consumers can use
|
|
53
|
+
* `instanceof PromptReplacedError` or `err.name === 'PromptReplacedError'` to
|
|
54
|
+
* branch (retry, bail, log).
|
|
55
|
+
*
|
|
56
|
+
* @experimental
|
|
57
|
+
* @since v5.2.0
|
|
58
|
+
*/
|
|
59
|
+
export declare class PromptReplacedError extends Error {
|
|
60
|
+
readonly name: "PromptReplacedError";
|
|
61
|
+
constructor(message?: string);
|
|
62
|
+
}
|
|
63
|
+
export interface ChatPromptController {
|
|
64
|
+
/**
|
|
65
|
+
* Register as the bus handler :
|
|
66
|
+
* `bus.commands.handle('showChatPrompt', ctrl.handle)`
|
|
67
|
+
*/
|
|
68
|
+
handle: (config: ChatPromptConfig, signal?: AbortSignal) => Promise<ChatPromptResponse>;
|
|
69
|
+
/**
|
|
70
|
+
* Reactive accessor for the currently active prompt config (null when no
|
|
71
|
+
* prompt is pending). Use in JSX to drive `<ChatPrompt>` rendering.
|
|
72
|
+
*/
|
|
73
|
+
activePrompt: Accessor<ChatPromptConfig | null>;
|
|
74
|
+
/** Call this from `<ChatPrompt>`'s `onSubmit` prop. */
|
|
75
|
+
resolveActive: (response: ChatPromptResponse) => void;
|
|
76
|
+
/** Call this from `<ChatPrompt>`'s `onDismiss` prop. */
|
|
77
|
+
dismissActive: () => void;
|
|
78
|
+
/**
|
|
79
|
+
* Cancel the active prompt programmatically (e.g. on route change). Rejects
|
|
80
|
+
* the pending Promise with the supplied reason or an `AbortError`.
|
|
81
|
+
*/
|
|
82
|
+
abort: (reason?: string) => void;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create a stateful controller that owns the active prompt Promise, the
|
|
86
|
+
* AbortSignal listener, and the re-entrance policy. See module JSDoc for
|
|
87
|
+
* full usage.
|
|
88
|
+
*
|
|
89
|
+
* @experimental
|
|
90
|
+
* @since v5.2.0
|
|
91
|
+
*/
|
|
92
|
+
export declare function createChatPromptController(): ChatPromptController;
|
|
93
|
+
//# sourceMappingURL=chat-prompt-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-prompt-controller.d.ts","sourceRoot":"","sources":["../../src/services/chat-prompt-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAI7E;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAG,qBAAqB,CAAS;gBAClC,OAAO,SAAmC;CAGvD;AAID,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAEvF;;;OAGG;IACH,YAAY,EAAE,QAAQ,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAE/C,uDAAuD;IACvD,aAAa,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAErD,wDAAwD;IACxD,aAAa,EAAE,MAAM,IAAI,CAAA;IAEzB;;;OAGG;IACH,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAID;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,IAAI,oBAAoB,CA0GjE"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { createSignal } from "solid-js";
|
|
5
|
+
class PromptReplacedError extends Error {
|
|
6
|
+
constructor(message = "Prompt replaced by a newer one") {
|
|
7
|
+
super(message);
|
|
8
|
+
__publicField(this, "name", "PromptReplacedError");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function createChatPromptController() {
|
|
12
|
+
const [activePrompt, setActivePrompt] = createSignal(null);
|
|
13
|
+
let pending = null;
|
|
14
|
+
function cleanupAbort(entry) {
|
|
15
|
+
if (entry.signal && entry.onAbort) {
|
|
16
|
+
entry.signal.removeEventListener("abort", entry.onAbort);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function handle(config, signal) {
|
|
20
|
+
if (pending) {
|
|
21
|
+
const previous = pending;
|
|
22
|
+
pending = null;
|
|
23
|
+
cleanupAbort(previous);
|
|
24
|
+
previous.reject(new PromptReplacedError());
|
|
25
|
+
}
|
|
26
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
27
|
+
setActivePrompt(null);
|
|
28
|
+
return Promise.reject(new DOMException("Prompt aborted", "AbortError"));
|
|
29
|
+
}
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const entry = { type: config.type, resolve, reject, signal };
|
|
32
|
+
if (signal) {
|
|
33
|
+
entry.onAbort = () => {
|
|
34
|
+
if (pending === entry) {
|
|
35
|
+
pending = null;
|
|
36
|
+
cleanupAbort(entry);
|
|
37
|
+
setActivePrompt(null);
|
|
38
|
+
reject(new DOMException("Prompt aborted", "AbortError"));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
signal.addEventListener("abort", entry.onAbort, { once: true });
|
|
42
|
+
}
|
|
43
|
+
pending = entry;
|
|
44
|
+
setActivePrompt(config);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function resolveActive(response) {
|
|
48
|
+
if (!pending) return;
|
|
49
|
+
const entry = pending;
|
|
50
|
+
pending = null;
|
|
51
|
+
cleanupAbort(entry);
|
|
52
|
+
setActivePrompt(null);
|
|
53
|
+
entry.resolve(response);
|
|
54
|
+
}
|
|
55
|
+
function dismissActive() {
|
|
56
|
+
if (!pending) return;
|
|
57
|
+
const entry = pending;
|
|
58
|
+
pending = null;
|
|
59
|
+
cleanupAbort(entry);
|
|
60
|
+
setActivePrompt(null);
|
|
61
|
+
entry.resolve({ type: entry.type, value: "", label: "", dismissed: true });
|
|
62
|
+
}
|
|
63
|
+
function abort(reason = "Prompt aborted") {
|
|
64
|
+
if (!pending) return;
|
|
65
|
+
const entry = pending;
|
|
66
|
+
pending = null;
|
|
67
|
+
cleanupAbort(entry);
|
|
68
|
+
setActivePrompt(null);
|
|
69
|
+
entry.reject(new DOMException(reason, "AbortError"));
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
handle,
|
|
73
|
+
activePrompt,
|
|
74
|
+
resolveActive,
|
|
75
|
+
dismissActive,
|
|
76
|
+
abort
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
PromptReplacedError,
|
|
81
|
+
createChatPromptController
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=chat-prompt-controller.js.map
|