@calliopelabs/ui 0.13.0 → 0.16.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 (115) hide show
  1. package/README.md +192 -4
  2. package/dist/agent/AgentChat.d.ts +63 -0
  3. package/dist/agent/AgentChat.js +117 -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/WorkflowRenderers.d.ts +61 -0
  11. package/dist/agent/WorkflowRenderers.js +102 -0
  12. package/dist/agent/controller.d.ts +167 -0
  13. package/dist/agent/controller.js +496 -0
  14. package/dist/agent/copilotkit/CopilotKitAgentProvider.d.ts +41 -0
  15. package/dist/agent/copilotkit/CopilotKitAgentProvider.js +145 -0
  16. package/dist/agent/copilotkit/index.d.ts +6 -0
  17. package/dist/agent/copilotkit/index.js +6 -0
  18. package/dist/agent/engine.d.ts +41 -0
  19. package/dist/agent/engine.js +93 -0
  20. package/dist/agent/index.d.ts +17 -0
  21. package/dist/agent/index.js +17 -0
  22. package/dist/agent/mock.d.ts +56 -0
  23. package/dist/agent/mock.js +143 -0
  24. package/dist/agent/renderers.d.ts +49 -0
  25. package/dist/agent/renderers.js +27 -0
  26. package/dist/agent/surface.d.ts +30 -0
  27. package/dist/agent/surface.js +90 -0
  28. package/dist/agent/tool.d.ts +98 -0
  29. package/dist/agent/tool.js +81 -0
  30. package/dist/appearance/brands.d.ts +31 -12
  31. package/dist/appearance/brands.js +32 -51
  32. package/dist/chat/AgentProgress.d.ts +19 -0
  33. package/dist/chat/AgentProgress.js +34 -0
  34. package/dist/chat/Composer.d.ts +31 -0
  35. package/dist/chat/Composer.js +44 -0
  36. package/dist/chat/MessageBubble.d.ts +23 -0
  37. package/dist/chat/MessageBubble.js +26 -0
  38. package/dist/chat/MessageList.d.ts +43 -0
  39. package/dist/chat/MessageList.js +71 -0
  40. package/dist/chat/ModelPicker.d.ts +17 -0
  41. package/dist/chat/ModelPicker.js +13 -0
  42. package/dist/chat/ToolCallCard.d.ts +27 -0
  43. package/dist/chat/ToolCallCard.js +52 -0
  44. package/dist/chat/model.d.ts +123 -0
  45. package/dist/chat/model.js +337 -0
  46. package/dist/chat/patch.d.ts +12 -0
  47. package/dist/chat/patch.js +149 -0
  48. package/dist/chat/useAgUiRun.d.ts +52 -0
  49. package/dist/chat/useAgUiRun.js +137 -0
  50. package/dist/css/appearance.js +1 -1
  51. package/dist/css/calliope.css +5 -0
  52. package/dist/css/tokens.css +5 -0
  53. package/dist/index.d.ts +15 -1
  54. package/dist/index.js +16 -1
  55. package/dist/primitives/Badge.d.ts +14 -0
  56. package/dist/primitives/Badge.js +25 -0
  57. package/dist/primitives/Button.d.ts +1 -1
  58. package/dist/primitives/ContextMenu.d.ts +18 -0
  59. package/dist/primitives/ContextMenu.js +34 -0
  60. package/dist/primitives/ScrollArea.d.ts +16 -0
  61. package/dist/primitives/ScrollArea.js +11 -0
  62. package/dist/primitives/Toast.d.ts +37 -0
  63. package/dist/primitives/Toast.js +43 -0
  64. package/dist/primitives/Tooltip.d.ts +16 -0
  65. package/dist/primitives/Tooltip.js +26 -0
  66. package/dist/provider/messages.d.ts +135 -0
  67. package/dist/provider/messages.js +131 -0
  68. package/dist/theme/css-layer.js +2 -1
  69. package/dist/theme/mappings/palette.d.ts +52 -0
  70. package/dist/theme/mappings/palette.js +208 -0
  71. package/dist/theme/mappings/render.d.ts +9 -0
  72. package/dist/theme/mappings/render.js +520 -0
  73. package/dist/theme/mappings/targets.d.ts +43 -0
  74. package/dist/theme/mappings/targets.js +222 -0
  75. package/dist/theme/tokens.d.ts +10 -9
  76. package/dist/theme/tokens.generated.d.ts +356 -0
  77. package/dist/theme/tokens.generated.js +360 -0
  78. package/dist/theme/tokens.js +20 -264
  79. package/dist/tokens/README.jupyterlab.md +125 -0
  80. package/dist/tokens/README.md +15 -0
  81. package/dist/tokens/README.native.md +29 -0
  82. package/dist/tokens/README.plain.md +84 -0
  83. package/dist/tokens/README.python.md +33 -0
  84. package/dist/tokens/README.vscode.md +114 -0
  85. package/dist/tokens/jupyterlab.css +1710 -0
  86. package/dist/tokens/plain.css +930 -0
  87. package/dist/tokens/tokens.native.d.ts +85 -0
  88. package/dist/tokens/tokens.native.js +773 -0
  89. package/dist/tokens/tokens.py +1043 -0
  90. package/dist/tokens/vscode.css +1138 -0
  91. package/dist/workbench/FileBrowser.d.ts +62 -0
  92. package/dist/workbench/FileBrowser.js +117 -0
  93. package/dist/workbench/FileGrid.d.ts +13 -0
  94. package/dist/workbench/FileGrid.js +114 -0
  95. package/dist/workbench/FileIcon.d.ts +10 -0
  96. package/dist/workbench/FileIcon.js +24 -0
  97. package/dist/workbench/FileList.d.ts +35 -0
  98. package/dist/workbench/FileList.js +160 -0
  99. package/dist/workbench/FilePreview.d.ts +66 -0
  100. package/dist/workbench/FilePreview.js +108 -0
  101. package/dist/workbench/FileTree.d.ts +37 -0
  102. package/dist/workbench/FileTree.js +218 -0
  103. package/dist/workbench/UploadDropzone.d.ts +52 -0
  104. package/dist/workbench/UploadDropzone.js +115 -0
  105. package/dist/workbench/dnd.d.ts +17 -0
  106. package/dist/workbench/dnd.js +77 -0
  107. package/dist/workbench/index.d.ts +16 -0
  108. package/dist/workbench/index.js +16 -0
  109. package/dist/workbench/model.d.ts +72 -0
  110. package/dist/workbench/model.js +214 -0
  111. package/dist/workbench/selection.d.ts +16 -0
  112. package/dist/workbench/selection.js +46 -0
  113. package/dist/workbench/typeahead.d.ts +15 -0
  114. package/dist/workbench/typeahead.js +22 -0
  115. package/package.json +39 -2
