@opengeni/react 1.0.1 → 2.3.0-canary.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 (174) hide show
  1. package/README.md +33 -10
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/chunk-5YK2ZDIG.js +116 -0
  8. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  9. package/dist/chunk-H5YGYNXS.js +329 -0
  10. package/dist/chunk-H5YGYNXS.js.map +1 -0
  11. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  12. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  13. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  14. package/dist/chunk-JOAVA2TW.js.map +1 -0
  15. package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
  16. package/dist/chunk-LS4POUTY.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
  24. package/dist/chunk-RSXJN73N.js.map +1 -0
  25. package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
  26. package/dist/chunk-SE4W4WQ3.js.map +1 -0
  27. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  28. package/dist/chunk-W6NFJXSA.js.map +1 -0
  29. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  30. package/dist/chunk-X4A66EWZ.js.map +1 -0
  31. package/dist/chunk-X4JAFD5R.js +3907 -0
  32. package/dist/chunk-X4JAFD5R.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +6 -3
  83. package/dist/realtime.js +36 -14
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +50 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +103 -59
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +215 -18
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +53 -17
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +529 -25
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +77 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
@@ -4,6 +4,7 @@ import {
4
4
  ChevronDownIcon,
5
5
  ChevronLeftIcon,
6
6
  ChevronRightIcon,
7
+ Globe2Icon,
7
8
  KeyRoundIcon,
8
9
  ZapIcon,
9
10
  } from "lucide-react";
