@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/README.md +64 -9
  3. package/dist/ag-ui-web-component.bundle.js +384 -147
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +4 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +10 -5
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/index.js +609 -33
  10. package/dist/index.js.map +4 -4
  11. package/dist/tools/tool_catalog.d.ts +5 -0
  12. package/dist/tools/tool_catalog.d.ts.map +1 -1
  13. package/dist/tools/tool_dispatch.d.ts.map +1 -1
  14. package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -1
  15. package/dist/ui/placement/surface_for.d.ts +12 -1
  16. package/dist/ui/placement/surface_for.d.ts.map +1 -1
  17. package/dist/ui/progress/thoughts_styles.d.ts.map +1 -1
  18. package/dist/ui/progress/tool_call_card.d.ts +8 -0
  19. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  20. package/dist/ui/progress/tool_group_styles.d.ts +2 -0
  21. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
  22. package/dist/ui/progress/tool_step_group.d.ts +47 -0
  23. package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
  24. package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
  25. package/dist/ui/shell/token_styles.d.ts.map +1 -1
  26. package/dist/ui/styles.d.ts.map +1 -1
  27. package/dist/ui/themes/ledger_styles.d.ts.map +1 -1
  28. package/dist/ui/themes/soft_styles.d.ts.map +1 -1
  29. package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
  30. package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
  31. package/dist/ui/transcript/message_styles.d.ts.map +1 -1
  32. package/dist/ui/transcript/transcript.d.ts +16 -1
  33. package/dist/ui/transcript/transcript.d.ts.map +1 -1
  34. package/dist/ui/ui_strings.d.ts +18 -0
  35. package/dist/ui/ui_strings.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/constants.ts +4 -1
  38. package/src/core/ag_ui_chat.ts +14 -6
  39. package/src/tools/tool_catalog.ts +23 -10
  40. package/src/tools/tool_dispatch.ts +26 -11
  41. package/src/ui/interrupts/confirmation_styles.ts +0 -2
  42. package/src/ui/placement/surface_for.ts +12 -1
  43. package/src/ui/progress/thoughts_styles.ts +0 -2
  44. package/src/ui/progress/tool_call_card.ts +11 -0
  45. package/src/ui/progress/tool_group_styles.ts +232 -0
  46. package/src/ui/progress/tool_step_group.ts +269 -0
  47. package/src/ui/progress/tool_step_styles.ts +0 -2
  48. package/src/ui/shell/token_styles.ts +2 -0
  49. package/src/ui/styles.ts +5 -0
  50. package/src/ui/themes/ledger_styles.ts +15 -0
  51. package/src/ui/themes/soft_styles.ts +20 -0
  52. package/src/ui/transcript/groups_tool_steps.ts +16 -0
  53. package/src/ui/transcript/message_styles.ts +0 -2
  54. package/src/ui/transcript/transcript.ts +109 -3
  55. package/src/ui/ui_strings.ts +31 -0
  56. 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;