@adecore/agents-react 0.25.8 → 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.
- package/dist/chat/index.d.ts +1 -0
- package/dist/chat/index.js +1 -0
- package/dist/chat/logic/command-looks.d.ts +19 -0
- package/dist/chat/logic/command-looks.js +123 -0
- package/dist/chat/logic/embedded-code.d.ts +25 -0
- package/dist/chat/logic/embedded-code.js +27 -5
- package/dist/chat/logic/timeline.d.ts +5 -3
- package/dist/chat/logic/timeline.js +64 -10
- package/dist/chat/ui/ShellCommand.d.ts +11 -1
- package/dist/chat/ui/ShellCommand.js +60 -20
- package/dist/chat/ui/SubagentTimeline.js +9 -1
- package/dist/chat/ui/Timeline.js +9 -2
- package/dist/chat/ui/rows/Rows.js +2 -2
- package/dist/chat/ui/rows/SubagentRow.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +13 -4
- package/dist/chat/ui/rows/WorkRows.js +55 -11
- package/dist/chat/ui/rows/WorkflowRow.js +1 -1
- package/dist/host.d.ts +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/locales/en/agent-chat.d.ts +4 -0
- package/dist/locales/en/agent-chat.js +5 -1
- package/dist/locales/nl/agent-chat.d.ts +4 -0
- package/dist/locales/nl/agent-chat.js +5 -1
- package/package.json +8 -8
- package/src/chat/index.ts +1 -0
- package/src/chat/logic/command-looks.ts +157 -0
- package/src/chat/logic/embedded-code.ts +57 -11
- package/src/chat/logic/timeline.ts +70 -13
- package/src/chat/ui/ShellCommand.tsx +83 -23
- package/src/chat/ui/SubagentTimeline.tsx +11 -2
- package/src/chat/ui/Timeline.tsx +9 -2
- package/src/chat/ui/rows/Rows.tsx +2 -2
- package/src/chat/ui/rows/SubagentRow.tsx +1 -1
- package/src/chat/ui/rows/WorkRows.tsx +136 -19
- package/src/chat/ui/rows/WorkflowRow.tsx +1 -1
- package/src/host.ts +43 -0
- package/src/index.ts +3 -0
- package/src/locales/en/agent-chat.ts +5 -1
- 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
|
-
|
|
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
|
-
/*
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
add(key, (count) => i18next.t(key, { count }));
|
|
125
177
|
} else {
|
|
126
178
|
other += 1;
|
|
127
179
|
}
|
|
128
180
|
}
|
|
129
|
-
const parts = [...counts].map((
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
51
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
86
|
-
parts.push(command.slice(at,
|
|
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
|
-
|
|
120
|
+
content
|
|
92
121
|
) : (
|
|
93
|
-
<span key={
|
|
94
|
-
{
|
|
122
|
+
<span key={start} style={style}>
|
|
123
|
+
{content}
|
|
95
124
|
</span>
|
|
96
125
|
)
|
|
97
126
|
);
|
|
98
|
-
at =
|
|
127
|
+
at = end;
|
|
128
|
+
}
|
|
129
|
+
if (at < to) {
|
|
130
|
+
parts.push(command.slice(at, to));
|
|
99
131
|
}
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
() =>
|
|
75
|
-
|
|
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(() => {
|
package/src/chat/ui/Timeline.tsx
CHANGED
|
@@ -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
|
-
{
|
|
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} />;
|