@moldable-ai/ui 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +75 -71
  2. package/dist/components/chat/chat-input.d.ts +16 -3
  3. package/dist/components/chat/chat-input.d.ts.map +1 -1
  4. package/dist/components/chat/chat-input.js +25 -8
  5. package/dist/components/chat/chat-message.d.ts +11 -0
  6. package/dist/components/chat/chat-message.d.ts.map +1 -1
  7. package/dist/components/chat/chat-message.js +503 -158
  8. package/dist/components/chat/chat-panel.d.ts +26 -2
  9. package/dist/components/chat/chat-panel.d.ts.map +1 -1
  10. package/dist/components/chat/chat-panel.js +69 -33
  11. package/dist/components/chat/conversation-history.d.ts +6 -1
  12. package/dist/components/chat/conversation-history.d.ts.map +1 -1
  13. package/dist/components/chat/conversation-history.js +6 -6
  14. package/dist/components/chat/index.d.ts +2 -2
  15. package/dist/components/chat/index.d.ts.map +1 -1
  16. package/dist/components/chat/index.js +1 -1
  17. package/dist/components/chat/model-selector.d.ts +3 -1
  18. package/dist/components/chat/model-selector.d.ts.map +1 -1
  19. package/dist/components/chat/model-selector.js +2 -2
  20. package/dist/components/chat/reasoning-effort-selector.d.ts +4 -1
  21. package/dist/components/chat/reasoning-effort-selector.d.ts.map +1 -1
  22. package/dist/components/chat/reasoning-effort-selector.js +6 -6
  23. package/dist/components/chat/tool-handlers.d.ts.map +1 -1
  24. package/dist/components/chat/tool-handlers.js +62 -6
  25. package/dist/components/markdown.js +2 -2
  26. package/dist/components/ui/command.d.ts +2 -1
  27. package/dist/components/ui/command.d.ts.map +1 -1
  28. package/dist/components/ui/command.js +2 -2
  29. package/dist/components/ui/scroll-area.d.ts +1 -1
  30. package/dist/components/ui/scroll-area.d.ts.map +1 -1
  31. package/dist/components/ui/scroll-area.js +5 -3
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/lib/commands.d.ts +57 -0
  36. package/dist/lib/commands.d.ts.map +1 -1
  37. package/dist/lib/commands.js +74 -0
  38. package/package.json +2 -2
  39. package/src/styles/index.css +1 -1
