@adecore/agents-react 0.26.1 → 0.26.3

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,7 +1,7 @@
1
1
  import { MountedRegistry } from '../mounted-registry.js';
2
2
  import { errorCode, isConnectionError } from '../transport.js';
3
3
  /* How many items a chat reads at once, on attach and for every page before it. */
4
- const HISTORY_PAGE = 60;
4
+ const HISTORY_PAGE = 90;
5
5
  /*
6
6
  * One chat per chat id on one host. A chat opens on mount and detaches on unmount, and every mounted
7
7
  * chat is attached again when the transport comes back. The provider list is fetched once per
@@ -268,7 +268,7 @@ export class ChatClient {
268
268
  }
269
269
  }
270
270
  else {
271
- const page = result.history === undefined ? undefined : { history: result.history, pending: result.pending ?? [] };
271
+ const page = result.history === undefined ? undefined : { history: result.history, pending: result.pending ?? [], subagents: result.subagents ?? [] };
272
272
  this.sink.reset(chatId, result.info, result.items, page);
273
273
  }
274
274
  // A host without bookmarks or visuals sends none, and then this chat has none.
@@ -1,4 +1,4 @@
1
- import type { ChatInfo, ChatItem, ChatSubagentItem } from '@adecore/agent-contracts';
1
+ import { type ChatInfo, type ChatItem, type ChatSubagentItem } from '@adecore/agent-contracts';
2
2
  import type { StatusWord } from '../agents/status-look.js';
3
3
  import type { SubagentTask } from '../host.js';
4
4
  export declare function subagentTitle(item: ChatSubagentItem): string;
@@ -1,4 +1,5 @@
1
1
  import i18next from 'i18next';
2
+ import { activeSubagents } from '@adecore/agent-contracts';
2
3
  import { formatMoment, formatElapsedShort, formatTokens } from '@adecore/ui/format';
3
4
  export function subagentTitle(item) {
4
5
  return item.description || item.summary || item.subagentType || i18next.t('agent-chat:rows.subagent.label');
@@ -45,26 +46,12 @@ export function progressOf(states) {
45
46
  active: states.filter((state) => state === 'running' || state === 'paused' || state === 'waiting').length
46
47
  };
47
48
  }
48
- /*
49
- * What the flyout over the composer lists: every sub-agent still at work, and the ones that settled
50
- * since the last message, so a finished batch stays until the next message is sent. That includes an
51
- * older row a message woke again. A settled one from before that message stays too while a sibling of
52
- * its turn still runs. Older ones are only in the thread.
53
- */
49
+ /* What the flyout over the composer lists (`activeSubagents`); `order` puts the rows from before the page first. */
54
50
  export function flyoutSubagents(order, structure) {
55
- const lastMessage = order.findLastIndex((id) => structure[id]?.kind === 'user');
56
- const lastMessageAt = lastMessage < 0 ? null : (structure[order[lastMessage]]?.createdAt ?? null);
57
- const items = order.flatMap((id, index) => {
51
+ return activeSubagents(order.flatMap((id) => {
58
52
  const item = structure[id];
59
- return item?.kind === 'subagent' ? [{ item, index }] : [];
60
- });
61
- const turns = new Set(items.flatMap(({ item }) => (item.status === 'running' && item.turnId !== null ? [item.turnId] : [])));
62
- return items
63
- .filter(({ item, index }) => item.status === 'running' ||
64
- index > lastMessage ||
65
- (lastMessageAt !== null && item.finishedAt !== null && item.finishedAt > lastMessageAt) ||
66
- (item.turnId !== null && turns.has(item.turnId)))
67
- .map(({ item }) => item);
53
+ return item === undefined ? [] : [item];
54
+ }));
68
55
  }
69
56
  /* The one state the badge shows for all of them: work in progress first, then work held up, then whatever went wrong. */
