@opengeni/react-native 4.2.1-canary.39593846423001 → 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.
Files changed (41) hide show
  1. package/android/build.gradle +15 -0
  2. package/android/src/main/AndroidManifest.xml +1 -0
  3. package/android/src/main/java/dev/opengeni/textselection/OpenGeniTextSelectionModule.kt +274 -0
  4. package/dist/{chunk-D3MFBDVR.js → chunk-2RNMGRTB.js} +3 -3
  5. package/dist/{chunk-D3MFBDVR.js.map → chunk-2RNMGRTB.js.map} +1 -1
  6. package/dist/{chunk-2W74OY6I.js → chunk-FHPMRXIG.js} +16 -2
  7. package/dist/chunk-FHPMRXIG.js.map +1 -0
  8. package/dist/{chunk-63PZ52YJ.js → chunk-QFV5DISV.js} +38 -5
  9. package/dist/chunk-QFV5DISV.js.map +1 -0
  10. package/dist/{chunk-U5L2CUMJ.js → chunk-ZZNZBZWV.js} +3 -3
  11. package/dist/{chunk-U5L2CUMJ.js.map → chunk-ZZNZBZWV.js.map} +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/timeline/activity.d.ts +3 -1
  14. package/dist/timeline/annotations.d.ts +81 -0
  15. package/dist/timeline/icon-nodes.generated.d.ts +5 -0
  16. package/dist/timeline/index.d.ts +2 -0
  17. package/dist/timeline/index.js +1223 -203
  18. package/dist/timeline/index.js.map +1 -1
  19. package/dist/timeline/markdown.js +5 -2
  20. package/dist/timeline/markdown.js.map +1 -1
  21. package/dist/timeline/message-timeline.d.ts +20 -2
  22. package/dist/timeline/previews.js +2 -2
  23. package/dist/timeline/primitives.d.ts +7 -1
  24. package/dist/timeline/queue-dock.d.ts +4 -1
  25. package/dist/timeline/text-selection.d.ts +44 -0
  26. package/expo-module.config.json +5 -2
  27. package/ios/OpenGeniTextSelectionModule.swift +560 -0
  28. package/package.json +6 -4
  29. package/src/timeline/activity.tsx +76 -21
  30. package/src/timeline/annotations.tsx +877 -0
  31. package/src/timeline/icon-nodes.generated.ts +14 -0
  32. package/src/timeline/index.ts +2 -0
  33. package/src/timeline/markdown.tsx +7 -0
  34. package/src/timeline/message-timeline.tsx +340 -174
  35. package/src/timeline/primitives.tsx +50 -1
  36. package/src/timeline/queue-dock.tsx +36 -0
  37. package/src/timeline/session-screen.tsx +122 -4
  38. package/src/timeline/sheet.tsx +3 -2
  39. package/src/timeline/text-selection.ts +115 -0
  40. package/dist/chunk-2W74OY6I.js.map +0 -1
  41. package/dist/chunk-63PZ52YJ.js.map +0 -1
@@ -1,19 +1,20 @@
1
- import { defaultSecretFieldMessages, ThinkingOrb, humanInputAnswerLines, isSecretQuestion, initialSecretDraft, secretSubmission, secretInputTraits, secretUsernameTraits, SECRET_SAVE_TO_OPTIONS, secretPatternMismatch } from '../chunk-D3MFBDVR.js';
2
- import { ActivityDisclosure, useNativeTimelineMessages, useReleaseFollow, ShimmerText, ReleaseFollowProvider, withAlpha, blendOver, NativeTimelineMessagesProvider, ROW_MIN_HEIGHT, PulseDot, BodyNote, PayloadBlock, TermBlock, Chip, RunningPreview } from '../chunk-63PZ52YJ.js';
3
- export { ActivityDisclosure, BodyNote, Chip, NativeTimelineMessagesProvider, PayloadBlock, PulseDot, ShimmerText, TermBlock, defaultNativeTimelineMessages, useNativeTimelineMessages, withAlpha } from '../chunk-63PZ52YJ.js';
4
- import { NativeImageViewer } from '../chunk-U5L2CUMJ.js';
5
- export { NativeImageViewer } from '../chunk-U5L2CUMJ.js';
1
+ import { defaultSecretFieldMessages, ThinkingOrb, humanInputAnswerLines, isSecretQuestion, initialSecretDraft, secretSubmission, secretInputTraits, secretUsernameTraits, SECRET_SAVE_TO_OPTIONS, secretPatternMismatch } from '../chunk-2RNMGRTB.js';
2
+ import { ActivityDisclosure, useNativeTimelineMessages, useReleaseFollow, ShimmerText, withAlpha, ReleaseFollowProvider, blendOver, NativeTimelineMessagesProvider, ROW_MIN_HEIGHT, PulseDot, BodyNote, PayloadBlock, TermBlock, Chip, RunningPreview } from '../chunk-QFV5DISV.js';
3
+ export { ActivityDisclosure, BodyNote, Chip, NativeTimelineMessagesProvider, PayloadBlock, PulseDot, ShimmerText, TermBlock, defaultNativeTimelineMessages, useNativeTimelineMessages, withAlpha } from '../chunk-QFV5DISV.js';
4
+ import { NativeImageViewer } from '../chunk-ZZNZBZWV.js';
5
+ export { NativeImageViewer } from '../chunk-ZZNZBZWV.js';
6
6
  import { useOpenNativeLink } from '../chunk-2WJTBHO6.js';
7
7
  import '../chunk-7ZDF5VGA.js';
8
- import { useNativeTimelineTheme, fontStyle, Icon } from '../chunk-2W74OY6I.js';
9
- export { Icon, NativeTimelineThemeProvider, createNativeTimelineTheme, fontStyle, useNativeTimelineTheme, webColorsDark, webColorsLight } from '../chunk-2W74OY6I.js';
8
+ import { useNativeTimelineTheme, fontStyle, Icon } from '../chunk-FHPMRXIG.js';
9
+ export { Icon, NativeTimelineThemeProvider, createNativeTimelineTheme, fontStyle, useNativeTimelineTheme, webColorsDark, webColorsLight } from '../chunk-FHPMRXIG.js';
10
10
  import { buildTimeline, groupTimeline, conversationTimeline } from '@opengeni/react/session';
11
- import { defaultHumanInputFormMessages, createTurnSummaryContext, resolveTurnSummaryFacets, selectTurnSummaryFacets, formatElapsed, turnSummaryFallbackLabel, rollingActivityItem, toolDisplayName, questionNavTarget, timelineGroupIndexAtSequence, QUESTION_NAV_MARGIN_PX, flattenActivityItems, timelineGroupContainsPresentedImage, durationBetween, isPreparingWork, readableWorkStatus, readableWorkShowsPreview, readableWorkDefaultOpen, compactedLandmarkCount, SESSION_STATUS_PRESENTATION, formatRelativeTime, recordedWaitSummaryText, noticeTone, noticeDisplayText, isActionableHumanInputRequest, humanInputHeading, initialDrafts, emptyDraft, SESSION_STATUS_BADGE, workerRowTitle, startupDuration, STARTUP_WAIT_TITLES, startupPhaseTitle, sandboxRowTitle, toolRowPresentation, GENIE_WAITING_PHRASES, GENIE_PREPARING_PHRASES, answersFromDrafts, notificationPlainText, pathDirname, pathBasename, formatClockTime, parseNotificationText } from '@opengeni/react/timeline-model';
11
+ import { defaultHumanInputFormMessages, createTurnSummaryContext, resolveTurnSummaryFacets, selectTurnSummaryFacets, formatElapsed, turnSummaryFallbackLabel, rollingActivityItem, toolDisplayName, annotationHasNote, annotationDisplayOrdinal, ANNOTATION_MAX_NOTE_LENGTH, questionNavTarget, timelineGroupIndexAtSequence, QUESTION_NAV_MARGIN_PX, flattenActivityItems, timelineGroupContainsPresentedImage, durationBetween, isPreparingWork, readableWorkStatus, readableWorkShowsPreview, readableWorkDefaultOpen, compactedLandmarkCount, SESSION_STATUS_PRESENTATION, formatRelativeTime, recordedWaitSummaryText, noticeTone, noticeDisplayText, isActionableHumanInputRequest, humanInputHeading, initialDrafts, emptyDraft, SESSION_STATUS_BADGE, workerRowTitle, startupDuration, STARTUP_WAIT_TITLES, startupPhaseTitle, sandboxRowTitle, toolRowPresentation, presentsToolOutputPreview, GENIE_WAITING_PHRASES, GENIE_PREPARING_PHRASES, truncateAnnotationQuote, annotationNoteNeedsDisclosure, annotationFromSelection, answersFromDrafts, notificationPlainText, pathDirname, pathBasename, formatClockTime, parseNotificationText } from '@opengeni/react/timeline-model';
12
12
  import { createContext, useContext, useRef, useState, useEffect, useMemo, useCallback, useId } from 'react';
13
- import { Platform, Text, View, Pressable, ScrollView, ActivityIndicator, Modal, KeyboardAvoidingView, TextInput, Image, useWindowDimensions, Animated as Animated$1, AccessibilityInfo, Easing as Easing$1, PanResponder, AppState, Alert, ActionSheetIOS, Linking } from 'react-native';
14
- import Animated, { FadeIn, FadeOut, FadeOutUp, FadeInDown, useAnimatedKeyboard, useAnimatedStyle, useReducedMotion, useSharedValue, cancelAnimation, withRepeat, withTiming, Easing } from 'react-native-reanimated';
13
+ import { Platform, Text, View, Pressable, useWindowDimensions, Animated as Animated$1, AccessibilityInfo, Easing, PanResponder, Modal, KeyboardAvoidingView, ScrollView, TextInput, Keyboard, ActivityIndicator, Image, Alert, AppState, ActionSheetIOS, Linking } from 'react-native';
14
+ import Animated, { FadeIn, FadeOut, FadeOutUp, FadeInDown, useAnimatedKeyboard, useAnimatedStyle, useReducedMotion, useSharedValue, cancelAnimation, withRepeat, withTiming, Easing as Easing$1 } from 'react-native-reanimated';
15
15
  import { createOpenGeniText, useOpenGeniText, SingleSendWording } from '@opengeni/react/messages';
16
16
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
17
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
17
18
  import { defaultChatComposerMessages } from '@opengeni/react/composer-messages';
18
19
  import { isLiquidGlassAvailable, GlassView } from 'expo-glass-effect';
19
20
  import { loadSessionFeedback, turnRatingsFromFeedback, feedbackSentimentLabel, feedbackDialogCopy, feedbackSubmission } from '@opengeni/react/session-feedback-model';
@@ -23,9 +24,60 @@ import { modelMarkVendor, MODEL_MARK_PATHS } from '@opengeni/react/model-mark-pa
23
24
  import { projectClientModelRows, labelReasoningEffort } from '@opengeni/react/model-policy';
