@juspay/svelte-ui-components 4.33.3 → 4.34.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.
Files changed (46) hide show
  1. package/dist/Accordion/Accordion.svelte +26 -2
  2. package/dist/Accordion/properties.d.ts +7 -0
  3. package/dist/ArmedButton/ArmedButton.svelte +147 -0
  4. package/dist/ArmedButton/ArmedButton.svelte.d.ts +4 -0
  5. package/dist/ArmedButton/properties.d.ts +24 -0
  6. package/dist/ArmedButton/properties.js +1 -0
  7. package/dist/Banner/Banner.svelte +13 -8
  8. package/dist/Banner/properties.d.ts +8 -1
  9. package/dist/Button/Button.svelte +26 -0
  10. package/dist/Button/properties.d.ts +17 -0
  11. package/dist/ChatComposer/ChatComposer.svelte +314 -1
  12. package/dist/ChatComposer/properties.d.ts +42 -0
  13. package/dist/ChatMessageList/ChatMessageList.svelte +22 -2
  14. package/dist/ChatMessageList/ChatMessageList.svelte.d.ts +1 -1
  15. package/dist/CommandMenu/CommandMenu.svelte +53 -20
  16. package/dist/CommandMenu/properties.d.ts +24 -0
  17. package/dist/DiffViewer/DiffViewer.svelte +210 -0
  18. package/dist/DiffViewer/DiffViewer.svelte.d.ts +4 -0
  19. package/dist/DiffViewer/properties.d.ts +25 -0
  20. package/dist/DiffViewer/properties.js +1 -0
  21. package/dist/EmptyState/EmptyState.svelte +22 -3
  22. package/dist/EmptyState/properties.d.ts +9 -0
  23. package/dist/HITL/HITL.svelte +318 -49
  24. package/dist/HITL/properties.d.ts +113 -11
  25. package/dist/Input/Input.svelte +45 -1
  26. package/dist/Input/properties.d.ts +15 -0
  27. package/dist/KeyValue/KeyValue.svelte +16 -3
  28. package/dist/KeyValue/properties.d.ts +2 -0
  29. package/dist/Progress/Progress.svelte +96 -41
  30. package/dist/Progress/properties.d.ts +19 -0
  31. package/dist/Snippet/Snippet.svelte +45 -1
  32. package/dist/Snippet/properties.d.ts +8 -0
  33. package/dist/Tabs/Tabs.svelte +75 -16
  34. package/dist/Tabs/properties.d.ts +21 -0
  35. package/dist/ThinkingIndicator/ThinkingIndicator.svelte.d.ts +1 -1
  36. package/dist/Toolbar/Toolbar.svelte +81 -3
  37. package/dist/Toolbar/properties.d.ts +37 -0
  38. package/dist/WindowedList/WindowedList.svelte +129 -0
  39. package/dist/WindowedList/WindowedList.svelte.d.ts +25 -0
  40. package/dist/WindowedList/properties.d.ts +38 -0
  41. package/dist/WindowedList/properties.js +1 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.js +3 -0
  44. package/dist/styles/theme-dark.css +42 -0
  45. package/dist-wc/index.js +2637 -1642
  46. package/package.json +2 -1
@@ -3,16 +3,24 @@
3
3
  import { get } from 'svelte/store';
4
4
  import Button from '../Button/Button.svelte';
5
5
  import Card from '../Card/Card.svelte';
6
+ import Input from '../Input/Input.svelte';
6
7
  import Progress from '../Progress/Progress.svelte';
7
8
  import { pauseAllConfirmationTimers } from './timers';
8
9
  import type {
9
10
  HITLAction,
11
+ HITLAskForTextExtraAction,
10
12
  HITLExtraAction,
11
13
  HITLProperties,
14
+ HITLQuestion,
15
+ HITLQuestionAnswer,
12
16
  HITLResponse,
13
17
  HITLSection
14
18
  } from './properties';
