paseo-plugin-chat-view 0.0.0-stage → 0.1.0

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 (136) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.compact-agent-activity +21 -0
  3. package/LICENSE.mermaid +21 -0
  4. package/README.md +428 -2
  5. package/README_zh.md +429 -0
  6. package/client/activity/Activity.test.ts +220 -0
  7. package/client/activity/Activity.tsx +784 -0
  8. package/client/activity/detail/detail.test.ts +102 -0
  9. package/client/activity/detail/dispatch.tsx +94 -0
  10. package/client/activity/detail/file.tsx +93 -0
  11. package/client/activity/detail/index.tsx +39 -0
  12. package/client/activity/detail/parts.tsx +221 -0
  13. package/client/activity/detail/search.tsx +59 -0
  14. package/client/activity/detail/terminal.tsx +53 -0
  15. package/client/activity/detail/texts.tsx +42 -0
  16. package/client/activity/disclosure.ts +49 -0
  17. package/client/activity/dom.test.ts +52 -0
  18. package/client/activity/dom.ts +138 -0
  19. package/client/activity/group-store.test.ts +126 -0
  20. package/client/activity/group-store.ts +265 -0
  21. package/client/activity/highlight.test.ts +116 -0
  22. package/client/activity/highlight.ts +418 -0
  23. package/client/activity/history.test.ts +602 -0
  24. package/client/activity/history.ts +239 -0
  25. package/client/activity/index.ts +74 -0
  26. package/client/activity/latest-marker.test.ts +41 -0
  27. package/client/activity/latest-marker.ts +70 -0
  28. package/client/activity/styles.ts +535 -0
  29. package/client/activity/tools/child-agent.tsx +308 -0
  30. package/client/activity/tools/exa.tsx +99 -0
  31. package/client/activity/tools/github.tsx +501 -0
  32. package/client/activity/tools/list.tsx +102 -0
  33. package/client/activity/tools/lists.test.ts +193 -0
  34. package/client/activity/tools/paseo/agent.tsx +384 -0
  35. package/client/activity/tools/paseo/browser.tsx +322 -0
  36. package/client/activity/tools/paseo/index.tsx +79 -0
  37. package/client/activity/tools/paseo/parts.tsx +133 -0
  38. package/client/activity/tools/paseo/provider.tsx +220 -0
  39. package/client/activity/tools/paseo/schedule.tsx +246 -0
  40. package/client/activity/tools/paseo/speak.tsx +20 -0
  41. package/client/activity/tools/paseo/terminal.tsx +214 -0
  42. package/client/activity/tools/paseo/workspace.tsx +162 -0
  43. package/client/activity/tools/shared.tsx +431 -0
  44. package/client/activity/transform.test.ts +28 -0
  45. package/client/activity/transform.ts +43 -0
  46. package/client/agent-pills.ts +56 -0
  47. package/client/dom.ts +75 -0
  48. package/client/enhance/controller.ts +111 -0
  49. package/client/enhance/dom.test.ts +28 -0
  50. package/client/enhance/dom.ts +101 -0
  51. package/client/enhance/index.ts +95 -0
  52. package/client/enhance/pill.tsx +19 -0
  53. package/client/enhance/state.ts +58 -0
  54. package/client/feature.ts +22 -0
  55. package/client/features.test.ts +149 -0
  56. package/client/features.ts +83 -0
  57. package/client/locale.ts +45 -0
  58. package/client/mermaid/diagram.tsx +438 -0
  59. package/client/mermaid/index.ts +27 -0
  60. package/client/mermaid/item.tsx +28 -0
  61. package/client/mermaid/markdown.tsx +217 -0
  62. package/client/mermaid/registration.ts +32 -0
  63. package/client/mermaid/toolbar.tsx +175 -0
  64. package/client/mermaid/viewer.tsx +314 -0
  65. package/client/readonly/dom.test.ts +66 -0
  66. package/client/readonly/dom.ts +32 -0
  67. package/client/readonly/index.ts +27 -0
  68. package/client/registration.test.ts +243 -0
  69. package/client/settings.tsx +438 -0
  70. package/client/sticky/StickyBar.tsx +144 -0
  71. package/client/sticky/dom.test.ts +230 -0
  72. package/client/sticky/dom.ts +517 -0
  73. package/client/sticky/index.ts +38 -0
  74. package/client/sticky/pill.tsx +70 -0
  75. package/client/timeline-pager.test.ts +121 -0
  76. package/client/timeline-pager.ts +123 -0
  77. package/index.client.tsx +44 -0
  78. package/index.server.ts +12 -0
  79. package/package.json +42 -4
  80. package/paseo-plugin.json +6 -0
  81. package/server/enhance/index.ts +63 -0
  82. package/server/enhance/manifest.ts +176 -0
  83. package/server/enhance/probe.test.ts +215 -0
  84. package/server/enhance/probe.ts +118 -0
  85. package/server/enhance/provider.ts +149 -0
  86. package/shared/activity/child-agent.test.ts +27 -0
  87. package/shared/activity/child-agent.ts +26 -0
  88. package/shared/activity/diff.test.ts +74 -0
  89. package/shared/activity/diff.ts +207 -0
  90. package/shared/activity/exa.test.ts +66 -0
  91. package/shared/activity/exa.ts +183 -0
  92. package/shared/activity/file-kind.test.ts +13 -0
  93. package/shared/activity/file-kind.ts +112 -0
  94. package/shared/activity/github.test.ts +56 -0
  95. package/shared/activity/github.ts +174 -0
  96. package/shared/activity/palette.test.ts +26 -0
  97. package/shared/activity/palette.ts +106 -0
  98. package/shared/activity/parse.test.ts +39 -0
  99. package/shared/activity/parse.ts +115 -0
  100. package/shared/activity/paseo-tools.test.ts +163 -0
  101. package/shared/activity/paseo-tools.ts +497 -0
  102. package/shared/activity/text.test.ts +85 -0
  103. package/shared/activity/text.ts +202 -0
  104. package/shared/activity/timeline.test.ts +26 -0
  105. package/shared/activity/timeline.ts +108 -0
  106. package/shared/activity/tool-presentation.test.ts +154 -0
  107. package/shared/activity/tool-presentation.ts +352 -0
  108. package/shared/enhance/contract.ts +105 -0
  109. package/shared/enhance/decide.ts +97 -0
  110. package/shared/enhance/enhance.test.ts +188 -0
  111. package/shared/enhance/failure.ts +40 -0
  112. package/shared/enhance/index.ts +8 -0
  113. package/shared/enhance/normalize.ts +16 -0
  114. package/shared/enhance/parse.ts +79 -0
  115. package/shared/enhance/policy.ts +48 -0
  116. package/shared/enhance/prompt.ts +134 -0
  117. package/shared/enhance/run.ts +84 -0
  118. package/shared/i18n.test.ts +81 -0
  119. package/shared/i18n.ts +300 -0
  120. package/shared/mermaid/flowchart.test.ts +169 -0
  121. package/shared/mermaid/flowchart.ts +271 -0
  122. package/shared/mermaid/layout.test.ts +305 -0
  123. package/shared/mermaid/layout.ts +496 -0
  124. package/shared/mermaid/parse.test.ts +110 -0
  125. package/shared/mermaid/parse.ts +193 -0
  126. package/shared/mermaid/segment.test.ts +55 -0
  127. package/shared/mermaid/segment.ts +95 -0
  128. package/shared/mermaid/sequence.test.ts +68 -0
  129. package/shared/mermaid/sequence.ts +130 -0
  130. package/shared/mermaid/transform.test.ts +52 -0
  131. package/shared/mermaid/transform.ts +45 -0
  132. package/shared/mermaid/zoom.test.ts +134 -0
  133. package/shared/mermaid/zoom.ts +114 -0
  134. package/shared/settings.ts +41 -0
  135. package/shared/sticky/history.test.ts +91 -0
  136. package/shared/sticky/history.ts +86 -0
