anyclaude-react 0.1.0 → 0.2.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/dist/client.d.ts CHANGED
@@ -1,11 +1,31 @@
1
1
  import type { SDKMessage } from 'anyclaude-sdk';
2
+ export interface ClientToolResult {
3
+ tool_use_id: string;
4
+ content: string | unknown;
5
+ is_error?: boolean;
6
+ }
2
7
  export interface RunOptions {
3
8
  prompt: string;
4
9
  sessionId: string;
5
10
  continueRun?: boolean;
11
+ /** Results of host-executed client tools, carried into a continuation run. */
12
+ clientToolResults?: ClientToolResult[];
6
13
  }
7
14
  /** Produces the raw SDKMessage stream for one underlying run (in-process or remote). */
8
15
  export type RunFn = (opts: RunOptions) => AsyncIterable<SDKMessage>;
16
+ /** Executor for a host/client-side tool (e.g. run `bash` on a WebContainer). */
17
+ export type ClientToolExecutor = (input: Record<string, unknown>, req: {
18
+ tool_use_id: string;
19
+ name: string;
20
+ }) => Promise<{
21
+ content: string | unknown;
22
+ is_error?: boolean;
23
+ }> | {
24
+ content: string | unknown;
25
+ is_error?: boolean;
26
+ };
27
+ /** Map of tool name → host executor. */
28
+ export type ClientToolMap = Record<string, ClientToolExecutor>;
9
29
  export interface AgentClient {
10
30
  /** Stream one logical run for `prompt` under `sessionId`, survivor-stitched. */
11
31
  send(prompt: string, sessionId: string): AsyncIterable<SDKMessage>;
@@ -13,10 +33,13 @@ export interface AgentClient {
13
33
  /**
14
34
  * Build an AgentClient from a `run` function. `run` does ONE underlying run —
15
35
  * e.g. wrapping the SDK's `query()` in-process, or a fetch to a serverless
16
- * endpoint. The stitching across `paused` boundaries is handled here.
36
+ * endpoint. This stitches across `paused` boundaries (survivor) AND executes
37
+ * client-side tools: on a `client_tool_request`, it runs the matching
38
+ * `clientTools` executor and feeds the result into the continuation.
17
39
  */
18
- export declare function createAgentClient({ run }: {
40
+ export declare function createAgentClient({ run, clientTools }: {
19
41
  run: RunFn;
42
+ clientTools?: ClientToolMap;
20
43
  }): AgentClient;
21
44
  export interface EndpointClientOptions {
22
45
  /** URL of a serverless function that streams NDJSON SDKMessages. */
@@ -24,6 +47,8 @@ export interface EndpointClientOptions {
24
47
  headers?: Record<string, string>;
25
48
  /** Extra fields merged into the POST body (e.g. model, auth context). */
26
49
  body?: Record<string, unknown>;
50
+ /** Host executors for client-side tools (e.g. run `bash` on a WebContainer). */
51
+ clientTools?: ClientToolMap;
27
52
  }
28
53
  /**
29
54
  * AgentClient backed by a serverless function. POSTs `{ prompt, sessionId,
package/dist/client.js CHANGED
@@ -1,25 +1,58 @@
1
1
  function isPaused(m) {
2
2
  return m.type === 'system' && m.subtype === 'paused';
3
3
  }
4
+ function clientToolReq(m) {
5
+ if (m.type === 'system' && m.subtype === 'client_tool_request') {
6
+ return m.request ?? null;
7
+ }
8
+ return null;
9
+ }
4
10
  /**
5
11
  * Build an AgentClient from a `run` function. `run` does ONE underlying run —
6
12
  * e.g. wrapping the SDK's `query()` in-process, or a fetch to a serverless
7
- * endpoint. The stitching across `paused` boundaries is handled here.
13
+ * endpoint. This stitches across `paused` boundaries (survivor) AND executes
14
+ * client-side tools: on a `client_tool_request`, it runs the matching
15
+ * `clientTools` executor and feeds the result into the continuation.
8
16
  */
9
- export function createAgentClient({ run }) {
17
+ export function createAgentClient({ run, clientTools }) {
10
18
  return {
11
19
  async *send(prompt, sessionId) {
12
20
  let continueRun = false;
21
+ let pending;
13
22
  for (;;) {
14
23
  let paused = false;
15
- for await (const m of run({ prompt: continueRun ? '' : prompt, sessionId, continueRun })) {
24
+ const requests = [];
25
+ for await (const m of run({ prompt: continueRun ? '' : prompt, sessionId, continueRun, clientToolResults: pending })) {
26
+ const req = clientToolReq(m);
27
+ if (req)
28
+ requests.push(req);
16
29
  if (isPaused(m))
17
30
  paused = true;
18
- yield m; // forward everything (incl. the paused boundary) consumers may show "paused"
31
+ yield m; // forward everything (incl. paused + client_tool_request) so the UI can react
19
32
  }
20
33
  if (!paused)
21
34
  break;
22
- continueRun = true; // next iteration resumes + continues the same session
35
+ continueRun = true;
36
+ // Execute any client-side tool requests, carry the results into the next run.
37
+ pending = undefined;
38
+ if (requests.length && clientTools) {
39
+ pending = [];
40
+ for (const req of requests) {
41
+ const exec = clientTools[req.name];
42
+ try {
43
+ if (!exec) {
44
+ pending.push({ tool_use_id: req.tool_use_id, content: `No client executor for tool "${req.name}".`, is_error: true });
45
+ }
46
+ else {
47
+ const r = await exec(req.input, { tool_use_id: req.tool_use_id, name: req.name });
48
+ pending.push({ tool_use_id: req.tool_use_id, content: r.content, is_error: r.is_error });
49
+ }
50
+ }
51
+ catch (e) {
52
+ pending.push({ tool_use_id: req.tool_use_id, content: `Error: ${e instanceof Error ? e.message : String(e)}`, is_error: true });
53
+ }
54
+ }
55
+ }
23
56
  }
24
57
  },
25
58
  };
@@ -30,11 +63,11 @@ export function createAgentClient({ run }) {
30
63
  * SDKMessages. Survivor-stitched automatically.
31
64
  */
32
65
  export function createEndpointClient(opts) {
33
- const run = async function* ({ prompt, sessionId, continueRun }) {
66
+ const run = async function* ({ prompt, sessionId, continueRun, clientToolResults }) {
34
67
  const res = await fetch(opts.endpoint, {
35
68
  method: 'POST',
36
69
  headers: { 'content-type': 'application/json', ...opts.headers },
37
- body: JSON.stringify({ prompt, sessionId, continueRun, ...opts.body }),
70
+ body: JSON.stringify({ prompt, sessionId, continueRun, clientToolResults, ...opts.body }),
38
71
  });
39
72
  if (!res.body)
40
73
  return;
@@ -70,5 +103,5 @@ export function createEndpointClient(opts) {
70
103
  }
71
104
  }
72
105
  };
73
- return createAgentClient({ run });
106
+ return createAgentClient({ run, clientTools: opts.clientTools });
74
107
  }
@@ -0,0 +1,21 @@
1
+ export interface AskUserQuestion {
2
+ question: string;
3
+ header?: string;
4
+ options: Array<{
5
+ label: string;
6
+ description?: string;
7
+ }>;
8
+ multiSelect?: boolean;
9
+ }
10
+ export interface AskUserProps {
11
+ question: AskUserQuestion;
12
+ /** Called with the chosen label (single) or labels (multiSelect). */
13
+ onAnswer: (answer: string | string[]) => void;
14
+ className?: string;
15
+ }
16
+ /**
17
+ * Renders an `ask_user_question` prompt as option buttons and resolves the
18
+ * answer. Wire it to the SDK's `onAskUser` handler: store the question + a
19
+ * resolver in state, render <AskUser>, and call the resolver from onAnswer.
20
+ */
21
+ export declare function AskUser({ question, onAnswer, className }: AskUserProps): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ /**
4
+ * Renders an `ask_user_question` prompt as option buttons and resolves the
5
+ * answer. Wire it to the SDK's `onAskUser` handler: store the question + a
6
+ * resolver in state, render <AskUser>, and call the resolver from onAnswer.
7
+ */
8
+ export function AskUser({ question, onAnswer, className }) {
9
+ const [selected, setSelected] = useState([]);
10
+ const multi = !!question.multiSelect;
11
+ const toggle = (label) => setSelected((s) => (s.includes(label) ? s.filter((l) => l !== label) : [...s, label]));
12
+ return (_jsxs("div", { className: `ac-askuser${className ? ' ' + className : ''}`, children: [question.header && _jsx("span", { className: "ac-askuser-header", children: question.header }), _jsx("div", { className: "ac-askuser-question", children: question.question }), _jsx("div", { className: "ac-askuser-options", children: question.options.map((o) => {
13
+ const isSel = selected.includes(o.label);
14
+ return (_jsxs("button", { className: 'ac-askuser-option' + (isSel ? ' ac-selected' : ''), onClick: () => (multi ? toggle(o.label) : onAnswer(o.label)), children: [_jsx("span", { className: "ac-askuser-label", children: o.label }), o.description && _jsx("span", { className: "ac-askuser-desc", children: o.description })] }, o.label));
15
+ }) }), multi && (_jsxs("button", { className: "ac-askuser-submit", disabled: !selected.length, onClick: () => onAnswer(selected), children: ["Submit", selected.length ? ` (${selected.length})` : ''] }))] }));
16
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type UseAgentOptions } from '../useAgent.js';
3
+ export interface ChatPanelProps extends UseAgentOptions {
4
+ className?: string;
5
+ /** Panel title shown in the header. */
6
+ title?: ReactNode;
7
+ placeholder?: string;
8
+ workingLabel?: string;
9
+ renderMarkdown?: (text: string) => ReactNode;
10
+ /** Show the tokens · cost · status line in the header. Default true. */
11
+ showStats?: boolean;
12
+ }
13
+ /**
14
+ * A polished chat panel: header (title + live status/tokens/cost) + Transcript +
15
+ * Working + Composer, wired to useAgent. Like <AgentChat> but with a header bar.
16
+ */
17
+ export declare function ChatPanel({ className, title, placeholder, workingLabel, renderMarkdown, showStats, ...agentOpts }: ChatPanelProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useAgent } from '../useAgent.js';
3
+ import { Transcript } from './Transcript.js';
4
+ import { Composer } from './Composer.js';
5
+ import { Working } from './Working.js';
6
+ /**
7
+ * A polished chat panel: header (title + live status/tokens/cost) + Transcript +
8
+ * Working + Composer, wired to useAgent. Like <AgentChat> but with a header bar.
9
+ */
10
+ export function ChatPanel({ className, title = 'Agent', placeholder, workingLabel, renderMarkdown, showStats = true, ...agentOpts }) {
11
+ const { messages, streamingText, status, tokens, cost, send } = useAgent(agentOpts);
12
+ const running = status !== 'idle';
13
+ return (_jsxs("div", { className: `ac-chatpanel${className ? ' ' + className : ''}`, children: [_jsxs("div", { className: "ac-chatpanel-head", children: [_jsx("span", { className: "ac-chatpanel-title", children: title }), showStats && (_jsxs("span", { className: "ac-chatpanel-stats", children: [_jsx("span", { className: 'ac-status ac-status-' + status, children: status }), tokens ? _jsxs("span", { className: "ac-stat", children: [" \u00B7 ", tokens.toLocaleString(), " tok"] }) : null, cost ? _jsxs("span", { className: "ac-stat", children: [" \u00B7 $", cost.toFixed(4)] }) : null] }))] }), _jsx(Transcript, { messages: messages, streamingText: streamingText, renderMarkdown: renderMarkdown }), _jsx(Working, { active: running, label: workingLabel, paused: status === 'paused' }), _jsx(Composer, { onSend: send, placeholder: placeholder, disabled: running })] }));
14
+ }
@@ -0,0 +1,17 @@
1
+ import { type Extension } from '@codemirror/state';
2
+ export interface CodeEditorProps {
3
+ /** The document text (controlled). */
4
+ value: string;
5
+ onChange?: (value: string) => void;
6
+ /** Language for highlighting. Currently 'javascript'/'typescript' supported out of the box. */
7
+ language?: string;
8
+ readOnly?: boolean;
9
+ className?: string;
10
+ /** Extra CodeMirror extensions to append. */
11
+ extensions?: Extension[];
12
+ }
13
+ /**
14
+ * A CodeMirror 6 editor. Controlled via `value`/`onChange`. Requires the optional
15
+ * peer deps `codemirror` + `@codemirror/*`.
16
+ */
17
+ export declare function CodeEditor({ value, onChange, language, readOnly, className, extensions }: CodeEditorProps): import("react").JSX.Element;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { EditorView, keymap, lineNumbers, highlightActiveLine } from '@codemirror/view';
4
+ import { EditorState } from '@codemirror/state';
5
+ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
6
+ import { javascript } from '@codemirror/lang-javascript';
7
+ /**
8
+ * A CodeMirror 6 editor. Controlled via `value`/`onChange`. Requires the optional
9
+ * peer deps `codemirror` + `@codemirror/*`.
10
+ */
11
+ export function CodeEditor({ value, onChange, language = 'typescript', readOnly = false, className, extensions = [] }) {
12
+ const hostRef = useRef(null);
13
+ const viewRef = useRef(null);
14
+ useEffect(() => {
15
+ if (!hostRef.current)
16
+ return;
17
+ const langExt = language === 'javascript' ? javascript({ jsx: true }) : javascript({ typescript: true, jsx: true });
18
+ const view = new EditorView({
19
+ parent: hostRef.current,
20
+ state: EditorState.create({
21
+ doc: value,
22
+ extensions: [
23
+ lineNumbers(),
24
+ highlightActiveLine(),
25
+ history(),
26
+ langExt,
27
+ keymap.of([...defaultKeymap, ...historyKeymap]),
28
+ EditorView.editable.of(!readOnly),
29
+ EditorView.updateListener.of((u) => {
30
+ if (u.docChanged && onChange)
31
+ onChange(u.state.doc.toString());
32
+ }),
33
+ EditorView.theme({ '&': { height: '100%', fontSize: '13px' }, '.cm-scroller': { fontFamily: 'ui-monospace, monospace' } }),
34
+ ...extensions,
35
+ ],
36
+ }),
37
+ });
38
+ viewRef.current = view;
39
+ return () => {
40
+ view.destroy();
41
+ viewRef.current = null;
42
+ };
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, [language, readOnly]);
45
+ // Sync external value changes into the editor (without clobbering local edits).
46
+ useEffect(() => {
47
+ const view = viewRef.current;
48
+ if (!view)
49
+ return;
50
+ if (value !== view.state.doc.toString()) {
51
+ view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } });
52
+ }
53
+ }, [value]);
54
+ return _jsx("div", { className: `ac-editor-host${className ? ' ' + className : ''}`, ref: hostRef });
55
+ }
@@ -0,0 +1,21 @@
1
+ export interface FileEntry {
2
+ name: string;
3
+ isDir: boolean;
4
+ }
5
+ export interface FileExplorerProps {
6
+ /** List a directory's immediate children. Adapt any FS (WebContainer, SDK FileSystem, …). */
7
+ list: (dir: string) => Promise<FileEntry[]>;
8
+ /** Root directory to show. Default '/'. */
9
+ root?: string;
10
+ /** Currently-open file path (highlighted). */
11
+ openPath?: string | null;
12
+ onOpen: (path: string) => void;
13
+ /** Bump to force a re-read (e.g. after the agent writes files). */
14
+ refreshKey?: number;
15
+ className?: string;
16
+ title?: string;
17
+ /** Directory names to skip. Default: node_modules, .git, .bcs. */
18
+ ignore?: string[];
19
+ }
20
+ /** A collapsible file tree over any filesystem (provide a `list(dir)` adapter). */
21
+ export declare function FileExplorer({ list, root, openPath, onOpen, refreshKey, className, title, ignore }: FileExplorerProps): import("react").JSX.Element;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ /** A collapsible file tree over any filesystem (provide a `list(dir)` adapter). */
4
+ export function FileExplorer({ list, root = '/', openPath, onOpen, refreshKey = 0, className, title = 'Files', ignore }) {
5
+ const [nodes, setNodes] = useState([]);
6
+ const ignoreSet = new Set(ignore ?? ['node_modules', '.git', '.bcs']);
7
+ useEffect(() => {
8
+ let cancelled = false;
9
+ list(root)
10
+ .then((entries) => {
11
+ if (cancelled)
12
+ return;
13
+ const mapped = entries
14
+ .filter((e) => !ignoreSet.has(e.name))
15
+ .map((e) => ({ ...e, path: root === '/' ? `/${e.name}` : `${root}/${e.name}` }))
16
+ .sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1));
17
+ setNodes(mapped);
18
+ })
19
+ .catch(() => setNodes([]));
20
+ return () => {
21
+ cancelled = true;
22
+ };
23
+ // eslint-disable-next-line react-hooks/exhaustive-deps
24
+ }, [root, refreshKey]);
25
+ return (_jsxs("div", { className: `ac-explorer${className ? ' ' + className : ''}`, children: [_jsx("div", { className: "ac-explorer-head", children: title }), _jsx("div", { className: "ac-tree", children: nodes.map((n) => (_jsx(TreeNode, { node: n, depth: 0, list: list, openPath: openPath ?? null, onOpen: onOpen, ignore: ignoreSet, refreshKey: refreshKey }, n.path))) })] }));
26
+ }
27
+ function TreeNode({ node, depth, list, openPath, onOpen, ignore, refreshKey }) {
28
+ const [open, setOpen] = useState(depth < 1);
29
+ const [children, setChildren] = useState(null);
30
+ const pad = { paddingLeft: 6 + depth * 12 };
31
+ useEffect(() => {
32
+ if (!node.isDir || !open)
33
+ return;
34
+ let cancelled = false;
35
+ list(node.path)
36
+ .then((entries) => {
37
+ if (cancelled)
38
+ return;
39
+ setChildren(entries
40
+ .filter((e) => !ignore.has(e.name))
41
+ .map((e) => ({ ...e, path: `${node.path}/${e.name}` }))
42
+ .sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1)));
43
+ })
44
+ .catch(() => setChildren([]));
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, [open, node.path, refreshKey]);
50
+ if (node.isDir) {
51
+ return (_jsxs("div", { children: [_jsxs("div", { className: "ac-tree-row ac-dir", style: pad, onClick: () => setOpen((o) => !o), children: [_jsx("span", { className: "ac-tree-caret", children: open ? '▾' : '▸' }), " ", node.name] }), open && children?.map((c) => _jsx(TreeNode, { node: c, depth: depth + 1, list: list, openPath: openPath, onOpen: onOpen, ignore: ignore, refreshKey: refreshKey }, c.path))] }));
52
+ }
53
+ return (_jsx("div", { className: 'ac-tree-row ac-file' + (openPath === node.path ? ' ac-active' : ''), style: pad, onClick: () => onOpen(node.path), children: node.name }));
54
+ }
@@ -10,8 +10,8 @@ export interface MarkdownMessageProps {
10
10
  text: string;
11
11
  role?: 'user' | 'assistant';
12
12
  className?: string;
13
- /** Override the markdown renderer (default: built-in safe renderer). */
13
+ /** Override the markdown renderer (default: streamdown, streaming-aware). */
14
14
  render?: (text: string) => ReactNode;
15
15
  }
16
- /** An assistant bubble whose text is rendered as markdown. */
16
+ /** An assistant bubble whose text is rendered as markdown via streamdown. */
17
17
  export declare function MarkdownMessage({ text, role, className, render }: MarkdownMessageProps): import("react").JSX.Element;
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { renderMarkdown } from '../markdown.js';
2
+ import { Streamdown } from 'streamdown';
3
3
  /** A plain chat bubble. */
4
4
  export function Message({ role, children, className }) {
5
5
  return (_jsxs("div", { className: `ac-msg ac-msg-${role}${className ? ' ' + className : ''}`, "data-role": role, children: [role === 'user' && _jsx("span", { className: "ac-msg-prefix", "aria-hidden": true, children: "\u203A" }), _jsx("div", { className: "ac-msg-body", children: children })] }));
6
6
  }
7
- /** An assistant bubble whose text is rendered as markdown. */
7
+ /** An assistant bubble whose text is rendered as markdown via streamdown. */
8
8
  export function MarkdownMessage({ text, role = 'assistant', className, render }) {
9
- return (_jsx(Message, { role: role, className: `ac-msg-md${className ? ' ' + className : ''}`, children: (render ?? renderMarkdown)(text) }));
9
+ return (_jsx(Message, { role: role, className: `ac-msg-md${className ? ' ' + className : ''}`, children: render ? render(text) : _jsx(Streamdown, { children: text }) }));
10
10
  }
@@ -0,0 +1,28 @@
1
+ import '@xterm/xterm/css/xterm.css';
2
+ /** A live shell process (structurally compatible with a WebContainer process). */
3
+ export interface ShellProcess {
4
+ output: ReadableStream<string>;
5
+ input: WritableStream<string>;
6
+ resize?(size: {
7
+ cols: number;
8
+ rows: number;
9
+ }): void;
10
+ kill?(): void;
11
+ }
12
+ export interface TerminalProps {
13
+ /** Spawn a shell process for the given terminal size (e.g. wc.spawn('jsh', {terminal})). */
14
+ spawn: (size: {
15
+ cols: number;
16
+ rows: number;
17
+ }) => Promise<ShellProcess>;
18
+ className?: string;
19
+ /** Header label; pass null to hide the header. */
20
+ title?: string | null;
21
+ fontSize?: number;
22
+ }
23
+ /**
24
+ * An interactive xterm.js terminal bound to a streaming shell process. Backend-
25
+ * agnostic — pass any `spawn` that returns a {output, input, resize?, kill?}.
26
+ * Requires the optional peer deps `@xterm/xterm` + `@xterm/addon-fit`.
27
+ */
28
+ export declare function Terminal({ spawn, className, title, fontSize }: TerminalProps): import("react").JSX.Element;
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { Terminal as XTerm } from '@xterm/xterm';
4
+ import { FitAddon } from '@xterm/addon-fit';
5
+ import '@xterm/xterm/css/xterm.css';
6
+ /**
7
+ * An interactive xterm.js terminal bound to a streaming shell process. Backend-
8
+ * agnostic — pass any `spawn` that returns a {output, input, resize?, kill?}.
9
+ * Requires the optional peer deps `@xterm/xterm` + `@xterm/addon-fit`.
10
+ */
11
+ export function Terminal({ spawn, className, title = 'Terminal', fontSize = 13 }) {
12
+ const hostRef = useRef(null);
13
+ useEffect(() => {
14
+ if (!hostRef.current)
15
+ return;
16
+ let disposed = false;
17
+ let shell = null;
18
+ let writer = null;
19
+ const term = new XTerm({
20
+ convertEol: true,
21
+ cursorBlink: true,
22
+ fontSize,
23
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
24
+ theme: { background: '#0b0e14', foreground: '#cbd5e1' },
25
+ });
26
+ const fit = new FitAddon();
27
+ term.loadAddon(fit);
28
+ term.open(hostRef.current);
29
+ const safeFit = () => {
30
+ const el = hostRef.current;
31
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0)
32
+ return;
33
+ try {
34
+ fit.fit();
35
+ shell?.resize?.({ cols: term.cols, rows: term.rows });
36
+ }
37
+ catch {
38
+ /* not ready */
39
+ }
40
+ };
41
+ const raf = requestAnimationFrame(safeFit);
42
+ void (async () => {
43
+ shell = await spawn({ cols: term.cols, rows: term.rows });
44
+ if (disposed) {
45
+ shell.kill?.();
46
+ return;
47
+ }
48
+ shell.output
49
+ .pipeTo(new WritableStream({ write: (chunk) => term.write(chunk) }))
50
+ .catch(() => { });
51
+ writer = shell.input.getWriter();
52
+ term.onData((d) => void writer?.write(d));
53
+ })();
54
+ const ro = new ResizeObserver(() => safeFit());
55
+ ro.observe(hostRef.current);
56
+ return () => {
57
+ disposed = true;
58
+ cancelAnimationFrame(raf);
59
+ ro.disconnect();
60
+ try {
61
+ writer?.releaseLock();
62
+ }
63
+ catch {
64
+ /* ignore */
65
+ }
66
+ shell?.kill?.();
67
+ term.dispose();
68
+ };
69
+ }, [spawn, fontSize]);
70
+ return (_jsxs("div", { className: `ac-terminal${className ? ' ' + className : ''}`, children: [title !== null && _jsx("div", { className: "ac-terminal-head", children: title }), _jsx("div", { className: "ac-terminal-host", ref: hostRef })] }));
71
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createAgentClient, createEndpointClient } from './client.js';
2
- export type { AgentClient, RunFn, RunOptions, EndpointClientOptions } from './client.js';
2
+ export type { AgentClient, RunFn, RunOptions, EndpointClientOptions, ClientToolMap, ClientToolExecutor, ClientToolResult, } from './client.js';
3
3
  export { useAgent } from './useAgent.js';
4
4
  export type { UseAgentOptions, UseAgentResult, AgentStatus } from './useAgent.js';
5
5
  export { renderMarkdown } from './markdown.js';
@@ -15,3 +15,13 @@ export { Transcript } from './components/Transcript.js';
15
15
  export type { TranscriptProps } from './components/Transcript.js';
16
16
  export { AgentChat } from './components/AgentChat.js';
17
17
  export type { AgentChatProps } from './components/AgentChat.js';
18
+ export { ChatPanel } from './components/ChatPanel.js';
19
+ export type { ChatPanelProps } from './components/ChatPanel.js';
20
+ export { Terminal } from './components/Terminal.js';
21
+ export type { TerminalProps, ShellProcess } from './components/Terminal.js';
22
+ export { FileExplorer } from './components/FileExplorer.js';
23
+ export type { FileExplorerProps, FileEntry } from './components/FileExplorer.js';
24
+ export { CodeEditor } from './components/CodeEditor.js';
25
+ export type { CodeEditorProps } from './components/CodeEditor.js';
26
+ export { AskUser } from './components/AskUser.js';
27
+ export type { AskUserProps, AskUserQuestion } from './components/AskUser.js';
package/dist/index.js CHANGED
@@ -8,3 +8,9 @@ export { Composer } from './components/Composer.js';
8
8
  export { Working } from './components/Working.js';
9
9
  export { Transcript } from './components/Transcript.js';
10
10
  export { AgentChat } from './components/AgentChat.js';
11
+ export { ChatPanel } from './components/ChatPanel.js';
12
+ // IDE-grade components (optional peer deps: @xterm/*, @codemirror/*)
13
+ export { Terminal } from './components/Terminal.js';
14
+ export { FileExplorer } from './components/FileExplorer.js';
15
+ export { CodeEditor } from './components/CodeEditor.js';
16
+ export { AskUser } from './components/AskUser.js';
@@ -1,5 +1,5 @@
1
1
  import type { SDKMessage } from 'anyclaude-sdk';
2
- import { type AgentClient, type RunFn } from './client.js';
2
+ import { type AgentClient, type RunFn, type ClientToolMap } from './client.js';
3
3
  export type AgentStatus = 'idle' | 'running' | 'paused';
4
4
  export interface UseAgentOptions {
5
5
  /** Provide ONE of these. */
@@ -7,6 +7,8 @@ export interface UseAgentOptions {
7
7
  run?: RunFn;
8
8
  endpoint?: string;
9
9
  headers?: Record<string, string>;
10
+ /** Host executors for client-side tools (e.g. run `bash` on a WebContainer). */
11
+ clientTools?: ClientToolMap;
10
12
  /** Stable id for this conversation (survivor continuation reuses it). Auto if omitted. */
11
13
  sessionId?: string;
12
14
  }
package/dist/useAgent.js CHANGED
@@ -10,9 +10,9 @@ function resolveClient(opts) {
10
10
  if (opts.client)
11
11
  return opts.client;
12
12
  if (opts.run)
13
- return createAgentClient({ run: opts.run });
13
+ return createAgentClient({ run: opts.run, clientTools: opts.clientTools });
14
14
  if (opts.endpoint)
15
- return createEndpointClient({ endpoint: opts.endpoint, headers: opts.headers });
15
+ return createEndpointClient({ endpoint: opts.endpoint, headers: opts.headers, clientTools: opts.clientTools });
16
16
  throw new Error('useAgent: provide one of `client`, `run`, or `endpoint`.');
17
17
  }
18
18
  export function useAgent(opts) {
@@ -26,7 +26,7 @@ export function useAgent(opts) {
26
26
  const runningRef = useRef(false);
27
27
  const client = useMemo(() => resolveClient(opts),
28
28
  // eslint-disable-next-line react-hooks/exhaustive-deps
29
- [opts.client, opts.run, opts.endpoint, opts.headers]);
29
+ [opts.client, opts.run, opts.endpoint, opts.headers, opts.clientTools]);
30
30
  const send = useCallback((text) => {
31
31
  const t = text.trim();
32
32
  if (!t || runningRef.current)
package/package.json CHANGED
@@ -1,22 +1,100 @@
1
1
  {
2
2
  "name": "anyclaude-react",
3
- "version": "0.1.0",
4
- "description": "React UI kit for anyclaude-sdk — restylable hooks + components (useAgent, Transcript, Composer, AgentChat) with built-in serverless 'survivor' stream-stitching. Build chatbots, agents, research assistants.",
3
+ "version": "0.2.0",
4
+ "description": "React UI kit for anyclaude-sdk — restylable hooks + components (useAgent, Transcript, Composer, AgentChat, Terminal, FileExplorer, CodeEditor, ChatPanel, AskUser) with built-in serverless 'survivor' stream-stitching. Build chatbots, agents, research assistants, browser IDEs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
- ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
11
14
  "./styles.css": "./styles.css"
12
15
  },
13
- "files": ["dist", "styles.css"],
14
- "sideEffects": ["*.css"],
15
- "scripts": { "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit", "prepublishOnly": "npm run build" },
16
- "keywords": ["anyclaude", "claude", "agent", "react", "ai-chat", "chatbot", "ui", "hooks"],
16
+ "files": [
17
+ "dist",
18
+ "styles.css"
19
+ ],
20
+ "sideEffects": [
21
+ "*.css",
22
+ "@xterm/xterm/css/xterm.css"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "anyclaude",
31
+ "claude",
32
+ "agent",
33
+ "react",
34
+ "ai-chat",
35
+ "chatbot",
36
+ "ui",
37
+ "hooks",
38
+ "terminal",
39
+ "ide",
40
+ "codemirror",
41
+ "xterm"
42
+ ],
17
43
  "license": "MIT",
18
44
  "author": "Hans Ade <anye.happiness@swisslinkedu.com>",
19
- "repository": { "type": "git", "url": "git+https://github.com/pipilot-dev/anyclaude-sdk.git", "directory": "anyclaude-react" },
20
- "peerDependencies": { "react": ">=18", "anyclaude-sdk": ">=0.1.0" },
21
- "devDependencies": { "@types/react": "^18.3.0", "react": "^18.3.1", "typescript": "^5.4.0" }
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/pipilot-dev/anyclaude-sdk.git",
48
+ "directory": "anyclaude-react"
49
+ },
50
+ "peerDependencies": {
51
+ "@codemirror/commands": ">=6.0.0",
52
+ "@codemirror/lang-javascript": ">=6.0.0",
53
+ "@codemirror/state": ">=6.0.0",
54
+ "@codemirror/view": ">=6.0.0",
55
+ "@xterm/addon-fit": ">=0.10.0",
56
+ "@xterm/xterm": ">=5.3.0",
57
+ "anyclaude-sdk": ">=0.1.0",
58
+ "codemirror": ">=6.0.0",
59
+ "react": ">=18"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@xterm/xterm": {
63
+ "optional": true
64
+ },
65
+ "@xterm/addon-fit": {
66
+ "optional": true
67
+ },
68
+ "codemirror": {
69
+ "optional": true
70
+ },
71
+ "@codemirror/view": {
72
+ "optional": true
73
+ },
74
+ "@codemirror/state": {
75
+ "optional": true
76
+ },
77
+ "@codemirror/commands": {
78
+ "optional": true
79
+ },
80
+ "@codemirror/lang-javascript": {
81
+ "optional": true
82
+ }
83
+ },
84
+ "devDependencies": {
85
+ "@codemirror/commands": "^6.7.1",
86
+ "@codemirror/lang-javascript": "^6.2.2",
87
+ "@codemirror/state": "^6.4.1",
88
+ "@codemirror/view": "^6.34.0",
89
+ "@types/react": "^18.3.0",
90
+ "@types/react-dom": "^18.3.0",
91
+ "@xterm/addon-fit": "^0.10.0",
92
+ "@xterm/xterm": "^5.5.0",
93
+ "react": "^18.3.1",
94
+ "react-dom": "^18.3.1",
95
+ "typescript": "^5.4.0"
96
+ },
97
+ "dependencies": {
98
+ "streamdown": "^2.5.0"
99
+ }
22
100
  }
package/styles.css CHANGED
@@ -58,3 +58,49 @@
58
58
  .ac-composer-input:focus { outline: none; border-color: var(--ac-accent); }
59
59
  .ac-composer-send { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 10px; background: var(--ac-accent); color: #00121a; border: 0; cursor: pointer; flex: none; }
60
60
  .ac-composer-send:disabled { opacity: 0.4; cursor: default; }
61
+
62
+ /* ---- shared vars (so IDE components work outside .ac-chat) ---- */
63
+ :root {
64
+ --ac-bg: #0d1117; --ac-fg: #e6edf3; --ac-muted: #8b949e; --ac-border: #232c3d;
65
+ --ac-accent: #4dd0e1; --ac-user: #5fd75f; --ac-panel: #161b22; --ac-radius: 12px;
66
+ --ac-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
67
+ }
68
+
69
+ /* ---- ChatPanel ---- */
70
+ .ac-chatpanel { display: flex; flex-direction: column; height: 100%; background: var(--ac-bg); color: var(--ac-fg); font: 15px/1.55 system-ui, sans-serif; }
71
+ .ac-chatpanel-head { display: flex; align-items: center; justify-content: space-between; padding: 9px 14px; border-bottom: 1px solid var(--ac-border); font-size: 14px; }
72
+ .ac-chatpanel-title { font-weight: 600; }
73
+ .ac-chatpanel-stats { color: var(--ac-muted); font-family: var(--ac-mono); font-size: 12.5px; }
74
+ .ac-status { text-transform: capitalize; }
75
+ .ac-status-running { color: var(--ac-accent); }
76
+ .ac-status-paused { color: #ffd75f; }
77
+
78
+ /* ---- Terminal ---- */
79
+ .ac-terminal { display: flex; flex-direction: column; height: 100%; background: #0b0e14; border: 1px solid var(--ac-border); border-radius: var(--ac-radius); overflow: hidden; }
80
+ .ac-terminal-head { padding: 6px 12px; font-family: var(--ac-mono); font-size: 12px; color: var(--ac-muted); border-bottom: 1px solid var(--ac-border); }
81
+ .ac-terminal-host { flex: 1; min-height: 0; padding: 6px; }
82
+
83
+ /* ---- FileExplorer ---- */
84
+ .ac-explorer { background: var(--ac-panel); border: 1px solid var(--ac-border); border-radius: var(--ac-radius); overflow: auto; font-size: 13px; color: var(--ac-fg); }
85
+ .ac-explorer-head { padding: 8px 12px; font-weight: 600; color: var(--ac-muted); border-bottom: 1px solid var(--ac-border); }
86
+ .ac-tree-row { padding: 4px 8px; cursor: pointer; white-space: nowrap; font-family: var(--ac-mono); }
87
+ .ac-tree-row:hover { background: rgba(255,255,255,0.05); }
88
+ .ac-tree-row.ac-active { background: rgba(77,208,225,0.16); color: var(--ac-accent); }
89
+ .ac-tree-caret { opacity: 0.7; }
90
+ .ac-dir { color: var(--ac-fg); }
91
+
92
+ /* ---- CodeEditor ---- */
93
+ .ac-editor-host { height: 100%; overflow: hidden; border: 1px solid var(--ac-border); border-radius: var(--ac-radius); }
94
+ .ac-editor-host .cm-editor { height: 100%; }
95
+
96
+ /* ---- AskUser ---- */
97
+ .ac-askuser { border: 1px solid var(--ac-accent); border-radius: var(--ac-radius); background: var(--ac-panel); padding: 12px 14px; margin: 8px 0; }
98
+ .ac-askuser-header { display: inline-block; font-family: var(--ac-mono); font-size: 11px; text-transform: uppercase; color: var(--ac-accent); margin-bottom: 4px; }
99
+ .ac-askuser-question { font-weight: 600; margin-bottom: 10px; }
100
+ .ac-askuser-options { display: flex; flex-direction: column; gap: 6px; }
101
+ .ac-askuser-option { text-align: left; background: var(--ac-bg); border: 1px solid var(--ac-border); border-radius: 8px; padding: 8px 10px; color: var(--ac-fg); cursor: pointer; display: flex; flex-direction: column; gap: 2px; }
102
+ .ac-askuser-option:hover, .ac-askuser-option.ac-selected { border-color: var(--ac-accent); }
103
+ .ac-askuser-label { font-weight: 600; }
104
+ .ac-askuser-desc { color: var(--ac-muted); font-size: 13px; }
105
+ .ac-askuser-submit { margin-top: 10px; background: var(--ac-accent); color: #001016; border: none; border-radius: 8px; padding: 8px 14px; font-weight: 600; cursor: pointer; }
106
+ .ac-askuser-submit:disabled { opacity: 0.5; cursor: default; }