@opengeni/react-native 1.4.5-canary.37615713321001

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 (132) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +74 -0
  3. package/dist/adapters.d.ts +53 -0
  4. package/dist/adapters.js +3 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/attachments.d.ts +75 -0
  7. package/dist/chunk-3VSZAQ7G.js +391 -0
  8. package/dist/chunk-3VSZAQ7G.js.map +1 -0
  9. package/dist/chunk-5ZVK7WKA.js +88 -0
  10. package/dist/chunk-5ZVK7WKA.js.map +1 -0
  11. package/dist/chunk-EEDIMY3L.js +240 -0
  12. package/dist/chunk-EEDIMY3L.js.map +1 -0
  13. package/dist/chunk-M23SDI4R.js +83 -0
  14. package/dist/chunk-M23SDI4R.js.map +1 -0
  15. package/dist/chunk-PHJBSAR3.js +2006 -0
  16. package/dist/chunk-PHJBSAR3.js.map +1 -0
  17. package/dist/environment.d.ts +19 -0
  18. package/dist/expo.d.ts +25 -0
  19. package/dist/expo.js +267 -0
  20. package/dist/expo.js.map +1 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.js +3345 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/presentation.d.ts +262 -0
  25. package/dist/read-acknowledgement.d.ts +39 -0
  26. package/dist/realtime/call-events.d.ts +11 -0
  27. package/dist/realtime/call-view.d.ts +26 -0
  28. package/dist/realtime/expo-call.d.ts +11 -0
  29. package/dist/realtime/native-call.d.ts +104 -0
  30. package/dist/realtime/native-realtime.d.ts +35 -0
  31. package/dist/realtime/use-native-session-realtime.d.ts +25 -0
  32. package/dist/session-view.d.ts +87 -0
  33. package/dist/timeline/activity.d.ts +34 -0
  34. package/dist/timeline/composer.d.ts +86 -0
  35. package/dist/timeline/controls.d.ts +27 -0
  36. package/dist/timeline/decisions.d.ts +29 -0
  37. package/dist/timeline/feedback.d.ts +26 -0
  38. package/dist/timeline/icon-nodes.generated.d.ts +738 -0
  39. package/dist/timeline/icon.d.ts +11 -0
  40. package/dist/timeline/index.d.ts +32 -0
  41. package/dist/timeline/index.js +5419 -0
  42. package/dist/timeline/index.js.map +1 -0
  43. package/dist/timeline/markdown.d.ts +11 -0
  44. package/dist/timeline/markdown.js +415 -0
  45. package/dist/timeline/markdown.js.map +1 -0
  46. package/dist/timeline/message-attachments.d.ts +13 -0
  47. package/dist/timeline/message-timeline.d.ts +36 -0
  48. package/dist/timeline/model-picker.d.ts +17 -0
  49. package/dist/timeline/primitives.d.ts +85 -0
  50. package/dist/timeline/queue-dock.d.ts +19 -0
  51. package/dist/timeline/session-actions.d.ts +34 -0
  52. package/dist/timeline/session-list.d.ts +50 -0
  53. package/dist/timeline/session-screen.d.ts +35 -0
  54. package/dist/timeline/session-signals.d.ts +37 -0
  55. package/dist/timeline/sheet.d.ts +14 -0
  56. package/dist/timeline/theme.d.ts +58 -0
  57. package/dist/timeline/thinking-orb.d.ts +13 -0
  58. package/dist/timeline/turn.d.ts +80 -0
  59. package/dist/ui/attention.d.ts +14 -0
  60. package/dist/ui/cards.d.ts +21 -0
  61. package/dist/ui/composer.d.ts +21 -0
  62. package/dist/ui/format.d.ts +13 -0
  63. package/dist/ui/icons.d.ts +9 -0
  64. package/dist/ui/index.d.ts +12 -0
  65. package/dist/ui/index.js +1757 -0
  66. package/dist/ui/index.js.map +1 -0
  67. package/dist/ui/steps-sheet.d.ts +10 -0
  68. package/dist/ui/theme.d.ts +63 -0
  69. package/dist/ui/timeline.d.ts +48 -0
  70. package/dist/ui/web-tokens.gen.d.ts +90 -0
  71. package/dist/use-native-session.d.ts +40 -0
  72. package/dist/view-model.d.ts +52 -0
  73. package/dist/voice-input.d.ts +102 -0
  74. package/dist/webrtc.d.ts +9 -0
  75. package/dist/webrtc.js +19 -0
  76. package/dist/webrtc.js.map +1 -0
  77. package/expo-module.config.json +7 -0
  78. package/ios/OpenGeniCall.podspec +22 -0
  79. package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
  80. package/ios/OpenGeniCallCenter.swift +189 -0
  81. package/ios/OpenGeniCallModule.swift +64 -0
  82. package/package.json +93 -0
  83. package/src/adapters.ts +150 -0
  84. package/src/attachments.ts +396 -0
  85. package/src/environment.tsx +214 -0
  86. package/src/expo.ts +295 -0
  87. package/src/index.ts +98 -0
  88. package/src/presentation.ts +285 -0
  89. package/src/read-acknowledgement.ts +72 -0
  90. package/src/realtime/call-events.ts +41 -0
  91. package/src/realtime/call-view.tsx +220 -0
  92. package/src/realtime/expo-call.ts +49 -0
  93. package/src/realtime/native-call.ts +266 -0
  94. package/src/realtime/native-realtime.ts +115 -0
  95. package/src/realtime/use-native-session-realtime.ts +29 -0
  96. package/src/session-view.tsx +2251 -0
  97. package/src/timeline/activity.tsx +736 -0
  98. package/src/timeline/composer.tsx +686 -0
  99. package/src/timeline/controls.tsx +151 -0
  100. package/src/timeline/decisions.tsx +651 -0
  101. package/src/timeline/feedback.tsx +251 -0
  102. package/src/timeline/icon-nodes.generated.ts +1830 -0
  103. package/src/timeline/icon.tsx +44 -0
  104. package/src/timeline/index.ts +67 -0
  105. package/src/timeline/markdown.tsx +510 -0
  106. package/src/timeline/message-attachments.tsx +155 -0
  107. package/src/timeline/message-timeline.tsx +1042 -0
  108. package/src/timeline/model-picker.tsx +450 -0
  109. package/src/timeline/primitives.tsx +561 -0
  110. package/src/timeline/queue-dock.tsx +331 -0
  111. package/src/timeline/session-actions.tsx +379 -0
  112. package/src/timeline/session-list.tsx +338 -0
  113. package/src/timeline/session-screen.tsx +488 -0
  114. package/src/timeline/session-signals.tsx +312 -0
  115. package/src/timeline/sheet.tsx +153 -0
  116. package/src/timeline/theme.tsx +128 -0
  117. package/src/timeline/thinking-orb.tsx +93 -0
  118. package/src/timeline/turn.tsx +569 -0
  119. package/src/ui/attention.tsx +220 -0
  120. package/src/ui/cards.tsx +267 -0
  121. package/src/ui/composer.tsx +296 -0
  122. package/src/ui/format.ts +86 -0
  123. package/src/ui/icons.tsx +131 -0
  124. package/src/ui/index.ts +18 -0
  125. package/src/ui/steps-sheet.tsx +71 -0
  126. package/src/ui/theme.ts +261 -0
  127. package/src/ui/timeline.tsx +837 -0
  128. package/src/ui/web-tokens.gen.ts +94 -0
  129. package/src/use-native-session.ts +219 -0
  130. package/src/view-model.ts +485 -0
  131. package/src/voice-input.ts +382 -0
  132. package/src/webrtc.ts +30 -0
