@blocksdiy/react-common 1.47.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,94 +1,65 @@
1
- export type AgentThreadStatus = "idle" | "interrupted" | "error";
2
- export type AgentThreadSource = "direct_chat" | "agent_call" | "send_message" | "feedback" | "unknown";
1
+ declare const THREAD_STATUSES: readonly ["idle", "interrupted", "waiting_on_agent", "busy", "error"];
2
+ export type AgentThreadStatus = (typeof THREAD_STATUSES)[number];
3
+ declare const THREAD_SOURCES: readonly ["direct_chat", "send_message", "feedback", "self_config_resume", "unknown"];
4
+ export type AgentThreadSource = (typeof THREAD_SOURCES)[number] | "agent_call";
3
5
  export interface AgentThreadItem {
4
6
  agentId: string | null;
5
- /** Never null — pass back verbatim to open the conversation. */
6
7
  chatId: string;
7
- /** Never null — pass back verbatim to open the conversation. */
8
8
  chatThreadId: string;
9
9
  source: AgentThreadSource;
10
10
  status: AgentThreadStatus;
11
11
  title: string | null;
12
12
  turnCount: number;
13
- /**
14
- * What KIND of conversation this is — `"feedback"` for a thread opened by a feedback send,
15
- * absent/null for an ordinary chat. Optional in the type, not on the wire: a server that
16
- * predates it sends nothing, and `undefined` must read as "an ordinary chat", not as a
17
- * missing field to work around.
18
- */
19
13
  type?: string | null;
20
- /**
21
- * For a thread spawned from another conversation: that conversation's `chatThreadId` — pass
22
- * it back verbatim to open the source, exactly like `chatThreadId` itself. Null when the
23
- * source predates this field; a UI must then say where the thread came from without linking.
24
- */
25
14
  fromSourceChatThreadId?: string | null;
26
- /** The message the feedback was about, when it was about one rather than the conversation. */
27
15
  fromSourceMessageId?: string | null;
28
- /** First seen by the index — NOT thread creation time. */
29
16
  firstSeenAt: string;
30
17
  lastActivityAt: string;
18
+ runningSince?: string | null;
19
+ runningRunId?: string | null;
31
20
  }
32
- /** The (chatId, chatThreadId) pair — the only stable identity of a thread row. */
33
21
  export declare const agentThreadKey: (thread: Pick<AgentThreadItem, "chatId" | "chatThreadId">) => string;
34
- /**
35
- * Whether a realtime payload's `thread` is a complete list-item row. The server
36
- * serializes the exact item shape into every event, so this normally passes; when
37
- * it doesn't (contract drift), the consumer falls back to the getOne endpoint.
38
- */
39
22
  export declare const isAgentThreadItem: (value: unknown) => value is AgentThreadItem;
40
- /**
41
- * Provisional row title from the prompt that started a conversation (ChatGPT-style):
42
- * one line, whitespace collapsed, capped like the server's generated titles. Null when
43
- * there's nothing usable (attachment-only sends) — the UI keeps its id-based fallback.
44
- */
45
23
  export declare const provisionalThreadTitle: (prompt: string | undefined) => string | null;
46
- /**
47
- * The optimistic row for a user send on `thread`, replacing `previous` (its current
48
- * optimistic entry, if any): the same identity, its recency bumped to `now`, and a title
49
- * that never regresses — the provisional title already shown, else the row's own, else
50
- * one from this prompt (null when the prompt has nothing usable).
51
- */
52
24
  export declare const optimisticThreadAfterSend: (thread: AgentThreadItem, previous: AgentThreadItem | undefined, prompt: string | undefined, now: string) => AgentThreadItem;
25
+ export type RemovedAgentThread = Pick<AgentThreadItem, "chatId" | "chatThreadId" | "runningRunId">;
26
+ export declare const removeAgentThread: (threads: AgentThreadItem[], optimistic: ReadonlyMap<string, AgentThreadItem>, removed: RemovedAgentThread) => {
27
+ threads: AgentThreadItem[];
28
+ optimistic: ReadonlyMap<string, AgentThreadItem>;
29
+ } | null;
53
30
  export interface AgentThreadLiveUpdateOptions {
54
- /** Active server-side filters — an updated row that stops matching leaves the list. */
55
31
  status?: AgentThreadStatus;
56
32
  source?: AgentThreadSource;
57
33
  chatId?: string;
58
- /** Active sort, mirroring the endpoint's defaults (lastActivity, desc). */
59
34
  sort?: "lastActivity" | "firstSeen";
60
35
  order?: "asc" | "desc";
61
- /** True when more pages exist server-side beyond the loaded window. */
62
36
  hasMore?: boolean;
63
37
  }
