@teaflask/assistant 0.7.0 → 0.8.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.
@@ -1,26 +1,15 @@
1
+ import { settledValueOf, theClickWouldSubmitOrLeaveThePage, theControlIsInert, typeLikeAUser, } from "./page-gestures.js";
2
+ import { CHECKBOX_VALUE_REASON, CONTROL_REDRAWN_REASON, EDITOR_UNAVAILABLE_REASON, FILE_FIELD_REASON, INERT_FIELD_REASON, MIRROR_INPUT_REASON, MULTI_CHOICE_REASON, noMatchingOptionReasonListing, NOT_FILLABLE_REASON, RADIO_VALUE_REASON, reportListingBudget, SECRET_FIELD_REASON, SUBMITTING_CONTROL_REASON, unavailableOptionReason, VALUE_DID_NOT_LAND_REASON, } from "./prefill-reasons.js";
1
3
  import { theFieldHoldsASecret } from "../reader/redaction.js";
2
- import { resolvedElementOfRef } from "../reader/snapshot.js";
3
- import { isContentEditableElement } from "../reader/roles.js";
4
- const SECRET_FIELD_REASON = "This field holds a secret (a password or payment detail) — the user " +
5
- "must type it themselves.";
6
- const FILE_FIELD_REASON = "File pickers cannot be filled by the assistant — the user must choose " +
7
- "the file themselves.";
8
- const INERT_FIELD_REASON = "This field is disabled or read-only and cannot take a value right now.";
9
- const NOT_FILLABLE_REASON = "This element is not a fillable form field — take a fresh read and " +
10
- "pick a textbox, dropdown, checkbox, or editor ref.";
11
- const CHECKBOX_VALUE_REASON = 'A checkbox takes "true" or "false", nothing else.';
12
- const RADIO_VALUE_REASON = 'A radio button can only be selected — pass "true" on the option to ' +
13
- "select instead.";
14
- const NO_MATCHING_OPTION_REASON = "No option in this dropdown matches that value — read the page to see " +
15
- "the options it offers.";
16
- const VALUE_DID_NOT_LAND_REASON = "The page's own scripts rejected this value — it did not stay in the " +
17
- "field.";
18
- const EDITOR_UNAVAILABLE_REASON = "This editor cannot be filled on this page — tell the user what to " +
19
- "type instead.";
20
- export function prefilledFieldsReport(fields) {
4
+ import { renderedOptionTextOf } from "../reader/render.js";
5
+ import { lastReadIgnoreSelectors, resolvedElementOfRef, } from "../reader/snapshot.js";
6
+ import { theElementSitsInACuratedSubtree } from "../reader/snapshot-root.js";
7
+ import { isContentEditableElement, roleOf, theControlIsDisabled, } from "../reader/roles.js";
8
+ export async function prefilledFieldsReport(fields, probe) {
21
9
  const report = { filled: [], refused: [] };
10
+ const listingBudget = reportListingBudget();
22
11
  for (const field of fields) {
23
- const refusal = _filledFieldRefusalOf(field);
12
+ const refusal = await _filledFieldRefusalOf(field, probe, listingBudget);
24
13
  if (refusal === null) {
25
14
  report.filled.push(field.refId);
26
15
  }
@@ -30,26 +19,52 @@ export function prefilledFieldsReport(fields) {
30
19
  }
31
20
  return report;
32
21
  }
33
- function _filledFieldRefusalOf(field) {
22
+ async function _filledFieldRefusalOf(field, probe, listingBudget) {
34
23
  const resolved = resolvedElementOfRef(field.refId);
35
24
  if (resolved.error !== null) {
36
25
  return resolved.error;
37
26
  }
38
27
  const element = resolved.element;
39
28
  if (element instanceof HTMLInputElement) {
40
- return _inputRefusalOf(element, field.value);
29
+ return _inputRefusalOf(element, field.value, probe, listingBudget);
41
30
  }
42
31
  if (element instanceof HTMLTextAreaElement) {
43
32
  return _textAreaRefusalOf(element, field.value);
44
33
  }
45
34
  if (element instanceof HTMLSelectElement) {
46
- return _selectRefusalOf(element, field.value);
35
+ return _selectRefusalOf(element, field.value, listingBudget);
47
36
  }
48
37
  if (isContentEditableElement(element)) {
49
38
  return _editorRefusalOf(element, field.value);
50
39
  }
40
+ if (_isAriaToggle(element)) {
41
+ const { ariaToggleRefusalOf } = await import("./aria-toggles.js");
42
+ return ariaToggleRefusalOf(element, field.value);
43
+ }
44
+ if (_isCustomDropdown(element)) {
45
+ const { customDropdownRefusalOf } = await import("./aria-dropdowns.js");
46
+ return customDropdownRefusalOf(element, field.value, probe, listingBudget);
47
+ }
48
+ if (_isStandingListbox(element)) {
49
+ const { listboxPickRefusalOf } = await import("./aria-dropdowns.js");
50
+ return listboxPickRefusalOf(element, field.value, probe, listingBudget);
51
+ }
51
52
  return NOT_FILLABLE_REASON;
52
53
  }
54
+ const _TOGGLE_ROLES = new Set(["checkbox", "radio", "switch"]);
55
+ function _isAriaToggle(element) {
56
+ return _TOGGLE_ROLES.has(roleOf(element));
57
+ }
58
+ function _isCustomDropdown(element) {
59
+ if (element instanceof HTMLSelectElement) {
60
+ return false;
61
+ }
62
+ return (roleOf(element) === "combobox" ||
63
+ (element.getAttribute("aria-haspopup") ?? "").toLowerCase() === "listbox");
64
+ }
65
+ function _isStandingListbox(element) {
66
+ return roleOf(element) === "listbox";
67
+ }
53
68
  const _NOT_A_FIELD_INPUT_TYPES = new Set([
54
69
  "hidden",
55
70
  "submit",
@@ -57,7 +72,7 @@ const _NOT_A_FIELD_INPUT_TYPES = new Set([
57
72
  "reset",
58
73
  "image",
59
74
  ]);
60
- function _inputRefusalOf(input, value) {
75
+ async function _inputRefusalOf(input, value, probe, listingBudget) {
61
76
  if (input.type === "password" || theFieldHoldsASecret(input)) {
62
77
  return SECRET_FIELD_REASON;
63
78
  }
@@ -67,59 +82,105 @@ function _inputRefusalOf(input, value) {
67
82
  if (_NOT_A_FIELD_INPUT_TYPES.has(input.type)) {
68
83
  return NOT_FILLABLE_REASON;
69
84
  }
70
- if (input.disabled || input.readOnly) {
85
+ const isComboboxInput = roleOf(input) === "combobox";
86
+ if (theControlIsInert(input) ||
87
+ input.disabled ||
88
+ (input.readOnly && !isComboboxInput)) {
71
89
  return INERT_FIELD_REASON;
72
90
  }
73
91
  if (input.type === "checkbox" || input.type === "radio") {
92
+ if (input.getAttribute("aria-hidden") === "true") {
93
+ return MIRROR_INPUT_REASON;
94
+ }
74
95
  return _checkedRefusalOf(input, value);
75
96
  }
76
- _setThroughThePrototype(input, value);
77
- _announceTheEdit(input);
78
- return input.value === value ? null : VALUE_DID_NOT_LAND_REASON;
97
+ if (isComboboxInput) {
98
+ const { customDropdownRefusalOf } = await import("./aria-dropdowns.js");
99
+ return customDropdownRefusalOf(input, value, probe, listingBudget, async (fallbackReason) => input.readOnly
100
+ ? fallbackReason
101
+ : _typedValueRefusalOf(input, value, () => input.value));
102
+ }
103
+ return _typedValueRefusalOf(input, value, () => input.value);
79
104
  }
80
- function _checkedRefusalOf(input, value) {
81
- if (value !== "true" && value !== "false") {
82
- return CHECKBOX_VALUE_REASON;
105
+ async function _typedValueRefusalOf(control, value, read) {
106
+ typeLikeAUser(control, value);
107
+ const landed = (await settledValueOf(read)) === value;
108
+ if (!control.isConnected) {
109
+ return CONTROL_REDRAWN_REASON;
83
110
  }
84
- if (input.type === "radio" && value === "false") {
111
+ return landed ? null : VALUE_DID_NOT_LAND_REASON;
112
+ }
113
+ async function _checkedRefusalOf(input, value) {
114
+ if (input.type === "radio" && value !== "true") {
85
115
  return RADIO_VALUE_REASON;
86
116
  }
117
+ if (value !== "true" && value !== "false") {
118
+ return CHECKBOX_VALUE_REASON;
119
+ }
87
120
  const desired = value === "true";
88
- if (input.checked !== desired) {
89
- input.click();
121
+ if (input.checked === desired) {
122
+ return null;
123
+ }
124
+ if (theClickWouldSubmitOrLeaveThePage(input)) {
125
+ return SUBMITTING_CONTROL_REASON;
90
126
  }
91
- return input.checked === desired ? null : VALUE_DID_NOT_LAND_REASON;
127
+ input.click();
128
+ const landed = (await settledValueOf(() => input.checked)) === desired;
129
+ if (!input.isConnected) {
130
+ return CONTROL_REDRAWN_REASON;
131
+ }
132
+ return landed ? null : VALUE_DID_NOT_LAND_REASON;
92
133
  }
93
- function _textAreaRefusalOf(textArea, value) {
134
+ async function _textAreaRefusalOf(textArea, value) {
94
135
  if (theFieldHoldsASecret(textArea)) {
95
136
  return SECRET_FIELD_REASON;
96
137
  }
97
- if (textArea.disabled || textArea.readOnly) {
138
+ if (theControlIsInert(textArea) || textArea.disabled || textArea.readOnly) {
98
139
  return INERT_FIELD_REASON;
99
140
  }
100
- _setThroughThePrototype(textArea, value);
101
- _announceTheEdit(textArea);
102
- return textArea.value === value ? null : VALUE_DID_NOT_LAND_REASON;
141
+ return _typedValueRefusalOf(textArea, value, () => textArea.value);
103
142
  }
104
- function _selectRefusalOf(select, value) {
105
- if (select.disabled) {
143
+ async function _selectRefusalOf(select, value, listingBudget) {
144
+ if (theControlIsInert(select) || select.disabled) {
106
145
  return INERT_FIELD_REASON;
107
146
  }
147
+ if (select.multiple) {
148
+ return MULTI_CHOICE_REASON;
149
+ }
108
150
  const option = _optionMatching(select, value);
109
151
  if (option === null) {
110
- return NO_MATCHING_OPTION_REASON;
152
+ const labels = _matchableOptionsOf(select)
153
+ .filter((candidate) => !theControlIsDisabled(candidate, "option"))
154
+ .map((candidate) => candidate.label.trim());
155
+ return noMatchingOptionReasonListing(labels, listingBudget);
156
+ }
157
+ if (select.value === option.value) {
158
+ return null;
111
159
  }
112
- _setThroughThePrototype(select, option.value);
113
- _announceTheEdit(select);
114
- return select.value === option.value ? null : VALUE_DID_NOT_LAND_REASON;
160
+ if (theControlIsDisabled(option, "option")) {
161
+ return unavailableOptionReason(option.label.trim());
162
+ }
163
+ return _typedValueRefusalOf(select, option.value, () => select.value);
115
164
  }
116
165
  function _optionMatching(select, value) {
117
- const options = Array.from(select.options);
166
+ const options = _matchableOptionsOf(select);
118
167
  return (options.find((option) => option.value === value) ??
119
168
  options.find((option) => option.label.trim() === value.trim()) ??
169
+ options.find((option) => {
170
+ const shownToTheModel = renderedOptionTextOf(option);
171
+ const full = option.text.replace(/\s+/g, " ").trim();
172
+ return (shownToTheModel !== full && shownToTheModel.trimEnd() === value.trim());
173
+ }) ??
120
174
  null);
121
175
  }
122
- function _editorRefusalOf(element, value) {
176
+ function _matchableOptionsOf(select) {
177
+ const ignoreSelectors = lastReadIgnoreSelectors();
178
+ return Array.from(select.options).filter((option) => !theElementSitsInACuratedSubtree(option, ignoreSelectors));
179
+ }
180
+ async function _editorRefusalOf(element, value) {
181
+ if (theControlIsInert(element)) {
182
+ return INERT_FIELD_REASON;
183
+ }
123
184
  if (!(element instanceof HTMLElement)) {
124
185
  return EDITOR_UNAVAILABLE_REASON;
125
186
  }
@@ -133,19 +194,9 @@ function _editorRefusalOf(element, value) {
133
194
  }
134
195
  selection.selectAllChildren(element);
135
196
  document.execCommand("insertText", false, value);
136
- const landed = element.textContent.includes(value);
137
- return landed ? null : VALUE_DID_NOT_LAND_REASON;
138
- }
139
- function _setThroughThePrototype(control, value) {
140
- const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(control), "value");
141
- if (descriptor?.set !== undefined) {
142
- descriptor.set.call(control, value);
197
+ const landed = await settledValueOf(() => element.textContent.includes(value));
198
+ if (!element.isConnected) {
199
+ return CONTROL_REDRAWN_REASON;
143
200
  }
144
- else {
145
- control.value = value;
146
- }
147
- }
148
- function _announceTheEdit(control) {
149
- control.dispatchEvent(new Event("input", { bubbles: true }));
150
- control.dispatchEvent(new Event("change", { bubbles: true }));
201
+ return landed ? null : VALUE_DID_NOT_LAND_REASON;
151
202
  }
@@ -0,0 +1,43 @@
1
+ import { prettyPrintParameters } from "../pretty-print.js";
2
+ import { CARD_BLOCK_CLASS } from "./parts-classes.js";
3
+ import { el, factRows, recordOf, scrollRegion } from "./view-dom.js";
4
+ const VALUE_PANE_CLASS = "tf:m-0 tf:whitespace-pre-wrap tf:font-mono tf:text-xs tf:text-tf-card-foreground";
5
+ export function valueRows(value) {
6
+ const record = recordOf(value);
7
+ if (record !== null) {
8
+ return scrollRegion([
9
+ factRows(Object.entries(record).map(([key, entry]) => [key, _valueOf(entry)])),
10
+ ]);
11
+ }
12
+ return valueBlock(value);
13
+ }
14
+ export function valueBlock(value) {
15
+ const wrapper = el("div", CARD_BLOCK_CLASS);
16
+ wrapper.dataset.tfActionValueBlock = "";
17
+ wrapper.append(el("pre", VALUE_PANE_CLASS, prettyPrintParameters(value)));
18
+ return scrollRegion([wrapper]);
19
+ }
20
+ function _valueOf(value) {
21
+ if (typeof value === "string") {
22
+ return value === "" ? "—" : value;
23
+ }
24
+ if (typeof value === "number" ||
25
+ typeof value === "boolean" ||
26
+ typeof value === "bigint") {
27
+ return String(value);
28
+ }
29
+ if (value === null || value === undefined) {
30
+ return "—";
31
+ }
32
+ return el("pre", VALUE_PANE_CLASS, prettyPrintParameters(value));
33
+ }
34
+ export function statusBadgeOf(status) {
35
+ const badge = el("span", status >= 400
36
+ ? "tf:font-medium tf:text-tf-destructive"
37
+ : "tf:text-tf-muted-foreground", `HTTP ${String(status)}`);
38
+ badge.dataset.tfActionStatus = String(status);
39
+ return badge;
40
+ }
41
+ export function isEmptyBody(body) {
42
+ return body === undefined || body === null || body === "";
43
+ }
@@ -1,12 +1,10 @@
1
- import { prettyPrintParameters } from "../pretty-print.js";
2
- import { CARD_BLOCK_CLASS } from "./parts-classes.js";
3
- import { actionEnvelopeOf, actionFailureOf, captionRow, card, el, factRows, note, recordOf, scrollRegion, settledOutcomeNotesOf, settledWithResult, statelessToolView, } from "./view-dom.js";
1
+ import { isEmptyBody, statusBadgeOf, valueRows } from "./action-values.js";
2
+ import { actionEnvelopeOf, actionFailureOf, captionRow, card, el, factRows, note, recordOf, settledOutcomeNotesOf, settledWithResult, statelessToolView, } from "./view-dom.js";
4
3
  const REQUEST_SECTIONS = [
5
4
  ["path_params", "Path parameters"],
6
5
  ["query", "Query"],
7
6
  ["body", "Body"],
8
7
  ];
9
- const VALUE_PANE_CLASS = "tf:m-0 tf:whitespace-pre-wrap tf:font-mono tf:text-xs tf:text-tf-card-foreground";
10
8
  export const actionToolView = statelessToolView(_renderAction);
11
9
  function _renderAction({ call }) {
12
10
  const root = el("div", "tf:flex tf:flex-col tf:gap-2");
@@ -21,7 +19,7 @@ function _renderAction({ call }) {
21
19
  if (record !== null && Object.keys(record).length === 0) {
22
20
  continue;
23
21
  }
24
- sections.push(captionRow(label), _valueRows(value));
22
+ sections.push(captionRow(label), valueRows(value));
25
23
  }
26
24
  if (sections.length > 0) {
27
25
  root.append(card(sections));
@@ -52,32 +50,6 @@ function _appendFieldProblems(root, call) {
52
50
  }
53
51
  root.append(card(parts));
54
52
  }
55
- function _valueRows(value) {
56
- const record = recordOf(value);
57
- if (record !== null) {
58
- return scrollRegion([
59
- factRows(Object.entries(record).map(([key, entry]) => [key, _valueOf(entry)])),
60
- ]);
61
- }
62
- const wrapper = el("div", CARD_BLOCK_CLASS);
63
- wrapper.dataset.tfActionValueBlock = "";
64
- wrapper.append(el("pre", VALUE_PANE_CLASS, prettyPrintParameters(value)));
65
- return scrollRegion([wrapper]);
66
- }
67
- function _valueOf(value) {
68
- if (typeof value === "string") {
69
- return value === "" ? "—" : value;
70
- }
71
- if (typeof value === "number" ||
72
- typeof value === "boolean" ||
73
- typeof value === "bigint") {
74
- return String(value);
75
- }
76
- if (value === null || value === undefined) {
77
- return "—";
78
- }
79
- return el("pre", VALUE_PANE_CLASS, prettyPrintParameters(value));
80
- }
81
53
  function _appendResponse(root, call) {
82
54
  if (call.resultText === undefined) {
83
55
  return;
@@ -88,24 +60,16 @@ function _appendResponse(root, call) {
88
60
  return;
89
61
  }
90
62
  const rows = [];
91
- let status;
92
- if (envelope.status !== null) {
93
- status = el("span", envelope.status >= 400
94
- ? "tf:font-medium tf:text-tf-destructive"
95
- : "tf:text-tf-muted-foreground", `HTTP ${String(envelope.status)}`);
96
- status.dataset.tfActionStatus = String(envelope.status);
97
- }
63
+ const status = envelope.status === null ? undefined : statusBadgeOf(envelope.status);
98
64
  rows.push(captionRow(envelope.truncated ? "Response (truncated)" : "Response", status));
99
65
  if (envelope.truncated) {
100
66
  rows.push(captionRow("The response body was truncated by the client to fit the size cap."));
101
67
  }
102
- if (envelope.body === undefined ||
103
- envelope.body === null ||
104
- envelope.body === "") {
68
+ if (isEmptyBody(envelope.body)) {
105
69
  rows.push(captionRow("Empty response body."));
106
70
  }
107
71
  else {
108
- rows.push(_valueRows(envelope.body));
72
+ rows.push(valueRows(envelope.body));
109
73
  }
110
74
  root.append(card(rows));
111
75
  }
@@ -0,0 +1,95 @@
1
+ import { prettyPrintParameters } from "../pretty-print.js";
2
+ import { isEmptyBody, statusBadgeOf, valueBlock, valueRows, } from "./action-values.js";
3
+ import { actionEnvelopeOf, captionRow, card, el, factRows, note, recordOf, settledOutcomeNotesOf, scrollRegion, settledWithResult, statelessToolView, } from "./view-dom.js";
4
+ export const graphqlActionToolView = statelessToolView(_renderGraphQLAction);
5
+ function _renderGraphQLAction({ call }) {
6
+ const root = el("div", "tf:flex tf:flex-col tf:gap-2");
7
+ root.dataset.tfGraphqlActionView = "";
8
+ const variables = recordOf(call.args.variables);
9
+ if (variables !== null && Object.keys(variables).length > 0) {
10
+ root.append(card([captionRow("Variables"), _valuesOf(variables)]));
11
+ }
12
+ root.append(...settledOutcomeNotesOf(call));
13
+ if (settledWithResult(call)) {
14
+ _appendResponse(root, call);
15
+ }
16
+ return root;
17
+ }
18
+ function _appendResponse(root, call) {
19
+ const envelope = actionEnvelopeOf(call.resultText);
20
+ if (envelope === null) {
21
+ root.append(note("The response couldn't be read as an action result and isn't rendered."));
22
+ return;
23
+ }
24
+ const response = recordOf(envelope.body);
25
+ const errors = Array.isArray(response?.errors) ? response.errors : [];
26
+ const graphqlShaped = _readsAsGraphQLResponse(response, errors);
27
+ const rows = [
28
+ captionRow(envelope.truncated ? "Response (truncated)" : "Response", _statusOf(envelope.status)),
29
+ ];
30
+ if (envelope.truncated) {
31
+ rows.push(captionRow(_cutSentenceOf(graphqlShaped, errors, response)));
32
+ }
33
+ if (response === null || !graphqlShaped) {
34
+ rows.push(isEmptyBody(envelope.body)
35
+ ? captionRow("Empty response body.")
36
+ : valueRows(envelope.body));
37
+ root.append(card(rows));
38
+ return;
39
+ }
40
+ if (errors.length > 0) {
41
+ rows.push(_errorsCaption(errors.length), _errorRows(errors));
42
+ }
43
+ const hasData = response.data !== null && response.data !== undefined;
44
+ rows.push(hasData ? captionRow("Data") : captionRow("Data", "none"));
45
+ if (hasData) {
46
+ rows.push(_valuesOf(_withoutCutMarker(response.data)));
47
+ }
48
+ root.append(card(rows));
49
+ }
50
+ function _readsAsGraphQLResponse(response, errors) {
51
+ return response !== null && (errors.length > 0 || "data" in response);
52
+ }
53
+ function _statusOf(status) {
54
+ return status === null || status === 200 ? undefined : statusBadgeOf(status);
55
+ }
56
+ function _cutSentenceOf(graphqlShaped, errors, response) {
57
+ if (!graphqlShaped || errors.length === 0) {
58
+ return "Too large to show in full: the response is shortened.";
59
+ }
60
+ return typeof response?.data === "string"
61
+ ? "Too large to show in full: the data is shortened and any other response fields dropped; the errors are complete."
62
+ : "Too large to show in full: other response fields are dropped; the data and errors are complete.";
63
+ }
64
+ function _errorsCaption(count) {
65
+ const row = captionRow("");
66
+ row.replaceChildren(el("span", "tf:font-medium tf:text-tf-destructive", `${String(count)} ${count === 1 ? "error" : "errors"}`));
67
+ return row;
68
+ }
69
+ function _errorRows(errors) {
70
+ const rows = factRows(errors.map((error) => {
71
+ const record = recordOf(error);
72
+ const path = Array.isArray(record?.path)
73
+ ? record.path.map(String).join(".")
74
+ : "";
75
+ const message = typeof record?.message === "string" && record.message !== ""
76
+ ? record.message
77
+ : prettyPrintParameters(error);
78
+ return [
79
+ path === "" ? "—" : path.replaceAll(".", ".\u200b"),
80
+ message,
81
+ ];
82
+ }));
83
+ rows.dataset.tfGraphqlErrors = String(errors.length);
84
+ return scrollRegion([rows]);
85
+ }
86
+ function _valuesOf(value) {
87
+ const record = recordOf(value);
88
+ const flat = record !== null &&
89
+ Object.values(record).every((entry) => entry === null || typeof entry !== "object");
90
+ return flat ? valueRows(value) : valueBlock(value);
91
+ }
92
+ const CUT_MARKER = /… \[truncated: [^\]]*\]$/;
93
+ function _withoutCutMarker(data) {
94
+ return typeof data === "string" ? data.replace(CUT_MARKER, "…") : data;
95
+ }
@@ -39,9 +39,41 @@ export interface ActionIntent {
39
39
  query: Record<string, unknown>;
40
40
  /**
41
41
  * The JSON request body; absent when the action sends none. The cookies and
42
- * headers adapters send it with Content-Type application/json.
42
+ * headers adapters send it with Content-Type application/json. For a
43
+ * GraphQL action it is the standard GraphQL request, {query, operationName,
44
+ * variables}.
43
45
  */
44
46
  body?: unknown;
47
+ /**
48
+ * Present when the action is an operation of your app's GraphQL API, so a
49
+ * request adapter can run it through your own GraphQL client; absent for
50
+ * every other action.
51
+ */
52
+ graphql?: ActionIntentGraphQL;
53
+ }
54
+ /**
55
+ * The GraphQL operation an action runs, one your app's own code defines. Run it
56
+ * through your app's GraphQL client. Looking it up by operationName among your
57
+ * app's own operations sends exactly the request your app sends; parsing
58
+ * document lets a client print it again, which a persisted-query allowlist may
59
+ * refuse.
60
+ */
61
+ export interface ActionIntentGraphQL {
62
+ /** The operation's name in your app's code, such as "RenamePost". */
63
+ operationName: string;
64
+ /** Whether the operation is a "query" or a "mutation". */
65
+ operationType: "query" | "mutation";
66
+ /**
67
+ * The operation's GraphQL document as found in your app's code, with every
68
+ * fragment it uses; a client that sends parsed documents, such as Apollo
69
+ * Client, can parse and run it.
70
+ */
71
+ document: string;
72
+ /**
73
+ * The operation's variables, already validated against the action's
74
+ * variables schema; empty when it takes none.
75
+ */
76
+ variables: Record<string, unknown>;
45
77
  }
46
78
  /**
47
79
  * For APIs authenticated by session cookies. The package fetches baseUrl plus
@@ -88,8 +120,10 @@ export interface RequestActionsAdapter {
88
120
  kind: "request";
89
121
  /**
90
122
  * Performs the request and resolves with your API's response body, which
91
- * the agent receives with status 200. Reject (for example by throwing on a
92
- * non-2xx response) to report the action as failed.
123
+ * the agent receives with status 200. For a GraphQL action, resolve the
124
+ * whole GraphQL response, {data, errors}, even when it carries errors.
125
+ * Reject (for example by throwing on a non-2xx response) to report the
126
+ * action as failed.
93
127
  */
94
128
  execute: (intent: ActionIntent) => Promise<unknown>;
95
129
  }
@@ -99,6 +133,10 @@ export interface RequestActionsAdapter {
99
133
  * actions and tells the user to do them. The response's status and body (parsed
100
134
  * as JSON, otherwise text) go back to the agent; a non-2xx status counts as
101
135
  * your API's answer, not a failure, and bodies over about 16,000 characters are
102
- * truncated.
136
+ * truncated. A GraphQL response that carries errors keeps all of them unless
137
+ * the errors alone exceed the limit: its other top-level fields such as
138
+ * extensions are dropped first, and its data is shortened only if it still does
139
+ * not fit. A GraphQL response without errors is truncated whole, like any other
140
+ * body.
103
141
  */
104
142
  export type ActionsAdapter = CookiesActionsAdapter | HeadersActionsAdapter | RequestActionsAdapter;
@@ -55,7 +55,7 @@ export function executionHandlersFor(navigate, executeActionIntent, threadId) {
55
55
  }
56
56
  handlers.set(READ_PAGE_REQUEST_KIND, (entry) => _readPageOutcomeOf(threadId, entry));
57
57
  handlers.set(HIGHLIGHT_REQUEST_KIND, (entry) => Promise.resolve(_highlightOutcomeOf(entry)));
58
- handlers.set(PREFILL_FORM_REQUEST_KIND, (entry) => Promise.resolve(_prefillFormOutcomeOf(entry)));
58
+ handlers.set(PREFILL_FORM_REQUEST_KIND, (entry) => _prefillFormOutcomeOf(entry));
59
59
  return handlers;
60
60
  }
61
61
  async function _navigateOutcomeOf(navigate, entry) {
@@ -316,13 +316,13 @@ function _highlightFailedOutcome(error) {
316
316
  },
317
317
  };
318
318
  }
319
- function _prefillFormOutcomeOf(entry) {
319
+ async function _prefillFormOutcomeOf(entry) {
320
320
  const fields = _prefillFieldsOf(entry.action);
321
321
  if (fields === null) {
322
322
  return UNUSABLE_FIELDS_OUTCOME;
323
323
  }
324
324
  try {
325
- const result = prefilledFieldsReport(fields);
325
+ const result = await prefilledFieldsReport(fields, browserLayoutProbe());
326
326
  return { ok: true, result: { ...result } };
327
327
  }
328
328
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import { actionToolView } from "../components/tool-views/action-view.js";
2
+ import { graphqlActionToolView } from "../components/tool-views/graphql-action-view.js";
2
3
  import { commandToolView } from "../components/tool-views/command-view.js";
3
4
  import { docsSearchToolView } from "../components/tool-views/docs-search-view.js";
4
5
  import { fileEditToolView } from "../components/tool-views/file-edit-view.js";
@@ -10,6 +11,7 @@ export function isReservedToolViewKey(key) {
10
11
  export const PACKAGE_TOOL_VIEWS = Object.freeze({
11
12
  "teaflask.questions": { version: 1, view: questionsToolView },
12
13
  "teaflask.action": { version: 1, view: actionToolView },
14
+ "teaflask.graphql-action": { version: 1, view: graphqlActionToolView },
13
15
  "teaflask.command": { version: 1, view: commandToolView },
14
16
  "teaflask.file-edit": { version: 1, view: fileEditToolView },
15
17
  "teaflask.docs-search": { version: 1, view: docsSearchToolView },
@@ -90,6 +90,9 @@ export declare const ApiErrorCode: {
90
90
  readonly ORG_ACTION_PROPOSAL_CHANGED: "ORG_ACTION_PROPOSAL_CHANGED";
91
91
  readonly ORG_ACTION_PROPOSAL_NOT_EDITABLE: "ORG_ACTION_PROPOSAL_NOT_EDITABLE";
92
92
  readonly ORG_ACTION_SCAN_ALREADY_RUNNING: "ORG_ACTION_SCAN_ALREADY_RUNNING";
93
+ readonly ORG_ACTION_SPEC_UPLOADS_NOT_AVAILABLE: "ORG_ACTION_SPEC_UPLOADS_NOT_AVAILABLE";
94
+ readonly ORG_ACTION_SPEC_UPLOAD_INVALID: "ORG_ACTION_SPEC_UPLOAD_INVALID";
95
+ readonly ORG_ACTION_SPEC_UPLOAD_TOO_LARGE: "ORG_ACTION_SPEC_UPLOAD_TOO_LARGE";
93
96
  readonly ORG_SITEMAP_PAGE_INVALID: "ORG_SITEMAP_PAGE_INVALID";
94
97
  readonly ORG_SITEMAP_ROUTE_ALREADY_REGISTERED: "ORG_SITEMAP_ROUTE_ALREADY_REGISTERED";
95
98
  readonly ORG_SITEMAP_PAGE_ALREADY_ARCHIVED: "ORG_SITEMAP_PAGE_ALREADY_ARCHIVED";
@@ -141,6 +144,12 @@ export declare const ApiErrorCode: {
141
144
  readonly SSO_REQUIRED_FOR_DOMAIN: "SSO_REQUIRED_FOR_DOMAIN";
142
145
  readonly SSO_CONNECTION_NOT_ACTIVE: "SSO_CONNECTION_NOT_ACTIVE";
143
146
  readonly EDGE_ORIGIN_REQUIRED: "EDGE_ORIGIN_REQUIRED";
147
+ readonly MCP_CONNECTIONS_NOT_AVAILABLE: "MCP_CONNECTIONS_NOT_AVAILABLE";
148
+ readonly MCP_AUTHORIZATION_EXPIRED: "MCP_AUTHORIZATION_EXPIRED";
149
+ readonly MCP_CAPABILITY_ABOVE_ROLE: "MCP_CAPABILITY_ABOVE_ROLE";
150
+ readonly MCP_CAPABILITIES_INVALID: "MCP_CAPABILITIES_INVALID";
151
+ readonly MCP_AUTHORIZATION_SERVER_UNAVAILABLE: "MCP_AUTHORIZATION_SERVER_UNAVAILABLE";
152
+ readonly MCP_CONNECTION_CONFLICT: "MCP_CONNECTION_CONFLICT";
144
153
  readonly DEMO_ERROR: "DEMO_ERROR";
145
154
  readonly HTTP_ERROR: "HTTP_ERROR";
146
155
  readonly INTERNAL_ERROR: "INTERNAL_ERROR";
@@ -89,6 +89,9 @@ export const ApiErrorCode = {
89
89
  ORG_ACTION_PROPOSAL_CHANGED: "ORG_ACTION_PROPOSAL_CHANGED",
90
90
  ORG_ACTION_PROPOSAL_NOT_EDITABLE: "ORG_ACTION_PROPOSAL_NOT_EDITABLE",
91
91
  ORG_ACTION_SCAN_ALREADY_RUNNING: "ORG_ACTION_SCAN_ALREADY_RUNNING",
92
+ ORG_ACTION_SPEC_UPLOADS_NOT_AVAILABLE: "ORG_ACTION_SPEC_UPLOADS_NOT_AVAILABLE",
93
+ ORG_ACTION_SPEC_UPLOAD_INVALID: "ORG_ACTION_SPEC_UPLOAD_INVALID",
94
+ ORG_ACTION_SPEC_UPLOAD_TOO_LARGE: "ORG_ACTION_SPEC_UPLOAD_TOO_LARGE",
92
95
  ORG_SITEMAP_PAGE_INVALID: "ORG_SITEMAP_PAGE_INVALID",
93
96
  ORG_SITEMAP_ROUTE_ALREADY_REGISTERED: "ORG_SITEMAP_ROUTE_ALREADY_REGISTERED",
94
97
  ORG_SITEMAP_PAGE_ALREADY_ARCHIVED: "ORG_SITEMAP_PAGE_ALREADY_ARCHIVED",
@@ -140,6 +143,12 @@ export const ApiErrorCode = {
140
143
  SSO_REQUIRED_FOR_DOMAIN: "SSO_REQUIRED_FOR_DOMAIN",
141
144
  SSO_CONNECTION_NOT_ACTIVE: "SSO_CONNECTION_NOT_ACTIVE",
142
145
  EDGE_ORIGIN_REQUIRED: "EDGE_ORIGIN_REQUIRED",
146
+ MCP_CONNECTIONS_NOT_AVAILABLE: "MCP_CONNECTIONS_NOT_AVAILABLE",
147
+ MCP_AUTHORIZATION_EXPIRED: "MCP_AUTHORIZATION_EXPIRED",
148
+ MCP_CAPABILITY_ABOVE_ROLE: "MCP_CAPABILITY_ABOVE_ROLE",
149
+ MCP_CAPABILITIES_INVALID: "MCP_CAPABILITIES_INVALID",
150
+ MCP_AUTHORIZATION_SERVER_UNAVAILABLE: "MCP_AUTHORIZATION_SERVER_UNAVAILABLE",
151
+ MCP_CONNECTION_CONFLICT: "MCP_CONNECTION_CONFLICT",
143
152
  DEMO_ERROR: "DEMO_ERROR",
144
153
  HTTP_ERROR: "HTTP_ERROR",
145
154
  INTERNAL_ERROR: "INTERNAL_ERROR",
@@ -1,6 +1,6 @@
1
1
  export { TeaflaskAssistantProvider, type TeaflaskAssistantAnonymousProps, type TeaflaskAssistantIdentityProps, type TeaflaskAssistantProviderBaseProps, type TeaflaskAssistantProviderProps, } from "./components/teaflask-assistant-provider.js";
2
2
  export type { AssistantTheme, AssistantThemeDarkTokens, AssistantThemeMode, AssistantThemeTokens, } from "./appearance/theme.js";
3
- export type { ActionsAdapter, ActionIntent, ActionIntentAction, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
3
+ export type { ActionsAdapter, ActionIntent, ActionIntentAction, ActionIntentGraphQL, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
4
4
  export type { ApprovalSubmitTelemetryEvent, AssistantTelemetryEvent, StreamFailedTelemetryEvent, } from "./contract/telemetry.js";
5
5
  export type { AssistantSuggestion, AssistantSuggestionInput, AssistantSuggestionKind, } from "./components/conversation-view.js";
6
6
  export type { IconAdapter, JsonSchema, ToolViewAdapter, ToolViewCall, ToolViewInstance, ToolViewProps, ToolViewRegistration, ToolViewRegistry, } from "./core/tool-view.js";
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { AssistantActivityResult, AssistantActivitySnapshot, } from "./core
9
9
  export type { PendingDecisionGap } from "./core/pending-decision-gap.js";
10
10
  export { resetAssistant } from "./persistence/stored-thread.js";
11
11
  export { ServingApiError } from "./transport/serving-error.js";
12
- export type { ActionIntent, ActionIntentAction, ActionsAdapter, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
12
+ export type { ActionIntent, ActionIntentAction, ActionIntentGraphQL, ActionsAdapter, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
13
13
  export type { ServingErrorCode, ServingErrorCodeOnTheWire, } from "./contract/errors.js";
14
14
  export { ASSISTANT_MESSAGE_MAX_CHARS, type ServingAssistantThread, type TurnStatus, } from "./contract/threads.js";
15
15
  export { activeFoldElapsedOf, settledFoldDurationOf, type BlockTiming, type SegmentDuration, } from "./core/segment-timing.js";