@softr/sdk 0.0.87-alpha.4 → 0.0.87-alpha.5
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/agent/agent-fns.d.ts +2 -2
- package/dist/agent/agent-fns.js +13 -7
- package/dist/agent/use-agent-chat.d.ts +6 -0
- package/dist/agent/use-agent-chat.js +61 -9
- package/dist/default-error-overlay.d.ts +2 -2
- package/dist/package.js +1 -1
- package/dist/pwa.d.ts +2 -2
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +1 -1
- package/skills/softr-sdk/agent-chat/SKILL.md +21 -4
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
|
@@ -21,10 +21,10 @@ declare const listAgents: _tanstack_react_start7.OptionalFetcher<undefined, unde
|
|
|
21
21
|
avatar?: string | null | undefined;
|
|
22
22
|
thumbnail?: string | null | undefined;
|
|
23
23
|
}[]>>;
|
|
24
|
-
/** One past chat with an agent. */
|
|
24
|
+
/** One past chat with an agent. `title` is null until the agent names the chat. */
|
|
25
25
|
type AgentChatSummary = {
|
|
26
26
|
id: string;
|
|
27
|
-
title: string;
|
|
27
|
+
title: string | null;
|
|
28
28
|
};
|
|
29
29
|
//#endregion
|
|
30
30
|
export { Agent, AgentChatSummary, listAgents };
|
package/dist/agent/agent-fns.js
CHANGED
|
@@ -47,6 +47,7 @@ const chatSchema = z.object({
|
|
|
47
47
|
sessionId: z.string().nullable(),
|
|
48
48
|
timezone: z.string(),
|
|
49
49
|
pageName: z.string(),
|
|
50
|
+
pageUrl: z.string(),
|
|
50
51
|
record: z.object({
|
|
51
52
|
id: z.string(),
|
|
52
53
|
fields: z.record(z.string(), z.unknown())
|
|
@@ -75,6 +76,7 @@ const agentChatFn = createServerFn({ method: "POST" }).validator((data) => {
|
|
|
75
76
|
...data.sessionId ? { sessionId: data.sessionId } : {},
|
|
76
77
|
context: {
|
|
77
78
|
pageName: data.pageName,
|
|
79
|
+
pageUrl: data.pageUrl,
|
|
78
80
|
...data.record ? { currentRecord: { data: data.record } } : {}
|
|
79
81
|
}
|
|
80
82
|
}
|
|
@@ -105,7 +107,7 @@ const listAgentChatsFn = createServerFn({ method: "POST" }).validator(z.object({
|
|
|
105
107
|
if (!res.ok) throw new Error(`Loading chats failed: ${await describeFailure(res)}`);
|
|
106
108
|
return z.array(z.object({
|
|
107
109
|
id: z.string(),
|
|
108
|
-
title: z.string()
|
|
110
|
+
title: z.string().nullable()
|
|
109
111
|
})).parse(await res.json());
|
|
110
112
|
});
|
|
111
113
|
const historySchema = z.object({
|
|
@@ -128,21 +130,23 @@ const historySchema = z.object({
|
|
|
128
130
|
})
|
|
129
131
|
}))
|
|
130
132
|
});
|
|
133
|
+
const CHAT_GONE = [
|
|
134
|
+
401,
|
|
135
|
+
403,
|
|
136
|
+
404,
|
|
137
|
+
410
|
|
138
|
+
];
|
|
131
139
|
/** What a past chat holds, or `null` when it's gone or belongs to someone else. */
|
|
132
140
|
const loadAgentChatFn = createServerFn({ method: "POST" }).validator(z.object({
|
|
133
141
|
agentId: z.string().min(1),
|
|
134
142
|
sessionId: z.string().min(1)
|
|
135
143
|
})).handler(async ({ data }) => {
|
|
136
144
|
const res = await agentsFetch(`${encodeURIComponent(data.agentId)}/sessions/${encodeURIComponent(data.sessionId)}`);
|
|
137
|
-
if (
|
|
138
|
-
401,
|
|
139
|
-
403,
|
|
140
|
-
404,
|
|
141
|
-
410
|
|
142
|
-
].includes(res.status)) return null;
|
|
145
|
+
if (CHAT_GONE.includes(res.status)) return null;
|
|
143
146
|
if (!res.ok) throw new Error(`Loading the chat failed: ${await describeFailure(res)}`);
|
|
144
147
|
return historySchema.parse(await res.json());
|
|
145
148
|
});
|
|
149
|
+
/** Saves the visitor's answer. `false` when the chat is gone, see `loadAgentChatFn`. */
|
|
146
150
|
const decideAgentToolFn = createServerFn({ method: "POST" }).validator(z.object({
|
|
147
151
|
agentId: z.string().min(1),
|
|
148
152
|
sessionId: z.string().min(1),
|
|
@@ -159,7 +163,9 @@ const decideAgentToolFn = createServerFn({ method: "POST" }).validator(z.object(
|
|
|
159
163
|
method: "POST",
|
|
160
164
|
body: decision
|
|
161
165
|
});
|
|
166
|
+
if (CHAT_GONE.includes(res.status)) return false;
|
|
162
167
|
if (!res.ok) throw new Error(`Saving the answer failed: ${await describeFailure(res)}`);
|
|
168
|
+
return true;
|
|
163
169
|
});
|
|
164
170
|
|
|
165
171
|
//#endregion
|
|
@@ -15,6 +15,10 @@ type AgentToolCall = {
|
|
|
15
15
|
status: "running" | "done" | "failed" | "awaiting-approval" | "approved" | "rejected" | "awaiting-connection";
|
|
16
16
|
error?: string;
|
|
17
17
|
integration?: string;
|
|
18
|
+
/** Why the agent calls it, in its own words. */
|
|
19
|
+
thought?: string;
|
|
20
|
+
/** How long a finished call took. */
|
|
21
|
+
durationMs?: number;
|
|
18
22
|
};
|
|
19
23
|
type AgentTodo = {
|
|
20
24
|
content: string;
|
|
@@ -55,6 +59,8 @@ type AgentChat = {
|
|
|
55
59
|
messages: AgentMessage[];
|
|
56
60
|
/** `thinking` until the first part of the reply arrives, `replying` until it ends. */
|
|
57
61
|
status: "idle" | "thinking" | "replying";
|
|
62
|
+
/** True while a past chat loads, including the one reopened on mount. */
|
|
63
|
+
loading: boolean;
|
|
58
64
|
/** Why the last request failed. Cleared by the next one. */
|
|
59
65
|
error: Error | null;
|
|
60
66
|
/** The chat the next message goes to, `null` for a new one. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assert } from "../assertions.js";
|
|
2
|
+
import { useCurrentUser } from "../auth/isomorphic.js";
|
|
2
3
|
import { agentChatFn, decideAgentToolFn, listAgentChatsFn, loadAgentChatFn } from "./agent-fns.js";
|
|
3
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
5
|
|
|
@@ -15,8 +16,10 @@ function useAgentChat(agentId, options = {}) {
|
|
|
15
16
|
const [messages, setMessages] = useState([]);
|
|
16
17
|
const [live, setLive] = useState(null);
|
|
17
18
|
const [status, setStatus] = useState("idle");
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
18
20
|
const [error, setError] = useState(null);
|
|
19
21
|
const [chatId, setChatId] = useState(null);
|
|
22
|
+
const storeKey = chatKey(agentId, useCurrentUser()?.id ?? null);
|
|
20
23
|
const chatIdRef = useRef(chatId);
|
|
21
24
|
chatIdRef.current = chatId;
|
|
22
25
|
const recordRef = useRef(options.record ?? null);
|
|
@@ -48,6 +51,7 @@ function useAgentChat(agentId, options = {}) {
|
|
|
48
51
|
sessionId: chatIdRef.current,
|
|
49
52
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
50
53
|
pageName: document.title,
|
|
54
|
+
pageUrl: location.href,
|
|
51
55
|
record: recordRef.current
|
|
52
56
|
};
|
|
53
57
|
const form = new FormData();
|
|
@@ -67,6 +71,7 @@ function useAgentChat(agentId, options = {}) {
|
|
|
67
71
|
if (event.sessionId && event.sessionId !== chatIdRef.current) {
|
|
68
72
|
chatIdRef.current = event.sessionId;
|
|
69
73
|
setChatId(event.sessionId);
|
|
74
|
+
saveChatId(storeKey, event.sessionId);
|
|
70
75
|
}
|
|
71
76
|
blocks = reduceEvent(blocks, event);
|
|
72
77
|
setLive(blocks);
|
|
@@ -89,7 +94,11 @@ function useAgentChat(agentId, options = {}) {
|
|
|
89
94
|
abortRef.current = null;
|
|
90
95
|
commitLive(blocks);
|
|
91
96
|
setStatus("idle");
|
|
92
|
-
}, [
|
|
97
|
+
}, [
|
|
98
|
+
agentId,
|
|
99
|
+
storeKey,
|
|
100
|
+
commitLive
|
|
101
|
+
]);
|
|
93
102
|
const send = useCallback((text) => {
|
|
94
103
|
const trimmed = text.trim();
|
|
95
104
|
if (!trimmed) return;
|
|
@@ -117,14 +126,17 @@ function useAgentChat(agentId, options = {}) {
|
|
|
117
126
|
abortRef.current = null;
|
|
118
127
|
chatIdRef.current = id;
|
|
119
128
|
setChatId(id);
|
|
129
|
+
saveChatId(storeKey, id);
|
|
120
130
|
setMessages([]);
|
|
121
131
|
setLive(null);
|
|
122
132
|
setStatus("idle");
|
|
133
|
+
setLoading(false);
|
|
123
134
|
setError(null);
|
|
124
|
-
}, []);
|
|
135
|
+
}, [storeKey]);
|
|
125
136
|
const newChat = useCallback(() => reset(null), [reset]);
|
|
126
|
-
const
|
|
137
|
+
const load = useCallback((id, restoring) => {
|
|
127
138
|
reset(id);
|
|
139
|
+
setLoading(true);
|
|
128
140
|
loadAgentChatFn({ data: {
|
|
129
141
|
agentId,
|
|
130
142
|
sessionId: id
|
|
@@ -132,14 +144,27 @@ function useAgentChat(agentId, options = {}) {
|
|
|
132
144
|
if (chatIdRef.current !== id) return;
|
|
133
145
|
if (history === null) {
|
|
134
146
|
reset(null);
|
|
135
|
-
setError(
|
|
147
|
+
if (!restoring) setError(new Error(CHAT_GONE_MESSAGE));
|
|
136
148
|
return;
|
|
137
149
|
}
|
|
150
|
+
setLoading(false);
|
|
138
151
|
setMessages(fromHistory(history));
|
|
139
152
|
}).catch((err) => {
|
|
140
|
-
if (chatIdRef.current
|
|
153
|
+
if (chatIdRef.current !== id) return;
|
|
154
|
+
setLoading(false);
|
|
155
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
141
156
|
});
|
|
142
157
|
}, [agentId, reset]);
|
|
158
|
+
const openChat = useCallback((id) => load(id, false), [load]);
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const saved = savedChatId(storeKey);
|
|
161
|
+
if (saved) load(saved, true);
|
|
162
|
+
else newChat();
|
|
163
|
+
}, [
|
|
164
|
+
storeKey,
|
|
165
|
+
load,
|
|
166
|
+
newChat
|
|
167
|
+
]);
|
|
143
168
|
const listChats = useCallback(() => listAgentChatsFn({ data: { agentId } }), [agentId]);
|
|
144
169
|
const setToolStatus = useCallback((toolCallId, next) => {
|
|
145
170
|
const update = (blocks) => blocks.map((b) => b.type === "tool" && b.id === toolCallId ? {
|
|
@@ -164,7 +189,12 @@ function useAgentChat(agentId, options = {}) {
|
|
|
164
189
|
toolName: tool.name,
|
|
165
190
|
decision,
|
|
166
191
|
scope
|
|
167
|
-
} }).then(() => {
|
|
192
|
+
} }).then((saved) => {
|
|
193
|
+
if (!saved) {
|
|
194
|
+
reset(null);
|
|
195
|
+
setError(new Error(CHAT_GONE_MESSAGE));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
168
198
|
if (decision !== "APPROVE") return;
|
|
169
199
|
abortRef.current?.abort();
|
|
170
200
|
commitLive(liveRef.current);
|
|
@@ -178,7 +208,8 @@ function useAgentChat(agentId, options = {}) {
|
|
|
178
208
|
messages,
|
|
179
209
|
setToolStatus,
|
|
180
210
|
commitLive,
|
|
181
|
-
stream
|
|
211
|
+
stream,
|
|
212
|
+
reset
|
|
182
213
|
]);
|
|
183
214
|
const approve = useCallback((toolCallId, scope = "ONCE") => decide(toolCallId, "APPROVE", scope), [decide]);
|
|
184
215
|
const reject = useCallback((toolCallId) => decide(toolCallId, "REJECT", "ONCE"), [decide]);
|
|
@@ -189,6 +220,7 @@ function useAgentChat(agentId, options = {}) {
|
|
|
189
220
|
blocks: live
|
|
190
221
|
}],
|
|
191
222
|
status,
|
|
223
|
+
loading,
|
|
192
224
|
error,
|
|
193
225
|
chatId,
|
|
194
226
|
send,
|
|
@@ -231,20 +263,23 @@ function reduceEvent(blocks, event) {
|
|
|
231
263
|
if (event.toolName === QUESTION_TOOL) return blocks;
|
|
232
264
|
if (blocks.some((b) => b.type === "tool" && b.id === event.toolCallId)) return updateTool(blocks, event.toolCallId, (b) => b.status === "running" ? {
|
|
233
265
|
...b,
|
|
234
|
-
input: event.input ?? b.input
|
|
266
|
+
input: event.input ?? b.input,
|
|
267
|
+
thought: event.innerThought ?? b.thought
|
|
235
268
|
} : b);
|
|
236
269
|
return [...blocks, {
|
|
237
270
|
type: "tool",
|
|
238
271
|
id: event.toolCallId,
|
|
239
272
|
name: event.toolName,
|
|
240
273
|
input: event.input,
|
|
274
|
+
thought: event.innerThought,
|
|
241
275
|
integration: event.integrationType,
|
|
242
276
|
status: "running"
|
|
243
277
|
}];
|
|
244
278
|
case "TOOL_CALL_END": return updateTool(blocks, event.toolCallId, (b) => b.status === "running" || b.status === "approved" ? {
|
|
245
279
|
...b,
|
|
246
280
|
status: event.failed ? "failed" : "done",
|
|
247
|
-
error: event.errorMessage
|
|
281
|
+
error: event.errorMessage,
|
|
282
|
+
durationMs: event.durationMs
|
|
248
283
|
} : b);
|
|
249
284
|
case "TOOL_PERMISSION_REQUEST":
|
|
250
285
|
case "INTEGRATION_AUTH_REQUEST": {
|
|
@@ -343,6 +378,8 @@ function fromHistory(history) {
|
|
|
343
378
|
id: call.id,
|
|
344
379
|
name: call.toolName,
|
|
345
380
|
input: call.input,
|
|
381
|
+
thought: call.metadata.innerThought ?? void 0,
|
|
382
|
+
durationMs: call.metadata.durationMs ?? void 0,
|
|
346
383
|
status: call.metadata.failed ? "failed" : "done"
|
|
347
384
|
});
|
|
348
385
|
}
|
|
@@ -357,6 +394,21 @@ function fromHistory(history) {
|
|
|
357
394
|
};
|
|
358
395
|
});
|
|
359
396
|
}
|
|
397
|
+
const CHAT_GONE_MESSAGE = "This chat is no longer available. Start a new one.";
|
|
398
|
+
const chatKey = (agentId, userId) => `softr-agent-chat:${agentId}:${userId ?? "visitor"}`;
|
|
399
|
+
function savedChatId(key) {
|
|
400
|
+
try {
|
|
401
|
+
return localStorage.getItem(key);
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function saveChatId(key, chatId) {
|
|
407
|
+
try {
|
|
408
|
+
if (chatId) localStorage.setItem(key, chatId);
|
|
409
|
+
else localStorage.removeItem(key);
|
|
410
|
+
} catch {}
|
|
411
|
+
}
|
|
360
412
|
async function failureMessage(res) {
|
|
361
413
|
const body = await res.json().catch(() => null);
|
|
362
414
|
if (typeof body === "object" && body !== null && "message" in body && typeof body.message === "string") return body.message;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react4 from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/default-error-overlay.d.ts
|
|
4
4
|
type Props = {
|
|
@@ -8,6 +8,6 @@ type Props = {
|
|
|
8
8
|
declare function DefaultErrorOverlay({
|
|
9
9
|
error,
|
|
10
10
|
reset
|
|
11
|
-
}: Props):
|
|
11
|
+
}: Props): react4.JSX.Element;
|
|
12
12
|
//#endregion
|
|
13
13
|
export { DefaultErrorOverlay };
|
package/dist/package.js
CHANGED
package/dist/pwa.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react5 from "react";
|
|
2
2
|
import "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/pwa.d.ts
|
|
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
|
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
25
|
*/
|
|
26
|
-
declare function SoftrPwaHead():
|
|
26
|
+
declare function SoftrPwaHead(): react5.FunctionComponentElement<react5.FragmentProps>;
|
|
27
27
|
type PwaPlatform = "ios" | "android" | "desktop";
|
|
28
28
|
type PwaInstall = {
|
|
29
29
|
available: false;
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.87-alpha.
|
|
13
|
+
library_version: 0.0.87-alpha.5
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `listAgents` and `useAgentChat` live on `@softr/sdk`. `listAgents` runs in a route loader, `useAgentChat` in a component. Neither needs a route, server function or registration of its own. It chats as the signed-in visitor, or anonymously when no one is signed in.
|
|
@@ -58,9 +58,10 @@ function AgentPanel({ agentId, ticket }: { agentId: string; ticket: Ticket | nul
|
|
|
58
58
|
|
|
59
59
|
- `messages`: the whole chat, oldest first. While the agent answers, its reply is the last message and grows as it streams.
|
|
60
60
|
- `status`: `thinking` until the first part of the reply arrives, `replying` until it ends, then `idle`. Show a typing hint while it's `thinking`.
|
|
61
|
+
- `loading`: `true` while a past chat loads. Show a spinner, not the empty-chat greeting.
|
|
61
62
|
- `error`: why the last request failed, or `null`.
|
|
62
63
|
- `send(text)`, `stop()`, `newChat()`.
|
|
63
|
-
- `listChats()` resolves to the visitor's past chats, `{ id, title }[]
|
|
64
|
+
- `listChats()` resolves to the visitor's past chats, `{ id, title }[]`, with `title` `null` until the agent names the chat. `openChat(id)` loads one. `chatId` is the open one, `null` for a new chat. The open chat is kept in the browser for each visitor, so the hook reopens it after a remount or a reload.
|
|
64
65
|
- `approve(toolCallId, scope?)` and `reject(toolCallId)` answer a tool waiting for approval. `scope` is `ONCE` (default), `SESSION` for the rest of this chat, or `USER` for always.
|
|
65
66
|
|
|
66
67
|
## Rendering a message
|
|
@@ -70,7 +71,7 @@ Each message has `role` (`user` or `assistant`) and `blocks`, in the order the a
|
|
|
70
71
|
| Block | Show it as |
|
|
71
72
|
| --- | --- |
|
|
72
73
|
| `text` | Markdown text |
|
|
73
|
-
| `tool` | One line: the tool `name` and its `status`. Collapse finished ones. |
|
|
74
|
+
| `tool` | One line: the tool `name` and its `status`. Collapse finished ones. Expanded, show `thought`, `input`, `error` and `durationMs`. |
|
|
74
75
|
| `todo` | A checklist of the agent's plan. It updates in place. |
|
|
75
76
|
| `question` | The questions with their options. A click on an option sends its label with `send`. |
|
|
76
77
|
| `error` | An error line inside the reply |
|
|
@@ -79,11 +80,25 @@ A `tool` with status `awaiting-approval` blocks the agent until the visitor answ
|
|
|
79
80
|
|
|
80
81
|
A `tool` with status `awaiting-connection` needs the visitor to connect the `integration` first. Code apps can't do that yet. Say which integration the agent needs, and that the builder has to connect it for this agent in Studio.
|
|
81
82
|
|
|
83
|
+
## The chat panel
|
|
84
|
+
|
|
85
|
+
Classic Softr apps ship a chat panel. Visitors expect the same from yours:
|
|
86
|
+
|
|
87
|
+
- A round button in a corner opens it. A right-side panel on desktop, full screen on mobile.
|
|
88
|
+
- The header shows the agent's avatar and name, a picker when there are several agents, new chat, past chats and close.
|
|
89
|
+
- An empty chat greets the visitor with the agent's avatar.
|
|
90
|
+
- The list scrolls to the newest message as the reply streams, until the visitor scrolls up. Then a button takes them back down.
|
|
91
|
+
- Text renders as Markdown, tables included.
|
|
92
|
+
- The input says "Ask {agent title}…". Enter sends. Shift+Enter adds a line.
|
|
93
|
+
- A line under the input: "AI can make mistakes. Please double-check responses."
|
|
94
|
+
- A question with one choice sends on click. One with `multiSelect` or several questions sends once all are answered. An answered question stays locked.
|
|
95
|
+
- Approve offers the three scopes: once, for this chat, always for this agent.
|
|
96
|
+
|
|
82
97
|
## Context
|
|
83
98
|
|
|
84
99
|
`record` is the one record the visitor is looking at, so they can say "close this one". Pass it where a page shows one record, and `null` elsewhere. Use readable field names as keys. The agent reads them as they are.
|
|
85
100
|
|
|
86
|
-
The page title
|
|
101
|
+
The page title and URL go along on their own. Nothing else about the page does, so don't try to send whole lists.
|
|
87
102
|
|
|
88
103
|
## Letting the agent act on the app
|
|
89
104
|
|
|
@@ -95,6 +110,8 @@ The agent reads and changes data through the app's own MCP, as the visitor who i
|
|
|
95
110
|
|
|
96
111
|
**Rendering only `text` blocks.** A tool waiting for approval never shows, and the agent waits forever. Render every block type.
|
|
97
112
|
|
|
113
|
+
**The hook inside a panel that unmounts when it closes.** Closing it stops a reply that is still coming. Call `useAgentChat` in the component that owns the open state, such as the layout, and pass `chat` into the panel. Keep the selected agent there too, or closing the panel resets it.
|
|
114
|
+
|
|
98
115
|
**An agent id written by hand.** The chat fails on the first message, and an agent hidden from the visitor still shows. Take the id from `listAgents`.
|
|
99
116
|
|
|
100
117
|
**Expecting the agent to change data with no MCP wired.** It answers but can't act. List the actions it needs in `softrMcpMiddleware`.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.87-alpha.
|
|
12
|
+
library_version: 0.0.87-alpha.5
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.87-alpha.
|
|
12
|
+
library_version: 0.0.87-alpha.5
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
metadata:
|
|
8
8
|
type: sub-skill
|
|
9
9
|
library: softr-sdk
|
|
10
|
-
library_version: 0.0.87-alpha.
|
|
10
|
+
library_version: 0.0.87-alpha.5
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
**Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
metadata:
|
|
8
8
|
type: sub-skill
|
|
9
9
|
library: softr-sdk
|
|
10
|
-
library_version: 0.0.87-alpha.
|
|
10
|
+
library_version: 0.0.87-alpha.5
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
**Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.87-alpha.
|
|
12
|
+
library_version: 0.0.87-alpha.5
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
|
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
metadata:
|
|
9
9
|
type: sub-skill
|
|
10
10
|
library: softr-sdk
|
|
11
|
-
library_version: 0.0.87-alpha.
|
|
11
|
+
library_version: 0.0.87-alpha.5
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.87-alpha.
|
|
12
|
+
library_version: 0.0.87-alpha.5
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. The same holds for any helper that calls `ds`: declare it inside the handler. A function at module level that calls `ds` keeps the server-only import in the browser bundle, and every page stops loading until it moves. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
metadata:
|
|
9
9
|
type: sub-skill
|
|
10
10
|
library: softr-sdk
|
|
11
|
-
library_version: 0.0.87-alpha.
|
|
11
|
+
library_version: 0.0.87-alpha.5
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
**Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.87-alpha.
|
|
12
|
+
library_version: 0.0.87-alpha.5
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.
|