24
25
  import { sessionDisplayTitle, recentSessionStatus, recentSessionModelPresentation, sessionRepoLabel, relativeTimeLabel, sessionRenameSeed, SESSION_TITLE_MAX_LENGTH, resolveRenameSubmission } from '@opengeni/react/session-list-model';
25
26
  import Svg, { Path } from 'react-native-svg';
26
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
27
27
  import { modelPickerMenuView, groupPresentationFor, compactEffortLabel, modelPickerChoice } from '@opengeni/react/model-picker-model';
28
28
 
29
+ var ANNOTATION_SOURCE_TEST_ID_PREFIX = "og-annotation-source:";
30
+ function annotationSourceTestId(sourceKey) {
31
+ return `${ANNOTATION_SOURCE_TEST_ID_PREFIX}${sourceKey ?? ""}`;
32
+ }
33
+ function textSelectionModule() {
34
+ if (Platform.OS !== "ios" && Platform.OS !== "android") return null;
35
+ const registry = globalThis.expo?.modules;
36
+ const module = registry?.OpenGeniTextSelection;
37
+ return module && typeof module.addListener === "function" ? module : null;
38
+ }
39
+ async function measureQuoteAnchors(sourceKey, queries) {
40
+ const module = textSelectionModule();
41
+ if (!module?.quoteAnchors || queries.length === 0) return queries.map(() => null);
42
+ try {
43
+ const anchors = await module.quoteAnchors(sourceKey, queries);
44
+ return queries.map((_, index) => {
45
+ const anchor = anchors[index];
46
+ if (typeof anchor?.x !== "number" || typeof anchor.top !== "number") return null;
47
+ const flat = anchor.rects ?? [];
48
+ const boxes = [];
49
+ for (let at = 0; at + 3 < flat.length; at += 4) {
50
+ boxes.push({ x: flat[at], y: flat[at + 1], width: flat[at + 2], height: flat[at + 3] });
51
+ }
52
+ return { x: anchor.x, top: anchor.top, boxes };
53
+ });
54
+ } catch {
55
+ return queries.map(() => null);
56
+ }
57
+ }
58
+ function useNativeTextAnnotation(sources, onAnnotate) {
59
+ const t = useOpenGeniText();
60
+ const latest = useRef({ sources, onAnnotate });
61
+ latest.current = { sources, onAnnotate };
62
+ const enabled = Boolean(onAnnotate);
63
+ const title = t("timelineAnnotationSelection.addNote");
64
+ useEffect(() => {
65
+ const module = textSelectionModule();
66
+ if (!module || !enabled) return;
67
+ module.setAnnotateTitle(title);
68
+ const subscription = module.addListener("onAnnotate", (event) => {
69
+ const source = latest.current.sources.get(event.sourceKey);
70
+ if (!source) return;
71
+ const annotation = annotationFromSelection(source, {
72
+ text: event.quote,
73
+ textBefore: event.before,
74
+ textAfter: event.after
75
+ });
76
+ if (annotation) latest.current.onAnnotate?.(annotation);
77
+ });
78
+ return () => subscription.remove();
79
+ }, [enabled, title]);
80
+ }
29
81
  var ActivityOptionsContext = createContext({});
30
82
  var NativeActivityOptionsProvider = ActivityOptionsContext.Provider;
31
83
  function useNativeActivityOptions() {
@@ -299,7 +351,8 @@ function PatchView({
299
351
  }
300
352
  function PresentedToolRow({
301
353
  presentation: p,
302
- compact = false
354
+ compact = false,
355
+ outputPreview
303
356
  }) {
304
357
  return /* @__PURE__ */ jsx(
305
358
  ActivityDisclosure,
@@ -315,6 +368,7 @@ function PresentedToolRow({
315
368
  compact,
316
369
  preview: p.preview ? previewContent(p.preview, compact) : void 0,
317
370
  expandable: p.body.kind !== "none",
371
+ outputPreview,
318
372
  children: /* @__PURE__ */ jsx(ToolBodyView, { body: p.body })
319
373
  }
320
374
  );
@@ -328,12 +382,41 @@ function ToolCallRow({ item, compact }) {
328
382
  });
329
383
  const leaf = item.name.includes("__") ? item.name.slice(item.name.lastIndexOf("__") + 2) : item.name;
330
384
  const custom = options.toolRenderers?.[item.name] ?? options.toolRenderers?.[leaf];
331
- for (const renderer of [custom, options.renderTool]) {
332
- if (!renderer) continue;
333
- const node = renderer({ item, presentation, compact });
334
- if (node != null) return /* @__PURE__ */ jsx(Fragment, { children: node });
385
+ const outputPreview = item.outputPreview ?? null;
386
+ const exactFirst = outputPreview !== null && (custom !== void 0 || options.renderTool !== void 0 || !presentsToolOutputPreview(item));
387
+ if (exactFirst && outputPreview.load && outputPreview.state !== "error") {
388
+ return /* @__PURE__ */ jsx(ToolOutputExactRow, { item, preview: outputPreview, compact });
335
389
  }
336
- return /* @__PURE__ */ jsx(PresentedToolRow, { presentation, compact });
390
+ if (!exactFirst) {
391
+ for (const renderer of [custom, options.renderTool]) {
392
+ if (!renderer) continue;
393
+ const node = renderer({ item, presentation, compact });
394
+ if (node != null) return /* @__PURE__ */ jsx(Fragment, { children: node });
395
+ }
396
+ }
397
+ return /* @__PURE__ */ jsx(PresentedToolRow, { presentation, compact, outputPreview });
398
+ }
399
+ function ToolOutputExactRow({
400
+ item,
401
+ preview,
402
+ compact
403
+ }) {
404
+ const t = useOpenGeniText();
405
+ const pending = preview.state === "preview";
406
+ const load = preview.load;
407
+ useEffect(() => {
408
+ if (pending) load?.();
409
+ }, [pending, load]);
410
+ return /* @__PURE__ */ jsx(
411
+ ActivityDisclosure,
412
+ {
413
+ icon: "wrench",
414
+ title: toolDisplayName(t, item.name),
415
+ preview: t("timeline.toolRenderers.loadingOutput"),
416
+ compact,
417
+ expandable: false
418
+ }
419
+ );
337
420
  }
338
421
  function ReasoningRow({ item, compact }) {
339
422
  const t = useOpenGeniText();
@@ -369,9 +452,10 @@ function ReasoningRow({ item, compact }) {
369
452
  ),
370
453
  accessibilityLabel: item.streaming ? m.thinking : m.thought,
371
454
  preview: plain || void 0,
372
- children: options.renderMarkdown ? options.renderMarkdown(item.text, { tone: "muted", streaming: item.streaming }) : /* @__PURE__ */ jsx(
455
+ children: /* @__PURE__ */ jsx(View, { testID: annotationSourceTestId(void 0), collapsable: false, children: options.renderMarkdown ? options.renderMarkdown(item.text, { tone: "muted", streaming: item.streaming }) : /* @__PURE__ */ jsx(
373
456
  Text,
374
457
  {
458
+ selectable: true,
375
459
  style: {
376
460
  ...fontStyle(theme),
377
461
  fontSize: theme.size.base,
@@ -380,7 +464,7 @@ function ReasoningRow({ item, compact }) {
380
464
  },
381
465
  children: item.text
382
466
  }
383
- )
467
+ ) })
384
468
  }
385
469
  );
386
470
  }
@@ -547,18 +631,27 @@ function ActivityNoteRow({ item }) {
547
631
  children: [
548
632
  /* @__PURE__ */ jsx(View, { style: { width: 14 } }),
549
633
  /* @__PURE__ */ jsx(View, { style: { marginTop: 4 }, children: /* @__PURE__ */ jsx(Icon, { name: "message-square-text", size: 14, color: theme.colors["fg-subtle"] }) }),
550
- /* @__PURE__ */ jsx(View, { style: { flex: 1, minWidth: 0 }, children: options.renderMarkdown ? options.renderMarkdown(item.text, { tone: "muted", streaming: item.streaming }) : /* @__PURE__ */ jsx(
551
- Text,
634
+ /* @__PURE__ */ jsx(
635
+ View,
552
636
  {
553
- style: {
554
- ...fontStyle(theme),
555
- fontSize: theme.size.sm,
556
- lineHeight: 20,
557
- color: theme.colors["fg-muted"]
558
- },
559
- children: item.text
637
+ testID: annotationSourceTestId(item.annotationSource?.eventId),
638
+ collapsable: false,
639
+ style: { flex: 1, minWidth: 0 },
640
+ children: options.renderMarkdown ? options.renderMarkdown(item.text, { tone: "muted", streaming: item.streaming }) : /* @__PURE__ */ jsx(
641
+ Text,
642
+ {
643
+ selectable: true,
644
+ style: {
645
+ ...fontStyle(theme),
646
+ fontSize: theme.size.sm,
647
+ lineHeight: 20,
648
+ color: theme.colors["fg-muted"]
649
+ },
650
+ children: item.text
651
+ }
652
+ )
560
653
  }
561
- ) })
654
+ )
562
655
  ]
563
656
  }
564
657
  );
@@ -1139,9 +1232,828 @@ function liveFoldShift(liveNotes, workId, open, heights, rowGap) {
1139
1232
  }, 0);
1140
1233
  return height === 0 ? 0 : open ? -height : height;
1141
1234
  }
