@neeter/react
Advanced tools
@@ -1,3 +0,5 @@ | ||
| export declare function ChatInput({ onSend, placeholder, disabled, className, }: { | ||
| export declare function ChatInput({ onSend, onStop, isStreaming, placeholder, disabled, className, }: { | ||
| onSend: (text: string) => void; | ||
| onStop?: () => void; | ||
| isStreaming?: boolean; | ||
| placeholder?: string; | ||
@@ -4,0 +6,0 @@ disabled?: boolean; |
| import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; | ||
| import { useCallback, useRef, useState } from "react"; | ||
| import { cn } from "./cn.js"; | ||
| import { SendIcon } from "./icons.js"; | ||
| export function ChatInput({ onSend, placeholder = "Type a message...", disabled, className, }) { | ||
| import { SendIcon, StopIcon } from "./icons.js"; | ||
| export function ChatInput({ onSend, onStop, isStreaming, placeholder = "Type a message...", disabled, className, }) { | ||
| const [text, setText] = useState(""); | ||
@@ -40,3 +40,3 @@ const textareaRef = useRef(null); | ||
| autoResize(); | ||
| }, onKeyDown: handleKeyDown, placeholder: placeholder, rows: 1, className: "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring max-h-40 overflow-y-hidden" }), _jsx("button", { type: "button", onClick: handleSend, disabled: !text.trim() || isDisabled, className: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none", children: _jsx(SendIcon, {}) })] })); | ||
| }, onKeyDown: handleKeyDown, placeholder: placeholder, rows: 1, className: "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring max-h-40 overflow-y-hidden" }), isStreaming && onStop ? (_jsx("button", { type: "button", onClick: onStop, className: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90", children: _jsx(StopIcon, {}) })) : (_jsx("button", { type: "button", onClick: handleSend, disabled: !text.trim() || isDisabled, className: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none", children: _jsx(SendIcon, {}) }))] })); | ||
| } |
+3
-0
@@ -5,4 +5,7 @@ export declare function ChevronIcon({ open, className }: { | ||
| }): import("react/jsx-runtime").JSX.Element; | ||
| export declare function StopIcon({ className }: { | ||
| className?: string; | ||
| }): import("react/jsx-runtime").JSX.Element; | ||
| export declare function SendIcon({ className }: { | ||
| className?: string; | ||
| }): import("react/jsx-runtime").JSX.Element; |
+3
-0
@@ -6,4 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; | ||
| } | ||
| export function StopIcon({ className }) { | ||
| return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: cn("h-4 w-4", className), "aria-hidden": "true", children: _jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "1" }) })); | ||
| } | ||
| export function SendIcon({ className }) { | ||
| 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" })] })); | ||
| } |
+4
-0
@@ -11,2 +11,4 @@ import type { ChatMessage, PermissionRequest } from "@neeter/types"; | ||
| pendingPermissions: PermissionRequest[]; | ||
| totalCost: number; | ||
| totalTurns: number; | ||
| } | ||
@@ -30,2 +32,4 @@ interface ChatStoreActions { | ||
| removePermissionRequest: (requestId: string) => void; | ||
| addCost: (cost: number, turns: number) => void; | ||
| cancelInflightToolCalls: () => void; | ||
| reset: () => void; | ||
@@ -32,0 +36,0 @@ } |
+22
-0
@@ -24,2 +24,4 @@ import { immer } from "zustand/middleware/immer"; | ||
| pendingPermissions: [], | ||
| totalCost: 0, | ||
| totalTurns: 0, | ||
| setSessionId: (id) => set((s) => { | ||
@@ -132,2 +134,20 @@ s.sessionId = id; | ||
| }), | ||
| addCost: (cost, turns) => set((s) => { | ||
| s.totalCost += cost; | ||
| s.totalTurns += turns; | ||
| }), | ||
| cancelInflightToolCalls: () => set((s) => { | ||
| for (const msg of s.messages) { | ||
| if (msg.toolCalls) { | ||
| for (const tc of msg.toolCalls) { | ||
| if (tc.status === "pending" || | ||
| tc.status === "streaming_input" || | ||
| tc.status === "running") { | ||
| tc.status = "error"; | ||
| tc.error = "Interrupted"; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }), | ||
| reset: () => set((s) => { | ||
@@ -141,4 +161,6 @@ s.sessionId = null; | ||
| s.pendingPermissions = []; | ||
| s.totalCost = 0; | ||
| s.totalTurns = 0; | ||
| }), | ||
| }))); | ||
| } |
@@ -10,4 +10,5 @@ import type { CustomEvent, PermissionResponse } from "@neeter/types"; | ||
| sendMessage: (text: string) => Promise<void>; | ||
| stopSession: () => Promise<void>; | ||
| respondToPermission: (response: PermissionResponse) => Promise<void>; | ||
| } | ||
| export declare function useAgent(store: ChatStore, config?: UseAgentConfig): UseAgentReturn; |
+40
-5
@@ -6,2 +6,3 @@ import { useCallback, useEffect, useRef, useSyncExternalStore } from "react"; | ||
| const eventSourceRef = useRef(null); | ||
| const abortedRef = useRef(false); | ||
| const sessionId = useSyncExternalStore(store.subscribe, () => store.getState().sessionId); | ||
@@ -26,2 +27,3 @@ useEffect(() => { | ||
| eventSourceRef.current = es; | ||
| abortedRef.current = false; | ||
| es.addEventListener("message_start", () => { | ||
@@ -71,7 +73,15 @@ store.getState().setThinking(true); | ||
| store.getState().setThinking(false); | ||
| const { subtype } = JSON.parse(e.data); | ||
| store.getState().addSystemMessage(`Session ended: ${subtype}`); | ||
| store.getState().setStreaming(false); | ||
| if (abortedRef.current) { | ||
| store.getState().cancelInflightToolCalls(); | ||
| store.getState().addSystemMessage("Interrupted"); | ||
| abortedRef.current = false; | ||
| } | ||
| else { | ||
| const { subtype } = JSON.parse(e.data); | ||
| store.getState().addSystemMessage(`Session ended: ${subtype}`); | ||
| } | ||
| }); | ||
| es.addEventListener("turn_complete", () => { | ||
| es.addEventListener("turn_complete", (e) => { | ||
| const { cost, numTurns } = JSON.parse(e.data); | ||
| store.getState().flushStreamingThinking(); | ||
@@ -81,9 +91,26 @@ store.getState().flushStreamingText(); | ||
| store.getState().setStreaming(false); | ||
| store.getState().addCost(cost ?? 0, numTurns ?? 0); | ||
| if (abortedRef.current) { | ||
| store.getState().cancelInflightToolCalls(); | ||
| store.getState().addSystemMessage("Interrupted"); | ||
| abortedRef.current = false; | ||
| } | ||
| }); | ||
| es.addEventListener("error", () => { | ||
| if (es.readyState === EventSource.CLOSED) { | ||
| if (abortedRef.current) { | ||
| store.getState().flushStreamingThinking(); | ||
| store.getState().flushStreamingText(); | ||
| store.getState().cancelInflightToolCalls(); | ||
| store.getState().setThinking(false); | ||
| store.getState().setStreaming(false); | ||
| store.getState().addSystemMessage("Interrupted"); | ||
| abortedRef.current = false; | ||
| es.close(); | ||
| } | ||
| else if (es.readyState === EventSource.CLOSED) { | ||
| store.getState().flushStreamingThinking(); | ||
| store.getState().flushStreamingText(); | ||
| store.getState().setThinking(false); | ||
| store.getState().setStreaming(false); | ||
| } | ||
| }); | ||
@@ -112,2 +139,10 @@ if (onCustomEvent) { | ||
| }, [sessionId, endpoint, store]); | ||
| const stopSession = useCallback(async () => { | ||
| if (!sessionId) | ||
| return; | ||
| abortedRef.current = true; | ||
| store.getState().setThinking(false); | ||
| store.getState().setStreaming(false); | ||
| await fetch(`${endpoint}/sessions/${sessionId}/abort`, { method: "POST" }); | ||
| }, [sessionId, endpoint, store]); | ||
| const respondToPermission = useCallback(async (response) => { | ||
@@ -123,3 +158,3 @@ if (!sessionId) | ||
| }, [sessionId, endpoint, store]); | ||
| return { sessionId, sendMessage, respondToPermission }; | ||
| return { sessionId, sendMessage, stopSession, respondToPermission }; | ||
| } |
+2
-2
| { | ||
| "name": "@neeter/react", | ||
| "version": "0.7.1", | ||
| "version": "0.8.0", | ||
| "description": "React components and hooks for building chat UIs on top of the Claude Agent SDK", | ||
@@ -28,3 +28,3 @@ "license": "MIT", | ||
| "tailwind-merge": "^3.4.0", | ||
| "@neeter/types": "0.7.1" | ||
| "@neeter/types": "0.8.0" | ||
| }, | ||
@@ -31,0 +31,0 @@ "peerDependencies": { |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
91074
3.75%1740
4.25%4
33.33%+ Added
- Removed
Updated