@blocksdiy/react-common 1.52.1 → 1.53.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.
@@ -27,6 +27,19 @@ export interface AgentConversationProviderProps extends Pick<UseAgentThreadsOpti
27
27
  chat: AgentConversationChat;
28
28
  children?: ReactNode;
29
29
  }
30
+ /**
31
+ * The Root over the agent's whole index: the sidebar's list under its filters, and the open
32
+ * conversation's bridge to the chat. The agent-layout registry item renders it.
33
+ */
30
34
  export declare function AgentConversationProvider({ agentId, conversation, isDraft, chat, token, headers, baseUrl, children, }: AgentConversationProviderProps): import("react").JSX.Element;
35
+ export type AgentThreadConversationProviderProps = AgentConversationProviderProps;
36
+ /**
37
+ * The Root over ONE conversation, with no index: the same bridge to the chat, fed by the
38
+ * single-conversation reader (useAgentThread) — which costs nothing beside a mounted index for
39
+ * the agent, one read otherwise, and nothing for a draft. For a surface that shows a conversation
40
+ * without the agent's list (the floating agent chat). The context's `threads` lists that one
41
+ * row; its filters are inert.
42
+ */
43
+ export declare function AgentThreadConversationProvider({ agentId, conversation, isDraft, chat, token, headers, baseUrl, children, }: AgentThreadConversationProviderProps): import("react").JSX.Element;
31
44
  export declare const useAgentConversationContext: () => AgentConversationContextValue;
32
45
  //# sourceMappingURL=agent-conversation-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-conversation-context.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-conversation-context.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAE5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAEzB,KAAK,0BAA0B,EAChC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAmB,KAAK,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE/G,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;AAEnF,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,oBAAoB,CAAC;IACnC,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,iHAAiH;IACjH,OAAO,EAAE,qBAAqB,CAAC;IAC/B,gGAAgG;IAChG,GAAG,EAAE,eAAe,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,UAAU,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAClD,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uFAAuF;IACvF,iBAAiB,EAAE,0BAA0B,CAAC,mBAAmB,CAAC,CAAC;CACpE;AAID,MAAM,WAAW,8BAA+B,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACnH,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,oBAAoB,CAAC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,yBAAyB,CAAC,EACxC,OAAO,EACP,YAAY,EACZ,OAAO,EACP,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EACP,QAAQ,GACT,EAAE,8BAA8B,+BA4BhC;AAED,eAAO,MAAM,2BAA2B,QAAO,6BAM9C,CAAC"}
1
+ {"version":3,"file":"agent-conversation-context.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-conversation-context.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAE5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAEzB,KAAK,0BAA0B,EAChC,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAmB,KAAK,sBAAsB,EAAE,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE/G,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;AAEnF,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,oBAAoB,CAAC;IACnC,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,iHAAiH;IACjH,OAAO,EAAE,qBAAqB,CAAC;IAC/B,gGAAgG;IAChG,GAAG,EAAE,eAAe,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,UAAU,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAClD,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uFAAuF;IACvF,iBAAiB,EAAE,0BAA0B,CAAC,mBAAmB,CAAC,CAAC;CACpE;AAID,MAAM,WAAW,8BAA+B,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACnH,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,oBAAoB,CAAC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,OAAO,EACP,YAAY,EACZ,OAAO,EACP,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EACP,QAAQ,GACT,EAAE,8BAA8B,+BAgBhC;AAED,MAAM,MAAM,oCAAoC,GAAG,8BAA8B,CAAC;AAKlF;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,OAAO,EACP,YAAY,EACZ,OAAO,EACP,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EACP,QAAQ,GACT,EAAE,oCAAoC,+BAetC;AA2CD,eAAO,MAAM,2BAA2B,QAAO,6BAQ9C,CAAC"}
@@ -5,11 +5,32 @@ import { jsx as _jsx } from "react/jsx-runtime";
5
5
  // agent-layout registry item renders it; a custom UI over the same behaviour can too.
6
6
  import { createContext, useContext, useState } from "react";
7
7
  import { useAgentConversation, } from "./use-agent-conversation.js";
8
+ import { useAgentThread } from "./use-agent-thread.js";
8
9
  import { useAgentThreads } from "./use-agent-threads.js";
9
10
  const AgentConversationContext = createContext(null);
