@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,488 @@
1
+ import {
2
+ conversationTimeline,
3
+ type AgentMessageItem,
4
+ type UserMessageItem,
5
+ } from "@opengeni/react/session";
6
+ import {
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ type ComponentRef,
13
+ type ReactNode,
14
+ } from "react";
15
+ import { ActivityIndicator, Pressable, Text, View, type TextInput } from "react-native";
16
+ import {
17
+ defaultChatComposerMessages,
18
+ type ChatComposerMessages,
19
+ } from "@opengeni/react/composer-messages";
20
+ import { Icon } from "./icon";
21
+ import { withAlpha } from "./primitives";
22
+ import type { OpenGeniNativeSessionController } from "../use-native-session";
23
+ import type { NativeFileAttachmentsResult } from "../attachments";
24
+ import { SessionComposer, type SessionComposerProps } from "./composer";
25
+ import { TurnFeedbackButtons, useTurnRatings, type TurnFeedbackTarget } from "./feedback";
26
+ import { Button } from "./controls";
27
+ import {
28
+ ApprovalStrip,
29
+ HumanInputCard,
30
+ type ApprovalStripProps,
31
+ type HumanInputCardProps,
32
+ } from "./decisions";
33
+ import { NativeMessageAttachments } from "./message-attachments";
34
+ import { QueueDock } from "./queue-dock";
35
+ import { SessionCommandsList, SessionSignals } from "./session-signals";
36
+ import type { ClientModel, OpenGeniClient, SessionBackgroundCommand } from "@opengeni/sdk";
37
+ import { MessageTimeline, type NativeMessageTimelineProps } from "./message-timeline";
38
+ import { fontStyle, useNativeTimelineTheme } from "./theme";
39
+
40
+ /* ----------------------------------------------------------------------------
41
+ The web session page body at phone width: timeline (with the question card
42
+ as its trailing state), the live approval strip and the composer, driven by
43
+ the shared session controller. Navigation chrome belongs to the host.
44
+ -------------------------------------------------------------------------- */
45
+
46
+ export interface NativeSessionScreenProps extends Omit<
47
+ NativeMessageTimelineProps,
48
+ "items" | "events" | "status" | "trailing"
49
+ > {
50
+ controller: OpenGeniNativeSessionController;
51
+ /** Composer slots (products add their own actions/options here). */
52
+ composer?:
53
+ | Partial<
54
+ Pick<
55
+ SessionComposerProps,
56
+ | "renderLeading"
57
+ | "options"
58
+ | "header"
59
+ | "placeholder"
60
+ | "onAttach"
61
+ | "messages"
62
+ | "onActionFeedback"
63
+ | "voice"
64
+ >
65
+ >
66
+ | undefined;
67
+ /** Extra content after the question card (host recovery, banners). */
68
+ trailing?: ReactNode;
69
+ /** Fixed host chrome above the timeline, inside the keyboard-avoiding area. */
70
+ topBar?: ReactNode;
71
+ bottomInset?: number | undefined;
72
+ /** Distance from this screen's bottom edge to the window bottom (e.g. a tab bar). */
73
+ keyboardBottomOffset?: number | undefined;
74
+ /** Reply feedback (thumbs beside Copy), as the web timeline offers it. */
75
+ feedback?: TurnFeedbackTarget | undefined;
76
+ /** Translations for the question card (the shared human-input catalog). */
77
+ humanInputMessages?: HumanInputCardProps["messages"];
78
+ /** Translations for the approval strip. */
79
+ approvalMessages?: ApprovalStripProps["messages"];
80
+ /** Reads the commands panel (the full client; omit to hide command details). */
81
+ client?: OpenGeniClient | undefined;
82
+ /** Model catalog for the agents panel rows. */
83
+ models?: readonly ClientModel[] | undefined;
84
+ }
85
+
86
+ export function NativeSessionScreen({
87
+ controller,
88
+ composer: composerSlots,
89
+ trailing,
90
+ topBar,
91
+ bottomInset,
92
+ keyboardBottomOffset,
93
+ feedback,
94
+ humanInputMessages,
95
+ approvalMessages,
96
+ client,
97
+ models,
98
+ renderMessageActions: hostMessageActions,
99
+ ...timelineProps
100
+ }: NativeSessionScreenProps) {
101
+ const theme = useNativeTimelineTheme();
102
+ const { composer, queue, humanInput, approvals, control, attachments } = controller;
103
+ const composerInput = useRef<ComponentRef<typeof TextInput>>(null);
104
+ // The composer floats over the conversation; its occupied height insets the timeline.
105
+ const [composerHeight, setComposerHeight] = useState(120);
106
+ const items = useMemo(
107
+ () => conversationTimeline(controller.timeline, queue, composer),
108
+ [controller.timeline, queue, composer],
109
+ );
110
+ const status = controller.sessionStatus;
111
+ const paused = queue.effectiveControl?.state === "paused";
112
+ const waitingOnInput = humanInput.requests.length > 0 && status === "requires_action";
113
+ const busy = composer.sending || composer.pausing || composer.resuming;
114
+ const { ratings, rate } = useTurnRatings(feedback);
115
+ const failed = controller.connectionState === "error";
116
+ useAutoRecover(
117
+ failed && controller.active,
118
+ controller.connectionState === "live",
119
+ controller.refresh,
120
+ );
121
+ const empty = items.length === 0;
122
+ // Web order beside Copy: reply feedback, then host actions (fork, share…).
123
+ const renderMessageActions = useCallback(
124
+ (item: AgentMessageItem | UserMessageItem) => {
125
+ const host = hostMessageActions?.(item);
126
+ if (!feedback || item.kind !== "agent-message" || item.streaming || !item.turnId) return host;
127
+ const turnId = item.turnId;
128
+ return (
129
+ <>
130
+ <TurnFeedbackButtons
131
+ target={feedback}
132
+ turnId={turnId}
133
+ saved={ratings[turnId]}
134
+ onRated={(sentiment) => rate(turnId, sentiment)}
135
+ />
136
+ {host}
137
+ </>
138
+ );
139
+ },
140
+ [feedback, hostMessageActions, rate, ratings],
141
+ );
142
+ return (
143
+ <View style={{ flex: 1, backgroundColor: theme.colors.bg }}>
144
+ {topBar}
145
+ <MessageTimeline
146
+ {...timelineProps}
147
+ contentInsetBottom={composerHeight + 20}
148
+ overlayInsetBottom={composerHeight}
149
+ renderMessageActions={feedback || hostMessageActions ? renderMessageActions : undefined}
150
+ renderUserAttachments={
151
+ timelineProps.renderUserAttachments ??
152
+ (client
153
+ ? (item) => (
154
+ <NativeMessageAttachments
155
+ client={client}
156
+ workspaceId={controller.workspaceId}
157
+ sessionId={controller.sessionId}
158
+ resources={item.resources}
159
+ />
160
+ )
161
+ : undefined)
162
+ }
163
+ items={items}
164
+ status={status}
165
+ emptyState={
166
+ empty && failed ? (
167
+ <LoadFailure
168
+ message={loadFailureMessage(controller.error)}
169
+ onRetry={() => void controller.refresh()}
170
+ />
171
+ ) : empty && controller.initialLoading ? (
172
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 8 }}>
173
+ <ActivityIndicator color={theme.colors["fg-muted"]} />
174
+ </View>
175
+ ) : (
176
+ timelineProps.emptyState
177
+ )
178
+ }
179
+ trailing={
180
+ <>
181
+ {waitingOnInput ? (
182
+ <HumanInputCard
183
+ requests={humanInput.requests}
184
+ respondingRequestId={humanInput.respondingRequestId}
185
+ error={humanInput.mutationError?.message}
186
+ messages={humanInputMessages}
187
+ onSubmit={(requestId, response) =>
188
+ humanInput.respond(requestId, response).then(() => undefined)
189
+ }
190
+ />
191
+ ) : null}
192
+ {trailing}
193
+ </>
194
+ }
195
+ />
196
+ <SessionComposer
197
+ floating
198
+ onHeightChange={(height) => setComposerHeight(Math.round(height))}
199
+ onActionFeedback={composerSlots?.onActionFeedback}
200
+ value={composer.value}
201
+ onChangeText={composer.setValue}
202
+ onSend={() => void composer.send()}
203
+ canSend={composer.canSend && !busy}
204
+ sending={composer.sending}
205
+ running={controller.runActive}
206
+ paused={paused}
207
+ pauseBusy={composer.pausing || composer.resuming}
208
+ onPause={() => void composer.pause()}
209
+ onResume={() => void composer.resume()}
210
+ onAttach={composerSlots?.onAttach ?? (() => void attachments.pickImages())}
211
+ renderLeading={composerSlots?.renderLeading}
212
+ options={composerSlots?.options}
213
+ placeholder={composerSlots?.placeholder}
214
+ bottomInset={bottomInset}
215
+ keyboardBottomOffset={keyboardBottomOffset}
216
+ inputRef={composerInput}
217
+ messages={composerSlots?.messages}
218
+ voice={composerSlots?.voice}
219
+ below={
220
+ composer.draftConflict ? (
221
+ <DraftConflictStrip
222
+ messages={composerSlots?.messages}
223
+ onResolve={(choice) => void composer.resolveDraftConflict(choice)}
224
+ />
225
+ ) : null
226
+ }
227
+ header={
228
+ <>
229
+ {composerSlots?.header}
230
+ <AttachmentChips attachments={attachments} />
231
+ </>
232
+ }
233
+ above={
234
+ <>
235
+ <QueueDock
236
+ queue={queue}
237
+ composer={composer}
238
+ onComposerFocus={() => composerInput.current?.focus()}
239
+ leading={
240
+ <SessionSignals
241
+ goal={controller.goal}
242
+ agents={controller.lineage.lineage?.children ?? []}
243
+ commandsCount={controller.session.session?.backgroundCommandActivity?.count ?? 0}
244
+ onOpenSession={timelineProps.onOpenSession}
245
+ models={models}
246
+ readOnly={status === "failed" || status === "cancelled"}
247
+ renderCommands={() =>
248
+ client ? (
249
+ <NativeSessionCommands
250
+ client={client}
251
+ workspaceId={controller.workspaceId}
252
+ sessionId={controller.sessionId}
253
+ />
254
+ ) : null
255
+ }
256
+ />
257
+ }
258
+ />
259
+ {approvals.length > 0 && status === "requires_action" ? (
260
+ <ApprovalStrip
261
+ approvals={approvals}
262
+ messages={approvalMessages}
263
+ onDecide={(id, decision) =>
264
+ (decision === "approve" ? control.approve(id) : control.reject(id)).then(
265
+ () => undefined,
266
+ )
267
+ }
268
+ />
269
+ ) : null}
270
+ </>
271
+ }
272
+ />
273
+ </View>
274
+ );
275
+ }
276
+
277
+ /**
278
+ * A failed session stream retries with backoff (1s doubling to 30s) while the
279
+ * screen is active, so a transient API outage recovers without a relaunch.
280
+ */
281
+ function useAutoRecover(failed: boolean, live: boolean, refresh: () => Promise<void>) {
282
+ const delay = useRef(1_000);
283
+ useEffect(() => {
284
+ // Only a live stream ends the outage; a retry briefly clears the error.
285
+ if (live) delay.current = 1_000;
286
+ }, [live]);
287
+ useEffect(() => {
288
+ if (!failed) return;
289
+ const timer = setTimeout(() => {
290
+ delay.current = Math.min(delay.current * 2, 30_000);
291
+ void refresh();
292
+ }, delay.current);
293
+ return () => clearTimeout(timer);
294
+ }, [failed, refresh]);
295
+ }
296
+
297
+ /** Transport failures read as a connection problem, never as a native stack. */
298
+ function loadFailureMessage(error: Error | null): string | undefined {
299
+ if (!error) return undefined;
300
+ if (
301
+ error.name === "TypeError" ||
302
+ /fetch failed|network request failed|could not connect|offline|timed out/i.test(error.message)
303
+ ) {
304
+ return "OpenGeni couldn't be reached. Check your connection; this retries automatically.";
305
+ }
306
+ return error.message;
307
+ }
308
+
309
+ function LoadFailure({ message, onRetry }: { message?: string | undefined; onRetry: () => void }) {
310
+ const theme = useNativeTimelineTheme();
311
+ return (
312
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 10, padding: 24 }}>
313
+ <Icon name="circle-alert" size={20} color={theme.colors["status-failed"]} />
314
+ <Text style={{ ...fontStyle(theme, 500), fontSize: 15, color: theme.colors.fg }}>
315
+ Couldn't load this session
316
+ </Text>
317
+ {message ? (
318
+ <Text
319
+ style={{
320
+ ...fontStyle(theme),
321
+ fontSize: 13,
322
+ lineHeight: 18,
323
+ color: theme.colors["fg-muted"],
324
+ textAlign: "center",
325
+ }}
326
+ >
327
+ {message}
328
+ </Text>
329
+ ) : null}
330
+ <Button label="Retry" onPress={onRetry} />
331
+ </View>
332
+ );
333
+ }
334
+
335
+ /** Composer attachment chips: name, upload state, tap to retry a failure, remove. */
336
+ export function AttachmentChips({ attachments }: { attachments: NativeFileAttachmentsResult }) {
337
+ const theme = useNativeTimelineTheme();
338
+ const c = theme.colors;
339
+ if (attachments.attachments.length === 0) return null;
340
+ return (
341
+ <View
342
+ style={{
343
+ flexDirection: "row",
344
+ flexWrap: "wrap",
345
+ gap: 6,
346
+ paddingHorizontal: 12,
347
+ paddingTop: 10,
348
+ }}
349
+ >
350
+ {attachments.attachments.map((item) => {
351
+ const failed = item.status === "failed";
352
+ const busy = item.status === "uploading" || item.status === "preparing";
353
+ return (
354
+ <View
355
+ key={item.id}
356
+ style={{
357
+ flexDirection: "row",
358
+ alignItems: "center",
359
+ borderRadius: theme.radius.sm,
360
+ borderWidth: 1,
361
+ borderColor: failed ? withAlpha(c["status-failed"], 0.4) : c.border,
362
+ paddingLeft: 8,
363
+ backgroundColor: c["surface-2"],
364
+ }}
365
+ >
366
+ <Pressable
367
+ accessibilityRole="button"
368
+ accessibilityLabel={failed ? `Retry ${item.name}` : item.name}
369
+ disabled={!failed}
370
+ onPress={() => void attachments.retry(item.id)}
371
+ style={{ flexDirection: "row", alignItems: "center", gap: 6, paddingVertical: 4 }}
372
+ >
373
+ {busy ? <ActivityIndicator size="small" color={c["fg-subtle"]} /> : null}
374
+ <Text
375
+ numberOfLines={1}
376
+ style={{
377
+ ...fontStyle(theme),
378
+ maxWidth: 160,
379
+ fontSize: theme.size.xs,
380
+ color: failed ? c["status-failed"] : c["fg-muted"],
381
+ }}
382
+ >
383
+ {item.name}
384
+ {failed ? " · tap to retry" : ""}
385
+ </Text>
386
+ </Pressable>
387
+ <Pressable
388
+ accessibilityRole="button"
389
+ accessibilityLabel={`Remove ${item.name}`}
390
+ onPress={() => attachments.remove(item.id)}
391
+ hitSlop={6}
392
+ style={{ width: 28, height: 28, alignItems: "center", justifyContent: "center" }}
393
+ >
394
+ <Icon name="x" size={12} color={c["fg-subtle"]} />
395
+ </Pressable>
396
+ </View>
397
+ );
398
+ })}
399
+ </View>
400
+ );
401
+ }
402
+
403
+ /** The web composer's draft-conflict line: keep this device's draft or take the other. */
404
+ function DraftConflictStrip(props: {
405
+ messages?: Partial<ChatComposerMessages> | undefined;
406
+ onResolve: (choice: "keep_mine" | "use_remote") => void;
407
+ }) {
408
+ const theme = useNativeTimelineTheme();
409
+ const c = theme.colors;
410
+ const messages = { ...defaultChatComposerMessages, ...props.messages };
411
+ const text = { ...fontStyle(theme), fontSize: theme.size.xs, color: c["status-failed"] };
412
+ return (
413
+ <View
414
+ accessibilityRole="alert"
415
+ style={{
416
+ flexDirection: "row",
417
+ flexWrap: "wrap",
418
+ alignItems: "center",
419
+ gap: 8,
420
+ paddingHorizontal: 16,
421
+ paddingTop: 12,
422
+ }}
423
+ >
424
+ <Text style={{ ...text, flexGrow: 1, flexShrink: 1, flexBasis: 180 }}>
425
+ {messages.draftConflict}
426
+ </Text>
427
+ <Pressable
428
+ accessibilityRole="button"
429
+ hitSlop={10}
430
+ onPress={() => props.onResolve("use_remote")}
431
+ >
432
+ <Text style={{ ...text, textDecorationLine: "underline" }}>{messages.useOtherDraft}</Text>
433
+ </Pressable>
434
+ <Pressable
435
+ accessibilityRole="button"
436
+ hitSlop={10}
437
+ onPress={() => props.onResolve("keep_mine")}
438
+ >
439
+ <Text
440
+ style={{
441
+ ...text,
442
+ ...fontStyle(theme, 500),
443
+ color: c["status-failed"],
444
+ textDecorationLine: "underline",
445
+ }}
446
+ >
447
+ {messages.keepMine}
448
+ </Text>
449
+ </Pressable>
450
+ </View>
451
+ );
452
+ }
453
+
454
+ /** Polls the session's commands only while the panel is open, as web does. */
455
+ function NativeSessionCommands(props: {
456
+ client: OpenGeniClient;
457
+ workspaceId: string;
458
+ sessionId: string;
459
+ }) {
460
+ const { client, workspaceId, sessionId } = props;
461
+ const [commands, setCommands] = useState<SessionBackgroundCommand[]>([]);
462
+ const [loading, setLoading] = useState(true);
463
+ const [error, setError] = useState<Error | null>(null);
464
+ const load = useCallback(async () => {
465
+ try {
466
+ const response = await client.listSessionBackgroundCommands(workspaceId, sessionId);
467
+ setCommands(response.commands);
468
+ setError(null);
469
+ } catch (cause) {
470
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
471
+ } finally {
472
+ setLoading(false);
473
+ }
474
+ }, [client, sessionId, workspaceId]);
475
+ useEffect(() => {
476
+ void load();
477
+ const timer = setInterval(() => void load(), 3_000);
478
+ return () => clearInterval(timer);
479
+ }, [load]);
480
+ const cancel = useCallback(
481
+ async (commandId: string) => {
482
+ await client.cancelSessionBackgroundCommand(workspaceId, sessionId, commandId);
483
+ await load();
484
+ },
485
+ [client, load, sessionId, workspaceId],
486
+ );
487
+ return <SessionCommandsList commands={{ commands, loading, error, cancel }} />;
488
+ }