@blocksdiy/react-common 1.40.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
@@ -388,9 +388,13 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
388
388
  const attachmentsEnabled = agentChat ? agentChat.disableAttachments !== true : false;
389
389
  const canSendMessage = Boolean(agentChatBlock && agentChat && agent && isConfigLoaded && isAgentReady && !isAgentRunning);
390
390
  const isFetchingMessages = !isConfigLoaded || (!isAgentReady && copilotMessages.length === 0);
391
- // CopilotKit reports `isLoading` during connect/replay as well as real runs.
392
- // Only expose thinking once the bootstrap connect phase has completed.
393
- 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;
394
398
  const runAgentWithRetry = useCallback(async (agentToRun, messageCountAtRunStart, forwardedProps) => {
395
399
  if (agentToRun.isRunning) {
396
400
  return false;
@@ -434,8 +438,20 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
434
438
  return;
435
439
  }
436
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
+ }
437
453
  await runAgentWithRetry(copilotAgent, copilotAgent.messages.length);
438
- }, [canSendMessage, copilotAgent, runAgentWithRetry]);
454
+ }, [canSendMessage, copilotAgent, runAgentWithRetry, onUserMessageSent]);
439
455
  // Not sourced from `useInterrupt`: that hook only hands `resolve` to its own
440
456
  // `render`, which it skips whenever nothing is pending — so after a reload there
441
457
  // is no way to obtain it. `runAgentWithRetry`'s `isRunning` guard makes a
@@ -868,7 +884,7 @@ export const AgentChatRoot = ({ ...props
868
884
  },
869
885
  // Explicit false: when undefined, CopilotKit auto-enables its inspector
870
886
  // devtools on localhost hostnames.
871
- 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 })] }));
872
888
  };