64
- /**
65
- * Merge one realtime thread update into a loaded list. Pure; returns the input array
66
- * untouched when the event is stale, filtered out, or out of the loaded window.
67
- *
68
- * - Out-of-order delivery can't regress a row: events older than the loaded row (by
69
- * lastActivityAt) are dropped — checked FIRST, so a stale event can't evict a
70
- * still-current row through the filter check below either. Renames keep
71
- * lastActivityAt, so equal passes.
72
- * - A row that stops matching the active filters is removed; a matching one is upserted
73
- * and the list re-sorted by the active sort (stable, so ties keep their server order).
74
- * - A NEW row sorting past every loaded row while more pages exist may belong beyond the
75
- * loaded window; it's skipped so the list stays contiguous with the pagination cursor
76
- * (unreachable under the default lastActivity-desc sort, where fresh activity sorts first).
77
- */
78
- /** Whether a row passes the list's active server-side filters. */
79
38
  export declare const matchesAgentThreadFilters: (item: AgentThreadItem, options?: AgentThreadLiveUpdateOptions) => boolean;
80
39
  export declare const applyAgentThreadUpdate: (threads: AgentThreadItem[], incoming: AgentThreadItem, options?: AgentThreadLiveUpdateOptions) => AgentThreadItem[];
81
- /**
82
- * Overlay optimistic rows (conversations the user just acted on, ChatGPT-style) onto the
83
- * loaded list. Each row goes through the same merge as a realtime event, so filters and
84
- * sorting apply — except that a server row still untitled borrows the optimistic
85
- * (prompt-derived) title until the generated one lands. Pure; returns `threads` untouched
86
- * when nothing changes.
87
- *
88
- * An entry steps aside once the loaded row has recorded MORE turns than it did: turnCount
89
- * is server-monotonic, so the handover can't be held off by a client clock running ahead
90
- * of the server (the entry's lastActivityAt is stamped locally, and the merge's staleness
91
- * guard compares it against server timestamps).
92
- */
40
+ /** Parked removal for an in-flight refresh walk (see reconcileWalkWithLive). */
41
+ export interface ParkedRemoval {
42
+ removed: RemovedAgentThread;
43
+ }
44
+ export declare const reconcileWalkWithLive: (items: AgentThreadItem[], live: ReadonlyMap<string, AgentThreadItem | ParkedRemoval>, options?: AgentThreadLiveUpdateOptions) => AgentThreadItem[];
93
45
  export declare const overlayOptimisticThreads: (threads: AgentThreadItem[], optimistic: ReadonlyMap<string, AgentThreadItem>, options?: AgentThreadLiveUpdateOptions) => AgentThreadItem[];
46
+ export declare const retireOptimisticThreadsTakenOver: (optimistic: ReadonlyMap<string, AgentThreadItem>, threads: AgentThreadItem[]) => ReadonlyMap<string, AgentThreadItem>;
47
+ /** The server's cap on a title a person sets (RENAME_TITLE_MAX_CHARS in wf-actions-py). */
48
+ export declare const RENAME_TITLE_MAX_CHARS = 255;
49
+ /** Conversation-index groups, in display order; a row's group follows its write-once `source`. */
50
+ export declare const AGENT_THREAD_GROUPS: readonly ["chats", "workflows", "feedback"];
51
+ export type AgentThreadGroup = (typeof AGENT_THREAD_GROUPS)[number];
52
+ /** Where a row files; a source this copy predates files under chats, so a row never renders nowhere. */
53
+ export declare const agentThreadGroup: (source: AgentThreadSource) => AgentThreadGroup;
54
+ export interface AgentThreadGroupRows {
55
+ group: AgentThreadGroup;
56
+ rows: AgentThreadItem[];
57
+ }
58
+ /** Rows by group in group order, each group in input order; empty groups dropped. */
59
+ export declare const groupAgentThreads: (threads: readonly AgentThreadItem[]) => AgentThreadGroupRows[];
60
+ /** A thread the agent opened to act on feedback — by `type`, or by `source` for rows from before the column. */
61
+ export declare const isFeedbackThread: (row: Pick<AgentThreadItem, "type" | "source">) => boolean;
62
+ /** The conversation a feedback thread was opened from, when `threads` lists it (same chat, the row's pointer). */
63
+ export declare const feedbackSourceThread: (row: AgentThreadItem, threads: readonly AgentThreadItem[]) => AgentThreadItem | undefined;
64
+ export {};
94
65
  //# sourceMappingURL=agent-thread-live-update.d.ts.map
