@opengeni/react 0.13.0 → 0.20.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 (113) hide show
  1. package/README.md +129 -23
  2. package/dist/chunk-6BXBGJ3B.js +1956 -0
  3. package/dist/chunk-6BXBGJ3B.js.map +1 -0
  4. package/dist/chunk-6UT5OC3P.js +2133 -0
  5. package/dist/chunk-6UT5OC3P.js.map +1 -0
  6. package/dist/chunk-EBTT3MYN.js +92 -0
  7. package/dist/chunk-EBTT3MYN.js.map +1 -0
  8. package/dist/chunk-HHFA4AFX.js +425 -0
  9. package/dist/chunk-HHFA4AFX.js.map +1 -0
  10. package/dist/chunk-LAZ2A743.js +2393 -0
  11. package/dist/chunk-LAZ2A743.js.map +1 -0
  12. package/dist/chunk-MRSECXRP.js +563 -0
  13. package/dist/chunk-MRSECXRP.js.map +1 -0
  14. package/dist/chunk-TK7G6XLT.js +18 -0
  15. package/dist/chunk-TK7G6XLT.js.map +1 -0
  16. package/dist/composer-DvUaa5ki.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +642 -1080
  21. package/dist/index.js +7672 -8005
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +25 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
  30. package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
  31. package/dist/session-Gd4iVPYw.d.ts +425 -0
  32. package/dist/session-context-DIFFlXKc.d.ts +52 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +50 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-BCfm4mzX.d.ts +112 -0
  37. package/package.json +23 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +50 -1
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +104 -751
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +82 -16
  51. package/src/components/machine-health-pill.tsx +68 -0
  52. package/src/components/machine-metrics.tsx +10 -24
  53. package/src/components/machines/health.ts +146 -0
  54. package/src/components/machines/machine-detail.tsx +220 -0
  55. package/src/components/machines/metric-history-chart.tsx +298 -0
  56. package/src/components/machines/metric-sparkline.tsx +76 -0
  57. package/src/components/machines/series.ts +113 -0
  58. package/src/components/machines-dashboard.tsx +16 -4
  59. package/src/components/message-timeline.tsx +15 -7
  60. package/src/components/model-picker.tsx +12 -3
  61. package/src/components/pierre-diff.tsx +32 -6
  62. package/src/components/queue-surface-implementation.tsx +1041 -0
  63. package/src/components/queue-surface-state.tsx +94 -0
  64. package/src/components/queue-surface.tsx +60 -0
  65. package/src/components/sandbox-files.tsx +230 -21
  66. package/src/components/sandbox-terminal.tsx +8 -2
  67. package/src/components/sandbox-workspace.tsx +489 -135
  68. package/src/components/session-status.tsx +0 -6
  69. package/src/components/workbench-changes.tsx +145 -50
  70. package/src/components/workspace-dock.tsx +329 -68
  71. package/src/composer.ts +60 -0
  72. package/src/hooks/internal.ts +115 -34
  73. package/src/hooks/use-composer.ts +635 -74
  74. package/src/hooks/use-human-input.ts +131 -0
  75. package/src/hooks/use-machine-chip.ts +2 -2
  76. package/src/hooks/use-machines.ts +27 -11
  77. package/src/hooks/use-sandbox-files.ts +310 -62
  78. package/src/hooks/use-sandbox-git.ts +154 -40
  79. package/src/hooks/use-sandbox-terminal.ts +28 -6
  80. package/src/hooks/use-session-capabilities.ts +38 -9
  81. package/src/hooks/use-session-control.ts +43 -15
  82. package/src/hooks/use-session-events.ts +523 -48
  83. package/src/hooks/use-session-lineage.ts +15 -6
  84. package/src/hooks/use-session.ts +10 -2
  85. package/src/hooks/use-slash-commands.ts +38 -38
  86. package/src/hooks/use-transcription.ts +757 -0
  87. package/src/hooks/use-turn-queue.ts +273 -77
  88. package/src/hooks/use-windowed-sections.ts +2 -2
  89. package/src/hooks/use-workspace-capture.ts +146 -40
  90. package/src/hooks/use-workspace-edit.ts +50 -14
  91. package/src/hooks/use-workspace-sessions.ts +3 -0
  92. package/src/human-input.ts +72 -0
  93. package/src/index.ts +88 -12
  94. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  95. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  96. package/src/lib/use-portal-token-style.ts +51 -0
  97. package/src/lib/use-unicode-fonts.ts +57 -0
  98. package/src/machines.ts +16 -0
  99. package/src/provider.tsx +163 -41
  100. package/src/session-context.ts +135 -0
  101. package/src/session.ts +79 -0
  102. package/src/timeline/parsers.ts +78 -6
  103. package/src/timeline/projection.ts +36 -6
  104. package/src/timeline/tool-renderers.tsx +41 -0
  105. package/src/timeline/turn-summary.tsx +2 -2
  106. package/src/timeline/types.ts +16 -8
  107. package/styles/index.css +78 -0
  108. package/styles/index.d.ts +2 -0
  109. package/styles/tokens.css +11 -6
  110. package/styles/tokens.d.ts +2 -0
  111. package/dist/chunk-NFYVQWIB.js +0 -1377
  112. package/dist/chunk-NFYVQWIB.js.map +0 -1
  113. package/dist/machines-CnlMb7E-.d.ts +0 -329
