@opengeni/react 0.36.1 → 0.40.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 (85) hide show
  1. package/README.md +92 -2
  2. package/dist/{chunk-J2RVJ6JX.js → chunk-4IJCL7YO.js} +13 -2
  3. package/dist/chunk-4IJCL7YO.js.map +1 -0
  4. package/dist/{chunk-7CJOAW3V.js → chunk-EB67J347.js} +1 -135
  5. package/dist/chunk-EB67J347.js.map +1 -0
  6. package/dist/chunk-EKZH6IDG.js +141 -0
  7. package/dist/chunk-EKZH6IDG.js.map +1 -0
  8. package/dist/{chunk-OKKMZDQF.js → chunk-JALF5FI3.js} +9 -7
  9. package/dist/{chunk-OKKMZDQF.js.map → chunk-JALF5FI3.js.map} +1 -1
  10. package/dist/{chunk-Z5FV355Z.js → chunk-KR2SK5GJ.js} +1382 -724
  11. package/dist/chunk-KR2SK5GJ.js.map +1 -0
  12. package/dist/{chunk-N4XEBE23.js → chunk-OMCFRWHL.js} +22 -13
  13. package/dist/{chunk-N4XEBE23.js.map → chunk-OMCFRWHL.js.map} +1 -1
  14. package/dist/{chunk-FT2TXNGZ.js → chunk-Q2NCKWTK.js} +183 -18
  15. package/dist/chunk-Q2NCKWTK.js.map +1 -0
  16. package/dist/{chunk-22RM4GMO.js → chunk-SJKT4TKW.js} +26 -2
  17. package/dist/chunk-SJKT4TKW.js.map +1 -0
  18. package/dist/{chunk-ALA3UGWB.js → chunk-SXIQK54Z.js} +8 -6
  19. package/dist/{chunk-ALA3UGWB.js.map → chunk-SXIQK54Z.js.map} +1 -1
  20. package/dist/chunk-WZT5G5OR.js +433 -0
  21. package/dist/chunk-WZT5G5OR.js.map +1 -0
  22. package/dist/client.d.ts +2 -0
  23. package/dist/components/chat-composer.d.ts +7 -1
  24. package/dist/components/human-input-form.d.ts +16 -6
  25. package/dist/components/human-input-surface.d.ts +19 -0
  26. package/dist/components/machine-input-display.d.ts +12 -0
  27. package/dist/components/message-timeline.d.ts +6 -1
  28. package/dist/components/model-policy-picker.d.ts +78 -0
  29. package/dist/components/session-chrome.d.ts +2 -1
  30. package/dist/components/tip-follow.d.ts +30 -0
  31. package/dist/composer.d.ts +2 -0
  32. package/dist/composer.js +22 -5
  33. package/dist/hooks/use-composer.d.ts +10 -1
  34. package/dist/index.d.ts +7 -1
  35. package/dist/index.js +2261 -2230
  36. package/dist/index.js.map +1 -1
  37. package/dist/machines.js +4 -3
  38. package/dist/model-policy.d.ts +12 -4
  39. package/dist/model-policy.js +5 -1
  40. package/dist/realtime/dropdown-menu.d.ts +16 -0
  41. package/dist/realtime/realtime-control.d.ts +126 -0
  42. package/dist/realtime.d.ts +11 -0
  43. package/dist/realtime.js +1023 -0
  44. package/dist/realtime.js.map +1 -0
  45. package/dist/session-context.d.ts +7 -1
  46. package/dist/session-ui.d.ts +2 -0
  47. package/dist/session-ui.js +6 -4
  48. package/dist/session.js +5 -4
  49. package/dist/timeline/compute-label.d.ts +2 -0
  50. package/dist/timeline/index.d.ts +1 -0
  51. package/dist/timeline/types.d.ts +5 -0
  52. package/package.json +7 -2
  53. package/src/client.ts +13 -0
  54. package/src/components/chat-composer.tsx +19 -5
  55. package/src/components/composer-transcription-control.tsx +2 -2
  56. package/src/components/composer.tsx +14 -5
  57. package/src/components/human-input-form.tsx +506 -186
  58. package/src/components/human-input-surface.tsx +80 -0
  59. package/src/components/machine-input-display.ts +83 -0
  60. package/src/components/message-timeline.tsx +348 -308
  61. package/src/components/model-policy-picker.tsx +587 -0
  62. package/src/components/sandbox-files.tsx +1 -1
  63. package/src/components/session-chrome.tsx +141 -15
  64. package/src/components/tip-follow.ts +127 -6
  65. package/src/composer.ts +13 -0
  66. package/src/hooks/use-composer.ts +229 -29
  67. package/src/index.ts +19 -0
  68. package/src/model-policy.ts +51 -7
  69. package/src/realtime/dropdown-menu.tsx +123 -0
  70. package/src/realtime/realtime-control.tsx +1178 -0
  71. package/src/realtime.ts +27 -0
  72. package/src/session-context.ts +16 -0
  73. package/src/session-ui.ts +2 -0
  74. package/src/timeline/compute-label.tsx +18 -0
  75. package/src/timeline/index.ts +3 -0
  76. package/src/timeline/projection.ts +21 -1
  77. package/src/timeline/tool-renderers.tsx +232 -43
  78. package/src/timeline/types.ts +5 -0
  79. package/styles/index.css +61 -0
  80. package/styles/tokens.css +2 -0
  81. package/dist/chunk-22RM4GMO.js.map +0 -1
  82. package/dist/chunk-7CJOAW3V.js.map +0 -1
  83. package/dist/chunk-FT2TXNGZ.js.map +0 -1
  84. package/dist/chunk-J2RVJ6JX.js.map +0 -1
  85. package/dist/chunk-Z5FV355Z.js.map +0 -1
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  healthPulses,
26
26
  pointsFor,
27
27
  useMachines
