@artooi/ag-ui-web-component 0.44.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 (187) hide show
  1. package/CHANGELOG.md +182 -1
  2. package/README.md +245 -32
  3. package/dist/ag-ui-web-component.bundle.js +3696 -2798
  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 +28 -8
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/read_flag.d.ts +11 -0
  10. package/dist/core/read_flag.d.ts.map +1 -0
  11. package/dist/core/read_json_attribute.d.ts +15 -0
  12. package/dist/core/read_json_attribute.d.ts.map +1 -0
  13. package/dist/index.js +5084 -3481
  14. package/dist/index.js.map +4 -4
  15. package/dist/tools/tool_catalog.d.ts +5 -0
  16. package/dist/tools/tool_catalog.d.ts.map +1 -1
  17. package/dist/tools/tool_dispatch.d.ts.map +1 -1
  18. package/dist/ui/charts/chart_styles.d.ts +2 -0
  19. package/dist/ui/charts/chart_styles.d.ts.map +1 -0
  20. package/dist/ui/composer/attachment_styles.d.ts +2 -0
  21. package/dist/ui/composer/attachment_styles.d.ts.map +1 -0
  22. package/dist/ui/composer/build_composer.d.ts +30 -0
  23. package/dist/ui/composer/build_composer.d.ts.map +1 -0
  24. package/dist/ui/composer/composer_styles.d.ts +2 -0
  25. package/dist/ui/composer/composer_styles.d.ts.map +1 -0
  26. package/dist/ui/composer/skill_styles.d.ts +2 -0
  27. package/dist/ui/composer/skill_styles.d.ts.map +1 -0
  28. package/dist/ui/history/history_styles.d.ts +2 -0
  29. package/dist/ui/history/history_styles.d.ts.map +1 -0
  30. package/dist/ui/interrupts/approval_styles.d.ts +2 -0
  31. package/dist/ui/interrupts/approval_styles.d.ts.map +1 -0
  32. package/dist/ui/interrupts/confirmation_styles.d.ts +2 -0
  33. package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -0
  34. package/dist/ui/interrupts/question_styles.d.ts +2 -0
  35. package/dist/ui/interrupts/question_styles.d.ts.map +1 -0
  36. package/dist/ui/placement/collapse_styles.d.ts +2 -0
  37. package/dist/ui/placement/collapse_styles.d.ts.map +1 -0
  38. package/dist/ui/placement/embedded_styles.d.ts +2 -0
  39. package/dist/ui/placement/embedded_styles.d.ts.map +1 -0
  40. package/dist/ui/placement/launcher_styles.d.ts +2 -0
  41. package/dist/ui/placement/launcher_styles.d.ts.map +1 -0
  42. package/dist/ui/placement/page_styles.d.ts +2 -0
  43. package/dist/ui/placement/page_styles.d.ts.map +1 -0
  44. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  45. package/dist/ui/placement/preset_styles.d.ts +2 -0
  46. package/dist/ui/placement/preset_styles.d.ts.map +1 -0
  47. package/dist/ui/placement/resize_styles.d.ts +2 -0
  48. package/dist/ui/placement/resize_styles.d.ts.map +1 -0
  49. package/dist/ui/placement/sidebar_styles.d.ts +2 -0
  50. package/dist/ui/placement/sidebar_styles.d.ts.map +1 -0
  51. package/dist/ui/placement/small_viewport_styles.d.ts +2 -0
  52. package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -0
  53. package/dist/ui/placement/surface_for.d.ts +74 -0
  54. package/dist/ui/placement/surface_for.d.ts.map +1 -0
  55. package/dist/ui/progress/run_notice_styles.d.ts +2 -0
  56. package/dist/ui/progress/run_notice_styles.d.ts.map +1 -0
  57. package/dist/ui/progress/thoughts_styles.d.ts +2 -0
  58. package/dist/ui/progress/thoughts_styles.d.ts.map +1 -0
  59. package/dist/ui/progress/tool_call_card.d.ts +8 -0
  60. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  61. package/dist/ui/progress/tool_group_styles.d.ts +2 -0
  62. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
  63. package/dist/ui/progress/tool_step_group.d.ts +47 -0
  64. package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
  65. package/dist/ui/progress/tool_step_styles.d.ts +2 -0
  66. package/dist/ui/progress/tool_step_styles.d.ts.map +1 -0
  67. package/dist/ui/shell/adopt_styles.d.ts +9 -4
  68. package/dist/ui/shell/adopt_styles.d.ts.map +1 -1
  69. package/dist/ui/shell/build_header.d.ts +40 -0
  70. package/dist/ui/shell/build_header.d.ts.map +1 -0
  71. package/dist/ui/shell/build_launcher.d.ts +20 -0
  72. package/dist/ui/shell/build_launcher.d.ts.map +1 -0
  73. package/dist/ui/shell/frame_styles.d.ts +2 -0
  74. package/dist/ui/shell/frame_styles.d.ts.map +1 -0
  75. package/dist/ui/shell/theme_styles.d.ts +2 -0
  76. package/dist/ui/shell/theme_styles.d.ts.map +1 -0
  77. package/dist/ui/shell/token_styles.d.ts +2 -0
  78. package/dist/ui/shell/token_styles.d.ts.map +1 -0
  79. package/dist/ui/styles.d.ts +1 -1
  80. package/dist/ui/styles.d.ts.map +1 -1
  81. package/dist/ui/themes/classic_styles.d.ts +2 -0
  82. package/dist/ui/themes/classic_styles.d.ts.map +1 -0
  83. package/dist/ui/themes/classic_tokens.d.ts +2 -0
  84. package/dist/ui/themes/classic_tokens.d.ts.map +1 -0
  85. package/dist/ui/themes/code_tokens.d.ts +2 -0
  86. package/dist/ui/themes/code_tokens.d.ts.map +1 -0
  87. package/dist/ui/themes/ledger_styles.d.ts +2 -0
  88. package/dist/ui/themes/ledger_styles.d.ts.map +1 -0
  89. package/dist/ui/themes/ledger_tokens.d.ts +2 -0
  90. package/dist/ui/themes/ledger_tokens.d.ts.map +1 -0
  91. package/dist/ui/themes/modern_styles.d.ts +2 -0
  92. package/dist/ui/themes/modern_styles.d.ts.map +1 -0
  93. package/dist/ui/themes/quiet_styles.d.ts +2 -0
  94. package/dist/ui/themes/quiet_styles.d.ts.map +1 -0
  95. package/dist/ui/themes/scheme_styles.d.ts +2 -0
  96. package/dist/ui/themes/scheme_styles.d.ts.map +1 -0
  97. package/dist/ui/themes/soft_styles.d.ts +2 -0
  98. package/dist/ui/themes/soft_styles.d.ts.map +1 -0
  99. package/dist/ui/themes/soft_tokens.d.ts +2 -0
  100. package/dist/ui/themes/soft_tokens.d.ts.map +1 -0
  101. package/dist/ui/themes/theme_hosts.d.ts +15 -0
  102. package/dist/ui/themes/theme_hosts.d.ts.map +1 -0
  103. package/dist/ui/transcript/attachment_chip_styles.d.ts +2 -0
  104. package/dist/ui/transcript/attachment_chip_styles.d.ts.map +1 -0
  105. package/dist/ui/transcript/build_transcript.d.ts +21 -0
  106. package/dist/ui/transcript/build_transcript.d.ts.map +1 -0
  107. package/dist/ui/transcript/greeting_styles.d.ts +2 -0
  108. package/dist/ui/transcript/greeting_styles.d.ts.map +1 -0
  109. package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
  110. package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
  111. package/dist/ui/transcript/message_action_styles.d.ts +2 -0
  112. package/dist/ui/transcript/message_action_styles.d.ts.map +1 -0
  113. package/dist/ui/transcript/message_styles.d.ts +2 -0
  114. package/dist/ui/transcript/message_styles.d.ts.map +1 -0
  115. package/dist/ui/transcript/transcript.d.ts +16 -1
  116. package/dist/ui/transcript/transcript.d.ts.map +1 -1
  117. package/dist/ui/transcript/transcript_styles.d.ts +2 -0
  118. package/dist/ui/transcript/transcript_styles.d.ts.map +1 -0
  119. package/dist/ui/ui_strings.d.ts +18 -0
  120. package/dist/ui/ui_strings.d.ts.map +1 -1
  121. package/package.json +2 -2
  122. package/src/constants.ts +4 -1
  123. package/src/core/ag_ui_chat.ts +164 -299
  124. package/src/core/read_flag.ts +13 -0
  125. package/src/core/read_json_attribute.ts +30 -0
  126. package/src/tools/tool_catalog.ts +23 -10
  127. package/src/tools/tool_dispatch.ts +26 -11
  128. package/src/ui/charts/chart_block.ts +9 -9
  129. package/src/ui/charts/chart_styles.ts +63 -0
  130. package/src/ui/composer/attachment_styles.ts +71 -0
  131. package/src/ui/composer/build_composer.ts +112 -0
  132. package/src/ui/composer/composer_styles.ts +174 -0
  133. package/src/ui/composer/skill_styles.ts +120 -0
  134. package/src/ui/history/history_styles.ts +484 -0
  135. package/src/ui/interrupts/approval_styles.ts +52 -0
  136. package/src/ui/interrupts/confirmation_styles.ts +114 -0
  137. package/src/ui/interrupts/question_styles.ts +73 -0
  138. package/src/ui/placement/collapse_styles.ts +137 -0
  139. package/src/ui/placement/embedded_styles.ts +14 -0
  140. package/src/ui/placement/launcher_styles.ts +189 -0
  141. package/src/ui/placement/page_styles.ts +47 -0
  142. package/src/ui/placement/panel_placement.ts +3 -12
  143. package/src/ui/placement/preset_styles.ts +33 -0
  144. package/src/ui/placement/resize_styles.ts +186 -0
  145. package/src/ui/placement/sidebar_styles.ts +88 -0
  146. package/src/ui/placement/small_viewport_styles.ts +56 -0
  147. package/src/ui/placement/surface_for.ts +135 -0
  148. package/src/ui/progress/run_notice_styles.ts +52 -0
  149. package/src/ui/progress/thoughts_styles.ts +65 -0
  150. package/src/ui/progress/tool_call_card.ts +11 -0
  151. package/src/ui/progress/tool_group_styles.ts +232 -0
  152. package/src/ui/progress/tool_step_group.ts +269 -0
  153. package/src/ui/progress/tool_step_styles.ts +472 -0
  154. package/src/ui/shell/adopt_styles.ts +11 -5
  155. package/src/ui/shell/build_header.ts +108 -0
  156. package/src/ui/shell/build_launcher.ts +50 -0
  157. package/src/ui/shell/frame_styles.ts +119 -0
  158. package/src/ui/shell/theme_styles.ts +11 -0
  159. package/src/ui/shell/token_styles.ts +406 -0
  160. package/src/ui/styles.ts +104 -3774
  161. package/src/ui/themes/classic_styles.ts +32 -0
  162. package/src/ui/themes/classic_tokens.ts +50 -0
  163. package/src/ui/themes/code_tokens.ts +21 -0
  164. package/src/ui/themes/ledger_styles.ts +168 -0
  165. package/src/ui/themes/ledger_tokens.ts +32 -0
  166. package/src/ui/themes/modern_styles.ts +80 -0
  167. package/src/ui/themes/quiet_styles.ts +115 -0
  168. package/src/ui/themes/scheme_styles.ts +17 -0
  169. package/src/ui/themes/soft_styles.ts +148 -0
  170. package/src/ui/themes/soft_tokens.ts +28 -0
  171. package/src/ui/themes/theme_hosts.ts +14 -0
  172. package/src/ui/transcript/attachment_chip_styles.ts +116 -0
  173. package/src/ui/transcript/build_transcript.ts +67 -0
  174. package/src/ui/transcript/greeting_styles.ts +171 -0
  175. package/src/ui/transcript/groups_tool_steps.ts +16 -0
  176. package/src/ui/transcript/message_action_styles.ts +155 -0
  177. package/src/ui/transcript/message_styles.ts +262 -0
  178. package/src/ui/transcript/transcript.ts +109 -3
  179. package/src/ui/transcript/transcript_styles.ts +140 -0
  180. package/src/ui/ui_strings.ts +31 -0
  181. package/src/version.ts +1 -1
  182. package/dist/ui/placement/is_collapsible_placement.d.ts +0 -18
  183. package/dist/ui/placement/is_collapsible_placement.d.ts.map +0 -1
  184. package/dist/ui/placement/is_draggable_placement.d.ts +0 -9
  185. package/dist/ui/placement/is_draggable_placement.d.ts.map +0 -1
  186. package/src/ui/placement/is_collapsible_placement.ts +0 -19
  187. package/src/ui/placement/is_draggable_placement.ts +0 -23
