@terreno/blocks 57.10.1
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/LICENSE +21 -0
- package/README.md +62 -0
- package/biome.jsonc +18 -0
- package/bunfig.toml +6 -0
- package/dist/asks/errors.d.ts +77 -0
- package/dist/asks/errors.js +364 -0
- package/dist/asks/files.d.ts +53 -0
- package/dist/asks/files.js +171 -0
- package/dist/asks/formValues.d.ts +32 -0
- package/dist/asks/formValues.js +229 -0
- package/dist/asks/headless.d.ts +227 -0
- package/dist/asks/headless.js +94 -0
- package/dist/asks/jsonSchema.d.ts +6 -0
- package/dist/asks/jsonSchema.js +62 -0
- package/dist/asks/limits.d.ts +58 -0
- package/dist/asks/limits.js +61 -0
- package/dist/asks/prompt.d.ts +10 -0
- package/dist/asks/prompt.js +144 -0
- package/dist/asks/schema.d.ts +826 -0
- package/dist/asks/schema.js +864 -0
- package/dist/asks/simpleCard.d.ts +85 -0
- package/dist/asks/simpleCard.js +279 -0
- package/dist/asks/validateInput.d.ts +13 -0
- package/dist/asks/validateInput.js +19 -0
- package/dist/asks/validateResponse.d.ts +13 -0
- package/dist/asks/validateResponse.js +278 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +58 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.js +106 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +111 -0
- package/dist/jsonSchema.d.ts +5 -0
- package/dist/jsonSchema.js +12 -0
- package/dist/limits.d.ts +27 -0
- package/dist/limits.js +30 -0
- package/dist/lint.d.ts +21 -0
- package/dist/lint.js +388 -0
- package/dist/parse.d.ts +16 -0
- package/dist/parse.js +95 -0
- package/dist/parsePartial.d.ts +12 -0
- package/dist/parsePartial.js +105 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.js +95 -0
- package/dist/schema.d.ts +199 -0
- package/dist/schema.js +277 -0
- package/dist/validate.d.ts +17 -0
- package/dist/validate.js +331 -0
- package/package.json +60 -0
- package/schemas/pendingAsksResponse.schema.json +195 -0
- package/schemas/simpleCard.schema.json +135 -0
- package/schemas/turnRequest.schema.json +108 -0
- package/schemas/turnResponse.schema.json +204 -0
- package/scripts/writeJsonSchemas.ts +11 -0
- package/src/asks/docParity.test.ts +65 -0
- package/src/asks/errors.test.ts +250 -0
- package/src/asks/errors.ts +449 -0
- package/src/asks/files.test.ts +294 -0
- package/src/asks/files.ts +216 -0
- package/src/asks/fixtures/invalid/choice-blank-text.json +20 -0
- package/src/asks/fixtures/invalid/choice-default-not-in-options.json +13 -0
- package/src/asks/fixtures/invalid/choice-default-selects-two.json +16 -0
- package/src/asks/fixtures/invalid/choice-description-too-long.json +24 -0
- package/src/asks/fixtures/invalid/choice-duplicate-ids.json +17 -0
- package/src/asks/fixtures/invalid/choice-label-too-long.json +23 -0
- package/src/asks/fixtures/invalid/choice-many-bounds-not-whole.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-bounds-out-of-range.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-default-duplicate.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-default-too-many.json +15 -0
- package/src/asks/fixtures/invalid/choice-many-max-above-options.json +14 -0
- package/src/asks/fixtures/invalid/choice-many-min-above-choices.json +13 -0
- package/src/asks/fixtures/invalid/choice-many-min-above-max.json +15 -0
- package/src/asks/fixtures/invalid/choice-missing-required.json +14 -0
- package/src/asks/fixtures/invalid/choice-mixed-errors.json +19 -0
- package/src/asks/fixtures/invalid/choice-not-an-object.json +5 -0
- package/src/asks/fixtures/invalid/choice-one-with-many-fields.json +19 -0
- package/src/asks/fixtures/invalid/choice-option-id-format.json +35 -0
- package/src/asks/fixtures/invalid/choice-options-not-objects.json +12 -0
- package/src/asks/fixtures/invalid/choice-other-label-too-long.json +25 -0
- package/src/asks/fixtures/invalid/choice-other-label-without-allow-other.json +14 -0
- package/src/asks/fixtures/invalid/choice-prompt-empty.json +12 -0
- package/src/asks/fixtures/invalid/choice-prompt-too-long.json +23 -0
- package/src/asks/fixtures/invalid/choice-select-unknown.json +12 -0
- package/src/asks/fixtures/invalid/choice-submit-label-too-long.json +13 -0
- package/src/asks/fixtures/invalid/choice-title-too-long.json +24 -0
- package/src/asks/fixtures/invalid/choice-too-few-options.json +9 -0
- package/src/asks/fixtures/invalid/choice-too-many-options.json +219 -0
- package/src/asks/fixtures/invalid/choice-unknown-key-option.json +15 -0
- package/src/asks/fixtures/invalid/choice-unknown-key-root.json +13 -0
- package/src/asks/fixtures/invalid/choice-wrong-types.json +16 -0
- package/src/asks/fixtures/invalid/confirm-emoji-label-too-long.json +13 -0
- package/src/asks/fixtures/invalid/confirm-label-too-long.json +14 -0
- package/src/asks/fixtures/invalid/confirm-labels-alike-after-trim.json +14 -0
- package/src/asks/fixtures/invalid/confirm-labels-alike.json +13 -0
- package/src/asks/fixtures/invalid/confirm-missing-prompt-blank-label.json +16 -0
- package/src/asks/fixtures/invalid/confirm-unknown-keys.json +23 -0
- package/src/asks/fixtures/invalid/confirm-wrong-types.json +18 -0
- package/src/asks/fixtures/invalid/files-accept-duplicate.json +13 -0
- package/src/asks/fixtures/invalid/files-accept-empty.json +13 -0
- package/src/asks/fixtures/invalid/files-accept-unknown.json +13 -0
- package/src/asks/fixtures/invalid/files-counts-out-of-range.json +19 -0
- package/src/asks/fixtures/invalid/files-min-above-max.json +15 -0
- package/src/asks/fixtures/invalid/files-missing-accept-unknown-key.json +17 -0
- package/src/asks/fixtures/invalid/form-default-bad-format.json +40 -0
- package/src/asks/fixtures/invalid/form-default-invalid-date.json +40 -0
- package/src/asks/fixtures/invalid/form-default-not-in-options.json +58 -0
- package/src/asks/fixtures/invalid/form-default-out-of-range.json +33 -0
- package/src/asks/fixtures/invalid/form-default-text-bounds.json +42 -0
- package/src/asks/fixtures/invalid/form-default-wrong-type.json +30 -0
- package/src/asks/fixtures/invalid/form-duplicate-field-ids.json +24 -0
- package/src/asks/fixtures/invalid/form-field-id-format.json +19 -0
- package/src/asks/fixtures/invalid/form-field-text-limits.json +33 -0
- package/src/asks/fixtures/invalid/form-keys-of-another-type.json +39 -0
- package/src/asks/fixtures/invalid/form-length-above-cap.json +30 -0
- package/src/asks/fixtures/invalid/form-min-above-max.json +32 -0
- package/src/asks/fixtures/invalid/form-no-fields.json +13 -0
- package/src/asks/fixtures/invalid/form-select-options.json +44 -0
- package/src/asks/fixtures/invalid/form-too-many-fields.json +59 -0
- package/src/asks/fixtures/invalid/form-unknown-field-type.json +27 -0
- package/src/asks/fixtures/invalid/markdown-bounds-out-of-range.json +18 -0
- package/src/asks/fixtures/invalid/markdown-initial-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-max-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-min-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-min-above-max.json +14 -0
- package/src/asks/fixtures/invalid/markdown-placeholder-too-long.json +13 -0
- package/src/asks/fixtures/invalid/markdown-wrong-types-unknown-key.json +27 -0
- package/src/asks/fixtures/valid/choice-emoji-label-cut.json +32 -0
- package/src/asks/fixtures/valid/choice-labels-collide-after-cut.json +21 -0
- package/src/asks/fixtures/valid/choice-long-text-cut.json +41 -0
- package/src/asks/fixtures/valid/choice-many-default-below-min.json +24 -0
- package/src/asks/fixtures/valid/choice-many-every-choice-with-other.json +24 -0
- package/src/asks/fixtures/valid/choice-many-no-default.json +21 -0
- package/src/asks/fixtures/valid/choice-many-optional-no-buttons.json +20 -0
- package/src/asks/fixtures/valid/choice-many-options-default-label-collides.json +23 -0
- package/src/asks/fixtures/valid/choice-many-options-no-buttons.json +21 -0
- package/src/asks/fixtures/valid/choice-many-options-no-default.json +22 -0
- package/src/asks/fixtures/valid/choice-many-options-with-default.json +30 -0
- package/src/asks/fixtures/valid/choice-many-required-with-default.json +29 -0
- package/src/asks/fixtures/valid/choice-many-single-pick-or-other.json +29 -0
- package/src/asks/fixtures/valid/choice-many-toppings-with-default.json +36 -0
- package/src/asks/fixtures/valid/choice-max-limits.json +245 -0
- package/src/asks/fixtures/valid/choice-plan-with-default.json +42 -0
- package/src/asks/fixtures/valid/choice-two-options-no-decline.json +32 -0
- package/src/asks/fixtures/valid/choice-two-options.json +32 -0
- package/src/asks/fixtures/valid/confirm-allow-decline.json +40 -0
- package/src/asks/fixtures/valid/confirm-default-labels.json +36 -0
- package/src/asks/fixtures/valid/confirm-destructive-delete.json +41 -0
- package/src/asks/fixtures/valid/confirm-emoji-labels.json +38 -0
- package/src/asks/fixtures/valid/confirm-long-text-cut.json +40 -0
- package/src/asks/fixtures/valid/files-every-type.json +28 -0
- package/src/asks/fixtures/valid/files-one-csv-no-decline.json +16 -0
- package/src/asks/fixtures/valid/files-receipt-photos.json +24 -0
- package/src/asks/fixtures/valid/form-boolean-newsletter.json +30 -0
- package/src/asks/fixtures/valid/form-date-due.json +44 -0
- package/src/asks/fixtures/valid/form-datetime-meeting.json +44 -0
- package/src/asks/fixtures/valid/form-datetime-minutes-offset.json +44 -0
- package/src/asks/fixtures/valid/form-eight-fields.json +71 -0
- package/src/asks/fixtures/valid/form-email-contact.json +44 -0
- package/src/asks/fixtures/valid/form-invoice-details.json +68 -0
- package/src/asks/fixtures/valid/form-multiselect-channels.json +64 -0
- package/src/asks/fixtures/valid/form-number-budget.json +45 -0
- package/src/asks/fixtures/valid/form-phone-callback.json +44 -0
- package/src/asks/fixtures/valid/form-select-shipping.json +58 -0
- package/src/asks/fixtures/valid/form-text-required-default.json +47 -0
- package/src/asks/fixtures/valid/form-textarea-notes.json +23 -0
- package/src/asks/fixtures/valid/form-time-reminder.json +44 -0
- package/src/asks/fixtures/valid/form-url-website.json +30 -0
- package/src/asks/fixtures/valid/markdown-announcement-draft.json +39 -0
- package/src/asks/fixtures/valid/markdown-empty-draft.json +34 -0
- package/src/asks/fixtures/valid/markdown-initial-blank-below-min.json +24 -0
- package/src/asks/fixtures/valid/markdown-initial-over-max.json +24 -0
- package/src/asks/fixtures/valid/markdown-long-text-cut.json +37 -0
- package/src/asks/fixtures/valid/markdown-no-decline.json +28 -0
- package/src/asks/fixtures/valid/markdown-no-initial-with-min.json +25 -0
- package/src/asks/formValues.ts +320 -0
- package/src/asks/headless.test.ts +146 -0
- package/src/asks/headless.ts +130 -0
- package/src/asks/jsonSchema.test.ts +171 -0
- package/src/asks/jsonSchema.ts +89 -0
- package/src/asks/limits.ts +58 -0
- package/src/asks/prompt.test.ts +200 -0
- package/src/asks/prompt.ts +173 -0
- package/src/asks/schema.test.ts +198 -0
- package/src/asks/schema.ts +1203 -0
- package/src/asks/simpleCard.test.ts +998 -0
- package/src/asks/simpleCard.ts +341 -0
- package/src/asks/validateInput.test.ts +875 -0
- package/src/asks/validateInput.ts +24 -0
- package/src/asks/validateResponse.test.ts +740 -0
- package/src/asks/validateResponse.ts +341 -0
- package/src/blocksDocParity.test.ts +21 -0
- package/src/cli.test.ts +134 -0
- package/src/cli.ts +65 -0
- package/src/display.test.ts +68 -0
- package/src/errors.test.ts +29 -0
- package/src/errors.ts +125 -0
- package/src/fixtures/invalid/column-not-found.yaml +14 -0
- package/src/fixtures/invalid/column-type.yaml +14 -0
- package/src/fixtures/invalid/dataset-not-found.yaml +7 -0
- package/src/fixtures/invalid/dataset-too-large.yaml +20 -0
- package/src/fixtures/invalid/depth-exceeded.yaml +10 -0
- package/src/fixtures/invalid/duplicate-id.yaml +6 -0
- package/src/fixtures/invalid/invalid-enum.yaml +5 -0
- package/src/fixtures/invalid/key-order.yaml +4 -0
- package/src/fixtures/invalid/missing-required.yaml +3 -0
- package/src/fixtures/invalid/not-a-document.txt +1 -0
- package/src/fixtures/invalid/row-arity.yaml +11 -0
- package/src/fixtures/invalid/select-target.yaml +12 -0
- package/src/fixtures/invalid/table-too-wide.yaml +9 -0
- package/src/fixtures/invalid/too-many-points.yaml +8 -0
- package/src/fixtures/invalid/unknown-key.yaml +5 -0
- package/src/fixtures/invalid/unsupported-version.yaml +4 -0
- package/src/fixtures/invalid/yaml-anchor.yaml +6 -0
- package/src/fixtures/partial/cut-after-divider.yaml +5 -0
- package/src/fixtures/valid/actions.yaml +32 -0
- package/src/fixtures/valid/chart.yaml +20 -0
- package/src/fixtures/valid/layout.yaml +26 -0
- package/src/fixtures/valid/prose.yaml +4 -0
- package/src/html.test.ts +52 -0
- package/src/index.test.ts +114 -0
- package/src/index.ts +175 -0
- package/src/jsonSchema.test.ts +33 -0
- package/src/jsonSchema.ts +11 -0
- package/src/limits.ts +27 -0
- package/src/lint.test.ts +289 -0
- package/src/lint.ts +484 -0
- package/src/parse.test.ts +281 -0
- package/src/parse.ts +104 -0
- package/src/parsePartial.test.ts +53 -0
- package/src/parsePartial.ts +116 -0
- package/src/prompt.test.ts +24 -0
- package/src/prompt.ts +104 -0
- package/src/schema.ts +509 -0
- package/src/tests/askFixtures.ts +40 -0
- package/src/validate.perf.test.ts +43 -0
- package/src/validate.ts +369 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {ASK_LIMITS} from "./limits";
|
|
2
|
+
import {type AskKind, type AskSurface, type CompactAskKind, isCompactAskKind} from "./schema";
|
|
3
|
+
|
|
4
|
+
const CHOICE_ANSWER_EXAMPLE =
|
|
5
|
+
'- An accepted answer looks like {"action": "accept", "content": {"selected": ["<id>"]}}.';
|
|
6
|
+
|
|
7
|
+
const CHOICE_DECLINE_RULE =
|
|
8
|
+
"- allowDecline: optional, default true (the user sees Skip). Set it to false only when you cannot continue without an answer.";
|
|
9
|
+
|
|
10
|
+
const CHOICE_ID_RULE = `- id: 1-${ASK_LIMITS.choice.optionIdMaxLength} lowercase letters, digits, "_", or "-", starting with a letter or digit. Unique within the ask.`;
|
|
11
|
+
|
|
12
|
+
const sharedRules = (): string =>
|
|
13
|
+
[
|
|
14
|
+
"Rules for every ask:",
|
|
15
|
+
`- prompt: required. Plain text with no markdown and no links, 1-${ASK_LIMITS.promptMaxLength} characters.`,
|
|
16
|
+
`- title: optional, at most ${ASK_LIMITS.titleMaxLength} characters.`,
|
|
17
|
+
"- Each ask kind below lists its other fields, including whether the user can skip it.",
|
|
18
|
+
].join("\n");
|
|
19
|
+
|
|
20
|
+
const compactSharedRules = (): string =>
|
|
21
|
+
[
|
|
22
|
+
sharedRules(),
|
|
23
|
+
`- The screen is small: it shows only the first ${ASK_LIMITS.simpleCard.textMaxLength} characters of prompt and the first ${ASK_LIMITS.simpleCard.titleMaxLength} characters of title, so keep both within those lengths.`,
|
|
24
|
+
].join("\n");
|
|
25
|
+
|
|
26
|
+
const choiceRules = (): string => {
|
|
27
|
+
const {choice, simpleCard} = ASK_LIMITS;
|
|
28
|
+
return [
|
|
29
|
+
"ask_choice: the user picks one or more options from a list you provide.",
|
|
30
|
+
'- select: "one" for exactly one option, or "many" to let the user pick several.',
|
|
31
|
+
`- options: ${choice.optionsMin}-${choice.optionsMax} items, each {id, label, description?}.`,
|
|
32
|
+
CHOICE_ID_RULE,
|
|
33
|
+
`- label: at most ${choice.optionLabelMaxLength} characters. description: optional, at most ${choice.optionDescriptionMaxLength} characters.`,
|
|
34
|
+
'- default: optional list of option ids to preselect, each listed once. With "one", at most one id; with "many", at most maxSelected ids.',
|
|
35
|
+
'- minSelected, maxSelected: optional whole numbers, "many" only. The user picks from minSelected (default 1, at least 0) to maxSelected (default: every choice) choices.',
|
|
36
|
+
`- allowOther: optional, "many" only. true adds a text field where the user types an answer of their own, up to ${choice.otherMaxLength} characters. It counts as one choice. otherLabel: optional label for that field, at most ${choice.optionLabelMaxLength} characters. For one option or Other, use "many" with maxSelected 1.`,
|
|
37
|
+
`- submitLabel: optional label for the submit button, at most ${ASK_LIMITS.submitLabelMaxLength} characters.`,
|
|
38
|
+
CHOICE_DECLINE_RULE,
|
|
39
|
+
`- Prefer select "one" with at most ${simpleCard.buttonsMax} options with labels of ${simpleCard.buttonLabelMaxLength} characters or fewer; small screens show those as buttons.`,
|
|
40
|
+
CHOICE_ANSWER_EXAMPLE,
|
|
41
|
+
'- With Other, it looks like {"action": "accept", "content": {"selected": ["<id>"], "other": "<text the user typed>"}}.',
|
|
42
|
+
].join("\n");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const compactChoiceRules = (): string => {
|
|
46
|
+
const {choice, simpleCard} = ASK_LIMITS;
|
|
47
|
+
return [
|
|
48
|
+
"ask_choice: the user picks one option by tapping its button.",
|
|
49
|
+
'- select: always "one".',
|
|
50
|
+
`- options: ${choice.optionsMin}-${simpleCard.buttonsMax} items, each {id, label, description?}.`,
|
|
51
|
+
CHOICE_ID_RULE,
|
|
52
|
+
`- label: at most ${simpleCard.buttonLabelMaxLength} characters, counting each emoji as 2 or more, and no two options share a label.`,
|
|
53
|
+
`- description: optional, at most ${choice.optionDescriptionMaxLength} characters. The buttons show only labels, so put what the user needs to decide in prompt and the labels.`,
|
|
54
|
+
"- default: optional list with at most one option id to preselect.",
|
|
55
|
+
CHOICE_DECLINE_RULE,
|
|
56
|
+
CHOICE_ANSWER_EXAMPLE,
|
|
57
|
+
].join("\n");
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** A confirm always fits a simple card, so the full and compact surfaces share these rules. */
|
|
61
|
+
const confirmRules = (): string => {
|
|
62
|
+
const {labelMaxLength} = ASK_LIMITS.confirm;
|
|
63
|
+
return [
|
|
64
|
+
"ask_confirm: the user approves or denies one action you describe in prompt.",
|
|
65
|
+
`- confirmLabel, denyLabel: optional labels for the approve and deny buttons, at most ${labelMaxLength} characters each, counting each emoji as 2 or more, and different from each other. They default to "Confirm" and "Cancel". Name the action, such as "Delete 14 todos" and "Keep them".`,
|
|
66
|
+
"- destructive: optional, default false. Set it to true when the action deletes data or cannot be undone; the approve button then shows as destructive.",
|
|
67
|
+
"- allowDecline: optional, default false, because the deny button is the negative answer. Set it to true to show Skip as well.",
|
|
68
|
+
`- Before you call a tool that deletes data, sends something on the user's behalf, spends money, or cannot be undone, call ask_confirm and say in prompt exactly what will happen. Make the call only after {"confirmed": true}.`,
|
|
69
|
+
'- An accepted answer looks like {"action": "accept", "content": {"confirmed": true}}. {"confirmed": false} means the user said no, so do not take the action.',
|
|
70
|
+
].join("\n");
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const markdownRules = (): string => {
|
|
74
|
+
const {markdown} = ASK_LIMITS;
|
|
75
|
+
return [
|
|
76
|
+
"ask_markdown: the user edits a markdown draft you write and sends it back.",
|
|
77
|
+
`- initial: optional, the draft in markdown, at most ${markdown.maxLength} characters. Put the whole draft here, not in prompt.`,
|
|
78
|
+
`- minLength, maxLength: optional whole numbers. The answer must have at least minLength (default 0) and at most maxLength (default ${markdown.maxLength}, the most allowed) characters. minLength must not be more than maxLength.`,
|
|
79
|
+
`- placeholder: optional hint shown while the editor is empty, at most ${markdown.placeholderMaxLength} characters.`,
|
|
80
|
+
`- submitLabel: optional label for the submit button, at most ${ASK_LIMITS.submitLabelMaxLength} characters.`,
|
|
81
|
+
"- allowDecline: optional, default true (the user sees Skip).",
|
|
82
|
+
'- An accepted answer looks like {"action": "accept", "content": {"markdown": "<the text>", "changed": true}}. changed is false when the user sent your draft unchanged.',
|
|
83
|
+
].join("\n");
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const formRules = (): string => {
|
|
87
|
+
const {choice, form} = ASK_LIMITS;
|
|
88
|
+
return [
|
|
89
|
+
"ask_form: the user fills in a few fields and submits them at once.",
|
|
90
|
+
`- fields: ${form.fieldsMin}-${form.fieldsMax} items, in display order, each {id, type, label, helperText?, required?, default?} plus the rules of its type. Fields are flat: no nesting, no conditional fields, and no password fields.`,
|
|
91
|
+
`- id: 1-${choice.optionIdMaxLength} lowercase letters, digits, "_", or "-", starting with a letter or digit. Unique within the form. It keys the field's value in the answer.`,
|
|
92
|
+
`- label: at most ${form.labelMaxLength} characters. helperText: optional, at most ${form.helperTextMaxLength} characters. required: optional, default false; true means the answer must hold a non-blank value.`,
|
|
93
|
+
`- type "text" (one line) or "textarea" (several lines): optional minLength and maxLength. maxLength is at most ${form.textMaxLength} for text and ${form.textareaMaxLength} for textarea. The value is a string.`,
|
|
94
|
+
`- type "email", "url" (http or https), or "phone" (${form.phoneDigitsMin}-${form.phoneDigitsMax} digits): the value is a string in that format.`,
|
|
95
|
+
'- type "number": optional min, max, and integer (true for whole numbers only). The value is a number.',
|
|
96
|
+
'- type "date" (YYYY-MM-DD), "time" (24-hour HH:mm), or "datetime" (ISO 8601 with Z or an offset; seconds are optional, such as "2026-10-01T09:30Z" or "2026-10-01T09:30:00+02:00"): the value is a string in that format.',
|
|
97
|
+
'- type "boolean": a checkbox. The value is true or false.',
|
|
98
|
+
`- type "select" (pick one) or "multiselect" (pick any): options, ${choice.optionsMin}-${choice.optionsMax} items, each {id, label}. The value is an option id, or a list of option ids.`,
|
|
99
|
+
"- default: optional, a value the field accepts, filled in when the form opens.",
|
|
100
|
+
`- submitLabel: optional label for the submit button, at most ${ASK_LIMITS.submitLabelMaxLength} characters.`,
|
|
101
|
+
"- allowDecline: optional, default true (the user sees Skip).",
|
|
102
|
+
'- An accepted answer looks like {"action": "accept", "content": {"values": {"<field id>": <value>}}}. Optional fields the user left empty are left out.',
|
|
103
|
+
].join("\n");
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const filesRules = (): string => {
|
|
107
|
+
const {files} = ASK_LIMITS;
|
|
108
|
+
return [
|
|
109
|
+
"ask_files: the user uploads one or more images or documents.",
|
|
110
|
+
'- accept: required list, each value once: "image" (JPEG, PNG, GIF, or WebP), "pdf", "text" (plain text), "csv", or "json".',
|
|
111
|
+
`- minFiles, maxFiles: optional whole numbers from ${files.minFiles} to ${files.maxFiles}. The user sends from minFiles (default ${files.minFiles}) to maxFiles (default ${files.maxFiles}) files.`,
|
|
112
|
+
`- submitLabel: optional label for the submit button, at most ${ASK_LIMITS.submitLabelMaxLength} characters.`,
|
|
113
|
+
"- allowDecline: optional, default true (the user sees Skip).",
|
|
114
|
+
`- The answer lists each file's filename, mimeType, and size, and the tool result then shows you each file: images and PDFs as they are, and text, CSV, and JSON as text cut to ${files.textMaxBytes} bytes.`,
|
|
115
|
+
].join("\n");
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const KIND_RULES: Record<AskKind, () => string> = {
|
|
119
|
+
choice: choiceRules,
|
|
120
|
+
confirm: confirmRules,
|
|
121
|
+
files: filesRules,
|
|
122
|
+
form: formRules,
|
|
123
|
+
markdown: markdownRules,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const COMPACT_KIND_RULES: Record<CompactAskKind, () => string> = {
|
|
127
|
+
choice: compactChoiceRules,
|
|
128
|
+
confirm: confirmRules,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const formatSection = ({
|
|
132
|
+
kindRules,
|
|
133
|
+
kinds,
|
|
134
|
+
shared,
|
|
135
|
+
}: {
|
|
136
|
+
kindRules: string[];
|
|
137
|
+
kinds: readonly AskKind[];
|
|
138
|
+
shared: string;
|
|
139
|
+
}): string => {
|
|
140
|
+
if (kinds.length === 0) {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
const toolNames = kinds.map((kind) => `ask_${kind}`).join(", ");
|
|
144
|
+
return [`Ask tools you can call: ${toolNames}.`, shared, ...kindRules].join("\n\n");
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The system prompt section that describes the enabled ask kinds and their limits. Every number
|
|
149
|
+
* comes from `ASK_LIMITS`. With `surface: "compact"`, it lists only the kinds the compact surface
|
|
150
|
+
* offers, with their narrowed limits. Returns an empty string when no kinds are offered.
|
|
151
|
+
*/
|
|
152
|
+
export const askPromptSection = ({
|
|
153
|
+
kinds,
|
|
154
|
+
surface = "full",
|
|
155
|
+
}: {
|
|
156
|
+
kinds: readonly AskKind[];
|
|
157
|
+
surface?: AskSurface;
|
|
158
|
+
}): string => {
|
|
159
|
+
const uniqueKinds = [...new Set(kinds)];
|
|
160
|
+
if (surface === "compact") {
|
|
161
|
+
const compactKinds = uniqueKinds.filter(isCompactAskKind);
|
|
162
|
+
return formatSection({
|
|
163
|
+
kindRules: compactKinds.map((kind) => COMPACT_KIND_RULES[kind]()),
|
|
164
|
+
kinds: compactKinds,
|
|
165
|
+
shared: compactSharedRules(),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return formatSection({
|
|
169
|
+
kindRules: uniqueKinds.map((kind) => KIND_RULES[kind]()),
|
|
170
|
+
kinds: uniqueKinds,
|
|
171
|
+
shared: sharedRules(),
|
|
172
|
+
});
|
|
173
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {describe, expect, it} from "bun:test";
|
|
2
|
+
import {validAskFixtures} from "../tests/askFixtures";
|
|
3
|
+
import {acceptedFileMimeTypes} from "./files";
|
|
4
|
+
import {formDefaultValues} from "./formValues";
|
|
5
|
+
import {
|
|
6
|
+
ASK_KINDS,
|
|
7
|
+
type AskKind,
|
|
8
|
+
askAllowsDecline,
|
|
9
|
+
askInputSchemaFor,
|
|
10
|
+
askKindsForSurface,
|
|
11
|
+
COMPACT_ASK_KINDS,
|
|
12
|
+
choiceAskInputSchema,
|
|
13
|
+
compactChoiceAskInputSchema,
|
|
14
|
+
confirmAskInputSchema,
|
|
15
|
+
confirmButtonLabels,
|
|
16
|
+
filesAskInputSchema,
|
|
17
|
+
filesCountBounds,
|
|
18
|
+
formAskInputSchema,
|
|
19
|
+
isCompactAskKind,
|
|
20
|
+
markdownAskInputSchema,
|
|
21
|
+
markdownLengthBounds,
|
|
22
|
+
} from "./schema";
|
|
23
|
+
|
|
24
|
+
describe("askKindsForSurface", () => {
|
|
25
|
+
it("keeps every kind on the full surface", () => {
|
|
26
|
+
const kinds: AskKind[] = ["confirm", "choice"];
|
|
27
|
+
const offered = askKindsForSurface({kinds, surface: "full"});
|
|
28
|
+
expect(offered).toEqual(["confirm", "choice"]);
|
|
29
|
+
expect(offered).not.toBe(kinds);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("keeps only the compact kinds on the compact surface", () => {
|
|
33
|
+
expect(askKindsForSurface({kinds: ASK_KINDS, surface: "compact"})).toEqual([
|
|
34
|
+
...COMPACT_ASK_KINDS,
|
|
35
|
+
]);
|
|
36
|
+
expect(askKindsForSurface({kinds: [], surface: "compact"})).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("offers choice and confirm on the compact surface", () => {
|
|
40
|
+
expect([...COMPACT_ASK_KINDS]).toEqual(["choice", "confirm"]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("askInputSchemaFor", () => {
|
|
45
|
+
it("returns the full schema by default and the narrowed schema for compact", () => {
|
|
46
|
+
expect(askInputSchemaFor({kind: "choice"})).toBe(choiceAskInputSchema);
|
|
47
|
+
expect(askInputSchemaFor({kind: "choice", surface: "full"})).toBe(choiceAskInputSchema);
|
|
48
|
+
expect(askInputSchemaFor({kind: "choice", surface: "compact"})).toBe(
|
|
49
|
+
compactChoiceAskInputSchema
|
|
50
|
+
);
|
|
51
|
+
expect(askInputSchemaFor({kind: "confirm"})).toBe(confirmAskInputSchema);
|
|
52
|
+
expect(askInputSchemaFor({kind: "confirm", surface: "compact"})).toBe(confirmAskInputSchema);
|
|
53
|
+
expect(askInputSchemaFor({kind: "markdown"})).toBe(markdownAskInputSchema);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns the form schema on the full surface and throws on compact", () => {
|
|
57
|
+
expect(askInputSchemaFor({kind: "form"})).toBe(formAskInputSchema);
|
|
58
|
+
expect(isCompactAskKind("form")).toBe(false);
|
|
59
|
+
expect(() => askInputSchemaFor({kind: "form", surface: "compact"})).toThrow(
|
|
60
|
+
'The compact surface does not offer ask kind "form".'
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not offer markdown on the compact surface, because a draft cannot be edited there", () => {
|
|
65
|
+
expect(isCompactAskKind("markdown")).toBe(false);
|
|
66
|
+
expect(() => askInputSchemaFor({kind: "markdown", surface: "compact"})).toThrow(
|
|
67
|
+
'The compact surface does not offer ask kind "markdown".'
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("only narrows: every fixture the compact schema accepts, the full schema accepts", () => {
|
|
72
|
+
for (const fixture of validAskFixtures()) {
|
|
73
|
+
if (!isCompactAskKind(fixture.kind)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const compact = askInputSchemaFor({kind: fixture.kind, surface: "compact"});
|
|
77
|
+
if (compact.safeParse(fixture.input).success) {
|
|
78
|
+
expect(askInputSchemaFor({kind: fixture.kind}).safeParse(fixture.input).success).toBe(true);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("askAllowsDecline", () => {
|
|
85
|
+
it("defaults to true for choice and to false for confirm", () => {
|
|
86
|
+
const prompt = "Go ahead?";
|
|
87
|
+
expect(
|
|
88
|
+
askAllowsDecline({
|
|
89
|
+
input: {
|
|
90
|
+
options: [
|
|
91
|
+
{id: "yes", label: "Yes"},
|
|
92
|
+
{id: "no", label: "No"},
|
|
93
|
+
],
|
|
94
|
+
prompt,
|
|
95
|
+
select: "one",
|
|
96
|
+
},
|
|
97
|
+
kind: "choice",
|
|
98
|
+
})
|
|
99
|
+
).toBe(true);
|
|
100
|
+
expect(askAllowsDecline({input: {prompt}, kind: "confirm"})).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("follows allowDecline when the ask sets it", () => {
|
|
104
|
+
const prompt = "Go ahead?";
|
|
105
|
+
expect(askAllowsDecline({input: {allowDecline: true, prompt}, kind: "confirm"})).toBe(true);
|
|
106
|
+
expect(askAllowsDecline({input: {allowDecline: false, prompt}, kind: "confirm"})).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("defaults to true for markdown", () => {
|
|
110
|
+
const prompt = "Edit the draft.";
|
|
111
|
+
expect(askAllowsDecline({input: {prompt}, kind: "markdown"})).toBe(true);
|
|
112
|
+
expect(askAllowsDecline({input: {allowDecline: false, prompt}, kind: "markdown"})).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("ASK_KINDS", () => {
|
|
117
|
+
it("appends each new kind, so existing tool order is kept", () => {
|
|
118
|
+
expect([...ASK_KINDS]).toEqual(["choice", "confirm", "markdown", "form", "files"]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("files asks", () => {
|
|
123
|
+
it("are offered on the full surface only, because files cannot be picked on a small screen", () => {
|
|
124
|
+
expect(askInputSchemaFor({kind: "files"})).toBe(filesAskInputSchema);
|
|
125
|
+
expect(isCompactAskKind("files")).toBe(false);
|
|
126
|
+
expect(() => askInputSchemaFor({kind: "files", surface: "compact"})).toThrow(
|
|
127
|
+
'The compact surface does not offer ask kind "files".'
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("allow declining by default", () => {
|
|
132
|
+
const input = {accept: ["image" as const], prompt: "Upload it."};
|
|
133
|
+
expect(askAllowsDecline({input, kind: "files"})).toBe(true);
|
|
134
|
+
expect(askAllowsDecline({input: {...input, allowDecline: false}, kind: "files"})).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("take 1 to 10 files unless the ask narrows the count", () => {
|
|
138
|
+
expect(filesCountBounds({accept: ["pdf"]})).toEqual({max: 10, min: 1});
|
|
139
|
+
expect(filesCountBounds({accept: ["pdf"], maxFiles: 3, minFiles: 2})).toEqual({
|
|
140
|
+
max: 3,
|
|
141
|
+
min: 2,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("map each accept value to the MIME types of the /files/upload allowlist", () => {
|
|
146
|
+
expect(acceptedFileMimeTypes(["image", "pdf", "text", "csv", "json"])).toEqual([
|
|
147
|
+
"image/jpeg",
|
|
148
|
+
"image/png",
|
|
149
|
+
"image/gif",
|
|
150
|
+
"image/webp",
|
|
151
|
+
"application/pdf",
|
|
152
|
+
"text/plain",
|
|
153
|
+
"text/csv",
|
|
154
|
+
"application/json",
|
|
155
|
+
]);
|
|
156
|
+
expect(acceptedFileMimeTypes(["csv"])).toEqual(["text/csv"]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("askAllowsDecline for form", () => {
|
|
161
|
+
it("defaults to true", () => {
|
|
162
|
+
const input = {fields: [{id: "name", label: "Name", type: "text" as const}], prompt: "Go."};
|
|
163
|
+
expect(askAllowsDecline({input, kind: "form"})).toBe(true);
|
|
164
|
+
expect(askAllowsDecline({input: {...input, allowDecline: false}, kind: "form"})).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("formDefaultValues", () => {
|
|
169
|
+
it("keys each field's default by id and leaves fields without one out", () => {
|
|
170
|
+
expect(
|
|
171
|
+
formDefaultValues({
|
|
172
|
+
fields: [
|
|
173
|
+
{default: "Acme", id: "company", label: "Company", type: "text"},
|
|
174
|
+
{id: "seats", label: "Seats", type: "number"},
|
|
175
|
+
{default: false, id: "notify", label: "Notify", type: "boolean"},
|
|
176
|
+
{default: 0, id: "count", label: "Count", type: "number"},
|
|
177
|
+
],
|
|
178
|
+
prompt: "Go.",
|
|
179
|
+
})
|
|
180
|
+
).toEqual({company: "Acme", count: 0, notify: false});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("markdownLengthBounds", () => {
|
|
185
|
+
it("defaults to 0 through the 20,000-character cap", () => {
|
|
186
|
+
expect(markdownLengthBounds({})).toEqual({max: 20_000, min: 0});
|
|
187
|
+
expect(markdownLengthBounds({maxLength: 400, minLength: 20})).toEqual({max: 400, min: 20});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("confirmButtonLabels", () => {
|
|
192
|
+
it("uses the ask's labels, defaulting to Confirm and Cancel", () => {
|
|
193
|
+
expect(confirmButtonLabels({prompt: "Go?"})).toEqual({confirm: "Confirm", deny: "Cancel"});
|
|
194
|
+
expect(
|
|
195
|
+
confirmButtonLabels({confirmLabel: "Delete 14 todos", denyLabel: "Keep them", prompt: "Go?"})
|
|
196
|
+
).toEqual({confirm: "Delete 14 todos", deny: "Keep them"});
|
|
197
|
+
});
|
|
198
|
+
});
|