neeter 0.6.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/dist/react/AgentProvider.d.ts +15 -0
  4. package/dist/react/AgentProvider.js +30 -0
  5. package/dist/react/ApprovalButtons.d.ts +5 -0
  6. package/dist/react/ApprovalButtons.js +30 -0
  7. package/dist/react/ChatInput.d.ts +6 -0
  8. package/dist/react/ChatInput.js +41 -0
  9. package/dist/react/CollapsibleCard.d.ts +9 -0
  10. package/dist/react/CollapsibleCard.js +9 -0
  11. package/dist/react/MessageList.d.ts +3 -0
  12. package/dist/react/MessageList.js +32 -0
  13. package/dist/react/PendingPermissions.d.ts +3 -0
  14. package/dist/react/PendingPermissions.js +35 -0
  15. package/dist/react/StatusDot.d.ts +8 -0
  16. package/dist/react/StatusDot.js +15 -0
  17. package/dist/react/TextMessage.d.ts +5 -0
  18. package/dist/react/TextMessage.js +8 -0
  19. package/dist/react/ThinkingBlock.d.ts +5 -0
  20. package/dist/react/ThinkingBlock.js +38 -0
  21. package/dist/react/ThinkingIndicator.d.ts +3 -0
  22. package/dist/react/ThinkingIndicator.js +5 -0
  23. package/dist/react/ToolApprovalCard.d.ts +7 -0
  24. package/dist/react/ToolApprovalCard.js +11 -0
  25. package/dist/react/ToolCallCard.d.ts +5 -0
  26. package/dist/react/ToolCallCard.js +59 -0
  27. package/dist/react/UserQuestionCard.d.ts +6 -0
  28. package/dist/react/UserQuestionCard.js +120 -0
  29. package/dist/react/approval-matching.d.ts +13 -0
  30. package/dist/react/approval-matching.js +30 -0
  31. package/dist/react/cn.d.ts +2 -0
  32. package/dist/react/cn.js +5 -0
  33. package/dist/react/icons.d.ts +7 -0
  34. package/dist/react/icons.js +8 -0
  35. package/dist/react/index.d.ts +28 -0
  36. package/dist/react/index.js +28 -0
  37. package/dist/react/markdown-overrides.d.ts +2 -0
  38. package/dist/react/markdown-overrides.js +8 -0
  39. package/dist/react/registry.d.ts +4 -0
  40. package/dist/react/registry.js +10 -0
  41. package/dist/react/store.d.ts +34 -0
  42. package/dist/react/store.js +141 -0
  43. package/dist/react/use-agent.d.ts +12 -0
  44. package/dist/react/use-agent.js +119 -0
  45. package/dist/react/widgets/AskUserQuestionWidget.d.ts +1 -0
  46. package/dist/react/widgets/AskUserQuestionWidget.js +42 -0
  47. package/dist/react/widgets/BashWidget.d.ts +1 -0
  48. package/dist/react/widgets/BashWidget.js +33 -0
  49. package/dist/react/widgets/EditWidget.d.ts +1 -0
  50. package/dist/react/widgets/EditWidget.js +36 -0
  51. package/dist/react/widgets/GlobWidget.d.ts +1 -0
  52. package/dist/react/widgets/GlobWidget.js +31 -0
  53. package/dist/react/widgets/GrepWidget.d.ts +1 -0
  54. package/dist/react/widgets/GrepWidget.js +36 -0
  55. package/dist/react/widgets/NotebookEditWidget.d.ts +1 -0
  56. package/dist/react/widgets/NotebookEditWidget.js +47 -0
  57. package/dist/react/widgets/ReadWidget.d.ts +1 -0
  58. package/dist/react/widgets/ReadWidget.js +46 -0
  59. package/dist/react/widgets/TodoWriteWidget.d.ts +1 -0
  60. package/dist/react/widgets/TodoWriteWidget.js +40 -0
  61. package/dist/react/widgets/WebFetchWidget.d.ts +1 -0
  62. package/dist/react/widgets/WebFetchWidget.js +48 -0
  63. package/dist/react/widgets/WebSearchWidget.d.ts +1 -0
  64. package/dist/react/widgets/WebSearchWidget.js +85 -0
  65. package/dist/react/widgets/WriteWidget.d.ts +1 -0
  66. package/dist/react/widgets/WriteWidget.js +30 -0
  67. package/dist/server/index.d.ts +6 -0
  68. package/dist/server/index.js +5 -0
  69. package/dist/server/permission-gate.d.ts +12 -0
  70. package/dist/server/permission-gate.js +41 -0
  71. package/dist/server/push-channel.d.ts +8 -0
  72. package/dist/server/push-channel.js +40 -0
  73. package/dist/server/router.d.ts +8 -0
  74. package/dist/server/router.js +67 -0
  75. package/dist/server/session.d.ts +40 -0
  76. package/dist/server/session.js +117 -0
  77. package/dist/server/translator.d.ts +15 -0
  78. package/dist/server/translator.js +236 -0
  79. package/dist/types.d.ts +79 -0
  80. package/dist/types.js +1 -0
  81. package/package.json +72 -0
  82. package/src/theme.css +170 -0