15
19
 
20
+ // What an extra action or an answer adds to the settled event;
21
+ // confirm/cancel add nothing.
22
+ type HITLSettleDetail = { value?: string; message?: string; answers?: HITLQuestionAnswer[] };
23
+
16
24
  /**
17
25
  * Human-in-the-loop approval card: the assistant wants to run an action and the
18
26
  * person approves or cancels it. Unless disabled, a countdown sweeps across the
@@ -29,6 +37,9 @@
29
37
  onconfirm,
30
38
  confirmLabel = 'Confirm',
31
39
  cancelLabel = 'Cancel',
40
+ showCancel = true,
41
+ showConfirm = true,
42
+ showEmptyParameters = true,
32
43
  countdownSeconds = 10,
33
44
  autoCancelSeconds = 0,
34
45
  isMicMuted = false,
@@ -50,6 +61,12 @@
50
61
  completionTestId,
51
62
  completionTextTestId,
52
63
  actions,
64
+ questions,
65
+ answerOnSelect = false,
66
+ questionsSettleAs = 'approved',
67
+ sendAnswersLabel = 'Send answers',
68
+ optionClasses,
69
+ selectedOptionClasses,
53
70
  children,
54
71
  classes
55
72
  }: HITLProperties = $props();
@@ -134,7 +151,7 @@
134
151
  // updated in between.
135
152
  let decisionPending = false;
136
153
 
137
- const settle = (action: HITLAction): void => {
154
+ const settle = (action: HITLAction, detail: HITLSettleDetail = {}): void => {
138
155
  if (isProcessing || isCompleted) {
139
156
  return;
140
157
  }
@@ -144,7 +161,12 @@
144
161
  onconfirm?.({
145
162
  confirmationId,
146
163
  action,
147
- approved: action !== 'rejected'
164
+ approved: action !== 'rejected',
165
+ // Only the keys that were set, so a plain confirm/cancel event keeps
166
+ // exactly its old three-key shape.
167
+ ...(typeof detail.value === 'string' ? { value: detail.value } : {}),
168
+ ...(typeof detail.message === 'string' ? { message: detail.message } : {}),
169
+ ...(Array.isArray(detail.answers) ? { answers: detail.answers } : {})
148
170
  });
149
171
  } catch {
150
172
  // The decision failed to hand off; release the card for another attempt.
@@ -155,13 +177,13 @@
155
177
  }
156
178
  };
157
179
 
158
- const decide = async (action: HITLAction): Promise<void> => {
180
+ const decide = async (action: HITLAction, detail: HITLSettleDetail = {}): Promise<void> => {
159
181
  if (decisionPending || isCompleted) {
160
182
  return;
161
183
  }
162
184
  decisionPending = true;
163
185
  await restoreMicState();
164
- settle(action);
186
+ settle(action, detail);
165
187
  };
166
188
 
167
189
  const startCountdown = (): void => {
@@ -181,27 +203,112 @@
181
203
  }, 100);
182
204
  };
183
205
 
184
- const interact = async (action: HITLAction): Promise<void> => {
206
+ // Any interaction pauses this card's own countdown and every sibling's.
207
+ const pauseTimers = (): void => {
185
208
  pauseAllConfirmationTimers.set(true);
186
209
  stopCountdown();
187
210
  clearAutoCancel();
188
- await decide(action);
189
211
  };
190
212
 
191
- // An extra action (e.g. "Deny with instructions") is a side path, not a
192
- // decision — it never settles the card, so `onconfirm` never fires for it.
193
- // It still counts as interacting with the card: pause this card's own
194
- // countdown and every sibling's, exactly like clicking confirm or cancel.
213
+ const interact = async (action: HITLAction, detail: HITLSettleDetail = {}): Promise<void> => {
214
+ pauseTimers();
215
+ await decide(action, detail);
216
+ };
217
+
218
+ // The 'ask-for-text' action whose reply box is open, if any. While set, the
219
+ // box replaces the action row.
220
+ let replyAction = $state<HITLAskForTextExtraAction | null>(null);
221
+ let replyText = $state('');
222
+ const canSendReply = $derived(replyText.trim().length > 0 && !isProcessing);
223
+
224
+ // A side action never settles the card, so `onconfirm` never fires for it.
225
+ // 'decision' settles immediately; 'ask-for-text' settles once text is sent.
226
+ // All three count as interacting with the card.
195
227
  const selectExtraAction = (action: HITLExtraAction): void => {
196
228
  if (decisionPending || isProcessing || isCompleted) {
197
229
  return;
198
230
  }
199
- pauseAllConfirmationTimers.set(true);
200
- stopCountdown();
201
- clearAutoCancel();
231
+ if (action.kind === 'decision') {
232
+ void interact(action.settleAs, { value: action.value });
233
+ return;
234
+ }
235
+ pauseTimers();
236
+ if (action.kind === 'ask-for-text') {
237
+ replyText = '';
238
+ replyAction = action;
239
+ return;
240
+ }
202
241
  action.onSelect();
203
242
  };
204
243
 
244
+ const sendReply = (): void => {
245
+ const text = replyText.trim();
246
+ if (replyAction === null || text.length === 0) {
247
+ return;
248
+ }
249
+ void decide(replyAction.settleAs, { value: replyAction.value, message: text });
250
+ };
251
+
252
+ const closeReply = (): void => {
253
+ replyAction = null;
254
+ replyText = '';
255
+ };
256
+
257
+ // Picks per question, by index.
258
+ let picks = $state<string[][]>([]);
259
+ const questionList = $derived(questions ?? []);
260
+ // A single-select question needs no Send when answerOnSelect settles it on
261
+ // click, so Send exists only if something is left to collect.
262
+ const settlesOnClick = (question: HITLQuestion): boolean =>
263
+ answerOnSelect && question.multiSelect !== true;
264
+ const showSendAnswers = $derived(
265
+ questionList.length > 0 && questionList.some((question) => !settlesOnClick(question))
266
+ );
267
+ const canSendAnswers = $derived(
268
+ !isProcessing &&
269
+ questionList.every(
270
+ (question, index) => settlesOnClick(question) || (picks[index] ?? []).length > 0
271
+ )
272
+ );
273
+
274
+ const answerFor = (question: HITLQuestion, picked: string[]): HITLQuestionAnswer => ({
275
+ ...(typeof question.header === 'string' ? { header: question.header } : {}),
276
+ question: question.question,
277
+ picks: picked
278
+ });
279
+
280
+ const pickOption = (questionIndex: number, label: string): void => {
281
+ const question = questionList.at(questionIndex);
282
+ if (decisionPending || isProcessing || isCompleted || typeof question !== 'object') {
283
+ return;
284
+ }
285
+ if (settlesOnClick(question)) {
286
+ void interact(questionsSettleAs, { answers: [answerFor(question, [label])] });
287
+ return;
288
+ }
289
+ pauseTimers();
290
+ const current = picks[questionIndex] ?? [];
291
+ const has = current.includes(label);
292
+ const next =
293
+ question.multiSelect === true
294
+ ? has
295
+ ? current.filter((picked) => picked !== label)
296
+ : [...current, label]
297
+ : has
298
+ ? []
299
+ : [label];
300
+ picks = questionList.map((_, index) => (index === questionIndex ? next : (picks[index] ?? [])));
301
+ };
302
+
303
+ const sendAnswers = (): void => {
304
+ if (!canSendAnswers) {
305
+ return;
306
+ }
307
+ void interact(questionsSettleAs, {
308
+ answers: questionList.map((question, index) => answerFor(question, picks[index] ?? []))
309
+ });
310
+ };
311
+
205
312
  onMount(() => {
206
313
  if (isHistoryMode) {
207
314
  return;
@@ -309,17 +416,31 @@
309
416
  built.push({ label: key.replace(/([A-Z])/g, ' $1').trim(), value: formatted });
310
417
  }
311
418
  }
312
- return built.length > 0 ? built : [{ label: 'PARAMETERS', value: 'No parameters' }];
419
+ return built;
313
420
  };
314
421
 
422
+ // What a card with nothing to list shows. A card that asks questions already
423
+ // has content of its own, and showEmptyParameters={false} opts a card out
424
+ // explicitly. `children` is deliberately NOT a reason to hide it: it is a
425
+ // pre-existing prop, so treating it as one would silently change the output
426
+ // of every existing children-only consumer -- including one whose children
427
+ // snippet renders nothing yet (e.g. gated on its own internal state), which
428
+ // is exactly the "No parameters" case this placeholder exists for.
429
+ const emptyParameters = $derived<HITLSection[]>(
430
+ questionList.length > 0 || !showEmptyParameters
431
+ ? []
432
+ : [{ label: 'PARAMETERS', value: 'No parameters' }]
433
+ );
434
+
315
435
  const parameterSections = $derived.by((): HITLSection[] => {
316
436
  if (sections && sections.length > 0) {
317
437
  return sections;
318
438
  }
319
439
  if (functionArguments && Object.keys(functionArguments).length > 0) {
320
- return formatArguments(functionArguments);
440
+ const formatted = formatArguments(functionArguments);
441
+ return formatted.length > 0 ? formatted : emptyParameters;
321
442
  }
322
- return [{ label: 'PARAMETERS', value: 'No parameters' }];
443
+ return emptyParameters;
323
444
  });
324
445
  </script>
325
446
 
@@ -402,43 +523,138 @@
402
523
  {@render children()}
403
524
  </div>
404
525
  {/if}
405
- <div class="action-buttons">
406
- <div class="cancel-button">
407
- <Button
408
- variant="secondary"
409
- text={cancelLabel}
410
- enable={!isProcessing}
411
- testId={cancelTestId ?? (testId && `${testId}-cancel`)}
412
- onclick={() => interact('rejected')}
413
- />
526
+ {#if questionList.length > 0}
527
+ <div class="questions">
528
+ {#each questionList as question, questionIndex (questionIndex)}
529
+ <div
530
+ class="question"
531
+ role="group"
532
+ aria-label={question.header ?? question.question}
533
+ data-pw={testId && `${testId}-question-${questionIndex}`}
534
+ >
535
+ {#if typeof question.header === 'string'}
536
+ <span class="question-header">{question.header}</span>
537
+ {/if}
538
+ <span class="question-text">{question.question}</span>
539
+ <div class="question-options">
540
+ {#each question.options as option, optionIndex (option.label)}
541
+ {@const picked = (picks[questionIndex] ?? []).includes(option.label)}
542
+ <Button
543
+ variant="secondary"
544
+ text={option.label}
545
+ title={option.description}
546
+ {...settlesOnClick(question) ? {} : { ariaPressed: picked }}
547
+ enable={!isProcessing}
548
+ classes={[optionClasses, picked ? selectedOptionClasses : '']
549
+ .filter((value) => typeof value === 'string' && value.length > 0)
550
+ .join(' ')}
551
+ testId={testId && `${testId}-question-${questionIndex}-option-${optionIndex}`}
552
+ onclick={() => pickOption(questionIndex, option.label)}
553
+ />
554
+ {/each}
555
+ </div>
556
+ </div>
557
+ {/each}
414
558
  </div>
415
- {#each actions ?? [] as action, index (action.testId ?? `${index}-${action.label}`)}
416
- <div class="extra-action-button">
417
- <Button
418
- variant="secondary"
419
- text={action.label}
420
- enable={!isProcessing}
421
- testId={action.testId ?? (testId && `${testId}-action-${index}`)}
422
- classes={action.classes}
423
- ariaLabel={action.ariaLabel}
424
- onclick={() => selectExtraAction(action)}
425
- />
559
+ {/if}
560
+ {#if replyAction !== null}
561
+ <div class="reply-box" data-pw={testId && `${testId}-reply`}>
562
+ <Input
563
+ bind:value={replyText}
564
+ placeholder={replyAction.placeholder ?? ''}
565
+ ariaLabel={replyAction.ariaLabel ?? replyAction.label}
566
+ autofocus
567
+ useTextArea={replyAction.multiline === true}
568
+ autoResize={replyAction.multiline === true}
569
+ rows={3}
570
+ minRows={3}
571
+ maxRows={10}
572
+ testId={testId && `${testId}-reply-input`}
573
+ onkeydown={(event) => {
574
+ // One line: Enter sends. Multiline: Enter is a newline and
575
+ // Ctrl/Cmd+Enter sends.
576
+ const sends =
577
+ replyAction?.multiline === true ? event.metaKey || event.ctrlKey : true;
578
+ if (event.key === 'Enter' && sends) {
579
+ event.preventDefault();
580
+ sendReply();
581
+ }
582
+ }}
583
+ />
584
+ <div class="action-buttons">
585
+ <div class="cancel-button">
586
+ <Button
587
+ variant="secondary"
588
+ text={replyAction.backLabel ?? 'Back'}
589
+ enable={!isProcessing}
590
+ testId={testId && `${testId}-reply-back`}
591
+ onclick={closeReply}
592
+ />
593
+ </div>
594
+ <div class="confirm-button">
595
+ <Button
596
+ text={replyAction.submitLabel ?? 'Send'}
597
+ classes={replyAction.submitClasses}
598
+ enable={canSendReply}
599
+ testId={testId && `${testId}-reply-send`}
600
+ onclick={sendReply}
601
+ />
602
+ </div>
426
603
  </div>
427
- {/each}
428
- <div class="confirm-button">
429
- {#if countdownActive}
430
- <div class="progress-anchor">
431
- <Progress value={elapsedTime} max={countdownSeconds} />
604
+ </div>
605
+ {:else}
606
+ <div class="action-buttons">
607
+ {#if showCancel}
608
+ <div class="cancel-button">
609
+ <Button
610
+ variant="secondary"
611
+ text={cancelLabel}
612
+ enable={!isProcessing}
613
+ testId={cancelTestId ?? (testId && `${testId}-cancel`)}
614
+ onclick={() => interact('rejected')}
615
+ />
616
+ </div>
617
+ {/if}
618
+ {#each actions ?? [] as action, index (action.testId ?? `${index}-${action.label}`)}
619
+ <div class="extra-action-button">
620
+ <Button
621
+ variant="secondary"
622
+ text={action.label}
623
+ enable={!isProcessing}
624
+ testId={action.testId ?? (testId && `${testId}-action-${index}`)}
625
+ classes={action.classes}
626
+ ariaLabel={action.ariaLabel}
627
+ onclick={() => selectExtraAction(action)}
628
+ />
629
+ </div>
630
+ {/each}
631
+ {#if showSendAnswers}
632
+ <div class="confirm-button">
633
+ <Button
634
+ text={sendAnswersLabel}
635
+ enable={canSendAnswers}
636
+ testId={testId && `${testId}-send-answers`}
637
+ onclick={sendAnswers}
638
+ />
639
+ </div>
640
+ {/if}
641
+ {#if showConfirm}
642
+ <div class="confirm-button">
643
+ {#if countdownActive}
644
+ <div class="progress-anchor">
645
+ <Progress value={elapsedTime} max={countdownSeconds} />
646
+ </div>
647
+ {/if}
648
+ <Button
649
+ text={confirmLabel}
650
+ enable={!isProcessing}
651
+ testId={confirmTestId ?? (testId && `${testId}-confirm`)}
652
+ onclick={() => interact('approved')}
653
+ />
432
654
  </div>
433
655
  {/if}
434
- <Button
435
- text={confirmLabel}
436
- enable={!isProcessing}
437
- testId={confirmTestId ?? (testId && `${testId}-confirm`)}
438
- onclick={() => interact('approved')}
439
- />
440
656
  </div>
441
- </div>
657
+ {/if}
442
658
  {/if}
443
659
  </div>
444
660
  </Card>
@@ -518,7 +734,9 @@
518
734
  line-height: var(--hitl-param-value-line-height, 1.4);
519
735
  color: var(--hitl-param-value-color, #1f1f23);
520
736
  word-spacing: var(--hitl-param-value-word-spacing, normal);
521
- text-transform: capitalize;
737
+ /* Capitalize suits prose values; a command or a path has to read verbatim,
738
+ so the consumer can turn it off. */
739
+ text-transform: var(--hitl-param-value-text-transform, capitalize);
522
740
  }
