@aparte/plugin-ask-user 0.10.0 → 0.12.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/dist/aparte-ask-user.d.ts +16 -1
- package/dist/aparte-ask-user.d.ts.map +1 -1
- package/dist/{ask-user-BEUi72kr.js → ask-user-C2BeIprM.js} +2 -5
- package/dist/ask-user-C2BeIprM.js.map +1 -0
- package/dist/ask-user.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.node.js +1 -1
- package/package.json +2 -2
- package/dist/ask-user-BEUi72kr.js.map +0 -1
|
@@ -7,7 +7,22 @@
|
|
|
7
7
|
* The two are interchangeable.
|
|
8
8
|
*/
|
|
9
9
|
import { AparteElicitation } from '@aparte/core';
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* A semantic alias of core's `<aparte-elicitation>`: same presenter, intent-revealing
|
|
12
|
+
* tag. `ask_user` runs on the core elicitation primitive and has no bespoke component,
|
|
13
|
+
* so this subclass adds no behaviour — it exists so markup can say what it means. The
|
|
14
|
+
* two are interchangeable, and neither dispatches anything.
|
|
15
|
+
*
|
|
16
|
+
* @element aparte-ask-user
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <!-- Identical to <aparte-elicitation>; mount either one, never both. -->
|
|
20
|
+
* <aparte-chat>
|
|
21
|
+
* <aparte-chat-viewport></aparte-chat-viewport>
|
|
22
|
+
* <aparte-ask-user></aparte-ask-user>
|
|
23
|
+
* <aparte-composer></aparte-composer>
|
|
24
|
+
* </aparte-chat>
|
|
25
|
+
*/
|
|
11
26
|
export declare class AparteAskUser extends AparteElicitation {
|
|
12
27
|
}
|
|
13
28
|
declare global {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aparte-ask-user.d.ts","sourceRoot":"","sources":["../src/aparte-ask-user.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD
|
|
1
|
+
{"version":3,"file":"aparte-ask-user.d.ts","sourceRoot":"","sources":["../src/aparte-ask-user.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,aAAc,SAAQ,iBAAiB;CAAG;AAMvD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,iBAAiB,EAAE,aAAa,CAAC;KACpC;CACJ"}
|
|
@@ -133,10 +133,7 @@ const askUserHandler = async (call, signal, context) => {
|
|
|
133
133
|
if (result.action === "accept") {
|
|
134
134
|
return { toolCallId: call.id, content: formatAnswer(result.content, labels) };
|
|
135
135
|
}
|
|
136
|
-
|
|
137
|
-
return { toolCallId: call.id, content: ASK_USER_DECLINED };
|
|
138
|
-
}
|
|
139
|
-
throw new DOMException("ask_user aborted", "AbortError");
|
|
136
|
+
return { toolCallId: call.id, content: ASK_USER_DECLINED };
|
|
140
137
|
};
|
|
141
138
|
function questionField(item) {
|
|
142
139
|
const options = item.options ?? [];
|
|
@@ -225,4 +222,4 @@ export {
|
|
|
225
222
|
askUserHandler as a,
|
|
226
223
|
createAskUserTool as c
|
|
227
224
|
};
|
|
228
|
-
//# sourceMappingURL=ask-user-
|
|
225
|
+
//# sourceMappingURL=ask-user-C2BeIprM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask-user-C2BeIprM.js","sources":["../src/ask-user.ts"],"sourcesContent":["/**\n * ask_user tool for aparté.\n *\n * Lets the AI ask the user a structured question (title + optional description),\n * as single (radio) or multiple (checkbox) choice. The handler is a thin ADAPTER\n * over the core elicitation primitive: it maps the tool input to an elicitation\n * schema and awaits `requestUserInput`, presented by `<aparte-elicitation>` — no\n * framework coupling, no window events, no per-tool contract to drift.\n *\n * Usage:\n * aparteGlobalConfig.registerTool(createAskUserTool(), askUserHandler);\n */\n\nimport type { AparteTool, AparteToolHandler, AparteToolResult } from '@aparte/core';\nimport { requestUserInput } from '@aparte/core';\nimport type {\n AparteElicitationSchema,\n AparteElicitationEnumField,\n AparteElicitationStringField,\n} from '@aparte/core';\n\nexport interface AskUserOption {\n title: string;\n description?: string;\n /** Highlights this option as the recommended choice */\n recommended?: boolean;\n}\n\n/** A single question within an ask_user call (multi-question form). */\nexport interface AskUserItem {\n question: string;\n /** A short label — the chip a stepped form shows for this question. */\n header?: string;\n options: AskUserOption[];\n /** If true, renders checkboxes (multi-select). Default false (radio). */\n multiple?: boolean;\n /** Pre-select the option whose title matches this value */\n defaultValue?: string;\n}\n\nexport interface AskUserDetail {\n toolCallId: string;\n /**\n * Multi-question form. When present and non-empty, EVERY question is rendered\n * and this takes precedence over the single-question fields below.\n */\n questions?: AskUserItem[];\n // ── Single-question fields — honoured for callers that build a request directly. ──\n question?: string;\n options?: AskUserOption[];\n multiple?: boolean;\n /** Pre-select the option whose title matches this value */\n defaultValue?: string;\n}\n\n/** Bounds the schema puts on what the model may ask for. */\nexport interface AskUserToolOptions {\n /**\n * Options per question. Default 4.\n *\n * Four because six plus the free-text escape is seven rows in a composer, and it\n * reads as a form that escaped into a chat — and because a model asked for four\n * writes better options than one asked for six: it has to choose. Configurable\n * because a real case may differ, and because a bound the host cannot move is a\n * bound the host has to fork the tool to change.\n */\n maxOptions?: number;\n /** Questions per call. Default 5. */\n maxQuestions?: number;\n}\n\n/**\n * Build the `ask_user` tool.\n *\n * A factory rather than a constant because the schema carries BOUNDS, and bounds are\n * the host's to set: this is the same reasoning that moved the free-text escape out\n * of the model's schema and onto the config. The defaults are what every serious\n * implementation of this pattern lands near (four options, a handful of questions),\n * so `createAskUserTool()` with no argument is the normal call.\n */\nexport function createAskUserTool(options: AskUserToolOptions = {}): AparteTool {\n const maxOptions = options.maxOptions ?? 4;\n const maxQuestions = options.maxQuestions ?? 5;\n return {\n name: 'ask_user',\n description: 'Ask the user a question with structured options (title + optional description). Use for single or multiple choice input.',\n systemPrompt: `You have access to the ask_user tool.\n\nWHEN TO USE IT: only when the user's request is genuinely ambiguous and requires a choice between distinct options before you can proceed (e.g. \"which framework should I use?\", \"what style do you prefer?\").\n\nWHEN NOT TO USE IT — respond directly instead:\n- Factual questions: \"what is 2+2\", \"what is the capital of France\"\n- Greetings or chitchat: \"hello\", \"how are you\"\n- Coding tasks where you can make a reasonable default choice\n- Any question you can answer without needing user input\n\nWhen you do use it, every question needs a short \"header\" (one or two words — it is the\ntab a user clicks to come back to that question) and 2 to ${maxOptions} options, each\nwith a short \"title\". Both are enforced by the schema, not preferences. Set\n\"multiple: true\" only when several options can apply simultaneously.`,\n inputSchema: {\n type: 'object',\n properties: {\n questions: {\n type: 'array',\n minItems: 1,\n maxItems: maxQuestions,\n description: 'One or more questions to ask the user (each rendered with its own options).',\n items: {\n type: 'object',\n properties: {\n question: {\n type: 'string',\n description: 'The question to display to the user'\n },\n header: {\n type: 'string',\n maxLength: 16,\n description: 'A SHORT label for this question — one or two words, no sentence (e.g. \"Colour\", \"Framework\"). It is the tab a user clicks to come back to this question, so it has to read as a name.'\n },\n options: {\n type: 'array',\n // minItems, and REQUIRED below.\n //\n // Both were missing, so `{question, allow_other: true}`\n // with no options at all was a schema-VALID call — and a\n // local model made exactly that call. The panel then had\n // nothing to offer and rendered a radio list whose only\n // entry was \"Other…\": a text box wearing the costume of a\n // choice. The 2–6 range was stated in the system prompt,\n // in prose. A small model reads the schema.\n minItems: 2,\n // FOUR, not six.\n //\n // Six options plus the free-text escape is seven rows in a\n // composer, and it looked like what it was: a form that had\n // escaped into a chat. Every serious implementation of this\n // pattern caps around four, and a model asked for four\n // writes better options than one asked for six — it has to\n // choose. Reversible if a real case needs more.\n maxItems: maxOptions,\n description: `The selectable options (2 to ${maxOptions}). A question with no options is not a choice — answer the user directly instead.`,\n items: {\n type: 'object',\n properties: {\n title: { type: 'string', description: 'Short label shown in bold' },\n description: { type: 'string', description: 'Optional detail shown below the title' }\n },\n required: ['title']\n }\n },\n multiple: {\n type: 'boolean',\n description: 'If true, renders checkboxes (multi-select) for this question. Default: false (radio).'\n }\n // No `allow_other`. Whether a choice offers a free-text escape\n // is the HOST's decision — `setElicitationOptions({ allowOther })`\n // — not a field for the model to fill. It used to be here, and a\n // small model sent `allow_other: true` with no options at all,\n // which is how the panel came to render a radio list whose only\n // entry was \"Other…\". An `allow_other` still sent is ignored, so\n // no existing call breaks.\n },\n // `header` is REQUIRED, and that is the difference between tabs\n // reading \"Forme\" and \"Couleur\" or reading \"1\" and \"2\". Left\n // optional, a model simply omits it — observed on the first real\n // run against a local model. The adapter still falls back to the\n // position, so a model that ignores this produces a usable panel\n // rather than an error; requiring it makes the good case normal.\n required: ['question', 'header', 'options']\n }\n },\n // ── Single-question form — also accepted (agnostic). ──\n question: {\n type: 'string',\n description: 'A single question (prefer `questions`).'\n },\n options: {\n type: 'array',\n minItems: 2,\n maxItems: maxOptions,\n description: `Options for the single-question form (2 to ${maxOptions}).`,\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n description: { type: 'string' }\n },\n required: ['title']\n }\n },\n multiple: {\n type: 'boolean',\n description: 'Multi-select for the single-question form.'\n }\n },\n // Accept EITHER the multi-question `questions` array OR a single `question`.\n // EITHER the multi-question array OR a single question — and in the single\n // case its options too. `anyOf` is honoured unevenly by local runtimes, which\n // is why the per-item `required` above carries the weight instead.\n anyOf: [\n { required: ['questions'] },\n { required: ['question', 'options'] }\n ]\n }\n };\n}\n\n/**\n * ask_user is a thin ADAPTER over the core elicitation primitive: the handler\n * maps the tool input to an elicitation schema (`enum` for one question, an\n * `object` form for several) and awaits `requestUserInput`, which routes to the\n * `<aparte-elicitation>` presenter. `accept` → the answer, `decline` → a\n * model-usable note, `cancel` → an AbortError the loop surfaces as a failed call.\n */\n/**\n * What the model is told when the user declines.\n *\n * A constant rather than a literal because the RECEIPT has to recognise it: the\n * transcript must show \"declined\" instead of pairing this sentence with the first\n * question as though the user had said it. Matching English at a distance would have\n * been the alternative, and it breaks the moment this string is localised.\n */\nexport const ASK_USER_DECLINED = 'The user declined to answer.';\n\nexport const askUserHandler: AparteToolHandler = async (call, signal, context): Promise<AparteToolResult> => {\n const { message, schema, labels } = buildRequest(call.input);\n // `target` is what makes the RIGHT chat answer. Without it `requestUserInput`\n // resolves its presenter from the global config, so a chat given its own\n // `config` — with its own `<aparte-elicitation>` — got `{ action: 'cancel' }`\n // and the model was told the user had refused a question never shown to them.\n // The handler had no way to know which chat it was running for until\n // `AparteToolContext` existed.\n // No try/catch and no third branch: a request that ends without an answer REJECTS\n // with an AbortError now, which is what this handler used to build by hand from\n // `{ action: 'cancel' }`. Letting it propagate is the same outcome with one fewer\n // place to get it wrong — and the conversion existing here is what showed the\n // rejection was the right shape for the primitive.\n const result = await requestUserInput({ message, schema, signal, target: context?.target });\n if (result.action === 'accept') {\n return { toolCallId: call.id, content: formatAnswer(result.content, labels) };\n }\n return { toolCallId: call.id, content: ASK_USER_DECLINED };\n};\n\n/**\n * Build the elicitation field for one normalised question.\n *\n * A question with no usable options degrades to a free-text field rather than to an\n * enum with nothing in it. The schema now forbids that shape, but a model that\n * ignores the schema is the normal case, not the exception — and the old code built\n * `{ type: 'enum', options: [] }`, which the panel rendered as a radio list whose\n * only entry was \"Other…\". Selecting a radio to reveal the text box you actually\n * needed is a worse text box.\n *\n * The question text becomes the field's `title` in both shapes. It used to be\n * carried by the object PROPERTY KEY instead, and the panel labelled the field only\n * because it falls back to printing the key when a field has no title — a label that\n * worked by accident.\n */\nfunction questionField(item: AskUserItem): AparteElicitationEnumField | AparteElicitationStringField {\n const options = item.options ?? [];\n if (options.length === 0) {\n return { type: 'string', title: item.question, header: item.header, default: item.defaultValue };\n }\n return {\n type: 'enum',\n title: item.question,\n header: item.header,\n options: options.map((o) => ({\n value: o.title,\n label: o.title,\n description: o.description,\n recommended: o.recommended,\n })),\n multiple: item.multiple,\n // Deliberately unset: the panel falls back to the host's policy\n // (`setElicitationOptions`). See the schema comment above.\n default: item.defaultValue,\n };\n}\n\n/**\n * Map the raw tool input to a `{ message, schema }` request. Supports the\n * multi-question shape `{ questions: [...] }` (→ an `object` form when there are\n * several, an `enum` when there is one) AND the single-question shape. The model\n * emits snake_case `allow_other` → mapped to `allowOther`.\n */\nfunction buildRequest(input: Record<string, unknown>): {\n message: string;\n schema: AparteElicitationSchema;\n /** Form key → the question text, so the answer sent back names the question. */\n labels: Record<string, string>;\n} {\n const raw = input['questions'];\n const toItem = (o: Record<string, unknown>): AskUserItem => ({\n question: (o['question'] as string) ?? '',\n header: (o['header'] as string) ?? undefined,\n options: normalizeOptions(o['options']),\n multiple: (o['multiple'] as boolean) ?? false,\n // `allow_other` is read from nowhere on purpose — it is the host's call.\n defaultValue: (o['default_value'] as string) ?? (o['defaultValue'] as string) ?? undefined,\n });\n\n if (Array.isArray(raw) && raw.length > 0) {\n const items = raw.map((q) => toItem((q ?? {}) as Record<string, unknown>));\n const [firstItem] = items;\n if (items.length === 1 && firstItem) {\n return { message: firstItem.question, schema: questionField(firstItem), labels: {} };\n }\n // STABLE keys, not the question text.\n //\n // The text used to be the property key, which had three costs: two\n // identically-worded questions silently collapsed into one field; the key is\n // what `formatAnswer` sends back, so a long question became a long key; and\n // the field's label depended on the panel's fallback of printing the key.\n // The text now travels as the field's `title` — and `labels` carries it to\n // the answer, so the model still reads \"question → answer\" and not \"q2 →\".\n const properties: Record<string, AparteElicitationEnumField | AparteElicitationStringField> = {};\n const labels: Record<string, string> = {};\n items.forEach((it, i) => {\n const key = `q${i + 1}`;\n properties[key] = questionField(it);\n labels[key] = it.question;\n });\n // No generic header: every field is labelled with its own question, so\n // \"Please answer:\" was one more line of untranslated English above questions\n // in the user's language.\n return { message: '', schema: { type: 'object', properties }, labels };\n }\n\n // Single-question shape.\n const item = toItem(input);\n return { message: item.question, schema: questionField(item), labels: {} };\n}\n\n/**\n * Flatten the elicitation content into the tool-result string fed back to the model.\n *\n * `labels` maps the form's stable keys back to the question text, so the model reads\n * \"Quelle couleur ? → bleu\" rather than \"q1 → bleu\". Without it, stable keys would\n * have made the answer unintelligible to the very reader it is for.\n */\nfunction formatAnswer(content: unknown, labels: Record<string, string> = {}): string {\n if (Array.isArray(content)) return content.join(', ');\n if (content && typeof content === 'object') {\n return Object.entries(content as Record<string, unknown>)\n .map(([k, v]) => `${labels[k] ?? k} → ${Array.isArray(v) ? v.join(', ') : String(v)}`)\n .join('\\n');\n }\n return String(content ?? '');\n}\n\nconst _OPT_DESC_KEYS = new Set(['description', 'desc', 'detail']);\n\n/**\n * Normalise a raw options array into AskUserOption[]. The schema asks the\n * model for `{title, description}`, but a small model may improvise the option\n * shape at inference: a plain string, or the label under `label`/`value`/`text`/\n * `name`/`option`, or some other key entirely. Accept all of these — and as a last\n * resort take the first non-description string field — so the panel renders real\n * options instead of collapsing to a lone \"Other…\". Entries with no usable label\n * are dropped.\n */\nfunction normalizeOptions(raw: unknown): AskUserOption[] {\n if (!Array.isArray(raw)) return [];\n const out: AskUserOption[] = [];\n for (const item of raw) {\n if (item == null) continue;\n if (typeof item === 'string') {\n if (item.trim()) out.push({ title: item });\n continue;\n }\n const o = item as Record<string, unknown>;\n let label: unknown = o['title'] ?? o['label'] ?? o['value'] ?? o['text'] ?? o['name'] ?? o['option'];\n if (label == null || String(label).trim() === '') {\n // Unknown improvised key → first non-description string field wins.\n const entry = Object.entries(o).find(([k, v]) => typeof v === 'string' && v.trim() !== '' && !_OPT_DESC_KEYS.has(k));\n if (entry) label = entry[1];\n }\n if (label == null || String(label).trim() === '') continue;\n const description = o['description'] ?? o['desc'] ?? o['detail'];\n out.push({\n title: String(label),\n description: description != null ? String(description) : undefined,\n recommended: (o['recommended'] ?? o['recommend']) as boolean | undefined,\n });\n }\n return out;\n}\n"],"names":[],"mappings":"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"}
|
package/dist/ask-user.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask-user.d.ts","sourceRoot":"","sources":["../src/ask-user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAoB,MAAM,cAAc,CAAC;AAQpF,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;IAE1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,4DAA4D;AAC5D,MAAM,WAAW,kBAAkB;IAC/B;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CA8H9E;AAED;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAEhE,eAAO,MAAM,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"ask-user.d.ts","sourceRoot":"","sources":["../src/ask-user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAoB,MAAM,cAAc,CAAC;AAQpF,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;IAE1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,4DAA4D;AAC5D,MAAM,WAAW,kBAAkB;IAC/B;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CA8H9E;AAED;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAEhE,eAAO,MAAM,cAAc,EAAE,iBAkB5B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { escapeAttr, AparteElicitation, aparteGlobalConfig, registerSegmentRenderer } from "@aparte/core";
|
|
2
|
-
import { A as ASK_USER_DECLINED, c as createAskUserTool, a as askUserHandler } from "./ask-user-
|
|
2
|
+
import { A as ASK_USER_DECLINED, c as createAskUserTool, a as askUserHandler } from "./ask-user-C2BeIprM.js";
|
|
3
3
|
const esc = escapeAttr;
|
|
4
4
|
const questionReceiptRenderer = {
|
|
5
5
|
type: "question-receipt",
|
|
6
6
|
render(seg) {
|
|
7
|
-
return `<div class="segment seg-qreceipt" data-segment-id="${esc(seg.id)}">
|
|
7
|
+
return `<div class="aparte-segment seg-qreceipt" data-segment-id="${esc(seg.id)}">
|
|
8
8
|
<span class="qr-question">${esc(seg.question)}</span>
|
|
9
9
|
<span class="qr-sep">→</span>
|
|
10
10
|
<span class="qr-answer">${esc(seg.answer)}</span>
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/question-receipt.renderer.ts","../src/receipt.ts","../src/aparte-ask-user.ts","../src/index.ts"],"sourcesContent":["import type { AparteSegmentRenderer } from '@aparte/core';\nimport { escapeAttr } from '@aparte/core';\n\nexport interface QuestionReceiptSegment {\n id: string;\n type: 'question-receipt';\n isStreaming?: boolean;\n question: string;\n answer: string;\n}\n\n// Core owns the escaping; this alias keeps the call sites short. The inlined\n// copy that used to live here left the apostrophe through.\nconst esc = escapeAttr;\n\nexport const questionReceiptRenderer: AparteSegmentRenderer<QuestionReceiptSegment> = {\n type: 'question-receipt',\n\n render(seg) {\n return `<div class=\"segment seg-qreceipt\" data-segment-id=\"${esc(seg.id)}\">\n <span class=\"qr-question\">${esc(seg.question)}</span>\n <span class=\"qr-sep\">→</span>\n <span class=\"qr-answer\">${esc(seg.answer)}</span>\n</div>`;\n },\n\n update(el, seg) {\n const q = el.querySelector('.qr-question');\n if (q) q.textContent = seg.question;\n const a = el.querySelector('.qr-answer');\n if (a) a.textContent = seg.answer;\n },\n\n getStyles() {\n return `\n/* ── Question Receipt Card ──────────────────────────────────────────────── */\n/* Several questions leave several cards, stacked. The tool renderer builds this\n group; the card below is shared with a question-receipt segment an app emits\n itself. (No backticks in here: this whole block is a template literal.) */\n/* A declined request: the outcome, in the muted voice of something that did not\n happen — not the green of an answer given. */\n.qr-declined {\n color: var(--aparte-text-muted);\n font-style: italic;\n}\n.seg-qreceipt-group {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n}\n.seg-qreceipt {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border-radius: var(--aparte-radius-full);\n background: var(--aparte-surface-2);\n border: 1px solid var(--aparte-border);\n font-size: 0.8rem;\n max-width: 100%;\n overflow: hidden;\n animation: qr-appear 0.2s ease-out both;\n}\n.qr-question {\n color: var(--aparte-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex-shrink: 1;\n min-width: 0;\n}\n.qr-sep {\n color: var(--aparte-text-muted);\n opacity: 0.4;\n flex-shrink: 0;\n}\n.qr-answer {\n color: var(--aparte-success);\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex-shrink: 0;\n max-width: 55%;\n}\n@keyframes qr-appear {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n`;\n },\n};\n","/**\n * What the conversation keeps after a question has been asked.\n *\n * The panel lives in the composer, so once it is answered it is gone. Without a\n * record in the thread, scrolling back shows nothing: no question, no answer, no\n * sign the assistant ever asked. Every product that asks a structured question puts\n * the question and the chosen answer in the conversation, and this is that.\n *\n * Built as an **HTMLElement** rather than an HTML string, deliberately: everything\n * here is model-chosen (the questions) or user-typed (a free-text answer), and the\n * element arm of `AparteToolRenderer` has no innerHTML surface at all. The string\n * arm's first natural line is a model-to-DOM XSS in the host page's origin.\n */\n\nimport type { AparteToolCall } from '@aparte/core';\nimport { ASK_USER_DECLINED } from './ask-user.js';\n\n/** One line of the record. */\ninterface ReceiptRow {\n question: string;\n answer: string;\n /**\n * The user declined the whole request — so this row is the OUTCOME, not an\n * answer, and it has no question of its own.\n *\n * Without this the decline sentence was split as though it were the answer to the\n * first question, leaving the others blank: \"Quelle est ta couleur préférée ? →\n * The user declined to answer.\" next to an empty row. Reported from a real\n * session, and it is worse than useless — it attributes words to the user.\n */\n declined?: true;\n}\n\n/**\n * What the renderer is handed: the model's input, and the result IF the call has\n * settled.\n *\n * `result` lives on the tool-call SEGMENT, not on `AparteToolCall` — which has only\n * `{ id, name, input }`. `AparteToolRenderer`'s own JSDoc said\n * \"`segment.toolCall.result` is whatever the tool returned\", which does not compile;\n * corrected there too.\n */\nexport interface ReceiptSource {\n input: AparteToolCall['input'];\n result?: string | undefined;\n}\n\n/** The questions the model asked, in the order it asked them. */\nfunction questionsOf(input: unknown): string[] {\n const obj = (input ?? {}) as Record<string, unknown>;\n const list = obj['questions'];\n if (Array.isArray(list) && list.length > 0) {\n return list.map((q) => String((q as Record<string, unknown>)?.['question'] ?? '').trim());\n }\n const single = obj['question'];\n return typeof single === 'string' && single.trim() ? [single.trim()] : [];\n}\n\n/**\n * Pair each question with its answer.\n *\n * The handler formats a multi-question result as `question → answer` per line, and a\n * single answer as itself. The questions come from the tool INPUT rather than from\n * that string, because the input is authoritative — an answer a user typed can\n * contain anything, including an arrow.\n */\nexport function receiptRows(call: ReceiptSource): ReceiptRow[] {\n const questions = questionsOf(call.input);\n const raw = call.result ?? '';\n if (!raw.trim()) return [];\n\n // Declining is a whole-request outcome: ONE row, no question attached, and\n // certainly not this sentence pinned to the first question as if the user had\n // typed it. `Skip` declines everything by design (MCP's `decline`), including\n // questions already answered — which is exactly what made the old rendering a\n // lie rather than merely wrong.\n if (raw.trim() === ASK_USER_DECLINED) {\n return [{ question: '', answer: raw.trim(), declined: true }];\n }\n\n if (questions.length <= 1) {\n return [{ question: questions[0] ?? '', answer: raw.trim() }];\n }\n\n const lines = raw.split('\\n').filter((l) => l.trim() !== '');\n return questions.map((question, i) => {\n const line = lines[i] ?? '';\n const sep = line.indexOf(' → ');\n // Split on the FIRST arrow: the question is ours, the answer is the user's,\n // so anything arrow-like later in the line belongs to the answer.\n return { question, answer: sep === -1 ? line.trim() : line.slice(sep + 3).trim() };\n });\n}\n\n/**\n * The card the transcript shows: one `question → answer` row per question.\n *\n * Returns an empty element while the call has no result yet — the live UI is the\n * panel in the composer, and a duplicate of it in the bubble would be two places to\n * read the same pending question. Once answered, this is the only record.\n */\nexport function buildReceipt(call: ReceiptSource): HTMLElement {\n const wrap = document.createElement('div');\n const rows = receiptRows(call);\n if (rows.length === 0) return wrap;\n\n wrap.className = 'seg-qreceipt-group';\n for (const row of rows) {\n const card = document.createElement('div');\n card.className = 'segment seg-qreceipt' + (row.declined ? ' seg-qreceipt--declined' : '');\n\n // A declined request has no question → answer pair to show, so it gets neither\n // a question nor an arrow: one line saying what happened.\n if (!row.declined) {\n const q = document.createElement('span');\n q.className = 'qr-question';\n q.textContent = row.question;\n\n const sep = document.createElement('span');\n sep.className = 'qr-sep';\n sep.textContent = '→';\n\n card.append(q, sep);\n }\n\n const a = document.createElement('span');\n a.className = row.declined ? 'qr-declined' : 'qr-answer';\n a.textContent = row.answer;\n\n card.appendChild(a);\n wrap.appendChild(card);\n }\n return wrap;\n}\n","/**\n * <aparte-ask-user> — a semantic alias of core's <aparte-elicitation>.\n *\n * ask_user runs on the core elicitation primitive; it has no bespoke Web\n * Component. This thin subclass lets you mount the presenter with intent-revealing\n * markup (`<aparte-ask-user>`) instead of the generic `<aparte-elicitation>`.\n * The two are interchangeable.\n */\n\nimport { AparteElicitation } from '@aparte/core';\n\n/** Subclass alias of {@link AparteElicitation}. */\nexport class AparteAskUser extends AparteElicitation {}\n\nif (typeof customElements !== 'undefined' && !customElements.get('aparte-ask-user')) {\n customElements.define('aparte-ask-user', AparteAskUser);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'aparte-ask-user': AparteAskUser;\n }\n}\n","/**\n * @aparte/plugin-ask-user\n *\n * The built-in `ask_user` tool — a thin adapter over the core elicitation\n * primitive. The AI asks the user a structured choice; the handler forwards it to\n * `requestUserInput`, presented by `<aparte-elicitation>` (or the semantic\n * `<aparte-ask-user>` alias registered by importing this package).\n *\n * Usage:\n * import { setupAskUser } from '@aparte/plugin-ask-user';\n * setupAskUser(); // registers the tool + hides its bubble segment\n * // mount <aparte-elicitation> (or <aparte-ask-user>) in your chat\n */\n\nimport { aparteGlobalConfig, registerSegmentRenderer, type AparteConfig } from '@aparte/core';\nimport { createAskUserTool, askUserHandler, type AskUserToolOptions } from './ask-user.js';\nimport { questionReceiptRenderer } from './question-receipt.renderer.js';\nimport { buildReceipt } from './receipt.js';\n\n// Register the <aparte-ask-user> semantic alias (subclass of <aparte-elicitation>).\nimport './aparte-ask-user.js';\n\n/**\n * Register the `ask_user` tool + its handler, and hide its bubble segment\n * (it is a UI-only tool presented via the elicitation panel, not a tool pill).\n * Explicit setup — rather than a top-level import side-effect — keeps the\n * aparteGlobalConfig singleton mutation predictable in SSR/test and tree-shaking\n * friendly. Call once at application startup.\n */\nexport function setupAskUser(config: AparteConfig = aparteGlobalConfig, options: AskUserToolOptions = {}): void {\n // The bounds the schema puts on the model are the HOST's, so they arrive here\n // rather than being frozen into a constant. Defaults are the normal call.\n config.registerTool(createAskUserTool(options), askUserHandler);\n\n /*\n * The conversation keeps the record.\n *\n * This used to be `render: () => ''` — render nothing, \"it is a UI-only tool\" —\n * and the panel lives in the composer, so once it was answered the transcript\n * held no trace that the assistant had asked anything or that the user had\n * answered. Scroll back and the exchange is simply missing, which is not what a\n * conversation is for; every product that asks a structured question puts the\n * question and the chosen answer in the thread.\n *\n * The pieces were all here and wired to nothing: `questionReceiptRenderer` has\n * existed with its own markup, styles and eleven tests, exported and registered\n * by nobody, while the renderer that WOULD have shown something returned the\n * empty string. Another consequence of a surface no example ever ran.\n *\n * Registered too, so its `getStyles()` reaches the document and an app that\n * builds `question-receipt` segments of its own gets the same card.\n */\n registerSegmentRenderer(questionReceiptRenderer, config);\n config.registerToolRenderer('ask_user', {\n render: (segment) => buildReceipt({ input: segment.toolCall.input, result: segment.result }),\n });\n}\n\nexport { createAskUserTool, askUserHandler } from './ask-user.js';\nexport type { AskUserToolOptions } from './ask-user.js';\nexport type { AskUserOption, AskUserItem, AskUserDetail } from './ask-user.js';\n\nexport { AparteAskUser } from './aparte-ask-user.js';\n\nexport { questionReceiptRenderer } from './question-receipt.renderer.js';\nexport { buildReceipt, receiptRows } from './receipt.js';\nexport type { QuestionReceiptSegment } from './question-receipt.renderer.js';\n\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/question-receipt.renderer.ts","../src/receipt.ts","../src/aparte-ask-user.ts","../src/index.ts"],"sourcesContent":["import type { AparteSegmentRenderer } from '@aparte/core';\nimport { escapeAttr } from '@aparte/core';\n\nexport interface QuestionReceiptSegment {\n id: string;\n type: 'question-receipt';\n isStreaming?: boolean;\n question: string;\n answer: string;\n}\n\n// Core owns the escaping; this alias keeps the call sites short. The inlined\n// copy that used to live here left the apostrophe through.\nconst esc = escapeAttr;\n\nexport const questionReceiptRenderer: AparteSegmentRenderer<QuestionReceiptSegment> = {\n type: 'question-receipt',\n\n render(seg) {\n return `<div class=\"aparte-segment seg-qreceipt\" data-segment-id=\"${esc(seg.id)}\">\n <span class=\"qr-question\">${esc(seg.question)}</span>\n <span class=\"qr-sep\">→</span>\n <span class=\"qr-answer\">${esc(seg.answer)}</span>\n</div>`;\n },\n\n update(el, seg) {\n const q = el.querySelector('.qr-question');\n if (q) q.textContent = seg.question;\n const a = el.querySelector('.qr-answer');\n if (a) a.textContent = seg.answer;\n },\n\n getStyles() {\n return `\n/* ── Question Receipt Card ──────────────────────────────────────────────── */\n/* Several questions leave several cards, stacked. The tool renderer builds this\n group; the card below is shared with a question-receipt segment an app emits\n itself. (No backticks in here: this whole block is a template literal.) */\n/* A declined request: the outcome, in the muted voice of something that did not\n happen — not the green of an answer given. */\n.qr-declined {\n color: var(--aparte-text-muted);\n font-style: italic;\n}\n.seg-qreceipt-group {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n}\n.seg-qreceipt {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border-radius: var(--aparte-radius-full);\n background: var(--aparte-surface-2);\n border: 1px solid var(--aparte-border);\n font-size: 0.8rem;\n max-width: 100%;\n overflow: hidden;\n animation: qr-appear 0.2s ease-out both;\n}\n.qr-question {\n color: var(--aparte-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex-shrink: 1;\n min-width: 0;\n}\n.qr-sep {\n color: var(--aparte-text-muted);\n opacity: 0.4;\n flex-shrink: 0;\n}\n.qr-answer {\n color: var(--aparte-success);\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex-shrink: 0;\n max-width: 55%;\n}\n@keyframes qr-appear {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n`;\n },\n};\n","/**\n * What the conversation keeps after a question has been asked.\n *\n * The panel lives in the composer, so once it is answered it is gone. Without a\n * record in the thread, scrolling back shows nothing: no question, no answer, no\n * sign the assistant ever asked. Every product that asks a structured question puts\n * the question and the chosen answer in the conversation, and this is that.\n *\n * Built as an **HTMLElement** rather than an HTML string, deliberately: everything\n * here is model-chosen (the questions) or user-typed (a free-text answer), and the\n * element arm of `AparteToolRenderer` has no innerHTML surface at all. The string\n * arm's first natural line is a model-to-DOM XSS in the host page's origin.\n */\n\nimport type { AparteToolCall } from '@aparte/core';\nimport { ASK_USER_DECLINED } from './ask-user.js';\n\n/** One line of the record. */\ninterface ReceiptRow {\n question: string;\n answer: string;\n /**\n * The user declined the whole request — so this row is the OUTCOME, not an\n * answer, and it has no question of its own.\n *\n * Without this the decline sentence was split as though it were the answer to the\n * first question, leaving the others blank: \"Quelle est ta couleur préférée ? →\n * The user declined to answer.\" next to an empty row. Reported from a real\n * session, and it is worse than useless — it attributes words to the user.\n */\n declined?: true;\n}\n\n/**\n * What the renderer is handed: the model's input, and the result IF the call has\n * settled.\n *\n * `result` lives on the tool-call SEGMENT, not on `AparteToolCall` — which has only\n * `{ id, name, input }`. `AparteToolRenderer`'s own JSDoc said\n * \"`segment.toolCall.result` is whatever the tool returned\", which does not compile;\n * corrected there too.\n */\nexport interface ReceiptSource {\n input: AparteToolCall['input'];\n result?: string | undefined;\n}\n\n/** The questions the model asked, in the order it asked them. */\nfunction questionsOf(input: unknown): string[] {\n const obj = (input ?? {}) as Record<string, unknown>;\n const list = obj['questions'];\n if (Array.isArray(list) && list.length > 0) {\n return list.map((q) => String((q as Record<string, unknown>)?.['question'] ?? '').trim());\n }\n const single = obj['question'];\n return typeof single === 'string' && single.trim() ? [single.trim()] : [];\n}\n\n/**\n * Pair each question with its answer.\n *\n * The handler formats a multi-question result as `question → answer` per line, and a\n * single answer as itself. The questions come from the tool INPUT rather than from\n * that string, because the input is authoritative — an answer a user typed can\n * contain anything, including an arrow.\n */\nexport function receiptRows(call: ReceiptSource): ReceiptRow[] {\n const questions = questionsOf(call.input);\n const raw = call.result ?? '';\n if (!raw.trim()) return [];\n\n // Declining is a whole-request outcome: ONE row, no question attached, and\n // certainly not this sentence pinned to the first question as if the user had\n // typed it. `Skip` declines everything by design (MCP's `decline`), including\n // questions already answered — which is exactly what made the old rendering a\n // lie rather than merely wrong.\n if (raw.trim() === ASK_USER_DECLINED) {\n return [{ question: '', answer: raw.trim(), declined: true }];\n }\n\n if (questions.length <= 1) {\n return [{ question: questions[0] ?? '', answer: raw.trim() }];\n }\n\n const lines = raw.split('\\n').filter((l) => l.trim() !== '');\n return questions.map((question, i) => {\n const line = lines[i] ?? '';\n const sep = line.indexOf(' → ');\n // Split on the FIRST arrow: the question is ours, the answer is the user's,\n // so anything arrow-like later in the line belongs to the answer.\n return { question, answer: sep === -1 ? line.trim() : line.slice(sep + 3).trim() };\n });\n}\n\n/**\n * The card the transcript shows: one `question → answer` row per question.\n *\n * Returns an empty element while the call has no result yet — the live UI is the\n * panel in the composer, and a duplicate of it in the bubble would be two places to\n * read the same pending question. Once answered, this is the only record.\n */\nexport function buildReceipt(call: ReceiptSource): HTMLElement {\n const wrap = document.createElement('div');\n const rows = receiptRows(call);\n if (rows.length === 0) return wrap;\n\n wrap.className = 'seg-qreceipt-group';\n for (const row of rows) {\n const card = document.createElement('div');\n card.className = 'segment seg-qreceipt' + (row.declined ? ' seg-qreceipt--declined' : '');\n\n // A declined request has no question → answer pair to show, so it gets neither\n // a question nor an arrow: one line saying what happened.\n if (!row.declined) {\n const q = document.createElement('span');\n q.className = 'qr-question';\n q.textContent = row.question;\n\n const sep = document.createElement('span');\n sep.className = 'qr-sep';\n sep.textContent = '→';\n\n card.append(q, sep);\n }\n\n const a = document.createElement('span');\n a.className = row.declined ? 'qr-declined' : 'qr-answer';\n a.textContent = row.answer;\n\n card.appendChild(a);\n wrap.appendChild(card);\n }\n return wrap;\n}\n","/**\n * <aparte-ask-user> — a semantic alias of core's <aparte-elicitation>.\n *\n * ask_user runs on the core elicitation primitive; it has no bespoke Web\n * Component. This thin subclass lets you mount the presenter with intent-revealing\n * markup (`<aparte-ask-user>`) instead of the generic `<aparte-elicitation>`.\n * The two are interchangeable.\n */\n\nimport { AparteElicitation } from '@aparte/core';\n\n/**\n * A semantic alias of core's `<aparte-elicitation>`: same presenter, intent-revealing\n * tag. `ask_user` runs on the core elicitation primitive and has no bespoke component,\n * so this subclass adds no behaviour — it exists so markup can say what it means. The\n * two are interchangeable, and neither dispatches anything.\n *\n * @element aparte-ask-user\n *\n * @example\n * <!-- Identical to <aparte-elicitation>; mount either one, never both. -->\n * <aparte-chat>\n * <aparte-chat-viewport></aparte-chat-viewport>\n * <aparte-ask-user></aparte-ask-user>\n * <aparte-composer></aparte-composer>\n * </aparte-chat>\n */\nexport class AparteAskUser extends AparteElicitation {}\n\nif (typeof customElements !== 'undefined' && !customElements.get('aparte-ask-user')) {\n customElements.define('aparte-ask-user', AparteAskUser);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'aparte-ask-user': AparteAskUser;\n }\n}\n","/**\n * @aparte/plugin-ask-user\n *\n * The built-in `ask_user` tool — a thin adapter over the core elicitation\n * primitive. The AI asks the user a structured choice; the handler forwards it to\n * `requestUserInput`, presented by `<aparte-elicitation>` (or the semantic\n * `<aparte-ask-user>` alias registered by importing this package).\n *\n * Usage:\n * import { setupAskUser } from '@aparte/plugin-ask-user';\n * setupAskUser(); // registers the tool + hides its bubble segment\n * // mount <aparte-elicitation> (or <aparte-ask-user>) in your chat\n */\n\nimport { aparteGlobalConfig, registerSegmentRenderer, type AparteConfig } from '@aparte/core';\nimport { createAskUserTool, askUserHandler, type AskUserToolOptions } from './ask-user.js';\nimport { questionReceiptRenderer } from './question-receipt.renderer.js';\nimport { buildReceipt } from './receipt.js';\n\n// Register the <aparte-ask-user> semantic alias (subclass of <aparte-elicitation>).\nimport './aparte-ask-user.js';\n\n/**\n * Register the `ask_user` tool + its handler, and hide its bubble segment\n * (it is a UI-only tool presented via the elicitation panel, not a tool pill).\n * Explicit setup — rather than a top-level import side-effect — keeps the\n * aparteGlobalConfig singleton mutation predictable in SSR/test and tree-shaking\n * friendly. Call once at application startup.\n */\nexport function setupAskUser(config: AparteConfig = aparteGlobalConfig, options: AskUserToolOptions = {}): void {\n // The bounds the schema puts on the model are the HOST's, so they arrive here\n // rather than being frozen into a constant. Defaults are the normal call.\n config.registerTool(createAskUserTool(options), askUserHandler);\n\n /*\n * The conversation keeps the record.\n *\n * This used to be `render: () => ''` — render nothing, \"it is a UI-only tool\" —\n * and the panel lives in the composer, so once it was answered the transcript\n * held no trace that the assistant had asked anything or that the user had\n * answered. Scroll back and the exchange is simply missing, which is not what a\n * conversation is for; every product that asks a structured question puts the\n * question and the chosen answer in the thread.\n *\n * The pieces were all here and wired to nothing: `questionReceiptRenderer` has\n * existed with its own markup, styles and eleven tests, exported and registered\n * by nobody, while the renderer that WOULD have shown something returned the\n * empty string. Another consequence of a surface no example ever ran.\n *\n * Registered too, so its `getStyles()` reaches the document and an app that\n * builds `question-receipt` segments of its own gets the same card.\n */\n registerSegmentRenderer(questionReceiptRenderer, config);\n config.registerToolRenderer('ask_user', {\n render: (segment) => buildReceipt({ input: segment.toolCall.input, result: segment.result }),\n });\n}\n\nexport { createAskUserTool, askUserHandler } from './ask-user.js';\nexport type { AskUserToolOptions } from './ask-user.js';\nexport type { AskUserOption, AskUserItem, AskUserDetail } from './ask-user.js';\n\nexport { AparteAskUser } from './aparte-ask-user.js';\n\nexport { questionReceiptRenderer } from './question-receipt.renderer.js';\nexport { buildReceipt, receiptRows } from './receipt.js';\nexport type { QuestionReceiptSegment } from './question-receipt.renderer.js';\n\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":"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"}
|
package/dist/index.node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { aparteGlobalConfig } from "@aparte/core";
|
|
2
|
-
import { c as createAskUserTool, a as askUserHandler } from "./ask-user-
|
|
2
|
+
import { c as createAskUserTool, a as askUserHandler } from "./ask-user-C2BeIprM.js";
|
|
3
3
|
function setupAskUser(config = aparteGlobalConfig, options = {}) {
|
|
4
4
|
config.registerTool(createAskUserTool(options), askUserHandler);
|
|
5
5
|
config.registerToolRenderer("ask_user", { render: () => "" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/plugin-ask-user",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"typescript": "^5.4.0",
|
|
38
38
|
"vite": "^6.0.0",
|
|
39
|
-
"@aparte/core": "0.
|
|
39
|
+
"@aparte/core": "0.12.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"aparte",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ask-user-BEUi72kr.js","sources":["../src/ask-user.ts"],"sourcesContent":["/**\n * ask_user tool for aparté.\n *\n * Lets the AI ask the user a structured question (title + optional description),\n * as single (radio) or multiple (checkbox) choice. The handler is a thin ADAPTER\n * over the core elicitation primitive: it maps the tool input to an elicitation\n * schema and awaits `requestUserInput`, presented by `<aparte-elicitation>` — no\n * framework coupling, no window events, no per-tool contract to drift.\n *\n * Usage:\n * aparteGlobalConfig.registerTool(createAskUserTool(), askUserHandler);\n */\n\nimport type { AparteTool, AparteToolHandler, AparteToolResult } from '@aparte/core';\nimport { requestUserInput } from '@aparte/core';\nimport type {\n AparteElicitationSchema,\n AparteElicitationEnumField,\n AparteElicitationStringField,\n} from '@aparte/core';\n\nexport interface AskUserOption {\n title: string;\n description?: string;\n /** Highlights this option as the recommended choice */\n recommended?: boolean;\n}\n\n/** A single question within an ask_user call (multi-question form). */\nexport interface AskUserItem {\n question: string;\n /** A short label — the chip a stepped form shows for this question. */\n header?: string;\n options: AskUserOption[];\n /** If true, renders checkboxes (multi-select). Default false (radio). */\n multiple?: boolean;\n /** Pre-select the option whose title matches this value */\n defaultValue?: string;\n}\n\nexport interface AskUserDetail {\n toolCallId: string;\n /**\n * Multi-question form. When present and non-empty, EVERY question is rendered\n * and this takes precedence over the single-question fields below.\n */\n questions?: AskUserItem[];\n // ── Single-question fields — honoured for callers that build a request directly. ──\n question?: string;\n options?: AskUserOption[];\n multiple?: boolean;\n /** Pre-select the option whose title matches this value */\n defaultValue?: string;\n}\n\n/** Bounds the schema puts on what the model may ask for. */\nexport interface AskUserToolOptions {\n /**\n * Options per question. Default 4.\n *\n * Four because six plus the free-text escape is seven rows in a composer, and it\n * reads as a form that escaped into a chat — and because a model asked for four\n * writes better options than one asked for six: it has to choose. Configurable\n * because a real case may differ, and because a bound the host cannot move is a\n * bound the host has to fork the tool to change.\n */\n maxOptions?: number;\n /** Questions per call. Default 5. */\n maxQuestions?: number;\n}\n\n/**\n * Build the `ask_user` tool.\n *\n * A factory rather than a constant because the schema carries BOUNDS, and bounds are\n * the host's to set: this is the same reasoning that moved the free-text escape out\n * of the model's schema and onto the config. The defaults are what every serious\n * implementation of this pattern lands near (four options, a handful of questions),\n * so `createAskUserTool()` with no argument is the normal call.\n */\nexport function createAskUserTool(options: AskUserToolOptions = {}): AparteTool {\n const maxOptions = options.maxOptions ?? 4;\n const maxQuestions = options.maxQuestions ?? 5;\n return {\n name: 'ask_user',\n description: 'Ask the user a question with structured options (title + optional description). Use for single or multiple choice input.',\n systemPrompt: `You have access to the ask_user tool.\n\nWHEN TO USE IT: only when the user's request is genuinely ambiguous and requires a choice between distinct options before you can proceed (e.g. \"which framework should I use?\", \"what style do you prefer?\").\n\nWHEN NOT TO USE IT — respond directly instead:\n- Factual questions: \"what is 2+2\", \"what is the capital of France\"\n- Greetings or chitchat: \"hello\", \"how are you\"\n- Coding tasks where you can make a reasonable default choice\n- Any question you can answer without needing user input\n\nWhen you do use it, every question needs a short \"header\" (one or two words — it is the\ntab a user clicks to come back to that question) and 2 to ${maxOptions} options, each\nwith a short \"title\". Both are enforced by the schema, not preferences. Set\n\"multiple: true\" only when several options can apply simultaneously.`,\n inputSchema: {\n type: 'object',\n properties: {\n questions: {\n type: 'array',\n minItems: 1,\n maxItems: maxQuestions,\n description: 'One or more questions to ask the user (each rendered with its own options).',\n items: {\n type: 'object',\n properties: {\n question: {\n type: 'string',\n description: 'The question to display to the user'\n },\n header: {\n type: 'string',\n maxLength: 16,\n description: 'A SHORT label for this question — one or two words, no sentence (e.g. \"Colour\", \"Framework\"). It is the tab a user clicks to come back to this question, so it has to read as a name.'\n },\n options: {\n type: 'array',\n // minItems, and REQUIRED below.\n //\n // Both were missing, so `{question, allow_other: true}`\n // with no options at all was a schema-VALID call — and a\n // local model made exactly that call. The panel then had\n // nothing to offer and rendered a radio list whose only\n // entry was \"Other…\": a text box wearing the costume of a\n // choice. The 2–6 range was stated in the system prompt,\n // in prose. A small model reads the schema.\n minItems: 2,\n // FOUR, not six.\n //\n // Six options plus the free-text escape is seven rows in a\n // composer, and it looked like what it was: a form that had\n // escaped into a chat. Every serious implementation of this\n // pattern caps around four, and a model asked for four\n // writes better options than one asked for six — it has to\n // choose. Reversible if a real case needs more.\n maxItems: maxOptions,\n description: `The selectable options (2 to ${maxOptions}). A question with no options is not a choice — answer the user directly instead.`,\n items: {\n type: 'object',\n properties: {\n title: { type: 'string', description: 'Short label shown in bold' },\n description: { type: 'string', description: 'Optional detail shown below the title' }\n },\n required: ['title']\n }\n },\n multiple: {\n type: 'boolean',\n description: 'If true, renders checkboxes (multi-select) for this question. Default: false (radio).'\n }\n // No `allow_other`. Whether a choice offers a free-text escape\n // is the HOST's decision — `setElicitationOptions({ allowOther })`\n // — not a field for the model to fill. It used to be here, and a\n // small model sent `allow_other: true` with no options at all,\n // which is how the panel came to render a radio list whose only\n // entry was \"Other…\". An `allow_other` still sent is ignored, so\n // no existing call breaks.\n },\n // `header` is REQUIRED, and that is the difference between tabs\n // reading \"Forme\" and \"Couleur\" or reading \"1\" and \"2\". Left\n // optional, a model simply omits it — observed on the first real\n // run against a local model. The adapter still falls back to the\n // position, so a model that ignores this produces a usable panel\n // rather than an error; requiring it makes the good case normal.\n required: ['question', 'header', 'options']\n }\n },\n // ── Single-question form — also accepted (agnostic). ──\n question: {\n type: 'string',\n description: 'A single question (prefer `questions`).'\n },\n options: {\n type: 'array',\n minItems: 2,\n maxItems: maxOptions,\n description: `Options for the single-question form (2 to ${maxOptions}).`,\n items: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n description: { type: 'string' }\n },\n required: ['title']\n }\n },\n multiple: {\n type: 'boolean',\n description: 'Multi-select for the single-question form.'\n }\n },\n // Accept EITHER the multi-question `questions` array OR a single `question`.\n // EITHER the multi-question array OR a single question — and in the single\n // case its options too. `anyOf` is honoured unevenly by local runtimes, which\n // is why the per-item `required` above carries the weight instead.\n anyOf: [\n { required: ['questions'] },\n { required: ['question', 'options'] }\n ]\n }\n };\n}\n\n/**\n * ask_user is a thin ADAPTER over the core elicitation primitive: the handler\n * maps the tool input to an elicitation schema (`enum` for one question, an\n * `object` form for several) and awaits `requestUserInput`, which routes to the\n * `<aparte-elicitation>` presenter. `accept` → the answer, `decline` → a\n * model-usable note, `cancel` → an AbortError the loop surfaces as a failed call.\n */\n/**\n * What the model is told when the user declines.\n *\n * A constant rather than a literal because the RECEIPT has to recognise it: the\n * transcript must show \"declined\" instead of pairing this sentence with the first\n * question as though the user had said it. Matching English at a distance would have\n * been the alternative, and it breaks the moment this string is localised.\n */\nexport const ASK_USER_DECLINED = 'The user declined to answer.';\n\nexport const askUserHandler: AparteToolHandler = async (call, signal, context): Promise<AparteToolResult> => {\n const { message, schema, labels } = buildRequest(call.input);\n // `target` is what makes the RIGHT chat answer. Without it `requestUserInput`\n // resolves its presenter from the global config, so a chat given its own\n // `config` — with its own `<aparte-elicitation>` — got `{ action: 'cancel' }`\n // and the model was told the user had refused a question never shown to them.\n // The handler had no way to know which chat it was running for until\n // `AparteToolContext` existed.\n const result = await requestUserInput({ message, schema, signal, target: context?.target });\n if (result.action === 'accept') {\n return { toolCallId: call.id, content: formatAnswer(result.content, labels) };\n }\n if (result.action === 'decline') {\n return { toolCallId: call.id, content: ASK_USER_DECLINED };\n }\n throw new DOMException('ask_user aborted', 'AbortError');\n};\n\n/**\n * Build the elicitation field for one normalised question.\n *\n * A question with no usable options degrades to a free-text field rather than to an\n * enum with nothing in it. The schema now forbids that shape, but a model that\n * ignores the schema is the normal case, not the exception — and the old code built\n * `{ type: 'enum', options: [] }`, which the panel rendered as a radio list whose\n * only entry was \"Other…\". Selecting a radio to reveal the text box you actually\n * needed is a worse text box.\n *\n * The question text becomes the field's `title` in both shapes. It used to be\n * carried by the object PROPERTY KEY instead, and the panel labelled the field only\n * because it falls back to printing the key when a field has no title — a label that\n * worked by accident.\n */\nfunction questionField(item: AskUserItem): AparteElicitationEnumField | AparteElicitationStringField {\n const options = item.options ?? [];\n if (options.length === 0) {\n return { type: 'string', title: item.question, header: item.header, default: item.defaultValue };\n }\n return {\n type: 'enum',\n title: item.question,\n header: item.header,\n options: options.map((o) => ({\n value: o.title,\n label: o.title,\n description: o.description,\n recommended: o.recommended,\n })),\n multiple: item.multiple,\n // Deliberately unset: the panel falls back to the host's policy\n // (`setElicitationOptions`). See the schema comment above.\n default: item.defaultValue,\n };\n}\n\n/**\n * Map the raw tool input to a `{ message, schema }` request. Supports the\n * multi-question shape `{ questions: [...] }` (→ an `object` form when there are\n * several, an `enum` when there is one) AND the single-question shape. The model\n * emits snake_case `allow_other` → mapped to `allowOther`.\n */\nfunction buildRequest(input: Record<string, unknown>): {\n message: string;\n schema: AparteElicitationSchema;\n /** Form key → the question text, so the answer sent back names the question. */\n labels: Record<string, string>;\n} {\n const raw = input['questions'];\n const toItem = (o: Record<string, unknown>): AskUserItem => ({\n question: (o['question'] as string) ?? '',\n header: (o['header'] as string) ?? undefined,\n options: normalizeOptions(o['options']),\n multiple: (o['multiple'] as boolean) ?? false,\n // `allow_other` is read from nowhere on purpose — it is the host's call.\n defaultValue: (o['default_value'] as string) ?? (o['defaultValue'] as string) ?? undefined,\n });\n\n if (Array.isArray(raw) && raw.length > 0) {\n const items = raw.map((q) => toItem((q ?? {}) as Record<string, unknown>));\n const [firstItem] = items;\n if (items.length === 1 && firstItem) {\n return { message: firstItem.question, schema: questionField(firstItem), labels: {} };\n }\n // STABLE keys, not the question text.\n //\n // The text used to be the property key, which had three costs: two\n // identically-worded questions silently collapsed into one field; the key is\n // what `formatAnswer` sends back, so a long question became a long key; and\n // the field's label depended on the panel's fallback of printing the key.\n // The text now travels as the field's `title` — and `labels` carries it to\n // the answer, so the model still reads \"question → answer\" and not \"q2 →\".\n const properties: Record<string, AparteElicitationEnumField | AparteElicitationStringField> = {};\n const labels: Record<string, string> = {};\n items.forEach((it, i) => {\n const key = `q${i + 1}`;\n properties[key] = questionField(it);\n labels[key] = it.question;\n });\n // No generic header: every field is labelled with its own question, so\n // \"Please answer:\" was one more line of untranslated English above questions\n // in the user's language.\n return { message: '', schema: { type: 'object', properties }, labels };\n }\n\n // Single-question shape.\n const item = toItem(input);\n return { message: item.question, schema: questionField(item), labels: {} };\n}\n\n/**\n * Flatten the elicitation content into the tool-result string fed back to the model.\n *\n * `labels` maps the form's stable keys back to the question text, so the model reads\n * \"Quelle couleur ? → bleu\" rather than \"q1 → bleu\". Without it, stable keys would\n * have made the answer unintelligible to the very reader it is for.\n */\nfunction formatAnswer(content: unknown, labels: Record<string, string> = {}): string {\n if (Array.isArray(content)) return content.join(', ');\n if (content && typeof content === 'object') {\n return Object.entries(content as Record<string, unknown>)\n .map(([k, v]) => `${labels[k] ?? k} → ${Array.isArray(v) ? v.join(', ') : String(v)}`)\n .join('\\n');\n }\n return String(content ?? '');\n}\n\nconst _OPT_DESC_KEYS = new Set(['description', 'desc', 'detail']);\n\n/**\n * Normalise a raw options array into AskUserOption[]. The schema asks the\n * model for `{title, description}`, but a small model may improvise the option\n * shape at inference: a plain string, or the label under `label`/`value`/`text`/\n * `name`/`option`, or some other key entirely. Accept all of these — and as a last\n * resort take the first non-description string field — so the panel renders real\n * options instead of collapsing to a lone \"Other…\". Entries with no usable label\n * are dropped.\n */\nfunction normalizeOptions(raw: unknown): AskUserOption[] {\n if (!Array.isArray(raw)) return [];\n const out: AskUserOption[] = [];\n for (const item of raw) {\n if (item == null) continue;\n if (typeof item === 'string') {\n if (item.trim()) out.push({ title: item });\n continue;\n }\n const o = item as Record<string, unknown>;\n let label: unknown = o['title'] ?? o['label'] ?? o['value'] ?? o['text'] ?? o['name'] ?? o['option'];\n if (label == null || String(label).trim() === '') {\n // Unknown improvised key → first non-description string field wins.\n const entry = Object.entries(o).find(([k, v]) => typeof v === 'string' && v.trim() !== '' && !_OPT_DESC_KEYS.has(k));\n if (entry) label = entry[1];\n }\n if (label == null || String(label).trim() === '') continue;\n const description = o['description'] ?? o['desc'] ?? o['detail'];\n out.push({\n title: String(label),\n description: description != null ? String(description) : undefined,\n recommended: (o['recommended'] ?? o['recommend']) as boolean | undefined,\n });\n }\n return out;\n}\n"],"names":[],"mappings":"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"}
|