@@ -0,0 +1,5 @@
1
+ export declare function ThinkingBlock({ thinking, streaming, className, }: {
2
+ thinking: string;
3
+ streaming?: boolean;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useState } from "react";
3
+ import Markdown from "react-markdown";
4
+ import { cn } from "./cn.js";
5
+ import { ChevronIcon } from "./icons.js";
6
+ import { markdownComponents } from "./markdown-overrides.js";
7
+ import { PulsingDot } from "./StatusDot.js";
8
+ const STORAGE_KEY = "neeter-thinking-open";
9
+ function readPref() {
10
+ try {
11
+ return localStorage.getItem(STORAGE_KEY) === "true";
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ function writePref(open) {
18
+ try {
19
+ localStorage.setItem(STORAGE_KEY, String(open));
20
+ }
21
+ catch {
22
+ /* SSR / restricted env */
23
+ }
24
+ }
25
+ export function ThinkingBlock({ thinking, streaming = false, className, }) {
26
+ const [open, setOpen] = useState(readPref);
27
+ const toggle = useCallback(() => {
28
+ setOpen((prev) => {
29
+ const next = !prev;
30
+ writePref(next);
31
+ return next;
32
+ });
33
+ }, []);
34
+ return (_jsx("div", { className: cn("flex justify-start", className), children: _jsxs("div", { className: "rounded-md border border-border/40 bg-accent/20 overflow-hidden", children: [_jsxs("button", { type: "button", onClick: toggle, className: "flex w-full items-center gap-2 px-2.5 py-1.5 text-xs text-muted-foreground/60 hover:bg-accent/30 transition-colors", children: [_jsx(ChevronIcon, { open: open }), streaming && !open && _jsx(PulsingDot, {}), _jsx("span", { className: "truncate min-w-0", children: "Thinking" })] }), open && (_jsx("div", { className: "px-2.5 pt-1 pb-2", children: _jsx(ThinkingContent, { children: thinking }) }))] }) }));
35
+ }
36
+ function ThinkingContent({ children }) {
37
+ return (_jsx("div", { className: "text-xs text-muted-foreground/70 italic [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", children: _jsx(Markdown, { components: markdownComponents, children: children }) }));
38
+ }
@@ -0,0 +1,3 @@
1
+ export declare function ThinkingIndicator({ className }: {
2
+ className?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "./cn.js";
3
+ export function ThinkingIndicator({ className }) {
4
+ return (_jsx("div", { className: cn("flex justify-start", className), children: _jsxs("div", { className: "flex items-center gap-1 rounded-lg bg-muted px-3 py-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:0ms]" }), _jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:150ms]" }), _jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:300ms]" })] }) }));
5
+ }
@@ -0,0 +1,7 @@
1
+ import type { ToolApprovalRequest } from "../types.js";
2
+ export declare function ToolApprovalCard({ request, onApprove, onDeny, className, }: {
3
+ request: ToolApprovalRequest;
4
+ onApprove: () => void;
5
+ onDeny: (message?: string) => void;
6
+ className?: string;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ApprovalButtons } from "./ApprovalButtons.js";
3
+ import { cn } from "./cn.js";
4
+ import { getWidget, stripMcpPrefix } from "./registry.js";
5
+ import { PulsingDot } from "./StatusDot.js";
6
+ export function ToolApprovalCard({ request, onApprove, onDeny, className, }) {
7
+ const reg = getWidget(stripMcpPrefix(request.toolName));
8
+ const label = reg?.label ?? request.toolName;
9
+ const InputRenderer = reg?.inputRenderer;
10
+ return (_jsxs("div", { className: cn("rounded-md border border-amber-500/40 bg-amber-500/5 px-3 py-2.5 text-xs", className), children: [_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [_jsx(PulsingDot, {}), _jsx("span", { className: "font-medium", children: label }), request.description && (_jsxs("span", { className: "ml-1 opacity-70", children: ["\u2014 ", request.description] }))] }), InputRenderer ? (_jsx(InputRenderer, { input: request.input })) : (Object.keys(request.input).length > 0 && (_jsx("pre", { className: "mt-1.5 max-h-32 overflow-auto rounded bg-background/60 px-2 py-1 font-mono text-[10px] text-muted-foreground", children: JSON.stringify(request.input, null, 2) }))), _jsx(ApprovalButtons, { onApprove: onApprove, onDeny: () => onDeny() })] }));
11
+ }
@@ -0,0 +1,5 @@
1
+ import type { ToolCallInfo } from "../types.js";
2
+ export declare function ToolCallCard({ toolCall, className, }: {
3
+ toolCall: ToolCallInfo;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useAgentContext, useChatStore } from "./AgentProvider.js";
3
+ import { ApprovalButtons } from "./ApprovalButtons.js";
4
+ import { findMatchingApproval } from "./approval-matching.js";
5
+ import { CollapsibleCard } from "./CollapsibleCard.js";
6
+ import { cn } from "./cn.js";
7
+ import { getWidget, stripMcpPrefix } from "./registry.js";
8
+ import { PulsingDot, StatusDot } from "./StatusDot.js";
9
+ export function ToolCallCard({ toolCall, className, }) {
10
+ const short = stripMcpPrefix(toolCall.name);
11
+ const reg = getWidget(short);
12
+ const label = reg?.label ?? short;
13
+ const pendingPermissions = useChatStore((s) => s.pendingPermissions);
14
+ const { respondToPermission, store } = useAgentContext();
15
+ const matchingApproval = findMatchingApproval(pendingPermissions, toolCall);
16
+ if (toolCall.status === "complete" && toolCall.result && reg) {
17
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(toolCall.result);
20
+ }
21
+ catch {
22
+ parsed = toolCall.result;
23
+ }
24
+ const displayLabel = reg.richLabel
25
+ ? (reg.richLabel(parsed, toolCall.input) ?? label)
26
+ : label;
27
+ const widgetProps = {
28
+ phase: toolCall.status,
29
+ toolUseId: toolCall.id,
30
+ input: toolCall.input,
31
+ partialInput: toolCall.partialInput,
32
+ result: parsed,
33
+ error: toolCall.error,
34
+ };
35
+ return (_jsx(CollapsibleCard, { label: displayLabel, status: toolCall.status, className: className, children: _jsx(reg.component, { ...widgetProps }) }));
36
+ }
37
+ if (toolCall.status === "error") {
38
+ return (_jsxs("div", { className: cn("flex items-center gap-2 rounded-md border border-destructive/50 bg-destructive/10 px-2.5 py-1.5 text-xs text-muted-foreground", className), children: [_jsx(StatusDot, { status: toolCall.status }), _jsx("span", { children: label }), toolCall.error && _jsx("span", { className: "ml-auto text-destructive", children: toolCall.error })] }));
39
+ }
40
+ if (matchingApproval) {
41
+ const InputRenderer = reg?.inputRenderer;
42
+ return (_jsxs("div", { className: cn("rounded-md border border-amber-500/40 bg-amber-500/5 px-3 py-2.5 text-xs", className), children: [_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [_jsx(PulsingDot, {}), _jsx("span", { className: "font-medium", children: label }), matchingApproval.description && (_jsxs("span", { className: "ml-1 opacity-70", children: ["\u2014 ", matchingApproval.description] }))] }), InputRenderer ? (_jsx(InputRenderer, { input: matchingApproval.input })) : (Object.keys(matchingApproval.input).length > 0 && (_jsx("pre", { className: "mt-1.5 max-h-32 overflow-auto rounded bg-background/60 px-2 py-1 font-mono text-[10px] text-muted-foreground", children: JSON.stringify(matchingApproval.input, null, 2) }))), _jsx(ApprovalButtons, { onApprove: () => respondToPermission({
43
+ kind: "tool_approval",
44
+ requestId: matchingApproval.requestId,
45
+ behavior: "allow",
46
+ }), onDeny: () => {
47
+ store.getState().errorToolCall(toolCall.id, "Not approved");
48
+ respondToPermission({
49
+ kind: "tool_approval",
50
+ requestId: matchingApproval.requestId,
51
+ behavior: "deny",
52
+ message: "Denied by user",
53
+ });
54
+ } })] }));
55
+ }
56
+ const FallbackInputRenderer = reg?.inputRenderer;
57
+ const hasInput = Object.keys(toolCall.input).length > 0;
58
+ return (_jsxs("div", { className: cn("rounded-md border border-border bg-accent/50 px-2.5 py-1.5 text-xs text-muted-foreground", className), children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(StatusDot, { status: toolCall.status }), _jsx("span", { children: label }), toolCall.status === "streaming_input" && toolCall.partialInput && (_jsx("span", { className: "ml-auto truncate max-w-[200px] opacity-50 font-mono text-[10px]", children: toolCall.partialInput.slice(0, 80) }))] }), hasInput && FallbackInputRenderer && _jsx(FallbackInputRenderer, { input: toolCall.input })] }));
59
+ }
@@ -0,0 +1,6 @@
1
+ import type { UserQuestionRequest } from "../types.js";
2
+ export declare function UserQuestionCard({ request, onSubmit, className, }: {
3
+ request: UserQuestionRequest;
4
+ onSubmit: (answers: Record<string, string>) => void;
5
+ className?: string;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { cn } from "./cn.js";
4
+ const OTHER_LABEL = "__other__";
5
+ export function UserQuestionCard({ request, onSubmit, className, }) {
6
+ const wrapperRef = useRef(null);
7
+ const [answers, setAnswers] = useState({});
8
+ const [otherText, setOtherText] = useState({});
9
+ const otherInputRefs = useRef({});
10
+ // Auto-focus wrapper so number-key shortcuts work immediately
11
+ useEffect(() => {
12
+ wrapperRef.current?.focus();
13
+ }, []);
14
+ const allAnswered = request.questions.every((q) => {
15
+ const vals = answers[q.question];
16
+ if (!vals || vals.length === 0)
17
+ return false;
18
+ if (vals.includes(OTHER_LABEL))
19
+ return !!otherText[q.question]?.trim();
20
+ return true;
21
+ });
22
+ function resolvedAnswers() {
23
+ const resolved = {};
24
+ for (const q of request.questions) {
25
+ const vals = answers[q.question] ?? [];
26
+ const mapped = vals.map((v) => (v === OTHER_LABEL ? (otherText[q.question] ?? "") : v));
27
+ resolved[q.question] = mapped.join(", ");
28
+ }
29
+ return resolved;
30
+ }
31
+ function selectOption(question, label, multiSelect) {
32
+ setAnswers((prev) => {
33
+ if (!multiSelect)
34
+ return { ...prev, [question]: [label] };
35
+ const current = prev[question] ?? [];
36
+ return {
37
+ ...prev,
38
+ [question]: current.includes(label)
39
+ ? current.filter((l) => l !== label)
40
+ : [...current, label],
41
+ };
42
+ });
43
+ if (label === OTHER_LABEL) {
44
+ setTimeout(() => otherInputRefs.current[question]?.focus(), 0);
45
+ }
46
+ }
47
+ // Keyboard shortcuts: number keys select options, Enter submits
48
+ useEffect(() => {
49
+ function handleKeyDown(e) {
50
+ const tag = e.target?.tagName;
51
+ if (tag === "INPUT" || tag === "TEXTAREA" || e.target?.isContentEditable) {
52
+ return;
53
+ }
54
+ if (request.questions.length !== 1)
55
+ return;
56
+ const q = request.questions[0];
57
+ if (!q.options?.length)
58
+ return;
59
+ if (e.key === "Enter" && allAnswered) {
60
+ e.preventDefault();
61
+ onSubmit(resolvedAnswers());
62
+ return;
63
+ }
64
+ const totalOptions = q.options.length + 1; // +1 for "Other"
65
+ const num = parseInt(e.key, 10);
66
+ if (num >= 1 && num <= totalOptions) {
67
+ e.preventDefault();
68
+ if (num <= q.options.length) {
69
+ if (!q.multiSelect) {
70
+ // Single-select: submit immediately (matches ApprovalButtons)
71
+ onSubmit({ [q.question]: q.options[num - 1].label });
72
+ }
73
+ else {
74
+ selectOption(q.question, q.options[num - 1].label, true);
75
+ }
76
+ }
77
+ else {
78
+ selectOption(q.question, OTHER_LABEL, q.multiSelect);
79
+ }
80
+ }
81
+ }
82
+ document.addEventListener("keydown", handleKeyDown);
83
+ return () => document.removeEventListener("keydown", handleKeyDown);
84
+ });
85
+ return (_jsxs("div", { ref: wrapperRef, tabIndex: -1, className: cn("rounded-md border border-blue-500/40 bg-blue-500/5 px-3 py-2.5 text-xs focus:outline-none", className), children: [request.questions.map((q) => {
86
+ const hasOptions = q.options && q.options.length > 0;
87
+ const isSingleQuestion = request.questions.length === 1;
88
+ return (_jsxs("div", { className: "mb-3 last:mb-0", children: [q.header && (_jsx("span", { className: "text-[10px] uppercase tracking-wider text-muted-foreground/70", children: q.header })), _jsx("p", { className: "mt-0.5 text-foreground", children: q.question }), hasOptions && (_jsxs("div", { className: "mt-1.5 flex flex-col gap-1", children: [q.options?.map((opt, idx) => {
89
+ const selected = (answers[q.question] ?? []).includes(opt.label);
90
+ return (_jsxs("button", { type: "button", onClick: () => {
91
+ if (!q.multiSelect && isSingleQuestion) {
92
+ onSubmit({ [q.question]: opt.label });
93
+ }
94
+ else {
95
+ selectOption(q.question, opt.label, q.multiSelect);
96
+ }
97
+ }, className: cn("flex items-start gap-2 rounded border px-2 py-1.5 text-left transition-colors", selected
98
+ ? "border-blue-500 bg-blue-500/15 text-foreground ring-1 ring-blue-500/30"
99
+ : "border-border text-muted-foreground hover:bg-accent"), children: [_jsx("span", { className: "mt-px shrink-0", children: q.multiSelect ? (selected ? "☑" : "☐") : selected ? "●" : "○" }), _jsxs("span", { className: "flex-1", children: [_jsx("span", { className: "font-medium", children: opt.label }), opt.description && (_jsxs("span", { className: "ml-1 opacity-70", children: ["\u2014 ", opt.description] }))] }), isSingleQuestion && (_jsx("kbd", { className: "ml-auto shrink-0 text-[9px] opacity-40", children: idx + 1 }))] }, opt.label));
100
+ }), (() => {
101
+ const isOther = (answers[q.question] ?? []).includes(OTHER_LABEL);
102
+ const otherIdx = (q.options?.length ?? 0) + 1;
103
+ return (_jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => selectOption(q.question, OTHER_LABEL, q.multiSelect), className: cn("flex w-full items-start gap-2 rounded border px-2 py-1.5 text-left transition-colors", isOther
104
+ ? "border-blue-500 bg-blue-500/15 text-foreground ring-1 ring-blue-500/30"
105
+ : "border-border text-muted-foreground hover:bg-accent"), children: [_jsx("span", { className: "mt-px shrink-0", children: q.multiSelect ? (isOther ? "☑" : "☐") : isOther ? "●" : "○" }), _jsx("span", { className: "font-medium", children: "Other" }), isSingleQuestion && (_jsx("kbd", { className: "ml-auto shrink-0 text-[9px] opacity-40", children: otherIdx }))] }), isOther && (_jsx("input", { ref: (el) => {
106
+ otherInputRefs.current[q.question] = el;
107
+ }, type: "text", className: "mt-1 w-full rounded border border-border bg-background px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-blue-500/50", placeholder: "Type your answer\u2026", value: otherText[q.question] ?? "", onChange: (e) => setOtherText((prev) => ({ ...prev, [q.question]: e.target.value })), onKeyDown: (e) => {
108
+ if (e.key === "Enter" && otherText[q.question]?.trim()) {
109
+ e.preventDefault();
110
+ onSubmit(resolvedAnswers());
111
+ }
112
+ } }))] }));
113
+ })()] })), !hasOptions && (_jsx("input", { type: "text", className: "mt-1.5 w-full rounded border border-border bg-background px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-blue-500/50", placeholder: "Type your answer\u2026", value: (answers[q.question] ?? [])[0] ?? "", onChange: (e) => setAnswers((prev) => ({ ...prev, [q.question]: [e.target.value] })), onKeyDown: (e) => {
114
+ if (e.key === "Enter" && (answers[q.question]?.[0] ?? "").trim()) {
115
+ e.preventDefault();
116
+ onSubmit(resolvedAnswers());
117
+ }
118
+ } }))] }, q.question));
119
+ }), _jsx("button", { type: "button", disabled: !allAnswered, onClick: () => onSubmit(resolvedAnswers()), className: cn("mt-2 rounded bg-primary px-3 py-1 text-primary-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50", allAnswered ? "hover:bg-primary/90" : "opacity-50 cursor-not-allowed"), children: "Submit" })] }));
120
+ }
@@ -0,0 +1,13 @@
1
+ import type { ChatMessage, PermissionRequest, ToolApprovalRequest, ToolCallInfo } from "../types.js";
2
+ /**
3
+ * Find the pending approval that matches a specific tool call.
4
+ * Matches by toolUseId when available, falls back to toolName.
5
+ * Only considers non-terminal tool calls (not complete/error).
6
+ */
7
+ export declare function findMatchingApproval(pendingPermissions: PermissionRequest[], toolCall: ToolCallInfo): ToolApprovalRequest | undefined;
8
+ /**
9
+ * Check if an approval request is "claimed" by any non-terminal tool call
10
+ * in the message list. Claimed approvals are rendered inline by ToolCallCard
11
+ * and should be skipped by PendingPermissions to avoid duplicate UI.
12
+ */
13
+ export declare function isApprovalClaimedByToolCall(request: ToolApprovalRequest, messages: ChatMessage[]): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Find the pending approval that matches a specific tool call.
3
+ * Matches by toolUseId when available, falls back to toolName.
4
+ * Only considers non-terminal tool calls (not complete/error).
5
+ */
6
+ export function findMatchingApproval(pendingPermissions, toolCall) {
7
+ if (toolCall.status === "complete" || toolCall.status === "error")
8
+ return undefined;
9
+ return pendingPermissions.find((p) => p.kind === "tool_approval" &&
10
+ (p.toolUseId ? p.toolUseId === toolCall.id : p.toolName === toolCall.name));
11
+ }
12
+ /**
13
+ * Check if an approval request is "claimed" by any non-terminal tool call
14
+ * in the message list. Claimed approvals are rendered inline by ToolCallCard
15
+ * and should be skipped by PendingPermissions to avoid duplicate UI.
16
+ */
17
+ export function isApprovalClaimedByToolCall(request, messages) {
18
+ for (const msg of messages) {
19
+ if (msg.toolCalls) {
20
+ for (const tc of msg.toolCalls) {
21
+ if (tc.status === "complete" || tc.status === "error")
22
+ continue;
23
+ if (request.toolUseId ? tc.id === request.toolUseId : tc.name === request.toolName) {
24
+ return true;
25
+ }
26
+ }
27
+ }
28
+ }
29
+ return false;
30
+ }
@@ -0,0 +1,2 @@
1
+ import { type ClassValue } from "clsx";
2
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,5 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
@@ -0,0 +1,7 @@
1
+ export declare function ChevronIcon({ open, className }: {
2
+ open: boolean;
3
+ className?: string;
4
+ }): import("react/jsx-runtime").JSX.Element;
5
+ export declare function SendIcon({ className }: {
6
+ className?: string;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "./cn.js";
3
+ export function ChevronIcon({ open, className }) {
4
+ return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: cn("h-3 w-3 transition-transform", open && "rotate-90", className), "aria-hidden": "true", children: _jsx("path", { d: "m9 18 6-6-6-6" }) }));
5
+ }
6
+ export function SendIcon({ className }) {
7
+ return (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: cn("h-4 w-4", className), "aria-hidden": "true", children: [_jsx("path", { d: "M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" }), _jsx("path", { d: "m21.854 2.147-10.94 10.939" })] }));
8
+ }
@@ -0,0 +1,28 @@
1
+ import "./widgets/AskUserQuestionWidget.js";
2
+ import "./widgets/BashWidget.js";
3
+ import "./widgets/EditWidget.js";
4
+ import "./widgets/GlobWidget.js";
5
+ import "./widgets/GrepWidget.js";
6
+ import "./widgets/NotebookEditWidget.js";
7
+ import "./widgets/ReadWidget.js";
8
+ import "./widgets/TodoWriteWidget.js";
9
+ import "./widgets/WebFetchWidget.js";
10
+ import "./widgets/WebSearchWidget.js";
11
+ import "./widgets/WriteWidget.js";
12
+ export type { ChatMessage, CustomEvent, PermissionRequest, PermissionResponse, SSEEvent, ToolApprovalRequest, ToolApprovalResponse, ToolCallInfo, ToolCallPhase, UserQuestion, UserQuestionOption, UserQuestionRequest, UserQuestionResponse, WidgetProps, WidgetRegistration, } from "../types.js";
13
+ export { AgentProvider, useAgentContext, useChatStore } from "./AgentProvider.js";
14
+ export { ChatInput } from "./ChatInput.js";
15
+ export { CollapsibleCard } from "./CollapsibleCard.js";
16
+ export { cn } from "./cn.js";
17
+ export { MessageList } from "./MessageList.js";
18
+ export { PendingPermissions } from "./PendingPermissions.js";
19
+ export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
20
+ export { StatusDot } from "./StatusDot.js";
21
+ export { type ChatStore, type ChatStoreShape, createChatStore } from "./store.js";
22
+ export { TextMessage } from "./TextMessage.js";
23
+ export { ThinkingBlock } from "./ThinkingBlock.js";
24
+ export { ThinkingIndicator } from "./ThinkingIndicator.js";
25
+ export { ToolApprovalCard } from "./ToolApprovalCard.js";
26
+ export { ToolCallCard } from "./ToolCallCard.js";
27
+ export { UserQuestionCard } from "./UserQuestionCard.js";
28
+ export { type UseAgentConfig, type UseAgentReturn, useAgent } from "./use-agent.js";
@@ -0,0 +1,28 @@
1
+ // Built-in widgets (side-effect: auto-registers)
2
+ import "./widgets/AskUserQuestionWidget.js";
3
+ import "./widgets/BashWidget.js";
4
+ import "./widgets/EditWidget.js";
5
+ import "./widgets/GlobWidget.js";
6
+ import "./widgets/GrepWidget.js";
7
+ import "./widgets/NotebookEditWidget.js";
8
+ import "./widgets/ReadWidget.js";
9
+ import "./widgets/TodoWriteWidget.js";
10
+ import "./widgets/WebFetchWidget.js";
11
+ import "./widgets/WebSearchWidget.js";
12
+ import "./widgets/WriteWidget.js";
13
+ export { AgentProvider, useAgentContext, useChatStore } from "./AgentProvider.js";
14
+ export { ChatInput } from "./ChatInput.js";
15
+ export { CollapsibleCard } from "./CollapsibleCard.js";
16
+ export { cn } from "./cn.js";
17
+ export { MessageList } from "./MessageList.js";
18
+ export { PendingPermissions } from "./PendingPermissions.js";
19
+ export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
20
+ export { StatusDot } from "./StatusDot.js";
21
+ export { createChatStore } from "./store.js";
22
+ export { TextMessage } from "./TextMessage.js";
23
+ export { ThinkingBlock } from "./ThinkingBlock.js";
24
+ export { ThinkingIndicator } from "./ThinkingIndicator.js";
25
+ export { ToolApprovalCard } from "./ToolApprovalCard.js";
26
+ export { ToolCallCard } from "./ToolCallCard.js";
27
+ export { UserQuestionCard } from "./UserQuestionCard.js";
28
+ export { useAgent } from "./use-agent.js";
@@ -0,0 +1,2 @@
1
+ import type { Components } from "react-markdown";
2
+ export declare const markdownComponents: Partial<Components>;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export const markdownComponents = {
3
+ p: ({ children }) => _jsx("p", { className: "mb-2 last:mb-0", children: children }),
4
+ ul: ({ children }) => _jsx("ul", { className: "mb-2 ml-4 list-disc last:mb-0", children: children }),
5
+ ol: ({ children }) => _jsx("ol", { className: "mb-2 ml-4 list-decimal last:mb-0", children: children }),
6
+ li: ({ children }) => _jsx("li", { className: "mb-0.5", children: children }),
7
+ strong: ({ children }) => _jsx("strong", { className: "font-semibold", children: children }),
8
+ };
@@ -0,0 +1,4 @@
1
+ import type { WidgetRegistration } from "../types.js";
2
+ export declare function registerWidget<TResult>(reg: WidgetRegistration<TResult>): void;
3
+ export declare function getWidget(toolName: string): WidgetRegistration | undefined;
4
+ export declare function stripMcpPrefix(name: string): string;
@@ -0,0 +1,10 @@
1
+ const widgets = new Map();
2
+ export function registerWidget(reg) {
3
+ widgets.set(reg.toolName, reg);
4
+ }
5
+ export function getWidget(toolName) {
6
+ return widgets.get(toolName);
7
+ }
8
+ export function stripMcpPrefix(name) {
9
+ return name.replace(/^mcp__[^_]+__/, "");
10
+ }
@@ -0,0 +1,34 @@
1
+ import { type StoreApi } from "zustand/vanilla";
2
+ import type { ChatMessage, PermissionRequest } from "../types.js";
3
+ interface ChatStoreState {
4
+ sessionId: string | null;
5
+ messages: ChatMessage[];
6
+ isStreaming: boolean;
7
+ isThinking: boolean;
8
+ streamingText: string;
9
+ streamingThinking: string;
10
+ pendingPermissions: PermissionRequest[];
11
+ }
12
+ interface ChatStoreActions {
13
+ setSessionId: (id: string) => void;
14
+ addUserMessage: (text: string) => void;
15
+ appendStreamingText: (text: string) => void;
16
+ appendStreamingThinking: (text: string) => void;
17
+ flushStreamingText: () => void;
18
+ flushStreamingThinking: () => void;
19
+ addSystemMessage: (text: string) => void;
20
+ startToolCall: (toolUseId: string, name: string) => void;
21
+ appendToolInput: (toolUseId: string, partialJson: string) => void;
22
+ finalizeToolCall: (toolUseId: string, name: string, input: Record<string, unknown>) => void;
23
+ completeToolCall: (toolUseId: string, result: string) => void;
24
+ errorToolCall: (toolUseId: string, error: string) => void;
25
+ setStreaming: (v: boolean) => void;
26
+ setThinking: (v: boolean) => void;
27
+ addPermissionRequest: (request: PermissionRequest) => void;
28
+ removePermissionRequest: (requestId: string) => void;
29
+ reset: () => void;
30
+ }
31
+ export type ChatStoreShape = ChatStoreState & ChatStoreActions;
32
+ export type ChatStore = StoreApi<ChatStoreShape>;
33
+ export declare function createChatStore(): ChatStore;
34
+ export {};
@@ -0,0 +1,141 @@
1
+ import { immer } from "zustand/middleware/immer";
2
+ import { createStore } from "zustand/vanilla";
3
+ let nextId = 0;
4
+ function findToolCall(messages, toolUseId) {
5
+ for (let i = messages.length - 1; i >= 0; i--) {
6
+ const tc = messages[i].toolCalls;
7
+ if (tc?.length) {
8
+ const match = tc.find((t) => t.id === toolUseId);
9
+ if (match)
10
+ return match;
11
+ }
12
+ }
13
+ return undefined;
14
+ }
15
+ export function createChatStore() {
16
+ return createStore()(immer((set) => ({
17
+ sessionId: null,
18
+ messages: [],
19
+ isStreaming: false,
20
+ isThinking: false,
21
+ streamingText: "",
22
+ streamingThinking: "",
23
+ pendingPermissions: [],
24
+ setSessionId: (id) => set((s) => {
25
+ s.sessionId = id;
26
+ }),
27
+ addUserMessage: (text) => set((s) => {
28
+ s.messages.push({
29
+ id: `msg-${++nextId}`,
30
+ role: "user",
31
+ content: text,
32
+ });
33
+ }),
34
+ addSystemMessage: (text) => set((s) => {
35
+ s.messages.push({
36
+ id: `msg-${++nextId}`,
37
+ role: "system",
38
+ content: text,
39
+ });
40
+ }),
41
+ appendStreamingText: (text) => set((s) => {
42
+ s.streamingText += text;
43
+ }),
44
+ appendStreamingThinking: (text) => set((s) => {
45
+ s.streamingThinking += text;
46
+ }),
47
+ flushStreamingThinking: () => set((s) => {
48
+ if (s.streamingThinking) {
49
+ const last = s.messages[s.messages.length - 1];
50
+ if (last?.role === "assistant" && !last.toolCalls?.length) {
51
+ last.thinking = (last.thinking ?? "") + s.streamingThinking;
52
+ }
53
+ else {
54
+ s.messages.push({
55
+ id: `msg-${++nextId}`,
56
+ role: "assistant",
57
+ content: "",
58
+ thinking: s.streamingThinking,
59
+ });
60
+ }
61
+ s.streamingThinking = "";
62
+ }
63
+ }),
64
+ flushStreamingText: () => set((s) => {
65
+ if (s.streamingText) {
66
+ const last = s.messages[s.messages.length - 1];
67
+ if (last?.role === "assistant" && !last.toolCalls?.length) {
68
+ last.content += s.streamingText;
69
+ }
70
+ else {
71
+ s.messages.push({
72
+ id: `msg-${++nextId}`,
73
+ role: "assistant",
74
+ content: s.streamingText,
75
+ });
76
+ }
77
+ s.streamingText = "";
78
+ }
79
+ }),
80
+ startToolCall: (toolUseId, name) => set((s) => {
81
+ s.messages.push({
82
+ id: `msg-${++nextId}`,
83
+ role: "assistant",
84
+ content: "",
85
+ toolCalls: [{ id: toolUseId, name, input: {}, status: "pending" }],
86
+ });
87
+ }),
88
+ appendToolInput: (toolUseId, partialJson) => set((s) => {
89
+ const tc = findToolCall(s.messages, toolUseId);
90
+ if (tc) {
91
+ tc.partialInput = (tc.partialInput ?? "") + partialJson;
92
+ tc.status = "streaming_input";
93
+ }
94
+ }),
95
+ finalizeToolCall: (toolUseId, name, input) => set((s) => {
96
+ const tc = findToolCall(s.messages, toolUseId);
97
+ if (tc) {
98
+ tc.name = name;
99
+ tc.input = input;
100
+ tc.status = "running";
101
+ }
102
+ }),
103
+ completeToolCall: (toolUseId, result) => set((s) => {
104
+ const tc = findToolCall(s.messages, toolUseId);
105
+ if (tc && tc.status !== "error") {
106
+ tc.result = result;
107
+ tc.status = "complete";
108
+ }
109
+ }),
110
+ errorToolCall: (toolUseId, error) => set((s) => {
111
+ const tc = findToolCall(s.messages, toolUseId);
112
+ if (tc) {
113
+ tc.error = error;
114
+ tc.status = "error";
115
+ }
116
+ }),
117
+ setStreaming: (v) => set((s) => {
118
+ s.isStreaming = v;
119
+ }),
120
+ setThinking: (v) => set((s) => {
121
+ s.isThinking = v;
122
+ }),
123
+ addPermissionRequest: (request) => set((s) => {
124
+ if (!s.pendingPermissions.some((p) => p.requestId === request.requestId)) {
125
+ s.pendingPermissions.push(request);
126
+ }
127
+ }),
128
+ removePermissionRequest: (requestId) => set((s) => {
129
+ s.pendingPermissions = s.pendingPermissions.filter((p) => p.requestId !== requestId);
130
+ }),
131
+ reset: () => set((s) => {
132
+ s.sessionId = null;
133
+ s.messages = [];
134
+ s.isStreaming = false;
135
+ s.isThinking = false;
136
+ s.streamingText = "";
137
+ s.streamingThinking = "";
138
+ s.pendingPermissions = [];
139
+ }),
140
+ })));
141
+ }
@@ -0,0 +1,12 @@
1
+ import type { CustomEvent, PermissionResponse } from "../types.js";
2
+ import type { ChatStore } from "./store.js";
3
+ export interface UseAgentConfig {
4
+ endpoint?: string;
5
+ onCustomEvent?: (event: CustomEvent) => void;
6
+ }
7
+ export interface UseAgentReturn {
8
+ sessionId: string | null;
9
+ sendMessage: (text: string) => Promise<void>;
10
+ respondToPermission: (response: PermissionResponse) => Promise<void>;
11
+ }
12
+ export declare function useAgent(store: ChatStore, config?: UseAgentConfig): UseAgentReturn;