@calliopelabs/ui 0.12.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 (80) hide show
  1. package/README.md +68 -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 +11 -2
  47. package/dist/index.js +11 -1
  48. package/dist/primitives/Button.d.ts +1 -1
  49. package/dist/provider/messages.d.ts +83 -1
  50. package/dist/provider/messages.js +91 -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/dist/workflow/canvas/WorkflowCanvas.js +41 -1
  74. package/dist/workflow/definition.d.ts +20 -0
  75. package/dist/workflow/definition.js +44 -0
  76. package/dist/workflow/editor/WorkflowInspector.d.ts +21 -3
  77. package/dist/workflow/editor/WorkflowInspector.js +13 -5
  78. package/dist/workflow/editor/WorkflowWorkspace.d.ts +28 -3
  79. package/dist/workflow/editor/WorkflowWorkspace.js +75 -16
  80. package/package.json +35 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The shared operator UI of Calliope AI: the app shell, navigation, page frame, data table and list patterns, status colours, design tokens and appearance settings that Astrolift AI and Zentinelle AI both run on. Extracted from the Astrolift AI frontend and generalized with a per-product config, so each console keeps its own name, accent, sidebar colour and navigation while sharing everything else.
4
4
 
5
- The shared base of the Calliope AI ui series. Product packages (`@calliopelabs/ui-astrolift`, `ui-zentinelle`, `ui-workbench`, later `ui-chat`) depend on it and carry only what is specific to their product. `0.2.0` was published as `@calliopelabs/ui-astrolift` with the same exports; it is deprecated in favour of this package.
5
+ The shared base of the Calliope AI ui series. Product packages (`@calliopelabs/ui-astrolift`, `ui-zentinelle`, `ui-workbench`) depend on it and carry only what is specific to their product. `0.2.0` was published as `@calliopelabs/ui-astrolift` with the same exports; it is deprecated in favour of this package.
6
6
 
7
7
  It is built Storybook first: every component and every screen pattern is built and reviewed in the catalog before an app uses it. The catalog is published at **https://calliope-ux.astro.conflict.cloud**.
8
8
 
