@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,13 @@
1
+ /**
2
+ * Read an opt-in flag attribute on `element` the way HTML reads a boolean
3
+ * attribute.
4
+ *
5
+ * Present means on: bare (`data-prompt-chips`), empty (`=""`), or any value
6
+ * except the literal `"false"`. Comparing against `"true"` instead would make
7
+ * the bare spelling every native boolean attribute uses silently disable the
8
+ * feature it names. `="false"` still turns it off.
9
+ */
10
+ export function readFlag(element: Element, name: string): boolean {
11
+ const value = element.getAttribute(name);
12
+ return value !== null && value !== "false";
13
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Parse a JSON-valued attribute on `element`, saying so when it will not parse.
3
+ *
4
+ * `null` for an absent attribute, and `null` again for one that is not JSON --
5
+ * but not quietly the second time. Quoting JSON inside an HTML attribute is
6
+ * fiddly, and the result of getting it wrong is indistinguishable from the
7
+ * feature being switched off: no chips appear, or the strings stay English,
8
+ * with nothing anywhere saying why. That is the same failure `data-paste-attach`
9
+ * already reports for a value it cannot read.
10
+ *
11
+ * Console only. A page author's typo is not the reader's business, nothing the
12
+ * reader did was refused, and the degraded widget is still perfectly usable.
13
+ */
14
+ export function readJsonAttribute(element: Element, name: string): unknown {
15
+ const raw = element.getAttribute(name);
16
+ if (raw === null) {
17
+ return null;
18
+ }
19
+ try {
20
+ return JSON.parse(raw);
21
+ } catch {
22
+ console.warn(
23
+ `<ag-ui-chat>: ${name} is not valid JSON, so it was ignored entirely and ` +
24
+ "the built-in default is being used. Check the quoting -- JSON inside " +
25
+ "an HTML attribute needs single quotes around the attribute value, or " +
26
+ "its own double quotes escaped.",
27
+ );
28
+ return null;
29
+ }
30
+ }
@@ -36,6 +36,11 @@ export interface ToolCatalogHost {
36
36
  readonly askUserRenderer: () => QuestionRenderer | null;
37
37
  /** The decision a run is suspended on, which a Stop abandons. */
38
38
  readonly decision: PendingDecision;
39
+ /**
40
+ * Mark the card of ``callId`` as asking the user, or no longer. The call id is
41
+ * absent for a handler run outside a tool call, which has no card to mark.
42
+ */
43
+ readonly setAsking: (callId: string | undefined, asking: boolean) => void;
39
44
  /** The current answer group, opening one if none is open. */
40
45
  readonly ensureGroup: () => HTMLDivElement;
41
46
  /** The resolved string table. */
@@ -293,17 +298,25 @@ export class ToolCatalog {
293
298
  // A host-supplied renderer takes full control of the UI.
294
299
  const renderer = this.#host.askUserRenderer();
295
300
  let answer: string;
296
- if (renderer === null) {
297
- answer = await builtIn();
298
- } else {
299
- // Awaited here rather than inside a helper, so an answering renderer
300
- // takes exactly as many turns to be heard as it always did.
301
- try {
302
- // Called on the element, as `this.askUserRenderer(...)` always was.
303
- answer = await renderer.call(this.#host.element, request, { signal });
304
- } catch (error) {
305
- answer = await this.#afterRendererFailed(error, signal, callId, builtIn);
301
+ // The card says it is asking for as long as a question is open on it, an
302
+ // answer, a Stop and a renderer that failed alike, so the step group's row
303
+ // does not read as running while the run waits on a person.
304
+ this.#host.setAsking(callId, true);
305
+ try {
306
+ if (renderer === null) {
307
+ answer = await builtIn();
308
+ } else {
309
+ // Awaited here rather than inside a helper, so an answering renderer
310
+ // takes exactly as many turns to be heard as it always did.
311
+ try {
312
+ // Called on the element, as `this.askUserRenderer(...)` always was.
313
+ answer = await renderer.call(this.#host.element, request, { signal });
314
+ } catch (error) {
315
+ answer = await this.#afterRendererFailed(error, signal, callId, builtIn);
316
+ }
306
317
  }
318
+ } finally {
319
+ this.#host.setAsking(callId, false);
307
320
  }
308
321
  this.#host.decision.close();
309
322
  this.#host.updateEmptyState();
@@ -166,6 +166,7 @@ export class ToolDispatch {
166
166
  // transcript places itself against its own card, and by the time it runs the
167
167
  // card is no longer reachable by id.
168
168
  this.#host.transcript.setCardElement(call.id, card.element);
169
+ this.#host.transcript.holdRunning(call.id, card);
169
170
  // Scoped out of this round's catalog ⇒ not a frontend tool of ours, for
170
171
  // this round. A host that offers `delete_record` only on the page where
171
172
  // deleting makes sense has said something about *this* run, and a call
@@ -254,17 +255,26 @@ export class ToolDispatch {
254
255
  // to the message list made it a sibling *after* the group, so anything
255
256
  // that streamed afterwards rendered above it and the prompt drifted to
256
257
  // the foot of the turn no matter when it was asked.
257
- const decision = requestConfirmation(this.#host.transcript.ensureGroup(), request, {
258
- signal,
259
- strings: this.#host.strings(),
260
- // Offered only where it can be honoured -- see `#confirmationRule`.
261
- ...(rule === "destructive"
262
- ? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) }
263
- : {}),
264
- });
265
- this.#host.transcript.updateEmptyState();
266
- this.#host.transcript.follow();
267
- const accepted = await decision;
258
+ //
259
+ // The card says it is asking for as long as the prompt is open, however it
260
+ // ends, so a step group's row reads "waiting for you" rather than running.
261
+ card.setAsking(true);
262
+ let accepted: boolean;
263
+ try {
264
+ const decision = requestConfirmation(this.#host.transcript.ensureGroup(), request, {
265
+ signal,
266
+ strings: this.#host.strings(),
267
+ // Offered only where it can be honoured -- see `#confirmationRule`.
268
+ ...(rule === "destructive"
269
+ ? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) }
270
+ : {}),
271
+ });
272
+ this.#host.transcript.updateEmptyState();
273
+ this.#host.transcript.follow();
274
+ accepted = await decision;
275
+ } finally {
276
+ card.setAsking(false);
277
+ }
268
278
  this.#host.decision.close();
269
279
  // A handover while the card was open declines it, as any Stop does, but
270
280
  // the transcript the decline would be drawn into is the next principal's
@@ -458,6 +468,11 @@ export class ToolDispatch {
458
468
  request.args = card.args;
459
469
  }
460
470
  card?.mark(TOOL_CALL_STATUS.DEFERRED);
471
+ // Before either renderer runs: the built-in card focuses its approve
472
+ // button as it is drawn, and a folded step group has no box to focus in.
473
+ if (card !== undefined) {
474
+ this.#host.transcript.revealStep(card);
475
+ }
461
476
  // The built-in card renders into the gated call's own card, falling back
462
477
  // to the answer group when the interrupt names no call we hold one for.
463
478
  const builtIn = (): Promise<boolean> =>
@@ -96,16 +96,16 @@ const WIDTH_STEP = 8;
96
96
  /** A rough advance width per character for the 10px axis font. */
97
97
  const AXIS_CHAR_WIDTH = 5.6;
98
98
 
99
- // Read from the host's own palette rather than a fixed ramp: the component
100
- // themes through custom properties everywhere else, and a chart that ignored
101
- // that would be the one element a host could not restyle.
99
+ // Read from the theme's palette rather than a fixed ramp. The private alias
100
+ // reads the host's --ag-ui-chart-N first (see token_styles), so a host's
101
+ // colour wins over any theme's, and a theme can still supply its own.
102
102
  const SERIES_COLORS: readonly string[] = [
103
- "var(--ag-ui-chart-1, #4f7cff)",
104
- "var(--ag-ui-chart-2, #21b573)",
105
- "var(--ag-ui-chart-3, #e0803c)",
106
- "var(--ag-ui-chart-4, #b563d8)",
107
- "var(--ag-ui-chart-5, #d84f6e)",
108
- "var(--ag-ui-chart-6, #3ba7c4)",
103
+ "var(--_chart-1)",
104
+ "var(--_chart-2)",
105
+ "var(--_chart-3)",
106
+ "var(--_chart-4)",
107
+ "var(--_chart-5)",
108
+ "var(--_chart-6)",
109
109
  ];
110
110
 
111
111
  /** The colour for series `index`, wrapping when there are more series than colours. */
@@ -0,0 +1,63 @@
1
+ // Charts drawn in the transcript: the block, its title and legend.
2
+ export const CHART_STYLES: string = `
3
+ /* Charts.
4
+ *
5
+ * The SVG scales to the column and carries no colours of its own beyond the
6
+ * series palette, so a host restyles it the same way it restyles everything
7
+ * else. Series colours are custom properties with fallbacks rather than fixed
8
+ * values, and the axis furniture inherits currentColor at low opacity so it
9
+ * reads correctly in either theme without a second palette.
10
+ */
11
+ /* A chart is sized like a message, not like a panel: it takes the width it
12
+ needs and stops, rather than stretching into whatever room the transcript
13
+ has. A message bubble caps at 80% for the same reason -- widening the panel
14
+ should not resize what is already in it. The cap is a token so a host with a
15
+ wide panel and a real reason can raise it; the default is the width this
16
+ renderer has always drawn at. Below the cap the block still fills its column,
17
+ which is what a narrow panel needs. */
18
+ .chart-block {
19
+ align-self: flex-start;
20
+ width: 100%;
21
+ max-width: var(--_chart-max-width);
22
+ margin: 6px 0;
23
+ color: var(--_fg);
24
+ }
25
+
26
+ /* The drawing fills the block's width and carries its own height in the
27
+ viewBox, so it is laid out rather than magnified: block display keeps the
28
+ inline baseline gap from adding a stripe under every chart. */
29
+ .chart-block svg {
30
+ display: block;
31
+ width: 100%;
32
+ height: auto;
33
+ }
34
+
35
+ .chart-title {
36
+ margin-bottom: 2px;
37
+ font-size: 0.85em;
38
+ font-weight: 600;
39
+ opacity: 0.85;
40
+ }
41
+
42
+ .chart-legend {
43
+ display: flex;
44
+ flex-wrap: wrap;
45
+ gap: 4px 12px;
46
+ margin-top: 4px;
47
+ font-size: 0.78em;
48
+ opacity: 0.75;
49
+ }
50
+
51
+ .chart-legend-item {
52
+ display: inline-flex;
53
+ align-items: center;
54
+ gap: 5px;
55
+ }
56
+
57
+ .chart-legend-swatch {
58
+ width: 9px;
59
+ height: 9px;
60
+ border-radius: 2px;
61
+ flex: 0 0 auto;
62
+ }
63
+ `;
@@ -0,0 +1,71 @@
1
+ // The composer's attachment picker, voice button and attachment tray.
2
+ export const ATTACHMENT_STYLES: string = `
3
+ /* ── File attachments ───────────────────────────────────────────────────── */
4
+ /* The picker button sits in the composer's tool row; hidden until
5
+ data-attachments-url. */
6
+ .attach-input {
7
+ display: none;
8
+ }
9
+
10
+ /* The mic button's mount point; filled only once ComposerVoice.wire mounts
11
+ the control, which it skips unless transcription is configured. */
12
+ .voice-slot {
13
+ display: contents;
14
+ }
15
+
16
+ /* Recording: a red tint + a gentle pulse so it's clearly "live". */
17
+ .voice-btn[data-state="recording"] {
18
+ background: var(--_danger);
19
+ color: var(--_on-danger);
20
+ animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
21
+ }
22
+
23
+ .voice-btn[data-state="recording"]:hover {
24
+ background: var(--_danger);
25
+ color: var(--_on-danger);
26
+ }
27
+
28
+ @keyframes ag-ui-voice-pulse {
29
+ 0%, 100% { opacity: 1; }
30
+ 50% { opacity: 0.6; }
31
+ }
32
+
33
+ @media (prefers-reduced-motion: reduce) {
34
+ .voice-btn[data-state="recording"] {
35
+ animation: none;
36
+ }
37
+ }
38
+
39
+ /* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
40
+ .attachment-slot {
41
+ display: contents;
42
+ }
43
+
44
+ /* The padding is the only separation the tray gets: its slot is display:
45
+ contents, so the tray is a direct child of the .chat column, and that column
46
+ sets no gap. The bottom value keeps a chip off the composer's top edge.
47
+ The inline value here is only the default: the page placement overrides
48
+ padding-inline further down to compute its reading-column gutter, and wins
49
+ on specificity whichever way this declaration is written. Read the two
50
+ together before changing either. */
51
+ .attachment-tray {
52
+ display: flex;
53
+ flex-wrap: wrap;
54
+ gap: 6px;
55
+ padding: 8px 12px;
56
+ }
57
+
58
+ /* The tray sets the hidden property while empty, and an author display beats
59
+ the UA stylesheet's rule for it, so without this the empty tray keeps
60
+ laying out and its padding is permanent dead space above the composer. */
61
+ .attachment-tray[hidden] {
62
+ display: none;
63
+ }
64
+
65
+ .attachment-chips {
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ gap: 6px;
69
+ margin-top: 6px;
70
+ }
71
+ `;
@@ -0,0 +1,112 @@
1
+ import { ICON_ATTACH, ICON_SEND, ICON_STOP } from "../../constants.js";
2
+ import { glyphSlot } from "../shell/glyph_slot.js";
3
+ import type { UiStrings } from "../ui_strings.js";
4
+
5
+ /** The composer's parts, owned by the element, and what they call back into. */
6
+ export interface ComposerOptions {
7
+ readonly input: HTMLTextAreaElement;
8
+ readonly send: HTMLButtonElement;
9
+ readonly attachButton: HTMLButtonElement;
10
+ readonly fileInput: HTMLInputElement;
11
+ readonly voiceSlot: HTMLElement;
12
+ /** The line saying what the composer still needs; sits above the input. */
13
+ readonly hint: HTMLElement;
14
+ /** The queued-message row; sits above the input beside the tray. */
15
+ readonly queuedRow: HTMLElement;
16
+ /** Where the attachment tray mounts; sits above the input. */
17
+ readonly attachSlot: HTMLElement;
18
+ readonly strings: UiStrings;
19
+ readonly signal: AbortSignal;
20
+ readonly onKeydown: (event: KeyboardEvent) => void;
21
+ readonly onInput: () => void;
22
+ /** Send while idle, Stop while a run is in flight: the element decides. */
23
+ readonly onSend: () => void;
24
+ readonly onFilesPicked: () => void;
25
+ }
26
+
27
+ /**
28
+ * Build the composer row -- the field, and the tool row under it with the
29
+ * attach, voice and send controls -- and ready the composer surfaces the
30
+ * element mounts above it: the hint, the queued row and the attachment slot.
31
+ * Returns the row; the element places it and the surfaces.
32
+ */
33
+ export function buildComposer(options: ComposerOptions): HTMLElement {
34
+ const { input, send, attachButton, fileInput, strings, signal } = options;
35
+
36
+ const queued = options.queuedRow;
37
+ queued.className = "queued";
38
+ queued.setAttribute("part", "queued");
39
+ queued.setAttribute("role", "group");
40
+ queued.setAttribute("aria-label", strings.queued);
41
+ queued.hidden = true;
42
+
43
+ const inputRow = document.createElement("div");
44
+ inputRow.className = "input-row";
45
+ inputRow.setAttribute("part", "composer");
46
+
47
+ // One bordered surface holds the field and the tool row under it, so the
48
+ // icon buttons stop competing with the field for weight.
49
+ const composer = document.createElement("div");
50
+ composer.className = "composer";
51
+ composer.setAttribute("part", "composer-surface");
52
+
53
+ const tools = document.createElement("div");
54
+ tools.className = "composer-tools";
55
+ tools.setAttribute("part", "composer-tools");
56
+
57
+ input.className = "input";
58
+ input.setAttribute("part", "input");
59
+ input.setAttribute("aria-label", strings.message);
60
+ input.rows = 1;
61
+ input.placeholder = strings.inputPlaceholder;
62
+ input.addEventListener("keydown", options.onKeydown, { signal });
63
+ input.addEventListener("input", options.onInput, { signal });
64
+
65
+ // Icon-only, with both glyphs mounted at once and CSS showing the one the
66
+ // state calls for — swapping a single glyph would leave a host that slotted
67
+ // its own Send mark holding a stop icon mid-run.
68
+ send.className = "send";
69
+ send.type = "button";
70
+ send.setAttribute("part", "send");
71
+ send.replaceChildren(
72
+ glyphSlot("icon-send", "send-send", ICON_SEND),
73
+ glyphSlot("icon-stop", "send-stop", ICON_STOP),
74
+ );
75
+ send.title = strings.send;
76
+ send.setAttribute("aria-label", strings.send);
77
+ send.dataset["state"] = "idle";
78
+ send.addEventListener("click", options.onSend, { signal });
79
+
80
+ const hint = options.hint;
81
+ hint.className = "skill-hint";
82
+ hint.setAttribute("part", "skill-hint");
83
+ hint.hidden = true;
84
+
85
+ // File-upload affordance: a paperclip button (hidden until
86
+ // `data-attachments-url` is wired) opening a hidden multi-file input.
87
+ // Drag-and-drop covers the whole shell (wired by ComposerAttachments).
88
+ attachButton.className = "attach-btn";
89
+ attachButton.type = "button";
90
+ attachButton.setAttribute("part", "attach-button");
91
+ attachButton.replaceChildren(glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
92
+ attachButton.title = strings.attachFiles;
93
+ attachButton.setAttribute("aria-label", strings.attachFiles);
94
+ attachButton.hidden = true;
95
+ attachButton.addEventListener("click", () => fileInput.click(), { signal });
96
+
97
+ fileInput.className = "attach-input";
98
+ fileInput.type = "file";
99
+ fileInput.multiple = true;
100
+ fileInput.hidden = true;
101
+ fileInput.addEventListener("change", options.onFilesPicked, { signal });
102
+
103
+ options.attachSlot.className = "attachment-slot";
104
+
105
+ // Mic button mount point (kept empty until ComposerVoice mounts the control).
106
+ options.voiceSlot.className = "voice-slot";
107
+
108
+ tools.append(attachButton, options.voiceSlot, send);
109
+ composer.append(input, tools);
110
+ inputRow.append(composer, fileInput);
111
+ return inputRow;
112
+ }
@@ -0,0 +1,174 @@
1
+ // The composer: field, tool row, icon buttons, send and stop, and the glyph
2
+ // class every icon button and icon holder paints with.
3
+ export const COMPOSER_STYLES: string = `
4
+ /* ── Composer ───────────────────────────────────────────────────────────────
5
+ One surface owns the border, the background and the focus ring; the field
6
+ and its tool row sit inside it, rather than being siblings stretched to the
7
+ textarea's height. */
8
+ .input-row {
9
+ display: flex;
10
+ padding: 12px;
11
+ border-top: 1px solid var(--_border);
12
+ }
13
+
14
+ .composer {
15
+ flex: 1;
16
+ min-width: 0;
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 4px;
20
+ padding: 6px 6px 6px 10px;
21
+ background: var(--_input-bg);
22
+ border: 1px solid var(--_border);
23
+ border-radius: var(--_composer-radius);
24
+ transition: border-color var(--_motion) var(--_ease);
25
+ }
26
+
27
+ .composer:focus-within {
28
+ border-color: var(--_accent);
29
+ }
30
+
31
+ /* The field grows with its content (sized by #autoGrow) up to the ceiling,
32
+ then scrolls. border-box keeps that measurement stable: with content-box the
33
+ padding would be added to every scrollHeight read and the field would creep
34
+ taller on each keystroke. */
35
+ .input {
36
+ box-sizing: border-box;
37
+ resize: none;
38
+ max-height: var(--_composer-max-height);
39
+ overflow-y: auto;
40
+ /* Scrolling past the end of this must not scroll the page behind it. */
41
+ overscroll-behavior: contain;
42
+ padding: 6px 4px 2px;
43
+ background: transparent;
44
+ border: none;
45
+ font: inherit;
46
+ color: inherit;
47
+ outline: none;
48
+ }
49
+
50
+ .composer-tools {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 2px;
54
+ }
55
+
56
+ /* Icon buttons: quiet at rest, so the field is what the eye lands on. */
57
+ .attach-btn,
58
+ .voice-btn {
59
+ display: inline-flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ width: var(--_tool-btn-size);
63
+ height: var(--_tool-btn-size);
64
+ padding: 0;
65
+ border: none;
66
+ border-radius: 8px;
67
+ background: transparent;
68
+ color: var(--_muted);
69
+ font: inherit;
70
+ line-height: 1;
71
+ cursor: pointer;
72
+ transition:
73
+ background-color var(--_motion) var(--_ease),
74
+ color var(--_motion) var(--_ease);
75
+ }
76
+
77
+ .attach-btn:hover,
78
+ .voice-btn:hover {
79
+ background: var(--_hover);
80
+ color: var(--_fg);
81
+ }
82
+
83
+ .attach-btn:disabled,
84
+ .voice-btn:disabled {
85
+ cursor: default;
86
+ opacity: 0.6;
87
+ }
88
+
89
+ /* The same trap the attachment tray carries a note about, two rules along: an
90
+ author display beats the UA stylesheet's rule for the hidden property, so a
91
+ button the element has explicitly hidden keeps laying out and painting. The
92
+ clip is hidden until a host supplies an upload handler or an attachments URL,
93
+ and without this it is a visible control that cannot do anything.
94
+
95
+ The mic needs no such rule, and the asymmetry is worth knowing before adding
96
+ one: it is not hidden when unconfigured, it is never built. The voice wiring
97
+ returns before constructing the button, leaving only an empty voice slot that
98
+ is display: contents. A hidden-state rule for the mic would match nothing. */
99
+ .attach-btn[hidden] {
100
+ display: none;
101
+ }
102
+
103
+ /* Send closes the row on the right: a circle, the only filled control in the
104
+ composer, so "the thing that acts" reads at a glance. */
105
+ .send {
106
+ margin-left: auto;
107
+ display: inline-flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ flex: none;
111
+ width: var(--_send-size);
112
+ height: var(--_send-size);
113
+ padding: 0;
114
+ border: none;
115
+ border-radius: 50%;
116
+ background: var(--_accent);
117
+ color: var(--_on-accent);
118
+ font: inherit;
119
+ line-height: 1;
120
+ cursor: pointer;
121
+ transition:
122
+ background-color var(--_motion) var(--_ease),
123
+ transform var(--_motion) var(--_ease-pop);
124
+ }
125
+
126
+ .send:hover {
127
+ transform: scale(1.08);
128
+ }
129
+
130
+ .send:active {
131
+ transform: scale(0.92);
132
+ }
133
+
134
+ .send:disabled {
135
+ opacity: 0.5;
136
+ cursor: default;
137
+ transform: none;
138
+ }
139
+
140
+ /* The composer button doubles as the Stop control while a run is in flight —
141
+ same circle, different glyph, so nothing moves when a run starts. */
142
+ .send[data-state="running"] {
143
+ background: var(--_muted);
144
+ }
145
+
146
+ .send[data-state="idle"] .send-stop,
147
+ .send[data-state="running"] .send-send {
148
+ display: none;
149
+ }
150
+
151
+ /* Glyphs — one class, painted from the button's own colour so every state
152
+ (hover, recording, running) carries the icon with it. */
153
+ .glyph {
154
+ width: var(--_glyph-size);
155
+ height: var(--_glyph-size);
156
+ fill: none;
157
+ stroke: currentColor;
158
+ stroke-width: var(--_glyph-stroke);
159
+ stroke-linecap: round;
160
+ stroke-linejoin: round;
161
+ }
162
+
163
+ .glyph--solid {
164
+ fill: currentColor;
165
+ stroke: none;
166
+ }
167
+
168
+ /* In an icon holder (header brand, launcher) the glyph takes the holder's size
169
+ rather than the composer's. */
170
+ .icon-holder .glyph {
171
+ width: 100%;
172
+ height: 100%;
173
+ }
174
+ `;