@backmatter/studio 0.0.16 → 0.0.18
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.
- package/dist/chunks/{UiKitApp-_EDqWID3.js → UiKitApp-DkuQBiQZ.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-B2PrFgpz.js → WorkspaceRoutes-DVvb3N-V.js} +27489 -26622
- package/dist/chunks/{dashboard-BnlnMnnr.js → dashboard-B4FITg6c.js} +2 -2
- package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
- package/dist/chunks/{index-D_BHF_8q.js → index-CegtXnEI.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/define.js +1 -1
- package/dist/index.js +4 -4
- package/dist/site-widget.css +45 -1
- package/dist/site-widget.js +2799 -2791
- package/dist/studio.css +113 -15
- package/package.json +2 -2
- package/src/api.ts +22 -0
- package/src/chat/ChatProvider.tsx +6 -0
- package/src/chat/Composer.tsx +3 -1
- package/src/chrome/Toast.tsx +115 -0
- package/src/chrome/WorkspaceStatusBar.tsx +12 -3
- package/src/chrome/commit-error.ts +13 -0
- package/src/chrome/pull-failure.ts +61 -0
- package/src/{pages/code-session-routes.ts → content-routes.ts} +6 -6
- package/src/fields/FieldControl.tsx +771 -0
- package/src/fields/LinkFieldInput.tsx +315 -0
- package/src/fields/choose-field-input.ts +69 -0
- package/src/fields/link-targets.ts +60 -0
- package/src/fields/load-relation-documents.ts +35 -0
- package/src/fields/omit-field.ts +12 -0
- package/src/fields/relation-targets.ts +44 -0
- package/src/fields/resolve-object-fields.ts +21 -0
- package/src/host-path.ts +13 -0
- package/src/pages/BlockSettingsDialog.tsx +19 -115
- package/src/pages/BlocksList.tsx +43 -35
- package/src/pages/BodyProse.tsx +16 -1
- package/src/pages/CodeSessionPage.tsx +186 -248
- package/src/pages/DocEditorPage.tsx +168 -213
- package/src/pages/MediaFieldInput.tsx +609 -26
- package/src/pages/MediaGalleryPage.tsx +14 -3
- package/src/pages/PreviewAddress.tsx +263 -0
- package/src/pages/RenderedBody.tsx +17 -2
- package/src/pages/branch-notice.ts +16 -0
- package/src/pages/code-session-boot.ts +0 -12
- package/src/pages/code-session-context.tsx +2 -0
- package/src/pages/media-selection.ts +79 -0
- package/src/shell-resets.css +15 -0
- package/src/ui/Input.tsx +178 -20
- package/src/ui/popup.ts +11 -2
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
// packages/studio/src/pages/CodeSessionPage.tsx
|
|
2
|
-
import { useCallback, useEffect, useRef, useState
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import type { RouteModel } from "@backmatter/core";
|
|
4
4
|
import { useOutletContext } from "react-router";
|
|
5
5
|
import * as stylex from "@stylexjs/stylex";
|
|
6
|
-
import { Combobox } from "@base-ui/react/combobox";
|
|
7
6
|
import {
|
|
8
|
-
IconArrowRotateClockwise,
|
|
9
|
-
IconArrowUpRight,
|
|
10
7
|
IconCheckmark1Small,
|
|
11
8
|
IconCrossSmall,
|
|
12
9
|
IconEyeOpen,
|
|
13
10
|
IconFiles,
|
|
14
|
-
IconGlobe,
|
|
15
11
|
} from "@central-icons-react/round-outlined-radius-2-stroke-1.5";
|
|
16
12
|
import { ChatPanel } from "../chat/ChatPanel";
|
|
17
|
-
import { ChatProvider, ChatSessionScope } from "../chat/ChatProvider";
|
|
13
|
+
import { ChatProvider, ChatSessionScope, useChatBusy } from "../chat/ChatProvider";
|
|
18
14
|
import { useReplicaState } from "../replica/useReplica";
|
|
15
|
+
import { routesForContentFiles } from "../content-routes";
|
|
19
16
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
20
17
|
import { apiCredentials, apiHeaders, apiUrl } from "../studio-config";
|
|
21
18
|
import { Text } from "../ui/Text";
|
|
22
|
-
import { cx, popup, popupClassName } from "../ui/popup";
|
|
23
19
|
import type { WorkspaceOutletContext } from "../workspace/types";
|
|
24
|
-
import { bootSteps,
|
|
20
|
+
import { bootSteps, type BootStep, type HostPhase } from "./code-session-boot";
|
|
25
21
|
import {
|
|
26
22
|
frameAddress,
|
|
27
23
|
framedLocation,
|
|
@@ -30,12 +26,13 @@ import {
|
|
|
30
26
|
previewUrl,
|
|
31
27
|
type HostPreview,
|
|
32
28
|
} from "./code-session-frame";
|
|
33
|
-
import {
|
|
29
|
+
import { PreviewAddress } from "./PreviewAddress";
|
|
34
30
|
import { CodeSessionFiles, type FilesView } from "./CodeSessionFiles";
|
|
35
31
|
import { PreviewSizeSwitch, PreviewViewport } from "./PreviewSize";
|
|
36
32
|
import { persistPreviewSize, readPreviewSize, type PreviewSize } from "./preview-size";
|
|
37
33
|
import type { WorkspaceFile } from "./code-session-files";
|
|
38
|
-
import { fetchCodeSessionEdits } from "../api";
|
|
34
|
+
import { clearCodeSessionEdits, fetchCodeSessionEdits, updateCodeSessionBranch } from "../api";
|
|
35
|
+
import { branchNoticeKind, type BranchNoticeKind } from "./branch-notice";
|
|
39
36
|
import { usePublishCodeSession } from "./code-session-context";
|
|
40
37
|
import { sessionDocument } from "./session-preview";
|
|
41
38
|
import { useSessionPreview } from "./use-session-preview";
|
|
@@ -80,6 +77,10 @@ export function CodeSessionScreen({
|
|
|
80
77
|
models = [],
|
|
81
78
|
files,
|
|
82
79
|
chat,
|
|
80
|
+
notice = null,
|
|
81
|
+
noticeBusy = false,
|
|
82
|
+
onUpdateBranch = () => {},
|
|
83
|
+
onClearEdits = () => {},
|
|
83
84
|
}: {
|
|
84
85
|
state: OpenState;
|
|
85
86
|
host: HostView;
|
|
@@ -108,9 +109,16 @@ export function CodeSessionScreen({
|
|
|
108
109
|
/** Models whose routes name the document under a pointed block. */
|
|
109
110
|
models?: readonly RouteModel[];
|
|
110
111
|
chat: React.ReactNode;
|
|
112
|
+
notice?: BranchNoticeKind | null;
|
|
113
|
+
/** Chat is writing, or the checkout is already being replaced. */
|
|
114
|
+
noticeBusy?: boolean;
|
|
115
|
+
onUpdateBranch?: () => void;
|
|
116
|
+
onClearEdits?: () => void;
|
|
111
117
|
}) {
|
|
112
118
|
// The entry's ticket sets the host's cookie on first load; later loads ride that cookie.
|
|
113
119
|
// A reload uses the route the frame is on, which can differ from the last address typed.
|
|
120
|
+
const chatBusy = useChatBusy();
|
|
121
|
+
const actionBusy = noticeBusy || chatBusy;
|
|
114
122
|
const frameSrc = preview ? frameAddress(preview, reloads > 0 ? location : path, reloads) : null;
|
|
115
123
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
|
116
124
|
const heldFrameRef = useRef<HTMLIFrameElement | null>(null);
|
|
@@ -182,7 +190,7 @@ export function CodeSessionScreen({
|
|
|
182
190
|
</TabButton>
|
|
183
191
|
</div>
|
|
184
192
|
|
|
185
|
-
<
|
|
193
|
+
<PreviewAddress
|
|
186
194
|
path={location}
|
|
187
195
|
enabled={frameSrc !== null && tab === "preview"}
|
|
188
196
|
routes={routes}
|
|
@@ -201,6 +209,38 @@ export function CodeSessionScreen({
|
|
|
201
209
|
</div>
|
|
202
210
|
</div>
|
|
203
211
|
|
|
212
|
+
{notice ? (
|
|
213
|
+
<div {...stylex.props(styles.notice)} role="status">
|
|
214
|
+
<span {...stylex.props(styles.noticeText)}>
|
|
215
|
+
{notice === "edits"
|
|
216
|
+
? "This branch has new commits. Commit them, or clear the edit set, before updating."
|
|
217
|
+
: notice === "fold"
|
|
218
|
+
? "This branch has new commits. The last commit is still folding."
|
|
219
|
+
: "This branch has new commits."}
|
|
220
|
+
</span>
|
|
221
|
+
{notice === "update" ? (
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
{...stylex.props(styles.noticeButton)}
|
|
225
|
+
disabled={actionBusy}
|
|
226
|
+
onClick={onUpdateBranch}
|
|
227
|
+
>
|
|
228
|
+
Update branch
|
|
229
|
+
</button>
|
|
230
|
+
) : null}
|
|
231
|
+
{notice === "edits" ? (
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
{...stylex.props(styles.noticeButton)}
|
|
235
|
+
disabled={actionBusy}
|
|
236
|
+
onClick={onClearEdits}
|
|
237
|
+
>
|
|
238
|
+
Clear edit set
|
|
239
|
+
</button>
|
|
240
|
+
) : null}
|
|
241
|
+
</div>
|
|
242
|
+
) : null}
|
|
243
|
+
|
|
204
244
|
<div {...stylex.props(styles.canvas)}>
|
|
205
245
|
{tab === "files" ? (
|
|
206
246
|
<CodeSessionFiles {...files} />
|
|
@@ -280,170 +320,6 @@ function TabButton({
|
|
|
280
320
|
);
|
|
281
321
|
}
|
|
282
322
|
|
|
283
|
-
function AddressBar({
|
|
284
|
-
path,
|
|
285
|
-
enabled,
|
|
286
|
-
routes,
|
|
287
|
-
routesPending,
|
|
288
|
-
onOpenRoutes,
|
|
289
|
-
onPath,
|
|
290
|
-
onReload,
|
|
291
|
-
onOpen,
|
|
292
|
-
}: {
|
|
293
|
-
path: string;
|
|
294
|
-
enabled: boolean;
|
|
295
|
-
routes: readonly string[];
|
|
296
|
-
routesPending: boolean;
|
|
297
|
-
onOpenRoutes?: () => void;
|
|
298
|
-
onPath: (path: string) => void;
|
|
299
|
-
onReload: () => void;
|
|
300
|
-
onOpen: () => void;
|
|
301
|
-
}) {
|
|
302
|
-
const [query, setQuery] = useState<string | null>(null);
|
|
303
|
-
const [open, setOpen] = useState(false);
|
|
304
|
-
const picked = useRef(false);
|
|
305
|
-
const fieldRef = useRef<HTMLButtonElement>(null);
|
|
306
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
307
|
-
const shown = query ? matchingRoutes(routes, query) : [...routes];
|
|
308
|
-
|
|
309
|
-
useEffect(() => {
|
|
310
|
-
if (!open) return;
|
|
311
|
-
const input = inputRef.current;
|
|
312
|
-
input?.focus();
|
|
313
|
-
input?.select();
|
|
314
|
-
}, [open]);
|
|
315
|
-
|
|
316
|
-
return (
|
|
317
|
-
<div {...stylex.props(styles.address)}>
|
|
318
|
-
<button
|
|
319
|
-
type="button"
|
|
320
|
-
aria-label="Reload preview"
|
|
321
|
-
disabled={!enabled}
|
|
322
|
-
onClick={onReload}
|
|
323
|
-
{...stylex.props(styles.iconButton)}
|
|
324
|
-
>
|
|
325
|
-
<IconArrowRotateClockwise size={14} ariaHidden />
|
|
326
|
-
</button>
|
|
327
|
-
<Combobox.Root
|
|
328
|
-
items={shown}
|
|
329
|
-
filteredItems={shown}
|
|
330
|
-
inputValue={query ?? ""}
|
|
331
|
-
disabled={!enabled}
|
|
332
|
-
open={open}
|
|
333
|
-
value={routes.includes(path) ? path : null}
|
|
334
|
-
onOpenChange={(next) => {
|
|
335
|
-
if (!enabled && next) return;
|
|
336
|
-
setOpen(next);
|
|
337
|
-
if (next) onOpenRoutes?.();
|
|
338
|
-
else setQuery(null);
|
|
339
|
-
}}
|
|
340
|
-
onInputValueChange={(value, details) => {
|
|
341
|
-
if (details.reason === "input-change" || details.reason === "input-clear") {
|
|
342
|
-
setQuery(value);
|
|
343
|
-
}
|
|
344
|
-
}}
|
|
345
|
-
onValueChange={(value) => {
|
|
346
|
-
if (typeof value !== "string") return;
|
|
347
|
-
picked.current = true;
|
|
348
|
-
setQuery(null);
|
|
349
|
-
onPath(hostPath(value));
|
|
350
|
-
queueMicrotask(() => {
|
|
351
|
-
picked.current = false;
|
|
352
|
-
});
|
|
353
|
-
}}
|
|
354
|
-
>
|
|
355
|
-
<Combobox.Trigger
|
|
356
|
-
ref={fieldRef}
|
|
357
|
-
aria-label="Preview path"
|
|
358
|
-
disabled={!enabled}
|
|
359
|
-
data-no-ring
|
|
360
|
-
className={cx(styles.addressTrigger)}
|
|
361
|
-
>
|
|
362
|
-
<IconGlobe size={13} ariaHidden />
|
|
363
|
-
<span {...stylex.props(styles.addressPath)}>{path}</span>
|
|
364
|
-
</Combobox.Trigger>
|
|
365
|
-
<Combobox.Portal>
|
|
366
|
-
<Combobox.Positioner
|
|
367
|
-
anchor={fieldRef}
|
|
368
|
-
side="bottom"
|
|
369
|
-
align="start"
|
|
370
|
-
sideOffset={6}
|
|
371
|
-
className={cx(popup.positioner)}
|
|
372
|
-
>
|
|
373
|
-
<Combobox.Popup
|
|
374
|
-
aria-label="Pages"
|
|
375
|
-
className={(state) => popupClassName(state, styles.routePopup)}
|
|
376
|
-
>
|
|
377
|
-
<form
|
|
378
|
-
onSubmit={(event: FormEvent<HTMLFormElement>) => {
|
|
379
|
-
event.preventDefault();
|
|
380
|
-
const fromList = picked.current;
|
|
381
|
-
picked.current = false;
|
|
382
|
-
if (fromList) return;
|
|
383
|
-
const typed = event.currentTarget.querySelector("input")?.value;
|
|
384
|
-
if (typed?.trim()) onPath(hostPath(typed));
|
|
385
|
-
setOpen(false);
|
|
386
|
-
setQuery(null);
|
|
387
|
-
}}
|
|
388
|
-
>
|
|
389
|
-
<Combobox.Input
|
|
390
|
-
ref={inputRef}
|
|
391
|
-
placeholder="Find a page"
|
|
392
|
-
aria-label="Find a page"
|
|
393
|
-
spellCheck={false}
|
|
394
|
-
autoComplete="off"
|
|
395
|
-
data-no-ring
|
|
396
|
-
className={cx(popup.quietInput, styles.routeSearch)}
|
|
397
|
-
onKeyDown={(event) => {
|
|
398
|
-
if (event.key !== "Enter") return;
|
|
399
|
-
// A highlighted row is chosen by the combobox. A bare Enter goes to the typed path.
|
|
400
|
-
if (event.currentTarget.getAttribute("aria-activedescendant")) return;
|
|
401
|
-
const typed = event.currentTarget.value.trim();
|
|
402
|
-
if (!typed) return;
|
|
403
|
-
picked.current = true;
|
|
404
|
-
onPath(hostPath(typed));
|
|
405
|
-
}}
|
|
406
|
-
/>
|
|
407
|
-
</form>
|
|
408
|
-
<div {...stylex.props(popup.separator)} />
|
|
409
|
-
<Combobox.Empty className={cx(popup.empty)}>
|
|
410
|
-
{routesPending ? "Loading pages" : "No pages"}
|
|
411
|
-
</Combobox.Empty>
|
|
412
|
-
<Combobox.List className={cx(styles.routeList)}>
|
|
413
|
-
{(route: string) => (
|
|
414
|
-
<Combobox.Item
|
|
415
|
-
key={route}
|
|
416
|
-
value={route}
|
|
417
|
-
className={(state) =>
|
|
418
|
-
cx(
|
|
419
|
-
popup.item,
|
|
420
|
-
styles.routeItem,
|
|
421
|
-
state.highlighted && popup.itemHighlighted,
|
|
422
|
-
state.selected && popup.itemSelected,
|
|
423
|
-
)
|
|
424
|
-
}
|
|
425
|
-
>
|
|
426
|
-
{route}
|
|
427
|
-
</Combobox.Item>
|
|
428
|
-
)}
|
|
429
|
-
</Combobox.List>
|
|
430
|
-
</Combobox.Popup>
|
|
431
|
-
</Combobox.Positioner>
|
|
432
|
-
</Combobox.Portal>
|
|
433
|
-
</Combobox.Root>
|
|
434
|
-
<button
|
|
435
|
-
type="button"
|
|
436
|
-
aria-label="Open preview in a new tab"
|
|
437
|
-
disabled={!enabled}
|
|
438
|
-
onClick={onOpen}
|
|
439
|
-
{...stylex.props(styles.iconButton)}
|
|
440
|
-
>
|
|
441
|
-
<IconArrowUpRight size={14} ariaHidden />
|
|
442
|
-
</button>
|
|
443
|
-
</div>
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
323
|
function BootCard({
|
|
448
324
|
title,
|
|
449
325
|
steps,
|
|
@@ -556,16 +432,26 @@ export default function CodeSessionPage() {
|
|
|
556
432
|
|
|
557
433
|
const sessionId = state.kind === "ready" ? state.sessionId : null;
|
|
558
434
|
const [editCount, setEditCount] = useState(0);
|
|
435
|
+
const [foldPending, setFoldPending] = useState(false);
|
|
436
|
+
const [baseSha, setBaseSha] = useState<string | null>(null);
|
|
437
|
+
const [updating, setUpdating] = useState(false);
|
|
559
438
|
const publishCodeSession = usePublishCodeSession();
|
|
560
439
|
|
|
561
440
|
const refreshEdits = useCallback(() => {
|
|
562
441
|
if (!sessionId) {
|
|
563
442
|
setEditCount(0);
|
|
443
|
+
setFoldPending(false);
|
|
564
444
|
return;
|
|
565
445
|
}
|
|
566
446
|
void fetchCodeSessionEdits(workspace.id, sessionId)
|
|
567
|
-
.then((list) =>
|
|
568
|
-
|
|
447
|
+
.then((list) => {
|
|
448
|
+
setEditCount(list.files.length);
|
|
449
|
+
setFoldPending(list.foldPending);
|
|
450
|
+
})
|
|
451
|
+
.catch(() => {
|
|
452
|
+
setEditCount(0);
|
|
453
|
+
setFoldPending(false);
|
|
454
|
+
});
|
|
569
455
|
}, [sessionId, workspace.id]);
|
|
570
456
|
|
|
571
457
|
// The host's dev server does not push a reload through the preview proxy.
|
|
@@ -591,9 +477,9 @@ export default function CodeSessionPage() {
|
|
|
591
477
|
publishCodeSession(null);
|
|
592
478
|
return;
|
|
593
479
|
}
|
|
594
|
-
publishCodeSession({ sessionId, editCount, refresh: refreshEdits });
|
|
480
|
+
publishCodeSession({ sessionId, editCount, refresh: refreshEdits, updating });
|
|
595
481
|
return () => publishCodeSession(null);
|
|
596
|
-
}, [sessionId, editCount, refreshEdits, publishCodeSession]);
|
|
482
|
+
}, [sessionId, editCount, refreshEdits, publishCodeSession, updating]);
|
|
597
483
|
|
|
598
484
|
// The agent and the dev server change the checkout, so read it fresh when the list is opened.
|
|
599
485
|
const loadCheckout = useCallback(async () => {
|
|
@@ -666,6 +552,7 @@ export default function CodeSessionPage() {
|
|
|
666
552
|
useEffect(() => {
|
|
667
553
|
let cancelled = false;
|
|
668
554
|
setState({ kind: "loading" });
|
|
555
|
+
setBaseSha(null);
|
|
669
556
|
setHost({ phase: null, furthest: null, log: "", timedOut: false });
|
|
670
557
|
setPreview(null);
|
|
671
558
|
const stored = storedPreviewPath(workspace.id);
|
|
@@ -707,8 +594,9 @@ export default function CodeSessionPage() {
|
|
|
707
594
|
setState({ kind: "fail", error: text || `request failed: ${res.status}` });
|
|
708
595
|
return;
|
|
709
596
|
}
|
|
710
|
-
const body = (await res.json()) as { session?: { id?: string } };
|
|
597
|
+
const body = (await res.json()) as { session?: { id?: string; baseSha?: string } };
|
|
711
598
|
const sessionId = body.session?.id;
|
|
599
|
+
setBaseSha(typeof body.session?.baseSha === "string" ? body.session.baseSha : null);
|
|
712
600
|
if (!sessionId) {
|
|
713
601
|
setState({ kind: "fail", error: "invalid response" });
|
|
714
602
|
return;
|
|
@@ -752,6 +640,78 @@ export default function CodeSessionPage() {
|
|
|
752
640
|
};
|
|
753
641
|
}, [workspace.id, branch]);
|
|
754
642
|
|
|
643
|
+
async function bootAgain(id: string, openPath: string) {
|
|
644
|
+
setPath(openPath);
|
|
645
|
+
setLocation(openPath);
|
|
646
|
+
setReloads(0);
|
|
647
|
+
setPreview(null);
|
|
648
|
+
setHost({ phase: null, furthest: null, log: "", timedOut: false });
|
|
649
|
+
const statusUrl = apiUrl(`/workspaces/${workspace.id}/code-sessions/${id}/status`);
|
|
650
|
+
const deadline = Date.now() + HOST_WAIT_MS;
|
|
651
|
+
while (Date.now() < deadline) {
|
|
652
|
+
const polled = await fetch(statusUrl, {
|
|
653
|
+
credentials: apiCredentials(),
|
|
654
|
+
headers: apiHeaders(),
|
|
655
|
+
}).catch(() => null);
|
|
656
|
+
if (polled?.ok) {
|
|
657
|
+
const status = (await polled.json()) as { phase: HostPhase; log: string };
|
|
658
|
+
setHost((current) => ({
|
|
659
|
+
phase: status.phase,
|
|
660
|
+
furthest:
|
|
661
|
+
status.phase === "installing" || status.phase === "starting"
|
|
662
|
+
? status.phase
|
|
663
|
+
: current.furthest,
|
|
664
|
+
log: status.log,
|
|
665
|
+
timedOut: false,
|
|
666
|
+
}));
|
|
667
|
+
if (status.phase === "ready") {
|
|
668
|
+
const entry = await requestPreview(workspace.id, id);
|
|
669
|
+
if (entry) setPreview(entry);
|
|
670
|
+
else setState({ kind: "fail", error: "The preview could not be opened." });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (status.phase === "failed" || status.phase === "stopped") return;
|
|
674
|
+
}
|
|
675
|
+
await new Promise((resolve) => setTimeout(resolve, STATUS_POLL_MS));
|
|
676
|
+
}
|
|
677
|
+
setHost((current) => ({ ...current, timedOut: true }));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function onUpdateBranch() {
|
|
681
|
+
if (!sessionId || updating) return;
|
|
682
|
+
setUpdating(true);
|
|
683
|
+
try {
|
|
684
|
+
const result = await updateCodeSessionBranch(workspace.id, sessionId);
|
|
685
|
+
setBaseSha(result.baseSha);
|
|
686
|
+
await bootAgain(sessionId, location);
|
|
687
|
+
} catch {
|
|
688
|
+
// The checkout stayed on its commit, and the preview that is up stays up.
|
|
689
|
+
} finally {
|
|
690
|
+
setUpdating(false);
|
|
691
|
+
refreshEdits();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function onClearEdits() {
|
|
696
|
+
if (!sessionId || updating) return;
|
|
697
|
+
const confirmed = window.confirm(
|
|
698
|
+
"Clear the edit set? The files go back to this session's commit. The chat stays.",
|
|
699
|
+
);
|
|
700
|
+
if (!confirmed) return;
|
|
701
|
+
setUpdating(true);
|
|
702
|
+
void (async () => {
|
|
703
|
+
try {
|
|
704
|
+
await clearCodeSessionEdits(workspace.id, sessionId);
|
|
705
|
+
await bootAgain(sessionId, location);
|
|
706
|
+
} catch {
|
|
707
|
+
// The edit set is still there.
|
|
708
|
+
} finally {
|
|
709
|
+
setUpdating(false);
|
|
710
|
+
refreshEdits();
|
|
711
|
+
}
|
|
712
|
+
})();
|
|
713
|
+
}
|
|
714
|
+
|
|
755
715
|
async function openInNewTab() {
|
|
756
716
|
if (state.kind !== "ready") return;
|
|
757
717
|
// The iframe's cookie is partitioned under Studio, so a new tab needs its own ticket.
|
|
@@ -792,7 +752,7 @@ export default function CodeSessionPage() {
|
|
|
792
752
|
live={live}
|
|
793
753
|
onReload={() => navigate(location)}
|
|
794
754
|
onOpen={() => void openInNewTab()}
|
|
795
|
-
routes={
|
|
755
|
+
routes={routesForContentFiles(models, fileList ?? [])}
|
|
796
756
|
routesPending={routesPending}
|
|
797
757
|
onOpenRoutes={openRoutes}
|
|
798
758
|
files={{
|
|
@@ -803,6 +763,19 @@ export default function CodeSessionPage() {
|
|
|
803
763
|
onSelect: (path) => void selectFile(path),
|
|
804
764
|
}}
|
|
805
765
|
chat={chatColumn}
|
|
766
|
+
notice={
|
|
767
|
+
updating
|
|
768
|
+
? null
|
|
769
|
+
: branchNoticeKind({
|
|
770
|
+
baseSha,
|
|
771
|
+
headSha: git?.head?.sha ?? null,
|
|
772
|
+
editCount,
|
|
773
|
+
foldPending,
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
noticeBusy={updating}
|
|
777
|
+
onUpdateBranch={() => void onUpdateBranch()}
|
|
778
|
+
onClearEdits={onClearEdits}
|
|
806
779
|
previewSize={previewSize}
|
|
807
780
|
onPreviewSize={(size) => {
|
|
808
781
|
setPreviewSize(size);
|
|
@@ -940,6 +913,38 @@ const styles = stylex.create({
|
|
|
940
913
|
backgroundColor: colors.text,
|
|
941
914
|
opacity: { default: 1, ":disabled": 0.3 },
|
|
942
915
|
},
|
|
916
|
+
notice: {
|
|
917
|
+
display: "flex",
|
|
918
|
+
alignItems: "center",
|
|
919
|
+
justifyContent: "space-between",
|
|
920
|
+
gap: space.md,
|
|
921
|
+
paddingInline: space.md,
|
|
922
|
+
paddingBlock: space.sm,
|
|
923
|
+
borderBottomWidth: 1,
|
|
924
|
+
borderBottomStyle: "solid",
|
|
925
|
+
borderBottomColor: colors.border,
|
|
926
|
+
backgroundColor: colors.bgSurface,
|
|
927
|
+
},
|
|
928
|
+
noticeText: {
|
|
929
|
+
fontSize: type.sm,
|
|
930
|
+
color: colors.text,
|
|
931
|
+
},
|
|
932
|
+
noticeButton: {
|
|
933
|
+
flexShrink: 0,
|
|
934
|
+
font: "inherit",
|
|
935
|
+
fontSize: type.xs,
|
|
936
|
+
fontWeight: 500,
|
|
937
|
+
cursor: { default: "pointer", ":disabled": "default" },
|
|
938
|
+
paddingBlock: "4px",
|
|
939
|
+
paddingInline: space.sm,
|
|
940
|
+
borderWidth: 1,
|
|
941
|
+
borderStyle: "solid",
|
|
942
|
+
borderColor: colors.border,
|
|
943
|
+
borderRadius: radius.sm,
|
|
944
|
+
color: colors.text,
|
|
945
|
+
backgroundColor: colors.bgPanelInset,
|
|
946
|
+
opacity: { default: 1, ":disabled": 0.4 },
|
|
947
|
+
},
|
|
943
948
|
stage: {
|
|
944
949
|
display: "flex",
|
|
945
950
|
flex: 1,
|
|
@@ -988,76 +993,9 @@ const styles = stylex.create({
|
|
|
988
993
|
backgroundColor: colors.bgSurface,
|
|
989
994
|
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
|
990
995
|
},
|
|
991
|
-
address: {
|
|
992
|
-
display: "flex",
|
|
993
|
-
alignItems: "center",
|
|
994
|
-
gap: space.xs,
|
|
995
|
-
minWidth: 0,
|
|
996
|
-
},
|
|
997
996
|
sizeSwitch: {
|
|
998
997
|
justifySelf: "end",
|
|
999
998
|
},
|
|
1000
|
-
addressTrigger: {
|
|
1001
|
-
display: "flex",
|
|
1002
|
-
flex: 1,
|
|
1003
|
-
alignItems: "center",
|
|
1004
|
-
gap: space.xs,
|
|
1005
|
-
minWidth: 0,
|
|
1006
|
-
height: "28px",
|
|
1007
|
-
paddingInline: space.sm,
|
|
1008
|
-
borderWidth: 0,
|
|
1009
|
-
borderRadius: "999px",
|
|
1010
|
-
font: "inherit",
|
|
1011
|
-
textAlign: "left",
|
|
1012
|
-
cursor: { default: "pointer", ":disabled": "default" },
|
|
1013
|
-
color: { default: colors.textMuted, ":disabled": colors.textMuted },
|
|
1014
|
-
backgroundColor: colors.bgPanelInset,
|
|
1015
|
-
},
|
|
1016
|
-
addressPath: {
|
|
1017
|
-
flex: 1,
|
|
1018
|
-
minWidth: 0,
|
|
1019
|
-
overflow: "hidden",
|
|
1020
|
-
textOverflow: "ellipsis",
|
|
1021
|
-
whiteSpace: "nowrap",
|
|
1022
|
-
fontFamily: type.mono,
|
|
1023
|
-
fontSize: type.xs,
|
|
1024
|
-
color: colors.text,
|
|
1025
|
-
},
|
|
1026
|
-
routeSearch: {
|
|
1027
|
-
fontFamily: type.mono,
|
|
1028
|
-
fontSize: type.xs,
|
|
1029
|
-
},
|
|
1030
|
-
routePopup: {
|
|
1031
|
-
width: "var(--anchor-width)",
|
|
1032
|
-
minWidth: "220px",
|
|
1033
|
-
maxHeight: "min(320px, var(--available-height))",
|
|
1034
|
-
display: "flex",
|
|
1035
|
-
flexDirection: "column",
|
|
1036
|
-
overflowY: "hidden",
|
|
1037
|
-
},
|
|
1038
|
-
routeList: {
|
|
1039
|
-
minHeight: 0,
|
|
1040
|
-
overflowY: "auto",
|
|
1041
|
-
},
|
|
1042
|
-
routeItem: {
|
|
1043
|
-
fontFamily: type.mono,
|
|
1044
|
-
},
|
|
1045
|
-
iconButton: {
|
|
1046
|
-
display: "inline-flex",
|
|
1047
|
-
alignItems: "center",
|
|
1048
|
-
justifyContent: "center",
|
|
1049
|
-
flexShrink: 0,
|
|
1050
|
-
width: "28px",
|
|
1051
|
-
height: "28px",
|
|
1052
|
-
borderWidth: 0,
|
|
1053
|
-
borderRadius: radius.sm,
|
|
1054
|
-
cursor: { default: "pointer", ":disabled": "default" },
|
|
1055
|
-
color: { default: colors.textMuted, ":disabled": colors.border },
|
|
1056
|
-
backgroundColor: {
|
|
1057
|
-
default: "transparent",
|
|
1058
|
-
[hover]: { default: null, ":hover:not(:disabled)": colors.bgHover },
|
|
1059
|
-
},
|
|
1060
|
-
},
|
|
1061
999
|
canvas: {
|
|
1062
1000
|
position: "relative",
|
|
1063
1001
|
display: "flex",
|