28
- } from "./chunk-ALA3UGWB.js";
28
+ } from "./chunk-SXIQK54Z.js";
29
29
  import {
30
30
  SESSION_EVENT_BROWSER_MAX_BYTES,
31
31
  SESSION_EVENT_BROWSER_MAX_COUNT,
@@ -52,7 +52,7 @@ import {
52
52
  useSessionLineage,
53
53
  useSessionMcpApprovalPolicy,
54
54
  useTurnQueue
55
- } from "./chunk-OKKMZDQF.js";
55
+ } from "./chunk-JALF5FI3.js";
56
56
  import {
57
57
  ActivityDisclosure,
58
58
  ActivityRail,
@@ -62,6 +62,7 @@ import {
62
62
  CopyHoverFrame,
63
63
  DisclosureDefaultsProvider,
64
64
  HumanInputForm,
65
+ HumanInputSurface,
65
66
  LightboxProvider,
66
67
  Markdown,
67
68
  MediaEmpty,
@@ -106,7 +107,8 @@ import {
106
107
  useLightboxOptional,
107
108
  useThemeType,
108
109
  v4aToGitFileDiff
109
- } from "./chunk-Z5FV355Z.js";
110
+ } from "./chunk-KR2SK5GJ.js";
111
+ import "./chunk-YDNWMKXO.js";
110
112
  import {
111
113
  buildTimeline,
112
114
  creditExhaustedFromEvents,
@@ -114,7 +116,7 @@ import {
114
116
  groupTimeline,
115
117
  sessionStatusFromEvents,
116
118
  toolDisplayName
117
- } from "./chunk-J2RVJ6JX.js";
119
+ } from "./chunk-4IJCL7YO.js";
118
120
  import {
119
121
  Actions,
120
122
  AttachButton,
@@ -154,21 +156,32 @@ import {
154
156
  useSlashCommands,
155
157
  useTranscription,
156
158
  useVoiceInput
157
- } from "./chunk-N4XEBE23.js";
159
+ } from "./chunk-OMCFRWHL.js";
158
160
  import {
159
161
  FILE_ONLY_MESSAGE_TEXT,
160
162
  composeSendInput,
161
163
  shouldSteerOnKey,
162
164
  shouldSubmitOnKey,
163
165
  useComposer
164
- } from "./chunk-FT2TXNGZ.js";
166
+ } from "./chunk-Q2NCKWTK.js";
165
167
  import {
166
168
  useMutationRunner,
167
- useOpenGeni,
168
- useOpenGeniClient,
169
169
  usePolledValue,
170
170
  useSessionEventTrigger
171
- } from "./chunk-7CJOAW3V.js";
171
+ } from "./chunk-EB67J347.js";
172
+ import {
173
+ BillingClassMark,
174
+ ModelPolicyPicker,
175
+ ModelPolicyPickerMenu,
176
+ PickerAnimatedPage,
177
+ PickerBackHeader,
178
+ PickerNavRow,
179
+ defaultModelPolicyPickerMessages
180
+ } from "./chunk-WZT5G5OR.js";
181
+ import {
182
+ useOpenGeni,
183
+ useOpenGeniClient
184
+ } from "./chunk-EKZH6IDG.js";
172
185
  import {
173
186
  advancedSourceSummary,
174
187
  availabilityReasonLabel,
@@ -180,11 +193,13 @@ import {
180
193
  findPickerRow,
181
194
  groupPickerRowsByBillingClass,
182
195
  labelLatencyMode,
196
+ labelReasoningEffort,
183
197
  payerSummaryForModel,
198
+ projectClientModelRows,
184
199
  projectPickerRows,
185
200
  runnableLatencyModesForModel,
186
201
  sortPickerRows
187
- } from "./chunk-22RM4GMO.js";
202
+ } from "./chunk-SJKT4TKW.js";
188
203
  import {
189
204
  Tooltip,
190
205
  TooltipContent,
@@ -192,9 +207,6 @@ import {
192
207
  TooltipTrigger,
193
208
  usePortalTokenStyle
194
209
  } from "./chunk-ATSTR5P3.js";
195
- import {
196
- cn
197
- } from "./chunk-TK7G6XLT.js";
198
210
  import {
199
211
  COMPOSER_PAYMENT_REQUIRED_MESSAGE,
200
212
  CREDIT_EXHAUSTION_MESSAGE,
@@ -208,7 +220,9 @@ import {
208
220
  truncate,
209
221
  tryParseJson
210
222
  } from "./chunk-LAKLF4PA.js";
211
- import "./chunk-YDNWMKXO.js";
223
+ import {
224
+ cn
225
+ } from "./chunk-TK7G6XLT.js";
212
226
 
213
227
  // src/hooks/use-last-started-turn-policy.ts
214
228
  import { useCallback, useEffect } from "react";
@@ -3324,7 +3338,10 @@ function ChatComposer({
3324
3338
  disabled,
3325
3339
  autoFocus,
3326
3340
  hint,
3341
+ controlsLeading,
3327
3342
  controlsStart,
3343
+ actionsStart,
3344
+ attachButtonClassName,
3328
3345
  transcription,
3329
3346
  header,
3330
3347
  onPaste,
@@ -3354,7 +3371,10 @@ function ChatComposer({
3354
3371
  onPaste,
3355
3372
  messages
3356
3373
  });
3357
- const hasControls = Boolean(attachments || models || controlsStart || transcription);
3374
+ const hasControls = Boolean(
3375
+ attachments || models || controlsLeading || controlsStart || transcription
3376
+ );
3377
+ const stackActions = hasControls && Boolean(actionsStart);
3358
3378
  return /* @__PURE__ */ jsxs2(Root, { controller, className, children: [
3359
3379
  /* @__PURE__ */ jsxs2(Frame, { children: [
3360
3380
  /* @__PURE__ */ jsx2(CommandPalette2, {}),
@@ -3364,14 +3384,16 @@ function ChatComposer({
3364
3384
  /* @__PURE__ */ jsx2(Attachments, {}),
3365
3385
  header,
3366
3386
  /* @__PURE__ */ jsx2(Input, { placeholder, autoFocus }),
3367
- controller.confirmState ? /* @__PURE__ */ jsx2(Confirmation, {}) : /* @__PURE__ */ jsxs2(Footer, { children: [
3368
- hasControls ? /* @__PURE__ */ jsxs2(Controls, { children: [
3369
- /* @__PURE__ */ jsx2(AttachButton, {}),
3387
+ controller.confirmState ? /* @__PURE__ */ jsx2(Confirmation, {}) : /* @__PURE__ */ jsxs2(Footer, { className: stackActions ? "flex-wrap" : void 0, children: [
3388
+ hasControls ? /* @__PURE__ */ jsxs2(Controls, { className: stackActions ? "w-full sm:w-auto" : void 0, children: [
3389
+ controlsLeading,
3390
+ /* @__PURE__ */ jsx2(AttachButton, { className: attachButtonClassName }),
3370
3391
  transcription ? /* @__PURE__ */ jsx2(ComposerTranscriptionControl, { ...transcription }) : null,
3371
3392
  models ? /* @__PURE__ */ jsx2(ModelPicker2, { models, value: selectedModel, onChange: onSelectModel }) : null,
3372
3393
  controlsStart
3373
3394
  ] }) : /* @__PURE__ */ jsx2(Hint, { children: hint }),
3374
- /* @__PURE__ */ jsxs2(Actions, { children: [
3395
+ /* @__PURE__ */ jsxs2(Actions, { className: stackActions ? "w-full justify-end sm:w-auto" : void 0, children: [
3396
+ actionsStart,
3375
3397
  /* @__PURE__ */ jsx2(PauseButton, {}),
3376
3398
  /* @__PURE__ */ jsx2(SendButton, {})
3377
3399
  ] })
@@ -5100,2414 +5122,2412 @@ function ContextMenu({
5100
5122
  );
5101
5123
  }
5102
5124
 
5103
- // src/components/diff-view.tsx
5104
- import { jsx as jsx6 } from "react/jsx-runtime";
5105
- function DiffView({
5106
- diff,
5107
- fallback,
5108
- layout = "unified",
5109
- themeType,
5110
- isRepo = true,
5111
- emptyState,
5112
- className
5113
- }) {
5114
- if (diff.length === 0) {
5115
- return /* @__PURE__ */ jsx6("div", { className: cn("p-3 text-og-sm text-og-fg-subtle", className), children: emptyState ?? (isRepo ? "No changes" : "No repository mounted") });
5116
- }
5117
- return /* @__PURE__ */ jsx6(
5118
- PierreDiff,
5119
- {
5120
- diff,
5121
- layout,
5122
- themeType,
5123
- ...fallback !== void 0 ? { fallback } : {},
5124
- className
5125
- }
5126
- );
5127
- }
5128
-
5129
- // src/components/pierre-file.tsx
5125
+ // src/components/workbench-changes.tsx
5126
+ import { ArrowUpRightIcon, FileWarningIcon, HistoryIcon } from "lucide-react";
5130
5127
  import {
5131
- lazy,
5132
- Suspense,
5133
- useEffect as useEffect16,
5134
- useState as useState13
5128
+ useCallback as useCallback18,
5129
+ useEffect as useEffect17,
5130
+ useId as useId3,
5131
+ useLayoutEffect as useLayoutEffect2,
5132
+ useMemo as useMemo9,
5133
+ useRef as useRef15,
5134
+ useState as useState14
5135
5135
  } from "react";
5136
- import { jsx as jsx7 } from "react/jsx-runtime";
5137
- var LazyFile = lazy(async () => {
5138
- const mod = await import("@pierre/diffs/react");
5139
- return { default: mod.File };
5140
- });
5141
- function PierreFile({
5142
- path,
5143
- contents,
5144
- themeType,
5145
- theme,
5146
- disableWorkerPool,
5147
- loading,
5148
- fallback,
5149
- className
5150
- }) {
5151
- const [failed, setFailed] = useState13(false);
5136
+ import { VList as VList2 } from "virtua";
5137
+
5138
+ // src/hooks/use-windowed-sections.ts
5139
+ import { useCallback as useCallback17, useEffect as useEffect16, useMemo as useMemo8, useRef as useRef14, useState as useState13 } from "react";
5140
+ function computeWindowRange(heights, scrollTop, viewport, overscan) {
5141
+ const n = heights.length;
5142
+ if (n === 0) return { start: 0, end: 0 };
5143
+ const top = Math.max(scrollTop, 0);
5144
+ const bottom = top + Math.max(viewport, 0);
5145
+ let firstVisible = -1;
5146
+ let lastVisible = -1;
5147
+ let acc = 0;
5148
+ let anchor = 0;
5149
+ for (let i = 0; i < n; i++) {
5150
+ const secTop = acc;
5151
+ const secBottom = secTop + Math.max(heights[i] ?? 0, 0);
5152
+ if (secTop <= top) anchor = i;
5153
+ if (secBottom > top && secTop < bottom) {
5154
+ if (firstVisible === -1) firstVisible = i;
5155
+ lastVisible = i;
5156
+ }
5157
+ acc = secBottom;
5158
+ }
5159
+ if (firstVisible === -1) {
5160
+ firstVisible = anchor;
5161
+ lastVisible = anchor;
5162
+ }
5163
+ return {
5164
+ start: Math.max(0, firstVisible - overscan),
5165
+ end: Math.min(n, lastVisible + 1 + overscan)
5166
+ };
5167
+ }
5168
+ function useWindowedSections(opts) {
5169
+ const { count, estimateHeight, overscan = 3 } = opts;
5170
+ const scrollRef = useRef14(null);
5171
+ const [heights, setHeights] = useState13(
5172
+ () => Array.from({ length: count }, (_, i) => estimateHeight(i))
5173
+ );
5152
5174
  useEffect16(() => {
5153
- let cancelled = false;
5154
- void import("@pierre/diffs/react").catch(() => {
5155
- if (!cancelled) setFailed(true);
5175
+ setHeights((prev) => {
5176
+ if (prev.length === count) return prev;
5177
+ return Array.from({ length: count }, (_, i) => prev[i] ?? estimateHeight(i));
5156
5178
  });
5179
+ }, [count]);
5180
+ const heightsRef = useRef14(heights);
5181
+ heightsRef.current = heights;
5182
+ const [range, setRange] = useState13(() => ({
5183
+ start: 0,
5184
+ end: Math.min(count, overscan + 1)
5185
+ }));
5186
+ const recompute = useCallback17(() => {
5187
+ const el = scrollRef.current;
5188
+ if (!el) return;
5189
+ const next = computeWindowRange(heightsRef.current, el.scrollTop, el.clientHeight, overscan);
5190
+ setRange((prev) => prev.start === next.start && prev.end === next.end ? prev : next);
5191
+ }, [overscan]);
5192
+ useEffect16(() => {
5193
+ const el = scrollRef.current;
5194
+ if (!el) return;
5195
+ let raf = 0;
5196
+ const onScroll = () => {
5197
+ if (raf) return;
5198
+ raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame(() => {
5199
+ raf = 0;
5200
+ recompute();
5201
+ }) : (recompute(), 0);
5202
+ };
5203
+ el.addEventListener("scroll", onScroll, { passive: true });
5204
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => recompute()) : null;
5205
+ ro?.observe(el);
5206
+ recompute();
5157
5207
  return () => {
5158
- cancelled = true;
5208
+ el.removeEventListener("scroll", onScroll);
5209
+ ro?.disconnect();
5210
+ if (raf && typeof cancelAnimationFrame === "function") cancelAnimationFrame(raf);
5159
5211
  };
5212
+ }, [recompute, count]);
5213
+ const measure = useCallback17((index, height) => {
5214
+ if (height <= 0) return;
5215
+ setHeights((prev) => {
5216
+ if (Math.abs((prev[index] ?? 0) - height) < 1) return prev;
5217
+ const next = prev.slice();
5218
+ next[index] = height;
5219
+ return next;
5220
+ });
5160
5221
  }, []);
5161
- const name = path.split("/").filter(Boolean).pop() ?? path;
5162
- if (failed) {
5163
- return /* @__PURE__ */ jsx7("div", { className, children: fallback ?? /* @__PURE__ */ jsx7(PlainFile, { name, contents }) });
5164
- }
5165
- const options = {
5166
- overflow: "scroll",
5167
- stickyHeader: true,
5168
- showLineNumbers: true,
5169
- ...theme ? { theme } : {
5170
- theme: {
5171
- dark: "github-dark-high-contrast",
5172
- light: "github-light-high-contrast"
5173
- }
5222
+ const offsets = useMemo8(() => {
5223
+ const out = new Array(count + 1);
5224
+ out[0] = 0;
5225
+ for (let i = 0; i < count; i++) out[i + 1] = (out[i] ?? 0) + Math.max(heights[i] ?? 0, 0);
5226
+ return out;
5227
+ }, [heights, count]);
5228
+ useEffect16(() => {
5229
+ recompute();
5230
+ }, [offsets, recompute]);
5231
+ const scrollToIndex = useCallback17(
5232
+ (index) => {
5233
+ const el = scrollRef.current;
5234
+ if (!el) return;
5235
+ const clamped = Math.max(0, Math.min(count - 1, index));
5236
+ el.scrollTop = offsets[clamped] ?? 0;
5174
5237
  },
5175
- themeType: themeType ?? "dark"
5176
- };
5177
- const pierreVars = {
5178
- "--diffs-dark-bg": "var(--og-color-bg)",
5179
- "--diffs-light-bg": "var(--og-color-bg)",
5180
- "--diffs-bg-buffer-override": "var(--og-color-surface-1)",
5181
- "--diffs-bg-separator-override": "var(--og-color-surface-1)",
5182
- "--diffs-font-size": "var(--og-code-font-size)",
5183
- "--diffs-line-height": "var(--og-code-line-height)"
5184
- };
5185
- return /* @__PURE__ */ jsx7("div", { className: cn("min-w-0", className), "data-opengeni-pierre-file": true, style: pierreVars, children: /* @__PURE__ */ jsx7(Suspense, { fallback: loading ?? /* @__PURE__ */ jsx7(FileSkeleton, {}), children: /* @__PURE__ */ jsx7(
5186
- LazyFile,
5187
- {
5188
- file: { name, contents },
5189
- options,
5190
- ...disableWorkerPool !== void 0 ? { disableWorkerPool } : {}
5191
- }
5192
- ) }) });
5193
- }
5194
- function PlainFile({ name, contents }) {
5195
- return /* @__PURE__ */ jsx7(
5196
- "pre",
5197
- {
5198
- className: "overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg",
5199
- "data-file": name,
5200
- children: contents
5201
- }
5238
+ [offsets, count]
5202
5239
  );
5203
- }
5204
- function FileSkeleton() {
5205
- return /* @__PURE__ */ jsx7("div", { className: "p-3 text-og-sm text-og-fg-subtle", children: "Loading file\u2026" });
5240
+ return {
5241
+ scrollRef,
5242
+ range,
5243
+ offsets,
5244
+ totalHeight: offsets[count] ?? 0,
5245
+ measure,
5246
+ scrollToIndex
5247
+ };
5206
5248
  }
5207
5249
 
5208
- // src/components/code-editor.tsx
5209
- import { Loader2Icon, SaveIcon } from "lucide-react";
5210
- import {
5211
- useCallback as useCallback17,
5212
- useEffect as useEffect17,
5213
- useMemo as useMemo8,
5214
- useRef as useRef14,
5215
- useState as useState14
5216
- } from "react";
5217
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
5218
- var LANGUAGE_LOADERS = {
5219
- javascript: async () => (await import("@codemirror/lang-javascript")).javascript({ jsx: true, typescript: true }),
5220
- json: async () => (await import("@codemirror/lang-json")).json(),
5221
- python: async () => (await import("@codemirror/lang-python")).python(),
5222
- markdown: async () => (await import("@codemirror/lang-markdown")).markdown(),
5223
- css: async () => (await import("@codemirror/lang-css")).css(),
5224
- html: async () => (await import("@codemirror/lang-html")).html()
5250
+ // src/components/workbench-changes.tsx
5251
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
5252
+ var GROUP_THRESHOLD = 20;
5253
+ var OVERSCAN = 2;
5254
+ var HEADER_PX = 30;
5255
+ var LINE_PX = 18;
5256
+ var GUARD_BODY_PX = 52;
5257
+ var COMPACT_SURFACE_PX = 560;
5258
+ var useChangesLayoutEffect = typeof window === "undefined" ? useEffect17 : useLayoutEffect2;
5259
+ var STATUS_TINT2 = {
5260
+ added: "text-og-status-idle",
5261
+ modified: "text-og-status-running",
5262
+ deleted: "text-og-status-failed",
5263
+ renamed: "text-og-accent",
5264
+ copied: "text-og-accent",
5265
+ untracked: "text-og-fg-subtle",
5266
+ ignored: "text-og-fg-subtle",
5267
+ conflicted: "text-og-status-failed",
5268
+ typechange: "text-og-status-running"
5225
5269
  };
5226
- function languageForPath(path) {
5227
- const name = path.split("/").pop() ?? path;
5228
- const ext = name.includes(".") ? (name.split(".").pop() ?? "").toLowerCase() : "";
5229
- switch (ext) {
5230
- case "js":
5231
- case "jsx":
5232
- case "ts":
5233
- case "tsx":
5234
- case "mjs":
5235
- case "cjs":
5236
- return "javascript";
5237
- case "json":
5238
- case "jsonc":
5239
- return "json";
5240
- case "py":
5241
- case "pyi":
5242
- return "python";
5243
- case "md":
5244
- case "markdown":
5245
- case "mdx":
5246
- return "markdown";
5247
- case "css":
5248
- case "scss":
5249
- case "less":
5250
- return "css";
5251
- case "html":
5252
- case "htm":
5253
- case "xml":
5254
- case "svg":
5255
- case "vue":
5256
- return "html";
5257
- default:
5258
- return null;
5270
+ var STATUS_LETTER = {
5271
+ added: "A",
5272
+ modified: "M",
5273
+ deleted: "D",
5274
+ renamed: "R",
5275
+ copied: "C",
5276
+ untracked: "U",
5277
+ ignored: "I",
5278
+ conflicted: "!",
5279
+ typechange: "T"
5280
+ };
5281
+ function compactFileLabel(file) {
5282
+ const slash = file.path.lastIndexOf("/");
5283
+ const basename = slash >= 0 ? file.path.slice(slash + 1) : file.path;
5284
+ const parent = slash >= 0 ? file.path.slice(0, slash + 1) : "";
5285
+ return `${STATUS_LETTER[file.status]} \xB7 ${basename}${parent ? ` \u2014 ${parent}` : ""}`;
5286
+ }
5287
+ function buildRail(files) {
5288
+ const roots = new Set(
5289
+ files.flatMap((file) => file.repoRoot === void 0 ? [] : [file.repoRoot])
5290
+ );
5291
+ const groupByRepo = roots.size > 1;
5292
+ if (!groupByRepo && files.length <= GROUP_THRESHOLD) {
5293
+ return {
5294
+ orderedFiles: files,
5295
+ rows: files.map((file, index) => ({ kind: "file", file, index })),
5296
+ grouped: false
5297
+ };
5298
+ }
5299
+ const groups = /* @__PURE__ */ new Map();
5300
+ for (const file of files) {
5301
+ const slash = file.path.indexOf("/");
5302
+ const key = groupByRepo ? file.repoRoot || "(workspace)" : slash > 0 ? file.path.slice(0, slash) : "(root)";
5303
+ const bucket = groups.get(key);
5304
+ if (bucket) bucket.push(file);
5305
+ else groups.set(key, [file]);
5306
+ }
5307
+ const labels = [...groups.keys()].sort((a, b) => a.localeCompare(b));
5308
+ const orderedFiles = [];
5309
+ const rows = [];
5310
+ for (const label of labels) {
5311
+ const bucket = (groups.get(label) ?? []).slice().sort((a, b) => a.path.localeCompare(b.path));
5312
+ rows.push({ kind: "group", label, count: bucket.length });
5313
+ for (const file of bucket) {
5314
+ rows.push({ kind: "file", file, index: orderedFiles.length });
5315
+ orderedFiles.push(file);
5316
+ }
5259
5317
  }
5318
+ return { orderedFiles, rows, grouped: true };
5260
5319
  }
5261
- function CodeEditor({
5262
- path,
5263
- initialContents,
5264
- onSave,
5265
- onOverwrite,
5266
- onReload,
5267
- onEditIntent,
5268
- readOnly = false,
5269
- themeType = "dark",
5270
- loading,
5271
- fallback,
5320
+ function isGuarded(file) {
5321
+ return file.isBinary || file.truncated;
5322
+ }
5323
+ function estimateSectionHeight(file) {
5324
+ if (isGuarded(file)) return HEADER_PX + GUARD_BODY_PX;
5325
+ let lines = 0;
5326
+ for (const hunk of file.hunks) lines += hunk.lines.length + 1;
5327
+ return HEADER_PX + Math.max(lines, 1) * LINE_PX + 8;
5328
+ }
5329
+ function WorkbenchChanges({
5330
+ diff,
5331
+ source,
5332
+ capturedAt,
5333
+ captureRevision,
5334
+ themeType,
5335
+ onOpenFile,
5272
5336
  className
5273
5337
  }) {
5274
- const [failed, setFailed] = useState14(false);
5275
- const [bundle, setBundle] = useState14(null);
5276
- const [value, setValue] = useState14(initialContents);
5277
- const [baseline, setBaseline] = useState14(initialContents);
5278
- const [saving, setSaving] = useState14(false);
5279
- const [saveError, setSaveError] = useState14(null);
5280
- const [savedTick, setSavedTick] = useState14(false);
5281
- const dirty = value !== baseline;
5282
- const saveConflict = saveError !== null && saveError.code === "file_write_conflict";
5283
- const editIntentFiredRef = useRef14(false);
5284
- const lastSeed = useRef14({ path, contents: initialContents });
5285
- useEffect17(() => {
5286
- const seedChanged = lastSeed.current.path !== path || lastSeed.current.contents !== initialContents;
5287
- if (!seedChanged) return;
5288
- lastSeed.current = { path, contents: initialContents };
5289
- editIntentFiredRef.current = false;
5290
- setValue(initialContents);
5291
- setBaseline(initialContents);
5292
- setSaveError(null);
5293
- setSavedTick(false);
5294
- }, [path, initialContents]);
5295
- const noteEdit = useCallback17(
5296
- (next) => {
5297
- setValue(next);
5298
- setSavedTick(false);
5299
- if (!editIntentFiredRef.current && next !== baseline) {
5300
- editIntentFiredRef.current = true;
5301
- onEditIntent?.();
5302
- }
5303
- },
5304
- [baseline, onEditIntent]
5338
+ const rootRef = useRef15(null);
5339
+ const [compact, setCompact] = useState14(false);
5340
+ const [coarsePointer, setCoarsePointer] = useState14(false);
5341
+ const resolvedTheme = useThemeType(themeType);
5342
+ const [layout, setLayout] = useState14("unified");
5343
+ const [activePath, setActivePath] = useState14(null);
5344
+ const pickerId = useId3();
5345
+ const unicodeFontPaths = useMemo9(
5346
+ () => diff.flatMap((file) => file.oldPath ? [file.path, file.oldPath] : [file.path]),
5347
+ [diff]
5305
5348
  );
5306
- const langKey = useMemo8(() => languageForPath(path), [path]);
5307
- useEffect17(() => {
5308
- let cancelled = false;
5309
- void (async () => {
5310
- try {
5311
- const cmMod = await import("@uiw/react-codemirror");
5312
- let languageExtension = null;
5313
- if (langKey) {
5314
- try {
5315
- languageExtension = await LANGUAGE_LOADERS[langKey]?.() ?? null;
5316
- } catch {
5317
- languageExtension = null;
5318
- }
5319
- }
5320
- if (cancelled) return;
5321
- const saveKeymapExtension = (run) => cmMod.Prec.highest(
5322
- cmMod.keymap.of([
5323
- {
5324
- key: "Mod-s",
5325
- preventDefault: true,
5326
- run: () => {
5327
- run();
5328
- return true;
5329
- }
5330
- }
5331
- ])
5332
- );
5333
- setBundle({
5334
- Editor: cmMod.default,
5335
- saveKeymapExtension,
5336
- languageExtension
5337
- });
5338
- setFailed(false);
5339
- } catch {
5340
- if (!cancelled) setFailed(true);
5341
- }
5342
- })();
5343
- return () => {
5344
- cancelled = true;
5349
+ useUnicodeFallbackFonts(unicodeFontPaths);
5350
+ const { orderedFiles, rows, grouped } = useMemo9(() => buildRail(diff), [diff]);
5351
+ const selectedIndex = orderedFiles.findIndex((file) => file.path === activePath);
5352
+ const activeIndex = selectedIndex >= 0 ? selectedIndex : 0;
5353
+ useChangesLayoutEffect(() => {
5354
+ const root = rootRef.current;
5355
+ if (!root || typeof ResizeObserver === "undefined") return;
5356
+ const update = (width) => {
5357
+ if (width > 0) setCompact(width < COMPACT_SURFACE_PX);
5345
5358
  };
5346
- }, [langKey]);
5347
- const saveRef = useRef14(() => {
5359
+ update(root.getBoundingClientRect().width);
5360
+ const observer = new ResizeObserver((entries) => {
5361
+ update(entries[0]?.contentRect.width ?? root.getBoundingClientRect().width);
5362
+ });
5363
+ observer.observe(root);
5364
+ return () => observer.disconnect();
5365
+ }, []);
5366
+ useChangesLayoutEffect(() => {
5367
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
5368
+ const query = window.matchMedia("(pointer: coarse)");
5369
+ const update = () => setCoarsePointer(query.matches);
5370
+ update();
5371
+ if (typeof query.addEventListener === "function") {
5372
+ query.addEventListener("change", update);
5373
+ return () => query.removeEventListener("change", update);
5374
+ }
5375
+ if (typeof query.addListener === "function") {
5376
+ query.addListener(update);
5377
+ return () => query.removeListener(update);
5378
+ }
5379
+ }, []);
5380
+ const additions = useMemo9(() => diff.reduce((sum, f) => sum + f.additions, 0), [diff]);
5381
+ const deletions = useMemo9(() => diff.reduce((sum, f) => sum + f.deletions, 0), [diff]);
5382
+ const estimateHeight = useCallback18(
5383
+ (index) => {
5384
+ const file = orderedFiles[index];
5385
+ return file ? estimateSectionHeight(file) : HEADER_PX + LINE_PX;
5386
+ },
5387
+ [orderedFiles]
5388
+ );
5389
+ const windowed = useWindowedSections({
5390
+ count: orderedFiles.length,
5391
+ estimateHeight,
5392
+ overscan: OVERSCAN
5348
5393
  });
5349
- const save = useCallback17(async () => {
5350
- if (readOnly) return;
5351
- const snapshot = value;
5352
- if (snapshot === baseline) return;
5353
- setSaving(true);
5354
- setSaveError(null);
5355
- setSavedTick(false);
5356
- try {
5357
- await onSave(snapshot);
5358
- setBaseline(snapshot);
5359
- lastSeed.current = { path, contents: snapshot };
5360
- setSavedTick(true);
5361
- } catch (cause) {
5362
- setSaveError(cause instanceof Error ? cause : new Error(String(cause)));
5363
- } finally {
5364
- setSaving(false);
5394
+ const jumpTo = useCallback18(
5395
+ (index) => {
5396
+ setActivePath(orderedFiles[index]?.path ?? null);
5397
+ windowed.scrollToIndex(index);
5398
+ },
5399
+ [orderedFiles, windowed]
5400
+ );
5401
+ const onPaneScroll = useCallback18(() => {
5402
+ const el = windowed.scrollRef.current;
5403
+ if (!el) {
5404
+ return;
5365
5405
  }
5366
- }, [readOnly, value, baseline, onSave, path]);
5367
- const overwrite = useCallback17(async () => {
5368
- if (readOnly || !onOverwrite) return;
5369
- const snapshot = value;
5370
- if (snapshot === baseline) return;
5371
- setSaving(true);
5372
- setSaveError(null);
5373
- setSavedTick(false);
5374
- try {
5375
- await onOverwrite(snapshot);
5376
- setBaseline(snapshot);
5377
- lastSeed.current = { path, contents: snapshot };
5378
- setSavedTick(true);
5379
- } catch (cause) {
5380
- setSaveError(cause instanceof Error ? cause : new Error(String(cause)));
5381
- } finally {
5382
- setSaving(false);
5406
+ const top = el.scrollTop;
5407
+ const offsets = windowed.offsets;
5408
+ let idx = 0;
5409
+ for (let i = 0; i < orderedFiles.length; i++) {
5410
+ if ((offsets[i] ?? 0) <= top + 4) idx = i;
5411
+ else break;
5383
5412
  }
5384
- }, [readOnly, onOverwrite, value, baseline, path]);
5385
- saveRef.current = () => {
5386
- void save();
5387
- };
5413
+ const nextPath = orderedFiles[idx]?.path ?? null;
5414
+ setActivePath((previous) => previous === nextPath ? previous : nextPath);
5415
+ }, [windowed, orderedFiles]);
5388
5416
  useEffect17(() => {
5389
- if (!savedTick) return;
5390
- const t = setTimeout(() => setSavedTick(false), 1800);
5391
- return () => clearTimeout(t);
5392
- }, [savedTick]);
5393
- const fileName = path.split("/").filter(Boolean).pop() ?? path;
5394
- const cmExtensions = useMemo8(() => {
5395
- if (!bundle) return [];
5396
- const exts = [bundle.saveKeymapExtension(() => saveRef.current())];
5397
- if (bundle.languageExtension) exts.push(bundle.languageExtension);
5398
- return exts;
5399
- }, [bundle]);
5400
- const Editor = bundle?.Editor;
5401
- return /* @__PURE__ */ jsxs6(
5402
- "div",
5403
- {
5404
- className: cn("flex h-full min-h-0 flex-col", className),
5405
- "data-opengeni-code-editor": true,
5406
- style: editorVars,
5417
+ const el = windowed.scrollRef.current;
5418
+ if (!el) return;
5419
+ el.addEventListener("scroll", onPaneScroll, { passive: true });
5420
+ return () => el.removeEventListener("scroll", onPaneScroll);
5421
+ }, [windowed.scrollRef, onPaneScroll]);
5422
+ const sourceBadge = describeSource(source, capturedAt, captureRevision ?? null);
5423
+ const activeFile = orderedFiles[activeIndex] ?? orderedFiles[0];
5424
+ return /* @__PURE__ */ jsxs6(
5425
+ "div",
5426
+ {
5427
+ ref: rootRef,
5428
+ className: cn("flex h-full min-h-0 min-w-0 flex-col", className),
5429
+ "data-workbench-changes-layout": compact ? "compact" : "rail",
5407
5430
  children: [
5408
- /* @__PURE__ */ jsxs6("div", { className: "flex min-h-9 shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1", children: [
5409
- /* @__PURE__ */ jsx8(
5410
- "span",
5411
- {
5412
- className: cn(
5413
- "size-1.5 shrink-0 rounded-full transition-colors",
5414
- readOnly ? "bg-transparent" : dirty ? "bg-og-status-running" : "bg-og-status-idle"
5415
- ),
5416
- title: readOnly ? "read-only" : dirty ? "unsaved changes" : "saved"
5417
- }
5418
- ),
5419
- /* @__PURE__ */ jsxs6("span", { className: "truncate font-og-mono text-og-xs text-og-fg-muted", children: [
5420
- fileName,
5421
- dirty && !readOnly ? " \u2022" : ""
5422
- ] }),
5423
- /* @__PURE__ */ jsxs6("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [
5424
- saveError && !saveConflict && /* @__PURE__ */ jsx8(
5425
- "span",
5426
- {
5427
- className: "max-w-[220px] truncate text-og-xs text-og-status-failed",
5428
- title: saveError.message,
5429
- children: saveError.message
5430
- }
5431
- ),
5432
- !saveError && savedTick && /* @__PURE__ */ jsx8("span", { className: "text-og-xs text-og-status-idle", children: "Saved" }),
5433
- readOnly ? /* @__PURE__ */ jsx8("span", { className: "text-og-xs uppercase tracking-wide text-og-fg-subtle", children: "Read-only" }) : !saveConflict ? /* @__PURE__ */ jsxs6(
5434
- "button",
5435
- {
5436
- type: "button",
5437
- onClick: () => void save(),
5438
- disabled: saving || !dirty,
5439
- className: cn(
5440
- "flex items-center gap-1 rounded-og-sm border border-og-border px-1.5 py-0.5 text-og-xs pointer-coarse:min-h-11",
5441
- saving || !dirty ? "cursor-default text-og-fg-subtle opacity-60" : "text-og-fg hover:bg-og-accent-soft"
5442
- ),
5443
- title: "Save (\u2318/Ctrl+S)",
5444
- children: [
5445
- saving ? /* @__PURE__ */ jsx8(Loader2Icon, { className: "size-3 animate-spin" }) : /* @__PURE__ */ jsx8(SaveIcon, { className: "size-3" }),
5446
- "Save"
5447
- ]
5448
- }
5449
- ) : null
5450
- ] })
5451
- ] }),
5452
- saveConflict ? /* @__PURE__ */ jsxs6(
5431
+ /* @__PURE__ */ jsxs6(
5453
5432
  "div",
5454
5433
  {
5455
- className: "flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-og-status-failed/25 bg-og-status-failed/8 px-2 py-2",
5456
- role: "alert",
5434
+ className: cn(
5435
+ "flex shrink-0 items-center justify-between gap-2 border-b border-og-border px-3 py-1.5",
5436
+ compact && source === "capture" && "flex-col items-stretch gap-1.5 py-2"
5437
+ ),
5457
5438
  children: [
5458
- /* @__PURE__ */ jsxs6("div", { className: "min-w-[12rem] flex-1 text-og-xs text-og-fg-muted", children: [
5459
- /* @__PURE__ */ jsx8("span", { className: "font-medium text-og-status-failed", children: "File changed on machine." }),
5460
- " ",
5461
- "Reloading discards your edits; overwriting replaces the live version."
5462
- ] }),
5463
- /* @__PURE__ */ jsxs6("div", { className: "ml-auto flex shrink-0 items-center gap-1.5", children: [
5464
- onReload ? /* @__PURE__ */ jsx8(
5465
- "button",
5466
- {
5467
- type: "button",
5468
- onClick: () => {
5469
- setSaveError(null);
5470
- onReload();
5471
- },
5472
- className: "min-h-8 rounded-og-sm border border-og-border bg-og-surface-1 px-2 text-og-xs font-medium text-og-fg hover:bg-og-surface-2 pointer-coarse:min-h-11",
5473
- children: "Reload live"
5474
- }
5475
- ) : null,
5476
- onOverwrite ? /* @__PURE__ */ jsx8(
5477
- "button",
5478
- {
5479
- type: "button",
5480
- onClick: () => void overwrite(),
5481
- disabled: saving,
5482
- className: "min-h-8 rounded-og-sm border border-og-status-failed/40 px-2 text-og-xs font-medium text-og-status-failed hover:bg-og-status-failed/10 disabled:opacity-50 pointer-coarse:min-h-11",
5483
- children: "Overwrite"
5484
- }
5485
- ) : null
5486
- ] })
5439
+ /* @__PURE__ */ jsxs6(
5440
+ "span",
5441
+ {
5442
+ role: "status",
5443
+ "aria-atomic": "true",
5444
+ "aria-live": "polite",
5445
+ "data-contrast-audited": true,
5446
+ className: "min-w-0 text-og-xs text-og-fg-muted",
5447
+ children: [
5448
+ diff.length,
5449
+ " ",
5450
+ diff.length === 1 ? "file" : "files",
5451
+ " changed",
5452
+ /* @__PURE__ */ jsxs6("span", { "data-contrast-audited": true, className: "ml-2 text-og-status-idle", children: [
5453
+ "+",
5454
+ additions
5455
+ ] }),
5456
+ /* @__PURE__ */ jsxs6("span", { "data-contrast-audited": true, className: "ml-1 text-og-status-failed", children: [
5457
+ "\u2212",
5458
+ deletions
5459
+ ] })
5460
+ ]
5461
+ }
5462
+ ),
5463
+ /* @__PURE__ */ jsxs6(
5464
+ "div",
5465
+ {
5466
+ className: cn(
5467
+ "flex shrink-0 items-center gap-2",
5468
+ compact && source === "capture" && "self-start"
5469
+ ),
5470
+ children: [
5471
+ compact ? null : /* @__PURE__ */ jsx6(LayoutToggle, { layout, onChange: setLayout }),
5472
+ /* @__PURE__ */ jsx6(SourceBadge, { source, capturedAt, label: sourceBadge })
5473
+ ]
5474
+ }
5475
+ )
5487
5476
  ]
5488
5477
  }
5489
- ) : null,
5490
- /* @__PURE__ */ jsx8("div", { className: "min-h-0 flex-1 overflow-auto", children: failed ? fallback ?? /* @__PURE__ */ jsx8(
5491
- PlainTextarea,
5492
- {
5493
- value,
5494
- readOnly,
5495
- onChange: noteEdit,
5496
- onSave: () => void save()
5497
- }
5498
- ) : Editor ? /* @__PURE__ */ jsx8(
5499
- Editor,
5500
- {
5501
- value,
5502
- theme: themeType === "light" ? "light" : "dark",
5503
- editable: !readOnly,
5504
- readOnly,
5505
- basicSetup: true,
5506
- extensions: cmExtensions,
5507
- height: "100%",
5508
- className: "og-cm-editor min-h-full text-og-sm",
5509
- onChange: readOnly ? void 0 : noteEdit
5510
- }
5511
- ) : loading ?? /* @__PURE__ */ jsx8(EditorSkeleton, {}) })
5478
+ ),
5479
+ compact ? /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-1 flex-col", children: [
5480
+ /* @__PURE__ */ jsxs6("div", { className: "shrink-0 border-b border-og-border bg-og-surface-1 p-2", children: [
5481
+ /* @__PURE__ */ jsx6("label", { className: "sr-only", htmlFor: pickerId, children: "Changed file" }),
5482
+ /* @__PURE__ */ jsx6(
5483
+ "select",
5484
+ {
5485
+ id: pickerId,
5486
+ "aria-label": "Changed file",
5487
+ value: activeIndex,
5488
+ onChange: (event) => {
5489
+ const index = Number(event.currentTarget.value);
5490
+ setActivePath(orderedFiles[index]?.path ?? null);
5491
+ },
5492
+ title: activeFile?.path,
5493
+ "data-compact-file-picker": true,
5494
+ className: "h-11 w-full rounded-og-md border border-og-border bg-og-bg px-3 font-og-mono text-og-sm text-og-fg outline-none transition-colors focus:border-og-accent focus:ring-2 focus:ring-og-accent-soft",
5495
+ children: orderedFiles.map((file, index) => /* @__PURE__ */ jsx6("option", { value: index, children: compactFileLabel(file) }, file.path))
5496
+ }
5497
+ )
5498
+ ] }),
5499
+ /* @__PURE__ */ jsx6("div", { className: "min-h-0 min-w-0 flex-1 overflow-auto", "data-opengeni-changes-pane": true, children: activeFile ? /* @__PURE__ */ jsx6(
5500
+ DiffSection,
5501
+ {
5502
+ file: activeFile,
5503
+ layout: "unified",
5504
+ themeType: resolvedTheme,
5505
+ ...onOpenFile ? { onOpenFile } : {}
5506
+ }
5507
+ ) : null })
5508
+ ] }) : /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-1", children: [
5509
+ /* @__PURE__ */ jsx6("div", { className: "w-[clamp(12rem,28%,15rem)] shrink-0 border-r border-og-border bg-og-surface-1/35", children: /* @__PURE__ */ jsx6(
5510
+ VList2,
5511
+ {
5512
+ className: "h-full",
5513
+ itemSize: coarsePointer ? 44 : 28,
5514
+ ssrCount: Math.min(30, rows.length),
5515
+ children: rows.map(
5516
+ (row) => row.kind === "group" ? /* @__PURE__ */ jsxs6(
5517
+ "div",
5518
+ {
5519
+ "data-rail-group": true,
5520
+ className: "flex items-center gap-1.5 px-2 pb-0.5 pt-2 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle",
5521
+ children: [
5522
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 truncate", children: row.label }),
5523
+ /* @__PURE__ */ jsx6("span", { className: "shrink-0", children: row.count })
5524
+ ]
5525
+ },
5526
+ `g:${row.label}`
5527
+ ) : /* @__PURE__ */ jsx6(
5528
+ RailFileRow,
5529
+ {
5530
+ file: row.file,
5531
+ grouped,
5532
+ active: row.index === activeIndex,
5533
+ onClick: () => jumpTo(row.index)
5534
+ },
5535
+ row.file.path
5536
+ )
5537
+ )
5538
+ }
5539
+ ) }),
5540
+ /* @__PURE__ */ jsx6(
5541
+ "div",
5542
+ {
5543
+ ref: windowed.scrollRef,
5544
+ className: "min-h-0 min-w-0 flex-1 overflow-auto",
5545
+ "data-opengeni-changes-pane": true,
5546
+ children: /* @__PURE__ */ jsx6("div", { style: { position: "relative", height: windowed.totalHeight }, children: orderedFiles.map((file, index) => {
5547
+ if (index < windowed.range.start || index >= windowed.range.end) return null;
5548
+ return /* @__PURE__ */ jsx6(
5549
+ MeasuredSection,
5550
+ {
5551
+ index,
5552
+ top: windowed.offsets[index] ?? 0,
5553
+ onMeasure: windowed.measure,
5554
+ children: /* @__PURE__ */ jsx6(
5555
+ DiffSection,
5556
+ {
5557
+ file,
5558
+ layout,
5559
+ themeType: resolvedTheme,
5560
+ ...onOpenFile ? { onOpenFile } : {}
5561
+ }
5562
+ )
5563
+ },
5564
+ file.path
5565
+ );
5566
+ }) })
5567
+ }
5568
+ )
5569
+ ] })
5512
5570
  ]
5513
5571
  }
5514
5572
  );
5515
5573
  }
5516
- var editorVars = {
5517
- "--og-cm-bg": "var(--og-color-bg)",
5518
- fontFamily: "var(--og-font-mono)"
5519
- };
5520
- function PlainTextarea({
5521
- value,
5522
- readOnly,
5523
- onChange,
5524
- onSave
5574
+ function SourceBadge({
5575
+ source,
5576
+ capturedAt,
5577
+ label
5525
5578
  }) {
5526
- return /* @__PURE__ */ jsx8(
5527
- "textarea",
5528
- {
5529
- value,
5530
- readOnly,
5531
- spellCheck: false,
5532
- onChange: (e) => onChange(e.target.value),
5533
- onKeyDown: (event) => {
5534
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
5535
- event.preventDefault();
5536
- onSave();
5537
- }
5538
- },
5539
- "aria-label": "Plain text file editor",
5540
- className: "h-full w-full resize-none bg-transparent p-2 font-og-mono text-og-sm text-og-fg outline-none"
5541
- }
5542
- );
5579
+ if (source === "capture" && capturedAt) {
5580
+ return /* @__PURE__ */ jsxs6(
5581
+ "span",
5582
+ {
5583
+ className: "inline-flex items-center gap-1 rounded-og-xs border border-og-border px-1.5 py-px text-og-xs text-og-fg-muted",
5584
+ title: new Date(capturedAt).toLocaleString(),
5585
+ children: [
5586
+ /* @__PURE__ */ jsx6(HistoryIcon, { className: "size-3 shrink-0 text-og-status-running", "aria-hidden": true }),
5587
+ label
5588
+ ]
5589
+ }
5590
+ );
5591
+ }
5592
+ return /* @__PURE__ */ jsx6("span", { className: "rounded-og-xs bg-og-surface-2 px-1.5 py-px text-og-xs text-og-fg-subtle", children: label });
5543
5593
  }
5544
- function EditorSkeleton() {
5545
- return /* @__PURE__ */ jsx8("div", { className: "p-3 text-og-sm text-og-fg-subtle", children: "Loading editor\u2026" });
5594
+ function describeSource(source, capturedAt, revision) {
5595
+ if (source !== "capture" || !capturedAt) return "Live diff";
5596
+ const time = formatAsOf(capturedAt, Date.now());
5597
+ return revision !== null ? `as of turn ${revision} \xB7 ${time}` : `as of ${time}`;
5546
5598
  }
5547
-
5548
- // src/components/sandbox-files.tsx
5549
- import { FileCode2Icon, FileWarningIcon, LoaderCircleIcon } from "lucide-react";
5550
- import { useCallback as useCallback18, useEffect as useEffect18, useRef as useRef15, useState as useState15 } from "react";
5551
- import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
5552
- function SandboxFiles({
5553
- files,
5554
- git,
5555
- fileSystemAvailable = true,
5556
- usePierre = true,
5557
- editable = true,
5558
- onEditIntent,
5559
- requestedPath,
5560
- requestedPathRequestId,
5561
- requestedPathReady = true,
5562
- workspaceResting = false,
5563
- workspaceWaking = false,
5564
- liveWorkspaceReady = true,
5565
- onWakeWorkspace,
5566
- themeType,
5567
- className
5599
+ function RailFileRow({
5600
+ file,
5601
+ grouped,
5602
+ active,
5603
+ onClick
5568
5604
  }) {
5569
- const [selected, setSelected] = useState15(null);
5570
- const [editMode, setEditMode] = useState15(false);
5571
- const [liveRequestedPath, setLiveRequestedPath] = useState15(null);
5572
- const [viewReloadRevision, setViewReloadRevision] = useState15(0);
5573
- const pendingRequestRef = useRef15(null);
5574
- const handledRequestRef = useRef15(null);
5575
- const requestKey = requestedPath ? requestedPathRequestId ?? requestedPath : null;
5576
- useEffect18(() => {
5577
- if (!requestedPath || requestKey === null) {
5578
- pendingRequestRef.current = null;
5579
- handledRequestRef.current = null;
5580
- return;
5581
- }
5582
- if (handledRequestRef.current === requestKey) {
5583
- return;
5584
- }
5585
- if (!requestedPathReady) {
5586
- pendingRequestRef.current = requestKey;
5587
- return;
5605
+ const repoPrefix = file.repoRoot ? `${file.repoRoot}/` : null;
5606
+ const shown = grouped ? file.repoRoot !== void 0 ? repoPrefix && file.path.startsWith(repoPrefix) ? file.path.slice(repoPrefix.length) : file.path : file.path.slice(file.path.indexOf("/") + 1) || file.path : file.path;
5607
+ return /* @__PURE__ */ jsxs6(
5608
+ "button",
5609
+ {
5610
+ type: "button",
5611
+ onClick,
5612
+ title: file.path,
5613
+ "data-rail-file": true,
5614
+ className: cn(
5615
+ "relative flex min-h-7 w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-og-sm transition-colors hover:bg-og-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent pointer-coarse:min-h-11",
5616
+ grouped && "pl-3",
5617
+ active && "bg-og-accent-soft text-og-fg before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-r-full before:bg-og-accent"
5618
+ ),
5619
+ children: [
5620
+ /* @__PURE__ */ jsx6(
5621
+ "span",
5622
+ {
5623
+ "data-contrast-audited": true,
5624
+ className: cn("w-3 shrink-0 text-center font-og-mono text-og-xs", STATUS_TINT2[file.status]),
5625
+ children: STATUS_LETTER[file.status]
5626
+ }
5627
+ ),
5628
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", children: shown }),
5629
+ /* @__PURE__ */ jsxs6("span", { className: "ml-auto flex shrink-0 items-center gap-1 pl-1 font-og-mono text-og-xs", children: [
5630
+ /* @__PURE__ */ jsxs6("span", { "data-contrast-audited": true, className: "text-og-status-idle", children: [
5631
+ "+",
5632
+ file.additions
5633
+ ] }),
5634
+ /* @__PURE__ */ jsxs6("span", { "data-contrast-audited": true, className: "text-og-status-failed", children: [
5635
+ "\u2212",
5636
+ file.deletions
5637
+ ] })
5638
+ ] })
5639
+ ]
5588
5640
  }
5589
- handledRequestRef.current = requestKey;
5590
- pendingRequestRef.current = null;
5591
- setSelected(requestedPath);
5592
- setEditMode(false);
5593
- }, [requestKey, requestedPath, requestedPathReady]);
5594
- const rootRef = useRef15(null);
5595
- const [wide, setWide] = useState15(false);
5596
- useEffect18(() => {
5597
- const el = rootRef.current;
5598
- if (!el || typeof ResizeObserver === "undefined") return;
5599
- const observer = new ResizeObserver((entries) => {
5600
- const w = entries[0]?.contentRect.width ?? 0;
5601
- setWide(w >= 720);
5602
- });
5603
- observer.observe(el);
5604
- return () => observer.disconnect();
5605
- }, []);
5606
- const resolvedTheme = useThemeType(themeType);
5607
- const viewPath = selected;
5608
- const fileView = useFileView(viewPath, files.readFile, viewReloadRevision);
5609
- const selectFile = useCallback18((path) => {
5610
- if (pendingRequestRef.current !== null) {
5611
- handledRequestRef.current = pendingRequestRef.current;
5612
- pendingRequestRef.current = null;
5641
+ );
5642
+ }
5643
+ function MeasuredSection({
5644
+ index,
5645
+ top,
5646
+ onMeasure,
5647
+ children
5648
+ }) {
5649
+ const ref = useRef15(null);
5650
+ useEffect17(() => {
5651
+ const el = ref.current;
5652
+ if (!el) return;
5653
+ const report = () => onMeasure(index, el.offsetHeight);
5654
+ report();
5655
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(report) : null;
5656
+ ro?.observe(el);
5657
+ return () => ro?.disconnect();
5658
+ }, [index, onMeasure]);
5659
+ return /* @__PURE__ */ jsx6(
5660
+ "div",
5661
+ {
5662
+ ref,
5663
+ "data-diff-section": true,
5664
+ "data-diff-index": index,
5665
+ style: { position: "absolute", top, left: 0, right: 0 },
5666
+ children
5613
5667
  }
5614
- setSelected(path);
5615
- setEditMode(false);
5616
- setLiveRequestedPath(null);
5617
- setViewReloadRevision(0);
5618
- }, []);
5619
- const canEdit = editable && viewPath !== null && !fileView.loading && fileView.error === null && !fileView.isBinary && !fileView.truncated && fileView.content !== null;
5620
- const showEditor = canEdit && editMode;
5621
- const captureFileUnavailable = fileView.error instanceof CapturedFileUnavailableError ? fileView.error : null;
5622
- const waitingForSelectedFile = liveRequestedPath === viewPath && (!liveWorkspaceReady || files.loading) && captureFileUnavailable !== null;
5623
- if (!fileSystemAvailable) {
5624
- return /* @__PURE__ */ jsx9(
5625
- Notice,
5626
- {
5627
- className,
5628
- icon: /* @__PURE__ */ jsx9(FileWarningIcon, { className: "size-5", "aria-hidden": true }),
5629
- title: "Files unavailable",
5630
- announce: "alert",
5631
- children: "This sandbox does not expose a file system."
5632
- }
5633
- );
5668
+ );
5669
+ }
5670
+ function DiffSection({
5671
+ file,
5672
+ layout,
5673
+ themeType,
5674
+ onOpenFile
5675
+ }) {
5676
+ if (isGuarded(file)) {
5677
+ const renamed = file.oldPath && file.oldPath !== file.path;
5678
+ return /* @__PURE__ */ jsxs6("section", { className: "border-b border-og-border pb-1", children: [
5679
+ /* @__PURE__ */ jsxs6("header", { className: "flex items-center justify-between gap-2 bg-og-surface-1 px-3 py-1.5 text-og-sm", children: [
5680
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 truncate font-og-mono text-og-xs", children: renamed ? `${file.oldPath} \u2192 ${file.path}` : file.path }),
5681
+ /* @__PURE__ */ jsxs6("span", { className: "flex shrink-0 items-center gap-2 font-og-mono text-og-xs", children: [
5682
+ /* @__PURE__ */ jsxs6("span", { className: "text-og-status-idle", children: [
5683
+ "+",
5684
+ file.additions
5685
+ ] }),
5686
+ /* @__PURE__ */ jsxs6("span", { className: "text-og-status-failed", children: [
5687
+ "\u2212",
5688
+ file.deletions
5689
+ ] })
5690
+ ] })
5691
+ ] }),
5692
+ /* @__PURE__ */ jsx6(GuardBody, { file, ...onOpenFile ? { onOpenFile } : {} })
5693
+ ] });
5634
5694
  }
5635
- if (workspaceResting || workspaceWaking) {
5636
- return /* @__PURE__ */ jsx9("div", { className: cn("h-full", className), "data-opengeni-workspace-resting": true, children: /* @__PURE__ */ jsxs7(
5637
- Notice,
5695
+ return /* @__PURE__ */ jsx6("section", { "data-pierre-section": true, className: "border-b border-og-border pb-2", children: /* @__PURE__ */ jsx6(PierreDiff, { diff: [file], layout, themeType, className: "px-1" }) });
5696
+ }
5697
+ function GuardBody({
5698
+ file,
5699
+ onOpenFile
5700
+ }) {
5701
+ const reason = file.isBinary ? "Binary file" : "Diff too large to show here";
5702
+ return /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-2 px-3 py-3 text-og-sm text-og-fg-subtle", children: [
5703
+ /* @__PURE__ */ jsx6(FileWarningIcon, { className: "size-3.5 shrink-0", "aria-hidden": true }),
5704
+ /* @__PURE__ */ jsxs6("span", { className: "min-w-0 flex-1", children: [
5705
+ reason,
5706
+ "."
5707
+ ] }),
5708
+ onOpenFile ? /* @__PURE__ */ jsxs6(
5709
+ "button",
5638
5710
  {
5639
- icon: workspaceWaking ? /* @__PURE__ */ jsx9(
5640
- LoaderCircleIcon,
5641
- {
5642
- className: "size-5 animate-spin motion-reduce:animate-none",
5643
- "aria-hidden": true
5644
- }
5645
- ) : /* @__PURE__ */ jsx9(FileCode2Icon, { className: "size-5", "aria-hidden": true }),
5646
- title: workspaceWaking ? "Waking workspace" : "Workspace is resting",
5647
- announce: "status",
5711
+ type: "button",
5712
+ onClick: () => onOpenFile(file.path),
5713
+ className: "inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-og-sm border border-og-border px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-11",
5648
5714
  children: [
5649
- /* @__PURE__ */ jsx9("p", { children: workspaceWaking ? "Connecting to the live file system\u2026" : "No captured revision is available yet. Wake the sandbox to browse its current files." }),
5650
- !workspaceWaking && onWakeWorkspace ? /* @__PURE__ */ jsx9(WakeButton, { onClick: onWakeWorkspace, children: "Open live workspace" }) : null
5715
+ "Open in Files",
5716
+ /* @__PURE__ */ jsx6(ArrowUpRightIcon, { className: "size-3.5", "aria-hidden": true })
5651
5717
  ]
5652
5718
  }
5653
- ) });
5654
- }
5655
- return /* @__PURE__ */ jsxs7("div", { ref: rootRef, className: cn("flex h-full min-h-0 min-w-0 flex-col", className), children: [
5656
- /* @__PURE__ */ jsx9(GitHeader, { git, dirtyCount: git.diff.length }),
5657
- /* @__PURE__ */ jsxs7("div", { className: cn("flex min-h-0 flex-1", wide ? "flex-row" : "flex-col"), children: [
5658
- /* @__PURE__ */ jsx9(
5659
- "div",
5660
- {
5661
- className: cn(
5662
- "flex min-h-0 flex-col",
5663
- wide ? "w-[280px] shrink-0 border-r border-og-border" : "flex-1"
5664
- ),
5665
- children: /* @__PURE__ */ jsx9(
5666
- FileBrowser,
5667
- {
5668
- result: files,
5669
- selectedPath: selected ?? void 0,
5670
- onSelectFile: selectFile,
5671
- editable,
5672
- emptyState: "This directory is empty",
5673
- className: "min-w-0 flex-1"
5674
- }
5675
- )
5676
- }
5719
+ ) : null
5720
+ ] });
5721
+ }
5722
+ function LayoutToggle({
5723
+ layout,
5724
+ onChange
5725
+ }) {
5726
+ return /* @__PURE__ */ jsx6("div", { className: "inline-flex items-center rounded-og-sm border border-og-border p-0.5", children: ["unified", "split"].map((value) => /* @__PURE__ */ jsx6(
5727
+ "button",
5728
+ {
5729
+ type: "button",
5730
+ onClick: () => onChange(value),
5731
+ className: cn(
5732
+ "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
5733
+ layout === value ? "bg-og-accent-soft text-og-fg" : "text-og-fg-subtle hover:text-og-fg"
5677
5734
  ),
5678
- /* @__PURE__ */ jsxs7(
5679
- "div",
5680
- {
5681
- className: cn(
5682
- "flex min-h-0 min-w-0 flex-col",
5683
- wide ? "flex-1" : "flex-[1.4] border-t border-og-border"
5684
- ),
5685
- children: [
5686
- /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1", children: [
5687
- /* @__PURE__ */ jsx9(
5688
- "span",
5689
- {
5690
- "data-opengeni-selected-file": true,
5691
- className: "min-w-0 truncate font-og-mono text-og-xs text-og-fg-muted",
5692
- children: selected ?? "No file selected"
5693
- }
5694
- ),
5695
- canEdit && /* @__PURE__ */ jsx9(
5696
- Segmented,
5697
- {
5698
- options: [
5699
- { value: "view", label: "View" },
5700
- { value: "edit", label: "Edit" }
5701
- ],
5702
- value: editMode ? "edit" : "view",
5703
- onChange: (v) => setEditMode(v === "edit")
5704
- }
5705
- )
5706
- ] }),
5707
- /* @__PURE__ */ jsx9("div", { className: "min-h-0 flex-1 overflow-auto", children: viewPath ? showEditor && fileView.content !== null ? /* @__PURE__ */ jsx9(
5708
- CodeEditor,
5709
- {
5710
- path: viewPath,
5711
- initialContents: fileView.content,
5712
- themeType: resolvedTheme,
5713
- onSave: (contents) => files.writeFile(viewPath, contents, { expectedContent: fileView.content }),
5714
- onOverwrite: (contents) => files.writeFile(viewPath, contents, { force: true }),
5715
- onReload: () => setViewReloadRevision((revision) => revision + 1),
5716
- ...onEditIntent ? { onEditIntent } : {},
5717
- className: "h-full"
5718
- },
5719
- viewPath
5720
- ) : waitingForSelectedFile ? /* @__PURE__ */ jsxs7(
5721
- Notice,
5722
- {
5723
- icon: /* @__PURE__ */ jsx9(
5724
- LoaderCircleIcon,
5725
- {
5726
- className: "size-5 animate-spin motion-reduce:animate-none",
5727
- "aria-hidden": true
5728
- }
5729
- ),
5730
- title: "Waking workspace",
5731
- announce: "status",
5732
- children: [
5733
- "Opening ",
5734
- viewPath,
5735
- " when the live file system is ready\u2026"
5736
- ]
5737
- }
5738
- ) : captureFileUnavailable ? /* @__PURE__ */ jsxs7(Notice, { icon: /* @__PURE__ */ jsx9(FileCode2Icon, { className: "size-5", "aria-hidden": true }), title: "On machine", children: [
5739
- /* @__PURE__ */ jsx9("p", { children: captureFileUnavailable.reason === "too-large" ? "This file is larger than the captured preview limit." : captureFileUnavailable.reason === "content-missing" ? "The captured copy is no longer available." : "This file was indexed, but it was not changed in the captured turn." }),
5740
- onWakeWorkspace ? /* @__PURE__ */ jsx9(
5741
- WakeButton,
5742
- {
5743
- onClick: () => {
5744
- setLiveRequestedPath(viewPath);
5745
- if (liveWorkspaceReady) void files.refresh();
5746
- else onWakeWorkspace();
5747
- },
5748
- children: liveWorkspaceReady ? "Retry live file" : "Open live file"
5749
- }
5750
- ) : null
5751
- ] }) : fileView.error ? /* @__PURE__ */ jsxs7(Notice, { announce: "alert", children: [
5752
- "Could not open ",
5753
- viewPath,
5754
- ": ",
5755
- fileView.error.message
5756
- ] }) : fileView.loading ? /* @__PURE__ */ jsxs7(Notice, { announce: "status", children: [
5757
- "Loading ",
5758
- viewPath,
5759
- "\u2026"
5760
- ] }) : fileView.isBinary ? /* @__PURE__ */ jsxs7(Notice, { children: [
5761
- viewPath,
5762
- " is a binary file (",
5763
- fileView.sizeBytes ?? 0,
5764
- " bytes)."
5765
- ] }) : fileView.content !== null ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
5766
- fileView.truncated && /* @__PURE__ */ jsxs7("div", { className: "border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-status-running", children: [
5767
- "Large file \u2014 showing a truncated preview (",
5768
- fileView.sizeBytes ?? 0,
5769
- " bytes loaded). Editing is disabled to avoid corrupting the file."
5770
- ] }),
5771
- usePierre ? /* @__PURE__ */ jsx9(
5772
- PierreFile,
5773
- {
5774
- path: viewPath,
5775
- contents: fileView.content,
5776
- themeType: resolvedTheme,
5777
- fallback: /* @__PURE__ */ jsx9("pre", { className: "overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg", children: fileView.content }),
5778
- className: "p-1"
5779
- }
5780
- ) : /* @__PURE__ */ jsx9("pre", { className: "overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg", children: fileView.content })
5781
- ] }) : /* @__PURE__ */ jsxs7(Notice, { announce: "status", children: [
5782
- "Loading ",
5783
- viewPath,
5784
- "\u2026"
5785
- ] }) : (
5786
- // Nothing selected — the tree shows the whole workspace; pick a file.
5787
- requestedPath && !requestedPathReady ? /* @__PURE__ */ jsxs7(
5788
- Notice,
5789
- {
5790
- icon: /* @__PURE__ */ jsx9(
5791
- LoaderCircleIcon,
5792
- {
5793
- className: "size-5 animate-spin motion-reduce:animate-none",
5794
- "aria-hidden": true
5795
- }
5796
- ),
5797
- title: "Waking sandbox",
5798
- announce: "status",
5799
- children: [
5800
- "Opening ",
5801
- requestedPath,
5802
- " when the live workspace is ready\u2026"
5803
- ]
5804
- }
5805
- ) : /* @__PURE__ */ jsx9(Notice, { icon: /* @__PURE__ */ jsx9(FileCode2Icon, { className: "size-5", "aria-hidden": true }), title: "Choose a file", children: "Select a file in the tree to preview it." })
5806
- ) })
5807
- ]
5808
- }
5809
- )
5810
- ] })
5811
- ] });
5735
+ children: value
5736
+ },
5737
+ value
5738
+ )) });
5812
5739
  }
5813
- function WakeButton({ children, onClick }) {
5814
- return /* @__PURE__ */ jsx9(
5815
- "button",
5740
+
5741
+ // src/components/diff-view.tsx
5742
+ import { jsx as jsx7 } from "react/jsx-runtime";
5743
+ function DiffView({
5744
+ diff,
5745
+ fallback,
5746
+ layout = "unified",
5747
+ themeType,
5748
+ isRepo = true,
5749
+ emptyState,
5750
+ className
5751
+ }) {
5752
+ if (diff.length === 0) {
5753
+ return /* @__PURE__ */ jsx7("div", { className: cn("p-3 text-og-sm text-og-fg-subtle", className), children: emptyState ?? (isRepo ? "No changes" : "No repository mounted") });
5754
+ }
5755
+ return /* @__PURE__ */ jsx7(
5756
+ PierreDiff,
5816
5757
  {
5817
- type: "button",
5818
- onClick,
5819
- className: "mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent px-3 py-2 text-og-sm font-medium text-og-on-accent shadow-sm transition-colors hover:bg-og-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg",
5820
- children
5758
+ diff,
5759
+ layout,
5760
+ themeType,
5761
+ ...fallback !== void 0 ? { fallback } : {},
5762
+ className
5821
5763
  }
5822
5764
  );
5823
5765
  }
5824
- function GitHeader({ git, dirtyCount }) {
5825
- const dirty = dirtyCount > 0;
5826
- return /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm", children: [
5827
- /* @__PURE__ */ jsx9(
5828
- "span",
5829
- {
5830
- "aria-hidden": "true",
5831
- className: cn(
5832
- "size-2 shrink-0 rounded-full",
5833
- dirty ? "bg-og-status-running" : "bg-og-status-idle"
5834
- )
5835
- }
5836
- ),
5837
- /* @__PURE__ */ jsx9("span", { className: "sr-only", children: dirty ? `Working tree has ${dirtyCount} changed files` : "Working tree clean" }),
5838
- /* @__PURE__ */ jsx9("span", { className: "truncate font-og-mono text-og-fg", children: git.repoCount > 1 ? `${git.repoCount} repositories` : git.branch ?? (git.isRepo ? "(detached)" : "no repo") }),
5839
- (git.ahead > 0 || git.behind > 0) && /* @__PURE__ */ jsxs7("span", { className: "flex shrink-0 items-center gap-1.5 text-og-xs text-og-fg-subtle", children: [
5840
- git.ahead > 0 && /* @__PURE__ */ jsxs7("span", { children: [
5841
- "\u2191",
5842
- git.ahead
5843
- ] }),
5844
- git.behind > 0 && /* @__PURE__ */ jsxs7("span", { children: [
5845
- "\u2193",
5846
- git.behind
5847
- ] })
5848
- ] }),
5849
- dirty && /* @__PURE__ */ jsxs7(
5850
- "span",
5851
- {
5852
- "aria-hidden": "true",
5853
- "data-contrast-audited": true,
5854
- className: "ml-auto shrink-0 text-og-xs text-og-fg-subtle",
5855
- children: [
5856
- dirtyCount,
5857
- " changed"
5858
- ]
5859
- }
5860
- )
5861
- ] });
5862
- }
5863
- function Segmented({
5864
- options,
5865
- value,
5866
- onChange
5766
+
5767
+ // src/components/pierre-file.tsx
5768
+ import {
5769
+ lazy,
5770
+ Suspense,
5771
+ useEffect as useEffect18,
5772
+ useState as useState15
5773
+ } from "react";
5774
+ import { jsx as jsx8 } from "react/jsx-runtime";
5775
+ var LazyFile = lazy(async () => {
5776
+ const mod = await import("@pierre/diffs/react");
5777
+ return { default: mod.File };
5778
+ });
5779
+ function PierreFile({
5780
+ path,
5781
+ contents,
5782
+ themeType,
5783
+ theme,
5784
+ disableWorkerPool,
5785
+ loading,
5786
+ fallback,
5787
+ className
5867
5788
  }) {
5868
- return /* @__PURE__ */ jsx9("div", { className: "flex flex-wrap items-center rounded-og-sm border border-og-border p-0.5", children: options.map((opt) => /* @__PURE__ */ jsx9(
5869
- "button",
5870
- {
5871
- type: "button",
5872
- onClick: () => onChange(opt.value),
5873
- className: cn(
5874
- "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
5875
- opt.value === value ? "bg-og-accent-soft text-og-fg" : "text-og-fg-subtle hover:text-og-fg"
5876
- ),
5877
- children: opt.label
5878
- },
5879
- opt.value
5880
- )) });
5881
- }
5882
- function useFileView(path, readFile, reloadRevision = 0) {
5883
- const [state, setState] = useState15({
5884
- content: null,
5885
- isBinary: false,
5886
- truncated: false,
5887
- sizeBytes: null,
5888
- loading: false,
5889
- error: null
5890
- });
5789
+ const [failed, setFailed] = useState15(false);
5891
5790
  useEffect18(() => {
5892
- if (!path) {
5893
- setState({
5894
- content: null,
5895
- isBinary: false,
5896
- truncated: false,
5897
- sizeBytes: null,
5898
- loading: false,
5899
- error: null
5900
- });
5901
- return;
5902
- }
5903
5791
  let cancelled = false;
5904
- const abort = new AbortController();
5905
- setState({
5906
- content: null,
5907
- isBinary: false,
5908
- truncated: false,
5909
- sizeBytes: null,
5910
- loading: true,
5911
- error: null
5912
- });
5913
- void readFile(path, { signal: abort.signal }).then((res) => {
5914
- if (cancelled) return;
5915
- const content = res.isBinary ? null : res.encoding === "base64" ? decodeBase64Utf8(res.content) : res.content;
5916
- setState({
5917
- content,
5918
- isBinary: res.isBinary,
5919
- truncated: res.truncated,
5920
- sizeBytes: res.sizeBytes,
5921
- loading: false,
5922
- error: null
5923
- });
5924
- }).catch((cause) => {
5925
- if (cancelled) return;
5926
- setState({
5927
- content: null,
5928
- isBinary: false,
5929
- truncated: false,
5930
- sizeBytes: null,
5931
- loading: false,
5932
- error: cause instanceof Error ? cause : new Error(String(cause))
5933
- });
5792
+ void import("@pierre/diffs/react").catch(() => {
5793
+ if (!cancelled) setFailed(true);
5934
5794
  });
5935
5795
  return () => {
5936
5796
  cancelled = true;
5937
- abort.abort();
5938
5797
  };
5939
- }, [path, readFile, reloadRevision]);
5940
- return state;
5941
- }
5942
- function decodeBase64Utf8(b64) {
5943
- try {
5944
- if (typeof atob === "function") {
5945
- const binary = atob(b64);
5946
- const bytes = new Uint8Array(binary.length);
5947
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
5948
- return new TextDecoder().decode(bytes);
5949
- }
5950
- } catch {
5798
+ }, []);
5799
+ const name = path.split("/").filter(Boolean).pop() ?? path;
5800
+ if (failed) {
5801
+ return /* @__PURE__ */ jsx8("div", { className, children: fallback ?? /* @__PURE__ */ jsx8(PlainFile, { name, contents }) });
5951
5802
  }
5952
- return b64;
5953
- }
5954
- function Notice({
5955
- children,
5956
- className,
5957
- icon,
5958
- title,
5959
- announce
5960
- }) {
5961
- return /* @__PURE__ */ jsx9(
5962
- "div",
5803
+ const options = {
5804
+ overflow: "scroll",
5805
+ stickyHeader: true,
5806
+ showLineNumbers: true,
5807
+ ...theme ? { theme } : {
5808
+ theme: {
5809
+ dark: "github-dark-high-contrast",
5810
+ light: "github-light-high-contrast"
5811
+ }
5812
+ },
5813
+ themeType: themeType ?? "dark"
5814
+ };
5815
+ const pierreVars = {
5816
+ "--diffs-dark-bg": "var(--og-color-bg)",
5817
+ "--diffs-light-bg": "var(--og-color-bg)",
5818
+ "--diffs-bg-buffer-override": "var(--og-color-surface-1)",
5819
+ "--diffs-bg-separator-override": "var(--og-color-surface-1)",
5820
+ "--diffs-font-size": "var(--og-code-font-size)",
5821
+ "--diffs-line-height": "var(--og-code-line-height)"
5822
+ };
5823
+ return /* @__PURE__ */ jsx8("div", { className: cn("min-w-0", className), "data-opengeni-pierre-file": true, style: pierreVars, children: /* @__PURE__ */ jsx8(Suspense, { fallback: loading ?? /* @__PURE__ */ jsx8(FileSkeleton, {}), children: /* @__PURE__ */ jsx8(
5824
+ LazyFile,
5963
5825
  {
5964
- role: announce,
5965
- "aria-atomic": announce ? "true" : void 0,
5966
- "aria-live": announce === "alert" ? "assertive" : announce === "status" ? "polite" : void 0,
5967
- className: cn(
5968
- "flex h-full items-center justify-center p-4 text-center text-og-sm text-og-fg-subtle",
5969
- className
5970
- ),
5971
- children: /* @__PURE__ */ jsxs7("div", { className: "flex max-w-sm flex-col items-center gap-2.5", children: [
5972
- icon ? /* @__PURE__ */ jsx9("span", { className: "grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-fg-muted shadow-sm", children: icon }) : null,
5973
- title ? /* @__PURE__ */ jsx9("p", { className: "font-medium text-og-fg", children: title }) : null,
5974
- /* @__PURE__ */ jsx9("div", { className: "leading-5", children })
5975
- ] })
5826
+ file: { name, contents },
5827
+ options,
5828
+ ...disableWorkerPool !== void 0 ? { disableWorkerPool } : {}
5829
+ }
5830
+ ) }) });
5831
+ }
5832
+ function PlainFile({ name, contents }) {
5833
+ return /* @__PURE__ */ jsx8(
5834
+ "pre",
5835
+ {
5836
+ className: "overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg",
5837
+ "data-file": name,
5838
+ children: contents
5976
5839
  }
5977
5840
  );
5978
5841
  }
5842
+ function FileSkeleton() {
5843
+ return /* @__PURE__ */ jsx8("div", { className: "p-3 text-og-sm text-og-fg-subtle", children: "Loading file\u2026" });
5844
+ }
5979
5845
 
5980
- // src/components/desktop-viewer.tsx
5981
- import { LoaderCircleIcon as LoaderCircleIcon2, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from "lucide-react";
5982
- import { useEffect as useEffect19, useRef as useRef16, useState as useState16 } from "react";
5983
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
5984
- var DESKTOP_MEDIA_BACKGROUND = "#000";
5985
- function isHardUnavailable(reason) {
5986
- switch (reason) {
5987
- case "backend_unsupported":
5988
- case "os_unsupported":
5989
- case "not_provisioned":
5990
- case "disabled_by_policy":
5991
- case "tier_headless":
5992
- return true;
5846
+ // src/components/code-editor.tsx
5847
+ import { Loader2Icon, SaveIcon } from "lucide-react";
5848
+ import {
5849
+ useCallback as useCallback19,
5850
+ useEffect as useEffect19,
5851
+ useMemo as useMemo10,
5852
+ useRef as useRef16,
5853
+ useState as useState16
5854
+ } from "react";
5855
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
5856
+ var LANGUAGE_LOADERS = {
5857
+ javascript: async () => (await import("@codemirror/lang-javascript")).javascript({ jsx: true, typescript: true }),
5858
+ json: async () => (await import("@codemirror/lang-json")).json(),
5859
+ python: async () => (await import("@codemirror/lang-python")).python(),
5860
+ markdown: async () => (await import("@codemirror/lang-markdown")).markdown(),
5861
+ css: async () => (await import("@codemirror/lang-css")).css(),
5862
+ html: async () => (await import("@codemirror/lang-html")).html()
5863
+ };
5864
+ function languageForPath(path) {
5865
+ const name = path.split("/").pop() ?? path;
5866
+ const ext = name.includes(".") ? (name.split(".").pop() ?? "").toLowerCase() : "";
5867
+ switch (ext) {
5868
+ case "js":
5869
+ case "jsx":
5870
+ case "ts":
5871
+ case "tsx":
5872
+ case "mjs":
5873
+ case "cjs":
5874
+ return "javascript";
5875
+ case "json":
5876
+ case "jsonc":
5877
+ return "json";
5878
+ case "py":
5879
+ case "pyi":
5880
+ return "python";
5881
+ case "md":
5882
+ case "markdown":
5883
+ case "mdx":
5884
+ return "markdown";
5885
+ case "css":
5886
+ case "scss":
5887
+ case "less":
5888
+ return "css";
5889
+ case "html":
5890
+ case "htm":
5891
+ case "xml":
5892
+ case "svg":
5893
+ case "vue":
5894
+ return "html";
5993
5895
  default:
5994
- return false;
5896
+ return null;
5995
5897
  }
5996
5898
  }
5997
- function DesktopViewer({
5998
- capability,
5999
- interactive,
6000
- showControlToggle = true,
6001
- scaleViewport,
6002
- rfbFactory,
6003
- webSocketFactory,
6004
- renderConsentGate,
6005
- onAcknowledge,
6006
- watching,
6007
- onWarm,
6008
- renderUnavailable,
6009
- renderWarming,
6010
- renderViewerCap,
6011
- viewerCapReached,
6012
- connectTimeoutMs = 13e3,
5899
+ function CodeEditor({
5900
+ path,
5901
+ initialContents,
5902
+ onSave,
5903
+ onOverwrite,
5904
+ onReload,
5905
+ onEditIntent,
5906
+ readOnly = false,
5907
+ themeType = "dark",
5908
+ loading,
5909
+ fallback,
6013
5910
  className
6014
5911
  }) {
6015
- const containerRef = useRef16(null);
6016
- const [consented, setConsented] = useState16(false);
6017
- const [takeControl, setTakeControl] = useState16(interactive ?? false);
6018
- const externallyControlled = interactive !== void 0;
6019
- const serverAllowsControl = capability?.mode !== "read-only";
6020
- const wantControl = externallyControlled ? interactive : takeControl;
6021
- const inControl = Boolean(wantControl) && serverAllowsControl;
6022
- const isWatching = watching ?? consented;
6023
- const transportNull = !capability || capability.transport === null;
6024
- const reason = capability?.reason ?? null;
6025
- const needsAck = capability?.requiresAcknowledgment === true && capability.acknowledged !== true && !consented;
6026
- const noLiveAddress = Boolean(capability) && !transportNull && !capability.url;
6027
- const coldWarmable = transportNull && reason === "lease_cold" || !transportNull && noLiveAddress;
6028
- const hardUnavailable = transportNull && isHardUnavailable(reason);
6029
- const accept = () => {
6030
- setConsented(true);
6031
- onAcknowledge?.();
6032
- };
5912
+ const [failed, setFailed] = useState16(false);
5913
+ const [bundle, setBundle] = useState16(null);
5914
+ const [value, setValue] = useState16(initialContents);
5915
+ const [baseline, setBaseline] = useState16(initialContents);
5916
+ const [saving, setSaving] = useState16(false);
5917
+ const [saveError, setSaveError] = useState16(null);
5918
+ const [savedTick, setSavedTick] = useState16(false);
5919
+ const dirty = value !== baseline;
5920
+ const saveConflict = saveError !== null && saveError.code === "file_write_conflict";
5921
+ const editIntentFiredRef = useRef16(false);
5922
+ const lastSeed = useRef16({ path, contents: initialContents });
6033
5923
  useEffect19(() => {
6034
- if (!inControl || externallyControlled) return;
6035
- const onKey = (event) => {
6036
- if (event.ctrlKey && event.altKey && event.shiftKey && (event.key === "Control" || event.key === "Alt" || event.key === "Shift")) {
6037
- event.preventDefault();
6038
- setTakeControl(false);
5924
+ const seedChanged = lastSeed.current.path !== path || lastSeed.current.contents !== initialContents;
5925
+ if (!seedChanged) return;
5926
+ lastSeed.current = { path, contents: initialContents };
5927
+ editIntentFiredRef.current = false;
5928
+ setValue(initialContents);
5929
+ setBaseline(initialContents);
5930
+ setSaveError(null);
5931
+ setSavedTick(false);
5932
+ }, [path, initialContents]);
5933
+ const noteEdit = useCallback19(
5934
+ (next) => {
5935
+ setValue(next);
5936
+ setSavedTick(false);
5937
+ if (!editIntentFiredRef.current && next !== baseline) {
5938
+ editIntentFiredRef.current = true;
5939
+ onEditIntent?.();
6039
5940
  }
6040
- };
6041
- window.addEventListener("keydown", onKey);
5941
+ },
5942
+ [baseline, onEditIntent]
5943
+ );
5944
+ const langKey = useMemo10(() => languageForPath(path), [path]);
5945
+ useEffect19(() => {
5946
+ let cancelled = false;
5947
+ void (async () => {
5948
+ try {
5949
+ const cmMod = await import("@uiw/react-codemirror");
5950
+ let languageExtension = null;
5951
+ if (langKey) {
5952
+ try {
5953
+ languageExtension = await LANGUAGE_LOADERS[langKey]?.() ?? null;
5954
+ } catch {
5955
+ languageExtension = null;
5956
+ }
5957
+ }
5958
+ if (cancelled) return;
5959
+ const saveKeymapExtension = (run) => cmMod.Prec.highest(
5960
+ cmMod.keymap.of([
5961
+ {
5962
+ key: "Mod-s",
5963
+ preventDefault: true,
5964
+ run: () => {
5965
+ run();
5966
+ return true;
5967
+ }
5968
+ }
5969
+ ])
5970
+ );
5971
+ setBundle({
5972
+ Editor: cmMod.default,
5973
+ saveKeymapExtension,
5974
+ languageExtension
5975
+ });
5976
+ setFailed(false);
5977
+ } catch {
5978
+ if (!cancelled) setFailed(true);
5979
+ }
5980
+ })();
6042
5981
  return () => {
6043
- window.removeEventListener("keydown", onKey);
5982
+ cancelled = true;
6044
5983
  };
6045
- }, [inControl, externallyControlled]);
6046
- const connectCapability = !transportNull && !needsAck && !viewerCapReached ? capability : null;
6047
- const stream = useDesktopStream({
6048
- capability: connectCapability,
6049
- containerRef,
6050
- interactive: inControl,
6051
- ...scaleViewport !== void 0 ? { scaleViewport } : {},
6052
- ...rfbFactory ? { rfbFactory } : {},
6053
- ...webSocketFactory ? { webSocketFactory } : {}
5984
+ }, [langKey]);
5985
+ const saveRef = useRef16(() => {
6054
5986
  });
6055
- const connected = stream.state === "connected";
6056
- const hasLiveUrl = Boolean(connectCapability?.url);
6057
- const streamStateRef = useRef16(stream.state);
6058
- streamStateRef.current = stream.state;
6059
- const warmKeyRef = useRef16(null);
6060
- useEffect19(() => {
6061
- if (!isWatching || !coldWarmable || needsAck || viewerCapReached) {
6062
- if (!coldWarmable) warmKeyRef.current = null;
6063
- return;
5987
+ const save = useCallback19(async () => {
5988
+ if (readOnly) return;
5989
+ const snapshot = value;
5990
+ if (snapshot === baseline) return;
5991
+ setSaving(true);
5992
+ setSaveError(null);
5993
+ setSavedTick(false);
5994
+ try {
5995
+ await onSave(snapshot);
5996
+ setBaseline(snapshot);
5997
+ lastSeed.current = { path, contents: snapshot };
5998
+ setSavedTick(true);
5999
+ } catch (cause) {
6000
+ setSaveError(cause instanceof Error ? cause : new Error(String(cause)));
6001
+ } finally {
6002
+ setSaving(false);
6064
6003
  }
6065
- const warm = onWarm ?? onAcknowledge;
6066
- if (!warm) return;
6067
- const key = `${capability?.reason ?? ""}:${capability?.url ?? ""}:${capability?.expiresAt ?? ""}`;
6068
- if (warmKeyRef.current === key) return;
6069
- warmKeyRef.current = key;
6070
- warm();
6071
- }, [isWatching, coldWarmable, needsAck, viewerCapReached, capability, onWarm, onAcknowledge]);
6072
- useEffect19(() => {
6073
- if (!hasLiveUrl || typeof document === "undefined") return;
6074
- const maybeReattach = () => {
6075
- if (document.visibilityState !== "visible") return;
6076
- if (streamStateRef.current === "idle") stream.reconnect();
6077
- };
6078
- document.addEventListener("visibilitychange", maybeReattach);
6079
- return () => document.removeEventListener("visibilitychange", maybeReattach);
6080
- }, [hasLiveUrl, connectCapability?.url]);
6081
- const [connectTimedOut, setConnectTimedOut] = useState16(false);
6082
- const [reconnectNonce, setReconnectNonce] = useState16(0);
6004
+ }, [readOnly, value, baseline, onSave, path]);
6005
+ const overwrite = useCallback19(async () => {
6006
+ if (readOnly || !onOverwrite) return;
6007
+ const snapshot = value;
6008
+ if (snapshot === baseline) return;
6009
+ setSaving(true);
6010
+ setSaveError(null);
6011
+ setSavedTick(false);
6012
+ try {
6013
+ await onOverwrite(snapshot);
6014
+ setBaseline(snapshot);
6015
+ lastSeed.current = { path, contents: snapshot };
6016
+ setSavedTick(true);
6017
+ } catch (cause) {
6018
+ setSaveError(cause instanceof Error ? cause : new Error(String(cause)));
6019
+ } finally {
6020
+ setSaving(false);
6021
+ }
6022
+ }, [readOnly, onOverwrite, value, baseline, path]);
6023
+ saveRef.current = () => {
6024
+ void save();
6025
+ };
6083
6026
  useEffect19(() => {
6084
- setConnectTimedOut(false);
6085
- if (!hasLiveUrl || connected || stream.error || connectTimeoutMs <= 0) return;
6086
- const timer = setTimeout(() => setConnectTimedOut(true), connectTimeoutMs);
6087
- return () => clearTimeout(timer);
6088
- }, [
6089
- hasLiveUrl,
6090
- connected,
6091
- connectTimeoutMs,
6092
- connectCapability?.url,
6093
- stream.error,
6094
- reconnectNonce
6095
- ]);
6096
- const reconnect = () => {
6097
- setConnectTimedOut(false);
6098
- setReconnectNonce((n) => n + 1);
6099
- stream.reconnect();
6100
- };
6101
- let uiState;
6102
- if (viewerCapReached) uiState = "viewer_cap";
6103
- else if (hardUnavailable) uiState = "unavailable";
6104
- else if (needsAck && !isWatching) uiState = "consent";
6105
- else if (coldWarmable) uiState = "warming";
6106
- else if (connected) uiState = "connected";
6107
- else if (stream.error) uiState = "error";
6108
- else if (connectTimedOut) uiState = "connect_failed";
6109
- else uiState = "connecting";
6110
- const overlayShown = uiState !== "connected" && uiState !== "connecting";
6111
- const showToggle = showControlToggle && !overlayShown;
6112
- let overlay = null;
6113
- switch (uiState) {
6114
- case "viewer_cap":
6115
- overlay = renderViewerCap?.() ?? defaultNotice(
6116
- "Too many viewers",
6117
- "This session has reached its live-viewer limit. Try again shortly."
6118
- );
6119
- break;
6120
- case "unavailable":
6121
- overlay = renderUnavailable?.(reason) ?? defaultNotice("Desktop unavailable", unavailableCopy(reason));
6122
- break;
6123
- case "consent":
6124
- overlay = renderConsentGate ? renderConsentGate(accept, capability?.shared ?? false) : /* @__PURE__ */ jsx10(DefaultConsentGate, { shared: capability?.shared ?? false, onAccept: accept });
6125
- break;
6126
- case "warming":
6127
- overlay = renderWarming?.() ?? /* @__PURE__ */ jsx10(WarmingNotice, {});
6128
- break;
6129
- case "connect_failed":
6130
- overlay = defaultNotice(
6131
- "Couldn\u2019t connect",
6132
- "The desktop is warm but the live stream didn\u2019t come up. This usually clears on a retry.",
6133
- reconnect
6134
- );
6135
- break;
6136
- case "error":
6137
- overlay = defaultNotice(
6138
- "Desktop disconnected",
6139
- stream.error?.message ?? "The stream dropped.",
6140
- reconnect
6141
- );
6142
- break;
6143
- case "connecting":
6144
- case "connected":
6145
- overlay = null;
6146
- break;
6147
- }
6148
- return /* @__PURE__ */ jsxs8(
6027
+ if (!savedTick) return;
6028
+ const t = setTimeout(() => setSavedTick(false), 1800);
6029
+ return () => clearTimeout(t);
6030
+ }, [savedTick]);
6031
+ const fileName = path.split("/").filter(Boolean).pop() ?? path;
6032
+ const cmExtensions = useMemo10(() => {
6033
+ if (!bundle) return [];
6034
+ const exts = [bundle.saveKeymapExtension(() => saveRef.current())];
6035
+ if (bundle.languageExtension) exts.push(bundle.languageExtension);
6036
+ return exts;
6037
+ }, [bundle]);
6038
+ const Editor = bundle?.Editor;
6039
+ return /* @__PURE__ */ jsxs7(
6149
6040
  "div",
6150
6041
  {
6151
- className: cn(
6152
- "relative h-full w-full overflow-hidden",
6153
- inControl && "ring-2 ring-inset ring-og-accent",
6154
- className
6155
- ),
6156
- style: { backgroundColor: DESKTOP_MEDIA_BACKGROUND },
6157
- "data-opengeni-desktop": true,
6158
- "data-state": stream.state,
6159
- "data-ui-state": uiState,
6160
- "data-in-control": inControl ? "true" : void 0,
6042
+ className: cn("flex h-full min-h-0 flex-col", className),
6043
+ "data-opengeni-code-editor": true,
6044
+ style: editorVars,
6161
6045
  children: [
6162
- /* @__PURE__ */ jsx10(
6163
- "div",
6164
- {
6165
- ref: containerRef,
6166
- className: "absolute inset-0 overflow-hidden",
6167
- "data-opengeni-desktop-canvas": true,
6168
- "data-state": stream.state
6169
- }
6170
- ),
6171
- uiState === "connecting" && /* @__PURE__ */ jsxs8("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-3 text-og-fg-subtle", children: [
6172
- /* @__PURE__ */ jsxs8("span", { className: "relative flex items-center justify-center", children: [
6173
- /* @__PURE__ */ jsx10(MonitorIcon, { className: "size-8 opacity-30", strokeWidth: 1.5 }),
6174
- /* @__PURE__ */ jsx10(
6175
- LoaderCircleIcon2,
6046
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-h-9 shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1", children: [
6047
+ /* @__PURE__ */ jsx9(
6048
+ "span",
6049
+ {
6050
+ className: cn(
6051
+ "size-1.5 shrink-0 rounded-full transition-colors",
6052
+ readOnly ? "bg-transparent" : dirty ? "bg-og-status-running" : "bg-og-status-idle"
6053
+ ),
6054
+ title: readOnly ? "read-only" : dirty ? "unsaved changes" : "saved"
6055
+ }
6056
+ ),
6057
+ /* @__PURE__ */ jsxs7("span", { className: "truncate font-og-mono text-og-xs text-og-fg-muted", children: [
6058
+ fileName,
6059
+ dirty && !readOnly ? " \u2022" : ""
6060
+ ] }),
6061
+ /* @__PURE__ */ jsxs7("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [
6062
+ saveError && !saveConflict && /* @__PURE__ */ jsx9(
6063
+ "span",
6176
6064
  {
6177
- className: "absolute size-12 animate-og-spin text-og-accent opacity-70",
6178
- strokeWidth: 1.25
6065
+ className: "max-w-[220px] truncate text-og-xs text-og-status-failed",
6066
+ title: saveError.message,
6067
+ children: saveError.message
6179
6068
  }
6180
- )
6181
- ] }),
6182
- /* @__PURE__ */ jsx10("span", { className: "text-og-sm", children: "Connecting to the desktop\u2026" })
6069
+ ),
6070
+ !saveError && savedTick && /* @__PURE__ */ jsx9("span", { className: "text-og-xs text-og-status-idle", children: "Saved" }),
6071
+ readOnly ? /* @__PURE__ */ jsx9("span", { className: "text-og-xs uppercase tracking-wide text-og-fg-subtle", children: "Read-only" }) : !saveConflict ? /* @__PURE__ */ jsxs7(
6072
+ "button",
6073
+ {
6074
+ type: "button",
6075
+ onClick: () => void save(),
6076
+ disabled: saving || !dirty,
6077
+ className: cn(
6078
+ "flex items-center gap-1 rounded-og-sm border border-og-border px-1.5 py-0.5 text-og-xs pointer-coarse:min-h-11",
6079
+ saving || !dirty ? "cursor-default text-og-fg-subtle opacity-60" : "text-og-fg hover:bg-og-accent-soft"
6080
+ ),
6081
+ title: "Save (\u2318/Ctrl+S)",
6082
+ children: [
6083
+ saving ? /* @__PURE__ */ jsx9(Loader2Icon, { className: "size-3 animate-spin" }) : /* @__PURE__ */ jsx9(SaveIcon, { className: "size-3" }),
6084
+ "Save"
6085
+ ]
6086
+ }
6087
+ ) : null
6088
+ ] })
6183
6089
  ] }),
6184
- showToggle && !externallyControlled && inControl && /* @__PURE__ */ jsx10(
6185
- InControlBar,
6090
+ saveConflict ? /* @__PURE__ */ jsxs7(
6091
+ "div",
6186
6092
  {
6187
- shared: capability?.shared ?? false,
6188
- onRelease: () => setTakeControl(false)
6093
+ className: "flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-og-status-failed/25 bg-og-status-failed/8 px-2 py-2",
6094
+ role: "alert",
6095
+ children: [
6096
+ /* @__PURE__ */ jsxs7("div", { className: "min-w-[12rem] flex-1 text-og-xs text-og-fg-muted", children: [
6097
+ /* @__PURE__ */ jsx9("span", { className: "font-medium text-og-status-failed", children: "File changed on machine." }),
6098
+ " ",
6099
+ "Reloading discards your edits; overwriting replaces the live version."
6100
+ ] }),
6101
+ /* @__PURE__ */ jsxs7("div", { className: "ml-auto flex shrink-0 items-center gap-1.5", children: [
6102
+ onReload ? /* @__PURE__ */ jsx9(
6103
+ "button",
6104
+ {
6105
+ type: "button",
6106
+ onClick: () => {
6107
+ setSaveError(null);
6108
+ onReload();
6109
+ },
6110
+ className: "min-h-8 rounded-og-sm border border-og-border bg-og-surface-1 px-2 text-og-xs font-medium text-og-fg hover:bg-og-surface-2 pointer-coarse:min-h-11",
6111
+ children: "Reload live"
6112
+ }
6113
+ ) : null,
6114
+ onOverwrite ? /* @__PURE__ */ jsx9(
6115
+ "button",
6116
+ {
6117
+ type: "button",
6118
+ onClick: () => void overwrite(),
6119
+ disabled: saving,
6120
+ className: "min-h-8 rounded-og-sm border border-og-status-failed/40 px-2 text-og-xs font-medium text-og-status-failed hover:bg-og-status-failed/10 disabled:opacity-50 pointer-coarse:min-h-11",
6121
+ children: "Overwrite"
6122
+ }
6123
+ ) : null
6124
+ ] })
6125
+ ]
6189
6126
  }
6190
- ),
6191
- showToggle && !externallyControlled && !inControl && connected && /* @__PURE__ */ jsx10(
6192
- TakeControlCallToAction,
6127
+ ) : null,
6128
+ /* @__PURE__ */ jsx9("div", { className: "min-h-0 flex-1 overflow-auto", children: failed ? fallback ?? /* @__PURE__ */ jsx9(
6129
+ PlainTextarea,
6193
6130
  {
6194
- disabled: !serverAllowsControl,
6195
- disabledReason: !serverAllowsControl ? capability?.client === "frames" ? "View-only \u2014 live control isn't available for this machine yet." : "This deployment streams the desktop read-only." : void 0,
6196
- onTakeControl: () => setTakeControl(true)
6131
+ value,
6132
+ readOnly,
6133
+ onChange: noteEdit,
6134
+ onSave: () => void save()
6197
6135
  }
6198
- ),
6199
- overlay && /* @__PURE__ */ jsx10("div", { className: "absolute inset-0 flex items-center justify-center p-4", children: overlay })
6136
+ ) : Editor ? /* @__PURE__ */ jsx9(
6137
+ Editor,
6138
+ {
6139
+ value,
6140
+ theme: themeType === "light" ? "light" : "dark",
6141
+ editable: !readOnly,
6142
+ readOnly,
6143
+ basicSetup: true,
6144
+ extensions: cmExtensions,
6145
+ height: "100%",
6146
+ className: "og-cm-editor min-h-full text-og-sm",
6147
+ onChange: readOnly ? void 0 : noteEdit
6148
+ }
6149
+ ) : loading ?? /* @__PURE__ */ jsx9(EditorSkeleton, {}) })
6200
6150
  ]
6201
6151
  }
6202
6152
  );
6203
6153
  }
6204
- function TakeControlCallToAction({
6205
- disabled,
6206
- disabledReason,
6207
- onTakeControl
6154
+ var editorVars = {
6155
+ "--og-cm-bg": "var(--og-color-bg)",
6156
+ fontFamily: "var(--og-font-mono)"
6157
+ };
6158
+ function PlainTextarea({
6159
+ value,
6160
+ readOnly,
6161
+ onChange,
6162
+ onSave
6208
6163
  }) {
6209
- return (
6210
- // The wrapper spans the surface but is click-through (pointer-events-none); only
6211
- // the button itself is interactive, so watchers can still see the desktop.
6212
- /* @__PURE__ */ jsx10("div", { className: "pointer-events-none absolute inset-0 flex items-end justify-center pb-[8%]", children: /* @__PURE__ */ jsxs8(
6213
- "button",
6214
- {
6215
- type: "button",
6216
- disabled,
6217
- "aria-label": "Take control of the desktop",
6218
- title: disabled ? disabledReason : "Take control of the desktop",
6219
- onClick: onTakeControl,
6220
- className: cn(
6221
- "group pointer-events-auto flex items-center gap-3 rounded-og-lg border px-5 py-3",
6222
- "border-og-border",
6223
- "bg-og-bg/85 backdrop-blur-md",
6224
- "shadow-og-lg",
6225
- "outline-none transition-all duration-150 ease-out",
6226
- "focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg",
6227
- disabled ? "cursor-not-allowed opacity-60" : cn(
6228
- "cursor-pointer opacity-90 hover:-translate-y-0.5 hover:opacity-100",
6229
- "hover:border-og-accent",
6230
- "hover:bg-og-accent/10"
6231
- )
6232
- ),
6233
- children: [
6234
- /* @__PURE__ */ jsx10(
6235
- "span",
6236
- {
6237
- className: cn(
6238
- "flex size-9 shrink-0 items-center justify-center rounded-full transition-colors",
6239
- "bg-og-accent",
6240
- "text-og-accent-fg",
6241
- disabled ? "" : "group-hover:scale-105"
6242
- ),
6243
- children: /* @__PURE__ */ jsx10(MousePointerClickIcon, { className: "size-5", strokeWidth: 2 })
6244
- }
6245
- ),
6246
- /* @__PURE__ */ jsxs8("span", { className: "flex flex-col items-start leading-tight", children: [
6247
- /* @__PURE__ */ jsx10("span", { className: "text-og-base font-semibold text-og-fg", children: "Take control" }),
6248
- /* @__PURE__ */ jsx10("span", { className: "text-og-xs text-og-fg-subtle", children: disabled && disabledReason ? disabledReason : "Drive the mouse & keyboard" })
6249
- ] })
6250
- ]
6251
- }
6252
- ) })
6164
+ return /* @__PURE__ */ jsx9(
6165
+ "textarea",
6166
+ {
6167
+ value,
6168
+ readOnly,
6169
+ spellCheck: false,
6170
+ onChange: (e) => onChange(e.target.value),
6171
+ onKeyDown: (event) => {
6172
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
6173
+ event.preventDefault();
6174
+ onSave();
6175
+ }
6176
+ },
6177
+ "aria-label": "Plain text file editor",
6178
+ className: "h-full w-full resize-none bg-transparent p-2 font-og-mono text-og-sm text-og-fg outline-none"
6179
+ }
6253
6180
  );
6254
6181
  }
6255
- function InControlBar({ shared, onRelease }) {
6256
- return /* @__PURE__ */ jsxs8("div", { className: "pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between gap-2 p-2", children: [
6257
- /* @__PURE__ */ jsxs8("span", { className: "pointer-events-auto inline-flex items-center gap-2 rounded-og-sm bg-og-accent px-2.5 py-1 text-og-xs font-medium text-og-accent-fg shadow-og-md", children: [
6258
- /* @__PURE__ */ jsx10("span", { className: "size-1.5 animate-pulse rounded-full bg-current", "aria-hidden": true }),
6259
- "You're in control",
6260
- /* @__PURE__ */ jsx10("span", { className: "opacity-75", children: "\xB7 click Return control (or Ctrl+Alt+Shift)" })
6261
- ] }),
6262
- /* @__PURE__ */ jsxs8("div", { className: "pointer-events-auto flex items-center gap-1.5", children: [
6263
- shared && /* @__PURE__ */ jsx10("span", { className: "rounded-og-sm bg-og-status-failed/85 px-2 py-0.5 text-og-xs text-og-accent-fg", children: "Shared sandbox \u2014 others are watching" }),
6182
+ function EditorSkeleton() {
6183
+ return /* @__PURE__ */ jsx9("div", { className: "p-3 text-og-sm text-og-fg-subtle", children: "Loading editor\u2026" });
6184
+ }
6185
+
6186
+ // src/components/sandbox-files.tsx
6187
+ import { FileCode2Icon, FileWarningIcon as FileWarningIcon2, LoaderCircleIcon } from "lucide-react";
6188
+ import { useCallback as useCallback20, useEffect as useEffect20, useRef as useRef17, useState as useState17 } from "react";
6189
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
6190
+ function SandboxFiles({
6191
+ files,
6192
+ git,
6193
+ fileSystemAvailable = true,
6194
+ usePierre = true,
6195
+ editable = true,
6196
+ onEditIntent,
6197
+ requestedPath,
6198
+ requestedPathRequestId,
6199
+ requestedPathReady = true,
6200
+ workspaceResting = false,
6201
+ workspaceWaking = false,
6202
+ liveWorkspaceReady = true,
6203
+ onWakeWorkspace,
6204
+ themeType,
6205
+ className
6206
+ }) {
6207
+ const [selected, setSelected] = useState17(null);
6208
+ const [editMode, setEditMode] = useState17(false);
6209
+ const [liveRequestedPath, setLiveRequestedPath] = useState17(null);
6210
+ const [viewReloadRevision, setViewReloadRevision] = useState17(0);
6211
+ const pendingRequestRef = useRef17(null);
6212
+ const handledRequestRef = useRef17(null);
6213
+ const requestKey = requestedPath ? requestedPathRequestId ?? requestedPath : null;
6214
+ useEffect20(() => {
6215
+ if (!requestedPath || requestKey === null) {
6216
+ pendingRequestRef.current = null;
6217
+ handledRequestRef.current = null;
6218
+ return;
6219
+ }
6220
+ if (handledRequestRef.current === requestKey) {
6221
+ return;
6222
+ }
6223
+ if (!requestedPathReady) {
6224
+ pendingRequestRef.current = requestKey;
6225
+ return;
6226
+ }
6227
+ handledRequestRef.current = requestKey;
6228
+ pendingRequestRef.current = null;
6229
+ setSelected(requestedPath);
6230
+ setEditMode(false);
6231
+ }, [requestKey, requestedPath, requestedPathReady]);
6232
+ const rootRef = useRef17(null);
6233
+ const [wide, setWide] = useState17(false);
6234
+ useEffect20(() => {
6235
+ const el = rootRef.current;
6236
+ if (!el || typeof ResizeObserver === "undefined") return;
6237
+ const observer = new ResizeObserver((entries) => {
6238
+ const w = entries[0]?.contentRect.width ?? 0;
6239
+ setWide(w >= 720);
6240
+ });
6241
+ observer.observe(el);
6242
+ return () => observer.disconnect();
6243
+ }, []);
6244
+ const resolvedTheme = useThemeType(themeType);
6245
+ const viewPath = selected;
6246
+ const fileView = useFileView(viewPath, files.readFile, viewReloadRevision);
6247
+ const selectFile = useCallback20((path) => {
6248
+ if (pendingRequestRef.current !== null) {
6249
+ handledRequestRef.current = pendingRequestRef.current;
6250
+ pendingRequestRef.current = null;
6251
+ }
6252
+ setSelected(path);
6253
+ setEditMode(false);
6254
+ setLiveRequestedPath(null);
6255
+ setViewReloadRevision(0);
6256
+ }, []);
6257
+ const canEdit = editable && viewPath !== null && !fileView.loading && fileView.error === null && !fileView.isBinary && !fileView.truncated && fileView.content !== null;
6258
+ const showEditor = canEdit && editMode;
6259
+ const captureFileUnavailable = fileView.error instanceof CapturedFileUnavailableError ? fileView.error : null;
6260
+ const waitingForSelectedFile = liveRequestedPath === viewPath && (!liveWorkspaceReady || files.loading) && captureFileUnavailable !== null;
6261
+ if (!fileSystemAvailable) {
6262
+ return /* @__PURE__ */ jsx10(
6263
+ Notice,
6264
+ {
6265
+ className,
6266
+ icon: /* @__PURE__ */ jsx10(FileWarningIcon2, { className: "size-5", "aria-hidden": true }),
6267
+ title: "Files unavailable",
6268
+ announce: "alert",
6269
+ children: "This sandbox does not expose a file system."
6270
+ }
6271
+ );
6272
+ }
6273
+ if (workspaceResting || workspaceWaking) {
6274
+ return /* @__PURE__ */ jsx10("div", { className: cn("h-full", className), "data-opengeni-workspace-resting": true, children: /* @__PURE__ */ jsxs8(
6275
+ Notice,
6276
+ {
6277
+ icon: workspaceWaking ? /* @__PURE__ */ jsx10(
6278
+ LoaderCircleIcon,
6279
+ {
6280
+ className: "size-5 animate-spin motion-reduce:animate-none",
6281
+ "aria-hidden": true
6282
+ }
6283
+ ) : /* @__PURE__ */ jsx10(FileCode2Icon, { className: "size-5", "aria-hidden": true }),
6284
+ title: workspaceWaking ? "Waking workspace" : "Workspace is resting",
6285
+ announce: "status",
6286
+ children: [
6287
+ /* @__PURE__ */ jsx10("p", { children: workspaceWaking ? "Connecting to the live file system\u2026" : "No captured revision is available yet. Wake the sandbox to browse its current files." }),
6288
+ !workspaceWaking && onWakeWorkspace ? /* @__PURE__ */ jsx10(WakeButton, { onClick: onWakeWorkspace, children: "Open live workspace" }) : null
6289
+ ]
6290
+ }
6291
+ ) });
6292
+ }
6293
+ return /* @__PURE__ */ jsxs8("div", { ref: rootRef, className: cn("flex h-full min-h-0 min-w-0 flex-col", className), children: [
6294
+ /* @__PURE__ */ jsx10(GitHeader, { git, dirtyCount: git.diff.length }),
6295
+ /* @__PURE__ */ jsxs8("div", { className: cn("flex min-h-0 flex-1", wide ? "flex-row" : "flex-col"), children: [
6296
+ /* @__PURE__ */ jsx10(
6297
+ "div",
6298
+ {
6299
+ className: cn(
6300
+ "flex min-h-0 flex-col",
6301
+ wide ? "w-[280px] shrink-0 border-r border-og-border" : "flex-1"
6302
+ ),
6303
+ children: /* @__PURE__ */ jsx10(
6304
+ FileBrowser,
6305
+ {
6306
+ result: files,
6307
+ selectedPath: selected ?? void 0,
6308
+ onSelectFile: selectFile,
6309
+ editable,
6310
+ emptyState: "This directory is empty",
6311
+ className: "min-w-0 flex-1"
6312
+ }
6313
+ )
6314
+ }
6315
+ ),
6264
6316
  /* @__PURE__ */ jsxs8(
6265
- "button",
6317
+ "div",
6266
6318
  {
6267
- type: "button",
6268
- onClick: onRelease,
6269
- title: "Return control (or press Ctrl+Alt+Shift)",
6270
6319
  className: cn(
6271
- "inline-flex items-center gap-1.5 rounded-og-sm border px-2.5 py-1 text-og-xs font-semibold shadow-og-md transition-colors",
6272
- "border-og-accent bg-og-bg/90 text-og-fg backdrop-blur-sm",
6273
- "outline-none hover:bg-og-accent hover:text-og-accent-fg focus-visible:ring-2 focus-visible:ring-og-accent"
6320
+ "flex min-h-0 min-w-0 flex-col",
6321
+ wide ? "flex-1" : "flex-[1.4] border-t border-og-border"
6274
6322
  ),
6275
6323
  children: [
6276
- /* @__PURE__ */ jsx10(MonitorIcon, { className: "size-3.5", strokeWidth: 2, "aria-hidden": true }),
6277
- "Return control"
6324
+ /* @__PURE__ */ jsxs8("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1", children: [
6325
+ /* @__PURE__ */ jsx10(
6326
+ "span",
6327
+ {
6328
+ "data-opengeni-selected-file": true,
6329
+ className: "min-w-0 truncate font-og-mono text-og-xs text-og-fg-muted",
6330
+ children: selected ?? "No file selected"
6331
+ }
6332
+ ),
6333
+ canEdit && /* @__PURE__ */ jsx10(
6334
+ Segmented,
6335
+ {
6336
+ options: [
6337
+ { value: "view", label: "View" },
6338
+ { value: "edit", label: "Edit" }
6339
+ ],
6340
+ value: editMode ? "edit" : "view",
6341
+ onChange: (v) => setEditMode(v === "edit")
6342
+ }
6343
+ )
6344
+ ] }),
6345
+ /* @__PURE__ */ jsx10("div", { className: "min-h-0 flex-1 overflow-auto", children: viewPath ? showEditor && fileView.content !== null ? /* @__PURE__ */ jsx10(
6346
+ CodeEditor,
6347
+ {
6348
+ path: viewPath,
6349
+ initialContents: fileView.content,
6350
+ themeType: resolvedTheme,
6351
+ onSave: (contents) => files.writeFile(viewPath, contents, { expectedContent: fileView.content }),
6352
+ onOverwrite: (contents) => files.writeFile(viewPath, contents, { force: true }),
6353
+ onReload: () => setViewReloadRevision((revision) => revision + 1),
6354
+ ...onEditIntent ? { onEditIntent } : {},
6355
+ className: "h-full"
6356
+ },
6357
+ viewPath
6358
+ ) : waitingForSelectedFile ? /* @__PURE__ */ jsxs8(
6359
+ Notice,
6360
+ {
6361
+ icon: /* @__PURE__ */ jsx10(
6362
+ LoaderCircleIcon,
6363
+ {
6364
+ className: "size-5 animate-spin motion-reduce:animate-none",
6365
+ "aria-hidden": true
6366
+ }
6367
+ ),
6368
+ title: "Waking workspace",
6369
+ announce: "status",
6370
+ children: [
6371
+ "Opening ",
6372
+ viewPath,
6373
+ " when the live file system is ready\u2026"
6374
+ ]
6375
+ }
6376
+ ) : captureFileUnavailable ? /* @__PURE__ */ jsxs8(Notice, { icon: /* @__PURE__ */ jsx10(FileCode2Icon, { className: "size-5", "aria-hidden": true }), title: "On machine", children: [
6377
+ /* @__PURE__ */ jsx10("p", { children: captureFileUnavailable.reason === "too-large" ? "This file is larger than the captured preview limit." : captureFileUnavailable.reason === "content-missing" ? "The captured copy is no longer available." : "This file was indexed, but it was not changed in the captured turn." }),
6378
+ onWakeWorkspace ? /* @__PURE__ */ jsx10(
6379
+ WakeButton,
6380
+ {
6381
+ onClick: () => {
6382
+ setLiveRequestedPath(viewPath);
6383
+ if (liveWorkspaceReady) void files.refresh();
6384
+ else onWakeWorkspace();
6385
+ },
6386
+ children: liveWorkspaceReady ? "Retry live file" : "Open live file"
6387
+ }
6388
+ ) : null
6389
+ ] }) : fileView.error ? /* @__PURE__ */ jsxs8(Notice, { announce: "alert", children: [
6390
+ "Could not open ",
6391
+ viewPath,
6392
+ ": ",
6393
+ fileView.error.message
6394
+ ] }) : fileView.loading ? /* @__PURE__ */ jsxs8(Notice, { announce: "status", children: [
6395
+ "Loading ",
6396
+ viewPath,
6397
+ "\u2026"
6398
+ ] }) : fileView.isBinary ? /* @__PURE__ */ jsxs8(Notice, { children: [
6399
+ viewPath,
6400
+ " is a binary file (",
6401
+ fileView.sizeBytes ?? 0,
6402
+ " bytes)."
6403
+ ] }) : fileView.content !== null ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
6404
+ fileView.truncated && /* @__PURE__ */ jsxs8("div", { className: "border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-status-running", children: [
6405
+ "Large file \u2014 showing a truncated preview (",
6406
+ fileView.sizeBytes ?? 0,
6407
+ " bytes loaded). Editing is disabled to avoid corrupting the file."
6408
+ ] }),
6409
+ usePierre ? /* @__PURE__ */ jsx10(
6410
+ PierreFile,
6411
+ {
6412
+ path: viewPath,
6413
+ contents: fileView.content,
6414
+ themeType: resolvedTheme,
6415
+ fallback: /* @__PURE__ */ jsx10("pre", { className: "overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg", children: fileView.content }),
6416
+ className: "p-1"
6417
+ }
6418
+ ) : /* @__PURE__ */ jsx10("pre", { className: "overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg", children: fileView.content })
6419
+ ] }) : /* @__PURE__ */ jsxs8(Notice, { announce: "status", children: [
6420
+ "Loading ",
6421
+ viewPath,
6422
+ "\u2026"
6423
+ ] }) : (
6424
+ // Nothing selected — the tree shows the whole workspace; pick a file.
6425
+ requestedPath && !requestedPathReady ? /* @__PURE__ */ jsxs8(
6426
+ Notice,
6427
+ {
6428
+ icon: /* @__PURE__ */ jsx10(
6429
+ LoaderCircleIcon,
6430
+ {
6431
+ className: "size-5 animate-spin motion-reduce:animate-none",
6432
+ "aria-hidden": true
6433
+ }
6434
+ ),
6435
+ title: "Waking sandbox",
6436
+ announce: "status",
6437
+ children: [
6438
+ "Opening ",
6439
+ requestedPath,
6440
+ " when the live workspace is ready\u2026"
6441
+ ]
6442
+ }
6443
+ ) : /* @__PURE__ */ jsx10(Notice, { icon: /* @__PURE__ */ jsx10(FileCode2Icon, { className: "size-5", "aria-hidden": true }), title: "Choose a file", children: "Select a file in the tree to preview it." })
6444
+ ) })
6278
6445
  ]
6279
6446
  }
6280
6447
  )
6281
6448
  ] })
6282
6449
  ] });
6283
6450
  }
6284
- function unavailableCopy(reason) {
6285
- switch (reason) {
6286
- case "backend_unsupported":
6287
- return "This sandbox backend cannot stream a desktop.";
6288
- case "tier_headless":
6289
- return "This deployment is headless \u2014 terminal, files, and diff only.";
6290
- case "os_unsupported":
6291
- return "The sandbox OS does not support a desktop stream.";
6292
- case "not_provisioned":
6293
- return "This sandbox doesn't have a desktop yet.";
6294
- case "disabled_by_policy":
6295
- return "Desktop streaming is disabled on this deployment.";
6296
- case "lease_cold":
6297
- return "Waiting for the sandbox to start\u2026";
6298
- default:
6299
- return "The desktop isn\u2019t available for this sandbox.";
6300
- }
6451
+ function WakeButton({ children, onClick }) {
6452
+ return /* @__PURE__ */ jsx10(
6453
+ "button",
6454
+ {
6455
+ type: "button",
6456
+ onClick,
6457
+ className: "mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent-deep px-3 py-2 text-og-sm font-medium text-og-accent-fg shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg",
6458
+ children
6459
+ }
6460
+ );
6301
6461
  }
6302
- function WarmingNotice({ onRetry }) {
6303
- return /* @__PURE__ */ jsxs8("div", { className: "flex max-w-sm flex-col items-center gap-3 rounded-og-lg border border-og-border bg-og-bg/90 p-5 text-center text-og-base text-og-fg backdrop-blur-sm", children: [
6304
- /* @__PURE__ */ jsx10(LoaderCircleIcon2, { className: "size-7 animate-og-spin text-og-accent", strokeWidth: 1.5 }),
6305
- /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [
6306
- /* @__PURE__ */ jsx10("div", { className: "font-medium", children: "Warming the sandbox\u2026" }),
6307
- /* @__PURE__ */ jsx10("p", { className: "text-og-sm text-og-fg-subtle", children: "Spinning up the desktop \u2014 this takes a few seconds." })
6308
- ] }),
6309
- onRetry && /* @__PURE__ */ jsx10(
6310
- "button",
6462
+ function GitHeader({ git, dirtyCount }) {
6463
+ const dirty = dirtyCount > 0;
6464
+ return /* @__PURE__ */ jsxs8("div", { className: "flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm", children: [
6465
+ /* @__PURE__ */ jsx10(
6466
+ "span",
6311
6467
  {
6312
- type: "button",
6313
- onClick: onRetry,
6314
- className: "rounded-og-sm border border-og-border px-3 py-1.5 text-og-sm text-og-fg-muted transition-colors hover:text-og-fg pointer-coarse:min-h-11",
6315
- children: "Taking too long? Retry"
6468
+ "aria-hidden": "true",
6469
+ className: cn(
6470
+ "size-2 shrink-0 rounded-full",
6471
+ dirty ? "bg-og-status-running" : "bg-og-status-idle"
6472
+ )
6316
6473
  }
6317
- )
6318
- ] });
6319
- }
6320
- function DefaultConsentGate({ shared, onAccept }) {
6321
- return /* @__PURE__ */ jsxs8("div", { className: "max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg", children: [
6322
- /* @__PURE__ */ jsx10("div", { className: "mb-1 font-medium", children: "Watch the live desktop?" }),
6323
- /* @__PURE__ */ jsxs8("p", { className: "mb-3 text-og-sm text-og-fg-subtle", children: [
6324
- "The desktop pixel stream is ",
6325
- /* @__PURE__ */ jsx10("strong", { children: "un-redacted" }),
6326
- " \u2014 it can show secrets the agent prints on screen.",
6327
- shared ? " This sandbox is shared: you'll also see other sessions' agents on the same screen." : ""
6474
+ ),
6475
+ /* @__PURE__ */ jsx10("span", { className: "sr-only", children: dirty ? `Working tree has ${dirtyCount} changed files` : "Working tree clean" }),
6476
+ /* @__PURE__ */ jsx10("span", { className: "truncate font-og-mono text-og-fg", children: git.repoCount > 1 ? `${git.repoCount} repositories` : git.branch ?? (git.isRepo ? "(detached)" : "no repo") }),
6477
+ (git.ahead > 0 || git.behind > 0) && /* @__PURE__ */ jsxs8("span", { className: "flex shrink-0 items-center gap-1.5 text-og-xs text-og-fg-subtle", children: [
6478
+ git.ahead > 0 && /* @__PURE__ */ jsxs8("span", { children: [
6479
+ "\u2191",
6480
+ git.ahead
6481
+ ] }),
6482
+ git.behind > 0 && /* @__PURE__ */ jsxs8("span", { children: [
6483
+ "\u2193",
6484
+ git.behind
6485
+ ] })
6328
6486
  ] }),
6329
- /* @__PURE__ */ jsx10(
6330
- "button",
6487
+ dirty && /* @__PURE__ */ jsxs8(
6488
+ "span",
6331
6489
  {
6332
- type: "button",
6333
- onClick: onAccept,
6334
- className: "rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg pointer-coarse:min-h-11",
6335
- children: "I understand \u2014 show the desktop"
6490
+ "aria-hidden": "true",
6491
+ "data-contrast-audited": true,
6492
+ className: "ml-auto shrink-0 text-og-xs text-og-fg-subtle",
6493
+ children: [
6494
+ dirtyCount,
6495
+ " changed"
6496
+ ]
6336
6497
  }
6337
6498
  )
6338
6499
  ] });
6339
6500
  }
6340
- function defaultNotice(title, body, onRetry) {
6341
- return /* @__PURE__ */ jsxs8("div", { className: "max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg", children: [
6342
- /* @__PURE__ */ jsxs8("div", { className: "mb-1 flex items-center justify-center gap-1.5 font-medium", children: [
6343
- onRetry && /* @__PURE__ */ jsx10(WifiOffIcon, { className: "size-4 opacity-70", strokeWidth: 1.75 }),
6344
- title
6345
- ] }),
6346
- /* @__PURE__ */ jsx10("p", { className: "text-og-sm text-og-fg-subtle", children: body }),
6347
- onRetry && /* @__PURE__ */ jsx10(
6348
- "button",
6349
- {
6350
- type: "button",
6351
- onClick: onRetry,
6352
- className: "mt-3 rounded-og-sm border border-og-border px-3 py-1.5 text-og-sm transition-colors hover:border-og-accent pointer-coarse:min-h-11",
6353
- children: "Reconnect"
6354
- }
6355
- )
6356
- ] });
6501
+ function Segmented({
6502
+ options,
6503
+ value,
6504
+ onChange
6505
+ }) {
6506
+ return /* @__PURE__ */ jsx10("div", { className: "flex flex-wrap items-center rounded-og-sm border border-og-border p-0.5", children: options.map((opt) => /* @__PURE__ */ jsx10(
6507
+ "button",
6508
+ {
6509
+ type: "button",
6510
+ onClick: () => onChange(opt.value),
6511
+ className: cn(
6512
+ "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
6513
+ opt.value === value ? "bg-og-accent-soft text-og-fg" : "text-og-fg-subtle hover:text-og-fg"
6514
+ ),
6515
+ children: opt.label
6516
+ },
6517
+ opt.value
6518
+ )) });
6357
6519
  }
6358
-
6359
- // src/components/workspace-dock.tsx
6360
- import {
6361
- useCallback as useCallback19,
6362
- useEffect as useEffect20,
6363
- useId as useId3,
6364
- useLayoutEffect as useLayoutEffect2,
6365
- useRef as useRef17,
6366
- useState as useState17
6367
- } from "react";
6368
- import {
6369
- ChevronsLeftRightIcon,
6370
- Maximize2Icon,
6371
- Minimize2Icon,
6372
- PanelRightCloseIcon,
6373
- XIcon as XIcon2
6374
- } from "lucide-react";
6375
- import {
6376
- Group,
6377
- Panel,
6378
- Separator,
6379
- useDefaultLayout,
6380
- usePanelRef
6381
- } from "react-resizable-panels";
6382
- import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
6383
- var SERVER_LAYOUT_STORAGE = {
6384
- getItem: () => null,
6385
- setItem: () => {
6386
- }
6387
- };
6388
- function getLayoutStorage() {
6389
- if (typeof window === "undefined") return SERVER_LAYOUT_STORAGE;
6520
+ function useFileView(path, readFile, reloadRevision = 0) {
6521
+ const [state, setState] = useState17({
6522
+ content: null,
6523
+ isBinary: false,
6524
+ truncated: false,
6525
+ sizeBytes: null,
6526
+ loading: false,
6527
+ error: null
6528
+ });
6529
+ useEffect20(() => {
6530
+ if (!path) {
6531
+ setState({
6532
+ content: null,
6533
+ isBinary: false,
6534
+ truncated: false,
6535
+ sizeBytes: null,
6536
+ loading: false,
6537
+ error: null
6538
+ });
6539
+ return;
6540
+ }
6541
+ let cancelled = false;
6542
+ const abort = new AbortController();
6543
+ setState({
6544
+ content: null,
6545
+ isBinary: false,
6546
+ truncated: false,
6547
+ sizeBytes: null,
6548
+ loading: true,
6549
+ error: null
6550
+ });
6551
+ void readFile(path, { signal: abort.signal }).then((res) => {
6552
+ if (cancelled) return;
6553
+ const content = res.isBinary ? null : res.encoding === "base64" ? decodeBase64Utf8(res.content) : res.content;
6554
+ setState({
6555
+ content,
6556
+ isBinary: res.isBinary,
6557
+ truncated: res.truncated,
6558
+ sizeBytes: res.sizeBytes,
6559
+ loading: false,
6560
+ error: null
6561
+ });
6562
+ }).catch((cause) => {
6563
+ if (cancelled) return;
6564
+ setState({
6565
+ content: null,
6566
+ isBinary: false,
6567
+ truncated: false,
6568
+ sizeBytes: null,
6569
+ loading: false,
6570
+ error: cause instanceof Error ? cause : new Error(String(cause))
6571
+ });
6572
+ });
6573
+ return () => {
6574
+ cancelled = true;
6575
+ abort.abort();
6576
+ };
6577
+ }, [path, readFile, reloadRevision]);
6578
+ return state;
6579
+ }
6580
+ function decodeBase64Utf8(b64) {
6390
6581
  try {
6391
- return window.localStorage;
6582
+ if (typeof atob === "function") {
6583
+ const binary = atob(b64);
6584
+ const bytes = new Uint8Array(binary.length);
6585
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
6586
+ return new TextDecoder().decode(bytes);
6587
+ }
6392
6588
  } catch {
6393
- return SERVER_LAYOUT_STORAGE;
6394
6589
  }
6590
+ return b64;
6395
6591
  }
6396
- var useDockLayoutEffect = typeof window === "undefined" ? useEffect20 : useLayoutEffect2;
6397
- var DOCK_OVERLAY_BREAKPOINT = 1024;
6398
- function useIsNarrow(maxWidth) {
6399
- const [narrow, setNarrow] = useState17(false);
6400
- useDockLayoutEffect(() => {
6401
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
6402
- return;
6403
- }
6404
- const mql = window.matchMedia(`(max-width: ${maxWidth - 1}px)`);
6405
- const update = () => setNarrow(mql.matches);
6406
- update();
6407
- if (typeof mql.addEventListener === "function") {
6408
- mql.addEventListener("change", update);
6409
- return () => mql.removeEventListener("change", update);
6592
+ function Notice({
6593
+ children,
6594
+ className,
6595
+ icon,
6596
+ title,
6597
+ announce
6598
+ }) {
6599
+ return /* @__PURE__ */ jsx10(
6600
+ "div",
6601
+ {
6602
+ role: announce,
6603
+ "aria-atomic": announce ? "true" : void 0,
6604
+ "aria-live": announce === "alert" ? "assertive" : announce === "status" ? "polite" : void 0,
6605
+ className: cn(
6606
+ "flex h-full items-center justify-center p-4 text-center text-og-sm text-og-fg-subtle",
6607
+ className
6608
+ ),
6609
+ children: /* @__PURE__ */ jsxs8("div", { className: "flex max-w-sm flex-col items-center gap-2.5", children: [
6610
+ icon ? /* @__PURE__ */ jsx10("span", { className: "grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-fg-muted shadow-sm", children: icon }) : null,
6611
+ title ? /* @__PURE__ */ jsx10("p", { className: "font-medium text-og-fg", children: title }) : null,
6612
+ /* @__PURE__ */ jsx10("div", { className: "leading-5", children })
6613
+ ] })
6410
6614
  }
6411
- mql.addListener(update);
6412
- return () => mql.removeListener(update);
6413
- }, [maxWidth]);
6414
- return narrow;
6615
+ );
6415
6616
  }
6416
- function WorkspaceDock({
6417
- primary,
6418
- tabs,
6419
- activeTab,
6420
- onActiveTabChange,
6421
- collapsed: collapsedProp,
6422
- onCollapsedChange,
6423
- headerAccessory,
6424
- mobileLeadingControl,
6425
- autoSaveId = "og.session.dock",
6426
- defaultSize = 34,
6427
- minSize = 22,
6428
- maxSize = 70,
6617
+
6618
+ // src/components/desktop-viewer.tsx
6619
+ import { LoaderCircleIcon as LoaderCircleIcon2, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from "lucide-react";
6620
+ import { useEffect as useEffect21, useRef as useRef18, useState as useState18 } from "react";
6621
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
6622
+ var DESKTOP_MEDIA_BACKGROUND = "#000";
6623
+ function isHardUnavailable(reason) {
6624
+ switch (reason) {
6625
+ case "backend_unsupported":
6626
+ case "os_unsupported":
6627
+ case "not_provisioned":
6628
+ case "disabled_by_policy":
6629
+ case "tier_headless":
6630
+ return true;
6631
+ default:
6632
+ return false;
6633
+ }
6634
+ }
6635
+ function DesktopViewer({
6636
+ capability,
6637
+ interactive,
6638
+ showControlToggle = true,
6639
+ scaleViewport,
6640
+ rfbFactory,
6641
+ webSocketFactory,
6642
+ renderConsentGate,
6643
+ onAcknowledge,
6644
+ watching,
6645
+ onWarm,
6646
+ renderUnavailable,
6647
+ renderWarming,
6648
+ renderViewerCap,
6649
+ viewerCapReached,
6650
+ connectTimeoutMs = 13e3,
6429
6651
  className
6430
6652
  }) {
6431
- const narrow = useIsNarrow(DOCK_OVERLAY_BREAKPOINT);
6432
- const rootRef = useRef17(null);
6433
- const reopenRef = useRef17(null);
6434
- const returnFocusRef = useRef17(null);
6435
- const initialNarrowFocusRef = useRef17(false);
6436
- const dockPanelRef = usePanelRef();
6437
- const [internalCollapsed, setInternalCollapsed] = useState17(false);
6438
- const [maximized, setMaximized] = useState17(false);
6439
- const [internalTab, setInternalTab] = useState17(tabs[0]?.id ?? "");
6440
- const collapsed = collapsedProp ?? internalCollapsed;
6441
- const hostControlled = collapsedProp !== void 0;
6442
- const previousCollapsedRef = useRef17(collapsed);
6443
- const { defaultLayout, onLayoutChanged } = useDefaultLayout({
6444
- panelIds: ["primary", "dock"],
6445
- storage: getLayoutStorage(),
6446
- id: autoSaveId
6447
- });
6448
- const requestedTab = activeTab ?? internalTab;
6449
- const tabIds = tabs.map((tab) => tab.id);
6450
- const firstTabId = tabs[0]?.id ?? "";
6451
- const requestedTabIsValid = tabIds.includes(requestedTab);
6452
- const current = requestedTabIsValid ? requestedTab : firstTabId;
6453
- const setTab = useCallback19(
6454
- (id) => {
6455
- setInternalTab(id);
6456
- onActiveTabChange?.(id);
6457
- },
6458
- [onActiveTabChange]
6459
- );
6460
- const setCollapsed = useCallback19(
6461
- (next) => {
6462
- setInternalCollapsed((previous) => previous === next ? previous : next);
6463
- onCollapsedChange?.(next);
6464
- },
6465
- [onCollapsedChange]
6466
- );
6467
- useDockLayoutEffect(() => {
6468
- if (collapsedProp === void 0) {
6469
- return;
6470
- }
6471
- if (collapsedProp) {
6472
- dockPanelRef.current?.collapse();
6473
- setMaximized(false);
6474
- } else {
6475
- dockPanelRef.current?.expand();
6476
- }
6477
- }, [collapsedProp, dockPanelRef]);
6478
- useEffect20(() => {
6479
- const previous = previousCollapsedRef.current;
6480
- previousCollapsedRef.current = collapsed;
6481
- if (previous === collapsed) return;
6482
- if (!collapsed) {
6483
- const active = document.activeElement;
6484
- const workspaceSurface = rootRef.current?.querySelector(
6485
- narrow ? '[role="dialog"]' : "[data-workspace-surface]"
6486
- );
6487
- if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
6488
- returnFocusRef.current = active;
6489
- }
6490
- if (narrow) {
6491
- const frame2 = requestAnimationFrame(() => {
6492
- rootRef.current?.querySelector('[role="dialog"] [role="tab"][aria-selected="true"]')?.focus();
6493
- });
6494
- return () => cancelAnimationFrame(frame2);
6495
- }
6496
- return;
6497
- }
6498
- const frame = requestAnimationFrame(() => {
6499
- if (!hostControlled) {
6500
- reopenRef.current?.focus();
6501
- return;
6502
- }
6503
- const target = returnFocusRef.current;
6504
- if (target?.isConnected) target.focus();
6505
- });
6506
- return () => cancelAnimationFrame(frame);
6507
- }, [collapsed, hostControlled, narrow]);
6508
- useEffect20(() => {
6509
- if (!narrow || collapsed || initialNarrowFocusRef.current) return;
6510
- initialNarrowFocusRef.current = true;
6511
- const active = document.activeElement;
6512
- const workspaceSurface = rootRef.current?.querySelector('[role="dialog"]');
6513
- if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
6514
- returnFocusRef.current = active;
6515
- }
6516
- const frame = requestAnimationFrame(() => {
6517
- workspaceSurface?.querySelector('[role="tab"][aria-selected="true"]')?.focus();
6518
- });
6519
- return () => cancelAnimationFrame(frame);
6520
- }, [collapsed, narrow]);
6521
- useEffect20(() => {
6522
- if (firstTabId && !requestedTabIsValid) {
6523
- setTab(firstTabId);
6524
- }
6525
- }, [firstTabId, requestedTabIsValid, setTab]);
6526
- const collapse = useCallback19(() => {
6527
- dockPanelRef.current?.collapse();
6528
- setCollapsed(true);
6529
- }, [dockPanelRef, setCollapsed]);
6530
- const expand = useCallback19(() => {
6531
- dockPanelRef.current?.expand();
6532
- setCollapsed(false);
6533
- }, [dockPanelRef, setCollapsed]);
6534
- useEffect20(() => {
6535
- const overlayOpen = maximized || narrow && !collapsed;
6536
- if (!overlayOpen) return;
6653
+ const containerRef = useRef18(null);
6654
+ const [consented, setConsented] = useState18(false);
6655
+ const [takeControl, setTakeControl] = useState18(interactive ?? false);
6656
+ const externallyControlled = interactive !== void 0;
6657
+ const serverAllowsControl = capability?.mode !== "read-only";
6658
+ const wantControl = externallyControlled ? interactive : takeControl;
6659
+ const inControl = Boolean(wantControl) && serverAllowsControl;
6660
+ const isWatching = watching ?? consented;
6661
+ const transportNull = !capability || capability.transport === null;
6662
+ const reason = capability?.reason ?? null;
6663
+ const needsAck = capability?.requiresAcknowledgment === true && capability.acknowledged !== true && !consented;
6664
+ const noLiveAddress = Boolean(capability) && !transportNull && !capability.url;
6665
+ const coldWarmable = transportNull && reason === "lease_cold" || !transportNull && noLiveAddress;
6666
+ const hardUnavailable = transportNull && isHardUnavailable(reason);
6667
+ const accept = () => {
6668
+ setConsented(true);
6669
+ onAcknowledge?.();
6670
+ };
6671
+ useEffect21(() => {
6672
+ if (!inControl || externallyControlled) return;
6537
6673
  const onKey = (event) => {
6538
- if (event.key === "Escape") {
6539
- if (maximized) setMaximized(false);
6540
- else collapse();
6541
- return;
6542
- }
6543
- if (event.key !== "Tab") return;
6544
- const surface = rootRef.current?.querySelector(
6545
- narrow ? '[role="dialog"]' : "[data-workspace-surface]"
6546
- );
6547
- if (!surface) return;
6548
- const focusable = Array.from(surface.querySelectorAll("*")).filter(
6549
- (element) => element.tabIndex >= 0 && !element.hasAttribute("disabled") && element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden"
6550
- );
6551
- const first = focusable[0];
6552
- const last = focusable.at(-1);
6553
- const active = document.activeElement;
6554
- if (!first || !last) {
6555
- event.preventDefault();
6556
- surface.focus();
6557
- return;
6558
- }
6559
- if (!surface.contains(active)) {
6560
- event.preventDefault();
6561
- (event.shiftKey ? last : first).focus();
6562
- return;
6563
- }
6564
- if (event.shiftKey && active === first || !event.shiftKey && active === last) {
6674
+ if (event.ctrlKey && event.altKey && event.shiftKey && (event.key === "Control" || event.key === "Alt" || event.key === "Shift")) {
6565
6675
  event.preventDefault();
6566
- (event.shiftKey ? last : first).focus();
6567
- return;
6676
+ setTakeControl(false);
6568
6677
  }
6569
- requestAnimationFrame(() => {
6570
- const next = document.activeElement;
6571
- if (!surface.contains(next)) (event.shiftKey ? last : first).focus();
6572
- });
6573
6678
  };
6574
6679
  window.addEventListener("keydown", onKey);
6575
- return () => window.removeEventListener("keydown", onKey);
6576
- }, [maximized, narrow, collapsed, collapse]);
6577
- if (narrow) {
6578
- return /* @__PURE__ */ jsxs9("div", { ref: rootRef, className: cn("relative flex h-full min-h-0 w-full min-w-0", className), children: [
6579
- /* @__PURE__ */ jsx11(
6580
- "div",
6581
- {
6582
- "data-workspace-primary": true,
6583
- "aria-hidden": !collapsed ? true : void 0,
6584
- className: "min-h-0 min-w-0 flex-1",
6585
- inert: !collapsed ? true : void 0,
6586
- children: primary
6587
- }
6588
- ),
6589
- /* @__PURE__ */ jsx11(
6590
- "div",
6591
- {
6592
- "data-workspace-surface": true,
6593
- role: "dialog",
6594
- "aria-modal": "true",
6595
- "aria-label": "Workspace",
6596
- className: "fixed inset-0 z-40 flex flex-col bg-og-bg",
6597
- hidden: collapsed,
6598
- style: {
6599
- paddingTop: "env(safe-area-inset-top)",
6600
- paddingBottom: "env(safe-area-inset-bottom)"
6601
- },
6602
- children: /* @__PURE__ */ jsx11(
6603
- DockChrome,
6604
- {
6605
- tabs,
6606
- current,
6607
- onTab: setTab,
6608
- leading: mobileLeadingControl,
6609
- accessory: headerAccessory,
6610
- controls: /* @__PURE__ */ jsx11(ChromeButton, { onClick: collapse, title: "Close workspace", label: "Close workspace", children: /* @__PURE__ */ jsx11(XIcon2, { className: "size-4" }) })
6611
- }
6612
- )
6613
- }
6614
- ),
6615
- collapsed && !hostControlled ? /* @__PURE__ */ jsx11(
6616
- "button",
6617
- {
6618
- ref: reopenRef,
6619
- type: "button",
6620
- onClick: expand,
6621
- title: "Open workspace",
6622
- "aria-label": "Open workspace",
6623
- className: "absolute right-3 top-3 z-30 inline-flex size-11 items-center justify-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted shadow-lg transition-colors hover:border-og-border-strong hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent",
6624
- style: { marginTop: "env(safe-area-inset-top)" },
6625
- children: /* @__PURE__ */ jsx11(ChevronsLeftRightIcon, { className: "size-4", "aria-hidden": true })
6626
- }
6627
- ) : null
6628
- ] });
6680
+ return () => {
6681
+ window.removeEventListener("keydown", onKey);
6682
+ };
6683
+ }, [inControl, externallyControlled]);
6684
+ const connectCapability = !transportNull && !needsAck && !viewerCapReached ? capability : null;
6685
+ const stream = useDesktopStream({
6686
+ capability: connectCapability,
6687
+ containerRef,
6688
+ interactive: inControl,
6689
+ ...scaleViewport !== void 0 ? { scaleViewport } : {},
6690
+ ...rfbFactory ? { rfbFactory } : {},
6691
+ ...webSocketFactory ? { webSocketFactory } : {}
6692
+ });
6693
+ const connected = stream.state === "connected";
6694
+ const hasLiveUrl = Boolean(connectCapability?.url);
6695
+ const streamStateRef = useRef18(stream.state);
6696
+ streamStateRef.current = stream.state;
6697
+ const warmKeyRef = useRef18(null);
6698
+ useEffect21(() => {
6699
+ if (!isWatching || !coldWarmable || needsAck || viewerCapReached) {
6700
+ if (!coldWarmable) warmKeyRef.current = null;
6701
+ return;
6702
+ }
6703
+ const warm = onWarm ?? onAcknowledge;
6704
+ if (!warm) return;
6705
+ const key = `${capability?.reason ?? ""}:${capability?.url ?? ""}:${capability?.expiresAt ?? ""}`;
6706
+ if (warmKeyRef.current === key) return;
6707
+ warmKeyRef.current = key;
6708
+ warm();
6709
+ }, [isWatching, coldWarmable, needsAck, viewerCapReached, capability, onWarm, onAcknowledge]);
6710
+ useEffect21(() => {
6711
+ if (!hasLiveUrl || typeof document === "undefined") return;
6712
+ const maybeReattach = () => {
6713
+ if (document.visibilityState !== "visible") return;
6714
+ if (streamStateRef.current === "idle") stream.reconnect();
6715
+ };
6716
+ document.addEventListener("visibilitychange", maybeReattach);
6717
+ return () => document.removeEventListener("visibilitychange", maybeReattach);
6718
+ }, [hasLiveUrl, connectCapability?.url]);
6719
+ const [connectTimedOut, setConnectTimedOut] = useState18(false);
6720
+ const [reconnectNonce, setReconnectNonce] = useState18(0);
6721
+ useEffect21(() => {
6722
+ setConnectTimedOut(false);
6723
+ if (!hasLiveUrl || connected || stream.error || connectTimeoutMs <= 0) return;
6724
+ const timer = setTimeout(() => setConnectTimedOut(true), connectTimeoutMs);
6725
+ return () => clearTimeout(timer);
6726
+ }, [
6727
+ hasLiveUrl,
6728
+ connected,
6729
+ connectTimeoutMs,
6730
+ connectCapability?.url,
6731
+ stream.error,
6732
+ reconnectNonce
6733
+ ]);
6734
+ const reconnect = () => {
6735
+ setConnectTimedOut(false);
6736
+ setReconnectNonce((n) => n + 1);
6737
+ stream.reconnect();
6738
+ };
6739
+ let uiState;
6740
+ if (viewerCapReached) uiState = "viewer_cap";
6741
+ else if (hardUnavailable) uiState = "unavailable";
6742
+ else if (needsAck && !isWatching) uiState = "consent";
6743
+ else if (coldWarmable) uiState = "warming";
6744
+ else if (connected) uiState = "connected";
6745
+ else if (stream.error) uiState = "error";
6746
+ else if (connectTimedOut) uiState = "connect_failed";
6747
+ else uiState = "connecting";
6748
+ const overlayShown = uiState !== "connected" && uiState !== "connecting";
6749
+ const showToggle = showControlToggle && !overlayShown;
6750
+ let overlay = null;
6751
+ switch (uiState) {
6752
+ case "viewer_cap":
6753
+ overlay = renderViewerCap?.() ?? defaultNotice(
6754
+ "Too many viewers",
6755
+ "This session has reached its live-viewer limit. Try again shortly."
6756
+ );
6757
+ break;
6758
+ case "unavailable":
6759
+ overlay = renderUnavailable?.(reason) ?? defaultNotice("Desktop unavailable", unavailableCopy(reason));
6760
+ break;
6761
+ case "consent":
6762
+ overlay = renderConsentGate ? renderConsentGate(accept, capability?.shared ?? false) : /* @__PURE__ */ jsx11(DefaultConsentGate, { shared: capability?.shared ?? false, onAccept: accept });
6763
+ break;
6764
+ case "warming":
6765
+ overlay = renderWarming?.() ?? /* @__PURE__ */ jsx11(WarmingNotice, {});
6766
+ break;
6767
+ case "connect_failed":
6768
+ overlay = defaultNotice(
6769
+ "Couldn\u2019t connect",
6770
+ "The desktop is warm but the live stream didn\u2019t come up. This usually clears on a retry.",
6771
+ reconnect
6772
+ );
6773
+ break;
6774
+ case "error":
6775
+ overlay = defaultNotice(
6776
+ "Desktop disconnected",
6777
+ stream.error?.message ?? "The stream dropped.",
6778
+ reconnect
6779
+ );
6780
+ break;
6781
+ case "connecting":
6782
+ case "connected":
6783
+ overlay = null;
6784
+ break;
6629
6785
  }
6630
- const dockChrome = /* @__PURE__ */ jsx11(
6631
- DockChrome,
6786
+ return /* @__PURE__ */ jsxs9(
6787
+ "div",
6632
6788
  {
6633
- tabs,
6634
- current,
6635
- onTab: setTab,
6636
- accessory: headerAccessory,
6637
- controls: /* @__PURE__ */ jsxs9(Fragment4, { children: [
6789
+ className: cn(
6790
+ "relative h-full w-full overflow-hidden",
6791
+ inControl && "ring-2 ring-inset ring-og-accent",
6792
+ className
6793
+ ),
6794
+ style: { backgroundColor: DESKTOP_MEDIA_BACKGROUND },
6795
+ "data-opengeni-desktop": true,
6796
+ "data-state": stream.state,
6797
+ "data-ui-state": uiState,
6798
+ "data-in-control": inControl ? "true" : void 0,
6799
+ children: [
6638
6800
  /* @__PURE__ */ jsx11(
6639
- ChromeButton,
6801
+ "div",
6640
6802
  {
6641
- onClick: () => setMaximized((m) => !m),
6642
- title: maximized ? "Restore (Esc)" : "Maximize",
6643
- label: maximized ? "Restore dock" : "Maximize dock",
6644
- children: maximized ? /* @__PURE__ */ jsx11(Minimize2Icon, { className: "size-3.5" }) : /* @__PURE__ */ jsx11(Maximize2Icon, { className: "size-3.5" })
6803
+ ref: containerRef,
6804
+ className: "absolute inset-0 overflow-hidden",
6805
+ "data-opengeni-desktop-canvas": true,
6806
+ "data-state": stream.state
6645
6807
  }
6646
6808
  ),
6647
- hostControlled ? null : /* @__PURE__ */ jsx11(ChromeButton, { onClick: collapse, title: "Collapse", label: "Collapse dock", children: /* @__PURE__ */ jsx11(PanelRightCloseIcon, { className: "size-3.5" }) })
6648
- ] })
6809
+ uiState === "connecting" && /* @__PURE__ */ jsxs9("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-3 text-og-fg-subtle", children: [
6810
+ /* @__PURE__ */ jsxs9("span", { className: "relative flex items-center justify-center", children: [
6811
+ /* @__PURE__ */ jsx11(MonitorIcon, { className: "size-8 opacity-30", strokeWidth: 1.5 }),
6812
+ /* @__PURE__ */ jsx11(
6813
+ LoaderCircleIcon2,
6814
+ {
6815
+ className: "absolute size-12 animate-og-spin text-og-accent opacity-70",
6816
+ strokeWidth: 1.25
6817
+ }
6818
+ )
6819
+ ] }),
6820
+ /* @__PURE__ */ jsx11("span", { className: "text-og-sm", children: "Connecting to the desktop\u2026" })
6821
+ ] }),
6822
+ showToggle && !externallyControlled && inControl && /* @__PURE__ */ jsx11(
6823
+ InControlBar,
6824
+ {
6825
+ shared: capability?.shared ?? false,
6826
+ onRelease: () => setTakeControl(false)
6827
+ }
6828
+ ),
6829
+ showToggle && !externallyControlled && !inControl && connected && /* @__PURE__ */ jsx11(
6830
+ TakeControlCallToAction,
6831
+ {
6832
+ disabled: !serverAllowsControl,
6833
+ disabledReason: !serverAllowsControl ? capability?.client === "frames" ? "View-only \u2014 live control isn't available for this machine yet." : "This deployment streams the desktop read-only." : void 0,
6834
+ onTakeControl: () => setTakeControl(true)
6835
+ }
6836
+ ),
6837
+ overlay && /* @__PURE__ */ jsx11("div", { className: "absolute inset-0 flex items-center justify-center p-4", children: overlay })
6838
+ ]
6649
6839
  }
6650
6840
  );
6651
- return /* @__PURE__ */ jsxs9("div", { ref: rootRef, className: cn("relative flex h-full min-h-0 w-full min-w-0", className), children: [
6652
- /* @__PURE__ */ jsxs9(
6653
- Group,
6841
+ }
6842
+ function TakeControlCallToAction({
6843
+ disabled,
6844
+ disabledReason,
6845
+ onTakeControl
6846
+ }) {
6847
+ return (
6848
+ // The wrapper spans the surface but is click-through (pointer-events-none); only
6849
+ // the button itself is interactive, so watchers can still see the desktop.
6850
+ /* @__PURE__ */ jsx11("div", { className: "pointer-events-none absolute inset-0 flex items-end justify-center pb-[8%]", children: /* @__PURE__ */ jsxs9(
6851
+ "button",
6654
6852
  {
6655
- orientation: "horizontal",
6656
- className: "min-h-0 flex-1",
6657
- ...defaultLayout ? { defaultLayout } : {},
6658
- onLayoutChanged,
6853
+ type: "button",
6854
+ disabled,
6855
+ "aria-label": "Take control of the desktop",
6856
+ title: disabled ? disabledReason : "Take control of the desktop",
6857
+ onClick: onTakeControl,
6858
+ className: cn(
6859
+ "group pointer-events-auto flex items-center gap-3 rounded-og-lg border px-5 py-3",
6860
+ "border-og-border",
6861
+ "bg-og-bg/85 backdrop-blur-md",
6862
+ "shadow-og-lg",
6863
+ "outline-none transition-all duration-150 ease-out",
6864
+ "focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg",
6865
+ disabled ? "cursor-not-allowed opacity-60" : cn(
6866
+ "cursor-pointer opacity-90 hover:-translate-y-0.5 hover:opacity-100",
6867
+ "hover:border-og-accent",
6868
+ "hover:bg-og-accent/10"
6869
+ )
6870
+ ),
6659
6871
  children: [
6660
- /* @__PURE__ */ jsx11(Panel, { id: "primary", minSize: "30%", className: "min-h-0 min-w-0", children: /* @__PURE__ */ jsx11(
6661
- "div",
6662
- {
6663
- "data-workspace-primary": true,
6664
- "aria-hidden": maximized ? true : void 0,
6665
- className: "h-full min-h-0 min-w-0",
6666
- inert: maximized ? true : void 0,
6667
- children: primary
6668
- }
6669
- ) }),
6670
- !collapsed && /* @__PURE__ */ jsx11(Separator, { className: "group relative z-10 w-1.5 shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent max-[1023px]:-mx-[19px] max-[1023px]:w-11 pointer-coarse:-mx-[19px] pointer-coarse:w-11", children: /* @__PURE__ */ jsx11("span", { className: "absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-og-border transition-colors group-hover:bg-og-accent group-focus-visible:bg-og-accent group-data-[separator-state=dragging]:bg-og-accent" }) }),
6671
6872
  /* @__PURE__ */ jsx11(
6672
- Panel,
6873
+ "span",
6673
6874
  {
6674
- id: "dock",
6675
- panelRef: dockPanelRef,
6676
- collapsible: true,
6677
- collapsedSize: "0%",
6678
- defaultSize: `${defaultSize}%`,
6679
- minSize: `${minSize}%`,
6680
- maxSize: `${maxSize}%`,
6681
- onResize: (size, _id, previousSize) => {
6682
- const isCollapsed = size.asPercentage <= 1;
6683
- const canInferCollapse = collapsedProp === void 0 || previousSize !== void 0;
6684
- if (canInferCollapse && isCollapsed !== collapsed) {
6685
- setCollapsed(isCollapsed);
6686
- }
6687
- },
6688
- className: "min-h-0 min-w-0",
6689
- children: /* @__PURE__ */ jsx11(
6690
- "div",
6691
- {
6692
- "data-workspace-surface": true,
6693
- role: maximized ? "dialog" : void 0,
6694
- "aria-modal": maximized ? true : void 0,
6695
- "aria-label": maximized ? "Workspace" : void 0,
6696
- "aria-hidden": collapsed ? true : void 0,
6697
- className: cn(
6698
- "flex h-full min-h-0 min-w-0 flex-col bg-og-bg",
6699
- maximized ? "fixed inset-0 z-40" : "border-l border-og-border",
6700
- collapsed && "invisible pointer-events-none"
6701
- ),
6702
- children: dockChrome
6703
- }
6704
- )
6875
+ className: cn(
6876
+ "flex size-9 shrink-0 items-center justify-center rounded-full transition-colors",
6877
+ "bg-og-accent",
6878
+ "text-og-accent-fg",
6879
+ disabled ? "" : "group-hover:scale-105"
6880
+ ),
6881
+ children: /* @__PURE__ */ jsx11(MousePointerClickIcon, { className: "size-5", strokeWidth: 2 })
6705
6882
  }
6706
- )
6883
+ ),
6884
+ /* @__PURE__ */ jsxs9("span", { className: "flex flex-col items-start leading-tight", children: [
6885
+ /* @__PURE__ */ jsx11("span", { className: "text-og-base font-semibold text-og-fg", children: "Take control" }),
6886
+ /* @__PURE__ */ jsx11("span", { className: "text-og-xs text-og-fg-subtle", children: disabled && disabledReason ? disabledReason : "Drive the mouse & keyboard" })
6887
+ ] })
6707
6888
  ]
6708
6889
  }
6709
- ),
6710
- collapsed && !maximized && !hostControlled && /* @__PURE__ */ jsx11(
6711
- "button",
6712
- {
6713
- ref: reopenRef,
6714
- type: "button",
6715
- onClick: expand,
6716
- title: "Open workspace",
6717
- className: "absolute inset-y-0 right-0 flex w-6 shrink-0 items-center justify-center border-l border-og-border bg-og-surface-1 text-og-fg-subtle hover:text-og-fg",
6718
- children: /* @__PURE__ */ jsx11(ChevronsLeftRightIcon, { className: "size-3.5" })
6719
- }
6720
- )
6721
- ] });
6722
- }
6723
- function ChromeButton({
6724
- onClick,
6725
- title,
6726
- label,
6727
- children
6728
- }) {
6729
- return /* @__PURE__ */ jsx11(
6730
- "button",
6731
- {
6732
- type: "button",
6733
- onClick,
6734
- title,
6735
- "aria-label": label,
6736
- className: "inline-flex size-7 items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:size-11 pointer-coarse:size-11",
6737
- children
6738
- }
6890
+ ) })
6739
6891
  );
6740
6892
  }
6741
- function DockChrome({
6742
- tabs,
6743
- current,
6744
- onTab,
6745
- leading,
6746
- accessory,
6747
- controls
6748
- }) {
6749
- const active = tabs.find((t) => t.id === current) ?? tabs[0];
6750
- const tabsetId = useId3();
6751
- const tabRefs = useRef17([]);
6752
- const tabListRef = useRef17(null);
6753
- const [visitedTabs, setVisitedTabs] = useState17(() => /* @__PURE__ */ new Set());
6754
- const [tabOverflow, setTabOverflow] = useState17({ start: false, end: false });
6755
- const activeId = active?.id ?? "";
6756
- const activeTabIndex = tabs.findIndex((tab) => tab.id === activeId);
6757
- const keepActiveTabVisible = useCallback19(() => {
6758
- const selected = activeTabIndex >= 0 ? tabRefs.current[activeTabIndex] : null;
6759
- selected?.scrollIntoView({ block: "nearest", inline: "nearest" });
6760
- }, [activeTabIndex]);
6761
- useEffect20(() => {
6762
- if (!activeId) return;
6763
- setVisitedTabs((previous) => {
6764
- if (previous.has(activeId)) return previous;
6765
- const next = new Set(previous);
6766
- next.add(activeId);
6767
- return next;
6768
- });
6769
- }, [activeId]);
6770
- useDockLayoutEffect(() => {
6771
- keepActiveTabVisible();
6772
- }, [activeId, keepActiveTabVisible]);
6773
- useDockLayoutEffect(() => {
6774
- const list = tabListRef.current;
6775
- if (!list) return;
6776
- const update = () => {
6777
- const max = Math.max(0, list.scrollWidth - list.clientWidth);
6778
- const next = { start: list.scrollLeft > 1, end: list.scrollLeft < max - 1 };
6779
- setTabOverflow(
6780
- (previous) => previous.start === next.start && previous.end === next.end ? previous : next
6781
- );
6782
- };
6783
- update();
6784
- list.addEventListener("scroll", update, { passive: true });
6785
- const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
6786
- keepActiveTabVisible();
6787
- update();
6788
- });
6789
- observer?.observe(list);
6790
- for (const tab of tabRefs.current) {
6791
- if (tab) observer?.observe(tab);
6792
- }
6793
- return () => {
6794
- list.removeEventListener("scroll", update);
6795
- observer?.disconnect();
6796
- };
6797
- }, [activeId, keepActiveTabVisible, tabs.length]);
6798
- const tabMask = tabOverflow.start && tabOverflow.end ? "linear-gradient(to right, transparent 0, black 14px, black calc(100% - 14px), transparent 100%)" : tabOverflow.start ? "linear-gradient(to right, transparent 0, black 14px, black 100%)" : tabOverflow.end ? "linear-gradient(to right, black 0, black calc(100% - 14px), transparent 100%)" : void 0;
6799
- const activateTab = (index) => {
6800
- const tab = tabs[index];
6801
- if (!tab) {
6802
- return;
6803
- }
6804
- onTab(tab.id);
6805
- tabRefs.current[index]?.focus();
6806
- };
6807
- const onTabKeyDown = (event, index) => {
6808
- if (tabs.length === 0) return;
6809
- let nextIndex = null;
6810
- if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
6811
- else if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
6812
- else if (event.key === "Home") nextIndex = 0;
6813
- else if (event.key === "End") nextIndex = tabs.length - 1;
6814
- if (nextIndex === null) return;
6815
- event.preventDefault();
6816
- activateTab(nextIndex);
6817
- };
6818
- return /* @__PURE__ */ jsxs9(Fragment4, { children: [
6819
- /* @__PURE__ */ jsxs9("div", { className: "grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0 min-[640px]:max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto_auto]", children: [
6820
- /* @__PURE__ */ jsx11("div", { className: "flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11 min-[640px]:max-[1023px]:col-start-1 min-[640px]:max-[1023px]:row-start-1", children: leading ?? /* @__PURE__ */ jsx11("span", { className: "hidden truncate px-1 text-og-sm font-semibold text-og-fg max-[1023px]:inline", children: "Workspace" }) }),
6821
- /* @__PURE__ */ jsx11(
6822
- "div",
6893
+ function InControlBar({ shared, onRelease }) {
6894
+ return /* @__PURE__ */ jsxs9("div", { className: "pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between gap-2 p-2", children: [
6895
+ /* @__PURE__ */ jsxs9("span", { className: "pointer-events-auto inline-flex items-center gap-2 rounded-og-sm bg-og-accent px-2.5 py-1 text-og-xs font-medium text-og-accent-fg shadow-og-md", children: [
6896
+ /* @__PURE__ */ jsx11("span", { className: "size-1.5 animate-pulse rounded-full bg-current", "aria-hidden": true }),
6897
+ "You're in control",
6898
+ /* @__PURE__ */ jsx11("span", { className: "opacity-75", children: "\xB7 click Return control (or Ctrl+Alt+Shift)" })
6899
+ ] }),
6900
+ /* @__PURE__ */ jsxs9("div", { className: "pointer-events-auto flex items-center gap-1.5", children: [
6901
+ shared && /* @__PURE__ */ jsx11("span", { className: "rounded-og-sm bg-og-status-failed/85 px-2 py-0.5 text-og-xs text-og-accent-fg", children: "Shared sandbox \u2014 others are watching" }),
6902
+ /* @__PURE__ */ jsxs9(
6903
+ "button",
6823
6904
  {
6824
- ref: tabListRef,
6825
- className: "flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-[1023px]:col-span-3 max-[1023px]:row-start-2 max-[1023px]:w-full min-[640px]:max-[1023px]:col-span-1 min-[640px]:max-[1023px]:col-start-2 min-[640px]:max-[1023px]:row-start-1",
6826
- role: "tablist",
6827
- "aria-orientation": "horizontal",
6828
- style: {
6829
- maskImage: tabMask,
6830
- WebkitMaskImage: tabMask,
6831
- scrollPaddingInline: "16px"
6832
- },
6833
- children: tabs.map((tab, index) => /* @__PURE__ */ jsxs9(
6834
- "button",
6835
- {
6836
- ref: (node) => {
6837
- tabRefs.current[index] = node;
6838
- },
6839
- id: `${tabsetId}-tab-${index}`,
6840
- type: "button",
6841
- role: "tab",
6842
- "aria-selected": tab.id === current,
6843
- "aria-controls": `${tabsetId}-panel-${index}`,
6844
- tabIndex: tab.id === current ? 0 : -1,
6845
- onClick: () => onTab(tab.id),
6846
- onKeyDown: (event) => onTabKeyDown(event, index),
6847
- className: cn(
6848
- "flex min-h-7 shrink-0 items-center justify-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
6849
- tab.id === current ? "bg-og-accent-soft text-og-fg" : "text-og-fg-subtle hover:text-og-fg"
6850
- ),
6851
- children: [
6852
- /* @__PURE__ */ jsx11("span", { "data-contrast-audited": true, children: tab.label }),
6853
- tab.badge
6854
- ]
6855
- },
6856
- tab.id
6857
- ))
6905
+ type: "button",
6906
+ onClick: onRelease,
6907
+ title: "Return control (or press Ctrl+Alt+Shift)",
6908
+ className: cn(
6909
+ "inline-flex items-center gap-1.5 rounded-og-sm border px-2.5 py-1 text-og-xs font-semibold shadow-og-md transition-colors",
6910
+ "border-og-accent bg-og-bg/90 text-og-fg backdrop-blur-sm",
6911
+ "outline-none hover:bg-og-accent hover:text-og-accent-fg focus-visible:ring-2 focus-visible:ring-og-accent"
6912
+ ),
6913
+ children: [
6914
+ /* @__PURE__ */ jsx11(MonitorIcon, { className: "size-3.5", strokeWidth: 2, "aria-hidden": true }),
6915
+ "Return control"
6916
+ ]
6858
6917
  }
6859
- ),
6860
- /* @__PURE__ */ jsx11("div", { className: "flex min-w-0 shrink-0 items-center justify-self-end min-[640px]:max-[1023px]:col-start-3 min-[640px]:max-[1023px]:row-start-1", children: accessory }),
6861
- /* @__PURE__ */ jsx11("div", { className: "flex shrink-0 items-center gap-0.5 text-og-fg-subtle min-[640px]:max-[1023px]:col-start-4 min-[640px]:max-[1023px]:row-start-1", children: controls })
6918
+ )
6919
+ ] })
6920
+ ] });
6921
+ }
6922
+ function unavailableCopy(reason) {
6923
+ switch (reason) {
6924
+ case "backend_unsupported":
6925
+ return "This sandbox backend cannot stream a desktop.";
6926
+ case "tier_headless":
6927
+ return "This deployment is headless \u2014 terminal, files, and diff only.";
6928
+ case "os_unsupported":
6929
+ return "The sandbox OS does not support a desktop stream.";
6930
+ case "not_provisioned":
6931
+ return "This sandbox doesn't have a desktop yet.";
6932
+ case "disabled_by_policy":
6933
+ return "Desktop streaming is disabled on this deployment.";
6934
+ case "lease_cold":
6935
+ return "Waiting for the sandbox to start\u2026";
6936
+ default:
6937
+ return "The desktop isn\u2019t available for this sandbox.";
6938
+ }
6939
+ }
6940
+ function WarmingNotice({ onRetry }) {
6941
+ return /* @__PURE__ */ jsxs9("div", { className: "flex max-w-sm flex-col items-center gap-3 rounded-og-lg border border-og-border bg-og-bg/90 p-5 text-center text-og-base text-og-fg backdrop-blur-sm", children: [
6942
+ /* @__PURE__ */ jsx11(LoaderCircleIcon2, { className: "size-7 animate-og-spin text-og-accent", strokeWidth: 1.5 }),
6943
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-1", children: [
6944
+ /* @__PURE__ */ jsx11("div", { className: "font-medium", children: "Warming the sandbox\u2026" }),
6945
+ /* @__PURE__ */ jsx11("p", { className: "text-og-sm text-og-fg-subtle", children: "Spinning up the desktop \u2014 this takes a few seconds." })
6862
6946
  ] }),
6863
- /* @__PURE__ */ jsx11("div", { className: "min-h-0 min-w-0 flex-1 overflow-hidden", children: tabs.length > 0 ? tabs.map((tab, index) => {
6864
- const selected = tab.id === activeId;
6865
- const shouldMount = selected || visitedTabs.has(tab.id);
6866
- return /* @__PURE__ */ jsx11(
6867
- "div",
6868
- {
6869
- id: `${tabsetId}-panel-${index}`,
6870
- "aria-labelledby": `${tabsetId}-tab-${index}`,
6871
- className: "h-full min-h-0 min-w-0 overflow-hidden",
6872
- hidden: !selected,
6873
- role: "tabpanel",
6874
- children: shouldMount ? tab.content : null
6875
- },
6876
- tab.id
6877
- );
6878
- }) : /* @__PURE__ */ jsx11("div", { className: "h-full min-h-0 min-w-0", "aria-label": "Workspace", role: "tabpanel" }) })
6947
+ onRetry && /* @__PURE__ */ jsx11(
6948
+ "button",
6949
+ {
6950
+ type: "button",
6951
+ onClick: onRetry,
6952
+ className: "rounded-og-sm border border-og-border px-3 py-1.5 text-og-sm text-og-fg-muted transition-colors hover:text-og-fg pointer-coarse:min-h-11",
6953
+ children: "Taking too long? Retry"
6954
+ }
6955
+ )
6956
+ ] });
6957
+ }
6958
+ function DefaultConsentGate({ shared, onAccept }) {
6959
+ return /* @__PURE__ */ jsxs9("div", { className: "max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg", children: [
6960
+ /* @__PURE__ */ jsx11("div", { className: "mb-1 font-medium", children: "Watch the live desktop?" }),
6961
+ /* @__PURE__ */ jsxs9("p", { className: "mb-3 text-og-sm text-og-fg-subtle", children: [
6962
+ "The desktop pixel stream is ",
6963
+ /* @__PURE__ */ jsx11("strong", { children: "un-redacted" }),
6964
+ " \u2014 it can show secrets the agent prints on screen.",
6965
+ shared ? " This sandbox is shared: you'll also see other sessions' agents on the same screen." : ""
6966
+ ] }),
6967
+ /* @__PURE__ */ jsx11(
6968
+ "button",
6969
+ {
6970
+ type: "button",
6971
+ onClick: onAccept,
6972
+ className: "rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg pointer-coarse:min-h-11",
6973
+ children: "I understand \u2014 show the desktop"
6974
+ }
6975
+ )
6976
+ ] });
6977
+ }
6978
+ function defaultNotice(title, body, onRetry) {
6979
+ return /* @__PURE__ */ jsxs9("div", { className: "max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg", children: [
6980
+ /* @__PURE__ */ jsxs9("div", { className: "mb-1 flex items-center justify-center gap-1.5 font-medium", children: [
6981
+ onRetry && /* @__PURE__ */ jsx11(WifiOffIcon, { className: "size-4 opacity-70", strokeWidth: 1.75 }),
6982
+ title
6983
+ ] }),
6984
+ /* @__PURE__ */ jsx11("p", { className: "text-og-sm text-og-fg-subtle", children: body }),
6985
+ onRetry && /* @__PURE__ */ jsx11(
6986
+ "button",
6987
+ {
6988
+ type: "button",
6989
+ onClick: onRetry,
6990
+ className: "mt-3 rounded-og-sm border border-og-border px-3 py-1.5 text-og-sm transition-colors hover:border-og-accent pointer-coarse:min-h-11",
6991
+ children: "Reconnect"
6992
+ }
6993
+ )
6879
6994
  ] });
6880
6995
  }
6881
6996
 
6882
- // src/components/sandbox-workspace.tsx
6883
- import { useCallback as useCallback22, useEffect as useEffect23, useMemo as useMemo11, useRef as useRef20, useState as useState20 } from "react";
6884
- import { Popover } from "radix-ui";
6885
- import {
6886
- CircleCheckIcon,
6887
- CpuIcon,
6888
- LaptopIcon,
6889
- LoaderCircleIcon as LoaderCircleIcon3,
6890
- RefreshCwIcon as RefreshCwIcon2,
6891
- TriangleAlertIcon as TriangleAlertIcon2
6892
- } from "lucide-react";
6893
-
6894
- // src/components/workbench-changes.tsx
6895
- import { ArrowUpRightIcon, FileWarningIcon as FileWarningIcon2, HistoryIcon } from "lucide-react";
6997
+ // src/components/workspace-dock.tsx
6896
6998
  import {
6897
6999
  useCallback as useCallback21,
6898
7000
  useEffect as useEffect22,
6899
7001
  useId as useId4,
6900
- useLayoutEffect as useLayoutEffect3,
6901
- useMemo as useMemo10,
6902
- useRef as useRef19,
6903
- useState as useState19
6904
- } from "react";
6905
- import { VList as VList2 } from "virtua";
6906
-
6907
- // src/hooks/use-windowed-sections.ts
6908
- import { useCallback as useCallback20, useEffect as useEffect21, useMemo as useMemo9, useRef as useRef18, useState as useState18 } from "react";
6909
- function computeWindowRange(heights, scrollTop, viewport, overscan) {
6910
- const n = heights.length;
6911
- if (n === 0) return { start: 0, end: 0 };
6912
- const top = Math.max(scrollTop, 0);
6913
- const bottom = top + Math.max(viewport, 0);
6914
- let firstVisible = -1;
6915
- let lastVisible = -1;
6916
- let acc = 0;
6917
- let anchor = 0;
6918
- for (let i = 0; i < n; i++) {
6919
- const secTop = acc;
6920
- const secBottom = secTop + Math.max(heights[i] ?? 0, 0);
6921
- if (secTop <= top) anchor = i;
6922
- if (secBottom > top && secTop < bottom) {
6923
- if (firstVisible === -1) firstVisible = i;
6924
- lastVisible = i;
6925
- }
6926
- acc = secBottom;
6927
- }
6928
- if (firstVisible === -1) {
6929
- firstVisible = anchor;
6930
- lastVisible = anchor;
6931
- }
6932
- return {
6933
- start: Math.max(0, firstVisible - overscan),
6934
- end: Math.min(n, lastVisible + 1 + overscan)
6935
- };
6936
- }
6937
- function useWindowedSections(opts) {
6938
- const { count, estimateHeight, overscan = 3 } = opts;
6939
- const scrollRef = useRef18(null);
6940
- const [heights, setHeights] = useState18(
6941
- () => Array.from({ length: count }, (_, i) => estimateHeight(i))
6942
- );
6943
- useEffect21(() => {
6944
- setHeights((prev) => {
6945
- if (prev.length === count) return prev;
6946
- return Array.from({ length: count }, (_, i) => prev[i] ?? estimateHeight(i));
6947
- });
6948
- }, [count]);
6949
- const heightsRef = useRef18(heights);
6950
- heightsRef.current = heights;
6951
- const [range, setRange] = useState18(() => ({
6952
- start: 0,
6953
- end: Math.min(count, overscan + 1)
6954
- }));
6955
- const recompute = useCallback20(() => {
6956
- const el = scrollRef.current;
6957
- if (!el) return;
6958
- const next = computeWindowRange(heightsRef.current, el.scrollTop, el.clientHeight, overscan);
6959
- setRange((prev) => prev.start === next.start && prev.end === next.end ? prev : next);
6960
- }, [overscan]);
6961
- useEffect21(() => {
6962
- const el = scrollRef.current;
6963
- if (!el) return;
6964
- let raf = 0;
6965
- const onScroll = () => {
6966
- if (raf) return;
6967
- raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame(() => {
6968
- raf = 0;
6969
- recompute();
6970
- }) : (recompute(), 0);
6971
- };
6972
- el.addEventListener("scroll", onScroll, { passive: true });
6973
- const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => recompute()) : null;
6974
- ro?.observe(el);
6975
- recompute();
6976
- return () => {
6977
- el.removeEventListener("scroll", onScroll);
6978
- ro?.disconnect();
6979
- if (raf && typeof cancelAnimationFrame === "function") cancelAnimationFrame(raf);
6980
- };
6981
- }, [recompute, count]);
6982
- const measure = useCallback20((index, height) => {
6983
- if (height <= 0) return;
6984
- setHeights((prev) => {
6985
- if (Math.abs((prev[index] ?? 0) - height) < 1) return prev;
6986
- const next = prev.slice();
6987
- next[index] = height;
6988
- return next;
6989
- });
6990
- }, []);
6991
- const offsets = useMemo9(() => {
6992
- const out = new Array(count + 1);
6993
- out[0] = 0;
6994
- for (let i = 0; i < count; i++) out[i + 1] = (out[i] ?? 0) + Math.max(heights[i] ?? 0, 0);
6995
- return out;
6996
- }, [heights, count]);
6997
- useEffect21(() => {
6998
- recompute();
6999
- }, [offsets, recompute]);
7000
- const scrollToIndex = useCallback20(
7001
- (index) => {
7002
- const el = scrollRef.current;
7003
- if (!el) return;
7004
- const clamped = Math.max(0, Math.min(count - 1, index));
7005
- el.scrollTop = offsets[clamped] ?? 0;
7006
- },
7007
- [offsets, count]
7008
- );
7009
- return {
7010
- scrollRef,
7011
- range,
7012
- offsets,
7013
- totalHeight: offsets[count] ?? 0,
7014
- measure,
7015
- scrollToIndex
7016
- };
7017
- }
7018
-
7019
- // src/components/workbench-changes.tsx
7020
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
7021
- var GROUP_THRESHOLD = 20;
7022
- var OVERSCAN = 2;
7023
- var HEADER_PX = 30;
7024
- var LINE_PX = 18;
7025
- var GUARD_BODY_PX = 52;
7026
- var COMPACT_SURFACE_PX = 560;
7027
- var useChangesLayoutEffect = typeof window === "undefined" ? useEffect22 : useLayoutEffect3;
7028
- var STATUS_TINT2 = {
7029
- added: "text-og-status-idle",
7030
- modified: "text-og-status-running",
7031
- deleted: "text-og-status-failed",
7032
- renamed: "text-og-accent",
7033
- copied: "text-og-accent",
7034
- untracked: "text-og-fg-subtle",
7035
- ignored: "text-og-fg-subtle",
7036
- conflicted: "text-og-status-failed",
7037
- typechange: "text-og-status-running"
7038
- };
7039
- var STATUS_LETTER = {
7040
- added: "A",
7041
- modified: "M",
7042
- deleted: "D",
7043
- renamed: "R",
7044
- copied: "C",
7045
- untracked: "U",
7046
- ignored: "I",
7047
- conflicted: "!",
7048
- typechange: "T"
7049
- };
7050
- function compactFileLabel(file) {
7051
- const slash = file.path.lastIndexOf("/");
7052
- const basename = slash >= 0 ? file.path.slice(slash + 1) : file.path;
7053
- const parent = slash >= 0 ? file.path.slice(0, slash + 1) : "";
7054
- return `${STATUS_LETTER[file.status]} \xB7 ${basename}${parent ? ` \u2014 ${parent}` : ""}`;
7055
- }
7056
- function buildRail(files) {
7057
- const roots = new Set(
7058
- files.flatMap((file) => file.repoRoot === void 0 ? [] : [file.repoRoot])
7059
- );
7060
- const groupByRepo = roots.size > 1;
7061
- if (!groupByRepo && files.length <= GROUP_THRESHOLD) {
7062
- return {
7063
- orderedFiles: files,
7064
- rows: files.map((file, index) => ({ kind: "file", file, index })),
7065
- grouped: false
7066
- };
7067
- }
7068
- const groups = /* @__PURE__ */ new Map();
7069
- for (const file of files) {
7070
- const slash = file.path.indexOf("/");
7071
- const key = groupByRepo ? file.repoRoot || "(workspace)" : slash > 0 ? file.path.slice(0, slash) : "(root)";
7072
- const bucket = groups.get(key);
7073
- if (bucket) bucket.push(file);
7074
- else groups.set(key, [file]);
7002
+ useLayoutEffect as useLayoutEffect3,
7003
+ useRef as useRef19,
7004
+ useState as useState19
7005
+ } from "react";
7006
+ import {
7007
+ ChevronsLeftRightIcon,
7008
+ Maximize2Icon,
7009
+ Minimize2Icon,
7010
+ PanelRightCloseIcon,
7011
+ XIcon as XIcon2
7012
+ } from "lucide-react";
7013
+ import {
7014
+ Group,
7015
+ Panel,
7016
+ Separator,
7017
+ useDefaultLayout,
7018
+ usePanelRef
7019
+ } from "react-resizable-panels";
7020
+ import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
7021
+ var SERVER_LAYOUT_STORAGE = {
7022
+ getItem: () => null,
7023
+ setItem: () => {
7075
7024
  }
7076
- const labels = [...groups.keys()].sort((a, b) => a.localeCompare(b));
7077
- const orderedFiles = [];
7078
- const rows = [];
7079
- for (const label of labels) {
7080
- const bucket = (groups.get(label) ?? []).slice().sort((a, b) => a.path.localeCompare(b.path));
7081
- rows.push({ kind: "group", label, count: bucket.length });
7082
- for (const file of bucket) {
7083
- rows.push({ kind: "file", file, index: orderedFiles.length });
7084
- orderedFiles.push(file);
7085
- }
7025
+ };
7026
+ function getLayoutStorage() {
7027
+ if (typeof window === "undefined") return SERVER_LAYOUT_STORAGE;
7028
+ try {
7029
+ return window.localStorage;
7030
+ } catch {
7031
+ return SERVER_LAYOUT_STORAGE;
7086
7032
  }
7087
- return { orderedFiles, rows, grouped: true };
7088
- }
7089
- function isGuarded(file) {
7090
- return file.isBinary || file.truncated;
7091
7033
  }
7092
- function estimateSectionHeight(file) {
7093
- if (isGuarded(file)) return HEADER_PX + GUARD_BODY_PX;
7094
- let lines = 0;
7095
- for (const hunk of file.hunks) lines += hunk.lines.length + 1;
7096
- return HEADER_PX + Math.max(lines, 1) * LINE_PX + 8;
7034
+ var useDockLayoutEffect = typeof window === "undefined" ? useEffect22 : useLayoutEffect3;
7035
+ var DOCK_OVERLAY_BREAKPOINT = 1024;
7036
+ function useIsNarrow(maxWidth) {
7037
+ const [narrow, setNarrow] = useState19(false);
7038
+ useDockLayoutEffect(() => {
7039
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
7040
+ return;
7041
+ }
7042
+ const mql = window.matchMedia(`(max-width: ${maxWidth - 1}px)`);
7043
+ const update = () => setNarrow(mql.matches);
7044
+ update();
7045
+ if (typeof mql.addEventListener === "function") {
7046
+ mql.addEventListener("change", update);
7047
+ return () => mql.removeEventListener("change", update);
7048
+ }
7049
+ mql.addListener(update);
7050
+ return () => mql.removeListener(update);
7051
+ }, [maxWidth]);
7052
+ return narrow;
7097
7053
  }
7098
- function WorkbenchChanges({
7099
- diff,
7100
- source,
7101
- capturedAt,
7102
- captureRevision,
7103
- themeType,
7104
- onOpenFile,
7054
+ function WorkspaceDock({
7055
+ primary,
7056
+ tabs,
7057
+ activeTab,
7058
+ onActiveTabChange,
7059
+ collapsed: collapsedProp,
7060
+ onCollapsedChange,
7061
+ headerAccessory,
7062
+ mobileLeadingControl,
7063
+ autoSaveId = "og.session.dock",
7064
+ defaultSize = 34,
7065
+ minSize = 22,
7066
+ maxSize = 70,
7105
7067
  className
7106
7068
  }) {
7069
+ const narrow = useIsNarrow(DOCK_OVERLAY_BREAKPOINT);
7107
7070
  const rootRef = useRef19(null);
7108
- const [compact, setCompact] = useState19(false);
7109
- const [coarsePointer, setCoarsePointer] = useState19(false);
7110
- const resolvedTheme = useThemeType(themeType);
7111
- const [layout, setLayout] = useState19("unified");
7112
- const [activePath, setActivePath] = useState19(null);
7113
- const pickerId = useId4();
7114
- const unicodeFontPaths = useMemo10(
7115
- () => diff.flatMap((file) => file.oldPath ? [file.path, file.oldPath] : [file.path]),
7116
- [diff]
7117
- );
7118
- useUnicodeFallbackFonts(unicodeFontPaths);
7119
- const { orderedFiles, rows, grouped } = useMemo10(() => buildRail(diff), [diff]);
7120
- const selectedIndex = orderedFiles.findIndex((file) => file.path === activePath);
7121
- const activeIndex = selectedIndex >= 0 ? selectedIndex : 0;
7122
- useChangesLayoutEffect(() => {
7123
- const root = rootRef.current;
7124
- if (!root || typeof ResizeObserver === "undefined") return;
7125
- const update = (width) => {
7126
- if (width > 0) setCompact(width < COMPACT_SURFACE_PX);
7127
- };
7128
- update(root.getBoundingClientRect().width);
7129
- const observer = new ResizeObserver((entries) => {
7130
- update(entries[0]?.contentRect.width ?? root.getBoundingClientRect().width);
7131
- });
7132
- observer.observe(root);
7133
- return () => observer.disconnect();
7134
- }, []);
7135
- useChangesLayoutEffect(() => {
7136
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
7137
- const query = window.matchMedia("(pointer: coarse)");
7138
- const update = () => setCoarsePointer(query.matches);
7139
- update();
7140
- if (typeof query.addEventListener === "function") {
7141
- query.addEventListener("change", update);
7142
- return () => query.removeEventListener("change", update);
7143
- }
7144
- if (typeof query.addListener === "function") {
7145
- query.addListener(update);
7146
- return () => query.removeListener(update);
7147
- }
7148
- }, []);
7149
- const additions = useMemo10(() => diff.reduce((sum, f) => sum + f.additions, 0), [diff]);
7150
- const deletions = useMemo10(() => diff.reduce((sum, f) => sum + f.deletions, 0), [diff]);
7151
- const estimateHeight = useCallback21(
7152
- (index) => {
7153
- const file = orderedFiles[index];
7154
- return file ? estimateSectionHeight(file) : HEADER_PX + LINE_PX;
7071
+ const reopenRef = useRef19(null);
7072
+ const returnFocusRef = useRef19(null);
7073
+ const initialNarrowFocusRef = useRef19(false);
7074
+ const dockPanelRef = usePanelRef();
7075
+ const [internalCollapsed, setInternalCollapsed] = useState19(false);
7076
+ const [maximized, setMaximized] = useState19(false);
7077
+ const [internalTab, setInternalTab] = useState19(tabs[0]?.id ?? "");
7078
+ const collapsed = collapsedProp ?? internalCollapsed;
7079
+ const hostControlled = collapsedProp !== void 0;
7080
+ const previousCollapsedRef = useRef19(collapsed);
7081
+ const { defaultLayout, onLayoutChanged } = useDefaultLayout({
7082
+ panelIds: ["primary", "dock"],
7083
+ storage: getLayoutStorage(),
7084
+ id: autoSaveId
7085
+ });
7086
+ const requestedTab = activeTab ?? internalTab;
7087
+ const tabIds = tabs.map((tab) => tab.id);
7088
+ const firstTabId = tabs[0]?.id ?? "";
7089
+ const requestedTabIsValid = tabIds.includes(requestedTab);
7090
+ const current = requestedTabIsValid ? requestedTab : firstTabId;
7091
+ const setTab = useCallback21(
7092
+ (id) => {
7093
+ setInternalTab(id);
7094
+ onActiveTabChange?.(id);
7155
7095
  },
7156
- [orderedFiles]
7096
+ [onActiveTabChange]
7157
7097
  );
7158
- const windowed = useWindowedSections({
7159
- count: orderedFiles.length,
7160
- estimateHeight,
7161
- overscan: OVERSCAN
7162
- });
7163
- const jumpTo = useCallback21(
7164
- (index) => {
7165
- setActivePath(orderedFiles[index]?.path ?? null);
7166
- windowed.scrollToIndex(index);
7098
+ const setCollapsed = useCallback21(
7099
+ (next) => {
7100
+ setInternalCollapsed((previous) => previous === next ? previous : next);
7101
+ onCollapsedChange?.(next);
7167
7102
  },
7168
- [orderedFiles, windowed]
7103
+ [onCollapsedChange]
7169
7104
  );
7170
- const onPaneScroll = useCallback21(() => {
7171
- const el = windowed.scrollRef.current;
7172
- if (!el) {
7105
+ useDockLayoutEffect(() => {
7106
+ if (collapsedProp === void 0) {
7173
7107
  return;
7174
7108
  }
7175
- const top = el.scrollTop;
7176
- const offsets = windowed.offsets;
7177
- let idx = 0;
7178
- for (let i = 0; i < orderedFiles.length; i++) {
7179
- if ((offsets[i] ?? 0) <= top + 4) idx = i;
7180
- else break;
7109
+ if (collapsedProp) {
7110
+ dockPanelRef.current?.collapse();
7111
+ setMaximized(false);
7112
+ } else {
7113
+ dockPanelRef.current?.expand();
7181
7114
  }
7182
- const nextPath = orderedFiles[idx]?.path ?? null;
7183
- setActivePath((previous) => previous === nextPath ? previous : nextPath);
7184
- }, [windowed, orderedFiles]);
7115
+ }, [collapsedProp, dockPanelRef]);
7185
7116
  useEffect22(() => {
7186
- const el = windowed.scrollRef.current;
7187
- if (!el) return;
7188
- el.addEventListener("scroll", onPaneScroll, { passive: true });
7189
- return () => el.removeEventListener("scroll", onPaneScroll);
7190
- }, [windowed.scrollRef, onPaneScroll]);
7191
- const sourceBadge = describeSource(source, capturedAt, captureRevision ?? null);
7192
- const activeFile = orderedFiles[activeIndex] ?? orderedFiles[0];
7193
- return /* @__PURE__ */ jsxs10(
7194
- "div",
7117
+ const previous = previousCollapsedRef.current;
7118
+ previousCollapsedRef.current = collapsed;
7119
+ if (previous === collapsed) return;
7120
+ if (!collapsed) {
7121
+ const active = document.activeElement;
7122
+ const workspaceSurface = rootRef.current?.querySelector(
7123
+ narrow ? '[role="dialog"]' : "[data-workspace-surface]"
7124
+ );
7125
+ if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
7126
+ returnFocusRef.current = active;
7127
+ }
7128
+ if (narrow) {
7129
+ const frame2 = requestAnimationFrame(() => {
7130
+ rootRef.current?.querySelector('[role="dialog"] [role="tab"][aria-selected="true"]')?.focus();
7131
+ });
7132
+ return () => cancelAnimationFrame(frame2);
7133
+ }
7134
+ return;
7135
+ }
7136
+ const frame = requestAnimationFrame(() => {
7137
+ if (!hostControlled) {
7138
+ reopenRef.current?.focus();
7139
+ return;
7140
+ }
7141
+ const target = returnFocusRef.current;
7142
+ if (target?.isConnected) target.focus();
7143
+ });
7144
+ return () => cancelAnimationFrame(frame);
7145
+ }, [collapsed, hostControlled, narrow]);
7146
+ useEffect22(() => {
7147
+ if (!narrow || collapsed || initialNarrowFocusRef.current) return;
7148
+ initialNarrowFocusRef.current = true;
7149
+ const active = document.activeElement;
7150
+ const workspaceSurface = rootRef.current?.querySelector('[role="dialog"]');
7151
+ if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
7152
+ returnFocusRef.current = active;
7153
+ }
7154
+ const frame = requestAnimationFrame(() => {
7155
+ workspaceSurface?.querySelector('[role="tab"][aria-selected="true"]')?.focus();
7156
+ });
7157
+ return () => cancelAnimationFrame(frame);
7158
+ }, [collapsed, narrow]);
7159
+ useEffect22(() => {
7160
+ if (firstTabId && !requestedTabIsValid) {
7161
+ setTab(firstTabId);
7162
+ }
7163
+ }, [firstTabId, requestedTabIsValid, setTab]);
7164
+ const collapse = useCallback21(() => {
7165
+ dockPanelRef.current?.collapse();
7166
+ setCollapsed(true);
7167
+ }, [dockPanelRef, setCollapsed]);
7168
+ const expand = useCallback21(() => {
7169
+ dockPanelRef.current?.expand();
7170
+ setCollapsed(false);
7171
+ }, [dockPanelRef, setCollapsed]);
7172
+ useEffect22(() => {
7173
+ const overlayOpen = maximized || narrow && !collapsed;
7174
+ if (!overlayOpen) return;
7175
+ const onKey = (event) => {
7176
+ if (event.key === "Escape") {
7177
+ if (maximized) setMaximized(false);
7178
+ else collapse();
7179
+ return;
7180
+ }
7181
+ if (event.key !== "Tab") return;
7182
+ const surface = rootRef.current?.querySelector(
7183
+ narrow ? '[role="dialog"]' : "[data-workspace-surface]"
7184
+ );
7185
+ if (!surface) return;
7186
+ const focusable = Array.from(surface.querySelectorAll("*")).filter(
7187
+ (element) => element.tabIndex >= 0 && !element.hasAttribute("disabled") && element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden"
7188
+ );
7189
+ const first = focusable[0];
7190
+ const last = focusable.at(-1);
7191
+ const active = document.activeElement;
7192
+ if (!first || !last) {
7193
+ event.preventDefault();
7194
+ surface.focus();
7195
+ return;
7196
+ }
7197
+ if (!surface.contains(active)) {
7198
+ event.preventDefault();
7199
+ (event.shiftKey ? last : first).focus();
7200
+ return;
7201
+ }
7202
+ if (event.shiftKey && active === first || !event.shiftKey && active === last) {
7203
+ event.preventDefault();
7204
+ (event.shiftKey ? last : first).focus();
7205
+ return;
7206
+ }
7207
+ requestAnimationFrame(() => {
7208
+ const next = document.activeElement;
7209
+ if (!surface.contains(next)) (event.shiftKey ? last : first).focus();
7210
+ });
7211
+ };
7212
+ window.addEventListener("keydown", onKey);
7213
+ return () => window.removeEventListener("keydown", onKey);
7214
+ }, [maximized, narrow, collapsed, collapse]);
7215
+ if (narrow) {
7216
+ return /* @__PURE__ */ jsxs10("div", { ref: rootRef, className: cn("relative flex h-full min-h-0 w-full min-w-0", className), children: [
7217
+ /* @__PURE__ */ jsx12(
7218
+ "div",
7219
+ {
7220
+ "data-workspace-primary": true,
7221
+ "aria-hidden": !collapsed ? true : void 0,
7222
+ className: "min-h-0 min-w-0 flex-1",
7223
+ inert: !collapsed ? true : void 0,
7224
+ children: primary
7225
+ }
7226
+ ),
7227
+ /* @__PURE__ */ jsx12(
7228
+ "div",
7229
+ {
7230
+ "data-workspace-surface": true,
7231
+ role: "dialog",
7232
+ "aria-modal": "true",
7233
+ "aria-label": "Workspace",
7234
+ className: "fixed inset-0 z-40 flex flex-col bg-og-bg",
7235
+ hidden: collapsed,
7236
+ style: {
7237
+ paddingTop: "env(safe-area-inset-top)",
7238
+ paddingBottom: "env(safe-area-inset-bottom)"
7239
+ },
7240
+ children: /* @__PURE__ */ jsx12(
7241
+ DockChrome,
7242
+ {
7243
+ tabs,
7244
+ current,
7245
+ onTab: setTab,
7246
+ leading: mobileLeadingControl,
7247
+ accessory: headerAccessory,
7248
+ controls: /* @__PURE__ */ jsx12(ChromeButton, { onClick: collapse, title: "Close workspace", label: "Close workspace", children: /* @__PURE__ */ jsx12(XIcon2, { className: "size-4" }) })
7249
+ }
7250
+ )
7251
+ }
7252
+ ),
7253
+ collapsed && !hostControlled ? /* @__PURE__ */ jsx12(
7254
+ "button",
7255
+ {
7256
+ ref: reopenRef,
7257
+ type: "button",
7258
+ onClick: expand,
7259
+ title: "Open workspace",
7260
+ "aria-label": "Open workspace",
7261
+ className: "absolute right-3 top-3 z-30 inline-flex size-11 items-center justify-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted shadow-lg transition-colors hover:border-og-border-strong hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent",
7262
+ style: { marginTop: "env(safe-area-inset-top)" },
7263
+ children: /* @__PURE__ */ jsx12(ChevronsLeftRightIcon, { className: "size-4", "aria-hidden": true })
7264
+ }
7265
+ ) : null
7266
+ ] });
7267
+ }
7268
+ const dockChrome = /* @__PURE__ */ jsx12(
7269
+ DockChrome,
7195
7270
  {
7196
- ref: rootRef,
7197
- className: cn("flex h-full min-h-0 min-w-0 flex-col", className),
7198
- "data-workbench-changes-layout": compact ? "compact" : "rail",
7199
- children: [
7200
- /* @__PURE__ */ jsxs10(
7201
- "div",
7271
+ tabs,
7272
+ current,
7273
+ onTab: setTab,
7274
+ accessory: headerAccessory,
7275
+ controls: /* @__PURE__ */ jsxs10(Fragment4, { children: [
7276
+ /* @__PURE__ */ jsx12(
7277
+ ChromeButton,
7202
7278
  {
7203
- className: cn(
7204
- "flex shrink-0 items-center justify-between gap-2 border-b border-og-border px-3 py-1.5",
7205
- compact && source === "capture" && "flex-col items-stretch gap-1.5 py-2"
7206
- ),
7207
- children: [
7208
- /* @__PURE__ */ jsxs10(
7209
- "span",
7210
- {
7211
- role: "status",
7212
- "aria-atomic": "true",
7213
- "aria-live": "polite",
7214
- "data-contrast-audited": true,
7215
- className: "min-w-0 text-og-xs text-og-fg-muted",
7216
- children: [
7217
- diff.length,
7218
- " ",
7219
- diff.length === 1 ? "file" : "files",
7220
- " changed",
7221
- /* @__PURE__ */ jsxs10("span", { "data-contrast-audited": true, className: "ml-2 text-og-status-idle", children: [
7222
- "+",
7223
- additions
7224
- ] }),
7225
- /* @__PURE__ */ jsxs10("span", { "data-contrast-audited": true, className: "ml-1 text-og-status-failed", children: [
7226
- "\u2212",
7227
- deletions
7228
- ] })
7229
- ]
7230
- }
7231
- ),
7232
- /* @__PURE__ */ jsxs10(
7233
- "div",
7234
- {
7235
- className: cn(
7236
- "flex shrink-0 items-center gap-2",
7237
- compact && source === "capture" && "self-start"
7238
- ),
7239
- children: [
7240
- compact ? null : /* @__PURE__ */ jsx12(LayoutToggle, { layout, onChange: setLayout }),
7241
- /* @__PURE__ */ jsx12(SourceBadge, { source, capturedAt, label: sourceBadge })
7242
- ]
7243
- }
7244
- )
7245
- ]
7279
+ onClick: () => setMaximized((m) => !m),
7280
+ title: maximized ? "Restore (Esc)" : "Maximize",
7281
+ label: maximized ? "Restore dock" : "Maximize dock",
7282
+ children: maximized ? /* @__PURE__ */ jsx12(Minimize2Icon, { className: "size-3.5" }) : /* @__PURE__ */ jsx12(Maximize2Icon, { className: "size-3.5" })
7246
7283
  }
7247
7284
  ),
7248
- compact ? /* @__PURE__ */ jsxs10("div", { className: "flex min-h-0 flex-1 flex-col", children: [
7249
- /* @__PURE__ */ jsxs10("div", { className: "shrink-0 border-b border-og-border bg-og-surface-1 p-2", children: [
7250
- /* @__PURE__ */ jsx12("label", { className: "sr-only", htmlFor: pickerId, children: "Changed file" }),
7251
- /* @__PURE__ */ jsx12(
7252
- "select",
7253
- {
7254
- id: pickerId,
7255
- "aria-label": "Changed file",
7256
- value: activeIndex,
7257
- onChange: (event) => {
7258
- const index = Number(event.currentTarget.value);
7259
- setActivePath(orderedFiles[index]?.path ?? null);
7260
- },
7261
- title: activeFile?.path,
7262
- "data-compact-file-picker": true,
7263
- className: "h-11 w-full rounded-og-md border border-og-border bg-og-bg px-3 font-og-mono text-og-sm text-og-fg outline-none transition-colors focus:border-og-accent focus:ring-2 focus:ring-og-accent-soft",
7264
- children: orderedFiles.map((file, index) => /* @__PURE__ */ jsx12("option", { value: index, children: compactFileLabel(file) }, file.path))
7265
- }
7266
- )
7267
- ] }),
7268
- /* @__PURE__ */ jsx12("div", { className: "min-h-0 min-w-0 flex-1 overflow-auto", "data-opengeni-changes-pane": true, children: activeFile ? /* @__PURE__ */ jsx12(
7269
- DiffSection,
7270
- {
7271
- file: activeFile,
7272
- layout: "unified",
7273
- themeType: resolvedTheme,
7274
- ...onOpenFile ? { onOpenFile } : {}
7275
- }
7276
- ) : null })
7277
- ] }) : /* @__PURE__ */ jsxs10("div", { className: "flex min-h-0 flex-1", children: [
7278
- /* @__PURE__ */ jsx12("div", { className: "w-[clamp(12rem,28%,15rem)] shrink-0 border-r border-og-border bg-og-surface-1/35", children: /* @__PURE__ */ jsx12(
7279
- VList2,
7285
+ hostControlled ? null : /* @__PURE__ */ jsx12(ChromeButton, { onClick: collapse, title: "Collapse", label: "Collapse dock", children: /* @__PURE__ */ jsx12(PanelRightCloseIcon, { className: "size-3.5" }) })
7286
+ ] })
7287
+ }
7288
+ );
7289
+ return /* @__PURE__ */ jsxs10("div", { ref: rootRef, className: cn("relative flex h-full min-h-0 w-full min-w-0", className), children: [
7290
+ /* @__PURE__ */ jsxs10(
7291
+ Group,
7292
+ {
7293
+ orientation: "horizontal",
7294
+ className: "min-h-0 flex-1",
7295
+ ...defaultLayout ? { defaultLayout } : {},
7296
+ onLayoutChanged,
7297
+ children: [
7298
+ /* @__PURE__ */ jsx12(Panel, { id: "primary", minSize: "30%", className: "min-h-0 min-w-0", children: /* @__PURE__ */ jsx12(
7299
+ "div",
7280
7300
  {
7281
- className: "h-full",
7282
- itemSize: coarsePointer ? 44 : 28,
7283
- ssrCount: Math.min(30, rows.length),
7284
- children: rows.map(
7285
- (row) => row.kind === "group" ? /* @__PURE__ */ jsxs10(
7286
- "div",
7287
- {
7288
- "data-rail-group": true,
7289
- className: "flex items-center gap-1.5 px-2 pb-0.5 pt-2 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle",
7290
- children: [
7291
- /* @__PURE__ */ jsx12("span", { className: "min-w-0 truncate", children: row.label }),
7292
- /* @__PURE__ */ jsx12("span", { className: "shrink-0", children: row.count })
7293
- ]
7294
- },
7295
- `g:${row.label}`
7296
- ) : /* @__PURE__ */ jsx12(
7297
- RailFileRow,
7298
- {
7299
- file: row.file,
7300
- grouped,
7301
- active: row.index === activeIndex,
7302
- onClick: () => jumpTo(row.index)
7303
- },
7304
- row.file.path
7305
- )
7306
- )
7301
+ "data-workspace-primary": true,
7302
+ "aria-hidden": maximized ? true : void 0,
7303
+ className: "h-full min-h-0 min-w-0",
7304
+ inert: maximized ? true : void 0,
7305
+ children: primary
7307
7306
  }
7308
7307
  ) }),
7308
+ !collapsed && /* @__PURE__ */ jsx12(Separator, { className: "group relative z-10 w-1.5 shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent max-[1023px]:-mx-[19px] max-[1023px]:w-11 pointer-coarse:-mx-[19px] pointer-coarse:w-11", children: /* @__PURE__ */ jsx12("span", { className: "absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-og-border transition-colors group-hover:bg-og-accent group-focus-visible:bg-og-accent group-data-[separator-state=dragging]:bg-og-accent" }) }),
7309
7309
  /* @__PURE__ */ jsx12(
7310
- "div",
7310
+ Panel,
7311
7311
  {
7312
- ref: windowed.scrollRef,
7313
- className: "min-h-0 min-w-0 flex-1 overflow-auto",
7314
- "data-opengeni-changes-pane": true,
7315
- children: /* @__PURE__ */ jsx12("div", { style: { position: "relative", height: windowed.totalHeight }, children: orderedFiles.map((file, index) => {
7316
- if (index < windowed.range.start || index >= windowed.range.end) return null;
7317
- return /* @__PURE__ */ jsx12(
7318
- MeasuredSection,
7319
- {
7320
- index,
7321
- top: windowed.offsets[index] ?? 0,
7322
- onMeasure: windowed.measure,
7323
- children: /* @__PURE__ */ jsx12(
7324
- DiffSection,
7325
- {
7326
- file,
7327
- layout,
7328
- themeType: resolvedTheme,
7329
- ...onOpenFile ? { onOpenFile } : {}
7330
- }
7331
- )
7332
- },
7333
- file.path
7334
- );
7335
- }) })
7312
+ id: "dock",
7313
+ panelRef: dockPanelRef,
7314
+ collapsible: true,
7315
+ collapsedSize: "0%",
7316
+ defaultSize: `${defaultSize}%`,
7317
+ minSize: `${minSize}%`,
7318
+ maxSize: `${maxSize}%`,
7319
+ onResize: (size, _id, previousSize) => {
7320
+ const isCollapsed = size.asPercentage <= 1;
7321
+ const canInferCollapse = collapsedProp === void 0 || previousSize !== void 0;
7322
+ if (canInferCollapse && isCollapsed !== collapsed) {
7323
+ setCollapsed(isCollapsed);
7324
+ }
7325
+ },
7326
+ className: "min-h-0 min-w-0",
7327
+ children: /* @__PURE__ */ jsx12(
7328
+ "div",
7329
+ {
7330
+ "data-workspace-surface": true,
7331
+ role: maximized ? "dialog" : void 0,
7332
+ "aria-modal": maximized ? true : void 0,
7333
+ "aria-label": maximized ? "Workspace" : void 0,
7334
+ "aria-hidden": collapsed ? true : void 0,
7335
+ className: cn(
7336
+ "flex h-full min-h-0 min-w-0 flex-col bg-og-bg",
7337
+ maximized ? "fixed inset-0 z-40" : "border-l border-og-border",
7338
+ collapsed && "invisible pointer-events-none"
7339
+ ),
7340
+ children: dockChrome
7341
+ }
7342
+ )
7336
7343
  }
7337
7344
  )
7338
- ] })
7339
- ]
7340
- }
7341
- );
7342
- }
7343
- function SourceBadge({
7344
- source,
7345
- capturedAt,
7346
- label
7347
- }) {
7348
- if (source === "capture" && capturedAt) {
7349
- return /* @__PURE__ */ jsxs10(
7350
- "span",
7351
- {
7352
- className: "inline-flex items-center gap-1 rounded-og-xs border border-og-border px-1.5 py-px text-og-xs text-og-fg-muted",
7353
- title: new Date(capturedAt).toLocaleString(),
7354
- children: [
7355
- /* @__PURE__ */ jsx12(HistoryIcon, { className: "size-3 shrink-0 text-og-status-running", "aria-hidden": true }),
7356
- label
7357
7345
  ]
7358
7346
  }
7359
- );
7360
- }
7361
- return /* @__PURE__ */ jsx12("span", { className: "rounded-og-xs bg-og-surface-2 px-1.5 py-px text-og-xs text-og-fg-subtle", children: label });
7362
- }
7363
- function describeSource(source, capturedAt, revision) {
7364
- if (source !== "capture" || !capturedAt) return "Live diff";
7365
- const time = formatAsOf(capturedAt, Date.now());
7366
- return revision !== null ? `as of turn ${revision} \xB7 ${time}` : `as of ${time}`;
7347
+ ),
7348
+ collapsed && !maximized && !hostControlled && /* @__PURE__ */ jsx12(
7349
+ "button",
7350
+ {
7351
+ ref: reopenRef,
7352
+ type: "button",
7353
+ onClick: expand,
7354
+ title: "Open workspace",
7355
+ className: "absolute inset-y-0 right-0 flex w-6 shrink-0 items-center justify-center border-l border-og-border bg-og-surface-1 text-og-fg-subtle hover:text-og-fg",
7356
+ children: /* @__PURE__ */ jsx12(ChevronsLeftRightIcon, { className: "size-3.5" })
7357
+ }
7358
+ )
7359
+ ] });
7367
7360
  }
7368
- function RailFileRow({
7369
- file,
7370
- grouped,
7371
- active,
7372
- onClick
7361
+ function ChromeButton({
7362
+ onClick,
7363
+ title,
7364
+ label,
7365
+ children
7373
7366
  }) {
7374
- const repoPrefix = file.repoRoot ? `${file.repoRoot}/` : null;
7375
- const shown = grouped ? file.repoRoot !== void 0 ? repoPrefix && file.path.startsWith(repoPrefix) ? file.path.slice(repoPrefix.length) : file.path : file.path.slice(file.path.indexOf("/") + 1) || file.path : file.path;
7376
- return /* @__PURE__ */ jsxs10(
7367
+ return /* @__PURE__ */ jsx12(
7377
7368
  "button",
7378
7369
  {
7379
7370
  type: "button",
7380
7371
  onClick,
7381
- title: file.path,
7382
- "data-rail-file": true,
7383
- className: cn(
7384
- "relative flex min-h-7 w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-og-sm transition-colors hover:bg-og-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent pointer-coarse:min-h-11",
7385
- grouped && "pl-3",
7386
- active && "bg-og-accent-soft text-og-fg before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-r-full before:bg-og-accent"
7387
- ),
7388
- children: [
7389
- /* @__PURE__ */ jsx12(
7390
- "span",
7391
- {
7392
- "data-contrast-audited": true,
7393
- className: cn("w-3 shrink-0 text-center font-og-mono text-og-xs", STATUS_TINT2[file.status]),
7394
- children: STATUS_LETTER[file.status]
7395
- }
7396
- ),
7397
- /* @__PURE__ */ jsx12("span", { className: "min-w-0 flex-1 truncate", children: shown }),
7398
- /* @__PURE__ */ jsxs10("span", { className: "ml-auto flex shrink-0 items-center gap-1 pl-1 font-og-mono text-og-xs", children: [
7399
- /* @__PURE__ */ jsxs10("span", { "data-contrast-audited": true, className: "text-og-status-idle", children: [
7400
- "+",
7401
- file.additions
7402
- ] }),
7403
- /* @__PURE__ */ jsxs10("span", { "data-contrast-audited": true, className: "text-og-status-failed", children: [
7404
- "\u2212",
7405
- file.deletions
7406
- ] })
7407
- ] })
7408
- ]
7372
+ title,
7373
+ "aria-label": label,
7374
+ className: "inline-flex size-7 items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:size-11 pointer-coarse:size-11",
7375
+ children
7409
7376
  }
7410
7377
  );
7411
7378
  }
7412
- function MeasuredSection({
7413
- index,
7414
- top,
7415
- onMeasure,
7416
- children
7379
+ function DockChrome({
7380
+ tabs,
7381
+ current,
7382
+ onTab,
7383
+ leading,
7384
+ accessory,
7385
+ controls
7417
7386
  }) {
7418
- const ref = useRef19(null);
7387
+ const active = tabs.find((t) => t.id === current) ?? tabs[0];
7388
+ const tabsetId = useId4();
7389
+ const tabRefs = useRef19([]);
7390
+ const tabListRef = useRef19(null);
7391
+ const [visitedTabs, setVisitedTabs] = useState19(() => /* @__PURE__ */ new Set());
7392
+ const [tabOverflow, setTabOverflow] = useState19({ start: false, end: false });
7393
+ const activeId = active?.id ?? "";
7394
+ const activeTabIndex = tabs.findIndex((tab) => tab.id === activeId);
7395
+ const keepActiveTabVisible = useCallback21(() => {
7396
+ const selected = activeTabIndex >= 0 ? tabRefs.current[activeTabIndex] : null;
7397
+ selected?.scrollIntoView({ block: "nearest", inline: "nearest" });
7398
+ }, [activeTabIndex]);
7419
7399
  useEffect22(() => {
7420
- const el = ref.current;
7421
- if (!el) return;
7422
- const report = () => onMeasure(index, el.offsetHeight);
7423
- report();
7424
- const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(report) : null;
7425
- ro?.observe(el);
7426
- return () => ro?.disconnect();
7427
- }, [index, onMeasure]);
7428
- return /* @__PURE__ */ jsx12(
7429
- "div",
7430
- {
7431
- ref,
7432
- "data-diff-section": true,
7433
- "data-diff-index": index,
7434
- style: { position: "absolute", top, left: 0, right: 0 },
7435
- children
7400
+ if (!activeId) return;
7401
+ setVisitedTabs((previous) => {
7402
+ if (previous.has(activeId)) return previous;
7403
+ const next = new Set(previous);
7404
+ next.add(activeId);
7405
+ return next;
7406
+ });
7407
+ }, [activeId]);
7408
+ useDockLayoutEffect(() => {
7409
+ keepActiveTabVisible();
7410
+ }, [activeId, keepActiveTabVisible]);
7411
+ useDockLayoutEffect(() => {
7412
+ const list = tabListRef.current;
7413
+ if (!list) return;
7414
+ const update = () => {
7415
+ const max = Math.max(0, list.scrollWidth - list.clientWidth);
7416
+ const next = { start: list.scrollLeft > 1, end: list.scrollLeft < max - 1 };
7417
+ setTabOverflow(
7418
+ (previous) => previous.start === next.start && previous.end === next.end ? previous : next
7419
+ );
7420
+ };
7421
+ update();
7422
+ list.addEventListener("scroll", update, { passive: true });
7423
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
7424
+ keepActiveTabVisible();
7425
+ update();
7426
+ });
7427
+ observer?.observe(list);
7428
+ for (const tab of tabRefs.current) {
7429
+ if (tab) observer?.observe(tab);
7436
7430
  }
7437
- );
7438
- }
7439
- function DiffSection({
7440
- file,
7441
- layout,
7442
- themeType,
7443
- onOpenFile
7444
- }) {
7445
- if (isGuarded(file)) {
7446
- const renamed = file.oldPath && file.oldPath !== file.path;
7447
- return /* @__PURE__ */ jsxs10("section", { className: "border-b border-og-border pb-1", children: [
7448
- /* @__PURE__ */ jsxs10("header", { className: "flex items-center justify-between gap-2 bg-og-surface-1 px-3 py-1.5 text-og-sm", children: [
7449
- /* @__PURE__ */ jsx12("span", { className: "min-w-0 truncate font-og-mono text-og-xs", children: renamed ? `${file.oldPath} \u2192 ${file.path}` : file.path }),
7450
- /* @__PURE__ */ jsxs10("span", { className: "flex shrink-0 items-center gap-2 font-og-mono text-og-xs", children: [
7451
- /* @__PURE__ */ jsxs10("span", { className: "text-og-status-idle", children: [
7452
- "+",
7453
- file.additions
7454
- ] }),
7455
- /* @__PURE__ */ jsxs10("span", { className: "text-og-status-failed", children: [
7456
- "\u2212",
7457
- file.deletions
7458
- ] })
7459
- ] })
7460
- ] }),
7461
- /* @__PURE__ */ jsx12(GuardBody, { file, ...onOpenFile ? { onOpenFile } : {} })
7462
- ] });
7463
- }
7464
- return /* @__PURE__ */ jsx12("section", { "data-pierre-section": true, className: "border-b border-og-border pb-2", children: /* @__PURE__ */ jsx12(PierreDiff, { diff: [file], layout, themeType, className: "px-1" }) });
7465
- }
7466
- function GuardBody({
7467
- file,
7468
- onOpenFile
7469
- }) {
7470
- const reason = file.isBinary ? "Binary file" : "Diff too large to show here";
7471
- return /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-center gap-2 px-3 py-3 text-og-sm text-og-fg-subtle", children: [
7472
- /* @__PURE__ */ jsx12(FileWarningIcon2, { className: "size-3.5 shrink-0", "aria-hidden": true }),
7473
- /* @__PURE__ */ jsxs10("span", { className: "min-w-0 flex-1", children: [
7474
- reason,
7475
- "."
7431
+ return () => {
7432
+ list.removeEventListener("scroll", update);
7433
+ observer?.disconnect();
7434
+ };
7435
+ }, [activeId, keepActiveTabVisible, tabs.length]);
7436
+ const tabMask = tabOverflow.start && tabOverflow.end ? "linear-gradient(to right, transparent 0, black 14px, black calc(100% - 14px), transparent 100%)" : tabOverflow.start ? "linear-gradient(to right, transparent 0, black 14px, black 100%)" : tabOverflow.end ? "linear-gradient(to right, black 0, black calc(100% - 14px), transparent 100%)" : void 0;
7437
+ const activateTab = (index) => {
7438
+ const tab = tabs[index];
7439
+ if (!tab) {
7440
+ return;
7441
+ }
7442
+ onTab(tab.id);
7443
+ tabRefs.current[index]?.focus();
7444
+ };
7445
+ const onTabKeyDown = (event, index) => {
7446
+ if (tabs.length === 0) return;
7447
+ let nextIndex = null;
7448
+ if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
7449
+ else if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
7450
+ else if (event.key === "Home") nextIndex = 0;
7451
+ else if (event.key === "End") nextIndex = tabs.length - 1;
7452
+ if (nextIndex === null) return;
7453
+ event.preventDefault();
7454
+ activateTab(nextIndex);
7455
+ };
7456
+ return /* @__PURE__ */ jsxs10(Fragment4, { children: [
7457
+ /* @__PURE__ */ jsxs10("div", { className: "grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0 min-[640px]:max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto_auto]", children: [
7458
+ /* @__PURE__ */ jsx12("div", { className: "flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11 min-[640px]:max-[1023px]:col-start-1 min-[640px]:max-[1023px]:row-start-1", children: leading ?? /* @__PURE__ */ jsx12("span", { className: "hidden truncate px-1 text-og-sm font-semibold text-og-fg max-[1023px]:inline", children: "Workspace" }) }),
7459
+ /* @__PURE__ */ jsx12(
7460
+ "div",
7461
+ {
7462
+ ref: tabListRef,
7463
+ className: "flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-[1023px]:col-span-3 max-[1023px]:row-start-2 max-[1023px]:w-full min-[640px]:max-[1023px]:col-span-1 min-[640px]:max-[1023px]:col-start-2 min-[640px]:max-[1023px]:row-start-1",
7464
+ role: "tablist",
7465
+ "aria-orientation": "horizontal",
7466
+ style: {
7467
+ maskImage: tabMask,
7468
+ WebkitMaskImage: tabMask,
7469
+ scrollPaddingInline: "16px"
7470
+ },
7471
+ children: tabs.map((tab, index) => /* @__PURE__ */ jsxs10(
7472
+ "button",
7473
+ {
7474
+ ref: (node) => {
7475
+ tabRefs.current[index] = node;
7476
+ },
7477
+ id: `${tabsetId}-tab-${index}`,
7478
+ type: "button",
7479
+ role: "tab",
7480
+ "aria-selected": tab.id === current,
7481
+ "aria-controls": `${tabsetId}-panel-${index}`,
7482
+ tabIndex: tab.id === current ? 0 : -1,
7483
+ onClick: () => onTab(tab.id),
7484
+ onKeyDown: (event) => onTabKeyDown(event, index),
7485
+ className: cn(
7486
+ "flex min-h-7 shrink-0 items-center justify-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
7487
+ tab.id === current ? "bg-og-accent-soft text-og-fg" : "text-og-fg-subtle hover:text-og-fg"
7488
+ ),
7489
+ children: [
7490
+ /* @__PURE__ */ jsx12("span", { "data-contrast-audited": true, children: tab.label }),
7491
+ tab.badge
7492
+ ]
7493
+ },
7494
+ tab.id
7495
+ ))
7496
+ }
7497
+ ),
7498
+ /* @__PURE__ */ jsx12("div", { className: "flex min-w-0 shrink-0 items-center justify-self-end min-[640px]:max-[1023px]:col-start-3 min-[640px]:max-[1023px]:row-start-1", children: accessory }),
7499
+ /* @__PURE__ */ jsx12("div", { className: "flex shrink-0 items-center gap-0.5 text-og-fg-subtle min-[640px]:max-[1023px]:col-start-4 min-[640px]:max-[1023px]:row-start-1", children: controls })
7476
7500
  ] }),
7477
- onOpenFile ? /* @__PURE__ */ jsxs10(
7478
- "button",
7479
- {
7480
- type: "button",
7481
- onClick: () => onOpenFile(file.path),
7482
- className: "inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-og-sm border border-og-border px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-11",
7483
- children: [
7484
- "Open in Files",
7485
- /* @__PURE__ */ jsx12(ArrowUpRightIcon, { className: "size-3.5", "aria-hidden": true })
7486
- ]
7487
- }
7488
- ) : null
7501
+ /* @__PURE__ */ jsx12("div", { className: "min-h-0 min-w-0 flex-1 overflow-hidden", children: tabs.length > 0 ? tabs.map((tab, index) => {
7502
+ const selected = tab.id === activeId;
7503
+ const shouldMount = selected || visitedTabs.has(tab.id);
7504
+ return /* @__PURE__ */ jsx12(
7505
+ "div",
7506
+ {
7507
+ id: `${tabsetId}-panel-${index}`,
7508
+ "aria-labelledby": `${tabsetId}-tab-${index}`,
7509
+ className: "h-full min-h-0 min-w-0 overflow-hidden",
7510
+ hidden: !selected,
7511
+ role: "tabpanel",
7512
+ children: shouldMount ? tab.content : null
7513
+ },
7514
+ tab.id
7515
+ );
7516
+ }) : /* @__PURE__ */ jsx12("div", { className: "h-full min-h-0 min-w-0", "aria-label": "Workspace", role: "tabpanel" }) })
7489
7517
  ] });
7490
7518
  }
7491
- function LayoutToggle({
7492
- layout,
7493
- onChange
7494
- }) {
7495
- return /* @__PURE__ */ jsx12("div", { className: "inline-flex items-center rounded-og-sm border border-og-border p-0.5", children: ["unified", "split"].map((value) => /* @__PURE__ */ jsx12(
7496
- "button",
7497
- {
7498
- type: "button",
7499
- onClick: () => onChange(value),
7500
- className: cn(
7501
- "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
7502
- layout === value ? "bg-og-accent-soft text-og-fg" : "text-og-fg-subtle hover:text-og-fg"
7503
- ),
7504
- children: value
7505
- },
7506
- value
7507
- )) });
7508
- }
7509
7519
 
7510
7520
  // src/components/sandbox-workspace.tsx
7521
+ import { useCallback as useCallback22, useEffect as useEffect23, useMemo as useMemo11, useRef as useRef20, useState as useState20 } from "react";
7522
+ import { Popover } from "radix-ui";
7523
+ import {
7524
+ CircleCheckIcon,
7525
+ CpuIcon,
7526
+ LaptopIcon,
7527
+ LoaderCircleIcon as LoaderCircleIcon3,
7528
+ RefreshCwIcon as RefreshCwIcon2,
7529
+ TriangleAlertIcon as TriangleAlertIcon2
7530
+ } from "lucide-react";
7511
7531
  import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
7512
7532
  var WORKBENCH_TAB_CHANGES = "changes";
7513
7533
  var WORKBENCH_TAB_FILES = "files";
@@ -8376,6 +8396,7 @@ export {
8376
8396
  ActivityRail,
8377
8397
  ApprovalSurface,
8378
8398
  BUILT_IN_TURN_SUMMARY_FACET_IDS,
8399
+ BillingClassMark,
8379
8400
  BodyNote,
8380
8401
  COMPOSER_PAYMENT_REQUIRED_MESSAGE,
8381
8402
  CONNECTION_STATUS_META,
@@ -8400,6 +8421,7 @@ export {
8400
8421
  FleetTile,
8401
8422
  HEALTH_TOKEN,
8402
8423
  HumanInputForm,
8424
+ HumanInputSurface,
8403
8425
  INITIAL_TRANSCRIPTION_CONTROL_STATE,
8404
8426
  LightboxProvider,
8405
8427
  MACHINE_STATE_BADGE_META,
@@ -8419,9 +8441,14 @@ export {
8419
8441
  MetricHistoryChart,
8420
8442
  MetricSparkline,
8421
8443
  ModelPicker,
8444
+ ModelPolicyPicker,
8445
+ ModelPolicyPickerMenu,
8422
8446
  OPEN_WORKSTREAM_CONTROL_EVENT,
8423
8447
  OpenGeniProvider,
8424
8448
  PayloadBlock,
8449
+ PickerAnimatedPage,
8450
+ PickerBackHeader,
8451
+ PickerNavRow,
8425
8452
  PierreDiff,
8426
8453
  PierreFile,
8427
8454
  QueueSurface,
@@ -8453,6 +8480,7 @@ export {
8453
8480
  WORKBENCH_TAB_DESKTOP,
8454
8481
  WORKBENCH_TAB_FILES,
8455
8482
  WORKBENCH_TAB_TERMINAL,
8483
+ WorkbenchChanges,
8456
8484
  WorkspaceDock,
8457
8485
  advancedSourceSummary,
8458
8486
  answersFromDrafts,
@@ -8480,6 +8508,7 @@ export {
8480
8508
  defaultCommands,
8481
8509
  defaultEffortForModel,
8482
8510
  defaultHumanInputFormMessages,
8511
+ defaultModelPolicyPickerMessages,
8483
8512
  defaultToolRegistry,
8484
8513
  deriveHealth,
8485
8514
  deriveMachineChip,
@@ -8513,6 +8542,7 @@ export {
8513
8542
  isTitleEvent,
8514
8543
  isTurnQueueEvent,
8515
8544
  labelLatencyMode,
8545
+ labelReasoningEffort,
8516
8546
  languageForPath,
8517
8547
  looksBinary,
8518
8548
  matchCommand,
@@ -8521,6 +8551,7 @@ export {
8521
8551
  parseToolArgs,
8522
8552
  payerSummaryForModel,
8523
8553
  pointsFor,
8554
+ projectClientModelRows,
8524
8555
  projectPendingApprovals,
8525
8556
  projectPendingHumanInputRequests,
8526
8557
  projectPickerRows,