@artooi/ag-ui-web-component 0.45.0 → 0.46.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 +60 -1
- package/README.md +64 -9
- package/dist/ag-ui-web-component.bundle.js +384 -147
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +4 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +10 -5
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.js +609 -33
- package/dist/index.js.map +4 -4
- package/dist/tools/tool_catalog.d.ts +5 -0
- package/dist/tools/tool_catalog.d.ts.map +1 -1
- package/dist/tools/tool_dispatch.d.ts.map +1 -1
- package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -1
- package/dist/ui/placement/surface_for.d.ts +12 -1
- package/dist/ui/placement/surface_for.d.ts.map +1 -1
- package/dist/ui/progress/thoughts_styles.d.ts.map +1 -1
- package/dist/ui/progress/tool_call_card.d.ts +8 -0
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
- package/dist/ui/progress/tool_group_styles.d.ts +2 -0
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_group.d.ts +47 -0
- package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
- package/dist/ui/shell/token_styles.d.ts.map +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/themes/ledger_styles.d.ts.map +1 -1
- package/dist/ui/themes/soft_styles.d.ts.map +1 -1
- package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
- package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
- package/dist/ui/transcript/message_styles.d.ts.map +1 -1
- package/dist/ui/transcript/transcript.d.ts +16 -1
- package/dist/ui/transcript/transcript.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +18 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +4 -1
- package/src/core/ag_ui_chat.ts +14 -6
- package/src/tools/tool_catalog.ts +23 -10
- package/src/tools/tool_dispatch.ts +26 -11
- package/src/ui/interrupts/confirmation_styles.ts +0 -2
- package/src/ui/placement/surface_for.ts +12 -1
- package/src/ui/progress/thoughts_styles.ts +0 -2
- package/src/ui/progress/tool_call_card.ts +11 -0
- package/src/ui/progress/tool_group_styles.ts +232 -0
- package/src/ui/progress/tool_step_group.ts +269 -0
- package/src/ui/progress/tool_step_styles.ts +0 -2
- package/src/ui/shell/token_styles.ts +2 -0
- package/src/ui/styles.ts +5 -0
- package/src/ui/themes/ledger_styles.ts +15 -0
- package/src/ui/themes/soft_styles.ts +20 -0
- package/src/ui/transcript/groups_tool_steps.ts +16 -0
- package/src/ui/transcript/message_styles.ts +0 -2
- package/src/ui/transcript/transcript.ts +109 -3
- package/src/ui/ui_strings.ts +31 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// The folded tool-step group: one row per run of consecutive tool calls,
|
|
2
|
+
// with the per-call cards one click down.
|
|
3
|
+
|
|
4
|
+
/** The four per-call card modes, as attribute selectors on the host. */
|
|
5
|
+
const CARD_MODES = [
|
|
6
|
+
'[data-tool-display="inline"]',
|
|
7
|
+
'[data-tool-display="minimal"]',
|
|
8
|
+
'[data-tool-display="compact"]',
|
|
9
|
+
'[data-tool-display="full"]',
|
|
10
|
+
].join(", ");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The host in grouped mode: any data-tool-display but the four card modes,
|
|
14
|
+
* which is how the element's toolDisplay getter reads it, so an absent or
|
|
15
|
+
* unknown value groups here as it does there. Wrapped in :where so it weighs
|
|
16
|
+
* what a bare :host weighs, and a theme restating a group rule on its own
|
|
17
|
+
* :host selector still wins at equal weight.
|
|
18
|
+
*/
|
|
19
|
+
const GROUPED = `:host(:where(:not(${CARD_MODES})))`;
|
|
20
|
+
|
|
21
|
+
/** The host in one of the four card modes, where a group drawn earlier is flattened. */
|
|
22
|
+
const CARDS = `:host(:is(${CARD_MODES}))`;
|
|
23
|
+
|
|
24
|
+
export const TOOL_GROUP_STYLES: string = `
|
|
25
|
+
/* ── The folded tool-step group ──────────────────────────────────
|
|
26
|
+
One quiet row per run of consecutive calls, the cards one click down as
|
|
27
|
+
plain lines rather than boxes. Every rule is scoped to grouped mode, so
|
|
28
|
+
none of it reaches a group drawn earlier once the host moves to a card
|
|
29
|
+
mode. */
|
|
30
|
+
${GROUPED} .tool-group {
|
|
31
|
+
align-self: stretch;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
${GROUPED} .tool-group-head {
|
|
38
|
+
appearance: none;
|
|
39
|
+
align-self: flex-start;
|
|
40
|
+
max-width: 100%;
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 8px;
|
|
44
|
+
margin: 0 0 0 -6px;
|
|
45
|
+
padding: 4px 6px;
|
|
46
|
+
border: 0;
|
|
47
|
+
border-radius: 6px;
|
|
48
|
+
background: transparent;
|
|
49
|
+
color: var(--_muted);
|
|
50
|
+
font: inherit;
|
|
51
|
+
font-size: var(--_text-sm);
|
|
52
|
+
line-height: 1.3;
|
|
53
|
+
text-align: left;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
${GROUPED} .tool-group-head:hover {
|
|
58
|
+
background: var(--_hover);
|
|
59
|
+
color: var(--_fg);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
${GROUPED} .tool-group-head:focus-visible {
|
|
63
|
+
outline: 2px solid var(--_ring);
|
|
64
|
+
outline-offset: 1px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
${GROUPED} .tool-group-label {
|
|
68
|
+
min-width: 0;
|
|
69
|
+
font-weight: 500;
|
|
70
|
+
color: var(--_ink-2);
|
|
71
|
+
overflow-wrap: anywhere;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
${GROUPED} .tool-group-meta {
|
|
75
|
+
min-width: 0;
|
|
76
|
+
color: var(--_muted);
|
|
77
|
+
overflow-wrap: anywhere;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Each note opens with a middle dot, which also parts the first note from
|
|
81
|
+
the label. */
|
|
82
|
+
${GROUPED} .tool-group-note::before {
|
|
83
|
+
content: "\\00B7";
|
|
84
|
+
margin: 0 6px 0 2px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
${GROUPED} .tool-group[data-state="error"] .tool-group-meta {
|
|
88
|
+
color: var(--_danger);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
${GROUPED} .tool-group-icon {
|
|
92
|
+
flex: none;
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
width: 14px;
|
|
95
|
+
height: 14px;
|
|
96
|
+
background: currentColor;
|
|
97
|
+
mask: var(--_ico-check) center / contain no-repeat;
|
|
98
|
+
color: var(--_success);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
${GROUPED} .tool-group[data-state="error"] .tool-group-icon {
|
|
102
|
+
mask-image: var(--_ico-error);
|
|
103
|
+
color: var(--_danger);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
${GROUPED} .tool-group[data-state="running"] .tool-group-icon {
|
|
107
|
+
mask: none;
|
|
108
|
+
background: none;
|
|
109
|
+
border: 2px solid var(--_muted);
|
|
110
|
+
border-top-color: transparent;
|
|
111
|
+
border-radius: 50%;
|
|
112
|
+
animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Waiting on the user: a steady accent dot rather than a spinner, and the
|
|
116
|
+
count in the accent's ink. */
|
|
117
|
+
${GROUPED} .tool-group[data-state="waiting"] .tool-group-icon {
|
|
118
|
+
mask: none;
|
|
119
|
+
border-radius: 50%;
|
|
120
|
+
transform: scale(0.6);
|
|
121
|
+
background: var(--_accent);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
${GROUPED} .tool-group[data-state="waiting"] .tool-group-meta {
|
|
125
|
+
color: var(--_accent-ink);
|
|
126
|
+
font-weight: 500;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
${GROUPED} .tool-group-chevron {
|
|
130
|
+
flex: none;
|
|
131
|
+
width: 12px;
|
|
132
|
+
height: 12px;
|
|
133
|
+
background: currentColor;
|
|
134
|
+
mask: var(--_ico-chevron) center / contain no-repeat;
|
|
135
|
+
opacity: 0.7;
|
|
136
|
+
transition: transform var(--_motion) var(--_ease);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
${GROUPED} .tool-group[data-open="true"] .tool-group-chevron {
|
|
140
|
+
transform: rotate(90deg);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@media (prefers-reduced-motion: reduce) {
|
|
144
|
+
${GROUPED} .tool-group[data-state="running"] .tool-group-icon {
|
|
145
|
+
animation: none;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
${GROUPED} .tool-group-list {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 2px;
|
|
153
|
+
margin: 2px 0 4px 6px;
|
|
154
|
+
padding-left: 13px;
|
|
155
|
+
border-left: 1px solid var(--_border);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
${GROUPED} .tool-group[data-open="false"] .tool-group-list {
|
|
159
|
+
display: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* A card inside the group is one line of the list: icon, name, and its
|
|
163
|
+
Details control at the end, with the arguments and result opening under
|
|
164
|
+
the whole row. */
|
|
165
|
+
${GROUPED} .tool-group .tool-call {
|
|
166
|
+
max-width: 100%;
|
|
167
|
+
align-self: stretch;
|
|
168
|
+
flex-flow: row wrap;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 2px 10px;
|
|
171
|
+
padding: 3px 0;
|
|
172
|
+
border: 0;
|
|
173
|
+
border-radius: 0;
|
|
174
|
+
background: transparent;
|
|
175
|
+
color: var(--_ink-2);
|
|
176
|
+
font-family: inherit;
|
|
177
|
+
font-size: var(--_text-sm);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
${GROUPED} .tool-group .tool-call-head {
|
|
181
|
+
flex: 1 1 auto;
|
|
182
|
+
justify-content: flex-start;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
${GROUPED} .tool-group .tool-call-name {
|
|
186
|
+
flex: 0 1 auto;
|
|
187
|
+
font-weight: 450;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* The repeat ordinal on a step whose label an earlier step has. */
|
|
191
|
+
${GROUPED} .tool-call-repeat {
|
|
192
|
+
color: var(--_muted);
|
|
193
|
+
font-variant-numeric: tabular-nums;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
${GROUPED} .tool-group .tool-call-toggle {
|
|
197
|
+
font-family: inherit;
|
|
198
|
+
font-size: var(--_text-xs);
|
|
199
|
+
color: var(--_muted);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
${GROUPED} .tool-group .tool-call-body,
|
|
203
|
+
${GROUPED} .tool-group .tool-call-approval,
|
|
204
|
+
${GROUPED} .tool-group .tool-call-subagent {
|
|
205
|
+
flex-basis: 100%;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Grouping is decided as each card is built, so a card mode set afterwards
|
|
209
|
+
finds groups already drawn. It flattens them: the group and its list stop
|
|
210
|
+
generating boxes, the row and the repeat ordinals go, and every card lays
|
|
211
|
+
out as a direct child of the answer, exactly as a card built in that mode
|
|
212
|
+
does. */
|
|
213
|
+
${CARDS} .tool-group,
|
|
214
|
+
${CARDS} .tool-group-list {
|
|
215
|
+
display: contents;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
${CARDS} .tool-group-head,
|
|
219
|
+
${CARDS} .tool-call-repeat {
|
|
220
|
+
display: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Each of these declares its own display, which beats the user-agent rule for
|
|
224
|
+
the hidden attribute, so each needs its own. The host prefix is there for
|
|
225
|
+
weight, not scope: it lifts these over the card-mode rules above that set
|
|
226
|
+
display, which are heavier than a bare class and attribute. */
|
|
227
|
+
:host .tool-group[hidden],
|
|
228
|
+
:host .tool-group-head[hidden],
|
|
229
|
+
:host .tool-group-list[hidden] {
|
|
230
|
+
display: none;
|
|
231
|
+
}
|
|
232
|
+
`;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { SUBAGENT_PHASE, TOOL_CALL_STATUS } from "../../constants.js";
|
|
2
|
+
import { fillUiString } from "../fill_ui_string.js";
|
|
3
|
+
import type { UiStrings } from "../ui_strings.js";
|
|
4
|
+
|
|
5
|
+
/** What a group says about itself, read by the shadow CSS from `data-state`. */
|
|
6
|
+
type GroupState = "waiting" | "running" | "done" | "error";
|
|
7
|
+
|
|
8
|
+
/** The statuses a step is still open in. */
|
|
9
|
+
const OPEN_STATUSES: ReadonlySet<string> = new Set([
|
|
10
|
+
TOOL_CALL_STATUS.PENDING,
|
|
11
|
+
TOOL_CALL_STATUS.DEFERRED,
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A sub-agent panel still working, by the phase it stamps on itself. A panel
|
|
16
|
+
* outlives the delegation it drew, so a settled one does not match.
|
|
17
|
+
*/
|
|
18
|
+
const RUNNING_SUBAGENT = `.tool-call-subagent > .subagent:is(${[
|
|
19
|
+
SUBAGENT_PHASE.STARTED,
|
|
20
|
+
SUBAGENT_PHASE.TOOL_CALL,
|
|
21
|
+
SUBAGENT_PHASE.TOOL_RESULT,
|
|
22
|
+
]
|
|
23
|
+
.map((phase) => `[data-phase="${phase}"]`)
|
|
24
|
+
.join(", ")})`;
|
|
25
|
+
|
|
26
|
+
/** One step of the group: its card, and its label as the row names it. */
|
|
27
|
+
interface Step {
|
|
28
|
+
readonly element: HTMLElement;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly label: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The attributes the row is drawn from: each card's `data-status`, its
|
|
35
|
+
* `data-asking` while a question or a confirmation is open on it, and the
|
|
36
|
+
* `data-phase` and `data-agent` a sub-agent panel stamps on itself inside the
|
|
37
|
+
* card that delegated to it. Watched rather than told, so a step settled,
|
|
38
|
+
* asked about or delegated by any path updates the row without the group being
|
|
39
|
+
* called.
|
|
40
|
+
*/
|
|
41
|
+
const WATCHED_ATTRIBUTES: readonly string[] = [
|
|
42
|
+
"data-status",
|
|
43
|
+
"data-asking",
|
|
44
|
+
"data-phase",
|
|
45
|
+
"data-agent",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Consecutive tool calls folded into one quiet row, "Worked through 5 steps",
|
|
50
|
+
* with the per-call cards one click down. Built by the transcript for each run
|
|
51
|
+
* of calls when the element's `toolDisplay` is `grouped`.
|
|
52
|
+
*
|
|
53
|
+
* The group owns no state of its own beyond its nodes and two facts: each
|
|
54
|
+
* step's label, and whether it has already opened itself for the current wait.
|
|
55
|
+
* Everything else is read back off the cards each time one changes.
|
|
56
|
+
*
|
|
57
|
+
* It opens itself while a step waits on the user, because a deferred step
|
|
58
|
+
* carries its approval prompt inside its card and a folded group would hide
|
|
59
|
+
* the very question it is asking. Once per wait, so the user can fold it
|
|
60
|
+
* again. A running sub-agent does not open it: the row names the sub-agent
|
|
61
|
+
* instead, beside the running mark.
|
|
62
|
+
*/
|
|
63
|
+
export class ToolStepGroup {
|
|
64
|
+
readonly element: HTMLDivElement;
|
|
65
|
+
readonly #strings: UiStrings;
|
|
66
|
+
readonly #list: HTMLDivElement;
|
|
67
|
+
readonly #head: HTMLButtonElement;
|
|
68
|
+
readonly #label: HTMLSpanElement;
|
|
69
|
+
readonly #meta: HTMLSpanElement;
|
|
70
|
+
/** The steps, in order. Cards only ever join a group, so this is its list's children. */
|
|
71
|
+
readonly #steps: Step[] = [];
|
|
72
|
+
/** Whether the group has already opened itself for the current wait. */
|
|
73
|
+
#openedForWait = false;
|
|
74
|
+
|
|
75
|
+
constructor(strings: UiStrings) {
|
|
76
|
+
this.#strings = strings;
|
|
77
|
+
this.element = document.createElement("div");
|
|
78
|
+
this.element.className = "tool-group";
|
|
79
|
+
this.element.setAttribute("part", "tool-group");
|
|
80
|
+
this.element.setAttribute("data-open", "false");
|
|
81
|
+
|
|
82
|
+
this.#head = document.createElement("button");
|
|
83
|
+
this.#head.type = "button";
|
|
84
|
+
this.#head.className = "tool-group-head";
|
|
85
|
+
this.#head.setAttribute("part", "tool-group-head");
|
|
86
|
+
this.#head.setAttribute("aria-expanded", "false");
|
|
87
|
+
|
|
88
|
+
const icon = document.createElement("span");
|
|
89
|
+
icon.className = "tool-group-icon";
|
|
90
|
+
icon.setAttribute("part", "tool-group-icon");
|
|
91
|
+
icon.setAttribute("aria-hidden", "true");
|
|
92
|
+
|
|
93
|
+
this.#label = document.createElement("span");
|
|
94
|
+
this.#label.className = "tool-group-label";
|
|
95
|
+
this.#label.setAttribute("part", "tool-group-label");
|
|
96
|
+
|
|
97
|
+
this.#meta = document.createElement("span");
|
|
98
|
+
this.#meta.className = "tool-group-meta";
|
|
99
|
+
this.#meta.setAttribute("part", "tool-group-meta");
|
|
100
|
+
|
|
101
|
+
const chevron = document.createElement("span");
|
|
102
|
+
chevron.className = "tool-group-chevron";
|
|
103
|
+
chevron.setAttribute("aria-hidden", "true");
|
|
104
|
+
|
|
105
|
+
this.#head.append(icon, this.#label, this.#meta, chevron);
|
|
106
|
+
this.#head.addEventListener("click", () => {
|
|
107
|
+
this.setOpen(this.element.getAttribute("data-open") !== "true");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.#list = document.createElement("div");
|
|
111
|
+
this.#list.className = "tool-group-list";
|
|
112
|
+
this.#list.setAttribute("part", "tool-group-list");
|
|
113
|
+
|
|
114
|
+
this.element.append(this.#head, this.#list);
|
|
115
|
+
|
|
116
|
+
new MutationObserver(() => {
|
|
117
|
+
this.#refresh();
|
|
118
|
+
}).observe(this.#list, {
|
|
119
|
+
attributes: true,
|
|
120
|
+
attributeFilter: [...WATCHED_ATTRIBUTES],
|
|
121
|
+
subtree: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Add a card's element as the group's next step, under the label the card
|
|
127
|
+
* was built with.
|
|
128
|
+
*
|
|
129
|
+
* A step whose label an earlier step already has gets a repeat ordinal,
|
|
130
|
+
* "Fill field (2)", in an element of its own beside the card's name. The
|
|
131
|
+
* name itself is never rewritten, so a group flattened back into per-call
|
|
132
|
+
* cards by a card display mode shows exactly the label the card was built
|
|
133
|
+
* with, and the CSS hides the ordinal there.
|
|
134
|
+
*/
|
|
135
|
+
add(card: HTMLElement, name: string): void {
|
|
136
|
+
const seen = this.#steps.filter((step) => step.name === name).length + 1;
|
|
137
|
+
let label = name;
|
|
138
|
+
if (seen > 1) {
|
|
139
|
+
const repeat = document.createElement("span");
|
|
140
|
+
repeat.className = "tool-call-repeat";
|
|
141
|
+
repeat.setAttribute("part", "tool-card-repeat");
|
|
142
|
+
repeat.textContent = fillUiString(this.#strings.toolStepRepeat, { n: seen });
|
|
143
|
+
card.querySelector(".tool-call-name")?.after(repeat);
|
|
144
|
+
label = `${name} ${repeat.textContent}`;
|
|
145
|
+
}
|
|
146
|
+
this.#steps.push({ element: card, name, label });
|
|
147
|
+
this.#list.append(card);
|
|
148
|
+
this.#refresh();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Show or hide the per-step cards. */
|
|
152
|
+
setOpen(open: boolean): void {
|
|
153
|
+
this.element.setAttribute("data-open", String(open));
|
|
154
|
+
this.#head.setAttribute("aria-expanded", String(open));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Open the group now, for the wait that is about to start.
|
|
159
|
+
*
|
|
160
|
+
* The group opens itself from its mutation observer, which runs a tick after
|
|
161
|
+
* the card is marked deferred. The approval prompt is drawn, and takes focus,
|
|
162
|
+
* in the same tick as the mark, while the list is still out of the layout: the
|
|
163
|
+
* focus is dropped and the scroll it would cause never happens. So the caller
|
|
164
|
+
* that is about to draw the prompt opens the group first, and this marks the
|
|
165
|
+
* wait as already answered for, so the observer's later refresh does not open
|
|
166
|
+
* it again over a user who folded it.
|
|
167
|
+
*/
|
|
168
|
+
reveal(): void {
|
|
169
|
+
this.#openedForWait = true;
|
|
170
|
+
this.setOpen(true);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The row's note for a running sub-agent, naming it, or `null` when no step
|
|
175
|
+
* is delegating right now. Only a step that is itself still running counts.
|
|
176
|
+
*/
|
|
177
|
+
#delegation(): string | null {
|
|
178
|
+
for (const { element } of this.#steps) {
|
|
179
|
+
const panel =
|
|
180
|
+
element.getAttribute("data-status") === TOOL_CALL_STATUS.PENDING
|
|
181
|
+
? element.querySelector(RUNNING_SUBAGENT)
|
|
182
|
+
: null;
|
|
183
|
+
if (panel !== null) {
|
|
184
|
+
const agent = panel.getAttribute("data-agent");
|
|
185
|
+
return agent === null
|
|
186
|
+
? this.#strings.subAgentWorking
|
|
187
|
+
: fillUiString(this.#strings.subAgentDelegatedTo, { agent });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#refresh(): void {
|
|
194
|
+
const strings = this.#strings;
|
|
195
|
+
const steps = this.#steps;
|
|
196
|
+
const statuses = steps.map((step) => step.element.getAttribute("data-status") ?? "");
|
|
197
|
+
const open = steps.filter((_, i) => OPEN_STATUSES.has(statuses[i] as string));
|
|
198
|
+
const count = (status: string): number => statuses.filter((s) => s === status).length;
|
|
199
|
+
const failed = count(TOOL_CALL_STATUS.ERROR);
|
|
200
|
+
const declined = count(TOOL_CALL_STATUS.DECLINED);
|
|
201
|
+
const deferred = count(TOOL_CALL_STATUS.DEFERRED);
|
|
202
|
+
// A step is waiting on the user when its approval is pending on the server,
|
|
203
|
+
// or when a question or confirmation is open on it. The second kind keeps
|
|
204
|
+
// its status at running, so it is read off its own flag.
|
|
205
|
+
const asking = steps.filter((step) => step.element.hasAttribute("data-asking")).length;
|
|
206
|
+
const waiting = deferred + asking;
|
|
207
|
+
const total = steps.length;
|
|
208
|
+
|
|
209
|
+
// Opens for a deferred step only. An approval renders inside its card, which
|
|
210
|
+
// a folded group would hide; a question or confirmation renders beside the
|
|
211
|
+
// group, in view, so there is nothing for it to open.
|
|
212
|
+
if (deferred > 0 && !this.#openedForWait) {
|
|
213
|
+
this.#openedForWait = true;
|
|
214
|
+
this.setOpen(true);
|
|
215
|
+
} else if (deferred === 0) {
|
|
216
|
+
this.#openedForWait = false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let state: GroupState = "done";
|
|
220
|
+
if (waiting > 0) {
|
|
221
|
+
state = "waiting";
|
|
222
|
+
} else if (open.length > 0) {
|
|
223
|
+
state = "running";
|
|
224
|
+
} else if (failed > 0) {
|
|
225
|
+
state = "error";
|
|
226
|
+
}
|
|
227
|
+
this.element.setAttribute("data-state", state);
|
|
228
|
+
this.element.setAttribute("data-count", String(total));
|
|
229
|
+
|
|
230
|
+
const only = total === 1 ? steps[0] : undefined;
|
|
231
|
+
if (only !== undefined) {
|
|
232
|
+
this.#label.textContent = only.label;
|
|
233
|
+
} else {
|
|
234
|
+
const template =
|
|
235
|
+
state === "waiting"
|
|
236
|
+
? strings.toolGroupSteps
|
|
237
|
+
: state === "running"
|
|
238
|
+
? strings.toolGroupWorking
|
|
239
|
+
: strings.toolGroupWorked;
|
|
240
|
+
this.#label.textContent = fillUiString(template, { count: total });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const notes: string[] = [];
|
|
244
|
+
const delegation = this.#delegation();
|
|
245
|
+
const current = open[0];
|
|
246
|
+
if (waiting > 0) {
|
|
247
|
+
notes.push(fillUiString(strings.toolGroupWaiting, { count: waiting }));
|
|
248
|
+
} else if (delegation !== null) {
|
|
249
|
+
notes.push(delegation);
|
|
250
|
+
} else if (current !== undefined && total > 1) {
|
|
251
|
+
notes.push(current.label);
|
|
252
|
+
}
|
|
253
|
+
if (failed > 0) {
|
|
254
|
+
notes.push(fillUiString(strings.toolGroupFailed, { count: failed }));
|
|
255
|
+
}
|
|
256
|
+
if (declined > 0) {
|
|
257
|
+
notes.push(fillUiString(strings.toolGroupDeclined, { count: declined }));
|
|
258
|
+
}
|
|
259
|
+
this.#meta.replaceChildren(
|
|
260
|
+
...notes.map((text) => {
|
|
261
|
+
const note = document.createElement("span");
|
|
262
|
+
note.className = "tool-group-note";
|
|
263
|
+
note.textContent = text;
|
|
264
|
+
return note;
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
this.#meta.hidden = notes.length === 0;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -196,8 +196,6 @@ export const TOOL_STEP_STYLES: string = `
|
|
|
196
196
|
padding: 6px 8px;
|
|
197
197
|
max-height: 160px;
|
|
198
198
|
overflow: auto;
|
|
199
|
-
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
200
|
-
overscroll-behavior: contain;
|
|
201
199
|
background: var(--_bg);
|
|
202
200
|
border: 1px solid var(--_border);
|
|
203
201
|
border-radius: 6px;
|
|
@@ -37,6 +37,7 @@ export const TOKEN_STYLES: string = `
|
|
|
37
37
|
--_line-strong: var(--ag-ui-line-strong, color-mix(in oklch, var(--_fg) 20%, var(--_bg)));
|
|
38
38
|
--_wash: var(--ag-ui-wash, color-mix(in oklch, var(--_fg) 4.5%, var(--_bg)));
|
|
39
39
|
--_accent-ink: var(--ag-ui-accent-ink, color-mix(in oklch, var(--_accent) 82%, var(--_fg)));
|
|
40
|
+
--_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 55%, transparent));
|
|
40
41
|
/* The user's turn is the accent at a whisper. With Quiet's graphite accent
|
|
41
42
|
that is a neutral wash; once a host sets a brand accent it becomes a tint
|
|
42
43
|
of the brand, which is the one thing Soft does that Quiet keeps. */
|
|
@@ -368,6 +369,7 @@ export const TOKEN_STYLES: string = `
|
|
|
368
369
|
--_ico-check: var(--ag-ui-ico-check, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E"));
|
|
369
370
|
--_ico-error: var(--ag-ui-ico-error, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.2'/%3E%3Cpath d='M8 4.9v3.7M8 11.1v.1'/%3E%3C/svg%3E"));
|
|
370
371
|
--_ico-declined: var(--ag-ui-ico-declined, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.2'/%3E%3Cpath d='M3.8 12.2l8.4-8.4'/%3E%3C/svg%3E"));
|
|
372
|
+
--_ico-chevron: var(--ag-ui-ico-chevron, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 3.5l4.5 4.5L6 12.5'/%3E%3C/svg%3E"));
|
|
371
373
|
}
|
|
372
374
|
|
|
373
375
|
/* The colour drawn on the accent, from the accent's own luminance: white on
|
package/src/ui/styles.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { SIDEBAR_STYLES } from "./placement/sidebar_styles.js";
|
|
|
32
32
|
import { SMALL_VIEWPORT_STYLES } from "./placement/small_viewport_styles.js";
|
|
33
33
|
import { RUN_NOTICE_STYLES } from "./progress/run_notice_styles.js";
|
|
34
34
|
import { THOUGHTS_STYLES } from "./progress/thoughts_styles.js";
|
|
35
|
+
import { TOOL_GROUP_STYLES } from "./progress/tool_group_styles.js";
|
|
35
36
|
import { TOOL_STEP_STYLES } from "./progress/tool_step_styles.js";
|
|
36
37
|
import { FRAME_STYLES } from "./shell/frame_styles.js";
|
|
37
38
|
import { THEME_STYLES } from "./shell/theme_styles.js";
|
|
@@ -87,6 +88,10 @@ export const STYLES: string = [
|
|
|
87
88
|
QUESTION_STYLES,
|
|
88
89
|
SKILL_STYLES,
|
|
89
90
|
HISTORY_STYLES,
|
|
91
|
+
// The step group restates, inside a group, card rules the modules above set
|
|
92
|
+
// at rest; it outweighs them rather than relying on order, and sits ahead
|
|
93
|
+
// of the themes so a theme restating a group rule at equal weight wins.
|
|
94
|
+
TOOL_GROUP_STYLES,
|
|
90
95
|
// Each theme's structure comes last: it restates rules the modules above
|
|
91
96
|
// set at rest, at equal weight. Classic and code have no structural
|
|
92
97
|
// module; they are the base stylesheet, and classic adds only its
|
|
@@ -78,6 +78,21 @@ export const LEDGER_STYLES: string = `
|
|
|
78
78
|
letter-spacing: 0.01em;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/* The step group's row reads as a ledger heading. Unscoped to grouped mode:
|
|
82
|
+
a card mode hides the row and drops the list's box, so neither reaches a
|
|
83
|
+
flattened card. */
|
|
84
|
+
:host([theme~="ledger"]) .tool-group-head {
|
|
85
|
+
font-family: var(--_code-font);
|
|
86
|
+
font-size: var(--_text-xs);
|
|
87
|
+
border-radius: 2px;
|
|
88
|
+
text-transform: uppercase;
|
|
89
|
+
letter-spacing: 0.06em;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:host([theme~="ledger"]) .tool-group-list {
|
|
93
|
+
border-left: 1px dashed var(--_line-strong);
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
:host([theme~="ledger"]) .chart-block {
|
|
82
97
|
border-top: 1px solid var(--_line-strong);
|
|
83
98
|
border-bottom: 1px solid var(--_line);
|
|
@@ -59,6 +59,26 @@ export const SOFT_STYLES: string = `
|
|
|
59
59
|
color: var(--_accent-ink);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/* The step group's row as a tinted pill. Unscoped to grouped mode: a card
|
|
63
|
+
mode hides the row and drops the list's box, so neither reaches a
|
|
64
|
+
flattened card. */
|
|
65
|
+
:host([theme~="soft"]) .tool-group-head {
|
|
66
|
+
background: var(--_tint);
|
|
67
|
+
border: 1px solid var(--_border);
|
|
68
|
+
border-radius: 999px;
|
|
69
|
+
padding: 5px 12px 5px 9px;
|
|
70
|
+
margin-left: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:host([theme~="soft"]) .tool-group-head:hover {
|
|
74
|
+
background: var(--_tint-2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
:host([theme~="soft"]) .tool-group-list {
|
|
78
|
+
border-left-color: var(--_tint-2);
|
|
79
|
+
margin-left: 14px;
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
:host([theme~="soft"]) .chart-block {
|
|
63
83
|
background: var(--_bg);
|
|
64
84
|
border: 1px solid var(--_border);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TOOL_DISPLAY } from "../../constants.js";
|
|
2
|
+
import type { ToolDisplayMode } from "../progress/tool_call_card.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether a new tool call folds into a step group, given the element's
|
|
6
|
+
* `toolDisplay`.
|
|
7
|
+
*
|
|
8
|
+
* The display mode alone decides, whatever the theme: `grouped` (the default,
|
|
9
|
+
* and what an unknown value reads as) folds, and the four card modes keep
|
|
10
|
+
* per-call cards. Asked as each card is built, so switching to `grouped`
|
|
11
|
+
* folds only the calls drawn afterwards; switching away flattens the groups
|
|
12
|
+
* already drawn, which the shadow CSS does from the host attribute.
|
|
13
|
+
*/
|
|
14
|
+
export function groupsToolSteps(mode: ToolDisplayMode): boolean {
|
|
15
|
+
return mode === TOOL_DISPLAY.GROUPED;
|
|
16
|
+
}
|
|
@@ -130,8 +130,6 @@ export const MESSAGE_STYLES: string = `
|
|
|
130
130
|
.message--assistant pre {
|
|
131
131
|
padding: 8px 10px;
|
|
132
132
|
overflow: auto;
|
|
133
|
-
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
134
|
-
overscroll-behavior: contain;
|
|
135
133
|
background: var(--_bg);
|
|
136
134
|
border: 1px solid var(--_border);
|
|
137
135
|
border-radius: 6px;
|