@@ -20,7 +21,6 @@ import {
20
21
  import { cn } from "../lib/cn";
21
22
  import { usePortalTokenSource, usePortalTokenStyle } from "../lib/use-portal-token-style";
22
23
  import {
23
- coerceReasoningEffortForModel,
24
24
  effortOptionsForModel,
25
25
  findPickerRow,
26
26
  groupPickerRowsByBillingClass,
@@ -64,6 +64,7 @@ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
64
64
  codexOnly: "Codex-only session",
65
65
  billingHints: {
66
66
  opengeni_credits: "Will use credits",
67
+ external: "Provider terms and limits apply",
67
68
  codex_subscription: "ChatGPT / Codex plan",
68
69
  supergrok_subscription: "SuperGrok / xAI plan",
69
70
  byok: "Billed to your AI Gateway",
@@ -92,6 +93,8 @@ export type ModelPolicyPickerProps = {
92
93
  sessionKey?: string | undefined;
93
94
  /** Prefer bottom on new-chat surfaces and top for bottom-docked composers. */
94
95
  menuSide?: "top" | "bottom" | undefined;
96
+ /** Hide latency controls on surfaces whose saved policy does not include latency. */
97
+ allowLatencyMode?: boolean | undefined;
95
98
  /** Classes for the portalled menu surface. Prefer --og-* tokens for theming. */
96
99
  contentClassName?: string | undefined;
97
100
  /** Inline styles for the portalled menu, applied after inherited --og-* tokens. */
@@ -151,6 +154,7 @@ export function BillingClassMark(props: {
151
154
  }) {
152
155
  const labels: Record<PickerBillingClass, string> = {
153
156
  opengeni_credits: "OpenGeni",
157
+ external: "External provider",
154
158
  codex_subscription: "Codex",
155
159
  supergrok_subscription: "SuperGrok",
156
160
  byok: "Bring your own key",
@@ -173,6 +177,8 @@ export function BillingClassMark(props: {
173
177
  >
174
178
  {props.billingClass === "opengeni_credits" ? (
175
179
  <OpenGeniMark className={mark} />
180
+ ) : props.billingClass === "external" ? (
181
+ <Globe2Icon className={mark} aria-hidden />
176
182
  ) : props.billingClass === "codex_subscription" ? (
177
183
  <ChatGptMark className={mark} />
178
184
  ) : props.billingClass === "supergrok_subscription" ? (
@@ -354,17 +360,6 @@ export function ModelPolicyPickerMenu(
354
360
  const focusModel =
355
361
  effectiveNav.modelId === null ? undefined : findPickerRow(rows, effectiveNav.modelId);
356
362
  const selectedRow = findPickerRow(rows, props.model);
357
- const { latencyMode, loading, onLatencyModeChange } = props;
358
-
359
- useEffect(() => {
360
- if (loading || !selectedRow?.selectable) return;
361
- const supportsCurrentMode = runnableLatencyModesForModel(selectedRow.catalog).includes(
362
- latencyMode,
363
- );
364
- if (!supportsCurrentMode && latencyMode !== "standard") {
365
- onLatencyModeChange("standard");
366
- }
367
- }, [latencyMode, loading, onLatencyModeChange, selectedRow]);
368
363
 
369
364
  const go = (next: PickerNavState, nextDirection: 1 | -1) => {
370
365
  setDirection(nextDirection);
@@ -374,7 +369,7 @@ export function ModelPolicyPickerMenu(
374
369
  const selectEffort = (row: ClientPickerModelRow, effort: ReasoningEffort) => {
375
370
  if (!row.selectable) return;
376
371
  if (row.id !== props.model) props.onModelChange(row.id);
377
- props.onEffortChange(coerceReasoningEffortForModel(row.catalog, effort));
372
+ props.onEffortChange(effort);
378
373
  };
379
374
 
380
375
  const pageKey =
@@ -445,7 +440,7 @@ export function ModelPolicyPickerMenu(
445
440
  icon={<BillingClassMark billingClass={focusModel.billingClass} />}
446
441
  onBack={() => go({ level: "models", rail: focusModel.billingClass, modelId: null }, -1)}
447
442
  trailing={
448
- supportsFast ? (
443
+ supportsFast && props.allowLatencyMode !== false ? (
449
444
  <button
450
445
  type="button"
451
446
  disabled={!focusModel.selectable}
@@ -460,9 +455,6 @@ export function ModelPolicyPickerMenu(
460
455
  onClick={() => {
461
456
  if (!activeModel) {
462
457
  props.onModelChange(focusModel.id);
463
- props.onEffortChange(
464
- coerceReasoningEffortForModel(focusModel.catalog, props.effort),
465
- );
466
458
  }
467
459
  props.onLatencyModeChange(
468
460
  activeModel && props.latencyMode === "fast" ? "standard" : "fast",
@@ -25,7 +25,7 @@ type FileComponent = ComponentType<{
25
25
  }>;
26
26
 
27
27
  export type PierreFileProps = {
28
- /** Workspace-relative path (used for the header + language inference). */
28
+ /** Target path (used for the header + language inference). */
29
29
  path: string;
30
30
  /** The decoded text contents. */
31
31
  contents: string;
@@ -84,6 +84,10 @@ export function QueueSurface({
84
84
  const [replaceDraftFor, setReplaceDraftFor] = useState<string | null>(null);
85
85
  const [announcement, setAnnouncement] = useState("");
86
86
  const [draggedTurnId, setDraggedTurnId] = useState<string | null>(null);
87
+ const [pendingMove, setPendingMove] = useState<{
88
+ baseVersion: number | null;
89
+ turnIds: string[];
90
+ } | null>(null);
87
91
  const surface = usePortalTokenSource<HTMLDivElement>();
88
92
  const surfaceRef = surface.currentRef;
89
93
  const portalTokenStyle = usePortalTokenStyle(surface.source);
@@ -112,30 +116,56 @@ export function QueueSurface({
112
116
  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
113
117
 
114
118
  const displayedQueue = useMemo(() => {
115
- if (!keyboardDrag) return queue.queue;
116
- const oldIndex = queue.queue.findIndex((turn) => turn.id === keyboardDrag.turnId);
117
- if (oldIndex < 0) return queue.queue;
118
- return arrayMove(queue.queue, oldIndex, keyboardDrag.projectedIndex);
119
- }, [keyboardDrag, queue.queue]);
119
+ if (keyboardDrag) {
120
+ const oldIndex = queue.queue.findIndex((turn) => turn.id === keyboardDrag.turnId);
121
+ if (oldIndex < 0) return queue.queue;
122
+ return arrayMove(queue.queue, oldIndex, keyboardDrag.projectedIndex);
123
+ }
124
+ if (
125
+ !pendingMove ||
126
+ pendingMove.baseVersion === null ||
127
+ queue.snapshot?.version !== pendingMove.baseVersion
128
+ ) {
129
+ return queue.queue;
130
+ }
131
+ const byId = new Map(queue.queue.map((turn) => [turn.id, turn]));
132
+ const projected = pendingMove.turnIds.map((turnId) => byId.get(turnId));
133
+ // Never let an optimistic ordering projection omit or invent a row. Any
134
+ // concurrent authoritative membership change wins immediately.
135
+ if (projected.some((turn) => !turn) || projected.length !== queue.queue.length) {
136
+ return queue.queue;
137
+ }
138
+ return projected as SessionTurn[];
139
+ }, [keyboardDrag, pendingMove, queue.queue, queue.snapshot?.version]);
120
140
 
121
141
  const ids = useMemo(() => displayedQueue.map((turn) => turn.id), [displayedQueue]);
122
142
  const moveToIndex = useCallback(
123
143
  async (turnId: string, nextIndex: number): Promise<void> => {
144
+ if (queue.mutating || pendingMove) return;
124
145
  const oldIndex = queue.queue.findIndex((turn) => turn.id === turnId);
125
146
  if (oldIndex < 0) return;
126
147
  const boundedIndex = Math.max(0, Math.min(nextIndex, queue.queue.length - 1));
127
148
  if (oldIndex === boundedIndex) return;
128
149
  const ordered = arrayMove(queue.queue, oldIndex, boundedIndex);
129
150
  const beforeTurnId = ordered[boundedIndex + 1]?.id ?? null;
130
- const moved = await queue.moveTurn(turnId, beforeTurnId);
131
- setAnnouncement(
132
- moved
133
- ? `Queued prompt moved to position ${boundedIndex + 1}.`
134
- : "The queue changed before that prompt could be moved. Refreshed server order.",
135
- );
136
- if (moved) focusQueueTurn(surfaceRef.current, turnId);
151
+ setPendingMove({
152
+ baseVersion: queue.snapshot?.version ?? null,
153
+ turnIds: ordered.map((turn) => turn.id),
154
+ });
155
+ setAnnouncement(`Saving queued prompt at position ${boundedIndex + 1}.`);
156
+ try {
157
+ const moved = await queue.moveTurn(turnId, beforeTurnId);
158
+ setAnnouncement(
159
+ moved
160
+ ? `Queued prompt moved to position ${boundedIndex + 1}.`
161
+ : "The queue changed before that prompt could be moved. Refreshed server order.",
162
+ );
163
+ if (moved) focusQueueTurn(surfaceRef.current, turnId);
164
+ } finally {
165
+ setPendingMove(null);
166
+ }
137
167
  },
138
- [queue, surfaceRef],
168
+ [pendingMove, queue, surfaceRef],
139
169
  );
140
170
 
141
171
  const onDragEnd = useCallback(
@@ -211,11 +241,15 @@ export function QueueSurface({
211
241
  const edit = useCallback(
212
242
  async (turn: SessionTurn, replaceDraft: boolean) => {
213
243
  if (!composer || !canEditInComposer) return;
244
+ setAnnouncement("Moving queued prompt to the composer…");
214
245
  const restored = await queue.editTurn(turn.id, {
215
246
  expectedDraftRevision: composer.draftRevision,
216
247
  replaceDraft,
217
248
  });
218
- if (!restored) return;
249
+ if (!restored) {
250
+ setAnnouncement("That prompt changed before it could be moved to the composer.");
251
+ return;
252
+ }
219
253
  composer.applyDraft(restored);
220
254
  setReplaceDraftFor(null);
221
255
  setAnnouncement("Queued prompt moved back to the composer for editing.");
@@ -358,6 +392,7 @@ export function QueueSurface({
358
392
  turn.id === queue.pendingInputAttachment?.turnId ? attachedInputs : []
359
393
  }
360
394
  pending={queue.mutationFor(turn.id)}
395
+ reorderDisabled={queue.mutating || pendingMove !== null}
361
396
  confirmingReplace={replaceDraftFor === turn.id}
362
397
  keyboardDragging={keyboardDrag?.turnId === turn.id}
363
398
  portalTokenStyle={portalTokenStyle}
@@ -367,6 +402,7 @@ export function QueueSurface({
367
402
  onConfirmReplace={() => void edit(turn, true)}
368
403
  onCancelReplace={() => setReplaceDraftFor(null)}
369
404
  onSteer={() => {
405
+ setAnnouncement("Requesting this queued prompt as the next direction…");
370
406
  void queue.steerTurn(turn.id).then((steered) => {
371
407
  setAnnouncement(
372
408
  steered
@@ -379,6 +415,7 @@ export function QueueSurface({
379
415
  });
380
416
  }}
381
417
  onDelete={() => {
418
+ setAnnouncement("Deleting queued prompt…");
382
419
  void queue.removeTurn(turn.id).then((removed) => {
383
420
  setAnnouncement(
384
421
  removed
@@ -930,6 +967,7 @@ function SortableQueueRow({
930
967
  count,
931
968
  attachedInputs,
932
969
  pending,
970
+ reorderDisabled,
933
971
  confirmingReplace,
934
972
  keyboardDragging,
935
973
  portalTokenStyle,
@@ -947,6 +985,7 @@ function SortableQueueRow({
947
985
  count: number;
948
986
  attachedInputs: SessionPendingInputPreview[];
949
987
  pending: QueueMutationKind | null;
988
+ reorderDisabled: boolean;
950
989
  confirmingReplace: boolean;
951
990
  keyboardDragging: boolean;
952
991
  portalTokenStyle: PortalTokenStyle;
@@ -961,7 +1000,7 @@ function SortableQueueRow({
961
1000
  }) {
962
1001
  const sortable = useSortable({
963
1002
  id: turn.id,
964
- disabled: pending !== null || keyboardDragging,
1003
+ disabled: reorderDisabled || keyboardDragging,
965
1004
  });
966
1005
  return (
967
1006
  <li
@@ -981,7 +1020,7 @@ function SortableQueueRow({
981
1020
  {...sortable.attributes}
982
1021
  {...sortable.listeners}
983
1022
  onKeyDown={onHandleKeyDown}
984
- disabled={pending !== null}
1023
+ disabled={reorderDisabled}
985
1024
  className="col-start-1 row-start-1 mt-0.5 inline-flex size-7 shrink-0 touch-none items-center justify-center rounded-md text-fg-subtle hover:bg-surface-2 hover:text-fg focus-visible:ring-2 focus-visible:ring-ring/40 pointer-coarse:size-[44px]"
986
1025
  aria-label={`Reorder queued prompt ${index + 1}`}
987
1026
  title="Drag to reorder. Press Space, arrows, then Space to drop."
@@ -1015,6 +1054,20 @@ function SortableQueueRow({
1015
1054
  <span className="min-w-0 truncate">{turn.model}</span>
1016
1055
  <span className="shrink-0">{turn.reasoningEffort}</span>
1017
1056
  </div>
1057
+ {pending ? (
1058
+ <div
1059
+ role="status"
1060
+ aria-live="polite"
1061
+ className="mt-1 flex items-center gap-1.5 text-og-xs font-medium text-status-waiting"
1062
+ data-testid={`queue-mutation-${pending}`}
1063
+ >
1064
+ <Loader2Icon
1065
+ aria-hidden="true"
1066
+ className="size-3 shrink-0 animate-spin motion-reduce:animate-none"
1067
+ />
1068
+ {queueMutationPendingLabel(pending)}
1069
+ </div>
1070
+ ) : null}
1018
1071
  </div>
1019
1072
  <div className="col-span-full row-start-3 flex min-w-0 items-start justify-end gap-1.5 sm:col-span-1 sm:col-start-4 sm:row-start-1 sm:gap-2">
1020
1073
  <button
@@ -1142,6 +1195,19 @@ function SortableQueueRow({
1142
1195
  );
1143
1196
  }
1144
1197
 
1198
+ function queueMutationPendingLabel(kind: QueueMutationKind): string {
1199
+ switch (kind) {
1200
+ case "move":
1201
+ return "Saving new position…";
1202
+ case "edit":
1203
+ return "Moving to composer…";
1204
+ case "steer":
1205
+ return "Changing direction…";
1206
+ case "delete":
1207
+ return "Deleting…";
1208
+ }
1209
+ }
1210
+
1145
1211
  const verticalOnly: Modifier = ({ transform }) => ({ ...transform, x: 0 });
1146
1212
 
1147
1213
  function focusQueueTurn(surface: HTMLElement | null, turnId: string): void {
@@ -2,6 +2,7 @@ import { FileCode2Icon, FileWarningIcon, LoaderCircleIcon } from "lucide-react";
2
2
  import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
3
3
  import { Group, Panel, Separator } from "react-resizable-panels";
4
4
  import { cn } from "../lib/cn";
5
+ import { prefersReducedMotion } from "../lib/motion";
5
6
  import { useThemeType } from "../lib/use-theme-type";
6
7
  import {
7
8
  CapturedFileUnavailableError,
@@ -32,8 +33,14 @@ export type SandboxFilesProps = {
32
33
  * dock warms the box on this so the save lands fast; opening/reading never fires
33
34
  * it. Browsing the tree/diff must not warm a box. */
34
35
  onEditIntent?: (() => void) | undefined;
35
- /** A guarded diff path routed here by the parent workspace. */
36
+ /** Restore the file last viewed in this task. Invalid/missing paths still
37
+ * surface the ordinary file-view error instead of changing files silently. */
38
+ initialSelectedPath?: string | null | undefined;
39
+ onSelectedPathChange?: ((path: string | null) => void) | undefined;
40
+ /** A deliberate file-open path routed here by the parent workspace. */
36
41
  requestedPath?: string | undefined;
42
+ /** 1-based line to reveal after `requestedPath` opens. */
43
+ requestedLine?: number | null | undefined;
37
44
  /** Identity for one guarded-file request. Increment this when the same path is
38
45
  * deliberately requested again; it also lets a pending request be consumed
39
46
  * without overriding later manual tree navigation. Defaults to the path. */
@@ -67,7 +74,10 @@ export function SandboxFiles({
67
74
  usePierre = true,
68
75
  editable = true,
69
76
  onEditIntent,
77
+ initialSelectedPath,
78
+ onSelectedPathChange,
70
79
  requestedPath,
80
+ requestedLine,
71
81
  requestedPathRequestId,
72
82
  requestedPathReady = true,
73
83
  workspaceResting = false,
@@ -77,12 +87,18 @@ export function SandboxFiles({
77
87
  themeType,
78
88
  className,
79
89
  }: SandboxFilesProps) {
80
- const [selected, setSelected] = useState<string | null>(null);
90
+ const [selected, setSelected] = useState<string | null>(() => initialSelectedPath ?? null);
81
91
  // View vs Edit for the selected file. Resets to View on every new selection so
82
92
  // opening a file never lands you in a stale dirty editor for a different path.
83
93
  const [editMode, setEditMode] = useState(false);
94
+ const [focusLine, setFocusLine] = useState<number | null>(null);
95
+ const [treeRevealRequest, setTreeRevealRequest] = useState<{
96
+ path: string;
97
+ requestId: string | number;
98
+ } | null>(null);
84
99
  const [liveRequestedPath, setLiveRequestedPath] = useState<string | null>(null);
85
100
  const [viewReloadRevision, setViewReloadRevision] = useState(0);
101
+ const viewerScrollRef = useRef<HTMLDivElement | null>(null);
86
102
  const pendingRequestRef = useRef<string | number | null>(null);
87
103
  const handledRequestRef = useRef<string | number | null>(null);
88
104
  const requestKey = requestedPath ? (requestedPathRequestId ?? requestedPath) : null;
@@ -91,6 +107,7 @@ export function SandboxFiles({
91
107
  if (!requestedPath || requestKey === null) {
92
108
  pendingRequestRef.current = null;
93
109
  handledRequestRef.current = null;
110
+ setTreeRevealRequest(null);
94
111
  return;
95
112
  }
96
113
  if (handledRequestRef.current === requestKey) {
@@ -98,13 +115,18 @@ export function SandboxFiles({
98
115
  }
99
116
  if (!requestedPathReady) {
100
117
  pendingRequestRef.current = requestKey;
118
+ setTreeRevealRequest(null);
101
119
  return;
102
120
  }
103
121
  handledRequestRef.current = requestKey;
104
122
  pendingRequestRef.current = null;
105
123
  setSelected(requestedPath);
124
+ onSelectedPathChange?.(requestedPath);
106
125
  setEditMode(false);
107
- }, [requestKey, requestedPath, requestedPathReady]);
126
+ setFocusLine(requestedLine != null && requestedLine > 0 ? requestedLine : null);
127
+ setTreeRevealRequest({ path: requestedPath, requestId: requestKey });
128
+ setViewReloadRevision((revision) => revision + 1);
129
+ }, [onSelectedPathChange, requestKey, requestedLine, requestedPath, requestedPathReady]);
108
130
 
109
131
  // Side-by-side (tree left, viewer right) once the surface is wide enough;
110
132
  // stacked (tree over viewer) on a narrow dock. Tracked off the container so it
@@ -147,23 +169,35 @@ export function SandboxFiles({
147
169
  const fileView = useFileView(
148
170
  viewPath,
149
171
  files.readFile,
150
- (files.contentRevision ?? 0) + viewReloadRevision,
172
+ `${files.contentRevision ?? 0}:${viewReloadRevision}`,
151
173
  );
152
174
 
175
+ useEffect(() => {
176
+ if (focusLine === null && viewReloadRevision > 0 && viewerScrollRef.current) {
177
+ viewerScrollRef.current.scrollTop = 0;
178
+ }
179
+ }, [focusLine, viewReloadRevision]);
180
+
153
181
  // Selecting a (different) file always returns to View — never drop the user into
154
182
  // an editor whose buffer belongs to the previously-selected path. Manual
155
183
  // navigation also consumes a pending guarded-file request: a late cold→warm
156
184
  // transition must never pull the user away from the file they chose meanwhile.
157
- const selectFile = useCallback((path: string) => {
158
- if (pendingRequestRef.current !== null) {
159
- handledRequestRef.current = pendingRequestRef.current;
160
- pendingRequestRef.current = null;
161
- }
162
- setSelected(path);
163
- setEditMode(false);
164
- setLiveRequestedPath(null);
165
- setViewReloadRevision(0);
166
- }, []);
185
+ const selectFile = useCallback(
186
+ (path: string) => {
187
+ if (pendingRequestRef.current !== null) {
188
+ handledRequestRef.current = pendingRequestRef.current;
189
+ pendingRequestRef.current = null;
190
+ }
191
+ setSelected(path);
192
+ onSelectedPathChange?.(path);
193
+ setEditMode(false);
194
+ setFocusLine(null);
195
+ setTreeRevealRequest(null);
196
+ setLiveRequestedPath(null);
197
+ setViewReloadRevision(0);
198
+ },
199
+ [onSelectedPathChange],
200
+ );
167
201
 
168
202
  // A tree file is editable only when it is a real, fully-loaded text file: not
169
203
  // binary (would corrupt on save) and not truncated (we only hold a PREFIX). The
@@ -264,6 +298,12 @@ export function SandboxFiles({
264
298
  <FileBrowser
265
299
  result={files}
266
300
  selectedPath={selected ?? undefined}
301
+ {...(treeRevealRequest
302
+ ? {
303
+ revealPath: treeRevealRequest.path,
304
+ revealPathRequestId: treeRevealRequest.requestId,
305
+ }
306
+ : {})}
267
307
  onSelectFile={selectFile}
268
308
  editable={editable}
269
309
  emptyState="This directory is empty"
@@ -295,7 +335,11 @@ export function SandboxFiles({
295
335
  data-opengeni-selected-file
296
336
  className="min-w-0 truncate font-og-mono text-og-xs text-og-fg-muted"
297
337
  >
298
- {selected ?? "No file selected"}
338
+ {selected
339
+ ? focusLine !== null
340
+ ? `${selected}:${focusLine}`
341
+ : selected
342
+ : "No file selected"}
299
343
  </span>
300
344
  {/* View/Edit toggle — only for a real, fully-loaded text file the editor
301
345
  can safely round-trip. Binary/truncated/read-only files never get an
@@ -311,7 +355,11 @@ export function SandboxFiles({
311
355
  />
312
356
  )}
313
357
  </div>
314
- <div className="min-h-0 flex-1 overflow-auto">
358
+ <div
359
+ ref={viewerScrollRef}
360
+ className="min-h-0 flex-1 overflow-auto"
361
+ data-opengeni-file-viewer-scroll
362
+ >
315
363
  {viewPath ? (
316
364
  showEditor && fileView.content !== null ? (
317
365
  <CodeEditor
@@ -379,8 +427,9 @@ export function SandboxFiles({
379
427
  loaded). Editing is disabled to avoid corrupting the file.
380
428
  </div>
381
429
  )}
382
- {usePierre ? (
430
+ {usePierre && focusLine === null ? (
383
431
  <PierreFile
432
+ key={`${viewPath}:${viewReloadRevision}`}
384
433
  path={viewPath}
385
434
  contents={fileView.content}
386
435
  themeType={resolvedTheme}
@@ -391,6 +440,12 @@ export function SandboxFiles({
391
440
  }
392
441
  className="p-1"
393
442
  />
443
+ ) : focusLine !== null ? (
444
+ <LineNumberedFile
445
+ path={viewPath}
446
+ contents={fileView.content}
447
+ focusLine={focusLine}
448
+ />
394
449
  ) : (
395
450
  <pre className="overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg">
396
451
  {fileView.content}
@@ -438,6 +493,65 @@ function WakeButton({ children, onClick }: { children: ReactNode; onClick: () =>
438
493
  );
439
494
  }
440
495
 
496
+ function LineNumberedFile({
497
+ path,
498
+ contents,
499
+ focusLine,
500
+ }: {
501
+ path: string;
502
+ contents: string;
503
+ focusLine: number;
504
+ }) {
505
+ const lines = contents.split("\n");
506
+ const inRange = focusLine >= 1 && focusLine <= lines.length;
507
+ const targetRef = useRef<HTMLDivElement | null>(null);
508
+
509
+ useEffect(() => {
510
+ targetRef.current?.scrollIntoView({
511
+ block: "center",
512
+ behavior: prefersReducedMotion() ? "auto" : "smooth",
513
+ });
514
+ }, [path, focusLine, contents]);
515
+
516
+ return (
517
+ <div
518
+ className="h-full min-h-0 overflow-auto font-og-mono text-og-sm text-og-fg"
519
+ role="region"
520
+ aria-label={inRange ? `${path} at line ${focusLine}` : path}
521
+ >
522
+ {!inRange ? (
523
+ <p
524
+ className="border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-fg-muted"
525
+ role="status"
526
+ >
527
+ Line {focusLine} is past the end of this file.
528
+ </p>
529
+ ) : null}
530
+ <div>
531
+ {lines.map((text, index) => {
532
+ const lineNumber = index + 1;
533
+ const focused = inRange && lineNumber === focusLine;
534
+ return (
535
+ <div
536
+ key={lineNumber}
537
+ ref={focused ? targetRef : undefined}
538
+ data-opengeni-file-line={lineNumber}
539
+ {...(focused ? { "data-opengeni-focus-line": "" } : {})}
540
+ aria-current={focused ? "location" : undefined}
541
+ className={cn("flex gap-3 px-2 py-0.5 leading-6", focused && "bg-og-accent-soft")}
542
+ >
543
+ <span className="w-11 shrink-0 select-none text-right tabular-nums text-og-fg-subtle">
544
+ {lineNumber}
545
+ </span>
546
+ <pre className="min-w-0 flex-1 whitespace-pre-wrap break-words">{text || " "}</pre>
547
+ </div>
548
+ );
549
+ })}
550
+ </div>
551
+ </div>
552
+ );
553
+ }
554
+
441
555
  function GitHeader({ git, loading }: { git: SandboxFilesGitSummary; loading: boolean }) {
442
556
  if (loading) {
443
557
  return (
@@ -542,7 +656,7 @@ type FileViewState = {
542
656
  function useFileView(
543
657
  path: string | null,
544
658
  readFile: UseSandboxFilesResult["readFile"],
545
- reloadRevision = 0,
659
+ reloadRevision: string | number = 0,
546
660
  ): FileViewState {
547
661
  const previousPathRef = useRef<string | null>(null);
548
662
  const [state, setState] = useState<FileViewState>({
@@ -671,7 +785,9 @@ function Notice({
671
785
  </span>
672
786
  ) : null}
673
787
  {title ? <p className="font-medium text-og-fg">{title}</p> : null}
674
- <div className="leading-5">{children}</div>
788
+ <div data-contrast-audited className="leading-5">
789
+ {children}
790
+ </div>
675
791
  </div>
676
792
  </div>
677
793
  );