@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.
- package/dist/chat/chat-client.js +2 -2
- package/dist/chat/subagent-list.d.ts +1 -1
- package/dist/chat/subagent-list.js +5 -18
- package/dist/chat/ui/ChatActivity.js +2 -1
- package/dist/chat/ui/Markdown.js +33 -1
- package/dist/host.d.ts +4 -0
- package/dist/state/chats.d.ts +3 -1
- package/dist/state/chats.js +19 -5
- package/package.json +8 -8
- package/src/chat/chat-client.ts +3 -2
- package/src/chat/subagent-list.ts +8 -23
- package/src/chat/ui/ChatActivity.tsx +2 -1
- package/src/chat/ui/Markdown.tsx +36 -1
- package/src/host.ts +2 -0
- package/src/state/chats.ts +27 -5
package/dist/chat/chat-client.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
package/dist/chat/ui/Markdown.js
CHANGED
|
@@ -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
package/dist/state/chats.d.ts
CHANGED
|
@@ -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'>>;
|
package/dist/state/chats.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
71
|
-
"@adecore/editor": "0.26.
|
|
72
|
-
"@adecore/editor-react": "0.26.
|
|
73
|
-
"@adecore/intelligent-ui": "0.26.
|
|
74
|
-
"@adecore/merge": "0.26.
|
|
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.
|
|
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.
|
|
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",
|
package/src/chat/chat-client.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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.
|
package/src/chat/ui/Markdown.tsx
CHANGED
|
@@ -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. */
|
package/src/state/chats.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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. */
|