@opengeni/react 0.45.0 → 0.48.0

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 (141) hide show
  1. package/README.md +68 -12
  2. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  3. package/dist/chunk-FK6VG4LL.js.map +1 -0
  4. package/dist/{chunk-HFO4ERGQ.js → chunk-FWXGSNXT.js} +2 -2
  5. package/dist/{chunk-SRFUT2ZU.js → chunk-I4MPFDRP.js} +119 -63
  6. package/dist/chunk-I4MPFDRP.js.map +1 -0
  7. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  8. package/dist/chunk-KZ73RL76.js.map +1 -0
  9. package/dist/{chunk-LWR4MXSS.js → chunk-LHLYWG5R.js} +10 -1
  10. package/dist/chunk-LHLYWG5R.js.map +1 -0
  11. package/dist/{chunk-L3EWO3UK.js → chunk-NJ7M2B6Z.js} +445 -84
  12. package/dist/chunk-NJ7M2B6Z.js.map +1 -0
  13. package/dist/{chunk-VCBFUSUZ.js → chunk-O4VXOBGF.js} +3 -3
  14. package/dist/{chunk-VCBFUSUZ.js.map → chunk-O4VXOBGF.js.map} +1 -1
  15. package/dist/{chunk-6EDV6YFE.js → chunk-OL6QVXGF.js} +121 -29
  16. package/dist/chunk-OL6QVXGF.js.map +1 -0
  17. package/dist/chunk-VZTQ73XT.js +86 -0
  18. package/dist/chunk-VZTQ73XT.js.map +1 -0
  19. package/dist/{chunk-EHSYZ4KP.js → chunk-WYA65Y3F.js} +16 -16
  20. package/dist/chunk-WYA65Y3F.js.map +1 -0
  21. package/dist/{chunk-FD72GJMK.js → chunk-XCNTXWBB.js} +274 -125
  22. package/dist/chunk-XCNTXWBB.js.map +1 -0
  23. package/dist/client.d.ts +1 -1
  24. package/dist/components/chat-composer.d.ts +7 -2
  25. package/dist/components/composer-transcription-control.d.ts +2 -0
  26. package/dist/components/composer.d.ts +14 -0
  27. package/dist/components/machines/machine-detail.d.ts +3 -1
  28. package/dist/components/machines-dashboard.d.ts +3 -3
  29. package/dist/components/message-timeline.d.ts +12 -2
  30. package/dist/components/sandbox-files.d.ts +2 -2
  31. package/dist/components/sandbox-terminal.d.ts +29 -0
  32. package/dist/components/sandbox-workspace.d.ts +5 -1
  33. package/dist/components/workspace-dock.d.ts +3 -1
  34. package/dist/composer.d.ts +2 -2
  35. package/dist/composer.js +6 -6
  36. package/dist/hooks/internal.d.ts +6 -0
  37. package/dist/hooks/use-codex-accounts.d.ts +1 -1
  38. package/dist/hooks/use-environments.d.ts +10 -4
  39. package/dist/hooks/use-machines.d.ts +7 -0
  40. package/dist/hooks/use-sandbox-files.d.ts +19 -0
  41. package/dist/hooks/use-sandbox-git.d.ts +7 -0
  42. package/dist/hooks/use-transcription.d.ts +2 -2
  43. package/dist/hooks/use-voice-input.d.ts +13 -4
  44. package/dist/index.d.ts +6 -5
  45. package/dist/index.js +904 -538
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/composer-responsive-context.d.ts +9 -0
  48. package/dist/lib/use-portal-token-style.d.ts +14 -3
  49. package/dist/machines.js +2 -2
  50. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-U5JFG7HT.js} +19 -18
  51. package/dist/queue-surface-implementation-U5JFG7HT.js.map +1 -0
  52. package/dist/realtime/realtime-control.d.ts +9 -0
  53. package/dist/realtime.js +29 -32
  54. package/dist/realtime.js.map +1 -1
  55. package/dist/session-ui.js +5 -5
  56. package/dist/session.js +4 -4
  57. package/dist/timeline/activity-rail.d.ts +3 -2
  58. package/dist/timeline/index.d.ts +2 -2
  59. package/dist/timeline/parsers.d.ts +3 -3
  60. package/dist/timeline/registry.d.ts +3 -0
  61. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  62. package/dist/timeline/types.d.ts +4 -0
  63. package/dist/voice-recording-store.d.ts +8 -2
  64. package/package.json +20 -4
  65. package/src/client.ts +3 -0
  66. package/src/components/chat-composer.tsx +12 -2
  67. package/src/components/code-editor.tsx +1 -1
  68. package/src/components/command-palette.tsx +1 -1
  69. package/src/components/composer-transcription-control.tsx +41 -16
  70. package/src/components/composer.tsx +52 -26
  71. package/src/components/copy-button.tsx +2 -2
  72. package/src/components/desktop-viewer.tsx +2 -2
  73. package/src/components/file-browser.tsx +39 -17
  74. package/src/components/human-input-form.tsx +2 -2
  75. package/src/components/machine-card.tsx +1 -1
  76. package/src/components/machines/machine-detail.tsx +39 -15
  77. package/src/components/machines-dashboard.tsx +7 -6
  78. package/src/components/message-timeline.tsx +44 -11
  79. package/src/components/model-picker.tsx +2 -2
  80. package/src/components/model-policy-picker.tsx +16 -15
  81. package/src/components/queue-surface-implementation.tsx +20 -16
  82. package/src/components/queue-surface-state.tsx +2 -2
  83. package/src/components/sandbox-files.tsx +140 -60
  84. package/src/components/sandbox-terminal.tsx +24 -5
  85. package/src/components/sandbox-workspace.tsx +180 -142
  86. package/src/components/session-chrome.tsx +14 -14
  87. package/src/components/tooltip.tsx +13 -16
  88. package/src/components/user-message-body.tsx +1 -1
  89. package/src/components/workbench-changes.tsx +4 -4
  90. package/src/components/workspace-dock.tsx +13 -5
  91. package/src/composer.ts +3 -0
  92. package/src/hooks/internal.ts +132 -38
  93. package/src/hooks/use-codex-accounts.ts +4 -4
  94. package/src/hooks/use-environments.ts +24 -5
  95. package/src/hooks/use-file-attachments.ts +70 -32
  96. package/src/hooks/use-goal.ts +17 -5
  97. package/src/hooks/use-machines.ts +47 -1
  98. package/src/hooks/use-sandbox-files.ts +310 -59
  99. package/src/hooks/use-sandbox-git.ts +148 -41
  100. package/src/hooks/use-session-capabilities.ts +35 -24
  101. package/src/hooks/use-session-events.ts +18 -4
  102. package/src/hooks/use-transcription.ts +4 -14
  103. package/src/hooks/use-turn-queue.ts +17 -5
  104. package/src/hooks/use-voice-input.ts +481 -36
  105. package/src/hooks/use-windowed-sections.ts +9 -7
  106. package/src/index.ts +11 -6
  107. package/src/lib/composer-responsive-context.ts +15 -0
  108. package/src/lib/use-portal-token-style.ts +60 -15
  109. package/src/provider.tsx +52 -7
  110. package/src/realtime/dropdown-menu.tsx +15 -19
  111. package/src/realtime/realtime-control.tsx +18 -7
  112. package/src/timeline/activity-rail.tsx +15 -6
  113. package/src/timeline/index.ts +1 -1
  114. package/src/timeline/parsers.ts +33 -20
  115. package/src/timeline/projection.ts +23 -0
  116. package/src/timeline/registry.ts +7 -0
  117. package/src/timeline/screenshot-lightbox.tsx +15 -4
  118. package/src/timeline/shared.tsx +8 -8
  119. package/src/timeline/tool-renderers.tsx +166 -22
  120. package/src/timeline/turn-summary.tsx +6 -6
  121. package/src/timeline/types.ts +4 -0
  122. package/src/voice-recording-store.ts +15 -0
  123. package/styles/compiled.css +5065 -0
  124. package/styles/compiled.d.ts +2 -0
  125. package/styles/effective-tokens.css +27 -0
  126. package/styles/index.css +3 -0
  127. package/styles/responsive.css +196 -0
  128. package/styles/responsive.d.ts +2 -0
  129. package/styles/tokens.css +50 -17
  130. package/dist/chunk-6EDV6YFE.js.map +0 -1
  131. package/dist/chunk-EB67J347.js.map +0 -1
  132. package/dist/chunk-EHSYZ4KP.js.map +0 -1
  133. package/dist/chunk-FD72GJMK.js.map +0 -1
  134. package/dist/chunk-JB5725BI.js +0 -48
  135. package/dist/chunk-JB5725BI.js.map +0 -1
  136. package/dist/chunk-L3EWO3UK.js.map +0 -1
  137. package/dist/chunk-LWR4MXSS.js.map +0 -1
  138. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  139. package/dist/chunk-U3CSCMWA.js.map +0 -1
  140. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
  141. /package/dist/{chunk-HFO4ERGQ.js.map → chunk-FWXGSNXT.js.map} +0 -0