@@ -0,0 +1,262 @@
1
+ // Messages: the answer well, incoming-text animations, rendered markdown,
2
+ // code blocks and their copy button, tables, and the pending dots.
3
+ export const MESSAGE_STYLES: string = `
4
+ /* ── Answer group / well ─────────────────────────────────────────
5
+ One .answer per assistant turn wraps the streamed text, its tool cards,
6
+ and the pending indicator so a whole answer reads (and can be boxed) as one
7
+ unit. A flex column on the message-list gap, stretched to the list width so
8
+ its children keep their own left/right alignment. data-answer-well opts into
9
+ the bordered "well"; without it the turn renders as a flat stack. */
10
+ .answer {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: var(--_space);
14
+ align-self: stretch;
15
+ min-width: 0;
16
+ }
17
+
18
+ :host([data-answer-well]) .answer {
19
+ padding: var(--_pad);
20
+ background: var(--_well-bg);
21
+ border: 1px solid var(--_well-border);
22
+ border-radius: var(--_well-radius);
23
+ }
24
+
25
+ .message {
26
+ max-width: 80%;
27
+ padding: var(--_msg-pad);
28
+ border-radius: var(--_msg-radius);
29
+ line-height: 1.4;
30
+ white-space: pre-wrap;
31
+ /* overflow-wrap, not word-break, and the difference is the whole reason a
32
+ markdown table used to be unreadable.
33
+
34
+ Both stop one long unbroken token blowing out the bubble, which is all this
35
+ is for. But word-break: break-word is the legacy spelling of "break
36
+ anywhere", and breaking anywhere drops the *min-content* width of every
37
+ descendant to one character. A table's column algorithm takes min-content
38
+ as an input, so the table always fitted max-width: 100%, the overflow-x:
39
+ auto below it never had anything to scroll, and the columns absorbed the
40
+ pressure by rendering one letter per line instead. A seven-column header
41
+ came out 162px tall.
42
+
43
+ overflow-wrap: break-word breaks a word only when it would otherwise
44
+ overflow its line, and leaves min-content alone -- so the bubble is still
45
+ protected and the table is free to be wider than the panel and scroll. */
46
+ overflow-wrap: break-word;
47
+ }
48
+
49
+ /* ── Incoming-text animations (data-text-animation) ─────────────────────── */
50
+ /* .message--restored (rehydrated history) is excluded: entrance animations are
51
+ for freshly-arriving messages, not the whole transcript replaying on reload.
52
+ Word mode is excluded implicitly — restored bubbles aren't wrapped. */
53
+ :host([data-text-animation="fade"]) .message--assistant:not(.message--restored) {
54
+ animation: ag-ui-msg-fade 0.25s ease both;
55
+ }
56
+
57
+ @keyframes ag-ui-msg-fade {
58
+ from { opacity: 0; transform: translateY(4px); }
59
+ to { opacity: 1; transform: none; }
60
+ }
61
+
62
+ .message--assistant .word {
63
+ animation: ag-ui-word-in 0.3s ease both;
64
+ animation-delay: calc(var(--ag-ui-word-index, 0) * 35ms);
65
+ }
66
+
67
+ @keyframes ag-ui-word-in {
68
+ from { opacity: 0; }
69
+ to { opacity: 1; }
70
+ }
71
+
72
+ @media (prefers-reduced-motion: reduce) {
73
+ :host([data-text-animation="fade"]) .message--assistant,
74
+ .message--assistant .word {
75
+ animation: none;
76
+ }
77
+ }
78
+
79
+ .message--user {
80
+ align-self: flex-end;
81
+ background: var(--_user-bg);
82
+ color: var(--_user-fg);
83
+ border-bottom-right-radius: 4px;
84
+ }
85
+
86
+ .message--assistant {
87
+ align-self: flex-start;
88
+ background: var(--_assistant-bg);
89
+ border-bottom-left-radius: 4px;
90
+ /* Assistant bubbles hold rendered markdown/HTML, so collapse the source
91
+ whitespace the renderer leaves between block tags. */
92
+ white-space: normal;
93
+ }
94
+
95
+ /* Rendered-markdown elements inside an assistant bubble. */
96
+ .message--assistant > :first-child {
97
+ margin-top: 0;
98
+ }
99
+
100
+ .message--assistant > :last-child {
101
+ margin-bottom: 0;
102
+ }
103
+
104
+ .message--assistant p,
105
+ .message--assistant ul,
106
+ .message--assistant ol,
107
+ .message--assistant blockquote,
108
+ .message--assistant pre {
109
+ margin: 0.5em 0;
110
+ }
111
+
112
+ .message--assistant ul,
113
+ .message--assistant ol {
114
+ padding-left: 1.3em;
115
+ }
116
+
117
+ .message--assistant a {
118
+ color: var(--_accent);
119
+ text-decoration: underline;
120
+ }
121
+
122
+ .message--assistant code {
123
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
124
+ font-size: 0.92em;
125
+ background: rgba(127, 127, 127, 0.16);
126
+ padding: 1px 4px;
127
+ border-radius: 4px;
128
+ }
129
+
130
+ .message--assistant pre {
131
+ padding: 8px 10px;
132
+ overflow: auto;
133
+ background: var(--_bg);
134
+ border: 1px solid var(--_border);
135
+ border-radius: 6px;
136
+ }
137
+
138
+ .message--assistant pre code {
139
+ background: none;
140
+ padding: 0;
141
+ }
142
+
143
+ /* The copy button sits inside the block, so it scrolls with wide code rather
144
+ than floating over the bubble. Positioning is on the pre; the button only
145
+ appears once one has been attached. */
146
+ .message--assistant pre.has-copy {
147
+ position: relative;
148
+ }
149
+
150
+ /* The only reader of --ag-ui-surface and --ag-ui-text. Their defaults are
151
+ transparent and the body foreground, which is what this button rendered as
152
+ before either token existed; changing them repaints only this control. */
153
+ .code-copy {
154
+ position: absolute;
155
+ top: 4px;
156
+ right: 4px;
157
+ padding: 2px 8px;
158
+ font: inherit;
159
+ font-size: 0.75em;
160
+ line-height: 1.6;
161
+ color: var(--_muted);
162
+ background: var(--_surface);
163
+ border: 1px solid var(--_border);
164
+ border-radius: 4px;
165
+ cursor: pointer;
166
+ opacity: 0;
167
+ transition: opacity 0.12s ease;
168
+ }
169
+
170
+ /* Revealed on hover or keyboard focus. Focus matters as much as hover here:
171
+ hidden-until-hover is invisible to a keyboard user otherwise. */
172
+ .message--assistant pre.has-copy:hover .code-copy,
173
+ .code-copy:focus-visible {
174
+ opacity: 1;
175
+ }
176
+
177
+ .code-copy:hover {
178
+ color: var(--_text);
179
+ background: var(--_hover);
180
+ }
181
+
182
+ .code-copy[data-state="copied"] {
183
+ opacity: 1;
184
+ color: var(--_text);
185
+ }
186
+
187
+ .code-copy[data-state="failed"] {
188
+ opacity: 1;
189
+ color: var(--_danger, var(--_text));
190
+ }
191
+
192
+ .message--assistant blockquote {
193
+ padding-left: 10px;
194
+ border-left: 3px solid var(--_border);
195
+ color: var(--_muted);
196
+ }
197
+
198
+ /* Markdown tables. table/thead/tbody/tr/th/td are all in the sanitizer's
199
+ ALLOWED_TAGS, so an agent can emit one. A wide table must scroll inside its
200
+ own box rather than stretch the message: the bubble is width-constrained, so
201
+ without this the columns either crush or push the layout sideways. */
202
+ .message--assistant table {
203
+ display: block;
204
+ width: fit-content;
205
+ max-width: 100%;
206
+ overflow-x: auto;
207
+ border-collapse: collapse;
208
+ font-size: 0.95em;
209
+ }
210
+
211
+ .message--assistant th,
212
+ .message--assistant td {
213
+ padding: 6px 10px;
214
+ border: 1px solid var(--_border);
215
+ text-align: left;
216
+ vertical-align: top;
217
+ }
218
+
219
+ .message--assistant th {
220
+ background: var(--_hover);
221
+ font-weight: 600;
222
+ }
223
+
224
+ .pending {
225
+ align-self: flex-start;
226
+ display: inline-flex;
227
+ gap: 4px;
228
+ align-items: center;
229
+ padding: 12px 14px;
230
+ background: var(--_assistant-bg);
231
+ border-radius: 14px;
232
+ border-bottom-left-radius: 4px;
233
+ }
234
+
235
+ .pending-dot {
236
+ width: 6px;
237
+ height: 6px;
238
+ border-radius: 50%;
239
+ background: var(--_muted);
240
+ animation: ag-ui-pending 1.2s infinite ease-in-out both;
241
+ }
242
+
243
+ .pending-dot:nth-child(2) {
244
+ animation-delay: 0.16s;
245
+ }
246
+
247
+ .pending-dot:nth-child(3) {
248
+ animation-delay: 0.32s;
249
+ }
250
+
251
+ @keyframes ag-ui-pending {
252
+ 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }
253
+ 40% { opacity: 1; transform: translateY(-3px); }
254
+ }
255
+
256
+ @media (prefers-reduced-motion: reduce) {
257
+ .pending-dot {
258
+ animation: none;
259
+ opacity: 0.6;
260
+ }
261
+ }
262
+ `;
@@ -8,8 +8,14 @@ import { fillUiString } from "../fill_ui_string.js";
8
8
  import { prettifyToolName } from "../progress/prettify_tool_name.js";
