@blocksdiy/react-common 1.48.0 → 1.49.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.
@@ -1 +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,UAAU,GAAG,SAAS,CAAC;AAEvG,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;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,8FAA8F;IAC9F,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,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;AAW7C;;;;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;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GACpC,QAAQ,eAAe,EACvB,UAAU,eAAe,GAAG,SAAS,EACrC,QAAQ,MAAM,GAAG,SAAS,EAC1B,KAAK,MAAM,KACV,eAID,CAAC;AAEH,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"}
1
+ {"version":3,"file":"agent-thread-live-update.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-thread-live-update.ts"],"names":[],"mappings":"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"}
@@ -1,21 +1,7 @@
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. */
1
+ // Wire types and pure merge logic for the agent-threads index; dependency-free for tests.
2
+ const THREAD_STATUSES = ["idle", "interrupted", "waiting_on_agent", "busy", "error"];
3
+ const THREAD_SOURCES = ["direct_chat", "send_message", "feedback", "self_config_resume", "unknown"];
5
4
  export const agentThreadKey = (thread) => `${thread.chatId}::${thread.chatThreadId}`;
6
- const THREAD_STATUSES = ["idle", "interrupted", "error"];
7
- const THREAD_SOURCES = [
8
- "direct_chat",
9
- "agent_call",
10
- "send_message",
11
- "feedback",
12
- "unknown",
13
- ];
14
- /**
15
- * Whether a realtime payload's `thread` is a complete list-item row. The server
16
- * serializes the exact item shape into every event, so this normally passes; when
17
- * it doesn't (contract drift), the consumer falls back to the getOne endpoint.
18
- */
19
5
  export const isAgentThreadItem = (value) => {
20
6
  const item = value;
21
7
  return (typeof item === "object" &&
@@ -30,15 +16,11 @@ export const isAgentThreadItem = (value) => {
30
16
  (typeof item.title === "string" || item.title === null) &&
31
17
  typeof item.turnCount === "number" &&
32
18
  typeof item.firstSeenAt === "string" &&
33
- typeof item.lastActivityAt === "string");
19
+ typeof item.lastActivityAt === "string" &&
20
+ (item.runningSince == null || typeof item.runningSince === "string") &&
21
+ (item.runningRunId == null || typeof item.runningRunId === "string"));
34
22
  };
35
- // Matches the server's cap for LLM-generated titles (wf-actions-py TITLE_MAX_CHARS).
36
23
  const PROVISIONAL_TITLE_MAX_CHARS = 80;