@@ -373,10 +373,61 @@ const registry: WorkflowRegistry = {
373
373
 
374
374
  It marks unsaved edits and a draft that differs from live (`documentStatus`). A newer saved revision is taken when there is nothing to lose; when it moves under unsaved edits, it stops and asks: compare, use theirs, or keep mine. Publishing shows the draft against live (`WorkflowDiff`) and is held while anything blocks. A rejected promise's message is shown and the edits stay. Persistence and authority stay with the host: it never saves, publishes or runs anything itself. Below a 64rem container the palette and the side panel open as drawers.
375
375
 
376
+ A host can reach further into the editor. Each is opt-in; without them the editor is as above.
377
+
378
+ - **Deep links.** `defaultSelection` opens the editor on a flow and a node (`{ type: "node", flow, id }`); `selection` with `onSelectionChange` holds it in the host. A selection in another flow opens that flow, and the canvas brings the node into view.
379
+ - **Flows.** `onFlowCreate` offers New flow and `onFlowRename` offers Rename flow for the open one. The dialog checks the name as it is typed against `WORKFLOW_FLOW_NAME` (a lowercase slug of up to 32 characters), against the other flows and against `WORKFLOW_MAX_FLOWS`, then hands the host the next definition to apply. `renameFlow` renames the key, `main` and every sub-flow node that runs it; anything else the host keeps that names flows is the host's to follow. `flowNameProblem`, `addFlow` and `renameFlow` are exported for hosts that edit flows themselves.
380
+ - **Host fields.** `renderNodeFields` adds the host's own fields to the inspector after a node's kind fields, with the node, its flow and an `onChange` that patches it. Switch on `node.kind` and return null for none.
381
+
382
+ ```tsx
383
+ <WorkflowWorkspace
384
+ {...props}
385
+ defaultSelection={at ? { type: "node", flow: at.flow, id: at.node } : null}
386
+ onFlowCreate={(next) => setDraft(next)}
387
+ onFlowRename={(next, from, to) => setDraft(next) /* and follow `from` to `to` in what you keep */}
388
+ renderNodeFields={({ node, readOnly }) =>
389
+ node.kind === "step" ? <RunnerPicker step={node.id} readOnly={readOnly} /> : null
390
+ }
391
+ />
392
+ ```
393
+
376
394
  The canvas pans, zooms and fits without going below `READABLE_ZOOM`; loop bodies and fan-out branches fold, and a sub-flow shows in place. Every drag has a keyboard equivalent: Tab to a node or edge, Enter to select, arrows to move, Delete to remove, and Add and Connect in its toolbar.
377
395
 
378
396
  `@xyflow/react` comes with the package; its structural CSS is in the preset.
379
397
 
398
+ ## Chat
399
+
400
+ Engine-agnostic building blocks for agent chat, over the [AG-UI](https://docs.ag-ui.com) event shape (`@ag-ui/core` 1.0). An engine that speaks AG-UI drives them directly; any other gets an adapter that turns its stream into AG-UI events. Products keep their own screens and brands: there is no shipped chat page, only the pieces. Host-driven like everything else here: the package never connects, authenticates, runs a tool or resumes a run.
401
+
402
+ - **The model.** `reduceChat(view, event)` folds one AG-UI event into a `ChatView`: messages with streaming text, tool calls with their arguments, results, status and approvals, the timeline that interleaves them, the run's steps and status (`idle`, `running`, `finished`, `interrupted`, `cancelled`, `error`), the shared state (`STATE_SNAPSHOT` plus `STATE_DELTA` as JSON Patch, `applyJsonPatch`), and `CUSTOM` and `RAW` events in order. `MESSAGES_SNAPSHOT` replaces the conversation, so a watcher can join part way. It is pure and immutable, never throws on an event it does not expect, and counts a delta that fails in `rejectedDeltas`. `foldChat(events, view?)` folds a list; `createChatView()` starts one. Activity, reasoning and sub-agent events leave the view as it is.
403
+ - **The hook.** `useAgUiRun({ initialView?, onError? })` holds the view and reads one run at a time from the source the host hands `run(source)`: an `AsyncIterable` of events or a subscribe function `(emit, done) => unsubscribe`. `stop()` lets go and closes the view as cancelled (telling the agent to stop is the host's request); `dispatch(userMessageEvents(id, text))` shows the person's message before the run starts; a stream that fails lands in the view as a run error.
404
+ - **Human in the loop.** A `RUN_FINISHED` interrupt outcome whose interrupt names a `toolCallId` puts an `approval` on that call. `ToolCallCard` then offers approve and deny through `ConfirmDialog`, a denial always with a reason, and hands the decision to `onApprove` or `onDeny`; the host resumes the run (AG-UI `resume`) and the next `RUN_STARTED` clears the approval.
405
+ - **The components.** `MessageList` (the timeline, tool calls inline, follows the stream until the person scrolls up, a failed run's error with retry), `MessageBubble` (user, assistant, system, developer and tool), `Composer` (Enter sends, Shift+Enter breaks the line, stop while running, `attachments` and `actions` slots), `ToolCallCard` (arguments, result or error, `renderResult` for a table or chart), `AgentProgress` (status and steps on `RunTimeline`) and `ModelPicker` (the host's options).
406
+ - **Markdown is the host's.** Text renders as plain text unless the host passes `renderContent` (its markdown component, given the text so far). Plain text is never parsed as HTML.
407
+
408
+ ```tsx
409
+ const chat = useAgUiRun();
410
+
411
+ <MessageList
412
+ view={chat.view}
413
+ renderContent={(text) => <Markdown>{text}</Markdown>}
414
+ onApprove={(call) => chat.run(agent.resume(call.approval!.interruptId, "resolved"))}
415
+ onDeny={(call, reason) => chat.run(agent.resume(call.approval!.interruptId, "cancelled", reason))}
416
+ />
417
+ <Composer
418
+ running={chat.running}
419
+ onStop={() => { agent.abort(); chat.stop(); }}
420
+ onSubmit={(text) => {
421
+ chat.dispatch(userMessageEvents(crypto.randomUUID(), text));
422
+ void chat.run(agent.stream({ threadId, text }));
423
+ }}
424
+ actions={<ModelPicker options={models} value={model} onValueChange={setModel} />}
425
+ />
426
+ <AgentProgress view={chat.view} />
427
+ ```
428
+
429
+ `agent` above is the host's own client. The catalog's `Chat/ChatPanel` composes the pieces the same way, driven by a recorded run. Their copy is in the `chat` message group.
430
+
380
431
  ## Appearance
381
432
 
382
433
  The person sets four axes, per product: the **ground** (black, charcoal, graphite, emerald, navy, paper, mist; it decides light or dark), the **accent** (the product's own, a family accent, or any colour that stands 3:1 off the ground), the **density** (`compact` hairline rows or boxed `cards`) and the **corners** (0, 2, 4 or 10 px). `AppearancePicker` is the settings UI for all four.
@@ -536,6 +587,20 @@ document.addEventListener("calliope:appearance", (event) => console.log(event.de
536
587
 
537
588
  A product that is not built in sets its sidebar with `[data-product="myapp"] { --brand-sidebar: #0b0b19; }` in its own CSS, picks a family accent, and boots with `CalliopeAppearance.boot(config)`, where `config` is the JSON `bootConfig(defineProduct(...))` returns (or one of `CalliopeAppearance.products`, edited).
538
589
 
590
+ ## Token mappings
591
+
592
+ Stacks that cannot take the preset or the CSS layer read the same tokens through generated files under `@calliopelabs/ui/tokens/`, each with a guide beside it in `dist/tokens/`:
593
+
594
+ | File | For |
595
+ | ---------------------------------------- | ----------------------------------------------------------- |
596
+ | `@calliopelabs/ui/tokens/jupyterlab.css` | JupyterLab and Notebook 7: the `--jp-*` theme variables |
597
+ | `@calliopelabs/ui/tokens/vscode.css` | VS Code webviews: `--vscode-*` fallbacks outside VS Code |
598
+ | `@calliopelabs/ui/tokens/tokens.py` | Python (the installer): RGB triplets and a small accessor |
599
+ | `@calliopelabs/ui/tokens/native` | React Native: a typed palette object per ground and accent |
600
+ | `@calliopelabs/ui/tokens/plain.css` | Sites and Cykick: every token as a plain, concrete variable |
601
+
602
+ Every file covers every ground and family accent, chosen by `data-ground` and `data-accent` in the CSS ones and by argument in the others. They are generated by `npm run theme` from the design tokens (`tokens/*.json`, through `src/theme/tokens.ts`), and a test holds every mapped value to what the preset paints on that ground with that accent. The mapping tables are in each guide.
603
+
539
604
  ## Copy and languages
540
605
 
541
606
  Every string the package renders is English by default and overridable, one group at a time:
@@ -555,11 +620,11 @@ Node 24 (`.nvmrc`).
555
620
  | `npm run storybook` | The catalog, on port 6006 |
556
621
  | `npm test` | Every test, including one that renders every story and runs the a11y addon on it |
557
622
  | `npm run lint`, `npm run typecheck`, `npm run format:check` | The gates CI runs |
558
- | `npm run theme` | Regenerates `src/styles/theme.css` and the CSS layer after a token change |
623
+ | `npm run theme` | Regenerates the constants, `theme.css`, the CSS layer and the token mappings |
559
624
  | `npm run build`, `npm run pack:check` | Builds `dist/` and checks the package a consumer receives |
560
625
  | `npm run build-storybook` | The static catalog |
561
626
 
562
- Conventions for contributors and agents are in [`bootstrap.md`](bootstrap.md).
627
+ Conventions for contributors and agents are in [`bootstrap.md`](bootstrap.md). Designers: the token files, what each one changes and how to propose a change are in the Storybook pages under **Designer guide** (`src/docs/`).
563
628
 
564
629
  ## Releasing
565
630
 
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import type { ChatContentRenderer } from "../chat/MessageBubble.js";
3
+ import { type ChatToolCall } from "../chat/model.js";
4
+ import { type ModelPickerProps } from "../chat/ModelPicker.js";
5
+ import type { SendOptions } from "./controller.js";
6
+ export interface AgentChatProps {
7
+ /** The panel's title. Defaults to the product's assistant ("Astrolift AI assistant"). */
8
+ title?: string;
9
+ /** Hides the heading row (inline and page surfaces that have their own). */
10
+ hideHeader?: boolean;
11
+ /** Controls beside the title, before the built-in ones. */
12
+ headerActions?: React.ReactNode;
13
+ /** Shows a close button. */
14
+ onClose?: () => void;
15
+ /** Shows expand and restore (the rail's full-width mode). */
16
+ expanded?: boolean;
17
+ onExpandedChange?: (expanded: boolean) => void;
18
+ /** Hides "New conversation". */
19
+ hideNewConversation?: boolean;
20
+ /** Draws message text: the host's markdown renderer. Plain text by default. */
21
+ renderContent?: ChatContentRenderer;
22
+ /** Draws a tool call the host has its own component for; undefined falls through to the tool's `render`, then the default card. */
23
+ renderToolCall?: (call: ChatToolCall) => React.ReactNode;
24
+ /** The tool's name for people, per call. */
25
+ toolTitle?: (call: ChatToolCall) => string | undefined;
26
+ /** "line": a one-line status while running (bubble, rail). "steps": the run's steps on the run timeline (page). */
27
+ progress?: "line" | "steps" | "none";
28
+ /** Replaces the empty state (the suggestions still show under it). */
29
+ empty?: React.ReactNode;
30
+ /** Above the composer's field: the host's attachment chips. */
31
+ attachments?: React.ReactNode;
32
+ /** Beside send: an attach button, other controls. */
33
+ composerActions?: React.ReactNode;
34
+ /** A model picker in the composer; the host passes the chosen model in `forwardedProps`. */
35
+ modelPicker?: ModelPickerProps;
36
+ /** What to send with the text: the attachments as AG-UI input parts. Called on send. */
37
+ sendOptions?: () => SendOptions | undefined;
38
+ placeholder?: string;
39
+ autoFocus?: boolean;
40
+ /** Offers the suggestions after each answer too, not only before the first message. */
41
+ suggestionsAfterRun?: boolean;
42
+ className?: string;
43
+ }
44
+ /** Starts a new, empty conversation with the nearest agent. */
45
+ export declare function NewConversationButton({ className }: {
46
+ className?: string;
47
+ }): React.JSX.Element;
48
+ /**
49
+ * The conversation with the nearest agent, whole: a heading row, the
50
+ * messages with tool cards and approvals, starting points, run status and
51
+ * the composer. Every surface of `CalliopeAgent` draws this; a host that
52
+ * lays out its own screen can too. Tools registered with a `render` draw
53
+ * their own calls; one that waits on the person gets `respond`, and the
54
+ * composer holds until it is answered.
55
+ */
56
+ export declare function AgentChat({ title, hideHeader, headerActions, onClose, expanded, onExpandedChange, hideNewConversation, renderContent, renderToolCall, toolTitle, progress, empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun, className, }: AgentChatProps): React.JSX.Element;
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Maximize2Icon, Minimize2Icon, SquarePenIcon, XIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { useProduct } from "../appearance/AppearanceProvider.js";
6
+ import { AgentProgress } from "../chat/AgentProgress.js";
7
+ import { Composer } from "../chat/Composer.js";
8
+ import { MessageList } from "../chat/MessageList.js";
9
+ import { parseToolCallArgs } from "../chat/model.js";
10
+ import { ModelPicker } from "../chat/ModelPicker.js";
11
+ import { cn } from "../lib/cn.js";
12
+ import { Button } from "../primitives/Button.js";
13
+ import { useUI } from "../provider/UIProvider.js";
14
+ import { EmptyState } from "../states/EmptyState.js";
15
+ import { StatusDot } from "../status/StatusDot.js";
16
+ import { useCalliopeAgent } from "./AgentProvider.js";
17
+ function ToolRender({ tool, call, waiting, respond, }) {
18
+ return (_jsx(_Fragment, { children: tool.render?.({
19
+ call,
20
+ args: parseToolCallArgs(call.args),
21
+ ...(waiting ? { respond: (result) => respond(call.id, result) } : {}),
22
+ }) }));
23
+ }
24
+ /** Starts a new, empty conversation with the nearest agent. */
25
+ export function NewConversationButton({ className }) {
26
+ const agent = useCalliopeAgent();
27
+ const { messages } = useUI();
28
+ const label = messages.agent.newConversation;
29
+ const fresh = agent.view.timeline.length === 0 && !agent.view.error;
30
+ return (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": label, title: label, disabled: fresh && !agent.running, onClick: () => void agent.newThread(), className: className, children: _jsx(SquarePenIcon, { "aria-hidden": true }) }));
31
+ }
32
+ function Suggestions({ suggestions, disabled, onPick, }) {
33
+ const { messages } = useUI();
34
+ if (suggestions.length === 0)
35
+ return null;
36
+ return (_jsx("div", { role: "group", "aria-label": messages.agent.suggestions, "data-slot": "agent-suggestions", className: "flex min-w-0 flex-wrap gap-2", children: suggestions.map((s) => (_jsx(Button, { variant: "outline", size: "sm", disabled: disabled, onClick: () => onPick(s), className: "max-w-full", children: _jsx("span", { className: "truncate", children: s.title }) }, s.message))) }));
37
+ }
38
+ /**
39
+ * The conversation with the nearest agent, whole: a heading row, the
40
+ * messages with tool cards and approvals, starting points, run status and
41
+ * the composer. Every surface of `CalliopeAgent` draws this; a host that
42
+ * lays out its own screen can too. Tools registered with a `render` draw
43
+ * their own calls; one that waits on the person gets `respond`, and the
44
+ * composer holds until it is answered.
45
+ */
46
+ export function AgentChat({ title, hideHeader = false, headerActions, onClose, expanded, onExpandedChange, hideNewConversation = false, renderContent, renderToolCall, toolTitle, progress = "line", empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun = false, className, }) {
47
+ const agent = useCalliopeAgent();
48
+ const { messages } = useUI();
49
+ const product = useProduct();
50
+ const t = messages.agent;
51
+ const name = title ?? t.title(product?.name);
52
+ const headingId = React.useId();
53
+ const { view, running, waiting, suggestions, tools } = agent;
54
+ const blocked = waiting.length > 0;
55
+ const fresh = view.timeline.length === 0 && !view.error;
56
+ const send = (text) => void agent.send(text, sendOptions?.());
57
+ const root = React.useRef(null);
58
+ const pick = (s) => {
59
+ void agent.send(s.message);
60
+ // The chip goes away with the empty state; the composer keeps the person's place.
61
+ root.current?.querySelector("[data-slot=composer] textarea")?.focus();
62
+ };
63
+ const drawCall = (call) => {
64
+ const custom = renderToolCall?.(call);
65
+ if (custom !== undefined)
66
+ return custom;
67
+ const tool = tools.find((t) => t.name === call.name && t.render);
68
+ if (!tool)
69
+ return undefined;
70
+ return (_jsx(ToolRender, { tool: tool, call: call, waiting: waiting.includes(call.id), respond: agent.respond }));
71
+ };
72
+ const step = [...view.steps].reverse().find((s) => s.status === "running");
73
+ return (_jsxs("section", { ref: root, "aria-labelledby": hideHeader ? undefined : headingId, "aria-label": hideHeader ? name : undefined, "data-slot": "agent-chat", className: cn("flex min-h-0 min-w-0 flex-col bg-card text-card-foreground", className), children: [!hideHeader && (_jsxs("div", { className: "flex min-w-0 items-center gap-1 border-b px-3 py-2", children: [_jsx("h2", { id: headingId, className: "min-w-0 flex-1 truncate text-sm font-semibold", children: name }), headerActions, !hideNewConversation && _jsx(NewConversationButton, {}), onExpandedChange && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": expanded ? t.restore : t.expand, title: expanded ? t.restore : t.expand, onClick: () => onExpandedChange(!expanded), children: expanded ? _jsx(Minimize2Icon, { "aria-hidden": true }) : _jsx(Maximize2Icon, { "aria-hidden": true }) })), onClose && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t.close, title: t.close, onClick: onClose, children: _jsx(XIcon, { "aria-hidden": true }) }))] })), progress === "steps" && view.steps.length > 0 && (_jsx(AgentProgress, { view: view, className: "border-b px-3 py-2" })), _jsx(MessageList, { view: view, renderContent: renderContent, renderToolCall: drawCall, toolTitle: toolTitle, onApprove: (call) => agent.approve(call), onDeny: (call, reason) => agent.deny(call, reason), onRetry: () => void agent.regenerate(), empty: _jsxs("div", { className: "flex flex-col items-center gap-4 py-6", children: [empty ?? _jsx(EmptyState, { title: t.emptyTitle, description: t.emptyDescription }), _jsx(Suggestions, { suggestions: suggestions, disabled: running, onPick: pick })] }), className: "min-h-0 flex-1" }), _jsxs("div", { className: "flex min-w-0 flex-col gap-2 border-t p-3", children: [suggestionsAfterRun && !fresh && !running && (_jsx(Suggestions, { suggestions: suggestions, disabled: running, onPick: pick })), _jsx("p", { role: "status", "data-slot": "agent-status", className: cn("flex min-w-0 items-center gap-2 text-xs text-muted-foreground", !(progress === "line" && running) && !blocked && "sr-only"), children: blocked ? (t.waitingOnYou) : running ? (_jsxs(_Fragment, { children: [_jsx(StatusDot, { tone: "info", pulse: true }), _jsx("span", { className: "truncate", children: step ? step.name : messages.chat.runStatus("running") })] })) : null }), _jsx(Composer, { onSubmit: send, running: running, onStop: agent.stop, disabled: blocked, placeholder: placeholder,
74
+ // eslint-disable-next-line jsx-a11y/no-autofocus -- the person just opened the assistant to type
75
+ autoFocus: autoFocus, attachments: attachments, actions: modelPicker || composerActions ? (_jsxs(_Fragment, { children: [modelPicker && _jsx(ModelPicker, { ...modelPicker }), composerActions] })) : undefined })] })] }));
76
+ }
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ export interface AgentLauncherProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ /** The assistant's name, for the button's label. */
6
+ title: string;
7
+ /** Answers that arrived while closed: a badge, and part of the name. */
8
+ unread?: number;
9
+ /** The id of the panel it opens. */
10
+ controls?: string;
11
+ /** Shown in the tooltip and announced ("mod+/"). */
12
+ shortcut?: string | false;
13
+ /** The mark on the button. Defaults to the product's logo mark, then a sparkle. */
14
+ icon?: React.ReactNode;
15
+ /** "floating": the round button at the page's corner. "inline": a header button. */
16
+ variant?: "floating" | "inline";
17
+ ref?: React.Ref<HTMLButtonElement>;
18
+ className?: string;
19
+ }
20
+ /**
21
+ * The button that opens the agent: floating at the page's corner (the
22
+ * bubble) or in a header (beside a rail). Carries the product's mark, an
23
+ * unread badge whose count is part of its name, and its shortcut.
24
+ */
25
+ export declare function AgentLauncher({ open, onOpenChange, title, unread, controls, shortcut, icon, variant, ref, className, }: AgentLauncherProps): React.JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { SparklesIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { useProduct } from "../appearance/AppearanceProvider.js";
6
+ import { cn } from "../lib/cn.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { shortcutAria, shortcutLabel } from "./surface.js";
9
+ /**
10
+ * The button that opens the agent: floating at the page's corner (the
11
+ * bubble) or in a header (beside a rail). Carries the product's mark, an
12
+ * unread badge whose count is part of its name, and its shortcut.
13
+ */
14
+ export function AgentLauncher({ open, onOpenChange, title, unread = 0, controls, shortcut, icon, variant = "floating", ref, className, }) {
15
+ const { messages } = useUI();
16
+ const product = useProduct();
17
+ const t = messages.agent;
18
+ const mark = icon ??
19
+ (product?.logo?.mark ? (_jsx("img", { src: product.logo.mark, alt: "", className: "size-5 rounded-sm object-contain" })) : (_jsx(SparklesIcon, { "aria-hidden": true, className: "size-5" })));
20
+ const label = open ? t.close : t.open(title);
21
+ const hint = shortcut ? `${label} (${shortcutLabel(shortcut)})` : label;
22
+ return (_jsxs("button", { ref: ref, type: "button", "data-slot": "agent-launcher", "aria-expanded": open, "aria-controls": controls, "aria-keyshortcuts": shortcut ? shortcutAria(shortcut) : undefined, title: hint, onClick: () => onOpenChange(!open), className: cn("relative inline-flex shrink-0 items-center justify-center outline-none focus-visible:ring-3 focus-visible:ring-ring/50", variant === "floating"
23
+ ? "size-12 rounded-full bg-primary text-primary-foreground shadow-lg transition-transform hover:scale-105 motion-reduce:transition-none motion-reduce:hover:scale-100"
24
+ : "h-8 gap-1.5 rounded-lg border border-border bg-background px-2.5 text-sm font-medium hover:bg-muted", className), children: [mark, variant === "inline" && _jsx("span", { children: title }), _jsxs("span", { className: "sr-only", children: [variant === "floating" ? label : "", unread > 0 ? `, ${t.unread(unread)}` : ""] }), unread > 0 && (_jsx("span", { "aria-hidden": true, "data-slot": "agent-unread", className: "absolute -top-1 -right-1 inline-flex min-w-5 items-center justify-center rounded-full border border-danger-border bg-danger-bg px-1 text-2xs font-semibold text-danger-fg", children: unread > 9 ? "9+" : unread }))] }));
25
+ }
@@ -0,0 +1,82 @@
1
+ import { type AbstractAgent } from "@ag-ui/client";
2
+ import * as React from "react";
3
+ import type { ChatToolCall } from "../chat/model.js";
4
+ import { AgentController, type AgentContextEntry, type AgentControllerOptions, type AgentSnapshot, type AgentSuggestion, type SendOptions } from "./controller.js";
5
+ import { type AgentEngine, type ToolInvoker } from "./engine.js";
6
+ import { type AgentTool } from "./tool.js";
7
+ /** What every agent provider takes, whatever runs the agent. */
8
+ export interface CalliopeAgentOptions extends AgentControllerOptions {
9
+ /** Tools for the whole app; a page adds its own with `useAgentTool`. */
10
+ tools?: readonly AgentTool<any>[];
11
+ /** Readables for the whole app; a page adds its own with `useAgentReadable`. */
12
+ context?: readonly AgentContextEntry[];
13
+ /** Prompts offered as chips; a page adds its own with `useAgentSuggestions`. */
14
+ suggestions?: readonly AgentSuggestion[];
15
+ }
16
+ export interface CalliopeAgentProviderProps extends Omit<CalliopeAgentOptions, "forwardedProps"> {
17
+ /** The AG-UI agent: an `HttpAgent`, any `AbstractAgent`, or `createMockAgent` in a story. */
18
+ agent?: AbstractAgent;
19
+ /** Or a plain AG-UI endpoint: an `HttpAgent` is made for it. Same origin, so the session cookie goes along. */
20
+ url?: string;
21
+ /** Request headers for `url` (a project id). Never a secret: this runs in the browser. */
22
+ headers?: Record<string, string>;
23
+ /** The conversation to start in; `switchThread` moves later. */
24
+ threadId?: string;
25
+ /** Sent with every run (the model picked, the page). */
26
+ forwardedProps?: Record<string, unknown>;
27
+ children: React.ReactNode;
28
+ }
29
+ /** The agent as a page reads it: the conversation and what the person can do. */
30
+ export interface CalliopeAgentApi extends AgentSnapshot {
31
+ send: (text: string, options?: SendOptions) => Promise<void>;
32
+ stop: () => void;
33
+ regenerate: () => Promise<void>;
34
+ approve: (call: ChatToolCall, payload?: Record<string, unknown>) => void;
35
+ deny: (call: ChatToolCall, reason: string) => void;
36
+ resolveInterrupt: AgentController["resolveInterrupt"];
37
+ respond: (toolCallId: string, result: unknown) => void;
38
+ getState: <T = unknown>() => T;
39
+ setState: (state: unknown) => void;
40
+ registerTool: AgentController["registerTool"];
41
+ addContext: (entry: AgentContextEntry) => () => void;
42
+ switchThread: (threadId: string) => Promise<void>;
43
+ newThread: () => Promise<string>;
44
+ setVisible: (visible: boolean) => void;
45
+ agent: AbstractAgent;
46
+ controller: AgentController;
47
+ }
48
+ /** The controller of the nearest agent provider, or null outside one. */
49
+ export declare function useAgentController(): AgentController | null;
50
+ /**
51
+ * Hosts a controller for any engine; the providers below and the CopilotKit
52
+ * one are this with their engine. Registers the option-level tools, context
53
+ * and suggestions, and connects after mount.
54
+ */
55
+ export declare function AgentHost({ createEngine, engineKey, options, children, }: {
56
+ createEngine: (invoke: ToolInvoker) => AgentEngine;
57
+ /** A new key swaps the engine (a new agent). */
58
+ engineKey: unknown;
59
+ options: CalliopeAgentOptions;
60
+ children: React.ReactNode;
61
+ }): React.JSX.Element;
62
+ /**
63
+ * The agent for a page tree, over a plain AG-UI endpoint (`url`) or any
64
+ * AG-UI agent (`agent`). Headless: it renders nothing of its own; put
65
+ * `CalliopeAgent` (a bubble, a rail, inline or a page) or the chat kit's
66
+ * components under it, and read it with `useCalliopeAgent`. For a CopilotKit
67
+ * runtime use `CopilotKitAgentProvider` from `@calliopelabs/ui/agent/copilotkit`.
68
+ */
69
+ export declare function CalliopeAgentProvider({ agent: given, url, headers, threadId, forwardedProps, children, ...options }: CalliopeAgentProviderProps): React.JSX.Element;
70
+ /** The nearest agent: its conversation, state and actions. Re-renders on every change. */
71
+ export declare function useCalliopeAgent(): CalliopeAgentApi;
72
+ /**
73
+ * Offers a tool to the agent while the component is mounted (a whiteboard's
74
+ * tools only while it is open). The handler and renderer may change every
75
+ * render; the tool re-registers only when its name, description, schema,
76
+ * availability or follow-up change.
77
+ */
78
+ export declare function useAgentTool<Args>(tool: AgentTool<Args>): void;
79
+ /** Tells the agent about the page while mounted: a readable. Re-sent when the value's JSON changes. */
80
+ export declare function useAgentReadable(description: string, value: unknown): void;
81
+ /** Offers prompts as chips while mounted (a page's starting points). */
82
+ export declare function useAgentSuggestions(suggestions: readonly AgentSuggestion[]): void;
@@ -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;