9
9
  import { renderRunNotice } from "../progress/run_notice.js";
10
10
  import { ThoughtsBlock } from "../progress/thoughts_block.js";
11
- import { ToolCallCard, type ToolPayload } from "../progress/tool_call_card.js";
11
+ import {
12
+ ToolCallCard,
13
+ type ToolDisplayMode,
14
+ type ToolPayload,
15
+ } from "../progress/tool_call_card.js";
16
+ import { ToolStepGroup } from "../progress/tool_step_group.js";
12
17
  import type { UiStrings } from "../ui_strings.js";
18
+ import { groupsToolSteps } from "./groups_tool_steps.js";
13
19
  import { renderMarkdown } from "./render_markdown.js";
14
20
  import { renderOrWarn } from "./render_or_warn.js";
15
21
  import { wrapWords } from "./reveal_words.js";
@@ -35,6 +41,8 @@ export interface TranscriptHost {
35
41
  readonly serverSummary: (name: string) => string | undefined;
36
42
  /** The element's `formatToolPayload`, asked per region of a card. */
37
43
  readonly formatToolPayload: (payload: ToolPayload) => Node | string | null;
44
+ /** The element's `toolDisplay`, read per card: it decides whether calls fold into a step group. */
45
+ readonly toolDisplay: () => ToolDisplayMode;
38
46
  }
