@calliopelabs/ui 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +50 -3
  2. package/dist/agent/AgentChat.d.ts +56 -0
  3. package/dist/agent/AgentChat.js +76 -0
  4. package/dist/agent/AgentLauncher.d.ts +25 -0
  5. package/dist/agent/AgentLauncher.js +25 -0
  6. package/dist/agent/AgentProvider.d.ts +82 -0
  7. package/dist/agent/AgentProvider.js +176 -0
  8. package/dist/agent/CalliopeAgent.d.ts +43 -0
  9. package/dist/agent/CalliopeAgent.js +111 -0
  10. package/dist/agent/controller.d.ts +148 -0
  11. package/dist/agent/controller.js +463 -0
  12. package/dist/agent/copilotkit/CopilotKitAgentProvider.d.ts +41 -0
  13. package/dist/agent/copilotkit/CopilotKitAgentProvider.js +145 -0
  14. package/dist/agent/copilotkit/index.d.ts +6 -0
  15. package/dist/agent/copilotkit/index.js +6 -0
  16. package/dist/agent/engine.d.ts +41 -0
  17. package/dist/agent/engine.js +93 -0
  18. package/dist/agent/index.d.ts +15 -0
  19. package/dist/agent/index.js +15 -0
  20. package/dist/agent/mock.d.ts +34 -0
  21. package/dist/agent/mock.js +112 -0
  22. package/dist/agent/surface.d.ts +30 -0
  23. package/dist/agent/surface.js +90 -0
  24. package/dist/agent/tool.d.ts +98 -0
  25. package/dist/agent/tool.js +81 -0
  26. package/dist/appearance/brands.d.ts +14 -12
  27. package/dist/appearance/brands.js +18 -51
  28. package/dist/chat/AgentProgress.d.ts +19 -0
  29. package/dist/chat/AgentProgress.js +34 -0
  30. package/dist/chat/Composer.d.ts +31 -0
  31. package/dist/chat/Composer.js +44 -0
  32. package/dist/chat/MessageBubble.d.ts +23 -0
  33. package/dist/chat/MessageBubble.js +26 -0
  34. package/dist/chat/MessageList.d.ts +36 -0
  35. package/dist/chat/MessageList.js +68 -0
  36. package/dist/chat/ModelPicker.d.ts +17 -0
  37. package/dist/chat/ModelPicker.js +13 -0
  38. package/dist/chat/ToolCallCard.d.ts +27 -0
  39. package/dist/chat/ToolCallCard.js +52 -0
  40. package/dist/chat/model.d.ts +123 -0
  41. package/dist/chat/model.js +337 -0
  42. package/dist/chat/patch.d.ts +12 -0
  43. package/dist/chat/patch.js +149 -0
  44. package/dist/chat/useAgUiRun.d.ts +52 -0
  45. package/dist/chat/useAgUiRun.js +137 -0
  46. package/dist/index.d.ts +9 -0
  47. package/dist/index.js +10 -0
  48. package/dist/primitives/Button.d.ts +1 -1
  49. package/dist/provider/messages.d.ts +66 -0
  50. package/dist/provider/messages.js +71 -0
  51. package/dist/theme/mappings/palette.d.ts +52 -0
  52. package/dist/theme/mappings/palette.js +208 -0
  53. package/dist/theme/mappings/render.d.ts +9 -0
  54. package/dist/theme/mappings/render.js +520 -0
  55. package/dist/theme/mappings/targets.d.ts +43 -0
  56. package/dist/theme/mappings/targets.js +222 -0
  57. package/dist/theme/tokens.d.ts +10 -9
  58. package/dist/theme/tokens.generated.d.ts +353 -0
  59. package/dist/theme/tokens.generated.js +357 -0
  60. package/dist/theme/tokens.js +20 -264
  61. package/dist/tokens/README.jupyterlab.md +125 -0
  62. package/dist/tokens/README.md +15 -0
  63. package/dist/tokens/README.native.md +29 -0
  64. package/dist/tokens/README.plain.md +84 -0
  65. package/dist/tokens/README.python.md +33 -0
  66. package/dist/tokens/README.vscode.md +114 -0
  67. package/dist/tokens/jupyterlab.css +1710 -0
  68. package/dist/tokens/plain.css +930 -0
  69. package/dist/tokens/tokens.native.d.ts +85 -0
  70. package/dist/tokens/tokens.native.js +773 -0
  71. package/dist/tokens/tokens.py +1043 -0
  72. package/dist/tokens/vscode.css +1138 -0
  73. package/package.json +35 -2
@@ -1,6 +1,8 @@
1
1
  /**
2
- * The two consoles' brands, kept here so both apps and the catalog read the
3
- * same values. An app adds its navigation:
2
+ * The family's brands, kept here so every app and the catalog read the same
3
+ * values. Their colours (sidebar and the product's own accent) live in
4
+ * `tokens/brand/*.json`; this file adds names, grounds, defaults and themes.
5
+ * An app adds its navigation:
4
6
  *
5
7
  * export const astrolift = defineProduct({ ...astroliftBrand, nav: NAV });
6
8
  */
