@artooi/ag-ui-web-component 0.17.0 → 0.19.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 +113 -1
- package/README.md +38 -5
- package/dist/ag-ui-web-component.bundle.js +171 -60
- 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/index.js +385 -108
- package/dist/index.js.map +4 -4
- package/dist/ui/attach_copy_buttons.d.ts +19 -0
- package/dist/ui/attach_copy_buttons.d.ts.map +1 -0
- package/dist/ui/checkpoint_menu.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/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 +16 -4
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +51 -7
- package/src/ui/attach_copy_buttons.ts +82 -0
- package/src/ui/checkpoint_menu.ts +57 -6
- package/src/ui/confirmation_card.ts +15 -5
- package/src/ui/styles.ts +117 -4
- package/src/ui/tool_call_card.ts +116 -58
- package/src/ui/ui_strings.ts +22 -4
- package/src/version.ts +1 -1
|
@@ -26,6 +26,8 @@ export class CheckpointMenu {
|
|
|
26
26
|
readonly #onPick: (runId: string, verb: CheckpointVerb) => void;
|
|
27
27
|
readonly #list: HTMLDivElement;
|
|
28
28
|
readonly #heading: HTMLSpanElement;
|
|
29
|
+
/** What had focus before the panel opened, restored on close. */
|
|
30
|
+
#lastFocused: HTMLElement | null = null;
|
|
29
31
|
#strings: UiStrings;
|
|
30
32
|
#runs: readonly RunRow[] = [];
|
|
31
33
|
|
|
@@ -41,6 +43,10 @@ export class CheckpointMenu {
|
|
|
41
43
|
this.element.setAttribute("part", "checkpoints");
|
|
42
44
|
this.element.setAttribute("role", "dialog");
|
|
43
45
|
this.element.setAttribute("aria-label", strings.checkpoints);
|
|
46
|
+
// Focusable as a fallback: with no continuable runs the panel holds no
|
|
47
|
+
// controls at all, and a dialog that cannot take focus strands the
|
|
48
|
+
// keyboard user outside it with no way back except Tab-ing blind.
|
|
49
|
+
this.element.tabIndex = -1;
|
|
44
50
|
this.element.hidden = true;
|
|
45
51
|
|
|
46
52
|
const header = document.createElement("div");
|
|
@@ -57,12 +63,7 @@ export class CheckpointMenu {
|
|
|
57
63
|
this.#list.setAttribute("part", "checkpoints-list");
|
|
58
64
|
|
|
59
65
|
this.element.append(header, this.#list);
|
|
60
|
-
this.element.addEventListener("keydown", (event) =>
|
|
61
|
-
if (event.key === "Escape") {
|
|
62
|
-
event.stopPropagation();
|
|
63
|
-
this.close();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
+
this.element.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/** Replace the rows. The host passes only `continuable` runs. */
|
|
@@ -80,11 +81,61 @@ export class CheckpointMenu {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
open(): void {
|
|
84
|
+
if (this.open_) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Remember what had focus so it is restored on close, then move focus into
|
|
88
|
+
// the panel so a keyboard user lands inside the dialog rather than being
|
|
89
|
+
// left behind it. The thread drawer already does this; a dialog that takes
|
|
90
|
+
// no focus is one a keyboard user cannot reach and a screen reader does not
|
|
91
|
+
// announce, however correct its role and label.
|
|
92
|
+
this.#lastFocused = this.#activeElement() as HTMLElement | null;
|
|
83
93
|
this.element.hidden = false;
|
|
94
|
+
(this.#focusables()[0] ?? this.element).focus();
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
close(): void {
|
|
98
|
+
if (!this.open_) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
87
101
|
this.element.hidden = true;
|
|
102
|
+
this.#lastFocused?.focus();
|
|
103
|
+
this.#lastFocused = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The focused element within this panel's root (shadow-aware). */
|
|
107
|
+
#activeElement(): Element | null {
|
|
108
|
+
return (this.element.getRootNode() as Document | ShadowRoot).activeElement;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The panel's tabbable controls, in document order. */
|
|
112
|
+
#focusables(): HTMLElement[] {
|
|
113
|
+
return Array.from(this.element.querySelectorAll<HTMLElement>("button, [tabindex]")).filter(
|
|
114
|
+
(el) => !el.hidden,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Escape closes; Tab is trapped so focus cannot wander behind the dialog. */
|
|
119
|
+
#onKeydown(event: KeyboardEvent): void {
|
|
120
|
+
if (event.key === "Escape") {
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
this.close();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (event.key !== "Tab") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const focusables = this.#focusables();
|
|
129
|
+
const first = focusables[0];
|
|
130
|
+
const last = focusables[focusables.length - 1];
|
|
131
|
+
const active = this.#activeElement();
|
|
132
|
+
if (event.shiftKey && active === first) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
last?.focus();
|
|
135
|
+
} else if (!event.shiftKey && active === last) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
first?.focus();
|
|
138
|
+
}
|
|
88
139
|
}
|
|
89
140
|
|
|
90
141
|
get open_(): boolean {
|
|
@@ -40,8 +40,14 @@ export interface ConfirmationOptions {
|
|
|
40
40
|
* Unlike a modal overlay, the card lives in the transcript right where the
|
|
41
41
|
* action is — it reads naturally after the assistant's explanation and never
|
|
42
42
|
* steals focus from the page. Resolves ``true`` on confirm, ``false`` on
|
|
43
|
-
* cancel.
|
|
44
|
-
*
|
|
43
|
+
* cancel.
|
|
44
|
+
*
|
|
45
|
+
* **Answering it removes it.** A prompt and a record are two different objects:
|
|
46
|
+
* the prompt owns the user's attention until it is answered, and the record of
|
|
47
|
+
* what was decided belongs in the transcript, in order, scrolling with
|
|
48
|
+
* everything else. The tool card this gates is that record — it settles to
|
|
49
|
+
* `done` or `declined` and carries the decision. Leaving the spent form in
|
|
50
|
+
* place made an answered question read as outstanding.
|
|
45
51
|
*/
|
|
46
52
|
export function requestConfirmation(
|
|
47
53
|
host: Node & ParentNode,
|
|
@@ -66,6 +72,8 @@ export function requestConfirmation(
|
|
|
66
72
|
args.className = "confirm-args";
|
|
67
73
|
args.setAttribute("part", "confirm-args");
|
|
68
74
|
args.textContent = JSON.stringify(request.args, null, 2);
|
|
75
|
+
// A call with no arguments rendered the string "{}" in a box of its own.
|
|
76
|
+
args.hidden = Object.keys(request.args).length === 0;
|
|
69
77
|
|
|
70
78
|
const actions = document.createElement("div");
|
|
71
79
|
actions.className = "confirm-actions";
|
|
@@ -80,9 +88,11 @@ export function requestConfirmation(
|
|
|
80
88
|
return;
|
|
81
89
|
}
|
|
82
90
|
settled = true;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
// The prompt leaves once it has been answered. What stays is the tool
|
|
92
|
+
// card next to it, which settles to the outcome and scrolls with the rest
|
|
93
|
+
// of the transcript -- a record of the action rather than a spent form
|
|
94
|
+
// sitting there as a standing reminder of one.
|
|
95
|
+
card.remove();
|
|
86
96
|
resolve(accepted);
|
|
87
97
|
};
|
|
88
98
|
|
package/src/ui/styles.ts
CHANGED
|
@@ -512,6 +512,52 @@ export const STYLES = `
|
|
|
512
512
|
padding: 0;
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
/* The copy button sits inside the block, so it scrolls with wide code rather
|
|
516
|
+
than floating over the bubble. Positioning is on the pre; the button only
|
|
517
|
+
appears once one has been attached. */
|
|
518
|
+
.message--assistant pre.has-copy {
|
|
519
|
+
position: relative;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.code-copy {
|
|
523
|
+
position: absolute;
|
|
524
|
+
top: 4px;
|
|
525
|
+
right: 4px;
|
|
526
|
+
padding: 2px 8px;
|
|
527
|
+
font: inherit;
|
|
528
|
+
font-size: 0.75em;
|
|
529
|
+
line-height: 1.6;
|
|
530
|
+
color: var(--ag-ui-muted);
|
|
531
|
+
background: var(--ag-ui-surface);
|
|
532
|
+
border: 1px solid var(--ag-ui-border);
|
|
533
|
+
border-radius: 4px;
|
|
534
|
+
cursor: pointer;
|
|
535
|
+
opacity: 0;
|
|
536
|
+
transition: opacity 0.12s ease;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* Revealed on hover or keyboard focus. Focus matters as much as hover here:
|
|
540
|
+
hidden-until-hover is invisible to a keyboard user otherwise. */
|
|
541
|
+
.message--assistant pre.has-copy:hover .code-copy,
|
|
542
|
+
.code-copy:focus-visible {
|
|
543
|
+
opacity: 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.code-copy:hover {
|
|
547
|
+
color: var(--ag-ui-text);
|
|
548
|
+
background: var(--ag-ui-hover);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.code-copy[data-state="copied"] {
|
|
552
|
+
opacity: 1;
|
|
553
|
+
color: var(--ag-ui-text);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.code-copy[data-state="failed"] {
|
|
557
|
+
opacity: 1;
|
|
558
|
+
color: var(--ag-ui-danger, var(--ag-ui-text));
|
|
559
|
+
}
|
|
560
|
+
|
|
515
561
|
.message--assistant blockquote {
|
|
516
562
|
padding-left: 10px;
|
|
517
563
|
border-left: 3px solid var(--ag-ui-border);
|
|
@@ -726,10 +772,9 @@ export const STYLES = `
|
|
|
726
772
|
}
|
|
727
773
|
}
|
|
728
774
|
|
|
729
|
-
/* Inline display mode: the lightest card
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
.tool-call[data-display="inline"] {
|
|
775
|
+
/* Inline display mode: the lightest card. Drop the box chrome so the status row
|
|
776
|
+
reads as one line of the answer; the result toggle still expands below it. */
|
|
777
|
+
:host([data-tool-display="inline"]) .tool-call {
|
|
733
778
|
max-width: 100%;
|
|
734
779
|
background: transparent;
|
|
735
780
|
border: none;
|
|
@@ -759,6 +804,37 @@ export const STYLES = `
|
|
|
759
804
|
color: var(--ag-ui-muted);
|
|
760
805
|
}
|
|
761
806
|
|
|
807
|
+
.tool-call-body {
|
|
808
|
+
display: flex;
|
|
809
|
+
flex-direction: column;
|
|
810
|
+
gap: 6px;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.tool-call-section {
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
gap: 3px;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* The heading that tells the two payloads apart. Without it the arguments and
|
|
820
|
+
the result were one run of text and a reader had to guess the boundary. */
|
|
821
|
+
.tool-call-section-label {
|
|
822
|
+
font-size: 10px;
|
|
823
|
+
font-weight: 700;
|
|
824
|
+
letter-spacing: 0.06em;
|
|
825
|
+
text-transform: uppercase;
|
|
826
|
+
color: var(--ag-ui-muted);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/* The record of a human decision on a gated call. An approved call used to
|
|
830
|
+
look exactly like one that was never gated. */
|
|
831
|
+
.tool-call-decision {
|
|
832
|
+
flex: none;
|
|
833
|
+
font-size: 11px;
|
|
834
|
+
font-style: italic;
|
|
835
|
+
color: var(--ag-ui-muted);
|
|
836
|
+
}
|
|
837
|
+
|
|
762
838
|
.tool-call-args,
|
|
763
839
|
.tool-call-result {
|
|
764
840
|
margin: 0;
|
|
@@ -773,6 +849,43 @@ export const STYLES = `
|
|
|
773
849
|
color: var(--ag-ui-fg);
|
|
774
850
|
}
|
|
775
851
|
|
|
852
|
+
/* Display modes are pure visibility over one DOM shape, selected from the host
|
|
853
|
+
attribute rather than a value stamped on the card when it was built. That is
|
|
854
|
+
what lets a host flip data-tool-display and have every card already on screen
|
|
855
|
+
re-read it, the way data-answer-well behaves. Baking the structure per mode
|
|
856
|
+
meant the setting only reached cards created afterwards.
|
|
857
|
+
|
|
858
|
+
Default (no attribute) is the full mode: arguments always visible, result
|
|
859
|
+
behind the toggle. */
|
|
860
|
+
.tool-call[data-expanded="false"] .tool-call-section--result {
|
|
861
|
+
display: none;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/* Compact: one toggle over both regions, so a settled card is a single line
|
|
865
|
+
until asked. */
|
|
866
|
+
:host([data-tool-display="compact"]) .tool-call[data-expanded="false"] .tool-call-section {
|
|
867
|
+
display: none;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* Inline: the result only; the call's arguments are noise at this density. */
|
|
871
|
+
:host([data-tool-display="inline"]) .tool-call .tool-call-section--args {
|
|
872
|
+
display: none;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* Minimal: the status row and nothing else, so there is no toggle to press. */
|
|
876
|
+
:host([data-tool-display="minimal"]) .tool-call .tool-call-toggle,
|
|
877
|
+
:host([data-tool-display="minimal"]) .tool-call .tool-call-body {
|
|
878
|
+
display: none;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/* A pending card has no result yet, and in the modes where the arguments are
|
|
882
|
+
hidden too there is nothing behind the toggle. Hide the control rather than
|
|
883
|
+
offer one that expands onto nothing. */
|
|
884
|
+
.tool-call[data-status="pending"] .tool-call-toggle,
|
|
885
|
+
:host([data-tool-display="inline"]) .tool-call[data-status="pending"] .tool-call-toggle {
|
|
886
|
+
display: none;
|
|
887
|
+
}
|
|
888
|
+
|
|
776
889
|
.tool-call-toggle {
|
|
777
890
|
align-self: flex-start;
|
|
778
891
|
border: none;
|
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,19 @@ export interface UiStrings {
|
|
|
89
89
|
toolError: string;
|
|
90
90
|
/** Status pill on a declined call. */
|
|
91
91
|
toolDeclined: string;
|
|
92
|
-
/**
|
|
92
|
+
/** Note on a tool card whose call a human approved. */
|
|
93
|
+
decisionApproved: string;
|
|
94
|
+
/** Note on a tool card whose call a human declined. */
|
|
95
|
+
decisionDeclined: string;
|
|
96
|
+
/** Heading over a tool card's arguments region. */
|
|
97
|
+
argumentsLabel: string;
|
|
98
|
+
/** Heading over a tool card's result region when the call succeeded. */
|
|
93
99
|
resultLabel: string;
|
|
94
|
-
/**
|
|
100
|
+
/** Heading over a tool card's result region when the call failed. */
|
|
95
101
|
errorLabel: string;
|
|
96
|
-
/**
|
|
102
|
+
/** Heading over a tool card's result region when the call was declined. */
|
|
97
103
|
declinedLabel: string;
|
|
98
|
-
/**
|
|
104
|
+
/** Label on the toggle that expands a tool card's body. */
|
|
99
105
|
details: string;
|
|
100
106
|
|
|
101
107
|
// ── Confirmation card ───────────────────────────────────────────────────────
|
|
@@ -168,6 +174,12 @@ export interface UiStrings {
|
|
|
168
174
|
/** Hours ago. Token: `{n}`. */
|
|
169
175
|
hoursAgo: string;
|
|
170
176
|
/** Title of the checkpoint panel. */
|
|
177
|
+
/** Label on a code block's copy button. */
|
|
178
|
+
copyCode: string;
|
|
179
|
+
/** Shown on the copy button after the code reached the clipboard. */
|
|
180
|
+
copied: string;
|
|
181
|
+
/** Shown when the clipboard was unavailable or refused the write. */
|
|
182
|
+
copyFailed: string;
|
|
171
183
|
checkpoints: string;
|
|
172
184
|
/** Empty state when no run can be continued. */
|
|
173
185
|
noCheckpoints: string;
|
|
@@ -191,6 +203,9 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
191
203
|
collapse: "Collapse",
|
|
192
204
|
expand: "Expand",
|
|
193
205
|
toggleTheme: "Toggle theme",
|
|
206
|
+
copyCode: "Copy",
|
|
207
|
+
copied: "Copied",
|
|
208
|
+
copyFailed: "Copy failed",
|
|
194
209
|
checkpoints: "Continue a run",
|
|
195
210
|
noCheckpoints: "Nothing to continue yet.",
|
|
196
211
|
resumeRun: "Resume",
|
|
@@ -228,6 +243,9 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
228
243
|
toolDone: "✓ done",
|
|
229
244
|
toolError: "⚠ error",
|
|
230
245
|
toolDeclined: "⊘ declined",
|
|
246
|
+
decisionApproved: "approved by you",
|
|
247
|
+
decisionDeclined: "declined by you",
|
|
248
|
+
argumentsLabel: "Arguments",
|
|
231
249
|
resultLabel: "Result",
|
|
232
250
|
errorLabel: "Error",
|
|
233
251
|
declinedLabel: "Declined",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.19.0";
|