@moldable-ai/ui 0.2.7 → 0.2.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/components/app-error-boundary.d.ts +26 -0
- package/dist/components/app-error-boundary.d.ts.map +1 -0
- package/dist/components/app-error-boundary.js +87 -0
- package/dist/components/chat/chat-input.d.ts +9 -1
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-input.js +13 -4
- package/dist/components/chat/chat-message.d.ts +6 -0
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +576 -157
- package/dist/components/chat/chat-messages.d.ts +2 -1
- package/dist/components/chat/chat-messages.d.ts.map +1 -1
- package/dist/components/chat/chat-messages.js +3 -3
- package/dist/components/chat/chat-panel.d.ts +25 -3
- package/dist/components/chat/chat-panel.d.ts.map +1 -1
- package/dist/components/chat/chat-panel.js +173 -49
- package/dist/components/chat/index.d.ts +3 -3
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/model-selector.d.ts.map +1 -1
- package/dist/components/chat/model-selector.js +1 -1
- package/dist/components/chat/tool-handlers.d.ts.map +1 -1
- package/dist/components/chat/tool-handlers.js +89 -1
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +18 -4
- package/dist/components/rich-media-player.d.ts +8 -0
- package/dist/components/rich-media-player.d.ts.map +1 -0
- package/dist/components/rich-media-player.js +99 -0
- package/dist/components/ui/command.d.ts +2 -1
- package/dist/components/ui/command.d.ts.map +1 -1
- package/dist/components/ui/command.js +2 -2
- package/dist/components/ui/resizable.d.ts +4 -1
- package/dist/components/ui/resizable.d.ts.map +1 -1
- package/dist/components/ui/resizable.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/lib/commands.d.ts +8 -0
- package/dist/lib/commands.d.ts.map +1 -1
- package/dist/lib/commands.js +18 -0
- package/package.json +17 -14
- package/src/styles/index.css +25 -0
- package/dist/lib/commands.test.d.ts +0 -2
- package/dist/lib/commands.test.d.ts.map +0 -1
- package/dist/lib/commands.test.js +0 -111
|
@@ -1,58 +1,484 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronDown, Image as ImageIcon } from 'lucide-react';
|
|
4
4
|
import { memo, useEffect, useMemo, useState } from 'react';
|
|
5
5
|
import { cn } from '../../lib/utils';
|
|
6
6
|
import { Markdown } from '../markdown';
|
|
7
7
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '../ui/collapsible';
|
|
8
|
-
import { ThinkingTimeline, ThinkingTimelineMarker, } from './thinking-timeline';
|
|
9
8
|
import { ToolApproval, ToolApprovalHeader, ToolApprovalRejected, } from './tool-approval';
|
|
10
9
|
import { useToolApprovalResponse } from './tool-approval-context';
|
|
11
|
-
import { getToolHandler } from './tool-handlers';
|
|
10
|
+
import { getToolHandler, } from './tool-handlers';
|
|
12
11
|
import { useToolProgress } from './tool-progress-context';
|
|
13
12
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
14
13
|
const DEFAULT_ACTIONS_LABEL = 'Thinking';
|
|
15
14
|
// OpenRouter sends these for encrypted reasoning msgs
|
|
16
15
|
const stripRedacted = (text) => text.replace(/\[REDACTED\]/g, '').trim();
|
|
16
|
+
function emptyActivityCounts() {
|
|
17
|
+
return {
|
|
18
|
+
createdFileCount: 0,
|
|
19
|
+
editedFileCount: 0,
|
|
20
|
+
deletedFileCount: 0,
|
|
21
|
+
exploredFileCount: 0,
|
|
22
|
+
searchCount: 0,
|
|
23
|
+
listCount: 0,
|
|
24
|
+
commandCount: 0,
|
|
25
|
+
runningCommandCount: 0,
|
|
26
|
+
webSearchCount: 0,
|
|
27
|
+
apiCallCount: 0,
|
|
28
|
+
skillActionCount: 0,
|
|
29
|
+
appActionCount: 0,
|
|
30
|
+
toolCallCount: 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function incrementActivityCount(counts, key, amount = 1) {
|
|
34
|
+
counts[key] += amount;
|
|
35
|
+
}
|
|
36
|
+
function asRecord(value) {
|
|
37
|
+
return value && typeof value === 'object'
|
|
38
|
+
? value
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
function stringValue(value) {
|
|
42
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
43
|
+
? value
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
function getString(record, keys) {
|
|
47
|
+
if (!record) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
const value = stringValue(record[key]);
|
|
52
|
+
if (value) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function getCount(record, keys) {
|
|
59
|
+
if (!record) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
const value = record[key];
|
|
64
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return value.length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function getFileName(path) {
|
|
74
|
+
const parts = path.split('/');
|
|
75
|
+
return parts[parts.length - 1] || path;
|
|
76
|
+
}
|
|
77
|
+
function getPathLabel(path) {
|
|
78
|
+
return path ? getFileName(path) : undefined;
|
|
79
|
+
}
|
|
80
|
+
function getToolPath(part) {
|
|
81
|
+
const args = asRecord(part.args);
|
|
82
|
+
const output = asRecord(part.output);
|
|
83
|
+
return (getString(output, ['path', 'filePath', 'targetPath']) ??
|
|
84
|
+
getString(args, ['path', 'filePath', 'targetPath']));
|
|
85
|
+
}
|
|
86
|
+
function getToolQuery(part) {
|
|
87
|
+
const args = asRecord(part.args);
|
|
88
|
+
const output = asRecord(part.output);
|
|
89
|
+
return (getString(args, ['query', 'pattern', 'glob', 'text']) ??
|
|
90
|
+
getString(output, ['query', 'pattern', 'glob']));
|
|
91
|
+
}
|
|
92
|
+
function getToolCommand(part, progress) {
|
|
93
|
+
const args = asRecord(part.args);
|
|
94
|
+
const output = asRecord(part.output);
|
|
95
|
+
return (progress?.command ??
|
|
96
|
+
getString(output, ['command']) ??
|
|
97
|
+
getString(args, ['command']));
|
|
98
|
+
}
|
|
99
|
+
function getToolTarget(part) {
|
|
100
|
+
const args = asRecord(part.args);
|
|
101
|
+
const output = asRecord(part.output);
|
|
102
|
+
return (getString(output, ['appName', 'appId', 'name', 'id']) ??
|
|
103
|
+
getString(args, ['appName', 'appId', 'name', 'id', 'repoName', 'skillName']));
|
|
104
|
+
}
|
|
105
|
+
function getAppApiTarget(part) {
|
|
106
|
+
const args = asRecord(part.args);
|
|
107
|
+
const output = asRecord(part.output);
|
|
108
|
+
return (getString(args, ['targetAppId', 'appId']) ??
|
|
109
|
+
getString(output, ['targetAppId', 'appId', 'appName']));
|
|
110
|
+
}
|
|
111
|
+
function getAppApiMethod(part) {
|
|
112
|
+
const args = asRecord(part.args);
|
|
113
|
+
const output = asRecord(part.output);
|
|
114
|
+
return getString(args, ['method']) ?? getString(output, ['method']);
|
|
115
|
+
}
|
|
116
|
+
function isToolLoading(part, progress) {
|
|
117
|
+
const isApprovalResponded = part.state === 'approval-responded' || part.state === 'output-denied';
|
|
118
|
+
const wasApproved = part.approval?.approved === true;
|
|
119
|
+
const hasOutput = part.output !== undefined;
|
|
120
|
+
return (part.state === 'partial-call' ||
|
|
121
|
+
part.state === 'call' ||
|
|
122
|
+
part.state === 'input-streaming' ||
|
|
123
|
+
part.state === 'input-available' ||
|
|
124
|
+
part.state === 'pending' ||
|
|
125
|
+
progress?.status === 'running' ||
|
|
126
|
+
(isApprovalResponded && wasApproved && !hasOutput) ||
|
|
127
|
+
(!hasOutput &&
|
|
128
|
+
part.state !== 'output-available' &&
|
|
129
|
+
part.state !== 'output-error' &&
|
|
130
|
+
part.state !== 'output-denied'));
|
|
131
|
+
}
|
|
132
|
+
function toolHasError(part) {
|
|
133
|
+
const output = asRecord(part.output);
|
|
134
|
+
if (!output) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return (output.success === false ||
|
|
138
|
+
output.ok === false ||
|
|
139
|
+
(typeof output.exitCode === 'number' && output.exitCode !== 0) ||
|
|
140
|
+
Boolean(output.error));
|
|
141
|
+
}
|
|
142
|
+
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
143
|
+
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
144
|
+
}
|
|
145
|
+
function sentenceCase(text) {
|
|
146
|
+
return text.length > 0 ? text.charAt(0).toUpperCase() + text.slice(1) : text;
|
|
147
|
+
}
|
|
148
|
+
function formatAppApiSummary(count, labels) {
|
|
149
|
+
if (labels.length === 0) {
|
|
150
|
+
return `called ${plural(count, 'app API', 'app APIs')}`;
|
|
151
|
+
}
|
|
152
|
+
if (count === 1) {
|
|
153
|
+
return `called app API ${labels[0]}`;
|
|
154
|
+
}
|
|
155
|
+
const shown = labels.slice(0, 2);
|
|
156
|
+
const remaining = labels.length - shown.length;
|
|
157
|
+
const labelText = remaining > 0 ? `${shown.join(', ')} +${remaining}` : shown.join(', ');
|
|
158
|
+
return `called ${plural(count, 'app API', 'app APIs')} (${labelText})`;
|
|
159
|
+
}
|
|
160
|
+
function formatActivitySummary(counts, fallback, isStreaming, apiCallLabels = []) {
|
|
161
|
+
const segments = [];
|
|
162
|
+
const completedCommands = Math.max(0, counts.commandCount - counts.runningCommandCount);
|
|
163
|
+
const explorationDetails = [
|
|
164
|
+
counts.exploredFileCount > 0
|
|
165
|
+
? plural(counts.exploredFileCount, 'file')
|
|
166
|
+
: null,
|
|
167
|
+
counts.searchCount > 0
|
|
168
|
+
? plural(counts.searchCount, 'search', 'searches')
|
|
169
|
+
: null,
|
|
170
|
+
counts.listCount > 0
|
|
171
|
+
? plural(counts.listCount, 'directory', 'directories')
|
|
172
|
+
: null,
|
|
173
|
+
].filter(Boolean);
|
|
174
|
+
if (counts.createdFileCount > 0) {
|
|
175
|
+
segments.push(`created ${plural(counts.createdFileCount, 'file')}`);
|
|
176
|
+
}
|
|
177
|
+
if (counts.editedFileCount > 0) {
|
|
178
|
+
segments.push(`edited ${plural(counts.editedFileCount, 'file')}`);
|
|
179
|
+
}
|
|
180
|
+
if (counts.deletedFileCount > 0) {
|
|
181
|
+
segments.push(`deleted ${plural(counts.deletedFileCount, 'file')}`);
|
|
182
|
+
}
|
|
183
|
+
if (explorationDetails.length > 0) {
|
|
184
|
+
segments.push(`explored ${explorationDetails.join(', ')}`);
|
|
185
|
+
}
|
|
186
|
+
if (completedCommands > 0) {
|
|
187
|
+
segments.push(`ran ${plural(completedCommands, 'command')}`);
|
|
188
|
+
}
|
|
189
|
+
if (counts.runningCommandCount > 0) {
|
|
190
|
+
segments.push(`running ${plural(counts.runningCommandCount, 'command')}`);
|
|
191
|
+
}
|
|
192
|
+
if (counts.webSearchCount > 0) {
|
|
193
|
+
segments.push(`ran ${plural(counts.webSearchCount, 'web search', 'web searches')}`);
|
|
194
|
+
}
|
|
195
|
+
if (counts.apiCallCount > 0) {
|
|
196
|
+
segments.push(formatAppApiSummary(counts.apiCallCount, apiCallLabels));
|
|
197
|
+
}
|
|
198
|
+
if (counts.skillActionCount > 0) {
|
|
199
|
+
segments.push(`updated ${plural(counts.skillActionCount, 'skill setting')}`);
|
|
200
|
+
}
|
|
201
|
+
if (counts.appActionCount > 0) {
|
|
202
|
+
segments.push(`updated ${plural(counts.appActionCount, 'app')}`);
|
|
203
|
+
}
|
|
204
|
+
if (counts.toolCallCount > 0) {
|
|
205
|
+
segments.push(`used ${plural(counts.toolCallCount, 'tool')}`);
|
|
206
|
+
}
|
|
207
|
+
if (segments.length === 0) {
|
|
208
|
+
return isStreaming ? fallback : fallback || DEFAULT_ACTIONS_LABEL;
|
|
209
|
+
}
|
|
210
|
+
return sentenceCase(segments.join(', '));
|
|
211
|
+
}
|
|
212
|
+
function ActivityCode({ children, title, }) {
|
|
213
|
+
return (_jsx("code", { className: "text-foreground/80 inline max-w-full truncate align-baseline font-mono text-[0.95em]", title: title, children: children }));
|
|
214
|
+
}
|
|
215
|
+
function toolActivityText({ verb, path, detail, fallback, }) {
|
|
216
|
+
const label = path ?? detail;
|
|
217
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { children: verb }), label ? (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: path, children: label })] })) : fallback ? (_jsxs(_Fragment, { children: [" ", fallback] })) : null] }));
|
|
218
|
+
}
|
|
219
|
+
function summarizeToolActivity(part, toolHandler, progress, fallbackId) {
|
|
220
|
+
const counts = emptyActivityCounts();
|
|
221
|
+
const running = isToolLoading(part, progress);
|
|
222
|
+
const hasError = toolHasError(part);
|
|
223
|
+
const path = getToolPath(part);
|
|
224
|
+
const pathLabel = getPathLabel(path);
|
|
225
|
+
const query = getToolQuery(part);
|
|
226
|
+
const command = getToolCommand(part, progress);
|
|
227
|
+
const target = getToolTarget(part);
|
|
228
|
+
const output = asRecord(part.output);
|
|
229
|
+
const args = asRecord(part.args);
|
|
230
|
+
const entryId = part.toolCallId ?? fallbackId ?? `${part.toolName}-${part.state}`;
|
|
231
|
+
const loadingLabel = toolHandler.loadingLabel.replace(/\.\.\.$/, '');
|
|
232
|
+
let content = running ? loadingLabel : `Used ${part.toolName}`;
|
|
233
|
+
switch (part.toolName) {
|
|
234
|
+
case 'readFile':
|
|
235
|
+
case 'fileExists':
|
|
236
|
+
incrementActivityCount(counts, 'exploredFileCount');
|
|
237
|
+
content = toolActivityText({
|
|
238
|
+
verb: running
|
|
239
|
+
? 'Reading'
|
|
240
|
+
: part.toolName === 'fileExists'
|
|
241
|
+
? 'Checked'
|
|
242
|
+
: 'Read',
|
|
243
|
+
path: pathLabel,
|
|
244
|
+
fallback: 'file',
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
case 'readToolOutput':
|
|
248
|
+
incrementActivityCount(counts, 'exploredFileCount');
|
|
249
|
+
content = running ? 'Reading tool output' : 'Read tool output';
|
|
250
|
+
break;
|
|
251
|
+
case 'listDirectory': {
|
|
252
|
+
incrementActivityCount(counts, 'listCount');
|
|
253
|
+
const itemCount = getCount(output, ['entries', 'items']);
|
|
254
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? 'Listing' : 'Listed' }), ' ', _jsx(ActivityCode, { title: path, children: pathLabel ?? 'directory' }), typeof itemCount === 'number' && (_jsxs("span", { children: [" (", plural(itemCount, 'item'), ")"] }))] }));
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'grep':
|
|
258
|
+
case 'globFileSearch': {
|
|
259
|
+
incrementActivityCount(counts, 'searchCount');
|
|
260
|
+
const resultCount = getCount(output, ['matches', 'files', 'results']);
|
|
261
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? 'Searching' : 'Searched' }), query && (_jsxs(_Fragment, { children: [' ', "for ", _jsx(ActivityCode, { title: query, children: query })] })), typeof resultCount === 'number' && (_jsxs("span", { children: [" (", plural(resultCount, 'result'), ")"] }))] }));
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case 'webSearch':
|
|
265
|
+
incrementActivityCount(counts, 'webSearchCount');
|
|
266
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? 'Searching web' : 'Searched web' }), query && (_jsxs(_Fragment, { children: [' ', "for ", _jsx(ActivityCode, { title: query, children: query })] }))] }));
|
|
267
|
+
break;
|
|
268
|
+
case 'executeBashCommand':
|
|
269
|
+
case 'runCommand':
|
|
270
|
+
incrementActivityCount(counts, 'commandCount');
|
|
271
|
+
if (running) {
|
|
272
|
+
incrementActivityCount(counts, 'runningCommandCount');
|
|
273
|
+
}
|
|
274
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? 'Running' : 'Ran' }), ' ', _jsx(ActivityCode, { title: command, children: command ?? 'command' })] }));
|
|
275
|
+
break;
|
|
276
|
+
case 'writeFile': {
|
|
277
|
+
const created = output?.created === true || args?.create === true;
|
|
278
|
+
incrementActivityCount(counts, created ? 'createdFileCount' : 'editedFileCount');
|
|
279
|
+
content = toolActivityText({
|
|
280
|
+
verb: running ? 'Writing' : created ? 'Created' : 'Wrote',
|
|
281
|
+
path: pathLabel,
|
|
282
|
+
fallback: 'file',
|
|
283
|
+
});
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'editFile':
|
|
287
|
+
incrementActivityCount(counts, 'editedFileCount');
|
|
288
|
+
content = toolActivityText({
|
|
289
|
+
verb: running ? 'Editing' : 'Edited',
|
|
290
|
+
path: pathLabel,
|
|
291
|
+
fallback: 'file',
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
case 'deleteFile':
|
|
295
|
+
incrementActivityCount(counts, 'deletedFileCount');
|
|
296
|
+
content = toolActivityText({
|
|
297
|
+
verb: running ? 'Deleting' : 'Deleted',
|
|
298
|
+
path: pathLabel,
|
|
299
|
+
fallback: 'file',
|
|
300
|
+
});
|
|
301
|
+
break;
|
|
302
|
+
case 'listMoldableAppApi':
|
|
303
|
+
case 'callMoldableAppApi':
|
|
304
|
+
incrementActivityCount(counts, 'apiCallCount');
|
|
305
|
+
{
|
|
306
|
+
const apiTarget = getAppApiTarget(part);
|
|
307
|
+
const apiMethod = getAppApiMethod(part);
|
|
308
|
+
const apiSummaryLabel = apiMethod ?? apiTarget;
|
|
309
|
+
const apiTitle = apiMethod && apiTarget
|
|
310
|
+
? `${apiTarget}: ${apiMethod}`
|
|
311
|
+
: apiSummaryLabel;
|
|
312
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running
|
|
313
|
+
? part.toolName === 'listMoldableAppApi'
|
|
314
|
+
? 'Listing app APIs'
|
|
315
|
+
: 'Calling app API'
|
|
316
|
+
: part.toolName === 'listMoldableAppApi'
|
|
317
|
+
? 'Listed app APIs'
|
|
318
|
+
: 'Called app API' }), apiSummaryLabel && (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: apiTitle, children: apiSummaryLabel })] }))] }));
|
|
319
|
+
return {
|
|
320
|
+
entry: {
|
|
321
|
+
id: entryId,
|
|
322
|
+
content: hasError ? (_jsxs(_Fragment, { children: [content, _jsx("span", { children: " failed" })] })) : (content),
|
|
323
|
+
isRunning: running,
|
|
324
|
+
isError: hasError,
|
|
325
|
+
summaryLabel: part.toolName === 'callMoldableAppApi'
|
|
326
|
+
? apiSummaryLabel
|
|
327
|
+
: undefined,
|
|
328
|
+
},
|
|
329
|
+
counts,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
case 'take_screenshot':
|
|
333
|
+
incrementActivityCount(counts, 'toolCallCount');
|
|
334
|
+
content = running ? 'Taking app screenshot' : 'Captured app screenshot';
|
|
335
|
+
break;
|
|
336
|
+
case 'viewImage': {
|
|
337
|
+
incrementActivityCount(counts, 'toolCallCount');
|
|
338
|
+
const imagePath = stringValue(args?.imagePath) ?? stringValue(output?.imagePath);
|
|
339
|
+
const imageLabel = getPathLabel(imagePath);
|
|
340
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? 'Viewing image' : 'Viewed image' }), imageLabel && (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: imagePath, children: imageLabel })] }))] }));
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case 'listSkillRepos':
|
|
344
|
+
case 'listAvailableSkills':
|
|
345
|
+
case 'syncSkills':
|
|
346
|
+
case 'addSkillRepo':
|
|
347
|
+
case 'updateSkillSelection':
|
|
348
|
+
case 'initSkillsConfig':
|
|
349
|
+
incrementActivityCount(counts, 'skillActionCount');
|
|
350
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? loadingLabel : 'Updated skills' }), target && (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: target, children: target })] }))] }));
|
|
351
|
+
break;
|
|
352
|
+
case 'scaffoldApp':
|
|
353
|
+
case 'getAppInfo':
|
|
354
|
+
case 'unregisterApp':
|
|
355
|
+
case 'deleteAppData':
|
|
356
|
+
case 'deleteApp':
|
|
357
|
+
incrementActivityCount(counts, 'appActionCount');
|
|
358
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? loadingLabel : 'Updated app' }), target && (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: target, children: target })] }))] }));
|
|
359
|
+
break;
|
|
360
|
+
default:
|
|
361
|
+
incrementActivityCount(counts, 'toolCallCount');
|
|
362
|
+
content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? loadingLabel : 'Used tool' }), ' ', _jsx(ActivityCode, { children: part.toolName })] }));
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (hasError) {
|
|
366
|
+
content = (_jsxs(_Fragment, { children: [content, _jsx("span", { children: " failed" })] }));
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
entry: {
|
|
370
|
+
id: entryId,
|
|
371
|
+
content,
|
|
372
|
+
isRunning: running,
|
|
373
|
+
isError: hasError,
|
|
374
|
+
},
|
|
375
|
+
counts,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function formatWorkDuration(ms) {
|
|
379
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
380
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
381
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
382
|
+
const seconds = totalSeconds % 60;
|
|
383
|
+
if (hours > 0) {
|
|
384
|
+
return `${hours}h ${minutes}m`;
|
|
385
|
+
}
|
|
386
|
+
if (minutes > 0) {
|
|
387
|
+
return `${minutes}m ${seconds}s`;
|
|
388
|
+
}
|
|
389
|
+
return `${seconds}s`;
|
|
390
|
+
}
|
|
391
|
+
function useNow(enabled) {
|
|
392
|
+
const [now, setNow] = useState(() => Date.now());
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
if (!enabled) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const interval = window.setInterval(() => {
|
|
398
|
+
setNow(Date.now());
|
|
399
|
+
}, 1000);
|
|
400
|
+
return () => window.clearInterval(interval);
|
|
401
|
+
}, [enabled]);
|
|
402
|
+
return now;
|
|
403
|
+
}
|
|
404
|
+
function WorkSummary({ messageId, startedAtMs, completedAtMs, isWorking, children, }) {
|
|
405
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
406
|
+
const now = useNow(isWorking && Boolean(startedAtMs));
|
|
407
|
+
const endAtMs = completedAtMs ?? (isWorking ? now : undefined);
|
|
408
|
+
const elapsedMs = typeof startedAtMs === 'number' && typeof endAtMs === 'number'
|
|
409
|
+
? Math.max(0, endAtMs - startedAtMs)
|
|
410
|
+
: null;
|
|
411
|
+
const durationLabel = elapsedMs !== null && (elapsedMs >= 1000 || !isWorking)
|
|
412
|
+
? formatWorkDuration(elapsedMs)
|
|
413
|
+
: null;
|
|
414
|
+
const summaryLabel = isWorking
|
|
415
|
+
? durationLabel
|
|
416
|
+
? `Working for ${durationLabel}`
|
|
417
|
+
: 'Working'
|
|
418
|
+
: durationLabel
|
|
419
|
+
? `Worked for ${durationLabel}`
|
|
420
|
+
: 'Worked';
|
|
421
|
+
return (_jsxs(Collapsible, { open: isOpen, onOpenChange: setIsOpen, className: "w-full", "data-work-summary": messageId, children: [_jsxs("div", { className: "text-muted-foreground/75 text-[13px] leading-5", children: [_jsxs(CollapsibleTrigger, { className: cn('group inline-flex max-w-full cursor-pointer items-center gap-1 rounded-md border border-transparent text-left transition-colors', 'hover:bg-muted/30 hover:text-foreground/80 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2'), "aria-label": `${summaryLabel} activity`, children: [_jsx("span", { className: "truncate", children: summaryLabel }), _jsx(ChevronDown, { className: cn('size-3 shrink-0 opacity-40 transition-transform duration-200 group-hover:opacity-70', isOpen ? 'rotate-0' : '-rotate-90') })] }), _jsx("div", { className: "border-border/60 mt-1 w-full border-t" })] }), _jsx(CollapsibleContent, { className: "overflow-hidden", children: _jsx("div", { className: "pt-4", children: children }) })] }));
|
|
422
|
+
}
|
|
17
423
|
function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
18
424
|
// Get tool progress for streaming stdout/stderr
|
|
19
425
|
const toolProgress = useToolProgress();
|
|
20
426
|
// Get approval response handler
|
|
21
427
|
const onApprovalResponse = useToolApprovalResponse();
|
|
22
|
-
// Track open state for each
|
|
23
|
-
const [
|
|
24
|
-
// Process parts chronologically, grouping
|
|
25
|
-
//
|
|
428
|
+
// Track open state for each background activity group independently.
|
|
429
|
+
const [activityGroupStates, setActivityGroupStates] = useState(new Map());
|
|
430
|
+
// Process parts chronologically, grouping ordinary tool work and reasoning as
|
|
431
|
+
// low-emphasis background activity. Approval prompts remain prominent because
|
|
432
|
+
// they need direct user action.
|
|
26
433
|
const { contentItems, hasFinalAssistantText } = useMemo(() => {
|
|
27
434
|
const items = [];
|
|
28
435
|
const state = {
|
|
29
|
-
|
|
30
|
-
|
|
436
|
+
currentActivityGroup: null,
|
|
437
|
+
activityGroupIndex: 0,
|
|
31
438
|
hasText: false,
|
|
32
439
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
440
|
+
const updateActivityTitle = (group) => {
|
|
441
|
+
group.title = formatActivitySummary(group.counts, group.title, group.isStreaming, group.apiCallLabels);
|
|
442
|
+
};
|
|
443
|
+
const addActivity = (group, entry, counts) => {
|
|
444
|
+
group.entries.push(entry);
|
|
445
|
+
if (entry.summaryLabel &&
|
|
446
|
+
!group.apiCallLabels.includes(entry.summaryLabel)) {
|
|
447
|
+
group.apiCallLabels.push(entry.summaryLabel);
|
|
448
|
+
}
|
|
449
|
+
group.isStreaming = group.isStreaming || Boolean(entry.isRunning);
|
|
450
|
+
if (counts) {
|
|
451
|
+
for (const key of Object.keys(counts)) {
|
|
452
|
+
group.counts[key] += counts[key];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
updateActivityTitle(group);
|
|
456
|
+
};
|
|
457
|
+
// Helper to close current activity group
|
|
458
|
+
const closeActivityGroup = () => {
|
|
459
|
+
if (state.currentActivityGroup) {
|
|
36
460
|
items.push({
|
|
37
|
-
type: '
|
|
38
|
-
id: state.
|
|
39
|
-
|
|
461
|
+
type: 'activity',
|
|
462
|
+
id: state.currentActivityGroup.id,
|
|
463
|
+
activityGroup: state.currentActivityGroup,
|
|
40
464
|
});
|
|
41
|
-
state.
|
|
465
|
+
state.currentActivityGroup = null;
|
|
42
466
|
}
|
|
43
467
|
};
|
|
44
|
-
// Helper to ensure
|
|
45
|
-
const
|
|
46
|
-
if (!state.
|
|
47
|
-
state.
|
|
48
|
-
state.
|
|
49
|
-
id: `
|
|
50
|
-
|
|
468
|
+
// Helper to ensure activity group exists
|
|
469
|
+
const ensureActivityGroup = () => {
|
|
470
|
+
if (!state.currentActivityGroup) {
|
|
471
|
+
state.activityGroupIndex++;
|
|
472
|
+
state.currentActivityGroup = {
|
|
473
|
+
id: `activity-${message.id}-${state.activityGroupIndex}`,
|
|
474
|
+
entries: [],
|
|
475
|
+
counts: emptyActivityCounts(),
|
|
476
|
+
apiCallLabels: [],
|
|
51
477
|
title: DEFAULT_ACTIONS_LABEL,
|
|
52
478
|
isStreaming: false,
|
|
53
479
|
};
|
|
54
480
|
}
|
|
55
|
-
return state.
|
|
481
|
+
return state.currentActivityGroup;
|
|
56
482
|
};
|
|
57
483
|
// If no parts, use content as text
|
|
58
484
|
const parts = message.parts ?? [];
|
|
@@ -74,85 +500,60 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
74
500
|
// Handle tool calls
|
|
75
501
|
if (part.type === 'tool-invocation') {
|
|
76
502
|
const toolHandler = getToolHandler(part.toolName);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const isToolLoading = part.state === 'partial-call' ||
|
|
90
|
-
part.state === 'call' ||
|
|
91
|
-
part.state === 'pending' ||
|
|
92
|
-
(isApprovalResponded && wasApproved && part.output === undefined);
|
|
93
|
-
// Check for streaming progress (stdout/stderr from command execution)
|
|
94
|
-
const progress = part.toolCallId
|
|
95
|
-
? toolProgress[part.toolCallId]
|
|
96
|
-
: undefined;
|
|
97
|
-
const hasStreamingOutput = progress && (progress.stdout || progress.stderr);
|
|
98
|
-
let toolContent;
|
|
99
|
-
// Handle approval request state
|
|
100
|
-
if (isApprovalRequested &&
|
|
101
|
-
toolHandler.renderApproval &&
|
|
102
|
-
part.approval?.id &&
|
|
103
|
-
onApprovalResponse) {
|
|
104
|
-
toolContent = toolHandler.renderApproval({
|
|
503
|
+
const isApprovalRequested = part.state === 'approval-requested';
|
|
504
|
+
const isApprovalResponded = part.state === 'approval-responded' || part.state === 'output-denied';
|
|
505
|
+
const wasRejected = part.approval?.approved === false;
|
|
506
|
+
if (isApprovalRequested &&
|
|
507
|
+
toolHandler.renderApproval &&
|
|
508
|
+
part.approval?.id &&
|
|
509
|
+
onApprovalResponse) {
|
|
510
|
+
closeActivityGroup();
|
|
511
|
+
items.push({
|
|
512
|
+
type: 'inline-tool',
|
|
513
|
+
id: key,
|
|
514
|
+
content: toolHandler.renderApproval({
|
|
105
515
|
approvalId: part.approval.id,
|
|
106
516
|
toolCallId: part.toolCallId || key,
|
|
107
517
|
args: part.args,
|
|
108
|
-
}, onApprovalResponse)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
else if (hasStreamingOutput && toolHandler.renderStreaming) {
|
|
115
|
-
// Show streaming output (live stdout/stderr)
|
|
116
|
-
toolContent = toolHandler.renderStreaming(part.args, progress);
|
|
117
|
-
}
|
|
118
|
-
else if (isToolLoading) {
|
|
119
|
-
// Show loading state (arguments streaming or waiting)
|
|
120
|
-
toolContent = toolHandler.renderLoading?.(part.args) || (_jsx("div", { className: "text-muted-foreground text-xs", children: toolHandler.loadingLabel }));
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
// Show final output
|
|
124
|
-
toolContent = toolHandler.renderOutput(part.output, part.toolCallId || key);
|
|
125
|
-
}
|
|
518
|
+
}, onApprovalResponse),
|
|
519
|
+
});
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (isApprovalResponded && wasRejected) {
|
|
523
|
+
closeActivityGroup();
|
|
126
524
|
items.push({
|
|
127
525
|
type: 'inline-tool',
|
|
128
526
|
id: key,
|
|
129
|
-
content:
|
|
527
|
+
content: (_jsx(ToolApproval, { state: "output-denied", approved: false, children: _jsx(ToolApprovalHeader, { children: _jsx(ToolApprovalRejected, { children: "Command execution was rejected" }) }) })),
|
|
130
528
|
});
|
|
131
529
|
continue;
|
|
132
530
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
part.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
content: (_jsx("div", { className: "px-2 py-1", children: _jsx("span", { className: "text-muted-foreground font-mono text-xs", children: toolLabel }) }, `${part.toolCallId || key}-loading`)),
|
|
531
|
+
const progress = part.toolCallId
|
|
532
|
+
? toolProgress[part.toolCallId]
|
|
533
|
+
: undefined;
|
|
534
|
+
const shouldRenderImageToolInline = part.toolName === 'take_screenshot' ||
|
|
535
|
+
part.toolName === 'generateImage';
|
|
536
|
+
if (shouldRenderImageToolInline && isToolLoading(part, progress)) {
|
|
537
|
+
closeActivityGroup();
|
|
538
|
+
items.push({
|
|
539
|
+
type: 'inline-tool',
|
|
540
|
+
id: key,
|
|
541
|
+
content: toolHandler.renderLoading?.(part.args) ?? null,
|
|
145
542
|
});
|
|
146
543
|
continue;
|
|
147
544
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
545
|
+
if (shouldRenderImageToolInline && part.output !== undefined) {
|
|
546
|
+
closeActivityGroup();
|
|
547
|
+
items.push({
|
|
548
|
+
type: 'inline-tool',
|
|
549
|
+
id: key,
|
|
152
550
|
content: toolHandler.renderOutput(part.output, part.toolCallId || key),
|
|
153
551
|
});
|
|
154
552
|
continue;
|
|
155
553
|
}
|
|
554
|
+
const group = ensureActivityGroup();
|
|
555
|
+
const activity = summarizeToolActivity(part, toolHandler, progress, key);
|
|
556
|
+
addActivity(group, activity.entry, activity.counts);
|
|
156
557
|
continue;
|
|
157
558
|
}
|
|
158
559
|
if (part.type === 'reasoning') {
|
|
@@ -161,7 +562,7 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
161
562
|
if (cleaned.length === 0) {
|
|
162
563
|
continue;
|
|
163
564
|
}
|
|
164
|
-
const group =
|
|
565
|
+
const group = ensureActivityGroup();
|
|
165
566
|
// Extract title from markdown **title** or use first line
|
|
166
567
|
const markdownTitleMatch = cleaned.match(/\*\*([^*]+)\*\*/);
|
|
167
568
|
const candidateTitle = markdownTitleMatch?.[1];
|
|
@@ -172,15 +573,15 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
172
573
|
const body = candidateTitle
|
|
173
574
|
? trimmed.replace(/\*\*[^*]+\*\*\s*/, '')
|
|
174
575
|
: trimmed;
|
|
175
|
-
group
|
|
176
|
-
|
|
177
|
-
content:
|
|
576
|
+
addActivity(group, {
|
|
577
|
+
id: key,
|
|
578
|
+
content: _jsx("span", { className: "whitespace-pre-wrap", children: body }),
|
|
178
579
|
});
|
|
179
580
|
continue;
|
|
180
581
|
}
|
|
181
582
|
if (part.type === 'text') {
|
|
182
|
-
// Close current
|
|
183
|
-
|
|
583
|
+
// Close current background activity group before text.
|
|
584
|
+
closeActivityGroup();
|
|
184
585
|
state.hasText = true;
|
|
185
586
|
items.push({
|
|
186
587
|
type: 'text',
|
|
@@ -190,7 +591,7 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
190
591
|
continue;
|
|
191
592
|
}
|
|
192
593
|
if (part.type === 'file') {
|
|
193
|
-
|
|
594
|
+
closeActivityGroup();
|
|
194
595
|
items.push({
|
|
195
596
|
type: 'file',
|
|
196
597
|
id: key,
|
|
@@ -198,8 +599,8 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
198
599
|
});
|
|
199
600
|
}
|
|
200
601
|
}
|
|
201
|
-
// Close any remaining
|
|
202
|
-
|
|
602
|
+
// Close any remaining activity group
|
|
603
|
+
closeActivityGroup();
|
|
203
604
|
return {
|
|
204
605
|
contentItems: items,
|
|
205
606
|
hasFinalAssistantText: state.hasText,
|
|
@@ -211,27 +612,27 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
211
612
|
toolProgress,
|
|
212
613
|
onApprovalResponse,
|
|
213
614
|
]);
|
|
214
|
-
// Determine if last
|
|
215
|
-
const
|
|
216
|
-
.filter((item) => item.type === '
|
|
217
|
-
.map((item) => (item.type === '
|
|
615
|
+
// Determine if last activity group is streaming
|
|
616
|
+
const lastActivityGroup = contentItems
|
|
617
|
+
.filter((item) => item.type === 'activity')
|
|
618
|
+
.map((item) => (item.type === 'activity' ? item.activityGroup : null))
|
|
218
619
|
.filter((g) => g !== null)
|
|
219
620
|
.pop();
|
|
220
621
|
// Check if we're still streaming overall and waiting for text
|
|
221
622
|
const isStreamingOverall = isStreaming && isLast && !hasFinalAssistantText;
|
|
222
|
-
// For the last
|
|
623
|
+
// For the last activity group, also keep it open if we just finished streaming
|
|
223
624
|
// but text hasn't appeared yet (grace period for AI to start responding)
|
|
224
625
|
const shouldKeepLastGroupOpen = isLast && !hasFinalAssistantText && message.role === 'assistant';
|
|
225
|
-
// Auto-open/close
|
|
626
|
+
// Auto-open/close activity groups based on streaming state
|
|
226
627
|
useEffect(() => {
|
|
227
|
-
|
|
628
|
+
setActivityGroupStates((prevStates) => {
|
|
228
629
|
let hasChanges = false;
|
|
229
630
|
const nextStates = new Map(prevStates);
|
|
230
631
|
contentItems.forEach((item) => {
|
|
231
|
-
if (item.type === '
|
|
232
|
-
const groupId = item.
|
|
233
|
-
const group = item.
|
|
234
|
-
const isLastGroup =
|
|
632
|
+
if (item.type === 'activity' && item.activityGroup) {
|
|
633
|
+
const groupId = item.activityGroup.id;
|
|
634
|
+
const group = item.activityGroup;
|
|
635
|
+
const isLastGroup = lastActivityGroup?.id === groupId;
|
|
235
636
|
const currentState = nextStates.get(groupId);
|
|
236
637
|
const shouldBeOpen = group.isStreaming ||
|
|
237
638
|
(isLastGroup && isStreamingOverall) ||
|
|
@@ -262,58 +663,76 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
262
663
|
}, [
|
|
263
664
|
contentItems,
|
|
264
665
|
isStreamingOverall,
|
|
265
|
-
|
|
666
|
+
lastActivityGroup,
|
|
266
667
|
shouldKeepLastGroupOpen,
|
|
267
668
|
]);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
669
|
+
const renderContentItem = (item) => {
|
|
670
|
+
if (item.type === 'activity' &&
|
|
671
|
+
item.activityGroup &&
|
|
672
|
+
message.role === 'assistant') {
|
|
673
|
+
const group = item.activityGroup;
|
|
674
|
+
const groupState = activityGroupStates.get(group.id);
|
|
675
|
+
const isLastGroup = lastActivityGroup?.id === group.id;
|
|
676
|
+
const isGroupStreaming = group.isStreaming || (isLastGroup && isStreamingOverall);
|
|
677
|
+
const shouldBeOpenByDefault = isGroupStreaming;
|
|
678
|
+
const isOpen = groupState?.isOpen ?? shouldBeOpenByDefault;
|
|
679
|
+
const titleForTrigger = group.title || DEFAULT_ACTIONS_LABEL;
|
|
680
|
+
return (_jsxs(Collapsible, { open: isOpen, onOpenChange: (open) => {
|
|
681
|
+
setActivityGroupStates((prev) => {
|
|
682
|
+
const next = new Map(prev);
|
|
683
|
+
const current = next.get(group.id) ?? {
|
|
684
|
+
isOpen: false,
|
|
685
|
+
userOpened: false,
|
|
686
|
+
};
|
|
687
|
+
next.set(group.id, {
|
|
688
|
+
...current,
|
|
689
|
+
isOpen: open,
|
|
690
|
+
userOpened: open ? true : current.userOpened,
|
|
691
|
+
});
|
|
692
|
+
return next;
|
|
693
|
+
});
|
|
694
|
+
}, className: "mb-1 w-full", "data-ai-reasoning": true, children: [_jsxs(CollapsibleTrigger, { className: cn('group flex max-w-full cursor-pointer items-center gap-1 py-0.5 text-[13px] font-normal leading-5 transition-colors', 'text-muted-foreground/75 hover:text-foreground/80 min-w-0'), children: [_jsx("span", { className: "flex-1 truncate", children: titleForTrigger }), _jsx(ChevronDown, { className: cn('size-3 shrink-0 opacity-0 transition-all duration-200 group-hover:opacity-70', isOpen && 'rotate-180 opacity-50') })] }), _jsx(CollapsibleContent, { className: "text-muted-foreground/75 min-w-0 overflow-hidden pb-1.5 pl-3 pt-0 text-[13px] leading-5", children: _jsx("div", { className: "max-h-28 space-y-0.5 overflow-y-auto pr-2", children: group.entries.map((entry) => (_jsx("div", { className: cn('min-w-0 overflow-hidden truncate', entry.isError && 'text-destructive'), children: entry.content }, entry.id))) }) })] }, item.id));
|
|
695
|
+
}
|
|
696
|
+
if (item.type === 'inline-tool') {
|
|
697
|
+
return (_jsx("div", { className: "w-full min-w-0", children: item.content }, item.id));
|
|
698
|
+
}
|
|
699
|
+
if (item.type === 'file' && item.filePart) {
|
|
700
|
+
const isImage = item.filePart.mediaType.startsWith('image/');
|
|
701
|
+
const label = item.filePart.filename || 'Attached image';
|
|
702
|
+
return (_jsxs("div", { className: cn('bg-secondary text-secondary-foreground flex w-full items-center gap-3 rounded-2xl px-3 py-2.5', message.role !== 'user' && 'bg-muted text-foreground'), children: [_jsx("div", { className: "bg-background flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-lg border", children: isImage && item.filePart.previewUrl ? (_jsx("img", { src: item.filePart.previewUrl, alt: "", className: "size-full object-cover", draggable: false })) : (_jsx(ImageIcon, { className: "text-muted-foreground size-5" })) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm font-medium", children: label }), _jsx("p", { className: "text-muted-foreground truncate text-xs", children: item.filePart.mediaType })] })] }, item.id));
|
|
703
|
+
}
|
|
704
|
+
if (item.type === 'text' && item.textPart) {
|
|
705
|
+
if (message.role === 'user') {
|
|
706
|
+
return (_jsx("div", { className: "flex w-full justify-end", children: _jsx("div", { className: "bg-foreground/5 text-foreground max-w-[77%] break-words rounded-2xl px-3 py-2 text-[13px] leading-5", children: _jsx("div", { className: "whitespace-pre-wrap break-words", children: item.textPart.text }) }) }, item.id));
|
|
707
|
+
}
|
|
708
|
+
return (_jsx("div", { className: cn('flex min-w-0 flex-col gap-4', {
|
|
709
|
+
'w-full': message.role === 'assistant',
|
|
710
|
+
}), children: _jsx(Markdown, { markdown: item.textPart.text, proseSize: "sm", className: cn('[&_.prose]:text-[13px] [&_.prose]:leading-[21px] [&_.prose]:text-current', '[&_.prose_pre]:bg-background/80 [&_.prose_code]:text-current', '[&_.prose_p:last-child]:mb-0 [&_.prose_p]:mb-2', '[&_.prose_ol:first-child]:mt-0 [&_.prose_ul:first-child]:mt-0', '[&_.prose>*:last-child]:mb-0') }) }, item.id));
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
};
|
|
714
|
+
const finalTextItemIndex = message.role === 'assistant'
|
|
715
|
+
? contentItems.reduce((lastIndex, item, index) => item.type === 'text' ? index : lastIndex, -1)
|
|
716
|
+
: -1;
|
|
717
|
+
const shouldCollapsePreAnswerWork = message.role === 'assistant' &&
|
|
718
|
+
hasFinalAssistantText &&
|
|
719
|
+
finalTextItemIndex > 0;
|
|
720
|
+
const preAnswerWorkItems = shouldCollapsePreAnswerWork
|
|
721
|
+
? contentItems.slice(0, finalTextItemIndex)
|
|
722
|
+
: [];
|
|
723
|
+
const finalAnswerItems = shouldCollapsePreAnswerWork
|
|
724
|
+
? contentItems.slice(finalTextItemIndex)
|
|
725
|
+
: contentItems;
|
|
726
|
+
const isTurnStillWorking = isLast &&
|
|
727
|
+
isStreaming &&
|
|
728
|
+
message.role === 'assistant' &&
|
|
729
|
+
!message.turn?.completedAtMs;
|
|
730
|
+
return (_jsx(AnimatePresence, { children: _jsx(motion.div, { "data-testid": `message-${message.role}`, className: "group/message w-full min-w-0 px-2", initial: { y: 5, opacity: 0 }, animate: { y: 0, opacity: 1 }, "data-role": message.role, children: _jsx("div", { className: "flex w-full min-w-0 items-start gap-4", children: _jsxs("div", { className: "mt-1 flex w-full min-w-0 flex-col gap-2", children: [shouldCollapsePreAnswerWork && (_jsx(WorkSummary, { messageId: message.id, startedAtMs: message.turn?.startedAtMs, completedAtMs: message.turn?.completedAtMs, isWorking: isTurnStillWorking, children: _jsx("div", { className: "flex min-w-0 flex-col gap-2", children: preAnswerWorkItems.map(renderContentItem) }) })), finalAnswerItems.map(renderContentItem)] }) }) }) }));
|
|
315
731
|
}
|
|
316
732
|
export const Message = memo(PureMessage);
|
|
317
733
|
export function ThinkingMessage() {
|
|
318
|
-
return (_jsx(motion.div, { "data-testid": "message-assistant-loading", className: "group/message w-full px-2", initial: { y: 5, opacity: 0 }, animate: { y: 0, opacity: 1, transition: { delay: 0.5 } }, "data-role": "assistant", children:
|
|
734
|
+
return (_jsx(motion.div, { "data-testid": "message-assistant-loading", className: "group/message w-full px-2", initial: { y: 5, opacity: 0 }, animate: { y: 0, opacity: 1, transition: { delay: 0.5 } }, "data-role": "assistant", children: _jsx("div", { className: "text-muted-foreground/75 px-2 text-[13px] leading-5", children: _jsx("span", { className: "animate-pulse select-none", children: "Thinking" }) }) }));
|
|
735
|
+
}
|
|
736
|
+
export function CompactingMessage() {
|
|
737
|
+
return (_jsx(motion.div, { "data-testid": "message-compacting", className: "w-full px-4 py-1", initial: { y: 3, opacity: 0 }, animate: { y: 0, opacity: 1 }, "data-role": "assistant", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "bg-border h-px min-w-8 flex-1" }), _jsx("span", { className: "moldable-chat-shimmer select-none whitespace-nowrap text-[13px] font-medium leading-5", children: "Compacting conversation..." }), _jsx("div", { className: "bg-border h-px min-w-8 flex-1" })] }) }));
|
|
319
738
|
}
|