@schlenkr/quassel 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Chat building blocks for React: a streaming transcript with thinking and tool st
4
4
  actions, an input card with attachments and steering, and a small event contract that works
5
5
  on the server as well. quassel is the chat of [RAgents](https://github.com/SchlenkR/RAgents),
6
6
  published as a library. It ships a compiled stylesheet, so hosts need no Tailwind, and every
7
- color, radius, font and font size is a `--qsl-*` variable, and the text size follows the host by default.
7
+ color, radius, font and font size is a `--qsl-*` variable, with compact defaults for text and tool steps.
8
8
 
9
9
  ![An agent chat with grouped steps, timestamps, Markdown and the input card](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/agent.png)
10
10
 
@@ -93,20 +93,28 @@ Any backend that emits the `@schlenkr/quassel/events` stream works with the same
93
93
 
94
94
  ## Text size
95
95
 
96
- quassel inherits the font size of the place it is mounted in. Chat text and the input use that
97
- size, meta lines, step rows and labels are derived from it as smaller steps, and nothing
98
- compounds when elements nest. One variable changes it for a chat:
96
+ quassel starts at 13px with a line height of 1.5, a 10px message gap and a 3px dense
97
+ step gap. Tool rows and timestamps use 88% of the text size. Expanded tool groups have a
98
+ 1px gap between rows; tool rows have 1px of vertical padding on each side.
99
99
 
100
- ```css
101
- .my-chat {
102
- --qsl-font-size: 15px;
103
- }
100
+ Every size derives from one base, so nested elements never compound. Set `--qsl-font-size`
101
+ on a wrapper, or `appearance.fontSize`, to change it. Use `1em` to follow the host's font size.
102
+ `appearance` also offers `toolFontScale`, `toolGroupGap` and `toolRowPadding`.
103
+
104
+ User message backgrounds fill the available row by default while text keeps its width and
105
+ alignment. `bubbleOptions={{ userBackgroundFullWidth: false }}` restores a fitted background.
106
+ Timestamps sit beside messages by default; their size follows the tool text unless overridden:
107
+
108
+ ```tsx
109
+ <ChatMessages
110
+ messages={messages}
111
+ showTimestamps
112
+ timestampOptions={{ position: "above", fontScale: 0.88 }}
113
+ />
104
114
  ```
105
115
 
106
- `--qsl-chat-font-size` and `--qsl-text-meta` (and `-trace`, `-label`, `-xs` to `-xl`) override
107
- single steps. Up to 0.4.9 the root was fixed at 14px and the chat text at 13px, so hosts had to
108
- override sizes by hand; from 0.5.0 on that is no longer needed. Details in the
109
- [guide](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md#text-size).
116
+ `--qsl-text-trace`, `--qsl-timestamp-font-size` and the other `--qsl-text-*` variables override
117
+ individual sizes. The [guide](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md#text-size) lists the full API.
110
118
 
111
119
  ## Development
112
120
 
package/chat.css CHANGED
@@ -46,6 +46,9 @@
46
46
  .qsl\:relative {
47
47
  position: relative;
48
48
  }
49
+ .qsl\:inset-0 {
50
+ inset: 0px;
51
+ }
49
52
  .qsl\:inset-x-0 {
50
53
  inset-inline: 0px;
51
54
  }
@@ -112,11 +115,11 @@
112
115
  .qsl\:mt-4 {
113
116
  margin-top: calc(var(--qsl-spacing) * 4);
114
117
  }
115
- .qsl\:mt-\[var\(--qsl-chat-dense-message-gap\,8px\)\] {
116
- margin-top: var(--qsl-chat-dense-message-gap,8px);
118
+ .qsl\:mt-\[var\(--qsl-chat-dense-message-gap\,3px\)\] {
119
+ margin-top: var(--qsl-chat-dense-message-gap,3px);
117
120
  }
118
- .qsl\:mt-\[var\(--qsl-chat-message-gap\,16px\)\] {
119
- margin-top: var(--qsl-chat-message-gap,16px);
121
+ .qsl\:mt-\[var\(--qsl-chat-message-gap\,10px\)\] {
122
+ margin-top: var(--qsl-chat-message-gap,10px);
120
123
  }
121
124
  .qsl\:mb-0\.5 {
122
125
  margin-bottom: calc(var(--qsl-spacing) * 0.5);
@@ -336,6 +339,9 @@
336
339
  .qsl\:items-end {
337
340
  align-items: flex-end;
338
341
  }
342
+ .qsl\:items-stretch {
343
+ align-items: stretch;
344
+ }
339
345
  .qsl\:justify-between {
340
346
  justify-content: space-between;
341
347
  }
@@ -354,9 +360,6 @@
354
360
  .qsl\:gap-0 {
355
361
  gap: 0px;
356
362
  }
357
- .qsl\:gap-0\.5 {
358
- gap: calc(var(--qsl-spacing) * 0.5);
359
- }
360
363
  .qsl\:gap-1 {
361
364
  gap: var(--qsl-spacing);
362
365
  }
@@ -375,8 +378,11 @@
375
378
  .qsl\:gap-\[5px\] {
376
379
  gap: 5px;
377
380
  }
378
- .qsl\:gap-\[var\(--qsl-chat-dense-message-gap\,8px\)\] {
379
- gap: var(--qsl-chat-dense-message-gap,8px);
381
+ .qsl\:gap-\[var\(--qsl-chat-dense-message-gap\,3px\)\] {
382
+ gap: var(--qsl-chat-dense-message-gap,3px);
383
+ }
384
+ .qsl\:gap-\[var\(--qsl-tool-group-gap\,1px\)\] {
385
+ gap: var(--qsl-tool-group-gap,1px);
380
386
  }
381
387
  :where(.qsl\:space-y-0 > :not(:last-child)) {
382
388
  --tw-space-y-reverse: 0;
@@ -395,6 +401,9 @@
395
401
  .qsl\:self-baseline {
396
402
  align-self: baseline;
397
403
  }
404
+ .qsl\:self-start {
405
+ align-self: flex-start;
406
+ }
398
407
  .qsl\:overflow-auto {
399
408
  overflow: auto;
400
409
  }
@@ -530,8 +539,8 @@
530
539
  .qsl\:bg-\[linear-gradient\(to_right\,var\(--qsl-scroll-cover\,var\(--qsl-background\)\)_50\%\,transparent\)_left\/20px_100\%_no-repeat_local\,linear-gradient\(to_left\,var\(--qsl-scroll-cover\,var\(--qsl-background\)\)_50\%\,transparent\)_right\/20px_100\%_no-repeat_local\,linear-gradient\(to_right\,\#00000047\,transparent\)_left\/14px_100\%_no-repeat_scroll\,linear-gradient\(to_left\,\#00000047\,transparent\)_right\/14px_100\%_no-repeat_scroll\] {
531
540
  background-image: linear-gradient(to right,var(--qsl-scroll-cover,var(--qsl-background)) 50%,transparent) left/20px 100% no-repeat local,linear-gradient(to left,var(--qsl-scroll-cover,var(--qsl-background)) 50%,transparent) right/20px 100% no-repeat local,linear-gradient(to right,#00000047,transparent) left/14px 100% no-repeat scroll,linear-gradient(to left,#00000047,transparent) right/14px 100% no-repeat scroll;
532
541
  }
533
- .qsl\:\[mask-image\:linear-gradient\(to_bottom\,\#000_calc\(100\%_-_var\(--qsl-chat-message-gap\,16px\)\)\,transparent\)\] {
534
- mask-image: linear-gradient(to bottom,#000 calc(100% - var(--qsl-chat-message-gap,16px)),transparent);
542
+ .qsl\:\[mask-image\:linear-gradient\(to_bottom\,\#000_calc\(100\%_-_var\(--qsl-chat-message-gap\,10px\)\)\,transparent\)\] {
543
+ mask-image: linear-gradient(to bottom,#000 calc(100% - var(--qsl-chat-message-gap,10px)),transparent);
535
544
  }
536
545
  .qsl\:\[mask-image\:linear-gradient\(to_bottom\,\#000_calc\(var\(--qsl-fade-edge\)_-_40px\)\,transparent_calc\(var\(--qsl-fade-edge\)_\+_16px\)\)\] {
537
546
  mask-image: linear-gradient(to bottom,#000 calc(var(--qsl-fade-edge) - 40px),transparent calc(var(--qsl-fade-edge) + 16px));
@@ -608,6 +617,9 @@
608
617
  .qsl\:py-\[3px\] {
609
618
  padding-block: 3px;
610
619
  }
620
+ .qsl\:py-\[var\(--qsl-tool-row-padding\,1px\)\] {
621
+ padding-block: var(--qsl-tool-row-padding,1px);
622
+ }
611
623
  .qsl\:py-px {
612
624
  padding-block: 1px;
613
625
  }
@@ -635,8 +647,8 @@
635
647
  .qsl\:pb-\[calc\(var\(--qsl-composer-height\,0px\)_\+_max\(3\.25em\,40px\)\)\] {
636
648
  padding-bottom: calc(var(--qsl-composer-height,0px) + max(3.25em, 40px));
637
649
  }
638
- .qsl\:pb-\[var\(--qsl-chat-message-gap\,16px\)\] {
639
- padding-bottom: var(--qsl-chat-message-gap,16px);
650
+ .qsl\:pb-\[var\(--qsl-chat-message-gap\,10px\)\] {
651
+ padding-bottom: var(--qsl-chat-message-gap,10px);
640
652
  }
641
653
  .qsl\:pl-1 {
642
654
  padding-left: var(--qsl-spacing);
@@ -692,8 +704,11 @@
692
704
  .qsl\:text-meta {
693
705
  font-size: var(--qsl-text-meta, calc(var(--qsl-base) * 0.8));
694
706
  }
707
+ .qsl\:text-timestamp {
708
+ font-size: var(--qsl-timestamp-font-size, var(--qsl-text-trace, calc(var(--qsl-base) * var(--qsl-tool-font-scale, 0.88))));
709
+ }
695
710
  .qsl\:text-trace {
696
- font-size: var(--qsl-text-trace, calc(var(--qsl-base) * 0.85));
711
+ font-size: var(--qsl-text-trace, calc(var(--qsl-base) * var(--qsl-tool-font-scale, 0.88)));
697
712
  }
698
713
  .qsl\:leading-\[1\.3\] {
699
714
  --tw-leading: 1.3;
@@ -719,10 +734,6 @@
719
734
  --tw-leading: var(--qsl-chat-line-height,1.5);
720
735
  line-height: var(--qsl-chat-line-height,1.5);
721
736
  }
722
- .qsl\:leading-\[var\(--qsl-chat-line-height\,1\.625\)\] {
723
- --tw-leading: var(--qsl-chat-line-height,1.625);
724
- line-height: var(--qsl-chat-line-height,1.625);
725
- }
726
737
  .qsl\:leading-normal {
727
738
  --tw-leading: var(--qsl-leading-normal);
728
739
  line-height: var(--qsl-leading-normal);
@@ -925,8 +936,8 @@
925
936
  .qsl\:\[--qsl-scroll-cover\:var\(--qsl-secondary\)\] {
926
937
  --qsl-scroll-cover: var(--qsl-secondary);
927
938
  }
928
- .qsl\:\[-webkit-mask-image\:linear-gradient\(to_bottom\,\#000_calc\(100\%_-_var\(--qsl-chat-message-gap\,16px\)\)\,transparent\)\] {
929
- -webkit-mask-image: linear-gradient(to bottom,#000 calc(100% - var(--qsl-chat-message-gap,16px)),transparent);
939
+ .qsl\:\[-webkit-mask-image\:linear-gradient\(to_bottom\,\#000_calc\(100\%_-_var\(--qsl-chat-message-gap\,10px\)\)\,transparent\)\] {
940
+ -webkit-mask-image: linear-gradient(to bottom,#000 calc(100% - var(--qsl-chat-message-gap,10px)),transparent);
930
941
  }
931
942
  .qsl\:\[-webkit-mask-image\:linear-gradient\(to_bottom\,\#000_calc\(var\(--qsl-fade-edge\)_-_40px\)\,transparent_calc\(var\(--qsl-fade-edge\)_\+_16px\)\)\] {
932
943
  -webkit-mask-image: linear-gradient(to bottom,#000 calc(var(--qsl-fade-edge) - 40px),transparent calc(var(--qsl-fade-edge) + 16px));
@@ -1119,9 +1130,6 @@
1119
1130
  :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:p-0 {
1120
1131
  padding: 0px;
1121
1132
  }
1122
- :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:text-foreground {
1123
- color: var(--qsl-foreground);
1124
- }
1125
1133
  :where([data-tone="on-color"]) .qsl\:in-data-\[tone\=on-color\]\:border-white\/35 {
1126
1134
  border-color: var(--qsl-color-white);
1127
1135
  @supports (color: color-mix(in lab, red, red)) {
@@ -1652,7 +1660,7 @@
1652
1660
  @property --qsl-base {
1653
1661
  syntax: "<length>";
1654
1662
  inherits: true;
1655
- initial-value: 14px;
1663
+ initial-value: 13px;
1656
1664
  }
1657
1665
  @layer theme {
1658
1666
  :where(:root, :host, [data-theme="light"], [data-theme="system"]) {
@@ -1735,7 +1743,7 @@
1735
1743
  font-family: var(--qsl-font);
1736
1744
  font-feature-settings: normal;
1737
1745
  font-variation-settings: normal;
1738
- font-size: var(--qsl-font-size, 1em);
1746
+ font-size: var(--qsl-font-size, 13px);
1739
1747
  --qsl-base: 1em;
1740
1748
  color: var(--qsl-foreground);
1741
1749
  -webkit-tap-highlight-color: transparent;
@@ -2018,7 +2026,7 @@
2018
2026
  @layer properties {
2019
2027
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
2020
2028
  :root, :host {
2021
- --qsl-base: 14px;
2029
+ --qsl-base: 13px;
2022
2030
  }
2023
2031
  *, ::before, ::after, ::backdrop {
2024
2032
  --tw-space-y-reverse: 0;
package/index.js CHANGED
@@ -158,7 +158,7 @@ var defaultTexts = englishTexts;
158
158
 
159
159
  // packages/quassel/src/ui/cn.ts
160
160
  import { createCn } from "cn/config";
161
- var cn = createCn({ prefix: "qsl", extend: { theme: { text: ["meta", "trace", "label", "chat"] } } });
161
+ var cn = createCn({ prefix: "qsl", extend: { theme: { text: ["meta", "trace", "timestamp", "label", "chat"] } } });
162
162
 
163
163
  // packages/quassel/src/ChatMessages.tsx
164
164
  import { useCallback as useCallback2, useContext as useContext3, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
@@ -933,10 +933,14 @@ import { CheckIcon as CheckIcon4, ChevronDownIcon, ChevronRightIcon, ChevronUpIc
933
933
  // packages/quassel/src/options.ts
934
934
  var cssLength = (value) => typeof value === "number" ? `${value}px` : value;
935
935
  var appearanceStyle = (appearance) => ({
936
+ "--qsl-font-size": cssLength(appearance?.fontSize),
936
937
  "--qsl-chat-font-size": cssLength(appearance?.fontSize),
937
938
  "--qsl-chat-line-height": appearance?.lineHeight,
938
939
  "--qsl-chat-message-gap": cssLength(appearance?.messageGap),
939
- "--qsl-chat-dense-message-gap": cssLength(appearance?.denseMessageGap)
940
+ "--qsl-chat-dense-message-gap": cssLength(appearance?.denseMessageGap),
941
+ "--qsl-tool-font-scale": appearance?.toolFontScale,
942
+ "--qsl-tool-group-gap": cssLength(appearance?.toolGroupGap),
943
+ "--qsl-tool-row-padding": cssLength(appearance?.toolRowPadding)
940
944
  });
941
945
 
942
946
  // packages/quassel/src/timestamps.ts
@@ -1159,10 +1163,10 @@ function announce(text) {
1159
1163
 
1160
1164
  // packages/quassel/src/ChatMessages.tsx
1161
1165
  import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
1162
- var stepClasses = "qsl:mt-[var(--qsl-chat-message-gap,16px)]";
1163
- var denseStepClasses = "qsl:mt-[var(--qsl-chat-dense-message-gap,8px)]";
1164
- var traceClasses = "qsl:flex qsl:w-full qsl:gap-2 qsl:pl-6 qsl:text-left qsl:text-trace qsl:leading-[1.375] qsl:text-muted-foreground";
1165
- var traceButtonClasses = "qsl:cursor-pointer qsl:rounded-md qsl:py-1 qsl:pr-2 qsl:transition-colors qsl:hover:bg-secondary qsl:hover:text-foreground qsl:focus-visible:bg-secondary qsl:focus-visible:text-foreground qsl:focus-visible:outline-none";
1166
+ var stepClasses = "qsl:mt-[var(--qsl-chat-message-gap,10px)]";
1167
+ var denseStepClasses = "qsl:mt-[var(--qsl-chat-dense-message-gap,3px)]";
1168
+ var traceClasses = "qsl:flex qsl:w-full qsl:gap-2 qsl:py-[var(--qsl-tool-row-padding,1px)] qsl:pl-6 qsl:text-left qsl:text-trace qsl:leading-[1.375] qsl:text-muted-foreground";
1169
+ var traceButtonClasses = "qsl:cursor-pointer qsl:rounded-md qsl:pr-2 qsl:transition-colors qsl:hover:bg-secondary qsl:hover:text-foreground qsl:focus-visible:bg-secondary qsl:focus-visible:text-foreground qsl:focus-visible:outline-none";
1166
1170
  var traceIconClasses = "qsl:mt-0.5 qsl:flex-none qsl:opacity-60";
1167
1171
  var traceErrorIconClasses = "qsl:mt-0.5 qsl:flex-none qsl:text-destructive qsl:opacity-100";
1168
1172
  var traceLineClasses = "qsl:min-w-0 qsl:self-baseline qsl:[overflow-wrap:anywhere]";
@@ -1181,7 +1185,7 @@ var chipClasses = cn(
1181
1185
  var chipInteractiveClasses = "qsl:cursor-pointer qsl:hover:border-border qsl:hover:bg-secondary qsl:hover:text-foreground qsl:focus-visible:border-border qsl:focus-visible:bg-secondary qsl:focus-visible:text-foreground qsl:focus-visible:outline-none qsl:data-[state=done]:hover:opacity-100 qsl:data-[state=done]:focus-visible:opacity-100";
1182
1186
  var collapseAtBottomFrom = 11;
1183
1187
  var maxAnnouncementsAtOnce = 2;
1184
- var bubbleBodyClasses = "qsl:relative qsl:max-w-[86%] qsl:px-3 qsl:py-2 qsl:leading-[var(--qsl-chat-line-height,1.625)] qsl:[overflow-wrap:anywhere] qsl:@max-[620px]/chat:max-w-full";
1188
+ var bubbleBodyClasses = "qsl:relative qsl:max-w-[86%] qsl:px-3 qsl:py-2 qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:[overflow-wrap:anywhere] qsl:@max-[620px]/chat:max-w-full";
1185
1189
  function isStep(message) {
1186
1190
  return !!message && (message.role === "thinking" || message.role === "tool");
1187
1191
  }
@@ -1337,13 +1341,22 @@ function ChatMessages({
1337
1341
  [selected, detailMode, running2, dock]
1338
1342
  );
1339
1343
  const openActions = dock ? messages.filter(isOpenAction) : [];
1340
- const withTime = (content, key, at, dense = false) => showTimestamps ? /* @__PURE__ */ jsxs6("div", { className: cn("qsl:flex qsl:items-baseline qsl:gap-2", dense ? denseStepClasses : stepClasses), children: [
1341
- /* @__PURE__ */ jsx13("time", { dateTime: messageDate(at)?.toISOString(), className: cn(
1342
- "qsl:flex-none qsl:text-meta qsl:tabular-nums qsl:text-muted-foreground qsl:opacity-85",
1343
- timestampOptions?.format && timestampOptions.format !== "time" ? "qsl:w-[clamp(60px,20%,120px)]" : "qsl:min-w-[5ch] qsl:whitespace-nowrap"
1344
- ), children: messageDate(at) ? timestampLabel(messageDate(at), timestampOptions, now) : "" }),
1345
- /* @__PURE__ */ jsx13("div", { className: "qsl:min-w-0 qsl:flex-1 qsl:[&>*:first-child]:mt-0", children: content })
1346
- ] }, `time-${key}`) : content;
1344
+ const withTime = (content, key, at, dense = false) => showTimestamps ? /* @__PURE__ */ jsxs6(
1345
+ "div",
1346
+ {
1347
+ className: cn("qsl:flex", timestampOptions?.position === "above" ? "qsl:flex-col qsl:items-stretch qsl:gap-1" : "qsl:items-baseline qsl:gap-2", dense ? denseStepClasses : stepClasses),
1348
+ "data-chat": "timestamped",
1349
+ "data-timestamp-position": timestampOptions?.position ?? "aside",
1350
+ children: [
1351
+ /* @__PURE__ */ jsx13("time", { dateTime: messageDate(at)?.toISOString(), className: cn(
1352
+ "qsl:flex-none qsl:text-timestamp qsl:leading-[1.375] qsl:tabular-nums qsl:text-muted-foreground qsl:opacity-85",
1353
+ timestampOptions?.position === "above" ? "qsl:self-start" : timestampOptions?.format && timestampOptions.format !== "time" ? "qsl:w-[clamp(60px,20%,120px)]" : "qsl:min-w-[5ch] qsl:whitespace-nowrap"
1354
+ ), children: messageDate(at) ? timestampLabel(messageDate(at), timestampOptions, now) : "" }),
1355
+ /* @__PURE__ */ jsx13("div", { className: "qsl:min-w-0 qsl:flex-1 qsl:[&>*:first-child]:mt-0", children: content })
1356
+ ]
1357
+ },
1358
+ `time-${key}`
1359
+ ) : content;
1347
1360
  const chipMode = detailMode === "current" || detailMode === "chips" || detailMode === "icons";
1348
1361
  const elapsedAfter = running2 && toolElapsedThreshold !== false ? toolElapsedThreshold : void 0;
1349
1362
  const groupMode = detailMode === "grouped";
@@ -1439,7 +1452,8 @@ function ChatMessages({
1439
1452
  const transcript = /* @__PURE__ */ jsxs6("div", { className: cn("qsl:@container/chat qsl:relative qsl:flex qsl:min-h-0 qsl:flex-1 qsl:flex-col", className), "data-quassel": "", style: {
1440
1453
  ...appearanceStyle(appearance),
1441
1454
  "--qsl-chat-content-max-width": typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth,
1442
- "--qsl-chat-horizontal-padding": typeof horizontalPadding === "number" ? `${horizontalPadding}px` : horizontalPadding
1455
+ "--qsl-chat-horizontal-padding": typeof horizontalPadding === "number" ? `${horizontalPadding}px` : horizontalPadding,
1456
+ "--qsl-timestamp-font-size": timestampOptions?.fontScale === void 0 ? void 0 : `calc(var(--qsl-base) * ${timestampOptions.fontScale})`
1443
1457
  }, children: [
1444
1458
  /* @__PURE__ */ jsx13(
1445
1459
  "div",
@@ -1505,7 +1519,7 @@ function ChatMessages({
1505
1519
  }
1506
1520
  }, children: /* @__PURE__ */ jsx13(ChevronDownIcon, {}) }) }),
1507
1521
  dock && openActions.length > 0 && createPortal(
1508
- /* @__PURE__ */ jsx13("div", { className: "qsl:flex qsl:flex-col qsl:gap-[var(--qsl-chat-dense-message-gap,8px)] qsl:text-chat qsl:text-foreground", style: appearanceStyle(appearance), children: openActions.map((message) => /* @__PURE__ */ jsx13("div", { children: /* @__PURE__ */ jsx13(ActionContent, { action: message.action, onDismissAction, renderAction, text: message.text, texts: allTexts }) }, message.key)) }),
1522
+ /* @__PURE__ */ jsx13("div", { className: "qsl:flex qsl:flex-col qsl:gap-[var(--qsl-chat-dense-message-gap,3px)] qsl:text-chat qsl:text-foreground", style: appearanceStyle(appearance), children: openActions.map((message) => /* @__PURE__ */ jsx13("div", { children: /* @__PURE__ */ jsx13(ActionContent, { action: message.action, onDismissAction, renderAction, text: message.text, texts: allTexts }) }, message.key)) }),
1509
1523
  dock
1510
1524
  )
1511
1525
  ] });
@@ -1604,7 +1618,7 @@ function StepGroup({
1604
1618
  ] })
1605
1619
  ] });
1606
1620
  if (!groupExpandable) {
1607
- return /* @__PURE__ */ jsx13("div", { className: stepClasses, "data-step": "group", children: /* @__PURE__ */ jsx13("div", { className: cn(traceClasses, "qsl:items-center qsl:py-1 qsl:pr-2 qsl:pl-1", active && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none"), children: summary }) });
1621
+ return /* @__PURE__ */ jsx13("div", { className: stepClasses, "data-step": "group", children: /* @__PURE__ */ jsx13("div", { className: cn(traceClasses, "qsl:items-center qsl:pr-2 qsl:pl-1", active && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none"), children: summary }) });
1608
1622
  }
1609
1623
  return /* @__PURE__ */ jsxs6("div", { className: stepClasses, "data-step": "group", children: [
1610
1624
  /* @__PURE__ */ jsxs6(
@@ -1621,7 +1635,7 @@ function StepGroup({
1621
1635
  ]
1622
1636
  }
1623
1637
  ),
1624
- open && /* @__PURE__ */ jsx13("div", { className: "qsl:mt-0.5 qsl:flex qsl:flex-col qsl:gap-0.5 qsl:pl-3", children: messages.map((message) => /* @__PURE__ */ jsx13(TraceLine, { className: "qsl:pl-3", elapsedAfter, expandable, message, texts, toolArgumentsText }, message.key)) }),
1638
+ open && /* @__PURE__ */ jsx13("div", { className: "qsl:mt-0.5 qsl:flex qsl:flex-col qsl:gap-[var(--qsl-tool-group-gap,1px)] qsl:pl-3", children: messages.map((message) => /* @__PURE__ */ jsx13(TraceLine, { className: "qsl:pl-3", elapsedAfter, expandable, message, texts, toolArgumentsText }, message.key)) }),
1625
1639
  open && messages.length >= collapseAtBottomFrom && /* @__PURE__ */ jsxs6(
1626
1640
  "button",
1627
1641
  {
@@ -1766,23 +1780,30 @@ function Bubble({
1766
1780
  /* @__PURE__ */ jsx13(MessageActions, { message, options: messageActions, texts })
1767
1781
  ] });
1768
1782
  const cursor = message.role === "assistant" && message.textCursor ? JSON.stringify(message.textCursor) : void 0;
1769
- if (!useBubble) return /* @__PURE__ */ jsx13("div", { "data-chat-text-cursor": cursor, className: cn(stepSpacing, "qsl:group qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.625)] qsl:text-foreground"), "data-message": "answer", children: content });
1770
- return /* @__PURE__ */ jsx13(
1783
+ if (!useBubble) return /* @__PURE__ */ jsx13("div", { "data-chat-text-cursor": cursor, className: cn(stepSpacing, "qsl:group qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:text-foreground"), "data-message": "answer", children: content });
1784
+ const fullBackground = message.role === "user" && bubbleOptions?.userBackgroundFullWidth !== false;
1785
+ const surfaceClasses = message.bubble ? "qsl:rounded-lg" : "qsl:rounded-[var(--qsl-bubble-radius,10px_10px_4px_10px)] qsl:border qsl:border-border qsl:bg-secondary qsl:in-data-[tone=material]:rounded-[var(--qsl-bubble-radius,9px)] qsl:in-data-[tone=material]:border-glass-edge/20 qsl:in-data-[tone=material]:bg-primary/15";
1786
+ return /* @__PURE__ */ jsxs6(
1771
1787
  "div",
1772
1788
  {
1773
1789
  "data-chat-text-cursor": cursor,
1774
- className: cn(stepSpacing, "qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
1790
+ className: cn(stepSpacing, "qsl:relative qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
1775
1791
  "data-message": message.role === "user" && !message.bubble && variant === "default" ? "user" : "bubble",
1776
1792
  "data-side": side,
1777
- children: /* @__PURE__ */ jsx13(
1778
- "div",
1779
- {
1780
- className: cn(bubbleBodyClasses, "qsl:group", message.bubble ? "qsl:rounded-lg qsl:text-white qsl:[--qsl-scroll-cover:var(--qsl-primary)]" : "qsl:rounded-[var(--qsl-bubble-radius,10px_10px_4px_10px)] qsl:border qsl:border-border qsl:bg-secondary qsl:text-foreground qsl:[--qsl-scroll-cover:var(--qsl-secondary)] qsl:in-data-[tone=material]:rounded-[var(--qsl-bubble-radius,9px)] qsl:in-data-[tone=material]:border-glass-edge/20 qsl:in-data-[tone=material]:bg-primary/15 qsl:in-data-[tone=material]:text-foreground"),
1781
- "data-tone": message.bubble ? "on-color" : void 0,
1782
- style: { background: message.bubble?.color, maxWidth: bubbleOptions?.maxWidth },
1783
- children: content
1784
- }
1785
- )
1793
+ "data-background-width": fullBackground ? "full" : "content",
1794
+ children: [
1795
+ fullBackground && /* @__PURE__ */ jsx13("div", { "aria-hidden": "true", className: cn("qsl:pointer-events-none qsl:absolute qsl:inset-0", surfaceClasses), "data-chat": "bubble-background", style: { background: message.bubble?.color } }),
1796
+ /* @__PURE__ */ jsx13(
1797
+ "div",
1798
+ {
1799
+ className: cn(bubbleBodyClasses, "qsl:group", !fullBackground && surfaceClasses, fullBackground && !message.bubble && "qsl:border qsl:border-transparent", message.bubble ? "qsl:text-white qsl:[--qsl-scroll-cover:var(--qsl-primary)]" : "qsl:text-foreground qsl:[--qsl-scroll-cover:var(--qsl-secondary)]"),
1800
+ "data-chat": "bubble-content",
1801
+ "data-tone": message.bubble ? "on-color" : void 0,
1802
+ style: { background: fullBackground ? void 0 : message.bubble?.color, maxWidth: bubbleOptions?.maxWidth },
1803
+ children: content
1804
+ }
1805
+ )
1806
+ ]
1786
1807
  }
1787
1808
  );
1788
1809
  }
@@ -1884,7 +1905,7 @@ function ChatPanel({
1884
1905
  /* @__PURE__ */ jsx14("div", { children: /* @__PURE__ */ jsx14(
1885
1906
  "div",
1886
1907
  {
1887
- className: "qsl:-mx-[16px] qsl:max-h-[calc(var(--qsl-panel-height,100dvh)_/_2)] qsl:overflow-y-auto qsl:overscroll-contain qsl:px-[16px] qsl:pb-[var(--qsl-chat-message-gap,16px)] qsl:[-webkit-mask-image:linear-gradient(to_bottom,#000_calc(100%_-_var(--qsl-chat-message-gap,16px)),transparent)] qsl:[mask-image:linear-gradient(to_bottom,#000_calc(100%_-_var(--qsl-chat-message-gap,16px)),transparent)] qsl:empty:hidden",
1908
+ className: "qsl:-mx-[16px] qsl:max-h-[calc(var(--qsl-panel-height,100dvh)_/_2)] qsl:overflow-y-auto qsl:overscroll-contain qsl:px-[16px] qsl:pb-[var(--qsl-chat-message-gap,10px)] qsl:[-webkit-mask-image:linear-gradient(to_bottom,#000_calc(100%_-_var(--qsl-chat-message-gap,10px)),transparent)] qsl:[mask-image:linear-gradient(to_bottom,#000_calc(100%_-_var(--qsl-chat-message-gap,10px)),transparent)] qsl:empty:hidden",
1888
1909
  "data-chat": "actions",
1889
1910
  ref: setDock
1890
1911
  }