@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,341 @@
|
|
|
1
|
+
import {z} from "zod";
|
|
2
|
+
import {type AskValidationError, askIssue, quoteValue} from "./errors";
|
|
3
|
+
import {formDefaultValues} from "./formValues";
|
|
4
|
+
import {ASK_LIMITS} from "./limits";
|
|
5
|
+
import {
|
|
6
|
+
ASK_KINDS,
|
|
7
|
+
type Ask,
|
|
8
|
+
type AskResponse,
|
|
9
|
+
askResponseSchema,
|
|
10
|
+
type ChoiceAskInput,
|
|
11
|
+
type ChoiceOption,
|
|
12
|
+
type ConfirmAskInput,
|
|
13
|
+
confirmButtonLabels,
|
|
14
|
+
type FilesAskInput,
|
|
15
|
+
type FormAskInput,
|
|
16
|
+
type MarkdownAskInput,
|
|
17
|
+
visibleLabel,
|
|
18
|
+
} from "./schema";
|
|
19
|
+
import {validateAskResponse} from "./validateResponse";
|
|
20
|
+
|
|
21
|
+
export const SIMPLE_CARD_BUTTON_STYLES = ["default", "primary", "destructive", "cancel"] as const;
|
|
22
|
+
|
|
23
|
+
const ELLIPSIS = "…";
|
|
24
|
+
const HIGH_SURROGATE = /[\uD800-\uDBFF]$/;
|
|
25
|
+
const LAST_WORD = /\s\S*$/;
|
|
26
|
+
const WHITESPACE = /\s/;
|
|
27
|
+
const USE_DEFAULT_LABEL_MAX_LENGTH = ASK_LIMITS.simpleCard.buttonLabelMaxLength - 'Use ""'.length;
|
|
28
|
+
|
|
29
|
+
export const simpleCardButtonSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
id: z.string().min(1),
|
|
32
|
+
label: z.string().min(1).max(ASK_LIMITS.simpleCard.buttonLabelMaxLength),
|
|
33
|
+
response: askResponseSchema,
|
|
34
|
+
style: z.enum(SIMPLE_CARD_BUTTON_STYLES),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
|
|
38
|
+
export type SimpleCardButton = z.infer<typeof simpleCardButtonSchema>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The small-screen form of an ask: short text and at most three buttons, each carrying the exact
|
|
42
|
+
* answer it sends. `handoff` is true when the buttons cannot show every option the ask offers, so
|
|
43
|
+
* the user needs the full app to answer; a select-many ask always hands off, because one tap cannot
|
|
44
|
+
* pick several, and so do a markdown ask, because a draft cannot be edited there, a form ask,
|
|
45
|
+
* because fields cannot be filled in there, and a files ask, because files cannot be picked there.
|
|
46
|
+
* A card with a button for every option has
|
|
47
|
+
* `handoff: false` even when Skip is left out to make room.
|
|
48
|
+
*/
|
|
49
|
+
export const simpleCardSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
buttons: z.array(simpleCardButtonSchema).max(ASK_LIMITS.simpleCard.buttonsMax),
|
|
52
|
+
handoff: z.boolean(),
|
|
53
|
+
kind: z.enum(ASK_KINDS),
|
|
54
|
+
text: z.string().min(1).max(ASK_LIMITS.simpleCard.textMaxLength),
|
|
55
|
+
title: z.string().min(1).max(ASK_LIMITS.simpleCard.titleMaxLength).optional(),
|
|
56
|
+
toolCallId: z.string().min(1),
|
|
57
|
+
})
|
|
58
|
+
.strict()
|
|
59
|
+
.superRefine((card, ctx) => {
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
card.buttons.forEach((button, index) => {
|
|
62
|
+
if (!seen.has(button.id)) {
|
|
63
|
+
seen.add(button.id);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
ctx.addIssue(
|
|
67
|
+
askIssue({
|
|
68
|
+
code: "DUPLICATE_ID",
|
|
69
|
+
fix: "Give every button a unique id.",
|
|
70
|
+
message: `Button id ${quoteValue(button.id)} is used more than once.`,
|
|
71
|
+
segments: ["buttons", index, "id"],
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type SimpleCard = z.infer<typeof simpleCardSchema>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Cuts `text` to at most `maxLength` UTF-16 units, ending in "…". Prefers a word boundary when one
|
|
81
|
+
* falls in the second half of the kept text, and never splits a surrogate pair.
|
|
82
|
+
*/
|
|
83
|
+
const shorten = (text: string, maxLength: number): string => {
|
|
84
|
+
if (text.length <= maxLength) {
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
let kept = text.slice(0, maxLength - ELLIPSIS.length);
|
|
88
|
+
if (HIGH_SURROGATE.test(kept)) {
|
|
89
|
+
kept = kept.slice(0, -1);
|
|
90
|
+
}
|
|
91
|
+
if (!WHITESPACE.test(text.charAt(kept.length))) {
|
|
92
|
+
const boundary = kept.search(LAST_WORD);
|
|
93
|
+
if (boundary >= Math.floor(maxLength / 2)) {
|
|
94
|
+
kept = kept.slice(0, boundary);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return `${kept.trimEnd()}${ELLIPSIS}`;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** A label as its button shows it: without spaces at either end, cut to fit. */
|
|
101
|
+
const buttonLabel = (
|
|
102
|
+
label: string,
|
|
103
|
+
maxLength: number = ASK_LIMITS.simpleCard.buttonLabelMaxLength
|
|
104
|
+
): string => shorten(visibleLabel(label), maxLength);
|
|
105
|
+
|
|
106
|
+
const SKIP_BUTTON: SimpleCardButton = {
|
|
107
|
+
id: "skip",
|
|
108
|
+
label: "Skip",
|
|
109
|
+
response: {action: "decline"},
|
|
110
|
+
style: "cancel",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const acceptSelected = (id: string): AskResponse => ({
|
|
114
|
+
action: "accept",
|
|
115
|
+
content: {selected: [id]},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const optionButton = (option: ChoiceOption, isDefault: boolean): SimpleCardButton => ({
|
|
119
|
+
id: `option:${option.id}`,
|
|
120
|
+
label: buttonLabel(option.label),
|
|
121
|
+
response: acceptSelected(option.id),
|
|
122
|
+
style: isDefault ? "primary" : "default",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const hasDistinctLabels = (buttons: SimpleCardButton[]): boolean =>
|
|
126
|
+
new Set(buttons.map((button) => button.label)).size === buttons.length;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The shortcut to the default when the options don't fit. It is left out when its cut label also
|
|
130
|
+
* fits another option, because the user could not tell which option a tap picks.
|
|
131
|
+
*/
|
|
132
|
+
const useDefaultButtons = (
|
|
133
|
+
options: ChoiceOption[],
|
|
134
|
+
defaultOption: ChoiceOption | undefined
|
|
135
|
+
): SimpleCardButton[] => {
|
|
136
|
+
if (!defaultOption) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const label = buttonLabel(defaultOption.label, USE_DEFAULT_LABEL_MAX_LENGTH);
|
|
140
|
+
const namesAnotherOption = options.some(
|
|
141
|
+
(option) =>
|
|
142
|
+
option !== defaultOption && buttonLabel(option.label, USE_DEFAULT_LABEL_MAX_LENGTH) === label
|
|
143
|
+
);
|
|
144
|
+
if (namesAnotherOption) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
id: "use-default",
|
|
150
|
+
label: `Use "${label}"`,
|
|
151
|
+
response: acceptSelected(defaultOption.id),
|
|
152
|
+
style: "primary",
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The shortcut to a many-select's default, when the default is a valid answer on its own. The
|
|
159
|
+
* button cannot list every option it picks, so the card still hands off.
|
|
160
|
+
*/
|
|
161
|
+
const useSuggestedButtons = (input: ChoiceAskInput): SimpleCardButton[] => {
|
|
162
|
+
const defaults = input.default ?? [];
|
|
163
|
+
if (defaults.length === 0) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const response: AskResponse = {action: "accept", content: {selected: [...defaults]}};
|
|
167
|
+
if (validateAskResponse({input, kind: "choice", response}).length > 0) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
return [{id: "use-default", label: "Use suggested", response, style: "primary"}];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const choiceCard = (input: ChoiceAskInput): Pick<SimpleCard, "buttons" | "handoff"> => {
|
|
174
|
+
const {buttonsMax} = ASK_LIMITS.simpleCard;
|
|
175
|
+
const skipButtons = input.allowDecline === false ? [] : [SKIP_BUTTON];
|
|
176
|
+
if (input.select === "many") {
|
|
177
|
+
return {buttons: [...useSuggestedButtons(input), ...skipButtons], handoff: true};
|
|
178
|
+
}
|
|
179
|
+
const defaultOption = input.options.find((option) => option.id === input.default?.[0]);
|
|
180
|
+
if (input.options.length > buttonsMax) {
|
|
181
|
+
return {
|
|
182
|
+
buttons: [...useDefaultButtons(input.options, defaultOption), ...skipButtons],
|
|
183
|
+
handoff: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const defaultFirst = defaultOption
|
|
188
|
+
? [defaultOption, ...input.options.filter((option) => option !== defaultOption)]
|
|
189
|
+
: input.options;
|
|
190
|
+
const optionButtons = defaultFirst.map((option) =>
|
|
191
|
+
optionButton(option, option === defaultOption)
|
|
192
|
+
);
|
|
193
|
+
if (!hasDistinctLabels(optionButtons)) {
|
|
194
|
+
return {buttons: skipButtons, handoff: true};
|
|
195
|
+
}
|
|
196
|
+
const hasRoomForSkip = optionButtons.length < buttonsMax;
|
|
197
|
+
return {
|
|
198
|
+
buttons: hasRoomForSkip ? [...optionButtons, ...skipButtons] : optionButtons,
|
|
199
|
+
handoff: false,
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Approve first and deny last (D26). A destructive approve is styled `destructive`, so a watch's
|
|
205
|
+
* Double Tap, which presses the first non-destructive button, can only deny. Deny is the negative
|
|
206
|
+
* answer, so the card has no Skip even when the ask allows declining.
|
|
207
|
+
*/
|
|
208
|
+
const confirmCard = (input: ConfirmAskInput): Pick<SimpleCard, "buttons" | "handoff"> => {
|
|
209
|
+
const labels = confirmButtonLabels(input);
|
|
210
|
+
return {
|
|
211
|
+
buttons: [
|
|
212
|
+
{
|
|
213
|
+
id: "approve",
|
|
214
|
+
label: buttonLabel(labels.confirm),
|
|
215
|
+
response: {action: "accept", content: {confirmed: true}},
|
|
216
|
+
style: input.destructive === true ? "destructive" : "primary",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "deny",
|
|
220
|
+
label: buttonLabel(labels.deny),
|
|
221
|
+
response: {action: "accept", content: {confirmed: false}},
|
|
222
|
+
style: "cancel",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
handoff: false,
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A draft cannot be edited on a small screen, so the card always hands off. Approve draft sends the
|
|
231
|
+
* draft unchanged, and is left out when the draft breaks the ask's length rules. Cancel declines.
|
|
232
|
+
*/
|
|
233
|
+
const markdownCard = (input: MarkdownAskInput): Pick<SimpleCard, "buttons" | "handoff"> => {
|
|
234
|
+
const response: AskResponse = {
|
|
235
|
+
action: "accept",
|
|
236
|
+
content: {changed: false, markdown: input.initial ?? ""},
|
|
237
|
+
};
|
|
238
|
+
const isDraftValid = validateAskResponse({input, kind: "markdown", response}).length === 0;
|
|
239
|
+
const approveButtons: SimpleCardButton[] = isDraftValid
|
|
240
|
+
? [{id: "approve", label: "Approve draft", response, style: "primary"}]
|
|
241
|
+
: [];
|
|
242
|
+
const cancelButtons: SimpleCardButton[] =
|
|
243
|
+
input.allowDecline === false
|
|
244
|
+
? []
|
|
245
|
+
: [{id: "cancel", label: "Cancel", response: {action: "decline"}, style: "cancel"}];
|
|
246
|
+
return {buttons: [...approveButtons, ...cancelButtons], handoff: true};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Fields cannot be filled in on a small screen, so the card always hands off. Submit defaults sends
|
|
251
|
+
* the fields' defaults, and is offered only when the form has a default and every required field
|
|
252
|
+
* has one, so the answer is valid. Cancel declines.
|
|
253
|
+
*/
|
|
254
|
+
const formCard = (input: FormAskInput): Pick<SimpleCard, "buttons" | "handoff"> => {
|
|
255
|
+
const values = formDefaultValues(input);
|
|
256
|
+
const hasEveryRequiredDefault = input.fields.every(
|
|
257
|
+
(field) => field.required !== true || Object.hasOwn(values, field.id)
|
|
258
|
+
);
|
|
259
|
+
const response: AskResponse = {action: "accept", content: {values}};
|
|
260
|
+
const isSubmittable =
|
|
261
|
+
Object.keys(values).length > 0 &&
|
|
262
|
+
hasEveryRequiredDefault &&
|
|
263
|
+
validateAskResponse({input, kind: "form", response}).length === 0;
|
|
264
|
+
const submitButtons: SimpleCardButton[] = isSubmittable
|
|
265
|
+
? [{id: "submit-defaults", label: "Submit defaults", response, style: "primary"}]
|
|
266
|
+
: [];
|
|
267
|
+
const cancelButtons: SimpleCardButton[] =
|
|
268
|
+
input.allowDecline === false
|
|
269
|
+
? []
|
|
270
|
+
: [{id: "cancel", label: "Cancel", response: {action: "decline"}, style: "cancel"}];
|
|
271
|
+
return {buttons: [...submitButtons, ...cancelButtons], handoff: true};
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/** Files cannot be picked on a small screen, so the card always hands off. Skip declines. */
|
|
275
|
+
const filesCard = (input: FilesAskInput): Pick<SimpleCard, "buttons" | "handoff"> => ({
|
|
276
|
+
buttons: input.allowDecline === false ? [] : [SKIP_BUTTON],
|
|
277
|
+
handoff: true,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const kindCard = (ask: Ask): Pick<SimpleCard, "buttons" | "handoff"> => {
|
|
281
|
+
switch (ask.kind) {
|
|
282
|
+
case "choice":
|
|
283
|
+
return choiceCard(ask.input);
|
|
284
|
+
case "confirm":
|
|
285
|
+
return confirmCard(ask.input);
|
|
286
|
+
case "markdown":
|
|
287
|
+
return markdownCard(ask.input);
|
|
288
|
+
case "form":
|
|
289
|
+
return formCard(ask.input);
|
|
290
|
+
case "files":
|
|
291
|
+
return filesCard(ask.input);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The answer a pressed button sends: the `response` stored on the card's button with that id.
|
|
297
|
+
* Returns an `UNKNOWN_BUTTON` error instead when the card has no such button, so a small client can
|
|
298
|
+
* only send answers the card offered.
|
|
299
|
+
*/
|
|
300
|
+
export const resolveButtonAnswer = ({
|
|
301
|
+
buttonId,
|
|
302
|
+
card,
|
|
303
|
+
}: {
|
|
304
|
+
buttonId: string;
|
|
305
|
+
card: SimpleCard;
|
|
306
|
+
}): {errors: AskValidationError[]; response?: AskResponse} => {
|
|
307
|
+
const button = card.buttons.find((candidate) => candidate.id === buttonId);
|
|
308
|
+
if (button) {
|
|
309
|
+
return {errors: [], response: button.response};
|
|
310
|
+
}
|
|
311
|
+
const buttonIds = card.buttons.map((candidate) => quoteValue(candidate.id)).join(", ");
|
|
312
|
+
return {
|
|
313
|
+
errors: [
|
|
314
|
+
{
|
|
315
|
+
code: "UNKNOWN_BUTTON",
|
|
316
|
+
fix:
|
|
317
|
+
buttonIds === ""
|
|
318
|
+
? "The card has no buttons. Send a full askResponse instead."
|
|
319
|
+
: `Send the id of one of the card's buttons: ${buttonIds}.`,
|
|
320
|
+
message: `Button ${quoteValue(buttonId)} is not on the pending ask's simple card.`,
|
|
321
|
+
path: "buttonId",
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Derives the simple card for a valid ask. Pure and deterministic: the same ask always gives the
|
|
329
|
+
* same card, and every button's `response` passes `validateAskResponse` for that ask.
|
|
330
|
+
*/
|
|
331
|
+
export const toSimpleCard = ({
|
|
332
|
+
toolCallId,
|
|
333
|
+
...askFields
|
|
334
|
+
}: Ask & {toolCallId: string}): SimpleCard => {
|
|
335
|
+
const ask = askFields as Ask;
|
|
336
|
+
const {textMaxLength, titleMaxLength} = ASK_LIMITS.simpleCard;
|
|
337
|
+
const {input, kind} = ask;
|
|
338
|
+
const title = input.title === undefined ? {} : {title: shorten(input.title, titleMaxLength)};
|
|
339
|
+
const card = kindCard(ask);
|
|
340
|
+
return {...card, kind, text: shorten(input.prompt, textMaxLength), ...title, toolCallId};
|
|
341
|
+
};
|