@softr/sdk 0.0.87-alpha.3 → 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 +18 -10
- 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/formatting.d.ts +3 -3
- 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
|
@@ -10,18 +10,20 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
10
10
|
|
|
11
11
|
//#region src/agent/agent-fns.ts
|
|
12
12
|
const AGENTS_PRODUCTION = "https://agents-api.softr.io";
|
|
13
|
+
const PLATFORM_HOST = /^(platform-api|platform-preview-api|preview-api)(?=[.-])/;
|
|
13
14
|
/**
|
|
14
15
|
* Where the agents service is. It sits next to the platform API, so unless
|
|
15
16
|
* `SOFTR_AGENTS_API_URL` says otherwise, it's the platform's host with
|
|
16
|
-
* `agents-api` in place of
|
|
17
|
+
* `agents-api` in place of the platform's name. Live and preview share one
|
|
18
|
+
* agents service.
|
|
17
19
|
*/
|
|
18
20
|
function agentsApiUrl() {
|
|
19
21
|
const explicit = process.env.SOFTR_AGENTS_API_URL;
|
|
20
22
|
if (explicit) return explicit.replace(/\/+$/, "");
|
|
21
23
|
if (!process.env.SOFTR_PLATFORM_API_URL) return AGENTS_PRODUCTION;
|
|
22
24
|
const platform = new URL(softrApiUrl("SOFTR_PLATFORM_API_URL"));
|
|
23
|
-
assert(platform.hostname
|
|
24
|
-
platform.hostname = platform.hostname.replace(
|
|
25
|
+
assert(PLATFORM_HOST.test(platform.hostname), () => `can't tell the agents API from ${platform.origin}, set SOFTR_AGENTS_API_URL`);
|
|
26
|
+
platform.hostname = platform.hostname.replace(PLATFORM_HOST, "agents-api");
|
|
25
27
|
return platform.origin;
|
|
26
28
|
}
|
|
27
29
|
function agentsFetch(path, init = {}) {
|
|
@@ -45,6 +47,7 @@ const chatSchema = z.object({
|
|
|
45
47
|
sessionId: z.string().nullable(),
|
|
46
48
|
timezone: z.string(),
|
|
47
49
|
pageName: z.string(),
|
|
50
|
+
pageUrl: z.string(),
|
|
48
51
|
record: z.object({
|
|
49
52
|
id: z.string(),
|
|
50
53
|
fields: z.record(z.string(), z.unknown())
|
|
@@ -73,6 +76,7 @@ const agentChatFn = createServerFn({ method: "POST" }).validator((data) => {
|
|
|
73
76
|
...data.sessionId ? { sessionId: data.sessionId } : {},
|
|
74
77
|
context: {
|
|
75
78
|
pageName: data.pageName,
|
|
79
|
+
pageUrl: data.pageUrl,
|
|
76
80
|
...data.record ? { currentRecord: { data: data.record } } : {}
|
|
77
81
|
}
|
|
78
82
|
}
|
|
@@ -103,7 +107,7 @@ const listAgentChatsFn = createServerFn({ method: "POST" }).validator(z.object({
|
|
|
103
107
|
if (!res.ok) throw new Error(`Loading chats failed: ${await describeFailure(res)}`);
|
|
104
108
|
return z.array(z.object({
|
|
105
109
|
id: z.string(),
|
|
106
|
-
title: z.string()
|
|
110
|
+
title: z.string().nullable()
|
|
107
111
|
})).parse(await res.json());
|
|
108
112
|
});
|
|
109
113
|
const historySchema = z.object({
|
|
@@ -126,21 +130,23 @@ const historySchema = z.object({
|
|
|
126
130
|
})
|
|
127
131
|
}))
|
|
128
132
|
});
|
|
133
|
+
const CHAT_GONE = [
|
|
134
|
+
401,
|
|
135
|
+
403,
|
|
136
|
+
404,
|
|
137
|
+
410
|
|
138
|
+
];
|
|
129
139
|
/** What a past chat holds, or `null` when it's gone or belongs to someone else. */
|
|
130
140
|
const loadAgentChatFn = createServerFn({ method: "POST" }).validator(z.object({
|
|
131
141
|
agentId: z.string().min(1),
|
|
132
142
|
sessionId: z.string().min(1)
|
|
133
143
|
})).handler(async ({ data }) => {
|
|
134
144
|
const res = await agentsFetch(`${encodeURIComponent(data.agentId)}/sessions/${encodeURIComponent(data.sessionId)}`);
|
|
135
|
-
if (
|
|
136
|
-
401,
|
|
137
|
-
403,
|
|
138
|
-
404,
|
|
139
|
-
410
|
|
140
|
-
].includes(res.status)) return null;
|
|
145
|
+
if (CHAT_GONE.includes(res.status)) return null;
|
|
141
146
|
if (!res.ok) throw new Error(`Loading the chat failed: ${await describeFailure(res)}`);
|
|
142
147
|
return historySchema.parse(await res.json());
|
|
143
148
|
});
|
|
149
|
+
/** Saves the visitor's answer. `false` when the chat is gone, see `loadAgentChatFn`. */
|
|
144
150
|
const decideAgentToolFn = createServerFn({ method: "POST" }).validator(z.object({
|
|
145
151
|
agentId: z.string().min(1),
|
|
146
152
|
sessionId: z.string().min(1),
|
|
@@ -157,7 +163,9 @@ const decideAgentToolFn = createServerFn({ method: "POST" }).validator(z.object(
|
|
|
157
163
|
method: "POST",
|
|
158
164
|
body: decision
|
|
159
165
|
});
|
|
166
|
+
if (CHAT_GONE.includes(res.status)) return false;
|
|
160
167
|
if (!res.ok) throw new Error(`Saving the answer failed: ${await describeFailure(res)}`);
|
|
168
|
+
return true;
|
|
161
169
|
});
|
|
162
170
|
|
|
163
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/formatting.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react0 from "react";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/formatting.d.ts
|
|
@@ -94,7 +94,7 @@ declare function FormattingScope({
|
|
|
94
94
|
/** A BCP 47 locale, such as `de-DE`. It drops the app's fixed date format inside the scope. */
|
|
95
95
|
locale?: string;
|
|
96
96
|
children?: ReactNode;
|
|
97
|
-
}):
|
|
97
|
+
}): react0.FunctionComponentElement<react0.ProviderProps<Scope>>;
|
|
98
98
|
/**
|
|
99
99
|
* A date in the app's format, safe to render on the server. A timestamp whose
|
|
100
100
|
* zone is not known yet renders `fallback` until the browser supplies it, so
|
|
@@ -115,6 +115,6 @@ declare function FormattedDate({
|
|
|
115
115
|
time?: boolean;
|
|
116
116
|
/** What renders while the visitor's zone is unknown. */
|
|
117
117
|
fallback?: ReactNode;
|
|
118
|
-
}):
|
|
118
|
+
}): react0.FunctionComponentElement<react0.FragmentProps>;
|
|
119
119
|
//#endregion
|
|
120
120
|
export { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting };
|
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.
|