39
47
 
40
48
  /**
@@ -54,6 +62,12 @@ export class Transcript {
54
62
  readonly #toolCards = new Map<string, ToolCallCard>();
55
63
  /** Card elements by call id, so a rendering handler can find its own card. */
56
64
  readonly #cardElements = new Map<string, HTMLElement>();
65
+ /**
66
+ * Cards whose call is being run, by call id. A card leaves the awaiting ones
67
+ * the moment its call is dispatched, and a handler that opens a prompt on its
68
+ * own card is still running long after, so it is found here.
69
+ */
70
+ readonly #running = new Map<string | undefined, ToolCallCard>();
57
71
  /**
58
72
  * Call ids whose card was already settled from a streamed server-side result
59
73
  * (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
@@ -74,6 +88,24 @@ export class Transcript {
74
88
  // the answer group while a reasoning model thinks and collapsed once the
75
89
  // answer's first text token arrives. `null` outside a reasoning turn.
76
90
  #thoughts: ThoughtsBlock | null = null;
91
+ /** The step group behind each group element, so a new card can find its run. */
92
+ readonly #stepGroups = new WeakMap<Element, ToolStepGroup>();
93
+ /**
94
+ * An empty comment after each step group that has rendered output, which
95
+ * every later output is placed before, so a group's outputs come out in call
96
+ * order. Every card of a round joins its group before any handler runs, so
97
+ * placing each output straight after the group would put the newest first;
98
+ * and a marker rather than the last output, because an output may be a
99
+ * fragment, which leaves nothing behind to place the next one after.
100
+ */
101
+ readonly #groupEnds = new WeakMap<Element, Comment>();
102
+ /**
103
+ * The step group element each placed output belongs to. A tool's own output
104
+ * does not end its group: the next call joins the group above it. Without
105
+ * that, replay -- which draws each restored call's output before the next
106
+ * call's card -- would split a run the live stream drew as one group.
107
+ */
108
+ readonly #outputGroups = new WeakMap<Node, Element>();
77
109
 
