@agentable-ui/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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +371 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agentable contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @agentable-ui/react
|
|
2
|
+
|
|
3
|
+
React bindings for [`@agentable-ui/core`](../core): `AgentableProvider`, `useCapability`, `useTool`, `ToolScope`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @agentable-ui/react @agentable-ui/core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { AgentableProvider, useTool } from "@agentable-ui/react";
|
|
13
|
+
|
|
14
|
+
function App() {
|
|
15
|
+
return (
|
|
16
|
+
<AgentableProvider budget={12}>
|
|
17
|
+
<Orders />
|
|
18
|
+
</AgentableProvider>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Orders() {
|
|
23
|
+
useTool({
|
|
24
|
+
name: "refresh_orders",
|
|
25
|
+
description: "Reload the orders list.",
|
|
26
|
+
annotations: { readOnlyHint: true },
|
|
27
|
+
execute: () => refetch(),
|
|
28
|
+
});
|
|
29
|
+
return <table>…</table>;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
**`<AgentableProvider>`** creates the registry with the WebMCP adapter and a built-in confirmation dialog. Pass `confirm` for your own handler, `confirm={false}` to run consequential tools unconfirmed (a dev warning is logged), `renderConfirm` for custom UI, or `agentable` to bring your own registry.
|
|
36
|
+
|
|
37
|
+
**`useCapability(cap | null)`** declares a full capability descriptor. The registry only sees a new declaration when the static shape changes, so a fresh `execute` closure every render costs nothing. Pass `null` to switch the agent layer off for that component.
|
|
38
|
+
|
|
39
|
+
**`useTool(options)`** is the shorthand for a single hand-rolled tool; `id` defaults to `tool.<name>`, and `enabled: false` disables it without unmounting.
|
|
40
|
+
|
|
41
|
+
**`<ToolScope group describe>`** groups the capabilities inside it and demotes them from tier 0 to tier 1 once the region has been offscreen for `demoteAfter` ms (default 5000). Scrolling back promotes them again with no re-declare.
|
|
42
|
+
|
|
43
|
+
**`useAgentableSnapshot()`** returns the live registry snapshot — resident tools, budget, cache stats, adapters, warnings, estimated tokens. Re-renders on every registry change, so use it sparingly. **`useAgentPresent()`** is true once the first agent call arrives.
|
|
44
|
+
|
|
45
|
+
**`useAgentable()`** returns the registry from the nearest provider, or `null`.
|
|
46
|
+
|
|
47
|
+
## SSR and StrictMode
|
|
48
|
+
|
|
49
|
+
Registration happens in effects only, so nothing runs during SSR. Declarations are refcounted and identity-stable across re-renders. The provider defers its `dispose()` by a macrotask so StrictMode's mount → unmount → remount cycle cannot tear down the registry the second mount reuses.
|
|
50
|
+
|
|
51
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { AgentAdapter, Agentable, AgentableOptions, Capability, CapabilityTool, ConfirmRequest, Snapshot, Tier } from "@agentable-ui/core";
|
|
3
|
+
//#region src/AgentableProvider.d.ts
|
|
4
|
+
interface AgentableProviderProps extends Omit<AgentableOptions, "confirm" | "adapters"> {
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
/** Bring your own registry (tests, multiple roots). Options are ignored when set. */
|
|
7
|
+
agentable?: Agentable;
|
|
8
|
+
/** Transports. Defaults to the WebMCP adapter. */
|
|
9
|
+
adapters?: AgentAdapter[];
|
|
10
|
+
/**
|
|
11
|
+
* Confirmation for consequential tools.
|
|
12
|
+
* - omitted: the built-in dialog
|
|
13
|
+
* - a function: your own handler
|
|
14
|
+
* - false: no confirmation (a dev warning is logged)
|
|
15
|
+
*/
|
|
16
|
+
confirm?: ((req: ConfirmRequest) => Promise<boolean>) | false;
|
|
17
|
+
/** Custom confirmation UI. Receives the pending request and a decide callback. */
|
|
18
|
+
renderConfirm?: (request: ConfirmRequest, decide: (ok: boolean) => void) => ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function AgentableProvider({ children, agentable: provided, adapters, confirm, renderConfirm, ...options }: AgentableProviderProps): import("react").JSX.Element;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/ConfirmDialog.d.ts
|
|
23
|
+
interface ConfirmDialogProps {
|
|
24
|
+
request: ConfirmRequest;
|
|
25
|
+
onDecide: (ok: boolean) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Default, unstyled confirmation. Style through the data-agentable-confirm
|
|
29
|
+
* attributes, or replace it with `renderConfirm` on the provider.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ConfirmDialog({ request, onDecide }: ConfirmDialogProps): import("react").JSX.Element;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/ToolScope.d.ts
|
|
34
|
+
interface ToolScopeProps {
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
/** Default group for capabilities declared inside. */
|
|
37
|
+
group?: string;
|
|
38
|
+
/** Human description of the group, shown in list_capabilities. */
|
|
39
|
+
describe?: string;
|
|
40
|
+
/** Milliseconds offscreen before the scope is demoted to tier 1. Default 5000. */
|
|
41
|
+
demoteAfter?: number;
|
|
42
|
+
/** IntersectionObserver rootMargin. Default "200px". */
|
|
43
|
+
rootMargin?: string;
|
|
44
|
+
/** Render a different element than div. */
|
|
45
|
+
as?: keyof HTMLElementTagNameMap;
|
|
46
|
+
className?: string;
|
|
47
|
+
style?: React.CSSProperties;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Scopes capabilities to a region of the page. When the region has been
|
|
51
|
+
* offscreen for `demoteAfter`, its tools drop from tier 0 to tier 1 (they stay
|
|
52
|
+
* listed and openable). Scrolling back promotes them again with no re-declare.
|
|
53
|
+
*/
|
|
54
|
+
export declare function ToolScope({ children, group, describe, demoteAfter, rootMargin, as: Tag, className, style }: ToolScopeProps): import("react").JSX.Element;
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/useCapability.d.ts
|
|
57
|
+
/**
|
|
58
|
+
* Declare a capability for as long as the component is mounted.
|
|
59
|
+
*
|
|
60
|
+
* - Pass `null`/`undefined` to switch the agent layer off for this component.
|
|
61
|
+
* - The registry only sees a new declaration when the static shape changes;
|
|
62
|
+
* a fresh `execute` closure on every render costs nothing.
|
|
63
|
+
* - Safe under StrictMode (refcounted) and SSR (effects only).
|
|
64
|
+
*/
|
|
65
|
+
export declare function useCapability<A = unknown, R = unknown>(cap: Capability<A, R> | null | undefined): void;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/useTool.d.ts
|
|
68
|
+
interface UseToolOptions<A, R> extends CapabilityTool<A, R> {
|
|
69
|
+
/** Capability id. Defaults to `tool.<name>`. */
|
|
70
|
+
id?: string;
|
|
71
|
+
tier?: Tier;
|
|
72
|
+
group?: string;
|
|
73
|
+
priority?: number;
|
|
74
|
+
summary?: string;
|
|
75
|
+
fanIn?: string | {
|
|
76
|
+
bucket: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
title?: string;
|
|
79
|
+
};
|
|
80
|
+
/** Set false to disable without unmounting. */
|
|
81
|
+
enabled?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** Register a hand-rolled tool with the same registry the components use. */
|
|
84
|
+
export declare function useTool<A = Record<string, unknown>, R = unknown>(tool: UseToolOptions<A, R>): void;
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/snapshot.d.ts
|
|
87
|
+
/** Live registry snapshot. Re-renders on every registry change; use sparingly. */
|
|
88
|
+
export declare function useAgentableSnapshot(): Snapshot;
|
|
89
|
+
/** True once the first agent tool call has arrived (the presence heuristic). */
|
|
90
|
+
export declare function useAgentPresent(): boolean;
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/context.d.ts
|
|
93
|
+
export declare const AgentableContext: import("react").Context<Agentable | null>;
|
|
94
|
+
export declare const ScopeContext: import("react").Context<string | undefined>;
|
|
95
|
+
export declare const GroupContext: import("react").Context<string | undefined>;
|
|
96
|
+
/** The registry from the nearest provider, or null when the agent layer is switched off. */
|
|
97
|
+
export declare function useAgentable(): Agentable | null;
|
|
98
|
+
export declare function useAgentGroup(): string | undefined;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/fingerprint.d.ts
|
|
101
|
+
/** Static shape of a capability: everything except the execute closure. */
|
|
102
|
+
export declare function fingerprint(cap: Capability | null | undefined): string;
|
|
103
|
+
//#endregion
|
|
104
|
+
export type { AgentableProviderProps, ConfirmDialogProps, ToolScopeProps, UseToolOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { createAgentable, toJsonSchema, webmcpAdapter } from "@agentable-ui/core";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/context.ts
|
|
5
|
+
const AgentableContext = createContext(null);
|
|
6
|
+
const ScopeContext = createContext(void 0);
|
|
7
|
+
const GroupContext = createContext(void 0);
|
|
8
|
+
/** The registry from the nearest provider, or null when the agent layer is switched off. */
|
|
9
|
+
function useAgentable() {
|
|
10
|
+
return useContext(AgentableContext);
|
|
11
|
+
}
|
|
12
|
+
function useAgentGroup() {
|
|
13
|
+
return useContext(GroupContext);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/ConfirmDialog.tsx
|
|
17
|
+
/**
|
|
18
|
+
* Default, unstyled confirmation. Style through the data-agentable-confirm
|
|
19
|
+
* attributes, or replace it with `renderConfirm` on the provider.
|
|
20
|
+
*/
|
|
21
|
+
function ConfirmDialog({ request, onDecide }) {
|
|
22
|
+
const ref = useRef(null);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const el = ref.current;
|
|
25
|
+
if (!el) return;
|
|
26
|
+
if (typeof el.showModal === "function" && !el.open) el.showModal();
|
|
27
|
+
const onCancel = (e) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
onDecide(false);
|
|
30
|
+
};
|
|
31
|
+
el.addEventListener("cancel", onCancel);
|
|
32
|
+
return () => el.removeEventListener("cancel", onCancel);
|
|
33
|
+
}, [onDecide]);
|
|
34
|
+
const args = request.args && typeof request.args === "object" && Object.keys(request.args).length ? JSON.stringify(request.args, null, 1) : null;
|
|
35
|
+
return /* @__PURE__ */ jsx("dialog", {
|
|
36
|
+
ref,
|
|
37
|
+
"data-agentable-confirm": "",
|
|
38
|
+
"aria-labelledby": "agentable-confirm-title",
|
|
39
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
40
|
+
method: "dialog",
|
|
41
|
+
"data-agentable-confirm-form": "",
|
|
42
|
+
onSubmit: (e) => e.preventDefault(),
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsx("p", {
|
|
45
|
+
"data-agentable-confirm-eyebrow": "",
|
|
46
|
+
children: "An agent wants to act on this page"
|
|
47
|
+
}),
|
|
48
|
+
/* @__PURE__ */ jsx("h2", {
|
|
49
|
+
id: "agentable-confirm-title",
|
|
50
|
+
"data-agentable-confirm-title": "",
|
|
51
|
+
children: request.title
|
|
52
|
+
}),
|
|
53
|
+
/* @__PURE__ */ jsx("p", {
|
|
54
|
+
"data-agentable-confirm-message": "",
|
|
55
|
+
children: request.message
|
|
56
|
+
}),
|
|
57
|
+
args ? /* @__PURE__ */ jsx("pre", {
|
|
58
|
+
"data-agentable-confirm-args": "",
|
|
59
|
+
children: args
|
|
60
|
+
}) : null,
|
|
61
|
+
/* @__PURE__ */ jsxs("div", {
|
|
62
|
+
"data-agentable-confirm-actions": "",
|
|
63
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
64
|
+
type: "button",
|
|
65
|
+
"data-agentable-confirm-deny": "",
|
|
66
|
+
onClick: () => onDecide(false),
|
|
67
|
+
autoFocus: true,
|
|
68
|
+
children: "Don't allow"
|
|
69
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
70
|
+
type: "button",
|
|
71
|
+
"data-agentable-confirm-allow": "",
|
|
72
|
+
onClick: () => onDecide(true),
|
|
73
|
+
children: "Allow"
|
|
74
|
+
})]
|
|
75
|
+
})
|
|
76
|
+
]
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/AgentableProvider.tsx
|
|
82
|
+
function AgentableProvider({ children, agentable: provided, adapters, confirm, renderConfirm, ...options }) {
|
|
83
|
+
const [queue, setQueue] = useState([]);
|
|
84
|
+
const queueRef = useRef(queue);
|
|
85
|
+
queueRef.current = queue;
|
|
86
|
+
const ask = useCallback((request) => {
|
|
87
|
+
return new Promise((resolve) => setQueue((q) => [...q, {
|
|
88
|
+
request,
|
|
89
|
+
resolve
|
|
90
|
+
}]));
|
|
91
|
+
}, []);
|
|
92
|
+
const optionsRef = useRef(options);
|
|
93
|
+
const [agentable] = useState(() => {
|
|
94
|
+
if (provided) return provided;
|
|
95
|
+
const confirmHandler = confirm === false ? false : confirm ?? ask;
|
|
96
|
+
return createAgentable({
|
|
97
|
+
...optionsRef.current,
|
|
98
|
+
adapters: adapters ?? [webmcpAdapter()],
|
|
99
|
+
confirm: confirmHandler
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
const teardown = useRef(void 0);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (provided) return;
|
|
105
|
+
clearTimeout(teardown.current);
|
|
106
|
+
teardown.current = void 0;
|
|
107
|
+
return () => {
|
|
108
|
+
teardown.current = setTimeout(() => agentable.dispose(), 0);
|
|
109
|
+
};
|
|
110
|
+
}, [agentable, provided]);
|
|
111
|
+
const current = queue[0];
|
|
112
|
+
const decide = useCallback((ok) => {
|
|
113
|
+
const head = queueRef.current[0];
|
|
114
|
+
if (!head) return;
|
|
115
|
+
head.resolve(ok);
|
|
116
|
+
setQueue((q) => q.slice(1));
|
|
117
|
+
}, []);
|
|
118
|
+
const dialog = useMemo(() => {
|
|
119
|
+
if (!current) return null;
|
|
120
|
+
if (renderConfirm) return renderConfirm(current.request, decide);
|
|
121
|
+
return /* @__PURE__ */ jsx(ConfirmDialog, {
|
|
122
|
+
request: current.request,
|
|
123
|
+
onDecide: decide
|
|
124
|
+
});
|
|
125
|
+
}, [
|
|
126
|
+
current,
|
|
127
|
+
renderConfirm,
|
|
128
|
+
decide
|
|
129
|
+
]);
|
|
130
|
+
return /* @__PURE__ */ jsxs(AgentableContext.Provider, {
|
|
131
|
+
value: agentable,
|
|
132
|
+
children: [children, dialog]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/ToolScope.tsx
|
|
137
|
+
/**
|
|
138
|
+
* Scopes capabilities to a region of the page. When the region has been
|
|
139
|
+
* offscreen for `demoteAfter`, its tools drop from tier 0 to tier 1 (they stay
|
|
140
|
+
* listed and openable). Scrolling back promotes them again with no re-declare.
|
|
141
|
+
*/
|
|
142
|
+
function ToolScope({ children, group, describe, demoteAfter = 5e3, rootMargin = "200px", as: Tag = "div", className, style }) {
|
|
143
|
+
const agentable = useContext(AgentableContext);
|
|
144
|
+
const parentGroup = useContext(GroupContext);
|
|
145
|
+
const id = useId();
|
|
146
|
+
const ref = useRef(null);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!agentable || !group || !describe) return;
|
|
149
|
+
agentable.describeGroup(group, describe);
|
|
150
|
+
}, [
|
|
151
|
+
agentable,
|
|
152
|
+
group,
|
|
153
|
+
describe
|
|
154
|
+
]);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const el = ref.current;
|
|
157
|
+
if (!agentable || !el || typeof IntersectionObserver === "undefined") return;
|
|
158
|
+
let timer;
|
|
159
|
+
const observer = new IntersectionObserver((entries) => {
|
|
160
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
161
|
+
if (timer) clearTimeout(timer);
|
|
162
|
+
timer = void 0;
|
|
163
|
+
agentable.showScope(id);
|
|
164
|
+
} else if (!timer) timer = setTimeout(() => {
|
|
165
|
+
timer = void 0;
|
|
166
|
+
agentable.hideScope(id);
|
|
167
|
+
}, demoteAfter);
|
|
168
|
+
}, { rootMargin });
|
|
169
|
+
observer.observe(el);
|
|
170
|
+
return () => {
|
|
171
|
+
observer.disconnect();
|
|
172
|
+
if (timer) clearTimeout(timer);
|
|
173
|
+
agentable.showScope(id);
|
|
174
|
+
};
|
|
175
|
+
}, [
|
|
176
|
+
agentable,
|
|
177
|
+
id,
|
|
178
|
+
demoteAfter,
|
|
179
|
+
rootMargin
|
|
180
|
+
]);
|
|
181
|
+
const Element = Tag;
|
|
182
|
+
return /* @__PURE__ */ jsx(ScopeContext.Provider, {
|
|
183
|
+
value: id,
|
|
184
|
+
children: /* @__PURE__ */ jsx(GroupContext.Provider, {
|
|
185
|
+
value: group ?? parentGroup,
|
|
186
|
+
children: /* @__PURE__ */ jsx(Element, {
|
|
187
|
+
ref,
|
|
188
|
+
"data-agentable-scope": group ?? "",
|
|
189
|
+
className,
|
|
190
|
+
style,
|
|
191
|
+
children
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/fingerprint.ts
|
|
198
|
+
/** Static shape of a capability: everything except the execute closure. */
|
|
199
|
+
function fingerprint(cap) {
|
|
200
|
+
if (!cap) return "";
|
|
201
|
+
const t = cap.tool;
|
|
202
|
+
let schema = "";
|
|
203
|
+
if (t?.inputSchema) try {
|
|
204
|
+
schema = JSON.stringify(toJsonSchema(t.inputSchema));
|
|
205
|
+
} catch {
|
|
206
|
+
schema = "?";
|
|
207
|
+
}
|
|
208
|
+
let props = "";
|
|
209
|
+
if (cap.render?.props) try {
|
|
210
|
+
props = JSON.stringify(toJsonSchema(cap.render.props));
|
|
211
|
+
} catch {
|
|
212
|
+
props = "?";
|
|
213
|
+
}
|
|
214
|
+
return JSON.stringify([
|
|
215
|
+
cap.id,
|
|
216
|
+
cap.tier ?? 0,
|
|
217
|
+
cap.group ?? "",
|
|
218
|
+
cap.priority ?? 0,
|
|
219
|
+
cap.summary ?? "",
|
|
220
|
+
cap.fanIn ?? "",
|
|
221
|
+
t?.name ?? "",
|
|
222
|
+
t?.title ?? "",
|
|
223
|
+
t?.description ?? "",
|
|
224
|
+
schema,
|
|
225
|
+
t?.annotations ?? null,
|
|
226
|
+
t?.cache === false ? false : t?.cache ? {
|
|
227
|
+
ttl: t.cache.ttl,
|
|
228
|
+
tags: t.cache.tags
|
|
229
|
+
} : null,
|
|
230
|
+
t?.invalidates ?? null,
|
|
231
|
+
t?.output ?? null,
|
|
232
|
+
typeof t?.confirm === "function" ? "fn" : t?.confirm ?? null,
|
|
233
|
+
t?.exposedTo ?? null,
|
|
234
|
+
cap.render ? [
|
|
235
|
+
cap.render.component,
|
|
236
|
+
cap.render.description ?? "",
|
|
237
|
+
props
|
|
238
|
+
] : null
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/useCapability.ts
|
|
243
|
+
let warnedNoProvider = false;
|
|
244
|
+
/**
|
|
245
|
+
* Declare a capability for as long as the component is mounted.
|
|
246
|
+
*
|
|
247
|
+
* - Pass `null`/`undefined` to switch the agent layer off for this component.
|
|
248
|
+
* - The registry only sees a new declaration when the static shape changes;
|
|
249
|
+
* a fresh `execute` closure on every render costs nothing.
|
|
250
|
+
* - Safe under StrictMode (refcounted) and SSR (effects only).
|
|
251
|
+
*/
|
|
252
|
+
function useCapability(cap) {
|
|
253
|
+
const agentable = useContext(AgentableContext);
|
|
254
|
+
const scope = useContext(ScopeContext);
|
|
255
|
+
const inheritedGroup = useContext(GroupContext);
|
|
256
|
+
const latest = useRef(cap);
|
|
257
|
+
latest.current = cap;
|
|
258
|
+
const effectiveGroup = cap?.group ?? inheritedGroup;
|
|
259
|
+
const key = fingerprint(cap ? {
|
|
260
|
+
...cap,
|
|
261
|
+
group: effectiveGroup
|
|
262
|
+
} : null);
|
|
263
|
+
const stable = useMemo(() => {
|
|
264
|
+
const current = latest.current;
|
|
265
|
+
if (!current) return null;
|
|
266
|
+
const out = {
|
|
267
|
+
...current,
|
|
268
|
+
group: effectiveGroup
|
|
269
|
+
};
|
|
270
|
+
if (current.tool) {
|
|
271
|
+
const tool = {
|
|
272
|
+
...current.tool,
|
|
273
|
+
execute: (args, ctx) => {
|
|
274
|
+
const t = latest.current?.tool;
|
|
275
|
+
if (!t) throw new Error("Capability is no longer mounted.");
|
|
276
|
+
return t.execute(args, ctx);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
if (typeof current.tool.confirm === "function") tool.confirm = (args) => {
|
|
280
|
+
const c = latest.current?.tool?.confirm;
|
|
281
|
+
return typeof c === "function" ? c(args) : "Allow this action?";
|
|
282
|
+
};
|
|
283
|
+
if (current.tool.cache && typeof current.tool.cache === "object" && current.tool.cache.key) {
|
|
284
|
+
const fn = current.tool.cache.key;
|
|
285
|
+
tool.cache = {
|
|
286
|
+
...current.tool.cache,
|
|
287
|
+
key: (a) => fn(a)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
out.tool = tool;
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}, [key]);
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!stable) return;
|
|
296
|
+
if (!agentable) {
|
|
297
|
+
if (!warnedNoProvider && typeof console !== "undefined") {
|
|
298
|
+
warnedNoProvider = true;
|
|
299
|
+
console.warn(`[agentable] Capability "${stable.id}" declared outside <AgentableProvider>; the agent layer is off.`);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const handle = agentable.declare(stable, scope ? { scope } : {});
|
|
304
|
+
return () => handle.release();
|
|
305
|
+
}, [
|
|
306
|
+
agentable,
|
|
307
|
+
stable,
|
|
308
|
+
scope
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/useTool.ts
|
|
313
|
+
/** Register a hand-rolled tool with the same registry the components use. */
|
|
314
|
+
function useTool(tool) {
|
|
315
|
+
const { id, tier, group, priority, summary, fanIn, enabled = true, ...rest } = tool;
|
|
316
|
+
useCapability(enabled ? {
|
|
317
|
+
id: id ?? `tool.${tool.name}`,
|
|
318
|
+
tier,
|
|
319
|
+
group,
|
|
320
|
+
priority,
|
|
321
|
+
summary,
|
|
322
|
+
fanIn,
|
|
323
|
+
tool: rest
|
|
324
|
+
} : null);
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/snapshot.ts
|
|
328
|
+
const EMPTY = {
|
|
329
|
+
resident: [],
|
|
330
|
+
capabilities: [],
|
|
331
|
+
groups: [],
|
|
332
|
+
budget: {
|
|
333
|
+
max: 0,
|
|
334
|
+
used: 0,
|
|
335
|
+
metaTools: 0
|
|
336
|
+
},
|
|
337
|
+
cache: {
|
|
338
|
+
hits: 0,
|
|
339
|
+
misses: 0,
|
|
340
|
+
size: 0
|
|
341
|
+
},
|
|
342
|
+
adapters: [],
|
|
343
|
+
warnings: [],
|
|
344
|
+
estimatedTokens: 0,
|
|
345
|
+
agentPresent: false
|
|
346
|
+
};
|
|
347
|
+
/** Live registry snapshot. Re-renders on every registry change; use sparingly. */
|
|
348
|
+
function useAgentableSnapshot() {
|
|
349
|
+
const agentable = useContext(AgentableContext);
|
|
350
|
+
const subscribe = useCallback((cb) => agentable ? agentable.subscribe(() => cb()) : () => {}, [agentable]);
|
|
351
|
+
const get = useCallback(() => agentable ? agentable.snapshot() : EMPTY, [agentable]);
|
|
352
|
+
return useSyncExternalStore(subscribe, useStable(get), () => EMPTY);
|
|
353
|
+
}
|
|
354
|
+
function useStable(get) {
|
|
355
|
+
const ref = useRef(null);
|
|
356
|
+
return () => {
|
|
357
|
+
const next = get();
|
|
358
|
+
const key = JSON.stringify(next);
|
|
359
|
+
if (!ref.current || key !== ref.current.key) ref.current = {
|
|
360
|
+
key,
|
|
361
|
+
value: next
|
|
362
|
+
};
|
|
363
|
+
return ref.current.value;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/** True once the first agent tool call has arrived (the presence heuristic). */
|
|
367
|
+
function useAgentPresent() {
|
|
368
|
+
return useAgentableSnapshot().agentPresent;
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
export { AgentableContext, AgentableProvider, ConfirmDialog, GroupContext, ScopeContext, ToolScope, fingerprint, useAgentGroup, useAgentPresent, useAgentable, useAgentableSnapshot, useCapability, useTool };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentable-ui/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React bindings for @agentable-ui/core: AgentableProvider, useTool, useCapability, ToolScope.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/harsh071/agentable-ui",
|
|
23
|
+
"directory": "packages/react"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"webmcp",
|
|
27
|
+
"agent",
|
|
28
|
+
"mcp",
|
|
29
|
+
"components",
|
|
30
|
+
"react",
|
|
31
|
+
"modelContext"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@agentable-ui/core": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "^18.2.0 || ^19.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"react": "^19.2.8",
|
|
44
|
+
"react-dom": "^19.2.8",
|
|
45
|
+
"@types/react": "^19.2.18",
|
|
46
|
+
"@types/react-dom": "^19.2.7",
|
|
47
|
+
"@testing-library/react": "^16.3.3"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
}
|
|
54
|
+
}
|