@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.
- package/dist/components/new-agent-chat.d.ts +14 -24
- package/dist/components/new-agent-chat.d.ts.map +1 -1
- package/dist/components/new-agent-chat.js +120 -74
- package/dist/hooks/agent-conversation-context.d.ts +32 -0
- package/dist/hooks/agent-conversation-context.d.ts.map +1 -0
- package/dist/hooks/agent-conversation-context.js +39 -0
- package/dist/hooks/agent-thread-live-update.d.ts +35 -64
- package/dist/hooks/agent-thread-live-update.d.ts.map +1 -1
- package/dist/hooks/agent-thread-live-update.js +81 -64
- package/dist/hooks/use-agent-conversation.d.ts +105 -0
- package/dist/hooks/use-agent-conversation.d.ts.map +1 -0
- package/dist/hooks/use-agent-conversation.js +204 -0
- package/dist/hooks/use-agent-threads.d.ts +3 -50
- package/dist/hooks/use-agent-threads.d.ts.map +1 -1
- package/dist/hooks/use-agent-threads.js +53 -150
- package/dist/utils/composer-message.d.ts +57 -0
- package/dist/utils/composer-message.d.ts.map +1 -0
- package/dist/utils/composer-message.js +96 -0
- package/dist/utils/message-run-ids.d.ts +23 -3
- package/dist/utils/message-run-ids.d.ts.map +1 -1
- package/dist/utils/message-run-ids.js +45 -13
- package/package.json +2 -2
|
@@ -1,94 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
export type
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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":"
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
|
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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"}
|