@aparte/plugin-ask-user 0.13.1 → 0.14.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/{ask-user-C2BeIprM.js → ask-user-BxzCXJgg.js} +5 -4
- package/dist/ask-user-BxzCXJgg.js.map +1 -0
- package/dist/ask-user.d.ts +28 -1
- package/dist/ask-user.d.ts.map +1 -1
- package/dist/custom-elements.json +19 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -6
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +3 -3
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +4 -3
- package/dist/index.node.js.map +1 -1
- package/dist/question-receipt.renderer.d.ts +7 -0
- package/dist/question-receipt.renderer.d.ts.map +1 -1
- package/dist/receipt.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/ask-user-C2BeIprM.js.map +0 -1
|
@@ -2,10 +2,11 @@ import { requestUserInput } from "@aparte/core";
|
|
|
2
2
|
function createAskUserTool(options = {}) {
|
|
3
3
|
const maxOptions = options.maxOptions ?? 4;
|
|
4
4
|
const maxQuestions = options.maxQuestions ?? 5;
|
|
5
|
+
const name = options.name ?? "ask_user";
|
|
5
6
|
return {
|
|
6
|
-
name
|
|
7
|
-
description: "Ask the user a question with structured options (title + optional description). Use for single or multiple choice input.",
|
|
8
|
-
systemPrompt: `You have access to the
|
|
7
|
+
name,
|
|
8
|
+
description: options.description ?? "Ask the user a question with structured options (title + optional description). Use for single or multiple choice input.",
|
|
9
|
+
systemPrompt: options.systemPrompt ?? `You have access to the ${name} tool.
|
|
9
10
|
|
|
10
11
|
WHEN 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?").
|
|
11
12
|
|
|
@@ -222,4 +223,4 @@ export {
|
|
|
222
223
|
askUserHandler as a,
|
|
223
224
|
createAskUserTool as c
|
|
224
225
|
};
|
|
225
|
-
//# sourceMappingURL=ask-user-
|
|
226
|
+
//# sourceMappingURL=ask-user-BxzCXJgg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask-user-BxzCXJgg.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, and the words the model reads. */\nexport interface AskUserToolOptions {\n /**\n * The tool's name. Default `ask_user`.\n *\n * A backend that already exposes an `ask_user` of its own, or a product that\n * wants the model to see `clarify`, used to have to fork the tool for one\n * string. The receipt follows the name: `setupAskUser` registers its renderer\n * under whatever is chosen here.\n */\n name?: string;\n /** The one-line description the model reads in the tool inventory. */\n description?: string;\n /**\n * The instructions telling the model WHEN to reach for the tool. The default is\n * the policy every serious implementation lands near — ask only when the request\n * is genuinely ambiguous — and it is written in English. A product with another\n * policy, or another language, passes its own text; it replaces the default whole.\n */\n systemPrompt?: string;\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/** What `setupAskUser` takes: the tool's options, plus whether the transcript keeps a receipt. */\nexport interface AskUserSetupOptions extends AskUserToolOptions {\n /**\n * Whether an answered question stays in the transcript as a receipt card.\n * Default true. `false` renders nothing for the call — for a product whose own\n * UI already records the exchange.\n */\n receipt?: boolean;\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 const name = options.name ?? 'ask_user';\n return {\n name,\n description: options.description ?? 'Ask the user a question with structured options (title + optional description). Use for single or multiple choice input.',\n systemPrompt: options.systemPrompt ?? `You have access to the ${name} 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
CHANGED
|
@@ -41,8 +41,26 @@ export interface AskUserDetail {
|
|
|
41
41
|
/** Pre-select the option whose title matches this value */
|
|
42
42
|
defaultValue?: string;
|
|
43
43
|
}
|
|
44
|
-
/** Bounds the schema puts on what the model may ask for. */
|
|
44
|
+
/** Bounds the schema puts on what the model may ask for, and the words the model reads. */
|
|
45
45
|
export interface AskUserToolOptions {
|
|
46
|
+
/**
|
|
47
|
+
* The tool's name. Default `ask_user`.
|
|
48
|
+
*
|
|
49
|
+
* A backend that already exposes an `ask_user` of its own, or a product that
|
|
50
|
+
* wants the model to see `clarify`, used to have to fork the tool for one
|
|
51
|
+
* string. The receipt follows the name: `setupAskUser` registers its renderer
|
|
52
|
+
* under whatever is chosen here.
|
|
53
|
+
*/
|
|
54
|
+
name?: string;
|
|
55
|
+
/** The one-line description the model reads in the tool inventory. */
|
|
56
|
+
description?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The instructions telling the model WHEN to reach for the tool. The default is
|
|
59
|
+
* the policy every serious implementation lands near — ask only when the request
|
|
60
|
+
* is genuinely ambiguous — and it is written in English. A product with another
|
|
61
|
+
* policy, or another language, passes its own text; it replaces the default whole.
|
|
62
|
+
*/
|
|
63
|
+
systemPrompt?: string;
|
|
46
64
|
/**
|
|
47
65
|
* Options per question. Default 4.
|
|
48
66
|
*
|
|
@@ -56,6 +74,15 @@ export interface AskUserToolOptions {
|
|
|
56
74
|
/** Questions per call. Default 5. */
|
|
57
75
|
maxQuestions?: number;
|
|
58
76
|
}
|
|
77
|
+
/** What `setupAskUser` takes: the tool's options, plus whether the transcript keeps a receipt. */
|
|
78
|
+
export interface AskUserSetupOptions extends AskUserToolOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Whether an answered question stays in the transcript as a receipt card.
|
|
81
|
+
* Default true. `false` renders nothing for the call — for a product whose own
|
|
82
|
+
* UI already records the exchange.
|
|
83
|
+
*/
|
|
84
|
+
receipt?: boolean;
|
|
85
|
+
}
|
|
59
86
|
/**
|
|
60
87
|
* Build the `ask_user` tool.
|
|
61
88
|
*
|
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,
|
|
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,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB;IAC/B;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,kGAAkG;AAClG,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC3D;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CA+H9E;AAED;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAEhE,eAAO,MAAM,cAAc,EAAE,iBAkB5B,CAAC"}
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"name": "options",
|
|
145
145
|
"default": "{}",
|
|
146
146
|
"type": {
|
|
147
|
-
"text": "
|
|
147
|
+
"text": "AskUserSetupOptions"
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
],
|
|
@@ -184,6 +184,14 @@
|
|
|
184
184
|
"module": "./ask-user.js"
|
|
185
185
|
}
|
|
186
186
|
},
|
|
187
|
+
{
|
|
188
|
+
"kind": "js",
|
|
189
|
+
"name": "AskUserSetupOptions",
|
|
190
|
+
"declaration": {
|
|
191
|
+
"name": "AskUserSetupOptions",
|
|
192
|
+
"module": "./ask-user.js"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
187
195
|
{
|
|
188
196
|
"kind": "js",
|
|
189
197
|
"name": "AskUserOption",
|
|
@@ -274,7 +282,7 @@
|
|
|
274
282
|
"name": "options",
|
|
275
283
|
"default": "{}",
|
|
276
284
|
"type": {
|
|
277
|
-
"text": "
|
|
285
|
+
"text": "AskUserSetupOptions"
|
|
278
286
|
}
|
|
279
287
|
}
|
|
280
288
|
],
|
|
@@ -314,6 +322,14 @@
|
|
|
314
322
|
"module": "./ask-user.js"
|
|
315
323
|
}
|
|
316
324
|
},
|
|
325
|
+
{
|
|
326
|
+
"kind": "js",
|
|
327
|
+
"name": "AskUserSetupOptions",
|
|
328
|
+
"declaration": {
|
|
329
|
+
"name": "AskUserSetupOptions",
|
|
330
|
+
"module": "./ask-user.js"
|
|
331
|
+
}
|
|
332
|
+
},
|
|
317
333
|
{
|
|
318
334
|
"kind": "js",
|
|
319
335
|
"name": "AskUserOption",
|
|
@@ -422,7 +438,7 @@
|
|
|
422
438
|
"type": {
|
|
423
439
|
"text": "AparteSegmentRenderer<QuestionReceiptSegment>"
|
|
424
440
|
},
|
|
425
|
-
"default": "{ type: 'question-receipt', render(seg) { return `<div class=\"aparte-segment aparte-tag aparte-question-receipt\" data-segment-id=\"${esc(seg.id)}\"> <span class=\"aparte-tag__label aparte-question-receipt__question\">${esc(seg.question)}</span> <span class=\"aparte-question-receipt__sep\">→</span> <span class=\"aparte-tag__label aparte-question-receipt__answer\">${esc(seg.answer)}</span> </div>`; }, update(el, seg) { const q = el.querySelector('.aparte-question-receipt__question'); if (q) q.textContent = seg.question; const a = el.querySelector('.aparte-question-receipt__answer'); if (a) a.textContent = seg.answer; }, getStyles() { return ` /* ── Question Receipt Card ──────────────────────────────────────────────── */ /* Several questions leave several cards, stacked. The tool renderer builds this group; the card below is shared with a question-receipt segment an app emits itself. (No backticks in here: this whole block is a template literal.) THE CARD IS AN .aparte-tag. It is a pill holding a truncating label, which is what that recipe is, and it used to redeclare the whole thing: inline-flex, gap, padding, border, radius, surface background, max-width — nine lines that core already owned. What is left below is only what a tag has no opinion about (the entrance, and the share of the width each half gets) plus this card's own measures, expressed as the tag's tokens so they land ON the recipe rather than beside it. This is also the only place in the repo where a PLUGIN reaches core's recipes, and that is the point: the recipes are plain classes on a stylesheet core already ships, so a plugin needs no import, no client, and no build step to use them. A capability that only core itself can reach would not be one. */ /* A declined request: the outcome, in the muted voice of something that did not happen — not the green of an answer given. */ .aparte-question-receipt__answer--declined { color: var(--aparte-text-muted); font-style: italic; } .aparte-question-receipt__group { display: flex; flex-direction: column; align-items: flex-start; gap: var(--aparte-space-2); } .aparte-question-receipt { --aparte-tag-gap: var(--aparte-space-4); --aparte-tag-padding: var(--aparte-space-3) var(--aparte-space-6); --aparte-tag-radius: var(--aparte-radius-full); --aparte-tag-font-size: var(--aparte-font-size-md); overflow: hidden; animation: aparte-question-receipt-appear var(--aparte-duration-slow) ease-out both; } /* The question yields the room, the answer keeps it: an answer cut in half is a receipt that records nothing, and the question is usually the re-readable half. */ .aparte-question-receipt__question { color: var(--aparte-text-muted); flex-shrink: 1; min-width: 0; } .aparte-question-receipt__sep { color: var(--aparte-text-muted); opacity: 0.4; flex-shrink: 0; } .aparte-question-receipt__answer { color: var(--aparte-
|
|
441
|
+
"default": "{ type: 'question-receipt', render(seg) { // The card wears core's mark (display/mark.css) — see `buildReceipt`, the DOM // path, for why an answer is success and a decline is quiet. if (seg.declined) { return `<div class=\"aparte-segment aparte-tag aparte-question-receipt aparte-mark aparte-mark--quiet aparte-question-receipt--declined\" data-segment-id=\"${esc(seg.id)}\"> <span class=\"aparte-tag__label aparte-question-receipt__answer--declined\">${esc(seg.answer)}</span> </div>`; } return `<div class=\"aparte-segment aparte-tag aparte-question-receipt aparte-mark aparte-mark--success\" data-segment-id=\"${esc(seg.id)}\"> <span class=\"aparte-tag__label aparte-question-receipt__question\">${esc(seg.question)}</span> <span class=\"aparte-question-receipt__sep\">→</span> <span class=\"aparte-tag__label aparte-question-receipt__answer\">${esc(seg.answer)}</span> </div>`; }, update(el, seg) { const q = el.querySelector('.aparte-question-receipt__question'); if (q) q.textContent = seg.question; const a = el.querySelector('.aparte-question-receipt__answer'); if (a) a.textContent = seg.answer; }, getStyles() { return ` /* ── Question Receipt Card ──────────────────────────────────────────────── */ /* Several questions leave several cards, stacked. The tool renderer builds this group; the card below is shared with a question-receipt segment an app emits itself. (No backticks in here: this whole block is a template literal.) THE CARD IS AN .aparte-tag. It is a pill holding a truncating label, which is what that recipe is, and it used to redeclare the whole thing: inline-flex, gap, padding, border, radius, surface background, max-width — nine lines that core already owned. What is left below is only what a tag has no opinion about (the entrance, and the share of the width each half gets) plus this card's own measures, expressed as the tag's tokens so they land ON the recipe rather than beside it. This is also the only place in the repo where a PLUGIN reaches core's recipes, and that is the point: the recipes are plain classes on a stylesheet core already ships, so a plugin needs no import, no client, and no build step to use them. A capability that only core itself can reach would not be one. */ /* A declined request: the outcome, in the muted voice of something that did not happen — not the green of an answer given. */ .aparte-question-receipt__answer--declined { color: var(--aparte-text-muted); font-style: italic; } .aparte-question-receipt__group { display: flex; flex-direction: column; align-items: flex-start; gap: var(--aparte-space-2); } .aparte-question-receipt { --aparte-tag-gap: var(--aparte-space-4); --aparte-tag-padding: var(--aparte-space-3) var(--aparte-space-6); --aparte-tag-radius: var(--aparte-radius-full); --aparte-tag-font-size: var(--aparte-font-size-md); overflow: hidden; animation: aparte-question-receipt-appear var(--aparte-duration-slow) ease-out both; } /* The question yields the room, the answer keeps it: an answer cut in half is a receipt that records nothing, and the question is usually the re-readable half. */ .aparte-question-receipt__question { color: var(--aparte-text-muted); flex-shrink: 1; min-width: 0; } .aparte-question-receipt__sep { color: var(--aparte-text-muted); opacity: 0.4; flex-shrink: 0; } .aparte-question-receipt__answer { /* The strong text colour, not the success green: the answer is a fact of the transcript, not a verdict, and green was the one hue outside the palette on the whole page (UI audit 2026-08-28, §8.3 #4). The weight does the emphasis. */ color: var(--aparte-text); font-weight: var(--aparte-font-weight-semibold); flex-shrink: 0; max-width: 55%; } @keyframes aparte-question-receipt-appear { from { opacity: 0; transform: translateY(var(--aparte-space-2)); } to { opacity: 1; transform: translateY(0); } } `; }, }"
|
|
426
442
|
}
|
|
427
443
|
],
|
|
428
444
|
"exports": [
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* // mount <aparte-elicitation> (or <aparte-ask-user>) in your chat
|
|
13
13
|
*/
|
|
14
14
|
import { type AparteConfig } from '@aparte/core';
|
|
15
|
-
import { type
|
|
15
|
+
import { type AskUserSetupOptions } from './ask-user.js';
|
|
16
16
|
import './aparte-ask-user.js';
|
|
17
17
|
/**
|
|
18
18
|
* Register the `ask_user` tool + its handler, and hide its bubble segment
|
|
@@ -21,9 +21,9 @@ import './aparte-ask-user.js';
|
|
|
21
21
|
* aparteGlobalConfig singleton mutation predictable in SSR/test and tree-shaking
|
|
22
22
|
* friendly. Call once at application startup.
|
|
23
23
|
*/
|
|
24
|
-
export declare function setupAskUser(config?: AparteConfig, options?:
|
|
24
|
+
export declare function setupAskUser(config?: AparteConfig, options?: AskUserSetupOptions): void;
|
|
25
25
|
export { createAskUserTool, askUserHandler } from './ask-user.js';
|
|
26
|
-
export type { AskUserToolOptions } from './ask-user.js';
|
|
26
|
+
export type { AskUserToolOptions, AskUserSetupOptions } from './ask-user.js';
|
|
27
27
|
export type { AskUserOption, AskUserItem, AskUserDetail } from './ask-user.js';
|
|
28
28
|
export { AparteAskUser } from './aparte-ask-user.js';
|
|
29
29
|
export { questionReceiptRenderer } from './question-receipt.renderer.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA+C,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9F,OAAO,EAAqC,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA+C,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9F,OAAO,EAAqC,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAK5F,OAAO,sBAAsB,CAAC;AAE9B;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,YAAiC,EAAE,OAAO,GAAE,mBAAwB,GAAG,IAAI,CAiC/G;AAED,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAClE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAE7E,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
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-BxzCXJgg.js";
|
|
3
3
|
const esc = escapeAttr;
|
|
4
4
|
const questionReceiptRenderer = {
|
|
5
5
|
type: "question-receipt",
|
|
6
6
|
render(seg) {
|
|
7
|
-
|
|
7
|
+
if (seg.declined) {
|
|
8
|
+
return `<div class="aparte-segment aparte-tag aparte-question-receipt aparte-mark aparte-mark--quiet aparte-question-receipt--declined" data-segment-id="${esc(seg.id)}">
|
|
9
|
+
<span class="aparte-tag__label aparte-question-receipt__answer--declined">${esc(seg.answer)}</span>
|
|
10
|
+
</div>`;
|
|
11
|
+
}
|
|
12
|
+
return `<div class="aparte-segment aparte-tag aparte-question-receipt aparte-mark aparte-mark--success" data-segment-id="${esc(seg.id)}">
|
|
8
13
|
<span class="aparte-tag__label aparte-question-receipt__question">${esc(seg.question)}</span>
|
|
9
14
|
<span class="aparte-question-receipt__sep">→</span>
|
|
10
15
|
<span class="aparte-tag__label aparte-question-receipt__answer">${esc(seg.answer)}</span>
|
|
@@ -67,7 +72,10 @@ const questionReceiptRenderer = {
|
|
|
67
72
|
flex-shrink: 0;
|
|
68
73
|
}
|
|
69
74
|
.aparte-question-receipt__answer {
|
|
70
|
-
|
|
75
|
+
/* The strong text colour, not the success green: the answer is a fact of the
|
|
76
|
+
transcript, not a verdict, and green was the one hue outside the palette on the
|
|
77
|
+
whole page (UI audit 2026-08-28, §8.3 #4). The weight does the emphasis. */
|
|
78
|
+
color: var(--aparte-text);
|
|
71
79
|
font-weight: var(--aparte-font-weight-semibold);
|
|
72
80
|
flex-shrink: 0;
|
|
73
81
|
max-width: 55%;
|
|
@@ -112,7 +120,7 @@ function buildReceipt(call) {
|
|
|
112
120
|
wrap.className = "aparte-question-receipt__group";
|
|
113
121
|
for (const row of rows) {
|
|
114
122
|
const card = document.createElement("div");
|
|
115
|
-
card.className = "aparte-segment aparte-tag aparte-question-receipt" + (row.declined ? " aparte-question-receipt--declined" : "");
|
|
123
|
+
card.className = "aparte-segment aparte-tag aparte-question-receipt aparte-mark" + (row.declined ? " aparte-mark--quiet aparte-question-receipt--declined" : " aparte-mark--success");
|
|
116
124
|
if (!row.declined) {
|
|
117
125
|
const q = document.createElement("span");
|
|
118
126
|
q.className = "aparte-tag__label aparte-question-receipt__question";
|
|
@@ -136,9 +144,14 @@ if (typeof customElements !== "undefined" && !customElements.get("aparte-ask-use
|
|
|
136
144
|
customElements.define("aparte-ask-user", AparteAskUser);
|
|
137
145
|
}
|
|
138
146
|
function setupAskUser(config = aparteGlobalConfig, options = {}) {
|
|
139
|
-
|
|
147
|
+
const tool = createAskUserTool(options);
|
|
148
|
+
config.registerTool(tool, askUserHandler);
|
|
149
|
+
if (options.receipt === false) {
|
|
150
|
+
config.registerToolRenderer(tool.name, { render: () => "" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
140
153
|
registerSegmentRenderer(questionReceiptRenderer, config);
|
|
141
|
-
config.registerToolRenderer(
|
|
154
|
+
config.registerToolRenderer(tool.name, {
|
|
142
155
|
render: (segment) => buildReceipt({ input: segment.toolCall.input, result: segment.result })
|
|
143
156
|
});
|
|
144
157
|
}
|
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=\"aparte-segment aparte-tag aparte-question-receipt\" data-segment-id=\"${esc(seg.id)}\">\n <span class=\"aparte-tag__label aparte-question-receipt__question\">${esc(seg.question)}</span>\n <span class=\"aparte-question-receipt__sep\">→</span>\n <span class=\"aparte-tag__label aparte-question-receipt__answer\">${esc(seg.answer)}</span>\n</div>`;\n },\n\n update(el, seg) {\n const q = el.querySelector('.aparte-question-receipt__question');\n if (q) q.textContent = seg.question;\n const a = el.querySelector('.aparte-question-receipt__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\n THE CARD IS AN .aparte-tag. It is a pill holding a truncating label, which is what\n that recipe is, and it used to redeclare the whole thing: inline-flex, gap, padding,\n border, radius, surface background, max-width — nine lines that core already owned.\n What is left below is only what a tag has no opinion about (the entrance, and the\n share of the width each half gets) plus this card's own measures, expressed as the\n tag's tokens so they land ON the recipe rather than beside it.\n\n This is also the only place in the repo where a PLUGIN reaches core's recipes, and\n that is the point: the recipes are plain classes on a stylesheet core already ships,\n so a plugin needs no import, no client, and no build step to use them. A capability\n that only core itself can reach would not be one. */\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.aparte-question-receipt__answer--declined {\n color: var(--aparte-text-muted);\n font-style: italic;\n}\n.aparte-question-receipt__group {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--aparte-space-2);\n}\n.aparte-question-receipt {\n --aparte-tag-gap: var(--aparte-space-4);\n --aparte-tag-padding: var(--aparte-space-3) var(--aparte-space-6);\n --aparte-tag-radius: var(--aparte-radius-full);\n --aparte-tag-font-size: var(--aparte-font-size-md);\n overflow: hidden;\n animation: aparte-question-receipt-appear var(--aparte-duration-slow) ease-out both;\n}\n/* The question yields the room, the answer keeps it: an answer cut in half is a\n receipt that records nothing, and the question is usually the re-readable half. */\n.aparte-question-receipt__question {\n color: var(--aparte-text-muted);\n flex-shrink: 1;\n min-width: 0;\n}\n.aparte-question-receipt__sep {\n color: var(--aparte-text-muted);\n opacity: 0.4;\n flex-shrink: 0;\n}\n.aparte-question-receipt__answer {\n color: var(--aparte-success);\n font-weight: var(--aparte-font-weight-semibold);\n flex-shrink: 0;\n max-width: 55%;\n}\n@keyframes aparte-question-receipt-appear {\n from { opacity: 0; transform: translateY(var(--aparte-space-2)); }\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 = 'aparte-question-receipt__group';\n for (const row of rows) {\n const card = document.createElement('div');\n card.className = 'aparte-segment aparte-tag aparte-question-receipt' + (row.declined ? ' aparte-question-receipt--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 = 'aparte-tag__label aparte-question-receipt__question';\n q.textContent = row.question;\n\n const sep = document.createElement('span');\n sep.className = 'aparte-question-receipt__sep';\n sep.textContent = '→';\n\n card.append(q, sep);\n }\n\n const a = document.createElement('span');\n a.className = 'aparte-tag__label ' + (row.declined ? 'aparte-question-receipt__answer--declined' : 'aparte-question-receipt__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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"}
|
|
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 * The user declined the request: `answer` is then the outcome sentence, not an\n * answer to `question`, and the card shows it alone in the mark's quiet voice.\n * The tool renderer's DOM path had this branch; an app emitting the segment\n * itself had no way to say it.\n */\n declined?: boolean;\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 // The card wears core's mark (display/mark.css) — see `buildReceipt`, the DOM\n // path, for why an answer is success and a decline is quiet.\n if (seg.declined) {\n return `<div class=\"aparte-segment aparte-tag aparte-question-receipt aparte-mark aparte-mark--quiet aparte-question-receipt--declined\" data-segment-id=\"${esc(seg.id)}\">\n <span class=\"aparte-tag__label aparte-question-receipt__answer--declined\">${esc(seg.answer)}</span>\n</div>`;\n }\n return `<div class=\"aparte-segment aparte-tag aparte-question-receipt aparte-mark aparte-mark--success\" data-segment-id=\"${esc(seg.id)}\">\n <span class=\"aparte-tag__label aparte-question-receipt__question\">${esc(seg.question)}</span>\n <span class=\"aparte-question-receipt__sep\">→</span>\n <span class=\"aparte-tag__label aparte-question-receipt__answer\">${esc(seg.answer)}</span>\n</div>`;\n },\n\n update(el, seg) {\n const q = el.querySelector('.aparte-question-receipt__question');\n if (q) q.textContent = seg.question;\n const a = el.querySelector('.aparte-question-receipt__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\n THE CARD IS AN .aparte-tag. It is a pill holding a truncating label, which is what\n that recipe is, and it used to redeclare the whole thing: inline-flex, gap, padding,\n border, radius, surface background, max-width — nine lines that core already owned.\n What is left below is only what a tag has no opinion about (the entrance, and the\n share of the width each half gets) plus this card's own measures, expressed as the\n tag's tokens so they land ON the recipe rather than beside it.\n\n This is also the only place in the repo where a PLUGIN reaches core's recipes, and\n that is the point: the recipes are plain classes on a stylesheet core already ships,\n so a plugin needs no import, no client, and no build step to use them. A capability\n that only core itself can reach would not be one. */\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.aparte-question-receipt__answer--declined {\n color: var(--aparte-text-muted);\n font-style: italic;\n}\n.aparte-question-receipt__group {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--aparte-space-2);\n}\n.aparte-question-receipt {\n --aparte-tag-gap: var(--aparte-space-4);\n --aparte-tag-padding: var(--aparte-space-3) var(--aparte-space-6);\n --aparte-tag-radius: var(--aparte-radius-full);\n --aparte-tag-font-size: var(--aparte-font-size-md);\n overflow: hidden;\n animation: aparte-question-receipt-appear var(--aparte-duration-slow) ease-out both;\n}\n/* The question yields the room, the answer keeps it: an answer cut in half is a\n receipt that records nothing, and the question is usually the re-readable half. */\n.aparte-question-receipt__question {\n color: var(--aparte-text-muted);\n flex-shrink: 1;\n min-width: 0;\n}\n.aparte-question-receipt__sep {\n color: var(--aparte-text-muted);\n opacity: 0.4;\n flex-shrink: 0;\n}\n.aparte-question-receipt__answer {\n /* The strong text colour, not the success green: the answer is a fact of the\n transcript, not a verdict, and green was the one hue outside the palette on the\n whole page (UI audit 2026-08-28, §8.3 #4). The weight does the emphasis. */\n color: var(--aparte-text);\n font-weight: var(--aparte-font-weight-semibold);\n flex-shrink: 0;\n max-width: 55%;\n}\n@keyframes aparte-question-receipt-appear {\n from { opacity: 0; transform: translateY(var(--aparte-space-2)); }\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 = 'aparte-question-receipt__group';\n for (const row of rows) {\n const card = document.createElement('div');\n // The card wears core's mark (display/mark.css): an answer given is the row the\n // choice landed on — the success tint and the bar on its start edge — and a\n // declined request is the outcome that did not happen, in the mark's quiet\n // voice. Same recipe as a chosen option in a dropdown or a checked field choice.\n card.className = 'aparte-segment aparte-tag aparte-question-receipt aparte-mark'\n + (row.declined ? ' aparte-mark--quiet aparte-question-receipt--declined' : ' aparte-mark--success');\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 = 'aparte-tag__label aparte-question-receipt__question';\n q.textContent = row.question;\n\n const sep = document.createElement('span');\n sep.className = 'aparte-question-receipt__sep';\n sep.textContent = '→';\n\n card.append(q, sep);\n }\n\n const a = document.createElement('span');\n a.className = 'aparte-tag__label ' + (row.declined ? 'aparte-question-receipt__answer--declined' : 'aparte-question-receipt__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 AskUserSetupOptions } 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: AskUserSetupOptions = {}): 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 const tool = createAskUserTool(options);\n config.registerTool(tool, askUserHandler);\n\n if (options.receipt === false) {\n config.registerToolRenderer(tool.name, { render: () => '' });\n return;\n }\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(tool.name, {\n render: (segment) => buildReceipt({ input: segment.toolCall.input, result: segment.result }),\n });\n}\n\nexport { createAskUserTool, askUserHandler } from './ask-user.js';\nexport type { AskUserToolOptions, AskUserSetupOptions } 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.d.ts
CHANGED
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
* which is the correct outcome: nothing is being rendered there.
|
|
25
25
|
*/
|
|
26
26
|
import { type AparteConfig } from '@aparte/core';
|
|
27
|
-
import { type
|
|
27
|
+
import { type AskUserSetupOptions } from './ask-user.js';
|
|
28
28
|
/**
|
|
29
29
|
* Register the `ask_user` tool + handler on the server.
|
|
30
30
|
*
|
|
31
31
|
* No receipt renderer here: it builds DOM, and this entry exists precisely so an SSR
|
|
32
32
|
* build can import the package without a document. The browser entry registers it.
|
|
33
33
|
*/
|
|
34
|
-
export declare function setupAskUser(config?: AparteConfig, options?:
|
|
34
|
+
export declare function setupAskUser(config?: AparteConfig, options?: AskUserSetupOptions): void;
|
|
35
35
|
export { createAskUserTool, askUserHandler } from './ask-user.js';
|
|
36
|
-
export type { AskUserToolOptions } from './ask-user.js';
|
|
36
|
+
export type { AskUserToolOptions, AskUserSetupOptions } from './ask-user.js';
|
|
37
37
|
export type { AskUserOption, AskUserItem, AskUserDetail } from './ask-user.js';
|
|
38
38
|
export type { QuestionReceiptSegment } from './question-receipt.renderer.js';
|
|
39
39
|
export type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';
|
package/dist/index.node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.node.d.ts","sourceRoot":"","sources":["../src/index.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAqC,KAAK,
|
|
1
|
+
{"version":3,"file":"index.node.d.ts","sourceRoot":"","sources":["../src/index.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAqC,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,YAAiC,EAAE,OAAO,GAAE,mBAAwB,GAAG,IAAI,CAI/G;AAED,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAClE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC/E,YAAY,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC7E,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.node.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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-BxzCXJgg.js";
|
|
3
3
|
function setupAskUser(config = aparteGlobalConfig, options = {}) {
|
|
4
|
-
|
|
5
|
-
config.
|
|
4
|
+
const tool = createAskUserTool(options);
|
|
5
|
+
config.registerTool(tool, askUserHandler);
|
|
6
|
+
config.registerToolRenderer(tool.name, { render: () => "" });
|
|
6
7
|
}
|
|
7
8
|
export {
|
|
8
9
|
askUserHandler,
|
package/dist/index.node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.node.js","sources":["../src/index.node.ts"],"sourcesContent":["/**\n * `@aparte/plugin-ask-user` — the DOM-free entry, for Node and SSR.\n *\n * Why this file exists: the browser barrel imports `./aparte-ask-user.js`,\n * which subclasses `AparteElicitation` — a browser-only export of `@aparte/core`.\n * Resolved through core's `node` condition that export does not exist, so\n * `import '@aparte/plugin-ask-user'` threw\n *\n * SyntaxError: The requested module '@aparte/core' does not provide an\n * export named 'AparteElicitation'\n *\n * in plain Node — and therefore in any Next / Nuxt / SvelteKit / Analog build that\n * evaluates the import on the server. Worse, the error names `@aparte/core`, so it\n * sent the reader to the wrong package.\n *\n * `@aparte/core` builds exactly this kind of entry and has a gate asserting it\n * stays DOM-free (`scripts/check-node-import.mjs`). The plugins were outside that\n * guard; they are inside it now.\n *\n * What is here: everything a server can legitimately use — the tool definition,\n * its handler, the setup call, and the types. What is NOT here: the custom\n * element and the segment renderer, both of which need a DOM. Calling\n * `setupAskUser()` on the server registers the tool without a presenter,\n * which is the correct outcome: nothing is being rendered there.\n */\n\nimport { aparteGlobalConfig, type AparteConfig } from '@aparte/core';\nimport { createAskUserTool, askUserHandler, type
|
|
1
|
+
{"version":3,"file":"index.node.js","sources":["../src/index.node.ts"],"sourcesContent":["/**\n * `@aparte/plugin-ask-user` — the DOM-free entry, for Node and SSR.\n *\n * Why this file exists: the browser barrel imports `./aparte-ask-user.js`,\n * which subclasses `AparteElicitation` — a browser-only export of `@aparte/core`.\n * Resolved through core's `node` condition that export does not exist, so\n * `import '@aparte/plugin-ask-user'` threw\n *\n * SyntaxError: The requested module '@aparte/core' does not provide an\n * export named 'AparteElicitation'\n *\n * in plain Node — and therefore in any Next / Nuxt / SvelteKit / Analog build that\n * evaluates the import on the server. Worse, the error names `@aparte/core`, so it\n * sent the reader to the wrong package.\n *\n * `@aparte/core` builds exactly this kind of entry and has a gate asserting it\n * stays DOM-free (`scripts/check-node-import.mjs`). The plugins were outside that\n * guard; they are inside it now.\n *\n * What is here: everything a server can legitimately use — the tool definition,\n * its handler, the setup call, and the types. What is NOT here: the custom\n * element and the segment renderer, both of which need a DOM. Calling\n * `setupAskUser()` on the server registers the tool without a presenter,\n * which is the correct outcome: nothing is being rendered there.\n */\n\nimport { aparteGlobalConfig, type AparteConfig } from '@aparte/core';\nimport { createAskUserTool, askUserHandler, type AskUserSetupOptions } from './ask-user.js';\n\n/**\n * Register the `ask_user` tool + handler on the server.\n *\n * No receipt renderer here: it builds DOM, and this entry exists precisely so an SSR\n * build can import the package without a document. The browser entry registers it.\n */\nexport function setupAskUser(config: AparteConfig = aparteGlobalConfig, options: AskUserSetupOptions = {}): void {\n const tool = createAskUserTool(options);\n config.registerTool(tool, askUserHandler);\n config.registerToolRenderer(tool.name, { render: () => '' });\n}\n\nexport { createAskUserTool, askUserHandler } from './ask-user.js';\nexport type { AskUserToolOptions, AskUserSetupOptions } from './ask-user.js';\nexport type { AskUserOption, AskUserItem, AskUserDetail } from './ask-user.js';\nexport type { QuestionReceiptSegment } from './question-receipt.renderer.js';\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":";;AAmCO,SAAS,aAAa,SAAuB,oBAAoB,UAA+B,CAAA,GAAU;AAC7G,QAAM,OAAO,kBAAkB,OAAO;AACtC,SAAO,aAAa,MAAM,cAAc;AACxC,SAAO,qBAAqB,KAAK,MAAM,EAAE,QAAQ,MAAM,IAAI;AAC/D;"}
|
|
@@ -5,6 +5,13 @@ export interface QuestionReceiptSegment {
|
|
|
5
5
|
isStreaming?: boolean;
|
|
6
6
|
question: string;
|
|
7
7
|
answer: string;
|
|
8
|
+
/**
|
|
9
|
+
* The user declined the request: `answer` is then the outcome sentence, not an
|
|
10
|
+
* answer to `question`, and the card shows it alone in the mark's quiet voice.
|
|
11
|
+
* The tool renderer's DOM path had this branch; an app emitting the segment
|
|
12
|
+
* itself had no way to say it.
|
|
13
|
+
*/
|
|
14
|
+
declined?: boolean;
|
|
8
15
|
}
|
|
9
16
|
export declare const questionReceiptRenderer: AparteSegmentRenderer<QuestionReceiptSegment>;
|
|
10
17
|
//# sourceMappingURL=question-receipt.renderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question-receipt.renderer.d.ts","sourceRoot":"","sources":["../src/question-receipt.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,kBAAkB,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"question-receipt.renderer.d.ts","sourceRoot":"","sources":["../src/question-receipt.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,kBAAkB,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAMD,eAAO,MAAM,uBAAuB,EAAE,qBAAqB,CAAC,sBAAsB,CA0FjF,CAAC"}
|
package/dist/receipt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receipt.d.ts","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,8BAA8B;AAC9B,UAAU,UAAU;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,IAAI,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAaD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,EAAE,CA0B7D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"receipt.d.ts","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,8BAA8B;AAC9B,UAAU,UAAU;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,IAAI,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAaD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,EAAE,CA0B7D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,WAAW,CAqC7D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/plugin-ask-user",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"node": ">=18"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@aparte/core": ">=0.
|
|
35
|
+
"@aparte/core": ">=0.14.0 <1.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
39
39
|
"typescript": "^5.4.0",
|
|
40
40
|
"vite": "^6.0.0",
|
|
41
|
-
"@aparte/core": "0.
|
|
41
|
+
"@aparte/core": "0.14.0"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"aparte",
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|