@aparte/plugin-ask-user 0.15.0 → 0.16.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.d.ts +12 -0
- package/dist/ask-user.d.ts.map +1 -1
- package/dist/custom-elements.json +92 -12
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -56
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +14 -7
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +5 -2
- package/dist/index.node.js.map +1 -1
- package/dist/{ask-user-CyQ5S6Wy.js → receipt-DUj-W-cI.js} +84 -4
- package/dist/receipt-DUj-W-cI.js.map +1 -0
- package/dist/receipt.d.ts +11 -3
- package/dist/receipt.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/ask-user-CyQ5S6Wy.js.map +0 -1
package/dist/ask-user.d.ts
CHANGED
|
@@ -115,4 +115,16 @@ export declare function createAskUserTool(options?: AskUserToolOptions): AparteT
|
|
|
115
115
|
*/
|
|
116
116
|
export declare const ASK_USER_DECLINED = "The user declined to answer.";
|
|
117
117
|
export declare const askUserHandler: AparteToolHandler;
|
|
118
|
+
/** One `{ question, value }` per question — `value` is a `string[]` for a multiple choice. */
|
|
119
|
+
export interface AskUserAnswer {
|
|
120
|
+
question: string;
|
|
121
|
+
value: string | string[];
|
|
122
|
+
}
|
|
123
|
+
/** The structured twin of `ask_user`'s prose result. */
|
|
124
|
+
export type AskUserStructuredResult = {
|
|
125
|
+
action: 'accept';
|
|
126
|
+
answers: AskUserAnswer[];
|
|
127
|
+
} | {
|
|
128
|
+
action: 'decline';
|
|
129
|
+
};
|
|
118
130
|
//# sourceMappingURL=ask-user.d.ts.map
|
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,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB;IAC/B;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC9B;;;;;;;;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,CAkI9E;AAED;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAEhE,eAAO,MAAM,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"ask-user.d.ts","sourceRoot":"","sources":["../src/ask-user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAoB,MAAM,cAAc,CAAC;AAQpF,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;IAE1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB;IAC/B;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC9B;;;;;;;;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,CAkI9E;AAED;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAEhE,eAAO,MAAM,cAAc,EAAE,iBAyB5B,CAAC;AAEF,8FAA8F;AAC9F,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC5B;AAED,wDAAwD;AACxD,MAAM,MAAM,uBAAuB,GAC7B;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,aAAa,EAAE,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC"}
|
|
@@ -134,17 +134,17 @@
|
|
|
134
134
|
},
|
|
135
135
|
"parameters": [
|
|
136
136
|
{
|
|
137
|
-
"name": "
|
|
138
|
-
"default": "
|
|
137
|
+
"name": "options",
|
|
138
|
+
"default": "{}",
|
|
139
139
|
"type": {
|
|
140
|
-
"text": "
|
|
140
|
+
"text": "AskUserSetupOptions"
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
|
-
"name": "
|
|
145
|
-
"default": "
|
|
144
|
+
"name": "config",
|
|
145
|
+
"default": "aparteGlobalConfig",
|
|
146
146
|
"type": {
|
|
147
|
-
"text": "
|
|
147
|
+
"text": "AparteConfig"
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
],
|
|
@@ -176,6 +176,38 @@
|
|
|
176
176
|
"module": "./ask-user.js"
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
|
+
{
|
|
180
|
+
"kind": "js",
|
|
181
|
+
"name": "ASK_USER_DECLINED",
|
|
182
|
+
"declaration": {
|
|
183
|
+
"name": "ASK_USER_DECLINED",
|
|
184
|
+
"module": "./ask-user.js"
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"kind": "js",
|
|
189
|
+
"name": "receiptRows",
|
|
190
|
+
"declaration": {
|
|
191
|
+
"name": "receiptRows",
|
|
192
|
+
"module": "./receipt.js"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"kind": "js",
|
|
197
|
+
"name": "ReceiptRow",
|
|
198
|
+
"declaration": {
|
|
199
|
+
"name": "ReceiptRow",
|
|
200
|
+
"module": "./receipt.js"
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"kind": "js",
|
|
205
|
+
"name": "ReceiptSource",
|
|
206
|
+
"declaration": {
|
|
207
|
+
"name": "ReceiptSource",
|
|
208
|
+
"module": "./receipt.js"
|
|
209
|
+
}
|
|
210
|
+
},
|
|
179
211
|
{
|
|
180
212
|
"kind": "js",
|
|
181
213
|
"name": "AskUserToolOptions",
|
|
@@ -192,6 +224,22 @@
|
|
|
192
224
|
"module": "./ask-user.js"
|
|
193
225
|
}
|
|
194
226
|
},
|
|
227
|
+
{
|
|
228
|
+
"kind": "js",
|
|
229
|
+
"name": "AskUserAnswer",
|
|
230
|
+
"declaration": {
|
|
231
|
+
"name": "AskUserAnswer",
|
|
232
|
+
"module": "./ask-user.js"
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"kind": "js",
|
|
237
|
+
"name": "AskUserStructuredResult",
|
|
238
|
+
"declaration": {
|
|
239
|
+
"name": "AskUserStructuredResult",
|
|
240
|
+
"module": "./ask-user.js"
|
|
241
|
+
}
|
|
242
|
+
},
|
|
195
243
|
{
|
|
196
244
|
"kind": "js",
|
|
197
245
|
"name": "AskUserOption",
|
|
@@ -272,17 +320,17 @@
|
|
|
272
320
|
},
|
|
273
321
|
"parameters": [
|
|
274
322
|
{
|
|
275
|
-
"name": "
|
|
276
|
-
"default": "
|
|
323
|
+
"name": "options",
|
|
324
|
+
"default": "{}",
|
|
277
325
|
"type": {
|
|
278
|
-
"text": "
|
|
326
|
+
"text": "AskUserSetupOptions"
|
|
279
327
|
}
|
|
280
328
|
},
|
|
281
329
|
{
|
|
282
|
-
"name": "
|
|
283
|
-
"default": "
|
|
330
|
+
"name": "config",
|
|
331
|
+
"default": "aparteGlobalConfig",
|
|
284
332
|
"type": {
|
|
285
|
-
"text": "
|
|
333
|
+
"text": "AparteConfig"
|
|
286
334
|
}
|
|
287
335
|
}
|
|
288
336
|
],
|
|
@@ -338,6 +386,22 @@
|
|
|
338
386
|
"module": "./ask-user.js"
|
|
339
387
|
}
|
|
340
388
|
},
|
|
389
|
+
{
|
|
390
|
+
"kind": "js",
|
|
391
|
+
"name": "AskUserAnswer",
|
|
392
|
+
"declaration": {
|
|
393
|
+
"name": "AskUserAnswer",
|
|
394
|
+
"module": "./ask-user.js"
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"kind": "js",
|
|
399
|
+
"name": "AskUserStructuredResult",
|
|
400
|
+
"declaration": {
|
|
401
|
+
"name": "AskUserStructuredResult",
|
|
402
|
+
"module": "./ask-user.js"
|
|
403
|
+
}
|
|
404
|
+
},
|
|
341
405
|
{
|
|
342
406
|
"kind": "js",
|
|
343
407
|
"name": "AskUserOption",
|
|
@@ -394,6 +458,22 @@
|
|
|
394
458
|
"module": "./receipt.js"
|
|
395
459
|
}
|
|
396
460
|
},
|
|
461
|
+
{
|
|
462
|
+
"kind": "js",
|
|
463
|
+
"name": "ReceiptRow",
|
|
464
|
+
"declaration": {
|
|
465
|
+
"name": "ReceiptRow",
|
|
466
|
+
"module": "./receipt.js"
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"kind": "js",
|
|
471
|
+
"name": "ReceiptSource",
|
|
472
|
+
"declaration": {
|
|
473
|
+
"name": "ReceiptSource",
|
|
474
|
+
"module": "./receipt.js"
|
|
475
|
+
}
|
|
476
|
+
},
|
|
397
477
|
{
|
|
398
478
|
"kind": "js",
|
|
399
479
|
"name": "QuestionReceiptSegment",
|
package/dist/index.d.ts
CHANGED
|
@@ -21,13 +21,14 @@ 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(
|
|
24
|
+
export declare function setupAskUser(options?: AskUserSetupOptions, config?: AparteConfig): void;
|
|
25
25
|
export { createAskUserTool, askUserHandler, ASK_USER_DECLINED } from './ask-user.js';
|
|
26
|
-
export type { AskUserToolOptions, AskUserSetupOptions } from './ask-user.js';
|
|
26
|
+
export type { AskUserToolOptions, AskUserSetupOptions, AskUserAnswer, AskUserStructuredResult } 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';
|
|
30
30
|
export { buildReceipt, receiptRows } from './receipt.js';
|
|
31
|
+
export type { ReceiptRow, ReceiptSource } from './receipt.js';
|
|
31
32
|
export type { QuestionReceiptSegment } from './question-receipt.renderer.js';
|
|
32
33
|
export type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';
|
|
33
34
|
//# sourceMappingURL=index.d.ts.map
|
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,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAK5F,OAAO,sBAAsB,CAAC;AAE9B;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,
|
|
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,OAAO,GAAE,mBAAwB,EAAE,MAAM,GAAE,YAAiC,GAAG,IAAI,CAsC/G;AAED,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACrF,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACrH,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,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC9D,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,5 +1,6 @@
|
|
|
1
1
|
import { escapeAttr, AparteElicitation, aparteGlobalConfig, registerSegmentRenderer } from "@aparte/core";
|
|
2
|
-
import {
|
|
2
|
+
import { c as createAskUserTool, a as askUserHandler, b as buildReceipt } from "./receipt-DUj-W-cI.js";
|
|
3
|
+
import { A, r } from "./receipt-DUj-W-cI.js";
|
|
3
4
|
const esc = escapeAttr;
|
|
4
5
|
const questionReceiptRenderer = {
|
|
5
6
|
type: "question-receipt",
|
|
@@ -87,63 +88,12 @@ const questionReceiptRenderer = {
|
|
|
87
88
|
`;
|
|
88
89
|
}
|
|
89
90
|
};
|
|
90
|
-
function questionsOf(input) {
|
|
91
|
-
const obj = input ?? {};
|
|
92
|
-
const list = obj["questions"];
|
|
93
|
-
if (Array.isArray(list) && list.length > 0) {
|
|
94
|
-
return list.map((q) => String(q?.["question"] ?? "").trim());
|
|
95
|
-
}
|
|
96
|
-
const single = obj["question"];
|
|
97
|
-
return typeof single === "string" && single.trim() ? [single.trim()] : [];
|
|
98
|
-
}
|
|
99
|
-
function receiptRows(call) {
|
|
100
|
-
const questions = questionsOf(call.input);
|
|
101
|
-
const raw = call.result ?? "";
|
|
102
|
-
if (!raw.trim()) return [];
|
|
103
|
-
if (raw.trim() === ASK_USER_DECLINED) {
|
|
104
|
-
return [{ question: "", answer: raw.trim(), declined: true }];
|
|
105
|
-
}
|
|
106
|
-
if (questions.length <= 1) {
|
|
107
|
-
return [{ question: questions[0] ?? "", answer: raw.trim() }];
|
|
108
|
-
}
|
|
109
|
-
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
110
|
-
return questions.map((question, i) => {
|
|
111
|
-
const line = lines[i] ?? "";
|
|
112
|
-
const sep = line.indexOf(" → ");
|
|
113
|
-
return { question, answer: sep === -1 ? line.trim() : line.slice(sep + 3).trim() };
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
function buildReceipt(call) {
|
|
117
|
-
const wrap = document.createElement("div");
|
|
118
|
-
const rows = receiptRows(call);
|
|
119
|
-
if (rows.length === 0) return wrap;
|
|
120
|
-
wrap.className = "aparte-question-receipt__group";
|
|
121
|
-
for (const row of rows) {
|
|
122
|
-
const card = document.createElement("div");
|
|
123
|
-
card.className = "aparte-segment aparte-tag aparte-question-receipt aparte-mark" + (row.declined ? " aparte-mark--quiet aparte-question-receipt--declined" : " aparte-mark--success");
|
|
124
|
-
if (!row.declined) {
|
|
125
|
-
const q = document.createElement("span");
|
|
126
|
-
q.className = "aparte-tag__label aparte-question-receipt__question";
|
|
127
|
-
q.textContent = row.question;
|
|
128
|
-
const sep = document.createElement("span");
|
|
129
|
-
sep.className = "aparte-question-receipt__sep";
|
|
130
|
-
sep.textContent = "→";
|
|
131
|
-
card.append(q, sep);
|
|
132
|
-
}
|
|
133
|
-
const a = document.createElement("span");
|
|
134
|
-
a.className = "aparte-tag__label " + (row.declined ? "aparte-question-receipt__answer--declined" : "aparte-question-receipt__answer");
|
|
135
|
-
a.textContent = row.answer;
|
|
136
|
-
card.appendChild(a);
|
|
137
|
-
wrap.appendChild(card);
|
|
138
|
-
}
|
|
139
|
-
return wrap;
|
|
140
|
-
}
|
|
141
91
|
class AparteAskUser extends AparteElicitation {
|
|
142
92
|
}
|
|
143
93
|
if (typeof customElements !== "undefined" && !customElements.get("aparte-ask-user")) {
|
|
144
94
|
customElements.define("aparte-ask-user", AparteAskUser);
|
|
145
95
|
}
|
|
146
|
-
function setupAskUser(
|
|
96
|
+
function setupAskUser(options = {}, config = aparteGlobalConfig) {
|
|
147
97
|
const tool = createAskUserTool(options);
|
|
148
98
|
config.registerTool(tool, askUserHandler);
|
|
149
99
|
if (options.receipt === false) {
|
|
@@ -152,17 +102,24 @@ function setupAskUser(config = aparteGlobalConfig, options = {}) {
|
|
|
152
102
|
}
|
|
153
103
|
registerSegmentRenderer(questionReceiptRenderer, config);
|
|
154
104
|
config.registerToolRenderer(tool.name, {
|
|
155
|
-
|
|
105
|
+
// The structured twin rides along: the receipt reads it first and only parses
|
|
106
|
+
// the prose for a result that came from elsewhere. It was left out here, so
|
|
107
|
+
// the structured path the receipt exists for was never taken on the real one.
|
|
108
|
+
render: (segment) => buildReceipt({
|
|
109
|
+
input: segment.toolCall.input,
|
|
110
|
+
result: segment.result,
|
|
111
|
+
structuredResult: segment.structuredResult
|
|
112
|
+
})
|
|
156
113
|
});
|
|
157
114
|
}
|
|
158
115
|
export {
|
|
159
|
-
ASK_USER_DECLINED,
|
|
116
|
+
A as ASK_USER_DECLINED,
|
|
160
117
|
AparteAskUser,
|
|
161
118
|
askUserHandler,
|
|
162
119
|
buildReceipt,
|
|
163
120
|
createAskUserTool,
|
|
164
121
|
questionReceiptRenderer,
|
|
165
|
-
receiptRows,
|
|
122
|
+
r as receiptRows,
|
|
166
123
|
setupAskUser
|
|
167
124
|
};
|
|
168
125
|
//# sourceMappingURL=index.js.map
|
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 * 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, ASK_USER_DECLINED } 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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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/question-receipt.renderer.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 * <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(options: AskUserSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): 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 // The structured twin rides along: the receipt reads it first and only parses\n // the prose for a result that came from elsewhere. It was left out here, so\n // the structured path the receipt exists for was never taken on the real one.\n render: (segment) => buildReceipt({\n input: segment.toolCall.input, result: segment.result, structuredResult: segment.structuredResult,\n }),\n });\n}\n\nexport { createAskUserTool, askUserHandler, ASK_USER_DECLINED } from './ask-user.js';\nexport type { AskUserToolOptions, AskUserSetupOptions, AskUserAnswer, AskUserStructuredResult } 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 { ReceiptRow, ReceiptSource } from './receipt.js';\nexport type { QuestionReceiptSegment } from './question-receipt.renderer.js';\n\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":";;;AAoBA,MAAM,MAAM;AAEL,MAAM,0BAAyE;AAAA,EAClF,MAAM;AAAA,EAEN,OAAO,KAAK;AAGR,QAAI,IAAI,UAAU;AACd,aAAO,oJAAoJ,IAAI,IAAI,EAAE,CAAC;AAAA,8EACpG,IAAI,IAAI,MAAM,CAAC;AAAA;AAAA,IAErF;AACA,WAAO,oHAAoH,IAAI,IAAI,EAAE,CAAC;AAAA,sEACxE,IAAI,IAAI,QAAQ,CAAC;AAAA;AAAA,oEAEnB,IAAI,IAAI,MAAM,CAAC;AAAA;AAAA,EAE/E;AAAA,EAEA,OAAO,IAAI,KAAK;AACZ,UAAM,IAAI,GAAG,cAAc,oCAAoC;AAC/D,QAAI,EAAG,GAAE,cAAc,IAAI;AAC3B,UAAM,IAAI,GAAG,cAAc,kCAAkC;AAC7D,QAAI,EAAG,GAAE,cAAc,IAAI;AAAA,EAC/B;AAAA,EAEA,YAAY;AACR,WAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+DX;AACJ;ACrFO,MAAM,sBAAsB,kBAAkB;AAAC;AAEtD,IAAI,OAAO,mBAAmB,eAAe,CAAC,eAAe,IAAI,iBAAiB,GAAG;AACjF,iBAAe,OAAO,mBAAmB,aAAa;AAC1D;ACFO,SAAS,aAAa,UAA+B,IAAI,SAAuB,oBAA0B;AAG7G,QAAM,OAAO,kBAAkB,OAAO;AACtC,SAAO,aAAa,MAAM,cAAc;AAExC,MAAI,QAAQ,YAAY,OAAO;AAC3B,WAAO,qBAAqB,KAAK,MAAM,EAAE,QAAQ,MAAM,IAAI;AAC3D;AAAA,EACJ;AAoBA,0BAAwB,yBAAyB,MAAM;AACvD,SAAO,qBAAqB,KAAK,MAAM;AAAA;AAAA;AAAA;AAAA,IAInC,QAAQ,CAAC,YAAY,aAAa;AAAA,MAC9B,OAAO,QAAQ,SAAS;AAAA,MAAO,QAAQ,QAAQ;AAAA,MAAQ,kBAAkB,QAAQ;AAAA,IAAA,CACpF;AAAA,EAAA,CACJ;AACL;"}
|
package/dist/index.node.d.ts
CHANGED
|
@@ -18,10 +18,15 @@
|
|
|
18
18
|
* guard; they are inside it now.
|
|
19
19
|
*
|
|
20
20
|
* What is here: everything a server can legitimately use — the tool definition,
|
|
21
|
-
* its handler, the setup call,
|
|
22
|
-
*
|
|
23
|
-
* `setupAskUser()` on the server registers
|
|
24
|
-
* which is the correct outcome: nothing is being
|
|
21
|
+
* its handler, the setup call, the decline sentence, `receiptRows` and the types.
|
|
22
|
+
* What is NOT here: the custom element, the segment renderer and `buildReceipt`,
|
|
23
|
+
* all three of which need a DOM. Calling `setupAskUser()` on the server registers
|
|
24
|
+
* the tool without a presenter, which is the correct outcome: nothing is being
|
|
25
|
+
* rendered there.
|
|
26
|
+
*
|
|
27
|
+
* `ASK_USER_DECLINED` and `receiptRows` were missing for no reason but the omission —
|
|
28
|
+
* both are pure — and the consequence was the same hard `SyntaxError: does not provide
|
|
29
|
+
* an export named …` this file was written to end.
|
|
25
30
|
*/
|
|
26
31
|
import { type AparteConfig } from '@aparte/core';
|
|
27
32
|
import { type AskUserSetupOptions } from './ask-user.js';
|
|
@@ -31,9 +36,11 @@ import { type AskUserSetupOptions } from './ask-user.js';
|
|
|
31
36
|
* No receipt renderer here: it builds DOM, and this entry exists precisely so an SSR
|
|
32
37
|
* build can import the package without a document. The browser entry registers it.
|
|
33
38
|
*/
|
|
34
|
-
export declare function setupAskUser(
|
|
35
|
-
export { createAskUserTool, askUserHandler } from './ask-user.js';
|
|
36
|
-
export
|
|
39
|
+
export declare function setupAskUser(options?: AskUserSetupOptions, config?: AparteConfig): void;
|
|
40
|
+
export { createAskUserTool, askUserHandler, ASK_USER_DECLINED } from './ask-user.js';
|
|
41
|
+
export { receiptRows } from './receipt.js';
|
|
42
|
+
export type { ReceiptRow, ReceiptSource } from './receipt.js';
|
|
43
|
+
export type { AskUserToolOptions, AskUserSetupOptions, AskUserAnswer, AskUserStructuredResult } from './ask-user.js';
|
|
37
44
|
export type { AskUserOption, AskUserItem, AskUserDetail } from './ask-user.js';
|
|
38
45
|
export type { QuestionReceiptSegment } from './question-receipt.renderer.js';
|
|
39
46
|
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
|
|
1
|
+
{"version":3,"file":"index.node.d.ts","sourceRoot":"","sources":["../src/index.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAqC,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,EAAE,MAAM,GAAE,YAAiC,GAAG,IAAI,CAI/G;AAED,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAIrF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC9D,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACrH,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,13 +1,16 @@
|
|
|
1
1
|
import { aparteGlobalConfig } from "@aparte/core";
|
|
2
|
-
import { c as createAskUserTool, a as askUserHandler } from "./
|
|
3
|
-
|
|
2
|
+
import { c as createAskUserTool, a as askUserHandler } from "./receipt-DUj-W-cI.js";
|
|
3
|
+
import { A, r } from "./receipt-DUj-W-cI.js";
|
|
4
|
+
function setupAskUser(options = {}, config = aparteGlobalConfig) {
|
|
4
5
|
const tool = createAskUserTool(options);
|
|
5
6
|
config.registerTool(tool, askUserHandler);
|
|
6
7
|
config.registerToolRenderer(tool.name, { render: () => "" });
|
|
7
8
|
}
|
|
8
9
|
export {
|
|
10
|
+
A as ASK_USER_DECLINED,
|
|
9
11
|
askUserHandler,
|
|
10
12
|
createAskUserTool,
|
|
13
|
+
r as receiptRows,
|
|
11
14
|
setupAskUser
|
|
12
15
|
};
|
|
13
16
|
//# sourceMappingURL=index.node.js.map
|
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
|
|
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, the decline sentence, `receiptRows` and the types.\n * What is NOT here: the custom element, the segment renderer and `buildReceipt`,\n * all three of which need a DOM. Calling `setupAskUser()` on the server registers\n * the tool without a presenter, which is the correct outcome: nothing is being\n * rendered there.\n *\n * `ASK_USER_DECLINED` and `receiptRows` were missing for no reason but the omission —\n * both are pure — and the consequence was the same hard `SyntaxError: does not provide\n * an export named …` this file was written to end.\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(options: AskUserSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): void {\n const tool = createAskUserTool(options);\n config.registerTool(tool, askUserHandler);\n config.registerToolRenderer(tool.name, { render: () => '' });\n}\n\nexport { createAskUserTool, askUserHandler, ASK_USER_DECLINED } from './ask-user.js';\n// Pure string work over the tool's input and result — no DOM anywhere in its path — so\n// a server rendering a transcript to HTML, or a log, can read the rows. `buildReceipt`\n// stays browser-only: it is the same data as an HTMLElement.\nexport { receiptRows } from './receipt.js';\nexport type { ReceiptRow, ReceiptSource } from './receipt.js';\nexport type { AskUserToolOptions, AskUserSetupOptions, AskUserAnswer, AskUserStructuredResult } 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":";;;AAwCO,SAAS,aAAa,UAA+B,IAAI,SAAuB,oBAA0B;AAC7G,QAAM,OAAO,kBAAkB,OAAO;AACtC,SAAO,aAAa,MAAM,cAAc;AACxC,SAAO,qBAAqB,KAAK,MAAM,EAAE,QAAQ,MAAM,IAAI;AAC/D;"}
|
|
@@ -133,10 +133,24 @@ const askUserHandler = async (call, signal, context) => {
|
|
|
133
133
|
const { message, schema, labels } = buildRequest(call.input);
|
|
134
134
|
const result = await requestUserInput({ message, schema, signal, target: context?.target });
|
|
135
135
|
if (result.action === "accept") {
|
|
136
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
toolCallId: call.id,
|
|
138
|
+
content: formatAnswer(result.content, labels),
|
|
139
|
+
// The same answer as a value — MCP's `action` beside the content, one entry
|
|
140
|
+
// per question, a `string[]` for a multiple choice — so the receipt reads it
|
|
141
|
+
// instead of re-parsing the sentence, and a host does not write a converter.
|
|
142
|
+
structuredContent: { action: "accept", answers: structuredAnswers(result.content, message, labels) }
|
|
143
|
+
};
|
|
137
144
|
}
|
|
138
|
-
return { toolCallId: call.id, content: ASK_USER_DECLINED };
|
|
145
|
+
return { toolCallId: call.id, content: ASK_USER_DECLINED, structuredContent: { action: "decline" } };
|
|
139
146
|
};
|
|
147
|
+
function structuredAnswers(content, question, labels) {
|
|
148
|
+
const value = (v) => Array.isArray(v) ? v.map(String) : String(v ?? "");
|
|
149
|
+
if (content && typeof content === "object" && !Array.isArray(content)) {
|
|
150
|
+
return Object.entries(content).map(([k, v]) => ({ question: labels[k] ?? k, value: value(v) }));
|
|
151
|
+
}
|
|
152
|
+
return [{ question, value: value(content) }];
|
|
153
|
+
}
|
|
140
154
|
function questionField(item) {
|
|
141
155
|
const options = item.options ?? [];
|
|
142
156
|
if (options.length === 0) {
|
|
@@ -219,9 +233,75 @@ function normalizeOptions(raw) {
|
|
|
219
233
|
}
|
|
220
234
|
return out;
|
|
221
235
|
}
|
|
236
|
+
function structuredRows(value) {
|
|
237
|
+
const s = value;
|
|
238
|
+
if (!s || typeof s !== "object") return null;
|
|
239
|
+
if (s.action === "decline") return [{ question: "", answer: ASK_USER_DECLINED, declined: true }];
|
|
240
|
+
if (s.action !== "accept" || !Array.isArray(s.answers)) return null;
|
|
241
|
+
return s.answers.map((a) => {
|
|
242
|
+
const row = a ?? {};
|
|
243
|
+
const v = row.value;
|
|
244
|
+
return { question: String(row.question ?? ""), answer: Array.isArray(v) ? v.map(String).join(", ") : String(v ?? "") };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function questionsOf(input) {
|
|
248
|
+
const obj = input ?? {};
|
|
249
|
+
const list = obj["questions"];
|
|
250
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
251
|
+
return list.map((q) => String(q?.["question"] ?? "").trim());
|
|
252
|
+
}
|
|
253
|
+
const single = obj["question"];
|
|
254
|
+
return typeof single === "string" && single.trim() ? [single.trim()] : [];
|
|
255
|
+
}
|
|
256
|
+
function receiptRows(call) {
|
|
257
|
+
const structured = structuredRows(call.structuredResult);
|
|
258
|
+
if (structured) return structured;
|
|
259
|
+
const questions = questionsOf(call.input);
|
|
260
|
+
const raw = call.result ?? "";
|
|
261
|
+
if (!raw.trim()) return [];
|
|
262
|
+
if (raw.trim() === ASK_USER_DECLINED) {
|
|
263
|
+
return [{ question: "", answer: raw.trim(), declined: true }];
|
|
264
|
+
}
|
|
265
|
+
if (questions.length <= 1) {
|
|
266
|
+
return [{ question: questions[0] ?? "", answer: raw.trim() }];
|
|
267
|
+
}
|
|
268
|
+
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
269
|
+
return questions.map((question, i) => {
|
|
270
|
+
const line = lines[i] ?? "";
|
|
271
|
+
const sep = line.indexOf(" → ");
|
|
272
|
+
return { question, answer: sep === -1 ? line.trim() : line.slice(sep + 3).trim() };
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function buildReceipt(call) {
|
|
276
|
+
const wrap = document.createElement("div");
|
|
277
|
+
const rows = receiptRows(call);
|
|
278
|
+
if (rows.length === 0) return wrap;
|
|
279
|
+
wrap.className = "aparte-question-receipt__group";
|
|
280
|
+
for (const row of rows) {
|
|
281
|
+
const card = document.createElement("div");
|
|
282
|
+
card.className = "aparte-segment aparte-tag aparte-question-receipt aparte-mark" + (row.declined ? " aparte-mark--quiet aparte-question-receipt--declined" : " aparte-mark--success");
|
|
283
|
+
if (!row.declined) {
|
|
284
|
+
const q = document.createElement("span");
|
|
285
|
+
q.className = "aparte-tag__label aparte-question-receipt__question";
|
|
286
|
+
q.textContent = row.question;
|
|
287
|
+
const sep = document.createElement("span");
|
|
288
|
+
sep.className = "aparte-question-receipt__sep";
|
|
289
|
+
sep.textContent = "→";
|
|
290
|
+
card.append(q, sep);
|
|
291
|
+
}
|
|
292
|
+
const a = document.createElement("span");
|
|
293
|
+
a.className = "aparte-tag__label " + (row.declined ? "aparte-question-receipt__answer--declined" : "aparte-question-receipt__answer");
|
|
294
|
+
a.textContent = row.answer;
|
|
295
|
+
card.appendChild(a);
|
|
296
|
+
wrap.appendChild(card);
|
|
297
|
+
}
|
|
298
|
+
return wrap;
|
|
299
|
+
}
|
|
222
300
|
export {
|
|
223
301
|
ASK_USER_DECLINED as A,
|
|
224
302
|
askUserHandler as a,
|
|
225
|
-
|
|
303
|
+
buildReceipt as b,
|
|
304
|
+
createAskUserTool as c,
|
|
305
|
+
receiptRows as r
|
|
226
306
|
};
|
|
227
|
-
//# sourceMappingURL=
|
|
307
|
+
//# sourceMappingURL=receipt-DUj-W-cI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receipt-DUj-W-cI.js","sources":["../src/ask-user.ts","../src/receipt.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 * `false` means NO system message at all — for a product whose model is trained on\n * a fixed contract and must not read any added prose. It is a distinct value rather\n * than `''`, because an empty string is still a field, and since 0.13 the field is\n * really sent as a system message.\n */\n systemPrompt?: string | false;\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 // Omitted, not emptied, when the host opts out: the loop sends whatever string\n // the field holds as a system message, and `''` is a string.\n const systemPrompt = options.systemPrompt === false ? undefined : 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 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 === undefined ? {} : { systemPrompt }),\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 {\n toolCallId: call.id,\n content: formatAnswer(result.content, labels),\n // The same answer as a value — MCP's `action` beside the content, one entry\n // per question, a `string[]` for a multiple choice — so the receipt reads it\n // instead of re-parsing the sentence, and a host does not write a converter.\n structuredContent: { action: 'accept', answers: structuredAnswers(result.content, message, labels) },\n };\n }\n return { toolCallId: call.id, content: ASK_USER_DECLINED, structuredContent: { action: 'decline' } };\n};\n\n/** One `{ question, value }` per question — `value` is a `string[]` for a multiple choice. */\nexport interface AskUserAnswer {\n question: string;\n value: string | string[];\n}\n\n/** The structured twin of `ask_user`'s prose result. */\nexport type AskUserStructuredResult =\n | { action: 'accept'; answers: AskUserAnswer[] }\n | { action: 'decline' };\n\nfunction structuredAnswers(content: unknown, question: string, labels: Record<string, string>): AskUserAnswer[] {\n const value = (v: unknown): string | string[] => (Array.isArray(v) ? v.map(String) : String(v ?? ''));\n if (content && typeof content === 'object' && !Array.isArray(content)) {\n return Object.entries(content as Record<string, unknown>).map(([k, v]) => ({ question: labels[k] ?? k, value: value(v) }));\n }\n return [{ question, value: value(content) }];\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","/**\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/**\n * One line of the record.\n *\n * Exported because `receiptRows()` returns them and a consumer writing their own\n * renderer has to name the type. It was package-private, so the declaration emit put an\n * un-exported `interface ReceiptRow` in the `.d.ts` — reachable by inference, nameable\n * by nobody.\n */\nexport interface 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 /** The handler's structured twin of `result`, read first when it is there. */\n structuredResult?: unknown;\n}\n\n/** The structure `askUserHandler` attaches, if `value` is one — a result from elsewhere is not. */\nfunction structuredRows(value: unknown): ReceiptRow[] | null {\n const s = value as { action?: unknown; answers?: unknown } | null;\n if (!s || typeof s !== 'object') return null;\n if (s.action === 'decline') return [{ question: '', answer: ASK_USER_DECLINED, declined: true }];\n if (s.action !== 'accept' || !Array.isArray(s.answers)) return null;\n return s.answers.map((a) => {\n const row = (a ?? {}) as { question?: unknown; value?: unknown };\n const v = row.value;\n return { question: String(row.question ?? ''), answer: Array.isArray(v) ? v.map(String).join(', ') : String(v ?? '') };\n });\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 // The value, when the handler attached one: nothing to parse, and an arrow typed\n // by the user cannot confuse it. The prose path stays for a result from elsewhere.\n const structured = structuredRows(call.structuredResult);\n if (structured) return structured;\n\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"],"names":[],"mappings":"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"}
|
package/dist/receipt.d.ts
CHANGED
|
@@ -12,8 +12,15 @@
|
|
|
12
12
|
* arm's first natural line is a model-to-DOM XSS in the host page's origin.
|
|
13
13
|
*/
|
|
14
14
|
import type { AparteToolCall } from '@aparte/core';
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* One line of the record.
|
|
17
|
+
*
|
|
18
|
+
* Exported because `receiptRows()` returns them and a consumer writing their own
|
|
19
|
+
* renderer has to name the type. It was package-private, so the declaration emit put an
|
|
20
|
+
* un-exported `interface ReceiptRow` in the `.d.ts` — reachable by inference, nameable
|
|
21
|
+
* by nobody.
|
|
22
|
+
*/
|
|
23
|
+
export interface ReceiptRow {
|
|
17
24
|
question: string;
|
|
18
25
|
answer: string;
|
|
19
26
|
/**
|
|
@@ -39,6 +46,8 @@ interface ReceiptRow {
|
|
|
39
46
|
export interface ReceiptSource {
|
|
40
47
|
input: AparteToolCall['input'];
|
|
41
48
|
result?: string | undefined;
|
|
49
|
+
/** The handler's structured twin of `result`, read first when it is there. */
|
|
50
|
+
structuredResult?: unknown;
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Pair each question with its answer.
|
|
@@ -57,5 +66,4 @@ export declare function receiptRows(call: ReceiptSource): ReceiptRow[];
|
|
|
57
66
|
* read the same pending question. Once answered, this is the only record.
|
|
58
67
|
*/
|
|
59
68
|
export declare function buildReceipt(call: ReceiptSource): HTMLElement;
|
|
60
|
-
export {};
|
|
61
69
|
//# sourceMappingURL=receipt.d.ts.map
|
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
|
|
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;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACvB,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;IAC5B,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC9B;AA0BD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,EAAE,CA+B7D;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.16.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.16.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.16.0"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"aparte",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ask-user-CyQ5S6Wy.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 * `false` means NO system message at all — for a product whose model is trained on\n * a fixed contract and must not read any added prose. It is a distinct value rather\n * than `''`, because an empty string is still a field, and since 0.13 the field is\n * really sent as a system message.\n */\n systemPrompt?: string | false;\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 // Omitted, not emptied, when the host opts out: the loop sends whatever string\n // the field holds as a system message, and `''` is a string.\n const systemPrompt = options.systemPrompt === false ? undefined : 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 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 === undefined ? {} : { systemPrompt }),\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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"}
|