@neeter/react 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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/dist/AgentProvider.d.ts +15 -0
  3. package/dist/AgentProvider.js +30 -0
  4. package/dist/ApprovalButtons.d.ts +5 -0
  5. package/dist/ApprovalButtons.js +30 -0
  6. package/dist/ChatInput.d.ts +6 -0
  7. package/dist/ChatInput.js +41 -0
  8. package/dist/CollapsibleCard.d.ts +9 -0
  9. package/dist/CollapsibleCard.js +9 -0
  10. package/dist/MessageList.d.ts +3 -0
  11. package/dist/MessageList.js +32 -0
  12. package/dist/PendingPermissions.d.ts +3 -0
  13. package/dist/PendingPermissions.js +35 -0
  14. package/dist/StatusDot.d.ts +8 -0
  15. package/dist/StatusDot.js +15 -0
  16. package/dist/TextMessage.d.ts +5 -0
  17. package/dist/TextMessage.js +8 -0
  18. package/dist/ThinkingBlock.d.ts +5 -0
  19. package/dist/ThinkingBlock.js +38 -0
  20. package/dist/ThinkingIndicator.d.ts +3 -0
  21. package/dist/ThinkingIndicator.js +5 -0
  22. package/dist/ToolApprovalCard.d.ts +7 -0
  23. package/dist/ToolApprovalCard.js +11 -0
  24. package/dist/ToolCallCard.d.ts +5 -0
  25. package/dist/ToolCallCard.js +59 -0
  26. package/dist/UserQuestionCard.d.ts +6 -0
  27. package/dist/UserQuestionCard.js +120 -0
  28. package/dist/approval-matching.d.ts +13 -0
  29. package/dist/approval-matching.js +30 -0
  30. package/dist/cn.d.ts +2 -0
  31. package/dist/cn.js +5 -0
  32. package/dist/icons.d.ts +7 -0
  33. package/dist/icons.js +8 -0
  34. package/dist/index.d.ts +29 -0
  35. package/dist/index.js +28 -0
  36. package/dist/markdown-overrides.d.ts +2 -0
  37. package/dist/markdown-overrides.js +8 -0
  38. package/dist/registry.d.ts +4 -0
  39. package/dist/registry.js +10 -0
  40. package/dist/store.d.ts +34 -0
  41. package/dist/store.js +141 -0
  42. package/dist/types.d.ts +19 -0
  43. package/dist/types.js +1 -0
  44. package/dist/use-agent.d.ts +12 -0
  45. package/dist/use-agent.js +119 -0
  46. package/dist/widgets/AskUserQuestionWidget.d.ts +1 -0
  47. package/dist/widgets/AskUserQuestionWidget.js +42 -0
  48. package/dist/widgets/BashWidget.d.ts +1 -0
  49. package/dist/widgets/BashWidget.js +33 -0
  50. package/dist/widgets/EditWidget.d.ts +1 -0
  51. package/dist/widgets/EditWidget.js +36 -0
  52. package/dist/widgets/GlobWidget.d.ts +1 -0
  53. package/dist/widgets/GlobWidget.js +31 -0
  54. package/dist/widgets/GrepWidget.d.ts +1 -0
  55. package/dist/widgets/GrepWidget.js +36 -0
  56. package/dist/widgets/NotebookEditWidget.d.ts +1 -0
  57. package/dist/widgets/NotebookEditWidget.js +47 -0
  58. package/dist/widgets/ReadWidget.d.ts +1 -0
  59. package/dist/widgets/ReadWidget.js +46 -0
  60. package/dist/widgets/TodoWriteWidget.d.ts +1 -0
  61. package/dist/widgets/TodoWriteWidget.js +40 -0
  62. package/dist/widgets/WebFetchWidget.d.ts +1 -0
  63. package/dist/widgets/WebFetchWidget.js +48 -0
  64. package/dist/widgets/WebSearchWidget.d.ts +1 -0
  65. package/dist/widgets/WebSearchWidget.js +85 -0
  66. package/dist/widgets/WriteWidget.d.ts +1 -0
  67. package/dist/widgets/WriteWidget.js +30 -0
  68. package/package.json +45 -0
  69. package/src/theme.css +170 -0
