@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.
- package/LICENSE +190 -0
- package/README.md +74 -0
- package/dist/adapters.d.ts +53 -0
- package/dist/adapters.js +3 -0
- package/dist/adapters.js.map +1 -0
- package/dist/attachments.d.ts +75 -0
- package/dist/chunk-3VSZAQ7G.js +391 -0
- package/dist/chunk-3VSZAQ7G.js.map +1 -0
- package/dist/chunk-5ZVK7WKA.js +88 -0
- package/dist/chunk-5ZVK7WKA.js.map +1 -0
- package/dist/chunk-EEDIMY3L.js +240 -0
- package/dist/chunk-EEDIMY3L.js.map +1 -0
- package/dist/chunk-M23SDI4R.js +83 -0
- package/dist/chunk-M23SDI4R.js.map +1 -0
- package/dist/chunk-PHJBSAR3.js +2006 -0
- package/dist/chunk-PHJBSAR3.js.map +1 -0
- package/dist/environment.d.ts +19 -0
- package/dist/expo.d.ts +25 -0
- package/dist/expo.js +267 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +3345 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation.d.ts +262 -0
- package/dist/read-acknowledgement.d.ts +39 -0
- package/dist/realtime/call-events.d.ts +11 -0
- package/dist/realtime/call-view.d.ts +26 -0
- package/dist/realtime/expo-call.d.ts +11 -0
- package/dist/realtime/native-call.d.ts +104 -0
- package/dist/realtime/native-realtime.d.ts +35 -0
- package/dist/realtime/use-native-session-realtime.d.ts +25 -0
- package/dist/session-view.d.ts +87 -0
- package/dist/timeline/activity.d.ts +34 -0
- package/dist/timeline/composer.d.ts +86 -0
- package/dist/timeline/controls.d.ts +27 -0
- package/dist/timeline/decisions.d.ts +29 -0
- package/dist/timeline/feedback.d.ts +26 -0
- package/dist/timeline/icon-nodes.generated.d.ts +738 -0
- package/dist/timeline/icon.d.ts +11 -0
- package/dist/timeline/index.d.ts +32 -0
- package/dist/timeline/index.js +5419 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/timeline/markdown.d.ts +11 -0
- package/dist/timeline/markdown.js +415 -0
- package/dist/timeline/markdown.js.map +1 -0
- package/dist/timeline/message-attachments.d.ts +13 -0
- package/dist/timeline/message-timeline.d.ts +36 -0
- package/dist/timeline/model-picker.d.ts +17 -0
- package/dist/timeline/primitives.d.ts +85 -0
- package/dist/timeline/queue-dock.d.ts +19 -0
- package/dist/timeline/session-actions.d.ts +34 -0
- package/dist/timeline/session-list.d.ts +50 -0
- package/dist/timeline/session-screen.d.ts +35 -0
- package/dist/timeline/session-signals.d.ts +37 -0
- package/dist/timeline/sheet.d.ts +14 -0
- package/dist/timeline/theme.d.ts +58 -0
- package/dist/timeline/thinking-orb.d.ts +13 -0
- package/dist/timeline/turn.d.ts +80 -0
- package/dist/ui/attention.d.ts +14 -0
- package/dist/ui/cards.d.ts +21 -0
- package/dist/ui/composer.d.ts +21 -0
- package/dist/ui/format.d.ts +13 -0
- package/dist/ui/icons.d.ts +9 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +1757 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/steps-sheet.d.ts +10 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/timeline.d.ts +48 -0
- package/dist/ui/web-tokens.gen.d.ts +90 -0
- package/dist/use-native-session.d.ts +40 -0
- package/dist/view-model.d.ts +52 -0
- package/dist/voice-input.d.ts +102 -0
- package/dist/webrtc.d.ts +9 -0
- package/dist/webrtc.js +19 -0
- package/dist/webrtc.js.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/OpenGeniCall.podspec +22 -0
- package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
- package/ios/OpenGeniCallCenter.swift +189 -0
- package/ios/OpenGeniCallModule.swift +64 -0
- package/package.json +93 -0
- package/src/adapters.ts +150 -0
- package/src/attachments.ts +396 -0
- package/src/environment.tsx +214 -0
- package/src/expo.ts +295 -0
- package/src/index.ts +98 -0
- package/src/presentation.ts +285 -0
- package/src/read-acknowledgement.ts +72 -0
- package/src/realtime/call-events.ts +41 -0
- package/src/realtime/call-view.tsx +220 -0
- package/src/realtime/expo-call.ts +49 -0
- package/src/realtime/native-call.ts +266 -0
- package/src/realtime/native-realtime.ts +115 -0
- package/src/realtime/use-native-session-realtime.ts +29 -0
- package/src/session-view.tsx +2251 -0
- package/src/timeline/activity.tsx +736 -0
- package/src/timeline/composer.tsx +686 -0
- package/src/timeline/controls.tsx +151 -0
- package/src/timeline/decisions.tsx +651 -0
- package/src/timeline/feedback.tsx +251 -0
- package/src/timeline/icon-nodes.generated.ts +1830 -0
- package/src/timeline/icon.tsx +44 -0
- package/src/timeline/index.ts +67 -0
- package/src/timeline/markdown.tsx +510 -0
- package/src/timeline/message-attachments.tsx +155 -0
- package/src/timeline/message-timeline.tsx +1042 -0
- package/src/timeline/model-picker.tsx +450 -0
- package/src/timeline/primitives.tsx +561 -0
- package/src/timeline/queue-dock.tsx +331 -0
- package/src/timeline/session-actions.tsx +379 -0
- package/src/timeline/session-list.tsx +338 -0
- package/src/timeline/session-screen.tsx +488 -0
- package/src/timeline/session-signals.tsx +312 -0
- package/src/timeline/sheet.tsx +153 -0
- package/src/timeline/theme.tsx +128 -0
- package/src/timeline/thinking-orb.tsx +93 -0
- package/src/timeline/turn.tsx +569 -0
- package/src/ui/attention.tsx +220 -0
- package/src/ui/cards.tsx +267 -0
- package/src/ui/composer.tsx +296 -0
- package/src/ui/format.ts +86 -0
- package/src/ui/icons.tsx +131 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/steps-sheet.tsx +71 -0
- package/src/ui/theme.ts +261 -0
- package/src/ui/timeline.tsx +837 -0
- package/src/ui/web-tokens.gen.ts +94 -0
- package/src/use-native-session.ts +219 -0
- package/src/view-model.ts +485 -0
- package/src/voice-input.ts +382 -0
- package/src/webrtc.ts +30 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import type { ActivityItem, TurnOutcome } from "@opengeni/react/session";
|
|
2
|
+
import {
|
|
3
|
+
createTurnSummaryContext,
|
|
4
|
+
formatElapsed,
|
|
5
|
+
GENIE_PREPARING_PHRASES,
|
|
6
|
+
GENIE_WAITING_PHRASES,
|
|
7
|
+
resolveTurnSummaryFacets,
|
|
8
|
+
rollingActivityItem,
|
|
9
|
+
selectTurnSummaryFacets,
|
|
10
|
+
toolDisplayName,
|
|
11
|
+
type TurnSummaryFacetConfiguration,
|
|
12
|
+
} from "@opengeni/react/timeline-model";
|
|
13
|
+
import {
|
|
14
|
+
createContext,
|
|
15
|
+
useContext,
|
|
16
|
+
useEffect,
|
|
17
|
+
useMemo,
|
|
18
|
+
useRef,
|
|
19
|
+
useState,
|
|
20
|
+
type ReactNode,
|
|
21
|
+
} from "react";
|
|
22
|
+
import { Platform, Pressable, Text, View, type TextStyle } from "react-native";
|
|
23
|
+
import Animated, { FadeIn, FadeInDown, FadeOut, FadeOutUp } from "react-native-reanimated";
|
|
24
|
+
import { ActivityRow } from "./activity";
|
|
25
|
+
import { ThinkingOrb } from "./thinking-orb";
|
|
26
|
+
import { Icon } from "./icon";
|
|
27
|
+
import { PulseDot, ShimmerText, ROW_MIN_HEIGHT, useReleaseFollow } from "./primitives";
|
|
28
|
+
import { fontStyle, useNativeTimelineTheme } from "./theme";
|
|
29
|
+
|
|
30
|
+
/* ----------------------------------------------------------------------------
|
|
31
|
+
Native TurnSummary / ActivityRail / RollingActivity — the web components of
|
|
32
|
+
the same names (timeline/turn-summary.tsx, activity-rail.tsx,
|
|
33
|
+
rolling-activity.tsx) in their rolling-turn configuration.
|
|
34
|
+
-------------------------------------------------------------------------- */
|
|
35
|
+
|
|
36
|
+
/** Native counterpart of the web `TurnSummaryStatus` with a renderable preview. */
|
|
37
|
+
export type NativeTurnStatus =
|
|
38
|
+
| { kind: "worked"; durationMs?: number | undefined; label?: string | undefined }
|
|
39
|
+
| {
|
|
40
|
+
kind: "working" | "waiting";
|
|
41
|
+
since?: string | undefined;
|
|
42
|
+
label?: string | undefined;
|
|
43
|
+
preview?: ReactNode;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type FoldMemory = Map<string, "open" | "closed">;
|
|
47
|
+
const FoldMemoryContext = createContext<FoldMemory | null>(null);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A shrink-wrapped single-line summary. Android sizes it to the ellipsized line without
|
|
51
|
+
* the "…" glyph's overhang, which then clips the last dot; a little trailing room keeps
|
|
52
|
+
* the ellipsis whole.
|
|
53
|
+
*/
|
|
54
|
+
const TRUNCATED_TAIL: TextStyle =
|
|
55
|
+
Platform.OS === "android" ? { flexShrink: 1, paddingRight: 2 } : { flexShrink: 1 };
|
|
56
|
+
export const FoldMemoryProvider = FoldMemoryContext.Provider;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An expanded outer turn header stays pinned to the top of the timeline while
|
|
60
|
+
* its section scrolls (web: sticky inside the section). Each open section
|
|
61
|
+
* reports its content-space frame and a renderer for the header row; the
|
|
62
|
+
* timeline draws the pinned copy over the scroll view.
|
|
63
|
+
*/
|
|
64
|
+
export type StickyWorkHeader = {
|
|
65
|
+
top: number;
|
|
66
|
+
height: number;
|
|
67
|
+
headerHeight: number;
|
|
68
|
+
render: () => ReactNode;
|
|
69
|
+
};
|
|
70
|
+
export type StickyWorkHeaderRegistry = {
|
|
71
|
+
register: (key: string, entry: StickyWorkHeader | null) => void;
|
|
72
|
+
/** Bring a section's top back into view (after collapsing from the pinned copy). */
|
|
73
|
+
reveal: (top: number) => void;
|
|
74
|
+
};
|
|
75
|
+
const StickyWorkHeaderContext = createContext<StickyWorkHeaderRegistry | null>(null);
|
|
76
|
+
export const StickyWorkHeaderProvider = StickyWorkHeaderContext.Provider;
|
|
77
|
+
|
|
78
|
+
export function useLiveNow(active: boolean): number {
|
|
79
|
+
const [now, setNow] = useState(() => Date.now());
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!active) return;
|
|
82
|
+
const timer = setInterval(() => setNow(Date.now()), 1000);
|
|
83
|
+
return () => clearInterval(timer);
|
|
84
|
+
}, [active]);
|
|
85
|
+
return now;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function LiveElapsed({ since, style }: { since: string; style: TextStyle }) {
|
|
89
|
+
const startedAt = Date.parse(since);
|
|
90
|
+
const now = useLiveNow(true);
|
|
91
|
+
if (!Number.isFinite(startedAt)) return null;
|
|
92
|
+
return (
|
|
93
|
+
<Text
|
|
94
|
+
style={[style, { fontVariant: ["tabular-nums"] }]}
|
|
95
|
+
>{` · ${formatElapsed(Math.max(0, now - startedAt))}`}</Text>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface TurnSummaryProps {
|
|
100
|
+
items: ActivityItem[];
|
|
101
|
+
outcome?: TurnOutcome | undefined;
|
|
102
|
+
failureText?: string | undefined;
|
|
103
|
+
durationMs?: number | undefined;
|
|
104
|
+
defaultOpen?: boolean | undefined;
|
|
105
|
+
liveHeader?: ReactNode;
|
|
106
|
+
bare?: boolean | undefined;
|
|
107
|
+
facets?: TurnSummaryFacetConfiguration | undefined;
|
|
108
|
+
foldKey?: string | undefined;
|
|
109
|
+
contextCompactionCount?: number | undefined;
|
|
110
|
+
status?: NativeTurnStatus | undefined;
|
|
111
|
+
children: ReactNode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function TurnSummary({
|
|
115
|
+
items,
|
|
116
|
+
outcome,
|
|
117
|
+
failureText,
|
|
118
|
+
durationMs,
|
|
119
|
+
defaultOpen,
|
|
120
|
+
liveHeader,
|
|
121
|
+
bare,
|
|
122
|
+
facets: facetConfiguration,
|
|
123
|
+
foldKey,
|
|
124
|
+
contextCompactionCount,
|
|
125
|
+
status,
|
|
126
|
+
children,
|
|
127
|
+
}: TurnSummaryProps) {
|
|
128
|
+
const theme = useNativeTimelineTheme();
|
|
129
|
+
const foldMemory = useContext(FoldMemoryContext);
|
|
130
|
+
const registerSticky = useContext(StickyWorkHeaderContext);
|
|
131
|
+
const releaseFollow = useReleaseFollow();
|
|
132
|
+
const frame = useRef<{ top: number; height: number; headerHeight: number }>({
|
|
133
|
+
top: 0,
|
|
134
|
+
height: 0,
|
|
135
|
+
headerHeight: 0,
|
|
136
|
+
});
|
|
137
|
+
const remembered = foldKey !== undefined ? foldMemory?.get(foldKey) : undefined;
|
|
138
|
+
const [open, setOpen] = useState(
|
|
139
|
+
remembered === "closed" ? false : remembered === "open" ? true : (defaultOpen ?? false),
|
|
140
|
+
);
|
|
141
|
+
const readerOwnsOpen = useRef(remembered !== undefined);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (status && defaultOpen && remembered === undefined && !readerOwnsOpen.current) setOpen(true);
|
|
144
|
+
}, [status, defaultOpen, remembered]);
|
|
145
|
+
const toggle = (next: boolean) => {
|
|
146
|
+
releaseFollow();
|
|
147
|
+
readerOwnsOpen.current = true;
|
|
148
|
+
if (foldKey !== undefined) foldMemory?.set(foldKey, next ? "open" : "closed");
|
|
149
|
+
setOpen(next);
|
|
150
|
+
};
|
|
151
|
+
const context = useMemo(
|
|
152
|
+
() =>
|
|
153
|
+
createTurnSummaryContext(
|
|
154
|
+
items,
|
|
155
|
+
outcome,
|
|
156
|
+
failureText,
|
|
157
|
+
durationMs,
|
|
158
|
+
contextCompactionCount ?? 0,
|
|
159
|
+
),
|
|
160
|
+
[items, outcome, failureText, durationMs, contextCompactionCount],
|
|
161
|
+
);
|
|
162
|
+
const definitions = useMemo(
|
|
163
|
+
() => resolveTurnSummaryFacets(facetConfiguration),
|
|
164
|
+
[facetConfiguration],
|
|
165
|
+
);
|
|
166
|
+
const facets = useMemo(
|
|
167
|
+
() => selectTurnSummaryFacets(definitions, context, status?.kind),
|
|
168
|
+
[definitions, context, status?.kind],
|
|
169
|
+
);
|
|
170
|
+
const liveShell = outcome === undefined && open && !bare;
|
|
171
|
+
const text: TextStyle = {
|
|
172
|
+
...fontStyle(theme),
|
|
173
|
+
fontSize: bare ? theme.size.sm : theme.size.base,
|
|
174
|
+
lineHeight: 20,
|
|
175
|
+
color: liveShell && !status ? theme.colors["fg-subtle"] : theme.colors["fg-muted"],
|
|
176
|
+
};
|
|
177
|
+
const showMarker = !(
|
|
178
|
+
status ||
|
|
179
|
+
outcome === "complete" ||
|
|
180
|
+
(!outcome && (liveHeader || context.settled))
|
|
181
|
+
);
|
|
182
|
+
const facetText = facets
|
|
183
|
+
.map(({ result }) =>
|
|
184
|
+
typeof result.content === "string" || typeof result.content === "number"
|
|
185
|
+
? String(result.content)
|
|
186
|
+
: "",
|
|
187
|
+
)
|
|
188
|
+
.filter(Boolean);
|
|
189
|
+
|
|
190
|
+
let statusNode: ReactNode = null;
|
|
191
|
+
let hasStatusText = false;
|
|
192
|
+
if (status?.kind === "worked") {
|
|
193
|
+
if (
|
|
194
|
+
status.durationMs !== undefined &&
|
|
195
|
+
Number.isFinite(status.durationMs) &&
|
|
196
|
+
status.durationMs >= 1000
|
|
197
|
+
) {
|
|
198
|
+
hasStatusText = true;
|
|
199
|
+
statusNode = (
|
|
200
|
+
<Text
|
|
201
|
+
style={text}
|
|
202
|
+
>{`${status.label ?? "Worked for"} ${formatElapsed(status.durationMs)}`}</Text>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
} else if (status) {
|
|
206
|
+
hasStatusText = true;
|
|
207
|
+
const label = status.label ?? (status.kind === "working" ? "Working" : "Waiting");
|
|
208
|
+
statusNode = (
|
|
209
|
+
<>
|
|
210
|
+
<ShimmerText style={text} active={status.kind === "working"}>
|
|
211
|
+
{label}
|
|
212
|
+
</ShimmerText>
|
|
213
|
+
{status.since ? <LiveElapsed since={status.since} style={text} /> : null}
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const showLive = Boolean(liveHeader) && !open && !status;
|
|
218
|
+
const tail =
|
|
219
|
+
(showLive
|
|
220
|
+
? ""
|
|
221
|
+
: facetText
|
|
222
|
+
.map((value, index) => (index > 0 || hasStatusText ? ` · ${value}` : value))
|
|
223
|
+
.join("")) + "";
|
|
224
|
+
|
|
225
|
+
const renderHeader = (pinned: boolean) => (
|
|
226
|
+
<Pressable
|
|
227
|
+
accessibilityRole="button"
|
|
228
|
+
accessibilityState={{ expanded: open }}
|
|
229
|
+
onPress={() => (pinned ? collapseFromPin() : toggle(!open))}
|
|
230
|
+
style={({ pressed }) => ({
|
|
231
|
+
flexDirection: "row",
|
|
232
|
+
alignItems: "center",
|
|
233
|
+
minHeight: ROW_MIN_HEIGHT,
|
|
234
|
+
paddingVertical: 10,
|
|
235
|
+
borderRadius: theme.radius.sm,
|
|
236
|
+
// Web `-mx-2 px-2 w-full`: shifted left 8 with the same width.
|
|
237
|
+
...(bare
|
|
238
|
+
? { gap: 8, paddingHorizontal: 6 }
|
|
239
|
+
: { gap: 10, paddingHorizontal: 8, marginLeft: -8, marginRight: 8 }),
|
|
240
|
+
backgroundColor: pressed
|
|
241
|
+
? theme.colors.hover
|
|
242
|
+
: !bare && open
|
|
243
|
+
? theme.colors.bg
|
|
244
|
+
: "transparent",
|
|
245
|
+
})}
|
|
246
|
+
>
|
|
247
|
+
<View style={{ transform: [{ rotate: open ? "90deg" : "0deg" }] }}>
|
|
248
|
+
<Icon name="chevron-right" size={14} color={theme.colors["fg-subtle"]} />
|
|
249
|
+
</View>
|
|
250
|
+
{showMarker ? (
|
|
251
|
+
<View
|
|
252
|
+
style={{
|
|
253
|
+
width: bare ? 14 : 20,
|
|
254
|
+
height: bare ? 14 : 20,
|
|
255
|
+
alignItems: "center",
|
|
256
|
+
justifyContent: "center",
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{outcome === "failed" ? (
|
|
260
|
+
<Icon name="triangle-alert" size={12} color={theme.colors["status-failed"]} />
|
|
261
|
+
) : outcome === "cancelled" ? (
|
|
262
|
+
<Icon name="circle-slash" size={12} color={theme.colors["fg-subtle"]} />
|
|
263
|
+
) : (
|
|
264
|
+
<PulseDot color={theme.colors["fg-subtle"]} />
|
|
265
|
+
)}
|
|
266
|
+
</View>
|
|
267
|
+
) : null}
|
|
268
|
+
<View
|
|
269
|
+
style={{
|
|
270
|
+
flexDirection: "row",
|
|
271
|
+
alignItems: "center",
|
|
272
|
+
minWidth: 0,
|
|
273
|
+
...(status?.kind === "worked" ? { flexShrink: 1 } : { flex: 1 }),
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
{statusNode}
|
|
277
|
+
{showLive ? (
|
|
278
|
+
<View style={{ flex: 1, minWidth: 0 }}>{liveHeader}</View>
|
|
279
|
+
) : (
|
|
280
|
+
<Text style={[text, TRUNCATED_TAIL]} numberOfLines={1}>
|
|
281
|
+
{tail}
|
|
282
|
+
{outcome === "failed" && failureText ? (
|
|
283
|
+
<Text style={{ color: theme.colors["status-failed"] }}>{` · ${failureText}`}</Text>
|
|
284
|
+
) : null}
|
|
285
|
+
{outcome === "cancelled" ? (
|
|
286
|
+
<Text style={{ color: theme.colors["fg-subtle"] }}>{" · interrupted"}</Text>
|
|
287
|
+
) : null}
|
|
288
|
+
</Text>
|
|
289
|
+
)}
|
|
290
|
+
</View>
|
|
291
|
+
{status?.kind === "worked" ? (
|
|
292
|
+
<View
|
|
293
|
+
style={{
|
|
294
|
+
marginLeft: 4,
|
|
295
|
+
height: 1,
|
|
296
|
+
flex: 1,
|
|
297
|
+
minWidth: 0,
|
|
298
|
+
backgroundColor: theme.colors.border,
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
) : null}
|
|
302
|
+
{status && status.kind !== "worked" && (contextCompactionCount ?? 0) > 0 ? (
|
|
303
|
+
<Icon name="shrink" size={14} color={theme.colors["fg-muted"]} />
|
|
304
|
+
) : null}
|
|
305
|
+
</Pressable>
|
|
306
|
+
);
|
|
307
|
+
const sticky = !bare && open && foldKey !== undefined && registerSticky !== null;
|
|
308
|
+
const collapseFromPin = () => {
|
|
309
|
+
toggle(false);
|
|
310
|
+
registerSticky?.reveal(frame.current.top);
|
|
311
|
+
};
|
|
312
|
+
const report = () => {
|
|
313
|
+
if (!sticky || !registerSticky || foldKey === undefined) return;
|
|
314
|
+
registerSticky.register(foldKey, { ...frame.current, render: () => renderHeader(true) });
|
|
315
|
+
};
|
|
316
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- re-report when the row content changes
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (!sticky) {
|
|
319
|
+
if (registerSticky && foldKey !== undefined) registerSticky.register(foldKey, null);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
report();
|
|
323
|
+
return () => registerSticky?.register(foldKey!, null);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<View
|
|
328
|
+
onLayout={(event) => {
|
|
329
|
+
const { y, height } = event.nativeEvent.layout;
|
|
330
|
+
frame.current = { ...frame.current, top: y, height };
|
|
331
|
+
report();
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<View
|
|
335
|
+
onLayout={(event) => {
|
|
336
|
+
frame.current = { ...frame.current, headerHeight: event.nativeEvent.layout.height };
|
|
337
|
+
report();
|
|
338
|
+
}}
|
|
339
|
+
>
|
|
340
|
+
{renderHeader(false)}
|
|
341
|
+
</View>
|
|
342
|
+
{status && status.kind !== "worked" && !open && status.preview ? (
|
|
343
|
+
<Pressable
|
|
344
|
+
onPress={() => toggle(true)}
|
|
345
|
+
style={{ paddingBottom: 4, paddingLeft: 24, minWidth: 0 }}
|
|
346
|
+
>
|
|
347
|
+
{status.preview}
|
|
348
|
+
</Pressable>
|
|
349
|
+
) : null}
|
|
350
|
+
{open ? (
|
|
351
|
+
<Animated.View
|
|
352
|
+
entering={FadeIn.duration(160)}
|
|
353
|
+
style={bare ? { paddingTop: 4, paddingLeft: 20 } : { paddingTop: 8 }}
|
|
354
|
+
>
|
|
355
|
+
{children}
|
|
356
|
+
</Animated.View>
|
|
357
|
+
) : null}
|
|
358
|
+
</View>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The stable left rule under a turn/activity body (web TurnRailFrame). */
|
|
363
|
+
export function TurnRailFrame({
|
|
364
|
+
children,
|
|
365
|
+
compact = false,
|
|
366
|
+
}: {
|
|
367
|
+
children: ReactNode;
|
|
368
|
+
compact?: boolean;
|
|
369
|
+
}) {
|
|
370
|
+
const theme = useNativeTimelineTheme();
|
|
371
|
+
return (
|
|
372
|
+
<View
|
|
373
|
+
style={{
|
|
374
|
+
borderLeftWidth: 2,
|
|
375
|
+
borderLeftColor: theme.colors.border,
|
|
376
|
+
paddingLeft: 12,
|
|
377
|
+
gap: compact ? 2 : 16,
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
{children}
|
|
381
|
+
</View>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function familyOf(item: ActivityItem): string {
|
|
386
|
+
if (item.kind === "tool-call") return `tool:${item.name}`;
|
|
387
|
+
return item.kind;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** The native ActivityRail (bare): tight same-family rows, breathing room between families. */
|
|
391
|
+
export function ActivityRail({
|
|
392
|
+
items,
|
|
393
|
+
startupActive,
|
|
394
|
+
renderPreparing,
|
|
395
|
+
}: {
|
|
396
|
+
items: ActivityItem[];
|
|
397
|
+
startupActive?: boolean | undefined;
|
|
398
|
+
renderPreparing?: ((props: PreparingProps) => ReactNode) | undefined;
|
|
399
|
+
}) {
|
|
400
|
+
const phases = items.filter((item) => item.kind === "startup-phase");
|
|
401
|
+
const hasWork = items.some(
|
|
402
|
+
(item) =>
|
|
403
|
+
item.kind !== "startup-phase" && (item.kind !== "reasoning" || item.text.trim().length > 0),
|
|
404
|
+
);
|
|
405
|
+
const interrupted = phases.some(
|
|
406
|
+
(item) => item.status === "failed" || item.status === "cancelled",
|
|
407
|
+
);
|
|
408
|
+
const providerResponded = phases.some(
|
|
409
|
+
(item) => item.phase === "provider_first_byte" && item.status === "complete",
|
|
410
|
+
);
|
|
411
|
+
const responsePending = phases.some((item) => item.phase === "provider_first_byte");
|
|
412
|
+
const loading =
|
|
413
|
+
!hasWork &&
|
|
414
|
+
!interrupted &&
|
|
415
|
+
(startupActive ?? (!providerResponded && phases.some((item) => item.status === "running")));
|
|
416
|
+
const visibleItems = items.filter((item) =>
|
|
417
|
+
item.kind === "startup-phase"
|
|
418
|
+
? item.status === "failed" || item.status === "cancelled"
|
|
419
|
+
: item.kind !== "reasoning" || item.text.trim().length > 0,
|
|
420
|
+
);
|
|
421
|
+
const startedAt = phases.reduce(
|
|
422
|
+
(first, item) => (item.startedAt < first ? item.startedAt : first),
|
|
423
|
+
phases[0]?.startedAt ?? "",
|
|
424
|
+
);
|
|
425
|
+
return (
|
|
426
|
+
<View style={{ gap: 2 }}>
|
|
427
|
+
{loading ? (
|
|
428
|
+
<Animated.View entering={FadeIn.duration(160)} exiting={FadeOut.duration(160)}>
|
|
429
|
+
<PreparingState
|
|
430
|
+
startedAt={startedAt}
|
|
431
|
+
phase={responsePending ? "waiting" : "preparing"}
|
|
432
|
+
render={renderPreparing}
|
|
433
|
+
/>
|
|
434
|
+
</Animated.View>
|
|
435
|
+
) : null}
|
|
436
|
+
{visibleItems.map((item, index) => {
|
|
437
|
+
const newFamily = index > 0 && familyOf(item) !== familyOf(visibleItems[index - 1]!);
|
|
438
|
+
return (
|
|
439
|
+
<View key={item.id} style={newFamily ? { marginTop: 12 } : undefined}>
|
|
440
|
+
<ActivityRow item={item} />
|
|
441
|
+
</View>
|
|
442
|
+
);
|
|
443
|
+
})}
|
|
444
|
+
</View>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface PreparingProps {
|
|
449
|
+
startedAt: string;
|
|
450
|
+
phase: "preparing" | "waiting";
|
|
451
|
+
phrase: string;
|
|
452
|
+
slow: boolean;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The web GenieLoading: a visual (orb by default; hosts supply a character) + rotating copy. */
|
|
456
|
+
export function PreparingState({
|
|
457
|
+
startedAt,
|
|
458
|
+
phase,
|
|
459
|
+
render,
|
|
460
|
+
}: {
|
|
461
|
+
startedAt: string;
|
|
462
|
+
phase: "preparing" | "waiting";
|
|
463
|
+
render?: ((props: PreparingProps) => ReactNode) | undefined;
|
|
464
|
+
}) {
|
|
465
|
+
const theme = useNativeTimelineTheme();
|
|
466
|
+
const phrases = phase === "waiting" ? GENIE_WAITING_PHRASES : GENIE_PREPARING_PHRASES;
|
|
467
|
+
const [index, setIndex] = useState(() => Math.floor(Math.random() * phrases.length));
|
|
468
|
+
const [slow, setSlow] = useState(false);
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
const update = () => {
|
|
471
|
+
setSlow(Date.now() - Date.parse(startedAt) >= 30_000);
|
|
472
|
+
setIndex(Math.floor(Math.random() * phrases.length));
|
|
473
|
+
};
|
|
474
|
+
const timer = setInterval(update, 5_000);
|
|
475
|
+
return () => clearInterval(timer);
|
|
476
|
+
}, [startedAt, phrases]);
|
|
477
|
+
const phrase = slow
|
|
478
|
+
? phase === "waiting"
|
|
479
|
+
? "Still waiting for a response…"
|
|
480
|
+
: "A little longer than usual…"
|
|
481
|
+
: (phrases[index % phrases.length] ?? "");
|
|
482
|
+
if (render) return <>{render({ startedAt, phase, phrase, slow })}</>;
|
|
483
|
+
return (
|
|
484
|
+
<View
|
|
485
|
+
style={{
|
|
486
|
+
flexDirection: "row",
|
|
487
|
+
alignItems: "center",
|
|
488
|
+
gap: 14,
|
|
489
|
+
minHeight: 88,
|
|
490
|
+
paddingVertical: 12,
|
|
491
|
+
}}
|
|
492
|
+
>
|
|
493
|
+
<ThinkingOrb />
|
|
494
|
+
<Animated.Text
|
|
495
|
+
key={phrase}
|
|
496
|
+
entering={FadeIn.duration(650)}
|
|
497
|
+
style={{
|
|
498
|
+
...fontStyle(theme),
|
|
499
|
+
fontSize: theme.size.sm,
|
|
500
|
+
lineHeight: 18,
|
|
501
|
+
color: theme.colors["fg-muted"],
|
|
502
|
+
}}
|
|
503
|
+
>
|
|
504
|
+
{phrase}
|
|
505
|
+
</Animated.Text>
|
|
506
|
+
</View>
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** One stable 24pt viewport; updates replace its content without moving the conversation. */
|
|
511
|
+
export function RollingActivity({
|
|
512
|
+
items,
|
|
513
|
+
previousItem,
|
|
514
|
+
showCount = true,
|
|
515
|
+
}: {
|
|
516
|
+
items: ActivityItem[];
|
|
517
|
+
previousItem?: ActivityItem | undefined;
|
|
518
|
+
showCount?: boolean;
|
|
519
|
+
}) {
|
|
520
|
+
const theme = useNativeTimelineTheme();
|
|
521
|
+
const [mounted, setMounted] = useState(false);
|
|
522
|
+
useEffect(() => setMounted(true), []);
|
|
523
|
+
const selected = rollingActivityItem(items, previousItem, mounted);
|
|
524
|
+
if (!selected) return null;
|
|
525
|
+
const { item, earlierCount } = selected;
|
|
526
|
+
return (
|
|
527
|
+
<View
|
|
528
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 12, minWidth: 0, flex: 1 }}
|
|
529
|
+
accessibilityLabel={
|
|
530
|
+
item.kind === "tool-call"
|
|
531
|
+
? toolDisplayName(item.name, item.display)
|
|
532
|
+
: item.kind === "reasoning"
|
|
533
|
+
? "Thinking"
|
|
534
|
+
: "Working"
|
|
535
|
+
}
|
|
536
|
+
>
|
|
537
|
+
<View style={{ flex: 1, minWidth: 0, height: 24, overflow: "hidden" }}>
|
|
538
|
+
<Animated.View
|
|
539
|
+
key={item.id}
|
|
540
|
+
entering={FadeInDown.duration(400)}
|
|
541
|
+
exiting={FadeOutUp.duration(400)}
|
|
542
|
+
style={{
|
|
543
|
+
position: "absolute",
|
|
544
|
+
left: 0,
|
|
545
|
+
right: 0,
|
|
546
|
+
top: 0,
|
|
547
|
+
bottom: 0,
|
|
548
|
+
flexDirection: "row",
|
|
549
|
+
alignItems: "center",
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
<ActivityRow item={item} compact />
|
|
553
|
+
</Animated.View>
|
|
554
|
+
</View>
|
|
555
|
+
{showCount && earlierCount > 0 ? (
|
|
556
|
+
<Text
|
|
557
|
+
style={{
|
|
558
|
+
...fontStyle(theme),
|
|
559
|
+
fontSize: theme.size.xs,
|
|
560
|
+
color: theme.colors["fg-subtle"],
|
|
561
|
+
fontVariant: ["tabular-nums"],
|
|
562
|
+
}}
|
|
563
|
+
>
|
|
564
|
+
{`+${earlierCount} earlier`}
|
|
565
|
+
</Text>
|
|
566
|
+
) : null}
|
|
567
|
+
</View>
|
|
568
|
+
);
|
|
569
|
+
}
|