quassel 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  chatAttachmentMediaType,
6
6
  compactToolLine,
7
7
  prettyJson
8
- } from "./chunk-IGCFQJZB.js";
8
+ } from "./chunk-3VEL4HEO.js";
9
9
 
10
10
  // packages/quassel/src/types.ts
11
11
  function stepState(message) {
@@ -21,67 +21,67 @@ function stepState(message) {
21
21
  // packages/quassel/src/texts.ts
22
22
  var fillText = (text, values) => text.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
23
23
  var defaultTexts = {
24
- working: "Arbeitet ...",
25
- toolRunning: "l\xE4uft ...",
26
- toolStillRunning: "Werkzeug l\xE4uft ...",
27
- currentTool: "Werkzeug l\xE4uft",
28
- thinkingChip: "Denken",
29
- toolChip: "Werkzeug",
30
- stepGroupOne: "1 Schritt",
31
- stepGroupMany: "{count} Schritte",
32
- stepGroupCollapse: "Einklappen",
33
- thinkingTitle: "Thinking-Trace",
34
- toolTitle: "Tool-Call",
35
- argumentsLabel: "Argumente",
36
- resultLabel: "Ergebnis",
37
- close: "Schlie\xDFen",
38
- jumpToEnd: "Zum Ende springen",
39
- send: "Senden",
40
- sendIntoRun: "In den laufenden Turn einspeisen",
41
- stop: "Arbeit stoppen",
42
- placeholder: "Nachricht schreiben ...",
43
- steeringPlaceholder: "Dazwischenfunken ...",
44
- steered: "In den laufenden Turn eingespeist",
45
- detailModeTitle: "Detailgrad der Schritte",
46
- detailModeOff: "nur Antworten",
47
- detailModeCurrent: "aktuell",
48
- detailModeIcons: "Symbole",
49
- detailModeChips: "kompakt",
50
- detailModeGrouped: "gruppiert",
51
- detailModeCompact: "einzeilig",
52
- detailModeFull: "alles",
53
- timestamps: "Zeitstempel",
54
- showTimestamps: "Zeitstempel anzeigen",
55
- hideTimestamps: "Zeitstempel ausblenden",
56
- copyMessage: "Nachricht kopieren",
57
- editMessage: "Nachricht bearbeiten",
58
- retryMessage: "Antwort erneut anfordern",
59
- copyCode: "Code kopieren",
60
- copied: "Kopiert",
61
- pendingAction: "wartet auf Eingabe",
62
- dismissAction: "Verwerfen",
63
- actionDismissed: "verworfen",
64
- actionFailed: "Aktion fehlgeschlagen",
65
- clipboardUnavailable: "Die Zwischenablage ist nicht verf\xFCgbar",
66
- transcript: "Chatverlauf",
67
- attachments: "Anh\xE4nge",
68
- attachFiles: "Dateien anh\xE4ngen",
69
- attachFilesHint: "Dateien anh\xE4ngen (bis zu {count} Dateien, insgesamt {size})",
70
- removeAttachment: "{name} entfernen",
71
- attachmentPreparing: "Wird vorbereitet ...",
72
- attachmentsPreparing: "Anh\xE4nge werden vorbereitet ...",
73
- attachmentReadFailed: "Datei konnte nicht gelesen werden. Entferne sie und f\xFCge sie erneut hinzu.",
74
- attachmentsWhileBusy: "Warte, bis die Eingabe wieder bereit ist, und f\xFCge die Dateien dann erneut hinzu.",
75
- tooManyAttachments: "H\xF6chstens {count} Anh\xE4nge pro Nachricht. Entferne zuerst einen Anhang.",
76
- attachmentsTooLarge: "Anh\xE4nge d\xFCrfen zusammen h\xF6chstens {size} gro\xDF sein. W\xE4hle kleinere Dateien.",
77
- attachmentUnsupported: "Dieser Chat unterst\xFCtzt {name} nicht als {kind}. Entferne den Anhang.",
78
- attachmentUnsupportedByModel: "Das Modell {model} unterst\xFCtzt {name} nicht als {kind}. W\xE4hle ein passendes Modell oder entferne den Anhang.",
79
- attachmentKindImage: "Bild",
80
- attachmentKindVideo: "Video",
24
+ working: "Working ...",
25
+ toolRunning: "running ...",
26
+ toolStillRunning: "Tool running ...",
27
+ toolElapsedSeconds: "{seconds} s",
28
+ currentTool: "Tool running",
29
+ thinkingChip: "Thinking",
30
+ toolChip: "Tool",
31
+ stepGroupOne: "1 step",
32
+ stepGroupMany: "{count} steps",
33
+ stepGroupCollapse: "Collapse",
34
+ thinkingTitle: "Thinking trace",
35
+ toolTitle: "Tool call",
36
+ argumentsLabel: "Arguments",
37
+ resultLabel: "Result",
38
+ close: "Close",
39
+ jumpToEnd: "Jump to end",
40
+ send: "Send",
41
+ sendIntoRun: "Feed into the running turn",
42
+ stop: "Stop work",
43
+ placeholder: "Write a message ...",
44
+ steeringPlaceholder: "Interject ...",
45
+ steered: "Fed into the running turn",
46
+ detailModeTitle: "Step detail level",
47
+ detailModeOff: "answers only",
48
+ detailModeCurrent: "current",
49
+ detailModeIcons: "icons",
50
+ detailModeChips: "compact",
51
+ detailModeGrouped: "grouped",
52
+ detailModeCompact: "single line",
53
+ detailModeFull: "everything",
54
+ showTimestamps: "Show timestamps",
55
+ hideTimestamps: "Hide timestamps",
56
+ copyMessage: "Copy message",
57
+ editMessage: "Edit message",
58
+ retryMessage: "Request answer again",
59
+ copyCode: "Copy code",
60
+ copied: "Copied",
61
+ pendingAction: "waiting for input",
62
+ dismissAction: "Dismiss",
63
+ actionDismissed: "dismissed",
64
+ actionFailed: "Action failed",
65
+ clipboardUnavailable: "The clipboard is not available",
66
+ transcript: "Chat history",
67
+ attachments: "Attachments",
68
+ attachFiles: "Attach files",
69
+ attachFilesHint: "Attach files (up to {count} files, {size} in total)",
70
+ removeAttachment: "Remove {name}",
71
+ attachmentPreparing: "Preparing ...",
72
+ attachmentsPreparing: "Preparing attachments ...",
73
+ attachmentReadFailed: "The file could not be read. Remove it and add it again.",
74
+ attachmentsWhileBusy: "Wait until the input is ready again, then add the files again.",
75
+ tooManyAttachments: "At most {count} attachments per message. Remove an attachment first.",
76
+ attachmentsTooLarge: "Attachments may be at most {size} in total. Choose smaller files.",
77
+ attachmentUnsupported: "This chat does not support {name} as {kind}. Remove the attachment.",
78
+ attachmentUnsupportedByModel: "The model {model} does not support {name} as {kind}. Choose a suitable model or remove the attachment.",
79
+ attachmentKindImage: "image",
80
+ attachmentKindVideo: "video",
81
81
  attachmentKindPdf: "PDF",
82
- notSent: "Nicht gesendet:",
83
- notSentAttachments: "({count} Anh\xE4nge)",
84
- insertUnsent: "Nicht gesendete Eingabe einf\xFCgen"
82
+ notSent: "Not sent:",
83
+ notSentAttachments: "({count} attachments)",
84
+ insertUnsent: "Insert unsent input"
85
85
  };
86
86
 
87
87
  // packages/quassel/src/ui/cn.ts
@@ -90,6 +90,7 @@ var cn = createCn({ prefix: "qsl", extend: { theme: { text: ["meta", "trace", "l
90
90
 
91
91
  // packages/quassel/src/ChatMessages.tsx
92
92
  import { useCallback as useCallback2, useContext as useContext3, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
93
+ import { createPortal } from "react-dom";
93
94
 
94
95
  // packages/quassel/src/chat-scroll.ts
95
96
  var atEnd = (viewport) => viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop <= 24;
@@ -459,7 +460,7 @@ var components = {
459
460
  a: ({ href, children }) => /* @__PURE__ */ jsx8(MarkdownLink, { href, children }),
460
461
  pre: ({ children }) => /* @__PURE__ */ jsx8(CodeBlock, { children }),
461
462
  code: ({ children, className }) => /* @__PURE__ */ jsx8("code", { className: cn(className, "qsl:rounded-sm qsl:bg-secondary qsl:px-1 qsl:py-0.5 qsl:font-mono qsl:text-sm qsl:[overflow-wrap:anywhere] qsl:in-data-[tone=on-color]:bg-white/20 qsl:in-data-[tone=on-color]:text-inherit", "qsl:[pre>&]:bg-transparent qsl:[pre>&]:p-0"), children }),
462
- // Ohne max-content schrumpft die Tabelle auf den Container und stapelt Buchstaben.
463
+ // Without max-content the table shrinks to the container and stacks letters.
463
464
  table: ({ children }) => /* @__PURE__ */ jsx8("div", { className: cn(scrollbar, scrollCover, "qsl:my-2 qsl:overflow-x-auto"), children: /* @__PURE__ */ jsx8("table", { className: "qsl:w-max qsl:min-w-full qsl:border-collapse qsl:text-sm", children }) }),
464
465
  thead: ({ children }) => /* @__PURE__ */ jsx8("thead", { children }),
465
466
  tbody: ({ children }) => /* @__PURE__ */ jsx8("tbody", { children }),
@@ -493,7 +494,7 @@ function Markdown({ text, streaming = false }) {
493
494
  );
494
495
  }
495
496
  function markdownPlainText(text) {
496
- return text.split("\n").filter((line) => !/^\s*```\w*\s*$/.test(line) && !isTableDivider(line)).map((line) => (line.trim().startsWith("|") ? splitTableRow(line).join(", ") : line).replace(/^(#{1,6})\s+/, "").replace(/^\s*>\s?/, "").replace(/^\s*([-*+]|\d+[.)])\s+/, "").replace(/!?\[([^\]]*)\]\(([^)]+)\)|\*\*([^*]+)\*\*|__([^_]+)__|\*([^*]+)\*|`([^`]+)`/g, (_, link, _href, fett, fettUnterstrich, kursiv, code) => link ?? fett ?? fettUnterstrich ?? kursiv ?? code).trim()).filter((line) => line !== "").join("\n");
497
+ return text.split("\n").filter((line) => !/^\s*```\w*\s*$/.test(line) && !isTableDivider(line)).map((line) => (line.trim().startsWith("|") ? splitTableRow(line).join(", ") : line).replace(/^(#{1,6})\s+/, "").replace(/^\s*>\s?/, "").replace(/^\s*([-*+]|\d+[.)])\s+/, "").replace(/!?\[([^\]]*)\]\(([^)]+)\)|\*\*([^*]+)\*\*|__([^_]+)__|\*([^*]+)\*|`([^`]+)`/g, (_, link, _href, bold, boldUnderscore, italic, code) => link ?? bold ?? boldUnderscore ?? italic ?? code).trim()).filter((line) => line !== "").join("\n");
497
498
  }
498
499
  function splitTableRow(line) {
499
500
  let row = line.trim();
@@ -603,159 +604,159 @@ function PendingActionCard({
603
604
  // packages/quassel/src/WorkingScenes.tsx
604
605
  import { useEffect as useEffect2, useState as useState2 } from "react";
605
606
  import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
606
- var SPUR = 13;
607
- var GRUNDTAKT_MS = 90;
608
- var SZENENWECHSEL_MS = 2600;
609
- function bahn(figuren, breite = SPUR) {
610
- return Array.from({ length: breite }, (_, x) => figuren[x] ?? " ");
607
+ var TRACK = 13;
608
+ var BASE_TICK_MS = 90;
609
+ var SCENE_CHANGE_MS = 2600;
610
+ function track(figures, width = TRACK) {
611
+ return Array.from({ length: width }, (_, x) => figures[x] ?? " ");
611
612
  }
612
- var SZENEN = [
613
+ var SCENES = [
613
614
  {
614
- id: "denken",
615
- takt: 2,
616
- zeichne: (schritt) => {
617
- const takt = schritt % 10;
618
- if (takt >= 8) return "\u{1F929}\u{1F4A1}";
619
- return "\u{1F914}" + "\u{1F4AD}".repeat(Math.min(takt, 3));
615
+ id: "thinking",
616
+ tick: 2,
617
+ draw: (step) => {
618
+ const tick = step % 10;
619
+ if (tick >= 8) return "\u{1F929}\u{1F4A1}";
620
+ return "\u{1F914}" + "\u{1F4AD}".repeat(Math.min(tick, 3));
620
621
  }
621
622
  },
622
623
  {
623
- // Fliegt nach rechts und zieht ihren Schweif hinterher.
624
- id: "rakete",
625
- takt: 2,
626
- laufend: true,
627
- zeichne: (schritt) => {
628
- const x = schritt % SPUR;
629
- return bahn({ [x - 2]: "\xB7", [x - 1]: "\u{1F4A8}", [x]: "\u{1F680}" });
624
+ // Flies to the right, dragging its tail behind.
625
+ id: "rocket",
626
+ tick: 2,
627
+ moving: true,
628
+ draw: (step) => {
629
+ const x = step % TRACK;
630
+ return track({ [x - 2]: "\xB7", [x - 1]: "\u{1F4A8}", [x]: "\u{1F680}" });
630
631
  }
631
632
  },
632
633
  {
633
- // Die Punkte bleiben stehen und werden einzeln gefressen.
634
+ // The dots stay put and are eaten one by one.
634
635
  id: "pacman",
635
- takt: 2,
636
- laufend: true,
637
- zeichne: (schritt) => {
638
- const x = schritt % SPUR;
639
- const figuren = {};
640
- for (let i = x + 1; i < SPUR; i++) {
641
- figuren[i] = "\xB7";
636
+ tick: 2,
637
+ moving: true,
638
+ draw: (step) => {
639
+ const x = step % TRACK;
640
+ const figures = {};
641
+ for (let i = x + 1; i < TRACK; i++) {
642
+ figures[i] = "\xB7";
642
643
  }
643
- figuren[x] = schritt % 2 === 0 ? "\u{1F7E1}" : "\u{1F315}";
644
- return bahn(figuren);
644
+ figures[x] = step % 2 === 0 ? "\u{1F7E1}" : "\u{1F315}";
645
+ return track(figures);
645
646
  }
646
647
  },
647
648
  {
648
- // Aus einem Samen wird ein Baum, dann von vorn.
649
- id: "wachsen",
649
+ // A seed becomes a tree, then start over.
650
+ id: "growing",
650
651
  compact: true,
651
- takt: 2,
652
- zeichne: (schritt) => {
653
- const stufen = ["\u{1F331}", "\u{1F331}", "\u{1F33F}", "\u{1F33F}", "\u{1F343}", "\u{1F333}", "\u{1F333}", "\u{1F333}"];
654
- return stufen[schritt % stufen.length];
652
+ tick: 2,
653
+ draw: (step) => {
654
+ const stages = ["\u{1F331}", "\u{1F331}", "\u{1F33F}", "\u{1F33F}", "\u{1F343}", "\u{1F333}", "\u{1F333}", "\u{1F333}"];
655
+ return stages[step % stages.length];
655
656
  }
656
657
  },
657
658
  {
658
- // Das Ei bekommt Risse, das Kueken schluepft.
659
- id: "schluepfen",
659
+ // The egg cracks, the chick hatches.
660
+ id: "hatching",
660
661
  compact: true,
661
- takt: 2,
662
- zeichne: (schritt) => {
663
- const takt = schritt % 12;
664
- if (takt < 4) return "\u{1F95A}";
665
- if (takt < 6) return "\u{1F95A}*";
666
- if (takt < 9) return "\u{1F423}";
662
+ tick: 2,
663
+ draw: (step) => {
664
+ const tick = step % 12;
665
+ if (tick < 4) return "\u{1F95A}";
666
+ if (tick < 6) return "\u{1F95A}*";
667
+ if (tick < 9) return "\u{1F423}";
667
668
  return "\u{1F425}";
668
669
  }
669
670
  },
670
671
  {
671
- // Morse-artiges Signal, das sich zu einem OK aufloest.
672
+ // Morse-like signal that resolves into an OK.
672
673
  id: "signal",
673
- takt: 2,
674
- zeichne: (schritt) => {
675
- const takt = schritt % 14;
676
- if (takt >= 11) return "\u{1F4E1} OK";
677
- const muster = ["\xB7", "\xB7\xB7", "\xB7-", "-\xB7", "\xB7\xB7-", "-\xB7\xB7"];
678
- return "\u{1F4E1} " + muster[takt % muster.length];
674
+ tick: 2,
675
+ draw: (step) => {
676
+ const tick = step % 14;
677
+ if (tick >= 11) return "\u{1F4E1} OK";
678
+ const pattern = ["\xB7", "\xB7\xB7", "\xB7-", "-\xB7", "\xB7\xB7-", "-\xB7\xB7"];
679
+ return "\u{1F4E1} " + pattern[tick % pattern.length];
679
680
  }
680
681
  },
681
682
  {
682
- // Sanduhr: der Sand laeuft durch, dann wird gedreht.
683
- id: "sanduhr",
684
- takt: 2,
685
- zeichne: (schritt) => {
686
- const takt = schritt % 12;
687
- const koerner = "\xB7".repeat(Math.max(0, 5 - Math.floor(takt / 2)));
688
- return (takt < 10 ? "\u23F3" : "\u231B") + " " + koerner;
683
+ // Hourglass: the sand runs through, then it is flipped.
684
+ id: "hourglass",
685
+ tick: 2,
686
+ draw: (step) => {
687
+ const tick = step % 12;
688
+ const grains = "\xB7".repeat(Math.max(0, 5 - Math.floor(tick / 2)));
689
+ return (tick < 10 ? "\u23F3" : "\u231B") + " " + grains;
689
690
  }
690
691
  },
691
692
  {
692
- // Ei, Kueken, Henne - und die legt das naechste Ei.
693
- id: "huhn-kreislauf",
693
+ // Egg, chick, hen - and she lays the next egg.
694
+ id: "chicken-cycle",
694
695
  compact: true,
695
- takt: 3,
696
- zeichne: (schritt) => {
697
- const takt = schritt % 16;
698
- if (takt < 3) return "\u{1F95A}";
699
- if (takt < 5) return "\u{1F95A}*";
700
- if (takt < 8) return "\u{1F423}";
701
- if (takt < 11) return "\u{1F424}";
702
- if (takt < 14) return "\u{1F414}";
696
+ tick: 3,
697
+ draw: (step) => {
698
+ const tick = step % 16;
699
+ if (tick < 3) return "\u{1F95A}";
700
+ if (tick < 5) return "\u{1F95A}*";
701
+ if (tick < 8) return "\u{1F423}";
702
+ if (tick < 11) return "\u{1F424}";
703
+ if (tick < 14) return "\u{1F414}";
703
704
  return "\u{1F414}\u{1F95A}";
704
705
  }
705
706
  },
706
707
  {
707
- // Hut, Funken - und das Kaninchen tuermt.
708
- id: "zauberhut",
708
+ // Hat, sparks - and the rabbit bolts.
709
+ id: "magic-hat",
709
710
  compact: true,
710
- takt: 3,
711
- zeichne: (schritt) => {
712
- const takt = schritt % 14;
713
- if (takt < 4) return "\u{1F3A9}";
714
- if (takt < 7) return "\u{1F3A9}*";
715
- if (takt < 11) return "\u{1F3A9}\u{1F407}";
711
+ tick: 3,
712
+ draw: (step) => {
713
+ const tick = step % 14;
714
+ if (tick < 4) return "\u{1F3A9}";
715
+ if (tick < 7) return "\u{1F3A9}*";
716
+ if (tick < 11) return "\u{1F3A9}\u{1F407}";
716
717
  return "\u{1F407}\u{1F4A8}";
717
718
  }
718
719
  },
719
720
  {
720
- // Rot, Gelb, Gruen - und ab.
721
- id: "ampel",
721
+ // Red, yellow, green - and off.
722
+ id: "traffic-light",
722
723
  compact: true,
723
- takt: 3,
724
- zeichne: (schritt) => {
725
- const takt = schritt % 12;
726
- if (takt < 4) return "\u{1F534}";
727
- if (takt < 6) return "\u{1F7E1}";
728
- if (takt < 8) return "\u{1F7E2}";
724
+ tick: 3,
725
+ draw: (step) => {
726
+ const tick = step % 12;
727
+ if (tick < 4) return "\u{1F534}";
728
+ if (tick < 6) return "\u{1F7E1}";
729
+ if (tick < 8) return "\u{1F7E2}";
729
730
  return "\u{1F697}\u{1F4A8}";
730
731
  }
731
732
  },
732
733
  {
733
- // Klingeln, verschlafen, losrennen.
734
- id: "wecker",
734
+ // Ringing, oversleeping, running off.
735
+ id: "alarm-clock",
735
736
  compact: true,
736
- takt: 2,
737
- zeichne: (schritt) => {
738
- const takt = schritt % 14;
739
- if (takt < 5) return takt % 2 === 0 ? "\u23F0" : "\u{1F514}";
740
- if (takt < 8) return "\u{1F634}";
741
- if (takt < 10) return "\u{1F633}";
737
+ tick: 2,
738
+ draw: (step) => {
739
+ const tick = step % 14;
740
+ if (tick < 5) return tick % 2 === 0 ? "\u23F0" : "\u{1F514}";
741
+ if (tick < 8) return "\u{1F634}";
742
+ if (tick < 10) return "\u{1F633}";
742
743
  return "\u{1F3C3}\u{1F4A8}";
743
744
  }
744
745
  },
745
746
  {
746
- // Schnecke kriecht zur Flagge - und feiert.
747
- id: "schnecke",
748
- takt: 4,
749
- zeichne: (schritt) => {
750
- const takt = schritt % 8;
751
- if (takt < 5) return bahn({ [takt]: "\u{1F40C}", 4: "\u{1F3C1}" }, 5);
752
- return bahn({ 4: "\u{1F389}" }, 5);
747
+ // A snail crawls to the flag - and celebrates.
748
+ id: "snail",
749
+ tick: 4,
750
+ draw: (step) => {
751
+ const tick = step % 8;
752
+ if (tick < 5) return track({ [tick]: "\u{1F40C}", 4: "\u{1F3C1}" }, 5);
753
+ return track({ 4: "\u{1F389}" }, 5);
753
754
  }
754
755
  }
755
756
  ];
756
- var COMPACT_SCENES = SZENEN.filter((scene) => scene.compact);
757
- function WorkingScenes({ label = "Arbeitet ...", compact = false }) {
758
- const scenes = compact ? COMPACT_SCENES : SZENEN;
757
+ var COMPACT_SCENES = SCENES.filter((scene) => scene.compact);
758
+ function WorkingScenes({ label = "Working ...", compact = false }) {
759
+ const scenes = compact ? COMPACT_SCENES : SCENES;
759
760
  const [sceneIndex, setSceneIndex] = useState2(() => Math.floor(Math.random() * scenes.length));
760
761
  const [offset, setOffset] = useState2(() => Math.floor(Math.random() * 20));
761
762
  const [frameTick, setFrameTick] = useState2(0);
@@ -774,15 +775,15 @@ function WorkingScenes({ label = "Arbeitet ...", compact = false }) {
774
775
  setSceneIndex((previous) => (previous + 1 + Math.floor(Math.random() * (scenes.length - 1))) % scenes.length);
775
776
  setOffset(Math.floor(Math.random() * 20));
776
777
  setFrameTick(0);
777
- }, SZENENWECHSEL_MS);
778
+ }, SCENE_CHANGE_MS);
778
779
  return () => window.clearInterval(timer);
779
780
  }, [reducedMotion, scenes]);
780
781
  useEffect2(() => {
781
782
  if (reducedMotion) return;
782
- const timer = window.setInterval(() => setFrameTick((value) => value + 1), GRUNDTAKT_MS);
783
+ const timer = window.setInterval(() => setFrameTick((value) => value + 1), BASE_TICK_MS);
783
784
  return () => window.clearInterval(timer);
784
785
  }, [reducedMotion]);
785
- const frame = scene.zeichne(Math.floor(frameTick / scene.takt) + (scene.laufend ? offset : 0));
786
+ const frame = scene.draw(Math.floor(frameTick / scene.tick) + (scene.moving ? offset : 0));
786
787
  return /* @__PURE__ */ jsxs4("span", { "aria-label": label, className: cn("qsl:flex qsl:items-center qsl:gap-2.5 qsl:text-[13px]", compact ? "qsl:text-inherit" : "qsl:mt-4 qsl:text-foreground"), "data-chat": "working", role: "status", children: [
787
788
  !compact && /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", className: "qsl:text-muted-foreground qsl:opacity-70", children: ">" }),
788
789
  /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", className: cn("qsl:inline-block qsl:flex-none qsl:font-mono qsl:[font-variant-ligatures:none]", compact ? "qsl:w-[2.4em] qsl:text-center" : "qsl:w-[15rem]"), children: /* @__PURE__ */ jsx11("span", { className: "qsl:tracking-[0.02em] qsl:whitespace-pre qsl:motion-reduce:opacity-60", children: typeof frame === "string" ? frame : frame.map((cell, index) => /* @__PURE__ */ jsx11("span", { className: "qsl:inline-block qsl:w-[1.35em] qsl:text-center", children: cell }, index)) }) }, scene.id)
@@ -887,6 +888,53 @@ function timestampDay(date, options = {}) {
887
888
  return { key, label };
888
889
  }
889
890
 
891
+ // packages/quassel/src/elapsed.ts
892
+ import { useSyncExternalStore } from "react";
893
+ var tickMs = 500;
894
+ function formatElapsed(ms, texts) {
895
+ const total = Math.max(0, Math.floor(ms / 1e3));
896
+ if (total < 60) return fillText(texts.toolElapsedSeconds, { seconds: total });
897
+ const hours = Math.floor(total / 3600);
898
+ const minutes = Math.floor(total / 60) % 60;
899
+ const seconds = String(total % 60).padStart(2, "0");
900
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
901
+ }
902
+ function createTicker(intervalMs2 = tickMs) {
903
+ const listeners = /* @__PURE__ */ new Set();
904
+ let now = Date.now();
905
+ let timer;
906
+ return {
907
+ subscribe(listener) {
908
+ listeners.add(listener);
909
+ if (timer === void 0) {
910
+ now = Date.now();
911
+ timer = setInterval(() => {
912
+ now = Date.now();
913
+ listeners.forEach((notify) => notify());
914
+ }, intervalMs2);
915
+ }
916
+ return () => {
917
+ listeners.delete(listener);
918
+ if (listeners.size === 0 && timer !== void 0) {
919
+ clearInterval(timer);
920
+ timer = void 0;
921
+ }
922
+ };
923
+ },
924
+ now() {
925
+ const current = Date.now();
926
+ if (timer === void 0 && (current - now >= intervalMs2 || current < now)) now = current;
927
+ return now;
928
+ },
929
+ active: () => timer !== void 0
930
+ };
931
+ }
932
+ var elapsedTicker = createTicker();
933
+ var nothingOnServer = () => void 0;
934
+ function useElapsed(start) {
935
+ return useSyncExternalStore(elapsedTicker.subscribe, () => Math.floor((elapsedTicker.now() - start) / 1e3) * 1e3, nothingOnServer);
936
+ }
937
+
890
938
  // packages/quassel/src/MessageActions.tsx
891
939
  import { CheckIcon as CheckIcon3, CopyIcon as CopyIcon2, PencilIcon, RotateCcwIcon } from "lucide-react";
892
940
  import { useState as useState3 } from "react";
@@ -952,13 +1000,17 @@ function createChatSendScope(enabled) {
952
1000
  };
953
1001
  }
954
1002
 
1003
+ // packages/quassel/src/ChatActionDock.ts
1004
+ import { createContext as createContext4 } from "react";
1005
+ var ChatActionDock = createContext4(void 0);
1006
+
955
1007
  // packages/quassel/src/announce.ts
956
- var abstandMs = 400;
957
- var hoechstensWartend = 5;
1008
+ var intervalMs = 400;
1009
+ var maxQueued = 5;
958
1010
  var region;
959
- var wartend = [];
960
- var laeuft = false;
961
- function ansageRegion() {
1011
+ var queued = [];
1012
+ var running = false;
1013
+ function liveRegion() {
962
1014
  if (region?.isConnected) {
963
1015
  return region;
964
1016
  }
@@ -981,33 +1033,33 @@ function ansageRegion() {
981
1033
  region = element;
982
1034
  return element;
983
1035
  }
984
- function naechste() {
985
- if (wartend.length === 0) {
986
- laeuft = false;
1036
+ function next() {
1037
+ if (queued.length === 0) {
1038
+ running = false;
987
1039
  return;
988
1040
  }
989
1041
  if (document.visibilityState === "hidden") {
990
- document.addEventListener("visibilitychange", naechste, { once: true });
1042
+ document.addEventListener("visibilitychange", next, { once: true });
991
1043
  return;
992
1044
  }
993
- const [text, ...rest] = wartend;
994
- wartend = rest;
995
- const ziel = ansageRegion();
996
- ziel.textContent = ziel.textContent === text ? `${text}\xA0` : text;
997
- window.setTimeout(naechste, abstandMs);
1045
+ const [text, ...rest] = queued;
1046
+ queued = rest;
1047
+ const target = liveRegion();
1048
+ target.textContent = target.textContent === text ? `${text}\xA0` : text;
1049
+ window.setTimeout(next, intervalMs);
998
1050
  }
999
1051
  function announce(text) {
1000
- const inhalt = text.trim();
1001
- if (!inhalt || typeof document === "undefined") {
1052
+ const content = text.trim();
1053
+ if (!content || typeof document === "undefined") {
1002
1054
  return;
1003
1055
  }
1004
- wartend = [...wartend, inhalt].slice(-hoechstensWartend);
1005
- if (laeuft) {
1056
+ queued = [...queued, content].slice(-maxQueued);
1057
+ if (running) {
1006
1058
  return;
1007
1059
  }
1008
- laeuft = true;
1009
- ansageRegion();
1010
- window.setTimeout(naechste, abstandMs);
1060
+ running = true;
1061
+ liveRegion();
1062
+ window.setTimeout(next, intervalMs);
1011
1063
  }
1012
1064
 
1013
1065
  // packages/quassel/src/ChatMessages.tsx
@@ -1030,11 +1082,14 @@ var chipClasses = cn(
1030
1082
  );
1031
1083
  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";
1032
1084
  var collapseAtBottomFrom = 11;
1033
- var hoechstensAnsagenAufEinmal = 2;
1085
+ var maxAnnouncementsAtOnce = 2;
1034
1086
  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";
1035
- function istSchritt(message) {
1087
+ function isStep(message) {
1036
1088
  return !!message && (message.role === "thinking" || message.role === "tool");
1037
1089
  }
1090
+ function isOpenAction(message) {
1091
+ return message.role === "action" && message.action !== void 0 && message.action.status === void 0;
1092
+ }
1038
1093
  function defaultArgumentsText(tool) {
1039
1094
  return prettyJson(tool.arguments);
1040
1095
  }
@@ -1042,7 +1097,7 @@ function ChatMessages({
1042
1097
  messages,
1043
1098
  owner,
1044
1099
  detailMode = "current",
1045
- running = false,
1100
+ running: running2 = false,
1046
1101
  working,
1047
1102
  stepsExpandable = true,
1048
1103
  texts,
@@ -1064,17 +1119,19 @@ function ChatMessages({
1064
1119
  timestampOptions,
1065
1120
  codeBlockOptions,
1066
1121
  bubbleOptions,
1067
- messageActions
1122
+ messageActions,
1123
+ toolElapsedThreshold = 3e3
1068
1124
  }) {
1069
- const alleTexte = { ...defaultTexts, ...texts };
1125
+ const allTexts = { ...defaultTexts, ...texts };
1070
1126
  const { Button: Button2 } = useQuasselComponents();
1071
- const scrollBereich = useRef3(null);
1127
+ const scrollArea = useRef3(null);
1072
1128
  const threadRef = useRef3(null);
1073
1129
  const scrollbarDragging = useRef3(false);
1074
1130
  const touchY = useRef3(void 0);
1075
1131
  const [showJumpToEnd, setShowJumpToEnd] = useState4(false);
1076
1132
  const [scroll] = useState4(() => createChatScroll());
1077
1133
  const sendScope = useContext3(ChatSendContext);
1134
+ const dock = useContext3(ChatActionDock);
1078
1135
  const [now, setNow] = useState4(() => Date.now());
1079
1136
  useEffect3(() => {
1080
1137
  if (!showTimestamps || timestampOptions?.format !== "relative") return;
@@ -1082,23 +1139,23 @@ function ChatMessages({
1082
1139
  const timer = window.setInterval(() => setNow(Date.now()), 15e3);
1083
1140
  return () => window.clearInterval(timer);
1084
1141
  }, [showTimestamps, timestampOptions?.format]);
1085
- const angesagt = useRef3(new Set(messages.map((message) => message.key)));
1142
+ const announced = useRef3(new Set(messages.map((message) => message.key)));
1086
1143
  useEffect3(() => {
1087
- const faellig = messages.filter((message, index) => !angesagt.current.has(message.key) && (message.role === "action" ? message.action !== void 0 && message.action.status === void 0 : message.role === "assistant" && message.text.trim() !== "" && (message.closed === true || !running && index === messages.length - 1)));
1088
- if (faellig.length === 0) {
1144
+ const due = messages.filter((message, index) => !announced.current.has(message.key) && (message.role === "action" ? isOpenAction(message) : message.role === "assistant" && message.text.trim() !== "" && (message.closed === true || !running2 && index === messages.length - 1)));
1145
+ if (due.length === 0) {
1089
1146
  return;
1090
1147
  }
1091
- angesagt.current = /* @__PURE__ */ new Set([...angesagt.current, ...faellig.map((message) => message.key)]);
1092
- if (announce2 === false || faellig.length > hoechstensAnsagenAufEinmal) {
1148
+ announced.current = /* @__PURE__ */ new Set([...announced.current, ...due.map((message) => message.key)]);
1149
+ if (announce2 === false || due.length > maxAnnouncementsAtOnce) {
1093
1150
  return;
1094
1151
  }
1095
- faellig.map((message) => announce2 ? announce2({ kind: message.role === "action" ? "action" : "reply", message }) : markdownPlainText(message.text)).forEach((text) => text && announce(text));
1096
- }, [messages, running, announce2]);
1152
+ due.map((message) => announce2 ? announce2({ kind: message.role === "action" ? "action" : "reply", message }) : markdownPlainText(message.text)).forEach((text) => text && announce(text));
1153
+ }, [messages, running2, announce2]);
1097
1154
  const updateJumpVisibility = useCallback2((viewport) => {
1098
1155
  if (viewport.clientHeight > 0) setShowJumpToEnd(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop > jumpToEndThreshold);
1099
1156
  }, [jumpToEndThreshold]);
1100
1157
  useLayoutEffect(() => sendScope?.subscribe(() => {
1101
- const viewport = scrollBereich.current;
1158
+ const viewport = scrollArea.current;
1102
1159
  if (!viewport) return;
1103
1160
  scroll.jump(viewport);
1104
1161
  updateJumpVisibility(viewport);
@@ -1111,16 +1168,16 @@ function ChatMessages({
1111
1168
  }
1112
1169
  scroll.pause(viewport);
1113
1170
  };
1114
- const pruefeEnde = useCallback2(() => {
1115
- const bereich = scrollBereich.current;
1116
- if (bereich) {
1117
- scroll.scroll(bereich, scrollbarDragging.current);
1118
- updateJumpVisibility(bereich);
1171
+ const checkEnd = useCallback2(() => {
1172
+ const area = scrollArea.current;
1173
+ if (area) {
1174
+ scroll.scroll(area, scrollbarDragging.current);
1175
+ updateJumpVisibility(area);
1119
1176
  }
1120
1177
  }, [scroll, updateJumpVisibility]);
1121
1178
  useEffect3(() => {
1122
1179
  const finishDrag = () => {
1123
- if (scrollbarDragging.current && scrollBereich.current) scroll.scroll(scrollBereich.current, true);
1180
+ if (scrollbarDragging.current && scrollArea.current) scroll.scroll(scrollArea.current, true);
1124
1181
  scrollbarDragging.current = false;
1125
1182
  };
1126
1183
  window.addEventListener("pointerup", finishDrag);
@@ -1131,24 +1188,27 @@ function ChatMessages({
1131
1188
  };
1132
1189
  }, [scroll]);
1133
1190
  useLayoutEffect(() => {
1134
- const bereich = scrollBereich.current;
1135
- if (!bereich) return;
1191
+ const area = scrollArea.current;
1192
+ if (!area) return;
1136
1193
  const align = () => {
1137
- scroll.layout(bereich, scrollbarDragging.current);
1138
- updateJumpVisibility(bereich);
1194
+ scroll.layout(area, scrollbarDragging.current);
1195
+ updateJumpVisibility(area);
1139
1196
  };
1140
1197
  align();
1141
- const beobachter = new ResizeObserver(align);
1142
- beobachter.observe(bereich);
1143
- if (threadRef.current) beobachter.observe(threadRef.current, { box: "border-box" });
1144
- return () => beobachter.disconnect();
1198
+ const observer = new ResizeObserver(align);
1199
+ observer.observe(area);
1200
+ if (threadRef.current) observer.observe(threadRef.current, { box: "border-box" });
1201
+ return () => observer.disconnect();
1145
1202
  });
1146
1203
  const visible = useMemo3(
1147
1204
  () => {
1148
- const letzterFremder = messages.reduce((last, message, index) => message.role === "user" ? last : index, -1);
1205
+ const lastNonUser = messages.reduce((last, message, index) => message.role === "user" ? last : index, -1);
1149
1206
  return messages.filter((message, index) => {
1150
- if (detailMode === "current" && istSchritt(message)) {
1151
- return running && index === letzterFremder && (message.role === "thinking" ? !message.closed : stepState(message) === "running");
1207
+ if (dock !== void 0 && isOpenAction(message)) {
1208
+ return false;
1209
+ }
1210
+ if (detailMode === "current" && isStep(message)) {
1211
+ return running2 && index === lastNonUser && (message.role === "thinking" ? !message.closed : stepState(message) === "running");
1152
1212
  }
1153
1213
  if ((message.role === "thinking" || message.role === "assistant") && message.text.trim() === "" && !message.attachments?.length) {
1154
1214
  return false;
@@ -1159,50 +1219,54 @@ function ChatMessages({
1159
1219
  return true;
1160
1220
  });
1161
1221
  },
1162
- [messages, detailMode, running]
1222
+ [messages, detailMode, running2, dock]
1163
1223
  );
1164
- const mitZeit = (inhalt, key, at, dicht = false) => showTimestamps ? /* @__PURE__ */ jsxs6("div", { className: cn("qsl:flex qsl:items-baseline qsl:gap-2", dicht ? denseStepClasses : stepClasses), children: [
1224
+ const openActions = dock ? messages.filter(isOpenAction) : [];
1225
+ 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: [
1165
1226
  /* @__PURE__ */ jsx13("time", { dateTime: messageDate(at)?.toISOString(), className: cn(
1166
1227
  "qsl:flex-none qsl:text-meta qsl:tabular-nums qsl:text-muted-foreground qsl:opacity-85",
1167
1228
  timestampOptions?.format && timestampOptions.format !== "time" ? "qsl:w-[clamp(60px,20%,120px)]" : "qsl:w-[34px]"
1168
1229
  ), children: messageDate(at) ? timestampLabel(messageDate(at), timestampOptions, now) : "" }),
1169
- /* @__PURE__ */ jsx13("div", { className: "qsl:min-w-0 qsl:flex-1 qsl:[&>*:first-child]:mt-0", children: inhalt })
1170
- ] }, `zeit-${key}`) : inhalt;
1171
- const chipModus = detailMode === "current" || detailMode === "chips" || detailMode === "icons";
1172
- const gruppenModus = detailMode === "grouped";
1173
- const bloecke = [];
1230
+ /* @__PURE__ */ jsx13("div", { className: "qsl:min-w-0 qsl:flex-1 qsl:[&>*:first-child]:mt-0", children: content })
1231
+ ] }, `time-${key}`) : content;
1232
+ const chipMode = detailMode === "current" || detailMode === "chips" || detailMode === "icons";
1233
+ const elapsedAfter = running2 && toolElapsedThreshold !== false ? toolElapsedThreshold : void 0;
1234
+ const groupMode = detailMode === "grouped";
1235
+ const blocks = [];
1174
1236
  let lastDay;
1175
- let gruppe = [];
1176
- const schliesseGruppe = () => {
1177
- if (gruppe.length > 0) {
1178
- const letzterZustand = stepState(gruppe[gruppe.length - 1]);
1179
- bloecke.push(mitZeit(
1180
- gruppenModus ? /* @__PURE__ */ jsx13(
1237
+ let group = [];
1238
+ const closeGroup = () => {
1239
+ if (group.length > 0) {
1240
+ const runningStep = running2 ? [...group].reverse().find((message) => ["running", "thinking"].includes(stepState(message))) : void 0;
1241
+ blocks.push(withTime(
1242
+ groupMode ? /* @__PURE__ */ jsx13(
1181
1243
  StepGroup,
1182
1244
  {
1183
- aktiv: running && (letzterZustand === "running" || letzterZustand === "thinking"),
1184
- expandierbar: stepsExpandable,
1185
- messages: gruppe,
1186
- texts: alleTexte,
1245
+ elapsedAfter,
1246
+ expandable: stepsExpandable,
1247
+ runningStep,
1248
+ messages: group,
1249
+ texts: allTexts,
1187
1250
  toolArgumentsText
1188
1251
  },
1189
- gruppe[0].key
1252
+ group[0].key
1190
1253
  ) : /* @__PURE__ */ jsx13(
1191
1254
  StepRow,
1192
1255
  {
1193
- expandierbar: stepsExpandable,
1194
- messages: gruppe,
1195
- mitText: detailMode !== "icons",
1196
- texts: alleTexte,
1256
+ elapsedAfter,
1257
+ expandable: stepsExpandable,
1258
+ messages: group,
1259
+ withText: detailMode !== "icons",
1260
+ texts: allTexts,
1197
1261
  toolArgumentsText,
1198
- toolLabel: detailMode === "current" && !stepsExpandable ? alleTexte.currentTool : void 0
1262
+ toolLabel: detailMode === "current" && !stepsExpandable ? allTexts.currentTool : void 0
1199
1263
  },
1200
- gruppe[0].key
1264
+ group[0].key
1201
1265
  ),
1202
- gruppe[0].key,
1203
- gruppe[0].at
1266
+ group[0].key,
1267
+ group[0].at
1204
1268
  ));
1205
- gruppe = [];
1269
+ group = [];
1206
1270
  }
1207
1271
  };
1208
1272
  visible.forEach((message, index) => {
@@ -1210,37 +1274,38 @@ function ChatMessages({
1210
1274
  if (date) {
1211
1275
  const day = timestampDay(date, timestampOptions);
1212
1276
  if (day.key !== lastDay) {
1213
- schliesseGruppe();
1214
- bloecke.push(/* @__PURE__ */ jsx13("div", { className: cn(stepClasses, "qsl:text-center qsl:text-xs qsl:text-muted-foreground"), "data-chat": "day-separator", "data-day": day.key, children: day.label }, `day-${message.key}`));
1277
+ closeGroup();
1278
+ blocks.push(/* @__PURE__ */ jsx13("div", { className: cn(stepClasses, "qsl:text-center qsl:text-xs qsl:text-muted-foreground"), "data-chat": "day-separator", "data-day": day.key, children: day.label }, `day-${message.key}`));
1215
1279
  lastDay = day.key;
1216
1280
  }
1217
1281
  }
1218
- const eigeneDarstellung = message.role === "tool" && message.tool && detailMode !== "current" ? renderTool?.(message.tool) : void 0;
1219
- if (eigeneDarstellung !== void 0) {
1220
- schliesseGruppe();
1221
- bloecke.push(mitZeit(
1222
- /* @__PURE__ */ jsx13("div", { className: stepClasses, children: eigeneDarstellung }, message.key),
1282
+ const customRendering = message.role === "tool" && message.tool && detailMode !== "current" ? renderTool?.(message.tool) : void 0;
1283
+ if (customRendering !== void 0) {
1284
+ closeGroup();
1285
+ blocks.push(withTime(
1286
+ /* @__PURE__ */ jsx13("div", { className: stepClasses, children: customRendering }, message.key),
1223
1287
  message.key,
1224
1288
  message.at
1225
1289
  ));
1226
1290
  return;
1227
1291
  }
1228
- if ((chipModus || gruppenModus) && istSchritt(message)) {
1229
- gruppe = [...gruppe, message];
1292
+ if ((chipMode || groupMode) && isStep(message)) {
1293
+ group = [...group, message];
1230
1294
  return;
1231
1295
  }
1232
- schliesseGruppe();
1233
- const dicht = istSchritt(message) && istSchritt(visible[index - 1]);
1234
- bloecke.push(mitZeit(
1296
+ closeGroup();
1297
+ const dense = isStep(message) && isStep(visible[index - 1]);
1298
+ blocks.push(withTime(
1235
1299
  /* @__PURE__ */ jsx13(
1236
1300
  Bubble,
1237
1301
  {
1238
1302
  detailMode,
1239
- dicht,
1240
- expandierbar: stepsExpandable,
1303
+ dense,
1304
+ elapsedAfter,
1305
+ expandable: stepsExpandable,
1241
1306
  message,
1242
1307
  plain: owner != null && message.sender === owner,
1243
- texts: alleTexte,
1308
+ texts: allTexts,
1244
1309
  toolArgumentsText,
1245
1310
  renderAction,
1246
1311
  onDismissAction,
@@ -1251,11 +1316,11 @@ function ChatMessages({
1251
1316
  ),
1252
1317
  message.key,
1253
1318
  message.at,
1254
- dicht
1319
+ dense
1255
1320
  ));
1256
1321
  });
1257
- schliesseGruppe();
1258
- const verlauf = /* @__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: {
1322
+ closeGroup();
1323
+ 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: {
1259
1324
  ...appearanceStyle(appearance),
1260
1325
  "--qsl-chat-content-max-width": typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth,
1261
1326
  "--qsl-chat-horizontal-padding": typeof horizontalPadding === "number" ? `${horizontalPadding}px` : horizontalPadding
@@ -1263,7 +1328,7 @@ function ChatMessages({
1263
1328
  /* @__PURE__ */ jsx13(
1264
1329
  "div",
1265
1330
  {
1266
- "aria-label": alleTexte.transcript,
1331
+ "aria-label": allTexts.transcript,
1267
1332
  className: cn(
1268
1333
  "qsl:min-h-0 qsl:flex-1 qsl:cursor-default qsl:overflow-y-auto qsl:text-[length:var(--qsl-chat-font-size,13px)] qsl:text-foreground",
1269
1334
  "qsl:[--qsl-fade-edge:calc(100%_-_var(--qsl-composer-height,0px))]",
@@ -1273,7 +1338,7 @@ function ChatMessages({
1273
1338
  scrollerClassName
1274
1339
  ),
1275
1340
  "data-quassel-transcript": "",
1276
- onScroll: pruefeEnde,
1341
+ onScroll: checkEnd,
1277
1342
  onWheel: (event) => {
1278
1343
  if (!event.ctrlKey && event.deltaY < 0) pauseUp(event.currentTarget, event.target);
1279
1344
  },
@@ -1289,20 +1354,20 @@ function ChatMessages({
1289
1354
  touchY.current = event.touches[0]?.clientY;
1290
1355
  },
1291
1356
  onTouchMove: (event) => {
1292
- const next = event.touches[0]?.clientY;
1293
- if (next !== void 0 && touchY.current !== void 0 && next > touchY.current) pauseUp(event.currentTarget, event.target);
1294
- touchY.current = next;
1357
+ const next2 = event.touches[0]?.clientY;
1358
+ if (next2 !== void 0 && touchY.current !== void 0 && next2 > touchY.current) pauseUp(event.currentTarget, event.target);
1359
+ touchY.current = next2;
1295
1360
  },
1296
1361
  role: "region",
1297
1362
  tabIndex: 0,
1298
1363
  ref: (element) => {
1299
- scrollBereich.current = element;
1364
+ scrollArea.current = element;
1300
1365
  scrollerRef?.(element);
1301
1366
  },
1302
- children: visible.length === 0 && !running ? emptyState ?? null : /* @__PURE__ */ jsxs6(
1367
+ children: visible.length === 0 && !running2 ? emptyState ?? null : /* @__PURE__ */ jsxs6(
1303
1368
  "div",
1304
1369
  {
1305
- "aria-busy": running || void 0,
1370
+ "aria-busy": running2 || void 0,
1306
1371
  className: cn(
1307
1372
  "qsl:mx-auto qsl:w-[calc(100%_-_2_*_var(--qsl-chat-horizontal-padding,24px))] qsl:max-w-[var(--qsl-chat-content-max-width,none)] qsl:pt-2",
1308
1373
  "qsl:pb-[calc(var(--qsl-composer-height,0px)_+_max(3.25em,40px))]",
@@ -1310,46 +1375,52 @@ function ChatMessages({
1310
1375
  ),
1311
1376
  ref: threadRef,
1312
1377
  children: [
1313
- bloecke,
1314
- running && (working ?? /* @__PURE__ */ jsx13(WorkingScenes, { label: alleTexte.working }))
1378
+ blocks,
1379
+ running2 && (working ?? /* @__PURE__ */ jsx13(WorkingScenes, { label: allTexts.working }))
1315
1380
  ]
1316
1381
  }
1317
1382
  )
1318
1383
  }
1319
1384
  ),
1320
- showJumpToEnd && /* @__PURE__ */ jsx13("div", { className: "qsl:pointer-events-none qsl:absolute qsl:inset-x-0 qsl:bottom-[calc(var(--qsl-composer-height,0px)_+_12px)] qsl:z-10 qsl:flex qsl:justify-center", children: /* @__PURE__ */ jsx13(Button2, { "aria-label": alleTexte.jumpToEnd, className: "qsl:pointer-events-auto qsl:rounded-full qsl:border qsl:border-primary-foreground/20 qsl:shadow-md", size: "icon", title: alleTexte.jumpToEnd, variant: "default", onClick: () => {
1321
- if (scrollBereich.current) {
1322
- scroll.jump(scrollBereich.current);
1323
- updateJumpVisibility(scrollBereich.current);
1385
+ showJumpToEnd && /* @__PURE__ */ jsx13("div", { className: "qsl:pointer-events-none qsl:absolute qsl:inset-x-0 qsl:bottom-[calc(var(--qsl-composer-height,0px)_+_12px)] qsl:z-10 qsl:flex qsl:justify-center", children: /* @__PURE__ */ jsx13(Button2, { "aria-label": allTexts.jumpToEnd, className: "qsl:pointer-events-auto qsl:rounded-full qsl:border qsl:border-primary-foreground/20 qsl:shadow-md", size: "icon", title: allTexts.jumpToEnd, variant: "default", onClick: () => {
1386
+ if (scrollArea.current) {
1387
+ scroll.jump(scrollArea.current);
1388
+ updateJumpVisibility(scrollArea.current);
1324
1389
  }
1325
- }, children: /* @__PURE__ */ jsx13(ChevronDownIcon, {}) }) })
1390
+ }, children: /* @__PURE__ */ jsx13(ChevronDownIcon, {}) }) }),
1391
+ dock && openActions.length > 0 && createPortal(
1392
+ /* @__PURE__ */ jsx13("div", { className: "qsl:flex qsl:flex-col qsl:gap-[var(--qsl-chat-dense-message-gap,8px)] qsl:text-[length:var(--qsl-chat-font-size,13px)] 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)) }),
1393
+ dock
1394
+ )
1326
1395
  ] });
1327
- return /* @__PURE__ */ jsx13(MarkdownLinks, { onLinkClick, children: /* @__PURE__ */ jsx13(MarkdownCodeBlocks, { options: codeBlockOptions, texts: alleTexte, children: verlauf }) });
1396
+ return /* @__PURE__ */ jsx13(MarkdownLinks, { onLinkClick, children: /* @__PURE__ */ jsx13(MarkdownCodeBlocks, { options: codeBlockOptions, texts: allTexts, children: transcript }) });
1328
1397
  }
1329
1398
  function StepRow({
1330
1399
  messages,
1331
- mitText,
1332
- expandierbar,
1400
+ withText,
1401
+ expandable,
1402
+ elapsedAfter,
1333
1403
  texts,
1334
1404
  toolArgumentsText,
1335
1405
  toolLabel
1336
1406
  }) {
1337
1407
  const [detail, setDetail] = useState4();
1338
- const offen = expandierbar && detail && messages.find((message) => message.key === detail.key);
1408
+ const open = expandable && detail && messages.find((message) => message.key === detail.key);
1339
1409
  return /* @__PURE__ */ jsxs6("div", { className: cn(stepClasses, "qsl:flex qsl:flex-wrap qsl:items-center qsl:gap-x-1.5 qsl:gap-y-1 qsl:pl-6"), "data-step": "row", children: [
1340
1410
  messages.map((message) => {
1341
1411
  const thinking = message.role === "thinking";
1342
1412
  const tool = message.tool;
1343
1413
  const state = stepState(message);
1344
- const running = state === "running" || state === "thinking";
1414
+ const running2 = state === "running" || state === "thinking";
1345
1415
  const label = thinking ? texts.thinkingChip : toolLabel ?? (tool?.name || message.text || texts.toolChip);
1346
1416
  const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1347
1417
  thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { size: 11 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? "qsl:text-destructive" : void 0, size: 11 }),
1348
- mitText && /* @__PURE__ */ jsx13("span", { className: "qsl:min-w-0 qsl:self-baseline qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap", children: label }),
1349
- mitText && /* @__PURE__ */ jsx13("span", { className: "qsl:inline-flex qsl:w-2.5 qsl:flex-none qsl:items-center qsl:justify-center qsl:text-success", children: state === "done" && /* @__PURE__ */ jsx13(CheckIcon4, { size: 10 }) })
1418
+ withText && /* @__PURE__ */ jsx13("span", { className: "qsl:min-w-0 qsl:self-baseline qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap", children: label }),
1419
+ withText && /* @__PURE__ */ jsx13(ToolElapsed, { after: elapsedAfter, className: "qsl:flex-none qsl:self-baseline", message, texts }),
1420
+ withText && /* @__PURE__ */ jsx13("span", { className: "qsl:inline-flex qsl:w-2.5 qsl:flex-none qsl:items-center qsl:justify-center qsl:text-success", children: state === "done" && /* @__PURE__ */ jsx13(CheckIcon4, { size: 10 }) })
1350
1421
  ] });
1351
- const classes = cn(chipClasses, !mitText && "qsl:p-1", running && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none");
1352
- if (!expandierbar) {
1422
+ const classes = cn(chipClasses, !withText && "qsl:p-1", running2 && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none");
1423
+ if (!expandable) {
1353
1424
  return /* @__PURE__ */ jsx13("span", { className: classes, "data-kind": thinking ? "thinking" : "tool", "data-state": state, "data-step": "chip", title: label, children: content }, message.key);
1354
1425
  }
1355
1426
  return /* @__PURE__ */ jsx13(
@@ -1368,10 +1439,10 @@ function StepRow({
1368
1439
  message.key
1369
1440
  );
1370
1441
  }),
1371
- offen && detail && /* @__PURE__ */ jsx13(
1442
+ open && detail && /* @__PURE__ */ jsx13(
1372
1443
  StepPopover,
1373
1444
  {
1374
- message: offen,
1445
+ message: open,
1375
1446
  position: detail.position,
1376
1447
  texts,
1377
1448
  toolArgumentsText,
@@ -1382,56 +1453,58 @@ function StepRow({
1382
1453
  }
1383
1454
  function StepGroup({
1384
1455
  messages,
1385
- aktiv,
1386
- expandierbar,
1456
+ runningStep,
1457
+ elapsedAfter,
1458
+ expandable,
1387
1459
  texts,
1388
1460
  toolArgumentsText
1389
1461
  }) {
1390
- const [offen, setOffen] = useState4(false);
1391
- const kopf = useRef3(null);
1392
- const vonUntenEingeklappt = useRef3(false);
1462
+ const [open, setOpen] = useState4(false);
1463
+ const header = useRef3(null);
1464
+ const collapsedFromBottom = useRef3(false);
1393
1465
  useLayoutEffect(() => {
1394
- if (!offen && vonUntenEingeklappt.current) {
1395
- vonUntenEingeklappt.current = false;
1396
- kopf.current?.scrollIntoView({ block: "nearest" });
1466
+ if (!open && collapsedFromBottom.current) {
1467
+ collapsedFromBottom.current = false;
1468
+ header.current?.scrollIntoView({ block: "nearest" });
1397
1469
  }
1398
- }, [offen]);
1399
- const letzte = messages[messages.length - 1];
1400
- const fehler = messages.some((message) => message.tool?.isError);
1401
- const anzahl = messages.length === 1 ? texts.stepGroupOne : texts.stepGroupMany.replace("{count}", String(messages.length));
1402
- const laufend = aktiv ? letzte.role === "thinking" ? texts.thinkingChip : letzte.tool?.name || letzte.text || texts.toolChip : void 0;
1470
+ }, [open]);
1471
+ const active = runningStep !== void 0;
1472
+ const failed = messages.some((message) => message.tool?.isError);
1473
+ const count = messages.length === 1 ? texts.stepGroupOne : texts.stepGroupMany.replace("{count}", String(messages.length));
1474
+ const runningLabel = runningStep && (runningStep.role === "thinking" ? texts.thinkingChip : runningStep.tool?.name || runningStep.text || texts.toolChip);
1403
1475
  return /* @__PURE__ */ jsxs6("div", { className: stepClasses, "data-step": "group", children: [
1404
1476
  /* @__PURE__ */ jsxs6(
1405
1477
  "button",
1406
1478
  {
1407
- "aria-expanded": offen,
1408
- className: cn(traceClasses, traceButtonClasses, "qsl:items-center qsl:pl-1", aktiv && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none"),
1409
- onClick: () => setOffen((wert) => !wert),
1410
- ref: kopf,
1479
+ "aria-expanded": open,
1480
+ className: cn(traceClasses, traceButtonClasses, "qsl:items-center qsl:pl-1", active && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none"),
1481
+ onClick: () => setOpen((value) => !value),
1482
+ ref: header,
1411
1483
  type: "button",
1412
1484
  children: [
1413
- /* @__PURE__ */ jsx13(ChevronRightIcon, { className: cn("qsl:flex-none qsl:opacity-60 qsl:transition-transform", offen && "qsl:rotate-90"), size: 12 }),
1414
- /* @__PURE__ */ jsx13(LayersIcon, { className: fehler ? traceErrorIconClasses : traceIconClasses, size: 12 }),
1485
+ /* @__PURE__ */ jsx13(ChevronRightIcon, { className: cn("qsl:flex-none qsl:opacity-60 qsl:transition-transform", open && "qsl:rotate-90"), size: 12 }),
1486
+ /* @__PURE__ */ jsx13(LayersIcon, { className: failed ? traceErrorIconClasses : traceIconClasses, size: 12 }),
1415
1487
  /* @__PURE__ */ jsxs6("span", { className: cn(traceLineClasses, "qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap"), children: [
1416
- anzahl,
1417
- laufend && /* @__PURE__ */ jsxs6("span", { className: traceRunningClasses, children: [
1418
- laufend,
1488
+ count,
1489
+ runningLabel && /* @__PURE__ */ jsxs6("span", { className: traceRunningClasses, children: [
1490
+ runningLabel,
1419
1491
  " ",
1420
1492
  texts.toolRunning
1421
- ] })
1493
+ ] }),
1494
+ runningStep && /* @__PURE__ */ jsx13(ToolElapsed, { after: elapsedAfter, message: runningStep, texts })
1422
1495
  ] })
1423
1496
  ]
1424
1497
  }
1425
1498
  ),
1426
- offen && /* @__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", expandierbar, message, texts, toolArgumentsText }, message.key)) }),
1427
- offen && messages.length >= collapseAtBottomFrom && /* @__PURE__ */ jsxs6(
1499
+ 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)) }),
1500
+ open && messages.length >= collapseAtBottomFrom && /* @__PURE__ */ jsxs6(
1428
1501
  "button",
1429
1502
  {
1430
1503
  className: cn(traceClasses, traceButtonClasses, "qsl:mt-0.5 qsl:items-center qsl:pl-1"),
1431
1504
  "data-step": "group-collapse",
1432
1505
  onClick: () => {
1433
- vonUntenEingeklappt.current = true;
1434
- setOffen(false);
1506
+ collapsedFromBottom.current = true;
1507
+ setOpen(false);
1435
1508
  },
1436
1509
  type: "button",
1437
1510
  children: [
@@ -1445,7 +1518,8 @@ function StepGroup({
1445
1518
  function TraceLine({
1446
1519
  message,
1447
1520
  className,
1448
- expandierbar,
1521
+ elapsedAfter,
1522
+ expandable,
1449
1523
  texts,
1450
1524
  toolArgumentsText
1451
1525
  }) {
@@ -1453,17 +1527,18 @@ function TraceLine({
1453
1527
  const thinking = message.role === "thinking";
1454
1528
  const tool = message.tool;
1455
1529
  const classes = cn(traceClasses, className);
1456
- const zeile = cn(traceLineClasses, thinking && "qsl:font-sans qsl:italic qsl:whitespace-pre-wrap", "qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap");
1457
- const inhalt = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1530
+ const line = cn(traceLineClasses, thinking && "qsl:font-sans qsl:italic qsl:whitespace-pre-wrap", "qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap");
1531
+ const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1458
1532
  thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { className: traceIconClasses, size: 12 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? traceErrorIconClasses : traceIconClasses, size: 12 }),
1459
- /* @__PURE__ */ jsxs6("span", { className: zeile, children: [
1533
+ /* @__PURE__ */ jsxs6("span", { className: line, children: [
1460
1534
  message.text,
1461
- tool && tool.result === void 0 && /* @__PURE__ */ jsx13("span", { className: traceRunningClasses, children: texts.toolRunning })
1535
+ tool && tool.result === void 0 && /* @__PURE__ */ jsx13("span", { className: traceRunningClasses, children: texts.toolRunning }),
1536
+ /* @__PURE__ */ jsx13(ToolElapsed, { after: elapsedAfter, message, texts })
1462
1537
  ] })
1463
1538
  ] });
1464
1539
  const marks = { "data-compact": "true", "data-kind": thinking ? "thinking" : "tool", "data-step": "line" };
1465
- if (!expandierbar) {
1466
- return /* @__PURE__ */ jsx13("div", { className: classes, ...marks, children: inhalt });
1540
+ if (!expandable) {
1541
+ return /* @__PURE__ */ jsx13("div", { className: classes, ...marks, children: content });
1467
1542
  }
1468
1543
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1469
1544
  /* @__PURE__ */ jsx13(
@@ -1475,7 +1550,7 @@ function TraceLine({
1475
1550
  ...marks,
1476
1551
  onClick: (event) => setDetailPosition({ x: event.clientX, y: event.clientY }),
1477
1552
  type: "button",
1478
- children: inhalt
1553
+ children: content
1479
1554
  }
1480
1555
  ),
1481
1556
  detailPosition && /* @__PURE__ */ jsx13(
@@ -1490,12 +1565,22 @@ function TraceLine({
1490
1565
  )
1491
1566
  ] });
1492
1567
  }
1568
+ function ToolElapsed({ message, after, texts, className = traceRunningClasses }) {
1569
+ const start = stepState(message) === "running" ? messageDate(message.at)?.getTime() : void 0;
1570
+ return after === void 0 || start === void 0 ? null : /* @__PURE__ */ jsx13(Elapsed, { after, className, start, texts });
1571
+ }
1572
+ function Elapsed({ start, after, className, texts }) {
1573
+ const elapsed = useElapsed(start);
1574
+ if (elapsed === void 0 || elapsed < after) return null;
1575
+ return /* @__PURE__ */ jsx13("span", { "aria-hidden": "true", className: cn(className, "qsl:tabular-nums"), "data-step": "elapsed", children: formatElapsed(elapsed, texts) });
1576
+ }
1493
1577
  function Bubble({
1494
1578
  message,
1495
1579
  plain,
1496
1580
  detailMode,
1497
- dicht,
1498
- expandierbar,
1581
+ dense,
1582
+ elapsedAfter,
1583
+ expandable,
1499
1584
  texts,
1500
1585
  toolArgumentsText,
1501
1586
  renderAction,
@@ -1503,22 +1588,23 @@ function Bubble({
1503
1588
  bubbleOptions,
1504
1589
  messageActions
1505
1590
  }) {
1506
- const schritt = dicht ? denseStepClasses : stepClasses;
1591
+ const stepSpacing = dense ? denseStepClasses : stepClasses;
1507
1592
  if (message.role === "thinking" || message.role === "tool" && message.tool) {
1508
1593
  if (detailMode === "compact") {
1509
- return /* @__PURE__ */ jsx13(TraceLine, { className: schritt, expandierbar, message, texts, toolArgumentsText });
1594
+ return /* @__PURE__ */ jsx13(TraceLine, { className: stepSpacing, elapsedAfter, expandable, message, texts, toolArgumentsText });
1510
1595
  }
1511
1596
  const thinking = message.role === "thinking";
1512
1597
  const tool = message.tool;
1513
1598
  const icon = thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { className: traceIconClasses, size: 12 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? traceErrorIconClasses : traceIconClasses, size: 12 });
1514
- const zeile = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1599
+ const line = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1515
1600
  message.text,
1516
- tool && tool.result === void 0 && /* @__PURE__ */ jsx13("span", { className: traceRunningClasses, children: texts.toolRunning })
1601
+ tool && tool.result === void 0 && /* @__PURE__ */ jsx13("span", { className: traceRunningClasses, children: texts.toolRunning }),
1602
+ /* @__PURE__ */ jsx13(ToolElapsed, { after: elapsedAfter, message, texts })
1517
1603
  ] });
1518
- return /* @__PURE__ */ jsxs6("div", { className: cn(traceClasses, schritt), "data-kind": thinking ? "thinking" : "tool", "data-step": "detail", children: [
1604
+ return /* @__PURE__ */ jsxs6("div", { className: cn(traceClasses, stepSpacing), "data-kind": thinking ? "thinking" : "tool", "data-step": "detail", children: [
1519
1605
  icon,
1520
1606
  /* @__PURE__ */ jsxs6("div", { className: "qsl:min-w-0 qsl:flex-1", children: [
1521
- /* @__PURE__ */ jsx13("div", { className: cn(traceLineClasses, thinking && "qsl:font-sans qsl:italic qsl:whitespace-pre-wrap"), children: zeile }),
1607
+ /* @__PURE__ */ jsx13("div", { className: cn(traceLineClasses, thinking && "qsl:font-sans qsl:italic qsl:whitespace-pre-wrap"), children: line }),
1522
1608
  tool && /* @__PURE__ */ jsxs6("div", { className: "qsl:mt-0.5 qsl:border-l-2 qsl:border-border-soft qsl:pl-2", children: [
1523
1609
  /* @__PURE__ */ jsx13("pre", { className: "qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:font-mono qsl:text-meta qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80", children: toolArgumentsText(tool) }),
1524
1610
  tool.result !== void 0 && /* @__PURE__ */ jsx13("pre", { className: cn(
@@ -1530,21 +1616,10 @@ function Bubble({
1530
1616
  ] });
1531
1617
  }
1532
1618
  if (message.role === "action" && message.action) {
1533
- const action = message.action;
1534
- return /* @__PURE__ */ jsx13("div", { className: schritt, children: renderAction?.(action, message.text) ?? /* @__PURE__ */ jsx13(
1535
- PendingActionCard,
1536
- {
1537
- action,
1538
- dismissedLabel: texts.actionDismissed,
1539
- dismissLabel: texts.dismissAction,
1540
- onDismiss: onDismissAction ? () => onDismissAction(action.actionId) : void 0,
1541
- text: message.text,
1542
- waitingLabel: texts.pendingAction
1543
- }
1544
- ) });
1619
+ return /* @__PURE__ */ jsx13("div", { className: stepSpacing, children: /* @__PURE__ */ jsx13(ActionContent, { action: message.action, onDismissAction, renderAction, text: message.text, texts }) });
1545
1620
  }
1546
1621
  if (message.role === "system") {
1547
- return /* @__PURE__ */ jsxs6("div", { className: cn(schritt, "qsl:border-l-2 qsl:border-destructive/50 qsl:pl-3 qsl:text-sm qsl:text-muted-foreground"), "data-message": "system", children: [
1622
+ return /* @__PURE__ */ jsxs6("div", { className: cn(stepSpacing, "qsl:border-l-2 qsl:border-destructive/50 qsl:pl-3 qsl:text-sm qsl:text-muted-foreground"), "data-message": "system", children: [
1548
1623
  /* @__PURE__ */ jsx13(Markdown, { text: message.text }),
1549
1624
  /* @__PURE__ */ jsx13(MessageAttachments, { label: texts.attachments, message })
1550
1625
  ] });
@@ -1566,12 +1641,12 @@ function Bubble({
1566
1641
  /* @__PURE__ */ jsx13(MessageActions, { message, options: messageActions, texts })
1567
1642
  ] });
1568
1643
  const cursor = message.role === "assistant" && message.textCursor ? JSON.stringify(message.textCursor) : void 0;
1569
- if (!useBubble) return /* @__PURE__ */ jsx13("div", { "data-chat-text-cursor": cursor, className: cn(schritt, "qsl:group qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.625)] qsl:text-foreground"), "data-message": "answer", children: content });
1644
+ 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 });
1570
1645
  return /* @__PURE__ */ jsx13(
1571
1646
  "div",
1572
1647
  {
1573
1648
  "data-chat-text-cursor": cursor,
1574
- className: cn(schritt, "qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
1649
+ className: cn(stepSpacing, "qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
1575
1650
  "data-message": message.role === "user" && !message.bubble && variant === "default" ? "user" : "bubble",
1576
1651
  "data-side": side,
1577
1652
  children: /* @__PURE__ */ jsx13(
@@ -1586,6 +1661,25 @@ function Bubble({
1586
1661
  }
1587
1662
  );
1588
1663
  }
1664
+ function ActionContent({
1665
+ action,
1666
+ text,
1667
+ texts,
1668
+ renderAction,
1669
+ onDismissAction
1670
+ }) {
1671
+ return renderAction?.(action, text) ?? /* @__PURE__ */ jsx13(
1672
+ PendingActionCard,
1673
+ {
1674
+ action,
1675
+ dismissedLabel: texts.actionDismissed,
1676
+ dismissLabel: texts.dismissAction,
1677
+ onDismiss: onDismissAction ? () => onDismissAction(action.actionId) : void 0,
1678
+ text,
1679
+ waitingLabel: texts.pendingAction
1680
+ }
1681
+ );
1682
+ }
1589
1683
  function MessageAttachments({ message, label }) {
1590
1684
  if (!message.attachments?.length) return null;
1591
1685
  return /* @__PURE__ */ jsx13("ul", { className: "qsl:mt-2.5 qsl:flex qsl:flex-wrap qsl:gap-2", "aria-label": label, children: message.attachments.map((attachment, index) => /* @__PURE__ */ jsxs6("li", { className: "qsl:relative qsl:flex qsl:w-[190px] qsl:max-w-full qsl:min-w-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-lg qsl:border qsl:border-border qsl:text-sm", children: [
@@ -1611,8 +1705,9 @@ function ChatPanel({
1611
1705
  appearance,
1612
1706
  scrollOnSend = false
1613
1707
  }) {
1614
- const wurzel = useRef4(null);
1615
- const eingabe = useRef4(null);
1708
+ const root = useRef4(null);
1709
+ const composerRef = useRef4(null);
1710
+ const [dock, setDock] = useState5(null);
1616
1711
  const hasComposer = composer !== void 0;
1617
1712
  const scrollOnSendRef = useRef4(scrollOnSend);
1618
1713
  const [sendScope] = useState5(() => createChatSendScope(scrollOnSendRef));
@@ -1620,21 +1715,26 @@ function ChatPanel({
1620
1715
  scrollOnSendRef.current = scrollOnSend;
1621
1716
  }, [scrollOnSend]);
1622
1717
  useEffect4(() => {
1623
- const element = eingabe.current;
1624
- const rahmen = wurzel.current;
1625
- if (!element || !rahmen) {
1718
+ const element = composerRef.current;
1719
+ const frame = root.current;
1720
+ if (!element || !frame) {
1626
1721
  return;
1627
1722
  }
1628
- const messen = () => rahmen.style.setProperty("--qsl-composer-height", `${element.offsetHeight}px`);
1629
- const beobachter = new ResizeObserver(messen);
1630
- beobachter.observe(element);
1631
- messen();
1723
+ const measure = () => {
1724
+ frame.style.setProperty("--qsl-composer-height", `${element.offsetHeight}px`);
1725
+ frame.style.setProperty("--qsl-panel-height", `${frame.offsetHeight}px`);
1726
+ };
1727
+ const observer = new ResizeObserver(measure);
1728
+ observer.observe(element);
1729
+ observer.observe(frame);
1730
+ measure();
1632
1731
  return () => {
1633
- beobachter.disconnect();
1634
- rahmen.style.removeProperty("--qsl-composer-height");
1732
+ observer.disconnect();
1733
+ frame.style.removeProperty("--qsl-composer-height");
1734
+ frame.style.removeProperty("--qsl-panel-height");
1635
1735
  };
1636
1736
  }, [hasComposer]);
1637
- return /* @__PURE__ */ jsx14(ChatSendContext.Provider, { value: sendScope, children: /* @__PURE__ */ jsxs7(
1737
+ return /* @__PURE__ */ jsx14(ChatSendContext.Provider, { value: sendScope, children: /* @__PURE__ */ jsx14(ChatActionDock.Provider, { value: hasComposer ? dock : void 0, children: /* @__PURE__ */ jsxs7(
1638
1738
  "div",
1639
1739
  {
1640
1740
  className: cn("qsl:relative qsl:flex qsl:min-h-0 qsl:min-w-0 qsl:flex-col qsl:overflow-hidden", className),
@@ -1646,7 +1746,7 @@ function ChatPanel({
1646
1746
  "--qsl-chat-horizontal-padding": typeof horizontalPadding === "number" ? `${horizontalPadding}px` : horizontalPadding
1647
1747
  },
1648
1748
  ref: (element) => {
1649
- wurzel.current = element;
1749
+ root.current = element;
1650
1750
  if (typeof nodeRef === "function") {
1651
1751
  nodeRef(element);
1652
1752
  } else if (nodeRef) {
@@ -1655,10 +1755,20 @@ function ChatPanel({
1655
1755
  },
1656
1756
  children: [
1657
1757
  children,
1658
- hasComposer && /* @__PURE__ */ jsx14("div", { className: "qsl:absolute qsl:inset-x-0 qsl:bottom-0 qsl:z-[5] qsl:px-[var(--qsl-chat-horizontal-padding,24px)] qsl:pb-3.5 qsl:*:mx-auto qsl:*:max-w-[var(--qsl-chat-content-max-width,none)]", "data-chat": "composer", ref: eingabe, children: composer })
1758
+ hasComposer && /* @__PURE__ */ jsxs7("div", { className: "qsl:absolute qsl:inset-x-0 qsl:bottom-0 qsl:z-[5] qsl:px-[var(--qsl-chat-horizontal-padding,24px)] qsl:pb-3.5 qsl:*:mx-auto qsl:*:max-w-[var(--qsl-chat-content-max-width,none)]", "data-chat": "composer", ref: composerRef, children: [
1759
+ /* @__PURE__ */ jsx14("div", { children: /* @__PURE__ */ jsx14(
1760
+ "div",
1761
+ {
1762
+ className: "qsl:-mx-[16px] qsl:max-h-[calc(var(--qsl-panel-height,100dvh)_/_2)] qsl:overflow-y-auto 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",
1763
+ "data-chat": "actions",
1764
+ ref: setDock
1765
+ }
1766
+ ) }),
1767
+ composer
1768
+ ] })
1659
1769
  ]
1660
1770
  }
1661
- ) });
1771
+ ) }) });
1662
1772
  }
1663
1773
 
1664
1774
  // packages/quassel/src/ChatInputToolbar.tsx
@@ -1714,7 +1824,7 @@ function useFileInput({ disabled, onFiles, onPasteText }) {
1714
1824
 
1715
1825
  // packages/quassel/src/ChatInputToolbar.tsx
1716
1826
  import { useCallback as useCallback3, useContext as useContext4, useEffect as useEffect5, useImperativeHandle, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState7 } from "react";
1717
- import { createPortal } from "react-dom";
1827
+ import { createPortal as createPortal2 } from "react-dom";
1718
1828
  import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1719
1829
  var COMPACT_WIDTH_PX = 480;
1720
1830
  var errorClasses = "qsl:px-3 qsl:py-2 qsl:text-sm qsl:text-destructive";
@@ -1723,7 +1833,7 @@ var attachmentMediaClasses = "qsl:block qsl:h-[112px] qsl:w-full qsl:bg-secondar
1723
1833
  function ChatInputToolbar({
1724
1834
  onSend,
1725
1835
  onStop,
1726
- running = false,
1836
+ running: running2 = false,
1727
1837
  disabled = false,
1728
1838
  sendDisabled = false,
1729
1839
  layout = "card",
@@ -1748,7 +1858,7 @@ function ChatInputToolbar({
1748
1858
  onAutoFocusSettled
1749
1859
  }) {
1750
1860
  const sendScope = useContext4(ChatSendContext);
1751
- const alleTexte = { ...defaultTexts, ...texts };
1861
+ const allTexts = { ...defaultTexts, ...texts };
1752
1862
  const { Button: Button2, StopButton: StopButton2, Toggle: Toggle2 } = useQuasselComponents();
1753
1863
  const [draft, setDraft] = useState7(initialValue);
1754
1864
  const [sending, setSending] = useState7(false);
@@ -1772,11 +1882,11 @@ function ChatInputToolbar({
1772
1882
  const addFiles = (files) => {
1773
1883
  if (!files.length) return;
1774
1884
  if (disabled || pendingSend.current) {
1775
- setAttachmentError(alleTexte.attachmentsWhileBusy);
1885
+ setAttachmentError(allTexts.attachmentsWhileBusy);
1776
1886
  return;
1777
1887
  }
1778
1888
  try {
1779
- validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), files, alleTexte);
1889
+ validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), files, allTexts);
1780
1890
  } catch (error) {
1781
1891
  setAttachmentError(error instanceof Error ? error.message : String(error));
1782
1892
  return;
@@ -1790,17 +1900,17 @@ function ChatInputToolbar({
1790
1900
  () => updateAttachments((current) => current.map((item) => item.id === entry.id ? {
1791
1901
  ...entry,
1792
1902
  status: "error",
1793
- error: alleTexte.attachmentReadFailed
1903
+ error: allTexts.attachmentReadFailed
1794
1904
  } : item))
1795
1905
  );
1796
1906
  }
1797
1907
  };
1798
1908
  const rootRef = useRef6(null);
1799
- const eingabeRef = useRef6(null);
1909
+ const textareaRef = useRef6(null);
1800
1910
  const autoFocusSettled = useRef6(false);
1801
1911
  useEffect5(() => {
1802
1912
  if (!autoFocus || autoFocusSettled.current) return;
1803
- const input2 = eingabeRef.current;
1913
+ const input2 = textareaRef.current;
1804
1914
  if (!input2) return;
1805
1915
  let frame = 0;
1806
1916
  const settle = () => {
@@ -1849,7 +1959,7 @@ function ChatInputToolbar({
1849
1959
  disabled: disabled || sending,
1850
1960
  onFiles: addFiles,
1851
1961
  onPasteText: (text) => {
1852
- const input2 = eingabeRef.current;
1962
+ const input2 = textareaRef.current;
1853
1963
  if (!input2 || disabled || pendingSend.current) return;
1854
1964
  const start = input2.selectionStart;
1855
1965
  const end = input2.selectionEnd;
@@ -1886,11 +1996,11 @@ function ChatInputToolbar({
1886
1996
  insert: (text) => {
1887
1997
  pendingFocus.current = true;
1888
1998
  changeDraft(text);
1889
- const element = eingabeRef.current;
1999
+ const element = textareaRef.current;
1890
2000
  element?.focus();
1891
2001
  if (element?.value === text) element.setSelectionRange(text.length, text.length);
1892
2002
  },
1893
- focus: () => eingabeRef.current?.focus(),
2003
+ focus: () => textareaRef.current?.focus(),
1894
2004
  submit: (onSubmit, options) => send(onSubmit, options?.allowEmpty),
1895
2005
  reset: () => {
1896
2006
  generation.current += 1;
@@ -1913,31 +2023,31 @@ function ChatInputToolbar({
1913
2023
  return;
1914
2024
  }
1915
2025
  pendingFocus.current = false;
1916
- const element = eingabeRef.current;
2026
+ const element = textareaRef.current;
1917
2027
  if (element) {
1918
2028
  element.focus();
1919
2029
  element.setSelectionRange(element.value.length, element.value.length);
1920
2030
  }
1921
2031
  }, [draft, sending]);
1922
2032
  const applyAutoGrow = useCallback3(() => {
1923
- const element = eingabeRef.current;
2033
+ const element = textareaRef.current;
1924
2034
  if (!element || inputMaxRows === void 0) {
1925
2035
  return;
1926
2036
  }
1927
- const stil = getComputedStyle(element);
1928
- const zeilenhoehe = Number.parseFloat(stil.lineHeight) || Number.parseFloat(stil.fontSize) * 1.4;
1929
- const polster = Number.parseFloat(stil.paddingTop) + Number.parseFloat(stil.paddingBottom);
1930
- const maximum = zeilenhoehe * Math.max(inputMaxRows, inputRows) + polster;
2037
+ const style = getComputedStyle(element);
2038
+ const lineHeight = Number.parseFloat(style.lineHeight) || Number.parseFloat(style.fontSize) * 1.4;
2039
+ const padding = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom);
2040
+ const maximum = lineHeight * Math.max(inputMaxRows, inputRows) + padding;
1931
2041
  element.style.height = "auto";
1932
- const inhalt = element.scrollHeight;
1933
- element.style.height = `${Math.min(inhalt, maximum)}px`;
1934
- element.style.overflowY = inhalt > maximum ? "auto" : "hidden";
2042
+ const contentHeight = element.scrollHeight;
2043
+ element.style.height = `${Math.min(contentHeight, maximum)}px`;
2044
+ element.style.overflowY = contentHeight > maximum ? "auto" : "hidden";
1935
2045
  }, [inputMaxRows, inputRows]);
1936
2046
  useEffect5(() => {
1937
2047
  applyAutoGrow();
1938
2048
  }, [draft, applyAutoGrow]);
1939
2049
  useEffect5(() => {
1940
- const element = eingabeRef.current;
2050
+ const element = textareaRef.current;
1941
2051
  if (!element || inputMaxRows === void 0) {
1942
2052
  return;
1943
2053
  }
@@ -1958,9 +2068,9 @@ function ChatInputToolbar({
1958
2068
  }
1959
2069
  const update = () => setCompact(element.clientWidth < COMPACT_WIDTH_PX);
1960
2070
  update();
1961
- const beobachter = new ResizeObserver(update);
1962
- beobachter.observe(element);
1963
- return () => beobachter.disconnect();
2071
+ const observer = new ResizeObserver(update);
2072
+ observer.observe(element);
2073
+ return () => observer.disconnect();
1964
2074
  }, []);
1965
2075
  const hasAttachments = attachments.length > 0;
1966
2076
  useEffect5(() => {
@@ -1969,7 +2079,7 @@ function ChatInputToolbar({
1969
2079
  const capabilityError = hasAttachments && attachmentCapabilitiesError ? attachmentCapabilitiesError : attachmentCapabilities ? attachmentCapabilityError(attachments.map((entry) => ({
1970
2080
  name: entry.file.name,
1971
2081
  mediaType: attachmentMediaType(entry.file)
1972
- })), attachmentCapabilities, alleTexte) : void 0;
2082
+ })), attachmentCapabilities, allTexts) : void 0;
1973
2083
  const hasContent = draft.trim().length > 0 || attachments.length > 0;
1974
2084
  const preparing = attachments.some((entry) => entry.status === "reading");
1975
2085
  const attachmentBlocked = attachments.some((entry) => entry.status !== "ready");
@@ -1988,7 +2098,7 @@ function ChatInputToolbar({
1988
2098
  const sentRevision = draftRevision.current;
1989
2099
  const sentDraft = draft;
1990
2100
  const focusOrigin = document.activeElement;
1991
- const startedFocused = focusOrigin === eingabeRef.current || rootRef.current?.contains(focusOrigin);
2101
+ const startedFocused = focusOrigin === textareaRef.current || rootRef.current?.contains(focusOrigin);
1992
2102
  let leftComposer = false;
1993
2103
  const observeFocus = (event) => {
1994
2104
  const target = event.target;
@@ -2019,7 +2129,7 @@ function ChatInputToolbar({
2019
2129
  document.removeEventListener("pointerdown", observeFocus);
2020
2130
  if (generation.current === sentGeneration) {
2021
2131
  pendingSend.current = false;
2022
- pendingFocus.current = !!startedFocused && !leftComposer && document.hasFocus() && (!toolbar || focusOrigin !== eingabeRef.current) && (document.activeElement === focusOrigin || !toolbar && document.activeElement === document.body);
2132
+ pendingFocus.current = !!startedFocused && !leftComposer && document.hasFocus() && (!toolbar || focusOrigin !== textareaRef.current) && (document.activeElement === focusOrigin || !toolbar && document.activeElement === document.body);
2023
2133
  setSending(false);
2024
2134
  }
2025
2135
  }
@@ -2030,28 +2140,28 @@ function ChatInputToolbar({
2030
2140
  if (!event.repeat) void send();
2031
2141
  }
2032
2142
  };
2033
- const attachmentPreviews = attachments.length > 0 && /* @__PURE__ */ jsx15("ul", { className: "qsl:flex qsl:max-h-[240px] qsl:flex-wrap qsl:gap-2 qsl:overflow-y-auto qsl:px-3 qsl:pt-2.5 qsl:in-data-[tone=overseer-details]:max-h-40 qsl:in-data-[tone=overseer-details]:overflow-y-auto", "aria-label": alleTexte.attachments, children: attachments.map((entry) => /* @__PURE__ */ jsxs8("li", { className: attachmentItemClasses, children: [
2143
+ const attachmentPreviews = attachments.length > 0 && /* @__PURE__ */ jsx15("ul", { className: "qsl:flex qsl:max-h-[240px] qsl:flex-wrap qsl:gap-2 qsl:overflow-y-auto qsl:px-3 qsl:pt-2.5 qsl:in-data-[tone=overseer-details]:max-h-40 qsl:in-data-[tone=overseer-details]:overflow-y-auto", "aria-label": allTexts.attachments, children: attachments.map((entry) => /* @__PURE__ */ jsxs8("li", { className: attachmentItemClasses, children: [
2034
2144
  entry.status === "ready" && entry.input.mediaType.startsWith("image/") && /* @__PURE__ */ jsx15("img", { className: attachmentMediaClasses, src: `data:${entry.input.mediaType};base64,${entry.input.data}`, alt: entry.file.name }),
2035
2145
  entry.status === "ready" && entry.input.mediaType.startsWith("video/") && /* @__PURE__ */ jsx15("video", { className: attachmentMediaClasses, controls: true, preload: "metadata", "aria-label": entry.file.name, src: `data:${entry.input.mediaType};base64,${entry.input.data}` }),
2036
2146
  /* @__PURE__ */ jsxs8("div", { className: "qsl:flex qsl:flex-col qsl:gap-1 qsl:p-2 qsl:pr-8 qsl:[overflow-wrap:anywhere]", children: [
2037
2147
  /* @__PURE__ */ jsx15("span", { className: "qsl:font-medium", title: entry.file.name, children: entry.file.name }),
2038
2148
  /* @__PURE__ */ jsx15("small", { children: formatAttachmentSize(entry.file.size) }),
2039
- entry.status === "reading" && /* @__PURE__ */ jsx15("small", { role: "status", children: alleTexte.attachmentPreparing }),
2149
+ entry.status === "reading" && /* @__PURE__ */ jsx15("small", { role: "status", children: allTexts.attachmentPreparing }),
2040
2150
  entry.status === "error" && /* @__PURE__ */ jsx15("small", { role: "alert", children: entry.error })
2041
2151
  ] }),
2042
2152
  /* @__PURE__ */ jsx15(
2043
2153
  Button2,
2044
2154
  {
2045
- "aria-label": fillText(alleTexte.removeAttachment, { name: entry.file.name }),
2155
+ "aria-label": fillText(allTexts.removeAttachment, { name: entry.file.name }),
2046
2156
  className: "qsl:absolute qsl:top-1 qsl:right-1",
2047
2157
  disabled: disabled || sending,
2048
2158
  onClick: () => {
2049
2159
  updateAttachments((current) => current.filter((item) => item.id !== entry.id));
2050
2160
  setAttachmentError(void 0);
2051
- eingabeRef.current?.focus();
2161
+ textareaRef.current?.focus();
2052
2162
  },
2053
2163
  size: "icon-sm",
2054
- title: fillText(alleTexte.removeAttachment, { name: entry.file.name }),
2164
+ title: fillText(allTexts.removeAttachment, { name: entry.file.name }),
2055
2165
  variant: "outline",
2056
2166
  children: /* @__PURE__ */ jsx15(XIcon3, {})
2057
2167
  }
@@ -2063,16 +2173,16 @@ function ChatInputToolbar({
2063
2173
  sendError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: sendError }),
2064
2174
  failedSubmissions.map((failed) => /* @__PURE__ */ jsxs8("div", { className: cn(errorClasses, "qsl:flex qsl:flex-wrap qsl:items-center qsl:gap-2"), role: "status", children: [
2065
2175
  /* @__PURE__ */ jsxs8("span", { children: [
2066
- alleTexte.notSent,
2176
+ allTexts.notSent,
2067
2177
  " ",
2068
2178
  failed.draft.slice(0, 100),
2069
2179
  failed.draft.length > 100 ? " ..." : "",
2070
- failed.attachments.length > 0 ? ` ${fillText(alleTexte.notSentAttachments, { count: failed.attachments.length })}` : "",
2180
+ failed.attachments.length > 0 ? ` ${fillText(allTexts.notSentAttachments, { count: failed.attachments.length })}` : "",
2071
2181
  " "
2072
2182
  ] }),
2073
2183
  /* @__PURE__ */ jsx15(Button2, { disabled: disabled || sending, size: "sm", variant: "outline", onClick: () => {
2074
2184
  try {
2075
- validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), failed.attachments.map((entry) => entry.file), alleTexte);
2185
+ validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), failed.attachments.map((entry) => entry.file), allTexts);
2076
2186
  } catch (error) {
2077
2187
  setAttachmentError(error instanceof Error ? error.message : String(error));
2078
2188
  return;
@@ -2083,22 +2193,22 @@ function ChatInputToolbar({
2083
2193
  setFailedSubmissions((current) => current.filter((entry) => entry.id !== failed.id));
2084
2194
  setAttachmentError(void 0);
2085
2195
  pendingFocus.current = true;
2086
- }, children: alleTexte.insertUnsent })
2196
+ }, children: allTexts.insertUnsent })
2087
2197
  ] }, failed.id))
2088
2198
  ] });
2089
2199
  const sendButtons = /* @__PURE__ */ jsxs8(Fragment3, { children: [
2090
- running && onStop && !hasContent && /* @__PURE__ */ jsx15(StopButton2, { className: "qsl:flex-none", label: alleTexte.stop, onClick: onStop, size: "icon-sm" }),
2091
- (!running || !onStop || hasContent) && /* @__PURE__ */ jsx15(
2200
+ running2 && onStop && !hasContent && /* @__PURE__ */ jsx15(StopButton2, { className: "qsl:flex-none", label: allTexts.stop, onClick: onStop, size: "icon-sm" }),
2201
+ (!running2 || !onStop || hasContent) && /* @__PURE__ */ jsx15(
2092
2202
  Button2,
2093
2203
  {
2094
- "aria-label": alleTexte.send,
2204
+ "aria-label": allTexts.send,
2095
2205
  className: "qsl:flex-none",
2096
2206
  disabled: disabled || sendDisabled || sending || attachmentBlocked || !!capabilityError || !hasContent,
2097
2207
  onClick: () => {
2098
2208
  void send();
2099
2209
  },
2100
2210
  size: "icon-sm",
2101
- title: running ? alleTexte.sendIntoRun : alleTexte.send,
2211
+ title: running2 ? allTexts.sendIntoRun : allTexts.send,
2102
2212
  children: /* @__PURE__ */ jsx15(SendIcon, { size: 14 })
2103
2213
  }
2104
2214
  )
@@ -2108,16 +2218,16 @@ function ChatInputToolbar({
2108
2218
  attachmentsEnabled && /* @__PURE__ */ jsx15(
2109
2219
  Button2,
2110
2220
  {
2111
- "aria-label": alleTexte.attachFiles,
2221
+ "aria-label": allTexts.attachFiles,
2112
2222
  disabled: disabled || sending,
2113
2223
  onClick: () => fileInput.inputRef.current?.click(),
2114
2224
  size: "icon-sm",
2115
- title: fillText(alleTexte.attachFilesHint, { count: MAX_CHAT_ATTACHMENTS, size: maxAttachmentSizeLabel }),
2225
+ title: fillText(allTexts.attachFilesHint, { count: MAX_CHAT_ATTACHMENTS, size: maxAttachmentSizeLabel }),
2116
2226
  variant: "ghost",
2117
2227
  children: /* @__PURE__ */ jsx15(PaperclipIcon, {})
2118
2228
  }
2119
2229
  ),
2120
- preparing && /* @__PURE__ */ jsx15("span", { className: cn("qsl:text-xs qsl:text-muted-foreground", inline && "qsl:sr-only"), role: "status", children: alleTexte.attachmentsPreparing }),
2230
+ preparing && /* @__PURE__ */ jsx15("span", { className: cn("qsl:text-xs qsl:text-muted-foreground", inline && "qsl:sr-only"), role: "status", children: allTexts.attachmentsPreparing }),
2121
2231
  actions?.map((action, index) => action.active === void 0 ? /* @__PURE__ */ jsxs8(Button2, { disabled: action.disabled, onClick: action.onClick, size: "sm", title: action.title ?? action.label, variant: "ghost", children: [
2122
2232
  action.icon,
2123
2233
  action.label && /* @__PURE__ */ jsx15("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
@@ -2142,13 +2252,13 @@ function ChatInputToolbar({
2142
2252
  {
2143
2253
  "aria-controls": inputAriaControls,
2144
2254
  className: textareaClasses,
2145
- "aria-label": alleTexte.placeholder,
2255
+ "aria-label": allTexts.placeholder,
2146
2256
  disabled: !toolbar && disabled,
2147
2257
  readOnly: toolbar && disabled,
2148
2258
  onChange: (event) => changeDraft(event.target.value),
2149
2259
  onKeyDown,
2150
- placeholder: running ? alleTexte.steeringPlaceholder : alleTexte.placeholder,
2151
- ref: eingabeRef,
2260
+ placeholder: running2 ? allTexts.steeringPlaceholder : allTexts.placeholder,
2261
+ ref: textareaRef,
2152
2262
  rows: inputRows,
2153
2263
  value: draft
2154
2264
  }
@@ -2160,14 +2270,14 @@ function ChatInputToolbar({
2160
2270
  "data-compact": compact,
2161
2271
  "data-layout": layout,
2162
2272
  "data-quassel": "",
2163
- "data-working": running || sending,
2273
+ "data-working": running2 || sending,
2164
2274
  ref: rootRef,
2165
2275
  ...attachmentsEnabled ? fileInput.dropProps : {},
2166
2276
  children: [
2167
2277
  attachmentsEnabled && /* @__PURE__ */ jsx15(
2168
2278
  "input",
2169
2279
  {
2170
- "aria-label": alleTexte.attachFiles,
2280
+ "aria-label": allTexts.attachFiles,
2171
2281
  hidden: true,
2172
2282
  multiple: true,
2173
2283
  type: "file",
@@ -2184,7 +2294,7 @@ function ChatInputToolbar({
2184
2294
  ] }) : input,
2185
2295
  toolbar ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2186
2296
  /* @__PURE__ */ jsx15("div", { className: "qsl:flex qsl:flex-none qsl:items-center qsl:gap-1 qsl:p-1", children: sendButtons }),
2187
- detailsContainer && createPortal(details, detailsContainer)
2297
+ detailsContainer && createPortal2(details, detailsContainer)
2188
2298
  ] }) : !inline && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2189
2299
  errors,
2190
2300
  detailToolbar
@@ -2195,8 +2305,8 @@ function ChatInputToolbar({
2195
2305
  }
2196
2306
 
2197
2307
  // packages/quassel/src/DetailModeSwitch.tsx
2198
- import { SparklesIcon as SparklesIcon3 } from "lucide-react";
2199
- import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
2308
+ import { CircleDotIcon, EyeOffIcon, ListCollapseIcon, ListIcon, ShapesIcon, TagsIcon, TextAlignJustifyIcon } from "lucide-react";
2309
+ import { jsx as jsx16 } from "react/jsx-runtime";
2200
2310
  var DETAIL_MODES = ["off", "current", "icons", "chips", "grouped", "compact", "full"];
2201
2311
  var TEXT_KEYS = {
2202
2312
  off: "detailModeOff",
@@ -2207,57 +2317,50 @@ var TEXT_KEYS = {
2207
2317
  compact: "detailModeCompact",
2208
2318
  full: "detailModeFull"
2209
2319
  };
2320
+ var ICONS = {
2321
+ off: EyeOffIcon,
2322
+ current: CircleDotIcon,
2323
+ icons: ShapesIcon,
2324
+ chips: TagsIcon,
2325
+ grouped: ListCollapseIcon,
2326
+ compact: ListIcon,
2327
+ full: TextAlignJustifyIcon
2328
+ };
2210
2329
  var detailModeLabel = (mode, texts) => ({ ...defaultTexts, ...texts })[TEXT_KEYS[mode]];
2211
2330
  function DetailModeSwitch({
2212
2331
  mode,
2213
2332
  onChange,
2214
2333
  modes = DETAIL_MODES,
2215
- collapsible = true,
2216
2334
  texts,
2217
2335
  className
2218
2336
  }) {
2219
- const alleTexte = { ...defaultTexts, ...texts };
2337
+ const allTexts = { ...defaultTexts, ...texts };
2220
2338
  const { Button: Button2 } = useQuasselComponents();
2221
- const aktuell = modes.indexOf(mode);
2222
- const naechster = modes[(aktuell < 0 ? 0 : aktuell + 1) % modes.length];
2223
- return /* @__PURE__ */ jsxs9(
2224
- Button2,
2225
- {
2226
- "aria-label": `${alleTexte.detailModeTitle}: ${detailModeLabel(mode, texts)}`,
2227
- className,
2228
- onClick: () => onChange(naechster),
2229
- size: "sm",
2230
- title: `${alleTexte.detailModeTitle}: ${detailModeLabel(mode, texts)}`,
2231
- variant: "ghost",
2232
- children: [
2233
- /* @__PURE__ */ jsx16(SparklesIcon3, {}),
2234
- /* @__PURE__ */ jsx16("span", { className: collapsible ? "qsl:in-data-[compact=true]:hidden" : void 0, children: detailModeLabel(mode, texts) })
2235
- ]
2236
- }
2237
- );
2339
+ const current = modes.indexOf(mode);
2340
+ const next2 = modes[(current < 0 ? 0 : current + 1) % modes.length];
2341
+ const Icon = ICONS[mode];
2342
+ const title = `${allTexts.detailModeTitle}: ${detailModeLabel(mode, texts)}`;
2343
+ return /* @__PURE__ */ jsx16(Button2, { "aria-label": title, className, onClick: () => onChange(next2), size: "icon-sm", title, variant: "ghost", children: /* @__PURE__ */ jsx16(Icon, {}) });
2238
2344
  }
2239
2345
 
2240
2346
  // packages/quassel/src/TimestampSwitch.tsx
2241
- import { ClockIcon } from "lucide-react";
2242
- import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
2243
- function TimestampSwitch({ showTimestamps, onChange, collapsible = true, texts, className }) {
2347
+ import { ClockFadingIcon, ClockIcon } from "lucide-react";
2348
+ import { jsx as jsx17 } from "react/jsx-runtime";
2349
+ function TimestampSwitch({ showTimestamps, onChange, texts, className }) {
2244
2350
  const labels = { ...defaultTexts, ...texts };
2245
2351
  const { Button: Button2 } = useQuasselComponents();
2246
2352
  const title = showTimestamps ? labels.hideTimestamps : labels.showTimestamps;
2247
- return /* @__PURE__ */ jsxs10(
2353
+ return /* @__PURE__ */ jsx17(
2248
2354
  Button2,
2249
2355
  {
2250
2356
  "aria-label": title,
2251
2357
  "aria-pressed": showTimestamps,
2252
2358
  className,
2253
2359
  onClick: () => onChange(!showTimestamps),
2254
- size: "sm",
2360
+ size: "icon-sm",
2255
2361
  title,
2256
2362
  variant: "ghost",
2257
- children: [
2258
- /* @__PURE__ */ jsx17(ClockIcon, {}),
2259
- /* @__PURE__ */ jsx17("span", { className: collapsible ? "qsl:in-data-[compact=true]:hidden" : void 0, children: labels.timestamps })
2260
- ]
2363
+ children: showTimestamps ? /* @__PURE__ */ jsx17(ClockIcon, {}) : /* @__PURE__ */ jsx17(ClockFadingIcon, {})
2261
2364
  }
2262
2365
  );
2263
2366
  }
@@ -2295,6 +2398,7 @@ export {
2295
2398
  encodeAttachment,
2296
2399
  fillText,
2297
2400
  formatAttachmentSize,
2401
+ formatElapsed,
2298
2402
  markdownPlainText,
2299
2403
  messageDate,
2300
2404
  prettyJson,
@@ -2302,6 +2406,7 @@ export {
2302
2406
  timestampDay,
2303
2407
  timestampLabel,
2304
2408
  useChatAction,
2409
+ useElapsed,
2305
2410
  useQuasselComponents,
2306
2411
  useQuasselPortalContainer,
2307
2412
  validateAttachmentSelection