873
889
  export function AgentChatMessage({ message, index, ...props }) {
874
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
+ };
@@ -1,19 +1,5 @@
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
- }
1
+ import { type AgentThreadItem, type AgentThreadSource, type AgentThreadStatus } from "./agent-thread-live-update.js";
2
+ export * from "./agent-thread-live-update.js";
17
3
  export interface AgentThreadsPage {
18
4
  items: AgentThreadItem[];
19
5
  nextCursor: string | null;
@@ -49,6 +35,19 @@ export interface UseAgentThreadsResult {
49
35
  isDraftThread: boolean;
50
36
  /** Move between threads; null mints a fresh draft ("new chat"). Resets when agentId changes. */
51
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>;
52
51
  /** First page (or refresh) in flight. */
53
52
  isLoading: boolean;
54
53
  /** A cursor page is being appended. */
@@ -56,10 +55,13 @@ export interface UseAgentThreadsResult {
56
55
  error: Error | null;
57
56
  hasMore: boolean;
58
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
+ */
59
63
  refresh: () => Promise<void>;
60
64
  }
61
- /** The (chatId, chatThreadId) pair — the only stable identity of a thread row. */
62
- export declare const agentThreadKey: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">) => string;
63
65
  /**
64
66
  * The platform inbox's fixed chatId for an agent: one shared value per agent block, so
65
67
  * index rows are recognizable as "direct chats with this agent on the platform" at a
@@ -76,10 +78,19 @@ export declare const agentInboxChatId: (agentId: string) => string;
76
78
  * the real half rather than labeling every such row "_".
77
79
  */
78
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;
79
83
  /**
80
84
  * Headless agent-threads index: fetches an agent's conversation threads from the platform
81
- * endpoint with keyset-cursor pagination. Data only — pair it with whatever list UI the host
82
- * app uses (the platform's ThreadList, a boilerplate app's own components, ...).
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.
83
94
  */
84
95
  export declare const useAgentThreads: (options: UseAgentThreadsOptions) => UseAgentThreadsResult;
85
96
  export default useAgentThreads;
@@ -1 +1 @@
1
- {"version":3,"file":"use-agent-threads.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-threads.ts"],"names":[],"mappings":"AASA,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,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,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,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,kFAAkF;AAClF,eAAO,MAAM,cAAc,GAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,KAAG,MAC9C,CAAC;AAE7C;;;;;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;AA8DF;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB,KAAG,qBAsPjE,CAAC;AAEF,eAAe,eAAe,CAAC"}
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"}
@@ -1,7 +1,15 @@
1
1
  import { getApiHost } from "@blocksdiy/blocks-client-api/envService";
2
+ import { websocketsService } from "@blocksdiy/blocks-client-api/websocketService";
2
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- /** The (chatId, chatThreadId) pair — the only stable identity of a thread row. */
4
- export const agentThreadKey = (thread) => `${thread.chatId}::${thread.chatThreadId}`;
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";
5
13
  /**
6
14
  * The platform inbox's fixed chatId for an agent: one shared value per agent block, so
7
15
  * index rows are recognizable as "direct chats with this agent on the platform" at a
@@ -22,6 +30,8 @@ export const agentThreadShortLabel = (thread) => {
22
30
  const other = varying === thread.chatId ? thread.chatThreadId : thread.chatId;
23
31
  return (varying === "_" ? other : varying).slice(0, 8);
24
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)}`;
25
35
  const buildQuery = (filters, cursor) => {
26
36
  const params = new URLSearchParams();
27
37
  if (filters.status) {
@@ -76,8 +86,15 @@ const makeDraftThread = (agentId) => {
76
86
  };
77
87
  /**
78
88
  * Headless agent-threads index: fetches an agent's conversation threads from the platform
79
- * endpoint with keyset-cursor pagination. Data only — pair it with whatever list UI the host
80
- * app uses (the platform's ThreadList, a boilerplate app's own components, ...).
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.
81
98
  */
82
99
  export const useAgentThreads = (options) => {
83
100
  const { agentId, status, source, chatId, limit, sort, order, baseUrl } = options;
@@ -106,6 +123,16 @@ export const useAgentThreads = (options) => {
106
123
  const [error, setError] = useState(null);
107
124
  const [selectedThread, setSelectedThread] = useState(null);
108
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
+ }, []);
109
136
  const setCurrentThread = useCallback((thread) => {
110
137
  // Null means "new chat": mint a fresh draft so each one is its own conversation.
111
138
  if (!thread) {
@@ -125,31 +152,61 @@ export const useAgentThreads = (options) => {
125
152
  prevAgentIdRef.current = agentId;
126
153
  setSelectedThread(null);
127
154
  setDraftThread(makeDraftThread(agentId));
128
- }, [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]);
129
162
  // Rows are rewritten server-side every turn, so after a refresh the stored item is stale;
130
163
  // prefer the list's row with the same identity, keep the stored one if it's not loaded.
131
- // The draft goes through the same lookup: once its first turn is indexed, the real row
132
- // wins under the same key — the draft has materialized, without changing identity.
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.
133
167
  const currentThread = useMemo(() => {
134
168
  const stored = selectedThread ?? draftThread;
135
169
  const storedKey = agentThreadKey(stored);
136
- return threads.find((thread) => agentThreadKey(thread) === storedKey) ?? stored;
137
- }, [threads, selectedThread, draftThread]);
170
+ return displayThreads.find((thread) => agentThreadKey(thread) === storedKey) ?? stored;
171
+ }, [displayThreads, selectedThread, draftThread]);
138
172
  const isDraftThread = !selectedThread && currentThread === draftThread;
139
- // Once the draft's first turn is indexed, its row IS the conversation — adopt it as the
140
- // selection. Otherwise a later filter change (which blanks the list before refetching)
141
- // would flip `isDraftThread` back to true for a conversation that has turns, and the
142
- // consumer's draft-only presentation ("New chat" header, empty-state slot) with it.
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.
143
177
  useEffect(() => {
144
178
  if (selectedThread) {
145
179
  return;
146
180
  }
147
181
  const draftKey = agentThreadKey(draftThread);
148
- const materialized = threads.find((thread) => agentThreadKey(thread) === draftKey);
182
+ const materialized = displayThreads.find((thread) => agentThreadKey(thread) === draftKey);
149
183
  if (materialized) {
150
184
  setSelectedThread(materialized);
151
185
  }
152
- }, [threads, selectedThread, draftThread]);
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]);
153
210
  // Auth material is read at request time via refs so an inline `headers` object literal
