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.
@@ -0,0 +1,2 @@
1
+ /** Target of open actions: undefined = in the transcript, null = the dock above the input is not mounted yet. */
2
+ export declare const ChatActionDock: import("react").Context<HTMLElement | null | undefined>;
@@ -2,7 +2,7 @@ import { ReactNode, Ref } from "react";
2
2
  import { ChatTexts } from "./texts";
3
3
  import type { ChatAttachmentInput } from "./events";
4
4
  import type { SendShortcut } from "./options";
5
- /** Ein Toolbar-Knopf, deklarativ: Symbol, Text oder beides - plus Klick-Handler. */
5
+ /** A toolbar button, declarative: icon, text or both - plus click handler. */
6
6
  export interface ToolbarAction {
7
7
  icon?: ReactNode;
8
8
  label?: string;
@@ -11,9 +11,9 @@ export interface ToolbarAction {
11
11
  active?: boolean;
12
12
  onClick: () => void;
13
13
  }
14
- /** Hosts steuern den Entwurf von aussen, z.B. um eine Vorlage zum Bearbeiten einzusetzen. */
14
+ /** Hosts control the draft from outside, e.g. to insert a template for editing. */
15
15
  export interface ChatInputHandle {
16
- /** Ersetzt den Entwurf, fokussiert die Eingabe und stellt die Schreibmarke ans Ende. */
16
+ /** Replaces the draft, focuses the input and puts the caret at the end. */
17
17
  insert: (text: string) => void;
18
18
  focus: () => void;
19
19
  reset: () => void;
@@ -22,11 +22,11 @@ export interface ChatInputHandle {
22
22
  }) => Promise<void>;
23
23
  }
24
24
  /**
25
- * Die Eingabe-Karte: rahmenlose Textarea oben, Toolbar unter einer Haarlinie. Eigene
26
- * Knöpfe kommen deklarativ über `actions` oder frei über die Slots links/rechts; bei
27
- * laufender Arbeit bleibt der Stop-Knopf sichtbar, und Tippen zeigt daneben den Senden-Knopf,
28
- * der zum Dazwischenfunken wird. `rows` bestimmt die Starthöhe; mit `maxRows` wächst die Textarea
29
- * mit dem Inhalt bis zu dieser Zeilenzahl und scrollt danach.
25
+ * The input card: borderless textarea on top, toolbar below a hairline. Custom buttons come
26
+ * declaratively through `actions` or freely through the left/right slots; while work is running
27
+ * the stop button stays visible, and typing shows the send button next to it, which then
28
+ * interjects. `rows` sets the initial height; with `maxRows` the textarea grows with its content
29
+ * up to that number of rows and scrolls after that.
30
30
  */