523
741
 
524
742
  /* Rendered only when `children` is supplied; the flex gap on
@@ -528,12 +746,63 @@
528
746
  width: 100%;
529
747
  }
530
748
 
749
+ /* The question and reply rules are written down the structure they live in
750
+ (.confirmation-body > ...), so a class of the same name that a consumer passes through
751
+ `classes`, which lands on the root, cannot match them. */
752
+ .confirmation-body > .questions {
753
+ display: flex;
754
+ flex-direction: column;
755
+ gap: var(--hitl-questions-gap, 0.75rem);
756
+ }
757
+
758
+ .questions > .question {
759
+ display: flex;
760
+ flex-direction: column;
761
+ gap: var(--hitl-question-gap, 0.375rem);
762
+ }
763
+
764
+ /* Same voice as the parameter blocks above: the header reads like a
765
+ parameter label, the question like its value. */
766
+ .question > .question-header {
767
+ font-size: var(--hitl-param-label-font-size, 0.6875rem);
768
+ font-weight: var(--hitl-param-label-font-weight, 600);
769
+ letter-spacing: var(--hitl-param-label-letter-spacing, 0.04em);
770
+ color: var(--hitl-param-label-color, #595959);
771
+ text-transform: uppercase;
772
+ }
773
+
774
+ .question > .question-text {
775
+ font-size: var(--hitl-param-value-font-size, 0.875rem);
776
+ line-height: var(--hitl-param-value-line-height, 1.4);
777
+ color: var(--hitl-param-value-color, #1f1f23);
778
+ }
779
+
780
+ .question > .question-options {
781
+ display: flex;
782
+ flex-wrap: wrap;
783
+ gap: var(--hitl-question-options-gap, 0.5rem);
784
+ }
785
+
786
+ .confirmation-body > .reply-box {
787
+ display: flex;
788
+ flex-direction: column;
789
+ gap: var(--hitl-reply-gap, 0.5rem);
790
+ width: 100%;
791
+ --input-container-width: 100%;
792
+ }
793
+
531
794
  .action-buttons {
532
795
  display: flex;
533
796
  gap: var(--hitl-buttons-gap, 0.75rem);
534
797
  width: 100%;
535
798
  }
536
799
 
800
+ /* Zero specificity, so a consumer's own flex-wrap rule for the row keeps winning, as it
801
+ did before this hook existed. */
802
+ :where(.action-buttons) {
803
+ flex-wrap: var(--hitl-buttons-wrap, nowrap);
804
+ }
805
+
537
806
  .cancel-button,
538
807
  .confirm-button,
539
808
  .extra-action-button {
@@ -9,22 +9,38 @@ export type HITLEvent = {
9
9
  confirmationId: string;
10
10
  action: HITLAction;
11
11
  approved: boolean;
12
+ /** The `value` of the extra action that settled the card, when one did. */
13
+ value?: string;
14
+ /** The text submitted through an `'ask-for-text'` extra action. */
15
+ message?: string;
16
+ /** Set when the card was settled by answering `questions`. */
17
+ answers?: HITLQuestionAnswer[];
18
+ };
19
+ export type HITLQuestionOption = {
20
+ label: string;
21
+ /** Shown as the option's hover tooltip. */
22
+ description?: string;
23
+ };
24
+ export type HITLQuestion = {
25
+ /** Short name for the question, e.g. "Auth method". */
26
+ header?: string;
27
+ question: string;
28
+ options: HITLQuestionOption[];
29
+ /** Any number of options may be picked. Default `false`: one at most. */
30
+ multiSelect?: boolean;
31
+ };
32
+ export type HITLQuestionAnswer = {
33
+ header?: string;
34
+ question: string;
35
+ picks: string[];
12
36
  };
13
37
  export type HITLInitialState = {
14
38
  approved?: boolean;
15
39
  /** `'EXPIRED'` renders the timed-out completion state. */
16
40
  status?: string;
17
41
  };
18
- /**
19
- * A side action rendered beside confirm/cancel — e.g. "Deny with instructions"
20
- * opening a compose panel, or a third disposition a plain approve/reject can't
21
- * express. Selecting one does NOT settle the card: no completion state, no
22
- * `onconfirm`. It only pauses the countdown (own and siblings', like any other
23
- * interaction) and calls `onSelect` — settling, if any, is the caller's job.
24
- */
25
- export type HITLExtraAction = {
42
+ export type HITLExtraActionBase = {
26
43
  label: string;
27
- onSelect: () => void;
28
44
  /** `data-pw` for this action's button (default: `<testId>-action-<index>`). */
29
45
  testId?: string;
30
46
  /** Class string on this action's Button, e.g. to pick a variant. */
@@ -32,6 +48,51 @@ export type HITLExtraAction = {
32
48
  /** Accessible name for this action's Button, when `label` alone isn't descriptive enough. */
33
49
  ariaLabel?: string;
34
50
  };
51
+ /**
52
+ * The default kind: a side path beside confirm/cancel, e.g. opening the
53
+ * caller's own compose panel. Selecting one does NOT settle the card: no
54
+ * completion state, no `onconfirm`. It only pauses the countdown (own and
55
+ * siblings', like any other interaction) and calls `onSelect`. Settling, if
56
+ * any, is the caller's job.
57
+ */
58
+ export type HITLSideExtraAction = HITLExtraActionBase & {
59
+ kind?: 'side';
60
+ onSelect: () => void;
61
+ };
62
+ /**
63
+ * A decision with its own label: settles the card as `settleAs`, exactly like
64
+ * confirm/cancel, with `value` carried on the `onconfirm` event so the caller
65
+ * can tell several same-`settleAs` decisions apart (e.g. plan-approval modes).
66
+ */
67
+ export type HITLDecisionExtraAction = HITLExtraActionBase & {
68
+ kind: 'decision';
69
+ settleAs: HITLAction;
70
+ value?: string;
71
+ };
72
+ /**
73
+ * Asks for text before deciding: opens a reply box inside the card in place of
74
+ * the action row. Submitting settles as `settleAs` with the text on
75
+ * `HITLEvent.message`; the Back button closes the box without settling.
76
+ */
77
+ export type HITLAskForTextExtraAction = HITLExtraActionBase & {
78
+ kind: 'ask-for-text';
79
+ settleAs: HITLAction;
80
+ value?: string;
81
+ placeholder?: string;
82
+ /** Default `'Send'`. */
83
+ submitLabel?: string;
84
+ /** Class string on the Send button, e.g. a danger variant for a deny. */
85
+ submitClasses?: string;
86
+ /** Default `'Back'`. */
87
+ backLabel?: string;
88
+ /**
89
+ * A growing textarea (3-10 rows) instead of a one-line input, for a reply
90
+ * that can run to paragraphs. Enter then adds a line; Send or
91
+ * Ctrl/Cmd+Enter sends. Default `false`.
92
+ */
93
+ multiline?: boolean;
94
+ };
95
+ export type HITLExtraAction = HITLSideExtraAction | HITLDecisionExtraAction | HITLAskForTextExtraAction;
35
96
  export type HITLProperties = OptionalHITLProperties & MandatoryHITLProperties;
36
97
  export type MandatoryHITLProperties = {
37
98
  confirmationId: string;
@@ -52,6 +113,24 @@ export type OptionalHITLProperties = {
52
113
  onconfirm?: (event: HITLEvent) => void;
53
114
  confirmLabel?: string;
54
115
  cancelLabel?: string;
116
+ /** Hides the cancel button entirely — for a card whose only real dispositions come through `actions`. Default `true`. */
117
+ showCancel?: boolean;
118
+ /**
119
+ * Hides the confirm button — for a card whose only real dispositions come
120
+ * through `actions` (e.g. several labelled `'decision'` actions). The
121
+ * countdown sweep lives on that button, so pair it with `countdownSeconds={0}`.
122
+ * Default `true`.
123
+ */
124
+ showConfirm?: boolean;
125
+ /**
126
+ * A card with nothing to list (no `sections`, and no `functionArguments`
127
+ * that survive `hiddenKeys`) shows a "PARAMETERS / No parameters"
128
+ * placeholder. `false` hides it -- for a card whose body is its own
129
+ * `children` (a plan, say) and genuinely has no parameters to show.
130
+ * Supplying `children` does not hide it by itself. A card that asks
131
+ * `questions` never shows it, either way. Default `true`.
132
+ */
133
+ showEmptyParameters?: boolean;
55
134
  /**
56
135
  * Seconds until the card auto-approves, with a sweep across the confirm
57
136
  * button. `0` disables auto-approval.
@@ -85,10 +164,33 @@ export type OptionalHITLProperties = {
85
164
  /** Override the completion text's test id (default: `<testId>-completion-text`). */
86
165
  completionTextTestId?: string;
87
166
  /**
88
- * Extra side actions rendered between cancel and confirm — e.g. "Deny with
89
- * instructions". None of these settle the card; see `HITLExtraAction`.
167
+ * Extra actions rendered between cancel and confirm. By default each is a
168
+ * side path that does not settle the card; `kind: 'decision'` and
169
+ * `kind: 'ask-for-text'` settle it. See `HITLExtraAction`.
90
170
  */
91
171
  actions?: HITLExtraAction[];
172
+ /**
173
+ * Questions answered by picking options inside the card. By default picks
174
+ * are collected (a single-select question toggles like a radio, a
175
+ * multi-select one like checkboxes) and the Send-answers button — enabled
176
+ * once every question has a pick — settles the card as `questionsSettleAs`
177
+ * with every question's picks on `HITLEvent.answers`.
178
+ */
179
+ questions?: HITLQuestion[];
180
+ /**
181
+ * Settle as soon as a single-select option is clicked, with only that
182
+ * question's answer. Multi-select questions still collect picks, and Send
183
+ * then needs only those. Default `false`.
184
+ */
185
+ answerOnSelect?: boolean;
186
+ /** What answering `questions` settles the card as. Default `'approved'`. */
187
+ questionsSettleAs?: HITLAction;
188
+ /** Default `'Send answers'`. */
189
+ sendAnswersLabel?: string;
190
+ /** Class string on every question option's Button, e.g. a chip recipe. */
191
+ optionClasses?: string;
192
+ /** Added to a picked option's Button, after `optionClasses`. */
193
+ selectedOptionClasses?: string;
92
194
  /**
93
195
  * Arbitrary extra controls — free-text input, multi-select chips — rendered
94
196
  * in the body above the action row while the card is pending. Never shown