Sign In

@neeter/react

Package Overview
Dependencies
Maintainers
1
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@neeter/react - npm Package Compare versions

Comparing version
0.12.0
to
0.13.0
+6
dist/RollbackButton.d.ts
import type { RewindFilesResult } from "@neeter/types";
export declare function RollbackButton({ checkpointId, isFirstCheckpoint, onFilesRewound, }: {
checkpointId: string;
isFirstCheckpoint: boolean;
onFilesRewound?: (result: RewindFilesResult) => void;
}): import("react/jsx-runtime").JSX.Element;
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useRef, useState } from "react";
import { useAgentContext, useChatStore } from "./AgentProvider.js";
export function RollbackButton({ checkpointId, isFirstCheckpoint, onFilesRewound, }) {
const { resumeSession, rewindSession, newSession } = useAgentContext();
const isStreaming = useChatStore((s) => s.isStreaming);
const fileCheckpointing = useChatStore((s) => s.fileCheckpointing);
const [open, setOpen] = useState(false);
const [feedback, setFeedback] = useState(null);
const ref = useRef(null);
useEffect(() => {
if (!open)
return;
function onClickOutside(e) {
if (ref.current && !ref.current.contains(e.target))
setOpen(false);
}
document.addEventListener("mousedown", onClickOutside);
return () => document.removeEventListener("mousedown", onClickOutside);
}, [open]);
const withClose = (fn) => async () => {
setOpen(false);
await fn();
};
const handleRewind = withClose(async () => {
if (isFirstCheckpoint) {
if (fileCheckpointing)
await rewindSession(checkpointId);
await newSession();
}
else {
await resumeSession({ resumeSessionAt: checkpointId });
}
});
const handleFilesOnly = withClose(async () => {
const result = await rewindSession(checkpointId);
if (result.canRewind) {
onFilesRewound?.(result);
const n = result.filesChanged?.length ?? 0;
setFeedback(`${n} file${n !== 1 ? "s" : ""} restored`);
setTimeout(() => setFeedback(null), 2500);
}
else {
setFeedback(result.error ?? "Cannot rewind");
setTimeout(() => setFeedback(null), 2500);
}
});
return (_jsxs("div", { ref: ref, className: "relative", children: [_jsxs("div", { className: "flex items-center gap-1.5 -mt-1 mb-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [_jsx("div", { className: "flex-1 border-t border-dashed border-muted-foreground/20" }), feedback && (_jsx("span", { className: "whitespace-nowrap text-[10px] text-emerald-600 dark:text-emerald-400 animate-pulse", children: feedback })), _jsx("button", { type: "button", disabled: isStreaming, onClick: () => setOpen((v) => !v), className: "rounded p-0.5 text-muted-foreground/40 hover:text-muted-foreground disabled:opacity-30 transition-colors", title: "Rollback to just prior to this message", children: _jsxs("svg", { "aria-hidden": "true", className: "size-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M1 4v6h6" }), _jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })] }) })] }), open && (_jsxs("div", { className: "absolute right-0 top-7 z-20 flex flex-col rounded-md border bg-popover p-1 shadow-md min-w-[200px]", children: [_jsxs("button", { type: "button", onClick: handleRewind, className: "flex items-center gap-2 rounded px-2 py-1.5 text-xs text-popover-foreground hover:bg-muted text-left", children: [_jsxs("svg", { "aria-hidden": "true", className: "size-3 shrink-0", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M1 4v6h6" }), _jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })] }), fileCheckpointing ? "Rewind conversation and files" : "Rewind conversation"] }), fileCheckpointing && (_jsxs("button", { type: "button", onClick: handleFilesOnly, className: "flex items-center gap-2 rounded px-2 py-1.5 text-xs text-popover-foreground hover:bg-muted text-left", children: [_jsxs("svg", { "aria-hidden": "true", className: "size-3 shrink-0", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M9 14 4 9l5-5" }), _jsx("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" })] }), "Rewind files only"] }))] }))] }));
}
+1
-0

@@ -19,2 +19,3 @@ import "./widgets/AskUserQuestionWidget.js";

export { PendingPermissions } from "./PendingPermissions.js";
export { RollbackButton } from "./RollbackButton.js";
export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";

@@ -21,0 +22,0 @@ export { StatusDot } from "./StatusDot.js";

@@ -19,2 +19,3 @@ // Built-in widgets (side-effect: auto-registers)

export { PendingPermissions } from "./PendingPermissions.js";
export { RollbackButton } from "./RollbackButton.js";
export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";

@@ -21,0 +22,0 @@ export { StatusDot } from "./StatusDot.js";

+3
-1

@@ -1,3 +0,5 @@

export declare function MessageList({ className }: {
import type { RewindFilesResult } from "@neeter/types";
export declare function MessageList({ className, onFilesRewound, }: {
className?: string;
onFilesRewound?: (result: RewindFilesResult) => void;
}): import("react/jsx-runtime").JSX.Element;

@@ -6,2 +6,3 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";

import { PendingPermissions } from "./PendingPermissions.js";
import { RollbackButton } from "./RollbackButton.js";
import { TextMessage } from "./TextMessage.js";

@@ -11,4 +12,5 @@ import { ThinkingBlock } from "./ThinkingBlock.js";

import { ToolCallCard } from "./ToolCallCard.js";
export function MessageList({ className }) {
export function MessageList({ className, onFilesRewound, }) {
const messages = useChatStore((s) => s.messages);
const checkpoints = useChatStore((s) => s.checkpoints);
const streamingText = useChatStore((s) => s.streamingText);

@@ -22,2 +24,4 @@ const streamingThinking = useChatStore((s) => s.streamingThinking);

}, [messages, streamingText, streamingThinking, isThinking]);
// Map nth user message → checkpoints[n]
let userMsgIndex = 0;
return (_jsx("div", { className: cn("flex-1 overflow-y-auto", className), children: _jsxs("div", { className: "flex flex-col gap-3 p-4 text-sm", children: [messages.map((msg) => {

@@ -30,2 +34,6 @@ if (msg.toolCalls?.length) {

}
if (msg.role === "user" && msg.content) {
const cpId = checkpoints[userMsgIndex++];
return (_jsxs("div", { className: "group", children: [cpId && (_jsx(RollbackButton, { checkpointId: cpId, isFirstCheckpoint: cpId === checkpoints[0], onFilesRewound: onFilesRewound })), _jsx("div", { className: "flex justify-end", children: _jsx(TextMessage, { role: "user", content: msg.content }) })] }, msg.id));
}
if (msg.content || msg.thinking) {

@@ -32,0 +40,0 @@ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [msg.thinking && _jsx(ThinkingBlock, { thinking: msg.thinking }), msg.content && (_jsx(TextMessage, { role: msg.role, content: msg.content }))] }, msg.id));

@@ -14,2 +14,3 @@ import type { ChatMessage, McpServerStatus, PermissionRequest, SSEEvent } from "@neeter/types";

checkpoints: string[];
fileCheckpointing: boolean;
totalCost: number;

@@ -38,2 +39,3 @@ totalTurns: number;

addCheckpoint: (uuid: string) => void;
setFileCheckpointing: (v: boolean) => void;
addCost: (cost: number, turns: number) => void;

@@ -40,0 +42,0 @@ cancelInflightToolCalls: () => void;

@@ -31,2 +31,3 @@ import { immer } from "zustand/middleware/immer";

checkpoints: [],
fileCheckpointing: false,
totalCost: 0,

@@ -150,2 +151,5 @@ totalTurns: 0,

}),
setFileCheckpointing: (v) => set((s) => {
s.fileCheckpointing = v;
}),
addCost: (cost, turns) => set((s) => {

@@ -180,2 +184,3 @@ s.totalCost += cost;

s.checkpoints = [];
s.fileCheckpointing = false;
s.totalCost = 0;

@@ -194,4 +199,20 @@ s.totalTurns = 0;

const stopAt = options?.stopAtCheckpoint;
let foundTarget = false;
for (const evt of events) {
// "Before" semantics: truncate events before the target checkpoint's
// user message. This matches rewindFiles() which restores files to
// their state before that message's turn.
let replayable = events;
if (stopAt) {
const cpIdx = events.findIndex((e) => e.event === "checkpoint" && JSON.parse(e.data).userMessageUuid === stopAt);
if (cpIdx >= 0) {
let cutIdx = cpIdx;
for (let i = cpIdx - 1; i >= 0; i--) {
if (events[i].event === "user_message") {
cutIdx = i;
break;
}
}
replayable = events.slice(0, cutIdx);
}
}
for (const evt of replayable) {
const data = JSON.parse(evt.data);

@@ -206,2 +227,4 @@ switch (evt.event) {

s.setMcpServers(data.mcpServers);
if (data.fileCheckpointing)
s.setFileCheckpointing(true);
break;

@@ -234,4 +257,2 @@ case "thinking_delta":

s.addCheckpoint(data.userMessageUuid);
if (stopAt && data.userMessageUuid === stopAt)
foundTarget = true;
break;

@@ -242,4 +263,2 @@ case "turn_complete":

s.addCost(data.cost ?? 0, data.numTurns ?? 0);
if (foundTarget)
return;
break;

@@ -250,4 +269,2 @@ case "session_error":

s.addSystemMessage(`Session ended: ${data.subtype}`);
if (foundTarget)
return;
break;

@@ -254,0 +271,0 @@ }

@@ -57,6 +57,8 @@ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";

es.addEventListener("session_init", (e) => {
const { sdkSessionId: id, mcpServers } = JSON.parse(e.data);
const { sdkSessionId: id, mcpServers, fileCheckpointing } = JSON.parse(e.data);
store.getState().setSdkSessionId(id);
if (mcpServers)
store.getState().setMcpServers(mcpServers);
if (fileCheckpointing)
store.getState().setFileCheckpointing(true);
});

@@ -63,0 +65,0 @@ es.addEventListener("message_start", () => {

{
"name": "@neeter/react",
"version": "0.12.0",
"version": "0.13.0",
"description": "React components and hooks for building chat UIs on top of the Claude Agent SDK",

@@ -29,3 +29,3 @@ "license": "MIT",

"tailwind-merge": "^3.5.0",
"@neeter/types": "0.12.0"
"@neeter/types": "0.13.0"
},

@@ -32,0 +32,0 @@ "peerDependencies": {

@@ -72,3 +72,3 @@ # @neeter/react

| [basic-chat](https://github.com/quantumleeps/neeter/tree/main/examples/basic-chat) | Minimal component setup |
| [live-preview](https://github.com/quantumleeps/neeter/tree/main/examples/live-preview) | Custom events, widgets, split-pane preview |
| [code-workbench](https://github.com/quantumleeps/neeter/tree/main/examples/code-workbench) | Custom events, widgets, split-pane preview, file checkpointing |

@@ -75,0 +75,0 @@ ## Documentation