@aparte/plugin-ask-user 0.12.0 → 0.13.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.
@@ -0,0 +1,500 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "readme": "",
4
+ "modules": [
5
+ {
6
+ "kind": "javascript-module",
7
+ "path": "src/aparte-ask-user.ts",
8
+ "declarations": [
9
+ {
10
+ "kind": "class",
11
+ "description": "A semantic alias of core's `<aparte-elicitation>`: same presenter, intent-revealing\ntag. `ask_user` runs on the core elicitation primitive and has no bespoke component,\nso this subclass adds no behaviour — it exists so markup can say what it means. The\ntwo are interchangeable, and neither dispatches anything.",
12
+ "name": "AparteAskUser",
13
+ "superclass": {
14
+ "name": "AparteElicitation",
15
+ "package": "@aparte/core"
16
+ },
17
+ "tagName": "aparte-ask-user",
18
+ "customElement": true,
19
+ "examples": [
20
+ "<!-- 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>"
21
+ ]
22
+ }
23
+ ],
24
+ "exports": [
25
+ {
26
+ "kind": "js",
27
+ "name": "AparteAskUser",
28
+ "declaration": {
29
+ "name": "AparteAskUser",
30
+ "module": "src/aparte-ask-user.ts"
31
+ }
32
+ },
33
+ {
34
+ "kind": "custom-element-definition",
35
+ "name": "aparte-ask-user",
36
+ "declaration": {
37
+ "name": "AparteAskUser",
38
+ "module": "src/aparte-ask-user.ts"
39
+ }
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "kind": "javascript-module",
45
+ "path": "src/ask-user.ts",
46
+ "declarations": [
47
+ {
48
+ "kind": "function",
49
+ "name": "createAskUserTool",
50
+ "return": {
51
+ "type": {
52
+ "text": "AparteTool"
53
+ }
54
+ },
55
+ "parameters": [
56
+ {
57
+ "name": "options",
58
+ "default": "{}",
59
+ "type": {
60
+ "text": "AskUserToolOptions"
61
+ }
62
+ }
63
+ ],
64
+ "description": "Build the `ask_user` tool.\n\nA factory rather than a constant because the schema carries BOUNDS, and bounds are\nthe host's to set: this is the same reasoning that moved the free-text escape out\nof the model's schema and onto the config. The defaults are what every serious\nimplementation of this pattern lands near (four options, a handful of questions),\nso `createAskUserTool()` with no argument is the normal call."
65
+ },
66
+ {
67
+ "kind": "variable",
68
+ "name": "ASK_USER_DECLINED",
69
+ "type": {
70
+ "text": "string"
71
+ },
72
+ "default": "'The user declined to answer.'",
73
+ "description": "What the model is told when the user declines.\n\nA constant rather than a literal because the RECEIPT has to recognise it: the\ntranscript must show \"declined\" instead of pairing this sentence with the first\nquestion as though the user had said it. Matching English at a distance would have\nbeen the alternative, and it breaks the moment this string is localised."
74
+ },
75
+ {
76
+ "kind": "function",
77
+ "name": "askUserHandler",
78
+ "return": {
79
+ "type": {
80
+ "text": "Promise<AparteToolResult>"
81
+ }
82
+ },
83
+ "parameters": [
84
+ {
85
+ "name": "call"
86
+ },
87
+ {
88
+ "name": "signal"
89
+ },
90
+ {
91
+ "name": "context"
92
+ }
93
+ ]
94
+ }
95
+ ],
96
+ "exports": [
97
+ {
98
+ "kind": "js",
99
+ "name": "createAskUserTool",
100
+ "declaration": {
101
+ "name": "createAskUserTool",
102
+ "module": "src/ask-user.ts"
103
+ }
104
+ },
105
+ {
106
+ "kind": "js",
107
+ "name": "ASK_USER_DECLINED",
108
+ "declaration": {
109
+ "name": "ASK_USER_DECLINED",
110
+ "module": "src/ask-user.ts"
111
+ }
112
+ },
113
+ {
114
+ "kind": "js",
115
+ "name": "askUserHandler",
116
+ "declaration": {
117
+ "name": "askUserHandler",
118
+ "module": "src/ask-user.ts"
119
+ }
120
+ }
121
+ ]
122
+ },
123
+ {
124
+ "kind": "javascript-module",
125
+ "path": "src/index.node.ts",
126
+ "declarations": [
127
+ {
128
+ "kind": "function",
129
+ "name": "setupAskUser",
130
+ "return": {
131
+ "type": {
132
+ "text": "void"
133
+ }
134
+ },
135
+ "parameters": [
136
+ {
137
+ "name": "config",
138
+ "default": "aparteGlobalConfig",
139
+ "type": {
140
+ "text": "AparteConfig"
141
+ }
142
+ },
143
+ {
144
+ "name": "options",
145
+ "default": "{}",
146
+ "type": {
147
+ "text": "AskUserToolOptions"
148
+ }
149
+ }
150
+ ],
151
+ "description": "Register the `ask_user` tool + handler on the server.\n\nNo receipt renderer here: it builds DOM, and this entry exists precisely so an SSR\nbuild can import the package without a document. The browser entry registers it."
152
+ }
153
+ ],
154
+ "exports": [
155
+ {
156
+ "kind": "js",
157
+ "name": "setupAskUser",
158
+ "declaration": {
159
+ "name": "setupAskUser",
160
+ "module": "src/index.node.ts"
161
+ }
162
+ },
163
+ {
164
+ "kind": "js",
165
+ "name": "createAskUserTool",
166
+ "declaration": {
167
+ "name": "createAskUserTool",
168
+ "module": "./ask-user.js"
169
+ }
170
+ },
171
+ {
172
+ "kind": "js",
173
+ "name": "askUserHandler",
174
+ "declaration": {
175
+ "name": "askUserHandler",
176
+ "module": "./ask-user.js"
177
+ }
178
+ },
179
+ {
180
+ "kind": "js",
181
+ "name": "AskUserToolOptions",
182
+ "declaration": {
183
+ "name": "AskUserToolOptions",
184
+ "module": "./ask-user.js"
185
+ }
186
+ },
187
+ {
188
+ "kind": "js",
189
+ "name": "AskUserOption",
190
+ "declaration": {
191
+ "name": "AskUserOption",
192
+ "module": "./ask-user.js"
193
+ }
194
+ },
195
+ {
196
+ "kind": "js",
197
+ "name": "AskUserItem",
198
+ "declaration": {
199
+ "name": "AskUserItem",
200
+ "module": "./ask-user.js"
201
+ }
202
+ },
203
+ {
204
+ "kind": "js",
205
+ "name": "AskUserDetail",
206
+ "declaration": {
207
+ "name": "AskUserDetail",
208
+ "module": "./ask-user.js"
209
+ }
210
+ },
211
+ {
212
+ "kind": "js",
213
+ "name": "QuestionReceiptSegment",
214
+ "declaration": {
215
+ "name": "QuestionReceiptSegment",
216
+ "module": "./question-receipt.renderer.js"
217
+ }
218
+ },
219
+ {
220
+ "kind": "js",
221
+ "name": "AparteTool",
222
+ "declaration": {
223
+ "name": "AparteTool",
224
+ "package": "@aparte/core"
225
+ }
226
+ },
227
+ {
228
+ "kind": "js",
229
+ "name": "AparteToolHandler",
230
+ "declaration": {
231
+ "name": "AparteToolHandler",
232
+ "package": "@aparte/core"
233
+ }
234
+ },
235
+ {
236
+ "kind": "js",
237
+ "name": "AparteToolCall",
238
+ "declaration": {
239
+ "name": "AparteToolCall",
240
+ "package": "@aparte/core"
241
+ }
242
+ },
243
+ {
244
+ "kind": "js",
245
+ "name": "AparteToolResult",
246
+ "declaration": {
247
+ "name": "AparteToolResult",
248
+ "package": "@aparte/core"
249
+ }
250
+ }
251
+ ]
252
+ },
253
+ {
254
+ "kind": "javascript-module",
255
+ "path": "src/index.ts",
256
+ "declarations": [
257
+ {
258
+ "kind": "function",
259
+ "name": "setupAskUser",
260
+ "return": {
261
+ "type": {
262
+ "text": "void"
263
+ }
264
+ },
265
+ "parameters": [
266
+ {
267
+ "name": "config",
268
+ "default": "aparteGlobalConfig",
269
+ "type": {
270
+ "text": "AparteConfig"
271
+ }
272
+ },
273
+ {
274
+ "name": "options",
275
+ "default": "{}",
276
+ "type": {
277
+ "text": "AskUserToolOptions"
278
+ }
279
+ }
280
+ ],
281
+ "description": "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).\nExplicit setup — rather than a top-level import side-effect — keeps the\naparteGlobalConfig singleton mutation predictable in SSR/test and tree-shaking\nfriendly. Call once at application startup."
282
+ }
283
+ ],
284
+ "exports": [
285
+ {
286
+ "kind": "js",
287
+ "name": "setupAskUser",
288
+ "declaration": {
289
+ "name": "setupAskUser",
290
+ "module": "src/index.ts"
291
+ }
292
+ },
293
+ {
294
+ "kind": "js",
295
+ "name": "createAskUserTool",
296
+ "declaration": {
297
+ "name": "createAskUserTool",
298
+ "module": "./ask-user.js"
299
+ }
300
+ },
301
+ {
302
+ "kind": "js",
303
+ "name": "askUserHandler",
304
+ "declaration": {
305
+ "name": "askUserHandler",
306
+ "module": "./ask-user.js"
307
+ }
308
+ },
309
+ {
310
+ "kind": "js",
311
+ "name": "AskUserToolOptions",
312
+ "declaration": {
313
+ "name": "AskUserToolOptions",
314
+ "module": "./ask-user.js"
315
+ }
316
+ },
317
+ {
318
+ "kind": "js",
319
+ "name": "AskUserOption",
320
+ "declaration": {
321
+ "name": "AskUserOption",
322
+ "module": "./ask-user.js"
323
+ }
324
+ },
325
+ {
326
+ "kind": "js",
327
+ "name": "AskUserItem",
328
+ "declaration": {
329
+ "name": "AskUserItem",
330
+ "module": "./ask-user.js"
331
+ }
332
+ },
333
+ {
334
+ "kind": "js",
335
+ "name": "AskUserDetail",
336
+ "declaration": {
337
+ "name": "AskUserDetail",
338
+ "module": "./ask-user.js"
339
+ }
340
+ },
341
+ {
342
+ "kind": "js",
343
+ "name": "AparteAskUser",
344
+ "declaration": {
345
+ "name": "AparteAskUser",
346
+ "module": "./aparte-ask-user.js"
347
+ }
348
+ },
349
+ {
350
+ "kind": "js",
351
+ "name": "questionReceiptRenderer",
352
+ "declaration": {
353
+ "name": "questionReceiptRenderer",
354
+ "module": "./question-receipt.renderer.js"
355
+ }
356
+ },
357
+ {
358
+ "kind": "js",
359
+ "name": "buildReceipt",
360
+ "declaration": {
361
+ "name": "buildReceipt",
362
+ "module": "./receipt.js"
363
+ }
364
+ },
365
+ {
366
+ "kind": "js",
367
+ "name": "receiptRows",
368
+ "declaration": {
369
+ "name": "receiptRows",
370
+ "module": "./receipt.js"
371
+ }
372
+ },
373
+ {
374
+ "kind": "js",
375
+ "name": "QuestionReceiptSegment",
376
+ "declaration": {
377
+ "name": "QuestionReceiptSegment",
378
+ "module": "./question-receipt.renderer.js"
379
+ }
380
+ },
381
+ {
382
+ "kind": "js",
383
+ "name": "AparteTool",
384
+ "declaration": {
385
+ "name": "AparteTool",
386
+ "package": "@aparte/core"
387
+ }
388
+ },
389
+ {
390
+ "kind": "js",
391
+ "name": "AparteToolHandler",
392
+ "declaration": {
393
+ "name": "AparteToolHandler",
394
+ "package": "@aparte/core"
395
+ }
396
+ },
397
+ {
398
+ "kind": "js",
399
+ "name": "AparteToolCall",
400
+ "declaration": {
401
+ "name": "AparteToolCall",
402
+ "package": "@aparte/core"
403
+ }
404
+ },
405
+ {
406
+ "kind": "js",
407
+ "name": "AparteToolResult",
408
+ "declaration": {
409
+ "name": "AparteToolResult",
410
+ "package": "@aparte/core"
411
+ }
412
+ }
413
+ ]
414
+ },
415
+ {
416
+ "kind": "javascript-module",
417
+ "path": "src/question-receipt.renderer.ts",
418
+ "declarations": [
419
+ {
420
+ "kind": "variable",
421
+ "name": "questionReceiptRenderer",
422
+ "type": {
423
+ "text": "AparteSegmentRenderer<QuestionReceiptSegment>"
424
+ },
425
+ "default": "{ type: 'question-receipt', render(seg) { return `<div class=\"aparte-segment aparte-tag aparte-question-receipt\" data-segment-id=\"${esc(seg.id)}\"> <span class=\"aparte-tag__label aparte-question-receipt__question\">${esc(seg.question)}</span> <span class=\"aparte-question-receipt__sep\">→</span> <span class=\"aparte-tag__label aparte-question-receipt__answer\">${esc(seg.answer)}</span> </div>`; }, update(el, seg) { const q = el.querySelector('.aparte-question-receipt__question'); if (q) q.textContent = seg.question; const a = el.querySelector('.aparte-question-receipt__answer'); if (a) a.textContent = seg.answer; }, getStyles() { return ` /* ── Question Receipt Card ──────────────────────────────────────────────── */ /* Several questions leave several cards, stacked. The tool renderer builds this group; the card below is shared with a question-receipt segment an app emits itself. (No backticks in here: this whole block is a template literal.) THE CARD IS AN .aparte-tag. It is a pill holding a truncating label, which is what that recipe is, and it used to redeclare the whole thing: inline-flex, gap, padding, border, radius, surface background, max-width — nine lines that core already owned. What is left below is only what a tag has no opinion about (the entrance, and the share of the width each half gets) plus this card's own measures, expressed as the tag's tokens so they land ON the recipe rather than beside it. This is also the only place in the repo where a PLUGIN reaches core's recipes, and that is the point: the recipes are plain classes on a stylesheet core already ships, so a plugin needs no import, no client, and no build step to use them. A capability that only core itself can reach would not be one. */ /* A declined request: the outcome, in the muted voice of something that did not happen — not the green of an answer given. */ .aparte-question-receipt__answer--declined { color: var(--aparte-text-muted); font-style: italic; } .aparte-question-receipt__group { display: flex; flex-direction: column; align-items: flex-start; gap: var(--aparte-space-2); } .aparte-question-receipt { --aparte-tag-gap: var(--aparte-space-4); --aparte-tag-padding: var(--aparte-space-3) var(--aparte-space-6); --aparte-tag-radius: var(--aparte-radius-full); --aparte-tag-font-size: var(--aparte-font-size-md); overflow: hidden; animation: aparte-question-receipt-appear var(--aparte-duration-slow) ease-out both; } /* The question yields the room, the answer keeps it: an answer cut in half is a receipt that records nothing, and the question is usually the re-readable half. */ .aparte-question-receipt__question { color: var(--aparte-text-muted); flex-shrink: 1; min-width: 0; } .aparte-question-receipt__sep { color: var(--aparte-text-muted); opacity: 0.4; flex-shrink: 0; } .aparte-question-receipt__answer { color: var(--aparte-success); font-weight: var(--aparte-font-weight-semibold); flex-shrink: 0; max-width: 55%; } @keyframes aparte-question-receipt-appear { from { opacity: 0; transform: translateY(var(--aparte-space-2)); } to { opacity: 1; transform: translateY(0); } } `; }, }"
426
+ }
427
+ ],
428
+ "exports": [
429
+ {
430
+ "kind": "js",
431
+ "name": "questionReceiptRenderer",
432
+ "declaration": {
433
+ "name": "questionReceiptRenderer",
434
+ "module": "src/question-receipt.renderer.ts"
435
+ }
436
+ }
437
+ ]
438
+ },
439
+ {
440
+ "kind": "javascript-module",
441
+ "path": "src/receipt.ts",
442
+ "declarations": [
443
+ {
444
+ "kind": "function",
445
+ "name": "receiptRows",
446
+ "return": {
447
+ "type": {
448
+ "text": "ReceiptRow[]"
449
+ }
450
+ },
451
+ "parameters": [
452
+ {
453
+ "name": "call",
454
+ "type": {
455
+ "text": "ReceiptSource"
456
+ }
457
+ }
458
+ ],
459
+ "description": "Pair each question with its answer.\n\nThe handler formats a multi-question result as `question → answer` per line, and a\nsingle answer as itself. The questions come from the tool INPUT rather than from\nthat string, because the input is authoritative — an answer a user typed can\ncontain anything, including an arrow."
460
+ },
461
+ {
462
+ "kind": "function",
463
+ "name": "buildReceipt",
464
+ "return": {
465
+ "type": {
466
+ "text": "HTMLElement"
467
+ }
468
+ },
469
+ "parameters": [
470
+ {
471
+ "name": "call",
472
+ "type": {
473
+ "text": "ReceiptSource"
474
+ }
475
+ }
476
+ ],
477
+ "description": "The card the transcript shows: one `question → answer` row per question.\n\nReturns an empty element while the call has no result yet — the live UI is the\npanel in the composer, and a duplicate of it in the bubble would be two places to\nread the same pending question. Once answered, this is the only record."
478
+ }
479
+ ],
480
+ "exports": [
481
+ {
482
+ "kind": "js",
483
+ "name": "receiptRows",
484
+ "declaration": {
485
+ "name": "receiptRows",
486
+ "module": "src/receipt.ts"
487
+ }
488
+ },
489
+ {
490
+ "kind": "js",
491
+ "name": "buildReceipt",
492
+ "declaration": {
493
+ "name": "buildReceipt",
494
+ "module": "src/receipt.ts"
495
+ }
496
+ }
497
+ ]
498
+ }
499
+ ]
500
+ }
package/dist/index.js CHANGED
@@ -4,16 +4,16 @@ const esc = escapeAttr;
4
4
  const questionReceiptRenderer = {
5
5
  type: "question-receipt",
6
6
  render(seg) {
7
- return `<div class="aparte-segment seg-qreceipt" data-segment-id="${esc(seg.id)}">
8
- <span class="qr-question">${esc(seg.question)}</span>
9
- <span class="qr-sep">→</span>
10
- <span class="qr-answer">${esc(seg.answer)}</span>
7
+ return `<div class="aparte-segment aparte-tag aparte-question-receipt" data-segment-id="${esc(seg.id)}">
8
+ <span class="aparte-tag__label aparte-question-receipt__question">${esc(seg.question)}</span>
9
+ <span class="aparte-question-receipt__sep">→</span>
10
+ <span class="aparte-tag__label aparte-question-receipt__answer">${esc(seg.answer)}</span>
11
11
  </div>`;
12
12
  },
13
13
  update(el, seg) {
14
- const q = el.querySelector(".qr-question");
14
+ const q = el.querySelector(".aparte-question-receipt__question");
15
15
  if (q) q.textContent = seg.question;
16
- const a = el.querySelector(".qr-answer");
16
+ const a = el.querySelector(".aparte-question-receipt__answer");
17
17
  if (a) a.textContent = seg.answer;
18
18
  },
19
19
  getStyles() {
@@ -21,56 +21,59 @@ const questionReceiptRenderer = {
21
21
  /* ── Question Receipt Card ──────────────────────────────────────────────── */
22
22
  /* Several questions leave several cards, stacked. The tool renderer builds this
23
23
  group; the card below is shared with a question-receipt segment an app emits
24
- itself. (No backticks in here: this whole block is a template literal.) */
24
+ itself. (No backticks in here: this whole block is a template literal.)
25
+
26
+ THE CARD IS AN .aparte-tag. It is a pill holding a truncating label, which is what
27
+ that recipe is, and it used to redeclare the whole thing: inline-flex, gap, padding,
28
+ border, radius, surface background, max-width — nine lines that core already owned.
29
+ What is left below is only what a tag has no opinion about (the entrance, and the
30
+ share of the width each half gets) plus this card's own measures, expressed as the
31
+ tag's tokens so they land ON the recipe rather than beside it.
32
+
33
+ This is also the only place in the repo where a PLUGIN reaches core's recipes, and
34
+ that is the point: the recipes are plain classes on a stylesheet core already ships,
35
+ so a plugin needs no import, no client, and no build step to use them. A capability
36
+ that only core itself can reach would not be one. */
25
37
  /* A declined request: the outcome, in the muted voice of something that did not
26
38
  happen — not the green of an answer given. */
27
- .qr-declined {
39
+ .aparte-question-receipt__answer--declined {
28
40
  color: var(--aparte-text-muted);
29
41
  font-style: italic;
30
42
  }
31
- .seg-qreceipt-group {
43
+ .aparte-question-receipt__group {
32
44
  display: flex;
33
45
  flex-direction: column;
34
46
  align-items: flex-start;
35
- gap: 4px;
47
+ gap: var(--aparte-space-2);
36
48
  }
37
- .seg-qreceipt {
38
- display: inline-flex;
39
- align-items: center;
40
- gap: 8px;
41
- padding: 6px 12px;
42
- border-radius: var(--aparte-radius-full);
43
- background: var(--aparte-surface-2);
44
- border: 1px solid var(--aparte-border);
45
- font-size: 0.8rem;
46
- max-width: 100%;
49
+ .aparte-question-receipt {
50
+ --aparte-tag-gap: var(--aparte-space-4);
51
+ --aparte-tag-padding: var(--aparte-space-3) var(--aparte-space-6);
52
+ --aparte-tag-radius: var(--aparte-radius-full);
53
+ --aparte-tag-font-size: var(--aparte-font-size-md);
47
54
  overflow: hidden;
48
- animation: qr-appear 0.2s ease-out both;
55
+ animation: aparte-question-receipt-appear var(--aparte-duration-slow) ease-out both;
49
56
  }
50
- .qr-question {
57
+ /* The question yields the room, the answer keeps it: an answer cut in half is a
58
+ receipt that records nothing, and the question is usually the re-readable half. */
59
+ .aparte-question-receipt__question {
51
60
  color: var(--aparte-text-muted);
52
- white-space: nowrap;
53
- overflow: hidden;
54
- text-overflow: ellipsis;
55
61
  flex-shrink: 1;
56
62
  min-width: 0;
57
63
  }
58
- .qr-sep {
64
+ .aparte-question-receipt__sep {
59
65
  color: var(--aparte-text-muted);
60
66
  opacity: 0.4;
61
67
  flex-shrink: 0;
62
68
  }
63
- .qr-answer {
69
+ .aparte-question-receipt__answer {
64
70
  color: var(--aparte-success);
65
- font-weight: 600;
66
- white-space: nowrap;
67
- overflow: hidden;
68
- text-overflow: ellipsis;
71
+ font-weight: var(--aparte-font-weight-semibold);
69
72
  flex-shrink: 0;
70
73
  max-width: 55%;
71
74
  }
72
- @keyframes qr-appear {
73
- from { opacity: 0; transform: translateY(4px); }
75
+ @keyframes aparte-question-receipt-appear {
76
+ from { opacity: 0; transform: translateY(var(--aparte-space-2)); }
74
77
  to { opacity: 1; transform: translateY(0); }
75
78
  }
76
79
  `;
@@ -106,21 +109,21 @@ function buildReceipt(call) {
106
109
  const wrap = document.createElement("div");
107
110
  const rows = receiptRows(call);
108
111
  if (rows.length === 0) return wrap;
109
- wrap.className = "seg-qreceipt-group";
112
+ wrap.className = "aparte-question-receipt__group";
110
113
  for (const row of rows) {
111
114
  const card = document.createElement("div");
112
- card.className = "segment seg-qreceipt" + (row.declined ? " seg-qreceipt--declined" : "");
115
+ card.className = "aparte-segment aparte-tag aparte-question-receipt" + (row.declined ? " aparte-question-receipt--declined" : "");
113
116
  if (!row.declined) {
114
117
  const q = document.createElement("span");
115
- q.className = "qr-question";
118
+ q.className = "aparte-tag__label aparte-question-receipt__question";
116
119
  q.textContent = row.question;
117
120
  const sep = document.createElement("span");
118
- sep.className = "qr-sep";
121
+ sep.className = "aparte-question-receipt__sep";
119
122
  sep.textContent = "→";
120
123
  card.append(q, sep);
121
124
  }
122
125
  const a = document.createElement("span");
123
- a.className = row.declined ? "qr-declined" : "qr-answer";
126
+ a.className = "aparte-tag__label " + (row.declined ? "aparte-question-receipt__answer--declined" : "aparte-question-receipt__answer");
124
127
  a.textContent = row.answer;
125
128
  card.appendChild(a);
126
129
  wrap.appendChild(card);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/question-receipt.renderer.ts","../src/receipt.ts","../src/aparte-ask-user.ts","../src/index.ts"],"sourcesContent":["import type { AparteSegmentRenderer } from '@aparte/core';\nimport { escapeAttr } from '@aparte/core';\n\nexport interface QuestionReceiptSegment {\n id: string;\n type: 'question-receipt';\n isStreaming?: boolean;\n question: string;\n answer: string;\n}\n\n// Core owns the escaping; this alias keeps the call sites short. The inlined\n// copy that used to live here left the apostrophe through.\nconst esc = escapeAttr;\n\nexport const questionReceiptRenderer: AparteSegmentRenderer<QuestionReceiptSegment> = {\n type: 'question-receipt',\n\n render(seg) {\n return `<div class=\"aparte-segment seg-qreceipt\" data-segment-id=\"${esc(seg.id)}\">\n <span class=\"qr-question\">${esc(seg.question)}</span>\n <span class=\"qr-sep\">→</span>\n <span class=\"qr-answer\">${esc(seg.answer)}</span>\n</div>`;\n },\n\n update(el, seg) {\n const q = el.querySelector('.qr-question');\n if (q) q.textContent = seg.question;\n const a = el.querySelector('.qr-answer');\n if (a) a.textContent = seg.answer;\n },\n\n getStyles() {\n return `\n/* ── Question Receipt Card ──────────────────────────────────────────────── */\n/* Several questions leave several cards, stacked. The tool renderer builds this\n group; the card below is shared with a question-receipt segment an app emits\n itself. (No backticks in here: this whole block is a template literal.) */\n/* A declined request: the outcome, in the muted voice of something that did not\n happen — not the green of an answer given. */\n.qr-declined {\n color: var(--aparte-text-muted);\n font-style: italic;\n}\n.seg-qreceipt-group {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n}\n.seg-qreceipt {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border-radius: var(--aparte-radius-full);\n background: var(--aparte-surface-2);\n border: 1px solid var(--aparte-border);\n font-size: 0.8rem;\n max-width: 100%;\n overflow: hidden;\n animation: qr-appear 0.2s ease-out both;\n}\n.qr-question {\n color: var(--aparte-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex-shrink: 1;\n min-width: 0;\n}\n.qr-sep {\n color: var(--aparte-text-muted);\n opacity: 0.4;\n flex-shrink: 0;\n}\n.qr-answer {\n color: var(--aparte-success);\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex-shrink: 0;\n max-width: 55%;\n}\n@keyframes qr-appear {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n`;\n },\n};\n","/**\n * What the conversation keeps after a question has been asked.\n *\n * The panel lives in the composer, so once it is answered it is gone. Without a\n * record in the thread, scrolling back shows nothing: no question, no answer, no\n * sign the assistant ever asked. Every product that asks a structured question puts\n * the question and the chosen answer in the conversation, and this is that.\n *\n * Built as an **HTMLElement** rather than an HTML string, deliberately: everything\n * here is model-chosen (the questions) or user-typed (a free-text answer), and the\n * element arm of `AparteToolRenderer` has no innerHTML surface at all. The string\n * arm's first natural line is a model-to-DOM XSS in the host page's origin.\n */\n\nimport type { AparteToolCall } from '@aparte/core';\nimport { ASK_USER_DECLINED } from './ask-user.js';\n\n/** One line of the record. */\ninterface ReceiptRow {\n question: string;\n answer: string;\n /**\n * The user declined the whole request — so this row is the OUTCOME, not an\n * answer, and it has no question of its own.\n *\n * Without this the decline sentence was split as though it were the answer to the\n * first question, leaving the others blank: \"Quelle est ta couleur préférée ? →\n * The user declined to answer.\" next to an empty row. Reported from a real\n * session, and it is worse than useless — it attributes words to the user.\n */\n declined?: true;\n}\n\n/**\n * What the renderer is handed: the model's input, and the result IF the call has\n * settled.\n *\n * `result` lives on the tool-call SEGMENT, not on `AparteToolCall` — which has only\n * `{ id, name, input }`. `AparteToolRenderer`'s own JSDoc said\n * \"`segment.toolCall.result` is whatever the tool returned\", which does not compile;\n * corrected there too.\n */\nexport interface ReceiptSource {\n input: AparteToolCall['input'];\n result?: string | undefined;\n}\n\n/** The questions the model asked, in the order it asked them. */\nfunction questionsOf(input: unknown): string[] {\n const obj = (input ?? {}) as Record<string, unknown>;\n const list = obj['questions'];\n if (Array.isArray(list) && list.length > 0) {\n return list.map((q) => String((q as Record<string, unknown>)?.['question'] ?? '').trim());\n }\n const single = obj['question'];\n return typeof single === 'string' && single.trim() ? [single.trim()] : [];\n}\n\n/**\n * Pair each question with its answer.\n *\n * The handler formats a multi-question result as `question → answer` per line, and a\n * single answer as itself. The questions come from the tool INPUT rather than from\n * that string, because the input is authoritative — an answer a user typed can\n * contain anything, including an arrow.\n */\nexport function receiptRows(call: ReceiptSource): ReceiptRow[] {\n const questions = questionsOf(call.input);\n const raw = call.result ?? '';\n if (!raw.trim()) return [];\n\n // Declining is a whole-request outcome: ONE row, no question attached, and\n // certainly not this sentence pinned to the first question as if the user had\n // typed it. `Skip` declines everything by design (MCP's `decline`), including\n // questions already answered — which is exactly what made the old rendering a\n // lie rather than merely wrong.\n if (raw.trim() === ASK_USER_DECLINED) {\n return [{ question: '', answer: raw.trim(), declined: true }];\n }\n\n if (questions.length <= 1) {\n return [{ question: questions[0] ?? '', answer: raw.trim() }];\n }\n\n const lines = raw.split('\\n').filter((l) => l.trim() !== '');\n return questions.map((question, i) => {\n const line = lines[i] ?? '';\n const sep = line.indexOf(' → ');\n // Split on the FIRST arrow: the question is ours, the answer is the user's,\n // so anything arrow-like later in the line belongs to the answer.\n return { question, answer: sep === -1 ? line.trim() : line.slice(sep + 3).trim() };\n });\n}\n\n/**\n * The card the transcript shows: one `question → answer` row per question.\n *\n * Returns an empty element while the call has no result yet — the live UI is the\n * panel in the composer, and a duplicate of it in the bubble would be two places to\n * read the same pending question. Once answered, this is the only record.\n */\nexport function buildReceipt(call: ReceiptSource): HTMLElement {\n const wrap = document.createElement('div');\n const rows = receiptRows(call);\n if (rows.length === 0) return wrap;\n\n wrap.className = 'seg-qreceipt-group';\n for (const row of rows) {\n const card = document.createElement('div');\n card.className = 'segment seg-qreceipt' + (row.declined ? ' seg-qreceipt--declined' : '');\n\n // A declined request has no question → answer pair to show, so it gets neither\n // a question nor an arrow: one line saying what happened.\n if (!row.declined) {\n const q = document.createElement('span');\n q.className = 'qr-question';\n q.textContent = row.question;\n\n const sep = document.createElement('span');\n sep.className = 'qr-sep';\n sep.textContent = '→';\n\n card.append(q, sep);\n }\n\n const a = document.createElement('span');\n a.className = row.declined ? 'qr-declined' : 'qr-answer';\n a.textContent = row.answer;\n\n card.appendChild(a);\n wrap.appendChild(card);\n }\n return wrap;\n}\n","/**\n * <aparte-ask-user> — a semantic alias of core's <aparte-elicitation>.\n *\n * ask_user runs on the core elicitation primitive; it has no bespoke Web\n * Component. This thin subclass lets you mount the presenter with intent-revealing\n * markup (`<aparte-ask-user>`) instead of the generic `<aparte-elicitation>`.\n * The two are interchangeable.\n */\n\nimport { AparteElicitation } from '@aparte/core';\n\n/**\n * A semantic alias of core's `<aparte-elicitation>`: same presenter, intent-revealing\n * tag. `ask_user` runs on the core elicitation primitive and has no bespoke component,\n * so this subclass adds no behaviour — it exists so markup can say what it means. The\n * two are interchangeable, and neither dispatches anything.\n *\n * @element aparte-ask-user\n *\n * @example\n * <!-- Identical to <aparte-elicitation>; mount either one, never both. -->\n * <aparte-chat>\n * <aparte-chat-viewport></aparte-chat-viewport>\n * <aparte-ask-user></aparte-ask-user>\n * <aparte-composer></aparte-composer>\n * </aparte-chat>\n */\nexport class AparteAskUser extends AparteElicitation {}\n\nif (typeof customElements !== 'undefined' && !customElements.get('aparte-ask-user')) {\n customElements.define('aparte-ask-user', AparteAskUser);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'aparte-ask-user': AparteAskUser;\n }\n}\n","/**\n * @aparte/plugin-ask-user\n *\n * The built-in `ask_user` tool — a thin adapter over the core elicitation\n * primitive. The AI asks the user a structured choice; the handler forwards it to\n * `requestUserInput`, presented by `<aparte-elicitation>` (or the semantic\n * `<aparte-ask-user>` alias registered by importing this package).\n *\n * Usage:\n * import { setupAskUser } from '@aparte/plugin-ask-user';\n * setupAskUser(); // registers the tool + hides its bubble segment\n * // mount <aparte-elicitation> (or <aparte-ask-user>) in your chat\n */\n\nimport { aparteGlobalConfig, registerSegmentRenderer, type AparteConfig } from '@aparte/core';\nimport { createAskUserTool, askUserHandler, type AskUserToolOptions } from './ask-user.js';\nimport { questionReceiptRenderer } from './question-receipt.renderer.js';\nimport { buildReceipt } from './receipt.js';\n\n// Register the <aparte-ask-user> semantic alias (subclass of <aparte-elicitation>).\nimport './aparte-ask-user.js';\n\n/**\n * Register the `ask_user` tool + its handler, and hide its bubble segment\n * (it is a UI-only tool presented via the elicitation panel, not a tool pill).\n * Explicit setup — rather than a top-level import side-effect — keeps the\n * aparteGlobalConfig singleton mutation predictable in SSR/test and tree-shaking\n * friendly. Call once at application startup.\n */\nexport function setupAskUser(config: AparteConfig = aparteGlobalConfig, options: AskUserToolOptions = {}): void {\n // The bounds the schema puts on the model are the HOST's, so they arrive here\n // rather than being frozen into a constant. Defaults are the normal call.\n config.registerTool(createAskUserTool(options), askUserHandler);\n\n /*\n * The conversation keeps the record.\n *\n * This used to be `render: () => ''` — render nothing, \"it is a UI-only tool\" —\n * and the panel lives in the composer, so once it was answered the transcript\n * held no trace that the assistant had asked anything or that the user had\n * answered. Scroll back and the exchange is simply missing, which is not what a\n * conversation is for; every product that asks a structured question puts the\n * question and the chosen answer in the thread.\n *\n * The pieces were all here and wired to nothing: `questionReceiptRenderer` has\n * existed with its own markup, styles and eleven tests, exported and registered\n * by nobody, while the renderer that WOULD have shown something returned the\n * empty string. Another consequence of a surface no example ever ran.\n *\n * Registered too, so its `getStyles()` reaches the document and an app that\n * builds `question-receipt` segments of its own gets the same card.\n */\n registerSegmentRenderer(questionReceiptRenderer, config);\n config.registerToolRenderer('ask_user', {\n render: (segment) => buildReceipt({ input: segment.toolCall.input, result: segment.result }),\n });\n}\n\nexport { createAskUserTool, askUserHandler } from './ask-user.js';\nexport type { AskUserToolOptions } from './ask-user.js';\nexport type { AskUserOption, AskUserItem, AskUserDetail } from './ask-user.js';\n\nexport { AparteAskUser } from './aparte-ask-user.js';\n\nexport { questionReceiptRenderer } from './question-receipt.renderer.js';\nexport { buildReceipt, receiptRows } from './receipt.js';\nexport type { QuestionReceiptSegment } from './question-receipt.renderer.js';\n\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/question-receipt.renderer.ts","../src/receipt.ts","../src/aparte-ask-user.ts","../src/index.ts"],"sourcesContent":["import type { AparteSegmentRenderer } from '@aparte/core';\nimport { escapeAttr } from '@aparte/core';\n\nexport interface QuestionReceiptSegment {\n id: string;\n type: 'question-receipt';\n isStreaming?: boolean;\n question: string;\n answer: string;\n}\n\n// Core owns the escaping; this alias keeps the call sites short. The inlined\n// copy that used to live here left the apostrophe through.\nconst esc = escapeAttr;\n\nexport const questionReceiptRenderer: AparteSegmentRenderer<QuestionReceiptSegment> = {\n type: 'question-receipt',\n\n render(seg) {\n return `<div class=\"aparte-segment aparte-tag aparte-question-receipt\" data-segment-id=\"${esc(seg.id)}\">\n <span class=\"aparte-tag__label aparte-question-receipt__question\">${esc(seg.question)}</span>\n <span class=\"aparte-question-receipt__sep\">→</span>\n <span class=\"aparte-tag__label aparte-question-receipt__answer\">${esc(seg.answer)}</span>\n</div>`;\n },\n\n update(el, seg) {\n const q = el.querySelector('.aparte-question-receipt__question');\n if (q) q.textContent = seg.question;\n const a = el.querySelector('.aparte-question-receipt__answer');\n if (a) a.textContent = seg.answer;\n },\n\n getStyles() {\n return `\n/* ── Question Receipt Card ──────────────────────────────────────────────── */\n/* Several questions leave several cards, stacked. The tool renderer builds this\n group; the card below is shared with a question-receipt segment an app emits\n itself. (No backticks in here: this whole block is a template literal.)\n\n THE CARD IS AN .aparte-tag. It is a pill holding a truncating label, which is what\n that recipe is, and it used to redeclare the whole thing: inline-flex, gap, padding,\n border, radius, surface background, max-width — nine lines that core already owned.\n What is left below is only what a tag has no opinion about (the entrance, and the\n share of the width each half gets) plus this card's own measures, expressed as the\n tag's tokens so they land ON the recipe rather than beside it.\n\n This is also the only place in the repo where a PLUGIN reaches core's recipes, and\n that is the point: the recipes are plain classes on a stylesheet core already ships,\n so a plugin needs no import, no client, and no build step to use them. A capability\n that only core itself can reach would not be one. */\n/* A declined request: the outcome, in the muted voice of something that did not\n happen — not the green of an answer given. */\n.aparte-question-receipt__answer--declined {\n color: var(--aparte-text-muted);\n font-style: italic;\n}\n.aparte-question-receipt__group {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: var(--aparte-space-2);\n}\n.aparte-question-receipt {\n --aparte-tag-gap: var(--aparte-space-4);\n --aparte-tag-padding: var(--aparte-space-3) var(--aparte-space-6);\n --aparte-tag-radius: var(--aparte-radius-full);\n --aparte-tag-font-size: var(--aparte-font-size-md);\n overflow: hidden;\n animation: aparte-question-receipt-appear var(--aparte-duration-slow) ease-out both;\n}\n/* The question yields the room, the answer keeps it: an answer cut in half is a\n receipt that records nothing, and the question is usually the re-readable half. */\n.aparte-question-receipt__question {\n color: var(--aparte-text-muted);\n flex-shrink: 1;\n min-width: 0;\n}\n.aparte-question-receipt__sep {\n color: var(--aparte-text-muted);\n opacity: 0.4;\n flex-shrink: 0;\n}\n.aparte-question-receipt__answer {\n color: var(--aparte-success);\n font-weight: var(--aparte-font-weight-semibold);\n flex-shrink: 0;\n max-width: 55%;\n}\n@keyframes aparte-question-receipt-appear {\n from { opacity: 0; transform: translateY(var(--aparte-space-2)); }\n to { opacity: 1; transform: translateY(0); }\n}\n`;\n },\n};\n","/**\n * What the conversation keeps after a question has been asked.\n *\n * The panel lives in the composer, so once it is answered it is gone. Without a\n * record in the thread, scrolling back shows nothing: no question, no answer, no\n * sign the assistant ever asked. Every product that asks a structured question puts\n * the question and the chosen answer in the conversation, and this is that.\n *\n * Built as an **HTMLElement** rather than an HTML string, deliberately: everything\n * here is model-chosen (the questions) or user-typed (a free-text answer), and the\n * element arm of `AparteToolRenderer` has no innerHTML surface at all. The string\n * arm's first natural line is a model-to-DOM XSS in the host page's origin.\n */\n\nimport type { AparteToolCall } from '@aparte/core';\nimport { ASK_USER_DECLINED } from './ask-user.js';\n\n/** One line of the record. */\ninterface ReceiptRow {\n question: string;\n answer: string;\n /**\n * The user declined the whole request — so this row is the OUTCOME, not an\n * answer, and it has no question of its own.\n *\n * Without this the decline sentence was split as though it were the answer to the\n * first question, leaving the others blank: \"Quelle est ta couleur préférée ? →\n * The user declined to answer.\" next to an empty row. Reported from a real\n * session, and it is worse than useless — it attributes words to the user.\n */\n declined?: true;\n}\n\n/**\n * What the renderer is handed: the model's input, and the result IF the call has\n * settled.\n *\n * `result` lives on the tool-call SEGMENT, not on `AparteToolCall` — which has only\n * `{ id, name, input }`. `AparteToolRenderer`'s own JSDoc said\n * \"`segment.toolCall.result` is whatever the tool returned\", which does not compile;\n * corrected there too.\n */\nexport interface ReceiptSource {\n input: AparteToolCall['input'];\n result?: string | undefined;\n}\n\n/** The questions the model asked, in the order it asked them. */\nfunction questionsOf(input: unknown): string[] {\n const obj = (input ?? {}) as Record<string, unknown>;\n const list = obj['questions'];\n if (Array.isArray(list) && list.length > 0) {\n return list.map((q) => String((q as Record<string, unknown>)?.['question'] ?? '').trim());\n }\n const single = obj['question'];\n return typeof single === 'string' && single.trim() ? [single.trim()] : [];\n}\n\n/**\n * Pair each question with its answer.\n *\n * The handler formats a multi-question result as `question → answer` per line, and a\n * single answer as itself. The questions come from the tool INPUT rather than from\n * that string, because the input is authoritative — an answer a user typed can\n * contain anything, including an arrow.\n */\nexport function receiptRows(call: ReceiptSource): ReceiptRow[] {\n const questions = questionsOf(call.input);\n const raw = call.result ?? '';\n if (!raw.trim()) return [];\n\n // Declining is a whole-request outcome: ONE row, no question attached, and\n // certainly not this sentence pinned to the first question as if the user had\n // typed it. `Skip` declines everything by design (MCP's `decline`), including\n // questions already answered — which is exactly what made the old rendering a\n // lie rather than merely wrong.\n if (raw.trim() === ASK_USER_DECLINED) {\n return [{ question: '', answer: raw.trim(), declined: true }];\n }\n\n if (questions.length <= 1) {\n return [{ question: questions[0] ?? '', answer: raw.trim() }];\n }\n\n const lines = raw.split('\\n').filter((l) => l.trim() !== '');\n return questions.map((question, i) => {\n const line = lines[i] ?? '';\n const sep = line.indexOf(' → ');\n // Split on the FIRST arrow: the question is ours, the answer is the user's,\n // so anything arrow-like later in the line belongs to the answer.\n return { question, answer: sep === -1 ? line.trim() : line.slice(sep + 3).trim() };\n });\n}\n\n/**\n * The card the transcript shows: one `question → answer` row per question.\n *\n * Returns an empty element while the call has no result yet — the live UI is the\n * panel in the composer, and a duplicate of it in the bubble would be two places to\n * read the same pending question. Once answered, this is the only record.\n */\nexport function buildReceipt(call: ReceiptSource): HTMLElement {\n const wrap = document.createElement('div');\n const rows = receiptRows(call);\n if (rows.length === 0) return wrap;\n\n wrap.className = 'aparte-question-receipt__group';\n for (const row of rows) {\n const card = document.createElement('div');\n card.className = 'aparte-segment aparte-tag aparte-question-receipt' + (row.declined ? ' aparte-question-receipt--declined' : '');\n\n // A declined request has no question → answer pair to show, so it gets neither\n // a question nor an arrow: one line saying what happened.\n if (!row.declined) {\n const q = document.createElement('span');\n q.className = 'aparte-tag__label aparte-question-receipt__question';\n q.textContent = row.question;\n\n const sep = document.createElement('span');\n sep.className = 'aparte-question-receipt__sep';\n sep.textContent = '→';\n\n card.append(q, sep);\n }\n\n const a = document.createElement('span');\n a.className = 'aparte-tag__label ' + (row.declined ? 'aparte-question-receipt__answer--declined' : 'aparte-question-receipt__answer');\n a.textContent = row.answer;\n\n card.appendChild(a);\n wrap.appendChild(card);\n }\n return wrap;\n}\n","/**\n * <aparte-ask-user> — a semantic alias of core's <aparte-elicitation>.\n *\n * ask_user runs on the core elicitation primitive; it has no bespoke Web\n * Component. This thin subclass lets you mount the presenter with intent-revealing\n * markup (`<aparte-ask-user>`) instead of the generic `<aparte-elicitation>`.\n * The two are interchangeable.\n */\n\nimport { AparteElicitation } from '@aparte/core';\n\n/**\n * A semantic alias of core's `<aparte-elicitation>`: same presenter, intent-revealing\n * tag. `ask_user` runs on the core elicitation primitive and has no bespoke component,\n * so this subclass adds no behaviour — it exists so markup can say what it means. The\n * two are interchangeable, and neither dispatches anything.\n *\n * @element aparte-ask-user\n *\n * @example\n * <!-- Identical to <aparte-elicitation>; mount either one, never both. -->\n * <aparte-chat>\n * <aparte-chat-viewport></aparte-chat-viewport>\n * <aparte-ask-user></aparte-ask-user>\n * <aparte-composer></aparte-composer>\n * </aparte-chat>\n */\nexport class AparteAskUser extends AparteElicitation {}\n\nif (typeof customElements !== 'undefined' && !customElements.get('aparte-ask-user')) {\n customElements.define('aparte-ask-user', AparteAskUser);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'aparte-ask-user': AparteAskUser;\n }\n}\n","/**\n * @aparte/plugin-ask-user\n *\n * The built-in `ask_user` tool — a thin adapter over the core elicitation\n * primitive. The AI asks the user a structured choice; the handler forwards it to\n * `requestUserInput`, presented by `<aparte-elicitation>` (or the semantic\n * `<aparte-ask-user>` alias registered by importing this package).\n *\n * Usage:\n * import { setupAskUser } from '@aparte/plugin-ask-user';\n * setupAskUser(); // registers the tool + hides its bubble segment\n * // mount <aparte-elicitation> (or <aparte-ask-user>) in your chat\n */\n\nimport { aparteGlobalConfig, registerSegmentRenderer, type AparteConfig } from '@aparte/core';\nimport { createAskUserTool, askUserHandler, type AskUserToolOptions } from './ask-user.js';\nimport { questionReceiptRenderer } from './question-receipt.renderer.js';\nimport { buildReceipt } from './receipt.js';\n\n// Register the <aparte-ask-user> semantic alias (subclass of <aparte-elicitation>).\nimport './aparte-ask-user.js';\n\n/**\n * Register the `ask_user` tool + its handler, and hide its bubble segment\n * (it is a UI-only tool presented via the elicitation panel, not a tool pill).\n * Explicit setup — rather than a top-level import side-effect — keeps the\n * aparteGlobalConfig singleton mutation predictable in SSR/test and tree-shaking\n * friendly. Call once at application startup.\n */\nexport function setupAskUser(config: AparteConfig = aparteGlobalConfig, options: AskUserToolOptions = {}): void {\n // The bounds the schema puts on the model are the HOST's, so they arrive here\n // rather than being frozen into a constant. Defaults are the normal call.\n config.registerTool(createAskUserTool(options), askUserHandler);\n\n /*\n * The conversation keeps the record.\n *\n * This used to be `render: () => ''` — render nothing, \"it is a UI-only tool\" —\n * and the panel lives in the composer, so once it was answered the transcript\n * held no trace that the assistant had asked anything or that the user had\n * answered. Scroll back and the exchange is simply missing, which is not what a\n * conversation is for; every product that asks a structured question puts the\n * question and the chosen answer in the thread.\n *\n * The pieces were all here and wired to nothing: `questionReceiptRenderer` has\n * existed with its own markup, styles and eleven tests, exported and registered\n * by nobody, while the renderer that WOULD have shown something returned the\n * empty string. Another consequence of a surface no example ever ran.\n *\n * Registered too, so its `getStyles()` reaches the document and an app that\n * builds `question-receipt` segments of its own gets the same card.\n */\n registerSegmentRenderer(questionReceiptRenderer, config);\n config.registerToolRenderer('ask_user', {\n render: (segment) => buildReceipt({ input: segment.toolCall.input, result: segment.result }),\n });\n}\n\nexport { createAskUserTool, askUserHandler } from './ask-user.js';\nexport type { AskUserToolOptions } from './ask-user.js';\nexport type { AskUserOption, AskUserItem, AskUserDetail } from './ask-user.js';\n\nexport { AparteAskUser } from './aparte-ask-user.js';\n\nexport { questionReceiptRenderer } from './question-receipt.renderer.js';\nexport { buildReceipt, receiptRows } from './receipt.js';\nexport type { QuestionReceiptSegment } from './question-receipt.renderer.js';\n\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"question-receipt.renderer.d.ts","sourceRoot":"","sources":["../src/question-receipt.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,kBAAkB,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAClB;AAMD,eAAO,MAAM,uBAAuB,EAAE,qBAAqB,CAAC,sBAAsB,CA6EjF,CAAC"}
1
+ {"version":3,"file":"question-receipt.renderer.d.ts","sourceRoot":"","sources":["../src/question-receipt.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,kBAAkB,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAClB;AAMD,eAAO,MAAM,uBAAuB,EAAE,qBAAqB,CAAC,sBAAsB,CAgFjF,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@aparte/plugin-ask-user",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "description": "The ask_user tool for aparté — the AI asks the user a structured choice, presented via the core elicitation panel.",
8
+ "customElements": "dist/custom-elements.json",
8
9
  "type": "module",
9
10
  "sideEffects": true,
10
11
  "main": "./dist/index.js",
@@ -31,12 +32,13 @@
31
32
  "node": ">=18"
32
33
  },
33
34
  "peerDependencies": {
34
- "@aparte/core": ">=0.7.0 <1.0.0"
35
+ "@aparte/core": ">=0.13.0 <1.0.0"
35
36
  },
36
37
  "devDependencies": {
38
+ "@custom-elements-manifest/analyzer": "^0.11.0",
37
39
  "typescript": "^5.4.0",
38
40
  "vite": "^6.0.0",
39
- "@aparte/core": "0.12.0"
41
+ "@aparte/core": "0.13.0"
40
42
  },
41
43
  "keywords": [
42
44
  "aparte",
@@ -56,7 +58,8 @@
56
58
  },
57
59
  "scripts": {
58
60
  "dev": "vite",
59
- "build": "vite build && tsc -b --emitDeclarationOnly --force",
61
+ "build": "vite build && tsc -b --emitDeclarationOnly --force && pnpm run analyze",
62
+ "analyze": "cem analyze",
60
63
  "preview": "vite preview",
61
64
  "test": "vitest",
62
65
  "test:run": "vitest run",