@adecore/agents-react 0.25.7 → 0.25.9

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.
Files changed (39) hide show
  1. package/dist/chat/index.d.ts +1 -0
  2. package/dist/chat/index.js +1 -0
  3. package/dist/chat/logic/command-looks.d.ts +19 -0
  4. package/dist/chat/logic/command-looks.js +123 -0
  5. package/dist/chat/logic/embedded-code.d.ts +25 -0
  6. package/dist/chat/logic/embedded-code.js +27 -5
  7. package/dist/chat/logic/timeline.d.ts +5 -3
  8. package/dist/chat/logic/timeline.js +64 -10
  9. package/dist/chat/ui/ShellCommand.d.ts +11 -1
  10. package/dist/chat/ui/ShellCommand.js +60 -20
  11. package/dist/chat/ui/SubagentTimeline.js +9 -1
  12. package/dist/chat/ui/Timeline.js +9 -2
  13. package/dist/chat/ui/rows/Rows.js +2 -2
  14. package/dist/chat/ui/rows/SubagentRow.js +1 -1
  15. package/dist/chat/ui/rows/WorkRows.d.ts +13 -4
  16. package/dist/chat/ui/rows/WorkRows.js +55 -11
  17. package/dist/chat/ui/rows/WorkflowRow.js +1 -1
  18. package/dist/host.d.ts +25 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/locales/en/agent-chat.d.ts +4 -0
  21. package/dist/locales/en/agent-chat.js +5 -1
  22. package/dist/locales/nl/agent-chat.d.ts +4 -0
  23. package/dist/locales/nl/agent-chat.js +5 -1
  24. package/package.json +8 -8
  25. package/src/chat/index.ts +1 -0
  26. package/src/chat/logic/command-looks.ts +157 -0
  27. package/src/chat/logic/embedded-code.ts +57 -11
  28. package/src/chat/logic/timeline.ts +70 -13
  29. package/src/chat/ui/ShellCommand.tsx +83 -23
  30. package/src/chat/ui/SubagentTimeline.tsx +11 -2
  31. package/src/chat/ui/Timeline.tsx +9 -2
  32. package/src/chat/ui/rows/Rows.tsx +2 -2
  33. package/src/chat/ui/rows/SubagentRow.tsx +1 -1
  34. package/src/chat/ui/rows/WorkRows.tsx +136 -19
  35. package/src/chat/ui/rows/WorkflowRow.tsx +1 -1
  36. package/src/host.ts +43 -0
  37. package/src/index.ts +3 -0
  38. package/src/locales/en/agent-chat.ts +5 -1
  39. package/src/locales/nl/agent-chat.ts +5 -1
@@ -14,12 +14,13 @@ import type {
14
14
  ChatWorkflow
15
15
  } from '@adecore/agent-contracts';
16
16
  import { abortedByMachine } from '@adecore/agent-contracts';
17
- import type { ThreadCard } from '../../host';
17
+ import type { CommandLook, ThreadCard } from '../../host';
18
18
  import { formatElapsedShort } from '@adecore/ui/format';
19
+ import { matchCommandLook } from './command-looks';
19
20
  import { isImageGeneration } from './generated-image';
20
21
  import { handbackReportOf } from './handback';
21
22
  import { toolEntry } from './tool-catalog';
22
- import { hasFileChanges, isFileChange } from './tools';
23
+ import { hasFileChanges, isFileChange, shellCommand } from './tools';
23
24
 
24
25
  /* A subagent's row: the work the thread kept of it, and the agents it opened in turn, each a row of its own under it. */