11
+ /**
12
+ * The Root over the agent's whole index: the sidebar's list under its filters, and the open
13
+ * conversation's bridge to the chat. The agent-layout registry item renders it.
14
+ */
10
15
  export function AgentConversationProvider({ agentId, conversation, isDraft, chat, token, headers, baseUrl, children, }) {
11
16
  const [filters, setFilters] = useState({});
12
17
  const threads = useAgentThreads({ agentId, status: filters.status, source: filters.source, token, headers, baseUrl });
18
+ return (_jsx(AgentConversationRoot, { agentId: agentId, conversation: conversation, isDraft: isDraft, chat: chat, threads: threads, filters: filters, setFilters: setFilters, children: children }));
19
+ }
20
+ const NO_FILTERS = {};
21
+ const noFilters = () => undefined;
22
+ /**
23
+ * The Root over ONE conversation, with no index: the same bridge to the chat, fed by the
24
+ * single-conversation reader (useAgentThread) — which costs nothing beside a mounted index for
25
+ * the agent, one read otherwise, and nothing for a draft. For a surface that shows a conversation
26
+ * without the agent's list (the floating agent chat). The context's `threads` lists that one
27
+ * row; its filters are inert.
28
+ */
29
+ export function AgentThreadConversationProvider({ agentId, conversation, isDraft, chat, token, headers, baseUrl, children, }) {
30
+ const threads = useAgentThread({ agentId, conversation, isDraft, token, headers, baseUrl });
31
+ return (_jsx(AgentConversationRoot, { agentId: agentId, conversation: conversation, isDraft: isDraft, chat: chat, threads: threads, filters: NO_FILTERS, setFilters: noFilters, children: children }));
32
+ }
33
+ function AgentConversationRoot({ agentId, conversation, isDraft, chat, threads, filters, setFilters, children, }) {
13
34
  const { row, isStandIn, renameThread, notifyMessageSent } = useAgentConversation({
14
35
  threads,
15
36
  agentId,
@@ -33,7 +54,7 @@ export function AgentConversationProvider({ agentId, conversation, isDraft, chat
33
54
  export const useAgentConversationContext = () => {
34
55
  const context = useContext(AgentConversationContext);
35
56
  if (!context) {
36
- throw new Error("useAgentConversationContext must be used within <AgentConversationProvider>.");
57
+ throw new Error("useAgentConversationContext must be used within <AgentConversationProvider> or <AgentThreadConversationProvider>.");
37
58
  }
38
59
  return context;
39
60
  };
@@ -0,0 +1,62 @@
1
+ import { type AgentThreadItem, type RemovedAgentThread } from "./agent-thread-live-update.js";
2
+ export declare const AGENT_THREAD_UPDATED = "AGENT_THREAD_UPDATED";
3
+ export type AgentThreadStoreEvent = {
4
+ kind: "upsert";
5
+ thread: AgentThreadItem;
6
+ } | {
7
+ kind: "removed";
8
+ removed: RemovedAgentThread;
9
+ }
10
+ /**
11
+ * The cache may have changed (an index walk or page landed rows), or the index stopped
12
+ * walking (done, failed, or unmounted): readers waiting on the index re-check it.
13
+ */
14
+ | {
15
+ kind: "primed";
16
+ };
17
+ export type AgentThreadStoreListener = (event: AgentThreadStoreEvent) => void;
18
+ /** Fetch one row by identity — the single-thread read; supplied by the acquiring reader (it holds the auth). */
19
+ export type AgentThreadFetcher = (conversation: Pick<AgentThreadItem, "chatId" | "chatThreadId">) => Promise<AgentThreadItem>;
20
+ export interface AgentThreadStore {
21
+ readonly agentId: string;
22
+ /** The latest row known for a conversation (`agentThreadKey`), from any reader or the wire. */
23
+ getRow(key: string): AgentThreadItem | undefined;
24
+ /** An index reader is mounted and walking: its pages will prime the cache, so wait for them. */
25
+ readonly isIndexLoading: boolean;
26
+ subscribe(listener: AgentThreadStoreListener): () => void;
27
+ /** Remember rows a reader fetched or walked, for everyone else; announces `primed`. */
28
+ prime(rows: readonly AgentThreadItem[]): void;
29
+ }
30
+ /** The websocket seam, injectable for tests. */
31
+ export interface AgentThreadStoreTransport {
32
+ listen(event: string, handler: (data: unknown) => Promise<void>, options: {
33
+ filter: {
34
+ agentId: string;
35
+ };
36
+ }): void;
37
+ unlisten(event: string, handler: (data: unknown) => Promise<void>): void;
38
+ }
39
+ /**
40
+ * The cache's one rule: a row older than the cached one (by lastActivityAt) is stale and
41
+ * dropped; equal passes, since renames keep lastActivityAt. The same rule the index applies.
42
+ */
43
+ export declare const isNewerOrSameRow: (incoming: AgentThreadItem, cached: AgentThreadItem | undefined) => boolean;
44
+ /** The wire payload's identity, or null for a payload that names no conversation. */
45
+ export declare const agentThreadEventIdentity: (data: unknown) => RemovedAgentThread | null;
46
+ declare const createRegistry: (transport: AgentThreadStoreTransport) => {
47
+ acquire: (agentId: string, options: {
48
+ fetchThread: AgentThreadFetcher;
49
+ isIndex?: boolean;
50
+ }) => {
51
+ store: AgentThreadStore;
52
+ setIndexLoading: (loading: boolean) => void;
53
+ release: () => void;
54
+ };
55
+ size: () => number;
56
+ };
57
+ export type AgentThreadStoreRegistry = ReturnType<typeof createRegistry>;
58
+ /** A registry over an injected transport — for tests. Production uses {@link acquireAgentThreadStore}. */
59
+ export declare const createAgentThreadStoreRegistry: (transport: AgentThreadStoreTransport) => AgentThreadStoreRegistry;
60
+ export declare const acquireAgentThreadStore: AgentThreadStoreRegistry["acquire"];
61
+ export {};
62
+ //# sourceMappingURL=agent-thread-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-thread-store.d.ts","sourceRoot":"","sources":["../../src/hooks/agent-thread-store.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,eAAe,EAGpB,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC;AAEpC,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAA;CAAE;AAClD;;;GAGG;GACD;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEvB,MAAM,MAAM,wBAAwB,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAE9E,gHAAgH;AAChH,MAAM,MAAM,kBAAkB,GAAG,CAC/B,YAAY,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,cAAc,CAAC,KAC3D,OAAO,CAAC,eAAe,CAAC,CAAC;AAE9B,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+FAA+F;IAC/F,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IACjD,gGAAgG;IAChG,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,SAAS,CAAC,QAAQ,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAAC;IAC1D,uFAAuF;IACvF,KAAK,CAAC,IAAI,EAAE,SAAS,eAAe,EAAE,GAAG,IAAI,CAAC;CAC/C;AAED,gDAAgD;AAChD,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAC;IACjH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC1E;AAOD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAU,eAAe,EAAE,QAAQ,eAAe,GAAG,SAAS,KAAG,OACjB,CAAC;AAElF,qFAAqF;AACrF,eAAO,MAAM,wBAAwB,GAAI,MAAM,OAAO,KAAG,kBAAkB,GAAG,IAO7E,CAAC;AA+GF,QAAA,MAAM,cAAc,GAAI,WAAW,yBAAyB;uBAQ/C,MAAM,WACN;QAAE,WAAW,EAAE,kBAAkB,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAC9D;QAAE,KAAK,EAAE,gBAAgB,CAAC;QAAC,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE;;CAoCjG,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAEzE,0GAA0G;AAC1G,eAAO,MAAM,8BAA8B,GAAI,WAAW,yBAAyB,KAAG,wBAC3D,CAAC;AAO5B,eAAO,MAAM,uBAAuB,EAAE,wBAAwB,CAAC,SAAS,CAC7B,CAAC"}
@@ -0,0 +1,170 @@
1
+ // One live subscription and one row cache per agent, shared by every reader in the app: the
2
+ // conversation index (useAgentThreads) and the single-conversation reader (useAgentThread).
3
+ // Readers acquire the store for an agent and release it when they unmount; the first acquire
4
+ // opens the websocket listener, the last release closes it and drops the rows. The store
5
+ // resolves a thin event payload to a full row once, for everyone, and remembers every row it
6
+ // sees — a list walk primes it, so a single-conversation reader mounted beside the index never
7
+ // fetches what the index already has.
8
+ import { websocketsService } from "@blocksdiy/blocks-client-api/websocketService";
9
+ import { agentThreadKey, isAgentThreadItem, } from "./agent-thread-live-update.js";
10
+ export const AGENT_THREAD_UPDATED = "AGENT_THREAD_UPDATED";
11
+ const sortTime = (iso) => {
12
+ const parsed = new Date(iso).getTime();
13
+ return Number.isNaN(parsed) ? 0 : parsed;
14
+ };
15
+ /**
16
+ * The cache's one rule: a row older than the cached one (by lastActivityAt) is stale and
17
+ * dropped; equal passes, since renames keep lastActivityAt. The same rule the index applies.
18
+ */
19
+ export const isNewerOrSameRow = (incoming, cached) => !cached || sortTime(incoming.lastActivityAt) >= sortTime(cached.lastActivityAt);
20
+ /** The wire payload's identity, or null for a payload that names no conversation. */
21
+ export const agentThreadEventIdentity = (data) => {
22
+ const event = data;
23
+ const raw = event?.thread;
24
+ if (typeof raw?.chatId !== "string" || typeof raw.chatThreadId !== "string") {
25
+ return null;
26
+ }
27
+ return { chatId: raw.chatId, chatThreadId: raw.chatThreadId, runningRunId: raw.runningRunId };
28
+ };
29
+ class AgentThreadStoreImpl {
30
+ agentId;
31
+ transport;
32
+ rows = new Map();
33
+ listeners = new Set();
34
+ /** The most recent reader's fetcher — all readers in one app share auth, so any of them serves. */
35
+ fetchThread = null;
36
+ indexLoadingCount = 0;
37
+ constructor(agentId, transport) {
38
+ this.agentId = agentId;
39
+ this.transport = transport;
40
+ }
41
+ get isIndexLoading() {
42
+ return this.indexLoadingCount > 0;
43
+ }
44
+ /**
45
+ * An index reader starts or stops walking. Back at idle, readers waiting on the index are
46
+ * told to re-check: a failed walk, or an index unmounting mid-walk, primes nothing, and they
47
+ * must not wait forever.
48
+ */
49
+ setIndexLoading(loading) {
50
+ this.indexLoadingCount += loading ? 1 : -1;
51
+ if (this.indexLoadingCount === 0) {
52
+ this.emit({ kind: "primed" });
53
+ }
54
+ }
55
+ getRow(key) {
56
+ return this.rows.get(key);
57
+ }
58
+ subscribe(listener) {
59
+ this.listeners.add(listener);
60
+ return () => {
61
+ this.listeners.delete(listener);
62
+ };
63
+ }
64
+ prime(rows) {
65
+ for (const row of rows) {
66
+ this.remember(row);
67
+ }
68
+ this.emit({ kind: "primed" });
69
+ }
70
+ open() {
71
+ this.transport.listen(AGENT_THREAD_UPDATED, this.onEvent, { filter: { agentId: this.agentId } });
72
+ }
73
+ close() {
74
+ this.transport.unlisten(AGENT_THREAD_UPDATED, this.onEvent);
75
+ this.rows.clear();
76
+ this.listeners.clear();
77
+ }
78
+ remember(row) {
79
+ const key = agentThreadKey(row);
80
+ if (!isNewerOrSameRow(row, this.rows.get(key))) {
81
+ return false;
82
+ }
83
+ this.rows.set(key, row);
84
+ return true;
85
+ }
86
+ emit(event) {
87
+ for (const listener of [...this.listeners]) {
88
+ listener(event);
89
+ }
90
+ }
91
+ onEvent = (data) => this.handleEvent(data);
92
+ async handleEvent(data) {
93
+ try {
94
+ const identity = agentThreadEventIdentity(data);
95
+ if (!identity) {
96
+ return;
97
+ }
98
+ const event = data;
99
+ if (event.removed) {
100
+ // A ghost thread (a run that ended before any turn) leaves the cache; a row with turns
101
+ // stays — the index decides for itself, with its own rule, whether to drop the listing.
102
+ const cached = this.rows.get(agentThreadKey(identity));
103
+ if (cached && cached.turnCount === 0) {
104
+ this.rows.delete(agentThreadKey(identity));
105
+ }
106
+ this.emit({ kind: "removed", removed: identity });
107
+ return;
108
+ }
109
+ // A thin payload (an older server) resolves to the full row ONCE here, not per reader.
110
+ const incoming = isAgentThreadItem(event.thread) ? event.thread : await this.fetchThread?.(identity);
111
+ if (!incoming) {
112
+ return;
113
+ }
114
+ this.remember(incoming);
115
+ this.emit({ kind: "upsert", thread: incoming });
116
+ }
117
+ catch {
118
+ // Best-effort: a websocket handler must not reject.
119
+ }
120
+ }
121
+ }
122
+ const createRegistry = (transport) => {
123
+ const entries = new Map();
124
+ /**
125
+ * A reader's handle on the agent's store. `isIndex` marks the conversation index: while any
126
+ * index reader is walking, single-conversation readers wait for its pages instead of fetching.
127
+ */
128
+ const acquire = (agentId, options) => {
129
+ let entry = entries.get(agentId);
130
+ if (!entry) {
131
+ const store = new AgentThreadStoreImpl(agentId, transport);
132
+ store.open();
133
+ entry = { store, refCount: 0, indexReaders: 0 };
134
+ entries.set(agentId, entry);
135
+ }
136
+ const current = entry;
137
+ current.refCount += 1;
138
+ current.store.fetchThread = options.fetchThread;
139
+ let loading = false;
140
+ const setIndexLoading = (next) => {
141
+ if (next === loading) {
142
+ return;
143
+ }
144
+ loading = next;
145
+ current.store.setIndexLoading(next);
146
+ };
147
+ let released = false;
148
+ const release = () => {
149
+ if (released) {
150
+ return;
151
+ }
152
+ released = true;
153
+ setIndexLoading(false);
154
+ current.refCount -= 1;
155
+ if (current.refCount === 0 && entries.get(agentId) === current) {
156
+ current.store.close();
157
+ entries.delete(agentId);
158
+ }
159
+ };
160
+ return { store: current.store, setIndexLoading, release };
161
+ };
162
+ return { acquire, size: () => entries.size };
163
+ };
164
+ /** A registry over an injected transport — for tests. Production uses {@link acquireAgentThreadStore}. */
165
+ export const createAgentThreadStoreRegistry = (transport) => createRegistry(transport);
166
+ const defaultRegistry = createRegistry({
167
+ listen: (event, handler, options) => websocketsService.listen(event, handler, options),
168
+ unlisten: (event, handler) => websocketsService.unlisten(event, handler),
169
+ });
170
+ export const acquireAgentThreadStore = (agentId, options) => defaultRegistry.acquire(agentId, options);
@@ -0,0 +1,77 @@
1
+ import type { AgentConversationRef } from "./use-agent-conversation.js";
2
+ /** The agent-chat handle an opener holds (`useAgentChat(<Agent>ChatAgentChat)` in an app). */
3
+ export interface FloatingAgentChatHandle {
4
+ getAgentChatComponentProps(): {
5
+ agentChatId: string;
6
+ token?: string;
7
+ isAdmin?: boolean;
8
+ };
9
+ }
10
+ /** What to open: a conversation (the `chatId` + `chatThreadId` pair), or the new chat. */
11
+ export type FloatingAgentChatTarget = AgentConversationRef | "new";
12
+ /**
13
+ * Where the panel's expand goes for the conversation it shows — the opener knows the full route
14
+ * (an agent mini-app's thread page); `null` hides the expand control. Never asked for a draft:
15
+ * an unsent new chat has no page, so the panel shows no expand for it.
16
+ */
17
+ export type FloatingAgentChatExpandHref = (conversation: AgentConversationRef) => string | null;
18
+ export interface FloatingAgentChatOpenOptions<Handle extends FloatingAgentChatHandle = FloatingAgentChatHandle> {
19
+ /** The agent to talk to — the surface's own agent-chat block. */
20
+ agentChat: Handle;
21
+ /** A conversation to pick up, or the new chat (the default). */
22
+ target?: FloatingAgentChatTarget;
23
+ expandHref?: FloatingAgentChatExpandHref;
24
+ /** Extra context handed to the agent on every turn (like `AgentChat`'s `chatContext`). */
25
+ chatContext?: unknown;
26
+ }
27
+ /** The open options with the ids resolved from the handle. */
28
+ export interface FloatingAgentChatOpenRequest<Handle extends FloatingAgentChatHandle = FloatingAgentChatHandle> extends Omit<FloatingAgentChatOpenOptions<Handle>, "target"> {
29
+ agentChatId: string;
30
+ /** The chat the agent's own conversations live under (agentInboxChatId). */
31
+ ownChatId: string;
32
+ target: FloatingAgentChatTarget;
33
+ }
34
+ export interface FloatingAgentChatState<Handle extends FloatingAgentChatHandle = FloatingAgentChatHandle> {
35
+ agentChat: Handle;
36
+ agentChatId: string;
37
+ ownChatId: string;
38
+ conversation: AgentConversationRef;
39
+ /** The draft's thread id — the new chat, until its first send turns it into a thread. */
40
+ draftThreadId: string;
41
+ draftSent: boolean;
42
+ expandHref?: FloatingAgentChatExpandHref;
43
+ chatContext?: unknown;
44
+ }
45
+ /** The panel shows the new chat, nothing sent on it yet. */
46
+ export declare const isFloatingAgentChatDraft: (state: FloatingAgentChatState) => boolean;
47
+ /**
48
+ * The state after an open. Within one agent the draft's bookkeeping carries over — the new chat
49
+ * is kept while untouched, stands by while a thread is open, and is minted afresh once sent;
50
+ * another agent starts clean. Opening what is already shown changes nothing but the opener's
51
+ * `expandHref` / `chatContext`.
52
+ */
53
+ export declare const floatingAgentChatAfterOpen: <Handle extends FloatingAgentChatHandle>(prev: FloatingAgentChatState<Handle> | null, request: FloatingAgentChatOpenRequest<Handle>, mintThreadId: () => string) => FloatingAgentChatState<Handle>;
54
+ /** After the first send on the draft; unchanged when the draft is not what is shown. */
55
+ export declare const floatingAgentChatAfterDraftSent: <Handle extends FloatingAgentChatHandle>(prev: FloatingAgentChatState<Handle> | null) => FloatingAgentChatState<Handle> | null;
56
+ export interface UseFloatingAgentChatControllerResult<Handle extends FloatingAgentChatHandle = FloatingAgentChatHandle> {
57
+ /** What the panel shows; null while it is closed. */
58
+ state: FloatingAgentChatState<Handle> | null;
59
+ isOpen: boolean;
60
+ /** Open (or re-point) the panel. The same agent's draft carries over; another agent starts clean. */
61
+ open: (options: FloatingAgentChatOpenOptions<Handle>) => void;
62
+ close: () => void;
63
+ /**
64
+ * Close the panel when it shows one of this agent's conversations — for a surface that puts
65
+ * that agent's full transcript on screen (an agent mini-app's conversation routes).
66
+ */
67
+ closeForAgent: (agentChatId: string) => void;
68
+ /** The panel's seam: the draft's first send made it a thread. */
69
+ markDraftSent: () => void;
70
+ }
71
+ /**
72
+ * The floating agent chat's state and transitions, for ONE provider at the app's root. Remembers
73
+ * what had focus on the open that showed the panel (a sidebar row, an "Ask" button) and hands it
74
+ * back on close, so keyboard users land where they were.
75
+ */
76
+ export declare const useFloatingAgentChatController: <Handle extends FloatingAgentChatHandle = FloatingAgentChatHandle>() => UseFloatingAgentChatControllerResult<Handle>;
77
+ //# sourceMappingURL=floating-agent-chat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floating-agent-chat.d.ts","sourceRoot":"","sources":["../../src/hooks/floating-agent-chat.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGrE,8FAA8F;AAC9F,MAAM,WAAW,uBAAuB;IACtC,0BAA0B,IAAI;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1F;AAED,0FAA0F;AAC1F,MAAM,MAAM,uBAAuB,GAAG,oBAAoB,GAAG,KAAK,CAAC;AAEnE;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,YAAY,EAAE,oBAAoB,KAAK,MAAM,GAAG,IAAI,CAAC;AAEhG,MAAM,WAAW,4BAA4B,CAAC,MAAM,SAAS,uBAAuB,GAAG,uBAAuB;IAC5G,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,0FAA0F;IAC1F,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,8DAA8D;AAC9D,MAAM,WAAW,4BAA4B,CAC3C,MAAM,SAAS,uBAAuB,GAAG,uBAAuB,CAChE,SAAQ,IAAI,CAAC,4BAA4B,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,uBAAuB,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB,CAAC,MAAM,SAAS,uBAAuB,GAAG,uBAAuB;IACtG,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,oBAAoB,CAAC;IACnC,yFAAyF;IACzF,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,4DAA4D;AAC5D,eAAO,MAAM,wBAAwB,GAAI,OAAO,sBAAsB,KAAG,OACI,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,GAAI,MAAM,SAAS,uBAAuB,EAC/E,MAAM,sBAAsB,CAAC,MAAM,CAAC,GAAG,IAAI,EAC3C,SAAS,4BAA4B,CAAC,MAAM,CAAC,EAC7C,cAAc,MAAM,MAAM,KACzB,sBAAsB,CAAC,MAAM,CAwB/B,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,+BAA+B,GAAI,MAAM,SAAS,uBAAuB,EACpF,MAAM,sBAAsB,CAAC,MAAM,CAAC,GAAG,IAAI,KAC1C,sBAAsB,CAAC,MAAM,CAAC,GAAG,IAC0C,CAAC;AAE/E,MAAM,WAAW,oCAAoC,CACnD,MAAM,SAAS,uBAAuB,GAAG,uBAAuB;IAEhE,qDAAqD;IACrD,KAAK,EAAE,sBAAsB,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,OAAO,CAAC;IAChB,qGAAqG;IACrG,IAAI,EAAE,CAAC,OAAO,EAAE,4BAA4B,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;IAC9D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;OAGG;IACH,aAAa,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,iEAAiE;IACjE,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,GACzC,MAAM,SAAS,uBAAuB,GAAG,uBAAuB,OAC7D,oCAAoC,CAAC,MAAM,CAuD/C,CAAC"}
@@ -0,0 +1,89 @@
1
+ // The floating agent chat's state, headless: what the panel shows (which agent, which
2
+ // conversation), the draft the new chat is until its first send, and the opener that gets focus
3
+ // back on close. The agent-chat registry item renders it. Mirrors the conversation selection
4
+ // rules of useAgentConversationSelection: a new chat is a draft minted here and listed nowhere
5
+ // until the first send; "New chat" on an untouched draft keeps it; a thread opened beside it
6
+ // leaves it standing by. The reducers are pure and tested; the hook is the thin React shell.
7
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
+ import { agentInboxChatId } from "./use-agent-threads.js";
9
+ /** The panel shows the new chat, nothing sent on it yet. */
10
+ export const isFloatingAgentChatDraft = (state) => state.conversation.chatThreadId === state.draftThreadId && !state.draftSent;
11
+ /**
12
+ * The state after an open. Within one agent the draft's bookkeeping carries over — the new chat
13
+ * is kept while untouched, stands by while a thread is open, and is minted afresh once sent;
14
+ * another agent starts clean. Opening what is already shown changes nothing but the opener's
15
+ * `expandHref` / `chatContext`.
16
+ */
17
+ export const floatingAgentChatAfterOpen = (prev, request, mintThreadId) => {
18
+ const sameAgent = prev !== null && prev.agentChatId === request.agentChatId;
19
+ const draft = sameAgent && prev
20
+ ? { draftThreadId: prev.draftThreadId, draftSent: prev.draftSent }
21
+ : { draftThreadId: mintThreadId(), draftSent: false };
22
+ const base = {
23
+ agentChat: request.agentChat,
24
+ agentChatId: request.agentChatId,
25
+ ownChatId: request.ownChatId,
26
+ expandHref: request.expandHref,
27
+ chatContext: request.chatContext,
28
+ };
29
+ if (request.target !== "new") {
30
+ return { ...base, ...draft, conversation: request.target };
31
+ }
32
+ // A sent draft is a thread now; the new chat needs a fresh one.
33
+ const draftThreadId = draft.draftSent ? mintThreadId() : draft.draftThreadId;
34
+ return {
35
+ ...base,
36
+ draftThreadId,
37
+ draftSent: false,
38
+ conversation: { chatId: request.ownChatId, chatThreadId: draftThreadId },
39
+ };
40
+ };
41
+ /** After the first send on the draft; unchanged when the draft is not what is shown. */
42
+ export const floatingAgentChatAfterDraftSent = (prev) => prev && isFloatingAgentChatDraft(prev) ? { ...prev, draftSent: true } : prev;
43
+ /**
44
+ * The floating agent chat's state and transitions, for ONE provider at the app's root. Remembers
45
+ * what had focus on the open that showed the panel (a sidebar row, an "Ask" button) and hands it
46
+ * back on close, so keyboard users land where they were.
47
+ */
48
+ export const useFloatingAgentChatController = () => {
49
+ const [state, setState] = useState(null);
50
+ const openerRef = useRef(null);
51
+ // Focus goes back to the opener once the panel is gone — after the close commits, so the
52
+ // panel's own focused element has left the DOM by then.
53
+ useEffect(() => {
54
+ if (state !== null) {
55
+ return;
56
+ }
57
+ const opener = openerRef.current;
58
+ openerRef.current = null;
59
+ if (opener?.isConnected) {
60
+ opener.focus({ preventScroll: true });
61
+ }
62
+ }, [state]);
63
+ const open = useCallback((options) => {
64
+ const active = typeof document !== "undefined" &&
65
+ document.activeElement instanceof HTMLElement &&
66
+ document.activeElement !== document.body
67
+ ? document.activeElement
68
+ : null;
69
+ const { agentChatId } = options.agentChat.getAgentChatComponentProps();
70
+ setState((prev) => {
71
+ // Remembered on the open that shows the panel, not on a re-point while it is up.
72
+ if (prev === null && active) {
73
+ openerRef.current = active;
74
+ }
75
+ return floatingAgentChatAfterOpen(prev, {
76
+ agentChat: options.agentChat,
77
+ agentChatId,
78
+ ownChatId: agentInboxChatId(agentChatId),
79
+ target: options.target ?? "new",
80
+ expandHref: options.expandHref,
81
+ chatContext: options.chatContext,
82
+ }, () => crypto.randomUUID());
83
+ });
84
+ }, []);
85
+ const close = useCallback(() => setState(null), []);
86
+ const closeForAgent = useCallback((agentChatId) => setState((prev) => (prev?.agentChatId === agentChatId ? null : prev)), []);
87
+ const markDraftSent = useCallback(() => setState(floatingAgentChatAfterDraftSent), []);
88
+ return useMemo(() => ({ state, isOpen: state !== null, open, close, closeForAgent, markDraftSent }), [state, open, close, closeForAgent, markDraftSent]);
89
+ };
@@ -0,0 +1,67 @@
1
+ import { type AgentThreadItem } from "./agent-thread-live-update.js";
2
+ import { type AgentThreadStore, type AgentThreadStoreEvent } from "./agent-thread-store.js";
3
+ import type { AgentConversationRef } from "./use-agent-conversation.js";
4
+ import { type UseAgentThreadsOptions, type UseAgentThreadsResult } from "./use-agent-threads.js";
5
+ export interface UseAgentThreadOptions extends Pick<UseAgentThreadsOptions, "token" | "headers" | "baseUrl"> {
6
+ agentId: string;
7
+ conversation: AgentConversationRef;
8
+ /**
9
+ * The conversation is the new chat, nothing sent on it yet: the server has no row for it, so
10
+ * nothing is read (not on mount, not on `refresh`). Once sent, its row arrives on the wire
11
+ * like any other — the store is already listening.
12
+ */
13
+ isDraft?: boolean;
14
+ }
15
+ export interface UseAgentThreadResult extends UseAgentThreadsResult {
16
+ /** The conversation's row, or null until read (and for a conversation the server has no row for yet). */
17
+ thread: AgentThreadItem | null;
18
+ }
19
+ /** The store, as the reader's decisions see it. */
20
+ export type AgentThreadStoreView = Pick<AgentThreadStore, "getRow" | "isIndexLoading">;
21
+ /** What the reader does when it mounts on a conversation. */
22
+ export type SingleThreadMountPlan = {
23
+ kind: "cached";
24
+ thread: AgentThreadItem;
25
+ }
26
+ /** A draft: nothing to read; the first send puts its row on the wire. */
27
+ | {
28
+ kind: "draft";
29
+ }
30
+ /** An index for the agent is walking: its pages arrive as `primed`; read only if they miss the row. */
31
+ | {
32
+ kind: "await-index";
33
+ } | {
34
+ kind: "read";
35
+ };
36
+ /**
37
+ * Mount: the store's row when the index already has it; a draft reads nothing; otherwise one
38
+ * read — deferred while an index is walking, so the two never race for the same row.
39
+ */
40
+ export declare const singleThreadMountPlan: (store: AgentThreadStoreView, key: string, isDraft: boolean) => SingleThreadMountPlan;
41
+ /** What a store event means for the reader of ONE conversation. */
42
+ export type SingleThreadEventOutcome = {
43
+ kind: "ignore";
44
+ }
45
+ /** Show this row — the cache's, the newest seen from any reader or the wire, never the raw event's. */
46
+ | {
47
+ kind: "row";
48
+ thread: AgentThreadItem;
49
+ }
50
+ /** The index stopped walking without the row: read it. */
51
+ | {
52
+ kind: "read";
53
+ }
54
+ /** The conversation left the wire (a ghost run); a row with turns stays. */
55
+ | {
56
+ kind: "removed";
57
+ };
58
+ /**
59
+ * The cache is the row's source of truth: it keeps the newest row it has seen, so what it holds
60
+ * is what the surface shows — a stale wire event, or a page an index walked later, never
61
+ * regresses it. `awaitingIndex`: the mount deferred its read to a walking index.
62
+ */
63
+ export declare const singleThreadEventOutcome: (event: AgentThreadStoreEvent, store: AgentThreadStoreView, key: string, awaitingIndex: boolean) => SingleThreadEventOutcome;
64
+ /** The single-thread read's "no row" answer — a conversation the server has not seen — as opposed to a failure. */
65
+ export declare const isMissingRowError: (error: unknown) => boolean;
66
+ export declare const useAgentThread: ({ agentId, conversation, isDraft, token, headers, baseUrl, }: UseAgentThreadOptions) => UseAgentThreadResult;
67
+ //# sourceMappingURL=use-agent-thread.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-agent-thread.d.ts","sourceRoot":"","sources":["../../src/hooks/use-agent-thread.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,KAAK,eAAe,EAKrB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAA2B,KAAK,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAKL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IAC1G,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,oBAAoB,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAqB,SAAQ,qBAAqB;IACjE,yGAAyG;IACzG,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;CAChC;AAED,mDAAmD;AACnD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,gBAAgB,CAAC,CAAC;AAEvF,6DAA6D;AAC7D,MAAM,MAAM,qBAAqB,GAC7B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE;AAC7C,yEAAyE;GACvE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE;AACnB,uGAAuG;GACrG;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,OAAO,oBAAoB,EAC3B,KAAK,MAAM,EACX,SAAS,OAAO,KACf,qBASF,CAAC;AAEF,mEAAmE;AACnE,MAAM,MAAM,wBAAwB,GAChC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE;AACpB,uGAAuG;GACrG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE;AAC1C,0DAA0D;GACxD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB,4EAA4E;GAC1E;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACnC,OAAO,qBAAqB,EAC5B,OAAO,oBAAoB,EAC3B,KAAK,MAAM,EACX,eAAe,OAAO,KACrB,wBAeF,CAAC;AAEF,mHAAmH;AACnH,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,OACgB,CAAC;AAIpE,eAAO,MAAM,cAAc,GAAI,8DAO5B,qBAAqB,KAAG,oBA4J1B,CAAC"}