@kanzo-tech/ai 0.1.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 (60) hide show
  1. package/dist/ai-mark.d.ts +54 -0
  2. package/dist/ai-mark.d.ts.map +1 -0
  3. package/dist/ai-mark.js +68 -0
  4. package/dist/ai-mark.js.map +1 -0
  5. package/dist/complete.d.ts +66 -0
  6. package/dist/complete.d.ts.map +1 -0
  7. package/dist/complete.js +259 -0
  8. package/dist/complete.js.map +1 -0
  9. package/dist/conversation.d.ts +38 -0
  10. package/dist/conversation.d.ts.map +1 -0
  11. package/dist/conversation.js +118 -0
  12. package/dist/conversation.js.map +1 -0
  13. package/dist/index.d.ts +25 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +60 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/markdown.d.ts +50 -0
  18. package/dist/markdown.d.ts.map +1 -0
  19. package/dist/markdown.js +36 -0
  20. package/dist/markdown.js.map +1 -0
  21. package/dist/message-part.d.ts +122 -0
  22. package/dist/message-part.d.ts.map +1 -0
  23. package/dist/message-part.js +8 -0
  24. package/dist/message-part.js.map +1 -0
  25. package/dist/message-text.d.ts +44 -0
  26. package/dist/message-text.d.ts.map +1 -0
  27. package/dist/message-text.js +49 -0
  28. package/dist/message-text.js.map +1 -0
  29. package/dist/message.d.ts +73 -0
  30. package/dist/message.d.ts.map +1 -0
  31. package/dist/message.js +120 -0
  32. package/dist/message.js.map +1 -0
  33. package/dist/prompt-input.d.ts +30 -0
  34. package/dist/prompt-input.d.ts.map +1 -0
  35. package/dist/prompt-input.js +113 -0
  36. package/dist/prompt-input.js.map +1 -0
  37. package/dist/reasoning.d.ts +32 -0
  38. package/dist/reasoning.d.ts.map +1 -0
  39. package/dist/reasoning.js +94 -0
  40. package/dist/reasoning.js.map +1 -0
  41. package/dist/styles.css +2 -0
  42. package/dist/suggest.d.ts +75 -0
  43. package/dist/suggest.d.ts.map +1 -0
  44. package/dist/suggest.js +123 -0
  45. package/dist/suggest.js.map +1 -0
  46. package/dist/task.d.ts +48 -0
  47. package/dist/task.d.ts.map +1 -0
  48. package/dist/task.js +91 -0
  49. package/dist/task.js.map +1 -0
  50. package/dist/tool.d.ts +56 -0
  51. package/dist/tool.d.ts.map +1 -0
  52. package/dist/tool.js +122 -0
  53. package/dist/tool.js.map +1 -0
  54. package/dist/types.d.ts +10 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/use-ai.d.ts +170 -0
  57. package/dist/use-ai.d.ts.map +1 -0
  58. package/dist/use-ai.js +186 -0
  59. package/dist/use-ai.js.map +1 -0
  60. package/package.json +111 -0