@@ -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":"AAQA,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,4BAA4B,CAAC;AAClF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,uGAAuG;AACvG,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,CAAC;AAIpF;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC,YAAY,EAAE,oBAAoB,CAAC;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,MAAM,EACd,gBAAe,MAA4B,KAC1C,0BAID,CAAC;AAEH,qGAAqG;AACrG,eAAO,MAAM,gBAAgB,GAC3B,WAAW,MAAM,EACjB,iBAAiB,oBAAoB,GAAG,SAAS,KAChD,0BAGF,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,4CAA4C,0BAA0B,KAAG,OAC/C,CAAC;AAE5D,uFAAuF;AACvF,eAAO,MAAM,mBAAmB,GAC9B,MAAM,0BAA0B,EAChC,cAAc,oBAAoB,KACjC,0BACoG,CAAC;AAExG,mFAAmF;AACnF,eAAO,MAAM,oBAAoB,GAC/B,MAAM,0BAA0B,EAChC,WAAW,MAAM,KAChB,0BAA4F,CAAC;AAEhG,gGAAgG;AAChG,eAAO,MAAM,uBAAuB,GAAI,MAAM,0BAA0B,KAAG,0BACb,CAAC;AAE/D,mGAAmG;AACnG,MAAM,MAAM,0BAA0B,GAAG,oBAAoB,GAAG,KAAK,GAAG,IAAI,CAAC;AAE7E,qFAAqF;AACrF,eAAO,MAAM,eAAe,GAAI,QAAQ,0BAA0B,KAAG,MAAM,GAAG,IACF,CAAC;AAE7E,MAAM,WAAW,oCAAoC;IACnD,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,0BAA0B,CAAC;CACvC;AAED,MAAM,WAAW,mCAAmC;IAClD,YAAY,EAAE,oBAAoB,CAAC;IACnC,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,+FAA+F;IAC/F,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GAAI,2BAG3C,oCAAoC,KAAG,mCAmBzC,CAAC;AAMF,eAAO,MAAM,YAAY,QAAiB,CAAC;AAM3C,mHAAmH;AACnH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,EACf,cAAc,oBAAoB,EAClC,SAAS,OAAO,KACf,eAaF,CAAC;AAEF,qGAAqG;AACrG,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,eAAe,EAAE,KAAG,MAAM,GAAG,IAS1E,CAAC;AAEF,2FAA2F;AAC3F,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,WAAW,cAAc,GAAG,IAAI,EAChC,KAAK,MAAM,EACX,WAAW,MAAM,EACjB,YAAY,OAAO,KAClB;IAAE,SAAS,EAAE,cAAc,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAQrD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,GACrC,WAAW,cAAc,GAAG,IAAI,EAChC,KAAK,MAAM,EACX,SAAS,OAAO,KACf,cAAc,GAAG,IAKnB,CAAC;AAEF,gGAAgG;AAChG,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,2BAA2B;IAC1C,+CAA+C;IAC/C,OAAO,EAAE,qBAAqB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,oBAAoB,CAAC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,qBAAqB,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,6EAA6E;IAC7E,GAAG,EAAE,eAAe,CAAC;IACrB,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAC;IACpD,uFAAuF;IACvF,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACxE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,oDAMlC,2BAA2B,KAAG,0BAyGhC,CAAC"}