1235
+ function BottomSheet(props) {
1236
+ const t = useOpenGeniText();
1237
+ const theme = useNativeTimelineTheme();
1238
+ const c = theme.colors;
1239
+ const insets = useSafeAreaInsets();
1240
+ const { height } = useWindowDimensions();
1241
+ const [mounted, setMounted] = useState(props.open);
1242
+ const progress = useRef(new Animated$1.Value(0)).current;
1243
+ const drag = useRef(new Animated$1.Value(0)).current;
1244
+ const onClose = useRef(props.onClose);
1245
+ onClose.current = props.onClose;
1246
+ useEffect(() => {
1247
+ let cancelled = false;
1248
+ if (props.open) {
1249
+ setMounted(true);
1250
+ drag.setValue(0);
1251
+ void AccessibilityInfo.isReduceMotionEnabled().then((reduce) => {
1252
+ if (cancelled) return;
1253
+ Animated$1.timing(progress, {
1254
+ toValue: 1,
1255
+ duration: reduce ? 0 : 260,
1256
+ easing: Easing.bezier(0.22, 1, 0.36, 1),
1257
+ useNativeDriver: true
1258
+ }).start();
1259
+ });
1260
+ } else if (mounted) {
1261
+ Animated$1.timing(progress, {
1262
+ toValue: 0,
1263
+ duration: 200,
1264
+ easing: Easing.in(Easing.quad),
1265
+ useNativeDriver: true
1266
+ }).start(({ finished }) => {
1267
+ if (finished && !cancelled) setMounted(false);
1268
+ });
1269
+ }
1270
+ return () => {
1271
+ cancelled = true;
1272
+ };
1273
+ }, [drag, mounted, progress, props.open]);
1274
+ const pan = useRef(
1275
+ PanResponder.create({
1276
+ onMoveShouldSetPanResponder: (_event, gesture) => gesture.dy > 6 && Math.abs(gesture.dy) > Math.abs(gesture.dx),
1277
+ onPanResponderMove: (_event, gesture) => drag.setValue(Math.max(0, gesture.dy)),
1278
+ onPanResponderRelease: (_event, gesture) => {
1279
+ if (gesture.dy > 90 || gesture.vy > 0.9) onClose.current();
1280
+ else Animated$1.spring(drag, { toValue: 0, useNativeDriver: true, bounciness: 0 }).start();
1281
+ },
1282
+ onPanResponderTerminate: () => Animated$1.spring(drag, { toValue: 0, useNativeDriver: true, bounciness: 0 }).start()
1283
+ })
1284
+ ).current;
1285
+ if (!mounted) return null;
1286
+ const translateY = Animated$1.add(
1287
+ progress.interpolate({ inputRange: [0, 1], outputRange: [height, 0] }),
1288
+ drag
1289
+ );
1290
+ return /* @__PURE__ */ jsx(
1291
+ Modal,
1292
+ {
1293
+ visible: true,
1294
+ transparent: true,
1295
+ animationType: "none",
1296
+ statusBarTranslucent: true,
1297
+ onRequestClose: props.onClose,
1298
+ children: /* @__PURE__ */ jsxs(
1299
+ KeyboardAvoidingView,
1300
+ {
1301
+ behavior: "padding",
1302
+ style: { flex: 1, justifyContent: "flex-end" },
1303
+ children: [
1304
+ /* @__PURE__ */ jsx(
1305
+ Animated$1.View,
1306
+ {
1307
+ style: {
1308
+ position: "absolute",
1309
+ top: 0,
1310
+ right: 0,
1311
+ bottom: 0,
1312
+ left: 0,
1313
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
1314
+ opacity: progress
1315
+ },
1316
+ children: /* @__PURE__ */ jsx(
1317
+ Pressable,
1318
+ {
1319
+ style: { flex: 1 },
1320
+ onPress: props.onClose,
1321
+ accessibilityRole: "button",
1322
+ accessibilityLabel: t("native.timeline.sheet.close")
1323
+ }
1324
+ )
1325
+ }
1326
+ ),
1327
+ /* @__PURE__ */ jsxs(
1328
+ Animated$1.View,
1329
+ {
1330
+ accessibilityViewIsModal: true,
1331
+ accessibilityLabel: props.accessibilityLabel,
1332
+ style: {
1333
+ maxHeight: height * (props.maxHeightRatio ?? 0.85),
1334
+ backgroundColor: c.bg,
1335
+ borderTopLeftRadius: 16,
1336
+ borderTopRightRadius: 16,
1337
+ paddingBottom: Math.max(insets.bottom, 12),
1338
+ transform: [{ translateY }],
1339
+ shadowColor: "#000",
1340
+ shadowOpacity: 0.12,
1341
+ shadowRadius: 16,
1342
+ shadowOffset: { width: 0, height: -2 },
1343
+ elevation: 16
1344
+ },
1345
+ children: [
1346
+ /* @__PURE__ */ jsx(
1347
+ View,
1348
+ {
1349
+ ...pan.panHandlers,
1350
+ style: { alignItems: "center", paddingTop: 8, paddingBottom: 4 },
1351
+ children: /* @__PURE__ */ jsx(
1352
+ View,
1353
+ {
1354
+ style: {
1355
+ width: 36,
1356
+ height: 5,
1357
+ borderRadius: 3,
1358
+ backgroundColor: c["border-strong"] ?? c.border
1359
+ }
1360
+ }
1361
+ )
1362
+ }
1363
+ ),
1364
+ props.children
1365
+ ]
1366
+ }
1367
+ )
1368
+ ]
1369
+ }
1370
+ )
1371
+ }
1372
+ );
1373
+ }
1374
+ function annotationQuoteQuery(annotation) {
1375
+ return {
1376
+ quote: annotation.quote,
1377
+ before: annotation.source.contextBefore,
1378
+ after: annotation.source.contextAfter
1379
+ };
1380
+ }
1381
+ var AnnotationSourcesContext = createContext(null);
1382
+ function draftMarkersBySource(annotations) {
1383
+ const markers = /* @__PURE__ */ new Map();
1384
+ (annotations ?? []).forEach((annotation, index) => {
1385
+ const list = markers.get(annotation.source.eventId) ?? [];
1386
+ list.push({
1387
+ id: annotation.id,
1388
+ ordinal: index + 1,
1389
+ incomplete: !annotationHasNote(annotation.note),
1390
+ quote: annotation.quote,
1391
+ before: annotation.source.contextBefore,
1392
+ after: annotation.source.contextAfter
1393
+ });
1394
+ markers.set(annotation.source.eventId, list);
1395
+ });
1396
+ return markers;
1397
+ }
1398
+ function quotePreview(quote) {
1399
+ const trimmed = quote.trim();
1400
+ return trimmed.includes("\n") ? trimmed : trimmed.replace(/\s+/g, " ");
1401
+ }
1402
+ function AnnotationSourceView({
1403
+ sourceKey,
1404
+ children,
1405
+ style
1406
+ }) {
1407
+ const theme = useNativeTimelineTheme();
1408
+ const sources = useContext(AnnotationSourcesContext);
1409
+ const glow = useRef(new Animated$1.Value(0)).current;
1410
+ const flash = sourceKey && sources?.flash?.sourceKey === sourceKey ? sources.flash : null;
1411
+ const [flashBoxes, setFlashBoxes] = useState(null);
1412
+ useEffect(() => {
1413
+ if (!flash || !sourceKey) return;
1414
+ let cancelled = false;
1415
+ const start = (boxes) => {
1416
+ if (cancelled) return;
1417
+ setFlashBoxes(boxes);
1418
+ glow.setValue(1);
1419
+ Animated$1.timing(glow, {
1420
+ toValue: 0,
1421
+ duration: boxes ? 1800 : 1400,
1422
+ delay: 300,
1423
+ easing: Easing.out(Easing.quad),
1424
+ useNativeDriver: false
1425
+ }).start();
1426
+ };
1427
+ if (!flash.quote) start(null);
1428
+ else
1429
+ void measureQuoteAnchors(sourceKey, [flash.quote]).then(
1430
+ ([anchor]) => start(anchor?.boxes.length ? anchor.boxes : null)
1431
+ );
1432
+ return () => {
1433
+ cancelled = true;
1434
+ };
1435
+ }, [flash, glow, sourceKey]);
1436
+ const register = sources?.register;
1437
+ const markers = sourceKey ? sources?.drafts.get(sourceKey) : void 0;
1438
+ const [anchors, setAnchors] = useState(null);
1439
+ const [size, setSize] = useState({ width: 0, height: 0 });
1440
+ const measure = useCallback(() => {
1441
+ if (!sourceKey || !markers?.length) return;
1442
+ const queries = markers.map(({ quote, before, after }) => ({ quote, before, after }));
1443
+ void measureQuoteAnchors(sourceKey, queries).then((at) => setAnchors({ markers, at }));
1444
+ }, [markers, sourceKey]);
1445
+ useEffect(() => {
1446
+ if (!markers?.length) {
1447
+ setAnchors(null);
1448
+ return;
1449
+ }
1450
+ const frame = requestAnimationFrame(measure);
1451
+ return () => cancelAnimationFrame(frame);
1452
+ }, [markers, measure, size.width, size.height]);
1453
+ return /* @__PURE__ */ jsxs(
1454
+ Animated$1.View,
1455
+ {
1456
+ ref: (node) => {
1457
+ if (sourceKey && register) register(sourceKey, node);
1458
+ },
1459
+ testID: annotationSourceTestId(sourceKey),
1460
+ collapsable: false,
1461
+ onLayout: (event) => {
1462
+ const { width, height } = event.nativeEvent.layout;
1463
+ setSize(
1464
+ (current) => current.width === width && current.height === height ? current : { width, height }
1465
+ );
1466
+ },
1467
+ style: [
1468
+ style,
1469
+ {
1470
+ borderRadius: theme.radius.md,
1471
+ backgroundColor: flashBoxes ? "transparent" : glow.interpolate({
1472
+ inputRange: [0, 1],
1473
+ outputRange: [withAlpha(theme.colors.fg, 0), withAlpha(theme.colors.fg, 0.08)]
1474
+ })
1475
+ }
1476
+ ],
1477
+ children: [
1478
+ flashBoxes ? /* @__PURE__ */ jsx(QuoteFlash, { boxes: flashBoxes, glow }) : null,
1479
+ markers?.length && anchors?.markers === markers ? /* @__PURE__ */ jsx(QuoteHighlights, { anchors: anchors.at }) : null,
1480
+ children,
1481
+ markers?.length && anchors?.markers === markers ? /* @__PURE__ */ jsx(
1482
+ QuoteBadges,
1483
+ {
1484
+ markers,
1485
+ anchors: anchors.at,
1486
+ size,
1487
+ onSelect: sources?.onSelectDraft
1488
+ }
1489
+ ) : null
1490
+ ]
1491
+ }
1492
+ );
1493
+ }
1494
+ var BADGE_SIZE = 15;
1495
+ function boxKey(box) {
1496
+ return `${Math.round(box.x)}:${Math.round(box.y)}:${Math.round(box.width)}`;
1497
+ }
1498
+ function QuoteFlash({ boxes, glow }) {
1499
+ const theme = useNativeTimelineTheme();
1500
+ const tint = withAlpha(theme.colors.accent, theme.scheme === "dark" ? 0.45 : 0.28);
1501
+ return /* @__PURE__ */ jsx(Fragment, { children: boxes.map((box) => /* @__PURE__ */ jsx(
1502
+ Animated$1.View,
1503
+ {
1504
+ pointerEvents: "none",
1505
+ style: {
1506
+ position: "absolute",
1507
+ left: box.x - 3,
1508
+ top: box.y - 2,
1509
+ width: box.width + 6,
1510
+ height: box.height + 4,
1511
+ borderRadius: 5,
1512
+ backgroundColor: tint,
1513
+ opacity: glow
1514
+ }
1515
+ },
1516
+ boxKey(box)
1517
+ )) });
1518
+ }
1519
+ function QuoteHighlights({ anchors }) {
1520
+ const theme = useNativeTimelineTheme();
1521
+ const dark = theme.scheme === "dark";
1522
+ const tint = withAlpha(theme.colors.accent, dark ? 0.16 : 0.08);
1523
+ const rule = withAlpha(theme.colors.accent, dark ? 0.7 : 0.55);
1524
+ return /* @__PURE__ */ jsx(Fragment, { children: anchors.flatMap(
1525
+ (anchor) => (anchor?.boxes ?? []).map((box) => /* @__PURE__ */ jsx(
1526
+ View,
1527
+ {
1528
+ pointerEvents: "none",
1529
+ style: {
1530
+ position: "absolute",
1531
+ left: box.x - 2,
1532
+ top: box.y - 1,
1533
+ width: box.width + 4,
1534
+ height: box.height + 2,
1535
+ borderTopLeftRadius: 3,
1536
+ borderTopRightRadius: 3,
1537
+ borderBottomWidth: 1.5,
1538
+ borderBottomColor: rule,
1539
+ backgroundColor: tint
1540
+ }
1541
+ },
1542
+ boxKey(box)
1543
+ ))
1544
+ ) });
1545
+ }
1546
+ function QuoteBadges({
1547
+ markers,
1548
+ anchors,
1549
+ size,
1550
+ onSelect
1551
+ }) {
1552
+ const t = useOpenGeniText();
1553
+ const theme = useNativeTimelineTheme();
1554
+ const c = theme.colors;
1555
+ let unplaced = 0;
1556
+ return /* @__PURE__ */ jsx(Fragment, { children: markers.map((marker, index) => {
1557
+ const anchor = anchors[index];
1558
+ const center = anchor ? { x: anchor.x + 2, y: anchor.top - 1 } : {
1559
+ x: size.width - BADGE_SIZE / 2 - unplaced++ * (BADGE_SIZE + 4),
1560
+ y: size.height - BADGE_SIZE / 2
1561
+ };
1562
+ const left = Math.min(Math.max(center.x - BADGE_SIZE / 2, 0), size.width - BADGE_SIZE);
1563
+ return /* @__PURE__ */ jsx(
1564
+ Pressable,
1565
+ {
1566
+ accessibilityRole: "button",
1567
+ accessibilityLabel: t("timelineAnnotationMarkers.annotationOrdinal", {
1568
+ ordinal: marker.ordinal
1569
+ }),
1570
+ hitSlop: 14,
1571
+ disabled: !onSelect,
1572
+ onPress: () => onSelect?.(marker.id),
1573
+ style: ({ pressed }) => ({
1574
+ position: "absolute",
1575
+ left,
1576
+ top: center.y - BADGE_SIZE / 2,
1577
+ zIndex: marker.ordinal,
1578
+ minWidth: BADGE_SIZE,
1579
+ height: BADGE_SIZE,
1580
+ paddingHorizontal: 2,
1581
+ borderRadius: BADGE_SIZE / 2,
1582
+ alignItems: "center",
1583
+ justifyContent: "center",
1584
+ backgroundColor: c.accent,
1585
+ borderWidth: marker.incomplete ? 1 : 0,
1586
+ borderColor: withAlpha(c["accent-fg"], 0.35),
1587
+ opacity: pressed ? 0.7 : 1,
1588
+ shadowColor: "#000",
1589
+ shadowOpacity: 0.12,
1590
+ shadowRadius: 2,
1591
+ shadowOffset: { width: 0, height: 1 }
1592
+ }),
1593
+ children: /* @__PURE__ */ jsx(
1594
+ Text,
1595
+ {
1596
+ style: {
1597
+ ...fontStyle(theme, 600),
1598
+ fontSize: 10,
1599
+ lineHeight: 12,
1600
+ color: c["accent-fg"],
1601
+ fontVariant: ["tabular-nums"]
1602
+ },
1603
+ children: marker.ordinal
1604
+ }
1605
+ )
1606
+ },
1607
+ marker.id
1608
+ );
1609
+ }) });
1610
+ }
1611
+ function AccentRow({ children, shrink = false }) {
1612
+ const theme = useNativeTimelineTheme();
1613
+ return /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", gap: 10 }, children: [
1614
+ /* @__PURE__ */ jsx(
1615
+ View,
1616
+ {
1617
+ style: {
1618
+ width: 2,
1619
+ borderRadius: 1,
1620
+ backgroundColor: withAlpha(theme.colors.accent, 0.55)
1621
+ }
1622
+ }
1623
+ ),
1624
+ /* @__PURE__ */ jsx(View, { style: shrink ? { flexShrink: 1, minWidth: 0 } : { flex: 1, minWidth: 0 }, children })
1625
+ ] });
1626
+ }
1627
+ function OrdinalLabel({ ordinal }) {
1628
+ const t = useOpenGeniText();
1629
+ const theme = useNativeTimelineTheme();
1630
+ return /* @__PURE__ */ jsx(
1631
+ Text,
1632
+ {
1633
+ style: {
1634
+ ...fontStyle(theme, 500),
1635
+ fontSize: theme.size.xs,
1636
+ lineHeight: 16,
1637
+ color: theme.colors["fg-subtle"],
1638
+ fontVariant: ["tabular-nums"]
1639
+ },
1640
+ children: t("timelineAnnotations.annotationOrdinal", { ordinal })
1641
+ }
1642
+ );
1643
+ }
1644
+ function QuoteButton({
1645
+ annotation,
1646
+ onReveal
1647
+ }) {
1648
+ const t = useOpenGeniText();
1649
+ const theme = useNativeTimelineTheme();
1650
+ const [unavailable, setUnavailable] = useState(false);
1651
+ const text = /* @__PURE__ */ jsx(
1652
+ Text,
1653
+ {
1654
+ numberOfLines: 2,
1655
+ style: {
1656
+ ...fontStyle(theme),
1657
+ fontSize: theme.size.sm,
1658
+ lineHeight: 20,
1659
+ color: theme.colors["fg-muted"]
1660
+ },
1661
+ children: quotePreview(annotation.quote)
1662
+ }
1663
+ );
1664
+ if (!onReveal) return text;
1665
+ return /* @__PURE__ */ jsxs(View, { children: [
1666
+ /* @__PURE__ */ jsx(
1667
+ Pressable,
1668
+ {
1669
+ accessibilityRole: "button",
1670
+ accessibilityLabel: t(
1671
+ "timelineAnnotationChrome.viewSourcelabelSourceTruncateannotationquote",
1672
+ {
1673
+ sourceLabel: annotation.source.label?.trim() || (annotation.source.kind === "user_message" ? t("timelineAnnotationShared.yourMessage") : annotation.source.kind === "assistant_message" ? t("timelineAnnotationShared.assistant") : t("timelineAnnotationShared.toolOutput")),
1674
+ truncateAnnotationQuote: truncateAnnotationQuote(annotation.quote, 88)
1675
+ }
1676
+ ),
1677
+ onPress: () => setUnavailable(!onReveal(annotation.source, annotationQuoteQuery(annotation))),
1678
+ style: ({ pressed }) => ({ opacity: pressed ? 0.55 : 1, paddingVertical: 2 }),
1679
+ children: text
1680
+ }
1681
+ ),
1682
+ unavailable ? /* @__PURE__ */ jsx(
1683
+ Text,
1684
+ {
1685
+ accessibilityLiveRegion: "polite",
1686
+ style: {
1687
+ ...fontStyle(theme),
1688
+ fontSize: theme.size.xs,
1689
+ lineHeight: 16,
1690
+ color: theme.colors["status-waiting"]
1691
+ },
1692
+ children: t("timelineAnnotations.sourceIsOutsideTheLoaded")
1693
+ }
1694
+ ) : null
1695
+ ] });
1696
+ }
1697
+ function NotePreview({ note, ordinal }) {
1698
+ const t = useOpenGeniText();
1699
+ const theme = useNativeTimelineTheme();
1700
+ const [expanded, setExpanded] = useState(false);
1701
+ if (!note) return null;
1702
+ const collapsible = annotationNoteNeedsDisclosure(note);
1703
+ return /* @__PURE__ */ jsxs(View, { style: { marginTop: 2 }, children: [
1704
+ /* @__PURE__ */ jsx(
1705
+ Text,
1706
+ {
1707
+ selectable: true,
1708
+ numberOfLines: expanded ? void 0 : 4,
1709
+ style: {
1710
+ ...fontStyle(theme),
1711
+ fontSize: theme.size.sm,
1712
+ lineHeight: 20,
1713
+ color: theme.colors.fg
1714
+ },
1715
+ children: note
1716
+ }
1717
+ ),
1718
+ collapsible ? /* @__PURE__ */ jsx(
1719
+ Pressable,
1720
+ {
1721
+ accessibilityRole: "button",
1722
+ accessibilityLabel: expanded ? t("timelineAnnotationChrome.showLessAnnotationOrdinal", { ordinal }) : t("timelineAnnotationChrome.showMoreAnnotationOrdinal", { ordinal }),
1723
+ hitSlop: 8,
1724
+ onPress: () => setExpanded((current) => !current),
1725
+ style: { alignSelf: "flex-start", paddingVertical: 4 },
1726
+ children: /* @__PURE__ */ jsx(
1727
+ Text,
1728
+ {
1729
+ style: {
1730
+ ...fontStyle(theme, 500),
1731
+ fontSize: theme.size.xs,
1732
+ color: theme.colors["fg-muted"]
1733
+ },
1734
+ children: expanded ? t("timelineAnnotationChrome.showLess") : t("timelineAnnotationChrome.showMore")
1735
+ }
1736
+ )
1737
+ }
1738
+ ) : null
1739
+ ] });
1740
+ }
1741
+ function SentAnnotationCards({
1742
+ annotations,
1743
+ onReveal
1744
+ }) {
1745
+ if (annotations.length === 0) return null;
1746
+ return /* @__PURE__ */ jsx(View, { style: { gap: 10 }, children: annotations.map((annotation, index) => {
1747
+ const ordinal = annotationDisplayOrdinal(annotation, index);
1748
+ return /* @__PURE__ */ jsxs(AccentRow, { shrink: true, children: [
1749
+ /* @__PURE__ */ jsx(OrdinalLabel, { ordinal }),
1750
+ /* @__PURE__ */ jsx(QuoteButton, { annotation, onReveal }),
1751
+ /* @__PURE__ */ jsx(NotePreview, { note: annotation.note, ordinal })
1752
+ ] }, annotation.id);
1753
+ }) });
1754
+ }
1755
+ function AnnotationsChip({
1756
+ annotations,
1757
+ onPress,
1758
+ onClear,
1759
+ compact = false
1760
+ }) {
1761
+ const t = useOpenGeniText();
1762
+ const theme = useNativeTimelineTheme();
1763
+ const c = theme.colors;
1764
+ if (annotations.length === 0) return null;
1765
+ const countLabel = t("counts.annotations", { count: annotations.length });
1766
+ const incomplete = onClear !== void 0 && annotations.some((item) => !annotationHasNote(item.note));
1767
+ const height = compact ? 26 : 32;
1768
+ return /* @__PURE__ */ jsxs(
1769
+ View,
1770
+ {
1771
+ style: {
1772
+ flexDirection: "row",
1773
+ alignItems: "center",
1774
+ alignSelf: "flex-start",
1775
+ height,
1776
+ borderRadius: height / 2,
1777
+ borderWidth: 1,
1778
+ borderColor: incomplete ? withAlpha(c.accent, 0.45) : c.border,
1779
+ backgroundColor: incomplete ? c["accent-soft"] : c["surface-1"]
1780
+ },
1781
+ children: [
1782
+ /* @__PURE__ */ jsxs(
1783
+ Pressable,
1784
+ {
1785
+ accessibilityRole: "button",
1786
+ accessibilityLabel: t("timelineAnnotations.reviewCountlabel", { countLabel }),
1787
+ hitSlop: { top: (44 - height) / 2, bottom: (44 - height) / 2 },
1788
+ onPress,
1789
+ style: ({ pressed }) => ({
1790
+ flexDirection: "row",
1791
+ alignItems: "center",
1792
+ gap: 6,
1793
+ height,
1794
+ paddingLeft: compact ? 8 : 10,
1795
+ paddingRight: onClear ? 2 : compact ? 8 : 12,
1796
+ opacity: pressed ? 0.6 : 1
1797
+ }),
1798
+ children: [
1799
+ /* @__PURE__ */ jsx(Icon, { name: "quote", size: compact ? 11 : 13, color: c.accent }),
1800
+ /* @__PURE__ */ jsx(
1801
+ Text,
1802
+ {
1803
+ numberOfLines: 1,
1804
+ style: {
1805
+ ...fontStyle(theme, 500),
1806
+ fontSize: compact ? theme.size.xs : theme.size.sm,
1807
+ color: c["fg-muted"]
1808
+ },
1809
+ children: countLabel
1810
+ }
1811
+ )
1812
+ ]
1813
+ }
1814
+ ),
1815
+ onClear ? /* @__PURE__ */ jsx(
1816
+ Pressable,
1817
+ {
1818
+ accessibilityRole: "button",
1819
+ accessibilityLabel: t("timelineAnnotations.removeAllAnnotations"),
1820
+ hitSlop: { top: 6, bottom: 6, right: 6 },
1821
+ onPress: onClear,
1822
+ style: ({ pressed }) => ({
1823
+ width: height,
1824
+ height,
1825
+ alignItems: "center",
1826
+ justifyContent: "center",
1827
+ opacity: pressed ? 0.5 : 1
1828
+ }),
1829
+ children: /* @__PURE__ */ jsx(Icon, { name: "x", size: 13, color: c["fg-subtle"] })
1830
+ }
1831
+ ) : null
1832
+ ]
1833
+ }
1834
+ );
1835
+ }
1836
+ function AnnotationsSheet({
1837
+ open,
1838
+ onClose,
1839
+ annotations,
1840
+ editable,
1841
+ focusId,
1842
+ onFocusConsumed,
1843
+ onUpdate,
1844
+ onRemove,
1845
+ onReveal,
1846
+ onCommit
1847
+ }) {
1848
+ const t = useOpenGeniText();
1849
+ const theme = useNativeTimelineTheme();
1850
+ const c = theme.colors;
1851
+ const inputs = useRef(/* @__PURE__ */ new Map());
1852
+ const scroll = useRef(null);
1853
+ const rowTops = useRef(/* @__PURE__ */ new Map());
1854
+ const empty = annotations.length === 0;
1855
+ useEffect(() => {
1856
+ if (open && empty) onClose();
1857
+ }, [empty, onClose, open]);
1858
+ const focusNote = (id) => {
1859
+ const top = rowTops.current.get(id);
1860
+ if (top !== void 0) scroll.current?.scrollTo({ y: Math.max(0, top - 8), animated: true });
1861
+ inputs.current.get(id)?.focus();
1862
+ };
1863
+ useEffect(() => {
1864
+ if (!open || !focusId || !editable) return;
1865
+ const timer = setTimeout(() => {
1866
+ focusNote(focusId);
1867
+ onFocusConsumed?.();
1868
+ }, 320);
1869
+ return () => clearTimeout(timer);
1870
+ }, [editable, focusId, open]);
1871
+ const commit = (fromId) => {
1872
+ const incomplete = annotations.filter((item) => !annotationHasNote(item.note));
1873
+ if (incomplete.length === 0) {
1874
+ onCommit?.();
1875
+ return;
1876
+ }
1877
+ const fromIndex = annotations.findIndex((item) => item.id === fromId);
1878
+ const next = incomplete.find((item) => annotations.indexOf(item) > fromIndex) ?? incomplete[0];
1879
+ focusNote(next.id);
1880
+ };
1881
+ const incompleteCount = annotations.filter((item) => !annotationHasNote(item.note)).length;
1882
+ const countLabel = t("counts.annotations", { count: annotations.length });
1883
+ return /* @__PURE__ */ jsxs(
1884
+ BottomSheet,
1885
+ {
1886
+ open: open && !empty,
1887
+ onClose,
1888
+ maxHeightRatio: 0.8,
1889
+ accessibilityLabel: editable ? t("timelineAnnotationsDialog.editQuotedNotes") : t("timelineAnnotationsDialog.quotedNotes"),
1890
+ children: [
1891
+ /* @__PURE__ */ jsxs(
1892
+ View,
1893
+ {
1894
+ style: {
1895
+ flexDirection: "row",
1896
+ alignItems: "center",
1897
+ justifyContent: "space-between",
1898
+ paddingLeft: 20,
1899
+ paddingRight: 8,
1900
+ paddingBottom: 4
1901
+ },
1902
+ children: [
1903
+ /* @__PURE__ */ jsx(
1904
+ Text,
1905
+ {
1906
+ accessibilityRole: "header",
1907
+ style: { ...fontStyle(theme, 600), fontSize: theme.size.md, color: c.fg },
1908
+ children: countLabel
1909
+ }
1910
+ ),
1911
+ /* @__PURE__ */ jsx(
1912
+ Pressable,
1913
+ {
1914
+ accessibilityRole: "button",
1915
+ onPress: () => {
1916
+ if (editable && incompleteCount === 0 && onCommit) onCommit();
1917
+ else onClose();
1918
+ },
1919
+ style: ({ pressed }) => ({
1920
+ height: 44,
1921
+ paddingHorizontal: 12,
1922
+ justifyContent: "center",
1923
+ opacity: pressed ? 0.5 : 1
1924
+ }),
1925
+ children: /* @__PURE__ */ jsx(Text, { style: { ...fontStyle(theme, 600), fontSize: theme.size.md, color: c.fg }, children: t("native.timeline.annotations.done") })
1926
+ }
1927
+ )
1928
+ ]
1929
+ }
1930
+ ),
1931
+ /* @__PURE__ */ jsxs(
1932
+ ScrollView,
1933
+ {
1934
+ ref: scroll,
1935
+ keyboardShouldPersistTaps: "handled",
1936
+ contentContainerStyle: { paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12, gap: 18 },
1937
+ style: { flexGrow: 0 },
1938
+ children: [
1939
+ annotations.map((annotation, index) => {
1940
+ const ordinal = annotationDisplayOrdinal(annotation, index);
1941
+ const last = index === annotations.length - 1;
1942
+ const othersIncomplete = annotations.some(
1943
+ (item) => item.id !== annotation.id && !annotationHasNote(item.note)
1944
+ );
1945
+ return /* @__PURE__ */ jsx(
1946
+ View,
1947
+ {
1948
+ onLayout: (event) => rowTops.current.set(annotation.id, event.nativeEvent.layout.y),
1949
+ children: /* @__PURE__ */ jsxs(AccentRow, { children: [
1950
+ /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "flex-start", gap: 4 }, children: [
1951
+ /* @__PURE__ */ jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [
1952
+ /* @__PURE__ */ jsx(OrdinalLabel, { ordinal }),
1953
+ /* @__PURE__ */ jsx(
1954
+ QuoteButton,
1955
+ {
1956
+ annotation,
1957
+ onReveal: onReveal ? (source, quote) => {
1958
+ const revealed = onReveal(source, quote);
1959
+ if (revealed) onClose();
1960
+ return revealed;
1961
+ } : void 0
1962
+ }
1963
+ )
1964
+ ] }),
1965
+ editable && onRemove ? /* @__PURE__ */ jsx(
1966
+ Pressable,
1967
+ {
1968
+ accessibilityRole: "button",
1969
+ accessibilityLabel: t("timelineAnnotationsDialog.removeAnnotationOrdinal", {
1970
+ ordinal
1971
+ }),
1972
+ onPress: () => onRemove(annotation.id),
1973
+ style: ({ pressed }) => ({
1974
+ width: 36,
1975
+ height: 36,
1976
+ marginTop: -6,
1977
+ marginRight: -10,
1978
+ alignItems: "center",
1979
+ justifyContent: "center",
1980
+ opacity: pressed ? 0.5 : 1
1981
+ }),
1982
+ children: /* @__PURE__ */ jsx(Icon, { name: "x", size: 15, color: c["fg-subtle"] })
1983
+ }
1984
+ ) : null
1985
+ ] }),
1986
+ editable && onUpdate ? /* @__PURE__ */ jsx(
1987
+ TextInput,
1988
+ {
1989
+ ref: (node) => {
1990
+ if (node) inputs.current.set(annotation.id, node);
1991
+ else inputs.current.delete(annotation.id);
1992
+ },
1993
+ value: annotation.note,
1994
+ onChangeText: (note) => onUpdate(annotation.id, note),
1995
+ placeholder: t("timelineAnnotationChrome.addANote"),
1996
+ placeholderTextColor: c["fg-subtle"],
1997
+ accessibilityLabel: t("timelineAnnotationChrome.note"),
1998
+ maxLength: ANNOTATION_MAX_NOTE_LENGTH,
1999
+ multiline: true,
2000
+ submitBehavior: "submit",
2001
+ returnKeyType: othersIncomplete || last && !onCommit ? "next" : "done",
2002
+ onSubmitEditing: () => commit(annotation.id),
2003
+ style: {
2004
+ ...fontStyle(theme),
2005
+ fontSize: theme.size.md,
2006
+ lineHeight: 22,
2007
+ color: c.fg,
2008
+ marginTop: 6,
2009
+ minHeight: 40,
2010
+ maxHeight: 132,
2011
+ paddingHorizontal: 12,
2012
+ paddingTop: 9,
2013
+ paddingBottom: 9,
2014
+ borderRadius: theme.radius.md,
2015
+ backgroundColor: c["surface-2"],
2016
+ borderWidth: 1,
2017
+ borderColor: c.border
2018
+ }
2019
+ }
2020
+ ) : /* @__PURE__ */ jsx(NotePreview, { note: annotation.note, ordinal })
2021
+ ] })
2022
+ },
2023
+ annotation.id
2024
+ );
2025
+ }),
2026
+ editable && incompleteCount > 0 ? /* @__PURE__ */ jsx(
2027
+ Text,
2028
+ {
2029
+ style: {
2030
+ ...fontStyle(theme),
2031
+ fontSize: theme.size.xs,
2032
+ lineHeight: 16,
2033
+ color: c["fg-subtle"]
2034
+ },
2035
+ children: t("composerMessages.addANoteToEach")
2036
+ }
2037
+ ) : null
2038
+ ]
2039
+ }
2040
+ )
2041
+ ]
2042
+ }
2043
+ );
2044
+ }
1142
2045
  var OLDER_HISTORY_PREFETCH_PX = 600;