37
- /**
38
- * Provisional row title from the prompt that started a conversation (ChatGPT-style):
39
- * one line, whitespace collapsed, capped like the server's generated titles. Null when
40
- * there's nothing usable (attachment-only sends) — the UI keeps its id-based fallback.
41
- */
42
24
  export const provisionalThreadTitle = (prompt) => {
43
25
  const collapsed = prompt?.replace(/\s+/g, " ").trim();
44
26
  if (!collapsed) {
@@ -48,42 +30,46 @@ export const provisionalThreadTitle = (prompt) => {
48
30
  ? `${collapsed.slice(0, PROVISIONAL_TITLE_MAX_CHARS - 1).trimEnd()}…`
49
31
  : collapsed;
50
32
  };
51
- /**
52
- * The optimistic row for a user send on `thread`, replacing `previous` (its current
53
- * optimistic entry, if any): the same identity, its recency bumped to `now`, and a title
54
- * that never regresses — the provisional title already shown, else the row's own, else
55
- * one from this prompt (null when the prompt has nothing usable).
56
- */
33
+ // Optimistic send row; never carries a run — the server's row for this send owns busy.
57
34
  export const optimisticThreadAfterSend = (thread, previous, prompt, now) => ({
58
35
  ...thread,
59
- title: previous?.title ?? thread.title ?? provisionalThreadTitle(prompt),
36
+ status: thread.status === "busy" ? "idle" : thread.status,
37
+ runningSince: null,
38
+ runningRunId: null,
39
+ title: thread.title ?? previous?.title ?? provisionalThreadTitle(prompt),
60
40
  lastActivityAt: now,
61
41
  });
42
+ // Stands aside when the listed row has turns or a different run in flight.
43
+ const removeListedAgentThread = (threads, removed) => {
44
+ const key = agentThreadKey(removed);
45
+ const listed = threads.find((thread) => agentThreadKey(thread) === key);
46
+ if (listed &&
47
+ (listed.turnCount > 0 || (listed.runningRunId != null && listed.runningRunId !== removed.runningRunId))) {
48
+ return null;
49
+ }
50
+ return listed ? threads.filter((thread) => thread !== listed) : threads;
51
+ };
52
+ export const removeAgentThread = (threads, optimistic, removed) => {
53
+ const remaining = removeListedAgentThread(threads, removed);
54
+ if (remaining === null) {
55
+ return null;
56
+ }
57
+ const key = agentThreadKey(removed);
58
+ const withoutEntry = new Map(optimistic);
59
+ withoutEntry.delete(key);
60
+ return { threads: remaining, optimistic: optimistic.has(key) ? withoutEntry : optimistic };
61
+ };
62
62
  const sortTime = (item, sort) => {
63
63
  const parsed = new Date(sort === "firstSeen" ? item.firstSeenAt : item.lastActivityAt).getTime();
64
64
  return Number.isNaN(parsed) ? 0 : parsed;
65
65
  };
66
- /**
67
- * Merge one realtime thread update into a loaded list. Pure; returns the input array
68
- * untouched when the event is stale, filtered out, or out of the loaded window.
69
- *
70
- * - Out-of-order delivery can't regress a row: events older than the loaded row (by
71
- * lastActivityAt) are dropped — checked FIRST, so a stale event can't evict a
72
- * still-current row through the filter check below either. Renames keep
73
- * lastActivityAt, so equal passes.
74
- * - A row that stops matching the active filters is removed; a matching one is upserted
75
- * and the list re-sorted by the active sort (stable, so ties keep their server order).
76
- * - A NEW row sorting past every loaded row while more pages exist may belong beyond the
77
- * loaded window; it's skipped so the list stays contiguous with the pagination cursor
78
- * (unreachable under the default lastActivity-desc sort, where fresh activity sorts first).
79
- */
80
- /** Whether a row passes the list's active server-side filters. */
81
66
  export const matchesAgentThreadFilters = (item, options = {}) => (!options.status || item.status === options.status) &&
82
67
  (!options.source || item.source === options.source) &&
83
68
  (!options.chatId || item.chatId === options.chatId);
84
69
  export const applyAgentThreadUpdate = (threads, incoming, options = {}) => {
85
70
  const key = agentThreadKey(incoming);
86
71
  const existing = threads.find((thread) => agentThreadKey(thread) === key);
72
+ // Stale by lastActivityAt is dropped; equal passes (renames keep lastActivityAt).
87
73
  if (existing && sortTime(incoming, "lastActivity") < sortTime(existing, "lastActivity")) {
88
74
  return threads;
89
75
  }
@@ -99,18 +85,16 @@ export const applyAgentThreadUpdate = (threads, incoming, options = {}) => {
99
85
  }
100
86
  return next;
101
87
  };
102
- /**
103
- * Overlay optimistic rows (conversations the user just acted on, ChatGPT-style) onto the
104
- * loaded list. Each row goes through the same merge as a realtime event, so filters and
105
- * sorting apply — except that a server row still untitled borrows the optimistic
106
- * (prompt-derived) title until the generated one lands. Pure; returns `threads` untouched
107
- * when nothing changes.
108
- *
109
- * An entry steps aside once the loaded row has recorded MORE turns than it did: turnCount
110
- * is server-monotonic, so the handover can't be held off by a client clock running ahead
111
- * of the server (the entry's lastActivityAt is stamped locally, and the merge's staleness
112
- * guard compares it against server timestamps).
113
- */
88
+ export const reconcileWalkWithLive = (items, live, options = {}) => {
89
+ let merged = items;
90
+ for (const entry of live.values()) {
91
+ merged =
92
+ "removed" in entry
93
+ ? (removeListedAgentThread(merged, entry.removed) ?? merged)
94
+ : applyAgentThreadUpdate(merged, entry, options);
95
+ }
96
+ return merged;
97
+ };
114
98
  export const overlayOptimisticThreads = (threads, optimistic, options = {}) => {
115
99
  if (optimistic.size === 0) {
116
100
  return threads;
@@ -122,11 +106,44 @@ export const overlayOptimisticThreads = (threads, optimistic, options = {}) => {
122
106
  merged = applyAgentThreadUpdate(merged, row, options);
123
107
  }
124
108
  }
125
- if (!merged.some((row) => row.title === null && optimistic.get(agentThreadKey(row))?.title)) {
126
- return merged;
109
+ return merged;
110
+ };
111
+ // Retires for good once the server row shows a run — a settled error must not be hidden.
112
+ export const retireOptimisticThreadsTakenOver = (optimistic, threads) => {
113
+ const taken = threads.filter((thread) => (thread.status === "busy" || thread.runningRunId != null) && optimistic.has(agentThreadKey(thread)));
114
+ if (taken.length === 0) {
115
+ return optimistic;
116
+ }
117
+ const next = new Map(optimistic);
118
+ for (const thread of taken) {
119
+ next.delete(agentThreadKey(thread));
120
+ }
121
+ return next;
122
+ };
123
+ /** The server's cap on a title a person sets (RENAME_TITLE_MAX_CHARS in wf-actions-py). */
124
+ export const RENAME_TITLE_MAX_CHARS = 255;
125
+ /** Conversation-index groups, in display order; a row's group follows its write-once `source`. */
126
+ export const AGENT_THREAD_GROUPS = ["chats", "workflows", "feedback"];
127
+ const GROUP_OF_SOURCE = {
128
+ direct_chat: "chats",
129
+ self_config_resume: "chats",
130
+ unknown: "chats",
131
+ send_message: "workflows",
132
+ feedback: "feedback",
133
+ };
134
+ /** Where a row files; a source this copy predates files under chats, so a row never renders nowhere. */
135
+ export const agentThreadGroup = (source) => GROUP_OF_SOURCE[source] ?? "chats";
136
+ /** Rows by group in group order, each group in input order; empty groups dropped. */
137
+ export const groupAgentThreads = (threads) => {
138
+ const grouped = AGENT_THREAD_GROUPS.map((group) => ({ group, rows: [] }));
139
+ for (const thread of threads) {
140
+ grouped[AGENT_THREAD_GROUPS.indexOf(agentThreadGroup(thread.source))].rows.push(thread);
127
141
  }
128
- return merged.map((row) => {
129
- const provisional = row.title === null ? optimistic.get(agentThreadKey(row))?.title : null;
130
- return provisional ? { ...row, title: provisional } : row;
131
- });
142
+ return grouped.filter(({ rows }) => rows.length > 0);
132
143
  };
144
+ /** A thread the agent opened to act on feedback — by `type`, or by `source` for rows from before the column. */
145
+ export const isFeedbackThread = (row) => row.type === "feedback" || row.source === "feedback";
146
+ /** The conversation a feedback thread was opened from, when `threads` lists it (same chat, the row's pointer). */
147
+ export const feedbackSourceThread = (row, threads) => row.fromSourceChatThreadId
148
+ ? threads.find((candidate) => candidate.chatId === row.chatId && candidate.chatThreadId === row.fromSourceChatThreadId)
149
+ : undefined;
@@ -0,0 +1,105 @@
1
+ import { type AgentThreadItem } from "./agent-thread-live-update.js";
2
+ import type { UseAgentThreadsResult } from "./use-agent-threads.js";
3
+ /** A conversation is the (chatId, chatThreadId) PAIR — rows under a fixed thread id differ by chat. */
4
+ export type AgentConversationRef = Pick<AgentThreadItem, "chatId" | "chatThreadId">;
5
+ /**
6
+ * A new chat is a draft: a thread id minted here, listed nowhere until the first send. "New
7
+ * chat" on an untouched draft keeps it; once sent, the draft is a thread like any other.
8
+ */
9
+ export interface AgentConversationSelection {
10
+ conversation: AgentConversationRef;
11
+ draftThreadId: string;
12
+ draftSent: boolean;
13
+ }
14
+ export declare const newDraftSelection: (chatId: string, draftThreadId?: string) => AgentConversationSelection;
15
+ /** A draft stands by even when the URL opens a thread: "New chat" from there must not mint twice. */
16
+ export declare const initialSelection: (ownChatId: string, urlConversation: AgentConversationRef | undefined) => AgentConversationSelection;
17
+ export declare const isDraftSelection: ({ conversation, draftThreadId, draftSent }: AgentConversationSelection) => boolean;
18
+ /** After choosing a thread; the same object when it is already open (no re-render). */
19
+ export declare const selectionWithThread: (prev: AgentConversationSelection, conversation: AgentConversationRef) => AgentConversationSelection;
20
+ /** After "New chat": an untouched draft stays; anything else mints a fresh one. */
21
+ export declare const selectionWithNewChat: (prev: AgentConversationSelection, ownChatId: string) => AgentConversationSelection;
22
+ /** After the first send on the draft; unchanged when the draft is not the open conversation. */
23
+ export declare const selectionAfterDraftSent: (prev: AgentConversationSelection) => AgentConversationSelection;
24
+ /** What the URL names: a thread, the new chat, or no conversation (one of the surface's pages). */
25
+ export type AgentConversationUrlTarget = AgentConversationRef | "new" | null;
26
+ /** Identity of a URL target; the selection follows the URL whenever this changes. */
27
+ export declare const urlSelectionKey: (target: AgentConversationUrlTarget) => string | null;
28
+ export interface UseAgentConversationSelectionOptions {
29
+ /** The chat the surface's own conversations live under (agentInboxChatId). */
30
+ ownChatId: string;
31
+ urlTarget: AgentConversationUrlTarget;
32
+ }
33
+ export interface UseAgentConversationSelectionResult {
34
+ conversation: AgentConversationRef;
35
+ /** The new chat, nothing sent on it yet. */
36
+ isDraft: boolean;
37
+ /** After the first send on the draft — the caller then navigates to the draft's thread URL. */
38
+ markDraftSent: () => void;
39
+ }
40
+ /**
41
+ * The open conversation follows the URL — the one source of truth, so choosing a thread is a
42
+ * navigation, never a call here; the draft is local state.
43
+ */
44
+ export declare const useAgentConversationSelection: ({ ownChatId, urlTarget, }: UseAgentConversationSelectionOptions) => UseAgentConversationSelectionResult;
45
+ export declare const RUN_STALE_MS: number;
46
+ /** A row for a conversation the index lists nothing for (yet): the draft, or a thread whose row has not landed. */
47
+ export declare const agentThreadStandIn: (agentId: string, conversation: AgentConversationRef, isDraft: boolean) => AgentThreadItem;
48
+ /** When the oldest run mark among `rows` reaches the stale ceiling (epoch ms); null with no mark. */
49
+ export declare const nextRunStaleAt: (rows: readonly AgentThreadItem[]) => number | null;
50
+ /** The turns this client has accounted for on one conversation (`key`: agentThreadKey). */
51
+ export interface AccountedTurns {
52
+ key: string;
53
+ turns: number;
54
+ }
55
+ /**
56
+ * The accounting after a row for `key` reports `turnCount` turns. The first row is a baseline —
57
+ * the chat replayed it on mount — not a turn to fetch. `turnCount` is server-monotonic, so more
58
+ * turns than accounted is a turn this client did not make: replayed once nothing is streaming
59
+ * (a run's own stream is the authoritative view; replaying underneath it would fight its events).
60
+ */
61
+ export declare const accountedTurnsAfterRow: (accounted: AccountedTurns | null, key: string, turnCount: number, isThinking: boolean) => {
62
+ accounted: AccountedTurns | null;
63
+ replay: boolean;
64
+ };
65
+ /**
66
+ * The accounting after this client's own send on `key`: one more turn. A draft's first send
67
+ * starts the count, so the row that lands for it is read as this send's, not a foreign turn.
68
+ */
69
+ export declare const accountedTurnsAfterOwnSend: (accounted: AccountedTurns | null, key: string, isDraft: boolean) => AccountedTurns | null;
70
+ /** The chat runtime's side of the bridge — a subset of react-common's AgentChatContextValue. */
71
+ export interface AgentConversationChat {
72
+ isThinking: boolean;
73
+ reloadMessages: () => Promise<void>;
74
+ /** Optional as on AgentChatContextValue: a context a host builds by hand may omit it. */
75
+ setRunningElsewhere?: (runId: string | null) => void;
76
+ }
77
+ export interface UseAgentConversationOptions {
78
+ /** The index the conversation is listed in. */
79
+ threads: UseAgentThreadsResult;
80
+ agentId: string;
81
+ conversation: AgentConversationRef;
82
+ isDraft: boolean;
83
+ chat: AgentConversationChat;
84
+ }
85
+ export interface UseAgentConversationResult {
86
+ /** The conversation's row: listed, else the last listed, else a stand-in. */
87
+ row: AgentThreadItem;
88
+ /** `row` is invented locally — the index lists nothing for this conversation. */
89
+ isStandIn: boolean;
90
+ /** `threads.renameThread`, keeping the remembered row's title current too. */
91
+ renameThread: UseAgentThreadsResult["renameThread"];
92
+ /** After this client sends on the conversation (the chat root's onUserMessageSent). */
93
+ notifyMessageSent: (text: string, message?: {
94
+ name?: string;
95
+ }) => void;
96
+ }
97
+ /**
98
+ * Keeps the index and the transcript agreeing for the open conversation:
99
+ * - a run on it this client is not streaming (the listed row's `runningRunId`) reads as the chat's own;
100
+ * - a turn this client did not send (`turnCount` ahead of what it accounted for) replays the transcript;
101
+ * - a run mark past the stale ceiling asks the server again (the settle publishes no event);
102
+ * - refocus re-walks the list (realtime has no replay), throttled.
103
+ */
104
+ export declare const useAgentConversation: ({ threads, agentId, conversation, isDraft, chat, }: UseAgentConversationOptions) => UseAgentConversationResult;
105
+ //# sourceMappingURL=use-agent-conversation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-agent-conversation.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-conversation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,204 @@
1
+ // Headless state for the conversation an agent surface has open, over the threads index
2
+ // (useAgentThreads) and the chat runtime (useAgentChat): which conversation is selected and
3
+ // how a draft becomes a thread; the conversation's row; and the bridge that keeps the index
4
+ // and the transcript agreeing. UI-free — the agent-layout registry item renders it.
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { FEEDBACK_MESSAGE_NAME } from "../utils/composer-message.js";
7
+ import { agentThreadKey } from "./agent-thread-live-update.js";
8
+ export const newDraftSelection = (chatId, draftThreadId = crypto.randomUUID()) => ({
9
+ conversation: { chatId, chatThreadId: draftThreadId },
10
+ draftThreadId,
11
+ draftSent: false,
12
+ });
13
+ /** A draft stands by even when the URL opens a thread: "New chat" from there must not mint twice. */
14
+ export const initialSelection = (ownChatId, urlConversation) => {
15
+ const draft = newDraftSelection(ownChatId);
16
+ return urlConversation ? { ...draft, conversation: urlConversation } : draft;
17
+ };
18
+ export const isDraftSelection = ({ conversation, draftThreadId, draftSent }) => conversation.chatThreadId === draftThreadId && !draftSent;
19
+ /** After choosing a thread; the same object when it is already open (no re-render). */
20
+ export const selectionWithThread = (prev, conversation) => agentThreadKey(prev.conversation) === agentThreadKey(conversation) ? prev : { ...prev, conversation };
21
+ /** After "New chat": an untouched draft stays; anything else mints a fresh one. */
22
+ export const selectionWithNewChat = (prev, ownChatId) => (isDraftSelection(prev) ? prev : newDraftSelection(ownChatId));
23
+ /** After the first send on the draft; unchanged when the draft is not the open conversation. */
24
+ export const selectionAfterDraftSent = (prev) => isDraftSelection(prev) ? { ...prev, draftSent: true } : prev;
25
+ /** Identity of a URL target; the selection follows the URL whenever this changes. */
26
+ export const urlSelectionKey = (target) => target === null ? null : target === "new" ? "new" : agentThreadKey(target);
27
+ /**
28
+ * The open conversation follows the URL — the one source of truth, so choosing a thread is a
29
+ * navigation, never a call here; the draft is local state.
30
+ */
31
+ export const useAgentConversationSelection = ({ ownChatId, urlTarget, }) => {
32
+ const urlConversation = urlTarget !== null && urlTarget !== "new" ? urlTarget : undefined;
33
+ const [selection, setSelection] = useState(() => initialSelection(ownChatId, urlConversation));
34
+ const markDraftSent = useCallback(() => setSelection(selectionAfterDraftSent), []);
35
+ // Synced during render (React's "adjusting state when a prop changes"), so the chat root
36
+ // below never renders a frame on the previous conversation.
37
+ const urlKey = urlSelectionKey(urlTarget);
38
+ const [syncedUrlKey, setSyncedUrlKey] = useState(urlKey);
39
+ if (syncedUrlKey !== urlKey) {
40
+ setSyncedUrlKey(urlKey);
41
+ if (urlConversation) {
42
+ setSelection((prev) => selectionWithThread(prev, urlConversation));
43
+ }
44
+ else if (urlTarget === "new") {
45
+ setSelection((prev) => selectionWithNewChat(prev, ownChatId));
46
+ }
47
+ }
48
+ return { conversation: selection.conversation, isDraft: isDraftSelection(selection), markDraftSent };
49
+ };
50
+ /* ---- The open conversation's row, and the index ↔ chat bridge ----------------------------- */
51
+ // Mirrors wf-actions-py RUN_STALE_AFTER: the server ends a run still marked this long after it
52
+ // started, ahead of a list read — and publishes nothing for it, so a viewer has to ask.
53
+ export const RUN_STALE_MS = 30 * 60 * 1000;
54
+ // A refresh re-walks every loaded page; refocus is throttled to this.
55
+ const FOCUS_REFRESH_MIN_INTERVAL_MS = 15_000;
56
+ // While a mark stays past the ceiling (this clock may run ahead of the server's), ask again each minute.
57
+ const STALE_RUN_REPOLL_MS = 60_000;
58
+ /** A row for a conversation the index lists nothing for (yet): the draft, or a thread whose row has not landed. */
59
+ export const agentThreadStandIn = (agentId, conversation, isDraft) => {
60
+ const now = new Date().toISOString();
61
+ return {
62
+ agentId,
63
+ chatId: conversation.chatId,
64
+ chatThreadId: conversation.chatThreadId,
65
+ source: "direct_chat",
66
+ status: "idle",
67
+ title: null,
68
+ turnCount: isDraft ? 0 : 1,
69
+ firstSeenAt: now,
70
+ lastActivityAt: now,
71
+ };
72
+ };
73
+ /** When the oldest run mark among `rows` reaches the stale ceiling (epoch ms); null with no mark. */
74
+ export const nextRunStaleAt = (rows) => {
75
+ let next = null;
76
+ for (const row of rows) {
77
+ const startedAt = Date.parse(row.runningSince ?? "");
78
+ if (Number.isFinite(startedAt) && (next === null || startedAt + RUN_STALE_MS < next)) {
79
+ next = startedAt + RUN_STALE_MS;
80
+ }
81
+ }
82
+ return next;
83
+ };
84
+ /**
85
+ * The accounting after a row for `key` reports `turnCount` turns. The first row is a baseline —
86
+ * the chat replayed it on mount — not a turn to fetch. `turnCount` is server-monotonic, so more
87
+ * turns than accounted is a turn this client did not make: replayed once nothing is streaming
88
+ * (a run's own stream is the authoritative view; replaying underneath it would fight its events).
89
+ */
90
+ export const accountedTurnsAfterRow = (accounted, key, turnCount, isThinking) => {
91
+ if (!accounted || accounted.key !== key) {
92
+ return { accounted: { key, turns: turnCount }, replay: false };
93
+ }
94
+ if (turnCount <= accounted.turns || isThinking) {
95
+ return { accounted, replay: false };
96
+ }
97
+ return { accounted: { key, turns: turnCount }, replay: true };
98
+ };
99
+ /**
100
+ * The accounting after this client's own send on `key`: one more turn. A draft's first send
101
+ * starts the count, so the row that lands for it is read as this send's, not a foreign turn.
102
+ */
103
+ export const accountedTurnsAfterOwnSend = (accounted, key, isDraft) => {
104
+ if (accounted && accounted.key === key) {
105
+ return { key, turns: accounted.turns + 1 };
106
+ }
107
+ return isDraft ? { key, turns: 1 } : accounted;
108
+ };
109
+ /**
110
+ * Keeps the index and the transcript agreeing for the open conversation:
111
+ * - a run on it this client is not streaming (the listed row's `runningRunId`) reads as the chat's own;
112
+ * - a turn this client did not send (`turnCount` ahead of what it accounted for) replays the transcript;
113
+ * - a run mark past the stale ceiling asks the server again (the settle publishes no event);
114
+ * - refocus re-walks the list (realtime has no replay), throttled.
115
+ */
116
+ export const useAgentConversation = ({ threads, agentId, conversation, isDraft, chat, }) => {
117
+ const { threads: rows, refresh, notifyThreadActivity, renameThread: renameListedThread } = threads;
118
+ const { isThinking, reloadMessages, setRunningElsewhere } = chat;
119
+ const conversationKey = agentThreadKey(conversation);
120
+ const listedRow = rows.find((row) => agentThreadKey(row) === conversationKey);
121
+ // A row a filter hides is remembered, so the open conversation keeps its title. Stored
122
+ // during render (React's "storing information from previous renders"), not in an effect.
123
+ const [lastListedRow, setLastListedRow] = useState(null);
124
+ if (listedRow && listedRow !== lastListedRow) {
125
+ setLastListedRow(listedRow);
126
+ }
127
+ const knownRow = listedRow ?? (lastListedRow && agentThreadKey(lastListedRow) === conversationKey ? lastListedRow : undefined);
128
+ // Not memoised: a stand-in's stamps are "now", and a send stands its row in with them.
129
+ const row = knownRow ?? agentThreadStandIn(agentId, conversation, isDraft);
130
+ const rowRef = useRef(row);
131
+ useEffect(() => {
132
+ rowRef.current = row;
133
+ });
134
+ const renameThread = useCallback(async (thread, title) => {
135
+ await renameListedThread(thread, title);
136
+ const trimmed = title.trim();
137
+ if (!trimmed) {
138
+ return;
139
+ }
140
+ setLastListedRow((prev) => prev && agentThreadKey(prev) === agentThreadKey(thread) && prev.title !== trimmed
141
+ ? { ...prev, title: trimmed }
142
+ : prev);
143
+ }, [renameListedThread]);
144
+ // A run elsewhere on this conversation — from the LISTED row only: a row a filter hides gets
145
+ // no live updates, so a remembered one would outlive its run.
146
+ const runningElsewhereRunId = listedRow?.runningRunId ?? null;
147
+ useEffect(() => {
148
+ setRunningElsewhere?.(runningElsewhereRunId);
149
+ // Nothing else clears it: told once, the chat would stay shut past this surface's lifetime.
150
+ return () => setRunningElsewhere?.(null);
151
+ }, [setRunningElsewhere, runningElsewhereRunId]);
152
+ // The stale ceiling: ask at the oldest mark's ceiling, then each minute while a mark stays.
153
+ const nextStaleAt = nextRunStaleAt(rows);
154
+ useEffect(() => {
155
+ if (nextStaleAt === null) {
156
+ return;
157
+ }
158
+ let timer;
159
+ const ask = () => {
160
+ void refresh();
161
+ timer = setTimeout(ask, STALE_RUN_REPOLL_MS);
162
+ };
163
+ timer = setTimeout(ask, Math.max(nextStaleAt - Date.now(), 0));
164
+ return () => clearTimeout(timer);
165
+ }, [nextStaleAt, refresh]);
166
+ // Realtime has no replay — refocus re-walks the list, throttled.
167
+ const lastFocusRefreshAtRef = useRef(0);
168
+ useEffect(() => {
169
+ const onFocus = () => {
170
+ const now = Date.now();
171
+ if (now - lastFocusRefreshAtRef.current < FOCUS_REFRESH_MIN_INTERVAL_MS) {
172
+ return;
173
+ }
174
+ lastFocusRefreshAtRef.current = now;
175
+ void refresh();
176
+ };
177
+ window.addEventListener("focus", onFocus);
178
+ return () => window.removeEventListener("focus", onFocus);
179
+ }, [refresh]);
180
+ // Turns this client did not send (see accountedTurnsAfterRow). `isThinking` is a dependency,
181
+ // so this runs again the moment a run on the conversation settles.
182
+ const accountedTurnsRef = useRef(null);
183
+ useEffect(() => {
184
+ if (!knownRow) {
185
+ return;
186
+ }
187
+ const next = accountedTurnsAfterRow(accountedTurnsRef.current, conversationKey, knownRow.turnCount, isThinking);
188
+ accountedTurnsRef.current = next.accounted;
189
+ if (next.replay) {
190
+ void reloadMessages();
191
+ }
192
+ }, [knownRow, conversationKey, isThinking, reloadMessages]);
193
+ const notifyMessageSent = useCallback((text, message) => {
194
+ // A feedback send is recorded on the conversation with no reply there: activity moves;
195
+ // no turn is accounted, and the prompt does not name the thread.
196
+ const isFeedbackSend = message?.name === FEEDBACK_MESSAGE_NAME;
197
+ notifyThreadActivity(rowRef.current, isFeedbackSend ? undefined : text);
198
+ if (isFeedbackSend) {
199
+ return;
200
+ }
201
+ accountedTurnsRef.current = accountedTurnsAfterOwnSend(accountedTurnsRef.current, conversationKey, isDraft);
202
+ }, [notifyThreadActivity, conversationKey, isDraft]);
203
+ return { row, isStandIn: knownRow === undefined, renameThread, notifyMessageSent };
204
+ };
@@ -1,5 +1,7 @@
1
1
  import { type AgentThreadItem, type AgentThreadSource, type AgentThreadStatus } from "./agent-thread-live-update.js";
2
2
  export * from "./agent-thread-live-update.js";
3
+ export { type AgentConversationChat, type AgentConversationRef, type AgentConversationUrlTarget, agentThreadStandIn, nextRunStaleAt, RUN_STALE_MS, useAgentConversation, type UseAgentConversationOptions, type UseAgentConversationResult, useAgentConversationSelection, type UseAgentConversationSelectionOptions, type UseAgentConversationSelectionResult, } from "./use-agent-conversation.js";
4
+ export { type AgentConversationContextValue, AgentConversationProvider, type AgentConversationProviderProps, type AgentThreadFilters, useAgentConversationContext, } from "./agent-conversation-context.js";
3
5
  export interface AgentThreadsPage {
4
6
  items: AgentThreadItem[];
5
7
  nextCursor: string | null;
@@ -12,75 +14,26 @@ export interface UseAgentThreadsOptions {
12
14
  limit?: number;
13
15
  sort?: "lastActivity" | "firstSeen";
14
16
  order?: "asc" | "desc";
15
- /** Platform host override; defaults to getApiHost() (same convention as the agent chat). */
16
17
  baseUrl?: string;
17
- /** Bearer token for callers that don't share the platform session cookie. */
18
18
  token?: string;
19
- /** Extra request headers, merged over the defaults. */
20
19
  headers?: Record<string, string>;
21
20
  }
22
21
  export interface UseAgentThreadsResult {
23
22
  threads: AgentThreadItem[];
24
- /**
25
- * Report a user send on a conversation — ChatGPT-style optimistic listing. Pass the
26
- * conversation's row (the listed one when the index has it, else one the consumer
27
- * builds from the (chatId, chatThreadId) pair) and the prompt text: a conversation with
28
- * no server row yet appears in `threads` immediately, titled by the truncated prompt,
29
- * and every send bumps the row's recency; server rows take over under the same identity
30
- * as events land. Identity-stable — safe to hand to memoized chat surfaces.
31
- */
32
23
  notifyThreadActivity: (thread: AgentThreadItem, prompt?: string) => void;
33
- /**
34
- * Rename a conversation. Optimistic — the list shows the new title immediately and
35
- * rolls back on failure (the promise rejects so callers can surface an error).
36
- */
37
24
  renameThread: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">, title: string) => Promise<void>;
38
- /** First page (or refresh) in flight. */
39
25
  isLoading: boolean;
40
- /** A cursor page is being appended. */
41
26
  isLoadingMore: boolean;
42
27
  error: Error | null;
43
28
  hasMore: boolean;
44
29
  loadMore: () => Promise<void>;
45
- /**
46
- * Re-walk every loaded page. Freshness is otherwise live (the hook subscribes to
47
- * AGENT_THREAD_UPDATED itself) — keep refresh for recovery, e.g. a throttled
48
- * window-focus refresh after a dropped connection.
49
- */
50
30
  refresh: () => Promise<void>;
51
31
  }
52
- /**
53
- * The platform inbox's fixed chatId for an agent: one shared value per agent block, so
54
- * index rows are recognizable as "direct chats with this agent on the platform" at a
55
- * glance (and filterable via the endpoint's chatId param). Conversations under it are
56
- * distinguished by their unique chatThreadId.
57
- */
58
32
  export declare const AGENT_INBOX_CHAT_PREFIX = "agent-inbox-";
59
33
  export declare const agentInboxChatId: (agentId: string) => string;
60
- /**
61
- * Short human label for a row with no generated title yet: the half of the identity
62
- * pair that varies for the row. Inbox rows share one fixed chatId (above) and vary by
63
- * thread id; rows from other surfaces (published-app chats) are the reverse. One-sided
64
- * writers (agent calls) index a "_" placeholder for the missing half — fall back to
65
- * the real half rather than labeling every such row "_".
66
- */
67
34
  export declare const agentThreadShortLabel: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">) => string;
68
- /** How a row is labelled: its title, or the id-based short label while it has none. */
69
35
  export declare const agentThreadDisplayTitle: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId" | "title">) => string;
70
- /**
71
- * Headless agent-threads index: fetches an agent's conversation threads from the platform
72
- * endpoint with keyset-cursor pagination, then keeps them live — every threads-table write
73
- * publishes AGENT_THREAD_UPDATED on the app's Pusher channel and the hook upserts the row,
74
- * falling back to the getOne endpoint when a payload lacks the full row. New conversations
75
- * list optimistically the moment the first prompt is sent (wire notifyThreadActivity to the
76
- * chat's send seam). The LIST only — which conversation is open is the consumer's (a
77
- * conversation is addressed by its (chatId, chatThreadId) pair; a new one is a fresh UUID
78
- * thread id under the surface's chatId — CopilotKit replaces non-UUID ids per load).
79
- * Pair it with whatever list UI the host app uses. No per-host wiring: both hosts already
80
- * run the shared websocketsService singleton and subscribe the app channel (the platform
81
- * via its layouts' useWebsockets hooks, a boilerplate app via the client SDK's
82
- * ClientProvider), and listening before the socket connects just parks the handler.
83
- */
36
+ /** Agent-threads index with pagination, realtime upserts, and optimistic send listing. */
84
37
  export declare const useAgentThreads: (options: UseAgentThreadsOptions) => UseAgentThreadsResult;
85
38
  export default useAgentThreads;
86
39
  //# sourceMappingURL=use-agent-threads.d.ts.map
@@ -1 +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;;;;;;;OAOG;IACH,oBAAoB,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE;;;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;AAuC1D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB,KAAG,qBAqYjE,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,EAUvB,MAAM,4BAA4B,CAAC;AAEpC,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,6BAA6B,EAC7B,KAAK,oCAAoC,EACzC,KAAK,mCAAmC,GACzC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,6BAA6B,EAClC,yBAAyB,EACzB,KAAK,8BAA8B,EACnC,KAAK,kBAAkB,EACvB,2BAA2B,GAC5B,MAAM,8BAA8B,CAAC;AAItC,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,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,oBAAoB,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE,YAAY,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzG,SAAS,EAAE,OAAO,CAAC;IACnB,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,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAAgD,CAAC;AAEpG,eAAO,MAAM,qBAAqB,GAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,KAAG,MAIhG,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,GAAG,OAAO,CAAC,KAAG,MACpD,CAAC;AAuC1D,0FAA0F;AAC1F,eAAO,MAAM,eAAe,GAAI,SAAS,sBAAsB,KAAG,qBA6UjE,CAAC;AAEF,eAAe,eAAe,CAAC"}