@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.
Files changed (44) hide show
  1. package/dist/components/app-error-boundary.d.ts +26 -0
  2. package/dist/components/app-error-boundary.d.ts.map +1 -0
  3. package/dist/components/app-error-boundary.js +87 -0
  4. package/dist/components/chat/chat-input.d.ts +9 -1
  5. package/dist/components/chat/chat-input.d.ts.map +1 -1
  6. package/dist/components/chat/chat-input.js +13 -4
  7. package/dist/components/chat/chat-message.d.ts +6 -0
  8. package/dist/components/chat/chat-message.d.ts.map +1 -1
  9. package/dist/components/chat/chat-message.js +576 -157
  10. package/dist/components/chat/chat-messages.d.ts +2 -1
  11. package/dist/components/chat/chat-messages.d.ts.map +1 -1
  12. package/dist/components/chat/chat-messages.js +3 -3
  13. package/dist/components/chat/chat-panel.d.ts +25 -3
  14. package/dist/components/chat/chat-panel.d.ts.map +1 -1
  15. package/dist/components/chat/chat-panel.js +173 -49
  16. package/dist/components/chat/index.d.ts +3 -3
  17. package/dist/components/chat/index.d.ts.map +1 -1
  18. package/dist/components/chat/index.js +1 -1
  19. package/dist/components/chat/model-selector.d.ts.map +1 -1
  20. package/dist/components/chat/model-selector.js +1 -1
  21. package/dist/components/chat/tool-handlers.d.ts.map +1 -1
  22. package/dist/components/chat/tool-handlers.js +89 -1
  23. package/dist/components/markdown.d.ts.map +1 -1
  24. package/dist/components/markdown.js +18 -4
  25. package/dist/components/rich-media-player.d.ts +8 -0
  26. package/dist/components/rich-media-player.d.ts.map +1 -0
  27. package/dist/components/rich-media-player.js +99 -0
  28. package/dist/components/ui/command.d.ts +2 -1
  29. package/dist/components/ui/command.d.ts.map +1 -1
  30. package/dist/components/ui/command.js +2 -2
  31. package/dist/components/ui/resizable.d.ts +4 -1
  32. package/dist/components/ui/resizable.d.ts.map +1 -1
  33. package/dist/components/ui/resizable.js +1 -1
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -1
  37. package/dist/lib/commands.d.ts +8 -0
  38. package/dist/lib/commands.d.ts.map +1 -1
  39. package/dist/lib/commands.js +18 -0
  40. package/package.json +17 -14
  41. package/src/styles/index.css +25 -0
  42. package/dist/lib/commands.test.d.ts +0 -2
  43. package/dist/lib/commands.test.d.ts.map +0 -1
  44. 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 { Brain, ChevronDown, Image as ImageIcon, Loader2 } from 'lucide-react';
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 thinking group independently
23
- const [thinkingGroupStates, setThinkingGroupStates] = useState(new Map());
24
- // Process parts chronologically, grouping only reasoning into thinking groups
25
- // Inline tools (file ops, commands) are rendered directly
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
- currentThinkingGroup: null,
30
- thinkingGroupIndex: 0,
436
+ currentActivityGroup: null,
437
+ activityGroupIndex: 0,
31
438
  hasText: false,
32
439
  };
33
- // Helper to close current thinking group
34
- const closeThinkingGroup = () => {
35
- if (state.currentThinkingGroup) {
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: 'thinking',
38
- id: state.currentThinkingGroup.id,
39
- thinkingGroup: state.currentThinkingGroup,
461
+ type: 'activity',
462
+ id: state.currentActivityGroup.id,
463
+ activityGroup: state.currentActivityGroup,
40
464
  });
41
- state.currentThinkingGroup = null;
465
+ state.currentActivityGroup = null;
42
466
  }
43
467
  };
