@artooi/ag-ui-web-component 0.18.0 → 0.20.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/CHANGELOG.md +165 -1
- package/README.md +86 -4
- package/dist/ag-ui-web-component.bundle.js +256 -47
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +5 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.js +579 -69
- package/dist/index.js.map +4 -4
- package/dist/skills/parse_skills.d.ts.map +1 -1
- package/dist/skills/skill.d.ts +14 -4
- package/dist/skills/skill.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +8 -2
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +81 -0
- package/dist/ui/resize_handle.d.ts.map +1 -0
- package/dist/ui/skills_menu.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +26 -14
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +12 -4
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +230 -10
- package/src/core/agui_client.ts +27 -1
- package/src/skills/parse_skills.ts +8 -2
- package/src/skills/skill.ts +14 -4
- package/src/ui/confirmation_card.ts +15 -5
- package/src/ui/resize_handle.ts +176 -0
- package/src/ui/skills_menu.ts +13 -1
- package/src/ui/styles.ts +217 -6
- package/src/ui/tool_call_card.ts +116 -58
- package/src/ui/ui_strings.ts +17 -5
- package/src/version.ts +1 -1
package/src/ui/skills_menu.ts
CHANGED
|
@@ -141,6 +141,10 @@ export class SkillsMenu {
|
|
|
141
141
|
button.className = "skill-chip";
|
|
142
142
|
button.setAttribute("part", "skill-chip");
|
|
143
143
|
button.textContent = skill.title;
|
|
144
|
+
// The chip stays a label — it is a one-click affordance, not something to
|
|
145
|
+
// memorise — but naming the command it stands for makes the two
|
|
146
|
+
// discoverable as the same thing.
|
|
147
|
+
button.title = `/${skill.name}`;
|
|
144
148
|
button.addEventListener("click", () => this.#pick(skill));
|
|
145
149
|
this.chips.appendChild(button);
|
|
146
150
|
}
|
|
@@ -159,7 +163,15 @@ export class SkillsMenu {
|
|
|
159
163
|
const title = document.createElement("span");
|
|
160
164
|
title.className = "skill-item-title";
|
|
161
165
|
title.setAttribute("part", "skill-item-title");
|
|
162
|
-
|
|
166
|
+
|
|
167
|
+
// The token leads, because it is the thing the user has to type. A row
|
|
168
|
+
// showing only the label taught the label and not the command, so the
|
|
169
|
+
// palette could not be used to learn its own vocabulary.
|
|
170
|
+
const token = document.createElement("code");
|
|
171
|
+
token.className = "skill-item-token";
|
|
172
|
+
token.setAttribute("part", "skill-item-token");
|
|
173
|
+
token.textContent = `/${skill.name}`;
|
|
174
|
+
title.append(token, document.createTextNode(` ${skill.title}`));
|
|
163
175
|
item.appendChild(title);
|
|
164
176
|
|
|
165
177
|
if (skill.description !== undefined) {
|
package/src/ui/styles.ts
CHANGED
|
@@ -168,7 +168,72 @@ export const STYLES = `
|
|
|
168
168
|
padding-inline: max(var(--ag-ui-pad), calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
:host([placement="page"])
|
|
171
|
+
:host([placement="page"]) /* Resize handle: a corner grip on a floating panel, an edge grip on a docked
|
|
172
|
+
one. Absent entirely where the placement is full-bleed, since there is
|
|
173
|
+
nothing to drag. */
|
|
174
|
+
.resize-handle {
|
|
175
|
+
position: absolute;
|
|
176
|
+
z-index: 2;
|
|
177
|
+
background: transparent;
|
|
178
|
+
touch-action: none;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.resize-handle:focus-visible {
|
|
182
|
+
outline: 2px solid var(--ag-ui-accent);
|
|
183
|
+
outline-offset: -2px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* The grip sits at the corner the panel grows toward. Which corner that is
|
|
187
|
+
depends on the host's layout, not on placement, so the element measures it
|
|
188
|
+
and stamps data-resize-anchor with the edges that stay put.
|
|
189
|
+
|
|
190
|
+
Default (bottom-right pinned, the floating case) puts the grip top-left. */
|
|
191
|
+
.resize-handle {
|
|
192
|
+
top: 0;
|
|
193
|
+
left: 0;
|
|
194
|
+
width: 14px;
|
|
195
|
+
height: 14px;
|
|
196
|
+
cursor: nwse-resize;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
:host([data-resize-anchor~="left"]) .resize-handle {
|
|
200
|
+
left: auto;
|
|
201
|
+
right: 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
:host([data-resize-anchor~="top"]) .resize-handle {
|
|
205
|
+
top: auto;
|
|
206
|
+
bottom: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* One axis flipped means the drag runs along the other diagonal. */
|
|
210
|
+
:host([data-resize-anchor="top-right"]) .resize-handle,
|
|
211
|
+
:host([data-resize-anchor="bottom-left"]) .resize-handle {
|
|
212
|
+
cursor: nesw-resize;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Docked: the placement owns the height, so only the inner edge is the user's. */
|
|
216
|
+
:host([placement="sidebar"]) .resize-handle,
|
|
217
|
+
:host([placement="side"]) .resize-handle {
|
|
218
|
+
top: 0;
|
|
219
|
+
bottom: auto;
|
|
220
|
+
width: 8px;
|
|
221
|
+
height: 100%;
|
|
222
|
+
cursor: ew-resize;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Full-bleed: nothing to drag. */
|
|
226
|
+
:host([placement="full"]) .resize-handle,
|
|
227
|
+
:host([placement="page"]) .resize-handle {
|
|
228
|
+
display: none;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.resize-handle[data-dragging] {
|
|
232
|
+
background: var(--ag-ui-accent);
|
|
233
|
+
opacity: 0.35;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.input-row {
|
|
172
237
|
padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
173
238
|
}
|
|
174
239
|
|
|
@@ -772,10 +837,9 @@ export const STYLES = `
|
|
|
772
837
|
}
|
|
773
838
|
}
|
|
774
839
|
|
|
775
|
-
/* Inline display mode: the lightest card
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
.tool-call[data-display="inline"] {
|
|
840
|
+
/* Inline display mode: the lightest card. Drop the box chrome so the status row
|
|
841
|
+
reads as one line of the answer; the result toggle still expands below it. */
|
|
842
|
+
:host([data-tool-display="inline"]) .tool-call {
|
|
779
843
|
max-width: 100%;
|
|
780
844
|
background: transparent;
|
|
781
845
|
border: none;
|
|
@@ -805,6 +869,44 @@ export const STYLES = `
|
|
|
805
869
|
color: var(--ag-ui-muted);
|
|
806
870
|
}
|
|
807
871
|
|
|
872
|
+
.tool-call-body {
|
|
873
|
+
display: flex;
|
|
874
|
+
flex-direction: column;
|
|
875
|
+
gap: 6px;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.tool-call-section {
|
|
879
|
+
display: flex;
|
|
880
|
+
flex-direction: column;
|
|
881
|
+
gap: 3px;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/* The heading that tells the two payloads apart. Without it the arguments and
|
|
885
|
+
the result were one run of text and a reader had to guess the boundary. */
|
|
886
|
+
.tool-call-section-label {
|
|
887
|
+
font-size: 10px;
|
|
888
|
+
font-weight: 700;
|
|
889
|
+
letter-spacing: 0.06em;
|
|
890
|
+
text-transform: uppercase;
|
|
891
|
+
color: var(--ag-ui-muted);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/* The record of a human decision on a gated call. An approved call used to
|
|
895
|
+
look exactly like one that was never gated. */
|
|
896
|
+
.skill-item-token {
|
|
897
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
898
|
+
font-size: 0.92em;
|
|
899
|
+
color: var(--ag-ui-accent);
|
|
900
|
+
margin-right: 6px;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.tool-call-decision {
|
|
904
|
+
flex: none;
|
|
905
|
+
font-size: 11px;
|
|
906
|
+
font-style: italic;
|
|
907
|
+
color: var(--ag-ui-muted);
|
|
908
|
+
}
|
|
909
|
+
|
|
808
910
|
.tool-call-args,
|
|
809
911
|
.tool-call-result {
|
|
810
912
|
margin: 0;
|
|
@@ -819,6 +921,43 @@ export const STYLES = `
|
|
|
819
921
|
color: var(--ag-ui-fg);
|
|
820
922
|
}
|
|
821
923
|
|
|
924
|
+
/* Display modes are pure visibility over one DOM shape, selected from the host
|
|
925
|
+
attribute rather than a value stamped on the card when it was built. That is
|
|
926
|
+
what lets a host flip data-tool-display and have every card already on screen
|
|
927
|
+
re-read it, the way data-answer-well behaves. Baking the structure per mode
|
|
928
|
+
meant the setting only reached cards created afterwards.
|
|
929
|
+
|
|
930
|
+
Default (no attribute) is the full mode: arguments always visible, result
|
|
931
|
+
behind the toggle. */
|
|
932
|
+
.tool-call[data-expanded="false"] .tool-call-section--result {
|
|
933
|
+
display: none;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/* Compact: one toggle over both regions, so a settled card is a single line
|
|
937
|
+
until asked. */
|
|
938
|
+
:host([data-tool-display="compact"]) .tool-call[data-expanded="false"] .tool-call-section {
|
|
939
|
+
display: none;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/* Inline: the result only; the call's arguments are noise at this density. */
|
|
943
|
+
:host([data-tool-display="inline"]) .tool-call .tool-call-section--args {
|
|
944
|
+
display: none;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* Minimal: the status row and nothing else, so there is no toggle to press. */
|
|
948
|
+
:host([data-tool-display="minimal"]) .tool-call .tool-call-toggle,
|
|
949
|
+
:host([data-tool-display="minimal"]) .tool-call .tool-call-body {
|
|
950
|
+
display: none;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/* A pending card has no result yet, and in the modes where the arguments are
|
|
954
|
+
hidden too there is nothing behind the toggle. Hide the control rather than
|
|
955
|
+
offer one that expands onto nothing. */
|
|
956
|
+
.tool-call[data-status="pending"] .tool-call-toggle,
|
|
957
|
+
:host([data-tool-display="inline"]) .tool-call[data-status="pending"] .tool-call-toggle {
|
|
958
|
+
display: none;
|
|
959
|
+
}
|
|
960
|
+
|
|
822
961
|
.tool-call-toggle {
|
|
823
962
|
align-self: flex-start;
|
|
824
963
|
border: none;
|
|
@@ -838,6 +977,75 @@ export const STYLES = `
|
|
|
838
977
|
content: "▾ ";
|
|
839
978
|
}
|
|
840
979
|
|
|
980
|
+
/* Resize handle: a corner grip on a floating panel, an edge grip on a docked
|
|
981
|
+
one. Absent entirely where the placement is full-bleed, since there is
|
|
982
|
+
nothing to drag. */
|
|
983
|
+
.resize-handle {
|
|
984
|
+
position: absolute;
|
|
985
|
+
z-index: 2;
|
|
986
|
+
background: transparent;
|
|
987
|
+
touch-action: none;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
.resize-handle:focus-visible {
|
|
991
|
+
outline: 2px solid var(--ag-ui-accent);
|
|
992
|
+
outline-offset: -2px;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/* The grip sits on the corner opposite the panel's anchor, because a resize
|
|
996
|
+
measures from whichever edge is not moving. Selected from the host attribute,
|
|
997
|
+
so switching placement moves the grip immediately.
|
|
998
|
+
|
|
999
|
+
Default is floating: pinned bottom-right, so the grip is top-left. */
|
|
1000
|
+
.resize-handle {
|
|
1001
|
+
top: 0;
|
|
1002
|
+
left: 0;
|
|
1003
|
+
width: 14px;
|
|
1004
|
+
height: 14px;
|
|
1005
|
+
cursor: nwse-resize;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/* Embedded sits in normal flow, pinned top-left, so it grows bottom-right. */
|
|
1009
|
+
:host([placement="embedded"]) .resize-handle {
|
|
1010
|
+
top: auto;
|
|
1011
|
+
left: auto;
|
|
1012
|
+
right: 0;
|
|
1013
|
+
bottom: 0;
|
|
1014
|
+
cursor: nwse-resize;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/* Pinned bottom-left, so the free corner is top-right. */
|
|
1018
|
+
:host([placement="bottom-left"]) .resize-handle {
|
|
1019
|
+
left: auto;
|
|
1020
|
+
right: 0;
|
|
1021
|
+
cursor: nesw-resize;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/* Docked: the placement owns the height, so only the inner edge is the user's. */
|
|
1025
|
+
:host([placement="sidebar"]) .resize-handle,
|
|
1026
|
+
:host([placement="side"]) .resize-handle {
|
|
1027
|
+
width: 8px;
|
|
1028
|
+
height: 100%;
|
|
1029
|
+
cursor: ew-resize;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/* Docked to the left, so the inner edge is the right-hand one. */
|
|
1033
|
+
:host([placement="sidebar"][data-side="left"]) .resize-handle {
|
|
1034
|
+
left: auto;
|
|
1035
|
+
right: 0;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/* Full-bleed: nothing to drag. */
|
|
1039
|
+
:host([placement="full"]) .resize-handle,
|
|
1040
|
+
:host([placement="page"]) .resize-handle {
|
|
1041
|
+
display: none;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.resize-handle[data-dragging] {
|
|
1045
|
+
background: var(--ag-ui-accent);
|
|
1046
|
+
opacity: 0.35;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
841
1049
|
.input-row {
|
|
842
1050
|
display: flex;
|
|
843
1051
|
gap: 8px;
|
|
@@ -1315,9 +1523,12 @@ export const STYLES = `
|
|
|
1315
1523
|
color: var(--ag-ui-muted);
|
|
1316
1524
|
}
|
|
1317
1525
|
|
|
1526
|
+
/* The hint sits directly above the composer's top border, so a zero bottom
|
|
1527
|
+
margin left the text touching the divider. */
|
|
1318
1528
|
.skill-hint {
|
|
1319
|
-
margin: 8px 12px
|
|
1529
|
+
margin: 8px 12px;
|
|
1320
1530
|
font-size: 0.85em;
|
|
1531
|
+
line-height: 1.4;
|
|
1321
1532
|
color: var(--ag-ui-danger);
|
|
1322
1533
|
}
|
|
1323
1534
|
|
package/src/ui/tool_call_card.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TOOL_CALL_STATUS, TOOL_DISPLAY } from "../constants.js";
|
|
1
|
+
import { TOOL_CALL_STATUS, type TOOL_DISPLAY } from "../constants.js";
|
|
2
2
|
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
3
3
|
|
|
4
4
|
/** Any state a tool-call card can be in. */
|
|
@@ -20,7 +20,7 @@ function statusLabels(strings: UiStrings): Record<ToolCallStatus, string> {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/** Section-heading text for each settled outcome's result region. */
|
|
24
24
|
function resultLabels(strings: UiStrings): Record<SettledStatus, string> {
|
|
25
25
|
return {
|
|
26
26
|
[TOOL_CALL_STATUS.DONE]: strings.resultLabel,
|
|
@@ -29,19 +29,32 @@ function resultLabels(strings: UiStrings): Record<SettledStatus, string> {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** Pretty-print a JSON payload; fall back to the raw text if it isn't JSON. */
|
|
33
|
+
function formatPayload(text: string): string {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
36
|
+
} catch {
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
/**
|
|
33
42
|
* A live tool-call card for the chat transcript.
|
|
34
43
|
*
|
|
35
|
-
* Construction renders a status icon, the tool name,
|
|
36
|
-
*
|
|
37
|
-
* {@link settle}
|
|
38
|
-
*
|
|
44
|
+
* Construction renders a status icon, the tool name, a `running…` pill, and the
|
|
45
|
+
* card's body: an **arguments** region and a **result** region, each with its own
|
|
46
|
+
* heading and its own `part`. {@link settle} fills in the result and flips the
|
|
47
|
+
* pill. Both payloads are pretty-printed, and the two are never concatenated —
|
|
48
|
+
* the previous compact layout ran `args: {...}` and the result together in one
|
|
49
|
+
* `<pre>`, leaving no way to see where the call ended and the answer began.
|
|
39
50
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
51
|
+
* **The card renders one DOM shape in every display mode, and CSS decides what
|
|
52
|
+
* shows.** That is what makes `data-tool-display` behave like `data-answer-well`
|
|
53
|
+
* — flip it on the host and every card already on screen re-reads it. Building a
|
|
54
|
+
* different structure per mode meant only cards created *after* the change
|
|
55
|
+
* picked it up, so the setting appeared not to work until the next conversation.
|
|
56
|
+
* Visibility is selected from the host attribute rather than a value copied onto
|
|
57
|
+
* the card at construction, for the same reason.
|
|
45
58
|
*
|
|
46
59
|
* The leading icon carries no text of its own: its glyph/spinner is drawn by
|
|
47
60
|
* the shadow CSS keyed off the card's `data-status`, so a host themes it via
|
|
@@ -57,20 +70,20 @@ export class ToolCallCard {
|
|
|
57
70
|
readonly element: HTMLDivElement;
|
|
58
71
|
|
|
59
72
|
readonly #status: HTMLSpanElement;
|
|
60
|
-
readonly #
|
|
61
|
-
readonly #
|
|
73
|
+
readonly #decision: HTMLSpanElement;
|
|
74
|
+
readonly #toggle: HTMLButtonElement;
|
|
75
|
+
readonly #resultSection: HTMLDivElement;
|
|
76
|
+
readonly #resultLabel: HTMLSpanElement;
|
|
77
|
+
readonly #resultBody: HTMLPreElement;
|
|
62
78
|
readonly #strings: UiStrings;
|
|
63
79
|
#settled = false;
|
|
64
80
|
|
|
65
81
|
constructor(
|
|
66
82
|
name: string,
|
|
67
83
|
args: Record<string, unknown>,
|
|
68
|
-
mode: ToolDisplayMode = TOOL_DISPLAY.FULL,
|
|
69
84
|
summary?: string,
|
|
70
85
|
strings: UiStrings = DEFAULT_UI_STRINGS,
|
|
71
86
|
) {
|
|
72
|
-
this.#mode = mode;
|
|
73
|
-
this.#args = args;
|
|
74
87
|
this.#strings = strings;
|
|
75
88
|
|
|
76
89
|
this.element = document.createElement("div");
|
|
@@ -78,7 +91,7 @@ export class ToolCallCard {
|
|
|
78
91
|
this.element.setAttribute("part", "tool-card");
|
|
79
92
|
this.element.setAttribute("data-tool-name", name);
|
|
80
93
|
this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
|
|
81
|
-
this.element.setAttribute("data-
|
|
94
|
+
this.element.setAttribute("data-expanded", "false");
|
|
82
95
|
|
|
83
96
|
const head = document.createElement("div");
|
|
84
97
|
head.className = "tool-call-head";
|
|
@@ -103,16 +116,56 @@ export class ToolCallCard {
|
|
|
103
116
|
this.#status.setAttribute("part", "tool-card-status");
|
|
104
117
|
this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
this.
|
|
119
|
+
this.#decision = document.createElement("span");
|
|
120
|
+
this.#decision.className = "tool-call-decision";
|
|
121
|
+
this.#decision.setAttribute("part", "tool-card-decision");
|
|
122
|
+
this.#decision.hidden = true;
|
|
123
|
+
|
|
124
|
+
head.append(icon, label, this.#status, this.#decision);
|
|
125
|
+
|
|
126
|
+
const argsSection = this.#section("args", strings.argumentsLabel);
|
|
127
|
+
argsSection.body.textContent = JSON.stringify(args, null, 2);
|
|
128
|
+
// A call with no arguments renders an empty object in a box of its own,
|
|
129
|
+
// which is a frame around nothing. Drop the region instead.
|
|
130
|
+
argsSection.root.hidden = Object.keys(args).length === 0;
|
|
131
|
+
|
|
132
|
+
const resultSection = this.#section("result", strings.resultLabel);
|
|
133
|
+
this.#resultSection = resultSection.root;
|
|
134
|
+
this.#resultLabel = resultSection.label;
|
|
135
|
+
this.#resultBody = resultSection.body;
|
|
136
|
+
// Nothing to show until `settle` supplies it; a pending card would
|
|
137
|
+
// otherwise expand onto an empty region.
|
|
138
|
+
resultSection.root.hidden = true;
|
|
139
|
+
|
|
140
|
+
this.#toggle = document.createElement("button");
|
|
141
|
+
this.#toggle.type = "button";
|
|
142
|
+
this.#toggle.className = "tool-call-toggle";
|
|
143
|
+
this.#toggle.setAttribute("part", "tool-card-toggle");
|
|
144
|
+
this.#toggle.setAttribute("aria-expanded", "false");
|
|
145
|
+
this.#toggle.textContent = strings.details;
|
|
146
|
+
this.#toggle.addEventListener("click", () => this.#setExpanded(!this.#expanded()));
|
|
147
|
+
|
|
148
|
+
const body = document.createElement("div");
|
|
149
|
+
body.className = "tool-call-body";
|
|
150
|
+
body.setAttribute("part", "tool-card-body");
|
|
151
|
+
body.append(argsSection.root, resultSection.root);
|
|
152
|
+
|
|
153
|
+
this.element.append(head, this.#toggle, body);
|
|
154
|
+
}
|
|
108
155
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Record that a human approved or declined this call, as a line in the card.
|
|
158
|
+
*
|
|
159
|
+
* Approval used to leave no trace at all: a declined call became a tool
|
|
160
|
+
* result saying so, while an approved one simply ran, making the transcript
|
|
161
|
+
* of a gated call byte-identical to one that was never gated. The prompt is
|
|
162
|
+
* gone once answered, so this is where the decision lives.
|
|
163
|
+
*/
|
|
164
|
+
recordDecision(kind: "approved" | "declined"): void {
|
|
165
|
+
this.element.setAttribute("data-decision", kind);
|
|
166
|
+
this.#decision.textContent =
|
|
167
|
+
kind === "approved" ? this.#strings.decisionApproved : this.#strings.decisionDeclined;
|
|
168
|
+
this.#decision.hidden = false;
|
|
116
169
|
}
|
|
117
170
|
|
|
118
171
|
/** Whether {@link settle} has already run (so a terminal sweep can skip it). */
|
|
@@ -121,50 +174,55 @@ export class ToolCallCard {
|
|
|
121
174
|
}
|
|
122
175
|
|
|
123
176
|
/**
|
|
124
|
-
* Flip the status pill to
|
|
125
|
-
*
|
|
126
|
-
* `inline`), or the args + result together (`compact`).
|
|
177
|
+
* Flip the status pill to `status` and fill in the result region, whose
|
|
178
|
+
* heading names the outcome (result / error / declined).
|
|
127
179
|
*/
|
|
128
180
|
settle(status: SettledStatus, text: string): void {
|
|
129
181
|
// Idempotent: a duplicate `TOOL_CALL_RESULT`, or a replayed tool message
|
|
130
|
-
// for an already-settled card, must not
|
|
131
|
-
// first settle wins; later calls are ignored.
|
|
182
|
+
// for an already-settled card, must not overwrite the first outcome.
|
|
132
183
|
if (this.#settled) {
|
|
133
184
|
return;
|
|
134
185
|
}
|
|
135
186
|
this.#settled = true;
|
|
136
187
|
this.element.setAttribute("data-status", status);
|
|
137
188
|
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
189
|
+
this.#resultLabel.textContent = resultLabels(this.#strings)[status];
|
|
190
|
+
this.#resultBody.textContent = formatPayload(text);
|
|
191
|
+
this.#resultSection.hidden = false;
|
|
192
|
+
}
|
|
138
193
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
194
|
+
/** Build one labelled region of the body: a heading plus a payload block. */
|
|
195
|
+
#section(
|
|
196
|
+
kind: string,
|
|
197
|
+
labelText: string,
|
|
198
|
+
): {
|
|
199
|
+
root: HTMLDivElement;
|
|
200
|
+
label: HTMLSpanElement;
|
|
201
|
+
body: HTMLPreElement;
|
|
202
|
+
} {
|
|
203
|
+
const root = document.createElement("div");
|
|
204
|
+
root.className = `tool-call-section tool-call-section--${kind}`;
|
|
205
|
+
root.setAttribute("part", `tool-card-section tool-card-${kind}-section`);
|
|
142
206
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
toggle.setAttribute("aria-expanded", "false");
|
|
148
|
-
|
|
149
|
-
const output = document.createElement("pre");
|
|
150
|
-
output.className = "tool-call-result";
|
|
151
|
-
output.setAttribute("part", "tool-card-result");
|
|
152
|
-
output.hidden = true;
|
|
153
|
-
|
|
154
|
-
if (this.#mode === TOOL_DISPLAY.COMPACT) {
|
|
155
|
-
toggle.textContent = this.#strings.details;
|
|
156
|
-
output.textContent = `args: ${JSON.stringify(this.#args)}\n\n${text}`;
|
|
157
|
-
} else {
|
|
158
|
-
toggle.textContent = resultLabels(this.#strings)[status];
|
|
159
|
-
output.textContent = text;
|
|
160
|
-
}
|
|
207
|
+
const label = document.createElement("span");
|
|
208
|
+
label.className = "tool-call-section-label";
|
|
209
|
+
label.setAttribute("part", `tool-card-section-label tool-card-${kind}-label`);
|
|
210
|
+
label.textContent = labelText;
|
|
161
211
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
const body = document.createElement("pre");
|
|
213
|
+
body.className = `tool-call-${kind}`;
|
|
214
|
+
body.setAttribute("part", `tool-card-${kind}`);
|
|
215
|
+
|
|
216
|
+
root.append(label, body);
|
|
217
|
+
return { root, label, body };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#expanded(): boolean {
|
|
221
|
+
return this.element.getAttribute("data-expanded") === "true";
|
|
222
|
+
}
|
|
167
223
|
|
|
168
|
-
|
|
224
|
+
#setExpanded(expand: boolean): void {
|
|
225
|
+
this.element.setAttribute("data-expanded", String(expand));
|
|
226
|
+
this.#toggle.setAttribute("aria-expanded", String(expand));
|
|
169
227
|
}
|
|
170
228
|
}
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -89,13 +89,21 @@ export interface UiStrings {
|
|
|
89
89
|
toolError: string;
|
|
90
90
|
/** Status pill on a declined call. */
|
|
91
91
|
toolDeclined: string;
|
|
92
|
-
/**
|
|
92
|
+
/** Accessible label on the panel resize handle. */
|
|
93
|
+
resizePanel: string;
|
|
94
|
+
/** Note on a tool card whose call a human approved. */
|
|
95
|
+
decisionApproved: string;
|
|
96
|
+
/** Note on a tool card whose call a human declined. */
|
|
97
|
+
decisionDeclined: string;
|
|
98
|
+
/** Heading over a tool card's arguments region. */
|
|
99
|
+
argumentsLabel: string;
|
|
100
|
+
/** Heading over a tool card's result region when the call succeeded. */
|
|
93
101
|
resultLabel: string;
|
|
94
|
-
/**
|
|
102
|
+
/** Heading over a tool card's result region when the call failed. */
|
|
95
103
|
errorLabel: string;
|
|
96
|
-
/**
|
|
104
|
+
/** Heading over a tool card's result region when the call was declined. */
|
|
97
105
|
declinedLabel: string;
|
|
98
|
-
/**
|
|
106
|
+
/** Label on the toggle that expands a tool card's body. */
|
|
99
107
|
details: string;
|
|
100
108
|
|
|
101
109
|
// ── Confirmation card ───────────────────────────────────────────────────────
|
|
@@ -221,7 +229,7 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
221
229
|
"The page changed since you last looked at it. Call read_page to see the current page, then retry.",
|
|
222
230
|
attachmentsStillUploading:
|
|
223
231
|
"{n} file still uploading — it was not sent with this message and is still attached.",
|
|
224
|
-
skillNeeds: "“{title}” needs
|
|
232
|
+
skillNeeds: "“{title}” needs {fields} — fill it in below, then send.",
|
|
225
233
|
|
|
226
234
|
message: "Message",
|
|
227
235
|
inputPlaceholder: "Ask anything…",
|
|
@@ -237,6 +245,10 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
237
245
|
toolDone: "✓ done",
|
|
238
246
|
toolError: "⚠ error",
|
|
239
247
|
toolDeclined: "⊘ declined",
|
|
248
|
+
resizePanel: "Resize the chat panel",
|
|
249
|
+
decisionApproved: "approved by you",
|
|
250
|
+
decisionDeclined: "declined by you",
|
|
251
|
+
argumentsLabel: "Arguments",
|
|
240
252
|
resultLabel: "Result",
|
|
241
253
|
errorLabel: "Error",
|
|
242
254
|
declinedLabel: "Declined",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.20.0";
|