1143
2046
  var USER_MESSAGE_COLLAPSED_PX = 224;
1144
2047
  var ROW_GAP_PX = 20;
2048
+ function annotationSources(items) {
2049
+ const sources = /* @__PURE__ */ new Map();
2050
+ for (const item of items) {
2051
+ if (item.kind !== "user-message" && item.kind !== "agent-message") continue;
2052
+ const source = item.annotationSource;
2053
+ if (source) sources.set(source.eventId, source);
2054
+ }
2055
+ return sources;
2056
+ }
1145
2057
  function MessageTimeline(props) {
1146
2058
  const theme = useNativeTimelineTheme();
1147
2059
  const m = useNativeTimelineMessages();
@@ -1151,6 +2063,8 @@ function MessageTimeline(props) {
1151
2063
  [t, props.items, props.events]
1152
2064
  );
1153
2065
  const groups = useMemo(() => groupTimeline(t, items, { readableTurns: true }), [t, items]);
2066
+ const sources = useMemo(() => annotationSources(items), [items]);
2067
+ useNativeTextAnnotation(sources, props.onAnnotate);
1154
2068
  const foldMemory = useRef(/* @__PURE__ */ new Map()).current;
1155
2069
  const scrollRef = useRef(null);
1156
2070
  const following = useRef(!props.focusSequence);
@@ -1160,6 +2074,72 @@ function MessageTimeline(props) {
1160
2074
  const [questionNav, setQuestionNav] = useState(null);
1161
2075
  const stickyHeaders = useRef(/* @__PURE__ */ new Map()).current;
1162
2076
  const scrollY = useRef(0);
2077
+ const sourceNodes = useRef(/* @__PURE__ */ new Map()).current;
2078
+ const [flash, setFlash] = useState(null);
2079
+ const overlayInsetBottom = useRef(0);
2080
+ overlayInsetBottom.current = props.overlayInsetBottom ?? 0;
2081
+ const registerSource = useCallback(
2082
+ (sourceKey, node) => {
2083
+ if (node) sourceNodes.set(sourceKey, node);
2084
+ else sourceNodes.delete(sourceKey);
2085
+ },
2086
+ [sourceNodes]
2087
+ );
2088
+ const revealSource = useCallback(
2089
+ (source, quote) => {
2090
+ const node = sourceNodes.get(source.eventId);
2091
+ const scroller = scrollRef.current?.getNativeScrollRef?.();
2092
+ if (!node || !scroller) return false;
2093
+ following.current = false;
2094
+ const keyboardGone = Keyboard.isVisible() ? new Promise((resolve) => {
2095
+ const done = () => {
2096
+ subscription.remove();
2097
+ clearTimeout(timer);
2098
+ setTimeout(resolve, 60);
2099
+ };
2100
+ const subscription = Keyboard.addListener("keyboardDidHide", done);
2101
+ const timer = setTimeout(done, 600);
2102
+ Keyboard.dismiss();
2103
+ }) : Promise.resolve();
2104
+ const located = quote ? measureQuoteAnchors(source.eventId, [quote]).then(([anchor]) => anchor ?? null) : Promise.resolve(null);
2105
+ void Promise.all([located, keyboardGone]).then(([anchor]) => {
2106
+ scroller.measureInWindow((_scrollX, scrollTop) => {
2107
+ node.measureInWindow((_x, top, _width, height) => {
2108
+ const y = scrollY.current + top - scrollTop;
2109
+ const visible = (viewportHeight.current || 600) - overlayInsetBottom.current;
2110
+ const first = anchor?.boxes[0];
2111
+ const last = anchor?.boxes[anchor.boxes.length - 1];
2112
+ const span = height < visible - 32 || !first || !last ? { top: y, height } : { top: y + first.y, height: last.y + last.height - first.y };
2113
+ const target = span.height < visible - 32 ? span.top - (visible - span.height) / 2 : span.top - 16;
2114
+ const end = Math.max(0, contentHeight.current - (viewportHeight.current || 0));
2115
+ scrollY.current = Math.min(Math.max(0, target), end || Number.POSITIVE_INFINITY);
2116
+ scrollRef.current?.scrollTo({ y: scrollY.current, animated: true });
2117
+ setFlash((current) => ({
2118
+ sourceKey: source.eventId,
2119
+ token: (current?.token ?? 0) + 1,
2120
+ ...quote ? { quote } : {}
2121
+ }));
2122
+ });
2123
+ });
2124
+ });
2125
+ return true;
2126
+ },
2127
+ [sourceNodes]
2128
+ );
2129
+ if (props.annotationRevealRef) props.annotationRevealRef.current = revealSource;
2130
+ const draftMarkers = useMemo(
2131
+ () => draftMarkersBySource(props.draftAnnotations),
2132
+ [props.draftAnnotations]
2133
+ );
2134
+ const annotationSourcesValue = useMemo(
2135
+ () => ({
2136
+ register: registerSource,
2137
+ flash,
2138
+ drafts: draftMarkers,
2139
+ onSelectDraft: props.onDraftAnnotationSelect
2140
+ }),
2141
+ [draftMarkers, flash, props.onDraftAnnotationSelect, registerSource]
2142
+ );
1163
2143
  const [pinned, setPinned] = useState(
1164
2144
  null
1165
2145
  );
@@ -1307,6 +2287,13 @@ function MessageTimeline(props) {
1307
2287
  },
1308
2288
  [requestOlder]
1309
2289
  );
2290
+ const followLatestSignal = props.followLatestSignal ?? 0;
2291
+ useEffect(() => {
2292
+ if (!followLatestSignal) return;
2293
+ following.current = true;
2294
+ setShowJump(false);
2295
+ scrollRef.current?.scrollToEnd({ animated: true });
2296
+ }, [followLatestSignal]);
1310
2297
  const groupTops = useRef(/* @__PURE__ */ new Map()).current;
1311
2298
  const landed = useRef(null);
1312
2299
  const [layoutPass, setLayoutPass] = useState(0);
@@ -1356,9 +2343,10 @@ function MessageTimeline(props) {
1356
2343
  renderMessageActions: props.renderMessageActions,
1357
2344
  renderUserAttachments: props.renderUserAttachments,
1358
2345
  onCopy: props.onCopy,
1359
- reportLiveFold
2346
+ reportLiveFold,
2347
+ onRevealAnnotation: revealSource
1360
2348
  };
1361
- return /* @__PURE__ */ jsx(NativeActivityOptionsProvider, { value: activityOptions, children: /* @__PURE__ */ jsx(FoldMemoryProvider, { value: foldMemory, children: /* @__PURE__ */ jsx(StickyWorkHeaderProvider, { value: registerSticky, children: /* @__PURE__ */ jsx(ReleaseFollowProvider, { value: releaseFollow, children: /* @__PURE__ */ jsxs(View, { style: [{ flex: 1, backgroundColor: theme.colors.bg }, props.style], children: [
2349
+ return /* @__PURE__ */ jsx(AnnotationSourcesContext.Provider, { value: annotationSourcesValue, children: /* @__PURE__ */ jsx(NativeActivityOptionsProvider, { value: activityOptions, children: /* @__PURE__ */ jsx(FoldMemoryProvider, { value: foldMemory, children: /* @__PURE__ */ jsx(StickyWorkHeaderProvider, { value: registerSticky, children: /* @__PURE__ */ jsx(ReleaseFollowProvider, { value: releaseFollow, children: /* @__PURE__ */ jsxs(View, { style: [{ flex: 1, backgroundColor: theme.colors.bg }, props.style], children: [
1362
2350
  /* @__PURE__ */ jsxs(
1363
2351
  ScrollView,
1364
2352
  {
@@ -1387,7 +2375,13 @@ function MessageTimeline(props) {
1387
2375
  },
1388
2376
  children: [
1389
2377
  /* @__PURE__ */ jsxs(View, { children: [
1390
- props.hasOlder ? /* @__PURE__ */ jsx(OlderHistoryRow, { loading: props.loadingOlder === true, onPress: requestOlder }) : null,
2378
+ props.hasOlder ? /* @__PURE__ */ jsx(
2379
+ OlderHistoryRow,
2380
+ {
2381
+ loading: props.loadingOlder === true,
2382
+ onPress: requestOlder
2383
+ }
2384
+ ) : null,
1391
2385
  props.header
1392
2386
  ] }),
1393
2387
  groups.length === 0 && props.emptyState ? props.emptyState : null,
@@ -1491,7 +2485,7 @@ function MessageTimeline(props) {
1491
2485
  )
1492
2486
  }
1493
2487
  ) : null
1494
- ] }) }) }) }) });
2488
+ ] }) }) }) }) }) });
1495
2489
  }
1496
2490
  function NavButton(props) {
1497
2491
  const theme = useNativeTimelineTheme();
@@ -1982,9 +2976,11 @@ function UserMessageRow({ item, context }) {
1982
2976
  const theme = useNativeTimelineTheme();
1983
2977
  const m = useNativeTimelineMessages();
1984
2978
  const failed = item.delivery?.state === "failed";
2979
+ const annotations = item.annotations ?? [];
2980
+ const sourceKey = item.annotationSource?.eventId;
1985
2981
  return /* @__PURE__ */ jsx(Animated.View, { entering: FadeIn.duration(180), style: { alignItems: "flex-end" }, children: /* @__PURE__ */ jsxs(View, { style: { maxWidth: "85%", alignItems: "flex-end", gap: 6 }, children: [
1986
2982
  item.resources.some((resource) => resource.kind === "file") ? context.renderUserAttachments?.(item) : null,
1987
- item.text ? /* @__PURE__ */ jsx(
2983
+ item.text || annotations.length > 0 ? /* @__PURE__ */ jsxs(
1988
2984
  View,
1989
2985
  {
1990
2986
  style: {
@@ -1998,19 +2994,35 @@ function UserMessageRow({ item, context }) {
1998
2994
  paddingHorizontal: 16,
1999
2995
  paddingVertical: 10
2000
2996
  },
2001
- children: /* @__PURE__ */ jsx(CollapsibleUserText, { children: context.renderMarkdown ? context.renderMarkdown(item.text, { tone: "body" }) : /* @__PURE__ */ jsx(
2002
- Text,
2003
- {
2004
- selectable: true,
2005
- style: {
2006
- ...fontStyle(theme),
2007
- fontSize: theme.size.md,
2008
- lineHeight: 28,
2009
- color: theme.colors.fg
2010
- },
2011
- children: item.text
2012
- }
2013
- ) })
2997
+ children: [
2998
+ item.text ? /* @__PURE__ */ jsx(CollapsibleUserText, { children: /* @__PURE__ */ jsx(
2999
+ AnnotationSourceView,
3000
+ {
3001
+ sourceKey,
3002
+ style: { marginHorizontal: -6, paddingHorizontal: 6 },
3003
+ children: context.renderMarkdown ? context.renderMarkdown(item.text, { tone: "body" }) : /* @__PURE__ */ jsx(
3004
+ Text,
3005
+ {
3006
+ selectable: true,
3007
+ style: {
3008
+ ...fontStyle(theme),
3009
+ fontSize: theme.size.md,
3010
+ lineHeight: 28,
3011
+ color: theme.colors.fg
3012
+ },
3013
+ children: item.text
3014
+ }
3015
+ )
3016
+ }
3017
+ ) }) : null,
3018
+ annotations.length > 0 ? /* @__PURE__ */ jsx(View, { style: item.text ? { marginTop: 10, marginBottom: 2 } : { paddingVertical: 2 }, children: /* @__PURE__ */ jsx(
3019
+ SentAnnotationCards,
3020
+ {
3021
+ annotations,
3022
+ onReveal: context.onRevealAnnotation
3023
+ }
3024
+ ) }) : null
3025
+ ]
2014
3026
  }
2015
3027
  ) : null,
2016
3028
  /* @__PURE__ */ jsx(
@@ -2060,17 +3072,24 @@ function UserMessageRow({ item, context }) {
2060
3072
  function AgentMessageRow({ item, context }) {
2061
3073
  const theme = useNativeTimelineTheme();
2062
3074
  return /* @__PURE__ */ jsxs(Animated.View, { entering: FadeIn.duration(180), style: { minWidth: 0 }, children: [
2063
- context.renderMarkdown ? context.renderMarkdown(item.text, { tone: "body", streaming: item.streaming }) : /* @__PURE__ */ jsx(
2064
- Text,
3075
+ /* @__PURE__ */ jsx(
3076
+ AnnotationSourceView,
2065
3077
  {
2066
- selectable: true,
2067
- style: {
2068
- ...fontStyle(theme),
2069
- fontSize: theme.size.md,
2070
- lineHeight: 28,
2071
- color: theme.colors.fg
2072
- },
2073
- children: item.text
3078
+ sourceKey: item.annotationSource?.eventId,
3079
+ style: { marginHorizontal: -8, paddingHorizontal: 8 },
3080
+ children: context.renderMarkdown ? context.renderMarkdown(item.text, { tone: "body", streaming: item.streaming }) : /* @__PURE__ */ jsx(
3081
+ Text,
3082
+ {
3083
+ selectable: true,
3084
+ style: {
3085
+ ...fontStyle(theme),
3086
+ fontSize: theme.size.md,
3087
+ lineHeight: 28,
3088
+ color: theme.colors.fg
3089
+ },
3090
+ children: item.text
3091
+ }
3092
+ )
2074
3093
  }
2075
3094
  ),
2076
3095
  item.streaming ? null : /* @__PURE__ */ jsx(
@@ -4302,7 +5321,7 @@ function StatusDot({
4302
5321
  return;
4303
5322
  }
4304
5323
  opacity.value = withRepeat(
4305
- withTiming(0.5, { duration: 1e3, easing: Easing.bezier(0.4, 0, 0.6, 1) }),
5324
+ withTiming(0.5, { duration: 1e3, easing: Easing$1.bezier(0.4, 0, 0.6, 1) }),
4306
5325
  -1,
4307
5326
  true
4308
5327
  );
@@ -4572,145 +5591,6 @@ function SessionHeaderTitle({
4572
5591
  }
4573
5592
  );
4574
5593
  }
4575
- function BottomSheet(props) {
4576
- const t = useOpenGeniText();
4577
- const theme = useNativeTimelineTheme();
4578
- const c = theme.colors;
4579
- const insets = useSafeAreaInsets();
4580
- const { height } = useWindowDimensions();
4581
- const [mounted, setMounted] = useState(props.open);
4582
- const progress = useRef(new Animated$1.Value(0)).current;
4583
- const drag = useRef(new Animated$1.Value(0)).current;
4584
- const onClose = useRef(props.onClose);
4585
- onClose.current = props.onClose;
4586
- useEffect(() => {
4587
- let cancelled = false;
4588
- if (props.open) {
4589
- setMounted(true);
4590
- drag.setValue(0);
4591
- void AccessibilityInfo.isReduceMotionEnabled().then((reduce) => {
4592
- if (cancelled) return;
4593
- Animated$1.timing(progress, {
4594
- toValue: 1,
4595
- duration: reduce ? 0 : 260,
4596
- easing: Easing$1.bezier(0.22, 1, 0.36, 1),
4597
- useNativeDriver: true
4598
- }).start();
4599
- });
4600
- } else if (mounted) {
4601
- Animated$1.timing(progress, {
4602
- toValue: 0,
4603
- duration: 200,
4604
- easing: Easing$1.in(Easing$1.quad),
4605
- useNativeDriver: true
4606
- }).start(({ finished }) => {
4607
- if (finished && !cancelled) setMounted(false);
4608
- });
4609
- }
4610
- return () => {
4611
- cancelled = true;
4612
- };
4613
- }, [drag, mounted, progress, props.open]);
4614
- const pan = useRef(
4615
- PanResponder.create({
4616
- onMoveShouldSetPanResponder: (_event, gesture) => gesture.dy > 6 && Math.abs(gesture.dy) > Math.abs(gesture.dx),
4617
- onPanResponderMove: (_event, gesture) => drag.setValue(Math.max(0, gesture.dy)),
4618
- onPanResponderRelease: (_event, gesture) => {
4619
- if (gesture.dy > 90 || gesture.vy > 0.9) onClose.current();
4620
- else Animated$1.spring(drag, { toValue: 0, useNativeDriver: true, bounciness: 0 }).start();
4621
- },
4622
- onPanResponderTerminate: () => Animated$1.spring(drag, { toValue: 0, useNativeDriver: true, bounciness: 0 }).start()
4623
- })
4624
- ).current;
4625
- if (!mounted) return null;
4626
- const translateY = Animated$1.add(
4627
- progress.interpolate({ inputRange: [0, 1], outputRange: [height, 0] }),
4628
- drag
4629
- );
4630
- return /* @__PURE__ */ jsx(
4631
- Modal,
4632
- {
4633
- visible: true,
4634
- transparent: true,
4635
- animationType: "none",
4636
- statusBarTranslucent: true,
4637
- onRequestClose: props.onClose,
4638
- children: /* @__PURE__ */ jsxs(
4639
- KeyboardAvoidingView,
4640
- {
4641
- behavior: Platform.OS === "ios" ? "padding" : void 0,
4642
- style: { flex: 1, justifyContent: "flex-end" },
4643
- children: [
4644
- /* @__PURE__ */ jsx(
4645
- Animated$1.View,
4646
- {
4647
- style: {
4648
- position: "absolute",
4649
- top: 0,
4650
- right: 0,
4651
- bottom: 0,
4652
- left: 0,
4653
- backgroundColor: "rgba(0, 0, 0, 0.4)",
4654
- opacity: progress
4655
- },
4656
- children: /* @__PURE__ */ jsx(
4657
- Pressable,
4658
- {
4659
- style: { flex: 1 },
4660
- onPress: props.onClose,
4661
- accessibilityRole: "button",
4662
- accessibilityLabel: t("native.timeline.sheet.close")
4663
- }
4664
- )
4665
- }
4666
- ),
4667
- /* @__PURE__ */ jsxs(
4668
- Animated$1.View,
4669
- {
4670
- accessibilityViewIsModal: true,
4671
- accessibilityLabel: props.accessibilityLabel,
4672
- style: {
4673
- maxHeight: height * (props.maxHeightRatio ?? 0.85),
4674
- backgroundColor: c.bg,
4675
- borderTopLeftRadius: 16,
4676
- borderTopRightRadius: 16,
4677
- paddingBottom: Math.max(insets.bottom, 12),
4678
- transform: [{ translateY }],
4679
- shadowColor: "#000",
4680
- shadowOpacity: 0.12,
4681
- shadowRadius: 16,
4682
- shadowOffset: { width: 0, height: -2 },
4683
- elevation: 16
4684
- },
4685
- children: [
4686
- /* @__PURE__ */ jsx(
4687
- View,
4688
- {
4689
- ...pan.panHandlers,
4690
- style: { alignItems: "center", paddingTop: 8, paddingBottom: 4 },
4691
- children: /* @__PURE__ */ jsx(
4692
- View,
4693
- {
4694
- style: {
4695
- width: 36,
4696
- height: 5,
4697
- borderRadius: 3,
4698
- backgroundColor: c["border-strong"] ?? c.border
4699
- }
4700
- }
4701
- )
4702
- }
4703
- ),
4704
- props.children
4705
- ]
4706
- }
4707
- )
4708
- ]
4709
- }
4710
- )
4711
- }
4712
- );
4713
- }
4714
5594
  var CompactSignalsContext = createContext(false);
4715
5595
  function SessionSignals(props) {
4716
5596
  const m = useNativeTimelineMessages();
@@ -4973,6 +5853,7 @@ function QueueDock({
4973
5853
  queue,
4974
5854
  composer,
4975
5855
  onComposerFocus,
5856
+ onReviewAnnotations,
4976
5857
  leading,
4977
5858
  singleSend = false
4978
5859
  }) {
@@ -5132,6 +6013,42 @@ function QueueDock({
5132
6013
  children: label
5133
6014
  }
5134
6015
  ),
6016
+ annotationCount > 0 && onReviewAnnotations ? /* @__PURE__ */ jsxs(
6017
+ Pressable,
6018
+ {
6019
+ accessibilityRole: "button",
6020
+ accessibilityLabel: t("timelineAnnotations.reviewCountlabel", {
6021
+ countLabel: t("counts.annotations", { count: annotationCount })
6022
+ }),
6023
+ hitSlop: 8,
6024
+ onPress: () => onReviewAnnotations(turn.annotations ?? []),
6025
+ style: ({ pressed }) => ({
6026
+ flexDirection: "row",
6027
+ alignItems: "center",
6028
+ gap: 3,
6029
+ height: 22,
6030
+ paddingHorizontal: 7,
6031
+ borderRadius: 11,
6032
+ backgroundColor: c["accent-soft"],
6033
+ opacity: pressed ? 0.6 : 1
6034
+ }),
6035
+ children: [
6036
+ /* @__PURE__ */ jsx(Icon, { name: "quote", size: 10, color: c["fg-muted"] }),
6037
+ /* @__PURE__ */ jsx(
6038
+ Text,
6039
+ {
6040
+ style: {
6041
+ ...fontStyle(theme, 500),
6042
+ fontSize: 11,
6043
+ color: c["fg-muted"],
6044
+ fontVariant: ["tabular-nums"]
6045
+ },
6046
+ children: annotationCount
6047
+ }
6048
+ )
6049
+ ]
6050
+ }
6051
+ ) : null,
5135
6052
  turns.length > 1 ? /* @__PURE__ */ jsxs(Fragment, { children: [
5136
6053
  /* @__PURE__ */ jsx(
5137
6054
  RowAction,
@@ -5333,9 +6250,27 @@ function SessionScreenBody({
5333
6250
  ...timelineProps
5334
6251
  }) {
5335
6252
  const theme = useNativeTimelineTheme();
6253
+ const t = useOpenGeniText();
5336
6254
  const m = useNativeTimelineMessages();
5337
6255
  const { composer, queue, humanInput, approvals, control, attachments } = controller;
5338
6256
  const composerInput = useRef(null);
6257
+ const annotationReveal = useRef(null);
6258
+ const draftAnnotations = composer.annotations ?? [];
6259
+ const annotationsIncomplete = draftAnnotations.some(
6260
+ (annotation) => annotation.note.trim().length === 0
6261
+ );
6262
+ const reviewTargetId = composer.annotationReviewTargetId ?? null;
6263
+ const [reviewOpen, setReviewOpen] = useState(false);
6264
+ useEffect(() => {
6265
+ if (reviewTargetId) setReviewOpen(true);
6266
+ }, [reviewTargetId]);
6267
+ const closeReview = useCallback(() => {
6268
+ setReviewOpen(false);
6269
+ composer.clearAnnotationReviewTarget?.();
6270
+ }, [composer]);
6271
+ const [queuedReview, setQueuedReview] = useState(null);
6272
+ const [followLatestSignal, setFollowLatestSignal] = useState(0);
6273
+ const followLatest = useCallback(() => setFollowLatestSignal((value) => value + 1), []);
5339
6274
  const [composerHeight, setComposerHeight] = useState(120);
5340
6275
  const items = useMemo(
5341
6276
  () => conversationTimeline(controller.timeline, queue, composer),
@@ -5378,6 +6313,11 @@ function SessionScreenBody({
5378
6313
  /* @__PURE__ */ jsx(
5379
6314
  MessageTimeline,
5380
6315
  {
6316
+ onAnnotate: composer.addAnnotation,
6317
+ draftAnnotations: composer.annotations,
6318
+ onDraftAnnotationSelect: composer.requestAnnotationReview,
6319
+ annotationRevealRef: annotationReveal,
6320
+ followLatestSignal,
5381
6321
  ...timelineProps,
5382
6322
  hasOlder: controller.hasOlder,
5383
6323
  loadingOlder: controller.loadingOlder,
@@ -5436,12 +6376,29 @@ function SessionScreenBody({
5436
6376
  onActionFeedback: composerSlots?.onActionFeedback,
5437
6377
  value: composer.value,
5438
6378
  onChangeText: composer.setValue,
5439
- onSend: () => void composer.send(),
6379
+ // A quote without its note cannot be sent: Send opens that note instead
6380
+ // of staying disabled with no reason given.
6381
+ onSend: () => {
6382
+ if (annotationsIncomplete) composer.requestAnnotationReview?.();
6383
+ else {
6384
+ followLatest();
6385
+ void composer.send();
6386
+ }
6387
+ },
5440
6388
  // The other choice beside Send while the agent works: Queue when
5441
6389
  // Send steers, Steer when Send queues.
5442
6390
  alternateSend: composer.alternateSend ?? null,
5443
- ...composer.sendAlternate ? { onAlternateSend: () => void composer.sendAlternate?.() } : {},
5444
- canSend: composer.canSend && !busy,
6391
+ ...composer.sendAlternate ? {
6392
+ onAlternateSend: () => {
6393
+ if (annotationsIncomplete) {
6394
+ composer.requestAnnotationReview?.();
6395
+ return;
6396
+ }
6397
+ followLatest();
6398
+ void composer.sendAlternate?.();
6399
+ }
6400
+ } : {},
6401
+ canSend: (composer.canSend || annotationsIncomplete && !composer.sending) && !busy,
5445
6402
  sending: composer.sending,
5446
6403
  running: controller.runActive,
5447
6404
  paused,
@@ -5468,7 +6425,41 @@ function SessionScreenBody({
5468
6425
  ) : null,
5469
6426
  header: /* @__PURE__ */ jsxs(Fragment, { children: [
5470
6427
  composerSlots?.header,
5471
- /* @__PURE__ */ jsx(AttachmentChips, { attachments })
6428
+ /* @__PURE__ */ jsx(AttachmentChips, { attachments }),
6429
+ draftAnnotations.length > 0 && composer.removeAnnotation ? /* @__PURE__ */ jsx(View, { style: { paddingHorizontal: 12, paddingTop: 10 }, children: /* @__PURE__ */ jsx(
6430
+ AnnotationsChip,
6431
+ {
6432
+ annotations: draftAnnotations,
6433
+ onPress: () => composer.requestAnnotationReview?.(),
6434
+ onClear: () => {
6435
+ const removeAll = () => {
6436
+ for (const annotation of draftAnnotations) {
6437
+ composer.removeAnnotation?.(annotation.id);
6438
+ }
6439
+ };
6440
+ if (!draftAnnotations.some((item) => annotationHasNote(item.note))) {
6441
+ removeAll();
6442
+ return;
6443
+ }
6444
+ Alert.alert(
6445
+ t("native.timeline.annotations.removeAllTitle", {
6446
+ count: draftAnnotations.length
6447
+ }),
6448
+ t("native.timeline.annotations.removeAllMessage", {
6449
+ count: draftAnnotations.filter((item) => annotationHasNote(item.note)).length
6450
+ }),
6451
+ [
6452
+ { text: t("native.timeline.annotations.cancel"), style: "cancel" },
6453
+ {
6454
+ text: t("native.timeline.annotations.removeAllConfirm"),
6455
+ style: "destructive",
6456
+ onPress: removeAll
6457
+ }
6458
+ ]
6459
+ );
6460
+ }
6461
+ }
6462
+ ) }) : null
5472
6463
  ] }),
5473
6464
  above: /* @__PURE__ */ jsxs(Fragment, { children: [
5474
6465
  /* @__PURE__ */ jsx(
@@ -5478,6 +6469,7 @@ function SessionScreenBody({
5478
6469
  composer,
5479
6470
  singleSend: composer.singleSend === true,
5480
6471
  onComposerFocus: () => composerInput.current?.focus(),
6472
+ onReviewAnnotations: setQueuedReview,
5481
6473
  leading: /* @__PURE__ */ jsx(
5482
6474
  SessionSignals,
5483
6475
  {
@@ -5510,7 +6502,35 @@ function SessionScreenBody({
5510
6502
  }
5511
6503
  ) : null
5512
6504
  ] })
5513
- })
6505
+ }),
6506
+ /* @__PURE__ */ jsx(
6507
+ AnnotationsSheet,
6508
+ {
6509
+ open: reviewOpen,
6510
+ onClose: closeReview,
6511
+ annotations: draftAnnotations,
6512
+ editable: Boolean(composer.updateAnnotation),
6513
+ focusId: reviewTargetId,
6514
+ onFocusConsumed: composer.clearAnnotationReviewTarget,
6515
+ onUpdate: composer.updateAnnotation,
6516
+ onRemove: composer.removeAnnotation,
6517
+ onReveal: (source, quote) => annotationReveal.current?.(source, quote) ?? false,
6518
+ onCommit: () => {
6519
+ closeReview();
6520
+ setTimeout(() => composerInput.current?.focus(), 250);
6521
+ }
6522
+ }
6523
+ ),
6524
+ /* @__PURE__ */ jsx(
6525
+ AnnotationsSheet,
6526
+ {
6527
+ open: queuedReview !== null,
6528
+ onClose: () => setQueuedReview(null),
6529
+ annotations: queuedReview ?? [],
6530
+ editable: false,
6531
+ onReveal: (source, quote) => annotationReveal.current?.(source, quote) ?? false
6532
+ }
6533
+ )
5514
6534
  ] });
5515
6535
  }
5516
6536
  function useAutoRecover(failed, live, refresh) {
@@ -7496,6 +8516,6 @@ function NativeInboxList({
7496
8516
  ] });
7497
8517
  }
7498
8518
 
7499
- export { ActivityRail, ActivityRow, ApprovalStrip, AttachmentChips, BottomSheet, Button, ComposerPill, FeedbackSheet, HumanInputCard, IconButton, MessageTimeline, ModelMark, ModelPickerSheet, NativeInboxList, NativeMessageAttachments, NativeSessionScreen, PreparingState, PresentedToolRow, QueueDock, RollingActivity, SectionLabel, SessionActionsButton, SessionCommandsList, SessionComposer, SessionHeaderTitle, SessionRow, SessionRowList, SessionSignals, SessionStatusBadge, StatusDot, TurnFeedbackButtons, TurnRailFrame, TurnSummary, defaultApprovalStripMessages, isNeedsYouKind, nativeInboxAttentionCount, useNativeInbox, useTurnRatings };
8519
+ export { ActivityRail, ActivityRow, AnnotationsChip, AnnotationsSheet, ApprovalStrip, AttachmentChips, BottomSheet, Button, ComposerPill, FeedbackSheet, HumanInputCard, IconButton, MessageTimeline, ModelMark, ModelPickerSheet, NativeInboxList, NativeMessageAttachments, NativeSessionScreen, PreparingState, PresentedToolRow, QueueDock, RollingActivity, SectionLabel, SessionActionsButton, SessionCommandsList, SessionComposer, SessionHeaderTitle, SessionRow, SessionRowList, SessionSignals, SessionStatusBadge, StatusDot, TurnFeedbackButtons, TurnRailFrame, TurnSummary, defaultApprovalStripMessages, isNeedsYouKind, nativeInboxAttentionCount, useNativeInbox, useTurnRatings };
7500
8520
  //# sourceMappingURL=index.js.map
7501
8521
  //# sourceMappingURL=index.js.map