25
26
  export interface SubagentBranch {
@@ -43,7 +44,8 @@ export type TimelineRow =
43
44
  | { kind: 'report'; id: string; text: string }
44
45
  | { kind: 'thinking'; id: string; item: ChatThinkingItem }
45
46
  | { kind: 'work'; id: string; tool: ChatToolItem }
46
- | { kind: 'work-group'; id: string; tools: ChatToolItem[]; summary: string; expanded: boolean }
47
+ // `look` is set when every call is a run of that look and the summary is its sentence.
48
+ | { kind: 'work-group'; id: string; tools: ChatToolItem[]; summary: string; expanded: boolean; look?: CommandLook }
47
49
  | { kind: 'work-live'; id: string; tool: ChatToolItem }
48
50
  // The latest thought of a running turn, repeated over the call it led to; `at` is when that call began.
49
51
  | { kind: 'live-thought'; id: string; item: ChatThinkingItem; at: number }
@@ -70,6 +72,8 @@ interface TimelineOptions {
70
72
  activeTurnId: string | null;
71
73
  /* The settled turns a fork went on after; a thread that cannot fork leaves it out. */
72
74
  forkedTurns?: ReadonlySet<string>;
75
+ /* The host's programs drawn as tools of their own, whose runs read as their own sentences. */
76
+ commandLooks?: readonly CommandLook[];
73
77
  }
74
78
 
75
79
  /*
@@ -83,8 +87,42 @@ export function isBlock(row: TimelineRow): boolean {
83
87
  return BLOCK_KINDS.has(row.kind) || isImageRow(row);
84
88
  }
85
89
 
86
- /* "Read 4 files", "Ran 2 commands", or "12 tool calls" when the run mixes kinds. */
87
- export function summarizeGroup(tools: ChatToolItem[]): string {
90
+ /*
91
+ * The sentence of a run of shell calls that are all runs of one look: its group's when they share a key,
92
+ * else the look's own; null for any other run, which reads as shell calls.
93
+ */
94
+ function lookSummary(tools: readonly ChatToolItem[], looks: readonly CommandLook[] | undefined): { look: CommandLook; summary: string } | null {
95
+ let look: CommandLook | null = null;
96
+ let group: { key: string; sentence(count: number): string } | undefined;
97
+ let sameKey = true;
98
+ for (const tool of tools) {
99
+ const command = shellCommand(tool.name, tool.input);
100
+ const run = command === null ? null : matchCommandLook(command, looks);
101
+ if (run === null || (look !== null && run.look !== look)) {
102
+ return null;
103
+ }
104
+ if (look === null) {
105
+ look = run.look;
106
+ group = run.match.group;
107
+ } else if (run.match.group?.key !== group?.key) {
108
+ sameKey = false;
109
+ }
110
+ }
111
+ if (look === null) {
112
+ return null;
113
+ }
114
+ if (sameKey && group !== undefined) {
115
+ return { look, summary: group.sentence(tools.length) };
116
+ }
117
+ return look.sentence === undefined ? null : { look, summary: look.sentence(tools.length) };
118
+ }
119
+
120
+ /* "Read 4 files", "Ran 2 commands", "12 tool calls" when the run mixes kinds, or the sentence of a look all its calls are runs of. */
121
+ export function summarizeGroup(tools: ChatToolItem[], looks?: readonly CommandLook[]): string {
122
+ const looked = lookSummary(tools, looks);
123
+ if (looked !== null) {
124
+ return looked.summary;
125
+ }
88
126
  const names = new Set(tools.map((tool) => tool.name));
89
127
  const only = names.size === 1 ? tools[0]!.name : null;
90
128
  if (only !== null && toolEntry(only)?.grouped === true) {
@@ -110,23 +148,37 @@ function editedPath(tool: ChatToolItem): string {
110
148
  * "Searched 2 patterns". Every file change reads as one sentence, and calls without a sentence of
111
149
  * their own are counted together at the end.
112
150
  */
113
- export function summarizeTurn(tools: readonly ChatToolItem[]): string[] {
151
+ export function summarizeTurn(tools: readonly ChatToolItem[], looks?: readonly CommandLook[]): string[] {
114
152
  // Keyed by the sentence, so its place is where the first call of that kind came.
115
- const counts = new Map<string, number>();
153
+ const counts = new Map<string, { count: number; say(count: number): string }>();
154
+ const add = (key: string, say: (count: number) => string, total?: number): void => {
155
+ const known = counts.get(key);
156
+ if (known === undefined) {
157
+ counts.set(key, { count: total ?? 1, say });
158
+ } else {
159
+ known.count = total ?? known.count + 1;
160
+ }
161
+ };
116
162
  const edited = new Set<string>();
117
163
  let other = 0;
118
164
  for (const tool of tools) {
119
- if (isFileChange(tool.name)) {
165
+ const command = shellCommand(tool.name, tool.input);
166
+ const run = command === null ? null : matchCommandLook(command, looks);
167
+ const group = run?.match.group;
168
+ if (run !== null && group !== undefined) {
169
+ // Keyed by the program too, so two looks with a key in common keep their own sentences.
170
+ add(`look:${run.look.program}:${group.key}`, group.sentence);
171
+ } else if (isFileChange(tool.name)) {
120
172
  edited.add(editedPath(tool));
121
- counts.set('agent-chat:group.edited', edited.size);
173
+ add('agent-chat:group.edited', (count) => i18next.t('agent-chat:group.edited', { count }), edited.size);
122
174
  } else if (toolEntry(tool.name)?.grouped === true) {
123
175
  const key = `agent-chat:group.tools.${tool.name}`;
124
- counts.set(key, (counts.get(key) ?? 0) + 1);
176
+ add(key, (count) => i18next.t(key, { count }));
125
177
  } else {
126
178
  other += 1;
127
179
  }
128
180
  }
129
- const parts = [...counts].map(([key, count]) => i18next.t(key, { count }));
181
+ const parts = [...counts.values()].map(({ count, say }) => say(count));
130
182
  if (other > 0) {
131
183
  parts.push(i18next.t(parts.length === 0 ? 'agent-chat:group.toolCalls' : 'agent-chat:group.otherCalls', { count: other }));
132
184
  }
@@ -239,7 +291,12 @@ function flushTools(buffer: ChatToolItem[], rows: TimelineRow[], options: Timeli
239
291
  } else if (settled.length > 1) {
240
292
  const id = `group-${settled[0]!.id}`;
241
293
  const expanded = options.expandedGroups.has(id);
242
- rows.push({ kind: 'work-group', id, tools: settled, summary: summarizeGroup(settled), expanded });
294
+ const looked = lookSummary(settled, options.commandLooks);
295
+ rows.push(
296
+ looked === null
297
+ ? { kind: 'work-group', id, tools: settled, summary: summarizeGroup(settled), expanded }
298
+ : { kind: 'work-group', id, tools: settled, summary: looked.summary, expanded, look: looked.look }
299
+ );
243
300
  if (expanded) {
244
301
  for (const tool of settled) {
245
302
  rows.push({ kind: 'work', id: tool.id, tool });
@@ -462,7 +519,7 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
462
519
  id: `fold-${turnId}`,
463
520
  turn,
464
521
  label: turnLabel(turn, chunk.items),
465
- work: summarizeTurn(tools),
522
+ work: summarizeTurn(tools, options.commandLooks),
466
523
  hiddenCount: folded.length,
467
524
  expanded
468
525
  });
@@ -1,8 +1,9 @@
1
1
  import { useEffect, useState, type ReactNode } from 'react';
2
2
  import type { BundledLanguage } from 'shiki';
3
- import { EMBEDDED_CODE_LIMIT, SHELL_LANGUAGE, embeddedCode } from '../logic/embedded-code';
3
+ import { Tooltip } from '@adecore/ui';
4
+ import { EMBEDDED_CODE_LIMIT, SHELL_LANGUAGE, embeddedCode, type EmbeddedCode } from '../logic/embedded-code';
4
5
  import type { CodeToken } from './code-lines';
5
- import { useChatHost } from '../../host';
6
+ import { useChatHost, type CommandName } from '../../host';
6
7
  import { useCodeTheme } from './code-theme';
7
8
  import { tokenStyle, useCodeHighlighter, type CodeHighlighter, type Highlighter } from './highlighter';
8
9
 
@@ -12,6 +13,17 @@ interface Run extends CodeToken {
12
13
  end: number;
13
14
  }
14
15
 
16
+ /* A word of a command drawn as the name a host gave it, as offsets into the command. */
17
+ export interface NamedWord {
18
+ readonly start: number;
19
+ readonly end: number;
20
+ readonly word: string;
21
+ readonly name: CommandName;
22
+ }
23
+
24
+ const NO_CODE: readonly EmbeddedCode[] = [];
25
+ const NO_NAMES: readonly NamedWord[] = [];
26
+
15
27
  // Rows redraw often and a thread holds many commands, so each is tokenized once per theme.
16
28
  const CACHE_LIMIT = 300;
17
29
  const cache = new Map<string, Run[]>();
@@ -47,13 +59,20 @@ function overlay(base: readonly Run[], start: number, end: number, inner: readon
47
59
  return [...kept, ...inner].sort((left, right) => left.start - right.start);
48
60
  }
49
61
 
50
- function languagesOf(command: string): Set<string> {
51
- return new Set([SHELL_LANGUAGE, ...embeddedCode(command).map(({ language }) => language)]);
62
+ /* The code inside a command, with the stretches a caller knows the language of on top. */
63
+ function codeIn(command: string, extra: readonly EmbeddedCode[]): EmbeddedCode[] {
64
+ return extra.length === 0
65
+ ? embeddedCode(command)
66
+ : [...embeddedCode(command), ...extra].sort((left, right) => left.start - right.start || right.end - left.end);
67
+ }
68
+
69
+ function languagesOf(command: string, extra: readonly EmbeddedCode[]): Set<string> {
70
+ return new Set([SHELL_LANGUAGE, ...codeIn(command, extra).map(({ language }) => language)]);
52
71
  }
53
72
 
54
73
  /* The colored runs of a command, or null while a grammar or the theme is still on its way. */
55
- function highlight(command: string, theme: string, code: CodeHighlighter): Run[] | null {
56
- const key = `${theme}\n${command}`;
74
+ function highlight(command: string, extra: readonly EmbeddedCode[], theme: string, code: CodeHighlighter): Run[] | null {
75
+ const key = `${theme}\n${extra.map(({ start, end, language }) => `${start}:${end}:${language}`).join(',')}\n${command}`;
57
76
  const known = cache.get(key);
58
77
  if (known !== undefined) {
59
78
  // Touched entries move to the end, so the oldest falls out first.
@@ -61,7 +80,7 @@ function highlight(command: string, theme: string, code: CodeHighlighter): Run[]
61
80
  cache.set(key, known);
62
81
  return known;
63
82
  }
64
- const embedded = embeddedCode(command);
83
+ const embedded = codeIn(command, extra);
65
84
  const highlighter = code.ready([SHELL_LANGUAGE, ...embedded.map(({ language }) => language)], theme);
66
85
  if (highlighter === null) {
67
86
  return null;
@@ -77,58 +96,99 @@ function highlight(command: string, theme: string, code: CodeHighlighter): Run[]
77
96
  return runs;
78
97
  }
79
98
 
80
- function draw(command: string, runs: readonly Run[]): ReactNode[] {
99
+ /* `command[from, to)` in its colors, or as it is while there are none. */
100
+ function draw(command: string, runs: readonly Run[] | null, from: number, to: number): ReactNode[] {
101
+ if (runs === null) {
102
+ return [command.slice(from, to)];
103
+ }
81
104
  const parts: ReactNode[] = [];
82
- let at = 0;
105
+ let at = from;
83
106
  for (const run of runs) {
107
+ if (run.end <= from || run.start >= to) {
108
+ continue;
109
+ }
110
+ const start = Math.max(run.start, from);
111
+ const end = Math.min(run.end, to);
84
112
  // Shiki leaves the line breaks out of its tokens; they come from the command itself.
85
- if (run.start > at) {
86
- parts.push(command.slice(at, run.start));
113
+ if (start > at) {
114
+ parts.push(command.slice(at, start));
87
115
  }
116
+ const content = run.content.slice(start - run.start, end - run.start);
88
117
  const style = tokenStyle(run);
89
118
  parts.push(
90
119
  style === undefined ? (
91
- run.content
120
+ content
92
121
  ) : (
93
- <span key={run.start} style={style}>
94
- {run.content}
122
+ <span key={start} style={style}>
123
+ {content}
95
124
  </span>
96
125
  )
97
126
  );
98
- at = run.end;
127
+ at = end;
128
+ }
129
+ if (at < to) {
130
+ parts.push(command.slice(at, to));
99
131
  }
100
- if (at < command.length) {
101
- parts.push(command.slice(at));
132
+ return parts;
133
+ }
134
+
135
+ /* A word as its name, in a frame so it reads as a reference; the word itself stays in the text, for a find. */
136
+ function Name({ named }: { named: NamedWord }) {
137
+ return (
138
+ <Tooltip label={named.word}>
139
+ <span className="inline-flex items-center gap-1 rounded bg-surface-hover px-1 font-sans text-text ring-1 ring-border ring-inset">
140
+ {named.name.icon}
141
+ {named.name.name}
142
+ <span className="sr-only">{named.word}</span>
143
+ </span>
144
+ </Tooltip>
145
+ );
146
+ }
147
+
148
+ function drawNamed(command: string, runs: readonly Run[] | null, names: readonly NamedWord[]): ReactNode[] {
149
+ const parts: ReactNode[] = [];
150
+ let at = 0;
151
+ for (const named of names) {
152
+ if (named.start < at) {
153
+ continue;
154
+ }
155
+ parts.push(...draw(command, runs, at, named.start), <Name key={`name-${named.start}`} named={named} />);
156
+ at = named.end;
102
157
  }
158
+ parts.push(...draw(command, runs, at, command.length));
103
159
  return parts;
104
160
  }
105
161
 
106
162
  /*
107
163
  * A shell command in the colors of the code theme, with the code inside it (a `bash -lc` script, a
108
164
  * `python3 -c` argument, a heredoc) in its own language. Plain text until the grammars are in, which
109
- * changes no size; a command past the limit stays plain.
165
+ * changes no size; a command past the limit stays plain. `code` adds stretches whose language the
166
+ * caller knows, and `names` draws words as the names a host gave them, both as offsets into `command`.
110
167
  */
111
- export function ShellCommand({ command }: { command: string }) {
168
+ export function ShellCommand({ command, code = NO_CODE, names = NO_NAMES }: { command: string; code?: readonly EmbeddedCode[]; names?: readonly NamedWord[] }) {
112
169
  const theme = useCodeTheme();
113
170
  const { custom } = useChatHost().code;
114
171
  const highlighter = useCodeHighlighter();
115
172
  // Bumped when a load settles, so the cache is asked again.
116
173
  const [, setLoads] = useState(0);
117
174
  const tooLong = command.length > EMBEDDED_CODE_LIMIT;
118
- const runs = tooLong ? null : highlight(command, theme, highlighter);
175
+ const runs = tooLong ? null : highlight(command, code, theme, highlighter);
119
176
 
120
177
  useEffect(() => {
121
178
  if (runs !== null || tooLong) {
122
179
  return;
123
180
  }
124
181
  let cancelled = false;
125
- Promise.all([highlighter.loadTheme(theme, custom), ...[...languagesOf(command)].map((lang) => highlighter.loadLanguage(lang))])
182
+ Promise.all([highlighter.loadTheme(theme, custom), ...[...languagesOf(command, code)].map((lang) => highlighter.loadLanguage(lang))])
126
183
  .then(() => !cancelled && setLoads((count) => count + 1))
127
184
  .catch(() => undefined);
128
185
  return () => {
129
186
  cancelled = true;
130
187
  };
131
- }, [highlighter, command, theme, custom, runs, tooLong]);
188
+ }, [highlighter, command, code, theme, custom, runs, tooLong]);
132
189
 
133
- return <>{runs === null ? command : draw(command, runs)}</>;
190
+ if (names.length > 0) {
191
+ return <>{drawNamed(command, runs, names)}</>;
192
+ }
193
+ return <>{runs === null ? command : draw(command, runs, 0, command.length)}</>;
134
194
  }
@@ -19,6 +19,7 @@ import { FileLinkContext } from './file-links';
19
19
  import { useChatRow } from '../../state/chats';
20
20
  import { useChatPreferences } from '../preferences';
21
21
  import { readingWidthStyle } from '../reading-width';
22
+ import { useChatHost } from '../../host';
22
23
  import { useChatScope } from '../../scope';
23
24
  import { EmptyState, ContextMenu, Icon } from '@adecore/ui';
24
25
 
@@ -31,6 +32,7 @@ const NO_TURNS = new Set<string>();
31
32
  export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUseId: string }) {
32
33
  const { t } = useTranslation('agent-chat');
33
34
  const scope = useChatScope();
35
+ const { commandLooks } = useChatHost();
34
36
  const connected = useSyncExternalStore(
35
37
  useCallback((changed) => scope.transport.subscribeStatus(changed), [scope]),
36
38
  useCallback(() => scope.transport.status === 'open', [scope]),
@@ -71,8 +73,15 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
71
73
  }, [state.unsupported, scope]);
72
74
 
73
75
  const rows = useMemo(
74
- () => deriveTimelineRows(state.items, { expandedGroups: groups.ids, expandedTurns: NO_TURNS, expandedSubagents: subagents.ids, activeTurnId: null }),
75
- [state.items, groups.ids, subagents.ids]
76
+ () =>
77
+ deriveTimelineRows(state.items, {
78
+ expandedGroups: groups.ids,
79
+ expandedTurns: NO_TURNS,
80
+ expandedSubagents: subagents.ids,
81
+ activeTurnId: null,
82
+ commandLooks
83
+ }),
84
+ [state.items, groups.ids, subagents.ids, commandLooks]
76
85
  );
77
86
 
78
87
  useLayoutEffect(() => {
@@ -252,9 +252,16 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
252
252
  }
253
253
  return deriveTimelineRows(
254
254
  order.map((id) => items[id]!),
255
- { expandedGroups: groups.ids, expandedTurns: turns.ids, expandedSubagents: subagents.ids, activeTurnId, forkedTurns }
255
+ {
256
+ expandedGroups: groups.ids,
257
+ expandedTurns: turns.ids,
258
+ expandedSubagents: subagents.ids,
259
+ activeTurnId,
260
+ forkedTurns,
261
+ commandLooks: host.commandLooks
262
+ }
256
263
  );
257
- }, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns]);
264
+ }, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns, host.commandLooks]);
258
265
  const cards = host.useThreadCards(scopeId, chatId);
259
266
  const drawsVisuals = host.visuals !== null;
260
267
  // Where the part of the thread this client holds begins, while there is a page before it.
@@ -66,13 +66,13 @@ export function Row({ row, chatId, toggleGroup, toggleTurn, toggleSubagent, open
66
66
  case 'live-thought':
67
67
  return <LiveThoughtRow chatId={chatId} item={row.item} />;
68
68
  case 'work':
69
- return isImageGeneration(row.tool) ? <GeneratedImageRow chatId={chatId} tool={row.tool} /> : <WorkRow tool={row.tool} />;
69
+ return isImageGeneration(row.tool) ? <GeneratedImageRow chatId={chatId} tool={row.tool} /> : <WorkRow chatId={chatId} tool={row.tool} />;
70
70
  case 'work-live':
71
71
  return isImageGeneration(row.tool) ? <GeneratedImageRow chatId={chatId} tool={row.tool} /> : <WorkLiveRow chatId={chatId} tool={row.tool} />;
72
72
  case 'workflow':
73
73
  return <WorkflowRow chatId={chatId} tool={row.tool} workflow={row.workflow} onOpenAgent={openConversation} />;
74
74
  case 'work-group':
75
- return <WorkGroupRow tools={row.tools} summary={row.summary} expanded={row.expanded} onToggle={() => toggleGroup(row.id)} />;
75
+ return <WorkGroupRow tools={row.tools} summary={row.summary} look={row.look} expanded={row.expanded} onToggle={() => toggleGroup(row.id)} />;
76
76
  case 'subagent':
77
77
  return <SubagentBranchRow chatId={chatId} branch={row} onToggle={toggleSubagent} onOpenConversation={openConversation} />;
78
78
  case 'turn-fold':
@@ -32,7 +32,7 @@ function ChildRow({ chatId, item }: { chatId: string; item: ChatItem }) {
32
32
  if (isImageGeneration(item)) {
33
33
  return <GeneratedImageRow chatId={chatId} tool={item} />;
34
34
  }
35
- return item.state === 'running' ? <WorkLiveRow chatId={chatId} tool={item} /> : <WorkRow tool={item} />;
35
+ return item.state === 'running' ? <WorkLiveRow chatId={chatId} tool={item} /> : <WorkRow chatId={chatId} tool={item} />;
36
36
  }
37
37
  if (item.kind === 'assistant') {
38
38
  return <ChildText chatId={chatId} item={item} />;