154
211
  // doesn't churn the effect below into a refetch loop.
155
212
  const authRef = useRef({ token: options.token, headers: options.headers });
@@ -157,20 +214,95 @@ export const useAgentThreads = (options) => {
157
214
  // Every state-writing path checks it still owns the latest request before touching state,
158
215
  // so a stale response (filter switched mid-flight, unmount) can never clobber a newer one.
159
216
  const requestSeqRef = useRef(0);
160
- const fetchPage = useCallback(async (cursor) => {
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) => {
161
220
  const { token, headers } = authRef.current;
162
- const base = baseUrl ?? getApiHost();
163
- const response = await fetch(`${base}/agent-threads/${encodeURIComponent(agentId)}${buildQuery({ status, source, chatId, limit, sort, order }, cursor)}`, {
221
+ const response = await fetch(`${baseUrl ?? getApiHost()}${path}`, {
222
+ ...init,
164
223
  headers: {
165
224
  ...(token ? { Authorization: `Bearer ${token}` } : {}),
166
225
  ...headers,
226
+ ...init?.headers,
167
227
  },
168
228
  });
169
229
  if (!response.ok) {
170
230
  throw new Error(`Agent threads request failed with status ${response.status}`);
171
231
  }
172
232
  return (await response.json());
173
- }, [agentId, status, source, chatId, limit, sort, order, baseUrl]);
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);
174
306
  // The in-flight refresh, if any, so loadMore can wait it out instead of dropping the
175
307
  // gesture (a click on "Load more" often arrives right behind the window-focus refresh
176
308
  // the same gesture triggered).
@@ -183,6 +315,7 @@ export const useAgentThreads = (options) => {
183
315
  return Promise.resolve();
184
316
  }
185
317
  const requestSeq = ++requestSeqRef.current;
318
+ liveWhileWalkingRef.current = new Map();
186
319
  setIsLoading(true);
187
320
  setError(null);
188
321
  const run = (async () => {
@@ -218,6 +351,9 @@ export const useAgentThreads = (options) => {
218
351
  }
219
352
  cursor = page.nextCursor;
220
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
+ }
221
357
  applyThreads(items);
222
358
  applyCursor(cursor);
223
359
  }
@@ -230,12 +366,13 @@ export const useAgentThreads = (options) => {
230
366
  finally {
231
367
  if (requestSeqRef.current === requestSeq) {
232
368
  setIsLoading(false);
369
+ liveWhileWalkingRef.current = null;
233
370
  }
234
371
  }
235
372
  })();
236
373
  refreshPromiseRef.current = run;
237
374
  return run;
238
- }, [agentId, fetchPage, applyThreads, applyCursor]);
375
+ }, [agentId, fetchPage, liveUpdateOptions, applyThreads, applyCursor]);
239
376
  // Ref-guarded rather than state-guarded: after the awaited refresh below, the closure's
240
377
  // state flags are stale by definition — and a ref also makes double-clicks race-free.
241
378
  const loadMoreInFlightRef = useRef(false);
@@ -286,17 +423,82 @@ export const useAgentThreads = (options) => {
286
423
  useEffect(() => {
287
424
  applyThreads([]);
288
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());
289
430
  void loadFirstPage();
290
431
  return () => {
291
432
  // Invalidate in-flight requests on unmount / query change.
292
433
  requestSeqRef.current += 1;
293
434
  };
294
- }, [loadFirstPage, applyThreads, applyCursor]);
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]);
295
495
  return {
296
- threads,
496
+ threads: displayThreads,
297
497
  currentThread,
298
498
  isDraftThread,
299
499
  setCurrentThread,
500
+ notifyThreadActivity,
501
+ renameThread,
300
502
  isLoading,
301
503
  isLoadingMore,
302
504
  error,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocksdiy/react-common",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "type": "module",
5
5
  "description": "React common",
6
6
  "keywords": [],