anyclaude-react 0.1.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/README.md +70 -0
- package/dist/client.d.ts +33 -0
- package/dist/client.js +74 -0
- package/dist/components/AgentChat.d.ts +13 -0
- package/dist/components/AgentChat.js +11 -0
- package/dist/components/Composer.d.ts +10 -0
- package/dist/components/Composer.js +20 -0
- package/dist/components/Message.d.ts +17 -0
- package/dist/components/Message.js +10 -0
- package/dist/components/ToolCall.d.ts +14 -0
- package/dist/components/ToolCall.js +18 -0
- package/dist/components/Transcript.d.ts +12 -0
- package/dist/components/Transcript.js +58 -0
- package/dist/components/Working.d.ts +10 -0
- package/dist/components/Working.js +8 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/markdown.d.ts +3 -0
- package/dist/markdown.js +108 -0
- package/dist/useAgent.d.ts +24 -0
- package/dist/useAgent.js +117 -0
- package/package.json +22 -0
- package/styles.css +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# anyclaude-react
|
|
2
|
+
|
|
3
|
+
Restylable React UI kit for [`anyclaude-sdk`](https://www.npmjs.com/package/anyclaude-sdk) — hooks + components to build chatbots, AI agents, research assistants, and more. Includes built-in **serverless "survivor" stream-stitching** so long agent runs span function time-limits transparently.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install anyclaude-react anyclaude-sdk react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start (browser / in-process)
|
|
10
|
+
|
|
11
|
+
Drive the agent in-process by wrapping the SDK's `query()` in a `run` function:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { AgentChat } from 'anyclaude-react'
|
|
15
|
+
import 'anyclaude-react/styles.css'
|
|
16
|
+
import { query, createOpenAIClient, MemoryFileSystem, NoopCommandExecutor, composeWorkspace } from 'anyclaude-sdk'
|
|
17
|
+
|
|
18
|
+
const ws = composeWorkspace(new MemoryFileSystem(), new NoopCommandExecutor(), '/work')
|
|
19
|
+
const llm = createOpenAIClient({ baseUrl: '…', model: 'gpt-4o', apiKey: KEY })
|
|
20
|
+
|
|
21
|
+
export default function App() {
|
|
22
|
+
return (
|
|
23
|
+
<AgentChat
|
|
24
|
+
run={({ prompt, sessionId, continueRun }) =>
|
|
25
|
+
query({ prompt: continueRun ? '' : prompt, workspace: ws, llm, model: 'gpt-4o',
|
|
26
|
+
sessionId, resume: continueRun, continueRun, includePartialMessages: true })}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Serverless (survivor)
|
|
33
|
+
|
|
34
|
+
Point at a function that streams NDJSON `SDKMessage`s. When the function pauses
|
|
35
|
+
at its time-limit (`{type:'system',subtype:'paused'}`), the client auto-continues
|
|
36
|
+
in a new request with the same `sessionId` — invisibly:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<AgentChat endpoint="/api/agent" />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Your function runs `query({ ..., maxDurationMs, sessionStore, sessionId, resume: continueRun, continueRun })` and writes each message as a JSON line.
|
|
43
|
+
|
|
44
|
+
## Hook
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
const { messages, streamingText, status, tokens, cost, send, interrupt, clear } =
|
|
48
|
+
useAgent({ run /* | endpoint | client */, sessionId })
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `status`: `'idle' | 'running' | 'paused'`
|
|
52
|
+
- `send(text)` starts/continues; `interrupt()` aborts; `clear()` resets (new session).
|
|
53
|
+
|
|
54
|
+
## Components
|
|
55
|
+
|
|
56
|
+
| Component | Purpose |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `<AgentChat>` | All-in-one: Transcript + Working + Composer wired to `useAgent`. |
|
|
59
|
+
| `<Transcript messages streamingText>` | Renders messages; pairs tool calls with results. |
|
|
60
|
+
| `<Message>` / `<MarkdownMessage>` | Chat bubbles; safe built-in markdown (override via `render`). |
|
|
61
|
+
| `<ToolCall>` | Collapsible tool call + result. |
|
|
62
|
+
| `<Composer onSend>` | Textarea + send (Enter sends, Shift+Enter newline). |
|
|
63
|
+
| `<Working active paused>` | Shimmering "Working…" indicator. |
|
|
64
|
+
|
|
65
|
+
## Styling
|
|
66
|
+
|
|
67
|
+
Everything is class-based (`.ac-*`) with `data-role` attributes. Import the
|
|
68
|
+
optional `anyclaude-react/styles.css` and override the CSS variables on `.ac-chat`
|
|
69
|
+
(`--ac-accent`, `--ac-bg`, `--ac-fg`, …), or skip it and style with your own
|
|
70
|
+
CSS / Tailwind. No emojis — icons are inline SVG.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SDKMessage } from 'anyclaude-sdk';
|
|
2
|
+
export interface RunOptions {
|
|
3
|
+
prompt: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
continueRun?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Produces the raw SDKMessage stream for one underlying run (in-process or remote). */
|
|
8
|
+
export type RunFn = (opts: RunOptions) => AsyncIterable<SDKMessage>;
|
|
9
|
+
export interface AgentClient {
|
|
10
|
+
/** Stream one logical run for `prompt` under `sessionId`, survivor-stitched. */
|
|
11
|
+
send(prompt: string, sessionId: string): AsyncIterable<SDKMessage>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build an AgentClient from a `run` function. `run` does ONE underlying run —
|
|
15
|
+
* 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.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createAgentClient({ run }: {
|
|
19
|
+
run: RunFn;
|
|
20
|
+
}): AgentClient;
|
|
21
|
+
export interface EndpointClientOptions {
|
|
22
|
+
/** URL of a serverless function that streams NDJSON SDKMessages. */
|
|
23
|
+
endpoint: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
/** Extra fields merged into the POST body (e.g. model, auth context). */
|
|
26
|
+
body?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* AgentClient backed by a serverless function. POSTs `{ prompt, sessionId,
|
|
30
|
+
* continueRun, ...body }` and reads a newline-delimited JSON stream of
|
|
31
|
+
* SDKMessages. Survivor-stitched automatically.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createEndpointClient(opts: EndpointClientOptions): AgentClient;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function isPaused(m) {
|
|
2
|
+
return m.type === 'system' && m.subtype === 'paused';
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Build an AgentClient from a `run` function. `run` does ONE underlying run —
|
|
6
|
+
* 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.
|
|
8
|
+
*/
|
|
9
|
+
export function createAgentClient({ run }) {
|
|
10
|
+
return {
|
|
11
|
+
async *send(prompt, sessionId) {
|
|
12
|
+
let continueRun = false;
|
|
13
|
+
for (;;) {
|
|
14
|
+
let paused = false;
|
|
15
|
+
for await (const m of run({ prompt: continueRun ? '' : prompt, sessionId, continueRun })) {
|
|
16
|
+
if (isPaused(m))
|
|
17
|
+
paused = true;
|
|
18
|
+
yield m; // forward everything (incl. the paused boundary) — consumers may show "paused"
|
|
19
|
+
}
|
|
20
|
+
if (!paused)
|
|
21
|
+
break;
|
|
22
|
+
continueRun = true; // next iteration resumes + continues the same session
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* AgentClient backed by a serverless function. POSTs `{ prompt, sessionId,
|
|
29
|
+
* continueRun, ...body }` and reads a newline-delimited JSON stream of
|
|
30
|
+
* SDKMessages. Survivor-stitched automatically.
|
|
31
|
+
*/
|
|
32
|
+
export function createEndpointClient(opts) {
|
|
33
|
+
const run = async function* ({ prompt, sessionId, continueRun }) {
|
|
34
|
+
const res = await fetch(opts.endpoint, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'content-type': 'application/json', ...opts.headers },
|
|
37
|
+
body: JSON.stringify({ prompt, sessionId, continueRun, ...opts.body }),
|
|
38
|
+
});
|
|
39
|
+
if (!res.body)
|
|
40
|
+
return;
|
|
41
|
+
const reader = res.body.getReader();
|
|
42
|
+
const dec = new TextDecoder();
|
|
43
|
+
let buf = '';
|
|
44
|
+
for (;;) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done)
|
|
47
|
+
break;
|
|
48
|
+
buf += dec.decode(value, { stream: true });
|
|
49
|
+
let nl;
|
|
50
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
51
|
+
const line = buf.slice(0, nl).trim();
|
|
52
|
+
buf = buf.slice(nl + 1);
|
|
53
|
+
if (line) {
|
|
54
|
+
try {
|
|
55
|
+
yield JSON.parse(line);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* skip non-JSON keepalive lines */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const tail = buf.trim();
|
|
64
|
+
if (tail) {
|
|
65
|
+
try {
|
|
66
|
+
yield JSON.parse(tail);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* ignore */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return createAgentClient({ run });
|
|
74
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type UseAgentOptions } from '../useAgent.js';
|
|
3
|
+
export interface AgentChatProps extends UseAgentOptions {
|
|
4
|
+
className?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
workingLabel?: string;
|
|
7
|
+
/** Override markdown rendering in the transcript. */
|
|
8
|
+
renderMarkdown?: (text: string) => ReactNode;
|
|
9
|
+
/** Optional header rendered above the transcript. */
|
|
10
|
+
header?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
/** All-in-one chat surface: Transcript + Working + Composer, wired to useAgent. */
|
|
13
|
+
export declare function AgentChat({ className, placeholder, workingLabel, renderMarkdown, header, ...agentOpts }: AgentChatProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
/** All-in-one chat surface: Transcript + Working + Composer, wired to useAgent. */
|
|
7
|
+
export function AgentChat({ className, placeholder, workingLabel, renderMarkdown, header, ...agentOpts }) {
|
|
8
|
+
const { messages, streamingText, status, send } = useAgent(agentOpts);
|
|
9
|
+
const running = status !== 'idle';
|
|
10
|
+
return (_jsxs("div", { className: `ac-chat${className ? ' ' + className : ''}`, children: [header, _jsx(Transcript, { messages: messages, streamingText: streamingText, renderMarkdown: renderMarkdown }), _jsx(Working, { active: running, label: workingLabel, paused: status === 'paused' }), _jsx(Composer, { onSend: send, placeholder: placeholder })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ComposerProps {
|
|
2
|
+
onSend: (text: string) => void;
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Send on Enter (Shift+Enter = newline). Default true. */
|
|
7
|
+
sendOnEnter?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Textarea + send button. Enter sends; Shift+Enter inserts a newline. */
|
|
10
|
+
export declare function Composer({ onSend, disabled, placeholder, className, sendOnEnter }: ComposerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
/** Textarea + send button. Enter sends; Shift+Enter inserts a newline. */
|
|
4
|
+
export function Composer({ onSend, disabled, placeholder = 'Send a message…', className, sendOnEnter = true }) {
|
|
5
|
+
const [value, setValue] = useState('');
|
|
6
|
+
const submit = () => {
|
|
7
|
+
const t = value.trim();
|
|
8
|
+
if (!t || disabled)
|
|
9
|
+
return;
|
|
10
|
+
onSend(t);
|
|
11
|
+
setValue('');
|
|
12
|
+
};
|
|
13
|
+
const onKeyDown = (e) => {
|
|
14
|
+
if (sendOnEnter && e.key === 'Enter' && !e.shiftKey) {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
submit();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return (_jsxs("div", { className: `ac-composer${className ? ' ' + className : ''}`, children: [_jsx("textarea", { className: "ac-composer-input", value: value, placeholder: placeholder, rows: 1, disabled: disabled, onChange: (e) => setValue(e.target.value), onKeyDown: onKeyDown }), _jsx("button", { className: "ac-composer-send", onClick: submit, disabled: disabled || !value.trim(), "aria-label": "Send", children: _jsxs("svg", { viewBox: "0 0 24 24", width: "18", height: "18", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [_jsx("path", { d: "M22 2 11 13" }), _jsx("path", { d: "M22 2 15 22l-4-9-9-4Z" })] }) })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface MessageProps {
|
|
3
|
+
role: 'user' | 'assistant';
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
/** A plain chat bubble. */
|
|
8
|
+
export declare function Message({ role, children, className }: MessageProps): import("react").JSX.Element;
|
|
9
|
+
export interface MarkdownMessageProps {
|
|
10
|
+
text: string;
|
|
11
|
+
role?: 'user' | 'assistant';
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Override the markdown renderer (default: built-in safe renderer). */
|
|
14
|
+
render?: (text: string) => ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/** An assistant bubble whose text is rendered as markdown. */
|
|
17
|
+
export declare function MarkdownMessage({ text, role, className, render }: MarkdownMessageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { renderMarkdown } from '../markdown.js';
|
|
3
|
+
/** A plain chat bubble. */
|
|
4
|
+
export function Message({ role, children, className }) {
|
|
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
|
+
}
|
|
7
|
+
/** An assistant bubble whose text is rendered as markdown. */
|
|
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) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ToolResultLike {
|
|
2
|
+
content: unknown;
|
|
3
|
+
isError?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolCallProps {
|
|
6
|
+
name: string;
|
|
7
|
+
input?: Record<string, unknown>;
|
|
8
|
+
result?: ToolResultLike;
|
|
9
|
+
className?: string;
|
|
10
|
+
/** Start expanded. Default false. */
|
|
11
|
+
defaultExpanded?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** A collapsible tool call + its result. */
|
|
14
|
+
export declare function ToolCall({ name, input, result, className, defaultExpanded }: ToolCallProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
function resultText(content) {
|
|
4
|
+
if (typeof content === 'string')
|
|
5
|
+
return content;
|
|
6
|
+
if (Array.isArray(content)) {
|
|
7
|
+
return content
|
|
8
|
+
.map((b) => (b && typeof b === 'object' && 'text' in b ? String(b.text ?? '') : '[…]'))
|
|
9
|
+
.join('\n');
|
|
10
|
+
}
|
|
11
|
+
return content == null ? '' : JSON.stringify(content);
|
|
12
|
+
}
|
|
13
|
+
/** A collapsible tool call + its result. */
|
|
14
|
+
export function ToolCall({ name, input, result, className, defaultExpanded = false }) {
|
|
15
|
+
const [open, setOpen] = useState(defaultExpanded);
|
|
16
|
+
const summary = result ? resultText(result.content).split('\n')[0].slice(0, 120) : 'running…';
|
|
17
|
+
return (_jsxs("div", { className: `ac-tool${result?.isError ? ' ac-tool-error' : ''}${className ? ' ' + className : ''}`, children: [_jsxs("button", { className: "ac-tool-head", onClick: () => setOpen((o) => !o), "aria-expanded": open, children: [_jsx("span", { className: "ac-tool-caret", "aria-hidden": true, children: open ? '▾' : '▸' }), _jsx("span", { className: "ac-tool-name", children: name }), !open && _jsx("span", { className: "ac-tool-summary", children: summary })] }), open && (_jsxs("div", { className: "ac-tool-body", children: [input && Object.keys(input).length > 0 && (_jsx("pre", { className: "ac-tool-args", children: _jsx("code", { children: JSON.stringify(input, null, 2) }) })), result && (_jsx("pre", { className: `ac-tool-result${result.isError ? ' ac-tool-result-error' : ''}`, children: _jsx("code", { children: resultText(result.content) }) }))] }))] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { SDKMessage } from 'anyclaude-sdk';
|
|
3
|
+
export interface TranscriptProps {
|
|
4
|
+
messages: SDKMessage[];
|
|
5
|
+
/** Live streaming text for the in-flight assistant turn (optional). */
|
|
6
|
+
streamingText?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
/** Override markdown rendering. */
|
|
9
|
+
renderMarkdown?: (text: string) => ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/** Renders an SDKMessage[] as chat bubbles + collapsible tool calls. */
|
|
12
|
+
export declare function Transcript({ messages, streamingText, className, renderMarkdown }: TranscriptProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
import { Message, MarkdownMessage } from './Message.js';
|
|
4
|
+
import { ToolCall } from './ToolCall.js';
|
|
5
|
+
/** Renders an SDKMessage[] as chat bubbles + collapsible tool calls. */
|
|
6
|
+
export function Transcript({ messages, streamingText, className, renderMarkdown }) {
|
|
7
|
+
// Pair tool_use blocks with their results (from synthetic user messages).
|
|
8
|
+
const results = new Map();
|
|
9
|
+
for (const m of messages) {
|
|
10
|
+
if (m.type === 'user') {
|
|
11
|
+
const c = m.message.content;
|
|
12
|
+
if (Array.isArray(c)) {
|
|
13
|
+
for (const b of c) {
|
|
14
|
+
if (b.type === 'tool_result' && b.tool_use_id)
|
|
15
|
+
results.set(b.tool_use_id, { content: b.content, isError: b.is_error });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const rendered = [];
|
|
21
|
+
let k = 0;
|
|
22
|
+
for (const m of messages) {
|
|
23
|
+
if (m.type === 'user') {
|
|
24
|
+
const c = m.message.content;
|
|
25
|
+
if (typeof c === 'string') {
|
|
26
|
+
if (c.trim())
|
|
27
|
+
rendered.push(_jsx(Message, { role: "user", children: c }, k++));
|
|
28
|
+
}
|
|
29
|
+
// array content = tool results → shown via paired ToolCall, skip here
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (m.type === 'assistant') {
|
|
33
|
+
const blocks = (m.message.content ?? []);
|
|
34
|
+
for (const b of blocks) {
|
|
35
|
+
if (b.type === 'text' && b.text && b.text.trim()) {
|
|
36
|
+
rendered.push(_jsx(MarkdownMessage, { text: b.text, render: renderMarkdown }, k++));
|
|
37
|
+
}
|
|
38
|
+
else if (b.type === 'tool_use' && b.id) {
|
|
39
|
+
rendered.push(_jsx(ToolCall, { name: b.name ?? 'tool', input: b.input, result: results.get(b.id) }, k++));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (m.type === 'system') {
|
|
45
|
+
const sub = m.subtype;
|
|
46
|
+
if (sub === 'local_command_output') {
|
|
47
|
+
const text = m.content ?? '';
|
|
48
|
+
if (text.trim())
|
|
49
|
+
rendered.push(_jsx(MarkdownMessage, { text: text, render: renderMarkdown }, k++));
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (streamingText && streamingText.trim()) {
|
|
55
|
+
rendered.push(_jsx(MarkdownMessage, { text: streamingText, render: renderMarkdown }, "streaming"));
|
|
56
|
+
}
|
|
57
|
+
return _jsx("div", { className: `ac-transcript${className ? ' ' + className : ''}`, children: _jsx(Fragment, { children: rendered }) });
|
|
58
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface WorkingProps {
|
|
2
|
+
/** Show the indicator. */
|
|
3
|
+
active: boolean;
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Shown when the run is paused mid-flight (survivor continuation). */
|
|
6
|
+
paused?: boolean;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
/** A shimmering "Working…" indicator (CSS animation in styles.css). */
|
|
10
|
+
export declare function Working({ active, label, paused, className }: WorkingProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** A shimmering "Working…" indicator (CSS animation in styles.css). */
|
|
3
|
+
export function Working({ active, label = 'Working', paused, className }) {
|
|
4
|
+
if (!active)
|
|
5
|
+
return null;
|
|
6
|
+
const text = paused ? 'Resuming' : label;
|
|
7
|
+
return (_jsxs("div", { className: `ac-working${className ? ' ' + className : ''}`, role: "status", "aria-live": "polite", children: [_jsx("span", { className: "ac-working-spinner", "aria-hidden": true }), _jsxs("span", { className: "ac-working-text", children: [text, "\u2026"] })] }));
|
|
8
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { createAgentClient, createEndpointClient } from './client.js';
|
|
2
|
+
export type { AgentClient, RunFn, RunOptions, EndpointClientOptions } from './client.js';
|
|
3
|
+
export { useAgent } from './useAgent.js';
|
|
4
|
+
export type { UseAgentOptions, UseAgentResult, AgentStatus } from './useAgent.js';
|
|
5
|
+
export { renderMarkdown } from './markdown.js';
|
|
6
|
+
export { Message, MarkdownMessage } from './components/Message.js';
|
|
7
|
+
export type { MessageProps, MarkdownMessageProps } from './components/Message.js';
|
|
8
|
+
export { ToolCall } from './components/ToolCall.js';
|
|
9
|
+
export type { ToolCallProps, ToolResultLike } from './components/ToolCall.js';
|
|
10
|
+
export { Composer } from './components/Composer.js';
|
|
11
|
+
export type { ComposerProps } from './components/Composer.js';
|
|
12
|
+
export { Working } from './components/Working.js';
|
|
13
|
+
export type { WorkingProps } from './components/Working.js';
|
|
14
|
+
export { Transcript } from './components/Transcript.js';
|
|
15
|
+
export type { TranscriptProps } from './components/Transcript.js';
|
|
16
|
+
export { AgentChat } from './components/AgentChat.js';
|
|
17
|
+
export type { AgentChatProps } from './components/AgentChat.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// anyclaude-react — restylable React UI kit for anyclaude-sdk.
|
|
2
|
+
export { createAgentClient, createEndpointClient } from './client.js';
|
|
3
|
+
export { useAgent } from './useAgent.js';
|
|
4
|
+
export { renderMarkdown } from './markdown.js';
|
|
5
|
+
export { Message, MarkdownMessage } from './components/Message.js';
|
|
6
|
+
export { ToolCall } from './components/ToolCall.js';
|
|
7
|
+
export { Composer } from './components/Composer.js';
|
|
8
|
+
export { Working } from './components/Working.js';
|
|
9
|
+
export { Transcript } from './components/Transcript.js';
|
|
10
|
+
export { AgentChat } from './components/AgentChat.js';
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Tiny, dependency-free, XSS-safe markdown → React renderer. Text goes through
|
|
2
|
+
// React text nodes (auto-escaped) — we never use dangerouslySetInnerHTML. Handles
|
|
3
|
+
// the common cases (headings, lists, blockquote, fenced + inline code, bold,
|
|
4
|
+
// italic, links). Consumers can pass their own renderer to <MarkdownMessage>.
|
|
5
|
+
import { createElement, Fragment } from 'react';
|
|
6
|
+
let _k = 0;
|
|
7
|
+
const key = () => 'md' + ++_k;
|
|
8
|
+
function inline(text) {
|
|
9
|
+
const nodes = [];
|
|
10
|
+
// order matters: code first (so ** inside `code` isn't parsed), then link, bold, italic
|
|
11
|
+
const re = /(`[^`]+`)|(\[[^\]]+\]\([^)]+\))|(\*\*[^*]+\*\*)|(\*[^*]+\*|_[^_]+_)/g;
|
|
12
|
+
let last = 0;
|
|
13
|
+
let m;
|
|
14
|
+
while ((m = re.exec(text))) {
|
|
15
|
+
if (m.index > last)
|
|
16
|
+
nodes.push(text.slice(last, m.index));
|
|
17
|
+
const tok = m[0];
|
|
18
|
+
if (tok.startsWith('`')) {
|
|
19
|
+
nodes.push(createElement('code', { key: key(), className: 'ac-code-inline' }, tok.slice(1, -1)));
|
|
20
|
+
}
|
|
21
|
+
else if (tok.startsWith('[')) {
|
|
22
|
+
const lm = /\[([^\]]+)\]\(([^)]+)\)/.exec(tok);
|
|
23
|
+
nodes.push(createElement('a', { key: key(), className: 'ac-link', href: lm[2], target: '_blank', rel: 'noreferrer' }, lm[1]));
|
|
24
|
+
}
|
|
25
|
+
else if (tok.startsWith('**')) {
|
|
26
|
+
nodes.push(createElement('strong', { key: key() }, tok.slice(2, -2)));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
nodes.push(createElement('em', { key: key() }, tok.slice(1, -1)));
|
|
30
|
+
}
|
|
31
|
+
last = m.index + tok.length;
|
|
32
|
+
}
|
|
33
|
+
if (last < text.length)
|
|
34
|
+
nodes.push(text.slice(last));
|
|
35
|
+
return nodes;
|
|
36
|
+
}
|
|
37
|
+
/** Render markdown text to React nodes. */
|
|
38
|
+
export function renderMarkdown(src) {
|
|
39
|
+
const lines = (src ?? '').split('\n');
|
|
40
|
+
const out = [];
|
|
41
|
+
let i = 0;
|
|
42
|
+
let list = null;
|
|
43
|
+
const flushList = () => {
|
|
44
|
+
if (!list)
|
|
45
|
+
return;
|
|
46
|
+
const items = list.items.map((it) => createElement('li', { key: key() }, inline(it)));
|
|
47
|
+
out.push(createElement(list.ordered ? 'ol' : 'ul', { key: key(), className: 'ac-list' }, items));
|
|
48
|
+
list = null;
|
|
49
|
+
};
|
|
50
|
+
while (i < lines.length) {
|
|
51
|
+
const line = lines[i];
|
|
52
|
+
// fenced code block
|
|
53
|
+
const fence = /^```(\w*)\s*$/.exec(line);
|
|
54
|
+
if (fence) {
|
|
55
|
+
flushList();
|
|
56
|
+
const lang = fence[1];
|
|
57
|
+
const body = [];
|
|
58
|
+
i++;
|
|
59
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i]))
|
|
60
|
+
body.push(lines[i++]);
|
|
61
|
+
i++; // closing fence
|
|
62
|
+
out.push(createElement('pre', { key: key(), className: 'ac-code-block', 'data-lang': lang || undefined }, createElement('code', null, body.join('\n'))));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const h = /^(#{1,4})\s+(.*)$/.exec(line);
|
|
66
|
+
if (h) {
|
|
67
|
+
flushList();
|
|
68
|
+
out.push(createElement('h' + h[1].length, { key: key(), className: 'ac-h' }, inline(h[2])));
|
|
69
|
+
i++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const li = /^\s*([-*]|\d+\.)\s+(.*)$/.exec(line);
|
|
73
|
+
if (li) {
|
|
74
|
+
const ordered = /\d+\./.test(li[1]);
|
|
75
|
+
if (!list || list.ordered !== ordered) {
|
|
76
|
+
flushList();
|
|
77
|
+
list = { ordered, items: [] };
|
|
78
|
+
}
|
|
79
|
+
list.items.push(li[2]);
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const bq = /^>\s?(.*)$/.exec(line);
|
|
84
|
+
if (bq) {
|
|
85
|
+
flushList();
|
|
86
|
+
out.push(createElement('blockquote', { key: key(), className: 'ac-quote' }, inline(bq[1])));
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (line.trim() === '') {
|
|
91
|
+
flushList();
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// paragraph — merge consecutive non-blank, non-special lines
|
|
96
|
+
flushList();
|
|
97
|
+
const para = [line];
|
|
98
|
+
i++;
|
|
99
|
+
while (i < lines.length &&
|
|
100
|
+
lines[i].trim() !== '' &&
|
|
101
|
+
!/^(#{1,4}\s|```|>\s?|\s*([-*]|\d+\.)\s)/.test(lines[i])) {
|
|
102
|
+
para.push(lines[i++]);
|
|
103
|
+
}
|
|
104
|
+
out.push(createElement('p', { key: key(), className: 'ac-p' }, inline(para.join(' '))));
|
|
105
|
+
}
|
|
106
|
+
flushList();
|
|
107
|
+
return createElement(Fragment, null, out);
|
|
108
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SDKMessage } from 'anyclaude-sdk';
|
|
2
|
+
import { type AgentClient, type RunFn } from './client.js';
|
|
3
|
+
export type AgentStatus = 'idle' | 'running' | 'paused';
|
|
4
|
+
export interface UseAgentOptions {
|
|
5
|
+
/** Provide ONE of these. */
|
|
6
|
+
client?: AgentClient;
|
|
7
|
+
run?: RunFn;
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
/** Stable id for this conversation (survivor continuation reuses it). Auto if omitted. */
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface UseAgentResult {
|
|
14
|
+
messages: SDKMessage[];
|
|
15
|
+
streamingText: string;
|
|
16
|
+
status: AgentStatus;
|
|
17
|
+
tokens: number;
|
|
18
|
+
cost: number;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
send: (text: string) => void;
|
|
21
|
+
interrupt: () => void;
|
|
22
|
+
clear: () => void;
|
|
23
|
+
}
|
|
24
|
+
export declare function useAgent(opts: UseAgentOptions): UseAgentResult;
|
package/dist/useAgent.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// React hook driving an agent run with live streaming + survivor stitching.
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { createAgentClient, createEndpointClient } from './client.js';
|
|
4
|
+
let _seq = 0;
|
|
5
|
+
function newSessionId() {
|
|
6
|
+
const c = globalThis.crypto;
|
|
7
|
+
return c?.randomUUID?.() ?? `sess-${Date.now()}-${++_seq}`;
|
|
8
|
+
}
|
|
9
|
+
function resolveClient(opts) {
|
|
10
|
+
if (opts.client)
|
|
11
|
+
return opts.client;
|
|
12
|
+
if (opts.run)
|
|
13
|
+
return createAgentClient({ run: opts.run });
|
|
14
|
+
if (opts.endpoint)
|
|
15
|
+
return createEndpointClient({ endpoint: opts.endpoint, headers: opts.headers });
|
|
16
|
+
throw new Error('useAgent: provide one of `client`, `run`, or `endpoint`.');
|
|
17
|
+
}
|
|
18
|
+
export function useAgent(opts) {
|
|
19
|
+
const [messages, setMessages] = useState([]);
|
|
20
|
+
const [streamingText, setStreamingText] = useState('');
|
|
21
|
+
const [status, setStatus] = useState('idle');
|
|
22
|
+
const [tokens, setTokens] = useState(0);
|
|
23
|
+
const [cost, setCost] = useState(0);
|
|
24
|
+
const sessionRef = useRef(opts.sessionId ?? newSessionId());
|
|
25
|
+
const abortRef = useRef(false);
|
|
26
|
+
const runningRef = useRef(false);
|
|
27
|
+
const client = useMemo(() => resolveClient(opts),
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
[opts.client, opts.run, opts.endpoint, opts.headers]);
|
|
30
|
+
const send = useCallback((text) => {
|
|
31
|
+
const t = text.trim();
|
|
32
|
+
if (!t || runningRef.current)
|
|
33
|
+
return;
|
|
34
|
+
runningRef.current = true;
|
|
35
|
+
abortRef.current = false;
|
|
36
|
+
const userMsg = {
|
|
37
|
+
type: 'user',
|
|
38
|
+
message: { role: 'user', content: t },
|
|
39
|
+
parent_tool_use_id: null,
|
|
40
|
+
};
|
|
41
|
+
setMessages((m) => [...m, userMsg]);
|
|
42
|
+
setStatus('running');
|
|
43
|
+
setStreamingText('');
|
|
44
|
+
void (async () => {
|
|
45
|
+
let buf = '';
|
|
46
|
+
try {
|
|
47
|
+
for await (const msg of client.send(t, sessionRef.current)) {
|
|
48
|
+
if (abortRef.current)
|
|
49
|
+
break;
|
|
50
|
+
if (msg.type === 'stream_event') {
|
|
51
|
+
const ev = msg.event;
|
|
52
|
+
if (ev?.type === 'content_block_delta' && ev.delta?.type === 'text_delta') {
|
|
53
|
+
buf += ev.delta.text ?? '';
|
|
54
|
+
setStreamingText(buf);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const sub = msg.subtype;
|
|
59
|
+
if (msg.type === 'system' && sub === 'paused') {
|
|
60
|
+
setStatus('paused');
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (msg.type === 'assistant') {
|
|
64
|
+
buf = '';
|
|
65
|
+
setStreamingText('');
|
|
66
|
+
setStatus('running');
|
|
67
|
+
setMessages((m) => [...m, msg]);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (msg.type === 'user') {
|
|
71
|
+
setMessages((m) => [...m, msg]);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (msg.type === 'result') {
|
|
75
|
+
const u = msg.usage;
|
|
76
|
+
if (u)
|
|
77
|
+
setTokens((u.input_tokens ?? 0) + (u.output_tokens ?? 0));
|
|
78
|
+
const c = msg.total_cost_usd;
|
|
79
|
+
if (typeof c === 'number')
|
|
80
|
+
setCost(c);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// system: init / local_command_output / compact_boundary
|
|
84
|
+
setMessages((m) => [...m, msg]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const errMsg = {
|
|
89
|
+
type: 'system',
|
|
90
|
+
subtype: 'local_command_output',
|
|
91
|
+
content: 'Error: ' + (err instanceof Error ? err.message : String(err)),
|
|
92
|
+
};
|
|
93
|
+
setMessages((m) => [...m, errMsg]);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
runningRef.current = false;
|
|
97
|
+
setStreamingText('');
|
|
98
|
+
setStatus('idle');
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
}, [client]);
|
|
102
|
+
const interrupt = useCallback(() => {
|
|
103
|
+
abortRef.current = true;
|
|
104
|
+
runningRef.current = false;
|
|
105
|
+
setStatus('idle');
|
|
106
|
+
setStreamingText('');
|
|
107
|
+
}, []);
|
|
108
|
+
const clear = useCallback(() => {
|
|
109
|
+
if (runningRef.current)
|
|
110
|
+
return;
|
|
111
|
+
setMessages([]);
|
|
112
|
+
setTokens(0);
|
|
113
|
+
setCost(0);
|
|
114
|
+
sessionRef.current = newSessionId();
|
|
115
|
+
}, []);
|
|
116
|
+
return { messages, streamingText, status, tokens, cost, sessionId: sessionRef.current, send, interrupt, clear };
|
|
117
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
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.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
11
|
+
"./styles.css": "./styles.css"
|
|
12
|
+
},
|
|
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"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"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" }
|
|
22
|
+
}
|
package/styles.css
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* anyclaude-react — optional default theme. Restyle by overriding the CSS
|
|
2
|
+
variables below, or skip this file entirely and style the .ac-* classes
|
|
3
|
+
(and data-role attributes) yourself / with Tailwind. */
|
|
4
|
+
.ac-chat {
|
|
5
|
+
--ac-bg: #0d1117;
|
|
6
|
+
--ac-fg: #e6edf3;
|
|
7
|
+
--ac-muted: #8b949e;
|
|
8
|
+
--ac-border: #232c3d;
|
|
9
|
+
--ac-accent: #4dd0e1;
|
|
10
|
+
--ac-user: #5fd75f;
|
|
11
|
+
--ac-panel: #161b22;
|
|
12
|
+
--ac-radius: 12px;
|
|
13
|
+
--ac-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
14
|
+
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: 10px;
|
|
18
|
+
height: 100%;
|
|
19
|
+
background: var(--ac-bg);
|
|
20
|
+
color: var(--ac-fg);
|
|
21
|
+
font: 15px/1.55 system-ui, sans-serif;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ac-transcript { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; padding: 14px; }
|
|
25
|
+
|
|
26
|
+
.ac-msg { display: flex; gap: 8px; }
|
|
27
|
+
.ac-msg-user { color: var(--ac-user); }
|
|
28
|
+
.ac-msg-prefix { font-family: var(--ac-mono); opacity: 0.8; }
|
|
29
|
+
.ac-msg-body { min-width: 0; }
|
|
30
|
+
.ac-msg-md .ac-msg-body { color: var(--ac-fg); }
|
|
31
|
+
|
|
32
|
+
.ac-p { margin: 0 0 8px; }
|
|
33
|
+
.ac-h { margin: 12px 0 6px; line-height: 1.25; }
|
|
34
|
+
.ac-list { margin: 6px 0; padding-left: 22px; }
|
|
35
|
+
.ac-quote { border-left: 3px solid var(--ac-border); margin: 8px 0; padding: 2px 12px; color: var(--ac-muted); }
|
|
36
|
+
.ac-link { color: var(--ac-accent); text-decoration: underline; }
|
|
37
|
+
.ac-code-inline { font-family: var(--ac-mono); background: var(--ac-panel); border: 1px solid var(--ac-border); border-radius: 6px; padding: 1px 5px; font-size: 0.9em; }
|
|
38
|
+
.ac-code-block { font-family: var(--ac-mono); background: var(--ac-panel); border: 1px solid var(--ac-border); border-radius: var(--ac-radius); padding: 12px 14px; overflow-x: auto; margin: 10px 0; }
|
|
39
|
+
|
|
40
|
+
.ac-tool { border: 1px solid var(--ac-border); border-radius: 10px; background: var(--ac-panel); margin: 2px 0; overflow: hidden; }
|
|
41
|
+
.ac-tool-error { border-color: #b3403a; }
|
|
42
|
+
.ac-tool-head { display: flex; align-items: center; gap: 8px; width: 100%; background: none; border: 0; color: var(--ac-accent); padding: 8px 12px; cursor: pointer; font: inherit; text-align: left; }
|
|
43
|
+
.ac-tool-caret { color: var(--ac-muted); }
|
|
44
|
+
.ac-tool-name { font-family: var(--ac-mono); font-weight: 600; }
|
|
45
|
+
.ac-tool-summary { color: var(--ac-muted); font-family: var(--ac-mono); font-size: 0.85em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
46
|
+
.ac-tool-body { padding: 0 12px 10px; }
|
|
47
|
+
.ac-tool-args, .ac-tool-result { font-family: var(--ac-mono); font-size: 0.85em; background: var(--ac-bg); border: 1px solid var(--ac-border); border-radius: 8px; padding: 8px 10px; overflow-x: auto; margin: 6px 0 0; }
|
|
48
|
+
.ac-tool-result-error { color: #ff8a80; }
|
|
49
|
+
|
|
50
|
+
.ac-working { display: flex; align-items: center; gap: 8px; padding: 6px 14px; color: var(--ac-muted); font-family: var(--ac-mono); font-size: 13px; }
|
|
51
|
+
.ac-working-text { background: linear-gradient(90deg, var(--ac-muted) 30%, var(--ac-accent) 50%, var(--ac-muted) 70%); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; color: transparent; animation: ac-shimmer 1.6s linear infinite; }
|
|
52
|
+
.ac-working-spinner { width: 9px; height: 9px; border-radius: 50%; background: var(--ac-accent); animation: ac-pulse 1s ease-in-out infinite; }
|
|
53
|
+
@keyframes ac-shimmer { to { background-position: -200% center; } }
|
|
54
|
+
@keyframes ac-pulse { 0%, 100% { opacity: 0.35; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1); } }
|
|
55
|
+
|
|
56
|
+
.ac-composer { display: flex; gap: 8px; align-items: flex-end; padding: 12px 14px; border-top: 1px solid var(--ac-border); }
|
|
57
|
+
.ac-composer-input { flex: 1; resize: none; background: var(--ac-panel); color: var(--ac-fg); border: 1px solid var(--ac-border); border-radius: 10px; padding: 10px 12px; font: inherit; min-height: 20px; max-height: 180px; }
|
|
58
|
+
.ac-composer-input:focus { outline: none; border-color: var(--ac-accent); }
|
|
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
|
+
.ac-composer-send:disabled { opacity: 0.4; cursor: default; }
|