@@ -0,0 +1,75 @@
1
+ import { ark } from '@ark-ui/react/factory';
2
+ import { Suggestions } from '@kanzo-tech/ui';
3
+ import { AiMarkProps } from './ai-mark.js';
4
+ import { Candidate } from './types.js';
5
+ import * as React from "react";
6
+ /** When the source is asked. */
7
+ export type SuggestTrigger =
8
+ /** On a press of the ✨. Nothing is requested until somebody asks for it. */
9
+ "press"
10
+ /** As soon as the field takes focus. Prefetches, and bills a model for a tab-through. */
11
+ | "focus";
12
+ export interface SuggestRootProps extends Omit<React.ComponentProps<typeof ark.div>, "onSelect"> {
13
+ /** The source. Yields candidates; honour the `AbortSignal`. */
14
+ suggest: (signal?: AbortSignal) => AsyncIterable<Candidate>;
15
+ /** What the field already holds — these are never offered back. */
16
+ existing?: string[];
17
+ /** Where a chosen value goes. */
18
+ onPick: (value: string) => void;
19
+ /**
20
+ * How many to take from the stream.
21
+ *
22
+ * @default 6
23
+ */
24
+ limit?: number;
25
+ /**
26
+ * @default "press"
27
+ */
28
+ trigger?: SuggestTrigger;
29
+ }
30
+ /**
31
+ * Candidate values for one field, offered **in the flow underneath it**.
32
+ *
33
+ * This replaced a popover, and the popover is the whole story. Hiding the candidates behind a door
34
+ * bought nothing and cost six workarounds, every one of them downstream of the door and not of the
35
+ * candidates: a non-modal popover, because a modal one put `pointer-events: none` on the very field
36
+ * the values were for; a `data-autofocus`, because a popover has to hand focus somewhere; a
37
+ * `Delete`/`Backspace` handler, because a button inside `role="option"` is unreachable by Tab; a
38
+ * shared label id, because a popover names itself from a title and a listbox from a label; a ✕ that
39
+ * had to be quieted because an offer looked identical to a committed tag; and a three-row window
40
+ * with a refill loop in the hook to keep it full. None of them exist here.
41
+ *
42
+ * **Focus decides visibility, a press decides billing**, and they are deliberately not the same
43
+ * gesture. The strip appears while the field has focus and there is something to show — which is
44
+ * what keeps a form of eleven fields from becoming a wall, since only one field is focused at a
45
+ * time. Asking is `trigger`, and it defaults to a press: a source that fires on focus bills a model
46
+ * for a tab-through, and that is a decision a caller should make on purpose.
47
+ */
48
+ export declare function SuggestRoot(props: SuggestRootProps): React.JSX.Element;
49
+ /**
50
+ * The ✨ — **a mark, not a door.**
51
+ *
52
+ * At rest it says the field is model-assisted, which a reader can see without touching anything.
53
+ * Pressing it asks. It no longer opens or closes a layer, so there is no `data-state` to light it
54
+ * from and nothing to trap focus in.
55
+ */
56
+ export declare function SuggestMark(props: Omit<AiMarkProps, "busy" | "offering">): React.JSX.Element;
57
+ export interface SuggestListProps extends React.ComponentProps<typeof Suggestions> {
58
+ /** Shown while the source is answering and has produced nothing yet. */
59
+ pending?: React.ReactNode;
60
+ /** Shown when the source finished with nothing to offer. */
61
+ empty?: React.ReactNode;
62
+ }
63
+ /**
64
+ * The strip, and the only part that decides whether anything is on screen.
65
+ *
66
+ * It renders `Suggestions` from `@kanzo-tech/ui` — a row of buttons that knows nothing about a
67
+ * model — so everything model-shaped stays on this side of the boundary: the ✨, the engine, and
68
+ * the rationale that comes back with a candidate.
69
+ *
70
+ * The rationale is the highlighted pill's `title` rather than a line under the strip. The line was
71
+ * tried and it moved the layout on every hover; a `title` is the browser's own tooltip, reaches the
72
+ * keyboard through focus, and costs no box.
73
+ */
74
+ export declare function SuggestList(props: SuggestListProps): React.JSX.Element | null;
75
+ //# sourceMappingURL=suggest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../src/suggest.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAC5C,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAgD,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAIxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAqB5C,gCAAgC;AAChC,MAAM,MAAM,cAAc;AACxB,4EAA4E;AAC1E,OAAO;AACT,yFAAyF;GACvF,OAAO,CAAC;AAEZ,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC;IAC9D,+DAA+D;IAC/D,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,aAAa,CAAC,SAAS,CAAC,CAAC;IAC5D,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,iCAAiC;IACjC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,qBA2ElD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CAAC,qBAmBxE;AAED,MAAM,WAAW,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,OAAO,WAAW,CAAC;IAChF,wEAAwE;IACxE,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,4DAA4D;IAC5D,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,4BAiElD"}
@@ -0,0 +1,123 @@
1
+ "use client";
2
+ import { jsx as r, jsxs as x } from "react/jsx-runtime";
3
+ import { XIcon as w } from "lucide-react";
4
+ import { ark as d } from "@ark-ui/react/factory";
5
+ import * as f from "react";
6
+ import { ButtonGroup as M, Suggestion as P, Button as R, Spinner as T, Suggestions as j, cn as h } from "@kanzo-tech/ui";
7
+ import { AiMark as A } from "./ai-mark.js";
8
+ import { useSuggestions as B } from "./use-ai.js";
9
+ const S = f.createContext(null), k = (o) => {
10
+ const n = f.useContext(S);
11
+ if (!n) throw new Error(`${o} must render inside <SuggestRoot>`);
12
+ return n;
13
+ };
14
+ function I(o) {
15
+ const {
16
+ suggest: n,
17
+ existing: l,
18
+ onPick: c,
19
+ limit: a,
20
+ trigger: u = "press",
21
+ children: t,
22
+ className: m,
23
+ onBlur: e,
24
+ onFocus: g,
25
+ slot: v,
26
+ ...N
27
+ } = o, s = B({ suggest: n, existing: l, limit: a }), [b, p] = f.useState(!1), y = (i) => {
28
+ c(i), s.dismiss(i);
29
+ }, C = () => {
30
+ s.status !== "loading" && (s.status === "error" || s.items.length > 0 ? s.refresh() : s.ask());
31
+ };
32
+ return /* @__PURE__ */ r(
33
+ S.Provider,
34
+ {
35
+ value: {
36
+ candidates: s.items,
37
+ status: s.status,
38
+ error: s.error,
39
+ active: b,
40
+ ask: C,
41
+ pick: y,
42
+ dismiss: s.dismiss
43
+ },
44
+ children: /* @__PURE__ */ r(
45
+ d.div,
46
+ {
47
+ className: h("flex w-full min-w-0 flex-col gap-2", m),
48
+ onBlur: (i) => {
49
+ e == null || e(i), i.currentTarget.contains(i.relatedTarget) || p(!1);
50
+ },
51
+ onFocus: (i) => {
52
+ g == null || g(i), p(!0), u === "focus" && s.ask();
53
+ },
54
+ ...N,
55
+ "data-slot": v ?? "suggest",
56
+ children: t
57
+ }
58
+ )
59
+ }
60
+ );
61
+ }
62
+ function X(o) {
63
+ const { label: n = "Suggest", onClick: l, ...c } = o, a = k("SuggestMark"), u = a.candidates.length > 0;
64
+ return /* @__PURE__ */ r(
65
+ A,
66
+ {
67
+ busy: a.status === "loading" && !u,
68
+ label: u ? "Suggest different values" : n,
69
+ offering: u,
70
+ onClick: (t) => {
71
+ l == null || l(t), t.defaultPrevented || a.ask();
72
+ },
73
+ ...c
74
+ }
75
+ );
76
+ }
77
+ function q(o) {
78
+ const { className: n, empty: l, pending: c, slot: a, ...u } = o, t = k("SuggestList");
79
+ if (!t.active) return null;
80
+ const m = t.error !== null ? /* @__PURE__ */ r(d.p, { className: "text-destructive-foreground text-sm", "data-slot": "suggest-error", children: t.error }) : t.candidates.length > 0 ? t.candidates.map((e) => {
81
+ const g = e.label ?? e.value;
82
+ return (
83
+ // **Two buttons, so a group — not a ✕ inside the pill.** `Suggestion` *is* a
84
+ // `<button>`, and a button inside a button is not a thing the DOM has; the dismiss has
85
+ // to be a sibling. `ButtonGroup` is the house cluster: it collapses the inner radii and
86
+ // overlaps the shared border, so the pair reads as one pill with a divided end, and it
87
+ // requires the accessible name that a `role="group"` owes.
88
+ /* @__PURE__ */ x(M, { "aria-label": g, slot: "suggest-item", children: [
89
+ /* @__PURE__ */ r(P, { onSelect: t.pick, title: e.rationale, value: e.value, children: g }),
90
+ /* @__PURE__ */ r(
91
+ R,
92
+ {
93
+ "aria-label": `Dismiss ${g}`,
94
+ className: "h-auto min-h-[24px] self-stretch px-2 py-1",
95
+ onClick: () => t.dismiss(e.value),
96
+ pill: !0,
97
+ size: "sm",
98
+ slot: "suggest-dismiss",
99
+ variant: "outline",
100
+ children: /* @__PURE__ */ r(w, {})
101
+ }
102
+ )
103
+ ] }, e.value)
104
+ );
105
+ }) : t.status === "loading" ? c ?? /* @__PURE__ */ x(
106
+ d.span,
107
+ {
108
+ className: "inline-flex items-center gap-2 text-muted-foreground text-sm",
109
+ "data-slot": "suggest-pending",
110
+ children: [
111
+ /* @__PURE__ */ r(T, { "aria-hidden": !0 }),
112
+ "Thinking…"
113
+ ]
114
+ }
115
+ ) : t.status === "ready" ? l ?? /* @__PURE__ */ r(d.span, { className: "text-muted-foreground text-sm", "data-slot": "suggest-empty", children: "Nothing to suggest." }) : null;
116
+ return m === null ? null : /* @__PURE__ */ r(j, { className: h("min-h-8", n), ...u, slot: a ?? "suggest-list", children: m });
117
+ }
118
+ export {
119
+ q as SuggestList,
120
+ X as SuggestMark,
121
+ I as SuggestRoot
122
+ };
123
+ //# sourceMappingURL=suggest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggest.js","sources":["../src/suggest.tsx"],"sourcesContent":["\"use client\";\n\nimport { XIcon } from \"lucide-react\";\nimport { ark } from \"@ark-ui/react/factory\";\nimport * as React from \"react\";\nimport { Button, ButtonGroup, cn, Spinner, Suggestion, Suggestions } from \"@kanzo-tech/ui\";\nimport { AiMark, type AiMarkProps } from \"./ai-mark.js\";\n// No alias any more, and that is the whole of this rename: the type and `@kanzo-tech/ui`'s\n// `Suggestion` component were both in scope in this file, both on public barrels, and one of them\n// had to be renamed at the import to be usable at all.\nimport type { Candidate } from \"./types.js\";\nimport { type AiStatus, useSuggestions } from \"./use-ai.js\";\n\ninterface SuggestCtx {\n candidates: Candidate[];\n status: AiStatus;\n error: string | null;\n /** The field, or something inside it, holds focus. */\n active: boolean;\n ask: () => void;\n pick: (value: string) => void;\n dismiss: (value: string) => void;\n}\n\nconst Ctx = React.createContext<SuggestCtx | null>(null);\nconst useCtx = (part: string): SuggestCtx => {\n const context = React.useContext(Ctx);\n if (!context) throw new Error(`${part} must render inside <SuggestRoot>`);\n return context;\n};\n\n/** When the source is asked. */\nexport type SuggestTrigger =\n /** On a press of the ✨. Nothing is requested until somebody asks for it. */\n | \"press\"\n /** As soon as the field takes focus. Prefetches, and bills a model for a tab-through. */\n | \"focus\";\n\nexport interface SuggestRootProps\n extends Omit<React.ComponentProps<typeof ark.div>, \"onSelect\"> {\n /** The source. Yields candidates; honour the `AbortSignal`. */\n suggest: (signal?: AbortSignal) => AsyncIterable<Candidate>;\n /** What the field already holds — these are never offered back. */\n existing?: string[];\n /** Where a chosen value goes. */\n onPick: (value: string) => void;\n /**\n * How many to take from the stream.\n *\n * @default 6\n */\n limit?: number;\n /**\n * @default \"press\"\n */\n trigger?: SuggestTrigger;\n}\n\n/**\n * Candidate values for one field, offered **in the flow underneath it**.\n *\n * This replaced a popover, and the popover is the whole story. Hiding the candidates behind a door\n * bought nothing and cost six workarounds, every one of them downstream of the door and not of the\n * candidates: a non-modal popover, because a modal one put `pointer-events: none` on the very field\n * the values were for; a `data-autofocus`, because a popover has to hand focus somewhere; a\n * `Delete`/`Backspace` handler, because a button inside `role=\"option\"` is unreachable by Tab; a\n * shared label id, because a popover names itself from a title and a listbox from a label; a ✕ that\n * had to be quieted because an offer looked identical to a committed tag; and a three-row window\n * with a refill loop in the hook to keep it full. None of them exist here.\n *\n * **Focus decides visibility, a press decides billing**, and they are deliberately not the same\n * gesture. The strip appears while the field has focus and there is something to show — which is\n * what keeps a form of eleven fields from becoming a wall, since only one field is focused at a\n * time. Asking is `trigger`, and it defaults to a press: a source that fires on focus bills a model\n * for a tab-through, and that is a decision a caller should make on purpose.\n */\nexport function SuggestRoot(props: SuggestRootProps) {\n const {\n suggest,\n existing,\n onPick,\n limit,\n trigger = \"press\",\n children,\n className,\n onBlur,\n onFocus,\n slot,\n ...rest\n } = props;\n\n const controller = useSuggestions({ suggest, existing, limit });\n const [active, setActive] = React.useState(false);\n\n const pick = (value: string) => {\n onPick(value);\n // A taken candidate is no longer a candidate. Nothing refills — the strip simply gets shorter,\n // which is the honest picture of a source that has already answered.\n controller.dismiss(value);\n };\n\n /**\n * **One meaning for a press, in both compounds: give me the assistance.**\n *\n * `Complete` reads that as *take the continuation* when there is one, because a ghost is a single\n * offer that needs a gesture to accept. A strip is N offers that already carry their own buttons,\n * so here the next useful thing is a different set — and after a failure it is a retry. The hook's\n * `ask` stays strict (idle only, so a press cannot re-bill a source that already answered) and\n * this is where the policy lives, which is why it is in the compound and not in `AiMark`.\n *\n * Loading is the one state where nothing happens: the mark is a spinner then, and a spinner you\n * can press is a lie.\n */\n const press = () => {\n if (controller.status === \"loading\") return;\n if (controller.status === \"error\" || controller.items.length > 0) controller.refresh();\n else controller.ask();\n };\n\n return (\n <Ctx.Provider\n value={{\n candidates: controller.items,\n status: controller.status,\n error: controller.error,\n active,\n ask: press,\n pick,\n dismiss: controller.dismiss,\n }}\n >\n <ark.div\n className={cn(\"flex w-full min-w-0 flex-col gap-2\", className)}\n onBlur={(event) => {\n onBlur?.(event);\n // `relatedTarget` is where focus is going. Inside our own subtree it is a pill being\n // clicked or the ✨ being tabbed to, and the strip must survive both.\n if (!event.currentTarget.contains(event.relatedTarget)) setActive(false);\n }}\n onFocus={(event) => {\n onFocus?.(event);\n setActive(true);\n if (trigger === \"focus\") controller.ask();\n }}\n {...rest}\n data-slot={slot ?? \"suggest\"}\n >\n {children}\n </ark.div>\n </Ctx.Provider>\n );\n}\n\n/**\n * The ✨ — **a mark, not a door.**\n *\n * At rest it says the field is model-assisted, which a reader can see without touching anything.\n * Pressing it asks. It no longer opens or closes a layer, so there is no `data-state` to light it\n * from and nothing to trap focus in.\n */\nexport function SuggestMark(props: Omit<AiMarkProps, \"busy\" | \"offering\">) {\n const { label = \"Suggest\", onClick, ...rest } = props;\n const ctx = useCtx(\"SuggestMark\");\n // Bound exactly as `CompleteMark` binds it. A mark that paints one way over a ghost and another\n // over a strip reads as two products; `ai-mark.test.tsx` is the guard on that.\n const offering = ctx.candidates.length > 0;\n\n return (\n <AiMark\n busy={ctx.status === \"loading\" && !offering}\n label={offering ? \"Suggest different values\" : label}\n offering={offering}\n onClick={(event) => {\n onClick?.(event);\n if (!event.defaultPrevented) ctx.ask();\n }}\n {...rest}\n />\n );\n}\n\nexport interface SuggestListProps extends React.ComponentProps<typeof Suggestions> {\n /** Shown while the source is answering and has produced nothing yet. */\n pending?: React.ReactNode;\n /** Shown when the source finished with nothing to offer. */\n empty?: React.ReactNode;\n}\n\n/**\n * The strip, and the only part that decides whether anything is on screen.\n *\n * It renders `Suggestions` from `@kanzo-tech/ui` — a row of buttons that knows nothing about a\n * model — so everything model-shaped stays on this side of the boundary: the ✨, the engine, and\n * the rationale that comes back with a candidate.\n *\n * The rationale is the highlighted pill's `title` rather than a line under the strip. The line was\n * tried and it moved the layout on every hover; a `title` is the browser's own tooltip, reaches the\n * keyboard through focus, and costs no box.\n */\nexport function SuggestList(props: SuggestListProps) {\n const { className, empty, pending, slot, ...rest } = props;\n const ctx = useCtx(\"SuggestList\");\n\n if (!ctx.active) return null;\n\n const body =\n ctx.error !== null ? (\n <ark.p className=\"text-destructive-foreground text-sm\" data-slot=\"suggest-error\">\n {ctx.error}\n </ark.p>\n ) : ctx.candidates.length > 0 ? (\n ctx.candidates.map((candidate) => {\n const label = candidate.label ?? candidate.value;\n return (\n // **Two buttons, so a group — not a ✕ inside the pill.** `Suggestion` *is* a\n // `<button>`, and a button inside a button is not a thing the DOM has; the dismiss has\n // to be a sibling. `ButtonGroup` is the house cluster: it collapses the inner radii and\n // overlaps the shared border, so the pair reads as one pill with a divided end, and it\n // requires the accessible name that a `role=\"group\"` owes.\n <ButtonGroup aria-label={label} key={candidate.value} slot=\"suggest-item\">\n <Suggestion onSelect={ctx.pick} title={candidate.rationale} value={candidate.value}>\n {label}\n </Suggestion>\n {/* Always drawn, never on hover: a hover-only control is unreachable by a keyboard and\n absent on a touch pointer, and this strip is explicitly reachable by Tab. */}\n <Button\n aria-label={`Dismiss ${label}`}\n className=\"h-auto min-h-[24px] self-stretch px-2 py-1\"\n onClick={() => ctx.dismiss(candidate.value)}\n pill\n size=\"sm\"\n slot=\"suggest-dismiss\"\n variant=\"outline\"\n >\n <XIcon />\n </Button>\n </ButtonGroup>\n );\n })\n ) : ctx.status === \"loading\" ? (\n (pending ?? (\n <ark.span\n className=\"inline-flex items-center gap-2 text-muted-foreground text-sm\"\n data-slot=\"suggest-pending\"\n >\n <Spinner aria-hidden />\n Thinking…\n </ark.span>\n ))\n ) : ctx.status === \"ready\" ? (\n (empty ?? (\n <ark.span className=\"text-muted-foreground text-sm\" data-slot=\"suggest-empty\">\n Nothing to suggest.\n </ark.span>\n ))\n ) : null;\n\n if (body === null) return null;\n\n return (\n <Suggestions className={cn(\"min-h-8\", className)} {...rest} slot={slot ?? \"suggest-list\"}>\n {body}\n </Suggestions>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAwBA;AAEE;AACA;AACA;AACF;AA+CO;AACL;AAAM;AACJ;AACA;AACA;AACA;AACU;AACV;AACA;AACA;AACA;AACA;AACG;AAOH;AAGwB;AAgBxB;AAEgB;AAGlB;AACE;AAAK;AAAJ;AACQ;AACkB;AACJ;AACD;AAClB;AACK;AACL;AACoB;AAAA;AAGtB;AAAK;AAAJ;AAC8D;AAE3D;AAGuE;AACzE;AAEE;AAEoC;AACtC;AACI;AACe;AAElB;AAAA;AAAA;AACH;AAGN;AASO;AACL;AAMA;AACE;AAAC;AAAA;AACoC;AACY;AAC/C;AAEE;AACiC;AACnC;AACI;AAAA;AAGV;AAoBO;AACL;AAGA;AAEA;AAOM;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOI;AAEA;AAGA;AAAC;AAAA;AAC6B;AAClB;AACgC;AACtC;AACC;AACA;AACG;AAED;AAAA;AAAA;AAEX;AAAA;AAKF;AAAK;AAAJ;AACW;AACA;AAEV;AAAqB;AAAE;AAAA;AAAA;AAY/B;AAOF;;;;;;"}
package/dist/task.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { ark } from '@ark-ui/react/factory';
2
+ import * as React from "react";
3
+ /**
4
+ * The life of one step. Shared with `Tool`, which imports it — a tool call is a step with a name,
5
+ * and giving the two their own four-word vocabularies is how they end up disagreeing.
6
+ */
7
+ export type RunState = "pending" | "running" | "done" | "failed";
8
+ /** The word for each state, in one place because `Tool`'s badge says the same four. */
9
+ export declare const RUN_LABEL: Record<RunState, string>;
10
+ /**
11
+ * The mark for each state — **shared with `Tool`, which imports it.** One icon family at one
12
+ * stroke weight, tinted by the family's own `-foreground` token, which for a status family is the
13
+ * readable-on-the-page variant rather than ink on a fill (`tokens.css` states both meanings).
14
+ *
15
+ * This was four `Status` dots, and `Status` is a presence dot: its recipe carries `ring-2
16
+ * ring-background`, a halo for sitting on an avatar, which cut a 2px hole in the rail the marks
17
+ * stand on. Fighting it took a `size-4` override of `size="lg"`'s 12px and a `bg-transparent
18
+ * ring-1` hollowing for pending — a primitive argued with in three places is the wrong primitive.
19
+ * `Tool` already drew these four states as icons; now there is one drawing of them, not two.
20
+ *
21
+ * Unsized on purpose: the parent sizes the glyph, so the same table serves a 16px rail mark and a
22
+ * 12px badge icon.
23
+ */
24
+ export declare const RUN_MARK: Record<RunState, React.ReactNode>;
25
+ export declare const TaskList: (props: React.ComponentProps<typeof ark.ol>) => React.JSX.Element;
26
+ export interface TaskProps extends React.ComponentProps<typeof ark.li> {
27
+ state?: RunState;
28
+ }
29
+ /**
30
+ * One labelled step. The state is declared here and read off `data-state` by the parts.
31
+ *
32
+ * The rail through the markers is the step's own `::after` rather than a border on the list: a run
33
+ * of five has to read as one run, and the last step must not trail a line into nothing.
34
+ */
35
+ export declare const Task: (props: TaskProps) => React.JSX.Element;
36
+ export declare const TaskTitle: (props: React.ComponentProps<typeof ark.span>) => React.JSX.Element;
37
+ /**
38
+ * The step's marker — {@link RUN_MARK} for this step's state, in a box the rail runs out of.
39
+ *
40
+ * The mark is decorative to a screen reader; the word beside it is what carries the state, and it
41
+ * is written here as `sr-only` rather than assumed of the title. Pass children to say it in your
42
+ * own words.
43
+ *
44
+ * The box is `size-5` and centred so the mark lands on the first line's optical centre: `text-sm`
45
+ * is a 20px line box, and a marker aligned to the top of a wrapping title sits above its own text.
46
+ */
47
+ export declare const TaskStatus: (props: React.ComponentProps<typeof ark.span>) => React.JSX.Element;
48
+ //# sourceMappingURL=task.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../src/task.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAE5C,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjE,uFAAuF;AACvF,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAK9C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAKtD,CAAC;AAkBF,eAAO,MAAM,QAAQ,GAAI,OAAO,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC,sBAalE,CAAC;AAEF,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;IACpE,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,SAAS,sBAoBpC,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,OAAO,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,sBAUrE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,sBAmBtE,CAAC"}
package/dist/task.js ADDED
@@ -0,0 +1,91 @@
1
+ "use client";
2
+ import { jsx as s, jsxs as u } from "react/jsx-runtime";
3
+ import { ark as o } from "@ark-ui/react/factory";
4
+ import { CircleXIcon as f, CircleCheckIcon as m, CircleDashedIcon as g } from "lucide-react";
5
+ import * as c from "react";
6
+ import { cn as l, Spinner as p } from "@kanzo-tech/ui";
7
+ const x = {
8
+ pending: "Pending",
9
+ running: "Running",
10
+ done: "Done",
11
+ failed: "Failed"
12
+ }, k = {
13
+ pending: /* @__PURE__ */ s(g, { className: "text-muted-foreground" }),
14
+ running: /* @__PURE__ */ s(p, { "aria-hidden": !0, className: "text-info-foreground" }),
15
+ done: /* @__PURE__ */ s(m, { className: "text-success-foreground" }),
16
+ failed: /* @__PURE__ */ s(f, { className: "text-destructive-foreground" })
17
+ }, N = [
18
+ "min-w-0 flex-1 truncate leading-5",
19
+ "group-data-[state=pending]/task:text-muted-foreground",
20
+ "group-data-[state=failed]/task:text-destructive-foreground"
21
+ ].join(" "), d = c.createContext(null), h = (e) => {
22
+ const t = c.useContext(d);
23
+ if (!t) throw new Error(`${e} must render inside <Task>`);
24
+ return t;
25
+ }, R = (e) => {
26
+ const { className: t, slot: a, ...r } = e;
27
+ return /* @__PURE__ */ s(
28
+ o.ol,
29
+ {
30
+ className: l("flex w-full list-none flex-col gap-2", t),
31
+ role: "list",
32
+ ...r,
33
+ "data-slot": a ?? "task-list"
34
+ }
35
+ );
36
+ }, b = (e) => {
37
+ const { state: t = "pending", className: a, children: r, slot: i, ...n } = e;
38
+ return /* @__PURE__ */ s(d.Provider, { value: { state: t }, children: /* @__PURE__ */ s(
39
+ o.li,
40
+ {
41
+ className: l(
42
+ "group/task relative flex w-full items-start gap-2.5 text-sm",
43
+ "after:absolute after:top-5 after:-bottom-2 after:start-2.5 after:w-px after:bg-border",
44
+ "last:after:hidden",
45
+ a
46
+ ),
47
+ "data-state": t,
48
+ ...n,
49
+ "data-slot": i ?? "task",
50
+ children: r
51
+ }
52
+ ) });
53
+ }, j = (e) => {
54
+ const { className: t, slot: a, ...r } = e;
55
+ return /* @__PURE__ */ s(
56
+ o.span,
57
+ {
58
+ className: l(N, t),
59
+ ...r,
60
+ "data-slot": a ?? "task-title"
61
+ }
62
+ );
63
+ }, z = (e) => {
64
+ const { className: t, children: a, slot: r, ...i } = e, { state: n } = h("TaskStatus");
65
+ return /* @__PURE__ */ u(
66
+ o.span,
67
+ {
68
+ className: l(
69
+ "relative z-10 flex size-5 shrink-0 items-center justify-center",
70
+ "[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
71
+ t
72
+ ),
73
+ "data-state": n,
74
+ ...i,
75
+ "data-slot": r ?? "task-status",
76
+ children: [
77
+ k[n],
78
+ /* @__PURE__ */ s(o.span, { className: "sr-only", children: a ?? x[n] })
79
+ ]
80
+ }
81
+ );
82
+ };
83
+ export {
84
+ x as RUN_LABEL,
85
+ k as RUN_MARK,
86
+ b as Task,
87
+ R as TaskList,
88
+ z as TaskStatus,
89
+ j as TaskTitle
90
+ };
91
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.js","sources":["../src/task.tsx"],"sourcesContent":["\"use client\";\n\nimport { ark } from \"@ark-ui/react/factory\";\nimport { CircleCheckIcon, CircleDashedIcon, CircleXIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn, Spinner } from \"@kanzo-tech/ui\";\n\n/**\n * The life of one step. Shared with `Tool`, which imports it — a tool call is a step with a name,\n * and giving the two their own four-word vocabularies is how they end up disagreeing.\n */\nexport type RunState = \"pending\" | \"running\" | \"done\" | \"failed\";\n\n/** The word for each state, in one place because `Tool`'s badge says the same four. */\nexport const RUN_LABEL: Record<RunState, string> = {\n pending: \"Pending\",\n running: \"Running\",\n done: \"Done\",\n failed: \"Failed\",\n};\n\n/**\n * The mark for each state — **shared with `Tool`, which imports it.** One icon family at one\n * stroke weight, tinted by the family's own `-foreground` token, which for a status family is the\n * readable-on-the-page variant rather than ink on a fill (`tokens.css` states both meanings).\n *\n * This was four `Status` dots, and `Status` is a presence dot: its recipe carries `ring-2\n * ring-background`, a halo for sitting on an avatar, which cut a 2px hole in the rail the marks\n * stand on. Fighting it took a `size-4` override of `size=\"lg\"`'s 12px and a `bg-transparent\n * ring-1` hollowing for pending — a primitive argued with in three places is the wrong primitive.\n * `Tool` already drew these four states as icons; now there is one drawing of them, not two.\n *\n * Unsized on purpose: the parent sizes the glyph, so the same table serves a 16px rail mark and a\n * 12px badge icon.\n */\nexport const RUN_MARK: Record<RunState, React.ReactNode> = {\n pending: <CircleDashedIcon className=\"text-muted-foreground\" />,\n running: <Spinner aria-hidden className=\"text-info-foreground\" />,\n done: <CircleCheckIcon className=\"text-success-foreground\" />,\n failed: <CircleXIcon className=\"text-destructive-foreground\" />,\n};\n\n/** The state reaches the title through the root's `data-state`, so the title needs no context. */\n// One weight across the five rows. The mark already says which step is running and which failed,\n// and a second emphasis on the same fact reads as three type styles in a five-row list.\nconst TITLE = [\n \"min-w-0 flex-1 truncate leading-5\",\n \"group-data-[state=pending]/task:text-muted-foreground\",\n \"group-data-[state=failed]/task:text-destructive-foreground\",\n].join(\" \");\n\nconst Ctx = React.createContext<{ state: RunState } | null>(null);\nconst useCtx = (part: string) => {\n const c = React.useContext(Ctx);\n if (!c) throw new Error(`${part} must render inside <Task>`);\n return c;\n};\n\nexport const TaskList = (props: React.ComponentProps<typeof ark.ol>) => {\n const { className, slot, ...rest } = props;\n\n return (\n <ark.ol\n className={cn(\"flex w-full list-none flex-col gap-2\", className)}\n // `role=\"list\"`, because `list-none` takes the implicit one away in WebKit. See\n // `DiagnosticList` in `@kanzo-tech/ui` for the argument.\n role=\"list\"\n {...rest}\n data-slot={slot ?? \"task-list\"}\n />\n );\n};\n\nexport interface TaskProps extends React.ComponentProps<typeof ark.li> {\n state?: RunState;\n}\n\n/**\n * One labelled step. The state is declared here and read off `data-state` by the parts.\n *\n * The rail through the markers is the step's own `::after` rather than a border on the list: a run\n * of five has to read as one run, and the last step must not trail a line into nothing.\n */\nexport const Task = (props: TaskProps) => {\n const { state = \"pending\", className, children, slot, ...rest } = props;\n\n return (\n <Ctx.Provider value={{ state }}>\n <ark.li\n className={cn(\n \"group/task relative flex w-full items-start gap-2.5 text-sm\",\n \"after:absolute after:top-5 after:-bottom-2 after:start-2.5 after:w-px after:bg-border\",\n \"last:after:hidden\",\n className\n )}\n data-state={state}\n {...rest}\n data-slot={slot ?? \"task\"}\n >\n {children}\n </ark.li>\n </Ctx.Provider>\n );\n};\n\nexport const TaskTitle = (props: React.ComponentProps<typeof ark.span>) => {\n const { className, slot, ...rest } = props;\n\n return (\n <ark.span\n className={cn(TITLE, className)}\n {...rest}\n data-slot={slot ?? \"task-title\"}\n />\n );\n};\n\n/**\n * The step's marker — {@link RUN_MARK} for this step's state, in a box the rail runs out of.\n *\n * The mark is decorative to a screen reader; the word beside it is what carries the state, and it\n * is written here as `sr-only` rather than assumed of the title. Pass children to say it in your\n * own words.\n *\n * The box is `size-5` and centred so the mark lands on the first line's optical centre: `text-sm`\n * is a 20px line box, and a marker aligned to the top of a wrapping title sits above its own text.\n */\nexport const TaskStatus = (props: React.ComponentProps<typeof ark.span>) => {\n const { className, children, slot, ...rest } = props;\n const { state } = useCtx(\"TaskStatus\");\n\n return (\n <ark.span\n className={cn(\n \"relative z-10 flex size-5 shrink-0 items-center justify-center\",\n \"[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0\",\n className\n )}\n data-state={state}\n {...rest}\n data-slot={slot ?? \"task-status\"}\n >\n {RUN_MARK[state]}\n <ark.span className=\"sr-only\">{children ?? RUN_LABEL[state]}</ark.span>\n </ark.span>\n );\n};\n"],"names":[],"mappings":";;;;;;AAcO;AAA4C;AACxC;AACA;AACH;AAER;AAgB2D;AACI;AACE;AACJ;AAE7D;AAKc;AACZ;AACA;AAEF;AAIE;AACA;AACA;AACF;AAGE;AAEA;AACE;AAAK;AAAJ;AACgE;AAG1D;AACD;AACe;AAAA;AAGzB;AAaE;AAEA;AAEI;AAAK;AAAJ;AACY;AACT;AACA;AACA;AACA;AAAA;AAEU;AACR;AACe;AAElB;AAAA;AAIT;AAGE;AAEA;AACE;AAAK;AAAJ;AAC+B;AAC1B;AACe;AAAA;AAGzB;AAaE;AAGA;AACE;AAAK;AAAJ;AACY;AACT;AACA;AACA;AAAA;AAEU;AACR;AACe;AAElB;AAAc;AAC6C;AAAA;AAAA;AAGlE;;;;;;;;;"}
package/dist/tool.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { ark } from '@ark-ui/react/factory';
2
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@kanzo-tech/ui';
3
+ import { RunState } from './task.js';
4
+ import * as React from "react";
5
+ export interface ToolProps extends React.ComponentProps<typeof ark.div> {
6
+ state?: RunState;
7
+ defaultOpen?: boolean;
8
+ open?: boolean;
9
+ onOpenChange?: React.ComponentProps<typeof Collapsible>["onOpenChange"];
10
+ }
11
+ /**
12
+ * One call the model made, folded away until it has an answer — reaching `done` or `failed` opens
13
+ * it, and the reader's own toggle ends that for good.
14
+ *
15
+ * **It opens on the transition, not on the initial value, and that distinction is the whole of it.**
16
+ * This was `defaultOpen={defaultOpen ?? state === "done"}`, which Ark's Collapsible reads once at
17
+ * mount: a tool mounted the moment the model plans it — state `pending` — never opened again, not
18
+ * when it finished and not when it failed. Failing silently closed is the worse half: the reason
19
+ * the call failed sat behind a click nobody knew to make. The docblock claimed the opposite, and a
20
+ * showcase had to work around it before anyone noticed.
21
+ *
22
+ * A caller passing `open` still wins outright; `defaultOpen` still decides the first paint.
23
+ *
24
+ * The state sits on a wrapper rather than on the collapsible, and that is not tidiness: Ark writes
25
+ * its own `data-state` (`open` / `closed`) on the machine's root, so a tool state written there
26
+ * replaces it — the parts keep working, and every `[data-state=open]` a consumer wrote stops
27
+ * matching, with nothing to see in a diff.
28
+ */
29
+ export declare const Tool: (props: ToolProps) => React.JSX.Element;
30
+ /**
31
+ * The row that says a call happened: the wrench, the tool's name in the face its arguments are
32
+ * written in, the state as a pill, and the chevron the whole row toggles.
33
+ */
34
+ export declare const ToolHeader: (props: React.ComponentProps<typeof CollapsibleTrigger>) => React.JSX.Element;
35
+ export declare const ToolContent: (props: React.ComponentProps<typeof CollapsibleContent>) => React.JSX.Element;
36
+ export interface ToolIoProps extends React.ComponentProps<typeof ark.div> {
37
+ /** Drawn as a JSON tree when there are no children. */
38
+ data?: unknown;
39
+ /** The small heading over the panel. `null` drops it. */
40
+ label?: string | null;
41
+ }
42
+ /**
43
+ * What was passed to the call — **children, not a formatted-JSON prop.**
44
+ *
45
+ * The reference takes `input` and renders it as JSON because a general chatbot cannot know what
46
+ * the tool was. We always know: our input is a SQL statement and our output is a result table, and
47
+ * both have a component in this house already. So the payload is composition, and JSON is only the
48
+ * default for when the caller has nothing better — never the one thing on offer.
49
+ *
50
+ * Which is also why the panel does not force a monospace face on everything inside it: a result
51
+ * table set in mono is the cost of a default that assumed JSON. Code sets itself.
52
+ */
53
+ export declare const ToolInput: (props: ToolIoProps) => React.JSX.Element;
54
+ /** What came back. Children, for the same reason as {@link ToolInput}. */
55
+ export declare const ToolOutput: (props: ToolIoProps) => React.JSX.Element;
56
+ //# sourceMappingURL=tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAE5C,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAIL,WAAW,EACX,kBAAkB,EAElB,kBAAkB,EAEnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAuB,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AA6B/D,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC;IACrE,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,WAAW,CAAC,CAAC,cAAc,CAAC,CAAC;CACzE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,SAAS,sBAwCpC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,sBAwBhF,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,KAAK,CAAC,cAAc,CAAC,OAAO,kBAAkB,CAAC,sBAejF,CAAC;AAEF,MAAM,WAAW,WAAY,SAAQ,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC;IACvE,uDAAuD;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,WAAW,sBAoB3C,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,UAAU,GAAI,OAAO,WAAW,sBA8B5C,CAAC"}
package/dist/tool.js ADDED
@@ -0,0 +1,122 @@
1
+ "use client";
2
+ import { jsx as t, jsxs as p, Fragment as N } from "react/jsx-runtime";
3
+ import { ark as c } from "@ark-ui/react/factory";
4
+ import { WrenchIcon as E } from "lucide-react";
5
+ import * as d from "react";
6
+ import { Collapsible as k, cn as i, CollapsibleContent as y, CollapsibleTrigger as L, Badge as O, CollapsibleIndicator as A, JsonTreeView as b } from "@kanzo-tech/ui";
7
+ import { RUN_MARK as _, RUN_LABEL as j } from "./task.js";
8
+ const z = {
9
+ pending: "outline",
10
+ running: "info",
11
+ done: "success",
12
+ failed: "destructive"
13
+ }, v = "mb-1.5 block font-medium text-muted-foreground text-xs uppercase tracking-wide", w = [
14
+ "min-w-0 overflow-x-auto",
15
+ "rounded-lg bg-muted",
16
+ "px-3 py-2 text-xs",
17
+ "[&_code]:font-mono"
18
+ ].join(" "), C = d.createContext(null), B = (o) => {
19
+ const e = d.useContext(C);
20
+ if (!e) throw new Error(`${o} must render inside <Tool>`);
21
+ return e;
22
+ }, F = (o) => {
23
+ const { state: e = "pending", defaultOpen: l, open: n, onOpenChange: a, className: s, children: u, slot: r, ...T } = o, m = e === "done" || e === "failed", [R, f] = d.useState(l ?? m), x = d.useRef(!1), h = d.useRef(m);
24
+ return d.useEffect(() => {
25
+ !x.current && m && !h.current && f(!0), h.current = m;
26
+ }, [m]), /* @__PURE__ */ t(C.Provider, { value: { state: e }, children: /* @__PURE__ */ t(
27
+ c.div,
28
+ {
29
+ className: i(
30
+ "group/tool w-full overflow-hidden",
31
+ "rounded-xl border bg-card shadow-xs/5",
32
+ s
33
+ ),
34
+ "data-state": e,
35
+ ...T,
36
+ "data-slot": r ?? "tool",
37
+ children: /* @__PURE__ */ t(
38
+ k,
39
+ {
40
+ onOpenChange: (g) => {
41
+ x.current = !0, f(g.open), a == null || a(g);
42
+ },
43
+ open: n ?? R,
44
+ slot: "tool-collapsible",
45
+ children: u
46
+ }
47
+ )
48
+ }
49
+ ) });
50
+ }, J = (o) => {
51
+ const { className: e, children: l, slot: n, ...a } = o, { state: s } = B("ToolHeader");
52
+ return /* @__PURE__ */ p(
53
+ L,
54
+ {
55
+ className: i(
56
+ "flex min-h-[24px] w-full items-center gap-2.5 px-3 py-2.5 text-sm",
57
+ "transition-colors hover:bg-accent motion-reduce:transition-none!",
58
+ "[&_svg:not([class*='size-'])]:size-4",
59
+ e
60
+ ),
61
+ ...a,
62
+ slot: n ?? "tool-header",
63
+ children: [
64
+ /* @__PURE__ */ t(E, { className: "shrink-0 text-muted-foreground" }),
65
+ /* @__PURE__ */ t("span", { className: "min-w-0 flex-1 truncate text-start font-medium font-mono", children: l }),
66
+ /* @__PURE__ */ p(O, { "data-state": s, pill: !0, size: "sm", slot: "tool-badge", variant: z[s], children: [
67
+ _[s],
68
+ j[s]
69
+ ] }),
70
+ /* @__PURE__ */ t(A, { className: "shrink-0 text-muted-foreground" })
71
+ ]
72
+ }
73
+ );
74
+ }, K = (o) => {
75
+ const { className: e, slot: l, ...n } = o;
76
+ return /* @__PURE__ */ t(
77
+ y,
78
+ {
79
+ className: i("flex min-w-0 flex-col gap-4 border-t px-3 py-3", e),
80
+ ...n,
81
+ slot: l ?? "tool-content"
82
+ }
83
+ );
84
+ }, M = (o) => {
85
+ const { data: e, label: l = "Parameters", className: n, children: a, slot: s, ...u } = o, r = a ?? (e === void 0 ? null : /* @__PURE__ */ t(b, { data: e }));
86
+ return /* @__PURE__ */ t(c.div, { className: i("min-w-0", n), ...u, "data-slot": s ?? "tool-input", children: r === null ? null : /* @__PURE__ */ p(N, { children: [
87
+ l === null ? null : /* @__PURE__ */ t(c.span, { className: v, "data-slot": "tool-label", children: l }),
88
+ /* @__PURE__ */ t(c.div, { className: w, "data-slot": "tool-panel", children: r })
89
+ ] }) });
90
+ }, V = (o) => {
91
+ const { data: e, label: l = "Result", className: n, children: a, slot: s, ...u } = o, r = a ?? (e === void 0 ? null : /* @__PURE__ */ t(b, { data: e }));
92
+ return /* @__PURE__ */ t(
93
+ c.div,
94
+ {
95
+ className: i("min-w-0", "group-data-[state=failed]/tool:text-destructive-foreground", n),
96
+ ...u,
97
+ "data-slot": s ?? "tool-output",
98
+ children: r === null ? null : /* @__PURE__ */ p(N, { children: [
99
+ l === null ? null : /* @__PURE__ */ t(c.span, { className: v, "data-slot": "tool-label", children: l }),
100
+ /* @__PURE__ */ t(
101
+ c.div,
102
+ {
103
+ className: i(
104
+ w,
105
+ "group-data-[state=failed]/tool:bg-destructive/7"
106
+ ),
107
+ "data-slot": "tool-panel",
108
+ children: r
109
+ }
110
+ )
111
+ ] })
112
+ }
113
+ );
114
+ };
115
+ export {
116
+ F as Tool,
117
+ K as ToolContent,
118
+ J as ToolHeader,
119
+ M as ToolInput,
120
+ V as ToolOutput
121
+ };
122
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.js","sources":["../src/tool.tsx"],"sourcesContent":["\"use client\";\n\nimport { ark } from \"@ark-ui/react/factory\";\nimport { WrenchIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n Badge,\n type BadgeVariant,\n cn,\n Collapsible,\n CollapsibleContent,\n CollapsibleIndicator,\n CollapsibleTrigger,\n JsonTreeView,\n} from \"@kanzo-tech/ui\";\nimport { RUN_LABEL, RUN_MARK, type RunState } from \"./task.js\";\n\n/** Which soft badge holds the mark. The mark itself is `Task`'s — one drawing of four states. */\nconst TONE: Record<RunState, BadgeVariant> = {\n pending: \"outline\",\n running: \"info\",\n done: \"success\",\n failed: \"destructive\",\n};\n\nconst LABEL = \"mb-1.5 block font-medium text-muted-foreground text-xs uppercase tracking-wide\";\n\n// A recess inside the card, and NOT a second frame: the card is already outlined, so a bordered\n// panel inside it draws a box in a box on every call in a transcript. The fill alone is the\n// separation — which is also what the source does (`bg-muted/50`, no border).\nconst PANEL = [\n \"min-w-0 overflow-x-auto\",\n \"rounded-lg bg-muted\",\n \"px-3 py-2 text-xs\",\n \"[&_code]:font-mono\",\n].join(\" \");\n\nconst Ctx = React.createContext<{ state: RunState } | null>(null);\nconst useCtx = (part: string) => {\n const c = React.useContext(Ctx);\n if (!c) throw new Error(`${part} must render inside <Tool>`);\n return c;\n};\n\nexport interface ToolProps extends React.ComponentProps<typeof ark.div> {\n state?: RunState;\n defaultOpen?: boolean;\n open?: boolean;\n onOpenChange?: React.ComponentProps<typeof Collapsible>[\"onOpenChange\"];\n}\n\n/**\n * One call the model made, folded away until it has an answer — reaching `done` or `failed` opens\n * it, and the reader's own toggle ends that for good.\n *\n * **It opens on the transition, not on the initial value, and that distinction is the whole of it.**\n * This was `defaultOpen={defaultOpen ?? state === \"done\"}`, which Ark's Collapsible reads once at\n * mount: a tool mounted the moment the model plans it — state `pending` — never opened again, not\n * when it finished and not when it failed. Failing silently closed is the worse half: the reason\n * the call failed sat behind a click nobody knew to make. The docblock claimed the opposite, and a\n * showcase had to work around it before anyone noticed.\n *\n * A caller passing `open` still wins outright; `defaultOpen` still decides the first paint.\n *\n * The state sits on a wrapper rather than on the collapsible, and that is not tidiness: Ark writes\n * its own `data-state` (`open` / `closed`) on the machine's root, so a tool state written there\n * replaces it — the parts keep working, and every `[data-state=open]` a consumer wrote stops\n * matching, with nothing to see in a diff.\n */\nexport const Tool = (props: ToolProps) => {\n const { state = \"pending\", defaultOpen, open, onOpenChange, className, children, slot, ...rest } =\n props;\n\n const settled = state === \"done\" || state === \"failed\";\n const [selfOpen, setSelfOpen] = React.useState(defaultOpen ?? settled);\n const touched = React.useRef(false);\n const wasSettled = React.useRef(settled);\n\n React.useEffect(() => {\n if (!touched.current && settled && !wasSettled.current) setSelfOpen(true);\n wasSettled.current = settled;\n }, [settled]);\n\n return (\n <Ctx.Provider value={{ state }}>\n <ark.div\n className={cn(\n \"group/tool w-full overflow-hidden\",\n \"rounded-xl border bg-card shadow-xs/5\",\n className\n )}\n data-state={state}\n {...rest}\n data-slot={slot ?? \"tool\"}\n >\n <Collapsible\n onOpenChange={(details) => {\n touched.current = true;\n setSelfOpen(details.open);\n onOpenChange?.(details);\n }}\n open={open ?? selfOpen}\n slot=\"tool-collapsible\"\n >\n {children}\n </Collapsible>\n </ark.div>\n </Ctx.Provider>\n );\n};\n\n/**\n * The row that says a call happened: the wrench, the tool's name in the face its arguments are\n * written in, the state as a pill, and the chevron the whole row toggles.\n */\nexport const ToolHeader = (props: React.ComponentProps<typeof CollapsibleTrigger>) => {\n const { className, children, slot, ...rest } = props;\n const { state } = useCtx(\"ToolHeader\");\n\n return (\n <CollapsibleTrigger\n className={cn(\n \"flex min-h-[24px] w-full items-center gap-2.5 px-3 py-2.5 text-sm\",\n \"transition-colors hover:bg-accent motion-reduce:transition-none!\",\n \"[&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...rest}\n slot={slot ?? \"tool-header\"}\n >\n <WrenchIcon className=\"shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate text-start font-medium font-mono\">{children}</span>\n <Badge data-state={state} pill size=\"sm\" slot=\"tool-badge\" variant={TONE[state]}>\n {RUN_MARK[state]}\n {RUN_LABEL[state]}\n </Badge>\n <CollapsibleIndicator className=\"shrink-0 text-muted-foreground\" />\n </CollapsibleTrigger>\n );\n};\n\nexport const ToolContent = (props: React.ComponentProps<typeof CollapsibleContent>) => {\n const { className, slot, ...rest } = props;\n\n return (\n <CollapsibleContent\n // `min-w-0`, and it is the whole reason a wide result was being sliced instead of scrolling: a\n // flex child's default `min-width: auto` refuses to shrink below its content, so the panel\n // below could carry `overflow-x-auto` all it liked and never get the chance to use it. The\n // card is `overflow-hidden`, so what the reader saw was a table with its last column cut off\n // and no scrollbar to recover it.\n className={cn(\"flex min-w-0 flex-col gap-4 border-t px-3 py-3\", className)}\n {...rest}\n slot={slot ?? \"tool-content\"}\n />\n );\n};\n\nexport interface ToolIoProps extends React.ComponentProps<typeof ark.div> {\n /** Drawn as a JSON tree when there are no children. */\n data?: unknown;\n /** The small heading over the panel. `null` drops it. */\n label?: string | null;\n}\n\n/**\n * What was passed to the call — **children, not a formatted-JSON prop.**\n *\n * The reference takes `input` and renders it as JSON because a general chatbot cannot know what\n * the tool was. We always know: our input is a SQL statement and our output is a result table, and\n * both have a component in this house already. So the payload is composition, and JSON is only the\n * default for when the caller has nothing better — never the one thing on offer.\n *\n * Which is also why the panel does not force a monospace face on everything inside it: a result\n * table set in mono is the cost of a default that assumed JSON. Code sets itself.\n */\nexport const ToolInput = (props: ToolIoProps) => {\n const { data, label = \"Parameters\", className, children, slot, ...rest } = props;\n const body = children ?? (data === undefined ? null : <JsonTreeView data={data} />);\n\n return (\n <ark.div className={cn(\"min-w-0\", className)} {...rest} data-slot={slot ?? \"tool-input\"}>\n {body === null ? null : (\n <>\n {label === null ? null : (\n <ark.span className={LABEL} data-slot=\"tool-label\">\n {label}\n </ark.span>\n )}\n <ark.div className={PANEL} data-slot=\"tool-panel\">\n {body}\n </ark.div>\n </>\n )}\n </ark.div>\n );\n};\n\n/** What came back. Children, for the same reason as {@link ToolInput}. */\nexport const ToolOutput = (props: ToolIoProps) => {\n const { data, label = \"Result\", className, children, slot, ...rest } = props;\n const body = children ?? (data === undefined ? null : <JsonTreeView data={data} />);\n\n return (\n <ark.div\n className={cn(\"min-w-0\", \"group-data-[state=failed]/tool:text-destructive-foreground\", className)}\n {...rest}\n data-slot={slot ?? \"tool-output\"}\n >\n {body === null ? null : (\n <>\n {label === null ? null : (\n <ark.span className={LABEL} data-slot=\"tool-label\">\n {label}\n </ark.span>\n )}\n <ark.div\n className={cn(\n PANEL,\n \"group-data-[state=failed]/tool:bg-destructive/7\"\n )}\n data-slot=\"tool-panel\"\n >\n {body}\n </ark.div>\n </>\n )}\n </ark.div>\n );\n};\n"],"names":[],"mappings":";;;;;;;AAkBA;AAA6C;AAClC;AACA;AACH;AAER;AAOc;AACZ;AACA;AACA;AAEF;AAIE;AACA;AACA;AACF;AA4BE;AAQA;AACE;AACqB;AAKnB;AAAK;AAAJ;AACY;AACT;AACA;AACA;AAAA;AAEU;AACR;AACe;AAEnB;AAAC;AAAA;AAEG;AAEe;AACjB;AACc;AACT;AAEJ;AAAA;AAAA;AACH;AAIR;AAOE;AAGA;AACE;AAAC;AAAA;AACY;AACT;AACA;AACA;AACA;AAAA;AAEE;AACU;AAEd;AAAuD;AAC8B;AAElF;AAAc;AACC;AAClB;AACiE;AAAA;AAAA;AAGvE;AAGE;AAEA;AACE;AAAC;AAAA;AAM0E;AACrE;AACU;AAAA;AAGpB;AAqBE;AAGA;AAIS;AAGC;AAIF;AAKV;AAIE;AAGA;AACE;AAAK;AAAJ;AACiG;AAC5F;AACe;AAId;AAGC;AAEF;AAAK;AAAJ;AACY;AACT;AACA;AAAA;AAEQ;AAET;AAAA;AAAA;AAEL;AAAA;AAIR;;;;;;;;"}
@@ -0,0 +1,10 @@
1
+ /** A streamed value candidate produced by `useSuggestions`. */
2
+ export interface Candidate {
3
+ /** Primitive value committed when the row is picked. */
4
+ value: string;
5
+ /** Human label; defaults to `value`. */
6
+ label?: string;
7
+ /** Optional rationale shown under the label. */
8
+ rationale?: string;
9
+ }
10
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,WAAW,SAAS;IACxB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}