@@ -0,0 +1,784 @@
1
+ import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
2
+ import { useSettings } from "@getpaseo/plugin/client";
3
+ import { Icon, ScrollView, useRevealedText } from "@getpaseo/plugin/client/react-native";
4
+ import type { ToolCallDetail } from "@getpaseo/protocol/agent-types";
5
+ import React, { useCallback, useMemo, useRef, useState, type ReactNode } from "react";
6
+
7
+ import {
8
+ Pressable,
9
+ Text,
10
+ View,
11
+ type NativeScrollEvent,
12
+ type ScrollView as NativeScrollView,
13
+ type NativeSyntheticEvent,
14
+ type TextStyle,
15
+ type ViewStyle,
16
+ } from "react-native";
17
+ import type { z } from "zod";
18
+
19
+ import { useActivityStyles, usePalette, type Theme } from "./styles";
20
+
21
+ import type { ActivityPalette } from "../../shared/activity/palette";
22
+
23
+ import { previewText } from "../../shared/activity/text";
24
+ import {
25
+ parseSubAgentActionLog,
26
+ resolveSubAgentActionPresentation,
27
+ } from "../../shared/activity/tool-presentation";
28
+
29
+ import { parseInlineMarkdown, parseReasoningMarkdown } from "../../shared/activity/parse";
30
+
31
+ import { activitySettings, DEFAULT_DISPLAY_MODE } from "../../shared/settings";
32
+ import {
33
+ getActivityExpansionState,
34
+ getReasoningExpansionState,
35
+ reasoningItemDataSchema,
36
+ toolCallItemDataSchema,
37
+ type ReasoningItemData,
38
+ type ToolCallItemData,
39
+ } from "../../shared/activity/timeline";
40
+ import {
41
+ createActivityGroup,
42
+ formatGroupSummary,
43
+ updateGroupStats,
44
+ type GroupItemData,
45
+ type TurnGroup,
46
+ } from "./group-store";
47
+ import { useActivityGroup } from "./history";
48
+ import { createLatestMarker, useIsLatestMarker } from "./latest-marker";
49
+ import { DetailBody, DetailLabel, HighlightedCodeBlock, asToolCallDetail } from "./detail";
50
+ import { useActivityDisclosure } from "./disclosure";
51
+ import { scrollActivityIntoView } from "./dom";
52
+
53
+ const MAX_VISIBLE_SUBAGENT_ACTIONS = 5;
54
+
55
+ type ReasoningData = z.output<typeof reasoningItemDataSchema>;
56
+ type ToolCallData = z.output<typeof toolCallItemDataSchema>;
57
+
58
+ const latestReasoningMarker = createLatestMarker();
59
+ const latestToolCallMarker = createLatestMarker();
60
+
61
+ function statusIcon(status: ToolCallData["status"]): string {
62
+ switch (status) {
63
+ case "running":
64
+ return "LoaderCircle";
65
+ case "completed":
66
+ return "CircleCheck";
67
+ case "failed":
68
+ return "CircleX";
69
+ case "canceled":
70
+ return "CircleSlash2";
71
+ }
72
+ }
73
+
74
+ function SubAgentProgress({
75
+ detail,
76
+ styles,
77
+ }: {
78
+ detail: Extract<ToolCallDetail, { type: "sub_agent" }>;
79
+ styles: ReturnType<typeof useActivityStyles>;
80
+ }) {
81
+ const actions = useMemo(() => {
82
+ const explicitActions = [...(detail.actions ?? [])].sort(
83
+ (left, right) => left.index - right.index,
84
+ );
85
+ return explicitActions.length > 0 ? explicitActions : parseSubAgentActionLog(detail.log);
86
+ }, [detail.actions, detail.log]);
87
+ const hiddenActionCount = Math.max(0, actions.length - MAX_VISIBLE_SUBAGENT_ACTIONS);
88
+ const visibleActions = actions.slice(hiddenActionCount);
89
+ const thinking =
90
+ actions.length > 0 && !(detail.actions?.length ?? 0)
91
+ ? null
92
+ : detail.log?.replace(/\s+/g, " ").trim();
93
+
94
+ return (
95
+ <View style={styles.subAgentProgress}>
96
+ {thinking ? (
97
+ <View style={styles.subAgentLine}>
98
+ <View style={styles.subAgentLineIcon}>
99
+ <Icon name="Brain" color={styles.subAgentLineSummary.color} size={11} />
100
+ </View>
101
+ <Text numberOfLines={1} style={styles.subAgentLineLabel}>
102
+ Thinking
103
+ </Text>
104
+ <Text numberOfLines={1} style={styles.subAgentLineSummary}>
105
+ {thinking}
106
+ </Text>
107
+ </View>
108
+ ) : null}
109
+ {hiddenActionCount > 0 ? (
110
+ <Text style={styles.subAgentMore}>+{hiddenActionCount} more…</Text>
111
+ ) : null}
112
+ {visibleActions.map((action) => {
113
+ const presentation = resolveSubAgentActionPresentation(action.toolName, action.summary);
114
+ return (
115
+ <View key={`${action.index}-${action.toolName}`} style={styles.subAgentAction}>
116
+ <View style={styles.subAgentActionIcon}>
117
+ <Icon name={presentation.icon} color={styles.subAgentActionSummary.color} size={11} />
118
+ </View>
119
+ <Text numberOfLines={1} style={styles.subAgentActionLabel}>
120
+ {presentation.label}
121
+ </Text>
122
+ {presentation.summaryIcon && action.summary ? (
123
+ <Icon
124
+ name={presentation.summaryIcon}
125
+ color={styles.subAgentActionSummary.color}
126
+ size={11}
127
+ />
128
+ ) : null}
129
+ {action.summary ? (
130
+ <Text numberOfLines={1} style={styles.subAgentActionSummary}>
131
+ {action.summary}
132
+ </Text>
133
+ ) : null}
134
+ </View>
135
+ );
136
+ })}
137
+ </View>
138
+ );
139
+ }
140
+
141
+ function renderInlineReasoning(
142
+ text: string,
143
+ styles: ReturnType<typeof useActivityStyles>,
144
+ ): ReactNode[] {
145
+ return parseInlineMarkdown(text).map((part, index) => {
146
+ if (part.type === "text") return part.text;
147
+ const style =
148
+ part.type === "bold"
149
+ ? [styles.reasoningLine, { fontWeight: "700" as const }]
150
+ : part.type === "italic"
151
+ ? [styles.reasoningLine, { fontStyle: "italic" as const }]
152
+ : styles.reasoningInlineCode;
153
+ return (
154
+ <Text key={`inline-${index}`} style={style}>
155
+ {part.text}
156
+ </Text>
157
+ );
158
+ });
159
+ }
160
+
161
+ function InlineReasoning({
162
+ text,
163
+ styles,
164
+ }: {
165
+ text: string;
166
+ styles: ReturnType<typeof useActivityStyles>;
167
+ }) {
168
+ return <Text style={styles.reasoningLine}>{renderInlineReasoning(text, styles)}</Text>;
169
+ }
170
+
171
+ function ReasoningMarkdown({
172
+ text,
173
+ theme,
174
+ styles,
175
+ }: {
176
+ text: string;
177
+ theme: Theme;
178
+ styles: ReturnType<typeof useActivityStyles>;
179
+ }) {
180
+ const blocks = useMemo(() => parseReasoningMarkdown(text), [text]);
181
+ return (
182
+ <View style={styles.reasoningBody}>
183
+ {blocks.map((block, index) => {
184
+ const key = `${block.type}-${index}`;
185
+ switch (block.type) {
186
+ case "code":
187
+ return (
188
+ <HighlightedCodeBlock
189
+ key={key}
190
+ code={block.text}
191
+ language={block.language ?? "text"}
192
+ theme={theme}
193
+ styles={styles}
194
+ />
195
+ );
196
+ case "spacer":
197
+ return <View key={key} style={styles.reasoningSpacer} />;
198
+ case "heading":
199
+ return (
200
+ <Text key={key} selectable style={styles.reasoningHeading}>
201
+ {renderInlineReasoning(block.text, styles)}
202
+ </Text>
203
+ );
204
+ case "unordered":
205
+ return (
206
+ <View key={key} style={styles.reasoningBulletRow}>
207
+ <Text style={styles.reasoningBullet}>•</Text>
208
+ <InlineReasoning text={block.text} styles={styles} />
209
+ </View>
210
+ );
211
+ case "ordered":
212
+ return (
213
+ <View key={key} style={styles.reasoningBulletRow}>
214
+ <Text style={styles.reasoningBullet}>{block.marker}</Text>
215
+ <InlineReasoning text={block.text} styles={styles} />
216
+ </View>
217
+ );
218
+ case "quote":
219
+ return (
220
+ <Text key={key} selectable style={styles.reasoningQuote}>
221
+ {renderInlineReasoning(block.text, styles)}
222
+ </Text>
223
+ );
224
+ case "paragraph":
225
+ return <InlineReasoning key={key} text={block.text} styles={styles} />;
226
+ }
227
+ })}
228
+ </View>
229
+ );
230
+ }
231
+
232
+ function ReasoningText({
233
+ text,
234
+ phase,
235
+ theme,
236
+ styles,
237
+ }: {
238
+ text: string;
239
+ phase: ReasoningData["phase"];
240
+ theme: Theme;
241
+ styles: ReturnType<typeof useActivityStyles>;
242
+ }) {
243
+ const [showAll, setShowAll] = useState(false);
244
+ const preview = useMemo(() => previewText(text), [text]);
245
+ const targetText = showAll ? text : preview.text;
246
+ const revealedText = useRevealedText(targetText, phase);
247
+ const scrollRef = useRef<NativeScrollView | null>(null);
248
+ const isNearBottom = useRef(true);
249
+ const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
250
+ const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
251
+ isNearBottom.current = layoutMeasurement.height + contentOffset.y >= contentSize.height - 32;
252
+ }, []);
253
+ const handleContentSizeChange = useCallback(() => {
254
+ if (isNearBottom.current) scrollRef.current?.scrollToEnd({ animated: false });
255
+ }, []);
256
+
257
+ return (
258
+ <View>
259
+ <ScrollView
260
+ ref={scrollRef}
261
+ nestedScrollEnabled
262
+ onContentSizeChange={handleContentSizeChange}
263
+ onScroll={handleScroll}
264
+ scrollEventThrottle={16}
265
+ showsVerticalScrollIndicator
266
+ style={styles.detailsScroll}
267
+ >
268
+ <ReasoningMarkdown text={revealedText} theme={theme} styles={styles} />
269
+ </ScrollView>
270
+ {preview.truncated ? (
271
+ <Pressable
272
+ accessibilityRole="button"
273
+ onPress={() => setShowAll(!showAll)}
274
+ style={styles.showMoreButton}
275
+ >
276
+ <Text style={styles.showMoreText}>
277
+ {showAll
278
+ ? "Show less"
279
+ : `Show all (${preview.totalLines} lines, ${preview.totalChars.toLocaleString()} chars)`}
280
+ </Text>
281
+ </Pressable>
282
+ ) : null}
283
+ </View>
284
+ );
285
+ }
286
+
287
+ function ActivityHeader({
288
+ icon,
289
+ iconColor,
290
+ title,
291
+ summary,
292
+ status,
293
+ statusColor,
294
+ stats,
295
+ expanded,
296
+ onPress,
297
+ styles,
298
+ }: {
299
+ icon: string;
300
+ iconColor: string;
301
+ title: string;
302
+ summary?: string;
303
+ status?: ToolCallData["status"];
304
+ statusColor?: string;
305
+ stats?: ToolCallData["presentation"]["diffStats"];
306
+ expanded: boolean;
307
+ onPress: () => void;
308
+ styles: ReturnType<typeof useActivityStyles>;
309
+ }) {
310
+ return (
311
+ <Pressable
312
+ accessibilityLabel={`${expanded ? "Collapse" : "Expand"} ${title}`}
313
+ accessibilityRole="button"
314
+ onPress={onPress}
315
+ style={styles.headerButton}
316
+ >
317
+ <View style={styles.iconBadge}>
318
+ <Icon name={icon} color={iconColor} size={12} />
319
+ </View>
320
+ <Text numberOfLines={1} style={styles.title}>
321
+ {title}
322
+ </Text>
323
+ {summary ? (
324
+ <Text numberOfLines={1} style={styles.summary}>
325
+ {summary}
326
+ </Text>
327
+ ) : null}
328
+ {stats ? (
329
+ <View style={styles.stats}>
330
+ <Text style={styles.additions}>+{stats.additions}</Text>
331
+ <Text style={styles.deletions}>-{stats.deletions}</Text>
332
+ </View>
333
+ ) : null}
334
+ {status ? (
335
+ <View style={styles.status}>
336
+ <Icon name={statusIcon(status)} color={statusColor ?? styles.title.color} size={12} />
337
+ </View>
338
+ ) : null}
339
+ <Icon
340
+ name={expanded ? "ChevronDown" : "ChevronRight"}
341
+ color={styles.summary.color}
342
+ size={12}
343
+ />
344
+ </Pressable>
345
+ );
346
+ }
347
+
348
+ function GroupSubItem({
349
+ agentId,
350
+ subItem,
351
+ theme,
352
+ palette,
353
+ styles,
354
+ }: {
355
+ agentId: string;
356
+ subItem: GroupItemData;
357
+ theme: Theme;
358
+ palette: ActivityPalette;
359
+ styles: ReturnType<typeof useActivityStyles>;
360
+ }) {
361
+ const [subExpanded, setSubExpanded] = useActivityDisclosure(agentId, subItem.id);
362
+
363
+ if (subItem.type === "reasoning" && subItem.reasoningData) {
364
+ return (
365
+ <View style={styles.card}>
366
+ <ActivityHeader
367
+ icon="Sparkles"
368
+ iconColor={palette.categoryColors.reasoning}
369
+ title="Thinking"
370
+ expanded={subExpanded ?? false}
371
+ onPress={() => setSubExpanded(!subExpanded)}
372
+ styles={styles}
373
+ />
374
+ {subExpanded ? (
375
+ <View style={styles.details}>
376
+ <ReasoningText
377
+ text={subItem.reasoningData.text}
378
+ phase={subItem.reasoningData.phase}
379
+ theme={theme}
380
+ styles={styles}
381
+ />
382
+ </View>
383
+ ) : null}
384
+ </View>
385
+ );
386
+ }
387
+
388
+ if (subItem.type === "tool_call" && subItem.toolCallData) {
389
+ const data = subItem.toolCallData;
390
+ const categoryColor = palette.categoryColors[data.presentation.category];
391
+ const statusColor = palette.statusColors[data.status];
392
+ const detail = asToolCallDetail(data.detail);
393
+ const subAgentDetail = detail?.type === "sub_agent" ? detail : null;
394
+
395
+ return (
396
+ <View style={styles.card}>
397
+ <ActivityHeader
398
+ icon={data.presentation.icon}
399
+ iconColor={categoryColor}
400
+ title={data.presentation.label}
401
+ summary={data.presentation.summary}
402
+ status={data.status}
403
+ statusColor={statusColor}
404
+ stats={data.presentation.diffStats}
405
+ expanded={subExpanded ?? false}
406
+ onPress={() => setSubExpanded(!subExpanded)}
407
+ styles={styles}
408
+ />
409
+ {subAgentDetail ? <SubAgentProgress detail={subAgentDetail} styles={styles} /> : null}
410
+ {subExpanded ? (
411
+ <View style={styles.details}>
412
+ <ScrollView
413
+ style={styles.detailsScroll}
414
+ contentContainerStyle={styles.detailsContent}
415
+ nestedScrollEnabled
416
+ showsVerticalScrollIndicator
417
+ >
418
+ <DetailBody data={data} theme={theme} palette={palette} styles={styles} />
419
+ {data.errorText ? (
420
+ <View style={styles.section}>
421
+ <DetailLabel style={styles.detailLabel}>Error</DetailLabel>
422
+ <Text
423
+ selectable
424
+ style={{ ...styles.detailText, color: theme.colors.statusDanger }}
425
+ >
426
+ {data.errorText}
427
+ </Text>
428
+ </View>
429
+ ) : null}
430
+ </ScrollView>
431
+ </View>
432
+ ) : null}
433
+ </View>
434
+ );
435
+ }
436
+
437
+ return null;
438
+ }
439
+
440
+ function FoldedActivityGroup({
441
+ group,
442
+ agentId,
443
+ theme,
444
+ palette,
445
+ styles,
446
+ }: {
447
+ group: TurnGroup;
448
+ agentId: string;
449
+ theme: Theme;
450
+ palette: ActivityPalette;
451
+ styles: ReturnType<typeof useActivityStyles>;
452
+ }) {
453
+ // Module-level so an explicit choice survives the virtual list recycling this row.
454
+ const [expanded, setExpanded] = useActivityDisclosure(agentId, `group:${group.leaderId}`);
455
+ const headerRef = useRef<View>(null);
456
+
457
+ const summaryText = formatGroupSummary(group);
458
+
459
+ const toggle = useCallback(() => {
460
+ const next = !expanded;
461
+ setExpanded(next);
462
+ if (!next) scrollActivityIntoView(headerRef.current);
463
+ }, [expanded, setExpanded]);
464
+
465
+ const groupHeaderStyle: ViewStyle = useMemo(
466
+ () => ({
467
+ flexDirection: "row",
468
+ alignItems: "center",
469
+ width: "100%",
470
+ alignSelf: "stretch",
471
+ paddingVertical: 1,
472
+ paddingHorizontal: 2,
473
+ minHeight: 20,
474
+ gap: 6,
475
+ cursor: "pointer",
476
+ }),
477
+ [],
478
+ );
479
+
480
+ const summaryTextStyle: TextStyle = useMemo(
481
+ () => ({
482
+ color: theme.colors.foregroundMuted,
483
+ fontSize: 12.5,
484
+ fontWeight: "500",
485
+ lineHeight: 16,
486
+ flexShrink: 1,
487
+ }),
488
+ [theme.colors.foregroundMuted],
489
+ );
490
+
491
+ const detailsContainerStyle: ViewStyle = useMemo(
492
+ () => ({
493
+ borderLeftWidth: 2,
494
+ borderLeftColor: theme.colors.border,
495
+ marginLeft: 10,
496
+ paddingLeft: 10,
497
+ marginTop: 4,
498
+ marginBottom: 6,
499
+ gap: 6,
500
+ }),
501
+ [theme.colors.border],
502
+ );
503
+
504
+ const containerStyle: ViewStyle = useMemo(
505
+ () => ({
506
+ width: "100%",
507
+ alignSelf: "stretch",
508
+ marginVertical: 0,
509
+ }),
510
+ [],
511
+ );
512
+
513
+ return (
514
+ <View testID="folded-activity-group" style={containerStyle}>
515
+ <Pressable
516
+ ref={headerRef}
517
+ accessibilityRole="button"
518
+ accessibilityLabel={`${expanded ? "Collapse" : "Expand"} ${summaryText}`}
519
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
520
+ onPress={toggle}
521
+ style={groupHeaderStyle}
522
+ >
523
+ <Icon
524
+ name={expanded ? "ChevronDown" : "ChevronRight"}
525
+ color={theme.colors.foregroundMuted}
526
+ size={12}
527
+ />
528
+ <Text numberOfLines={1} style={summaryTextStyle}>
529
+ {summaryText}
530
+ </Text>
531
+ </Pressable>
532
+
533
+ {expanded ? (
534
+ <View style={detailsContainerStyle}>
535
+ {group.items.map((entry) => {
536
+ if (entry.type === "reasoning" && entry.reasoningData?.text?.trim())
537
+ return (
538
+ <ReasoningText
539
+ key={entry.id}
540
+ text={entry.reasoningData.text}
541
+ phase={entry.reasoningData.phase}
542
+ theme={theme}
543
+ styles={styles}
544
+ />
545
+ );
546
+ if (entry.type === "tool_call" && entry.toolCallData)
547
+ return (
548
+ <GroupSubItem
549
+ key={entry.id}
550
+ agentId={agentId}
551
+ subItem={entry}
552
+ theme={theme}
553
+ palette={palette}
554
+ styles={styles}
555
+ />
556
+ );
557
+ return null;
558
+ })}
559
+ </View>
560
+ ) : null}
561
+ </View>
562
+ );
563
+ }
564
+
565
+ function singleActivityGroup(
566
+ timestamp: Date,
567
+ data: ReasoningItemData | ToolCallItemData,
568
+ ): TurnGroup {
569
+ const tool = "name" in data;
570
+ const id = tool ? `tool:${data.callId}` : `reasoning:${timestamp.getTime()}`;
571
+ const group = createActivityGroup(timestamp.getTime(), id, timestamp.getTime(), [
572
+ {
573
+ id,
574
+ timestamp: timestamp.getTime(),
575
+ type: tool ? "tool_call" : "reasoning",
576
+ ...(tool ? { toolCallData: data } : { reasoningData: data }),
577
+ },
578
+ ]);
579
+ updateGroupStats(group);
580
+ return group;
581
+ }
582
+
583
+ export function ColorfulReasoning({
584
+ agentId,
585
+ item,
586
+ theme,
587
+ timestamp,
588
+ ...hostProps
589
+ }: PluginTimelineItemProps<ReasoningItemData>) {
590
+ const settings = useSettings(activitySettings);
591
+ const mode = settings.status === "ready" ? settings.values.displayMode : DEFAULT_DISPLAY_MODE;
592
+ const palette = usePalette(theme);
593
+ const styles = useActivityStyles(theme, palette);
594
+ const activity = useActivityGroup(agentId, timestamp, item.data, mode === "folded");
595
+ if (mode === "folded") {
596
+ if (activity && !activity.isLeader)
597
+ return <View testID="folded-activity-follower" style={{ height: 0 }} />;
598
+ if (activity && activity.isLeader) {
599
+ return (
600
+ <FoldedActivityGroup
601
+ group={activity.group}
602
+ agentId={agentId}
603
+ theme={theme}
604
+ palette={palette}
605
+ styles={styles}
606
+ />
607
+ );
608
+ }
609
+ // 历史非活跃条目未就绪时默认静默,杜绝单卡片撑开刷屏
610
+ if (item.data.phase !== "streaming") {
611
+ return <View testID="folded-activity-follower" style={{ height: 0 }} />;
612
+ }
613
+ return (
614
+ <FoldedActivityGroup
615
+ group={singleActivityGroup(timestamp, item.data)}
616
+ agentId={agentId}
617
+ theme={theme}
618
+ palette={palette}
619
+ styles={styles}
620
+ />
621
+ );
622
+ }
623
+ return (
624
+ <DetailedColorfulReasoning
625
+ {...hostProps}
626
+ agentId={agentId}
627
+ item={item}
628
+ theme={theme}
629
+ timestamp={timestamp}
630
+ />
631
+ );
632
+ }
633
+
634
+ function DetailedColorfulReasoning({
635
+ agentId,
636
+ item,
637
+ theme,
638
+ timestamp,
639
+ }: PluginTimelineItemProps<ReasoningItemData>) {
640
+ const palette = usePalette(theme);
641
+ const styles = useActivityStyles(theme, palette);
642
+ const isStreaming = item.data.phase === "streaming";
643
+ const isLatest = useIsLatestMarker(latestReasoningMarker, agentId, timestamp, isStreaming);
644
+ const [userExpanded, setUserExpanded] = useActivityDisclosure(
645
+ agentId,
646
+ `reasoning:${timestamp.getTime()}`,
647
+ );
648
+ const expanded = getReasoningExpansionState(isStreaming, isLatest, userExpanded ?? null);
649
+ const toggle = useCallback(() => setUserExpanded(!expanded), [expanded, setUserExpanded]);
650
+ return (
651
+ <View style={styles.card}>
652
+ <ActivityHeader
653
+ icon="Sparkles"
654
+ iconColor={palette.categoryColors.reasoning}
655
+ title="Thinking"
656
+ expanded={expanded}
657
+ onPress={toggle}
658
+ styles={styles}
659
+ />
660
+ {expanded ? (
661
+ <View style={styles.details}>
662
+ <ReasoningText
663
+ text={item.data.text}
664
+ phase={item.data.phase}
665
+ theme={theme}
666
+ styles={styles}
667
+ />
668
+ </View>
669
+ ) : null}
670
+ </View>
671
+ );
672
+ }
673
+
674
+ export function ColorfulToolCall({
675
+ agentId,
676
+ item,
677
+ theme,
678
+ timestamp,
679
+ ...hostProps
680
+ }: PluginTimelineItemProps<ToolCallItemData>) {
681
+ const settings = useSettings(activitySettings);
682
+ const mode = settings.status === "ready" ? settings.values.displayMode : DEFAULT_DISPLAY_MODE;
683
+ const palette = usePalette(theme);
684
+ const styles = useActivityStyles(theme, palette);
685
+ const activity = useActivityGroup(agentId, timestamp, item.data, mode === "folded");
686
+ if (mode === "folded") {
687
+ if (activity && !activity.isLeader)
688
+ return <View testID="folded-activity-follower" style={{ height: 0 }} />;
689
+ if (activity && activity.isLeader) {
690
+ return (
691
+ <FoldedActivityGroup
692
+ group={activity.group}
693
+ agentId={agentId}
694
+ theme={theme}
695
+ palette={palette}
696
+ styles={styles}
697
+ />
698
+ );
699
+ }
700
+ // 历史非活跃条目未就绪时默认静默,杜绝几十行单条展开刷屏
701
+ if (item.data.status !== "running") {
702
+ return <View testID="folded-activity-follower" style={{ height: 0 }} />;
703
+ }
704
+ return (
705
+ <FoldedActivityGroup
706
+ group={singleActivityGroup(timestamp, item.data)}
707
+ agentId={agentId}
708
+ theme={theme}
709
+ palette={palette}
710
+ styles={styles}
711
+ />
712
+ );
713
+ }
714
+ return (
715
+ <DetailedColorfulToolCall
716
+ {...hostProps}
717
+ agentId={agentId}
718
+ item={item}
719
+ theme={theme}
720
+ timestamp={timestamp}
721
+ />
722
+ );
723
+ }
724
+
725
+ function DetailedColorfulToolCall({
726
+ agentId,
727
+ item,
728
+ theme,
729
+ timestamp,
730
+ }: PluginTimelineItemProps<ToolCallItemData>) {
731
+ const palette = usePalette(theme);
732
+ const styles = useActivityStyles(theme, palette);
733
+ const isRunning = item.data.status === "running";
734
+ const isLatest = useIsLatestMarker(latestToolCallMarker, agentId, timestamp, isRunning);
735
+ const detail = asToolCallDetail(item.data.detail);
736
+ const [userExpanded, setUserExpanded] = useActivityDisclosure(
737
+ agentId,
738
+ `tool:${item.data.callId ?? timestamp.getTime()}`,
739
+ );
740
+ const categoryColor = palette.categoryColors[item.data.presentation.category];
741
+ const statusColor = palette.statusColors[item.data.status];
742
+ const expanded = getActivityExpansionState(isRunning, isLatest, userExpanded ?? null);
743
+ const toggle = useCallback(() => {
744
+ if (!isRunning) setUserExpanded(!expanded);
745
+ }, [expanded, isRunning, setUserExpanded]);
746
+ const subAgentDetail = detail?.type === "sub_agent" ? detail : null;
747
+ return (
748
+ <View style={styles.card}>
749
+ <ActivityHeader
750
+ icon={item.data.presentation.icon}
751
+ iconColor={categoryColor}
752
+ title={item.data.presentation.label}
753
+ summary={item.data.presentation.summary}
754
+ status={item.data.status}
755
+ statusColor={statusColor}
756
+ stats={item.data.presentation.diffStats}
757
+ expanded={expanded}
758
+ onPress={toggle}
759
+ styles={styles}
760
+ />
761
+ {subAgentDetail ? <SubAgentProgress detail={subAgentDetail} styles={styles} /> : null}
762
+ {expanded ? (
763
+ <View style={styles.details}>
764
+ <ScrollView
765
+ style={styles.detailsScroll}
766
+ contentContainerStyle={styles.detailsContent}
767
+ nestedScrollEnabled
768
+ showsVerticalScrollIndicator
769
+ >
770
+ <DetailBody data={item.data} theme={theme} palette={palette} styles={styles} />
771
+ {item.data.errorText ? (
772
+ <View style={styles.section}>
773
+ <DetailLabel style={styles.detailLabel}>Error</DetailLabel>
774
+ <Text selectable style={{ ...styles.detailText, color: theme.colors.statusDanger }}>
775
+ {item.data.errorText}
776
+ </Text>
777
+ </View>
778
+ ) : null}
779
+ </ScrollView>
780
+ </View>
781
+ ) : null}
782
+ </View>
783
+ );
784
+ }