@@ -78,12 +80,12 @@ export declare const zentinelleBrand: {
78
80
  readonly id: "zentinelle";
79
81
  readonly name: "Zentinelle AI";
80
82
  readonly accent: {
81
- readonly key: "teal";
82
- readonly label: "Teal";
83
83
  readonly dark: "#37efed";
84
84
  readonly onDark: "#0b0b19";
85
85
  readonly light: "#0d7470";
86
86
  readonly onLight: "#f3fbf7";
87
+ readonly key: "teal";
88
+ readonly label: "Teal";
87
89
  };
88
90
  readonly sidebar: "#0b0b19";
89
91
  readonly grounds: readonly ["black", "charcoal", "graphite", "paper", "mist"];
@@ -102,12 +104,12 @@ export declare const workbenchBrand: {
102
104
  readonly id: "workbench";
103
105
  readonly name: "Calliope AI Workbench";
104
106
  readonly accent: {
105
- readonly key: "workbench-blue";
106
- readonly label: "Workbench blue";
107
107
  readonly dark: "#8fb8e8";
108
108
  readonly onDark: "#0b1120";
109
109
  readonly light: "#2f5d94";
110
110
  readonly onLight: "#f3fbf7";
111
+ readonly key: "workbench-blue";
112
+ readonly label: "Workbench blue";
111
113
  };
112
114
  readonly sidebar: "#0e1730";
113
115
  readonly grounds: readonly ["navy", "black", "charcoal", "graphite", "paper", "mist"];
@@ -124,12 +126,12 @@ export declare const chatStudioBrand: {
124
126
  readonly id: "chat-studio";
125
127
  readonly name: "Calliope AI Chat Studio";
126
128
  readonly accent: {
127
- readonly key: "studio-blue";
128
- readonly label: "Studio blue";
129
129
  readonly dark: "#8ab4f8";
130
130
  readonly onDark: "#0c0c0c";
131
131
  readonly light: "#1d4ed8";
132
132
  readonly onLight: "#ffffff";
133
+ readonly key: "studio-blue";
134
+ readonly label: "Studio blue";
133
135
  };
134
136
  readonly sidebar: "#080808";
135
137
  readonly grounds: readonly ["black", "charcoal", "graphite", "paper", "mist"];
@@ -147,12 +149,12 @@ export declare const loadrBrand: {
147
149
  readonly id: "loadr";
148
150
  readonly name: "Calliope AI Loadr";
149
151
  readonly accent: {
150
- readonly key: "loadr-blue";
151
- readonly label: "Loadr blue";
152
152
  readonly dark: "#89b4fa";
153
153
  readonly onDark: "#11111b";
154
154
  readonly light: "#1e66f5";
155
155
  readonly onLight: "#ffffff";
156
+ readonly key: "loadr-blue";
157
+ readonly label: "Loadr blue";
156
158
  };
157
159
  readonly sidebar: "#181825";
158
160
  readonly grounds: readonly ["black", "charcoal", "graphite", "paper", "mist"];
@@ -171,12 +173,12 @@ export declare const cykickBrand: {
171
173
  readonly id: "cykick";
172
174
  readonly name: "Cykick";
173
175
  readonly accent: {
174
- readonly key: "cykick-cyan";
175
- readonly label: "Cykick cyan";
176
176
  readonly dark: "#22d3ee";
177
177
  readonly onDark: "#07090e";
178
178
  readonly light: "#0e7490";
179
179
  readonly onLight: "#ffffff";
180
+ readonly key: "cykick-cyan";
181
+ readonly label: "Cykick cyan";
180
182
  };
181
183
  readonly sidebar: "#07090e";
182
184
  readonly grounds: readonly ["black", "charcoal", "graphite", "paper", "mist"];
@@ -1,6 +1,9 @@
1
+ import { TOKENS } from "../theme/tokens.generated.js";
1
2
  /**
2
- * The two consoles' brands, kept here so both apps and the catalog read the
3
- * same values. An app adds its navigation:
3
+ * The family's brands, kept here so every app and the catalog read the same
4
+ * values. Their colours (sidebar and the product's own accent) live in
5
+ * `tokens/brand/*.json`; this file adds names, grounds, defaults and themes.
6
+ * An app adds its navigation:
4
7
  *
5
8
  * export const astrolift = defineProduct({ ...astroliftBrand, nav: NAV });
6
9
  */
@@ -13,7 +16,7 @@ export const astroliftBrand = {
13
16
  id: "astrolift",
14
17
  name: "Astrolift AI",
15
18
  accent: "green",
16
- sidebar: "#04231a",
19
+ sidebar: TOKENS.brand.astrolift.sidebar,
17
20
  grounds: ["black", "charcoal", "graphite", "emerald", "paper", "mist"],
18
21
  defaultGround: { dark: "graphite", light: "mist" },
19
22
  themes: [
@@ -47,15 +50,8 @@ export const astroliftBrand = {
47
50
  export const zentinelleBrand = {
48
51
  id: "zentinelle",
49
52
  name: "Zentinelle AI",
50
- accent: {
51
- key: "teal",
52
- label: "Teal",
53
- dark: "#37efed",
54
- onDark: "#0b0b19",
55
- light: "#0d7470",
56
- onLight: "#f3fbf7",
57
- },
58
- sidebar: "#0b0b19",
53
+ accent: { key: "teal", label: "Teal", ...TOKENS.brand.zentinelle.accent },
54
+ sidebar: TOKENS.brand.zentinelle.sidebar,
59
55
  grounds: ["black", "charcoal", "graphite", "paper", "mist"],
60
56
  defaultGround: { dark: "black", light: "mist" },
61
57
  };
@@ -63,7 +59,8 @@ export const zentinelleBrand = {
63
59
  * The brands below are copied from what each product ships today, so nothing
64
60
  * changes when an app moves onto the library. Where a product has no value
65
61
  * the model needs (usually a light-ground accent), the nearest one that
66
- * passes the contrast tests is used and marked "derived" for the designer.
62
+ * passes the contrast tests is used and its token's description says
63
+ * "Derived" for the designer.
67
64
  */
68
65
  /**
69
66
  * Calliope AI Workbench (JunoHub, the installer's default theme, the desktop
@@ -74,16 +71,8 @@ export const zentinelleBrand = {
74
71
  export const workbenchBrand = {
75
72
  id: "workbench",
76
73
  name: "Calliope AI Workbench",
77
- accent: {
78
- key: "workbench-blue",
79
- label: "Workbench blue",
80
- dark: "#8fb8e8",
81
- onDark: "#0b1120",
82
- // derived: the hub has no light ground
83
- light: "#2f5d94",
84
- onLight: "#f3fbf7",
85
- },
86
- sidebar: "#0e1730",
74
+ accent: { key: "workbench-blue", label: "Workbench blue", ...TOKENS.brand.workbench.accent },
75
+ sidebar: TOKENS.brand.workbench.sidebar,
87
76
  grounds: ["navy", "black", "charcoal", "graphite", "paper", "mist"],
88
77
  defaultGround: { dark: "navy", light: "mist" },
89
78
  };
@@ -94,15 +83,8 @@ export const workbenchBrand = {
94
83
  export const chatStudioBrand = {
95
84
  id: "chat-studio",
96
85
  name: "Calliope AI Chat Studio",
97
- accent: {
98
- key: "studio-blue",
99
- label: "Studio blue",
100
- dark: "#8ab4f8",
101
- onDark: "#0c0c0c",
102
- light: "#1d4ed8",
103
- onLight: "#ffffff",
104
- },
105
- sidebar: "#080808",
86
+ accent: { key: "studio-blue", label: "Studio blue", ...TOKENS.brand["chat-studio"].accent },
87
+ sidebar: TOKENS.brand["chat-studio"].sidebar,
106
88
  grounds: ["black", "charcoal", "graphite", "paper", "mist"],
107
89
  defaultGround: { dark: "black", light: "paper" },
108
90
  };
@@ -114,15 +96,8 @@ export const chatStudioBrand = {
114
96
  export const loadrBrand = {
115
97
  id: "loadr",
116
98
  name: "Calliope AI Loadr",
117
- accent: {
118
- key: "loadr-blue",
119
- label: "Loadr blue",
120
- dark: "#89b4fa",
121
- onDark: "#11111b",
122
- light: "#1e66f5",
123
- onLight: "#ffffff",
124
- },
125
- sidebar: "#181825",
99
+ accent: { key: "loadr-blue", label: "Loadr blue", ...TOKENS.brand.loadr.accent },
100
+ sidebar: TOKENS.brand.loadr.sidebar,
126
101
  grounds: ["black", "charcoal", "graphite", "paper", "mist"],
127
102
  defaultGround: { dark: "charcoal", light: "paper" },
128
103
  };
@@ -135,16 +110,8 @@ export const loadrBrand = {
135
110
  export const cykickBrand = {
136
111
  id: "cykick",
137
112
  name: "Cykick",
138
- accent: {
139
- key: "cykick-cyan",
140
- label: "Cykick cyan",
141
- dark: "#22d3ee",
142
- onDark: "#07090e",
143
- // derived: no light accent in native-themes.json
144
- light: "#0e7490",
145
- onLight: "#ffffff",
146
- },
147
- sidebar: "#07090e",
113
+ accent: { key: "cykick-cyan", label: "Cykick cyan", ...TOKENS.brand.cykick.accent },
114
+ sidebar: TOKENS.brand.cykick.sidebar,
148
115
  grounds: ["black", "charcoal", "graphite", "paper", "mist"],
149
116
  defaultGround: { dark: "black", light: "mist" },
150
117
  defaultCorners: 0,
@@ -0,0 +1,19 @@
1
+ import type { StatusTone } from "../status/tones.js";
2
+ import type { RunStage } from "../workflow/run.js";
3
+ import type { ChatRunStatus, ChatStep, ChatView } from "./model.js";
4
+ export interface AgentProgressProps {
5
+ /** The run: a `ChatView`, or the part of one this reads. */
6
+ view: Pick<ChatView, "status" | "steps" | "error">;
7
+ /** Names the section. Defaults to "Agent progress". */
8
+ label?: string;
9
+ className?: string;
10
+ }
11
+ export declare const CHAT_RUN_TONE: Record<ChatRunStatus, StatusTone>;
12
+ /** A run's steps as the run timeline's stages. */
13
+ export declare function chatStepStages(steps: readonly ChatStep[]): RunStage[];
14
+ /**
15
+ * Where the agent's run stands: its status, and its steps on the library's
16
+ * RunTimeline, so a chat run reads like any other run. The error, when the
17
+ * run failed, sits beside the status.
18
+ */
19
+ export declare function AgentProgress({ view, label, className }: AgentProgressProps): import("react").JSX.Element;
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../lib/cn.js";
4
+ import { useUI } from "../provider/UIProvider.js";
5
+ import { StatusBadge } from "../status/StatusBadge.js";
6
+ import { RunTimeline } from "../workflow/RunTimeline.js";
7
+ export const CHAT_RUN_TONE = {
8
+ idle: "muted",
9
+ running: "info",
10
+ finished: "ok",
11
+ interrupted: "warn",
12
+ cancelled: "muted",
13
+ error: "error",
14
+ };
15
+ const STAGE = {
16
+ running: "running",
17
+ finished: "passed",
18
+ failed: "failed",
19
+ cancelled: "cancelled",
20
+ };
21
+ /** A run's steps as the run timeline's stages. */
22
+ export function chatStepStages(steps) {
23
+ return steps.map((step, i) => ({ id: String(i), label: step.name, status: STAGE[step.status] }));
24
+ }
25
+ /**
26
+ * Where the agent's run stands: its status, and its steps on the library's
27
+ * RunTimeline, so a chat run reads like any other run. The error, when the
28
+ * run failed, sits beside the status.
29
+ */
30
+ export function AgentProgress({ view, label, className }) {
31
+ const { messages } = useUI();
32
+ const t = messages.chat;
33
+ return (_jsxs("section", { "aria-label": label ?? t.progress, "data-slot": "agent-progress", className: cn("flex min-w-0 flex-col gap-3", className), children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx(StatusBadge, { tone: CHAT_RUN_TONE[view.status], pulse: view.status === "running", children: t.runStatus(view.status) }), view.status === "error" && view.error && (_jsx("p", { className: "min-w-0 text-sm [overflow-wrap:anywhere] text-danger-fg", children: view.error.message }))] }), view.steps.length > 0 && _jsx(RunTimeline, { stages: chatStepStages(view.steps), label: t.steps })] }));
34
+ }
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+ export interface ComposerProps {
3
+ /** The draft, when the host holds it. Leave it out and the composer keeps its own. */
4
+ value?: string;
5
+ onValueChange?: (value: string) => void;
6
+ /**
7
+ * Sends the trimmed draft. A composer that keeps its own draft clears it;
8
+ * a host that holds it clears it when it wants to.
9
+ */
10
+ onSubmit: (text: string) => void;
11
+ /** A run is in flight: send is held, and stop shows when `onStop` is given. */
12
+ running?: boolean;
13
+ onStop?: () => void;
14
+ disabled?: boolean;
15
+ placeholder?: string;
16
+ /** Names the field. Defaults to "Message". */
17
+ label?: string;
18
+ /** Above the field: the files the host has attached, as chips. */
19
+ attachments?: React.ReactNode;
20
+ /** Beside send: an attach button, a ModelPicker. */
21
+ actions?: React.ReactNode;
22
+ autoFocus?: boolean;
23
+ className?: string;
24
+ }
25
+ /**
26
+ * Where the person writes. Multi-line, growing with the text; Enter sends,
27
+ * Shift+Enter breaks the line, and nothing sends while an input method is
28
+ * composing. While a run is in flight, send is held and stop is offered.
29
+ * Attachments and actions are slots: the host owns files and models.
30
+ */
31
+ export declare function Composer({ value, onValueChange, onSubmit, running, onStop, disabled, placeholder, label, attachments, actions, autoFocus, className, }: ComposerProps): React.JSX.Element;
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ArrowUpIcon, SquareIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Button } from "../primitives/Button.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ /**
9
+ * Where the person writes. Multi-line, growing with the text; Enter sends,
10
+ * Shift+Enter breaks the line, and nothing sends while an input method is
11
+ * composing. While a run is in flight, send is held and stop is offered.
12
+ * Attachments and actions are slots: the host owns files and models.
13
+ */
14
+ export function Composer({ value, onValueChange, onSubmit, running = false, onStop, disabled = false, placeholder, label, attachments, actions, autoFocus, className, }) {
15
+ const { messages } = useUI();
16
+ const t = messages.chat;
17
+ const hintId = React.useId();
18
+ const [own, setOwn] = React.useState("");
19
+ const draft = value ?? own;
20
+ const canSend = !disabled && !running && draft.trim() !== "";
21
+ const change = (next) => {
22
+ if (value === undefined)
23
+ setOwn(next);
24
+ onValueChange?.(next);
25
+ };
26
+ const submit = () => {
27
+ if (!canSend)
28
+ return;
29
+ onSubmit(draft.trim());
30
+ if (value === undefined)
31
+ setOwn("");
32
+ };
33
+ return (_jsxs("form", { "data-slot": "composer", onSubmit: (event) => {
34
+ event.preventDefault();
35
+ submit();
36
+ }, className: cn("flex min-w-0 flex-col rounded-lg border border-input bg-background focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 dark:bg-input/30", disabled && "opacity-50", className), children: [attachments && (_jsx("div", { role: "group", "aria-label": t.attachments, className: "flex min-w-0 flex-wrap gap-2 px-3 pt-2", children: attachments })), _jsx("textarea", { "aria-label": label ?? t.composer, "aria-describedby": hintId, value: draft, disabled: disabled, placeholder: placeholder ?? t.placeholder, rows: 1,
37
+ // eslint-disable-next-line jsx-a11y/no-autofocus -- the host asks for it, on a chat screen
38
+ autoFocus: autoFocus, onChange: (event) => change(event.target.value), onKeyDown: (event) => {
39
+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing)
40
+ return;
41
+ event.preventDefault();
42
+ submit();
43
+ }, className: "field-sizing-content max-h-48 min-h-10 w-full min-w-0 resize-none bg-transparent px-3 py-2 text-base outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed md:text-sm" }), _jsx("p", { id: hintId, className: "sr-only", children: t.composerHint }), _jsxs("div", { className: "flex min-w-0 items-center gap-2 px-2 pb-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-2", children: actions }), running && onStop ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: onStop, children: [_jsx(SquareIcon, { "aria-hidden": true }), t.stop] })) : (_jsxs(Button, { type: "submit", size: "sm", disabled: !canSend, children: [_jsx(ArrowUpIcon, { "aria-hidden": true }), t.send] }))] })] }));
44
+ }
@@ -0,0 +1,23 @@
1
+ import type * as React from "react";
2
+ import type { ChatMessage } from "./model.js";
3
+ /**
4
+ * Renders a message's text: the host's markdown renderer, say. It receives
5
+ * the text so far (it grows while the message streams) and must treat it as
6
+ * untrusted. Without one, the text shows as plain text.
7
+ */
8
+ export type ChatContentRenderer = (content: string, message: ChatMessage) => React.ReactNode;
9
+ export interface MessageBubbleProps {
10
+ message: ChatMessage;
11
+ renderContent?: ChatContentRenderer;
12
+ /** Beside the speaker: a time, a copy button. */
13
+ meta?: React.ReactNode;
14
+ className?: string;
15
+ }
16
+ /**
17
+ * One message, by role: the person's on the right in a muted bubble, the
18
+ * assistant's full width, system and developer notes dashed and quiet, a tool
19
+ * message in mono. While it streams, an empty one says it is thinking and a
20
+ * growing one shows a caret. Text is plain unless the host passes
21
+ * `renderContent`; plain text is never parsed as HTML.
22
+ */
23
+ export declare function MessageBubble({ message, renderContent, meta, className }: MessageBubbleProps): React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { cn } from "../lib/cn.js";
4
+ import { useUI } from "../provider/UIProvider.js";
5
+ import { StatusDot } from "../status/StatusDot.js";
6
+ const ROLE_CLASS = {
7
+ user: "ml-auto max-w-[85%] rounded-lg bg-muted px-3 py-2",
8
+ assistant: "",
9
+ system: "rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground",
10
+ developer: "rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground",
11
+ tool: "rounded-lg border bg-card px-3 py-2 font-mono text-xs",
12
+ };
13
+ /**
14
+ * One message, by role: the person's on the right in a muted bubble, the
15
+ * assistant's full width, system and developer notes dashed and quiet, a tool
16
+ * message in mono. While it streams, an empty one says it is thinking and a
17
+ * growing one shows a caret. Text is plain unless the host passes
18
+ * `renderContent`; plain text is never parsed as HTML.
19
+ */
20
+ export function MessageBubble({ message, renderContent, meta, className }) {
21
+ const { messages } = useUI();
22
+ const t = messages.chat;
23
+ const speaker = message.name ?? t.role(message.role);
24
+ const empty = message.content === "";
25
+ return (_jsxs("article", { "aria-label": t.messageFrom(speaker), "aria-busy": message.streaming || undefined, "data-slot": "message-bubble", "data-role": message.role, className: cn("flex min-w-0 flex-col gap-1", ROLE_CLASS[message.role], className), children: [(message.role !== "user" || meta) && (_jsxs("header", { className: "flex min-w-0 items-center gap-2 text-xs text-muted-foreground", children: [message.role !== "user" && _jsx("span", { className: "font-medium", children: speaker }), meta && _jsx("span", { className: "ml-auto flex items-center gap-2", children: meta })] })), message.streaming && empty ? (_jsxs("p", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(StatusDot, { tone: "info", pulse: true }), t.thinking] })) : (_jsxs("div", { className: "min-w-0 text-sm [overflow-wrap:anywhere]", children: [renderContent ? (renderContent(message.content, message)) : (_jsx("p", { className: "whitespace-pre-wrap", children: message.content })), message.streaming && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: "ml-0.5 inline-block h-4 w-1.5 translate-y-0.5 bg-foreground/60 motion-safe:animate-pulse" }), _jsx("span", { className: "sr-only", children: t.writing })] }))] }))] }));
26
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ import { type ChatContentRenderer } from "./MessageBubble.js";
3
+ import type { ChatToolCall, ChatView } from "./model.js";
4
+ import { type ToolCallCardProps } from "./ToolCallCard.js";
5
+ export interface MessageListProps {
6
+ /** The conversation: a `ChatView`, or the part of one the list reads. */
7
+ view: Pick<ChatView, "messages" | "toolCalls" | "timeline" | "status" | "error">;
8
+ renderContent?: ChatContentRenderer;
9
+ /**
10
+ * Draws a tool call the host has its own component for; return undefined
11
+ * for the default `ToolCallCard`.
12
+ */
13
+ renderToolCall?: (call: ChatToolCall) => React.ReactNode;
14
+ /** The tool's name for people, per call. */
15
+ toolTitle?: (call: ChatToolCall) => string | undefined;
16
+ onApprove?: ToolCallCardProps["onApprove"];
17
+ onDeny?: ToolCallCardProps["onDeny"];
18
+ /** Offered beside a failed run's error. */
19
+ onRetry?: () => void;
20
+ /** The first page of history is loading. */
21
+ loading?: boolean;
22
+ /** Replaces the default empty state. */
23
+ empty?: React.ReactNode;
24
+ /** Names the list. Defaults to "Conversation". */
25
+ label?: string;
26
+ className?: string;
27
+ }
28
+ /**
29
+ * The conversation in arrival order: message bubbles with tool call cards
30
+ * between them, a failed run's error at the end. It follows the stream while
31
+ * the person is at the bottom and stops following when they scroll up, with
32
+ * a button back to the latest. Each bubble re-renders only when its message
33
+ * changes, so a long conversation streams without virtualization; a host
34
+ * with thousands of turns pages its history.
35
+ */
36
+ export declare function MessageList({ view, renderContent, renderToolCall, toolTitle, onApprove, onDeny, onRetry, loading, empty, label, className, }: MessageListProps): React.JSX.Element;
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { ArrowDownIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Button } from "../primitives/Button.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { EmptyState } from "../states/EmptyState.js";
9
+ import { LoadingState } from "../states/LoadingState.js";
10
+ import { MessageBubble } from "./MessageBubble.js";
11
+ import { ToolCallCard } from "./ToolCallCard.js";
12
+ const MemoBubble = React.memo(MessageBubble);
13
+ const MemoToolCall = React.memo(ToolCallCard);
14
+ /** How close to the end, in pixels, still counts as reading the latest. */
15
+ const PINNED = 48;
16
+ /**
17
+ * The conversation in arrival order: message bubbles with tool call cards
18
+ * between them, a failed run's error at the end. It follows the stream while
19
+ * the person is at the bottom and stops following when they scroll up, with
20
+ * a button back to the latest. Each bubble re-renders only when its message
21
+ * changes, so a long conversation streams without virtualization; a host
22
+ * with thousands of turns pages its history.
23
+ */
24
+ export function MessageList({ view, renderContent, renderToolCall, toolTitle, onApprove, onDeny, onRetry, loading = false, empty, label, className, }) {
25
+ const { messages } = useUI();
26
+ const t = messages.chat;
27
+ const scroller = React.useRef(null);
28
+ const [pinned, setPinned] = React.useState(true);
29
+ const byId = React.useMemo(() => ({
30
+ messages: new Map(view.messages.map((m) => [m.id, m])),
31
+ calls: new Map(view.toolCalls.map((c) => [c.id, c])),
32
+ }), [view.messages, view.toolCalls]);
33
+ React.useLayoutEffect(() => {
34
+ const el = scroller.current;
35
+ if (pinned && el)
36
+ el.scrollTop = el.scrollHeight;
37
+ }, [pinned, byId, view.error]);
38
+ const jump = () => {
39
+ const el = scroller.current;
40
+ if (el)
41
+ el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
42
+ setPinned(true);
43
+ };
44
+ let body;
45
+ if (loading)
46
+ body = _jsx(LoadingState, { variant: "rows", rows: 3 });
47
+ else if (view.timeline.length === 0 && !view.error)
48
+ body = empty ?? _jsx(EmptyState, { title: t.empty, description: t.emptyDescription });
49
+ else
50
+ body = (_jsxs(_Fragment, { children: [view.timeline.map((item) => {
51
+ if (item.kind === "message") {
52
+ const message = byId.messages.get(item.id);
53
+ return message ? (_jsx(MemoBubble, { message: message, renderContent: renderContent }, `m:${item.id}`)) : null;
54
+ }
55
+ const call = byId.calls.get(item.id);
56
+ if (!call)
57
+ return null;
58
+ const custom = renderToolCall?.(call);
59
+ return custom !== undefined ? (_jsx(React.Fragment, { children: custom }, `t:${item.id}`)) : (_jsx(MemoToolCall, { call: call, title: toolTitle?.(call), onApprove: onApprove, onDeny: onDeny }, `t:${item.id}`));
60
+ }), view.status === "error" && view.error && (_jsxs("div", { role: "alert", className: "flex min-w-0 flex-col gap-2 rounded-lg border border-danger-border bg-danger-bg px-3 py-2 text-sm text-danger-fg sm:flex-row sm:items-center", children: [_jsxs("p", { className: "min-w-0 flex-1 [overflow-wrap:anywhere]", children: [_jsx("span", { className: "font-medium", children: t.runFailed }), ": ", view.error.message] }), onRetry && (_jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, children: t.retry }))] }))] }));
61
+ return (_jsxs("div", { "data-slot": "message-list", className: cn("relative flex min-h-0 flex-col", className), children: [_jsx("div", { ref: scroller, role: "log", "aria-label": label ?? t.conversation, "aria-busy": view.status === "running" || loading || undefined,
62
+ // A scrolling region is reached by keyboard (WCAG 2.1.1).
63
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
64
+ tabIndex: 0, onScroll: (event) => {
65
+ const el = event.currentTarget;
66
+ setPinned(el.scrollHeight - el.scrollTop - el.clientHeight < PINNED);
67
+ }, className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children: body }), !pinned && (_jsxs(Button, { variant: "outline", size: "sm", onClick: jump, className: "absolute bottom-3 left-1/2 -translate-x-1/2 shadow-md", children: [_jsx(ArrowDownIcon, { "aria-hidden": true }), t.jumpToLatest] }))] }));
68
+ }
@@ -0,0 +1,17 @@
1
+ import { type SelectOption } from "../primitives/Select.js";
2
+ export interface ModelPickerProps {
3
+ /** The models this host offers, in its words. The package names none. */
4
+ options: readonly SelectOption[];
5
+ value: string;
6
+ onValueChange: (value: string) => void;
7
+ disabled?: boolean;
8
+ /** Names the picker. Defaults to "Model". */
9
+ label?: string;
10
+ className?: string;
11
+ }
12
+ /**
13
+ * The model for the next turn, from the options the host passes: which
14
+ * models exist, which the person may use and what each costs are the host's.
15
+ * Small enough for a composer's actions.
16
+ */
17
+ export declare function ModelPicker({ options, value, onValueChange, disabled, label, className, }: ModelPickerProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useUI } from "../provider/UIProvider.js";
4
+ import { Select } from "../primitives/Select.js";
5
+ /**
6
+ * The model for the next turn, from the options the host passes: which
7
+ * models exist, which the person may use and what each costs are the host's.
8
+ * Small enough for a composer's actions.
9
+ */
10
+ export function ModelPicker({ options, value, onValueChange, disabled, label, className, }) {
11
+ const { messages } = useUI();
12
+ return (_jsx(Select, { options: options, value: value, onValueChange: onValueChange, disabled: disabled, "aria-label": label ?? messages.chat.model, size: "sm", className: className }));
13
+ }
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ import { type ChatToolCall } from "./model.js";
3
+ export interface ToolCallCardProps {
4
+ call: ChatToolCall;
5
+ /** The tool's name for people ("Run SQL"). Defaults to the call's name. */
6
+ title?: string;
7
+ /** Draws the result: a table, a chart. Defaults to the result as text, pretty-printed when it is JSON. */
8
+ renderResult?: (call: ChatToolCall) => React.ReactNode;
9
+ /** Whether the details start open. A call waiting for approval always starts open. */
10
+ defaultOpen?: boolean;
11
+ /**
12
+ * Approves a call the run paused on, after the confirm step. Awaited; see
13
+ * ConfirmDialog. The host resumes the run (AG-UI `resume`); the card only
14
+ * reports the decision.
15
+ */
16
+ onApprove?: (call: ChatToolCall) => Promise<unknown> | unknown;
17
+ /** Denies it, after the confirm step, with the reason the person gave. */
18
+ onDeny?: (call: ChatToolCall, reason: string) => Promise<unknown> | unknown;
19
+ className?: string;
20
+ }
21
+ /**
22
+ * One tool call: its name and status, its arguments as they stream and its
23
+ * result or error behind a disclosure, and, when the run paused on it,
24
+ * approve and deny through the library's confirm flow (a denial always with
25
+ * a reason). Host-driven: it never runs or resumes anything.
26
+ */
27
+ export declare function ToolCallCard({ call, title, renderResult, defaultOpen, onApprove, onDeny, className, }: ToolCallCardProps): React.JSX.Element;
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronRightIcon, WrenchIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { ConfirmDialog } from "../overlays/ConfirmDialog.js";
7
+ import { Button } from "../primitives/Button.js";
8
+ import { useUI } from "../provider/UIProvider.js";
9
+ import { StatusBadge } from "../status/StatusBadge.js";
10
+ import { parseToolCallArgs } from "./model.js";
11
+ const TONE = {
12
+ streaming: "info",
13
+ running: "info",
14
+ complete: "ok",
15
+ failed: "error",
16
+ cancelled: "muted",
17
+ approval: "warn",
18
+ };
19
+ /** JSON text shown indented when it parses, as it came otherwise. */
20
+ const pretty = (text) => {
21
+ const value = parseToolCallArgs(text);
22
+ return value === undefined ? text : JSON.stringify(value, null, 2);
23
+ };
24
+ /**
25
+ * One tool call: its name and status, its arguments as they stream and its
26
+ * result or error behind a disclosure, and, when the run paused on it,
27
+ * approve and deny through the library's confirm flow (a denial always with
28
+ * a reason). Host-driven: it never runs or resumes anything.
29
+ */
30
+ export function ToolCallCard({ call, title, renderResult, defaultOpen = false, onApprove, onDeny, className, }) {
31
+ const { messages } = useUI();
32
+ const t = messages.chat;
33
+ const id = React.useId();
34
+ const name = title ?? call.name;
35
+ const waiting = call.approval !== undefined;
36
+ const [open, setOpen] = React.useState(defaultOpen || waiting);
37
+ const [confirming, setConfirming] = React.useState(null);
38
+ // A call that comes to wait for approval opens, so the person sees what they approve.
39
+ const [wasWaiting, setWasWaiting] = React.useState(waiting);
40
+ if (waiting !== wasWaiting) {
41
+ setWasWaiting(waiting);
42
+ if (waiting)
43
+ setOpen(true);
44
+ }
45
+ const status = waiting ? "approval" : call.status;
46
+ const detailsId = `${id}-details`;
47
+ return (_jsxs("section", { "aria-label": t.toolCall(name), "data-slot": "tool-call-card", "data-status": status, className: cn("flex min-w-0 flex-col rounded-lg border bg-card text-sm", className), children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2 px-3 py-2", children: [_jsx(WrenchIcon, { "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("button", { type: "button", "aria-expanded": open, "aria-controls": detailsId, onClick: () => setOpen((o) => !o), className: "inline-flex min-w-0 items-center gap-1 rounded-sm text-left outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx(ChevronRightIcon, { "aria-hidden": true, className: cn("size-3.5 shrink-0 transition-transform", open && "rotate-90") }), _jsx("span", { className: "font-mono text-xs font-medium [overflow-wrap:anywhere]", children: name }), " ", _jsx("span", { className: "sr-only", children: t.details })] }), _jsx(StatusBadge, { tone: TONE[status], pulse: status === "streaming" || status === "running", className: "ml-auto", children: t.toolStatus(status) })] }), open && (_jsxs("div", { id: detailsId, className: "flex min-w-0 flex-col gap-2 border-t px-3 py-2", children: [_jsx(Block, { label: t.arguments, children: call.args ? pretty(call.args) : t.noArguments }), call.error !== undefined && (_jsx(Block, { label: t.toolError, tone: "error", children: call.error })), call.result !== undefined &&
48
+ (renderResult ? (_jsx("div", { className: "min-w-0", children: renderResult(call) })) : (_jsx(Block, { label: t.result, children: pretty(call.result) })))] })), waiting && (onApprove || onDeny) && (_jsxs("div", { className: "flex min-w-0 flex-col gap-2 border-t px-3 py-2 sm:flex-row sm:items-center", children: [_jsx("p", { className: "min-w-0 flex-1 [overflow-wrap:anywhere]", children: call.approval?.message ?? t.approvalNeeded }), _jsxs("div", { className: "flex gap-2", children: [onDeny && (_jsx(Button, { variant: "outline", size: "sm", onClick: () => setConfirming("deny"), children: t.deny })), onApprove && (_jsx(Button, { size: "sm", onClick: () => setConfirming("approve"), children: t.approve }))] })] })), _jsx(ConfirmDialog, { open: confirming === "approve", onOpenChange: (next) => !next && setConfirming(null), title: t.approveTitle(name), description: call.approval?.message ?? t.approveDescription, confirmLabel: t.approve, onConfirm: () => onApprove?.(call) }), _jsx(ConfirmDialog, { open: confirming === "deny", onOpenChange: (next) => !next && setConfirming(null), title: t.denyTitle(name), description: t.denyDescription, confirmLabel: t.deny, destructive: true, reason: { label: t.denyReason }, onConfirm: (reason) => onDeny?.(call, reason) })] }));
49
+ }
50
+ function Block({ label, tone, children, }) {
51
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx("p", { className: "font-mono text-2xs font-medium tracking-wider text-muted-foreground uppercase", children: label }), _jsx("pre", { role: "region", "aria-label": label, tabIndex: 0, className: cn("max-h-64 min-w-0 overflow-auto rounded-md bg-muted p-2 font-mono text-xs [overflow-wrap:anywhere] whitespace-pre-wrap outline-none focus-visible:ring-2 focus-visible:ring-ring", tone === "error" && "bg-danger-bg text-danger-fg"), children: children })] }));
52
+ }