@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.
@@ -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":"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"}
@@ -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
- // Limitation: doesn't abort an in-flight run on thread-id swap — fine
318
- // because `currentThreadId` is effectively immutable today.
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
- // Only expose thinking once the bootstrap connect phase has completed.
335
- const isThinking = isAgentRunning && !isFetchingMessages;
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
@@ -1,3 +1,4 @@
1
+ export * from "./hooks/use-agent-threads.js";
1
2
  export * from "./hooks/use-ai-stream-chunks.js";
2
3
  export * from "./components/agent-chat.js";
3
4
  export * from "./iframe-communication/index.js";
@@ -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
@@ -1,3 +1,4 @@
1
+ export * from "./hooks/use-agent-threads.js";
1
2
  export * from "./hooks/use-ai-stream-chunks.js";
2
3
  export * from "./components/agent-chat.js";
3
4
  export * from "./iframe-communication/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocksdiy/react-common",
3
- "version": "1.39.0",
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"