@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,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
|
+
}
|