@opengeni/react-native 4.2.1-canary.39642758903001 → 4.3.1
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/android/build.gradle +15 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/opengeni/textselection/OpenGeniTextSelectionModule.kt +274 -0
- package/dist/{chunk-D3MFBDVR.js → chunk-2RNMGRTB.js} +3 -3
- package/dist/{chunk-D3MFBDVR.js.map → chunk-2RNMGRTB.js.map} +1 -1
- package/dist/{chunk-2W74OY6I.js → chunk-FHPMRXIG.js} +16 -2
- package/dist/chunk-FHPMRXIG.js.map +1 -0
- package/dist/{chunk-YTT4WSJZ.js → chunk-QFV5DISV.js} +3 -3
- package/dist/{chunk-YTT4WSJZ.js.map → chunk-QFV5DISV.js.map} +1 -1
- package/dist/{chunk-U5L2CUMJ.js → chunk-ZZNZBZWV.js} +3 -3
- package/dist/{chunk-U5L2CUMJ.js.map → chunk-ZZNZBZWV.js.map} +1 -1
- package/dist/index.js +2 -2
- package/dist/timeline/annotations.d.ts +81 -0
- package/dist/timeline/icon-nodes.generated.d.ts +5 -0
- package/dist/timeline/index.d.ts +2 -0
- package/dist/timeline/index.js +1186 -197
- package/dist/timeline/index.js.map +1 -1
- package/dist/timeline/markdown.js +5 -2
- package/dist/timeline/markdown.js.map +1 -1
- package/dist/timeline/message-timeline.d.ts +20 -2
- package/dist/timeline/previews.js +2 -2
- package/dist/timeline/queue-dock.d.ts +4 -1
- package/dist/timeline/text-selection.d.ts +44 -0
- package/expo-module.config.json +5 -2
- package/ios/OpenGeniTextSelectionModule.swift +560 -0
- package/package.json +6 -4
- package/src/timeline/activity.tsx +24 -15
- package/src/timeline/annotations.tsx +877 -0
- package/src/timeline/icon-nodes.generated.ts +14 -0
- package/src/timeline/index.ts +2 -0
- package/src/timeline/markdown.tsx +7 -0
- package/src/timeline/message-timeline.tsx +340 -174
- package/src/timeline/queue-dock.tsx +36 -0
- package/src/timeline/session-screen.tsx +122 -4
- package/src/timeline/sheet.tsx +3 -2
- package/src/timeline/text-selection.ts +115 -0
- package/dist/chunk-2W74OY6I.js.map +0 -1
|
@@ -966,6 +966,20 @@ export const iconNodes = {
|
|
|
966
966
|
},
|
|
967
967
|
],
|
|
968
968
|
],
|
|
969
|
+
quote: [
|
|
970
|
+
[
|
|
971
|
+
"path",
|
|
972
|
+
{
|
|
973
|
+
d: "M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z",
|
|
974
|
+
},
|
|
975
|
+
],
|
|
976
|
+
[
|
|
977
|
+
"path",
|
|
978
|
+
{
|
|
979
|
+
d: "M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z",
|
|
980
|
+
},
|
|
981
|
+
],
|
|
982
|
+
],
|
|
969
983
|
search: [
|
|
970
984
|
[
|
|
971
985
|
"path",
|
package/src/timeline/index.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type { NativeTimelineMessages } from "./messages";
|
|
|
47
47
|
export { ComposerPill, SessionComposer } from "./composer";
|
|
48
48
|
export { ModelPickerSheet } from "./model-picker";
|
|
49
49
|
export { BottomSheet } from "./sheet";
|
|
50
|
+
export { AnnotationsChip, AnnotationsSheet } from "./annotations";
|
|
51
|
+
export type { AnnotationReveal, NativeAnnotationLike } from "./annotations";
|
|
50
52
|
export { SessionActionsButton } from "./session-actions";
|
|
51
53
|
export type { SessionAction, SessionActionsClient, SessionActionsProps } from "./session-actions";
|
|
52
54
|
export type { ModelPickerSheetProps } from "./model-picker";
|
|
@@ -469,6 +469,13 @@ function webRules(
|
|
|
469
469
|
const line = tone === "muted" ? 24 : 28;
|
|
470
470
|
return {
|
|
471
471
|
...renderRules,
|
|
472
|
+
// Every run of text is selectable, as on the web: a long press selects
|
|
473
|
+
// words (and, inside a timeline message, offers Add note).
|
|
474
|
+
textgroup: (node, children, _parent, styles) => (
|
|
475
|
+
<Text key={node.key} selectable style={styles.textgroup}>
|
|
476
|
+
{children}
|
|
477
|
+
</Text>
|
|
478
|
+
),
|
|
472
479
|
// Web lists use a disc marker in fg-subtle; the library's iOS default is a middle dot.
|
|
473
480
|
list_item: (node, children, parent, styles, inheritedStyles = {}) => {
|
|
474
481
|
if (parent.some((entry: { type: string }) => entry.type === "bullet_list")) {
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DraftTimelineAnnotation,
|
|
3
|
+
SessionEvent,
|
|
4
|
+
SessionStatus,
|
|
5
|
+
TimelineAnnotationSource,
|
|
6
|
+
} from "@opengeni/sdk";
|
|
2
7
|
import {
|
|
3
8
|
buildTimeline,
|
|
4
9
|
groupTimeline,
|
|
5
10
|
type AgentMessageItem,
|
|
6
11
|
type HumanInputItem,
|
|
7
12
|
type TimelineGroup,
|
|
13
|
+
type TimelineAnnotationSourceDescriptor,
|
|
8
14
|
type TimelineItem,
|
|
9
15
|
type UserMessageItem,
|
|
10
16
|
} from "@opengeni/react/session";
|
|
@@ -35,10 +41,12 @@ import {
|
|
|
35
41
|
useRef,
|
|
36
42
|
useState,
|
|
37
43
|
type ComponentRef,
|
|
44
|
+
type MutableRefObject,
|
|
38
45
|
type ReactNode,
|
|
39
46
|
} from "react";
|
|
40
47
|
import {
|
|
41
48
|
ActivityIndicator,
|
|
49
|
+
Keyboard,
|
|
42
50
|
Pressable,
|
|
43
51
|
ScrollView,
|
|
44
52
|
Text,
|
|
@@ -70,6 +78,16 @@ import { useOpenGeniText, type OpenGeniText } from "@opengeni/react/messages";
|
|
|
70
78
|
import { TurnStepsLoader } from "./turn-steps";
|
|
71
79
|
import { useNativeTimelineMessages } from "./messages";
|
|
72
80
|
import { collectLiveNotes, foldedLiveNotes, liveFoldShift } from "./live-notes";
|
|
81
|
+
import {
|
|
82
|
+
AnnotationSourcesContext,
|
|
83
|
+
AnnotationSourceView,
|
|
84
|
+
SentAnnotationCards,
|
|
85
|
+
draftMarkersBySource,
|
|
86
|
+
type AnnotationFlash,
|
|
87
|
+
type AnnotationReveal,
|
|
88
|
+
type AnnotationSourceNode,
|
|
89
|
+
} from "./annotations";
|
|
90
|
+
import { measureQuoteAnchors, useNativeTextAnnotation } from "./text-selection";
|
|
73
91
|
|
|
74
92
|
/* ----------------------------------------------------------------------------
|
|
75
93
|
Native MessageTimeline: the web MessageTimeline (components/message-timeline)
|
|
@@ -125,9 +143,39 @@ export interface NativeMessageTimelineProps extends NativeActivityOptions {
|
|
|
125
143
|
contentInsetBottom?: number | undefined;
|
|
126
144
|
/** Height of host chrome floating over the timeline's bottom edge (a floating composer). */
|
|
127
145
|
overlayInsetBottom?: number | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* Changes whenever the reader sends: the timeline resumes following the
|
|
148
|
+
* latest row, so a message sent while reading (or annotating) older
|
|
149
|
+
* history comes into view.
|
|
150
|
+
*/
|
|
151
|
+
followLatestSignal?: number | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Add note on selected message text (iOS): receives the quote as a draft
|
|
154
|
+
* annotation, as the web timeline's selection popover does.
|
|
155
|
+
*/
|
|
156
|
+
onAnnotate?: ((annotation: DraftTimelineAnnotation) => void) | undefined;
|
|
157
|
+
/** The draft's annotations, marked on the messages they quote. */
|
|
158
|
+
draftAnnotations?: readonly DraftTimelineAnnotation[] | undefined;
|
|
159
|
+
/** A draft marker was tapped: review that annotation. */
|
|
160
|
+
onDraftAnnotationSelect?: ((id: string) => void) | undefined;
|
|
161
|
+
/** Receives a function that scrolls a quoted message into view and highlights it. */
|
|
162
|
+
annotationRevealRef?: MutableRefObject<AnnotationReveal | null> | undefined;
|
|
128
163
|
style?: StyleProp<ViewStyle>;
|
|
129
164
|
}
|
|
130
165
|
|
|
166
|
+
/** Each message's annotation source, by source key (its durable event id). */
|
|
167
|
+
function annotationSources(
|
|
168
|
+
items: readonly TimelineItem[],
|
|
169
|
+
): Map<string, TimelineAnnotationSourceDescriptor> {
|
|
170
|
+
const sources = new Map<string, TimelineAnnotationSourceDescriptor>();
|
|
171
|
+
for (const item of items) {
|
|
172
|
+
if (item.kind !== "user-message" && item.kind !== "agent-message") continue;
|
|
173
|
+
const source = item.annotationSource;
|
|
174
|
+
if (source) sources.set(source.eventId, source);
|
|
175
|
+
}
|
|
176
|
+
return sources;
|
|
177
|
+
}
|
|
178
|
+
|
|
131
179
|
export function MessageTimeline(props: NativeMessageTimelineProps) {
|
|
132
180
|
const theme = useNativeTimelineTheme();
|
|
133
181
|
const m = useNativeTimelineMessages();
|
|
@@ -137,6 +185,8 @@ export function MessageTimeline(props: NativeMessageTimelineProps) {
|
|
|
137
185
|
[t, props.items, props.events],
|
|
138
186
|
);
|
|
139
187
|
const groups = useMemo(() => groupTimeline(t, items, { readableTurns: true }), [t, items]);
|
|
188
|
+
const sources = useMemo(() => annotationSources(items), [items]);
|
|
189
|
+
useNativeTextAnnotation(sources, props.onAnnotate);
|
|
140
190
|
const foldMemory = useRef(new Map<string, "open" | "closed">()).current;
|
|
141
191
|
const scrollRef = useRef<ComponentRef<typeof ScrollView>>(null);
|
|
142
192
|
// Opening on a moment starts away from the tip; the landing decides where.
|
|
@@ -151,6 +201,86 @@ export function MessageTimeline(props: NativeMessageTimelineProps) {
|
|
|
151
201
|
// Expanded outer turn headers, for the pinned copy (web: sticky in its section).
|
|
152
202
|
const stickyHeaders = useRef(new Map<string, StickyWorkHeader>()).current;
|
|
153
203
|
const scrollY = useRef(0);
|
|
204
|
+
const sourceNodes = useRef(new Map<string, AnnotationSourceNode>()).current;
|
|
205
|
+
const [flash, setFlash] = useState<AnnotationFlash | null>(null);
|
|
206
|
+
// Read when a reveal lands, after the keyboard has gone and the composer settled.
|
|
207
|
+
const overlayInsetBottom = useRef(0);
|
|
208
|
+
overlayInsetBottom.current = props.overlayInsetBottom ?? 0;
|
|
209
|
+
const registerSource = useCallback(
|
|
210
|
+
(sourceKey: string, node: AnnotationSourceNode | null) => {
|
|
211
|
+
if (node) sourceNodes.set(sourceKey, node);
|
|
212
|
+
else sourceNodes.delete(sourceKey);
|
|
213
|
+
},
|
|
214
|
+
[sourceNodes],
|
|
215
|
+
);
|
|
216
|
+
const revealSource = useCallback<AnnotationReveal>(
|
|
217
|
+
(source: TimelineAnnotationSource, quote) => {
|
|
218
|
+
const node = sourceNodes.get(source.eventId);
|
|
219
|
+
const scroller = scrollRef.current?.getNativeScrollRef?.();
|
|
220
|
+
if (!node || !scroller) return false;
|
|
221
|
+
following.current = false;
|
|
222
|
+
// Reading the source, not writing: the keyboard would hide it.
|
|
223
|
+
const keyboardGone = Keyboard.isVisible()
|
|
224
|
+
? new Promise<void>((resolve) => {
|
|
225
|
+
const done = () => {
|
|
226
|
+
subscription.remove();
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
// The composer re-measures as it drops back down.
|
|
229
|
+
setTimeout(resolve, 60);
|
|
230
|
+
};
|
|
231
|
+
const subscription = Keyboard.addListener("keyboardDidHide", done);
|
|
232
|
+
const timer = setTimeout(done, 600);
|
|
233
|
+
Keyboard.dismiss();
|
|
234
|
+
})
|
|
235
|
+
: Promise.resolve();
|
|
236
|
+
const located = quote
|
|
237
|
+
? measureQuoteAnchors(source.eventId, [quote]).then(([anchor]) => anchor ?? null)
|
|
238
|
+
: Promise.resolve(null);
|
|
239
|
+
void Promise.all([located, keyboardGone]).then(([anchor]) => {
|
|
240
|
+
scroller.measureInWindow((_scrollX, scrollTop) => {
|
|
241
|
+
node.measureInWindow((_x, top, _width, height) => {
|
|
242
|
+
// Center the message in the space the composer leaves; a message
|
|
243
|
+
// taller than that centers the quote instead, or shows its top.
|
|
244
|
+
const y = scrollY.current + top - scrollTop;
|
|
245
|
+
const visible = (viewportHeight.current || 600) - overlayInsetBottom.current;
|
|
246
|
+
const first = anchor?.boxes[0];
|
|
247
|
+
const last = anchor?.boxes[anchor.boxes.length - 1];
|
|
248
|
+
const span =
|
|
249
|
+
height < visible - 32 || !first || !last
|
|
250
|
+
? { top: y, height }
|
|
251
|
+
: { top: y + first.y, height: last.y + last.height - first.y };
|
|
252
|
+
const target =
|
|
253
|
+
span.height < visible - 32 ? span.top - (visible - span.height) / 2 : span.top - 16;
|
|
254
|
+
const end = Math.max(0, contentHeight.current - (viewportHeight.current || 0));
|
|
255
|
+
scrollY.current = Math.min(Math.max(0, target), end || Number.POSITIVE_INFINITY);
|
|
256
|
+
scrollRef.current?.scrollTo({ y: scrollY.current, animated: true });
|
|
257
|
+
// Flash once the scroll is under way, so the reader sees it land.
|
|
258
|
+
setFlash((current) => ({
|
|
259
|
+
sourceKey: source.eventId,
|
|
260
|
+
token: (current?.token ?? 0) + 1,
|
|
261
|
+
...(quote ? { quote } : {}),
|
|
262
|
+
}));
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
return true;
|
|
267
|
+
},
|
|
268
|
+
[sourceNodes],
|
|
269
|
+
);
|
|
270
|
+
if (props.annotationRevealRef) props.annotationRevealRef.current = revealSource;
|
|
271
|
+
const draftMarkers = useMemo(
|
|
272
|
+
() => draftMarkersBySource(props.draftAnnotations),
|
|
273
|
+
[props.draftAnnotations],
|
|
274
|
+
);
|
|
275
|
+
const annotationSourcesValue = useMemo(
|
|
276
|
+
() => ({
|
|
277
|
+
register: registerSource,
|
|
278
|
+
flash,
|
|
279
|
+
drafts: draftMarkers,
|
|
280
|
+
onSelectDraft: props.onDraftAnnotationSelect,
|
|
281
|
+
}),
|
|
282
|
+
[draftMarkers, flash, props.onDraftAnnotationSelect, registerSource],
|
|
283
|
+
);
|
|
154
284
|
const [pinned, setPinned] = useState<{ key: string; offset: number; height: number } | null>(
|
|
155
285
|
null,
|
|
156
286
|
);
|
|
@@ -318,6 +448,13 @@ export function MessageTimeline(props: NativeMessageTimelineProps) {
|
|
|
318
448
|
},
|
|
319
449
|
[requestOlder],
|
|
320
450
|
);
|
|
451
|
+
const followLatestSignal = props.followLatestSignal ?? 0;
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (!followLatestSignal) return;
|
|
454
|
+
following.current = true;
|
|
455
|
+
setShowJump(false);
|
|
456
|
+
scrollRef.current?.scrollToEnd({ animated: true });
|
|
457
|
+
}, [followLatestSignal]);
|
|
321
458
|
// Landing on a moment: each group's offset, read as it lays out.
|
|
322
459
|
const groupTops = useRef(new Map<string, number>()).current;
|
|
323
460
|
const landed = useRef<number | null>(null);
|
|
@@ -371,155 +508,161 @@ export function MessageTimeline(props: NativeMessageTimelineProps) {
|
|
|
371
508
|
renderUserAttachments: props.renderUserAttachments,
|
|
372
509
|
onCopy: props.onCopy,
|
|
373
510
|
reportLiveFold,
|
|
511
|
+
onRevealAnnotation: revealSource,
|
|
374
512
|
};
|
|
375
513
|
return (
|
|
376
|
-
<
|
|
377
|
-
<
|
|
378
|
-
<
|
|
379
|
-
<
|
|
380
|
-
<
|
|
381
|
-
<
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
paddingBottom: props.contentInsetBottom ?? 24,
|
|
405
|
-
gap: ROW_GAP_PX,
|
|
406
|
-
flexGrow: 1,
|
|
407
|
-
}}
|
|
408
|
-
>
|
|
409
|
-
<View>
|
|
410
|
-
{props.hasOlder ? (
|
|
411
|
-
<OlderHistoryRow loading={props.loadingOlder === true} onPress={requestOlder} />
|
|
412
|
-
) : null}
|
|
413
|
-
{props.header}
|
|
414
|
-
</View>
|
|
415
|
-
{groups.length === 0 && props.emptyState ? props.emptyState : null}
|
|
416
|
-
{groups.map((group, index) => {
|
|
417
|
-
const key = groupKey(group);
|
|
418
|
-
const prompt = group.kind === "item" && group.item.kind === "user-message";
|
|
419
|
-
// Agent messages keep one wrapper so a progress note can fold
|
|
420
|
-
// (stay mounted, out of layout) without remounting.
|
|
421
|
-
const noteId =
|
|
422
|
-
group.kind === "item" && group.item.kind === "agent-message"
|
|
423
|
-
? group.item.id
|
|
424
|
-
: null;
|
|
425
|
-
const liveNote = noteId !== null && liveNotes.notes.has(noteId);
|
|
426
|
-
const folded = noteId !== null && foldedNotes.has(noteId);
|
|
427
|
-
// The landing row keeps one wrapper for its life, so landing never remounts it.
|
|
428
|
-
const landing = focusSequence !== null && index === focusIndex;
|
|
429
|
-
return prompt || landing || noteId !== null ? (
|
|
430
|
-
<View
|
|
431
|
-
key={key}
|
|
432
|
-
style={folded ? { display: "none" } : undefined}
|
|
433
|
-
onLayout={(event) => {
|
|
434
|
-
const { y, height } = event.nativeEvent.layout;
|
|
435
|
-
if (prompt) promptFrames.set(key, { top: y, bottom: y + height });
|
|
436
|
-
if (liveNote && !folded && height > 0) {
|
|
437
|
-
liveNoteHeights.set(noteId, height);
|
|
438
|
-
}
|
|
439
|
-
if (landing && landed.current !== focusSequence) {
|
|
440
|
-
groupTops.set(key, y);
|
|
441
|
-
setLayoutPass((pass) => pass + 1);
|
|
442
|
-
}
|
|
443
|
-
}}
|
|
444
|
-
>
|
|
445
|
-
<TimelineGroupView group={group} context={context} />
|
|
446
|
-
</View>
|
|
447
|
-
) : (
|
|
448
|
-
<TimelineGroupView key={key} group={group} context={context} />
|
|
449
|
-
);
|
|
450
|
-
})}
|
|
451
|
-
{props.trailing}
|
|
452
|
-
</ScrollView>
|
|
453
|
-
{pinned && stickyHeaders.get(pinned.key) ? (
|
|
454
|
-
<View
|
|
455
|
-
style={{
|
|
456
|
-
position: "absolute",
|
|
457
|
-
top: 0,
|
|
458
|
-
left: 0,
|
|
459
|
-
right: 0,
|
|
514
|
+
<AnnotationSourcesContext.Provider value={annotationSourcesValue}>
|
|
515
|
+
<NativeActivityOptionsProvider value={activityOptions}>
|
|
516
|
+
<FoldMemoryProvider value={foldMemory}>
|
|
517
|
+
<StickyWorkHeaderProvider value={registerSticky}>
|
|
518
|
+
<ReleaseFollowProvider value={releaseFollow}>
|
|
519
|
+
<View style={[{ flex: 1, backgroundColor: theme.colors.bg }, props.style]}>
|
|
520
|
+
<ScrollView
|
|
521
|
+
ref={scrollRef}
|
|
522
|
+
onScroll={onScroll}
|
|
523
|
+
onScrollBeginDrag={() => {
|
|
524
|
+
readerScrolling.current = true;
|
|
525
|
+
// The reader takes over: a live turn re-lays out the content many
|
|
526
|
+
// times a second, and snapping to the end on each pass would undo
|
|
527
|
+
// the drag before it got far. Follow is re-decided when it ends.
|
|
528
|
+
following.current = false;
|
|
529
|
+
}}
|
|
530
|
+
onScrollEndDrag={onScrollEnd}
|
|
531
|
+
onMomentumScrollEnd={onScrollEnd}
|
|
532
|
+
onLayout={(event: LayoutChangeEvent) => {
|
|
533
|
+
viewportHeight.current = event.nativeEvent.layout.height;
|
|
534
|
+
if (following.current) scrollRef.current?.scrollToEnd({ animated: false });
|
|
535
|
+
}}
|
|
536
|
+
scrollEventThrottle={32}
|
|
537
|
+
onContentSizeChange={onContentSizeChange}
|
|
538
|
+
keyboardDismissMode="interactive"
|
|
539
|
+
keyboardShouldPersistTaps="handled"
|
|
540
|
+
contentContainerStyle={{
|
|
541
|
+
paddingTop: props.contentInsetTop ?? 16,
|
|
460
542
|
paddingHorizontal: 16,
|
|
461
|
-
|
|
462
|
-
|
|
543
|
+
paddingBottom: props.contentInsetBottom ?? 24,
|
|
544
|
+
gap: ROW_GAP_PX,
|
|
545
|
+
flexGrow: 1,
|
|
463
546
|
}}
|
|
464
547
|
>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
548
|
+
<View>
|
|
549
|
+
{props.hasOlder ? (
|
|
550
|
+
<OlderHistoryRow
|
|
551
|
+
loading={props.loadingOlder === true}
|
|
552
|
+
onPress={requestOlder}
|
|
553
|
+
/>
|
|
554
|
+
) : null}
|
|
555
|
+
{props.header}
|
|
556
|
+
</View>
|
|
557
|
+
{groups.length === 0 && props.emptyState ? props.emptyState : null}
|
|
558
|
+
{groups.map((group, index) => {
|
|
559
|
+
const key = groupKey(group);
|
|
560
|
+
const prompt = group.kind === "item" && group.item.kind === "user-message";
|
|
561
|
+
// Agent messages keep one wrapper so a progress note can fold
|
|
562
|
+
// (stay mounted, out of layout) without remounting.
|
|
563
|
+
const noteId =
|
|
564
|
+
group.kind === "item" && group.item.kind === "agent-message"
|
|
565
|
+
? group.item.id
|
|
566
|
+
: null;
|
|
567
|
+
const liveNote = noteId !== null && liveNotes.notes.has(noteId);
|
|
568
|
+
const folded = noteId !== null && foldedNotes.has(noteId);
|
|
569
|
+
// The landing row keeps one wrapper for its life, so landing never remounts it.
|
|
570
|
+
const landing = focusSequence !== null && index === focusIndex;
|
|
571
|
+
return prompt || landing || noteId !== null ? (
|
|
572
|
+
<View
|
|
573
|
+
key={key}
|
|
574
|
+
style={folded ? { display: "none" } : undefined}
|
|
575
|
+
onLayout={(event) => {
|
|
576
|
+
const { y, height } = event.nativeEvent.layout;
|
|
577
|
+
if (prompt) promptFrames.set(key, { top: y, bottom: y + height });
|
|
578
|
+
if (liveNote && !folded && height > 0) {
|
|
579
|
+
liveNoteHeights.set(noteId, height);
|
|
580
|
+
}
|
|
581
|
+
if (landing && landed.current !== focusSequence) {
|
|
582
|
+
groupTops.set(key, y);
|
|
583
|
+
setLayoutPass((pass) => pass + 1);
|
|
584
|
+
}
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
<TimelineGroupView group={group} context={context} />
|
|
588
|
+
</View>
|
|
589
|
+
) : (
|
|
590
|
+
<TimelineGroupView key={key} group={group} context={context} />
|
|
591
|
+
);
|
|
592
|
+
})}
|
|
593
|
+
{props.trailing}
|
|
594
|
+
</ScrollView>
|
|
595
|
+
{pinned && stickyHeaders.get(pinned.key) ? (
|
|
596
|
+
<View
|
|
597
|
+
style={{
|
|
598
|
+
position: "absolute",
|
|
599
|
+
top: 0,
|
|
600
|
+
left: 0,
|
|
601
|
+
right: 0,
|
|
602
|
+
paddingHorizontal: 16,
|
|
603
|
+
backgroundColor: theme.colors.bg,
|
|
604
|
+
transform: [{ translateY: pinned.offset }],
|
|
605
|
+
}}
|
|
606
|
+
>
|
|
607
|
+
{stickyHeaders.get(pinned.key)!.render()}
|
|
608
|
+
</View>
|
|
609
|
+
) : null}
|
|
610
|
+
{/* Web timeline navigation: two identical small round arrow buttons at
|
|
469
611
|
fixed, centered spots that never dodge the content beneath them. */}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}}
|
|
481
|
-
>
|
|
482
|
-
<NavButton
|
|
483
|
-
icon="arrow-up"
|
|
484
|
-
label={m.backToYourMessage}
|
|
485
|
-
onPress={() => {
|
|
486
|
-
const frame = promptFrames.get(questionNav);
|
|
487
|
-
if (!frame) return;
|
|
488
|
-
following.current = false;
|
|
489
|
-
scrollRef.current?.scrollTo({
|
|
490
|
-
y: Math.max(0, frame.top - QUESTION_NAV_MARGIN_PX),
|
|
491
|
-
animated: true,
|
|
492
|
-
});
|
|
612
|
+
{questionNav ? (
|
|
613
|
+
<View
|
|
614
|
+
pointerEvents="box-none"
|
|
615
|
+
style={{
|
|
616
|
+
position: "absolute",
|
|
617
|
+
// Below a pinned work header so that strip stays a full-width target.
|
|
618
|
+
top: pinned ? pinned.height + pinned.offset + 8 : 12,
|
|
619
|
+
left: 0,
|
|
620
|
+
right: 0,
|
|
621
|
+
alignItems: "center",
|
|
493
622
|
}}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
623
|
+
>
|
|
624
|
+
<NavButton
|
|
625
|
+
icon="arrow-up"
|
|
626
|
+
label={m.backToYourMessage}
|
|
627
|
+
onPress={() => {
|
|
628
|
+
const frame = promptFrames.get(questionNav);
|
|
629
|
+
if (!frame) return;
|
|
630
|
+
following.current = false;
|
|
631
|
+
scrollRef.current?.scrollTo({
|
|
632
|
+
y: Math.max(0, frame.top - QUESTION_NAV_MARGIN_PX),
|
|
633
|
+
animated: true,
|
|
634
|
+
});
|
|
635
|
+
}}
|
|
636
|
+
/>
|
|
637
|
+
</View>
|
|
638
|
+
) : null}
|
|
639
|
+
{showJump ? (
|
|
640
|
+
<View
|
|
641
|
+
pointerEvents="box-none"
|
|
642
|
+
style={{
|
|
643
|
+
position: "absolute",
|
|
644
|
+
bottom: (props.overlayInsetBottom ?? 0) + 12,
|
|
645
|
+
left: 0,
|
|
646
|
+
right: 0,
|
|
647
|
+
alignItems: "center",
|
|
514
648
|
}}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
649
|
+
>
|
|
650
|
+
<NavButton
|
|
651
|
+
icon="arrow-down"
|
|
652
|
+
label={m.jumpToLatest}
|
|
653
|
+
onPress={() => {
|
|
654
|
+
following.current = true;
|
|
655
|
+
scrollRef.current?.scrollToEnd({ animated: true });
|
|
656
|
+
}}
|
|
657
|
+
/>
|
|
658
|
+
</View>
|
|
659
|
+
) : null}
|
|
660
|
+
</View>
|
|
661
|
+
</ReleaseFollowProvider>
|
|
662
|
+
</StickyWorkHeaderProvider>
|
|
663
|
+
</FoldMemoryProvider>
|
|
664
|
+
</NativeActivityOptionsProvider>
|
|
665
|
+
</AnnotationSourcesContext.Provider>
|
|
523
666
|
);
|
|
524
667
|
}
|
|
525
668
|
|
|
@@ -570,6 +713,7 @@ type GroupContext = {
|
|
|
570
713
|
onCopy?: ((text: string) => void) | undefined;
|
|
571
714
|
/** A live work row reports its open state, so its progress notes above fold. */
|
|
572
715
|
reportLiveFold?: ((workId: string, open: boolean) => void) | undefined;
|
|
716
|
+
onRevealAnnotation?: AnnotationReveal | undefined;
|
|
573
717
|
};
|
|
574
718
|
|
|
575
719
|
/** A wait's one quiet secondary line under its work row. */
|
|
@@ -1108,13 +1252,15 @@ function UserMessageRow({ item, context }: { item: UserMessageItem; context: Gro
|
|
|
1108
1252
|
const theme = useNativeTimelineTheme();
|
|
1109
1253
|
const m = useNativeTimelineMessages();
|
|
1110
1254
|
const failed = item.delivery?.state === "failed";
|
|
1255
|
+
const annotations = item.annotations ?? [];
|
|
1256
|
+
const sourceKey = item.annotationSource?.eventId;
|
|
1111
1257
|
return (
|
|
1112
1258
|
<Animated.View entering={FadeIn.duration(180)} style={{ alignItems: "flex-end" }}>
|
|
1113
1259
|
<View style={{ maxWidth: "85%", alignItems: "flex-end", gap: 6 }}>
|
|
1114
1260
|
{item.resources.some((resource) => resource.kind === "file")
|
|
1115
1261
|
? context.renderUserAttachments?.(item)
|
|
1116
1262
|
: null}
|
|
1117
|
-
{item.text ? (
|
|
1263
|
+
{item.text || annotations.length > 0 ? (
|
|
1118
1264
|
<View
|
|
1119
1265
|
style={{
|
|
1120
1266
|
backgroundColor: theme.colors["surface-2"],
|
|
@@ -1128,23 +1274,38 @@ function UserMessageRow({ item, context }: { item: UserMessageItem; context: Gro
|
|
|
1128
1274
|
paddingVertical: 10,
|
|
1129
1275
|
}}
|
|
1130
1276
|
>
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
selectable
|
|
1137
|
-
style={{
|
|
1138
|
-
...fontStyle(theme),
|
|
1139
|
-
fontSize: theme.size.md,
|
|
1140
|
-
lineHeight: 28,
|
|
1141
|
-
color: theme.colors.fg,
|
|
1142
|
-
}}
|
|
1277
|
+
{item.text ? (
|
|
1278
|
+
<CollapsibleUserText>
|
|
1279
|
+
<AnnotationSourceView
|
|
1280
|
+
sourceKey={sourceKey}
|
|
1281
|
+
style={{ marginHorizontal: -6, paddingHorizontal: 6 }}
|
|
1143
1282
|
>
|
|
1144
|
-
{
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1283
|
+
{context.renderMarkdown ? (
|
|
1284
|
+
context.renderMarkdown(item.text, { tone: "body" })
|
|
1285
|
+
) : (
|
|
1286
|
+
<Text
|
|
1287
|
+
selectable
|
|
1288
|
+
style={{
|
|
1289
|
+
...fontStyle(theme),
|
|
1290
|
+
fontSize: theme.size.md,
|
|
1291
|
+
lineHeight: 28,
|
|
1292
|
+
color: theme.colors.fg,
|
|
1293
|
+
}}
|
|
1294
|
+
>
|
|
1295
|
+
{item.text}
|
|
1296
|
+
</Text>
|
|
1297
|
+
)}
|
|
1298
|
+
</AnnotationSourceView>
|
|
1299
|
+
</CollapsibleUserText>
|
|
1300
|
+
) : null}
|
|
1301
|
+
{annotations.length > 0 ? (
|
|
1302
|
+
<View style={item.text ? { marginTop: 10, marginBottom: 2 } : { paddingVertical: 2 }}>
|
|
1303
|
+
<SentAnnotationCards
|
|
1304
|
+
annotations={annotations}
|
|
1305
|
+
onReveal={context.onRevealAnnotation}
|
|
1306
|
+
/>
|
|
1307
|
+
</View>
|
|
1308
|
+
) : null}
|
|
1148
1309
|
</View>
|
|
1149
1310
|
) : null}
|
|
1150
1311
|
<MessageFooter
|
|
@@ -1193,21 +1354,26 @@ function AgentMessageRow({ item, context }: { item: AgentMessageItem; context: G
|
|
|
1193
1354
|
const theme = useNativeTimelineTheme();
|
|
1194
1355
|
return (
|
|
1195
1356
|
<Animated.View entering={FadeIn.duration(180)} style={{ minWidth: 0 }}>
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1357
|
+
<AnnotationSourceView
|
|
1358
|
+
sourceKey={item.annotationSource?.eventId}
|
|
1359
|
+
style={{ marginHorizontal: -8, paddingHorizontal: 8 }}
|
|
1360
|
+
>
|
|
1361
|
+
{context.renderMarkdown ? (
|
|
1362
|
+
context.renderMarkdown(item.text, { tone: "body", streaming: item.streaming })
|
|
1363
|
+
) : (
|
|
1364
|
+
<Text
|
|
1365
|
+
selectable
|
|
1366
|
+
style={{
|
|
1367
|
+
...fontStyle(theme),
|
|
1368
|
+
fontSize: theme.size.md,
|
|
1369
|
+
lineHeight: 28,
|
|
1370
|
+
color: theme.colors.fg,
|
|
1371
|
+
}}
|
|
1372
|
+
>
|
|
1373
|
+
{item.text}
|
|
1374
|
+
</Text>
|
|
1375
|
+
)}
|
|
1376
|
+
</AnnotationSourceView>
|
|
1211
1377
|
{item.streaming ? null : (
|
|
1212
1378
|
<MessageFooter
|
|
1213
1379
|
text={item.text}
|