@@ -540,6 +540,18 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
540
540
 
541
541
  case "tool.auth_needed":
542
542
  case "credential.auth_needed": {
543
+ if (
544
+ event.type === "tool.auth_needed" &&
545
+ !stringValue(payload.toolName) &&
546
+ stringValue(payload.serverId) !== "codex_apps"
547
+ ) {
548
+ // Historical optional-MCP initialize/tools-list credential misses are
549
+ // setup availability, not evidence of a concrete conversational tool
550
+ // call. Keep the raw event in Debug/audit but do not manufacture an
551
+ // actionable reconnect card in the transcript. Actual tools/call auth
552
+ // failures carry a concrete toolName and remain visible.
553
+ break;
554
+ }
543
555
  // Keep the whole structured payload — the renderer turns it into a clean
544
556
  // inline reconnect card, and the app starts the recovery flow off the
545
557
  // connectionId/resource. Losing it to a plain-text notice was the ugly
@@ -549,6 +561,8 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
549
561
  kind: "auth-needed",
550
562
  id: event.id,
551
563
  turnId,
564
+ serverId: typeof payload.serverId === "string" ? payload.serverId : null,
565
+ source: event.type === "tool.auth_needed" ? "tool" : "credential",
552
566
  providerDomain: stringValue(payload.providerDomain),
553
567
  connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null,
554
568
  reason: authNeededReason(payload.reason),
@@ -1609,6 +1623,15 @@ export function extractSessionRef(value: unknown, depth = 0): string | null {
1609
1623
  return null;
1610
1624
  }
1611
1625
  const record = value as Record<string, unknown>;
1626
+ const receiptResource = asRecord(record.resource);
1627
+ if (
1628
+ record.receiptVersion === "mcp-mutation-receipt.v1" &&
1629
+ receiptResource.type === "session" &&
1630
+ typeof receiptResource.id === "string" &&
1631
+ looksLikeId(receiptResource.id)
1632
+ ) {
1633
+ return receiptResource.id;
1634
+ }
1612
1635
  if (typeof record.sessionId === "string" && looksLikeId(record.sessionId)) {
1613
1636
  return record.sessionId;
1614
1637
  }
@@ -1,3 +1,4 @@
1
+ import type { RetainedArtifactReference } from "@opengeni/sdk";
1
2
  import type { ComponentType } from "react";
2
3
  import { mcpToolLeaf } from "./tool-display-name";
3
4
  import type { ToolCallItem } from "./types";
@@ -22,8 +23,14 @@ import type { ToolCallItem } from "./types";
22
23
 
23
24
  export type ToolRendererProps = {
24
25
  item: ToolCallItem;
26
+ loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
25
27
  };
26
28
 
29
+ export type RetainedScreenshotLoader = (
30
+ artifact: RetainedArtifactReference,
31
+ signal: AbortSignal,
32
+ ) => Promise<Uint8Array | null>;
33
+
27
34
  export type ToolRenderer = ComponentType<ToolRendererProps>;
28
35
 
29
36
  /** A registry entry: which key it matches and the component that renders it. */
@@ -3,6 +3,7 @@ import { AnimatePresence, motion } from "motion/react";
3
3
  import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
4
4
  import { Dialog } from "radix-ui";
5
5
  import { cn } from "../lib/cn";
6
+ import { usePortalTokenStyle } from "../lib/use-portal-token-style";
6
7
 
7
8
  /* ----------------------------------------------------------------------------
8
9
  Screenshot lightbox
@@ -18,7 +19,7 @@ import { cn } from "../lib/cn";
18
19
  -------------------------------------------------------------------------- */
19
20
 
20
21
  type LightboxController = {
21
- open: (src: string, caption?: string) => void;
22
+ open: (src: string, caption?: string, source?: HTMLElement | null) => void;
22
23
  };
23
24
 
24
25
  const LightboxContext = createContext<LightboxController | null>(null);
@@ -59,17 +60,25 @@ export function LightboxProvider({ children }: { children: ReactNode }) {
59
60
 
60
61
  function LightboxRoot({ children }: { children: ReactNode }) {
61
62
  const [state, setState] = useState<{ src: string; caption?: string } | null>(null);
63
+ const [tokenSource, setTokenSource] = useState<HTMLElement | null>(null);
64
+ const portalStyle = usePortalTokenStyle(tokenSource);
62
65
 
63
- const open = useCallback((src: string, caption?: string) => {
66
+ const open = useCallback((src: string, caption?: string, source?: HTMLElement | null) => {
67
+ setTokenSource(source ?? null);
64
68
  setState(caption ? { src, caption } : { src });
65
69
  }, []);
66
70
 
71
+ const close = useCallback(() => {
72
+ setState(null);
73
+ setTokenSource(null);
74
+ }, []);
75
+
67
76
  const controller = useMemo<LightboxController>(() => ({ open }), [open]);
68
77
 
69
78
  return (
70
79
  <LightboxContext.Provider value={controller}>
71
80
  {children}
72
- <Dialog.Root open={state !== null} onOpenChange={(next) => !next && setState(null)}>
81
+ <Dialog.Root open={state !== null} onOpenChange={(next) => !next && close()}>
73
82
  <AnimatePresence>
74
83
  {state !== null ? (
75
84
  <Dialog.Portal forceMount>
@@ -80,6 +89,7 @@ function LightboxRoot({ children }: { children: ReactNode }) {
80
89
  exit={{ opacity: 0 }}
81
90
  transition={{ duration: 0.15 }}
82
91
  className="og-root fixed inset-0 z-50 bg-black/90 backdrop-blur-md"
92
+ style={portalStyle}
83
93
  />
84
94
  </Dialog.Overlay>
85
95
  <Dialog.Content
@@ -101,10 +111,11 @@ function LightboxRoot({ children }: { children: ReactNode }) {
101
111
  // figcaption's "click outside to close" affordance is real.
102
112
  onClick={(event) => {
103
113
  if (event.target === event.currentTarget) {
104
- setState(null);
114
+ close();
105
115
  }
106
116
  }}
107
117
  className="og-root fixed inset-0 z-50 flex items-center justify-center p-6 sm:p-12"
118
+ style={portalStyle}
108
119
  >
109
120
  <Dialog.Title className="sr-only">Screenshot</Dialog.Title>
110
121
  {/* The figure is one self-contained object: image, caption, and
@@ -154,16 +154,16 @@ export function ActivityDisclosure({
154
154
  const rowClass = cn(
155
155
  "group/disclosure flex w-full min-w-0 items-center gap-2 rounded-og-sm px-1.5 py-1.5 text-left text-og-base",
156
156
  "text-og-fg-muted transition-colors duration-150",
157
- // A tool row is a touch target on coarse pointers: grow its padding so the
158
- // hit area clears the 40px minimum without loosening the dense desktop rail.
159
- "pointer-coarse:py-2.5",
157
+ // A tool row is a touch target on coarse pointers: grow its hit area to the
158
+ // 44px mobile minimum without loosening the dense desktop rail.
159
+ "pointer-coarse:min-h-11 pointer-coarse:py-2.5",
160
160
  );
161
161
  // The chevron rotates to point down when open; it tracks `data-state` on this
162
162
  // same row (the Trigger), so the affordance never freezes.
163
163
  const inner = (
164
164
  <>
165
165
  {hasBody ? (
166
- <ChevronRightIcon className="size-3.5 shrink-0 text-og-fg-subtle transition-transform duration-[var(--og-duration-disclose)] ease-og-in-out group-data-[state=open]/disclosure:rotate-90" />
166
+ <ChevronRightIcon className="size-3.5 shrink-0 text-og-fg-subtle transition-transform duration-[var(--_og-duration-disclose)] ease-og-in-out group-data-[state=open]/disclosure:rotate-90" />
167
167
  ) : (
168
168
  <span className="size-3.5 shrink-0" />
169
169
  )}
@@ -226,7 +226,7 @@ export function ActivityDisclosure({
226
226
  data-status={dataStatus}
227
227
  className={cn(
228
228
  rowClass,
229
- "cursor-pointer outline-none hover:bg-og-surface-1 hover:text-og-fg",
229
+ "cursor-pointer outline-hidden hover:bg-og-surface-1 hover:text-og-fg",
230
230
  "focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-0",
231
231
  )}
232
232
  >
@@ -498,7 +498,7 @@ export function Thumbnail({
498
498
  type="button"
499
499
  onClick={(event) => {
500
500
  event.stopPropagation();
501
- lightbox.open(src, caption);
501
+ lightbox.open(src, caption, event.currentTarget);
502
502
  }}
503
503
  onKeyDown={(event) => {
504
504
  if (event.key === "Enter" || event.key === " ") {
@@ -507,7 +507,7 @@ export function Thumbnail({
507
507
  }}
508
508
  className={cn(
509
509
  MEDIA_BOX,
510
- "group/thumb relative inline-flex overflow-hidden bg-og-bg outline-none",
510
+ "group/thumb relative inline-flex overflow-hidden bg-og-bg outline-hidden",
511
511
  "focus-visible:ring-2 focus-visible:ring-og-accent",
512
512
  )}
513
513
  aria-label="Expand screenshot"
@@ -554,7 +554,7 @@ export function ScreenshotFigure({
554
554
  ) : lightbox ? (
555
555
  <button
556
556
  type="button"
557
- onClick={() => lightbox.open(src, caption)}
557
+ onClick={(event) => lightbox.open(src, caption, event.currentTarget)}
558
558
  className={surface}
559
559
  aria-label="Expand screenshot"
560
560
  >
@@ -1,4 +1,4 @@
1
- import type { GitFileDiff } from "@opengeni/sdk";
1
+ import type { GitFileDiff, RetainedArtifactReference } from "@opengeni/sdk";
2
2
  import {
3
3
  BoxIcon,
4
4
  BrainCircuitIcon,
@@ -28,7 +28,7 @@ import {
28
28
  TerminalIcon,
29
29
  WrenchIcon,
30
30
  } from "lucide-react";
31
- import type { ReactNode } from "react";
31
+ import { useEffect, useState, type ReactNode } from "react";
32
32
  import { stringifyPayload, tryParseJson } from "../lib/format";
33
33
  import { useTimelineComputeLabel } from "./compute-label";
34
34
  import {
@@ -40,7 +40,7 @@ import {
40
40
  mediaPreviewFact,
41
41
  parseExecBannerSessionId,
42
42
  parseToolArgs,
43
- redactSecrets,
43
+ retainedScreenshotMetadata,
44
44
  sandboxCommandExitCode,
45
45
  stripExecBanner,
46
46
  tailPeek,
@@ -559,7 +559,7 @@ function asComputerArgs(args: unknown): Partial<ComputerAction> {
559
559
  };
560
560
  }
561
561
 
562
- function ComputerCallRenderer({ item }: ToolRendererProps) {
562
+ function ComputerCallRenderer({ item, loadRetainedScreenshot }: ToolRendererProps) {
563
563
  const raw = (item.raw ?? {}) as {
564
564
  action?: ComputerAction;
565
565
  actions?: ComputerAction[];
@@ -582,6 +582,7 @@ function ComputerCallRenderer({ item }: ToolRendererProps) {
582
582
  const rejected = raw.providerData?.approvalStatus === "rejected";
583
583
  const readOnly = typeof out === "string" && out.includes("read-only");
584
584
  const shotUrl = screenshotDataUrl(out);
585
+ const retained = retainedScreenshotMetadata(out);
585
586
  const omittedMedia = mediaPreviewFact(out);
586
587
  const empty = out === "" || out == null;
587
588
  const batched = actions.length > 1 ? actions.map((a) => computerVerb(a)).join(" · ") : null;
@@ -640,6 +641,41 @@ function ComputerCallRenderer({ item }: ToolRendererProps) {
640
641
  const isFailed = item.status === "failed";
641
642
  const isCancelled = item.status === "cancelled";
642
643
 
644
+ if (retained) {
645
+ if (!retained.available) {
646
+ const state =
647
+ retained.reason === "expired" || retained.reason === "deleted"
648
+ ? retained.reason
649
+ : "unavailable";
650
+ return (
651
+ <ActivityDisclosure
652
+ icon={<CameraOffIcon className={ICON_SIZE} />}
653
+ iconTone={isFailed ? "failed" : "muted"}
654
+ title={`${verb}${countSuffix} · ${state}`}
655
+ failed={isFailed}
656
+ cancelled={isCancelled}
657
+ preview={`screenshot ${state}`}
658
+ media={<MediaEmpty />}
659
+ >
660
+ <BodyNote tone={isFailed ? "error" : undefined}>
661
+ Screenshot {state}: {retained.reason.replaceAll("_", " ")}.
662
+ </BodyNote>
663
+ </ActivityDisclosure>
664
+ );
665
+ }
666
+ return (
667
+ <RetainedScreenshotDisclosure
668
+ artifact={retained}
669
+ load={loadRetainedScreenshot}
670
+ verb={verb}
671
+ countSuffix={countSuffix}
672
+ batched={batched}
673
+ failed={isFailed}
674
+ cancelled={isCancelled}
675
+ />
676
+ );
677
+ }
678
+
643
679
  if (shotUrl) {
644
680
  const caption = `${verb}${actions.length > 1 ? ` (+${actions.length - 1} more)` : ""}`;
645
681
  return (
@@ -720,6 +756,112 @@ function ComputerCallRenderer({ item }: ToolRendererProps) {
720
756
  );
721
757
  }
722
758
 
759
+ function RetainedScreenshotDisclosure({
760
+ artifact,
761
+ load,
762
+ verb,
763
+ countSuffix,
764
+ batched,
765
+ failed,
766
+ cancelled,
767
+ }: {
768
+ artifact: RetainedArtifactReference;
769
+ load: ToolRendererProps["loadRetainedScreenshot"];
770
+ verb: string;
771
+ countSuffix: string;
772
+ batched: string | null;
773
+ failed: boolean;
774
+ cancelled: boolean;
775
+ }) {
776
+ const [state, setState] = useState<
777
+ | { kind: "loading" }
778
+ | { kind: "ready"; url: string }
779
+ | { kind: "unavailable"; label: string }
780
+ | { kind: "error"; message: string }
781
+ >({ kind: "loading" });
782
+
783
+ useEffect(() => {
784
+ if (!load) {
785
+ setState({ kind: "unavailable", label: "retrieval is not configured" });
786
+ return;
787
+ }
788
+ const controller = new AbortController();
789
+ let objectUrl: string | null = null;
790
+ setState({ kind: "loading" });
791
+ void load(artifact, controller.signal)
792
+ .then((bytes) => {
793
+ if (controller.signal.aborted) return;
794
+ if (!bytes) {
795
+ setState({ kind: "unavailable", label: "bytes are unavailable" });
796
+ return;
797
+ }
798
+ objectUrl = URL.createObjectURL(new Blob([Uint8Array.from(bytes)], { type: "image/png" }));
799
+ setState({ kind: "ready", url: objectUrl });
800
+ })
801
+ .catch((error: unknown) => {
802
+ if (controller.signal.aborted) return;
803
+ const status =
804
+ error && typeof error === "object" && "status" in error
805
+ ? Number((error as { status?: unknown }).status)
806
+ : null;
807
+ setState(
808
+ status === 404
809
+ ? { kind: "unavailable", label: "deleted" }
810
+ : status === 410
811
+ ? { kind: "unavailable", label: "expired or unavailable" }
812
+ : {
813
+ kind: "error",
814
+ message: "retrieval failed",
815
+ },
816
+ );
817
+ });
818
+ return () => {
819
+ controller.abort();
820
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
821
+ };
822
+ }, [artifact, load]);
823
+
824
+ const caption = `${verb}${countSuffix}`;
825
+ return (
826
+ <ActivityDisclosure
827
+ icon={<CameraIcon className={ICON_SIZE} />}
828
+ iconTone={failed ? "failed" : state.kind === "ready" ? "accent" : "muted"}
829
+ title={caption}
830
+ failed={failed}
831
+ cancelled={cancelled}
832
+ preview={
833
+ state.kind === "loading"
834
+ ? "loading retained screenshot…"
835
+ : state.kind === "error"
836
+ ? "screenshot retrieval failed"
837
+ : state.kind === "unavailable"
838
+ ? `screenshot ${state.label}`
839
+ : undefined
840
+ }
841
+ media={
842
+ state.kind === "ready" ? (
843
+ <Thumbnail src={state.url} caption={caption} />
844
+ ) : state.kind === "loading" ? (
845
+ <MediaSkeleton />
846
+ ) : (
847
+ <MediaEmpty />
848
+ )
849
+ }
850
+ >
851
+ {state.kind === "ready" ? (
852
+ <ScreenshotFigure src={state.url} caption={caption} />
853
+ ) : state.kind === "loading" ? (
854
+ <BodyNote>Loading the retained screenshot…</BodyNote>
855
+ ) : state.kind === "unavailable" ? (
856
+ <BodyNote>Screenshot {state.label}.</BodyNote>
857
+ ) : (
858
+ <BodyNote tone="error">Screenshot retrieval failed: {state.message}</BodyNote>
859
+ )}
860
+ {batched ? <BodyNote>batched: {batched}</BodyNote> : null}
861
+ </ActivityDisclosure>
862
+ );
863
+ }
864
+
723
865
  /* ---- web_search ------------------------------------------------------------ */
724
866
 
725
867
  type WebSearchResult = { title: string; domain: string; snippet: string };
@@ -995,7 +1137,7 @@ function SecretSetRenderer({ item }: ToolRendererProps) {
995
1137
  running
996
1138
  preview={<RunningPreview>setting…</RunningPreview>}
997
1139
  >
998
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1140
+ <PayloadBlock label="Arguments" value={args} />
999
1141
  </ActivityDisclosure>
1000
1142
  );
1001
1143
  }
@@ -1010,7 +1152,7 @@ function SecretSetRenderer({ item }: ToolRendererProps) {
1010
1152
  failed
1011
1153
  preview={errorText ?? "variable write failed"}
1012
1154
  >
1013
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1155
+ <PayloadBlock label="Arguments" value={args} />
1014
1156
  {errorText ? (
1015
1157
  <PayloadBlock label="Error" value={errorText} failed />
1016
1158
  ) : (
@@ -1026,10 +1168,12 @@ function SecretSetRenderer({ item }: ToolRendererProps) {
1026
1168
  iconTone="muted"
1027
1169
  title={`Set ${name}`}
1028
1170
  cancelled={item.status === "cancelled"}
1029
- preview="value write-only · never returned"
1171
+ preview="exact value preserved"
1030
1172
  >
1031
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1032
- <BodyNote>the value is a secret — redacted in every view; the API never returns it.</BodyNote>
1173
+ <PayloadBlock label="Arguments" value={args} />
1174
+ <BodyNote>
1175
+ The configured value is preserved exactly and available through authorized secret reads.
1176
+ </BodyNote>
1033
1177
  </ActivityDisclosure>
1034
1178
  );
1035
1179
  }
@@ -1175,7 +1319,7 @@ function ToolSearchRenderer({ item }: ToolRendererProps) {
1175
1319
  }
1176
1320
  >
1177
1321
  {query ? <BodyNote>capability query: {query}</BodyNote> : null}
1178
- <PayloadBlock label="Arguments" value={redactSecrets(parseToolArgs(item.arguments))} />
1322
+ <PayloadBlock label="Arguments" value={parseToolArgs(item.arguments)} />
1179
1323
  </ActivityDisclosure>
1180
1324
  );
1181
1325
  }
@@ -1191,7 +1335,7 @@ function ToolSearchRenderer({ item }: ToolRendererProps) {
1191
1335
  preview={truncatePreview(outText, 80) || queryPreview || "Lookup failed"}
1192
1336
  >
1193
1337
  {query ? <BodyNote>capability query: {query}</BodyNote> : null}
1194
- <PayloadBlock label="Arguments" value={redactSecrets(parseToolArgs(item.arguments))} />
1338
+ <PayloadBlock label="Arguments" value={parseToolArgs(item.arguments)} />
1195
1339
  <PayloadBlock label="Error" value={outText} failed />
1196
1340
  </ActivityDisclosure>
1197
1341
  );
@@ -1226,7 +1370,7 @@ function ToolSearchRenderer({ item }: ToolRendererProps) {
1226
1370
  ) : tools && tools.length === 0 ? (
1227
1371
  <BodyNote>no deferred tools matched this capability query.</BodyNote>
1228
1372
  ) : null}
1229
- <PayloadBlock label="Arguments" value={redactSecrets(parseToolArgs(item.arguments))} />
1373
+ <PayloadBlock label="Arguments" value={parseToolArgs(item.arguments)} />
1230
1374
  {tools == null && outText ? <PayloadBlock label="Result" value={outText} /> : null}
1231
1375
  </ActivityDisclosure>
1232
1376
  );
@@ -1249,7 +1393,7 @@ function DocsSearchRenderer({ item }: ToolRendererProps) {
1249
1393
  running
1250
1394
  preview={<RunningPreview>Searching…</RunningPreview>}
1251
1395
  >
1252
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1396
+ <PayloadBlock label="Arguments" value={args} />
1253
1397
  </ActivityDisclosure>
1254
1398
  );
1255
1399
  }
@@ -1264,7 +1408,7 @@ function DocsSearchRenderer({ item }: ToolRendererProps) {
1264
1408
  failed
1265
1409
  preview={truncatePreview(outText, 80) || "Search failed"}
1266
1410
  >
1267
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1411
+ <PayloadBlock label="Arguments" value={args} />
1268
1412
  <PayloadBlock label="Error" value={outText} failed />
1269
1413
  </ActivityDisclosure>
1270
1414
  );
@@ -1300,7 +1444,7 @@ function DocsSearchRenderer({ item }: ToolRendererProps) {
1300
1444
  ))}
1301
1445
  </ul>
1302
1446
  ) : null}
1303
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1447
+ <PayloadBlock label="Arguments" value={args} />
1304
1448
  <PayloadBlock label="Result" value={outText} />
1305
1449
  </ActivityDisclosure>
1306
1450
  );
@@ -1330,7 +1474,7 @@ function SetSessionTitleRenderer({ item }: ToolRendererProps) {
1330
1474
  )
1331
1475
  }
1332
1476
  >
1333
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1477
+ <PayloadBlock label="Arguments" value={args} />
1334
1478
  </ActivityDisclosure>
1335
1479
  );