@@ -1,58 +1,419 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Brain, ChevronDown, 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 isToolLoading(part, progress) {
106
+ const isApprovalResponded = part.state === 'approval-responded' || part.state === 'output-denied';
107
+ const wasApproved = part.approval?.approved === true;
108
+ return (part.state === 'partial-call' ||
109
+ part.state === 'call' ||
110
+ part.state === 'pending' ||
111
+ progress?.status === 'running' ||
112
+ (isApprovalResponded && wasApproved && part.output === undefined));
113
+ }
114
+ function toolHasError(part) {
115
+ const output = asRecord(part.output);
116
+ if (!output) {
117
+ return false;
118
+ }
119
+ return (output.success === false ||
120
+ output.ok === false ||
121
+ (typeof output.exitCode === 'number' && output.exitCode !== 0) ||
122
+ Boolean(output.error));
123
+ }
124
+ function plural(count, singular, pluralForm = `${singular}s`) {
125
+ return `${count} ${count === 1 ? singular : pluralForm}`;
126
+ }
127
+ function sentenceCase(text) {
128
+ return text.length > 0 ? text.charAt(0).toUpperCase() + text.slice(1) : text;
129
+ }
130
+ function formatActivitySummary(counts, fallback, isStreaming) {
131
+ const segments = [];
132
+ const completedCommands = Math.max(0, counts.commandCount - counts.runningCommandCount);
133
+ const explorationDetails = [
134
+ counts.exploredFileCount > 0
135
+ ? plural(counts.exploredFileCount, 'file')
136
+ : null,
137
+ counts.searchCount > 0
138
+ ? plural(counts.searchCount, 'search', 'searches')
139
+ : null,
140
+ counts.listCount > 0
141
+ ? plural(counts.listCount, 'directory', 'directories')
142
+ : null,
143
+ ].filter(Boolean);
144
+ if (counts.createdFileCount > 0) {
145
+ segments.push(`created ${plural(counts.createdFileCount, 'file')}`);
146
+ }
147
+ if (counts.editedFileCount > 0) {
148
+ segments.push(`edited ${plural(counts.editedFileCount, 'file')}`);
149
+ }
150
+ if (counts.deletedFileCount > 0) {
151
+ segments.push(`deleted ${plural(counts.deletedFileCount, 'file')}`);
152
+ }
153
+ if (explorationDetails.length > 0) {
154
+ segments.push(`explored ${explorationDetails.join(', ')}`);
155
+ }
156
+ if (completedCommands > 0) {
157
+ segments.push(`ran ${plural(completedCommands, 'command')}`);
158
+ }
159
+ if (counts.runningCommandCount > 0) {
160
+ segments.push(`running ${plural(counts.runningCommandCount, 'command')}`);
161
+ }
162
+ if (counts.webSearchCount > 0) {
163
+ segments.push(`ran ${plural(counts.webSearchCount, 'web search', 'web searches')}`);
164
+ }
165
+ if (counts.apiCallCount > 0) {
166
+ segments.push(`called ${plural(counts.apiCallCount, 'app API', 'app APIs')}`);
167
+ }
168
+ if (counts.skillActionCount > 0) {
169
+ segments.push(`updated ${plural(counts.skillActionCount, 'skill setting')}`);
170
+ }
171
+ if (counts.appActionCount > 0) {
172
+ segments.push(`updated ${plural(counts.appActionCount, 'app')}`);
173
+ }
174
+ if (counts.toolCallCount > 0) {
175
+ segments.push(`used ${plural(counts.toolCallCount, 'tool')}`);
176
+ }
177
+ if (segments.length === 0) {
178
+ return isStreaming ? fallback : fallback || DEFAULT_ACTIONS_LABEL;
179
+ }
180
+ return sentenceCase(segments.join(', '));
181
+ }
182
+ function ActivityCode({ children, title, }) {
183
+ return (_jsx("code", { className: "text-foreground/80 inline max-w-full truncate align-baseline font-mono text-[0.95em]", title: title, children: children }));
184
+ }
185
+ function toolActivityText({ verb, path, detail, fallback, }) {
186
+ const label = path ?? detail;
187
+ return (_jsxs(_Fragment, { children: [_jsx("span", { children: verb }), label ? (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: path, children: label })] })) : fallback ? (_jsxs(_Fragment, { children: [" ", fallback] })) : null] }));
188
+ }
189
+ function summarizeToolActivity(part, toolHandler, progress, fallbackId) {
190
+ const counts = emptyActivityCounts();
191
+ const running = isToolLoading(part, progress);
192
+ const hasError = toolHasError(part);
193
+ const path = getToolPath(part);
194
+ const pathLabel = getPathLabel(path);
195
+ const query = getToolQuery(part);
196
+ const command = getToolCommand(part, progress);
197
+ const target = getToolTarget(part);
198
+ const output = asRecord(part.output);
199
+ const args = asRecord(part.args);
200
+ const entryId = part.toolCallId ?? fallbackId ?? `${part.toolName}-${part.state}`;
201
+ const loadingLabel = toolHandler.loadingLabel.replace(/\.\.\.$/, '');
202
+ let content = running ? loadingLabel : `Used ${part.toolName}`;
203
+ switch (part.toolName) {
204
+ case 'readFile':
205
+ case 'fileExists':
206
+ incrementActivityCount(counts, 'exploredFileCount');
207
+ content = toolActivityText({
208
+ verb: running
209
+ ? 'Reading'
210
+ : part.toolName === 'fileExists'
211
+ ? 'Checked'
212
+ : 'Read',
213
+ path: pathLabel,
214
+ fallback: 'file',
215
+ });
216
+ break;
217
+ case 'readToolOutput':
218
+ incrementActivityCount(counts, 'exploredFileCount');
219
+ content = running ? 'Reading tool output' : 'Read tool output';
220
+ break;
221
+ case 'listDirectory': {
222
+ incrementActivityCount(counts, 'listCount');
223
+ const itemCount = getCount(output, ['entries', 'items']);
224
+ 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'), ")"] }))] }));
225
+ break;
226
+ }
227
+ case 'grep':
228
+ case 'globFileSearch': {
229
+ incrementActivityCount(counts, 'searchCount');
230
+ const resultCount = getCount(output, ['matches', 'files', 'results']);
231
+ 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'), ")"] }))] }));
232
+ break;
233
+ }
234
+ case 'webSearch':
235
+ incrementActivityCount(counts, 'webSearchCount');
236
+ content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? 'Searching web' : 'Searched web' }), query && (_jsxs(_Fragment, { children: [' ', "for ", _jsx(ActivityCode, { title: query, children: query })] }))] }));
237
+ break;
238
+ case 'executeBashCommand':
239
+ case 'runCommand':
240
+ incrementActivityCount(counts, 'commandCount');
241
+ if (running) {
242
+ incrementActivityCount(counts, 'runningCommandCount');
243
+ }
244
+ content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? 'Running' : 'Ran' }), ' ', _jsx(ActivityCode, { title: command, children: command ?? 'command' })] }));
245
+ break;
246
+ case 'writeFile': {
247
+ const created = output?.created === true || args?.create === true;
248
+ incrementActivityCount(counts, created ? 'createdFileCount' : 'editedFileCount');
249
+ content = toolActivityText({
250
+ verb: running ? 'Writing' : created ? 'Created' : 'Wrote',
251
+ path: pathLabel,
252
+ fallback: 'file',
253
+ });
254
+ break;
255
+ }
256
+ case 'editFile':
257
+ incrementActivityCount(counts, 'editedFileCount');
258
+ content = toolActivityText({
259
+ verb: running ? 'Editing' : 'Edited',
260
+ path: pathLabel,
261
+ fallback: 'file',
262
+ });
263
+ break;
264
+ case 'deleteFile':
265
+ incrementActivityCount(counts, 'deletedFileCount');
266
+ content = toolActivityText({
267
+ verb: running ? 'Deleting' : 'Deleted',
268
+ path: pathLabel,
269
+ fallback: 'file',
270
+ });
271
+ break;
272
+ case 'listMoldableAppApi':
273
+ case 'callMoldableAppApi':
274
+ incrementActivityCount(counts, 'apiCallCount');
275
+ content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running
276
+ ? part.toolName === 'listMoldableAppApi'
277
+ ? 'Listing app APIs'
278
+ : 'Calling app API'
279
+ : part.toolName === 'listMoldableAppApi'
280
+ ? 'Listed app APIs'
281
+ : 'Called app API' }), target && (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: target, children: target })] }))] }));
282
+ break;
283
+ case 'listSkillRepos':
284
+ case 'listAvailableSkills':
285
+ case 'syncSkills':
286
+ case 'addSkillRepo':
287
+ case 'updateSkillSelection':
288
+ case 'initSkillsConfig':
289
+ incrementActivityCount(counts, 'skillActionCount');
290
+ content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? loadingLabel : 'Updated skills' }), target && (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: target, children: target })] }))] }));
291
+ break;
292
+ case 'scaffoldApp':
293
+ case 'getAppInfo':
294
+ case 'unregisterApp':
295
+ case 'deleteAppData':
296
+ case 'deleteApp':
297
+ incrementActivityCount(counts, 'appActionCount');
298
+ content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? loadingLabel : 'Updated app' }), target && (_jsxs(_Fragment, { children: [' ', _jsx(ActivityCode, { title: target, children: target })] }))] }));
299
+ break;
300
+ default:
301
+ incrementActivityCount(counts, 'toolCallCount');
302
+ content = (_jsxs(_Fragment, { children: [_jsx("span", { children: running ? loadingLabel : 'Used tool' }), ' ', _jsx(ActivityCode, { children: part.toolName })] }));
303
+ break;
304
+ }
305
+ if (hasError) {
306
+ content = (_jsxs(_Fragment, { children: [content, _jsx("span", { children: " failed" })] }));
307
+ }
308
+ return {
309
+ entry: {
310
+ id: entryId,
311
+ content,
312
+ isRunning: running,
313
+ isError: hasError,
314
+ },
315
+ counts,
316
+ };
317
+ }
318
+ function formatWorkDuration(ms) {
319
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
320
+ const hours = Math.floor(totalSeconds / 3600);
321
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
322
+ const seconds = totalSeconds % 60;
323
+ if (hours > 0) {
324
+ return `${hours}h ${minutes}m`;
325
+ }
326
+ if (minutes > 0) {
327
+ return `${minutes}m ${seconds}s`;
328
+ }
329
+ return `${seconds}s`;
330
+ }
331
+ function useNow(enabled) {
332
+ const [now, setNow] = useState(() => Date.now());
333
+ useEffect(() => {
334
+ if (!enabled) {
335
+ return;
336
+ }
337
+ const interval = window.setInterval(() => {
338
+ setNow(Date.now());
339
+ }, 1000);
340
+ return () => window.clearInterval(interval);
341
+ }, [enabled]);
342
+ return now;
343
+ }
344
+ function WorkSummary({ messageId, startedAtMs, completedAtMs, isWorking, children, }) {
345
+ const [isOpen, setIsOpen] = useState(false);
346
+ const now = useNow(isWorking && Boolean(startedAtMs));
347
+ const endAtMs = completedAtMs ?? (isWorking ? now : undefined);
348
+ const elapsedMs = typeof startedAtMs === 'number' && typeof endAtMs === 'number'
349
+ ? Math.max(0, endAtMs - startedAtMs)
350
+ : null;
351
+ const durationLabel = elapsedMs !== null && (elapsedMs >= 1000 || !isWorking)
352
+ ? formatWorkDuration(elapsedMs)
353
+ : null;
354
+ const summaryLabel = isWorking
355
+ ? durationLabel
356
+ ? `Working for ${durationLabel}`
357
+ : 'Working'
358
+ : durationLabel
359
+ ? `Worked for ${durationLabel}`
360
+ : 'Worked';
361
+ 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 }) })] }));
362
+ }
17
363
  function PureMessage({ message, isLast = false, isStreaming = false, }) {
18
364
  // Get tool progress for streaming stdout/stderr
19
365
  const toolProgress = useToolProgress();
20
366
  // Get approval response handler
21
367
  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
368
+ // Track open state for each background activity group independently.
369
+ const [activityGroupStates, setActivityGroupStates] = useState(new Map());
370
+ // Process parts chronologically, grouping ordinary tool work and reasoning as
371
+ // low-emphasis background activity. Approval prompts remain prominent because
372
+ // they need direct user action.
26
373
  const { contentItems, hasFinalAssistantText } = useMemo(() => {
27
374
  const items = [];
28
375
  const state = {
29
- currentThinkingGroup: null,
30
- thinkingGroupIndex: 0,
376
+ currentActivityGroup: null,
377
+ activityGroupIndex: 0,
31
378
  hasText: false,
32
379
  };
33
- // Helper to close current thinking group
34
- const closeThinkingGroup = () => {
35
- if (state.currentThinkingGroup) {
380
+ const updateActivityTitle = (group) => {
381
+ group.title = formatActivitySummary(group.counts, group.title, group.isStreaming);
382
+ };
383
+ const addActivity = (group, entry, counts) => {
384
+ group.entries.push(entry);
385
+ group.isStreaming = group.isStreaming || Boolean(entry.isRunning);
386
+ if (counts) {
387
+ for (const key of Object.keys(counts)) {
388
+ group.counts[key] += counts[key];
389
+ }
390
+ }
391
+ updateActivityTitle(group);
392
+ };
393
+ // Helper to close current activity group
394
+ const closeActivityGroup = () => {
395
+ if (state.currentActivityGroup) {
36
396
  items.push({
37
- type: 'thinking',
38
- id: state.currentThinkingGroup.id,
39
- thinkingGroup: state.currentThinkingGroup,
397
+ type: 'activity',
398
+ id: state.currentActivityGroup.id,
399
+ activityGroup: state.currentActivityGroup,
40
400
  });
41
- state.currentThinkingGroup = null;
401
+ state.currentActivityGroup = null;
42
402
  }
43
403
  };
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: [],
404
+ // Helper to ensure activity group exists
405
+ const ensureActivityGroup = () => {
406
+ if (!state.currentActivityGroup) {
407
+ state.activityGroupIndex++;
408
+ state.currentActivityGroup = {
409
+ id: `activity-${message.id}-${state.activityGroupIndex}`,
410
+ entries: [],
411
+ counts: emptyActivityCounts(),
51
412
  title: DEFAULT_ACTIONS_LABEL,
52
413
  isStreaming: false,
53
414
  };
54
415
  }
55
- return state.currentThinkingGroup;
416
+ return state.currentActivityGroup;
56
417
  };
57
418
  // If no parts, use content as text
58
419
  const parts = message.parts ?? [];
@@ -74,85 +435,40 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
74
435
  // Handle tool calls
75
436
  if (part.type === 'tool-invocation') {
76
437
  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({
105
- approvalId: part.approval.id,
106
- toolCallId: part.toolCallId || key,
107
- 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
- }
438
+ const isApprovalRequested = part.state === 'approval-requested';
439
+ const isApprovalResponded = part.state === 'approval-responded' || part.state === 'output-denied';
440
+ const wasRejected = part.approval?.approved === false;
441
+ if (isApprovalRequested &&
442
+ toolHandler.renderApproval &&
443
+ part.approval?.id &&
444
+ onApprovalResponse) {
445
+ closeActivityGroup();
126
446
  items.push({
127
447
  type: 'inline-tool',
128
448
  id: key,
129
- content: toolContent,
130
- });
131
- continue;
132
- }
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`)),
449
+ content: toolHandler.renderApproval({
450
+ approvalId: part.approval.id,
451
+ toolCallId: part.toolCallId || key,
452
+ args: part.args,
453
+ }, onApprovalResponse),
145
454
  });
146
455
  continue;
147
456
  }
148
- // Show tool output
149
- if (part.state === 'result' || part.state === 'output-available') {
150
- group.timeline.push({
151
- marker: toolHandler.marker ?? ThinkingTimelineMarker.Default,
152
- content: toolHandler.renderOutput(part.output, part.toolCallId || key),
457
+ if (isApprovalResponded && wasRejected) {
458
+ closeActivityGroup();
459
+ items.push({
460
+ type: 'inline-tool',
461
+ id: key,
462
+ content: (_jsx(ToolApproval, { state: "output-denied", approved: false, children: _jsx(ToolApprovalHeader, { children: _jsx(ToolApprovalRejected, { children: "Command execution was rejected" }) }) })),
153
463
  });
154
464
  continue;
155
465
  }
466
+ const group = ensureActivityGroup();
467
+ const progress = part.toolCallId
468
+ ? toolProgress[part.toolCallId]
469
+ : undefined;
470
+ const activity = summarizeToolActivity(part, toolHandler, progress, key);
471
+ addActivity(group, activity.entry, activity.counts);
156
472
  continue;
157
473
  }
158
474
  if (part.type === 'reasoning') {
@@ -161,7 +477,7 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
161
477
  if (cleaned.length === 0) {
162
478
  continue;
163
479
  }
164
- const group = ensureThinkingGroup();
480
+ const group = ensureActivityGroup();
165
481
  // Extract title from markdown **title** or use first line
166
482
  const markdownTitleMatch = cleaned.match(/\*\*([^*]+)\*\*/);
167
483
  const candidateTitle = markdownTitleMatch?.[1];
@@ -172,25 +488,34 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
172
488
  const body = candidateTitle
173
489
  ? trimmed.replace(/\*\*[^*]+\*\*\s*/, '')
174
490
  : trimmed;
175
- group.timeline.push({
176
- marker: ThinkingTimelineMarker.Default,
177
- content: (_jsx("div", { className: "whitespace-pre-wrap px-2 py-1", children: body }, key)),
491
+ addActivity(group, {
492
+ id: key,
493
+ content: _jsx("span", { className: "whitespace-pre-wrap", children: body }),
178
494
  });
179
495
  continue;
180
496
  }
181
497
  if (part.type === 'text') {
182
- // Close current thinking group before text
183
- closeThinkingGroup();
498
+ // Close current background activity group before text.
499
+ closeActivityGroup();
184
500
  state.hasText = true;
185
501
  items.push({
186
502
  type: 'text',
187
503
  id: key,
188
504
  textPart: part,
189
505
  });
506
+ continue;
507
+ }
508
+ if (part.type === 'file') {
509
+ closeActivityGroup();
510
+ items.push({
511
+ type: 'file',
512
+ id: key,
513
+ filePart: part,
514
+ });
190
515
  }
191
516
  }
192
- // Close any remaining thinking group
193
- closeThinkingGroup();
517
+ // Close any remaining activity group
518
+ closeActivityGroup();
194
519
  return {
195
520
  contentItems: items,
196
521
  hasFinalAssistantText: state.hasText,
@@ -202,27 +527,27 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
202
527
  toolProgress,
203
528
  onApprovalResponse,
204
529
  ]);
205
- // Determine if last thinking group is streaming
206
- const lastThinkingGroup = contentItems
207
- .filter((item) => item.type === 'thinking')
208
- .map((item) => (item.type === 'thinking' ? item.thinkingGroup : null))
530
+ // Determine if last activity group is streaming
531
+ const lastActivityGroup = contentItems
532
+ .filter((item) => item.type === 'activity')
533
+ .map((item) => (item.type === 'activity' ? item.activityGroup : null))
209
534
  .filter((g) => g !== null)
210
535
  .pop();
211
536
  // Check if we're still streaming overall and waiting for text
212
537
  const isStreamingOverall = isStreaming && isLast && !hasFinalAssistantText;
213
- // For the last thinking group, also keep it open if we just finished streaming
538
+ // For the last activity group, also keep it open if we just finished streaming
214
539
  // but text hasn't appeared yet (grace period for AI to start responding)
215
540
  const shouldKeepLastGroupOpen = isLast && !hasFinalAssistantText && message.role === 'assistant';
216
- // Auto-open/close reasoning groups based on streaming state
541
+ // Auto-open/close activity groups based on streaming state
217
542
  useEffect(() => {
218
- setThinkingGroupStates((prevStates) => {
543
+ setActivityGroupStates((prevStates) => {
219
544
  let hasChanges = false;
220
545
  const nextStates = new Map(prevStates);
221
546
  contentItems.forEach((item) => {
222
- if (item.type === 'thinking' && item.thinkingGroup) {
223
- const groupId = item.thinkingGroup.id;
224
- const group = item.thinkingGroup;
225
- const isLastGroup = lastThinkingGroup?.id === groupId;
547
+ if (item.type === 'activity' && item.activityGroup) {
548
+ const groupId = item.activityGroup.id;
549
+ const group = item.activityGroup;
550
+ const isLastGroup = lastActivityGroup?.id === groupId;
226
551
  const currentState = nextStates.get(groupId);
227
552
  const shouldBeOpen = group.isStreaming ||
228
553
  (isLastGroup && isStreamingOverall) ||
@@ -253,53 +578,73 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
253
578
  }, [
254
579
  contentItems,
255
580
  isStreamingOverall,
256
- lastThinkingGroup,
581
+ lastActivityGroup,
257
582
  shouldKeepLastGroupOpen,
258
583
  ]);
259
- 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) => {
260
- if (item.type === 'thinking' &&
261
- item.thinkingGroup &&
262
- message.role === 'assistant') {
263
- const group = item.thinkingGroup;
264
- const groupState = thinkingGroupStates.get(group.id);
265
- const isLastGroup = lastThinkingGroup?.id === group.id;
266
- const isGroupStreaming = group.isStreaming || (isLastGroup && isStreamingOverall);
267
- const shouldBeOpenByDefault = isGroupStreaming;
268
- const isOpen = groupState?.isOpen ?? shouldBeOpenByDefault;
269
- const titleForTrigger = hasFinalAssistantText && isLastGroup && !isGroupStreaming
270
- ? DEFAULT_ACTIONS_LABEL
271
- : group.title || DEFAULT_ACTIONS_LABEL;
272
- return (_jsxs(Collapsible, { open: isOpen, onOpenChange: (open) => {
273
- setThinkingGroupStates((prev) => {
274
- const next = new Map(prev);
275
- const current = next.get(group.id) ?? {
276
- isOpen: false,
277
- userOpened: false,
278
- };
279
- next.set(group.id, {
280
- ...current,
281
- isOpen: open,
282
- userOpened: open ? true : current.userOpened,
283
- });
284
- return next;
285
- });
286
- }, 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));
287
- }
288
- if (item.type === 'inline-tool') {
289
- return (_jsx("div", { className: "w-full min-w-0", children: item.content }, item.id));
290
- }
291
- if (item.type === 'text' && item.textPart) {
292
- if (message.role === 'user') {
293
- 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));
294
- }
295
- return (_jsx("div", { className: cn('flex min-w-0 flex-col gap-4', {
296
- 'w-full': message.role === 'assistant',
297
- }), 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));
298
- }
299
- return null;
300
- }) }) }) }) }));
584
+ const renderContentItem = (item) => {
585
+ if (item.type === 'activity' &&
586
+ item.activityGroup &&
587
+ message.role === 'assistant') {
588
+ const group = item.activityGroup;
589
+ const groupState = activityGroupStates.get(group.id);
590
+ const isLastGroup = lastActivityGroup?.id === group.id;
591
+ const isGroupStreaming = group.isStreaming || (isLastGroup && isStreamingOverall);
592
+ const shouldBeOpenByDefault = isGroupStreaming;
593
+ const isOpen = groupState?.isOpen ?? shouldBeOpenByDefault;
594
+ const titleForTrigger = group.title || DEFAULT_ACTIONS_LABEL;
595
+ return (_jsxs(Collapsible, { open: isOpen, onOpenChange: (open) => {
596
+ setActivityGroupStates((prev) => {
597
+ const next = new Map(prev);
598
+ const current = next.get(group.id) ?? {
599
+ isOpen: false,
600
+ userOpened: false,
601
+ };
602
+ next.set(group.id, {
603
+ ...current,
604
+ isOpen: open,
605
+ userOpened: open ? true : current.userOpened,
606
+ });
607
+ return next;
608
+ });
609
+ }, 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));
610
+ }
611
+ if (item.type === 'inline-tool') {
612
+ return (_jsx("div", { className: "w-full min-w-0", children: item.content }, item.id));
613
+ }
614
+ if (item.type === 'file' && item.filePart) {
615
+ const isImage = item.filePart.mediaType.startsWith('image/');
616
+ const label = item.filePart.filename || 'Attached image';
617
+ 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));
618
+ }
619
+ if (item.type === 'text' && item.textPart) {
620
+ if (message.role === 'user') {
621
+ 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));
622
+ }
623
+ return (_jsx("div", { className: cn('flex min-w-0 flex-col gap-4', {
624
+ 'w-full': message.role === 'assistant',
625
+ }), 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));
626
+ }
627
+ return null;
628
+ };
629
+ const finalTextItemIndex = message.role === 'assistant'
630
+ ? contentItems.reduce((lastIndex, item, index) => item.type === 'text' ? index : lastIndex, -1)
631
+ : -1;
632
+ const shouldCollapsePreAnswerWork = message.role === 'assistant' &&
633
+ hasFinalAssistantText &&
634
+ finalTextItemIndex > 0;
635
+ const preAnswerWorkItems = shouldCollapsePreAnswerWork
636
+ ? contentItems.slice(0, finalTextItemIndex)
637
+ : [];
638
+ const finalAnswerItems = shouldCollapsePreAnswerWork
639
+ ? contentItems.slice(finalTextItemIndex)
640
+ : contentItems;
641
+ const isTurnStillWorking = isLast &&
642
+ isStreaming &&
643
+ message.role === 'assistant' &&
644
+ !message.turn?.completedAtMs;
645
+ 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)] }) }) }) }));
301
646
  }
302
647
  export const Message = memo(PureMessage);
303
648
  export function ThinkingMessage() {
304
- 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..." })] }) }));
649
+ 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" }) }) }));
305
650
  }