78
110
  constructor(host: TranscriptHost) {
79
111
  this.#host = host;
@@ -185,6 +217,20 @@ export class Transcript {
185
217
  forgetCards(): void {
186
218
  this.#serverSettled.clear();
187
219
  this.#cardElements.clear();
220
+ this.#running.clear();
221
+ }
222
+
223
+ /** Remember the card of a call being run, for {@link setAsking}. */
224
+ holdRunning(callId: string, card: ToolCallCard): void {
225
+ this.#running.set(callId, card);
226
+ }
227
+
228
+ /**
229
+ * Say whether a prompt is open on the card of a running call. A call id of
230
+ * none, or one no card was held for, has no card to say it on.
231
+ */
232
+ setAsking(callId: string | undefined, asking: boolean): void {
233
+ this.#running.get(callId)?.setAsking(asking);
188
234
  }
189
235
 
190
236
  /** Empty the list back to its empty-state region. The last half of wiping it. */
@@ -383,7 +429,26 @@ export class Transcript {
383
429
  // `after` rather than an insert-or-append branch: both callers set the card
384
430
  // element immediately before calling, and a parentless anchor makes `after`
385
431
  // a no-op, so the alternative would be a branch nothing can reach.
386
- this.#cardElements.get(call.id)?.after(node);
432
+ //
433
+ // A card folded into a step group has its output placed after the group
434
+ // rather than inside it, so a chart is never hidden one click down.
435
+ const anchor = this.#cardElements.get(call.id);
436
+ const group = anchor?.closest(".tool-group");
437
+ if (group === null || group === undefined) {
438
+ anchor?.after(node);
439
+ } else {
440
+ let end = this.#groupEnds.get(group);
441
+ if (end === undefined) {
442
+ end = document.createComment("");
443
+ group.after(end);
444
+ this.#groupEnds.set(group, end);
445
+ }
446
+ const placed = node instanceof DocumentFragment ? [...node.children] : [node];
447
+ end.before(node);
448
+ for (const output of placed) {
449
+ this.#outputGroups.set(output, group);
450
+ }
451
+ }
387
452
  this.afterGrew();
388
453
  }
389
454
 
@@ -421,9 +486,50 @@ export class Transcript {
421
486
  formatPayload: (payload) => this.#host.formatToolPayload(payload),
422
487
  });
423
488
  this.#toolCards.set(call.id, card);
424
- this.ensureGroup().appendChild(card.element);
489
+ if (groupsToolSteps(this.#host.toolDisplay())) {
490
+ this.#stepGroup().add(card.element, summary);
491
+ } else {
492
+ this.ensureGroup().appendChild(card.element);
493
+ }
425
494
  this.updateEmptyState();
426
495
  this.follow();
427
496
  return card;
428
497
  }
498
+
499
+ /**
500
+ * Open the step group ``card`` sits in, at once, ahead of a prompt about to be
501
+ * drawn inside it. A no-op for a card drawn in a card display mode, which has
502
+ * no group.
503
+ */
504
+ revealStep(card: ToolCallCard): void {
505
+ const group = card.element.closest(".tool-group");
506
+ (group === null ? undefined : this.#stepGroups.get(group))?.reveal();
507
+ }
508
+
509
+ /**
510
+ * The step group a new card joins. Consecutive cards share one, and so do
511
+ * cards separated only by the group's own rendered output; anything else in
512
+ * between (text, a confirmation, a question) starts the next.
513
+ *
514
+ * The pending dots are not skipped over: a tool call takes them down before
515
+ * its card is drawn, so they are never what a card finds at the foot of the
516
+ * turn.
517
+ */
518
+ #stepGroup(): ToolStepGroup {
519
+ const answer = this.ensureGroup();
520
+ const last = answer.lastElementChild;
521
+ const existing =
522
+ last === null ? undefined : this.#stepGroups.get(this.#outputGroups.get(last) ?? last);
523
+ if (existing !== undefined) {
524
+ return existing;
525
+ }
526
+ const group = new ToolStepGroup(this.#host.strings());
527
+ this.#stepGroups.set(group.element, group);
528
+ if (last === null) {
529
+ answer.prepend(group.element);
530
+ } else {
531
+ last.after(group.element);
532
+ }
533
+ return group;
534
+ }
429
535
  }
@@ -0,0 +1,140 @@
1
+ // The scrolling message list and what is positioned against it: jump to
2
+ // latest, the quote-selection offer, the live region and the inset probe.
3
+ export const TRANSCRIPT_STYLES: string = `
4
+ /* Jump-to-latest: shown only once the reader has scrolled away *and* missed
5
+ something. Anchored to the panel rather than the list so it does not scroll
6
+ with the content it is offering to scroll to. */
7
+ /* The transcript's own box, and the only one whose foot is the transcript's
8
+ foot. The panel's foot is below the composer, the chips and the footer. */
9
+ .messages-wrap {
10
+ position: relative;
11
+ flex: 1;
12
+ min-height: 0;
13
+ display: flex;
14
+ flex-direction: column;
15
+ }
16
+
17
+ .jump-latest {
18
+ position: absolute;
19
+ left: 50%;
20
+ transform: translateX(-50%);
21
+ bottom: var(--_pad);
22
+ z-index: 2;
23
+ display: none;
24
+ align-items: center;
25
+ gap: 0.35em;
26
+ padding: 0.4em 0.9em;
27
+ border: 1px solid var(--_border);
28
+ border-radius: 999px;
29
+ /* A raised surface, not the panel's own background. Reusing --_bg made the
30
+ pill the same colour as everything behind it, leaving a 1px border and a
31
+ shadow to carry the whole affordance -- and a dark-on-dark shadow carries
32
+ nothing. --_hover is the token that already means "lifted off the panel",
33
+ and it separates in both themes without competing with the accent the send
34
+ button owns. */
35
+ background: var(--_hover);
36
+ color: var(--_text);
37
+ font: inherit;
38
+ font-size: 0.85em;
39
+ cursor: pointer;
40
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
41
+ }
42
+
43
+ .jump-latest[data-missed="true"] {
44
+ display: flex;
45
+ }
46
+
47
+ .jump-latest:hover {
48
+ border-color: var(--_accent);
49
+ }
50
+
51
+ /* The offer to quote a selection. Positioned in script against the transcript
52
+ box, which is the only ancestor whose top and foot are the transcript's --
53
+ the same reason .jump-latest lives here. The translate is the half the
54
+ script does not do: script sets the point the offer hangs from, CSS decides
55
+ which corner of the offer that point is. */
56
+ .quote-selection {
57
+ position: absolute;
58
+ z-index: 2;
59
+ transform: translate(-50%, -100%);
60
+ padding: 0.25em 0.7em;
61
+ border: 1px solid var(--_border);
62
+ border-radius: 999px;
63
+ background: var(--_hover);
64
+ color: var(--_text);
65
+ font: inherit;
66
+ font-size: 0.8em;
67
+ line-height: 1.6;
68
+ white-space: nowrap;
69
+ cursor: pointer;
70
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
71
+ }
72
+
73
+ /* Flipped under the selection when there was no room above it. Only the
74
+ vertical half of the translate changes: it still hangs from its own centre
75
+ horizontally. */
76
+ .quote-selection[data-below="true"] {
77
+ transform: translate(-50%, 0);
78
+ }
79
+
80
+ .quote-selection:hover {
81
+ border-color: var(--_accent);
82
+ }
83
+
84
+ /* Screen-reader-only status region. Off-screen rather than display:none or
85
+ visibility:hidden, both of which take the element out of the accessibility
86
+ tree entirely -- a hidden live region announces nothing at all, which is the
87
+ classic way this pattern is written wrong.
88
+
89
+ The 1px box with clip-path, rather than width/height 0, is the shape that
90
+ survives: a zero-sized element is dropped from the tree by some engines. */
91
+ /* A used-value reader for the four viewport-inset tokens, and the only
92
+ reliable one. getComputedStyle().getPropertyValue() on an unregistered
93
+ custom property hands back the substituted token stream, not a length: it
94
+ returns "4rem" verbatim, and "calc(56px + env(safe-area-inset-top))" as
95
+ "calc(56px + 0px)" -- which parses as NaN and takes the whole inset with it.
96
+ Padding is a real property, so the same tokens come back resolved to px.
97
+
98
+ visibility rather than display: none, because a box that generates no
99
+ layout has no used values to read. Zero-sized, absolutely positioned and
100
+ inert, so it costs nothing but the read. */
101
+ .viewport-probe {
102
+ position: absolute;
103
+ top: 0;
104
+ left: 0;
105
+ width: 0;
106
+ height: 0;
107
+ visibility: hidden;
108
+ pointer-events: none;
109
+ padding: var(--_viewport-inset-top) var(--_viewport-inset-right)
110
+ var(--_viewport-inset-bottom) var(--_viewport-inset-left);
111
+ }
112
+
113
+ .sr-only {
114
+ position: absolute;
115
+ width: 1px;
116
+ height: 1px;
117
+ margin: -1px;
118
+ padding: 0;
119
+ border: 0;
120
+ overflow: hidden;
121
+ white-space: nowrap;
122
+ clip-path: inset(50%);
123
+ }
124
+
125
+ .messages {
126
+ flex: 1;
127
+ overflow-y: auto;
128
+ /* Scrolling past the end of this must not scroll the page behind it. */
129
+ overscroll-behavior: contain;
130
+ /* The browser's own scroll anchoring competes with the scroller for the same
131
+ job and wins unpredictably -- it can hold the view still exactly when we
132
+ want to follow. Turned off so following is decided in one place. Safari
133
+ does not implement it, which is itself a reason not to depend on it. */
134
+ overflow-anchor: none;
135
+ padding: var(--_pad);
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: var(--_space);
139
+ }
140
+ `;
@@ -184,6 +184,29 @@ export interface UiStrings {
184
184
  /** Label on the toggle that expands a tool card's body. */
185
185
  details: string;
186
186
 
187
+ // ── Tool-step group ─────────────────────────────────────────────────────────
188
+ // The row a run's consecutive calls fold into under the default
189
+ // data-tool-display. A group of one shows that call's own label instead, so
190
+ // the three labels are filled with 2 or more; the notes count from 1.
191
+ /** The group's label while a step waits on the user. Token: `{count}` (steps). */
192
+ toolGroupSteps: string;
193
+ /** The group's label while a step runs. Token: `{count}` (steps). */
194
+ toolGroupWorking: string;
195
+ /** The group's label once every step has settled. Token: `{count}` (steps). */
196
+ toolGroupWorked: string;
197
+ /** Note on the group's row: steps waiting on the user. Token: `{count}`. */
198
+ toolGroupWaiting: string;
199
+ /** Note on the group's row: steps that failed. Token: `{count}`. */
200
+ toolGroupFailed: string;
201
+ /** Note on the group's row: steps that were declined. Token: `{count}`. */
202
+ toolGroupDeclined: string;
203
+ /**
204
+ * Set after a step's label when an earlier step in its group has the same
205
+ * one, so two "Fill field" steps read "Fill field" and "Fill field (2)".
206
+ * Grouped mode only. Token: `{n}` (which occurrence, from 2).
207
+ */
208
+ toolStepRepeat: string;
209
+
187
210
  // ── Delegated sub-agent ─────────────────────────────────────────────────────
188
211
  /**
189
212
  * The delegation row's text before the server's own status line lands.
@@ -423,6 +446,14 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
423
446
  interruptedLabel: "Not finished",
424
447
  details: "Details",
425
448
 
449
+ toolGroupSteps: "{count} steps",
450
+ toolGroupWorking: "Working through {count} steps",
451
+ toolGroupWorked: "Worked through {count} steps",
452
+ toolGroupWaiting: "{count} waiting for you",
453
+ toolGroupFailed: "{count} failed",
454
+ toolGroupDeclined: "{count} declined",
455
+ toolStepRepeat: "({n})",
456
+
426
457
  subAgentWorking: "Working…",
427
458
  subAgentDelegatedTo: "Delegated to {agent}",
428
459
  subAgentFinished: "{agent} finished",
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.44.0";
1
+ export const VERSION: string = "0.46.0";
@@ -1,18 +0,0 @@
1
- /**
2
- * Whether `placement` has a collapsed state at all.
3
- *
4
- * `page` does not. It is a dedicated route rather than a panel sitting on
5
- * someone else's page, so there is no "away" for it to go to: collapsing it
6
- * left a strip of application chrome fixed over a route that no longer had an
7
- * owner. It is also the placement that hides the launcher, so the usual way
8
- * back does not exist here.
9
- *
10
- * The header hides its collapse control under this placement, but a control
11
- * removed from the UI is not a state removed from the model -- the property,
12
- * the attribute and a value restored from storage all still reach it. This is
13
- * what the reachable paths are gated on; the stylesheet covers the one path
14
- * that never passes through here, an attribute written straight onto the
15
- * element.
16
- */
17
- export declare function isCollapsiblePlacement(placement: string | null): boolean;
18
- //# sourceMappingURL=is_collapsible_placement.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"is_collapsible_placement.d.ts","sourceRoot":"","sources":["../../../src/ui/placement/is_collapsible_placement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAExE"}
@@ -1,9 +0,0 @@
1
- /**
2
- * Whether `placement` is one whose launcher can be dragged.
3
- *
4
- * Asked by two owners: the placement, deciding whether a gesture may move the
5
- * widget, and the element, deciding whether a first mount starts collapsed --
6
- * the corner placements are the two with a launcher to collapse to.
7
- */
8
- export declare function isDraggablePlacement(placement: string | null): boolean;
9
- //# sourceMappingURL=is_draggable_placement.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"is_draggable_placement.d.ts","sourceRoot":"","sources":["../../../src/ui/placement/is_draggable_placement.ts"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEtE"}
@@ -1,19 +0,0 @@
1
- /**
2
- * Whether `placement` has a collapsed state at all.
3
- *
4
- * `page` does not. It is a dedicated route rather than a panel sitting on
5
- * someone else's page, so there is no "away" for it to go to: collapsing it
6
- * left a strip of application chrome fixed over a route that no longer had an
7
- * owner. It is also the placement that hides the launcher, so the usual way
8
- * back does not exist here.
9
- *
10
- * The header hides its collapse control under this placement, but a control
11
- * removed from the UI is not a state removed from the model -- the property,
12
- * the attribute and a value restored from storage all still reach it. This is
13
- * what the reachable paths are gated on; the stylesheet covers the one path
14
- * that never passes through here, an attribute written straight onto the
15
- * element.
16
- */
17
- export function isCollapsiblePlacement(placement: string | null): boolean {
18
- return placement !== "page";
19
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Placements whose launcher can be dragged. The rest have nowhere to put it:
3
- * a sidebar collapses to a full-height edge rail, "embedded" and "page" hide
4
- * the launcher entirely and keep their header bar, and a full-bleed panel
5
- * covers the screen it would be opening into.
6
- */
7
- const DRAGGABLE_PLACEMENTS: ReadonlySet<string | null> = new Set([
8
- null,
9
- "",
10
- "floating",
11
- "bottom-left",
12
- ]);
13
-
14
- /**
15
- * Whether `placement` is one whose launcher can be dragged.
16
- *
17
- * Asked by two owners: the placement, deciding whether a gesture may move the
18
- * widget, and the element, deciding whether a first mount starts collapsed --
19
- * the corner placements are the two with a launcher to collapse to.
20
- */
21
- export function isDraggablePlacement(placement: string | null): boolean {
22
- return DRAGGABLE_PLACEMENTS.has(placement);
23
- }