@@ -0,0 +1,837 @@
1
+ import type {
2
+ ActivityItem,
3
+ AgentMessageItem,
4
+ PendingApproval,
5
+ PendingHumanInputRequest,
6
+ TimelineGroup,
7
+ TimelineItem,
8
+ ToolCallItem,
9
+ } from "@opengeni/react/session";
10
+ import type { HumanInputAnswer } from "@opengeni/sdk";
11
+ import { type ReactNode, useEffect, useMemo, useState } from "react";
12
+ import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
13
+ import Animated, {
14
+ FadeIn,
15
+ FadeInDown,
16
+ LinearTransition,
17
+ useAnimatedStyle,
18
+ useSharedValue,
19
+ withRepeat,
20
+ withTiming,
21
+ } from "react-native-reanimated";
22
+ import { ApprovalCard, QuestionCard } from "./cards";
23
+ import {
24
+ activityItemsOf,
25
+ formatDuration,
26
+ itemTime,
27
+ toolCallsOf,
28
+ toolDetail,
29
+ toolOutputPreview,
30
+ toolTitle,
31
+ } from "./format";
32
+ import { AgentIcon, toolIconName } from "./icons";
33
+ import type { AgentTheme } from "./theme";
34
+
35
+ export type MarkdownRenderer = (text: string, theme: AgentTheme) => ReactNode;
36
+
37
+ export interface AgentTimelineProps {
38
+ groups: readonly TimelineGroup[];
39
+ theme: AgentTheme;
40
+ running: boolean;
41
+ approvals?: readonly PendingApproval[] | undefined;
42
+ humanInput?: readonly PendingHumanInputRequest[] | undefined;
43
+ onApproval?: ((approvalId: string, decision: "approve" | "reject") => void) | undefined;
44
+ onAnswer?: ((requestId: string, answers: HumanInputAnswer[] | null) => void) | undefined;
45
+ /**
46
+ * Where pending approvals and questions render. `tray`: the host renders
47
+ * `AgentAttentionTray` above its composer and the timeline only marks the wait.
48
+ */
49
+ attentionPlacement?: "inline" | "tray";
50
+ /** Open a full step view (e.g. `AgentStepsSheet`) instead of expanding work in place. */
51
+ onOpenSteps?: ((steps: ActivityItem[], title: string) => void) | undefined;
52
+ renderMarkdown?: MarkdownRenderer | undefined;
53
+ /** Host display name for the agent. */
54
+ agentName?: string | undefined;
55
+ agentMark?: ReactNode | undefined;
56
+ contentInsetTop?: number | undefined;
57
+ contentInsetBottom?: number | undefined;
58
+ emptyState?: ReactNode | undefined;
59
+ }
60
+
61
+ type Row =
62
+ | { key: string; kind: "group"; group: TimelineGroup; live: boolean; failed: boolean }
63
+ | { key: string; kind: "working" }
64
+ | { key: string; kind: "approval"; approval: PendingApproval }
65
+ | { key: string; kind: "question"; request: PendingHumanInputRequest };
66
+
67
+ const plainMarkdown: MarkdownRenderer = (text, theme) => (
68
+ <Text
69
+ style={{ color: theme.colors.text, fontSize: theme.type.body, lineHeight: theme.type.bodyLine }}
70
+ >
71
+ {text}
72
+ </Text>
73
+ );
74
+
75
+ function groupTurnId(group: TimelineGroup): string | null {
76
+ if (group.kind === "item")
77
+ return "turnId" in group.item
78
+ ? ((group.item as { turnId?: string | null }).turnId ?? null)
79
+ : null;
80
+ const first = activityItemsOf(group)[0] as { turnId?: string | null } | undefined;
81
+ return first?.turnId ?? null;
82
+ }
83
+
84
+ /**
85
+ * Within one turn, show the work summary before that turn's assistant prose: work happens
86
+ * first and the answer reads as its conclusion (the projection lists prose first).
87
+ */
88
+ export function orderWorkBeforeProse(groups: readonly TimelineGroup[]): TimelineGroup[] {
89
+ const out = [...groups];
90
+ for (let index = 1; index < out.length; index += 1) {
91
+ const group = out[index]!;
92
+ if (group.kind === "item") continue;
93
+ const turnId = groupTurnId(group);
94
+ if (!turnId) continue;
95
+ let start = index;
96
+ while (start > 0) {
97
+ const previous = out[start - 1]!;
98
+ if (
99
+ previous.kind !== "item" ||
100
+ previous.item.kind !== "agent-message" ||
101
+ groupTurnId(previous) !== turnId
102
+ )
103
+ break;
104
+ start -= 1;
105
+ }
106
+ if (start < index) {
107
+ out.splice(index, 1);
108
+ out.splice(start, 0, group);
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+
114
+ export function AgentTimeline(props: AgentTimelineProps) {
115
+ const { theme, running } = props;
116
+ const groups = useMemo(() => orderWorkBeforeProse(props.groups), [props.groups]);
117
+ const rows = useMemo<Row[]>(() => {
118
+ // The live turn is the newest turn in the log; its work block stays live while running.
119
+ let liveTurnId: string | null = null;
120
+ for (let index = groups.length - 1; index >= 0 && !liveTurnId; index -= 1)
121
+ liveTurnId = groupTurnId(groups[index]!);
122
+ const isLive = (group: TimelineGroup) =>
123
+ running && group.kind !== "item" && groupTurnId(group) === liveTurnId;
124
+ const failedTurns = new Set(
125
+ groups.flatMap((group) =>
126
+ group.kind === "item" &&
127
+ group.item.kind === "turn-end" &&
128
+ group.item.outcome === "failed" &&
129
+ group.item.turnId
130
+ ? [group.item.turnId]
131
+ : [],
132
+ ),
133
+ );
134
+ const out: Row[] = groups.map((group, index) => ({
135
+ key: groupKey(group, index),
136
+ kind: "group",
137
+ group,
138
+ live: isLive(group),
139
+ failed:
140
+ group.kind !== "item" &&
141
+ (group.outcome === "failed" || failedTurns.has(groupTurnId(group) ?? "")),
142
+ }));
143
+ const needsYou = (props.approvals?.length ?? 0) > 0 || (props.humanInput?.length ?? 0) > 0;
144
+ if (running && !needsYou && !groups.some(isLive)) out.push({ key: "working", kind: "working" });
145
+ if (props.attentionPlacement !== "tray") {
146
+ for (const approval of props.approvals ?? [])
147
+ out.push({ key: `approval:${approval.id}`, kind: "approval", approval });
148
+ for (const request of props.humanInput ?? [])
149
+ out.push({ key: `question:${request.id}`, kind: "question", request });
150
+ }
151
+ return out;
152
+ }, [groups, running, props.approvals, props.humanInput, props.attentionPlacement]);
153
+
154
+ return (
155
+ <FlatList
156
+ contentContainerStyle={{
157
+ paddingTop: props.contentInsetTop ?? 12,
158
+ paddingBottom: props.contentInsetBottom ?? 24,
159
+ paddingHorizontal: theme.space.gutter,
160
+ flexGrow: 1,
161
+ }}
162
+ data={rows}
163
+ keyboardDismissMode="interactive"
164
+ keyboardShouldPersistTaps="handled"
165
+ keyExtractor={(row) => row.key}
166
+ ListEmptyComponent={props.emptyState ? <>{props.emptyState}</> : undefined}
167
+ renderItem={({ item: row }) => (
168
+ <Animated.View entering={FadeInDown.duration(220)} layout={LinearTransition.duration(180)}>
169
+ <TimelineRow {...props} groups={groups} row={row} />
170
+ </Animated.View>
171
+ )}
172
+ style={{ backgroundColor: theme.colors.background }}
173
+ />
174
+ );
175
+ }
176
+
177
+ function groupKey(group: TimelineGroup, index: number): string {
178
+ if (group.kind === "item") {
179
+ const item = group.item as TimelineItem & { reconciliationKey?: string };
180
+ return item.reconciliationKey ?? item.id ?? `item:${index}`;
181
+ }
182
+ return `${group.kind}:${group.id}`;
183
+ }
184
+
185
+ function TimelineRow(props: AgentTimelineProps & { row: Row }) {
186
+ const { row, theme } = props;
187
+ if (row.kind === "working") return <WorkingLine theme={theme} label="Thinking" />;
188
+ if (row.kind === "approval") {
189
+ return (
190
+ <ApprovalCard
191
+ approval={row.approval}
192
+ theme={theme}
193
+ onDecision={(decision) => props.onApproval?.(row.approval.id, decision)}
194
+ />
195
+ );
196
+ }
197
+ if (row.kind === "question") {
198
+ return (
199
+ <QuestionCard
200
+ request={row.request}
201
+ theme={theme}
202
+ onAnswer={(answers) => props.onAnswer?.(row.request.id, answers)}
203
+ />
204
+ );
205
+ }
206
+ const group = row.group;
207
+ if (group.kind === "item") return <ItemRow item={group.item} {...props} />;
208
+ const waiting = (props.approvals?.length ?? 0) > 0 || (props.humanInput?.length ?? 0) > 0;
209
+ return (
210
+ <WorkBlock
211
+ failed={row.failed}
212
+ group={group}
213
+ live={row.live}
214
+ waiting={row.live && waiting}
215
+ {...props}
216
+ />
217
+ );
218
+ }
219
+
220
+ function ItemRow(props: AgentTimelineProps & { item: TimelineItem }) {
221
+ const { item, theme } = props;
222
+ const render = props.renderMarkdown ?? plainMarkdown;
223
+ switch (item.kind) {
224
+ case "user-message":
225
+ return <UserMessage text={item.text} theme={theme} />;
226
+ case "agent-message":
227
+ return <AssistantMessage item={item} render={render} theme={theme} />;
228
+ case "turn-end":
229
+ return item.outcome === "failed" && item.failureText ? (
230
+ <Notice theme={theme} tone="danger" text={item.failureText} />
231
+ ) : null;
232
+ default:
233
+ return null;
234
+ }
235
+ }
236
+
237
+ export function UserMessage({ text, theme }: { text: string; theme: AgentTheme }) {
238
+ return (
239
+ <View
240
+ style={{ alignItems: "flex-end", marginTop: theme.space.turn, marginBottom: theme.space.row }}
241
+ >
242
+ <View
243
+ style={{
244
+ maxWidth: "86%",
245
+ backgroundColor: theme.colors.userBubble,
246
+ borderRadius: theme.radius.bubble,
247
+ paddingHorizontal: theme.type.body,
248
+ paddingVertical: theme.type.body * 0.6,
249
+ }}
250
+ >
251
+ <Text
252
+ selectable
253
+ style={{
254
+ color: theme.colors.onUserBubble,
255
+ fontSize: theme.type.body,
256
+ lineHeight: theme.type.bodyLine,
257
+ }}
258
+ >
259
+ {text}
260
+ </Text>
261
+ </View>
262
+ </View>
263
+ );
264
+ }
265
+
266
+ function AssistantMessage({
267
+ item,
268
+ render,
269
+ theme,
270
+ }: {
271
+ item: AgentMessageItem;
272
+ render: MarkdownRenderer;
273
+ theme: AgentTheme;
274
+ }) {
275
+ if (!item.text.trim()) return null;
276
+ const commentary = item.phase === "commentary";
277
+ return (
278
+ <View style={{ marginVertical: theme.space.row, opacity: commentary ? 0.82 : 1 }}>
279
+ {render(item.text, theme)}
280
+ </View>
281
+ );
282
+ }
283
+
284
+ function Notice({
285
+ theme,
286
+ tone,
287
+ text,
288
+ }: {
289
+ theme: AgentTheme;
290
+ tone: "danger" | "muted";
291
+ text: string;
292
+ }) {
293
+ const danger = tone === "danger";
294
+ return (
295
+ <View
296
+ style={{
297
+ flexDirection: "row",
298
+ gap: 10,
299
+ alignItems: "flex-start",
300
+ marginVertical: theme.space.row,
301
+ padding: 12,
302
+ borderRadius: theme.radius.card,
303
+ backgroundColor: danger ? theme.colors.dangerSurface : theme.colors.surface,
304
+ }}
305
+ >
306
+ <AgentIcon
307
+ color={danger ? theme.colors.danger : theme.colors.textMuted}
308
+ name="alert"
309
+ size={18}
310
+ />
311
+ <Text
312
+ style={{
313
+ flex: 1,
314
+ color: danger ? theme.colors.danger : theme.colors.textMuted,
315
+ fontSize: theme.type.small,
316
+ lineHeight: theme.type.smallLine,
317
+ }}
318
+ >
319
+ {text}
320
+ </Text>
321
+ </View>
322
+ );
323
+ }
324
+
325
+ /* ------------------------------------------------------------------------------------------ */
326
+ /* Agent work */
327
+ /* ------------------------------------------------------------------------------------------ */
328
+
329
+ function WorkBlock(
330
+ props: AgentTimelineProps & {
331
+ group: Exclude<TimelineGroup, { kind: "item" }>;
332
+ live: boolean;
333
+ waiting: boolean;
334
+ failed: boolean;
335
+ },
336
+ ) {
337
+ const { group, theme, live, waiting, failed } = props;
338
+ const items = activityItemsOf(group);
339
+ const prose =
340
+ group.kind === "activity"
341
+ ? group.items.filter((item): item is AgentMessageItem => item.kind === "agent-message")
342
+ : [];
343
+ const steps = items.filter(
344
+ (item) => item.kind === "tool-call" || item.kind === "reasoning" || item.kind === "worker",
345
+ );
346
+ const tools = toolCallsOf(items);
347
+ const outcome = group.outcome;
348
+ const failureText = group.failureText;
349
+ const started = group.kind === "turn" ? Date.parse(group.startedAt) : itemTime(items[0] ?? {});
350
+ const ended =
351
+ group.kind === "turn" ? Date.parse(group.endedAt) : itemTime(items[items.length - 1] ?? {});
352
+ const duration = formatDuration(ended - started);
353
+ const running = live && !outcome;
354
+ const current =
355
+ running && !waiting
356
+ ? [...tools].reverse().find((tool) => tool.status === "running")
357
+ : undefined;
358
+ const render = props.renderMarkdown ?? plainMarkdown;
359
+
360
+ const work =
361
+ steps.length === 0 ? null : theme.layout.activity === "rail" ? (
362
+ <RailSteps items={steps} theme={theme} waiting={waiting} />
363
+ ) : theme.layout.activity === "cards" ? (
364
+ <CardSteps
365
+ current={current}
366
+ duration={duration}
367
+ failed={failed}
368
+ items={steps}
369
+ onOpen={props.onOpenSteps}
370
+ running={running}
371
+ theme={theme}
372
+ toolCount={tools.length}
373
+ waiting={waiting}
374
+ />
375
+ ) : (
376
+ <FoldSteps
377
+ current={current}
378
+ duration={duration}
379
+ failed={failed}
380
+ items={steps}
381
+ onOpen={props.onOpenSteps}
382
+ running={running}
383
+ theme={theme}
384
+ toolCount={tools.length}
385
+ waiting={waiting}
386
+ />
387
+ );
388
+
389
+ return (
390
+ <View style={{ marginVertical: theme.space.row }}>
391
+ {work}
392
+ {prose.map((item) => (
393
+ <View key={item.id} style={{ marginTop: theme.space.row }}>
394
+ {render(item.text, theme)}
395
+ </View>
396
+ ))}
397
+ {outcome === "failed" && failureText ? (
398
+ <Notice text={failureText} theme={theme} tone="danger" />
399
+ ) : null}
400
+ </View>
401
+ );
402
+ }
403
+
404
+ function stepLabel(item: ActivityItem): string {
405
+ if (item.kind === "tool-call") return toolTitle(item);
406
+ if (item.kind === "reasoning") return "Thought";
407
+ if (item.kind === "worker") return "Worker";
408
+ return "Step";
409
+ }
410
+
411
+ const LIVE_VERBS: Partial<Record<ReturnType<typeof toolIconName>, string>> = {
412
+ terminal: "Running",
413
+ search: "Searching",
414
+ edit: "Editing",
415
+ globe: "Browsing",
416
+ rocket: "Deploying",
417
+ };
418
+
419
+ /** Present-tense description of a running tool ("Running bun test"). */
420
+ function liveLabel(tool: ToolCallItem): string {
421
+ const verb = LIVE_VERBS[toolIconName(tool.name)];
422
+ const detail = toolDetail(tool);
423
+ if (!verb) return detail ? `${toolTitle(tool)} · ${detail}` : toolTitle(tool);
424
+ return detail ? `${verb} ${detail}` : verb;
425
+ }
426
+
427
+ function StatusDot({
428
+ item,
429
+ theme,
430
+ size = 8,
431
+ waiting,
432
+ }: {
433
+ item: ActivityItem;
434
+ theme: AgentTheme;
435
+ size?: number;
436
+ waiting?: boolean;
437
+ }) {
438
+ const status = item.kind === "tool-call" ? item.status : "complete";
439
+ const color =
440
+ status === "failed"
441
+ ? theme.colors.danger
442
+ : status === "running"
443
+ ? waiting
444
+ ? theme.colors.attention
445
+ : theme.colors.accent
446
+ : theme.colors.textFaint;
447
+ return (
448
+ <View style={{ width: size, height: size, borderRadius: size / 2, backgroundColor: color }} />
449
+ );
450
+ }
451
+
452
+ /** Calm: one quiet line; tap to reveal the steps. */
453
+ type StepSummaryProps = {
454
+ items: ActivityItem[];
455
+ theme: AgentTheme;
456
+ running: boolean;
457
+ waiting: boolean;
458
+ failed: boolean;
459
+ duration: string;
460
+ toolCount: number;
461
+ current?: ToolCallItem | undefined;
462
+ onOpen?: ((steps: ActivityItem[], title: string) => void) | undefined;
463
+ };
464
+
465
+ function FoldSteps(props: StepSummaryProps) {
466
+ const { theme, running } = props;
467
+ const [open, setOpen] = useState(false);
468
+ const steps = props.toolCount
469
+ ? `${props.toolCount} ${props.toolCount === 1 ? "step" : "steps"}`
470
+ : "";
471
+ const label = props.waiting
472
+ ? "Waiting for you"
473
+ : running
474
+ ? props.current
475
+ ? liveLabel(props.current)
476
+ : "Working"
477
+ : props.failed
478
+ ? `Stopped${steps ? ` after ${steps}` : ""}`
479
+ : [props.duration ? `Worked for ${props.duration}` : "Worked", steps]
480
+ .filter(Boolean)
481
+ .join(" · ");
482
+ return (
483
+ <View>
484
+ <Pressable
485
+ accessibilityRole="button"
486
+ accessibilityState={{ expanded: open }}
487
+ hitSlop={8}
488
+ onPress={() =>
489
+ props.onOpen ? props.onOpen(props.items, label) : setOpen((value) => !value)
490
+ }
491
+ style={{ flexDirection: "row", alignItems: "center", gap: 6, minHeight: 32 }}
492
+ >
493
+ {props.waiting ? (
494
+ <View
495
+ style={{
496
+ width: 8,
497
+ height: 8,
498
+ borderRadius: 4,
499
+ backgroundColor: theme.colors.attention,
500
+ }}
501
+ />
502
+ ) : running ? (
503
+ <Pulse theme={theme} />
504
+ ) : null}
505
+ <Text
506
+ numberOfLines={1}
507
+ style={{
508
+ flexShrink: 1,
509
+ color: props.waiting ? theme.colors.attention : theme.colors.textMuted,
510
+ fontSize: theme.type.small + 1,
511
+ }}
512
+ >
513
+ {label}
514
+ </Text>
515
+ <AgentIcon
516
+ color={theme.colors.textFaint}
517
+ name={open ? "chevron-down" : "chevron-right"}
518
+ size={14}
519
+ />
520
+ </Pressable>
521
+ {open ? (
522
+ <Animated.View
523
+ entering={FadeIn.duration(160)}
524
+ style={{ marginTop: 4, marginLeft: 2, gap: 10, paddingVertical: 4 }}
525
+ >
526
+ {props.items.map((item) => (
527
+ <CompactStep item={item} key={item.id} theme={theme} />
528
+ ))}
529
+ </Animated.View>
530
+ ) : null}
531
+ </View>
532
+ );
533
+ }
534
+
535
+ function CompactStep({ item, theme }: { item: ActivityItem; theme: AgentTheme }) {
536
+ const detail =
537
+ item.kind === "tool-call" ? toolDetail(item) : item.kind === "reasoning" ? item.text : null;
538
+ return (
539
+ <View style={{ flexDirection: "row", gap: 10 }}>
540
+ <View style={{ paddingTop: 2 }}>
541
+ <AgentIcon
542
+ color={theme.colors.textFaint}
543
+ name={item.kind === "tool-call" ? toolIconName(item.name) : "sparkle"}
544
+ size={15}
545
+ />
546
+ </View>
547
+ <View style={{ flex: 1 }}>
548
+ <Text
549
+ style={{
550
+ color: theme.colors.text,
551
+ fontSize: theme.type.small,
552
+ lineHeight: theme.type.smallLine,
553
+ }}
554
+ >
555
+ {stepLabel(item)}
556
+ </Text>
557
+ {detail ? (
558
+ <Text
559
+ numberOfLines={2}
560
+ style={{
561
+ color: theme.colors.textMuted,
562
+ fontSize: theme.type.small,
563
+ lineHeight: theme.type.smallLine,
564
+ }}
565
+ >
566
+ {detail}
567
+ </Text>
568
+ ) : null}
569
+ </View>
570
+ </View>
571
+ );
572
+ }
573
+
574
+ /** Workbench: always-visible rail with status dots, monospace input and output preview. */
575
+ export function AgentStepList(props: {
576
+ items: readonly ActivityItem[];
577
+ theme: AgentTheme;
578
+ waiting?: boolean;
579
+ }) {
580
+ return <RailSteps items={props.items} theme={props.theme} waiting={props.waiting ?? false} />;
581
+ }
582
+
583
+ function RailSteps({
584
+ items,
585
+ theme,
586
+ waiting,
587
+ }: {
588
+ items: readonly ActivityItem[];
589
+ theme: AgentTheme;
590
+ waiting: boolean;
591
+ }) {
592
+ return (
593
+ <View style={{ paddingLeft: 4 }}>
594
+ {items.map((item, index) => {
595
+ const detail = item.kind === "tool-call" ? toolDetail(item) : null;
596
+ const output =
597
+ item.kind === "tool-call" && item.status !== "running" ? toolOutputPreview(item) : null;
598
+ const reasoning = item.kind === "reasoning" ? item.text : null;
599
+ const last = index === items.length - 1;
600
+ return (
601
+ <View key={item.id} style={{ flexDirection: "row", gap: 12 }}>
602
+ <View style={{ alignItems: "center", width: 10 }}>
603
+ <View style={{ height: 6 }} />
604
+ <StatusDot item={item} theme={theme} waiting={waiting} />
605
+ {last ? null : (
606
+ <View
607
+ style={{
608
+ flex: 1,
609
+ width: StyleSheet.hairlineWidth * 2,
610
+ backgroundColor: theme.colors.rail,
611
+ marginTop: 4,
612
+ }}
613
+ />
614
+ )}
615
+ </View>
616
+ <View style={{ flex: 1, paddingBottom: last ? 0 : 14 }}>
617
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
618
+ <AgentIcon
619
+ color={theme.colors.textMuted}
620
+ name={item.kind === "tool-call" ? toolIconName(item.name) : "sparkle"}
621
+ size={14}
622
+ />
623
+ <Text
624
+ style={{
625
+ color: theme.colors.text,
626
+ fontSize: theme.type.small + 1,
627
+ fontWeight: "500",
628
+ }}
629
+ >
630
+ {stepLabel(item)}
631
+ </Text>
632
+ {item.kind === "tool-call" && item.status === "running" ? (
633
+ waiting ? (
634
+ <Text
635
+ style={{
636
+ color: theme.colors.attention,
637
+ fontSize: theme.type.small,
638
+ fontWeight: "600",
639
+ }}
640
+ >
641
+ Waiting for you
642
+ </Text>
643
+ ) : (
644
+ <Pulse theme={theme} />
645
+ )
646
+ ) : null}
647
+ </View>
648
+ {detail ? (
649
+ <Text
650
+ numberOfLines={1}
651
+ style={{
652
+ marginTop: 3,
653
+ color: theme.colors.textMuted,
654
+ fontFamily: theme.type.mono,
655
+ fontSize: theme.type.small - 0.5,
656
+ }}
657
+ >
658
+ {detail}
659
+ </Text>
660
+ ) : null}
661
+ {reasoning ? (
662
+ <Text
663
+ numberOfLines={3}
664
+ style={{
665
+ marginTop: 3,
666
+ color: theme.colors.textMuted,
667
+ fontSize: theme.type.small,
668
+ lineHeight: theme.type.smallLine,
669
+ fontStyle: "italic",
670
+ }}
671
+ >
672
+ {reasoning}
673
+ </Text>
674
+ ) : null}
675
+ {output ? (
676
+ <View
677
+ style={{
678
+ marginTop: 6,
679
+ padding: 8,
680
+ borderRadius: theme.radius.control,
681
+ backgroundColor: theme.colors.codeBackground,
682
+ }}
683
+ >
684
+ <Text
685
+ numberOfLines={3}
686
+ style={{
687
+ color: theme.colors.textMuted,
688
+ fontFamily: theme.type.mono,
689
+ fontSize: theme.type.small - 1,
690
+ lineHeight: theme.type.smallLine - 1,
691
+ }}
692
+ >
693
+ {output}
694
+ </Text>
695
+ </View>
696
+ ) : null}
697
+ </View>
698
+ </View>
699
+ );
700
+ })}
701
+ </View>
702
+ );
703
+ }
704
+
705
+ /** Field: one bold card stating what is happening now, steps on demand. */
706
+ function CardSteps(props: StepSummaryProps) {
707
+ const { theme, running } = props;
708
+ const [open, setOpen] = useState(false);
709
+ const headline = props.waiting
710
+ ? "Waiting for you"
711
+ : running
712
+ ? props.current
713
+ ? (LIVE_VERBS[toolIconName(props.current.name)] ?? toolTitle(props.current))
714
+ : "Working on it"
715
+ : props.failed
716
+ ? "Couldn't finish"
717
+ : `Done · ${props.toolCount} ${props.toolCount === 1 ? "step" : "steps"}`;
718
+ const sub = props.waiting
719
+ ? "Answer below to continue"
720
+ : running
721
+ ? props.current
722
+ ? toolDetail(props.current)
723
+ : null
724
+ : props.duration
725
+ ? `Took ${props.duration}`
726
+ : null;
727
+ return (
728
+ <Pressable
729
+ accessibilityRole="button"
730
+ accessibilityState={{ expanded: open }}
731
+ onPress={() =>
732
+ props.onOpen ? props.onOpen(props.items, headline) : setOpen((value) => !value)
733
+ }
734
+ style={{
735
+ borderRadius: theme.radius.card,
736
+ backgroundColor: theme.colors.surface,
737
+ borderWidth: StyleSheet.hairlineWidth,
738
+ borderColor: theme.colors.border,
739
+ padding: 16,
740
+ minHeight: theme.layout.target,
741
+ }}
742
+ >
743
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
744
+ <View
745
+ style={{
746
+ width: 36,
747
+ height: 36,
748
+ borderRadius: 18,
749
+ alignItems: "center",
750
+ justifyContent: "center",
751
+ backgroundColor: props.waiting
752
+ ? theme.colors.attention
753
+ : running
754
+ ? theme.colors.accent
755
+ : props.failed
756
+ ? theme.colors.dangerSurface
757
+ : theme.colors.background,
758
+ }}
759
+ >
760
+ {props.waiting ? (
761
+ <AgentIcon color={theme.colors.onAccent} name="alert" size={18} />
762
+ ) : running ? (
763
+ <Pulse color={theme.colors.onAccent} theme={theme} />
764
+ ) : props.failed ? (
765
+ <AgentIcon color={theme.colors.danger} name="x" size={18} strokeWidth={2.5} />
766
+ ) : (
767
+ <AgentIcon color={theme.colors.success} name="check" size={18} strokeWidth={2.5} />
768
+ )}
769
+ </View>
770
+ <View style={{ flex: 1 }}>
771
+ <Text
772
+ style={{
773
+ color: theme.colors.text,
774
+ fontSize: theme.type.body,
775
+ fontWeight: theme.type.weightStrong,
776
+ }}
777
+ >
778
+ {headline}
779
+ </Text>
780
+ {sub ? (
781
+ <Text
782
+ numberOfLines={1}
783
+ style={{ color: theme.colors.textMuted, fontSize: theme.type.small, marginTop: 2 }}
784
+ >
785
+ {sub}
786
+ </Text>
787
+ ) : null}
788
+ </View>
789
+ <AgentIcon
790
+ color={theme.colors.textFaint}
791
+ name={open ? "chevron-down" : "chevron-right"}
792
+ size={20}
793
+ />
794
+ </View>
795
+ {open ? (
796
+ <Animated.View entering={FadeIn.duration(160)} style={{ marginTop: 14, gap: 12 }}>
797
+ {props.items.map((item) => (
798
+ <CompactStep item={item} key={item.id} theme={theme} />
799
+ ))}
800
+ </Animated.View>
801
+ ) : null}
802
+ </Pressable>
803
+ );
804
+ }
805
+
806
+ export function WorkingLine({ theme, label }: { theme: AgentTheme; label: string }) {
807
+ return (
808
+ <View
809
+ style={{
810
+ flexDirection: "row",
811
+ alignItems: "center",
812
+ gap: 8,
813
+ marginVertical: theme.space.row,
814
+ minHeight: 32,
815
+ }}
816
+ >
817
+ <Pulse theme={theme} />
818
+ <Text style={{ color: theme.colors.textMuted, fontSize: theme.type.small + 1 }}>{label}</Text>
819
+ </View>
820
+ );
821
+ }
822
+
823
+ function Pulse({ theme, color }: { theme: AgentTheme; color?: string }) {
824
+ const opacity = useSharedValue(0.35);
825
+ useEffect(() => {
826
+ opacity.value = withRepeat(withTiming(1, { duration: 700 }), -1, true);
827
+ }, [opacity]);
828
+ const style = useAnimatedStyle(() => ({ opacity: opacity.value }));
829
+ return (
830
+ <Animated.View
831
+ style={[
832
+ { width: 8, height: 8, borderRadius: 4, backgroundColor: color ?? theme.colors.accent },
833
+ style,
834
+ ]}
835
+ />
836
+ );
837
+ }