1336
1480
  }
@@ -1345,7 +1489,7 @@ function SetSessionTitleRenderer({ item }: ToolRendererProps) {
1345
1489
  failed
1346
1490
  preview={truncatePreview(outText, 80) || "Rename failed"}
1347
1491
  >
1348
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1492
+ <PayloadBlock label="Arguments" value={args} />
1349
1493
  <PayloadBlock label="Error" value={outText} failed />
1350
1494
  </ActivityDisclosure>
1351
1495
  );
@@ -1371,7 +1515,7 @@ function SetSessionTitleRenderer({ item }: ToolRendererProps) {
1371
1515
  cancelled={item.status === "cancelled"}
1372
1516
  preview={item.status === "cancelled" ? undefined : settledTitle || undefined}
1373
1517
  >
1374
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1518
+ <PayloadBlock label="Arguments" value={args} />
1375
1519
  {outText ? <PayloadBlock label="Result" value={outText} /> : null}
1376
1520
  </ActivityDisclosure>
1377
1521
  );
@@ -1431,7 +1575,7 @@ function MemoryProposeRenderer({ item }: ToolRendererProps) {
1431
1575
  running
1432
1576
  preview={<RunningPreview>Proposing…</RunningPreview>}
1433
1577
  >
1434
- <PayloadBlock label="Arguments" value={redactSecrets(args)} />
1578
+ <PayloadBlock label="Arguments" value={args} />
1435
1579
  </ActivityDisclosure>
1436
1580
  );