70
57
  export function summaryWordOf(words) {
@@ -24,8 +24,9 @@ export function ChatActivity({ chatId }) {
24
24
  const host = useChatHost();
25
25
  const { t } = useTranslation('agent-chat');
26
26
  const order = useChatRow(chatId, (row) => row?.order) ?? NO_ITEMS;
27
+ const before = useChatRow(chatId, (row) => row?.subagentsBefore) ?? NO_ITEMS;
27
28
  const structure = useChatRow(chatId, (row) => row?.structure);
28
- const subagents = useMemo(() => (structure === undefined ? [] : flyoutSubagents(order, structure)), [order, structure]);
29
+ const subagents = useMemo(() => (structure === undefined ? [] : flyoutSubagents([...before, ...order], structure)), [before, order, structure]);
29
30
  const { id, keyOf } = useChatScope();
30
31
  const tasks = host.tasks.useTasks(id);
31
32
  // The infos of these sub-agents' own chats, so the info of any other chat changing does not draw this again.
@@ -112,6 +112,36 @@ function MarkdownLink({ href, children }) {
112
112
  function ExternalLink({ href, children }) {
113
113
  return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", children: children }));
114
114
  }
115
+ /*
116
+ * The path on the host's machine an image target names, or null for a URL and for a relative path
117
+ * nobody gave a folder for. react-markdown has already emptied a target of a scheme it distrusts.
118
+ */
119
+ function localImagePath(src, cwd) {
120
+ if (src === '' || /^[a-z][a-z0-9+.-]*:/i.test(src)) {
121
+ return null;
122
+ }
123
+ const target = hrefPath(src);
124
+ if (target.startsWith('/')) {
125
+ return target;
126
+ }
127
+ if (cwd === null) {
128
+ return null;
129
+ }
130
+ return `${cwd.replace(/\/+$/, '')}/${target.replace(/^\.\//, '')}`;
131
+ }
132
+ /* A markdown image, which only the host can read when it names a file on its machine. */
133
+ function MarkdownImage({ src, alt }) {
134
+ const { MarkdownImage: HostImage } = useChatHost();
135
+ const cwd = useFileLinkCwd();
136
+ const path = typeof src === 'string' ? localImagePath(src, cwd) : null;
137
+ if (HostImage === undefined || path === null) {
138
+ return _jsx("img", { src: src, alt: alt });
139
+ }
140
+ return _jsx(HostImage, { path: path, alt: alt ?? '' });
141
+ }
142
+ function PlainImage({ src, alt }) {
143
+ return _jsx("img", { src: src, alt: alt });
144
+ }
115
145
  /* A ```mermaid fence, which draws what it says. */
116
146
  function Diagram({ code }) {
117
147
  const { t } = useTranslation('agent-chat');
@@ -135,6 +165,7 @@ const components = {
135
165
  a({ href, children }) {
136
166
  return _jsx(MarkdownLink, { href: href, children: children });
137
167
  },
168
+ img: MarkdownImage,
138
169
  table({ children }) {
139
170
  // A table wider than the column scrolls on its own instead of pushing the thread sideways.
140
171
  return (_jsx("div", { className: "chat-table", children: _jsx("table", { children: children }) }));
@@ -146,7 +177,8 @@ const plainComponents = {
146
177
  code({ className, children }) {
147
178
  return (_jsx(MarkdownCode, { className: className, plain: true, children: children }));
148
179
  },
149
- a: ExternalLink
180
+ a: ExternalLink,
181
+ img: PlainImage
150
182
  };
151
183
  // A reply sits under the heading of its message, an `h3`, so its own headings start one level below
152
184
  // that. Only `aria-level` moves. The tag stays, since the styles and a copy of the thread read it.
package/dist/host.d.ts CHANGED
@@ -195,6 +195,10 @@ export interface ChatHost {
195
195
  ReadImage: ComponentType<{
196
196
  path: string;
197
197
  }> | null;
198
+ MarkdownImage?: ComponentType<{
199
+ path: string;
200
+ alt: string;
201
+ }>;
198
202
  code: {
199
203
  useMode(): 'light' | 'dark';
200
204
  useThemes(): {
@@ -1,4 +1,4 @@
1
- import type { AgentKind, ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatVisual } from '@adecore/agent-contracts';
1
+ import type { AgentKind, ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatSubagentItem, ChatVisual } from '@adecore/agent-contracts';
2
2
  export interface ChatState {
3
3
  info: ChatInfo;
4
4
  items: Record<string, ChatItem>;
@@ -6,6 +6,7 @@ export interface ChatState {
6
6
  order: string[];
7
7
  history?: ChatHistoryPage;
8
8
  waitingBefore?: string[];
9
+ subagentsBefore?: string[];
9
10
  bookmarks?: ChatBookmark[];
10
11
  visuals?: ChatVisual[];
11
12
  }
@@ -13,6 +14,7 @@ export type ChatHistoryPage = ChatHistoryResult['history'];
13
14
  export interface ChatPage {
14
15
  history: ChatHistoryPage;
15
16
  pending: ChatItem[];
17
+ subagents?: ChatSubagentItem[];
16
18
  }
17
19
  export type ChatsById = Record<string, ChatState>;
18
20
  export type ChatStatuses = Record<string, Pick<ChatState, 'info'>>;
@@ -12,17 +12,30 @@ function stateOf(info, items, page) {
12
12
  return { info, items: byId, structure: byId, order };
13
13
  }
14
14
  const before = page.pending.filter((item) => !(item.id in byId));
15
- for (const item of before) {
15
+ const subagents = (page.subagents ?? []).filter((item) => !(item.id in byId));
16
+ for (const item of [...before, ...subagents]) {
16
17
  byId[item.id] = item;
17
18
  }
18
- return { info, items: byId, structure: byId, order, history: page.history, waitingBefore: before.map((item) => item.id) };
19
+ return {
20
+ info,
21
+ items: byId,
22
+ structure: byId,
23
+ order,
24
+ history: page.history,
25
+ waitingBefore: before.map((item) => item.id),
26
+ subagentsBefore: subagents.map((item) => item.id)
27
+ };
19
28
  }
20
29
  /* An older host names no place in the thread, and it always hands over the whole of it. */
21
30
  function isBeforePage(state, historyIndex) {
22
31
  return state.history !== undefined && historyIndex !== undefined && historyIndex < state.history.start;
23
32
  }
24
- /* An item from before the page this client holds matters only as a request that waits, or one that stopped waiting. */
33
+ /* An item from before the page this client holds matters only as a sub-agent, a request that waits, or one that stopped waiting. */
25
34
  function withEarlierItem(state, item) {
35
+ if (item.kind === 'subagent') {
36
+ const subagents = state.subagentsBefore ?? [];
37
+ return { ...withItem(state, item), subagentsBefore: subagents.includes(item.id) ? subagents : [...subagents, item.id] };
38
+ }
26
39
  const waiting = state.waitingBefore ?? [];
27
40
  const held = waiting.includes(item.id);
28
41
  if (isWaiting(item)) {
@@ -45,11 +58,12 @@ export function prependPage(state, cursor, page) {
45
58
  }
46
59
  const order = [...olderIds, ...state.order];
47
60
  if (page.history.cursor === null) {
48
- const { history: _history, waitingBefore: _waitingBefore, ...rest } = state;
61
+ const { history: _history, waitingBefore: _waitingBefore, subagentsBefore: _subagentsBefore, ...rest } = state;
49
62
  return { ...rest, items, structure: items, order };
50
63
  }
51
64
  const waitingBefore = (state.waitingBefore ?? []).filter((id) => !olderIds.has(id));
52
- return { ...state, items, structure: items, order, history: page.history, waitingBefore };
65
+ const subagentsBefore = (state.subagentsBefore ?? []).filter((id) => !olderIds.has(id));
66
+ return { ...state, items, structure: items, order, history: page.history, waitingBefore, subagentsBefore };
53
67
  }
54
68
  /* The requests that still wait, oldest first: those from before the page this client holds, then the thread's own. */
55
69
  export function waitingRequestsOf(chat) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
4
4
  "description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -67,11 +67,11 @@
67
67
  "test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
68
68
  },
69
69
  "dependencies": {
70
- "@adecore/agent-contracts": "0.26.1",
71
- "@adecore/editor": "0.26.1",
72
- "@adecore/editor-react": "0.26.1",
73
- "@adecore/intelligent-ui": "0.26.1",
74
- "@adecore/merge": "0.26.1",
70
+ "@adecore/agent-contracts": "0.26.3",
71
+ "@adecore/editor": "0.26.3",
72
+ "@adecore/editor-react": "0.26.3",
73
+ "@adecore/intelligent-ui": "0.26.3",
74
+ "@adecore/merge": "0.26.3",
75
75
  "@codemirror/commands": "^6.11.1",
76
76
  "@codemirror/language": "^6.12.4",
77
77
  "@codemirror/state": "^6.7.6",
@@ -94,14 +94,14 @@
94
94
  "zustand": "^5.0.15"
95
95
  },
96
96
  "peerDependencies": {
97
- "@adecore/ui": "^0.26.1",
97
+ "@adecore/ui": "^0.26.3",
98
98
  "i18next": "^26.4.2",
99
99
  "react": "^19.3.0",
100
100
  "react-dom": "^19.3.0",
101
101
  "react-i18next": "^17.0.15"
102
102
  },
103
103
  "devDependencies": {
104
- "@adecore/ui": "0.26.1",
104
+ "@adecore/ui": "0.26.3",
105
105
  "@types/bun": "^1.4.2",
106
106
  "@types/react": "^19.3.0",
107
107
  "@types/react-dom": "^19.3.0",
@@ -47,7 +47,7 @@ interface Mounted extends ChatOpenOptions, MountedEntry {
47
47
  }
48
48
 
49
49
  /* How many items a chat reads at once, on attach and for every page before it. */
50
- const HISTORY_PAGE = 60;
50
+ const HISTORY_PAGE = 90;
51
51
 
52
52
  interface ProviderSink {
53
53
  setProviders(providers: ProviderInfo[]): void;
@@ -339,7 +339,8 @@ export class ChatClient {
339
339
  this.sink.apply(chatId, event);
340
340
  }
341
341
  } else {
342
- const page = result.history === undefined ? undefined : { history: result.history, pending: result.pending ?? [] };
342
+ const page =
343
+ result.history === undefined ? undefined : { history: result.history, pending: result.pending ?? [], subagents: result.subagents ?? [] };
343
344
  this.sink.reset(chatId, result.info, result.items, page);
344
345
  }
345
346
  // A host without bookmarks or visuals sends none, and then this chat has none.
@@ -1,5 +1,5 @@
1
1
  import i18next from 'i18next';
2
- import type { ChatInfo, ChatItem, ChatSubagentItem } from '@adecore/agent-contracts';
2
+ import { activeSubagents, type ChatInfo, type ChatItem, type ChatSubagentItem } from '@adecore/agent-contracts';
3
3
  import { formatMoment, formatElapsedShort, formatTokens } from '@adecore/ui/format';
4
4
  import type { StatusWord } from '../agents/status-look';
5
5
  import type { SubagentTask } from '../host';
@@ -58,29 +58,14 @@ export function progressOf(states: readonly TaskRowState[]): { done: number; tot
58
58
  };
59
59
  }
60
60
 
61
- /*
62
- * What the flyout over the composer lists: every sub-agent still at work, and the ones that settled
63
- * since the last message, so a finished batch stays until the next message is sent. That includes an
64
- * older row a message woke again. A settled one from before that message stays too while a sibling of
65
- * its turn still runs. Older ones are only in the thread.
66
- */
61
+ /* What the flyout over the composer lists (`activeSubagents`); `order` puts the rows from before the page first. */
67
62
  export function flyoutSubagents(order: readonly string[], structure: Readonly<Record<string, ChatItem>>): ChatSubagentItem[] {
68
- const lastMessage = order.findLastIndex((id) => structure[id]?.kind === 'user');
69
- const lastMessageAt = lastMessage < 0 ? null : (structure[order[lastMessage]!]?.createdAt ?? null);
70
- const items = order.flatMap((id, index) => {
71
- const item = structure[id];
72
- return item?.kind === 'subagent' ? [{ item, index }] : [];
73
- });
74
- const turns = new Set(items.flatMap(({ item }) => (item.status === 'running' && item.turnId !== null ? [item.turnId] : [])));
75
- return items
76
- .filter(
77
- ({ item, index }) =>
78
- item.status === 'running' ||
79
- index > lastMessage ||
80
- (lastMessageAt !== null && item.finishedAt !== null && item.finishedAt > lastMessageAt) ||
81
- (item.turnId !== null && turns.has(item.turnId))
82
- )
83
- .map(({ item }) => item);
63
+ return activeSubagents(
64
+ order.flatMap((id) => {
65
+ const item = structure[id];
66
+ return item === undefined ? [] : [item];
67
+ })
68
+ );
84
69
  }
85
70
 
86
71
  /* The one state the badge shows for all of them: work in progress first, then work held up, then whatever went wrong. */
@@ -28,8 +28,9 @@ export function ChatActivity({ chatId }: { chatId: string }) {
28
28
  const host = useChatHost();
29
29
  const { t } = useTranslation('agent-chat');
30
30
  const order = useChatRow(chatId, (row) => row?.order) ?? NO_ITEMS;
31
+ const before = useChatRow(chatId, (row) => row?.subagentsBefore) ?? NO_ITEMS;
31
32
  const structure = useChatRow(chatId, (row) => row?.structure);
32
- const subagents = useMemo(() => (structure === undefined ? [] : flyoutSubagents(order, structure)), [order, structure]);
33
+ const subagents = useMemo(() => (structure === undefined ? [] : flyoutSubagents([...before, ...order], structure)), [before, order, structure]);
33
34
  const { id, keyOf } = useChatScope();
34
35
  const tasks = host.tasks.useTasks(id);
35
36
  // The infos of these sub-agents' own chats, so the info of any other chat changing does not draw this again.
@@ -161,6 +161,39 @@ function ExternalLink({ href, children }: { href?: string; children?: ReactNode
161
161
  );
162
162
  }
163
163
 
164
+ /*
165
+ * The path on the host's machine an image target names, or null for a URL and for a relative path
166
+ * nobody gave a folder for. react-markdown has already emptied a target of a scheme it distrusts.
167
+ */
168
+ function localImagePath(src: string, cwd: string | null): string | null {
169
+ if (src === '' || /^[a-z][a-z0-9+.-]*:/i.test(src)) {
170
+ return null;
171
+ }
172
+ const target = hrefPath(src);
173
+ if (target.startsWith('/')) {
174
+ return target;
175
+ }
176
+ if (cwd === null) {
177
+ return null;
178
+ }
179
+ return `${cwd.replace(/\/+$/, '')}/${target.replace(/^\.\//, '')}`;
180
+ }
181
+
182
+ /* A markdown image, which only the host can read when it names a file on its machine. */
183
+ function MarkdownImage({ src, alt }: { src?: string; alt?: string }) {
184
+ const { MarkdownImage: HostImage } = useChatHost();
185
+ const cwd = useFileLinkCwd();
186
+ const path = typeof src === 'string' ? localImagePath(src, cwd) : null;
187
+ if (HostImage === undefined || path === null) {
188
+ return <img src={src} alt={alt} />;
189
+ }
190
+ return <HostImage path={path} alt={alt ?? ''} />;
191
+ }
192
+
193
+ function PlainImage({ src, alt }: { src?: string; alt?: string }) {
194
+ return <img src={src} alt={alt} />;
195
+ }
196
+
164
197
  /* A ```mermaid fence, which draws what it says. */
165
198
  function Diagram({ code }: { code: string }) {
166
199
  const { t } = useTranslation('agent-chat');
@@ -190,6 +223,7 @@ const components = {
190
223
  a({ href, children }: { href?: string; children?: ReactNode }) {
191
224
  return <MarkdownLink href={href}>{children}</MarkdownLink>;
192
225
  },
226
+ img: MarkdownImage,
193
227
  table({ children }: { children?: ReactNode }) {
194
228
  // A table wider than the column scrolls on its own instead of pushing the thread sideways.
195
229
  return (
@@ -210,7 +244,8 @@ const plainComponents = {
210
244
  </MarkdownCode>
211
245
  );
212
246
  },
213
- a: ExternalLink
247
+ a: ExternalLink,
248
+ img: PlainImage
214
249
  };
215
250
 
216
251
  // A reply sits under the heading of its message, an `h3`, so its own headings start one level below
package/src/host.ts CHANGED
@@ -313,6 +313,8 @@ export interface ChatHost {
313
313
  };
314
314
  /* The image a tool read, drawn under its row; null draws none. */
315
315
  ReadImage: ComponentType<{ path: string }> | null;
316
+ /* An image a reply's markdown names by a path on the host's machine, absolute or under the chat's cwd; left out, the `<img>` stays as written. */
317
+ MarkdownImage?: ComponentType<{ path: string; alt: string }>;
316
318
  code: {
317
319
  useMode(): 'light' | 'dark';
318
320
  /* The Shiki theme ids code is drawn in, one per mode. */
@@ -9,6 +9,7 @@ import type {
9
9
  ChatInfo,
10
10
  ChatItem,
11
11
  ChatQuestionItem,
12
+ ChatSubagentItem,
12
13
  ChatVisual
13
14
  } from '@adecore/agent-contracts';
14
15
  import { appendToolOutput } from '@adecore/agent-contracts/text';
@@ -32,6 +33,11 @@ export interface ChatState {
32
33
  * `structure` but not in `order`, since the thread on screen does not reach back to them yet.
33
34
  */
34
35
  waitingBefore?: string[];
36
+ /*
37
+ * Sub-agents from before that part, held the same way, so the pill over the composer knows of one
38
+ * that a message woke again without a scroll reaching back to its row.
39
+ */
40
+ subagentsBefore?: string[];
35
41
  /* The messages marked to come back to, as the host last said; absent until it said anything. */
36
42
  bookmarks?: ChatBookmark[];
37
43
  /* The pages agents published in the chat, as the host last said; absent until it said anything. */
@@ -45,6 +51,8 @@ export interface ChatPage {
45
51
  history: ChatHistoryPage;
46
52
  /* Every request of the chat that still waits, wherever in the thread it is. */
47
53
  pending: ChatItem[];
54
+ /* The active sub-agents from before the page; absent from a host that sends none. */
55
+ subagents?: ChatSubagentItem[];
48
56
  }
49
57
 
50
58
  /* Rows keyed with the scope's `keyOf`, so a thread says which host it runs on. */
@@ -93,10 +101,19 @@ function stateOf(info: ChatInfo, items: ChatItem[], page?: ChatPage): ChatState
93
101
  return { info, items: byId, structure: byId, order };
94
102
  }
95
103
  const before = page.pending.filter((item) => !(item.id in byId));
96
- for (const item of before) {
104
+ const subagents = (page.subagents ?? []).filter((item) => !(item.id in byId));
105
+ for (const item of [...before, ...subagents]) {
97
106
  byId[item.id] = item;
98
107
  }
99
- return { info, items: byId, structure: byId, order, history: page.history, waitingBefore: before.map((item) => item.id) };
108
+ return {
109
+ info,
110
+ items: byId,
111
+ structure: byId,
112
+ order,
113
+ history: page.history,
114
+ waitingBefore: before.map((item) => item.id),
115
+ subagentsBefore: subagents.map((item) => item.id)
116
+ };
100
117
  }
101
118
 
102
119
  /* An older host names no place in the thread, and it always hands over the whole of it. */
@@ -104,8 +121,12 @@ function isBeforePage(state: ChatState, historyIndex: number | undefined): boole
104
121
  return state.history !== undefined && historyIndex !== undefined && historyIndex < state.history.start;
105
122
  }
106
123
 
107
- /* An item from before the page this client holds matters only as a request that waits, or one that stopped waiting. */
124
+ /* An item from before the page this client holds matters only as a sub-agent, a request that waits, or one that stopped waiting. */
108
125
  function withEarlierItem(state: ChatState, item: ChatItem): ChatState {
126
+ if (item.kind === 'subagent') {
127
+ const subagents = state.subagentsBefore ?? [];
128
+ return { ...withItem(state, item), subagentsBefore: subagents.includes(item.id) ? subagents : [...subagents, item.id] };
129
+ }
109
130
  const waiting = state.waitingBefore ?? [];
110
131
  const held = waiting.includes(item.id);
111
132
  if (isWaiting(item)) {
@@ -129,11 +150,12 @@ export function prependPage(state: ChatState, cursor: string, page: ChatHistoryR
129
150
  }
130
151
  const order = [...olderIds, ...state.order];
131
152
  if (page.history.cursor === null) {
132
- const { history: _history, waitingBefore: _waitingBefore, ...rest } = state;
153
+ const { history: _history, waitingBefore: _waitingBefore, subagentsBefore: _subagentsBefore, ...rest } = state;
133
154
  return { ...rest, items, structure: items, order };
134
155
  }
135
156
  const waitingBefore = (state.waitingBefore ?? []).filter((id) => !olderIds.has(id));
136
- return { ...state, items, structure: items, order, history: page.history, waitingBefore };
157
+ const subagentsBefore = (state.subagentsBefore ?? []).filter((id) => !olderIds.has(id));
158
+ return { ...state, items, structure: items, order, history: page.history, waitingBefore, subagentsBefore };
137
159
  }
138
160
 
139
161
  /* The requests that still wait, oldest first: those from before the page this client holds, then the thread's own. */