@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.
- package/README.md +1 -1
- package/dist/affordances/aria-dropdowns.js +315 -0
- package/dist/affordances/aria-toggles.js +39 -0
- package/dist/affordances/dropdown-display.js +129 -0
- package/dist/affordances/page-gestures.js +91 -0
- package/dist/affordances/prefill-reasons.js +75 -0
- package/dist/affordances/prefill.js +113 -62
- package/dist/components/tool-views/action-values.js +43 -0
- package/dist/components/tool-views/action-view.js +6 -42
- package/dist/components/tool-views/graphql-action-view.js +95 -0
- package/dist/contract/actions-adapter.d.ts +42 -4
- package/dist/core/execution-handlers.js +3 -3
- package/dist/core/tool-view.js +2 -0
- package/dist/generated/models/apiErrorCode.d.ts +9 -0
- package/dist/generated/models/apiErrorCode.js +9 -0
- package/dist/headless.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/reader/render.js +7 -4
- package/dist/reader/roles.js +28 -1
- package/dist/reader/snapshot-node.js +2 -29
- package/dist/reader/snapshot-root.js +14 -8
- package/dist/reader/snapshot.js +6 -1
- package/dist/transport/actions-adapter.js +83 -6
- package/package.json +1 -1
|
@@ -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 {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
121
|
+
if (input.checked === desired) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (theClickWouldSubmitOrLeaveThePage(input)) {
|
|
125
|
+
return SUBMITTING_CONTROL_REASON;
|
|
90
126
|
}
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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),
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
92
|
-
*
|
|
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) =>
|
|
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) {
|
package/dist/core/tool-view.js
CHANGED
|
@@ -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",
|
package/dist/headless.d.ts
CHANGED
|
@@ -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";
|