@@ -0,0 +1,135 @@
1
+ import type { StreamConnectionState, WorkspaceControlEvent } from "@opengeni/sdk";
2
+ import { createContext, useContext } from "react";
3
+ import type {
4
+ EmbeddedHumanInputSessionClientLike,
5
+ EmbeddedSessionClientLike,
6
+ SessionClientLike,
7
+ } from "./client";
8
+
9
+ export type OpenGeniContextValue = {
10
+ client: SessionClientLike;
11
+ workspaceId: string;
12
+ workspaceControlEvent: WorkspaceControlEvent | null;
13
+ workspaceControlConnectionState: StreamConnectionState | "idle" | "error";
14
+ registerSessionReconciler: (
15
+ sessionId: string,
16
+ key: string,
17
+ reconcile: () => Promise<void>,
18
+ ) => () => void;
19
+ reconcileSession: (sessionId: string) => Promise<void>;
20
+ };
21
+
22
+ export const OpenGeniContext = createContext<OpenGeniContextValue | null>(null);
23
+
24
+ const NOOP_REGISTER_RECONCILER: OpenGeniContextValue["registerSessionReconciler"] = () => () =>
25
+ undefined;
26
+ const NOOP_RECONCILE_SESSION: OpenGeniContextValue["reconcileSession"] = async () => undefined;
27
+
28
+ export type ClientOverride = {
29
+ client?: SessionClientLike | undefined;
30
+ workspaceId?: string | undefined;
31
+ };
32
+
33
+ export type EmbeddedSessionClientOverride = {
34
+ client?: EmbeddedSessionClientLike | undefined;
35
+ workspaceId?: string | undefined;
36
+ };
37
+
38
+ export type EmbeddedHumanInputClientOverride = {
39
+ client?: EmbeddedHumanInputSessionClientLike | undefined;
40
+ workspaceId?: string | undefined;
41
+ };
42
+
43
+ export type EmbeddedSessionContextValue = Omit<OpenGeniContextValue, "client"> & {
44
+ client: EmbeddedSessionClientLike;
45
+ };
46
+
47
+ /** Resolve client + workspace from explicit overrides or the provider. */
48
+ export function useOpenGeni(override: ClientOverride = {}): OpenGeniContextValue {
49
+ const context = useContext(OpenGeniContext);
50
+ const client = override.client ?? context?.client;
51
+ const workspaceId = override.workspaceId ?? context?.workspaceId;
52
+ if (!client || !workspaceId) {
53
+ throw new Error(
54
+ "@opengeni/react: no OpenGeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook.",
55
+ );
56
+ }
57
+ return {
58
+ client,
59
+ workspaceId,
60
+ workspaceControlEvent: context?.workspaceControlEvent ?? null,
61
+ workspaceControlConnectionState: context?.workspaceControlConnectionState ?? "idle",
62
+ registerSessionReconciler: context?.registerSessionReconciler ?? NOOP_REGISTER_RECONCILER,
63
+ reconcileSession: context?.reconcileSession ?? NOOP_RECONCILE_SESSION,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Resolve the narrow client required by the session-only hooks. The full
69
+ * provider client is structurally compatible, while an explicit host proxy
70
+ * only needs to expose session/event/composer/queue/control operations.
71
+ */
72
+ export function useEmbeddedSession(
73
+ override: EmbeddedSessionClientOverride = {},
74
+ ): EmbeddedSessionContextValue {
75
+ const context = useContext(OpenGeniContext);
76
+ const client = override.client ?? context?.client;
77
+ const workspaceId = override.workspaceId ?? context?.workspaceId;
78
+ if (!client || !workspaceId) {
79
+ throw new Error(
80
+ "@opengeni/react: no OpenGeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook.",
81
+ );
82
+ }
83
+ return {
84
+ client,
85
+ workspaceId,
86
+ workspaceControlEvent: context?.workspaceControlEvent ?? null,
87
+ workspaceControlConnectionState: context?.workspaceControlConnectionState ?? "idle",
88
+ registerSessionReconciler: context?.registerSessionReconciler ?? NOOP_REGISTER_RECONCILER,
89
+ reconcileSession: context?.reconcileSession ?? NOOP_RECONCILE_SESSION,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Resolve the structured-input refinement without widening the baseline
95
+ * session-only proxy contract.
96
+ */
97
+ export function useEmbeddedHumanInputSession(override: EmbeddedHumanInputClientOverride = {}): Omit<
98
+ EmbeddedSessionContextValue,
99
+ "client"
100
+ > & {
101
+ client: EmbeddedHumanInputSessionClientLike;
102
+ } {
103
+ const embedded = useEmbeddedSession(override);
104
+ const client = embedded.client as Partial<EmbeddedHumanInputSessionClientLike>;
105
+ if (
106
+ typeof client.listHumanInputRequests !== "function" ||
107
+ typeof client.getHumanInputRequest !== "function" ||
108
+ typeof client.submitHumanInputResponse !== "function"
109
+ ) {
110
+ throw new Error(
111
+ "@opengeni/react: useHumanInputRequests requires listHumanInputRequests, getHumanInputRequest, and submitHumanInputResponse.",
112
+ );
113
+ }
114
+ return {
115
+ ...embedded,
116
+ client: client as EmbeddedHumanInputSessionClientLike,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Resolve the client only — for hooks that are not workspace-scoped
122
+ * (`useWorkspaces`, `useBillingUsage`).
123
+ */
124
+ export function useOpenGeniClient(
125
+ override: Pick<ClientOverride, "client"> = {},
126
+ ): SessionClientLike {
127
+ const context = useContext(OpenGeniContext);
128
+ const client = override.client ?? context?.client;
129
+ if (!client) {
130
+ throw new Error(
131
+ "@opengeni/react: no OpenGeni client available. Wrap the tree in <OpenGeniProvider> or pass { client } to the hook.",
132
+ );
133
+ }
134
+ return client;
135
+ }
package/src/session.ts ADDED
@@ -0,0 +1,79 @@
1
+ // @opengeni/react/session — session state and pure timeline projection.
2
+ //
3
+ // This entry deliberately excludes styled components, workbench surfaces, CSS,
4
+ // and their optional peers. Hosts keep OpenGeni's session semantics while
5
+ // rendering their own product UI.
6
+
7
+ export type {
8
+ EmbeddedHumanInputSessionClientLike as HumanInputSessionClientLike,
9
+ EmbeddedSessionClientLike as SessionClientLike,
10
+ } from "./client";
11
+ export type {
12
+ EmbeddedHumanInputClientOverride as HumanInputClientOverride,
13
+ EmbeddedSessionClientOverride as ClientOverride,
14
+ } from "./session-context";
15
+
16
+ export { useSessionEvents } from "./hooks/use-session-events";
17
+ export type {
18
+ SessionEventsConnectionState,
19
+ UseSessionEventsOptions,
20
+ UseSessionEventsResult,
21
+ } from "./hooks/use-session-events";
22
+ export {
23
+ useComposer,
24
+ composeSendInput,
25
+ shouldSteerOnKey,
26
+ shouldSubmitOnKey,
27
+ FILE_ONLY_MESSAGE_TEXT,
28
+ } from "./hooks/use-composer";
29
+ export type { ComposerSendExtras, ComposerState, UseComposerOptions } from "./hooks/use-composer";
30
+ export { useTurnQueue, isTurnQueueEvent } from "./hooks/use-turn-queue";
31
+ export type {
32
+ QueueMutationKind,
33
+ UseTurnQueueOptions,
34
+ UseTurnQueueResult,
35
+ } from "./hooks/use-turn-queue";
36
+ export { useSessionControl } from "./hooks/use-session-control";
37
+ export type {
38
+ UseSessionControlOptions,
39
+ UseSessionControlResult,
40
+ } from "./hooks/use-session-control";
41
+
42
+ export { approvalsFromRequiresAction, projectPendingApprovals } from "./approvals";
43
+ export type { PendingApproval } from "./approvals";
44
+
45
+ export { humanInputRequestFromEvent, projectPendingHumanInputRequests } from "./human-input";
46
+ export type { PendingHumanInputRequest } from "./human-input";
47
+ export { isHumanInputEvent, useHumanInputRequests } from "./hooks/use-human-input";
48
+ export type {
49
+ UseHumanInputRequestsOptions,
50
+ UseHumanInputRequestsResult,
51
+ } from "./hooks/use-human-input";
52
+
53
+ export {
54
+ buildTimeline,
55
+ creditExhaustedFromEvents,
56
+ extractSessionRef,
57
+ groupTimeline,
58
+ sessionStatusFromEvents,
59
+ toolDisplayName,
60
+ } from "./timeline/projection";
61
+ export type {
62
+ ActivityItem,
63
+ AgentMessageItem,
64
+ AuthNeededItem,
65
+ GoalItem,
66
+ MemoryItem,
67
+ NoticeItem,
68
+ ReasoningItem,
69
+ SandboxItem,
70
+ SessionStatusItem,
71
+ TimelineGroup,
72
+ TimelineItem,
73
+ TurnOutcome,
74
+ ToolCallItem,
75
+ TurnEndItem,
76
+ UserMessageItem,
77
+ WorkerCompletionItem,
78
+ WorkerItem,
79
+ } from "./timeline/types";
@@ -148,17 +148,34 @@ export function v4aToGitFileDiff(op: ApplyPatchOperation): GitFileDiff {
148
148
  // `@@ -0,0 +1,N @@` from the range fields once newLines is counted — a
149
149
  // pre-baked partial header (e.g. `@@ +1 @@`) renders zero lines in a
150
150
  // generic unified-diff parser.
151
- cur = { oldStart: 0, oldLines: 0, newStart: 1, newLines: 0, header: "", lines: [] };
151
+ cur = {
152
+ oldStart: 0,
153
+ oldLines: 0,
154
+ newStart: 1,
155
+ newLines: 0,
156
+ header: "",
157
+ lines: [],
158
+ };
152
159
  hunks.push(cur);
153
160
  oldNo = 0;
154
161
  newNo = 1;
155
162
  }
156
163
  if (raw.startsWith("+")) {
157
- cur.lines.push({ type: "add", oldNo: null, newNo: newNo++, text: raw.slice(1) });
164
+ cur.lines.push({
165
+ type: "add",
166
+ oldNo: null,
167
+ newNo: newNo++,
168
+ text: raw.slice(1),
169
+ });
158
170
  cur.newLines += 1;
159
171
  additions += 1;
160
172
  } else if (raw.startsWith("-")) {
161
- cur.lines.push({ type: "del", oldNo: oldNo++, newNo: null, text: raw.slice(1) });
173
+ cur.lines.push({
174
+ type: "del",
175
+ oldNo: oldNo++,
176
+ newNo: null,
177
+ text: raw.slice(1),
178
+ });
162
179
  cur.oldLines += 1;
163
180
  deletions += 1;
164
181
  } else {
@@ -200,7 +217,10 @@ export function v4aToGitFileDiff(op: ApplyPatchOperation): GitFileDiff {
200
217
  * renderer and the turn-summary facet counter never drift.
201
218
  */
202
219
  export function applyPatchOps(raw: unknown): ApplyPatchOperation[] {
203
- const r = (raw ?? {}) as { operation?: ApplyPatchOperation; operations?: ApplyPatchOperation[] };
220
+ const r = (raw ?? {}) as {
221
+ operation?: ApplyPatchOperation;
222
+ operations?: ApplyPatchOperation[];
223
+ };
204
224
  if (Array.isArray(r.operations)) {
205
225
  return r.operations;
206
226
  }
@@ -264,7 +284,21 @@ export function tailPeek(text: string): string {
264
284
  * into a flat `{ text, isError }`. Non-MCP outputs pass through as their string
265
285
  * form.
266
286
  */
267
- export function unwrapMcpOutput(output: unknown): { text: string; isError: boolean } {
287
+ export function unwrapMcpOutput(output: unknown): {
288
+ text: string;
289
+ isError: boolean;
290
+ } {
291
+ // Managed MCP events persist a normalized single text part as
292
+ // `{ type: "text", text }`; accept it alongside the SDK-native content array.
293
+ if (
294
+ output &&
295
+ typeof output === "object" &&
296
+ (output as { type?: unknown }).type === "text" &&
297
+ typeof (output as { text?: unknown }).text === "string"
298
+ ) {
299
+ const record = output as { text: string; isError?: unknown };
300
+ return { text: record.text, isError: Boolean(record.isError) };
301
+ }
268
302
  if (output && typeof output === "object" && "content" in output) {
269
303
  const record = output as { content?: unknown; isError?: unknown };
270
304
  const isError = Boolean(record.isError);
@@ -273,7 +307,10 @@ export function unwrapMcpOutput(output: unknown): { text: string; isError: boole
273
307
  (part): part is { type: string; text: string } =>
274
308
  !!part && typeof part === "object" && (part as { type?: unknown }).type === "text",
275
309
  );
276
- return { text: textPart ? String(textPart.text) : JSON.stringify(output), isError };
310
+ return {
311
+ text: textPart ? String(textPart.text) : JSON.stringify(output),
312
+ isError,
313
+ };
277
314
  }
278
315
  return { text: JSON.stringify(output), isError };
279
316
  }
@@ -350,6 +387,41 @@ export function screenshotDataUrl(out: unknown): string | null {
350
387
  return null;
351
388
  }
352
389
 
390
+ export type TimelineMediaPreview = {
391
+ type: "media_preview";
392
+ mediaType: string;
393
+ inlineBytes: number | null;
394
+ fullOutputAvailable: false;
395
+ preview: string;
396
+ };
397
+
398
+ /** Find the explicit non-retained inline-media fact in a tool output. */
399
+ export function mediaPreviewFact(out: unknown): TimelineMediaPreview | null {
400
+ if (typeof out === "string" && (out.startsWith("{") || out.startsWith("["))) {
401
+ const parsed = tryParseJson(out);
402
+ if (parsed !== undefined && parsed !== out) return mediaPreviewFact(parsed);
403
+ }
404
+ if (Array.isArray(out)) {
405
+ for (const entry of out) {
406
+ const preview = mediaPreviewFact(entry);
407
+ if (preview) return preview;
408
+ }
409
+ return null;
410
+ }
411
+ if (!out || typeof out !== "object") return null;
412
+ const record = out as Record<string, unknown>;
413
+ if (
414
+ record.type !== "media_preview" ||
415
+ typeof record.mediaType !== "string" ||
416
+ record.fullOutputAvailable !== false ||
417
+ typeof record.preview !== "string" ||
418
+ (record.inlineBytes !== null && typeof record.inlineBytes !== "number")
419
+ ) {
420
+ return null;
421
+ }
422
+ return record as TimelineMediaPreview;
423
+ }
424
+
353
425
  /** Serialize whatever a Uint8Array became in JSON (number[], {"0":n,…} index
354
426
  * map, or Buffer-JSON {type:"Buffer",data:[…]}) back into base64. */
355
427
  function bytesToBase64(data: unknown): string | null {
@@ -393,7 +393,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
393
393
  tone: "waiting",
394
394
  text:
395
395
  before && after
396
- ? `Context compacted from approximately ${before} to ${after} tokens.`
396
+ ? `Active conversation history compacted from approximately ${before} to ${after} tokens.`
397
397
  : "Context compacted so the turn could continue.",
398
398
  occurredAt: event.occurredAt,
399
399
  });
@@ -406,13 +406,17 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
406
406
  items.push({
407
407
  kind: "notice",
408
408
  id: event.id,
409
- tone: "waiting",
409
+ tone: reason === "summarization_failed" ? "failed" : "waiting",
410
410
  text:
411
411
  reason === "no_history"
412
412
  ? "Context compaction skipped because there is no active history to compact."
413
413
  : reason === "replacement_not_smaller"
414
414
  ? "Context compaction skipped because the generated checkpoint would not reduce the context."
415
- : "Context compaction was not needed.",
415
+ : reason === "replacement_unchanged"
416
+ ? "Context compaction stopped because it reproduced the same checkpoint without making progress."
417
+ : reason === "summarization_failed"
418
+ ? "Context compaction failed without replacing the active conversation history. Request it again to retry."
419
+ : "Context compaction was not needed.",
416
420
  occurredAt: event.occurredAt,
417
421
  });
418
422
  break;
@@ -432,7 +436,8 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
432
436
  break;
433
437
  }
434
438
 
435
- case "tool.auth_needed": {
439
+ case "tool.auth_needed":
440
+ case "credential.auth_needed": {
436
441
  // Keep the whole structured payload — the renderer turns it into a clean
437
442
  // inline reconnect card, and the app starts the recovery flow off the
438
443
  // connectionId/resource. Losing it to a plain-text notice was the ugly
@@ -708,13 +713,13 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
708
713
  cancelledTurnIds.add(turnId);
709
714
  }
710
715
 
711
- if (turnId && event.type !== "turn.queued" && event.type !== "turn.cancelled") {
716
+ if (turnId && isTurnExecutionEvidence(event.type)) {
712
717
  const previous = fallbackSeqByTurn.get(turnId);
713
718
  if (previous === undefined || event.sequence < previous) {
714
719
  fallbackSeqByTurn.set(turnId, event.sequence);
715
720
  }
716
721
  }
717
- if (turnId && isAgentActivityEvent(event.type)) {
722
+ if (turnId && isTurnExecutionEvidence(event.type)) {
718
723
  startedTurnIds.add(turnId);
719
724
  }
720
725
  }
@@ -798,6 +803,29 @@ function isAgentActivityEvent(type: string): boolean {
798
803
  return type.startsWith("agent.") || type.startsWith("sandbox.");
799
804
  }
800
805
 
806
+ /**
807
+ * Evidence that a queued prompt crossed the execution boundary when an older
808
+ * or partially recovered ledger is missing its canonical `turn.started`.
809
+ * Queue/control bookkeeping intentionally does not qualify: moving, editing,
810
+ * steering, or deleting a waiting row must never make its prompt appear in the
811
+ * transcript as though inference had begun.
812
+ */
813
+ function isTurnExecutionEvidence(type: string): boolean {
814
+ return (
815
+ isAgentActivityEvent(type) ||
816
+ type === "turn.completed" ||
817
+ type === "turn.failed" ||
818
+ type === "turn.recovery.requested" ||
819
+ type === "turn.capacity_waiting" ||
820
+ type === "session.requiresAction" ||
821
+ type === "tool.auth_needed" ||
822
+ type === "credential.auth_needed" ||
823
+ type.startsWith("rig.setup.") ||
824
+ type === "codex.capacity.waiting" ||
825
+ type === "codex.capacity.resumed"
826
+ );
827
+ }
828
+
801
829
  function turnEndItem(
802
830
  event: SessionEvent,
803
831
  outcome: TurnEndItem["outcome"],
@@ -1171,6 +1199,8 @@ const AUTH_NEEDED_REASONS: ReadonlySet<string> = new Set([
1171
1199
  "expired",
1172
1200
  "insufficient_scope",
1173
1201
  "refresh_failed",
1202
+ "unsupported_auth",
1203
+ "resource_scope_unavailable",
1174
1204
  ]);
1175
1205
 
1176
1206
  function authNeededReason(value: unknown): AuthNeededItem["reason"] {
@@ -22,6 +22,7 @@ import {
22
22
  execTruncated,
23
23
  isExecSessionLostBanner,
24
24
  looksBinary,
25
+ mediaPreviewFact,
25
26
  parseExecBannerSessionId,
26
27
  parseToolArgs,
27
28
  redactSecrets,
@@ -555,6 +556,7 @@ function ComputerCallRenderer({ item }: ToolRendererProps) {
555
556
  const rejected = raw.providerData?.approvalStatus === "rejected";
556
557
  const readOnly = typeof out === "string" && out.includes("read-only");
557
558
  const shotUrl = screenshotDataUrl(out);
559
+ const omittedMedia = mediaPreviewFact(out);
558
560
  const empty = out === "" || out == null;
559
561
  const batched = actions.length > 1 ? actions.map((a) => computerVerb(a)).join(" · ") : null;
560
562
  // Fold the batched-action count into the title (one media affordance per row),
@@ -635,6 +637,26 @@ function ComputerCallRenderer({ item }: ToolRendererProps) {
635
637
  );
636
638
  }
637
639
 
640
+ if (omittedMedia) {
641
+ return (
642
+ <ActivityDisclosure
643
+ icon={<CameraOffIcon className={ICON_SIZE} />}
644
+ iconTone={isFailed ? "failed" : "muted"}
645
+ title={`${verb}${countSuffix} · image omitted · not retained`}
646
+ failed={isFailed}
647
+ cancelled={isCancelled}
648
+ preview="inline image omitted · not retained"
649
+ media={<MediaEmpty />}
650
+ >
651
+ <BodyNote>
652
+ The inline {omittedMedia.mediaType} output was omitted from the audit timeline and its
653
+ source bytes were not retained.
654
+ </BodyNote>
655
+ {batched ? <BodyNote>batched: {batched}</BodyNote> : null}
656
+ </ActivityDisclosure>
657
+ );
658
+ }
659
+
638
660
  if (empty) {
639
661
  return (
640
662
  <ActivityDisclosure
@@ -759,6 +781,7 @@ function ViewImageRenderer({ item }: ToolRendererProps) {
759
781
  const path = typeof args.path === "string" ? args.path : "";
760
782
  const out = item.output;
761
783
  const text = typeof out === "string" ? out : "";
784
+ const omittedMedia = mediaPreviewFact(out);
762
785
 
763
786
  if (item.status === "running") {
764
787
  return (
@@ -807,6 +830,24 @@ function ViewImageRenderer({ item }: ToolRendererProps) {
807
830
  </ActivityDisclosure>
808
831
  );
809
832
  }
833
+ if (omittedMedia) {
834
+ return (
835
+ <ActivityDisclosure
836
+ icon={<ImageIcon className={ICON_SIZE} />}
837
+ iconTone={viewFailed ? "failed" : "muted"}
838
+ title={`Viewed ${basename(path)} · image omitted · not retained`}
839
+ failed={viewFailed}
840
+ cancelled={viewCancelled}
841
+ preview="inline image omitted · not retained"
842
+ media={<MediaEmpty />}
843
+ >
844
+ <BodyNote>
845
+ The inline {omittedMedia.mediaType} output was omitted from the audit timeline and its
846
+ source bytes were not retained.
847
+ </BodyNote>
848
+ </ActivityDisclosure>
849
+ );
850
+ }
810
851
  if (text.includes("No image data")) {
811
852
  return (
812
853
  <ActivityDisclosure
@@ -4,7 +4,7 @@ import { Collapsible } from "radix-ui";
4
4
  import { cn } from "../lib/cn";
5
5
  import { useForcedDefaultOpen } from "./disclosure-context";
6
6
  import { useEntranceAnimation } from "./entrance";
7
- import { applyPatchOps, isApplyPatch, screenshotDataUrl } from "./parsers";
7
+ import { applyPatchOps, isApplyPatch, mediaPreviewFact, screenshotDataUrl } from "./parsers";
8
8
  import { rawTypeOf } from "./registry";
9
9
  import type { ActivityItem, TurnOutcome } from "./types";
10
10
  export type { TurnOutcome } from "./types";
@@ -181,7 +181,7 @@ function summarizeTurn(items: ActivityItem[], durationMs?: number): string {
181
181
  item.name === "computer_call" ||
182
182
  item.name === "computer_screenshot"
183
183
  ) {
184
- if (screenshotDataUrl(item.output) !== null) {
184
+ if (screenshotDataUrl(item.output) !== null || mediaPreviewFact(item.output) !== null) {
185
185
  screenshots += 1;
186
186
  }
187
187
  }
@@ -167,13 +167,14 @@ export type NoticeItem = {
167
167
  };
168
168
 
169
169
  /**
170
- * A tool call hit a connection whose credential lapsed the broker asked the
171
- * user to reconnect the provider before the turn can continue. Carries the
172
- * structured `tool.auth_needed` payload so the renderer can draw a clean inline
173
- * reconnect affordance (provider logo + one human line + a Reconnect button)
174
- * and the app can start the right recovery flow (OAuth reconnect for the
175
- * surviving connection, or credential re-entry for an api-key one). The `reason`
176
- * shapes the human copy but is never shown raw.
170
+ * A tool call hit a connection that needs action or cannot satisfy the selected
171
+ * endpoint/resource contract. Carries the
172
+ * structured `tool.auth_needed` or `credential.auth_needed` payload so the renderer can draw a clean inline
173
+ * reconnect affordance when recovery is possible, or a calm unavailable state
174
+ * for incompatible auth/resource containment. The app can start the right
175
+ * recovery flow (OAuth reconnect for the surviving connection, or credential
176
+ * re-entry for an api-key one). The `reason` shapes the human copy but is never
177
+ * shown raw.
177
178
  */
178
179
  export type AuthNeededItem = {
179
180
  kind: "auth-needed";
@@ -183,7 +184,14 @@ export type AuthNeededItem = {
183
184
  providerDomain: string;
184
185
  /** The lapsed connection to reconnect, when the row survived. */
185
186
  connectionId: string | null;
186
- reason: "missing_connection" | "expired" | "insufficient_scope" | "refresh_failed" | null;
187
+ reason:
188
+ | "missing_connection"
189
+ | "expired"
190
+ | "insufficient_scope"
191
+ | "refresh_failed"
192
+ | "unsupported_auth"
193
+ | "resource_scope_unavailable"
194
+ | null;
187
195
  /** Scopes the provider now needs; may inform the copy, never shown as a raw label. */
188
196
  scopes: string[];
189
197
  /** The OAuth `resource` (RFC 8707) the reconnect should target, when supplied. */
package/styles/index.css CHANGED
@@ -165,6 +165,28 @@
165
165
  }
166
166
  }
167
167
 
168
+ /* Metric-history line draw-in. The chart path carries `pathLength={1}`, so the
169
+ dash math is resolution-independent: one full-length dash whose offset animates
170
+ from 1 (hidden) to 0 (drawn). Degrades to a solid line if animations are off. */
171
+ @keyframes og-mh-draw {
172
+ from {
173
+ stroke-dashoffset: 1;
174
+ }
175
+ to {
176
+ stroke-dashoffset: 0;
177
+ }
178
+ }
179
+ .og-mh-draw {
180
+ stroke-dasharray: 1;
181
+ animation: og-mh-draw 900ms var(--og-ease-out) forwards;
182
+ }
183
+ @media (prefers-reduced-motion: reduce) {
184
+ .og-mh-draw {
185
+ animation: none;
186
+ stroke-dasharray: none;
187
+ }
188
+ }
189
+
168
190
  /* Shimmer helper: gradient text the keyframe scrolls through. */
169
191
  .og-shimmer-text {
170
192
  background: linear-gradient(
@@ -194,6 +216,17 @@
194
216
  scrollbar-color: var(--og-color-border-strong) transparent;
195
217
  }
196
218
 
219
+ /* The machine popover stays mounted so its trigger's `aria-controls` always
220
+ resolves. Keep Radix's positioning wrapper measurable while closed, but
221
+ remove the entire fixed layer from painting, hit testing, and the a11y tree.
222
+ Hiding only the content leaves the wrapper able to intercept pointer input. */
223
+ [data-radix-popper-content-wrapper]:has(
224
+ > [data-machine-state-popover][data-state="closed"]
225
+ ) {
226
+ visibility: hidden;
227
+ pointer-events: none;
228
+ }
229
+
197
230
  @media (prefers-reduced-motion: reduce) {
198
231
  .og-root *,
199
232
  .og-root *::before,
@@ -206,6 +239,51 @@
206
239
  }
207
240
  }
208
241
 
242
+ @media (forced-colors: active) {
243
+ /* Forced-colors can replace a painted background with Canvas while custom
244
+ properties retain their authored dark/light values. That combination is
245
+ dangerous: descendants may resolve a pale dark-theme foreground against a
246
+ light system canvas. Collapse the whole component palette onto guaranteed
247
+ system colors. Meaning in status/diff UI is also carried by text, icons,
248
+ and +/- markers, so it never depends on these colors alone. */
249
+ .og-root {
250
+ --og-color-bg: Canvas;
251
+ --og-color-surface-1: Canvas;
252
+ --og-color-surface-2: Canvas;
253
+ --og-color-surface-3: Canvas;
254
+ --og-color-border: CanvasText;
255
+ --og-color-border-strong: CanvasText;
256
+ --og-color-fg: CanvasText;
257
+ --og-color-fg-muted: CanvasText;
258
+ --og-color-fg-subtle: CanvasText;
259
+ --og-color-accent: CanvasText;
260
+ --og-color-accent-strong: CanvasText;
261
+ --og-color-accent-deep: CanvasText;
262
+ --og-color-accent-fg: CanvasText;
263
+ --og-color-accent-soft: Canvas;
264
+ --og-color-status-queued: CanvasText;
265
+ --og-color-status-running: CanvasText;
266
+ --og-color-status-idle: CanvasText;
267
+ --og-color-status-waiting: CanvasText;
268
+ --og-color-status-failed: CanvasText;
269
+ --og-color-status-cancelled: CanvasText;
270
+ --og-color-danger: CanvasText;
271
+ --og-color-diff-add-bg: Canvas;
272
+ --og-color-diff-del-bg: Canvas;
273
+ --og-shadow-sm: none;
274
+ --og-shadow-md: none;
275
+ --og-shadow-lg: none;
276
+ --og-shadow-glow: none;
277
+ }
278
+
279
+ /* Selected fills are flattened by the OS palette. Preserve a visible,
280
+ non-color-only tab selection affordance in every high-contrast theme. */
281
+ .og-root [role="tab"][aria-selected="true"] {
282
+ outline: 2px solid Highlight;
283
+ outline-offset: -2px;
284
+ }
285
+ }
286
+
209
287
  /* ----------------------------------------------------------------------------
210
288
  xterm.js hardening (SandboxTerminal)
211
289
 
@@ -0,0 +1,2 @@
1
+ declare const stylesheet: string;
2
+ export default stylesheet;