@@ -0,0 +1,176 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { HttpAgent } from "@ag-ui/client";
4
+ import * as React from "react";
5
+ import { AgentController, } from "./controller.js";
6
+ import { createDirectEngine } from "./engine.js";
7
+ import { toolJsonSchema } from "./tool.js";
8
+ const AgentContext = React.createContext(null);
9
+ /** The controller of the nearest agent provider, or null outside one. */
10
+ export function useAgentController() {
11
+ return React.useContext(AgentContext);
12
+ }
13
+ function useController() {
14
+ const controller = React.useContext(AgentContext);
15
+ if (!controller) {
16
+ throw new Error("No agent here: wrap the app in CalliopeAgentProvider or CopilotKitAgentProvider.");
17
+ }
18
+ return controller;
19
+ }
20
+ /** Keeps a value's identity while its JSON stays the same, so inline arrays do not re-register. */
21
+ function useStable(value) {
22
+ const key = safeKey(value);
23
+ // The key is the value's content; a new array with the same content keeps the old identity.
24
+ // eslint-disable-next-line react-hooks/exhaustive-deps
25
+ return React.useMemo(() => value, [key]);
26
+ }
27
+ /** Sets an HttpAgent's request headers for its next run. */
28
+ function assignHeaders(agent, headers) {
29
+ if (agent instanceof HttpAgent && headers)
30
+ agent.headers = { ...headers };
31
+ }
32
+ function safeKey(value) {
33
+ try {
34
+ return JSON.stringify(value, (_k, v) => typeof v === "function" ? `fn:${v.name}` : v);
35
+ }
36
+ catch {
37
+ return String(Math.random());
38
+ }
39
+ }
40
+ /**
41
+ * Hosts a controller for any engine; the providers below and the CopilotKit
42
+ * one are this with their engine. Registers the option-level tools, context
43
+ * and suggestions, and connects after mount.
44
+ */
45
+ export function AgentHost({ createEngine, engineKey, options, children, }) {
46
+ const [controller] = React.useState(() => new AgentController(createEngine, options));
47
+ const engineRef = React.useRef(engineKey);
48
+ React.useEffect(() => {
49
+ if (engineRef.current === engineKey)
50
+ return;
51
+ engineRef.current = engineKey;
52
+ controller.attach(createEngine);
53
+ // createEngine follows engineKey; the key is the identity that matters.
54
+ // eslint-disable-next-line react-hooks/exhaustive-deps
55
+ }, [controller, engineKey]);
56
+ React.useEffect(() => controller.setOptions(options));
57
+ React.useEffect(() => controller.connect(), [controller]);
58
+ const { tools, context, suggestions } = options;
59
+ React.useEffect(() => {
60
+ if (!tools?.length)
61
+ return;
62
+ const offs = tools.map((tool) => controller.registerTool(tool));
63
+ return () => offs.forEach((off) => off());
64
+ }, [controller, tools]);
65
+ const stableContext = useStable(context);
66
+ React.useEffect(() => {
67
+ if (!stableContext?.length)
68
+ return;
69
+ const offs = stableContext.map((entry) => controller.addContext(entry));
70
+ return () => offs.forEach((off) => off());
71
+ }, [controller, stableContext]);
72
+ const stableSuggestions = useStable(suggestions);
73
+ React.useEffect(() => (stableSuggestions?.length ? controller.addSuggestions(stableSuggestions) : undefined), [controller, stableSuggestions]);
74
+ return _jsx(AgentContext.Provider, { value: controller, children: children });
75
+ }
76
+ /**
77
+ * The agent for a page tree, over a plain AG-UI endpoint (`url`) or any
78
+ * AG-UI agent (`agent`). Headless: it renders nothing of its own; put
79
+ * `CalliopeAgent` (a bubble, a rail, inline or a page) or the chat kit's
80
+ * components under it, and read it with `useCalliopeAgent`. For a CopilotKit
81
+ * runtime use `CopilotKitAgentProvider` from `@calliopelabs/ui/agent/copilotkit`.
82
+ */
83
+ export function CalliopeAgentProvider({ agent: given, url, headers, threadId, forwardedProps, children, ...options }) {
84
+ const agent = React.useMemo(() => {
85
+ if (given)
86
+ return given;
87
+ if (!url)
88
+ throw new Error("CalliopeAgentProvider needs an agent or a url.");
89
+ return new HttpAgent({ url, ...(threadId ? { threadId } : {}) });
90
+ // threadId only seeds a new agent; switchThread moves it afterwards.
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, [given, url]);
93
+ React.useEffect(() => {
94
+ assignHeaders(agent, headers);
95
+ }, [agent, headers]);
96
+ const props = React.useRef(forwardedProps);
97
+ React.useEffect(() => {
98
+ props.current = forwardedProps;
99
+ });
100
+ return (_jsx(AgentHost, { engineKey: agent, createEngine: (invoke) => createDirectEngine(agent, invoke), options: { ...options, forwardedProps: () => props.current }, children: children }));
101
+ }
102
+ /** The nearest agent: its conversation, state and actions. Re-renders on every change. */
103
+ export function useCalliopeAgent() {
104
+ const controller = useController();
105
+ const snapshot = React.useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
106
+ return React.useMemo(() => ({
107
+ ...snapshot,
108
+ send: controller.send,
109
+ stop: controller.stop,
110
+ regenerate: controller.regenerate,
111
+ approve: controller.approve,
112
+ deny: controller.deny,
113
+ resolveInterrupt: controller.resolveInterrupt,
114
+ respond: controller.respond,
115
+ getState: controller.getState,
116
+ setState: controller.setState,
117
+ registerTool: controller.registerTool,
118
+ addContext: controller.addContext,
119
+ switchThread: controller.switchThread,
120
+ newThread: controller.newThread,
121
+ setVisible: controller.setVisible,
122
+ agent: controller.agent,
123
+ controller,
124
+ }), [controller, snapshot]);
125
+ }
126
+ /**
127
+ * Offers a tool to the agent while the component is mounted (a whiteboard's
128
+ * tools only while it is open). The handler and renderer may change every
129
+ * render; the tool re-registers only when its name, description, schema,
130
+ * availability or follow-up change.
131
+ */
132
+ export function useAgentTool(tool) {
133
+ const controller = useController();
134
+ const latest = React.useRef(tool);
135
+ React.useEffect(() => {
136
+ latest.current = tool;
137
+ });
138
+ const schema = safeKey(toolJsonSchema(tool));
139
+ const hasHandler = tool.handler !== undefined;
140
+ const hasRender = tool.render !== undefined;
141
+ React.useEffect(() => {
142
+ const t = latest.current;
143
+ return controller.registerTool({
144
+ name: t.name,
145
+ ...(t.description !== undefined ? { description: t.description } : {}),
146
+ ...(t.parameters !== undefined ? { parameters: t.parameters } : {}),
147
+ ...(t.followUp !== undefined ? { followUp: t.followUp } : {}),
148
+ ...(t.available !== undefined ? { available: t.available } : {}),
149
+ ...(hasHandler
150
+ ? { handler: (args, context) => latest.current.handler?.(args, context) }
151
+ : {}),
152
+ ...(hasRender ? { render: (props) => latest.current.render?.(props) } : {}),
153
+ });
154
+ }, [
155
+ controller,
156
+ tool.name,
157
+ tool.description,
158
+ tool.followUp,
159
+ tool.available,
160
+ schema,
161
+ hasHandler,
162
+ hasRender,
163
+ ]);
164
+ }
165
+ /** Tells the agent about the page while mounted: a readable. Re-sent when the value's JSON changes. */
166
+ export function useAgentReadable(description, value) {
167
+ const controller = useController();
168
+ const stable = useStable(value);
169
+ React.useEffect(() => controller.addContext({ description, value: stable }), [controller, description, stable]);
170
+ }
171
+ /** Offers prompts as chips while mounted (a page's starting points). */
172
+ export function useAgentSuggestions(suggestions) {
173
+ const controller = useController();
174
+ const stable = useStable(suggestions);
175
+ React.useEffect(() => (stable.length ? controller.addSuggestions(stable) : undefined), [controller, stable]);
176
+ }
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ import { type AgentChatProps } from "./AgentChat.js";
3
+ export type CalliopeAgentMode = "bubble" | "rail" | "inline" | "page";
4
+ export interface CalliopeAgentProps extends Omit<AgentChatProps, "onClose" | "hideHeader" | "expanded" | "onExpandedChange"> {
5
+ /**
6
+ * `bubble`: a launcher at the page's corner opening a floating panel (a
7
+ * drawer on phones). `rail`: a RailPanel for AppShell's `rightRail` (the
8
+ * shell's drawer on phones), with the launcher while closed. `inline`: the
9
+ * conversation in place, filling its box. `page`: the conversation as the
10
+ * page, centred to a reading width.
11
+ */
12
+ mode: CalliopeAgentMode;
13
+ /** Open (bubble, rail), when the host holds it. */
14
+ open?: boolean;
15
+ defaultOpen?: boolean;
16
+ onOpenChange?: (open: boolean) => void;
17
+ /** Remembers open or closed per person in this browser under this key ("studio.assistant.open.<user>"). */
18
+ persistKey?: string;
19
+ /** Toggles the bubble or rail: "mod+/" (Cmd or Ctrl and /) by default; false for none. */
20
+ shortcut?: string | false;
21
+ /** The launcher's mark. Defaults to the product's logo mark, then a sparkle. */
22
+ launcherIcon?: React.ReactNode;
23
+ /** Hides the launcher (a rail opened from the host's own header button). */
24
+ hideLauncher?: boolean;
25
+ /** Rail: its width in px, and resizable when `onRailWidthChange` is given. */
26
+ railWidth?: number;
27
+ onRailWidthChange?: (width: number) => void;
28
+ /** Rail: taking the content column (pair with AppShell's `rightRailExpanded`). */
29
+ expanded?: boolean;
30
+ onExpandedChange?: (expanded: boolean) => void;
31
+ /** Hides the header for inline and page surfaces that have their own. */
32
+ hideHeader?: boolean;
33
+ className?: string;
34
+ }
35
+ /**
36
+ * The embedded agent of a Calliope AI product, one primitive for every
37
+ * surface: a floating bubble, the shell's right rail, inline in a page or a
38
+ * page of its own. Reads the nearest `CalliopeAgentProvider` (or the
39
+ * CopilotKit one); draws everything with the chat kit and the product's
40
+ * tokens, so it carries each product's and each deployment's brand. Answers
41
+ * that land while it is closed count as unread on the launcher.
42
+ */
43
+ export declare function CalliopeAgent({ mode, open: openProp, defaultOpen, onOpenChange, persistKey, shortcut, launcherIcon, hideLauncher, railWidth, onRailWidthChange, expanded, onExpandedChange, hideHeader, title, className, ...chat }: CalliopeAgentProps): React.JSX.Element;
@@ -0,0 +1,111 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useProduct } from "../appearance/AppearanceProvider.js";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Drawer, DrawerContent, DrawerTitle } from "../overlays/Drawer.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { useRailPlacement } from "../shell/placement.js";
9
+ import { RailPanel } from "../shell/RailPanel.js";
10
+ import { AgentChat, NewConversationButton } from "./AgentChat.js";
11
+ import { AgentLauncher } from "./AgentLauncher.js";
12
+ import { useAgentController } from "./AgentProvider.js";
13
+ import { useMediaQuery, useOpenState, useShortcut } from "./surface.js";
14
+ /**
15
+ * The embedded agent of a Calliope AI product, one primitive for every
16
+ * surface: a floating bubble, the shell's right rail, inline in a page or a
17
+ * page of its own. Reads the nearest `CalliopeAgentProvider` (or the
18
+ * CopilotKit one); draws everything with the chat kit and the product's
19
+ * tokens, so it carries each product's and each deployment's brand. Answers
20
+ * that land while it is closed count as unread on the launcher.
21
+ */
22
+ export function CalliopeAgent({ mode, open: openProp, defaultOpen, onOpenChange, persistKey, shortcut = "mod+/", launcherIcon, hideLauncher = false, railWidth = 380, onRailWidthChange, expanded, onExpandedChange, hideHeader, title, className, ...chat }) {
23
+ const controller = useAgentController();
24
+ if (!controller) {
25
+ throw new Error("CalliopeAgent needs CalliopeAgentProvider or CopilotKitAgentProvider above it.");
26
+ }
27
+ const { messages } = useUI();
28
+ const product = useProduct();
29
+ const name = title ?? messages.agent.title(product?.name);
30
+ const floating = mode === "bubble" || mode === "rail";
31
+ const [open, setStoredOpen] = useOpenState({
32
+ ...(openProp !== undefined ? { open: openProp } : {}),
33
+ defaultOpen: defaultOpen ?? !floating,
34
+ ...(onOpenChange ? { onOpenChange } : {}),
35
+ ...(persistKey ? { persistKey } : {}),
36
+ });
37
+ // The composer takes focus when the person opens the assistant, never on a page load that restores it open.
38
+ const [focusComposer, setFocusComposer] = React.useState(false);
39
+ const setOpen = React.useCallback((next) => {
40
+ setFocusComposer(next);
41
+ setStoredOpen(next);
42
+ }, [setStoredOpen]);
43
+ const shown = !floating || open;
44
+ React.useEffect(() => controller.setVisible(shown), [controller, shown]);
45
+ useShortcut(floating ? shortcut : false, () => setOpen(!open));
46
+ const unread = React.useSyncExternalStore(controller.subscribe, () => controller.getSnapshot().unread, () => 0);
47
+ const panelId = React.useId();
48
+ const launcher = React.useRef(null);
49
+ const refocus = React.useRef(false);
50
+ const close = () => {
51
+ refocus.current = true;
52
+ setOpen(false);
53
+ };
54
+ // Closing from inside the panel hands focus back to the launcher once it is back.
55
+ React.useEffect(() => {
56
+ if (open || !refocus.current)
57
+ return;
58
+ refocus.current = false;
59
+ launcher.current?.focus();
60
+ }, [open]);
61
+ if (mode === "inline" || mode === "page") {
62
+ return (_jsx(AgentChat, { ...chat, title: name, hideHeader: hideHeader ?? mode === "inline", progress: chat.progress ?? (mode === "page" ? "steps" : "line"), className: cn(mode === "inline" ? "h-full rounded-lg border" : "mx-auto h-full w-full max-w-3xl", className) }));
63
+ }
64
+ const launcherButton = !hideLauncher && !open ? (_jsx(AgentLauncher, { ref: launcher, open: open, onOpenChange: setOpen, title: name, unread: unread, controls: panelId, shortcut: shortcut, icon: launcherIcon, className: "fixed right-4 bottom-4 z-40" })) : null;
65
+ if (mode === "rail") {
66
+ return (_jsx(RailSurface, { id: panelId, name: name, open: open, onClose: close, railWidth: railWidth, onRailWidthChange: onRailWidthChange, expanded: expanded, onExpandedChange: onExpandedChange, launcher: launcherButton, className: className, chat: { ...chat, autoFocus: chat.autoFocus ?? focusComposer } }));
67
+ }
68
+ return (_jsx(BubbleSurface, { id: panelId, name: name, open: open, onOpenChange: setOpen, onClose: close, launcher: launcherButton, className: className, chat: { ...chat, autoFocus: chat.autoFocus ?? focusComposer } }));
69
+ }
70
+ function RailSurface({ id, name, open, onClose, railWidth, onRailWidthChange, expanded, onExpandedChange, launcher, className, chat, }) {
71
+ const inDrawer = useRailPlacement() === "drawer";
72
+ return (_jsxs(_Fragment, { children: [_jsx(RailPanel, { label: name, collapsed: !open, onCollapsedChange: (collapsed) => (collapsed ? onClose() : undefined), collapsedMode: "hidden", actions: chat.hideNewConversation ? undefined : (_jsx(NewConversationButton, { className: "text-sidebar-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-foreground" })), width: railWidth, minWidth: 300, maxWidth: 720, ...(onRailWidthChange ? { onWidthChange: onRailWidthChange } : {}), expanded: expanded ?? false, ...(onExpandedChange ? { onExpandedChange } : {}), className: className, children: _jsx("div", { id: id, className: "flex h-full min-h-0 flex-col", children: _jsx(AgentChat, { ...chat, title: name, hideHeader: true,
73
+ // eslint-disable-next-line jsx-a11y/no-autofocus -- the person just opened the assistant to type
74
+ autoFocus: chat.autoFocus, className: "min-h-0 flex-1" }) }) }), !inDrawer && launcher] }));
75
+ }
76
+ function BubbleSurface({ id, name, open, onOpenChange, onClose, launcher, className, chat, }) {
77
+ const wide = useMediaQuery("(min-width: 640px)");
78
+ const panel = React.useRef(null);
79
+ // A non-modal dialog closes on Escape (WAI-ARIA dialog pattern): from inside it, or from
80
+ // nowhere in particular (focus left on the page after a chip it held went away).
81
+ const latestClose = React.useRef(onClose);
82
+ React.useEffect(() => {
83
+ latestClose.current = onClose;
84
+ });
85
+ React.useEffect(() => {
86
+ if (!open || !wide)
87
+ return;
88
+ const onKey = (event) => {
89
+ if (event.key !== "Escape" || event.defaultPrevented)
90
+ return;
91
+ const target = event.target instanceof Node ? event.target : null;
92
+ const inside = target !== null && panel.current?.contains(target);
93
+ if (!inside && target !== document.body)
94
+ return;
95
+ // A confirm dialog inside handles its own Escape.
96
+ if (target instanceof Element &&
97
+ target.closest("[role='alertdialog'], [role='dialog'][aria-modal='true']"))
98
+ return;
99
+ event.preventDefault();
100
+ latestClose.current();
101
+ };
102
+ document.addEventListener("keydown", onKey);
103
+ return () => document.removeEventListener("keydown", onKey);
104
+ }, [open, wide]);
105
+ if (!wide) {
106
+ return (_jsxs(_Fragment, { children: [launcher, _jsx(Drawer, { open: open, onOpenChange: onOpenChange, children: _jsxs(DrawerContent, { side: "bottom", hideCloseButton: true, "aria-describedby": undefined, className: "h-dvh max-h-dvh gap-0 p-0", children: [_jsx(DrawerTitle, { className: "sr-only", children: name }), _jsx(AgentChat, { ...chat, title: name, onClose: onClose, className: "min-h-0 flex-1" })] }) })] }));
107
+ }
108
+ return (_jsxs(_Fragment, { children: [launcher, open && (_jsx("div", { id: id, role: "dialog", "aria-modal": "false", "aria-label": name, "data-slot": "agent-bubble", ref: panel, className: cn("fixed right-4 bottom-4 z-40 flex h-[min(40rem,calc(100dvh-2rem))] w-[min(26rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-lg border bg-card shadow-xl", className), children: _jsx(AgentChat, { ...chat, title: name,
109
+ // eslint-disable-next-line jsx-a11y/no-autofocus -- the person just opened the assistant to type
110
+ autoFocus: chat.autoFocus, onClose: onClose, className: "min-h-0 flex-1" }) }))] }));
111
+ }
@@ -0,0 +1,61 @@
1
+ import * as React from "react";
2
+ import { type DocumentReviewProps } from "../workflow/DocumentReview.js";
3
+ import { type FormReviewProps } from "../workflow/FormReview.js";
4
+ import { type MediaReviewProps } from "../workflow/MediaReview.js";
5
+ import type { WorkflowQuestion } from "../workflow/question.js";
6
+ import type { Artifact, RunStage } from "../workflow/run.js";
7
+ import type { AgentRenderers, CustomRendererProps, InterruptRendererProps } from "./renderers.js";
8
+ /** `calliope.run`: a run's stages as the agent reports them. Send it again with the same `id` to update. */
9
+ export interface RunProgressValue {
10
+ id?: string;
11
+ label?: string;
12
+ stages: readonly RunStage[];
13
+ }
14
+ /** The review viewer an artifact opens in. */
15
+ export type ArtifactReviewValue = ({
16
+ viewer: "document";
17
+ } & Pick<DocumentReviewProps, "title" | "pages" | "annotations" | "signOffs" | "signOffRole">) | ({
18
+ viewer: "media";
19
+ } & Pick<MediaReviewProps, "kind" | "src" | "title" | "poster" | "captions" | "duration" | "comments">) | ({
20
+ viewer: "form";
21
+ } & Pick<FormReviewProps, "title" | "pages" | "fields" | "threshold">);
22
+ /**
23
+ * `calliope.artifact`: an output for the person to look at, and to review
24
+ * when the run waits on it. `interruptId` names the run's interrupt the
25
+ * review answers: signing a document resolves it with `{ action: "sign-off" }`,
26
+ * sending a form's review with `{ action: "form-review", fields }`.
27
+ */
28
+ export interface ArtifactValue {
29
+ id: string;
30
+ artifact: Artifact;
31
+ review?: ArtifactReviewValue;
32
+ interruptId?: string;
33
+ }
34
+ /** `calliope.question` interrupts carry a question in `metadata.question`. */
35
+ export interface QuestionInterruptMetadata {
36
+ question: WorkflowQuestion;
37
+ }
38
+ /** The run timeline for `calliope.run` events: progress drawn in the conversation. */
39
+ export declare function RunProgressCard({ value }: CustomRendererProps<RunProgressValue>): React.JSX.Element;
40
+ /**
41
+ * An artifact the agent produced, with its review viewer (document, media or
42
+ * form) as the preview. While the run waits on its review, signing off or
43
+ * sending the form's review answers the interrupt and resumes the run.
44
+ */
45
+ export declare function ArtifactReviewCard({ value, agent }: CustomRendererProps<ArtifactValue>): React.JSX.Element;
46
+ /** A `calliope.question` interrupt: the runtime question card, its answers resuming the run. */
47
+ export declare function QuestionInterruptCard({ interrupt, resolve }: InterruptRendererProps): React.JSX.Element;
48
+ /**
49
+ * Any other open interrupt not tied to a tool call: the agent's question in
50
+ * its words, with approve and decline (a decline always with a reason).
51
+ */
52
+ export declare function InterruptCard({ interrupt, resolve, className, }: InterruptRendererProps & {
53
+ className?: string;
54
+ }): React.JSX.Element;
55
+ /**
56
+ * The workflow run and review components as generative UI: `calliope.run`
57
+ * custom events draw the run timeline, `calliope.artifact` events draw the
58
+ * artifact with its document, media or form review, and `calliope.question`
59
+ * interrupts draw the runtime question card. Merge with the host's own.
60
+ */
61
+ export declare const workflowRenderers: AgentRenderers;
@@ -0,0 +1,102 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Button } from "../primitives/Button.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ import { ArtifactCard } from "../workflow/ArtifactCard.js";
8
+ import { DocumentReview } from "../workflow/DocumentReview.js";
9
+ import { FormReview } from "../workflow/FormReview.js";
10
+ import { MediaReview } from "../workflow/MediaReview.js";
11
+ import { RunTimeline } from "../workflow/RunTimeline.js";
12
+ import { WorkflowQuestionCard } from "../workflow/WorkflowQuestionCard.js";
13
+ /** The run timeline for `calliope.run` events: progress drawn in the conversation. */
14
+ export function RunProgressCard({ value }) {
15
+ return (_jsx("div", { className: "min-w-0 rounded-lg border bg-card p-3", children: _jsx(RunTimeline, { stages: value.stages, label: value.label }) }));
16
+ }
17
+ function openInterrupt(agent, id) {
18
+ if (!id || agent.view.status !== "interrupted")
19
+ return undefined;
20
+ return agent.view.interrupts.find((i) => i.id === id);
21
+ }
22
+ /**
23
+ * An artifact the agent produced, with its review viewer (document, media or
24
+ * form) as the preview. While the run waits on its review, signing off or
25
+ * sending the form's review answers the interrupt and resumes the run.
26
+ */
27
+ export function ArtifactReviewCard({ value, agent }) {
28
+ const { messages } = useUI();
29
+ const waiting = openInterrupt(agent, value.interruptId);
30
+ const [fields, setFields] = React.useState({});
31
+ const resolve = (payload) => waiting && agent.resolveInterrupt(waiting.id, { status: "resolved", payload });
32
+ const review = value.review;
33
+ let preview = null;
34
+ if (review?.viewer === "document") {
35
+ const { viewer: _, ...props } = review;
36
+ preview = (_jsx(DocumentReview, { ...props, ...(waiting
37
+ ? { onSignOff: () => resolve({ action: "sign-off", artifactId: value.id }) }
38
+ : {}) }));
39
+ }
40
+ else if (review?.viewer === "media") {
41
+ const { viewer: _, ...props } = review;
42
+ preview = _jsx(MediaReview, { ...props });
43
+ }
44
+ else if (review?.viewer === "form") {
45
+ const { viewer: _, ...props } = review;
46
+ const current = props.fields.map((f) => fields[f.id] ?? f);
47
+ preview = (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(FormReview, { ...props, fields: current, ...(waiting
48
+ ? {
49
+ onAccept: (field) => setFields((s) => ({ ...s, [field.id]: { ...field, status: "accepted" } })),
50
+ onCorrect: (field, corrected) => setFields((s) => ({
51
+ ...s,
52
+ [field.id]: { ...field, status: "corrected", corrected },
53
+ })),
54
+ }
55
+ : {}) }), waiting && (_jsx(Button, { size: "sm", className: "self-end", onClick: () => resolve({
56
+ action: "form-review",
57
+ artifactId: value.id,
58
+ fields: current.map(({ id, status, corrected }) => ({ id, status, corrected })),
59
+ }), children: messages.agent.sendReview }))] }));
60
+ }
61
+ return _jsx(ArtifactCard, { artifact: value.artifact, preview: preview, headingLevel: "h3" });
62
+ }
63
+ /** A `calliope.question` interrupt: the runtime question card, its answers resuming the run. */
64
+ export function QuestionInterruptCard({ interrupt, resolve }) {
65
+ const question = interrupt.metadata?.question;
66
+ const [state, setState] = React.useState("waiting");
67
+ return (_jsx(WorkflowQuestionCard, { question: question ?? null, state: state, onSubmit: (submission) => {
68
+ setState("submitting");
69
+ resolve({ answers: submission.answers, revision: submission.revision });
70
+ } }));
71
+ }
72
+ /**
73
+ * Any other open interrupt not tied to a tool call: the agent's question in
74
+ * its words, with approve and decline (a decline always with a reason).
75
+ */
76
+ export function InterruptCard({ interrupt, resolve, className, }) {
77
+ const { messages } = useUI();
78
+ const t = messages.agent;
79
+ const [declining, setDeclining] = React.useState(false);
80
+ const [reason, setReason] = React.useState("");
81
+ const reasonId = React.useId();
82
+ return (_jsxs("section", { "aria-label": t.interrupt, "data-slot": "agent-interrupt", className: cn("flex min-w-0 flex-col gap-2 rounded-lg border border-warning-border bg-warning-bg p-3 text-sm text-warning-fg", className), children: [_jsx("p", { className: "[overflow-wrap:anywhere]", children: interrupt.message ?? messages.chat.approvalNeeded }), declining ? (_jsxs("form", { className: "flex min-w-0 flex-col gap-2", onSubmit: (event) => {
83
+ event.preventDefault();
84
+ if (reason.trim())
85
+ resolve({ approved: false, reason: reason.trim() });
86
+ }, children: [_jsx("label", { htmlFor: reasonId, className: "text-xs font-medium", children: messages.chat.denyReason }), _jsx("textarea", { id: reasonId, value: reason, onChange: (event) => setReason(event.target.value), rows: 2, className: "min-w-0 rounded-md border border-input bg-background p-2 text-sm text-foreground" }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setDeclining(false), children: t.back }), _jsx(Button, { type: "submit", variant: "destructive", size: "sm", disabled: !reason.trim(), children: t.decline })] })] })) : (_jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDeclining(true), children: t.decline }), _jsx(Button, { size: "sm", onClick: () => resolve({ approved: true }), children: messages.chat.approve })] }))] }));
87
+ }
88
+ /**
89
+ * The workflow run and review components as generative UI: `calliope.run`
90
+ * custom events draw the run timeline, `calliope.artifact` events draw the
91
+ * artifact with its document, media or form review, and `calliope.question`
92
+ * interrupts draw the runtime question card. Merge with the host's own.
93
+ */
94
+ export const workflowRenderers = {
95
+ custom: {
96
+ "calliope.run": RunProgressCard,
97
+ "calliope.artifact": ArtifactReviewCard,
98
+ },
99
+ interrupts: {
100
+ "calliope.question": QuestionInterruptCard,
101
+ },
102
+ };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The agent controller: one conversation with one AG-UI agent, as a store a
3
+ * React hook reads. It folds every event the agent emits into the chat kit's
4
+ * `ChatView` (the same reducer every chat surface reads), and owns what the
5
+ * person does: send, stop, regenerate, approve or deny, answer a tool that
6
+ * waits on them, edit the shared state, switch threads. The engine does the
7
+ * running; the controller never touches the network itself.
8
+ */
9
+ import type { AbstractAgent } from "@ag-ui/client";
10
+ import { type InputContent, type Message } from "@ag-ui/core";
11
+ import { type AgUiEvent, type ChatToolCall, type ChatView } from "../chat/model.js";
12
+ import type { AgentEngine, ToolInvoker } from "./engine.js";
13
+ import { type AgentTool } from "./tool.js";
14
+ /** A prompt offered as a chip: the title shown, the message sent. */
15
+ export interface AgentSuggestion {
16
+ title: string;
17
+ message: string;
18
+ }
19
+ /** Something the agent should know about the page: a readable. */
20
+ export interface AgentContextEntry {
21
+ /** What it is, for the agent ("The open project"). */
22
+ description: string;
23
+ /** Sent as JSON text unless it is a string. */
24
+ value: unknown;
25
+ }
26
+ /** A saved conversation, as a host's history endpoint returns it. */
27
+ export interface AgentThread {
28
+ messages: Message[];
29
+ state?: unknown;
30
+ }
31
+ export interface AgentControllerOptions {
32
+ /** Sent with every run: the model picked, the page. */
33
+ forwardedProps?: () => Record<string, unknown> | undefined;
34
+ /** Loads a thread's history on `switchThread`. Without it a switched-to thread starts empty. */
35
+ loadThread?: (threadId: string) => Promise<AgentThread> | AgentThread;
36
+ /** A run that failed (also shown in the conversation). */
37
+ onError?: (error: unknown) => void;
38
+ /** Every AG-UI event, for the host's telemetry. */
39
+ onEvent?: (event: AgUiEvent) => void;
40
+ }
41
+ /**
42
+ * A custom event as the conversation draws it: events whose value carries
43
+ * the same `id` are one item (progress updating in place), placed where the
44
+ * first of them arrived.
45
+ */
46
+ export interface AgentCustomItem {
47
+ key: string;
48
+ name: string;
49
+ /** The latest value. */
50
+ value: unknown;
51
+ history: readonly unknown[];
52
+ /** The timeline item it arrived after ("message:<id>", "tool-call:<id>"), or null before any. */
53
+ after: string | null;
54
+ }
55
+ export interface AgentSnapshot {
56
+ view: ChatView;
57
+ /** True from send until the agent is done, follow-ups and browser tools included. */
58
+ running: boolean;
59
+ /** Tool calls waiting on the person (human-in-the-loop tools), by id. */
60
+ waiting: readonly string[];
61
+ threadId: string;
62
+ /** Answers that finished while the surface was not visible. */
63
+ unread: number;
64
+ suggestions: readonly AgentSuggestion[];
65
+ tools: readonly AgentTool[];
66
+ /** The conversation's custom events, grouped and placed (see `AgentCustomItem`). */
67
+ custom: readonly AgentCustomItem[];
68
+ }
69
+ export interface SendOptions {
70
+ /** Attachments and other parts after the text (AG-UI input content). */
71
+ parts?: readonly InputContent[];
72
+ }
73
+ type Listener = () => void;
74
+ /** An id unique in this page: crypto when there is one, a counter otherwise. */
75
+ export declare function agentId(prefix?: string): string;
76
+ export declare class AgentController {
77
+ private options;
78
+ private engine;
79
+ private unsubscribe;
80
+ private listeners;
81
+ private view;
82
+ private running;
83
+ private runSeq;
84
+ private tools;
85
+ private toolOrder;
86
+ private context;
87
+ private suggestionSets;
88
+ private waiting;
89
+ private executing;
90
+ private failed;
91
+ private decisions;
92
+ private visible;
93
+ private read;
94
+ private snapshot;
95
+ /** Where each custom event arrived, by its index in the view. */
96
+ private anchors;
97
+ private disposed;
98
+ private connected;
99
+ private threadSeq;
100
+ /** Set by stop until the next run: a stopped run's late events (its abort error) are dropped. */
101
+ private halted;
102
+ constructor(createEngine: (invoke: ToolInvoker) => AgentEngine, options?: AgentControllerOptions);
103
+ /**
104
+ * Starts reading the agent's events; returns the disconnect, which stops a
105
+ * run in flight. Called from an effect, so nothing subscribes on the server
106
+ * and a StrictMode remount reconnects cleanly.
107
+ */
108
+ connect(): () => void;
109
+ get agent(): AbstractAgent;
110
+ setOptions(options: AgentControllerOptions): void;
111
+ /** Swaps the engine (CopilotKit hands over a new agent once its runtime connects). */
112
+ attach(createEngine: (invoke: ToolInvoker) => AgentEngine): void;
113
+ subscribe: (listener: Listener) => (() => void);
114
+ getSnapshot: () => AgentSnapshot;
115
+ private emit;
116
+ private finishedAnswers;
117
+ private build;
118
+ private customItems;
119
+ private bind;
120
+ /** The view the agent's own messages and state make, for a fresh start or a thread switch. */
121
+ private fromAgent;
122
+ private fold;
123
+ /** Tool results the agent's messages gained outside the event stream: the browser's own tools. */
124
+ private syncResults;
125
+ private invoke;
126
+ private run;
127
+ /** Answers every interrupt still open as cancelled, so a new message can start a run. */
128
+ private cancelOpen;
129
+ /** Sends the person's message and runs the agent. Ignored while a run is in flight. */
130
+ send: (text: string, options?: SendOptions) => Promise<void>;
131
+ /** Stops the run in flight and any tool waiting on the person. */
132
+ stop: () => void;
133
+ /** Drops the answer to the last message and asks again. */
134
+ regenerate: () => Promise<void>;
135
+ /**
136
+ * Answers one interrupt. When every open interrupt has an answer, the run
137
+ * resumes with them (AG-UI `resume`).
138
+ */
139
+ resolveInterrupt: (interruptId: string, response: {
140
+ status: "resolved";
141
+ payload?: unknown;
142
+ } | {
143
+ status: "cancelled";
144
+ }) => void;
145
+ /** Approves a tool call the run paused on. */
146
+ approve: (call: ChatToolCall, payload?: Record<string, unknown>) => void;
147
+ /** Denies it, with the person's reason. */
148
+ deny: (call: ChatToolCall, reason: string) => void;
149
+ /** Answers a human-in-the-loop tool call: the result goes back to the agent. */
150
+ respond: (toolCallId: string, result: unknown) => void;
151
+ getState: <T = unknown>() => T;
152
+ /** Replaces the shared state: the next run sends it, and the view shows it now. */
153
+ setState: (state: unknown) => void;
154
+ registerTool: <Args>(given: AgentTool<Args>) => (() => void);
155
+ private syncTools;
156
+ addContext: (entry: AgentContextEntry) => (() => void);
157
+ private syncContext;
158
+ addSuggestions: (suggestions: readonly AgentSuggestion[]) => (() => void);
159
+ /** Opens another conversation: stops this one, loads the other's history. */
160
+ switchThread: (threadId: string) => Promise<void>;
161
+ /** Starts a new, empty conversation and returns its id. */
162
+ newThread: () => Promise<string>;
163
+ /** Whether the person can see the conversation: answers that land while hidden count as unread. */
164
+ setVisible: (visible: boolean) => void;
165
+ dispose(): void;
166
+ }
167
+ export {};