@@ -0,0 +1,36 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { registerWidget } from "../registry.js";
3
+ function GrepInputRenderer({ input }) {
4
+ const pattern = typeof input.pattern === "string" ? input.pattern : null;
5
+ const path = typeof input.path === "string" ? input.path : null;
6
+ const glob = typeof input.glob === "string" ? input.glob : null;
7
+ const outputMode = typeof input.output_mode === "string" ? input.output_mode : null;
8
+ if (!pattern)
9
+ return null;
10
+ return (_jsx("div", { className: "mt-1.5 space-y-1", children: _jsx("pre", { className: "text-[11px] leading-snug text-muted-foreground bg-accent rounded px-2 py-1 overflow-x-auto whitespace-pre-wrap break-all", children: _jsxs("code", { children: ["/", pattern, "/", path ? ` in ${path}` : "", glob ? ` --glob ${glob}` : "", outputMode && outputMode !== "files_with_matches" ? ` (${outputMode})` : ""] }) }) }));
11
+ }
12
+ function GrepWidget({ result, input, phase }) {
13
+ const pattern = typeof input.pattern === "string" ? input.pattern : null;
14
+ if (phase === "running" || phase === "pending") {
15
+ return (_jsx("div", { className: "flex items-center gap-2 text-xs text-muted-foreground py-1", children: _jsxs("span", { className: "animate-pulse", children: ["Searching ", pattern ? `/${pattern}/` : "", "\u2026"] }) }));
16
+ }
17
+ if (typeof result !== "string" || !result)
18
+ return null;
19
+ return (_jsx("div", { className: "py-1 text-xs", children: _jsx("pre", { className: "leading-snug text-foreground bg-accent rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all max-h-[300px] overflow-y-auto", children: _jsx("code", { children: result }) }) }));
20
+ }
21
+ registerWidget({
22
+ toolName: "Grep",
23
+ label: "Grep",
24
+ richLabel: (_r, input) => {
25
+ const pattern = typeof input.pattern === "string" ? input.pattern : null;
26
+ if (!pattern)
27
+ return null;
28
+ const type = typeof input.type === "string" ? input.type : null;
29
+ const glob = typeof input.glob === "string" ? input.glob : null;
30
+ const scope = type ? ` in ${type} files` : glob ? ` in ${glob}` : "";
31
+ const pat = pattern.length > 40 ? `${pattern.slice(0, 37)}…` : pattern;
32
+ return `${pat}${scope}`;
33
+ },
34
+ inputRenderer: GrepInputRenderer,
35
+ component: GrepWidget,
36
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { registerWidget } from "../registry.js";
3
+ function basename(filePath) {
4
+ return filePath.split("/").pop() ?? filePath;
5
+ }
6
+ const modeLabel = {
7
+ replace: "Replace cell",
8
+ insert: "Insert cell",
9
+ delete: "Delete cell",
10
+ };
11
+ function NotebookEditInputRenderer({ input }) {
12
+ const notebookPath = typeof input.notebook_path === "string" ? input.notebook_path : null;
13
+ const editMode = typeof input.edit_mode === "string" ? input.edit_mode : "replace";
14
+ const cellType = typeof input.cell_type === "string" ? input.cell_type : null;
15
+ const cellId = typeof input.cell_id === "string" ? input.cell_id : null;
16
+ const cellNumber = typeof input.cell_number === "number" ? input.cell_number : null;
17
+ const newSource = typeof input.new_source === "string" ? input.new_source : null;
18
+ if (!notebookPath)
19
+ return null;
20
+ const cellRef = cellId ?? (cellNumber != null ? `cell ${cellNumber}` : null);
21
+ const cellLabel = cellRef && editMode === "insert" ? `after ${cellRef}` : cellRef;
22
+ return (_jsxs("div", { className: "mt-1.5 space-y-1", children: [_jsxs("div", { className: "text-xs text-muted-foreground", children: [modeLabel[editMode] ?? editMode, cellType && _jsxs("span", { className: "ml-1 opacity-70", children: ["[", cellType, "]"] }), cellLabel && _jsx("span", { className: "ml-1 font-mono opacity-70", children: cellLabel }), _jsx("span", { className: "ml-1.5 font-mono opacity-70", children: basename(notebookPath) })] }), newSource && editMode !== "delete" && (_jsx("pre", { className: "text-[11px] leading-snug text-muted-foreground bg-accent rounded px-2 py-1 overflow-x-auto whitespace-pre-wrap break-all max-h-[120px] overflow-y-auto", children: _jsx("code", { children: newSource }) }))] }));
23
+ }
24
+ function NotebookEditWidget({ input, phase }) {
25
+ const notebookPath = typeof input.notebook_path === "string" ? input.notebook_path : null;
26
+ const editMode = typeof input.edit_mode === "string" ? input.edit_mode : "replace";
27
+ const cellType = typeof input.cell_type === "string" ? input.cell_type : null;
28
+ const newSource = typeof input.new_source === "string" ? input.new_source : null;
29
+ if (phase === "running" || phase === "pending") {
30
+ return (_jsx("div", { className: "flex items-center gap-2 text-xs text-muted-foreground py-1", children: _jsxs("span", { className: "animate-pulse", children: ["Editing ", notebookPath ? basename(notebookPath) : "notebook", "\u2026"] }) }));
31
+ }
32
+ return (_jsxs("div", { className: "py-1 space-y-1.5", children: [_jsxs("div", { className: "text-xs text-muted-foreground", children: [editMode === "insert" ? "Inserted" : editMode === "delete" ? "Deleted" : "Replaced", " cell", cellType && _jsxs("span", { className: "ml-1 opacity-70", children: ["[", cellType, "]"] })] }), newSource && editMode !== "delete" && (_jsx("pre", { className: "text-[11px] leading-snug text-foreground bg-accent rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all max-h-[200px] overflow-y-auto", children: _jsx("code", { children: newSource }) }))] }));
33
+ }
34
+ registerWidget({
35
+ toolName: "NotebookEdit",
36
+ label: "Notebook Edit",
37
+ richLabel: (_r, input) => {
38
+ const notebookPath = typeof input.notebook_path === "string" ? input.notebook_path : null;
39
+ const editMode = typeof input.edit_mode === "string" ? input.edit_mode : null;
40
+ const name = notebookPath ? basename(notebookPath) : null;
41
+ if (!name)
42
+ return null;
43
+ return editMode && editMode !== "replace" ? `${editMode} ${name}` : name;
44
+ },
45
+ inputRenderer: NotebookEditInputRenderer,
46
+ component: NotebookEditWidget,
47
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { registerWidget } from "../registry.js";
3
+ /** Show path relative to likely project root, falling back to last 3 segments. */
4
+ function shortPath(filePath) {
5
+ const parts = filePath.split("/");
6
+ // Heuristic: strip up to the first segment after common prefixes
7
+ // e.g. /Users/dan/Documents/my-project/src/server/index.ts → src/server/index.ts
8
+ // Look for common root markers and take everything after
9
+ const markers = ["Documents", "Projects", "repos", "src", "home"];
10
+ for (let i = 0; i < parts.length - 1; i++) {
11
+ if (markers.includes(parts[i])) {
12
+ // Take from the segment AFTER the marker's child (the project dir)
13
+ const projectStart = i + 2; // marker + project-name
14
+ if (projectStart < parts.length) {
15
+ return parts.slice(projectStart).join("/");
16
+ }
17
+ }
18
+ }
19
+ // Fallback: last 3 segments
20
+ return parts.slice(-3).join("/");
21
+ }
22
+ function ReadInputRenderer({ input }) {
23
+ const filePath = typeof input.file_path === "string" ? input.file_path : null;
24
+ if (!filePath)
25
+ return null;
26
+ return _jsx("div", { className: "mt-1.5 text-xs text-muted-foreground font-mono truncate", children: filePath });
27
+ }
28
+ function ReadWidget({ result, input, phase }) {
29
+ const filePath = typeof input.file_path === "string" ? input.file_path : null;
30
+ if (phase === "running" || phase === "pending") {
31
+ return (_jsx("div", { className: "flex items-center gap-2 text-xs text-muted-foreground py-1", children: _jsxs("span", { className: "animate-pulse", children: ["Reading ", filePath ? shortPath(filePath) : "file", "\u2026"] }) }));
32
+ }
33
+ if (typeof result !== "string" || !result)
34
+ return null;
35
+ return (_jsx("div", { className: "py-1 text-xs", children: _jsx("pre", { className: "leading-snug text-foreground bg-accent rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all max-h-[300px] overflow-y-auto", children: _jsx("code", { children: result }) }) }));
36
+ }
37
+ registerWidget({
38
+ toolName: "Read",
39
+ label: "Read",
40
+ richLabel: (_r, input) => {
41
+ const filePath = typeof input.file_path === "string" ? input.file_path : null;
42
+ return filePath ? shortPath(filePath) : null;
43
+ },
44
+ inputRenderer: ReadInputRenderer,
45
+ component: ReadWidget,
46
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { registerWidget } from "../registry.js";
3
+ function parseTodos(input) {
4
+ if (!Array.isArray(input.todos))
5
+ return [];
6
+ return input.todos.filter((t) => t != null &&
7
+ typeof t === "object" &&
8
+ typeof t.content === "string" &&
9
+ typeof t.status === "string");
10
+ }
11
+ function TodoList({ todos }) {
12
+ return (_jsx("div", { className: "space-y-0.5 text-xs", children: todos.map((todo, i) => (
13
+ // biome-ignore lint/suspicious/noArrayIndexKey: todo items lack stable IDs
14
+ _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { className: "shrink-0 w-4 text-center", children: todo.status === "completed" ? (_jsx("span", { className: "text-green-600 dark:text-green-400", children: "\u2713" })) : todo.status === "in_progress" ? (_jsx("span", { className: "text-blue-600 dark:text-blue-400", children: "\u25CB" })) : (_jsx("span", { className: "text-muted-foreground", children: "\u25CB" })) }), _jsx("span", { className: todo.status === "completed"
15
+ ? "text-muted-foreground line-through"
16
+ : todo.status === "in_progress"
17
+ ? "text-foreground font-medium"
18
+ : "text-muted-foreground", children: todo.content })] }, i))) }));
19
+ }
20
+ function TodoWriteWidget({ input, phase }) {
21
+ const todos = parseTodos(input);
22
+ if (phase === "running" || phase === "pending") {
23
+ return (_jsx("div", { className: "flex items-center gap-2 text-xs text-muted-foreground py-1", children: _jsx("span", { className: "animate-pulse", children: "Updating tasks\u2026" }) }));
24
+ }
25
+ if (!todos.length)
26
+ return null;
27
+ return (_jsx("div", { className: "py-1", children: _jsx(TodoList, { todos: todos }) }));
28
+ }
29
+ registerWidget({
30
+ toolName: "TodoWrite",
31
+ label: "Todo",
32
+ richLabel: (_r, input) => {
33
+ const todos = parseTodos(input);
34
+ if (!todos.length)
35
+ return null;
36
+ const done = todos.filter((t) => t.status === "completed").length;
37
+ return `${done}/${todos.length} done`;
38
+ },
39
+ component: TodoWriteWidget,
40
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Markdown from "react-markdown";
3
+ import { markdownComponents } from "../markdown-overrides.js";
4
+ import { registerWidget } from "../registry.js";
5
+ function domain(url) {
6
+ try {
7
+ return new URL(url).hostname.replace(/^www\./, "");
8
+ }
9
+ catch {
10
+ return url;
11
+ }
12
+ }
13
+ function faviconUrl(url) {
14
+ try {
15
+ const host = new URL(url).hostname;
16
+ return `https://www.google.com/s2/favicons?sz=32&domain=${host}`;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ function WebFetchInputRenderer({ input }) {
23
+ const url = typeof input.url === "string" ? input.url : null;
24
+ const prompt = typeof input.prompt === "string" ? input.prompt : null;
25
+ if (!url)
26
+ return null;
27
+ return (_jsxs("div", { className: "mt-1.5 space-y-1", children: [_jsxs("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1.5 rounded-full bg-accent px-2.5 py-1 text-[11px] leading-none whitespace-nowrap max-w-[280px] text-muted-foreground no-underline hover:bg-accent/80 hover:text-primary transition-colors", children: [_jsx("img", { src: faviconUrl(url), alt: "", width: 14, height: 14, className: "rounded-full shrink-0" }), _jsx("span", { className: "truncate", children: domain(url) })] }), prompt && _jsxs("div", { className: "text-xs italic text-muted-foreground", children: ["\u201C", prompt, "\u201D"] })] }));
28
+ }
29
+ function WebFetchWidget({ result, input, phase }) {
30
+ if (phase === "running" || phase === "pending") {
31
+ const url = typeof input.url === "string" ? input.url : null;
32
+ return (_jsx("div", { className: "flex items-center gap-2 text-xs text-muted-foreground py-1", children: _jsxs("span", { className: "animate-pulse", children: ["Fetching ", url ? domain(url) : "page", "\u2026"] }) }));
33
+ }
34
+ if (typeof result !== "string" || !result)
35
+ return null;
36
+ const url = typeof input.url === "string" ? input.url : null;
37
+ return (_jsxs("div", { className: "py-1 space-y-1.5 text-xs", children: [url && (_jsxs("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1.5 rounded-full bg-accent px-2.5 py-1 text-[11px] leading-none whitespace-nowrap max-w-[280px] text-muted-foreground no-underline hover:bg-accent/80 hover:text-primary transition-colors", children: [_jsx("img", { src: faviconUrl(url), alt: "", width: 14, height: 14, className: "rounded-full shrink-0" }), _jsx("span", { className: "truncate", children: domain(url) })] })), _jsx("div", { className: "text-foreground leading-relaxed [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", children: _jsx(Markdown, { components: markdownComponents, children: result }) })] }));
38
+ }
39
+ registerWidget({
40
+ toolName: "WebFetch",
41
+ label: "Web Fetch",
42
+ richLabel: (_r, input) => {
43
+ const url = typeof input.url === "string" ? input.url : null;
44
+ return url ? domain(url) : null;
45
+ },
46
+ inputRenderer: WebFetchInputRenderer,
47
+ component: WebFetchWidget,
48
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { registerWidget } from "../registry.js";
4
+ function extractJsonArray(str, start) {
5
+ if (str[start] !== "[")
6
+ return null;
7
+ let depth = 0;
8
+ for (let i = start; i < str.length; i++) {
9
+ if (str[i] === "[")
10
+ depth++;
11
+ else if (str[i] === "]") {
12
+ depth--;
13
+ if (depth === 0)
14
+ return str.slice(start, i + 1);
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+ function parseSearchResult(raw) {
20
+ if (typeof raw !== "string")
21
+ return null;
22
+ const queryMatch = raw.match(/^Web search results for query: "(.+?)"\n/);
23
+ if (!queryMatch)
24
+ return null;
25
+ const linksIdx = raw.indexOf("Links: [");
26
+ if (linksIdx === -1)
27
+ return null;
28
+ const jsonStr = extractJsonArray(raw, linksIdx + 7);
29
+ if (!jsonStr)
30
+ return null;
31
+ try {
32
+ const links = JSON.parse(jsonStr);
33
+ return { query: queryMatch[1], links };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function faviconUrl(url) {
40
+ try {
41
+ const host = new URL(url).hostname;
42
+ return `https://www.google.com/s2/favicons?sz=32&domain=${host}`;
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ function domain(url) {
49
+ try {
50
+ return new URL(url).hostname.replace(/^www\./, "");
51
+ }
52
+ catch {
53
+ return url;
54
+ }
55
+ }
56
+ function WebSearchInputRenderer({ input }) {
57
+ const query = typeof input.query === "string" ? input.query : null;
58
+ if (!query)
59
+ return null;
60
+ return _jsxs("div", { className: "mt-1.5 text-xs italic text-muted-foreground", children: ["\u201C", query, "\u201D"] });
61
+ }
62
+ function WebSearchWidget({ result, phase }) {
63
+ const [expanded, setExpanded] = useState(false);
64
+ if (phase === "running" || phase === "pending") {
65
+ return (_jsx("div", { className: "flex items-center gap-2 text-xs text-muted-foreground py-1", children: _jsx("span", { className: "animate-pulse", children: "Searching the web\u2026" }) }));
66
+ }
67
+ const parsed = parseSearchResult(result);
68
+ if (!parsed)
69
+ return null;
70
+ const pills = parsed.links.map((link) => (_jsxs("a", { href: link.url, target: "_blank", rel: "noopener noreferrer", title: link.title, className: "inline-flex items-center gap-1.5 rounded-full bg-accent px-2.5 py-1 text-[11px] leading-none whitespace-nowrap max-w-[180px] text-muted-foreground no-underline hover:bg-accent/80 hover:text-primary transition-colors shrink-0", children: [_jsx("img", { src: faviconUrl(link.url), alt: "", width: 14, height: 14, className: "rounded-full shrink-0" }), _jsx("span", { className: "truncate", children: domain(link.url) })] }, link.url)));
71
+ if (expanded) {
72
+ return (_jsxs("div", { className: "py-1 space-y-1.5", children: [_jsx("div", { className: "flex flex-wrap gap-1.5", children: pills }), _jsx("button", { type: "button", onClick: () => setExpanded(false), className: "text-[10px] text-muted-foreground/60 hover:text-muted-foreground transition-colors", children: "Show less" })] }));
73
+ }
74
+ return (_jsxs("div", { className: "py-1 space-y-0.5", children: [_jsxs("div", { className: "relative", children: [_jsx("div", { className: "flex flex-nowrap gap-1.5 overflow-hidden max-h-[22px]", children: pills }), _jsx("div", { className: "pointer-events-none absolute inset-y-0 right-0 w-12 bg-gradient-to-l from-accent/50 to-transparent" })] }), _jsxs("button", { type: "button", onClick: () => setExpanded(true), className: "text-[10px] text-muted-foreground/60 hover:text-muted-foreground transition-colors pt-0.5", children: ["Show all ", parsed.links.length, " sources"] })] }));
75
+ }
76
+ registerWidget({
77
+ toolName: "WebSearch",
78
+ label: "Web Search",
79
+ richLabel: (r) => {
80
+ const p = parseSearchResult(r);
81
+ return p ? `"${p.query}" · ${p.links.length} sources` : null;
82
+ },
83
+ inputRenderer: WebSearchInputRenderer,
84
+ component: WebSearchWidget,
85
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { registerWidget } from "../registry.js";
3
+ function basename(filePath) {
4
+ return filePath.split("/").pop() ?? filePath;
5
+ }
6
+ function WriteInputRenderer({ input }) {
7
+ const filePath = typeof input.file_path === "string" ? input.file_path : null;
8
+ const content = typeof input.content === "string" ? input.content : null;
9
+ if (!filePath)
10
+ return null;
11
+ return (_jsxs("div", { className: "mt-1.5 space-y-1", children: [_jsx("div", { className: "text-xs text-muted-foreground font-mono truncate", children: filePath }), content && (_jsx("pre", { className: "text-[11px] leading-snug text-muted-foreground bg-accent rounded px-2 py-1 overflow-x-auto whitespace-pre-wrap break-all max-h-[120px] overflow-y-auto", children: _jsx("code", { children: content }) }))] }));
12
+ }
13
+ function WriteWidget({ input, phase }) {
14
+ const filePath = typeof input.file_path === "string" ? input.file_path : null;
15
+ if (phase === "running" || phase === "pending") {
16
+ return (_jsx("div", { className: "flex items-center gap-2 text-xs text-muted-foreground py-1", children: _jsxs("span", { className: "animate-pulse", children: ["Writing ", filePath ? basename(filePath) : "file", "\u2026"] }) }));
17
+ }
18
+ const content = typeof input.content === "string" ? input.content : null;
19
+ return (_jsx("div", { className: "py-1 space-y-1.5", children: content && (_jsx("pre", { className: "text-[11px] leading-snug text-foreground bg-accent rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap break-all max-h-[200px] overflow-y-auto", children: _jsx("code", { children: content }) })) }));
20
+ }
21
+ registerWidget({
22
+ toolName: "Write",
23
+ label: "Write",
24
+ richLabel: (_r, input) => {
25
+ const filePath = typeof input.file_path === "string" ? input.file_path : null;
26
+ return filePath ? basename(filePath) : null;
27
+ },
28
+ inputRenderer: WriteInputRenderer,
29
+ component: WriteWidget,
30
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@neeter/react",
3
+ "version": "0.6.0",
4
+ "description": "React components and hooks for building chat UIs on top of the Claude Agent SDK",
5
+ "license": "MIT",
6
+ "author": "Dan Leeper",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/quantumleeps/neeter.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./theme.css": "./src/theme.css"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src/theme.css"
24
+ ],
25
+ "dependencies": {
26
+ "clsx": "^2.1.1",
27
+ "tailwind-merge": "^3.4.0",
28
+ "@neeter/types": "0.6.0"
29
+ },
30
+ "peerDependencies": {
31
+ "react": ">=18.0.0",
32
+ "react-markdown": ">=10.0.0",
33
+ "zustand": ">=5.0.0",
34
+ "immer": ">=10.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/react": "^19.2.7",
38
+ "immer": "^11.1.4",
39
+ "react": "^19.2.0",
40
+ "zustand": "^5.0.11"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json"
44
+ }
45
+ }
package/src/theme.css ADDED
@@ -0,0 +1,170 @@
1
+ /*
2
+ * neeter/theme.css — default theme for neeter components.
3
+ *
4
+ * Provides the CSS variable bridge (via @theme inline) and a neutral color
5
+ * palette so neeter components render correctly out of the box.
6
+ *
7
+ * If your app already uses shadcn/ui you don't need this file — neeter
8
+ * components use only standard shadcn variables plus Tailwind's built-in
9
+ * color palette (amber, emerald, blue) for status indicators.
10
+ *
11
+ * Usage (non-shadcn apps):
12
+ * @import "@neeter/react/theme.css";
13
+ */
14
+
15
+ /* -- Source scanning ------------------------------------------------------- */
16
+ /* Tells Tailwind to scan neeter's component source for utility classes. */
17
+
18
+ @source ".";
19
+
20
+ /* -- Tailwind v4 variable bridge ------------------------------------------ */
21
+ /* Maps CSS custom properties to Tailwind utility classes. */
22
+ /* Same mappings as shadcn/tailwind.css — if you have that, skip this file. */
23
+
24
+ @theme inline {
25
+ --radius-sm: calc(var(--radius) - 4px);
26
+ --radius-md: calc(var(--radius) - 2px);
27
+ --radius-lg: var(--radius);
28
+ --radius-xl: calc(var(--radius) + 4px);
29
+ --color-background: var(--background);
30
+ --color-foreground: var(--foreground);
31
+ --color-card: var(--card);
32
+ --color-card-foreground: var(--card-foreground);
33
+ --color-popover: var(--popover);
34
+ --color-popover-foreground: var(--popover-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-primary-foreground: var(--primary-foreground);
37
+ --color-secondary: var(--secondary);
38
+ --color-secondary-foreground: var(--secondary-foreground);
39
+ --color-muted: var(--muted);
40
+ --color-muted-foreground: var(--muted-foreground);
41
+ --color-accent: var(--accent);
42
+ --color-accent-foreground: var(--accent-foreground);
43
+ --color-destructive: var(--destructive);
44
+ --color-warning: var(--warning);
45
+ --color-success: var(--success);
46
+ --color-info: var(--info);
47
+ --color-border: var(--border);
48
+ --color-input: var(--input);
49
+ --color-ring: var(--ring);
50
+ }
51
+
52
+ /* -- Light mode (default) ------------------------------------------------- */
53
+
54
+ :root {
55
+ --radius: 0.625rem;
56
+ --background: oklch(1 0 0);
57
+ --foreground: oklch(0.145 0 0);
58
+ --card: oklch(1 0 0);
59
+ --card-foreground: oklch(0.145 0 0);
60
+ --popover: oklch(1 0 0);
61
+ --popover-foreground: oklch(0.145 0 0);
62
+ --primary: oklch(0.205 0 0);
63
+ --primary-foreground: oklch(0.985 0 0);
64
+ --secondary: oklch(0.97 0 0);
65
+ --secondary-foreground: oklch(0.205 0 0);
66
+ --muted: oklch(0.97 0 0);
67
+ --muted-foreground: oklch(0.556 0 0);
68
+ --accent: oklch(0.97 0 0);
69
+ --accent-foreground: oklch(0.205 0 0);
70
+ --destructive: oklch(0.577 0.245 27.325);
71
+ --warning: oklch(0.795 0.184 86.047);
72
+ --success: oklch(0.723 0.219 149.579);
73
+ --info: oklch(0.623 0.214 259.815);
74
+ --border: oklch(0.922 0 0);
75
+ --input: oklch(0.922 0 0);
76
+ --ring: oklch(0.708 0 0);
77
+ }
78
+
79
+ /* -- Dark mode (class-based) ---------------------------------------------- */
80
+
81
+ .dark {
82
+ --background: oklch(0.145 0 0);
83
+ --foreground: oklch(0.985 0 0);
84
+ --card: oklch(0.205 0 0);
85
+ --card-foreground: oklch(0.985 0 0);
86
+ --popover: oklch(0.205 0 0);
87
+ --popover-foreground: oklch(0.985 0 0);
88
+ --primary: oklch(0.922 0 0);
89
+ --primary-foreground: oklch(0.205 0 0);
90
+ --secondary: oklch(0.269 0 0);
91
+ --secondary-foreground: oklch(0.985 0 0);
92
+ --muted: oklch(0.269 0 0);
93
+ --muted-foreground: oklch(0.708 0 0);
94
+ --accent: oklch(0.269 0 0);
95
+ --accent-foreground: oklch(0.985 0 0);
96
+ --destructive: oklch(0.704 0.191 22.216);
97
+ --warning: oklch(0.852 0.199 91.936);
98
+ --success: oklch(0.792 0.209 151.711);
99
+ --info: oklch(0.707 0.165 254.624);
100
+ --border: oklch(1 0 0 / 10%);
101
+ --input: oklch(1 0 0 / 15%);
102
+ --ring: oklch(0.556 0 0);
103
+ }
104
+
105
+ /* -- Dark mode (system preference) ---------------------------------------- */
106
+ /* Activates automatically unless .light is set on :root. */
107
+
108
+ @media (prefers-color-scheme: dark) {
109
+ :root:not(.light) {
110
+ --background: oklch(0.145 0 0);
111
+ --foreground: oklch(0.985 0 0);
112
+ --card: oklch(0.205 0 0);
113
+ --card-foreground: oklch(0.985 0 0);
114
+ --popover: oklch(0.205 0 0);
115
+ --popover-foreground: oklch(0.985 0 0);
116
+ --primary: oklch(0.922 0 0);
117
+ --primary-foreground: oklch(0.205 0 0);
118
+ --secondary: oklch(0.269 0 0);
119
+ --secondary-foreground: oklch(0.985 0 0);
120
+ --muted: oklch(0.269 0 0);
121
+ --muted-foreground: oklch(0.708 0 0);
122
+ --accent: oklch(0.269 0 0);
123
+ --accent-foreground: oklch(0.985 0 0);
124
+ --destructive: oklch(0.704 0.191 22.216);
125
+ --warning: oklch(0.852 0.199 91.936);
126
+ --success: oklch(0.792 0.209 151.711);
127
+ --info: oklch(0.707 0.165 254.624);
128
+ --border: oklch(1 0 0 / 10%);
129
+ --input: oklch(1 0 0 / 15%);
130
+ --ring: oklch(0.556 0 0);
131
+ }
132
+ }
133
+
134
+ /* -- Base resets ---------------------------------------------------------- */
135
+
136
+ *,
137
+ *::before,
138
+ *::after {
139
+ border-color: var(--border);
140
+ }
141
+
142
+ body {
143
+ background-color: var(--background);
144
+ color: var(--foreground);
145
+ }
146
+
147
+ /* -- Scrollbar styling --------------------------------------------------- */
148
+
149
+ * {
150
+ scrollbar-color: var(--muted-foreground) transparent;
151
+ scrollbar-width: thin;
152
+ }
153
+
154
+ ::-webkit-scrollbar {
155
+ width: 6px;
156
+ height: 6px;
157
+ }
158
+
159
+ ::-webkit-scrollbar-track {
160
+ background: transparent;
161
+ }
162
+
163
+ ::-webkit-scrollbar-thumb {
164
+ background: var(--muted-foreground);
165
+ border-radius: 3px;
166
+ }
167
+
168
+ ::-webkit-scrollbar-thumb:hover {
169
+ background: var(--foreground);
170
+ }