44
- // Helper to ensure thinking group exists
45
- const ensureThinkingGroup = () => {
46
- if (!state.currentThinkingGroup) {
47
- state.thinkingGroupIndex++;
48
- state.currentThinkingGroup = {
49
- id: `thinking-${message.id}-${state.thinkingGroupIndex}`,
50
- timeline: [],
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.currentThinkingGroup;
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
- // Check if this tool should be rendered inline
78
- if (toolHandler.inline) {
79
- // Close any open thinking group first
80
- closeThinkingGroup();
81
- // Check for approval states
82
- const isApprovalRequested = part.state === 'approval-requested';
83
- const isApprovalResponded = part.state === 'approval-responded' ||
84
- part.state === 'output-denied';
85
- const wasRejected = part.approval?.approved === false;
86
- const wasApproved = part.approval?.approved === true;
87
- // Render inline tool
88
- // Also treat approval-responded (when approved) as loading until output arrives
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
- else if (isApprovalResponded && wasRejected) {
111
- // Show rejection message
112
- toolContent = (_jsx(ToolApproval, { state: "output-denied", approved: false, children: _jsx(ToolApprovalHeader, { children: _jsx(ToolApprovalRejected, { children: "Command execution was rejected" }) }) }));
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: toolContent,
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
- // Non-inline tools go into thinking group
134
- const group = ensureThinkingGroup();
135
- // Show loading state for tools
136
- if (part.state === 'partial-call' ||
137
- part.state === 'call' ||
138
- part.state === 'pending') {
139
- const toolLabel = toolHandler.loadingLabel;
140
- group.title = toolLabel;
141
- group.isStreaming = true;
142
- group.timeline.push({
143
- marker: ThinkingTimelineMarker.Loading,
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
- // Show tool output
149
- if (part.state === 'result' || part.state === 'output-available') {
150
- group.timeline.push({
151
- marker: toolHandler.marker ?? ThinkingTimelineMarker.Default,
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 = ensureThinkingGroup();
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.timeline.push({
176
- marker: ThinkingTimelineMarker.Default,
177
- content: (_jsx("div", { className: "whitespace-pre-wrap px-2 py-1", children: body }, key)),
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 thinking group before text
183
- closeThinkingGroup();
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
- closeThinkingGroup();
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 thinking group
202
- closeThinkingGroup();
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 thinking group is streaming
215
- const lastThinkingGroup = contentItems
216
- .filter((item) => item.type === 'thinking')
217
- .map((item) => (item.type === 'thinking' ? item.thinkingGroup : null))
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 thinking group, also keep it open if we just finished streaming
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 reasoning groups based on streaming state
626
+ // Auto-open/close activity groups based on streaming state
226
627
  useEffect(() => {
227
- setThinkingGroupStates((prevStates) => {
628
+ setActivityGroupStates((prevStates) => {
228
629
  let hasChanges = false;
229
630
  const nextStates = new Map(prevStates);
230
631
  contentItems.forEach((item) => {
231
- if (item.type === 'thinking' && item.thinkingGroup) {
232
- const groupId = item.thinkingGroup.id;
233
- const group = item.thinkingGroup;
234
- const isLastGroup = lastThinkingGroup?.id === groupId;
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
- lastThinkingGroup,
666
+ lastActivityGroup,
266
667
  shouldKeepLastGroupOpen,
267
668
  ]);
268
- 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: _jsx("div", { className: "mt-1 flex w-full min-w-0 flex-col gap-2", children: contentItems.map((item) => {
269
- if (item.type === 'thinking' &&
270
- item.thinkingGroup &&
271
- message.role === 'assistant') {
272
- const group = item.thinkingGroup;
273
- const groupState = thinkingGroupStates.get(group.id);
274
- const isLastGroup = lastThinkingGroup?.id === group.id;
275
- const isGroupStreaming = group.isStreaming || (isLastGroup && isStreamingOverall);
276
- const shouldBeOpenByDefault = isGroupStreaming;
277
- const isOpen = groupState?.isOpen ?? shouldBeOpenByDefault;
278
- const titleForTrigger = hasFinalAssistantText && isLastGroup && !isGroupStreaming
279
- ? DEFAULT_ACTIONS_LABEL
280
- : group.title || DEFAULT_ACTIONS_LABEL;
281
- return (_jsxs(Collapsible, { open: isOpen, onOpenChange: (open) => {
282
- setThinkingGroupStates((prev) => {
283
- const next = new Map(prev);
284
- const current = next.get(group.id) ?? {
285
- isOpen: false,
286
- userOpened: false,
287
- };
288
- next.set(group.id, {
289
- ...current,
290
- isOpen: open,
291
- userOpened: open ? true : current.userOpened,
292
- });
293
- return next;
294
- });
295
- }, className: "mb-2 w-full", "data-ai-reasoning": true, children: [_jsxs(CollapsibleTrigger, { className: cn('group flex max-w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-[11px] font-semibold uppercase tracking-wide transition-colors', 'text-muted-foreground hover:bg-accent hover:text-foreground min-w-0'), children: [_jsx(Brain, { className: cn('size-3.5 shrink-0 opacity-80', isGroupStreaming && 'animate-pulse') }), _jsx("span", { className: "flex-1 truncate", children: titleForTrigger }), _jsx(ChevronDown, { className: cn('size-3.5 shrink-0 transition-transform', 'group-data-[state=open]:rotate-180') })] }), _jsx(CollapsibleContent, { className: "text-muted-foreground min-w-0 overflow-hidden rounded-md px-[5px] py-2 font-mono text-xs leading-relaxed", children: _jsx(ThinkingTimeline, { items: group.timeline }) })] }, item.id));
296
- }
297
- if (item.type === 'inline-tool') {
298
- return (_jsx("div", { className: "w-full min-w-0", children: item.content }, item.id));
299
- }
300
- if (item.type === 'file' && item.filePart) {
301
- const isImage = item.filePart.mediaType.startsWith('image/');
302
- const label = item.filePart.filename || 'Attached image';
303
- 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));
304
- }
305
- if (item.type === 'text' && item.textPart) {
306
- if (message.role === 'user') {
307
- return (_jsx("div", { className: "bg-secondary text-secondary-foreground w-full rounded-2xl px-4 py-2.5 pb-4", children: _jsx("div", { className: "whitespace-pre-wrap break-words text-sm leading-relaxed", children: item.textPart.text }) }, item.id));
308
- }
309
- return (_jsx("div", { className: cn('flex min-w-0 flex-col gap-4', {
310
- 'w-full': message.role === 'assistant',
311
- }), children: _jsx(Markdown, { markdown: item.textPart.text, proseSize: "sm", className: cn('[&_.prose]:leading-relaxed [&_.prose]:text-current', '[&_.prose_pre]:bg-background/80 [&_.prose_code]:text-current', '[&_.prose_p:last-child]:mb-0 [&_.prose_p]:mb-2') }) }, item.id));
312
- }
313
- return null;
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: _jsxs("div", { className: "text-muted-foreground flex flex-row items-center gap-2 px-2", children: [_jsx(Loader2, { className: "size-4 shrink-0 animate-spin" }), _jsx("span", { className: "font-mono text-xs font-medium uppercase", children: "Thinking..." })] }) }));
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
  }