31
31
  export declare function ChatInputToolbar({ onSend, onStop, running, disabled, sendDisabled, layout, detailsContainer, onErrorChange, inputAriaControls, rows, maxRows, actions, toolbarLeft, toolbarRight, texts, handleRef, attachments: attachmentsEnabled, attachmentCapabilities, attachmentCapabilitiesError, onAttachmentsChange, initialValue, onDraftChange, sendShortcut, autoFocus, onAutoFocusSettled, }: {
32
32
  onSend: (text: string, attachments?: ChatAttachmentInput[]) => void | Promise<void>;
@@ -45,7 +45,7 @@ export declare function ChatInputToolbar({ onSend, onStop, running, disabled, se
45
45
  toolbarRight?: ReactNode;
46
46
  texts?: Partial<ChatTexts>;
47
47
  handleRef?: Ref<ChatInputHandle>;
48
- /** false = kein Anhängen: weder Knopf noch Ablegen oder Einfügen von Dateien. */
48
+ /** false = no attaching: neither button nor dropping or pasting files. */
49
49
  attachments?: boolean;
50
50
  attachmentCapabilities?: {
51
51
  input: readonly string[];
package/ChatMessages.d.ts CHANGED
@@ -4,34 +4,34 @@ import { ChatTexts } from "./texts";
4
4
  import { type LinkClickHandler } from "./Markdown";
5
5
  import { type ChatAppearance, type TimestampOptions, type CodeBlockOptions, type BubbleOptions, type MessageActionsOptions } from "./options";
6
6
  /**
7
- * Der Verlauf: eigener Scroll-Container mit Autoscroll, solange man unten ist,
8
- * und einem Zum-Ende-Knopf, sobald man liest. Ohne Eingabe read-only nutzbar.
7
+ * The transcript: its own scroll container with autoscroll while you are at the bottom, and a
8
+ * jump-to-end button as soon as you are reading. Usable read-only without a composer.
9
9
  */
10
- export declare function ChatMessages({ messages, owner, detailMode, running, working, stepsExpandable, texts, toolArgumentsText, renderTool, renderAction, onDismissAction, onLinkClick, emptyState, className, scrollerClassName, showTimestamps, announce, scrollerRef, maxWidth, horizontalPadding, jumpToEndThreshold, appearance, timestampOptions, codeBlockOptions, bubbleOptions, messageActions, }: {
10
+ export declare function ChatMessages({ messages, owner, detailMode, running, working, stepsExpandable, texts, toolArgumentsText, renderTool, renderAction, onDismissAction, onLinkClick, emptyState, className, scrollerClassName, showTimestamps, announce, scrollerRef, maxWidth, horizontalPadding, jumpToEndThreshold, appearance, timestampOptions, codeBlockOptions, bubbleOptions, messageActions, toolElapsedThreshold, }: {
11
11
  messages: Message[];
12
- /** Beiträge mit diesem Message.sender erscheinen ohne Sprechblase; null behält die Nachrichtenvorgaben. */
12
+ /** Contributions with this Message.sender appear without a speech bubble; null keeps the message defaults. */
13
13
  owner?: string | null;
14
14
  detailMode?: DetailMode;
15
15
  running?: boolean;
16
16
  working?: ReactNode;
17
- /** false = Denk- und Werkzeug-Schritte lassen sich nicht aufklappen (kein Popover). */
17
+ /** false = thinking and tool steps cannot be expanded (no popover). */
18
18
  stepsExpandable?: boolean;
19
19
  texts?: Partial<ChatTexts>;
20
20
  toolArgumentsText?: (tool: ToolInfo) => string;
21
- /** Eigene Darstellung fuer einzelne Werkzeuge; undefined = Standarddarstellung. */
21
+ /** Custom rendering for single tools; undefined = default rendering. */
22
22
  renderTool?: (tool: ToolInfo) => ReactNode | undefined;
23
- /** Darstellung einer wartenden Aktion durch ihren Eigentümer; undefined = generische Karte. */
23
+ /** Rendering of a pending action by its owner; undefined = generic card. */
24
24
  renderAction?: (action: PendingAction, text: string) => ReactNode | undefined;
25
25
  onDismissAction?: (actionId: string) => void;
26
- /** Faengt Klicks auf Markdown-Links ab; true = behandelt, der Browser folgt nicht. */
26
+ /** Intercepts clicks on Markdown links; true = handled, the browser does not follow. */
27
27
  onLinkClick?: LinkClickHandler;
28
28
  emptyState?: ReactNode;
29
29
  className?: string;
30
- /** Klassen des Hosts für den Scroll-Container (data-quassel-transcript). */
30
+ /** Host classes for the scroll container (data-quassel-transcript). */
31
31
  scrollerClassName?: string;
32
- /** true = dezente HH:MM-Spalte links an jedem Block (Message.at). */
32
+ /** true = subtle HH:MM column to the left of each block (Message.at). */
33
33
  showTimestamps?: boolean;
34
- /** Screenreader-Ansage fertiger Antworten und neuer Aktionen; false = der Host sagt selbst an. */
34
+ /** Screen reader announcement of finished answers and new actions; false = the host announces itself. */
35
35
  announce?: false | ((announcement: ChatAnnouncement) => string | undefined);
36
36
  /** Exposes the scroll container so hosts can scroll without querying the DOM. */
37
37
  scrollerRef?: (element: HTMLDivElement | null) => void;
@@ -43,4 +43,6 @@ export declare function ChatMessages({ messages, owner, detailMode, running, wor
43
43
  codeBlockOptions?: CodeBlockOptions;
44
44
  bubbleOptions?: BubbleOptions;
45
45
  messageActions?: MessageActionsOptions;
46
+ /** Milliseconds before a running tool call shows its elapsed time (from Message.at) while running; false = never. */
47
+ toolElapsedThreshold?: number | false;
46
48
  }): import("react").JSX.Element;
package/ChatPanel.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { type CSSProperties, type ReactNode, type Ref } from "react";
2
2
  import { type ChatAppearance } from "./options";
3
3
  /**
4
- * Verlauf mit ueberlagerter Eingabe. Der Rahmen misst die Eingabe selbst und
5
- * gibt dem Verlauf den noetigen Fussraum, damit kein Host Innenmasse setzen muss.
4
+ * Transcript with an overlaid composer. The frame measures the composer itself and gives the
5
+ * transcript the footer space it needs, so no host has to set inner dimensions.
6
6
  */
7
7
  export declare function ChatPanel({ children, composer, className, nodeRef, maxWidth, horizontalPadding, appearance, scrollOnSend, }: {
8
8
  children: ReactNode;
9
- /** Die Eingabe; sie liegt ueber dem Verlauf, nicht darunter. */
9
+ /** The composer; it lies over the transcript, not below it. */
10
10
  composer?: ReactNode;
11
11
  className?: string;
12
12
  nodeRef?: Ref<HTMLDivElement>;
@@ -3,16 +3,14 @@ import { ChatTexts } from "./texts";
3
3
  export declare const DETAIL_MODES: readonly DetailMode[];
4
4
  export declare const detailModeLabel: (mode: DetailMode, texts?: Partial<ChatTexts>) => string;
5
5
  /**
6
- * Ein Knopf, der den Detailgrad der Denk- und Werkzeug-Schritte weiterschaltet. Die
7
- * Reihenfolge kommt aus `modes`, der aktuelle Wert steht als Beschriftung daneben.
6
+ * An icon button that cycles the detail level of the thinking and tool steps. The order comes
7
+ * from `modes`, the current value shows in the icon and in the tooltip.
8
8
  */
9
- export declare function DetailModeSwitch({ mode, onChange, modes, collapsible, texts, className, }: {
9
+ export declare function DetailModeSwitch({ mode, onChange, modes, texts, className, }: {
10
10
  mode: DetailMode;
11
11
  onChange: (mode: DetailMode) => void;
12
- /** Auswahl und Reihenfolge des Weiterschaltens; Default sind alle Modi. */
12
+ /** Selection and order of cycling; the default is all modes. */
13
13
  modes?: readonly DetailMode[];
14
- /** false = die Beschriftung bleibt auch in einer schmalen Eingabe-Karte stehen. */
15
- collapsible?: boolean;
16
14
  texts?: Partial<ChatTexts>;
17
15
  className?: string;
18
16
  }): import("react").JSX.Element;
package/Markdown.d.ts CHANGED
@@ -15,5 +15,5 @@ export declare function Markdown({ text, streaming }: {
15
15
  text: string;
16
16
  streaming?: boolean;
17
17
  }): import("react").JSX.Element;
18
- /** Dieselbe Syntax wie der Renderer, als Klartext ohne Markup, etwa für Screenreader-Ansagen. */
18
+ /** The same syntax as the renderer, as plain text without markup, e.g. for screen reader announcements. */
19
19
  export declare function markdownPlainText(text: string): string;
@@ -1,7 +1,7 @@
1
1
  import type { PendingAction } from "./types";
2
2
  /**
3
- * Die Darstellung einer wartenden Aktion ohne Beitrag ihres Eigentümers: Titel, der Hinweis,
4
- * dass eine Eingabe erwartet wird, und das Verwerfen. Die Form kennt nur das Plugin.
3
+ * The rendering of a pending action without a contribution from its owner: title, the hint
4
+ * that input is expected, and dismissing. Only the plugin knows the form.
5
5
  */
6
6
  export declare function PendingActionCard({ text, action, waitingLabel, dismissLabel, dismissedLabel, onDismiss, }: {
7
7
  text: string;
package/README.md CHANGED
@@ -12,12 +12,15 @@ color, radius, font and font size is a `--qsl-*` variable.
12
12
 
13
13
  **Agent chats that show their work.** Thinking and tool calls appear as steps. Pick how much
14
14
  of them the user sees - only the current step, icons, chips, a collapsible group, single lines
15
- or everything - and let them switch at runtime.
15
+ or everything - and let them switch at runtime. A tool call that runs longer than a few seconds
16
+ shows how long it has been running, on its row and on the collapsed group header, so a slow step
17
+ does not look like a hung one.
16
18
 
17
19
  ![The same steps as icons, chips and single lines](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/details.png)
18
20
 
19
21
  **Pending actions.** The agent waits for input: a generic card with dismiss, or the host's own
20
- rendering per action owner. Resolved actions stay in the transcript as a record.
22
+ rendering per action owner. Open actions sit right above the input, like a question in a terminal
23
+ agent; resolved actions stay in the transcript as a record.
21
24
 
22
25
  **Multi-party conversations.** Colored bubbles with labels for agents that talk to each other.
23
26
 
@@ -43,7 +46,7 @@ variables too.
43
46
  - Timestamps, day separators, message actions, configurable bubbles and appearance.
44
47
  - Screen reader friendly: the transcript is not a live region; finished replies and new
45
48
  actions are announced once, with the host in control of the wording.
46
- - All labels are replaceable through `texts` (defaults are German).
49
+ - All labels are replaceable through `texts` (defaults are English).
47
50
 
48
51
  ## Install
49
52
 
@@ -89,12 +92,13 @@ Any backend that emits the `quassel/events` stream works with the same component
89
92
  pnpm install
90
93
  pnpm dev # stylesheet in watch mode plus the gallery on http://localhost:3210
91
94
  pnpm check # type check package and gallery
95
+ pnpm test # package tests (node:test with happy-dom)
92
96
  pnpm build # stylesheet and gallery build
93
97
  pnpm release --dry-run
94
98
  ```
95
99
 
96
100
  The full guide - every prop, the event contract, theming, slots, dark mode and publishing -
97
- is in [docs/guide.md](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md) (German). It is written so that an AI assistant can build
101
+ is in [docs/guide.md](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md). It is written so that an AI assistant can build
98
102
  an application with quassel from it.
99
103
 
100
104
  ## License
@@ -1,8 +1,7 @@
1
1
  import { type ChatTexts } from "./texts";
2
- export declare function TimestampSwitch({ showTimestamps, onChange, collapsible, texts, className }: {
2
+ export declare function TimestampSwitch({ showTimestamps, onChange, texts, className }: {
3
3
  showTimestamps: boolean;
4
4
  onChange: (showTimestamps: boolean) => void;
5
- collapsible?: boolean;
6
5
  texts?: Partial<ChatTexts>;
7
6
  className?: string;
8
7
  }): import("react").JSX.Element;
@@ -1,12 +1,12 @@
1
1
  export interface WorkingScenesProps {
2
2
  /** Limits scenes to one or two visible characters for small status fields. */
3
3
  compact?: boolean;
4
- /** Text fuer Screenreader, Default "Arbeitet ...". */
4
+ /** Text for screen readers, default "Working ...". */
5
5
  label?: string;
6
6
  }
7
7
  /**
8
- * Der Default-Working-Indikator: zufaellig rotierende Mini-Szenen. Zufaellige Reihenfolge
9
- * und Startphase, damit dieselben Szenen nie wie eine Schleife wirken. Ueber die
10
- * working-Prop von ChatMessages laesst er sich komplett ersetzen.
8
+ * The default working indicator: randomly rotating mini scenes. Random order and start phase,
9
+ * so the same scenes never feel like a loop. The working prop of ChatMessages replaces it
10
+ * completely.
11
11
  */
12
12
  export declare function WorkingScenes({ label, compact }: WorkingScenesProps): import("react").JSX.Element;
package/announce.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- /** Sagt Text über eine gemeinsame, versteckte Live-Region an: gedrosselt, und bei verstecktem Tab angehalten. */
1
+ /** Announces text through a shared, hidden live region: throttled, and paused while the tab is hidden. */
2
2
  export declare function announce(text: string): void;
package/chat.css CHANGED
@@ -87,6 +87,9 @@
87
87
  .qsl\:z-\[110\] {
88
88
  z-index: 110;
89
89
  }
90
+ .qsl\:-mx-\[16px\] {
91
+ margin-inline: calc(16px * -1);
92
+ }
90
93
  .qsl\:mx-auto {
91
94
  margin-inline: auto;
92
95
  }
@@ -200,6 +203,9 @@
200
203
  .qsl\:max-h-\[240px\] {
201
204
  max-height: 240px;
202
205
  }
206
+ .qsl\:max-h-\[calc\(var\(--qsl-panel-height\,100dvh\)_\/_2\)\] {
207
+ max-height: calc(var(--qsl-panel-height,100dvh) / 2);
208
+ }
203
209
  .qsl\:max-h-\[min\(70vh\,620px\)\] {
204
210
  max-height: min(70vh, 620px);
205
211
  }
@@ -377,6 +383,9 @@
377
383
  .qsl\:gap-\[5px\] {
378
384
  gap: 5px;
379
385
  }
386
+ .qsl\:gap-\[var\(--qsl-chat-dense-message-gap\,8px\)\] {
387
+ gap: var(--qsl-chat-dense-message-gap,8px);
388
+ }
380
389
  :where(.qsl\:space-y-0 > :not(:last-child)) {
381
390
  --tw-space-y-reverse: 0;
382
391
  margin-block-start: 0;
@@ -523,6 +532,9 @@
523
532
  .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\] {
524
533
  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;
525
534
  }
535
+ .qsl\:\[mask-image\:linear-gradient\(to_bottom\,\#000_calc\(100\%_-_var\(--qsl-chat-message-gap\,16px\)\)\,transparent\)\] {
536
+ mask-image: linear-gradient(to bottom,#000 calc(100% - var(--qsl-chat-message-gap,16px)),transparent);
537
+ }
526
538
  .qsl\:\[mask-image\:linear-gradient\(to_bottom\,\#000_calc\(var\(--qsl-fade-edge\)_-_40px\)\,transparent_calc\(var\(--qsl-fade-edge\)_\+_16px\)\)\] {
527
539
  mask-image: linear-gradient(to bottom,#000 calc(var(--qsl-fade-edge) - 40px),transparent calc(var(--qsl-fade-edge) + 16px));
528
540
  }
@@ -571,6 +583,9 @@
571
583
  .qsl\:px-\[7px\] {
572
584
  padding-inline: 7px;
573
585
  }
586
+ .qsl\:px-\[16px\] {
587
+ padding-inline: 16px;
588
+ }
574
589
  .qsl\:px-\[var\(--qsl-chat-horizontal-padding\,24px\)\] {
575
590
  padding-inline: var(--qsl-chat-horizontal-padding,24px);
576
591
  }
@@ -622,6 +637,9 @@
622
637
  .qsl\:pb-\[calc\(var\(--qsl-composer-height\,0px\)_\+_max\(3\.25em\,40px\)\)\] {
623
638
  padding-bottom: calc(var(--qsl-composer-height,0px) + max(3.25em, 40px));
624
639
  }
640
+ .qsl\:pb-\[var\(--qsl-chat-message-gap\,16px\)\] {
641
+ padding-bottom: var(--qsl-chat-message-gap,16px);
642
+ }
625
643
  .qsl\:pl-1 {
626
644
  padding-left: var(--qsl-spacing);
627
645
  }
@@ -916,6 +934,9 @@
916
934
  .qsl\:\[--qsl-scroll-cover\:var\(--qsl-secondary\)\] {
917
935
  --qsl-scroll-cover: var(--qsl-secondary);
918
936
  }
937
+ .qsl\:\[-webkit-mask-image\:linear-gradient\(to_bottom\,\#000_calc\(100\%_-_var\(--qsl-chat-message-gap\,16px\)\)\,transparent\)\] {
938
+ -webkit-mask-image: linear-gradient(to bottom,#000 calc(100% - var(--qsl-chat-message-gap,16px)),transparent);
939
+ }
919
940
  .qsl\:\[-webkit-mask-image\:linear-gradient\(to_bottom\,\#000_calc\(var\(--qsl-fade-edge\)_-_40px\)\,transparent_calc\(var\(--qsl-fade-edge\)_\+_16px\)\)\] {
920
941
  -webkit-mask-image: linear-gradient(to bottom,#000 calc(var(--qsl-fade-edge) - 40px),transparent calc(var(--qsl-fade-edge) + 16px));
921
942
  }
@@ -951,6 +972,9 @@
951
972
  .qsl\:placeholder\:text-muted-foreground::placeholder {
952
973
  color: var(--qsl-muted-foreground);
953
974
  }
975
+ .qsl\:empty\:hidden:empty {
976
+ display: none;
977
+ }
954
978
  .qsl\:focus-within\:pointer-events-auto:focus-within {
955
979
  pointer-events: auto;
956
980
  }
@@ -43,29 +43,29 @@ var chatAttachmentMediaType = (name, mediaType) => {
43
43
  var parseChatAttachments = (value) => {
44
44
  if (value === void 0) return [];
45
45
  if (!Array.isArray(value) || value.length > MAX_CHAT_ATTACHMENTS) {
46
- throw new Error(`Anh\xE4nge m\xFCssen eine Liste mit h\xF6chstens ${MAX_CHAT_ATTACHMENTS} Dateien sein.`);
46
+ throw new Error(`Attachments must be a list of at most ${MAX_CHAT_ATTACHMENTS} files.`);
47
47
  }
48
48
  let total = 0;
49
49
  return value.map((entry, index) => {
50
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`Anhang ${index + 1} ist ung\xFCltig.`);
50
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`Attachment ${index + 1} is invalid.`);
51
51
  const { name, mediaType, data } = entry;
52
- if (typeof name !== "string" || !name.trim() || name.length > 255 || /[/\\\u0000-\u001f\u007f]/.test(name) || name === "." || name === "..") throw new Error(`Anhang ${index + 1} ben\xF6tigt einen g\xFCltigen Dateinamen ohne Pfad.`);
52
+ if (typeof name !== "string" || !name.trim() || name.length > 255 || /[/\\\u0000-\u001f\u007f]/.test(name) || name === "." || name === "..") throw new Error(`Attachment ${index + 1} needs a valid file name without a path.`);
53
53
  const normalizedType = typeof mediaType === "string" ? chatAttachmentMediaType(name, mediaType) : "";
54
54
  if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i.test(normalizedType)) {
55
- throw new Error(`Anhang ${name} ben\xF6tigt einen g\xFCltigen MIME-Typ.`);
55
+ throw new Error(`Attachment ${name} needs a valid MIME type.`);
56
56
  }
57
57
  if (typeof data === "string" && data.length > Math.ceil(MAX_CHAT_ATTACHMENT_BYTES / 3) * 4)
58
- throw new Error("Anh\xE4nge d\xFCrfen zusammen h\xF6chstens 20 MiB gro\xDF sein.");
58
+ throw new Error("Attachments may be at most 20 MiB in total.");
59
59
  if (typeof data !== "string" || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
60
- throw new Error(`Anhang ${name} enth\xE4lt ung\xFCltiges Base64.`);
60
+ throw new Error(`Attachment ${name} contains invalid Base64.`);
61
61
  }
62
62
  const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
63
63
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
64
64
  if (padding && (alphabet.indexOf(data[data.length - padding - 1]) & (padding === 2 ? 15 : 3)) !== 0) {
65
- throw new Error(`Anhang ${name} enth\xE4lt nichtkanonisches Base64.`);
65
+ throw new Error(`Attachment ${name} contains non-canonical Base64.`);
66
66
  }
67
67
  total += data.length / 4 * 3 - padding;
68
- if (total > MAX_CHAT_ATTACHMENT_BYTES) throw new Error("Anh\xE4nge d\xFCrfen zusammen h\xF6chstens 20 MiB gro\xDF sein.");
68
+ if (total > MAX_CHAT_ATTACHMENT_BYTES) throw new Error("Attachments may be at most 20 MiB in total.");
69
69
  return { name, mediaType: normalizedType, data };
70
70
  });
71
71
  };
@@ -180,4 +180,4 @@ export {
180
180
  prettyJson,
181
181
  compactToolLine
182
182
  };
183
- //# sourceMappingURL=chunk-IGCFQJZB.js.map
183
+ //# sourceMappingURL=chunk-3VEL4HEO.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/chat-attachments.ts", "../src/events.ts"],
4
+ "sourcesContent": ["import type { ChatAttachmentInput } from \"./events\";\n\nexport const MAX_CHAT_ATTACHMENTS = 8;\nexport const MAX_CHAT_ATTACHMENT_BYTES = 20 * 1024 * 1024;\nexport const MAX_CHAT_REQUEST_BYTES = 29 * 1024 * 1024;\n\nexport const chatAttachmentMediaType = (name: string, mediaType: string): string => {\n const explicit = mediaType.trim().toLowerCase();\n if (explicit) return explicit === \"video/quicktime\" ? \"video/mov\" : explicit;\n const types: Record<string, string> = {\n png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\", gif: \"image/gif\", webp: \"image/webp\",\n avif: \"image/avif\", svg: \"image/svg+xml\", pdf: \"application/pdf\",\n mp4: \"video/mp4\", webm: \"video/webm\", mov: \"video/mov\", mpeg: \"video/mpeg\", mpg: \"video/mpeg\",\n txt: \"text/plain\", md: \"text/markdown\", csv: \"text/csv\", tsv: \"text/tab-separated-values\",\n json: \"application/json\", jsonl: \"application/x-ndjson\", xml: \"application/xml\",\n yaml: \"application/yaml\", yml: \"application/yaml\", ts: \"text/plain\", tsx: \"text/plain\",\n js: \"application/javascript\", py: \"text/plain\", cs: \"text/plain\", fs: \"text/plain\",\n html: \"text/html\", css: \"text/css\", log: \"text/plain\",\n };\n return types[name.split(\".\").at(-1)?.toLowerCase() ?? \"\"] ?? \"application/octet-stream\";\n};\n\nexport const parseChatAttachments = (value: unknown): ChatAttachmentInput[] => {\n if (value === undefined) return [];\n if (!Array.isArray(value) || value.length > MAX_CHAT_ATTACHMENTS) {\n throw new Error(`Attachments must be a list of at most ${MAX_CHAT_ATTACHMENTS} files.`);\n }\n let total = 0;\n return value.map((entry, index) => {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) throw new Error(`Attachment ${index + 1} is invalid.`);\n const { name, mediaType, data } = entry as Record<string, unknown>;\n if (typeof name !== \"string\" || !name.trim() || name.length > 255 || /[/\\\\\\u0000-\\u001f\\u007f]/.test(name)\n || name === \".\" || name === \"..\") throw new Error(`Attachment ${index + 1} needs a valid file name without a path.`);\n const normalizedType = typeof mediaType === \"string\" ? chatAttachmentMediaType(name, mediaType) : \"\";\n if (!/^[a-z0-9!#$&^_.+-]+\\/[a-z0-9!#$&^_.+-]+$/i.test(normalizedType)) {\n throw new Error(`Attachment ${name} needs a valid MIME type.`);\n }\n if (typeof data === \"string\" && data.length > Math.ceil(MAX_CHAT_ATTACHMENT_BYTES / 3) * 4)\n throw new Error(\"Attachments may be at most 20 MiB in total.\");\n if (typeof data !== \"string\" || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(data)) {\n throw new Error(`Attachment ${name} contains invalid Base64.`);\n }\n const padding = data.endsWith(\"==\") ? 2 : data.endsWith(\"=\") ? 1 : 0;\n const alphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n if (padding && (alphabet.indexOf(data[data.length - padding - 1]) & (padding === 2 ? 15 : 3)) !== 0) {\n throw new Error(`Attachment ${name} contains non-canonical Base64.`);\n }\n total += data.length / 4 * 3 - padding;\n if (total > MAX_CHAT_ATTACHMENT_BYTES) throw new Error(\"Attachments may be at most 20 MiB in total.\");\n return { name, mediaType: normalizedType, data };\n });\n};\n", "export type Role = \"user\" | \"assistant\" | \"thinking\" | \"tool\" | \"system\" | \"action\";\n\nexport interface ChatAttachmentInput {\n name: string;\n mediaType: string;\n data: string;\n}\n\nexport interface ChatAttachment {\n name: string;\n mediaType: string;\n size: number;\n url: string;\n}\n\nexport interface ChatAttachmentCapabilities {\n input: readonly string[];\n model: string;\n}\n\nexport interface ToolInfo {\n id: string;\n name: string;\n arguments: string;\n result?: string;\n isError?: boolean;\n}\n\nexport interface ChatTextCursor {\n conversationId: string;\n sequence: number;\n /** Accumulated non-whitespace UTF-16 units in the turn anchored by sequence. */\n offset: number;\n}\n\nexport interface ChatJournalCursor {\n conversationId: string;\n eventId: string;\n sequence: number;\n}\n\n/**\n * An action waiting for input from the user. Shape and content of `payload` and `result`\n * belong to the plugin in `owner`; a set `status` means: done, the card is a record.\n */\nexport interface PendingAction {\n actionId: string;\n owner: string | null;\n payload: unknown;\n status?: \"approved\" | \"dismissed\";\n result?: unknown;\n}\n\nexport interface Message {\n key: string;\n role: Role;\n /** Sender identifier for display; independent of role and bubble label. */\n sender?: string;\n text: string;\n textCursor?: ChatTextCursor;\n closed?: boolean;\n attachments?: ChatAttachment[];\n tool?: ToolInfo;\n action?: PendingAction;\n /** ISO timestamp of the message; display optional (ChatMessages showTimestamps). */\n at?: string;\n /** The ActorInput behind an incoming message; the steering mark hangs on it. */\n inputId?: string;\n /** The message was fed into an already running turn instead of starting its own. */\n steered?: boolean;\n /** Colored speech bubble instead of running text, e.g. for multi-party conversations. */\n bubble?: { color: string; side: \"start\" | \"end\"; label?: string };\n}\n\nexport interface ChatStartupStatus {\n status: \"preparing\" | \"failed\";\n message: string;\n}\n\nexport type ChatEvent =\n | { kind: \"reset\"; reason?: \"conversation-reset\"; conversationId: string | null }\n | { kind: \"replay-end\"; conversationId: string | null }\n | { kind: \"user\"; text: string; inputId?: string; at?: string; attachments?: ChatAttachment[] }\n | { kind: \"steered\"; inputId: string }\n | { kind: \"text\"; delta: string; at?: string; cursor: ChatTextCursor }\n | { kind: \"thinking\"; delta: string; at?: string }\n | { kind: \"tool\"; id: string; name: string; arguments: string; label?: string; at?: string }\n | { kind: \"tool-result\"; id: string; result: string; isError?: boolean }\n | { kind: \"action\"; actionId: string; owner: string | null; text: string; payload: unknown; at?: string }\n | { kind: \"action-resolved\"; actionId: string; status: \"approved\" | \"dismissed\"; result: unknown }\n | { kind: \"system\"; text: string; at?: string }\n | { kind: \"status\"; running: boolean; startup?: ChatStartupStatus }\n | { kind: \"turn-done\" }\n | { kind: \"plugin\"; pluginId: string; type: string; payload?: unknown; at?: string; journal?: ChatJournalCursor };\n\nlet keyCounter = 0;\n\n// crypto.randomUUID is missing outside secure contexts, e.g. for a host served over http.\nconst newKey = (): string => typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\"\n ? crypto.randomUUID()\n : `qsl-${Date.now().toString(36)}-${(keyCounter += 1).toString(36)}`;\n\nfunction openMessageIndex(messages: Message[]): number {\n for (let index = messages.length - 1; index >= 0; index--) {\n const message = messages[index];\n if (!message.closed && (message.role === \"assistant\" || message.role === \"thinking\")) return index;\n }\n return -1;\n}\n\nfunction closeOpen(messages: Message[]): Message[] {\n const index = openMessageIndex(messages);\n if (index === -1) {\n return messages;\n }\n return messages.map((message, position) => position === index ? { ...message, closed: true } : message);\n}\n\nfunction appendDelta(messages: Message[], role: Role, delta: string, at?: string, textCursor?: ChatTextCursor): Message[] {\n const index = openMessageIndex(messages);\n const current = messages[index];\n if (current && current.role === role\n && current.textCursor?.conversationId === textCursor?.conversationId\n && current.textCursor?.sequence === textCursor?.sequence) {\n return messages.map((message, position) => position === index\n ? { ...message, text: message.text + delta, ...(textCursor ? { textCursor } : {}) }\n : message);\n }\n return [...closeOpen(messages), { key: newKey(), role, text: delta, at, ...(textCursor ? { textCursor } : {}) }];\n}\n\n/** Incoming messages leave the running output block open. */\nexport function applyEvent(messages: Message[], event: ChatEvent): Message[] {\n switch (event.kind) {\n case \"reset\":\n return [];\n case \"user\":\n return [...messages, { key: newKey(), role: \"user\", text: event.text, closed: true, at: event.at,\n ...(event.inputId ? { inputId: event.inputId } : {}),\n ...(event.attachments?.length ? { attachments: event.attachments } : {}) }];\n case \"steered\":\n return messages.map((message) => message.inputId === event.inputId ? { ...message, steered: true } : message);\n case \"text\":\n return appendDelta(messages, \"assistant\", event.delta, event.at, event.cursor);\n case \"thinking\":\n return appendDelta(messages, \"thinking\", event.delta, event.at);\n case \"tool\":\n return [\n ...closeOpen(messages),\n {\n key: newKey(),\n role: \"tool\",\n text: event.label ?? event.name,\n closed: true,\n tool: { id: event.id, name: event.name, arguments: event.arguments },\n at: event.at,\n },\n ];\n case \"tool-result\": {\n const index = messages.map((message) => message.tool?.id).lastIndexOf(event.id);\n return messages.map((message, position) =>\n position === index && message.tool\n ? { ...message, tool: { ...message.tool, result: event.result, isError: event.isError } }\n : message,\n );\n }\n case \"action\":\n return [\n ...closeOpen(messages),\n {\n key: newKey(),\n role: \"action\",\n text: event.text,\n closed: true,\n action: { actionId: event.actionId, owner: event.owner, payload: event.payload },\n at: event.at,\n },\n ];\n case \"action-resolved\":\n return messages.map((message) =>\n message.action?.actionId === event.actionId\n ? { ...message, action: { ...message.action, status: event.status, result: event.result } }\n : message,\n );\n case \"system\":\n return [...closeOpen(messages), { key: newKey(), role: \"system\", text: event.text, closed: true, at: event.at }];\n case \"turn-done\":\n return closeOpen(messages);\n default:\n return messages;\n }\n}\n\nexport function prettyJson(value: string): string {\n try {\n return JSON.stringify(JSON.parse(value), null, 2);\n } catch {\n return value;\n }\n}\n\nexport function compactToolLine(tool: ToolInfo): string {\n const args = tool.arguments.replace(/\\s+/g, \" \").trim();\n const line = args && args !== \"{}\" ? `${tool.name} ${args}` : tool.name;\n return line.length > 160 ? `${line.slice(0, 160)} ...` : line;\n}\n\nexport { MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES, MAX_CHAT_REQUEST_BYTES, chatAttachmentMediaType, parseChatAttachments } from \"./chat-attachments\";\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/elapsed.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type ChatTexts } from "./texts";
2
+ /** Under a minute "{seconds} s" from texts, then m:ss, from an hour on h:mm:ss. */
3
+ export declare function formatElapsed(ms: number, texts: Pick<ChatTexts, "toolElapsedSeconds">): string;
4
+ /** One interval for every subscriber; it runs only while someone listens. */
5
+ export declare function createTicker(intervalMs?: number): {
6
+ subscribe(listener: () => void): () => void;
7
+ now(): number;
8
+ active: () => boolean;
9
+ };
10
+ export declare const elapsedTicker: {
11
+ subscribe(listener: () => void): () => void;
12
+ now(): number;
13
+ active: () => boolean;
14
+ };
15
+ /** Milliseconds since start in whole seconds, so a row renders only when its display changes; undefined on the server. */
16
+ export declare function useElapsed(start: number): number | undefined;
package/events.d.ts CHANGED
@@ -33,8 +33,8 @@ export interface ChatJournalCursor {
33
33
  sequence: number;
34
34
  }
35
35
  /**
36
- * Eine Aktion, die auf eine Eingabe des Benutzers wartet. Form und Inhalt von `payload` und
37
- * `result` gehören dem Plugin in `owner`; `status` gesetzt heißt: erledigt, die Karte ist Beleg.
36
+ * An action waiting for input from the user. Shape and content of `payload` and `result`
37
+ * belong to the plugin in `owner`; a set `status` means: done, the card is a record.
38
38
  */
39
39
  export interface PendingAction {
40
40
  actionId: string;
@@ -46,7 +46,7 @@ export interface PendingAction {
46
46
  export interface Message {
47
47
  key: string;
48
48
  role: Role;
49
- /** Absenderkennung für die Darstellung; unabhängig von Rolle und Sprechblasenlabel. */
49
+ /** Sender identifier for display; independent of role and bubble label. */
50
50
  sender?: string;
51
51
  text: string;
52
52
  textCursor?: ChatTextCursor;
@@ -54,13 +54,13 @@ export interface Message {
54
54
  attachments?: ChatAttachment[];
55
55
  tool?: ToolInfo;
56
56
  action?: PendingAction;
57
- /** ISO-Zeitpunkt der Nachricht; Anzeige optional (ChatMessages showTimestamps). */
57
+ /** ISO timestamp of the message; display optional (ChatMessages showTimestamps). */
58
58
  at?: string;
59
- /** Der ActorInput hinter einer eingehenden Nachricht; an ihm hängt die Steering-Markierung. */
59
+ /** The ActorInput behind an incoming message; the steering mark hangs on it. */
60
60
  inputId?: string;
61
- /** Die Nachricht ist in einen schon laufenden Turn eingespeist worden, statt einen eigenen zu beginnen. */
61
+ /** The message was fed into an already running turn instead of starting its own. */
62
62
  steered?: boolean;
63
- /** Farbige Sprechblase statt Fliesstext, z.B. fuer Mehrparteien-Gespraeche. */
63
+ /** Colored speech bubble instead of running text, e.g. for multi-party conversations. */
64
64
  bubble?: {
65
65
  color: string;
66
66
  side: "start" | "end";
@@ -138,7 +138,7 @@ export type ChatEvent = {
138
138
  at?: string;
139
139
  journal?: ChatJournalCursor;
140
140
  };
141
- /** Eingehende Nachrichten lassen den laufenden Ausgabeblock offen. */
141
+ /** Incoming messages leave the running output block open. */
142
142
  export declare function applyEvent(messages: Message[], event: ChatEvent): Message[];
143
143
  export declare function prettyJson(value: string): string;
144
144
  export declare function compactToolLine(tool: ToolInfo): string;
package/events.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  compactToolLine,
8
8
  parseChatAttachments,
9
9
  prettyJson
10
- } from "./chunk-IGCFQJZB.js";
10
+ } from "./chunk-3VEL4HEO.js";
11
11
  export {
12
12
  MAX_CHAT_ATTACHMENTS,
13
13
  MAX_CHAT_ATTACHMENT_BYTES,
package/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { useChatAction, copyChatText } from "./useChatAction";
17
17
  export { createChatScroll } from "./chat-scroll";
18
18
  export { ChatSendContext, createChatSendScope, type ChatSendScope } from "./ChatSendContext";
19
19
  export { messageDate, timestampDay, timestampLabel } from "./timestamps";
20
+ export { formatElapsed, useElapsed } from "./elapsed";
20
21
  export { attachmentCapabilityError, attachmentDownloadUrl, attachmentMediaType, encodeAttachment, formatAttachmentSize, validateAttachmentSelection, MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES, } from "./attachments";
21
22
  export type { ChatAppearance, TimestampOptions, CodeBlockOptions, BubbleOptions, MessageAction, MessageActionsOptions, SendShortcut, } from "./options";
22
23
  export { appearanceStyle } from "./options";