@datum-cloud/datum-ui 3.5.0 → 3.7.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.
@@ -5,12 +5,12 @@ import { t as Dialog } from "../dialog-Gu70fban.mjs";
5
5
  import { a as DropdownMenuRadioGroup, c as DropdownMenuTrigger, i as DropdownMenuLabel, o as DropdownMenuRadioItem, r as DropdownMenuContent, s as DropdownMenuSeparator, t as DropdownMenu } from "../dropdown-menu-b8zY5Cs6.mjs";
6
6
  import { t as Tooltip } from "../tooltip-DhbN1BDK.mjs";
7
7
  import { n as EASE, t as DURATION } from "../motion-DvGRWdsm.mjs";
8
- import { Archive, ArchiveRestore, ArrowDown, ArrowRight, ArrowUp, Brain, Check, ChevronDown, Copy, Download, MessagesSquare, Mic, MicOff, PanelLeft, Plus, RotateCw, Square, SquarePen, Trash2, X } from "lucide-react";
8
+ import { Archive, ArchiveRestore, ArrowDown, ArrowRight, ArrowUp, Box, Brain, Check, ChevronDown, ClipboardList, Clock, Code, Copy, Database, Download, Globe, Laptop, Link, MapPin, MessagesSquare, Mic, MicOff, Network, PanelLeft, Plus, RotateCw, Send, Server, Shield, Split, Square, SquarePen, Trash2, X } from "lucide-react";
9
9
  import { Fragment, createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
10
- import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
11
  import { AnimatePresence, motion } from "motion/react";
12
12
  import { EditorContent } from "@tiptap/react";
13
- import { getToolName, isReasoningUIPart, isTextUIPart, isToolUIPart } from "ai";
13
+ import { DEFAULT_ASSISTANT_CONFIG as DEFAULT_ASSISTANT_CONFIG$1, activeToolPart, assistantErrorMessage, chatFileName, chatToMarkdown, deriveTurns, formatRelativeTime, hasText, isReasoningUIPart, isStreamingMessage, isTextUIPart, isToolUIPart, messageText, showInitialLoading, showToolIndicator, thinkingLabel, toolLabel } from "@datum-cloud/datum-ui-assistant-core";
14
14
  import { code } from "@streamdown/code";
15
15
  import { Streamdown } from "streamdown";
16
16
  import "streamdown/styles.css";
@@ -40,11 +40,7 @@ function defaultRenderLink({ href, children }) {
40
40
  * config. Host apps override via `<AssistantConfigProvider config={…}>`.
41
41
  */
42
42
  const DEFAULT_ASSISTANT_CONFIG = {
43
- greeting: (name) => `Hey there${name ? `, ${name}` : ""}`,
44
- suggestions: [],
45
- showReasoning: true,
46
- modelSelector: false,
47
- toolLabels: {},
43
+ ...DEFAULT_ASSISTANT_CONFIG$1,
48
44
  renderLink: defaultRenderLink
49
45
  };
50
46
  const AssistantConfigContext = createContext(DEFAULT_ASSISTANT_CONFIG);
@@ -147,15 +143,17 @@ function ModelSelector({ modelId, effortId, onModelChange, onEffortChange, disab
147
143
  }
148
144
  //#endregion
149
145
  //#region src/components/features/assistant/components/composer/prompt-card.tsx
150
- function PromptCard({ editor, isReady, canRetry, onSend, onStop, onRetry, modelId, effortId, onModelChange, onEffortChange, speechSupported, isListening, frequencyData, onMicToggle, className }) {
146
+ function PromptCard({ editor, isReady, canRetry, onSend, onStop, onRetry, modelId, effortId, onModelChange, onEffortChange, speechSupported, isListening, frequencyData, onMicToggle, density = "default", className }) {
151
147
  const { modelSelector } = useAssistantConfig();
148
+ const compact = density === "compact";
149
+ const sendClasses = compact ? "text-muted-foreground hover:text-foreground hover:bg-accent shrink-0 rounded-full p-1.5 transition-colors" : "bg-primary text-primary-foreground hover:bg-primary/90 shrink-0 rounded-full p-1.5 transition-colors";
152
150
  return /* @__PURE__ */ jsxs("div", {
153
- className: cn("bg-card border-primary/30 rounded-2xl border p-3 shadow-sm transition-shadow", className),
151
+ className: cn(compact ? "bg-card border-input flex items-end gap-2 rounded-lg border py-1 pr-1.5 pl-3" : "bg-card border-primary/30 rounded-2xl border p-3 shadow-sm transition-shadow", className),
154
152
  children: [/* @__PURE__ */ jsx(EditorContent, {
155
153
  editor,
156
- className: "max-h-52 min-h-9 overflow-y-auto px-1 py-1"
154
+ className: cn("max-h-52 overflow-y-auto", compact ? "min-w-0 flex-1 [&_.ProseMirror]:py-0.5" : "min-h-9 px-1 py-1")
157
155
  }), /* @__PURE__ */ jsx("div", {
158
- className: "mt-1 flex items-center justify-end gap-2",
156
+ className: cn("flex items-center justify-end gap-2", compact ? "pb-0.5" : "mt-1"),
159
157
  children: /* @__PURE__ */ jsxs("div", {
160
158
  className: "flex items-center gap-1",
161
159
  children: [
@@ -223,9 +221,9 @@ function PromptCard({ editor, isReady, canRetry, onSend, onStop, onRetry, modelI
223
221
  type: "button",
224
222
  onClick: onSend,
225
223
  "aria-label": "Send message",
226
- className: "bg-primary text-primary-foreground hover:bg-primary/90 shrink-0 rounded-full p-1.5 transition-colors",
224
+ className: sendClasses,
227
225
  children: /* @__PURE__ */ jsx(Icon, {
228
- icon: ArrowUp,
226
+ icon: compact ? Send : ArrowUp,
229
227
  className: "size-4"
230
228
  })
231
229
  })
@@ -236,7 +234,7 @@ function PromptCard({ editor, isReady, canRetry, onSend, onStop, onRetry, modelI
236
234
  type: "button",
237
235
  onClick: onStop,
238
236
  "aria-label": "Stop generating",
239
- className: "bg-primary text-primary-foreground hover:bg-primary/90 shrink-0 rounded-full p-1.5 transition-colors",
237
+ className: sendClasses,
240
238
  children: /* @__PURE__ */ jsx(Icon, {
241
239
  icon: Square,
242
240
  className: "size-4 fill-current"
@@ -260,10 +258,7 @@ function useTurnRail(messages, containerRef, userScrolledUpRef) {
260
258
  const [activeTurn, setActiveTurn] = useState(null);
261
259
  const scrollElRef = useRef(null);
262
260
  const programmaticUntilRef = useRef(0);
263
- const turns = useMemo(() => messages.filter((m) => m.role === "user").map((m) => ({
264
- id: m.id,
265
- text: m.parts.find(isTextUIPart)?.text ?? ""
266
- })), [messages]);
261
+ const turns = useMemo(() => deriveTurns(messages), [messages]);
267
262
  const activeTurnId = activeTurn ?? turns.at(-1)?.id ?? null;
268
263
  const setScrollEl = useCallback((node) => {
269
264
  scrollElRef.current = node;
@@ -315,20 +310,11 @@ function useTurnRail(messages, containerRef, userScrolledUpRef) {
315
310
  };
316
311
  }
317
312
  //#endregion
318
- //#region src/components/features/assistant/utils/format-relative-time.ts
319
- /** Human-friendly relative timestamp for the chat history list. */
320
- function formatRelativeTime(timestamp) {
321
- const seconds = Math.floor((Date.now() - timestamp) / 1e3);
322
- if (seconds < 60) return "just now";
323
- const minutes = Math.floor(seconds / 60);
324
- if (minutes < 60) return `${minutes}m ago`;
325
- const hours = Math.floor(minutes / 60);
326
- if (hours < 24) return `${hours}h ago`;
327
- const days = Math.floor(hours / 24);
328
- if (days === 1) return "Yesterday";
329
- if (days < 7) return `${days}d ago`;
330
- return new Date(timestamp).toLocaleDateString();
331
- }
313
+ //#region src/components/features/assistant/types.ts
314
+ /** The message part type that carries a pick-one question (see {@link AssistantChoicesData}). */
315
+ const CHOICES_PART_TYPE = "data-choices";
316
+ /** The message part type that carries a plan the person can approve (see {@link AssistantPlanData}). */
317
+ const PLAN_PART_TYPE = "data-plan";
332
318
  //#endregion
333
319
  //#region src/components/features/assistant/utils/sanitize-html.ts
334
320
  const HTML_ESCAPE_MAP = {
@@ -365,6 +351,59 @@ function sanitizeUserHtml(raw) {
365
351
  return result.startsWith("<p>") ? result : `<p>${result}</p>`;
366
352
  }
367
353
  //#endregion
354
+ //#region src/components/features/assistant/components/message/choice-chips.tsx
355
+ const CHOICE_ICONS = {
356
+ clock: Clock,
357
+ container: Box,
358
+ database: Database,
359
+ domain: Link,
360
+ globe: Globe,
361
+ laptop: Laptop,
362
+ location: MapPin,
363
+ network: Network,
364
+ server: Server,
365
+ shield: Shield
366
+ };
367
+ /**
368
+ * A pick-one question as a row of buttons. Once an option is picked it stays
369
+ * marked with a check and the others fade, so a scrolled-back conversation
370
+ * still shows what was chosen.
371
+ */
372
+ function AssistantChoiceChips({ data, onChoose, disabled = false }) {
373
+ const answered = data.selectedId !== void 0;
374
+ const interactive = !answered && !disabled && onChoose !== void 0;
375
+ return /* @__PURE__ */ jsxs("div", {
376
+ className: "my-1 space-y-2",
377
+ "data-slot": "assistant-choices",
378
+ children: [/* @__PURE__ */ jsx("p", {
379
+ className: "font-medium",
380
+ children: data.question
381
+ }), /* @__PURE__ */ jsx("div", {
382
+ role: "group",
383
+ "aria-label": data.question,
384
+ className: "flex flex-wrap gap-2",
385
+ children: data.options.map((option) => {
386
+ const selected = option.id === data.selectedId;
387
+ const icon = option.icon ? CHOICE_ICONS[option.icon] : void 0;
388
+ return /* @__PURE__ */ jsxs("button", {
389
+ type: "button",
390
+ disabled: !interactive,
391
+ "aria-pressed": answered ? selected : void 0,
392
+ onClick: () => onChoose?.(option),
393
+ className: cn("inline-flex items-center gap-1.5 rounded-full border px-3.5 py-1.5 text-sm transition-colors", selected && "border-btn-primary text-btn-primary bg-btn-primary/5", !selected && answered && "text-muted-foreground/60 border-border/60", !selected && !answered && "text-foreground border-border", interactive ? "hover:bg-accent cursor-pointer" : "cursor-default", !answered && !interactive && "opacity-60"),
394
+ children: [selected ? /* @__PURE__ */ jsx(Icon, {
395
+ icon: Check,
396
+ className: "size-3.5 shrink-0"
397
+ }) : icon && /* @__PURE__ */ jsx(Icon, {
398
+ icon,
399
+ className: "size-3.5 shrink-0"
400
+ }), option.label]
401
+ }, option.id);
402
+ })
403
+ })]
404
+ });
405
+ }
406
+ //#endregion
368
407
  //#region src/components/features/assistant/components/message/loading-dots.tsx
369
408
  /** Three bouncing dots — the assistant's "thinking" indicator. */
370
409
  function LoadingDots({ className }) {
@@ -378,6 +417,238 @@ function LoadingDots({ className }) {
378
417
  });
379
418
  }
380
419
  //#endregion
420
+ //#region src/components/features/assistant/components/message/plan-card.tsx
421
+ /** How a few common kinds read to someone who doesn't know their API names. */
422
+ const KIND_LABELS = {
423
+ HTTPProxy: "Load balancer",
424
+ NetworkService: "Network service",
425
+ Workload: "Workload",
426
+ Domain: "Domain",
427
+ Network: "Network"
428
+ };
429
+ const KIND_ICONS = {
430
+ HTTPProxy: Split,
431
+ NetworkService: Network,
432
+ Workload: Server,
433
+ Domain: Globe,
434
+ Network
435
+ };
436
+ const BADGES = {
437
+ pending: {
438
+ label: "Nothing created yet",
439
+ className: "bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300"
440
+ },
441
+ applying: {
442
+ label: "Applying",
443
+ className: "bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300"
444
+ },
445
+ applied: {
446
+ label: "Applied",
447
+ className: "bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"
448
+ },
449
+ failed: {
450
+ label: "Not applied",
451
+ className: "bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300"
452
+ },
453
+ expired: {
454
+ label: "Expired",
455
+ className: "bg-muted text-muted-foreground"
456
+ },
457
+ replaced: {
458
+ label: "Replaced",
459
+ className: "bg-muted text-muted-foreground"
460
+ }
461
+ };
462
+ function cardState(data, latest, now) {
463
+ if (data.status !== "pending") return data.status;
464
+ if (!latest) return "replaced";
465
+ const expires = Date.parse(data.expiresAt);
466
+ if (!Number.isNaN(expires) && expires <= now) return "expired";
467
+ return "pending";
468
+ }
469
+ /** "3 resources to create", "1 resource to change", "2 resources to create or change". */
470
+ function headline(resources) {
471
+ const creates = resources.some((r) => r.action === "create");
472
+ const updates = resources.some((r) => r.action === "update");
473
+ const verb = creates && updates ? "create or change" : updates ? "change" : "create";
474
+ return `${resources.length} ${resources.length === 1 ? "resource" : "resources"} to ${verb}`;
475
+ }
476
+ function approveLabel(resources) {
477
+ const updates = resources.some((r) => r.action === "update");
478
+ const creates = resources.some((r) => r.action === "create");
479
+ return creates && !updates ? "Approve and create" : updates && !creates ? "Approve and update" : "Approve and apply";
480
+ }
481
+ function timeOf(iso) {
482
+ if (!iso) return void 0;
483
+ const t = new Date(iso);
484
+ return Number.isNaN(t.getTime()) ? void 0 : t.toLocaleTimeString([], {
485
+ hour: "2-digit",
486
+ minute: "2-digit"
487
+ });
488
+ }
489
+ /**
490
+ * A plan the assistant settled, as a card: one row per resource with what it
491
+ * does, the YAML and differences on request, and the person's two answers.
492
+ * Nothing is created until they click Approve, and the card then shows how it
493
+ * went.
494
+ */
495
+ function AssistantPlanCard({ data, latest = true, canApprove = false, onApprove, onChange, now = Date.now() }) {
496
+ const [showYaml, setShowYaml] = useState(false);
497
+ const state = cardState(data, latest, now);
498
+ const badge = BADGES[state];
499
+ const approvedAt = timeOf(data.approvedAt);
500
+ const appliedNames = new Set((data.applied ?? []).map((a) => `${a.kind}/${a.name}`));
501
+ return /* @__PURE__ */ jsxs("section", {
502
+ "aria-label": `Plan: ${headline(data.resources)}`,
503
+ "data-slot": "assistant-plan",
504
+ "data-state": state,
505
+ className: "bg-card my-2 overflow-hidden rounded-xl border",
506
+ children: [
507
+ /* @__PURE__ */ jsxs("header", {
508
+ className: "bg-muted/40 flex items-center gap-2 border-b px-4 py-3",
509
+ children: [
510
+ /* @__PURE__ */ jsx(Icon, {
511
+ icon: ClipboardList,
512
+ className: "size-4 shrink-0"
513
+ }),
514
+ /* @__PURE__ */ jsxs("span", {
515
+ className: "flex-1 font-medium",
516
+ children: [
517
+ "Plan ·",
518
+ " ",
519
+ headline(data.resources)
520
+ ]
521
+ }),
522
+ /* @__PURE__ */ jsx("span", {
523
+ className: cn("rounded-full px-2.5 py-0.5 text-xs", badge.className),
524
+ children: badge.label
525
+ })
526
+ ]
527
+ }),
528
+ /* @__PURE__ */ jsx("ul", {
529
+ className: "divide-y",
530
+ children: data.resources.map((r) => {
531
+ const kindIcon = KIND_ICONS[r.kind] ?? Box;
532
+ const done = state === "applied" && appliedNames.has(`${r.kind}/${r.name}`);
533
+ return /* @__PURE__ */ jsxs("li", {
534
+ className: "flex gap-3 px-4 py-3",
535
+ children: [
536
+ /* @__PURE__ */ jsx("span", {
537
+ className: "bg-btn-primary/10 text-btn-primary flex size-8 shrink-0 items-center justify-center rounded-md",
538
+ children: /* @__PURE__ */ jsx(Icon, {
539
+ icon: kindIcon,
540
+ className: "size-4"
541
+ })
542
+ }),
543
+ /* @__PURE__ */ jsxs("div", {
544
+ className: "min-w-0 flex-1",
545
+ children: [
546
+ /* @__PURE__ */ jsxs("p", { children: [
547
+ /* @__PURE__ */ jsx("span", {
548
+ className: "font-medium",
549
+ children: KIND_LABELS[r.kind] ?? r.kind
550
+ }),
551
+ " ",
552
+ /* @__PURE__ */ jsx("span", {
553
+ className: "text-muted-foreground font-mono text-[0.9em]",
554
+ children: r.name
555
+ })
556
+ ] }),
557
+ r.summary && /* @__PURE__ */ jsx("p", {
558
+ className: "text-muted-foreground",
559
+ children: r.summary
560
+ }),
561
+ r.diff && r.diff.length > 0 && /* @__PURE__ */ jsx("div", {
562
+ className: "mt-1 flex flex-wrap gap-1",
563
+ children: r.diff.map((line) => /* @__PURE__ */ jsx("code", {
564
+ className: "rounded bg-emerald-50 px-1.5 py-0.5 font-mono text-xs text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200",
565
+ children: line
566
+ }, line))
567
+ })
568
+ ]
569
+ }),
570
+ /* @__PURE__ */ jsx("span", {
571
+ className: cn("h-fit shrink-0 rounded-full px-2.5 py-0.5 text-xs", r.action === "update" ? "bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300" : "bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"),
572
+ children: done ? r.action === "update" ? "Updated" : "Created" : r.action === "update" ? "Update" : "Create"
573
+ })
574
+ ]
575
+ }, `${r.kind}/${r.namespace ?? ""}/${r.name}`);
576
+ })
577
+ }),
578
+ showYaml && /* @__PURE__ */ jsx("div", {
579
+ className: "bg-muted/30 space-y-3 border-t px-4 py-3",
580
+ children: data.resources.map((r) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("p", {
581
+ className: "text-muted-foreground mb-1 text-xs",
582
+ children: [
583
+ r.kind,
584
+ " ",
585
+ r.name
586
+ ]
587
+ }), /* @__PURE__ */ jsx("pre", {
588
+ className: "bg-card overflow-x-auto rounded-md border p-2 font-mono text-xs",
589
+ children: r.manifest
590
+ })] }, `yaml-${r.kind}/${r.name}`))
591
+ }),
592
+ /* @__PURE__ */ jsxs("footer", {
593
+ className: "flex flex-wrap items-center gap-2 border-t px-4 py-3",
594
+ children: [
595
+ /* @__PURE__ */ jsxs("button", {
596
+ type: "button",
597
+ onClick: () => setShowYaml((v) => !v),
598
+ "aria-expanded": showYaml,
599
+ className: "text-muted-foreground hover:text-foreground mr-auto inline-flex items-center gap-1.5 text-sm transition-colors",
600
+ children: [/* @__PURE__ */ jsx(Icon, {
601
+ icon: Code,
602
+ className: "size-4"
603
+ }), showYaml ? "Hide YAML and diff" : "View YAML and diff"]
604
+ }),
605
+ state === "pending" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
606
+ type: "tertiary",
607
+ theme: "outline",
608
+ onClick: onChange,
609
+ disabled: !canApprove,
610
+ children: "Change something"
611
+ }), /* @__PURE__ */ jsx(Button, {
612
+ type: "primary",
613
+ theme: "solid",
614
+ icon: /* @__PURE__ */ jsx(Icon, {
615
+ icon: Check,
616
+ className: "size-4"
617
+ }),
618
+ onClick: onApprove,
619
+ disabled: !canApprove,
620
+ children: approveLabel(data.resources)
621
+ })] }),
622
+ state === "applying" && /* @__PURE__ */ jsx(Button, {
623
+ type: "primary",
624
+ theme: "solid",
625
+ loading: true,
626
+ disabled: true,
627
+ children: "Applying"
628
+ }),
629
+ (state === "applied" || state === "failed") && approvedAt && /* @__PURE__ */ jsxs("span", {
630
+ className: "text-muted-foreground text-sm",
631
+ children: [
632
+ "Approved by you at",
633
+ " ",
634
+ approvedAt
635
+ ]
636
+ }),
637
+ state === "expired" && /* @__PURE__ */ jsx("span", {
638
+ className: "text-muted-foreground text-sm",
639
+ children: "This plan expired. Ask for a new one to go ahead."
640
+ })
641
+ ]
642
+ }),
643
+ state === "failed" && data.error && /* @__PURE__ */ jsx("p", {
644
+ role: "alert",
645
+ className: "text-destructive border-t px-4 py-3 text-sm",
646
+ children: data.error
647
+ })
648
+ ]
649
+ });
650
+ }
651
+ //#endregion
381
652
  //#region src/components/features/assistant/components/message/thinking-block.tsx
382
653
  function ThinkingBlock({ text, isStreaming = false }) {
383
654
  const [open, setOpen] = useState(false);
@@ -385,7 +656,7 @@ function ThinkingBlock({ text, isStreaming = false }) {
385
656
  const duration = useRef(null);
386
657
  if (isStreaming && startedAt.current === null) startedAt.current = Date.now();
387
658
  if (!isStreaming && startedAt.current !== null && duration.current === null) duration.current = Math.round((Date.now() - startedAt.current) / 1e3);
388
- const label = isStreaming ? "Thinking…" : duration.current !== null ? duration.current < 1 ? "Thought for less than 1 second" : `Thought for ${duration.current}s` : "Thought";
659
+ const label = thinkingLabel(isStreaming, duration.current);
389
660
  return /* @__PURE__ */ jsxs("div", {
390
661
  className: "mb-3",
391
662
  children: [/* @__PURE__ */ jsxs("button", {
@@ -417,74 +688,127 @@ function ThinkingBlock({ text, isStreaming = false }) {
417
688
  }
418
689
  //#endregion
419
690
  //#region src/components/features/assistant/components/message/assistant-message.tsx
420
- function messageText(msg) {
421
- return msg.parts.filter(isTextUIPart).map((p) => p.text).join("\n\n");
422
- }
423
- function AssistantMessage({ msg, isLastMessage, status }) {
691
+ /** Streamdown overrides shared by both densities (code surfaces). */
692
+ const STREAMDOWN_BASE_CLASSES = "**:data-[streamdown='code-block-body']:bg-card **:data-[streamdown='inline-code']:bg-card dark:**:data-[streamdown='inline-code']:bg-accent **:data-[streamdown='inline-code']:border";
693
+ /**
694
+ * Markdown overrides for `compact`: tighter blocks, headings at body size, and
695
+ * inline code that reads as part of the sentence (a long image reference wraps
696
+ * anywhere instead of breaking the line around a bordered pill).
697
+ */
698
+ const COMPACT_MARKDOWN_CLASSES = cn("space-y-2 leading-relaxed", "[&_li]:py-0.5", "[&_:is(h1,h2,h3,h4,h5,h6)]:mt-3 [&_:is(h1,h2,h3,h4,h5,h6)]:mb-1", "[&_:is(h1,h2,h3,h4,h5,h6)]:text-sm [&_:is(h1,h2,h3,h4,h5,h6)]:font-semibold", "**:data-[streamdown='code-block-body']:bg-card", "**:data-[streamdown='inline-code']:bg-transparent **:data-[streamdown='inline-code']:p-0", "**:data-[streamdown='inline-code']:text-[0.95em] **:data-[streamdown='inline-code']:[overflow-wrap:anywhere]");
699
+ function AssistantMessage({ msg, isLastMessage, status, density = "default", onChoose, canChoose = false, onApprovePlan, onChangePlan, latestPlanId, canApprovePlan = false }) {
424
700
  const { toolLabels, showReasoning, messageActions, renderLink, renderToolOutput } = useAssistantConfig();
425
- const isStreaming = isLastMessage && status === "streaming";
426
- const hasText = msg.parts.some((p) => isTextUIPart(p) && p.text);
427
- const activeToolPart = msg.parts.find((p) => isToolUIPart(p) && (p.state === "input-streaming" || p.state === "input-available"));
428
- const showInitialLoading = isStreaming && !hasText && !activeToolPart;
429
- const showToolIndicator = isLastMessage && activeToolPart != null;
430
- return /* @__PURE__ */ jsxs("div", {
431
- className: "text-foreground w-full text-sm",
432
- children: [
433
- msg.parts.map((part, i) => {
434
- if (isReasoningUIPart(part)) {
435
- if (!showReasoning) return null;
436
- const isThinkingStreaming = isStreaming && i === msg.parts.length - 1;
437
- return /* @__PURE__ */ jsx(ThinkingBlock, {
438
- text: part.text,
439
- isStreaming: isThinkingStreaming
440
- }, i);
441
- }
442
- if (isTextUIPart(part) && part.text) return /* @__PURE__ */ jsx(Streamdown, {
443
- className: cn("**:data-[streamdown='code-block-body']:bg-card **:data-[streamdown='inline-code']:bg-card dark:**:data-[streamdown='inline-code']:bg-accent **:data-[streamdown='inline-code']:border [&_h2]:text-xl [&_h3]:text-lg", isStreaming && "**:data-[streamdown='inline-code']:animate-[sd-blurIn_300ms_ease-out_both]"),
444
- isAnimating: isStreaming,
445
- plugins: { code },
446
- animated: {
447
- animation: "blurIn",
448
- duration: 300,
449
- easing: "ease-out",
450
- sep: "word",
451
- stagger: 30
452
- },
453
- components: {
454
- a: ({ href, children }) => renderLink({
455
- href,
456
- children
457
- }),
458
- img: ({ src, alt }) => /* @__PURE__ */ jsx("img", {
459
- src,
460
- alt: alt ?? "",
461
- className: "my-2 inline-block max-h-16 rounded-full",
462
- referrerPolicy: "no-referrer"
463
- })
464
- },
465
- children: part.text
701
+ const isStreaming = isStreamingMessage({
702
+ isLastMessage,
703
+ status
704
+ });
705
+ const hasText$1 = hasText(msg);
706
+ const activeToolPart$1 = activeToolPart(msg);
707
+ const showInitialLoading$1 = showInitialLoading({
708
+ msg,
709
+ isLastMessage,
710
+ status
711
+ });
712
+ const showToolIndicator$1 = showToolIndicator({
713
+ msg,
714
+ isLastMessage
715
+ });
716
+ const compact = density === "compact";
717
+ const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
718
+ msg.parts.map((part, i) => {
719
+ if (isReasoningUIPart(part)) {
720
+ if (!showReasoning) return null;
721
+ const isThinkingStreaming = isStreaming && i === msg.parts.length - 1;
722
+ return /* @__PURE__ */ jsx(ThinkingBlock, {
723
+ text: part.text,
724
+ isStreaming: isThinkingStreaming
466
725
  }, i);
467
- if (isToolUIPart(part) && part.state === "output-available" && renderToolOutput) {
468
- const rendered = renderToolOutput(part, msg);
469
- if (rendered) return /* @__PURE__ */ jsx(Fragment, { children: rendered }, i);
470
- }
471
- return null;
472
- }),
473
- showInitialLoading && /* @__PURE__ */ jsx("div", {
474
- className: "py-1",
475
- children: /* @__PURE__ */ jsx(LoadingDots, {})
476
- }),
477
- showToolIndicator && /* @__PURE__ */ jsxs("div", {
478
- className: "text-muted-foreground flex items-center gap-1.5 py-1 text-xs",
479
- children: [
480
- /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full [animation-delay:-0.3s]" }),
481
- /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full [animation-delay:-0.15s]" }),
482
- /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full" }),
483
- /* @__PURE__ */ jsx("span", { children: toolLabels[getToolName(activeToolPart)] ?? "Working…" })
484
- ]
485
- }),
486
- !isStreaming && hasText && (messageActions ? messageActions(msg) : /* @__PURE__ */ jsx(MessageActions, { msg }))
487
- ]
726
+ }
727
+ if (isTextUIPart(part) && part.text) return /* @__PURE__ */ jsx(Streamdown, {
728
+ className: cn(compact ? COMPACT_MARKDOWN_CLASSES : `${STREAMDOWN_BASE_CLASSES} [&_h2]:text-xl [&_h3]:text-lg`, isStreaming && "**:data-[streamdown='inline-code']:animate-[sd-blurIn_300ms_ease-out_both]"),
729
+ isAnimating: isStreaming,
730
+ plugins: { code },
731
+ animated: {
732
+ animation: "blurIn",
733
+ duration: 300,
734
+ easing: "ease-out",
735
+ sep: "word",
736
+ stagger: 30
737
+ },
738
+ components: {
739
+ a: ({ href, children }) => renderLink({
740
+ href,
741
+ children
742
+ }),
743
+ img: ({ src, alt }) => /* @__PURE__ */ jsx("img", {
744
+ src,
745
+ alt: alt ?? "",
746
+ className: "my-2 inline-block max-h-16 rounded-full",
747
+ referrerPolicy: "no-referrer"
748
+ })
749
+ },
750
+ children: part.text
751
+ }, i);
752
+ if (part.type === "data-choices") return /* @__PURE__ */ jsx(AssistantChoiceChips, {
753
+ data: part.data,
754
+ onChoose,
755
+ disabled: !canChoose
756
+ }, i);
757
+ if (part.type === "data-plan") {
758
+ const { id, data } = part;
759
+ const latest = latestPlanId === void 0 || latestPlanId === id;
760
+ return /* @__PURE__ */ jsx(AssistantPlanCard, {
761
+ data,
762
+ latest,
763
+ canApprove: canApprovePlan && latest && onApprovePlan !== void 0,
764
+ onApprove: () => onApprovePlan?.(id),
765
+ onChange: () => onChangePlan?.(id)
766
+ }, i);
767
+ }
768
+ if (isToolUIPart(part) && part.state === "output-available" && renderToolOutput) {
769
+ const rendered = renderToolOutput(part, msg);
770
+ if (rendered) return /* @__PURE__ */ jsx(Fragment, { children: rendered }, i);
771
+ }
772
+ return null;
773
+ }),
774
+ showInitialLoading$1 && /* @__PURE__ */ jsx("div", {
775
+ className: "py-1",
776
+ children: /* @__PURE__ */ jsx(LoadingDots, {})
777
+ }),
778
+ showToolIndicator$1 && activeToolPart$1 && /* @__PURE__ */ jsxs("div", {
779
+ className: "text-muted-foreground flex items-center gap-1.5 py-1 text-xs",
780
+ children: [
781
+ /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full [animation-delay:-0.3s]" }),
782
+ /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full [animation-delay:-0.15s]" }),
783
+ /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/50 h-1.5 w-1.5 animate-bounce rounded-full" }),
784
+ /* @__PURE__ */ jsx("span", { children: toolLabel(activeToolPart$1, toolLabels) })
785
+ ]
786
+ }),
787
+ !isStreaming && hasText$1 && (messageActions ? messageActions(msg) : !compact && /* @__PURE__ */ jsx(MessageActions, { msg }))
788
+ ] });
789
+ if (!compact) return /* @__PURE__ */ jsx("div", {
790
+ className: "text-foreground w-full text-sm",
791
+ children: body
792
+ });
793
+ return /* @__PURE__ */ jsxs("div", {
794
+ className: "text-foreground flex w-full gap-3 text-sm",
795
+ "data-density": "compact",
796
+ children: [/* @__PURE__ */ jsx(AssistantAvatar, {}), /* @__PURE__ */ jsx("div", {
797
+ className: "min-w-0 flex-1 pt-1",
798
+ children: body
799
+ })]
800
+ });
801
+ }
802
+ /** The assistant's mark beside each compact turn. Decorative: the turn itself carries the content. */
803
+ function AssistantAvatar() {
804
+ return /* @__PURE__ */ jsx("span", {
805
+ "aria-hidden": "true",
806
+ "data-slot": "assistant-avatar",
807
+ className: "bg-btn-primary flex size-7 shrink-0 items-center justify-center rounded-full text-white",
808
+ children: /* @__PURE__ */ jsx(Icon, {
809
+ icon: Brain,
810
+ className: "size-3.5"
811
+ })
488
812
  });
489
813
  }
490
814
  function MessageActions({ msg }) {
@@ -506,6 +830,7 @@ function MessageActions({ msg }) {
506
830
  URL.revokeObjectURL(url);
507
831
  };
508
832
  return /* @__PURE__ */ jsxs("div", {
833
+ "data-slot": "message-actions",
509
834
  className: "text-muted-foreground/60 mt-1.5 flex items-center gap-0.5",
510
835
  children: [/* @__PURE__ */ jsx(Tooltip, {
511
836
  message: copied ? "Copied" : "Copy",
@@ -542,11 +867,11 @@ function MessageActions({ msg }) {
542
867
  //#endregion
543
868
  //#region src/components/features/assistant/components/message/user-message.tsx
544
869
  /** A user's message — a right-aligned bubble rendering the sanitized input HTML. */
545
- function UserMessage({ html }) {
870
+ function UserMessage({ html, density = "default" }) {
546
871
  return /* @__PURE__ */ jsx("div", {
547
872
  className: "flex justify-end",
548
873
  children: /* @__PURE__ */ jsx("div", {
549
- className: "bg-border text-foreground max-w-[80%] rounded-2xl rounded-br-sm px-4 py-2.5 text-sm",
874
+ className: cn("text-foreground max-w-[80%] rounded-2xl px-4 py-2.5 text-sm", density === "compact" ? "bg-muted" : "bg-border rounded-br-sm"),
550
875
  children: /* @__PURE__ */ jsx("div", {
551
876
  className: "[&_em]:italic [&_p]:my-0.5 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_s]:line-through [&_strong]:font-semibold [&_u]:underline",
552
877
  dangerouslySetInnerHTML: { __html: html }
@@ -614,10 +939,12 @@ function TurnRail({ turns, activeId, onJump }) {
614
939
  }
615
940
  //#endregion
616
941
  //#region src/components/features/assistant/components/conversation/conversation.tsx
617
- function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRef, bottomRef, userScrolledUpRef, footer }) {
942
+ function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRef, bottomRef, userScrolledUpRef, footer, density = "default", onChoose, onApprovePlan, onChangePlan }) {
943
+ const compact = density === "compact";
618
944
  const [showScrollButton, setShowScrollButton] = useState(false);
619
945
  const { turns, activeTurnId, setScrollEl, handleScroll, scrollToTurn } = useTurnRail(messages, containerRef, userScrolledUpRef);
620
946
  let userMsgIdx = 0;
947
+ const latestPlanId = newestPlanId(messages);
621
948
  return /* @__PURE__ */ jsxs("div", {
622
949
  className: "relative flex min-h-0 flex-1 flex-col",
623
950
  children: [
@@ -626,7 +953,7 @@ function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRe
626
953
  onScroll: (e) => setShowScrollButton(handleScroll(e.currentTarget)),
627
954
  className: "flex-1 overflow-y-auto scroll-smooth",
628
955
  children: /* @__PURE__ */ jsxs("div", {
629
- className: "mx-auto flex w-full max-w-3xl flex-col gap-6 px-4 py-6",
956
+ className: compact ? "flex w-full flex-col gap-5 px-7 py-4" : "mx-auto flex w-full max-w-3xl flex-col gap-6 px-4 py-6",
630
957
  children: [
631
958
  messages.map((msg, msgIdx) => {
632
959
  const isLastMessage = msgIdx === messages.length - 1;
@@ -648,7 +975,10 @@ function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRe
648
975
  stiffness: 500,
649
976
  damping: 30
650
977
  },
651
- children: /* @__PURE__ */ jsx(UserMessage, { html: html ?? sanitizeUserHtml(fallbackText) })
978
+ children: /* @__PURE__ */ jsx(UserMessage, {
979
+ html: html ?? sanitizeUserHtml(fallbackText),
980
+ density
981
+ })
652
982
  }, msg.id);
653
983
  }
654
984
  return /* @__PURE__ */ jsx(motion.div, {
@@ -668,7 +998,14 @@ function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRe
668
998
  children: /* @__PURE__ */ jsx(AssistantMessage, {
669
999
  msg,
670
1000
  isLastMessage,
671
- status
1001
+ status,
1002
+ density,
1003
+ onChoose: onChoose && ((option) => onChoose(msg.id, option)),
1004
+ canChoose: isLastMessage && status === "ready",
1005
+ onApprovePlan: onApprovePlan && ((planId) => onApprovePlan(msg.id, planId)),
1006
+ onChangePlan: onChangePlan && ((planId) => onChangePlan(msg.id, planId)),
1007
+ latestPlanId,
1008
+ canApprovePlan: status === "ready"
672
1009
  })
673
1010
  }, msg.id);
674
1011
  }),
@@ -690,8 +1027,8 @@ function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRe
690
1027
  stiffness: 500,
691
1028
  damping: 30
692
1029
  },
693
- className: "text-destructive bg-destructive/10 w-fit rounded-lg px-3 py-2 text-sm",
694
- children: error.message.includes("429") || error.message.includes("Too Many") ? "Easy there, speed racer! You've hit the rate limit. Give it a minute and try again." : error.message.includes("503") || error.message.includes("not configured") ? "AI assistant is not configured." : `Something went wrong — ${error.message}`
1030
+ className: cn("text-destructive bg-destructive/10 w-fit rounded-lg px-3 py-2 text-sm", compact && "ml-10"),
1031
+ children: assistantErrorMessage(error)
695
1032
  }, "error") }),
696
1033
  /* @__PURE__ */ jsx(AnimatePresence, { children: status === "submitted" && /* @__PURE__ */ jsx(motion.div, {
697
1034
  initial: {
@@ -711,7 +1048,7 @@ function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRe
711
1048
  stiffness: 500,
712
1049
  damping: 30
713
1050
  },
714
- className: "flex justify-start",
1051
+ className: cn("flex justify-start", compact && "pl-10"),
715
1052
  children: /* @__PURE__ */ jsx(LoadingDots, {})
716
1053
  }, "loading") }),
717
1054
  /* @__PURE__ */ jsx("div", { ref: bottomRef })
@@ -724,7 +1061,7 @@ function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRe
724
1061
  onJump: scrollToTurn
725
1062
  }),
726
1063
  /* @__PURE__ */ jsxs("div", {
727
- className: "relative shrink-0 px-4 pb-4",
1064
+ className: cn("relative shrink-0", compact ? "px-7 pb-6" : "px-4 pb-4"),
728
1065
  children: [/* @__PURE__ */ jsx(ScrollToBottomButton, {
729
1066
  show: showScrollButton && messages.length > 0,
730
1067
  onClick: () => {
@@ -733,13 +1070,23 @@ function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRe
733
1070
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
734
1071
  }
735
1072
  }), /* @__PURE__ */ jsx("div", {
736
- className: "mx-auto w-full max-w-3xl",
1073
+ className: compact ? "w-full" : "mx-auto w-full max-w-3xl",
737
1074
  children: footer
738
1075
  })]
739
1076
  })
740
1077
  ]
741
1078
  });
742
1079
  }
1080
+ /** The id of the newest plan part in the conversation, if any. */
1081
+ function newestPlanId(messages) {
1082
+ for (let m = messages.length - 1; m >= 0; m--) {
1083
+ const parts = messages[m]?.parts ?? [];
1084
+ for (let p = parts.length - 1; p >= 0; p--) {
1085
+ const part = parts[p];
1086
+ if (part.type === "data-plan" && part.id) return part.id;
1087
+ }
1088
+ }
1089
+ }
743
1090
  //#endregion
744
1091
  //#region src/components/features/assistant/components/brain-glyph.tsx
745
1092
  /**
@@ -872,15 +1219,11 @@ function ChatRail({ historyOpen, onNewChat, onToggleHistory }) {
872
1219
  //#endregion
873
1220
  //#region src/components/features/assistant/components/sidebar/history-panel.tsx
874
1221
  function downloadChat(chat) {
875
- const lines = chat.messages.map((msg) => {
876
- return `## ${msg.role === "user" ? "You" : "Assistant"}\n\n${msg.parts.filter(isTextUIPart).map((p) => p.text).join("\n")}`;
877
- });
878
- const markdown = `# ${chat.title}\n\n${lines.join("\n\n---\n\n")}\n`;
879
- const blob = new Blob([markdown], { type: "text/markdown" });
1222
+ const blob = new Blob([chatToMarkdown(chat)], { type: "text/markdown" });
880
1223
  const url = URL.createObjectURL(blob);
881
1224
  const a = document.createElement("a");
882
1225
  a.href = url;
883
- a.download = `${chat.title.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}.md`;
1226
+ a.download = chatFileName(chat.title);
884
1227
  a.click();
885
1228
  URL.revokeObjectURL(url);
886
1229
  }
@@ -1068,7 +1411,7 @@ const PANEL_SPRING = {
1068
1411
  stiffness: 400,
1069
1412
  damping: 32
1070
1413
  };
1071
- function AssistantWorkspace({ orientation = "horizontal", onClose, config, userName, title, messages, status, error, isReady, chatList, currentChatId, sidebarHeader, editor, htmlByUserMsgIndex, bottomRef, containerRef, userScrolledUpRef, onSend, onStop, onRetry, onNewChat, onLoadChat, onDeleteChat, onArchiveChat, onUnarchiveChat, confirmDelete, onSuggestion, modelId, effortId, onModelChange, onEffortChange, micSupported, micListening, micFrequencyData, onMicToggle, historyOpen, onToggleHistory }) {
1414
+ function AssistantWorkspace({ orientation = "horizontal", onClose, config, userName, title, messages, status, error, isReady, chatList, currentChatId, sidebarHeader, editor, htmlByUserMsgIndex, bottomRef, containerRef, userScrolledUpRef, onSend, onStop, onRetry, onNewChat, onLoadChat, onDeleteChat, onArchiveChat, onUnarchiveChat, confirmDelete, onSuggestion, onChoose, onApprovePlan, onChangePlan, modelId, effortId, onModelChange, onEffortChange, micSupported, micListening, micFrequencyData, onMicToggle, historyOpen, onToggleHistory }) {
1072
1415
  const hasMessages = messages.length > 0;
1073
1416
  const isVertical = orientation === "vertical";
1074
1417
  const promptCard = /* @__PURE__ */ jsx(PromptCard, {
@@ -1137,7 +1480,10 @@ function AssistantWorkspace({ orientation = "horizontal", onClose, config, userN
1137
1480
  containerRef,
1138
1481
  bottomRef,
1139
1482
  userScrolledUpRef,
1140
- footer: promptCard
1483
+ footer: promptCard,
1484
+ onChoose,
1485
+ onApprovePlan,
1486
+ onChangePlan
1141
1487
  }) : /* @__PURE__ */ jsx(EmptyState, {
1142
1488
  name: userName,
1143
1489
  isReady,
@@ -1245,4 +1591,4 @@ function AssistantWorkspace({ orientation = "horizontal", onClose, config, userN
1245
1591
  });
1246
1592
  }
1247
1593
  //#endregion
1248
- export { AssistantConfigProvider, AssistantMessage, AssistantWorkspace, BrainGlyph, ChatRail, Conversation, DEFAULT_ASSISTANT_CONFIG, EmptyState, Equalizer, HistoryPanel, LoadingDots, ModelSelector, PromptCard, ScrollToBottomButton, ThinkingBlock, TurnRail, UserMessage, defaultRenderLink, formatRelativeTime, sanitizeUserHtml, useAssistantConfig, useTurnRail };
1594
+ export { AssistantChoiceChips, AssistantConfigProvider, AssistantMessage, AssistantPlanCard, AssistantWorkspace, BrainGlyph, CHOICES_PART_TYPE, ChatRail, Conversation, DEFAULT_ASSISTANT_CONFIG, EmptyState, Equalizer, HistoryPanel, LoadingDots, ModelSelector, PLAN_PART_TYPE, PromptCard, ScrollToBottomButton, ThinkingBlock, TurnRail, UserMessage, defaultRenderLink, formatRelativeTime, sanitizeUserHtml, useAssistantConfig, useTurnRail };
@@ -1,7 +1,7 @@
1
1
  import type { Editor } from '@tiptap/react';
2
2
  import type { UIMessage } from 'ai';
3
3
  import type { MouseEvent as ReactMouseEvent, ReactNode, RefObject } from 'react';
4
- import type { AssistantConfig, ChatSummary, EffortId } from '../types';
4
+ import type { AssistantChoiceOption, AssistantConfig, ChatSummary, EffortId } from '../types';
5
5
  export type AssistantWorkspaceOrientation = 'horizontal' | 'vertical';
6
6
  /**
7
7
  * Presentational, props-driven assistant layout: left rail + collapsible
@@ -66,6 +66,12 @@ export interface AssistantWorkspaceProps {
66
66
  /** Confirm (deletion can't be undone) before calling `onDeleteChat`. */
67
67
  confirmDelete?: boolean;
68
68
  onSuggestion: (text: string) => void;
69
+ /** Called when the person picks an option of a question asked as buttons (see `Conversation`). */
70
+ onChoose?: (messageId: string, option: AssistantChoiceOption) => void;
71
+ /** Called when the person approves a plan card (see `Conversation`). */
72
+ onApprovePlan?: (messageId: string, planId: string) => void;
73
+ /** Called when the person clicks "Change something" on a plan card. */
74
+ onChangePlan?: (messageId: string, planId: string) => void;
69
75
  modelId: string;
70
76
  effortId: EffortId;
71
77
  onModelChange: (id: string) => void;
@@ -77,4 +83,4 @@ export interface AssistantWorkspaceProps {
77
83
  historyOpen: boolean;
78
84
  onToggleHistory: () => void;
79
85
  }
80
- export declare function AssistantWorkspace({ orientation, onClose, config, userName, title, messages, status, error, isReady, chatList, currentChatId, sidebarHeader, editor, htmlByUserMsgIndex, bottomRef, containerRef, userScrolledUpRef, onSend, onStop, onRetry, onNewChat, onLoadChat, onDeleteChat, onArchiveChat, onUnarchiveChat, confirmDelete, onSuggestion, modelId, effortId, onModelChange, onEffortChange, micSupported, micListening, micFrequencyData, onMicToggle, historyOpen, onToggleHistory, }: AssistantWorkspaceProps): import("react").JSX.Element;
86
+ export declare function AssistantWorkspace({ orientation, onClose, config, userName, title, messages, status, error, isReady, chatList, currentChatId, sidebarHeader, editor, htmlByUserMsgIndex, bottomRef, containerRef, userScrolledUpRef, onSend, onStop, onRetry, onNewChat, onLoadChat, onDeleteChat, onArchiveChat, onUnarchiveChat, confirmDelete, onSuggestion, onChoose, onApprovePlan, onChangePlan, modelId, effortId, onModelChange, onEffortChange, micSupported, micListening, micFrequencyData, onMicToggle, historyOpen, onToggleHistory, }: AssistantWorkspaceProps): import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import type { Editor } from '@tiptap/react';
2
- import type { EffortId } from '../../types';
2
+ import type { ConversationDensity, EffortId } from '../../types';
3
3
  interface PromptCardProps {
4
4
  editor: Editor | null;
5
5
  isReady: boolean;
@@ -15,7 +15,12 @@ interface PromptCardProps {
15
15
  isListening?: boolean;
16
16
  frequencyData?: number[];
17
17
  onMicToggle?: () => void;
18
+ /**
19
+ * `compact` is one row: the input and its buttons side by side in a plain
20
+ * bordered field, with an outline send button. For a chat embedded in a page.
21
+ */
22
+ density?: ConversationDensity;
18
23
  className?: string;
19
24
  }
20
- export declare function PromptCard({ editor, isReady, canRetry, onSend, onStop, onRetry, modelId, effortId, onModelChange, onEffortChange, speechSupported, isListening, frequencyData, onMicToggle, className, }: PromptCardProps): import("react").JSX.Element;
25
+ export declare function PromptCard({ editor, isReady, canRetry, onSend, onStop, onRetry, modelId, effortId, onModelChange, onEffortChange, speechSupported, isListening, frequencyData, onMicToggle, density, className, }: PromptCardProps): import("react").JSX.Element;
21
26
  export {};
@@ -1,5 +1,6 @@
1
1
  import type { UIMessage } from 'ai';
2
2
  import type { RefObject } from 'react';
3
+ import type { AssistantChoiceOption, ConversationDensity } from '../../types';
3
4
  interface ConversationProps {
4
5
  messages: UIMessage[];
5
6
  status: string;
@@ -10,6 +11,27 @@ interface ConversationProps {
10
11
  userScrolledUpRef: RefObject<boolean>;
11
12
  /** The docked prompt card, pinned to the bottom. */
12
13
  footer: React.ReactNode;
14
+ /**
15
+ * `default` is the full workspace layout. `compact` is for a conversation
16
+ * embedded in another page: it fills the host's width with 1.75rem side
17
+ * padding, puts an avatar beside each assistant turn and tightens markdown
18
+ * spacing.
19
+ */
20
+ density?: ConversationDensity;
21
+ /**
22
+ * Called when the person picks an option of a question the assistant asked
23
+ * as buttons, with the message it belongs to. Only the latest turn's
24
+ * question can be answered, and only once it has finished streaming.
25
+ */
26
+ onChoose?: (messageId: string, option: AssistantChoiceOption) => void;
27
+ /**
28
+ * Called when the person approves a plan card, with the message it is in
29
+ * and the plan's id. Only the newest plan can be approved, and only while
30
+ * the conversation is idle.
31
+ */
32
+ onApprovePlan?: (messageId: string, planId: string) => void;
33
+ /** Called when the person clicks "Change something" on a plan card. */
34
+ onChangePlan?: (messageId: string, planId: string) => void;
13
35
  }
14
- export declare function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRef, bottomRef, userScrolledUpRef, footer, }: ConversationProps): import("react").JSX.Element;
36
+ export declare function Conversation({ messages, status, error, htmlByUserMsgIndex, containerRef, bottomRef, userScrolledUpRef, footer, density, onChoose, onApprovePlan, onChangePlan, }: ConversationProps): import("react").JSX.Element;
15
37
  export {};
@@ -1,9 +1,35 @@
1
1
  import type { UIMessage } from 'ai';
2
+ import type { AssistantChoiceOption, ConversationDensity } from '../../types';
2
3
  import 'streamdown/styles.css';
3
4
  interface AssistantMessageProps {
4
5
  msg: UIMessage;
5
6
  isLastMessage: boolean;
6
7
  status: string;
8
+ /**
9
+ * `compact` puts the assistant avatar beside the turn, tightens markdown
10
+ * spacing (headings render at body size), sets inline code as plain mono
11
+ * text, and leaves out the copy and download actions (a host's own
12
+ * `messageActions` still render).
13
+ */
14
+ density?: ConversationDensity;
15
+ /**
16
+ * Called when the person picks an option of a question in this turn (a
17
+ * `data-choices` part). The host sends the option's label as their reply.
18
+ */
19
+ onChoose?: (option: AssistantChoiceOption) => void;
20
+ /**
21
+ * Whether this turn's question can still be answered: the host passes true
22
+ * only for the latest turn once it has finished streaming.
23
+ */
24
+ canChoose?: boolean;
25
+ /** Called when the person approves a plan in this turn (a `data-plan` part), with its id. */
26
+ onApprovePlan?: (planId: string) => void;
27
+ /** Called when the person asks to change a plan in this turn. */
28
+ onChangePlan?: (planId: string) => void;
29
+ /** The id of the newest plan in the conversation; any other reads as replaced. */
30
+ latestPlanId?: string;
31
+ /** Whether a plan can be approved now: the conversation is idle. */
32
+ canApprovePlan?: boolean;
7
33
  }
8
- export declare function AssistantMessage({ msg, isLastMessage, status }: AssistantMessageProps): import("react").JSX.Element;
34
+ export declare function AssistantMessage({ msg, isLastMessage, status, density, onChoose, canChoose, onApprovePlan, onChangePlan, latestPlanId, canApprovePlan, }: AssistantMessageProps): import("react").JSX.Element;
9
35
  export {};
@@ -0,0 +1,17 @@
1
+ import type { AssistantChoiceOption, AssistantChoicesData } from '../../types';
2
+ export interface AssistantChoiceChipsProps {
3
+ data: AssistantChoicesData;
4
+ /**
5
+ * Called with the option the person picked. Omit it, or pass `disabled`,
6
+ * for a question that can no longer be answered (an earlier turn, or one
7
+ * answered by typing instead).
8
+ */
9
+ onChoose?: (option: AssistantChoiceOption) => void;
10
+ disabled?: boolean;
11
+ }
12
+ /**
13
+ * A pick-one question as a row of buttons. Once an option is picked it stays
14
+ * marked with a check and the others fade, so a scrolled-back conversation
15
+ * still shows what was chosen.
16
+ */
17
+ export declare function AssistantChoiceChips({ data, onChoose, disabled }: AssistantChoiceChipsProps): import("react").JSX.Element;
@@ -1,4 +1,6 @@
1
1
  export * from './assistant-message';
2
+ export * from './choice-chips';
2
3
  export * from './loading-dots';
4
+ export * from './plan-card';
3
5
  export * from './thinking-block';
4
6
  export * from './user-message';
@@ -0,0 +1,20 @@
1
+ import type { AssistantPlanData } from '../../types';
2
+ export interface AssistantPlanCardProps {
3
+ data: AssistantPlanData;
4
+ /** Whether this is the newest plan in the conversation; an older one reads as replaced. */
5
+ latest?: boolean;
6
+ /** Whether Approve can be clicked now (the conversation is idle). */
7
+ canApprove?: boolean;
8
+ onApprove?: () => void;
9
+ /** "Change something": the host puts the person in the composer to say what. */
10
+ onChange?: () => void;
11
+ /** The clock, for tests. */
12
+ now?: number;
13
+ }
14
+ /**
15
+ * A plan the assistant settled, as a card: one row per resource with what it
16
+ * does, the YAML and differences on request, and the person's two answers.
17
+ * Nothing is created until they click Approve, and the card then shows how it
18
+ * went.
19
+ */
20
+ export declare function AssistantPlanCard({ data, latest, canApprove, onApprove, onChange, now, }: AssistantPlanCardProps): import("react").JSX.Element;
@@ -1,4 +1,9 @@
1
- /** A user's message — a right-aligned bubble rendering the sanitized input HTML. */
2
- export declare function UserMessage({ html }: {
1
+ import type { ConversationDensity } from '../../types';
2
+ interface UserMessageProps {
3
3
  html: string;
4
- }): import("react").JSX.Element;
4
+ /** `compact` uses a lighter bubble with even corners. */
5
+ density?: ConversationDensity;
6
+ }
7
+ /** A user's message — a right-aligned bubble rendering the sanitized input HTML. */
8
+ export declare function UserMessage({ html, density }: UserMessageProps): import("react").JSX.Element;
9
+ export {};
@@ -7,5 +7,6 @@ export * from './components/message';
7
7
  export * from './components/sidebar';
8
8
  export { AssistantConfigProvider, DEFAULT_ASSISTANT_CONFIG, defaultRenderLink, useAssistantConfig, } from './context';
9
9
  export { useTurnRail } from './hooks';
10
- export type { AssistantConfig, ChatSummary, EffortId, EffortOption, LinkRenderProps, ModelOption, ModelSelectorConfig, Turn, } from './types';
10
+ export { CHOICES_PART_TYPE, PLAN_PART_TYPE } from './types';
11
+ export type { AssistantAppliedResource, AssistantChoiceIcon, AssistantChoiceOption, AssistantChoicesData, AssistantConfig, AssistantPlanData, AssistantPlanResource, AssistantPlanStatus, ChatSummary, ConversationDensity, EffortId, EffortOption, LinkRenderProps, ModelOption, ModelSelectorConfig, Turn, } from './types';
11
12
  export { formatRelativeTime, sanitizeUserHtml } from './utils';
@@ -1,71 +1,81 @@
1
- import type { DynamicToolUIPart, ToolUIPart, UIMessage } from 'ai';
1
+ import type { AssistantConfig as CoreAssistantConfig, LinkRenderProps as CoreLinkRenderProps } from '@datum-cloud/datum-ui-assistant-core';
2
2
  import type { ReactNode } from 'react';
3
- export type EffortId = 'low' | 'medium' | 'high';
4
- export interface ModelOption {
5
- /** Stable id sent to the server; must match the allowlist in server/routes/assistant.ts. */
3
+ export type { ChatSummary, EffortId, EffortOption, ModelOption, ModelSelectorConfig, Turn, } from '@datum-cloud/datum-ui-assistant-core';
4
+ /** Props passed to a `renderLink` implementation (a link in the assistant's markdown). */
5
+ export type LinkRenderProps = CoreLinkRenderProps<ReactNode>;
6
+ /**
7
+ * Everything that differs between host apps (staff vs cloud). Supplied once at
8
+ * the workspace root and read by leaf components via `useAssistantConfig()`, so
9
+ * the presentational pieces stay generic and shell-agnostic.
10
+ */
11
+ export type AssistantConfig = CoreAssistantConfig<ReactNode>;
12
+ /**
13
+ * How tightly a conversation is laid out. `default` is the full assistant
14
+ * workspace; `compact` suits a conversation embedded in another page: it fills
15
+ * the host's width, puts an avatar beside each assistant turn, tightens
16
+ * markdown spacing and uses a one-line composer.
17
+ */
18
+ export type ConversationDensity = 'default' | 'compact';
19
+ /** The message part type that carries a pick-one question (see {@link AssistantChoicesData}). */
20
+ export declare const CHOICES_PART_TYPE = "data-choices";
21
+ /** Icon names a choice option may carry. Anything else renders with no icon. */
22
+ export type AssistantChoiceIcon = 'clock' | 'container' | 'database' | 'domain' | 'globe' | 'laptop' | 'location' | 'network' | 'server' | 'shield';
23
+ /** One answer to a pick-one question. `label` is sent back as the person's reply. */
24
+ export interface AssistantChoiceOption {
6
25
  id: string;
7
26
  label: string;
27
+ icon?: AssistantChoiceIcon | string;
8
28
  }
9
- export interface EffortOption {
10
- id: EffortId;
11
- label: string;
12
- }
13
- /** One submitted prompt — the unit tracked by the right-edge turn rail. */
14
- export interface Turn {
15
- id: string;
16
- text: string;
29
+ /**
30
+ * The data of a `data-choices` message part: a question the assistant put to
31
+ * the person as buttons. `selectedId` is set once they pick one.
32
+ */
33
+ export interface AssistantChoicesData {
34
+ question: string;
35
+ options: AssistantChoiceOption[];
36
+ selectedId?: string;
17
37
  }
18
- /** Props passed to a `renderLink` implementation (a link in the assistant's markdown). */
19
- export interface LinkRenderProps {
20
- href?: string;
21
- children?: ReactNode;
38
+ /** The message part type that carries a plan the person can approve (see {@link AssistantPlanData}). */
39
+ export declare const PLAN_PART_TYPE = "data-plan";
40
+ /** One resource a plan creates or changes, in apply order. */
41
+ export interface AssistantPlanResource {
42
+ kind: string;
43
+ name: string;
44
+ namespace?: string;
45
+ action: 'create' | 'update' | string;
46
+ /** One plain-language line on what it does for the person. */
47
+ summary?: string;
48
+ /** What an update changes, one line per change. */
49
+ diff?: string[];
50
+ /** The manifest the plan covers, as YAML. */
51
+ manifest: string;
22
52
  }
23
- export interface ModelSelectorConfig {
24
- models: ModelOption[];
25
- efforts: EffortOption[];
26
- defaultModelId: string;
27
- defaultEffortId: EffortId;
53
+ /** One resource applying a plan created or changed. */
54
+ export interface AssistantAppliedResource {
55
+ action: string;
56
+ kind: string;
57
+ name: string;
58
+ namespace?: string;
28
59
  }
29
60
  /**
30
- * Everything that differs between host apps (staff vs cloud). Supplied once at
31
- * the workspace root and read by leaf components via `useAssistantConfig()`, so
32
- * the presentational pieces stay generic and shell-agnostic.
61
+ * Where a plan is. `pending` waits for the person; `applying` is their
62
+ * approval in flight; `applied` and `failed` are its outcome. Expired and
63
+ * replaced are not stored: the card works them out from `expiresAt` and from
64
+ * whether a newer plan exists.
33
65
  */
34
- export interface AssistantConfig {
35
- /** Greeting line on the empty state, e.g. "Hey there, Jacob". */
36
- greeting: (name?: string) => string;
37
- /** Starter prompts on the empty state. */
38
- suggestions: string[];
39
- /** Whether to render the assistant's reasoning ("thinking") blocks. */
40
- showReasoning: boolean;
41
- /** Model + effort picker config, or `false` to hide the control. */
42
- modelSelector: ModelSelectorConfig | false;
43
- /** Tool name → progress label ("Searching users…"). Host-specific tool set. */
44
- toolLabels: Record<string, string>;
45
- /** Renders links inside the assistant's markdown (internal routes, external, host-specific icons). */
46
- renderLink: (props: LinkRenderProps) => ReactNode;
47
- /** Override the per-message action toolbar (defaults to copy + download). */
48
- messageActions?: (msg: UIMessage) => ReactNode;
49
- /**
50
- * Renders a completed tool call inline, in message order. Return a falsy value
51
- * to leave that tool invisible — the default for every tool, since most only
52
- * feed the model. Hosts use this to surface a result as UI (cloud-portal turns
53
- * an `openSupportTicket` result into a button).
54
- */
55
- renderToolOutput?: (part: ToolUIPart | DynamicToolUIPart, msg: UIMessage) => ReactNode;
56
- }
66
+ export type AssistantPlanStatus = 'pending' | 'applying' | 'applied' | 'failed';
57
67
  /**
58
- * Minimal shape the history list needs. Host apps pass their own richer chat
59
- * objects (they're structurally compatible) — persistence lives in the host.
68
+ * The data of a `data-plan` message part: a change the assistant settled and
69
+ * the token that applies exactly it. The part's `id` names the plan.
60
70
  */
61
- export interface ChatSummary {
62
- id: string;
63
- title: string;
64
- updatedAt: number;
65
- messages: UIMessage[];
66
- /**
67
- * Archived (hidden but restorable). Only honoured when the host passes
68
- * `onArchiveChat`; otherwise every chat is listed regardless of this flag.
69
- */
70
- archived?: boolean;
71
+ export interface AssistantPlanData {
72
+ token: string;
73
+ /** When the plan stops being accepted, RFC 3339. */
74
+ expiresAt: string;
75
+ resources: AssistantPlanResource[];
76
+ status: AssistantPlanStatus;
77
+ /** When the person approved it, RFC 3339. */
78
+ approvedAt?: string;
79
+ applied?: AssistantAppliedResource[];
80
+ error?: string;
71
81
  }
@@ -1,2 +1,2 @@
1
- /** Human-friendly relative timestamp for the chat history list. */
2
- export declare function formatRelativeTime(timestamp: number): string;
1
+ /** Human-friendly relative timestamp for the chat history list (shared with React Native). */
2
+ export { formatRelativeTime } from '@datum-cloud/datum-ui-assistant-core';
package/dist/index.mjs CHANGED
@@ -41,7 +41,7 @@ import { t as Switch } from "./switch-BaR-Vna4.mjs";
41
41
  import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "./table/index.mjs";
42
42
  import { Tabs, TabsContent, TabsLinkTrigger, TabsList, TabsTrigger } from "./tabs/index.mjs";
43
43
  import { t as Textarea } from "./textarea-DiSGxCBO.mjs";
44
- import { a as ListItem, c as Title, d as titleVariants, i as List, l as paragraphVariants, n as Code, o as Paragraph, r as Link, s as Text, t as Blockquote, u as textVariants } from "./typography-q71iTG72.mjs";
44
+ import { a as ListItem, c as Title, d as titleVariants, i as List, l as paragraphVariants, n as Code, o as Paragraph, r as Link, s as Text, t as Blockquote, u as textVariants } from "./typography-B1bpHKmV.mjs";
45
45
  import { t as VisuallyHidden } from "./visuallyhidden-D7L_qs8b.mjs";
46
46
  import { a as hasActiveDescendant, i as findItemByHref, n as NavMenu, o as isNavItemActive, r as NavMenuItem, s as normalizePath, t as AppNavigation } from "./app-navigation-BB_NLEWv.mjs";
47
47
  import { n as EASE, t as DURATION } from "./motion-DvGRWdsm.mjs";
@@ -79,7 +79,7 @@ import { t as MoreActions } from "./more-actions-C3DYWO0a.mjs";
79
79
  import { configureProgress, startProgress, stopProgress } from "./nprogress/index.mjs";
80
80
  import { t as PageTitle } from "./page-title-ipRWEOeM.mjs";
81
81
  import { t as RichTextContent } from "./rich-text-content-DNonBD39.mjs";
82
- import { n as useRichTextEditor, t as RichTextEditor } from "./rich-text-editor-DP5JnAoO.mjs";
82
+ import { n as useRichTextEditor, t as RichTextEditor } from "./rich-text-editor-CP1YyxMR.mjs";
83
83
  import { t as defineStepper } from "./stepper-dNvkipop.mjs";
84
84
  import { t as TagsInput } from "./tag-input-c9OvWTSe.mjs";
85
85
  import { _ as RedisTaskStorage, a as TaskPanelItem, b as createProjectMetadata, c as TaskPanelHeader, d as useCurrentScope, f as useTasksWithLabels, g as detectStorage, h as TaskQueue, i as TaskPanel, l as getContextLabel, m as TaskQueueProvider, n as TaskSummaryDialog, o as TaskPanelCounter, p as useTaskQueue, r as TaskQueueTrigger, s as TaskPanelActions, t as TaskQueueDropdown, u as matchesCurrentScope, v as LocalTaskStorage, x as createUserMetadata, y as createOrgMetadata } from "./task-queue-dropdown-CHPP5WIm.mjs";
@@ -1,3 +1,3 @@
1
1
  import { t as RichTextContent } from "../rich-text-content-DNonBD39.mjs";
2
- import { n as useRichTextEditor, t as RichTextEditor } from "../rich-text-editor-DP5JnAoO.mjs";
2
+ import { n as useRichTextEditor, t as RichTextEditor } from "../rich-text-editor-CP1YyxMR.mjs";
3
3
  export { RichTextContent, RichTextEditor, useRichTextEditor };
@@ -8,7 +8,7 @@ import { BoldIcon, ItalicIcon, LinkIcon, StrikethroughIcon, UnderlineIcon, Unlin
8
8
  import { createContext, use, useCallback, useEffect, useState } from "react";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
10
  import CharacterCount from "@tiptap/extension-character-count";
11
- import Link from "@tiptap/extension-link";
11
+ import Link$1 from "@tiptap/extension-link";
12
12
  import Placeholder from "@tiptap/extension-placeholder";
13
13
  import Underline from "@tiptap/extension-underline";
14
14
  import { EditorContent, useEditor } from "@tiptap/react";
@@ -191,7 +191,7 @@ function RichTextEditorRoot({ content, onChange, onBlur, autoFocus = false, plac
191
191
  horizontalRule: false
192
192
  }),
193
193
  Underline,
194
- Link.configure({
194
+ Link$1.configure({
195
195
  autolink: true,
196
196
  openOnClick: false,
197
197
  validate: (url) => /^https?:\/\/|^mailto:/.test(url),
@@ -589,7 +589,7 @@ TAILWIND CUSTOM UTILITIES FOR THEME
589
589
  /* ========================================
590
590
  TYPE SCALE
591
591
  One named scale shared by raw `text-*` utilities and the Typography
592
- components. See Storybook "Docs/Type Scale" for the rules.
592
+ components. See Storybook "Foundations/Type Scale" for the rules.
593
593
 
594
594
  Deliberately a plain `@theme`, not `@theme inline`: utilities must read
595
595
  `var(--text-*)` at runtime so the display steps below can shrink per
@@ -1,2 +1,2 @@
1
- import { a as ListItem, c as Title, d as titleVariants, i as List, l as paragraphVariants, n as Code, o as Paragraph, r as Link, s as Text, t as Blockquote, u as textVariants } from "../typography-q71iTG72.mjs";
1
+ import { a as ListItem, c as Title, d as titleVariants, i as List, l as paragraphVariants, n as Code, o as Paragraph, r as Link, s as Text, t as Blockquote, u as textVariants } from "../typography-B1bpHKmV.mjs";
2
2
  export { Blockquote, Code, Link, List, ListItem, Paragraph, Text, Title, paragraphVariants, textVariants, titleVariants };
@@ -162,7 +162,7 @@ function Paragraph({ className, size, spacing, as = "p", children, ...props }) {
162
162
  ...props
163
163
  }, children);
164
164
  }
165
- function Link({ className, children, href, target = "_self", rel, ...props }) {
165
+ function Link$1({ className, children, href, target = "_self", rel, ...props }) {
166
166
  return /* @__PURE__ */ jsx("a", {
167
167
  href,
168
168
  target,
@@ -193,7 +193,7 @@ function Blockquote({ className, children, ...props }) {
193
193
  children
194
194
  });
195
195
  }
196
- function Code({ className, as = "code", children, ...props }) {
196
+ function Code$1({ className, as = "code", children, ...props }) {
197
197
  if (as === "pre") return /* @__PURE__ */ jsx("pre", {
198
198
  className: cn("bg-muted relative overflow-x-auto rounded p-4 font-mono text-sm", className),
199
199
  ...props,
@@ -206,4 +206,4 @@ function Code({ className, as = "code", children, ...props }) {
206
206
  });
207
207
  }
208
208
  //#endregion
209
- export { ListItem as a, Title as c, titleVariants as d, List as i, paragraphVariants as l, Code as n, Paragraph as o, Link as r, Text as s, Blockquote as t, textVariants as u };
209
+ export { ListItem as a, Title as c, titleVariants as d, List as i, paragraphVariants as l, Code$1 as n, Paragraph as o, Link$1 as r, Text as s, Blockquote as t, textVariants as u };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datum-cloud/datum-ui",
3
3
  "type": "module",
4
- "version": "3.5.0",
4
+ "version": "3.7.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "url": "https://github.com/datum-cloud/datum-ui"
@@ -621,7 +621,8 @@
621
621
  "cmdk": "^1.1.1",
622
622
  "isomorphic-dompurify": "^3.22.0",
623
623
  "tailwind-merge": "^3.6.0",
624
- "tw-animate-css": "^1.4.0"
624
+ "tw-animate-css": "^1.4.0",
625
+ "@datum-cloud/datum-ui-assistant-core": "^0.1.0"
625
626
  },
626
627
  "devDependencies": {
627
628
  "@conform-to/react": "^1.21.0",