@blocksdiy/react-common 1.39.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/new-agent-chat.d.ts +7 -0
- package/dist/components/new-agent-chat.d.ts.map +1 -1
- package/dist/components/new-agent-chat.js +95 -9
- package/dist/hooks/agent-thread-live-update.d.ts +72 -0
- package/dist/hooks/agent-thread-live-update.d.ts.map +1 -0
- package/dist/hooks/agent-thread-live-update.js +115 -0
- package/dist/hooks/use-agent-threads.d.ts +97 -0
- package/dist/hooks/use-agent-threads.d.ts.map +1 -0
- package/dist/hooks/use-agent-threads.js +510 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +2 -1
|
@@ -114,6 +114,13 @@ export interface AgentChatRootProps {
|
|
|
114
114
|
* between an app that can draw these pauses and one that cannot.
|
|
115
115
|
*/
|
|
116
116
|
supportsServerInterrupt?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Fires once a user send has been added to the conversation, with its text (empty
|
|
119
|
+
* string for attachment-only sends) — the seam for ChatGPT-style optimistic thread
|
|
120
|
+
* listing: hosts pair it with useAgentThreads' notifyThreadActivity. Covers sends
|
|
121
|
+
* routed through `sendMessage`, not the auto-sent `initialPrompt`.
|
|
122
|
+
*/
|
|
123
|
+
onUserMessageSent?: (text: string) => void;
|
|
117
124
|
}
|
|
118
125
|
export declare const AgentChatRoot: ({ ...props }: AgentChatRootProps) => import("react").JSX.Element | null;
|
|
119
126
|
export interface AgentChatMessageProps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"new-agent-chat.d.ts","sourceRoot":"","sources":["../../src/components/new-agent-chat.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"new-agent-chat.d.ts","sourceRoot":"","sources":["../../src/components/new-agent-chat.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAI5E,OAAO,EAAiB,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAEL,SAAS,EACT,cAAc,EAQf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAEzE,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACtF,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,cAAc,GACf,MAAM,2BAA2B,CAAC;AAOnC,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,WAEzD,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,aAAa,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqRD,MAAM,WAAW,qBAAqB;IAEpC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC,aAAa,EAAE;QAAE,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAAA;KAAE,EAAE,CAAC;IACtE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,cAAc,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IAGrD,WAAW,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,MAAM,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,oFAAoF;IACpF,4EAA4E;IAC5E,gBAAgB,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,gFAAgF;IAChF,eAAe,EAAE,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,cAAc,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IACpD,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAG7B,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,cAAc,EAAE,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;IAC7D,kBAAkB,EAAE,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,kBAAkB,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAED,eAAO,MAAM,gBAAgB,uDAAoD,CAAC;AAElF,eAAO,MAAM,YAAY,6BAMxB,CAAC;AAmoBF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mGAAmG;IACnG,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,GAAG,CAAC;IAClB,gBAAgB,CAAC,EAAE,UAAU,EAAE,CAAC;IAChC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;OASG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAI5C;AAED,eAAO,MAAM,aAAa,GAAI,cAK3B,kBAAkB,uCA8LpB,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,qBAAqB,sCAMjH;AAED,MAAM,WAAW,0BAA0B;IACzC,YAAY,EAAE;QAAE,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAAA;KAAE,CAAC;CACpE;AAED,wBAAgB,qBAAqB,CAAC,EACpC,YAAY,EACZ,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,0BAA0B,+BAS1D;AAED,MAAM,WAAW,sBAAsB;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,mBAAmB,EACnB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,sBAAsB,+BAmCtD;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAe,EACf,MAAM,EACN,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,sBAAsB,sCAUtD;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAe,EACf,MAAM,EACN,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,sBAAsB,sCA+BtD;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,mBAAmB,CAAC,KAAK,IAAI,CAAC;CAC9D;AAED,wBAAgB,cAAc,CAAC,EAC7B,OAAe,EACf,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,EACP,UAAU,EACV,WAAW,EACX,MAAM,EACN,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,GAAG,mBAAmB,+BAgKxD;AACD,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,OAAe,EACf,MAAM,EACN,OAAO,EACP,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,yBAAyB,+BAoC5D;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,0BAA0B,CAAC,EACzC,OAAe,EACf,MAAM,EACN,OAAO,EACP,QAAQ,EACR,MAAM,EACN,QAAe,EACf,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,+BAA+B,+BAmElE;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,UAAU,EACV,OAAe,EACf,MAAM,EACN,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,wBAAwB,+BAWxD;AAED,MAAM,WAAW,8BAA8B;IAC7C,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,wBAAgB,yBAAyB,CAAC,EACxC,UAAU,EACV,OAAe,EACf,MAAM,EACN,OAAO,EACP,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,8BAA8B,+BAgBjE"}
|
|
@@ -242,7 +242,7 @@ const SessionReadables = () => {
|
|
|
242
242
|
// });
|
|
243
243
|
return null;
|
|
244
244
|
};
|
|
245
|
-
const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, components, currentThreadId, setCurrentThreadId, children, }) => {
|
|
245
|
+
const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, components, currentThreadId, setCurrentThreadId, children, onUserMessageSent, }) => {
|
|
246
246
|
const [agentChat, setAgentChat] = useState(null);
|
|
247
247
|
const [agent, setAgent] = useState(null);
|
|
248
248
|
// `isConfigLoaded` covers the (small, parallel) async fetch of the
|
|
@@ -313,9 +313,9 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
313
313
|
// prop only feeds React context, but `agent.threadId` is what hits the
|
|
314
314
|
// wire and the LangGraph checkpoint. `<CopilotChat>` does this for you;
|
|
315
315
|
// we use the headless hook so we do it ourselves. Layout-effect timing
|
|
316
|
-
// is required to beat the library's connect-on-mount effect.
|
|
317
|
-
//
|
|
318
|
-
//
|
|
316
|
+
// is required to beat the library's connect-on-mount/on-swap effect.
|
|
317
|
+
// A swap doesn't abort an in-flight run — the connect effect's cleanup
|
|
318
|
+
// detaches it client-side, and the run completes server-side as usual.
|
|
319
319
|
useIsomorphicLayoutEffect(() => {
|
|
320
320
|
if (copilotAgent && currentThreadId) {
|
|
321
321
|
copilotAgent.threadId = currentThreadId;
|
|
@@ -326,13 +326,75 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
326
326
|
}, [copilotAgent, currentThreadId]);
|
|
327
327
|
const [prompt, setPrompt] = useState("");
|
|
328
328
|
const [attachments, setAttachments] = useState([]);
|
|
329
|
+
// Conversation identity. When a mounted chat is pointed at another conversation, the
|
|
330
|
+
// reconnect replay takes a round-trip — clear the previous transcript and composer
|
|
331
|
+
// immediately so the old conversation never lingers on screen (same visual contract as
|
|
332
|
+
// a remount). Deliberately NOT keyed on `initKey`: that also changes when the block
|
|
333
|
+
// configs finish loading, and clearing then would wipe a freshly replayed transcript.
|
|
334
|
+
const conversationKey = `${chatId ?? ""}|${currentThreadId ?? ""}`;
|
|
335
|
+
const prevConversationKeyRef = useRef(conversationKey);
|
|
336
|
+
useIsomorphicLayoutEffect(() => {
|
|
337
|
+
if (prevConversationKeyRef.current === conversationKey) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
prevConversationKeyRef.current = conversationKey;
|
|
341
|
+
copilotAgent?.setMessages?.([]);
|
|
342
|
+
setPrompt("");
|
|
343
|
+
setAttachments([]);
|
|
344
|
+
// The clear above runs in the layout phase, but the previous conversation's run is
|
|
345
|
+
// detached in passive effects — and even past detach, events already buffered in the
|
|
346
|
+
// apply pipeline keep applying, each reassigning `agent.messages` WHOLESALE. One late
|
|
347
|
+
// chunk would resurrect the entire previous transcript (and re-arm its auto-executing
|
|
348
|
+
// tool-call follow-ups) under the new conversation's header. Detach here too and
|
|
349
|
+
// re-clear once the drain settles; the reconnect that follows replays on top of the
|
|
350
|
+
// (guaranteed-empty) mirror. On threadId switches the core's fresh-restore re-clears
|
|
351
|
+
// as well — harmless overlap — but on chatId-only switches this is the only re-clear.
|
|
352
|
+
const detachable = copilotAgent;
|
|
353
|
+
const drained = detachable?.detachActiveRun?.();
|
|
354
|
+
if (drained) {
|
|
355
|
+
const clearedForKey = conversationKey;
|
|
356
|
+
void drained.then(() => {
|
|
357
|
+
if (prevConversationKeyRef.current === clearedForKey) {
|
|
358
|
+
copilotAgent?.setMessages?.([]);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}, [conversationKey]);
|
|
363
|
+
// CopilotKit reconnects only when its threadId changes — but a conversation is the
|
|
364
|
+
// (chatId, threadId) PAIR, and rows written by surfaces that use the fixed default
|
|
365
|
+
// thread id differ only by chatId. On such a chatId-only switch the library's connect
|
|
366
|
+
// effect sees unchanged deps and does nothing, which would leave the (just cleared)
|
|
367
|
+
// transcript empty forever. Reconnect manually, replicating the core's fresh-restore
|
|
368
|
+
// steps: reset state and drop the replay cursor — cursors are keyed by threadId, so
|
|
369
|
+
// the shared id makes the previous conversation's cursor actively wrong here — then
|
|
370
|
+
// connect (the core detaches any in-flight run itself). The core reads `properties`
|
|
371
|
+
// after an await, so the new chatId is already synced by the time the request builds.
|
|
372
|
+
const prevConnectionRef = useRef({ chatId, threadId: currentThreadId });
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
const previous = prevConnectionRef.current;
|
|
375
|
+
prevConnectionRef.current = { chatId, threadId: currentThreadId };
|
|
376
|
+
const chatIdChanged = previous.chatId !== chatId;
|
|
377
|
+
const threadIdChanged = previous.threadId !== currentThreadId;
|
|
378
|
+
if (!copilotAgent || !chatIdChanged || threadIdChanged) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
copilotAgent.setState?.({});
|
|
382
|
+
copilotAgent.clearReplayCursor?.(currentThreadId);
|
|
383
|
+
// Same guard convention as the composer send path: a failed reconnect already
|
|
384
|
+
// surfaces through the availability state; the rejection carries no extra signal.
|
|
385
|
+
void copilotkit.connectAgent({ agent: copilotAgent }).catch(() => undefined);
|
|
386
|
+
}, [chatId, currentThreadId, copilotAgent, copilotkit]);
|
|
329
387
|
const [isDraggingFiles, setIsDraggingFiles] = useState(false);
|
|
330
388
|
const attachmentsEnabled = agentChat ? agentChat.disableAttachments !== true : false;
|
|
331
389
|
const canSendMessage = Boolean(agentChatBlock && agentChat && agent && isConfigLoaded && isAgentReady && !isAgentRunning);
|
|
332
390
|
const isFetchingMessages = !isConfigLoaded || (!isAgentReady && copilotMessages.length === 0);
|
|
333
|
-
// CopilotKit reports `isLoading` during connect/replay as well as real runs.
|
|
334
|
-
//
|
|
335
|
-
|
|
391
|
+
// CopilotKit reports `isLoading` during connect/replay as well as real runs. Only expose
|
|
392
|
+
// thinking once the bootstrap connect phase has completed — and that needs the readiness
|
|
393
|
+
// latch, not just `isFetchingMessages`, which goes false as soon as ONE replayed message
|
|
394
|
+
// exists: without `isAgentReady`, thinking leaks true for the tail of every replay, and
|
|
395
|
+
// every load or thread switch flashes the typing dots under the transcript. Real runs are
|
|
396
|
+
// unaffected — sending requires readiness (canSendMessage), so the latch is already set.
|
|
397
|
+
const isThinking = isAgentRunning && !isFetchingMessages && isAgentReady;
|
|
336
398
|
const runAgentWithRetry = useCallback(async (agentToRun, messageCountAtRunStart, forwardedProps) => {
|
|
337
399
|
if (agentToRun.isRunning) {
|
|
338
400
|
return false;
|
|
@@ -376,8 +438,20 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
376
438
|
return;
|
|
377
439
|
}
|
|
378
440
|
copilotAgent.addMessage({ ...message, id: generateId(), role: "user" });
|
|
441
|
+
// The send seam, fired once the message is IN the conversation, so hosts can react
|
|
442
|
+
// to it (e.g. list the thread optimistically). Covers every send routed through
|
|
443
|
+
// `sendMessage` — composer, empty-state heroes, user-generated chat components —
|
|
444
|
+
// but deliberately NOT the auto-sent `initialPrompt` below, whose hidden variant is
|
|
445
|
+
// a primer rather than something the user wrote.
|
|
446
|
+
if (onUserMessageSent) {
|
|
447
|
+
const { content } = message;
|
|
448
|
+
const textPart = Array.isArray(content)
|
|
449
|
+
? content.find((part) => part.type === "text")
|
|
450
|
+
: undefined;
|
|
451
|
+
onUserMessageSent(typeof content === "string" ? content : (textPart?.text ?? ""));
|
|
452
|
+
}
|
|
379
453
|
await runAgentWithRetry(copilotAgent, copilotAgent.messages.length);
|
|
380
|
-
}, [canSendMessage, copilotAgent, runAgentWithRetry]);
|
|
454
|
+
}, [canSendMessage, copilotAgent, runAgentWithRetry, onUserMessageSent]);
|
|
381
455
|
// Not sourced from `useInterrupt`: that hook only hands `resolve` to its own
|
|
382
456
|
// `render`, which it skips whenever nothing is pending — so after a reload there
|
|
383
457
|
// is no way to obtain it. `runAgentWithRetry`'s `isRunning` guard makes a
|
|
@@ -701,6 +775,18 @@ export const AgentChatRoot = ({ ...props
|
|
|
701
775
|
...(appId ? { "x-app-id": appId } : {}),
|
|
702
776
|
}), [token, socketId, appId]);
|
|
703
777
|
const [currentThreadId, setCurrentThreadId] = useState(() => defaultThreadId ?? (noPersistency ? crypto.randomUUID() : DEFAULT_CHAT_THREAD_ID));
|
|
778
|
+
// Follow later `defaultThreadId` changes — a mounted chat can be pointed at another
|
|
779
|
+
// conversation (the agent inbox switches threads without remounting). Adjusted during
|
|
780
|
+
// render (the React derived-state pattern) so children commit with the new thread and
|
|
781
|
+
// the new chatId prop atomically; the headless connect effect keys on the configuration
|
|
782
|
+
// threadId and replays the new transcript. Static-prop consumers never hit this branch.
|
|
783
|
+
const [prevDefaultThreadId, setPrevDefaultThreadId] = useState(defaultThreadId);
|
|
784
|
+
if (defaultThreadId !== prevDefaultThreadId) {
|
|
785
|
+
setPrevDefaultThreadId(defaultThreadId);
|
|
786
|
+
if (defaultThreadId && defaultThreadId !== currentThreadId) {
|
|
787
|
+
setCurrentThreadId(defaultThreadId);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
704
790
|
const blocksApiService = useMemo(() => new BlocksApiService({ token }), [token]);
|
|
705
791
|
const [agentChatBlock, setAgentChatBlock] = useState(null);
|
|
706
792
|
const [agentBlock, setAgentBlock] = useState(null);
|
|
@@ -798,7 +884,7 @@ export const AgentChatRoot = ({ ...props
|
|
|
798
884
|
},
|
|
799
885
|
// Explicit false: when undefined, CopilotKit auto-enables its inspector
|
|
800
886
|
// devtools on localhost hostnames.
|
|
801
|
-
enableInspector: false, children: [_jsx(SessionReadables, {}), _jsx(ChatInitializer, { agentBlock: agentBlock, agentChatBlock: agentChatBlock, chatId: chatId, noPersistency: noPersistency, token: token, appId: appId, components: components, currentThreadId: currentThreadId, setCurrentThreadId: setCurrentThreadId, children: children })] }));
|
|
887
|
+
enableInspector: false, children: [_jsx(SessionReadables, {}), _jsx(ChatInitializer, { agentBlock: agentBlock, agentChatBlock: agentChatBlock, chatId: chatId, noPersistency: noPersistency, token: token, appId: appId, components: components, currentThreadId: currentThreadId, setCurrentThreadId: setCurrentThreadId, onUserMessageSent: props.onUserMessageSent, children: children })] }));
|
|
802
888
|
};
|
|
803
889
|
export function AgentChatMessage({ message, index, ...props }) {
|
|
804
890
|
if (message.role === "user" && message.name === HIDDEN_INITIAL_PROMPT_MESSAGE_NAME) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type AgentThreadStatus = "idle" | "interrupted" | "error";
|
|
2
|
+
export type AgentThreadSource = "direct_chat" | "agent_call" | "send_message" | "unknown";
|
|
3
|
+
export interface AgentThreadItem {
|
|
4
|
+
agentId: string | null;
|
|
5
|
+
/** Never null — pass back verbatim to open the conversation. */
|
|
6
|
+
chatId: string;
|
|
7
|
+
/** Never null — pass back verbatim to open the conversation. */
|
|
8
|
+
chatThreadId: string;
|
|
9
|
+
source: AgentThreadSource;
|
|
10
|
+
status: AgentThreadStatus;
|
|
11
|
+
title: string | null;
|
|
12
|
+
turnCount: number;
|
|
13
|
+
/** First seen by the index — NOT thread creation time. */
|
|
14
|
+
firstSeenAt: string;
|
|
15
|
+
lastActivityAt: string;
|
|
16
|
+
}
|
|
17
|
+
/** The (chatId, chatThreadId) pair — the only stable identity of a thread row. */
|
|
18
|
+
export declare const agentThreadKey: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a realtime payload's `thread` is a complete list-item row. The server
|
|
21
|
+
* serializes the exact item shape into every event, so this normally passes; when
|
|
22
|
+
* it doesn't (contract drift), the consumer falls back to the getOne endpoint.
|
|
23
|
+
*/
|
|
24
|
+
export declare const isAgentThreadItem: (value: unknown) => value is AgentThreadItem;
|
|
25
|
+
/**
|
|
26
|
+
* Provisional row title from the prompt that started a conversation (ChatGPT-style):
|
|
27
|
+
* one line, whitespace collapsed, capped like the server's generated titles. Null when
|
|
28
|
+
* there's nothing usable (attachment-only sends) — the UI keeps its id-based fallback.
|
|
29
|
+
*/
|
|
30
|
+
export declare const provisionalThreadTitle: (prompt: string | undefined) => string | null;
|
|
31
|
+
export interface AgentThreadLiveUpdateOptions {
|
|
32
|
+
/** Active server-side filters — an updated row that stops matching leaves the list. */
|
|
33
|
+
status?: AgentThreadStatus;
|
|
34
|
+
source?: AgentThreadSource;
|
|
35
|
+
chatId?: string;
|
|
36
|
+
/** Active sort, mirroring the endpoint's defaults (lastActivity, desc). */
|
|
37
|
+
sort?: "lastActivity" | "firstSeen";
|
|
38
|
+
order?: "asc" | "desc";
|
|
39
|
+
/** True when more pages exist server-side beyond the loaded window. */
|
|
40
|
+
hasMore?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Merge one realtime thread update into a loaded list. Pure; returns the input array
|
|
44
|
+
* untouched when the event is stale, filtered out, or out of the loaded window.
|
|
45
|
+
*
|
|
46
|
+
* - Out-of-order delivery can't regress a row: events older than the loaded row (by
|
|
47
|
+
* lastActivityAt) are dropped — checked FIRST, so a stale event can't evict a
|
|
48
|
+
* still-current row through the filter check below either. Renames keep
|
|
49
|
+
* lastActivityAt, so equal passes.
|
|
50
|
+
* - A row that stops matching the active filters is removed; a matching one is upserted
|
|
51
|
+
* and the list re-sorted by the active sort (stable, so ties keep their server order).
|
|
52
|
+
* - A NEW row sorting past every loaded row while more pages exist may belong beyond the
|
|
53
|
+
* loaded window; it's skipped so the list stays contiguous with the pagination cursor
|
|
54
|
+
* (unreachable under the default lastActivity-desc sort, where fresh activity sorts first).
|
|
55
|
+
*/
|
|
56
|
+
/** Whether a row passes the list's active server-side filters. */
|
|
57
|
+
export declare const matchesAgentThreadFilters: (item: AgentThreadItem, options?: AgentThreadLiveUpdateOptions) => boolean;
|
|
58
|
+
export declare const applyAgentThreadUpdate: (threads: AgentThreadItem[], incoming: AgentThreadItem, options?: AgentThreadLiveUpdateOptions) => AgentThreadItem[];
|
|
59
|
+
/**
|
|
60
|
+
* Overlay optimistic rows (conversations the user just acted on, ChatGPT-style) onto the
|
|
61
|
+
* loaded list. Each row goes through the same merge as a realtime event, so filters and
|
|
62
|
+
* sorting apply — except that a server row still untitled borrows the optimistic
|
|
63
|
+
* (prompt-derived) title until the generated one lands. Pure; returns `threads` untouched
|
|
64
|
+
* when nothing changes.
|
|
65
|
+
*
|
|
66
|
+
* An entry steps aside once the loaded row has recorded MORE turns than it did: turnCount
|
|
67
|
+
* is server-monotonic, so the handover can't be held off by a client clock running ahead
|
|
68
|
+
* of the server (the entry's lastActivityAt is stamped locally, and the merge's staleness
|
|
69
|
+
* guard compares it against server timestamps).
|
|
70
|
+
*/
|
|
71
|
+
export declare const overlayOptimisticThreads: (threads: AgentThreadItem[], optimistic: ReadonlyMap<string, AgentThreadItem>, options?: AgentThreadLiveUpdateOptions) => AgentThreadItem[];
|
|
72
|
+
//# sourceMappingURL=agent-thread-live-update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-thread-live-update.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-thread-live-update.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,aAAa,GAAG,OAAO,CAAC;AAEjE,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,YAAY,GAAG,cAAc,GAAG,SAAS,CAAC;AAE1F,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,kFAAkF;AAClF,eAAO,MAAM,cAAc,GAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,KAAG,MAC9C,CAAC;AAK7C;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,eAiB3D,CAAC;AAKF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,IAQ5E,CAAC;AAEF,MAAM,WAAW,4BAA4B;IAC3C,uFAAuF;IACvF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,cAAc,GAAG,WAAW,CAAC;IACpC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAOD;;;;;;;;;;;;;GAaG;AACH,kEAAkE;AAClE,eAAO,MAAM,yBAAyB,GAAI,MAAM,eAAe,EAAE,UAAS,4BAAiC,KAAG,OAGzD,CAAC;AAEtD,eAAO,MAAM,sBAAsB,GACjC,SAAS,eAAe,EAAE,EAC1B,UAAU,eAAe,EACzB,UAAS,4BAAiC,KACzC,eAAe,EAqBjB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACnC,SAAS,eAAe,EAAE,EAC1B,YAAY,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,EAChD,UAAS,4BAAiC,KACzC,eAAe,EAkBjB,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Wire types and pure merge logic for the agent-threads index and its realtime
|
|
2
|
+
// AGENT_THREAD_UPDATED events. Dependency-free on purpose: use-agent-threads
|
|
3
|
+
// re-exports everything here, and node tests load this module directly.
|
|
4
|
+
/** The (chatId, chatThreadId) pair — the only stable identity of a thread row. */
|
|
5
|
+
export const agentThreadKey = (thread) => `${thread.chatId}::${thread.chatThreadId}`;
|
|
6
|
+
const THREAD_STATUSES = ["idle", "interrupted", "error"];
|
|
7
|
+
const THREAD_SOURCES = ["direct_chat", "agent_call", "send_message", "unknown"];
|
|
8
|
+
/**
|
|
9
|
+
* Whether a realtime payload's `thread` is a complete list-item row. The server
|
|
10
|
+
* serializes the exact item shape into every event, so this normally passes; when
|
|
11
|
+
* it doesn't (contract drift), the consumer falls back to the getOne endpoint.
|
|
12
|
+
*/
|
|
13
|
+
export const isAgentThreadItem = (value) => {
|
|
14
|
+
const item = value;
|
|
15
|
+
return (typeof item === "object" &&
|
|
16
|
+
item !== null &&
|
|
17
|
+
(typeof item.agentId === "string" || item.agentId === null) &&
|
|
18
|
+
typeof item.chatId === "string" &&
|
|
19
|
+
item.chatId.length > 0 &&
|
|
20
|
+
typeof item.chatThreadId === "string" &&
|
|
21
|
+
item.chatThreadId.length > 0 &&
|
|
22
|
+
THREAD_SOURCES.includes(item.source) &&
|
|
23
|
+
THREAD_STATUSES.includes(item.status) &&
|
|
24
|
+
(typeof item.title === "string" || item.title === null) &&
|
|
25
|
+
typeof item.turnCount === "number" &&
|
|
26
|
+
typeof item.firstSeenAt === "string" &&
|
|
27
|
+
typeof item.lastActivityAt === "string");
|
|
28
|
+
};
|
|
29
|
+
// Matches the server's cap for LLM-generated titles (wf-actions-py TITLE_MAX_CHARS).
|
|
30
|
+
const PROVISIONAL_TITLE_MAX_CHARS = 80;
|
|
31
|
+
/**
|
|
32
|
+
* Provisional row title from the prompt that started a conversation (ChatGPT-style):
|
|
33
|
+
* one line, whitespace collapsed, capped like the server's generated titles. Null when
|
|
34
|
+
* there's nothing usable (attachment-only sends) — the UI keeps its id-based fallback.
|
|
35
|
+
*/
|
|
36
|
+
export const provisionalThreadTitle = (prompt) => {
|
|
37
|
+
const collapsed = prompt?.replace(/\s+/g, " ").trim();
|
|
38
|
+
if (!collapsed) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return collapsed.length > PROVISIONAL_TITLE_MAX_CHARS
|
|
42
|
+
? `${collapsed.slice(0, PROVISIONAL_TITLE_MAX_CHARS - 1).trimEnd()}…`
|
|
43
|
+
: collapsed;
|
|
44
|
+
};
|
|
45
|
+
const sortTime = (item, sort) => {
|
|
46
|
+
const parsed = new Date(sort === "firstSeen" ? item.firstSeenAt : item.lastActivityAt).getTime();
|
|
47
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Merge one realtime thread update into a loaded list. Pure; returns the input array
|
|
51
|
+
* untouched when the event is stale, filtered out, or out of the loaded window.
|
|
52
|
+
*
|
|
53
|
+
* - Out-of-order delivery can't regress a row: events older than the loaded row (by
|
|
54
|
+
* lastActivityAt) are dropped — checked FIRST, so a stale event can't evict a
|
|
55
|
+
* still-current row through the filter check below either. Renames keep
|
|
56
|
+
* lastActivityAt, so equal passes.
|
|
57
|
+
* - A row that stops matching the active filters is removed; a matching one is upserted
|
|
58
|
+
* and the list re-sorted by the active sort (stable, so ties keep their server order).
|
|
59
|
+
* - A NEW row sorting past every loaded row while more pages exist may belong beyond the
|
|
60
|
+
* loaded window; it's skipped so the list stays contiguous with the pagination cursor
|
|
61
|
+
* (unreachable under the default lastActivity-desc sort, where fresh activity sorts first).
|
|
62
|
+
*/
|
|
63
|
+
/** Whether a row passes the list's active server-side filters. */
|
|
64
|
+
export const matchesAgentThreadFilters = (item, options = {}) => (!options.status || item.status === options.status) &&
|
|
65
|
+
(!options.source || item.source === options.source) &&
|
|
66
|
+
(!options.chatId || item.chatId === options.chatId);
|
|
67
|
+
export const applyAgentThreadUpdate = (threads, incoming, options = {}) => {
|
|
68
|
+
const key = agentThreadKey(incoming);
|
|
69
|
+
const existing = threads.find((thread) => agentThreadKey(thread) === key);
|
|
70
|
+
if (existing && sortTime(incoming, "lastActivity") < sortTime(existing, "lastActivity")) {
|
|
71
|
+
return threads;
|
|
72
|
+
}
|
|
73
|
+
if (!matchesAgentThreadFilters(incoming, options)) {
|
|
74
|
+
return existing ? threads.filter((thread) => thread !== existing) : threads;
|
|
75
|
+
}
|
|
76
|
+
const sort = options.sort ?? "lastActivity";
|
|
77
|
+
const direction = options.order === "asc" ? 1 : -1;
|
|
78
|
+
const next = existing ? threads.map((thread) => (thread === existing ? incoming : thread)) : [...threads, incoming];
|
|
79
|
+
next.sort((a, b) => direction * (sortTime(a, sort) - sortTime(b, sort)));
|
|
80
|
+
if (!existing && options.hasMore && threads.length > 0 && next[next.length - 1] === incoming) {
|
|
81
|
+
return threads;
|
|
82
|
+
}
|
|
83
|
+
return next;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Overlay optimistic rows (conversations the user just acted on, ChatGPT-style) onto the
|
|
87
|
+
* loaded list. Each row goes through the same merge as a realtime event, so filters and
|
|
88
|
+
* sorting apply — except that a server row still untitled borrows the optimistic
|
|
89
|
+
* (prompt-derived) title until the generated one lands. Pure; returns `threads` untouched
|
|
90
|
+
* when nothing changes.
|
|
91
|
+
*
|
|
92
|
+
* An entry steps aside once the loaded row has recorded MORE turns than it did: turnCount
|
|
93
|
+
* is server-monotonic, so the handover can't be held off by a client clock running ahead
|
|
94
|
+
* of the server (the entry's lastActivityAt is stamped locally, and the merge's staleness
|
|
95
|
+
* guard compares it against server timestamps).
|
|
96
|
+
*/
|
|
97
|
+
export const overlayOptimisticThreads = (threads, optimistic, options = {}) => {
|
|
98
|
+
if (optimistic.size === 0) {
|
|
99
|
+
return threads;
|
|
100
|
+
}
|
|
101
|
+
let merged = threads;
|
|
102
|
+
for (const [key, row] of optimistic) {
|
|
103
|
+
const handedOver = threads.some((thread) => agentThreadKey(thread) === key && thread.turnCount > row.turnCount);
|
|
104
|
+
if (!handedOver) {
|
|
105
|
+
merged = applyAgentThreadUpdate(merged, row, options);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!merged.some((row) => row.title === null && optimistic.get(agentThreadKey(row))?.title)) {
|
|
109
|
+
return merged;
|
|
110
|
+
}
|
|
111
|
+
return merged.map((row) => {
|
|
112
|
+
const provisional = row.title === null ? optimistic.get(agentThreadKey(row))?.title : null;
|
|
113
|
+
return provisional ? { ...row, title: provisional } : row;
|
|
114
|
+
});
|
|
115
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type AgentThreadItem, type AgentThreadSource, type AgentThreadStatus } from "./agent-thread-live-update.js";
|
|
2
|
+
export * from "./agent-thread-live-update.js";
|
|
3
|
+
export interface AgentThreadsPage {
|
|
4
|
+
items: AgentThreadItem[];
|
|
5
|
+
nextCursor: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface UseAgentThreadsOptions {
|
|
8
|
+
agentId: string;
|
|
9
|
+
status?: AgentThreadStatus;
|
|
10
|
+
source?: AgentThreadSource;
|
|
11
|
+
chatId?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
sort?: "lastActivity" | "firstSeen";
|
|
14
|
+
order?: "asc" | "desc";
|
|
15
|
+
/** Platform host override; defaults to getApiHost() (same convention as the agent chat). */
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
/** Bearer token for callers that don't share the platform session cookie. */
|
|
18
|
+
token?: string;
|
|
19
|
+
/** Extra request headers, merged over the defaults. */
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
export interface UseAgentThreadsResult {
|
|
23
|
+
threads: AgentThreadItem[];
|
|
24
|
+
/**
|
|
25
|
+
* The thread the consumer is currently viewing — never null. When nothing is
|
|
26
|
+
* selected the hook auto-generates a draft with a fresh (chatId, chatThreadId)
|
|
27
|
+
* pair the chat surface can use verbatim; once the draft's first turn is indexed
|
|
28
|
+
* and the list refreshes, this resolves to that real row (same key) seamlessly.
|
|
29
|
+
* For selected threads it prefers the LIST's row for the same identity — so
|
|
30
|
+
* status/turnCount stay fresh after a refresh — falling back to the last set
|
|
31
|
+
* item when the row isn't in the loaded pages, so selection never vanishes.
|
|
32
|
+
*/
|
|
33
|
+
currentThread: AgentThreadItem;
|
|
34
|
+
/** True while currentThread is an auto-generated draft not yet in the index. */
|
|
35
|
+
isDraftThread: boolean;
|
|
36
|
+
/** Move between threads; null mints a fresh draft ("new chat"). Resets when agentId changes. */
|
|
37
|
+
setCurrentThread: (thread: AgentThreadItem | null) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Report a user send on the current conversation (pass the prompt text) — ChatGPT-style
|
|
40
|
+
* optimistic listing. A conversation with no server row yet appears in `threads`
|
|
41
|
+
* immediately, titled by the truncated prompt, and every send bumps the row's recency;
|
|
42
|
+
* server rows take over under the same identity as events land. Identity-stable — safe
|
|
43
|
+
* to hand to memoized chat surfaces (e.g. a chat root's onUserMessageSent).
|
|
44
|
+
*/
|
|
45
|
+
notifyThreadActivity: (prompt?: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Rename a conversation. Optimistic — the list shows the new title immediately and
|
|
48
|
+
* rolls back on failure (the promise rejects so callers can surface an error).
|
|
49
|
+
*/
|
|
50
|
+
renameThread: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">, title: string) => Promise<void>;
|
|
51
|
+
/** First page (or refresh) in flight. */
|
|
52
|
+
isLoading: boolean;
|
|
53
|
+
/** A cursor page is being appended. */
|
|
54
|
+
isLoadingMore: boolean;
|
|
55
|
+
error: Error | null;
|
|
56
|
+
hasMore: boolean;
|
|
57
|
+
loadMore: () => Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Re-walk every loaded page. Freshness is otherwise live (the hook subscribes to
|
|
60
|
+
* AGENT_THREAD_UPDATED itself) — keep refresh for recovery, e.g. a throttled
|
|
61
|
+
* window-focus refresh after a dropped connection.
|
|
62
|
+
*/
|
|
63
|
+
refresh: () => Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The platform inbox's fixed chatId for an agent: one shared value per agent block, so
|
|
67
|
+
* index rows are recognizable as "direct chats with this agent on the platform" at a
|
|
68
|
+
* glance (and filterable via the endpoint's chatId param). Conversations under it are
|
|
69
|
+
* distinguished by their unique chatThreadId.
|
|
70
|
+
*/
|
|
71
|
+
export declare const AGENT_INBOX_CHAT_PREFIX = "agent-inbox-";
|
|
72
|
+
export declare const agentInboxChatId: (agentId: string) => string;
|
|
73
|
+
/**
|
|
74
|
+
* Short human label for a row with no generated title yet: the half of the identity
|
|
75
|
+
* pair that varies for the row. Inbox rows share one fixed chatId (above) and vary by
|
|
76
|
+
* thread id; rows from other surfaces (published-app chats) are the reverse. One-sided
|
|
77
|
+
* writers (agent calls) index a "_" placeholder for the missing half — fall back to
|
|
78
|
+
* the real half rather than labeling every such row "_".
|
|
79
|
+
*/
|
|
80
|
+
export declare const agentThreadShortLabel: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">) => string;
|
|
81
|
+
/** How a row is labelled: its title, or the id-based short label while it has none. */
|
|
82
|
+
export declare const agentThreadDisplayTitle: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId" | "title">) => string;
|
|
83
|
+
/**
|
|
84
|
+
* Headless agent-threads index: fetches an agent's conversation threads from the platform
|
|
85
|
+
* endpoint with keyset-cursor pagination, then keeps them live — every threads-table write
|
|
86
|
+
* publishes AGENT_THREAD_UPDATED on the app's Pusher channel and the hook upserts the row,
|
|
87
|
+
* falling back to the getOne endpoint when a payload lacks the full row. New conversations
|
|
88
|
+
* list optimistically the moment the first prompt is sent (wire notifyThreadActivity to the
|
|
89
|
+
* chat's send seam). Data only — pair it with whatever list UI the host app uses. No
|
|
90
|
+
* per-host wiring: both hosts already run the shared websocketsService singleton and
|
|
91
|
+
* subscribe the app channel (the platform via its layouts' useWebsockets hooks, a
|
|
92
|
+
* boilerplate app via the client SDK's ClientProvider), and listening before the socket
|
|
93
|
+
* connects just parks the handler.
|
|
94
|
+
*/
|
|
95
|
+
export declare const useAgentThreads: (options: UseAgentThreadsOptions) => UseAgentThreadsResult;
|
|
96
|
+
export default useAgentThreads;
|
|
97
|
+
//# sourceMappingURL=use-agent-threads.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-agent-threads.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-threads.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EAMvB,MAAM,4BAA4B,CAAC;AAKpC,cAAc,4BAA4B,CAAC;AAO3C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,GAAG,WAAW,CAAC;IACpC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B;;;;;;;;OAQG;IACH,aAAa,EAAE,eAAe,CAAC;IAC/B,gFAAgF;IAChF,aAAa,EAAE,OAAO,CAAC;IACvB,gGAAgG;IAChG,gBAAgB,EAAE,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3D;;;;;;OAMG;IACH,oBAAoB,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD;;;OAGG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzG,yCAAyC;IACzC,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAAgD,CAAC;AAEpG;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,KAAG,MAIhG,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,GAAG,OAAO,CAAC,KAAG,MACpD,CAAC;AA8D1D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB,KAAG,qBAsdjE,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { getApiHost } from "@blocksdiy/blocks-client-api/envService";
|
|
2
|
+
import { websocketsService } from "@blocksdiy/blocks-client-api/websocketService";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { agentThreadKey, applyAgentThreadUpdate, isAgentThreadItem, matchesAgentThreadFilters, overlayOptimisticThreads, provisionalThreadTitle, } from "./agent-thread-live-update.js";
|
|
5
|
+
// Wire types, the identity key, and the realtime merge logic live in
|
|
6
|
+
// ./agent-thread-live-update (dependency-free, unit-testable); re-exported so
|
|
7
|
+
// consumers keep importing everything from this module.
|
|
8
|
+
export * from "./agent-thread-live-update.js";
|
|
9
|
+
/* public decouple - AGENT_THREAD_UPDATED rides AgentThreadEventPayload (packages/common
|
|
10
|
+
agent-thread-events.ts): the event's `thread` field is exactly the list/GET item shape,
|
|
11
|
+
relayed verbatim from wf-actions-py to the app's Pusher channel. */
|
|
12
|
+
const AGENT_THREAD_UPDATED = "AGENT_THREAD_UPDATED";
|
|
13
|
+
/**
|
|
14
|
+
* The platform inbox's fixed chatId for an agent: one shared value per agent block, so
|
|
15
|
+
* index rows are recognizable as "direct chats with this agent on the platform" at a
|
|
16
|
+
* glance (and filterable via the endpoint's chatId param). Conversations under it are
|
|
17
|
+
* distinguished by their unique chatThreadId.
|
|
18
|
+
*/
|
|
19
|
+
export const AGENT_INBOX_CHAT_PREFIX = "agent-inbox-";
|
|
20
|
+
export const agentInboxChatId = (agentId) => `${AGENT_INBOX_CHAT_PREFIX}${agentId}`;
|
|
21
|
+
/**
|
|
22
|
+
* Short human label for a row with no generated title yet: the half of the identity
|
|
23
|
+
* pair that varies for the row. Inbox rows share one fixed chatId (above) and vary by
|
|
24
|
+
* thread id; rows from other surfaces (published-app chats) are the reverse. One-sided
|
|
25
|
+
* writers (agent calls) index a "_" placeholder for the missing half — fall back to
|
|
26
|
+
* the real half rather than labeling every such row "_".
|
|
27
|
+
*/
|
|
28
|
+
export const agentThreadShortLabel = (thread) => {
|
|
29
|
+
const varying = thread.chatId.startsWith(AGENT_INBOX_CHAT_PREFIX) ? thread.chatThreadId : thread.chatId;
|
|
30
|
+
const other = varying === thread.chatId ? thread.chatThreadId : thread.chatId;
|
|
31
|
+
return (varying === "_" ? other : varying).slice(0, 8);
|
|
32
|
+
};
|
|
33
|
+
/** How a row is labelled: its title, or the id-based short label while it has none. */
|
|
34
|
+
export const agentThreadDisplayTitle = (thread) => thread.title ?? `Chat ${agentThreadShortLabel(thread)}`;
|
|
35
|
+
const buildQuery = (filters, cursor) => {
|
|
36
|
+
const params = new URLSearchParams();
|
|
37
|
+
if (filters.status) {
|
|
38
|
+
params.set("status", filters.status);
|
|
39
|
+
}
|
|
40
|
+
if (filters.source) {
|
|
41
|
+
params.set("source", filters.source);
|
|
42
|
+
}
|
|
43
|
+
if (filters.chatId) {
|
|
44
|
+
params.set("chatId", filters.chatId);
|
|
45
|
+
}
|
|
46
|
+
if (filters.limit) {
|
|
47
|
+
params.set("limit", String(filters.limit));
|
|
48
|
+
}
|
|
49
|
+
if (filters.sort) {
|
|
50
|
+
params.set("sort", filters.sort);
|
|
51
|
+
}
|
|
52
|
+
if (filters.order) {
|
|
53
|
+
params.set("order", filters.order);
|
|
54
|
+
}
|
|
55
|
+
if (cursor) {
|
|
56
|
+
params.set("cursor", cursor);
|
|
57
|
+
}
|
|
58
|
+
const query = params.toString();
|
|
59
|
+
return query ? `?${query}` : "";
|
|
60
|
+
};
|
|
61
|
+
const appendPage = (existing, page) => {
|
|
62
|
+
const seen = new Set(existing.map(agentThreadKey));
|
|
63
|
+
return [...existing, ...page.filter((thread) => !seen.has(agentThreadKey(thread)))];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A not-yet-indexed conversation. chatId is ALWAYS the agent's static inbox id (the
|
|
67
|
+
* surface marker — identical for every inbox conversation with this agent); only the
|
|
68
|
+
* thread id makes each conversation unique, and it must be a UUID — CopilotKit
|
|
69
|
+
* replaces non-UUID thread ids per load, which breaks history. Sharing the chatId
|
|
70
|
+
* also means inbox thread switches are pure thread-id changes, the chat stack's
|
|
71
|
+
* native reconnect path.
|
|
72
|
+
*/
|
|
73
|
+
const makeDraftThread = (agentId) => {
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
return {
|
|
76
|
+
agentId: agentId || null,
|
|
77
|
+
chatId: agentInboxChatId(agentId),
|
|
78
|
+
chatThreadId: crypto.randomUUID(),
|
|
79
|
+
source: "direct_chat",
|
|
80
|
+
status: "idle",
|
|
81
|
+
title: null,
|
|
82
|
+
turnCount: 0,
|
|
83
|
+
firstSeenAt: now,
|
|
84
|
+
lastActivityAt: now,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Headless agent-threads index: fetches an agent's conversation threads from the platform
|
|
89
|
+
* endpoint with keyset-cursor pagination, then keeps them live — every threads-table write
|
|
90
|
+
* publishes AGENT_THREAD_UPDATED on the app's Pusher channel and the hook upserts the row,
|
|
91
|
+
* falling back to the getOne endpoint when a payload lacks the full row. New conversations
|
|
92
|
+
* list optimistically the moment the first prompt is sent (wire notifyThreadActivity to the
|
|
93
|
+
* chat's send seam). Data only — pair it with whatever list UI the host app uses. No
|
|
94
|
+
* per-host wiring: both hosts already run the shared websocketsService singleton and
|
|
95
|
+
* subscribe the app channel (the platform via its layouts' useWebsockets hooks, a
|
|
96
|
+
* boilerplate app via the client SDK's ClientProvider), and listening before the socket
|
|
97
|
+
* connects just parks the handler.
|
|
98
|
+
*/
|
|
99
|
+
export const useAgentThreads = (options) => {
|
|
100
|
+
const { agentId, status, source, chatId, limit, sort, order, baseUrl } = options;
|
|
101
|
+
const [threads, setThreads] = useState([]);
|
|
102
|
+
// Mirror of `threads` for reads outside render (a refresh needs the loaded count without
|
|
103
|
+
// re-closing over state). Every write goes through applyThreads so the two can't diverge;
|
|
104
|
+
// reads are race-free because writers are serialized (loadMore awaits any in-flight
|
|
105
|
+
// refresh, and every write re-checks the request sequence first).
|
|
106
|
+
const threadsRef = useRef([]);
|
|
107
|
+
const applyThreads = useCallback((items) => {
|
|
108
|
+
threadsRef.current = items;
|
|
109
|
+
setThreads(items);
|
|
110
|
+
}, []);
|
|
111
|
+
const [nextCursor, setNextCursor] = useState(null);
|
|
112
|
+
// Cursor mirror, same contract as threadsRef: loadMore reads it after awaiting an
|
|
113
|
+
// in-flight refresh, when its render-time closure value is stale by definition.
|
|
114
|
+
const nextCursorRef = useRef(null);
|
|
115
|
+
const applyCursor = useCallback((cursor) => {
|
|
116
|
+
nextCursorRef.current = cursor;
|
|
117
|
+
setNextCursor(cursor);
|
|
118
|
+
}, []);
|
|
119
|
+
// Starts true (when a fetch is coming): the first-page effect only runs AFTER the mount
|
|
120
|
+
// paint, so initializing false would flash the "no threads" empty state for a frame.
|
|
121
|
+
const [isLoading, setIsLoading] = useState(Boolean(agentId));
|
|
122
|
+
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
123
|
+
const [error, setError] = useState(null);
|
|
124
|
+
const [selectedThread, setSelectedThread] = useState(null);
|
|
125
|
+
const [draftThread, setDraftThread] = useState(() => makeDraftThread(agentId));
|
|
126
|
+
// ChatGPT-style optimistic rows, keyed by thread identity: conversations listed from
|
|
127
|
+
// the moment a prompt is sent (see notifyThreadActivity), before/ahead of their server
|
|
128
|
+
// row. The display overlay hands each one over to its server row as events land.
|
|
129
|
+
// Ref-mirrored like threadsRef, for reads outside render (renameThread's snapshots).
|
|
130
|
+
const [optimisticThreads, setOptimisticThreads] = useState(new Map());
|
|
131
|
+
const optimisticThreadsRef = useRef(optimisticThreads);
|
|
132
|
+
const applyOptimisticThreads = useCallback((next) => {
|
|
133
|
+
optimisticThreadsRef.current = next;
|
|
134
|
+
setOptimisticThreads(next);
|
|
135
|
+
}, []);
|
|
136
|
+
const setCurrentThread = useCallback((thread) => {
|
|
137
|
+
// Null means "new chat": mint a fresh draft so each one is its own conversation.
|
|
138
|
+
if (!thread) {
|
|
139
|
+
setDraftThread(makeDraftThread(agentId));
|
|
140
|
+
}
|
|
141
|
+
setSelectedThread(thread);
|
|
142
|
+
}, [agentId]);
|
|
143
|
+
// Selection follows the agent, not the query: filter changes keep it (the fallback in
|
|
144
|
+
// `currentThread` covers rows the new filter drops), but another agent's thread — or
|
|
145
|
+
// draft — is meaningless here. Guarded by the previous id so the mount render keeps the
|
|
146
|
+
// initial draft (regenerating it would remount a consumer's keyed chat surface for nothing).
|
|
147
|
+
const prevAgentIdRef = useRef(agentId);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (prevAgentIdRef.current === agentId) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
prevAgentIdRef.current = agentId;
|
|
153
|
+
setSelectedThread(null);
|
|
154
|
+
setDraftThread(makeDraftThread(agentId));
|
|
155
|
+
applyOptimisticThreads(new Map());
|
|
156
|
+
}, [agentId, applyOptimisticThreads]);
|
|
157
|
+
// The realtime merge's list-shape inputs, shared by the display overlay, the live
|
|
158
|
+
// event handler, and the walk-result reconciliation in loadFirstPage.
|
|
159
|
+
const liveUpdateOptions = useMemo(() => ({ status, source, chatId, sort, order }), [status, source, chatId, sort, order]);
|
|
160
|
+
// What consumers see: the loaded pages with the optimistic rows overlaid.
|
|
161
|
+
const displayThreads = useMemo(() => overlayOptimisticThreads(threads, optimisticThreads, liveUpdateOptions), [threads, optimisticThreads, liveUpdateOptions]);
|
|
162
|
+
// Rows are rewritten server-side every turn, so after a refresh the stored item is stale;
|
|
163
|
+
// prefer the list's row with the same identity, keep the stored one if it's not loaded.
|
|
164
|
+
// The draft goes through the same lookup: once its row is listed — optimistically on the
|
|
165
|
+
// first send, or when its first turn is indexed — that row wins under the same key: the
|
|
166
|
+
// draft has materialized, without changing identity.
|
|
167
|
+
const currentThread = useMemo(() => {
|
|
168
|
+
const stored = selectedThread ?? draftThread;
|
|
169
|
+
const storedKey = agentThreadKey(stored);
|
|
170
|
+
return displayThreads.find((thread) => agentThreadKey(thread) === storedKey) ?? stored;
|
|
171
|
+
}, [displayThreads, selectedThread, draftThread]);
|
|
172
|
+
const isDraftThread = !selectedThread && currentThread === draftThread;
|
|
173
|
+
// Once the draft's row is listed, it IS the conversation — adopt it as the selection.
|
|
174
|
+
// Otherwise a later filter change (which blanks the list before refetching) would flip
|
|
175
|
+
// `isDraftThread` back to true for a conversation that has messages, and the consumer's
|
|
176
|
+
// draft-only presentation ("New chat" header, empty-state slot) with it.
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (selectedThread) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const draftKey = agentThreadKey(draftThread);
|
|
182
|
+
const materialized = displayThreads.find((thread) => agentThreadKey(thread) === draftKey);
|
|
183
|
+
if (materialized) {
|
|
184
|
+
setSelectedThread(materialized);
|
|
185
|
+
}
|
|
186
|
+
}, [displayThreads, selectedThread, draftThread]);
|
|
187
|
+
// Read at call time via a ref so notifyThreadActivity stays identity-stable for
|
|
188
|
+
// memoized chat surfaces (same render-time-assignment pattern as authRef below).
|
|
189
|
+
const currentThreadRef = useRef(currentThread);
|
|
190
|
+
currentThreadRef.current = currentThread;
|
|
191
|
+
/**
|
|
192
|
+
* Report a user send on the current conversation — ChatGPT-style optimistic listing.
|
|
193
|
+
* A conversation with no server row yet appears in `threads` immediately, titled by
|
|
194
|
+
* the truncated prompt, and every send bumps the row's recency. Server rows take over
|
|
195
|
+
* under the same identity as events land (turn end, title generation), the provisional
|
|
196
|
+
* title holding until the generated one arrives.
|
|
197
|
+
*/
|
|
198
|
+
const notifyThreadActivity = useCallback((prompt) => {
|
|
199
|
+
const current = currentThreadRef.current;
|
|
200
|
+
const key = agentThreadKey(current);
|
|
201
|
+
const prev = optimisticThreadsRef.current;
|
|
202
|
+
const next = new Map(prev);
|
|
203
|
+
next.set(key, {
|
|
204
|
+
...current,
|
|
205
|
+
title: prev.get(key)?.title ?? current.title ?? provisionalThreadTitle(prompt),
|
|
206
|
+
lastActivityAt: new Date().toISOString(),
|
|
207
|
+
});
|
|
208
|
+
applyOptimisticThreads(next);
|
|
209
|
+
}, [applyOptimisticThreads]);
|
|
210
|
+
// Auth material is read at request time via refs so an inline `headers` object literal
|
|
211
|
+
// doesn't churn the effect below into a refetch loop.
|
|
212
|
+
const authRef = useRef({ token: options.token, headers: options.headers });
|
|
213
|
+
authRef.current = { token: options.token, headers: options.headers };
|
|
214
|
+
// Every state-writing path checks it still owns the latest request before touching state,
|
|
215
|
+
// so a stale response (filter switched mid-flight, unmount) can never clobber a newer one.
|
|
216
|
+
const requestSeqRef = useRef(0);
|
|
217
|
+
// Authed request against the platform host, JSON-decoded — shared by the page and
|
|
218
|
+
// getOne fetchers below and by renameThread.
|
|
219
|
+
const fetchJson = useCallback(async (path, init) => {
|
|
220
|
+
const { token, headers } = authRef.current;
|
|
221
|
+
const response = await fetch(`${baseUrl ?? getApiHost()}${path}`, {
|
|
222
|
+
...init,
|
|
223
|
+
headers: {
|
|
224
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
225
|
+
...headers,
|
|
226
|
+
...init?.headers,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
throw new Error(`Agent threads request failed with status ${response.status}`);
|
|
231
|
+
}
|
|
232
|
+
return (await response.json());
|
|
233
|
+
}, [baseUrl]);
|
|
234
|
+
const fetchPage = useCallback((cursor) => fetchJson(`/agent-threads/${encodeURIComponent(agentId)}${buildQuery({ status, source, chatId, limit, sort, order }, cursor)}`), [agentId, status, source, chatId, limit, sort, order, fetchJson]);
|
|
235
|
+
// getOne, mirroring the upstream GET /thread endpoint — the realtime fallback for
|
|
236
|
+
// events whose payload doesn't carry the full row.
|
|
237
|
+
const fetchThread = useCallback((threadChatId, threadChatThreadId) => fetchJson(`/agent-threads/${encodeURIComponent(agentId)}/thread?${new URLSearchParams({ chatId: threadChatId, chatThreadId: threadChatThreadId })}`), [agentId, fetchJson]);
|
|
238
|
+
/**
|
|
239
|
+
* Rename a conversation — optimistic with rollback, rejecting on failure so callers
|
|
240
|
+
* can surface it. The new title is applied to EVERY local title source: the loaded
|
|
241
|
+
* row, any optimistic entry (whose snapshot the overlay may currently be showing),
|
|
242
|
+
* and the stored selection — `currentThread`'s fallback when a filter change dropped
|
|
243
|
+
* the row from the list; without it, a rename of the open-but-unlisted conversation
|
|
244
|
+
* would never reach the header. Other tabs get the rename via the pusher event; this
|
|
245
|
+
* tab is excluded from the echo (x-socket-id), matching the optimistic apply.
|
|
246
|
+
*/
|
|
247
|
+
const renameThread = useCallback(async (thread, title) => {
|
|
248
|
+
const trimmed = title.trim();
|
|
249
|
+
if (!trimmed) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const key = agentThreadKey(thread);
|
|
253
|
+
// `undefined` means "that source held no row when we started" — leave whatever is
|
|
254
|
+
// there now alone. Collapsing it to null would let a rollback erase a title that
|
|
255
|
+
// landed from the server while the request was in flight.
|
|
256
|
+
const setLocalTitle = (rowTitle, entryTitle, selectedTitle) => {
|
|
257
|
+
const row = threadsRef.current.find((item) => agentThreadKey(item) === key);
|
|
258
|
+
if (row && rowTitle !== undefined && row.title !== rowTitle) {
|
|
259
|
+
applyThreads(threadsRef.current.map((item) => (item === row ? { ...item, title: rowTitle } : item)));
|
|
260
|
+
}
|
|
261
|
+
const entry = optimisticThreadsRef.current.get(key);
|
|
262
|
+
if (entry && entryTitle !== undefined && entry.title !== entryTitle) {
|
|
263
|
+
const next = new Map(optimisticThreadsRef.current);
|
|
264
|
+
next.set(key, { ...entry, title: entryTitle });
|
|
265
|
+
applyOptimisticThreads(next);
|
|
266
|
+
}
|
|
267
|
+
setSelectedThread((prev) => prev && agentThreadKey(prev) === key && selectedTitle !== undefined && prev.title !== selectedTitle
|
|
268
|
+
? { ...prev, title: selectedTitle }
|
|
269
|
+
: prev);
|
|
270
|
+
};
|
|
271
|
+
const previousRowTitle = threadsRef.current.find((item) => agentThreadKey(item) === key)?.title;
|
|
272
|
+
const previousEntryTitle = optimisticThreadsRef.current.get(key)?.title;
|
|
273
|
+
// currentThread IS the stored selection whenever the selection matters as a
|
|
274
|
+
// fallback (and the fresher list row otherwise) — either way, the right value
|
|
275
|
+
// to restore the selection's title to.
|
|
276
|
+
const current = currentThreadRef.current;
|
|
277
|
+
const previousSelectedTitle = agentThreadKey(current) === key ? current.title : undefined;
|
|
278
|
+
setLocalTitle(trimmed, trimmed, trimmed);
|
|
279
|
+
try {
|
|
280
|
+
const socketId = websocketsService.getSocketId();
|
|
281
|
+
const { updated } = await fetchJson(`/agent-threads/${encodeURIComponent(agentId)}/thread`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: {
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
...(socketId ? { "x-socket-id": socketId } : {}),
|
|
286
|
+
},
|
|
287
|
+
body: JSON.stringify({ chatId: thread.chatId, chatThreadId: thread.chatThreadId, title: trimmed }),
|
|
288
|
+
});
|
|
289
|
+
// `updated: false` means no such row upstream (e.g. renaming before the first
|
|
290
|
+
// turn indexes the conversation) — a failure like any other, or the optimistic
|
|
291
|
+
// title would stick locally and silently vanish on reload.
|
|
292
|
+
if (!updated) {
|
|
293
|
+
throw new Error("Thread rename was not applied");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
setLocalTitle(previousRowTitle, previousEntryTitle, previousSelectedTitle);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}, [agentId, fetchJson, applyThreads, applyOptimisticThreads]);
|
|
301
|
+
// Realtime rows that land while a refresh walk is in flight: the walk's fetches may
|
|
302
|
+
// predate those writes, so replacing the list with the raw walk result could silently
|
|
303
|
+
// revert or drop them — re-merging afterwards keeps whichever side is newer (the
|
|
304
|
+
// merge's staleness guard makes this idempotent).
|
|
305
|
+
const liveWhileWalkingRef = useRef(null);
|
|
306
|
+
// The in-flight refresh, if any, so loadMore can wait it out instead of dropping the
|
|
307
|
+
// gesture (a click on "Load more" often arrives right behind the window-focus refresh
|
|
308
|
+
// the same gesture triggered).
|
|
309
|
+
const refreshPromiseRef = useRef(null);
|
|
310
|
+
const loadFirstPage = useCallback(() => {
|
|
311
|
+
if (!agentId) {
|
|
312
|
+
// Nothing will fetch — make sure the optimistic initial `true` (or a fetch
|
|
313
|
+
// superseded by agentId turning falsy) can't leave the loading flag latched on.
|
|
314
|
+
setIsLoading(false);
|
|
315
|
+
return Promise.resolve();
|
|
316
|
+
}
|
|
317
|
+
const requestSeq = ++requestSeqRef.current;
|
|
318
|
+
liveWhileWalkingRef.current = new Map();
|
|
319
|
+
setIsLoading(true);
|
|
320
|
+
setError(null);
|
|
321
|
+
const run = (async () => {
|
|
322
|
+
try {
|
|
323
|
+
// A refresh re-covers every page the user has already loaded — one request in the
|
|
324
|
+
// common one-page case (the query-change effect blanks the list first, so filter
|
|
325
|
+
// switches also fetch a single page). The alternatives both break pagination:
|
|
326
|
+
// replacing with page one collapses the list back to page one on every focus
|
|
327
|
+
// refresh, and splicing page one over the old tail either rewinds the cursor
|
|
328
|
+
// ("Load more" then refetches known rows and looks dead) or keeps the old cursor
|
|
329
|
+
// and permanently skips rows that arrived beyond page one while the user was away.
|
|
330
|
+
// Walking the cursor keeps the list contiguous from the top, in server order,
|
|
331
|
+
// with the cursor pointing past everything shown.
|
|
332
|
+
const loadedCount = threadsRef.current.length;
|
|
333
|
+
let items = [];
|
|
334
|
+
let cursor = null;
|
|
335
|
+
do {
|
|
336
|
+
const page = await fetchPage(cursor);
|
|
337
|
+
if (requestSeqRef.current !== requestSeq) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (page.items.length === 0) {
|
|
341
|
+
// Defensive: an empty page must terminate the walk even with a non-null cursor.
|
|
342
|
+
cursor = null;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
items = appendPage(items, page.items);
|
|
346
|
+
if (page.nextCursor === cursor) {
|
|
347
|
+
// Defensive: dedupe can hold items.length under loadedCount, so a
|
|
348
|
+
// non-advancing cursor must terminate too or the walk loops unbounded.
|
|
349
|
+
cursor = null;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
cursor = page.nextCursor;
|
|
353
|
+
} while (cursor !== null && items.length < loadedCount);
|
|
354
|
+
for (const live of liveWhileWalkingRef.current?.values() ?? []) {
|
|
355
|
+
items = applyAgentThreadUpdate(items, live, { ...liveUpdateOptions, hasMore: cursor !== null });
|
|
356
|
+
}
|
|
357
|
+
applyThreads(items);
|
|
358
|
+
applyCursor(cursor);
|
|
359
|
+
}
|
|
360
|
+
catch (fetchError) {
|
|
361
|
+
if (requestSeqRef.current !== requestSeq) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
setError(fetchError instanceof Error ? fetchError : new Error(String(fetchError)));
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
if (requestSeqRef.current === requestSeq) {
|
|
368
|
+
setIsLoading(false);
|
|
369
|
+
liveWhileWalkingRef.current = null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
refreshPromiseRef.current = run;
|
|
374
|
+
return run;
|
|
375
|
+
}, [agentId, fetchPage, liveUpdateOptions, applyThreads, applyCursor]);
|
|
376
|
+
// Ref-guarded rather than state-guarded: after the awaited refresh below, the closure's
|
|
377
|
+
// state flags are stale by definition — and a ref also makes double-clicks race-free.
|
|
378
|
+
const loadMoreInFlightRef = useRef(false);
|
|
379
|
+
const loadMore = useCallback(async () => {
|
|
380
|
+
if (!agentId || loadMoreInFlightRef.current) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
loadMoreInFlightRef.current = true;
|
|
384
|
+
setIsLoadingMore(true);
|
|
385
|
+
// Captured BEFORE the wait below: this closure's fetchPage belongs to the render the
|
|
386
|
+
// click landed on, so anything that supersedes the list while we sit behind the
|
|
387
|
+
// refresh (a query change, a newer refresh) must invalidate this gesture too.
|
|
388
|
+
const requestSeq = requestSeqRef.current;
|
|
389
|
+
try {
|
|
390
|
+
// A refresh may be in flight — typically fired by the very window-focus that
|
|
391
|
+
// preceded this click. Wait it out instead of silently swallowing the gesture;
|
|
392
|
+
// the cursor ref then points past everything the refresh walk re-covered.
|
|
393
|
+
if (refreshPromiseRef.current) {
|
|
394
|
+
await refreshPromiseRef.current;
|
|
395
|
+
}
|
|
396
|
+
if (requestSeqRef.current !== requestSeq) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const cursor = nextCursorRef.current;
|
|
400
|
+
if (cursor === null) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const page = await fetchPage(cursor);
|
|
404
|
+
if (requestSeqRef.current !== requestSeq) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
applyThreads(appendPage(threadsRef.current, page.items));
|
|
408
|
+
applyCursor(page.nextCursor);
|
|
409
|
+
}
|
|
410
|
+
catch (fetchError) {
|
|
411
|
+
if (requestSeqRef.current !== requestSeq) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
setError(fetchError instanceof Error ? fetchError : new Error(String(fetchError)));
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
// Unconditional: even when a refresh superseded this request the flags must clear
|
|
418
|
+
// or pagination locks up.
|
|
419
|
+
loadMoreInFlightRef.current = false;
|
|
420
|
+
setIsLoadingMore(false);
|
|
421
|
+
}
|
|
422
|
+
}, [agentId, fetchPage, applyThreads, applyCursor]);
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
applyThreads([]);
|
|
425
|
+
applyCursor(null);
|
|
426
|
+
// Server-side filters know nothing of the optimistic rows — drop them on a query
|
|
427
|
+
// change rather than risk re-listing a snapshot whose server truth the new filter
|
|
428
|
+
// excludes (see the retire-on-eviction note in the realtime handler below).
|
|
429
|
+
applyOptimisticThreads(new Map());
|
|
430
|
+
void loadFirstPage();
|
|
431
|
+
return () => {
|
|
432
|
+
// Invalidate in-flight requests on unmount / query change.
|
|
433
|
+
requestSeqRef.current += 1;
|
|
434
|
+
};
|
|
435
|
+
}, [loadFirstPage, applyThreads, applyCursor, applyOptimisticThreads]);
|
|
436
|
+
// Live sync: every threads-table write lands here as a serialized row, so the happy
|
|
437
|
+
// path is a pure client-side upsert; a payload without the full row falls back to the
|
|
438
|
+
// getOne endpoint. The service-level filter drops other agents' events.
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (!agentId) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const onThreadUpdated = async (data) => {
|
|
444
|
+
// Same superseded-request contract as the fetch paths, guarding the async fallback
|
|
445
|
+
// (trivially true on the no-await happy path).
|
|
446
|
+
const requestSeq = requestSeqRef.current;
|
|
447
|
+
try {
|
|
448
|
+
const raw = data?.thread;
|
|
449
|
+
let incoming;
|
|
450
|
+
if (isAgentThreadItem(raw)) {
|
|
451
|
+
incoming = raw;
|
|
452
|
+
}
|
|
453
|
+
else if (typeof raw?.chatId === "string" && typeof raw.chatThreadId === "string") {
|
|
454
|
+
incoming = await fetchThread(raw.chatId, raw.chatThreadId);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (requestSeqRef.current !== requestSeq) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const key = agentThreadKey(incoming);
|
|
463
|
+
// An authoritative row the active filters reject must retire the thread's
|
|
464
|
+
// optimistic entry too: the merge below evicts the server row, and with no
|
|
465
|
+
// listed row there is no staleness guard to stop the overlay from re-adding
|
|
466
|
+
// the entry's stale snapshot — which no later event or refresh walk could
|
|
467
|
+
// correct, since the server filters the row out as well. Gated on turnCount
|
|
468
|
+
// (server-monotonic, like the overlay's handover) rather than the entry's
|
|
469
|
+
// CLIENT-stamped lastActivityAt, which a fast browser clock could hold ahead
|
|
470
|
+
// of every server timestamp.
|
|
471
|
+
const entry = optimisticThreadsRef.current.get(key);
|
|
472
|
+
if (entry && !matchesAgentThreadFilters(incoming, liveUpdateOptions) && incoming.turnCount >= entry.turnCount) {
|
|
473
|
+
const next = new Map(optimisticThreadsRef.current);
|
|
474
|
+
next.delete(key);
|
|
475
|
+
applyOptimisticThreads(next);
|
|
476
|
+
}
|
|
477
|
+
// Park the row for the in-flight refresh walk (if any) — its fetches may
|
|
478
|
+
// predate this write — then merge it into the current list.
|
|
479
|
+
liveWhileWalkingRef.current?.set(key, incoming);
|
|
480
|
+
applyThreads(applyAgentThreadUpdate(threadsRef.current, incoming, {
|
|
481
|
+
...liveUpdateOptions,
|
|
482
|
+
hasMore: nextCursorRef.current !== null,
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// Best-effort: a failed fallback fetch skips this event — the next event or a
|
|
487
|
+
// recovery refresh re-converges. Must not reject; the websocket service doesn't catch.
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
websocketsService.listen(AGENT_THREAD_UPDATED, onThreadUpdated, { filter: { agentId } });
|
|
491
|
+
return () => {
|
|
492
|
+
websocketsService.unlisten(AGENT_THREAD_UPDATED, onThreadUpdated);
|
|
493
|
+
};
|
|
494
|
+
}, [agentId, liveUpdateOptions, fetchThread, applyThreads, applyOptimisticThreads]);
|
|
495
|
+
return {
|
|
496
|
+
threads: displayThreads,
|
|
497
|
+
currentThread,
|
|
498
|
+
isDraftThread,
|
|
499
|
+
setCurrentThread,
|
|
500
|
+
notifyThreadActivity,
|
|
501
|
+
renameThread,
|
|
502
|
+
isLoading,
|
|
503
|
+
isLoadingMore,
|
|
504
|
+
error,
|
|
505
|
+
hasMore: nextCursor !== null,
|
|
506
|
+
loadMore,
|
|
507
|
+
refresh: loadFirstPage,
|
|
508
|
+
};
|
|
509
|
+
};
|
|
510
|
+
export default useAgentThreads;
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocksdiy/react-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React common",
|
|
6
6
|
"keywords": [],
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./dist/index.js",
|
|
13
13
|
"./useAiStreamChunks": "./dist/hooks/use-ai-stream-chunks.js",
|
|
14
|
+
"./agent-threads": "./dist/hooks/use-agent-threads.js",
|
|
14
15
|
"./agent-chat": "./dist/components/agent-chat.js",
|
|
15
16
|
"./new-agent-chat": "./dist/components/new-agent-chat.js",
|
|
16
17
|
"./iframe-communication": "./dist/iframe-communication/index.js"
|