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