1437
1581
  }
@@ -1493,7 +1637,7 @@ function askToolPreview(args: unknown): string | null {
1493
1637
  }
1494
1638
 
1495
1639
  function AskRenderer({ item }: ToolRendererProps) {
1496
- const args = redactSecrets(parseToolArgs(item.arguments));
1640
+ const args = parseToolArgs(item.arguments);
1497
1641
  const preview = askToolPreview(args);
1498
1642
  const icon = <MessageCircleQuestionIcon className={ICON_SIZE} />;
1499
1643
  const title = "Ask";
@@ -1583,7 +1727,7 @@ function runOnOpPreview(args: Record<string, unknown>): string | null {
1583
1727
 
1584
1728
  function RunOnRenderer({ item }: ToolRendererProps) {
1585
1729
  const parsedArgs = parseToolArgs(item.arguments);
1586
- const args = redactSecrets(parsedArgs);
1730
+ const args = parsedArgs;
1587
1731
  const targetName = runOnTargetName(item.output);
1588
1732
  const title = targetName ? `Run on ${targetName}` : "Run on";
1589
1733
  const opPreview = runOnOpPreview(parsedArgs);
@@ -1648,7 +1792,7 @@ function RunOnRenderer({ item }: ToolRendererProps) {
1648
1792
  */
1649
1793
  function GenericRenderer({ item }: ToolRendererProps) {
1650
1794
  const running = item.status === "running";
1651
- const args = redactSecrets(parseToolArgs(item.arguments));
1795
+ const args = parseToolArgs(item.arguments);
1652
1796
  const display = toolDisplayName(item.name);
1653
1797
  const icon = <GenericToolIcon name={item.name} />;
1654
1798
  // Goal tools: surface the objective text on the collapsed row so the in-cluster
@@ -416,8 +416,8 @@ export function TurnSummary({
416
416
  // as a turn landmark above any nested cluster rows it groups.
417
417
  "group flex w-full items-center rounded-og-sm text-left transition-colors",
418
418
  // A folded turn is a touch target on coarse pointers: grow the row so it
419
- // clears the 40px minimum without disturbing the calm desktop rhythm.
420
- "pointer-coarse:py-2.5",
419
+ // clears the 44px minimum without disturbing the calm desktop rhythm.
420
+ "pointer-coarse:min-h-11 pointer-coarse:py-2.5",
421
421
  bare
422
422
  ? "gap-2 px-1.5 py-1.5 text-og-sm text-og-fg-muted"
423
423
  : "-mx-2 gap-2.5 px-2 py-1.5 text-og-base text-og-fg-muted",
@@ -437,8 +437,8 @@ export function TurnSummary({
437
437
  className={cn(
438
438
  "size-3.5 shrink-0 text-og-fg-subtle transition-transform ease-og-in-out group-data-[state=open]:rotate-90",
439
439
  settlePhase
440
- ? "duration-[var(--og-duration-disclose-settle)]"
441
- : "duration-[var(--og-duration-disclose)]",
440
+ ? "duration-[var(--_og-duration-disclose-settle)]"
441
+ : "duration-[var(--_og-duration-disclose)]",
442
442
  )}
443
443
  />
444
444
  {/* Completion is the quiet default and needs no repeated glyph. Failed,
@@ -646,8 +646,8 @@ const BUILT_IN_TURN_SUMMARY_FACETS: readonly TurnSummaryFacet[] = Object.freeze(
646
646
  contextCompactionCount === 1 ? "compacted" : `${contextCompactionCount} compacts`,
647
647
  ariaLabel:
648
648
  contextCompactionCount === 1
649
- ? "Conversation memory compacted"
650
- : `${contextCompactionCount} conversation memory compactions`,
649
+ ? "Conversation history compacted"
650
+ : `${contextCompactionCount} conversation history compactions`,
651
651
  }
652
652
  : null,
653
653
  },
@@ -288,6 +288,10 @@ export type AuthNeededItem = {
288
288
  kind: "auth-needed";
289
289
  id: string;
290
290
  turnId: string | null;
291
+ /** The runtime surface that requested recovery, when the event is an MCP auth signal. */
292
+ serverId: string | null;
293
+ /** Durable event family that produced this notice. */
294
+ source?: "tool" | "credential" | undefined;
291
295
  /** The connection's registrable domain, e.g. "linear.app". */
292
296
  providerDomain: string;
293
297
  /** The lapsed connection to reconnect, when the row survived. */
@@ -9,6 +9,8 @@ export type VoiceRecordingUploadState = "pending" | "syncing" | "retrying" | "co
9
9
  export type VoiceRecordingTranscriptionState = "pending" | "transcribing" | "retrying" | "complete";
10
10
  export type VoiceRecordingFinalizationState = "pending" | "transcript-ready" | "handed-off";
11
11
  export type VoiceRecordingChunkUploadState = "pending" | "syncing" | "complete";
12
+ export type VoiceRecordingRecoveryMode = "automatic" | "manual";
13
+ export type VoiceRecordingHandoffMode = "append" | "explicit";
12
14
 
13
15
  export type VoiceRecordingManifest = {
14
16
  version: 1;
@@ -22,6 +24,10 @@ export type VoiceRecordingManifest = {
22
24
  uploadState: VoiceRecordingUploadState;
23
25
  transcriptionState: VoiceRecordingTranscriptionState;
24
26
  finalizationState: VoiceRecordingFinalizationState;
27
+ /** Whether safe server reconciliation should continue without another user action. */
28
+ recoveryMode: VoiceRecordingRecoveryMode;
29
+ /** Delayed/recovered results require explicit insertion instead of mutating an uncertain draft. */
30
+ handoffMode: VoiceRecordingHandoffMode;
25
31
  /** Tab/process lease. A different owner may take over only after this heartbeat is stale. */
26
32
  ownerId: string | null;
27
33
  ownerHeartbeatAt: string | null;
@@ -91,6 +97,8 @@ export interface VoiceRecordingStore {
91
97
  | "ownerId"
92
98
  | "ownerHeartbeatAt"
93
99
  | "transcriptText"
100
+ | "recoveryMode"
101
+ | "handoffMode"
94
102
  >
95
103
  >,
96
104
  updatedAt: string,
@@ -155,6 +163,8 @@ export function createVoiceRecordingManifest(input: {
155
163
  uploadState: "pending",
156
164
  transcriptionState: "pending",
157
165
  finalizationState: "pending",
166
+ recoveryMode: "automatic",
167
+ handoffMode: "append",
158
168
  ownerId: input.ownerId ?? null,
159
169
  ownerHeartbeatAt: input.ownerId ? input.createdAt : null,
160
170
  transcriptText: null,
@@ -306,6 +316,7 @@ export class IndexedDbVoiceRecordingStore implements VoiceRecordingStore {
306
316
  const updated: VoiceRecordingManifest = {
307
317
  ...manifest,
308
318
  captureState: manifest.captureState === "capturing" ? "stopped" : manifest.captureState,
319
+ handoffMode: "explicit",
309
320
  ownerId,
310
321
  ownerHeartbeatAt: claimedAt,
311
322
  updatedAt: claimedAt,
@@ -370,6 +381,8 @@ export class IndexedDbVoiceRecordingStore implements VoiceRecordingStore {
370
381
  | "ownerId"
371
382
  | "ownerHeartbeatAt"
372
383
  | "transcriptText"
384
+ | "recoveryMode"
385
+ | "handoffMode"
373
386
  >
374
387
  >,
375
388
  updatedAt: string,
@@ -455,6 +468,8 @@ function normalizeManifest(manifest: VoiceRecordingManifest): VoiceRecordingMani
455
468
  ownerId: manifest.ownerId ?? null,
456
469
  ownerHeartbeatAt: manifest.ownerHeartbeatAt ?? null,
457
470
  transcriptText: manifest.transcriptText ?? null,
471
+ recoveryMode: manifest.recoveryMode ?? "manual",
472
+ handoffMode: manifest.handoffMode ?? "explicit",
458
473
  };
459
474
  }
460
475