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.
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/dist/react/AgentProvider.d.ts +15 -0
- package/dist/react/AgentProvider.js +30 -0
- package/dist/react/ApprovalButtons.d.ts +5 -0
- package/dist/react/ApprovalButtons.js +30 -0
- package/dist/react/ChatInput.d.ts +6 -0
- package/dist/react/ChatInput.js +41 -0
- package/dist/react/CollapsibleCard.d.ts +9 -0
- package/dist/react/CollapsibleCard.js +9 -0
- package/dist/react/MessageList.d.ts +3 -0
- package/dist/react/MessageList.js +32 -0
- package/dist/react/PendingPermissions.d.ts +3 -0
- package/dist/react/PendingPermissions.js +35 -0
- package/dist/react/StatusDot.d.ts +8 -0
- package/dist/react/StatusDot.js +15 -0
- package/dist/react/TextMessage.d.ts +5 -0
- package/dist/react/TextMessage.js +8 -0
- package/dist/react/ThinkingBlock.d.ts +5 -0
- package/dist/react/ThinkingBlock.js +38 -0
- package/dist/react/ThinkingIndicator.d.ts +3 -0
- package/dist/react/ThinkingIndicator.js +5 -0
- package/dist/react/ToolApprovalCard.d.ts +7 -0
- package/dist/react/ToolApprovalCard.js +11 -0
- package/dist/react/ToolCallCard.d.ts +5 -0
- package/dist/react/ToolCallCard.js +59 -0
- package/dist/react/UserQuestionCard.d.ts +6 -0
- package/dist/react/UserQuestionCard.js +120 -0
- package/dist/react/approval-matching.d.ts +13 -0
- package/dist/react/approval-matching.js +30 -0
- package/dist/react/cn.d.ts +2 -0
- package/dist/react/cn.js +5 -0
- package/dist/react/icons.d.ts +7 -0
- package/dist/react/icons.js +8 -0
- package/dist/react/index.d.ts +28 -0
- package/dist/react/index.js +28 -0
- package/dist/react/markdown-overrides.d.ts +2 -0
- package/dist/react/markdown-overrides.js +8 -0
- package/dist/react/registry.d.ts +4 -0
- package/dist/react/registry.js +10 -0
- package/dist/react/store.d.ts +34 -0
- package/dist/react/store.js +141 -0
- package/dist/react/use-agent.d.ts +12 -0
- package/dist/react/use-agent.js +119 -0
- package/dist/react/widgets/AskUserQuestionWidget.d.ts +1 -0
- package/dist/react/widgets/AskUserQuestionWidget.js +42 -0
- package/dist/react/widgets/BashWidget.d.ts +1 -0
- package/dist/react/widgets/BashWidget.js +33 -0
- package/dist/react/widgets/EditWidget.d.ts +1 -0
- package/dist/react/widgets/EditWidget.js +36 -0
- package/dist/react/widgets/GlobWidget.d.ts +1 -0
- package/dist/react/widgets/GlobWidget.js +31 -0
- package/dist/react/widgets/GrepWidget.d.ts +1 -0
- package/dist/react/widgets/GrepWidget.js +36 -0
- package/dist/react/widgets/NotebookEditWidget.d.ts +1 -0
- package/dist/react/widgets/NotebookEditWidget.js +47 -0
- package/dist/react/widgets/ReadWidget.d.ts +1 -0
- package/dist/react/widgets/ReadWidget.js +46 -0
- package/dist/react/widgets/TodoWriteWidget.d.ts +1 -0
- package/dist/react/widgets/TodoWriteWidget.js +40 -0
- package/dist/react/widgets/WebFetchWidget.d.ts +1 -0
- package/dist/react/widgets/WebFetchWidget.js +48 -0
- package/dist/react/widgets/WebSearchWidget.d.ts +1 -0
- package/dist/react/widgets/WebSearchWidget.js +85 -0
- package/dist/react/widgets/WriteWidget.d.ts +1 -0
- package/dist/react/widgets/WriteWidget.js +30 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.js +5 -0
- package/dist/server/permission-gate.d.ts +12 -0
- package/dist/server/permission-gate.js +41 -0
- package/dist/server/push-channel.d.ts +8 -0
- package/dist/server/push-channel.js +40 -0
- package/dist/server/router.d.ts +8 -0
- package/dist/server/router.js +67 -0
- package/dist/server/session.d.ts +40 -0
- package/dist/server/session.js +117 -0
- package/dist/server/translator.d.ts +15 -0
- package/dist/server/translator.js +236 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +1 -0
- package/package.json +72 -0
- package/src/theme.css +170 -0
|
@@ -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,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,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
|
+
}
|
package/dist/react/cn.js
ADDED
|
@@ -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,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;
|