@agent-native/core 0.84.30 → 0.84.32
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/session-replay.ts +5 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +13 -1
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-keep-inlined-css-without-live-resource-loa.md +6 -0
- package/corpus/templates/design/AGENTS.md +18 -0
- package/corpus/templates/design/actions/apply-source-edit.ts +87 -0
- package/corpus/templates/design/actions/list-source-files.ts +52 -0
- package/corpus/templates/design/actions/navigate.ts +3 -2
- package/corpus/templates/design/actions/preview-source-edit.ts +85 -0
- package/corpus/templates/design/actions/read-source-file.ts +55 -0
- package/corpus/templates/design/actions/resolve-selection-source.ts +101 -0
- package/corpus/templates/design/actions/view-screen.ts +43 -1
- package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +630 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +9 -6
- package/corpus/templates/design/app/pages/DesignEditor.tsx +144 -9
- package/corpus/templates/design/changelog/2026-07-01-design-code-workspace.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +215 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +5 -5
- package/corpus/templates/design/shared/source-workspace.ts +149 -0
- package/dist/client/session-replay.d.ts +1 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +2 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -149,6 +149,10 @@ import {
|
|
|
149
149
|
CanvasContextMenu,
|
|
150
150
|
type CanvasContextMenuHandle,
|
|
151
151
|
} from "@/components/design/CanvasContextMenu";
|
|
152
|
+
import {
|
|
153
|
+
CodeWorkbenchHost,
|
|
154
|
+
type CodeWorkbenchActiveFile,
|
|
155
|
+
} from "@/components/design/CodeWorkbenchHost";
|
|
152
156
|
import {
|
|
153
157
|
DesignCanvas,
|
|
154
158
|
type IframeContextMenuPayload,
|
|
@@ -550,18 +554,34 @@ export function getDesignEditorStateUrlSearch(args: {
|
|
|
550
554
|
currentSearch: string;
|
|
551
555
|
viewMode: "single" | "overview";
|
|
552
556
|
screenId?: string | null;
|
|
557
|
+
codeFileId?: string | null;
|
|
558
|
+
codeFilename?: string | null;
|
|
553
559
|
selectionId?: string | null;
|
|
560
|
+
leftPanel?: DesignLeftPanel | null;
|
|
554
561
|
zoom?: number | null;
|
|
555
562
|
}) {
|
|
556
563
|
const params = new URLSearchParams(args.currentSearch);
|
|
557
564
|
params.set("view", args.viewMode);
|
|
565
|
+
if (args.leftPanel && args.leftPanel !== "file") {
|
|
566
|
+
params.set("panel", args.leftPanel);
|
|
567
|
+
} else {
|
|
568
|
+
params.delete("panel");
|
|
569
|
+
}
|
|
558
570
|
if (args.screenId) {
|
|
559
571
|
params.set("screen", args.screenId);
|
|
560
572
|
} else {
|
|
561
573
|
params.delete("screen");
|
|
562
574
|
}
|
|
563
|
-
|
|
564
|
-
|
|
575
|
+
if (args.leftPanel === "code" && args.codeFileId) {
|
|
576
|
+
params.set("fileId", args.codeFileId);
|
|
577
|
+
} else {
|
|
578
|
+
params.delete("fileId");
|
|
579
|
+
}
|
|
580
|
+
if (args.leftPanel === "code" && !args.codeFileId && args.codeFilename) {
|
|
581
|
+
params.set("filename", args.codeFilename);
|
|
582
|
+
} else {
|
|
583
|
+
params.delete("filename");
|
|
584
|
+
}
|
|
565
585
|
if (args.selectionId) {
|
|
566
586
|
params.set("selection", args.selectionId);
|
|
567
587
|
} else {
|
|
@@ -3495,13 +3515,20 @@ function AgentNativeMenuMark({ className }: { className?: string }) {
|
|
|
3495
3515
|
);
|
|
3496
3516
|
}
|
|
3497
3517
|
|
|
3498
|
-
type DesignLeftPanel =
|
|
3518
|
+
type DesignLeftPanel =
|
|
3519
|
+
| "file"
|
|
3520
|
+
| "agent"
|
|
3521
|
+
| "assets"
|
|
3522
|
+
| "tools"
|
|
3523
|
+
| "tokens"
|
|
3524
|
+
| "code";
|
|
3499
3525
|
|
|
3500
3526
|
const INITIAL_GENERATION_DISABLED_LEFT_PANELS = new Set<DesignLeftPanel>([
|
|
3501
3527
|
"file",
|
|
3502
3528
|
"assets",
|
|
3503
3529
|
"tools",
|
|
3504
3530
|
"tokens",
|
|
3531
|
+
"code",
|
|
3505
3532
|
]);
|
|
3506
3533
|
|
|
3507
3534
|
function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
|
|
@@ -3510,7 +3537,8 @@ function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
|
|
|
3510
3537
|
value === "agent" ||
|
|
3511
3538
|
value === "assets" ||
|
|
3512
3539
|
value === "tools" ||
|
|
3513
|
-
value === "tokens"
|
|
3540
|
+
value === "tokens" ||
|
|
3541
|
+
value === "code"
|
|
3514
3542
|
? value
|
|
3515
3543
|
: undefined;
|
|
3516
3544
|
}
|
|
@@ -3564,6 +3592,11 @@ function DesignWorkspaceRail({
|
|
|
3564
3592
|
icon: <IconAssembly className="size-[15px]" />,
|
|
3565
3593
|
},
|
|
3566
3594
|
];
|
|
3595
|
+
const codeItem = {
|
|
3596
|
+
panel: "code" as const,
|
|
3597
|
+
label: "Code" /* i18n-ignore */,
|
|
3598
|
+
icon: <IconCode className="size-[15px]" />,
|
|
3599
|
+
};
|
|
3567
3600
|
|
|
3568
3601
|
return (
|
|
3569
3602
|
<nav
|
|
@@ -3622,6 +3655,51 @@ function DesignWorkspaceRail({
|
|
|
3622
3655
|
</Tooltip>
|
|
3623
3656
|
);
|
|
3624
3657
|
})}
|
|
3658
|
+
<div className="mt-auto flex w-full flex-col items-center border-t border-border/70 pt-3">
|
|
3659
|
+
<Tooltip>
|
|
3660
|
+
<TooltipTrigger asChild>
|
|
3661
|
+
<button
|
|
3662
|
+
type="button"
|
|
3663
|
+
aria-label={codeItem.label}
|
|
3664
|
+
aria-disabled={disabledPanels?.has(codeItem.panel) || undefined}
|
|
3665
|
+
aria-current={
|
|
3666
|
+
activePanel === codeItem.panel ? "page" : undefined
|
|
3667
|
+
}
|
|
3668
|
+
tabIndex={disabledPanels?.has(codeItem.panel) ? -1 : undefined}
|
|
3669
|
+
onClick={(event) => {
|
|
3670
|
+
if (disabledPanels?.has(codeItem.panel)) {
|
|
3671
|
+
event.preventDefault();
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
onPanelChange(codeItem.panel);
|
|
3675
|
+
}}
|
|
3676
|
+
className={cn(
|
|
3677
|
+
"group flex w-12 cursor-pointer flex-col items-center justify-start gap-1 rounded-none text-[10px] font-[450] leading-none text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
|
|
3678
|
+
disabledPanels?.has(codeItem.panel) &&
|
|
3679
|
+
"cursor-default opacity-35 hover:text-muted-foreground",
|
|
3680
|
+
activePanel === codeItem.panel && "text-foreground",
|
|
3681
|
+
)}
|
|
3682
|
+
>
|
|
3683
|
+
<span
|
|
3684
|
+
className={cn(
|
|
3685
|
+
"flex size-8 items-center justify-center rounded-lg transition-colors",
|
|
3686
|
+
activePanel === codeItem.panel
|
|
3687
|
+
? "bg-[var(--design-editor-selection-color)] text-[var(--design-editor-accent-color)]"
|
|
3688
|
+
: "text-muted-foreground group-hover:bg-[var(--design-editor-layer-hover-color)] group-hover:text-foreground",
|
|
3689
|
+
disabledPanels?.has(codeItem.panel) &&
|
|
3690
|
+
"group-hover:bg-transparent group-hover:text-muted-foreground",
|
|
3691
|
+
)}
|
|
3692
|
+
>
|
|
3693
|
+
{codeItem.icon}
|
|
3694
|
+
</span>
|
|
3695
|
+
<span className="max-w-full truncate leading-none">
|
|
3696
|
+
{codeItem.label}
|
|
3697
|
+
</span>
|
|
3698
|
+
</button>
|
|
3699
|
+
</TooltipTrigger>
|
|
3700
|
+
<TooltipContent side="right">{codeItem.label}</TooltipContent>
|
|
3701
|
+
</Tooltip>
|
|
3702
|
+
</div>
|
|
3625
3703
|
</div>
|
|
3626
3704
|
{onMotionToggle ? (
|
|
3627
3705
|
<div className="mt-4 flex w-full flex-col items-center border-t border-border/70 pt-3">
|
|
@@ -4608,6 +4686,8 @@ export default function DesignEditor() {
|
|
|
4608
4686
|
useState<InspectorTab>("design");
|
|
4609
4687
|
const [activeLeftPanel, setActiveLeftPanel] =
|
|
4610
4688
|
useState<DesignLeftPanel>("file");
|
|
4689
|
+
const [activeCodeFile, setActiveCodeFile] =
|
|
4690
|
+
useState<CodeWorkbenchActiveFile | null>(null);
|
|
4611
4691
|
const initialSearchCommandAppliedForIdRef = useRef<string | null>(null);
|
|
4612
4692
|
const initialUrlSelectionHydratedForIdRef = useRef<string | null>(null);
|
|
4613
4693
|
const [leftSidebarWidth, setLeftSidebarWidth] = useState(256);
|
|
@@ -4896,11 +4976,17 @@ export default function DesignEditor() {
|
|
|
4896
4976
|
event.stopPropagation();
|
|
4897
4977
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
4898
4978
|
const startX = event.clientX;
|
|
4899
|
-
const
|
|
4979
|
+
const codePanelOpen = side === "left" && activeLeftPanel === "code";
|
|
4980
|
+
const startWidth =
|
|
4981
|
+
side === "left"
|
|
4982
|
+
? codePanelOpen
|
|
4983
|
+
? Math.max(leftSidebarWidth, 640)
|
|
4984
|
+
: Math.min(leftSidebarWidth, 420)
|
|
4985
|
+
: rightSidebarWidth;
|
|
4900
4986
|
const setWidth =
|
|
4901
4987
|
side === "left" ? setLeftSidebarWidth : setRightSidebarWidth;
|
|
4902
|
-
const minWidth = side === "left" ? 220 : 240;
|
|
4903
|
-
const maxWidth = side === "left" ? 420 : 390;
|
|
4988
|
+
const minWidth = side === "left" ? (codePanelOpen ? 520 : 220) : 240;
|
|
4989
|
+
const maxWidth = side === "left" ? (codePanelOpen ? 860 : 420) : 390;
|
|
4904
4990
|
const previousCursor = document.body.style.cursor;
|
|
4905
4991
|
const previousUserSelect = document.body.style.userSelect;
|
|
4906
4992
|
const dragShield = document.createElement("div");
|
|
@@ -4939,7 +5025,7 @@ export default function DesignEditor() {
|
|
|
4939
5025
|
window.addEventListener("pointerup", cleanup);
|
|
4940
5026
|
window.addEventListener("pointercancel", cleanup);
|
|
4941
5027
|
},
|
|
4942
|
-
[leftSidebarWidth, rightSidebarWidth],
|
|
5028
|
+
[activeLeftPanel, leftSidebarWidth, rightSidebarWidth],
|
|
4943
5029
|
);
|
|
4944
5030
|
// Undo/redo state driven by Y.UndoManager
|
|
4945
5031
|
const [canUndo, setCanUndo] = useState(false);
|
|
@@ -9134,6 +9220,14 @@ export default function DesignEditor() {
|
|
|
9134
9220
|
activeTool,
|
|
9135
9221
|
inspectorTab: activeInspectorTab,
|
|
9136
9222
|
leftPanel: activeLeftPanel,
|
|
9223
|
+
codeWorkspace: {
|
|
9224
|
+
open: activeLeftPanel === "code",
|
|
9225
|
+
backendKind: activeCodeFile?.backendKind ?? "virtual-inline",
|
|
9226
|
+
activePath: activeCodeFile?.path ?? null,
|
|
9227
|
+
activeFileId: activeCodeFile?.fileId ?? null,
|
|
9228
|
+
dirty: activeCodeFile?.dirty ?? false,
|
|
9229
|
+
versionHash: activeCodeFile?.versionHash ?? null,
|
|
9230
|
+
},
|
|
9137
9231
|
// §8 DesignNavigationState additions — dock + breakpoint context
|
|
9138
9232
|
dock: { kind: "motion" as const, open: motionDockOpen },
|
|
9139
9233
|
motion: {
|
|
@@ -9202,6 +9296,7 @@ export default function DesignEditor() {
|
|
|
9202
9296
|
activeTool: selection.activeTool,
|
|
9203
9297
|
inspectorTab: selection.inspectorTab,
|
|
9204
9298
|
leftPanel: selection.leftPanel,
|
|
9299
|
+
codeWorkspace: selection.codeWorkspace,
|
|
9205
9300
|
dock: selection.dock,
|
|
9206
9301
|
motion: selection.motion,
|
|
9207
9302
|
breakpoint: selection.breakpoint,
|
|
@@ -9243,6 +9338,7 @@ export default function DesignEditor() {
|
|
|
9243
9338
|
activeTool,
|
|
9244
9339
|
activeInspectorTab,
|
|
9245
9340
|
activeLeftPanel,
|
|
9341
|
+
activeCodeFile,
|
|
9246
9342
|
overviewSelectedScreenIds,
|
|
9247
9343
|
viewMode,
|
|
9248
9344
|
zoom,
|
|
@@ -14352,6 +14448,9 @@ ${serializedHtml}
|
|
|
14352
14448
|
currentSearch: location.search,
|
|
14353
14449
|
viewMode,
|
|
14354
14450
|
screenId: activeFile?.id ?? activeFileId,
|
|
14451
|
+
leftPanel: activeLeftPanel,
|
|
14452
|
+
codeFileId: activeLeftPanel === "code" ? activeCodeFile?.fileId : null,
|
|
14453
|
+
codeFilename: activeLeftPanel === "code" ? activeCodeFile?.path : null,
|
|
14355
14454
|
selectionId:
|
|
14356
14455
|
selectedUrlSelectionId ??
|
|
14357
14456
|
(preserveInitialRouteSelection ? initialRouteSelectionId : null),
|
|
@@ -14369,6 +14468,9 @@ ${serializedHtml}
|
|
|
14369
14468
|
}, [
|
|
14370
14469
|
activeFile?.id,
|
|
14371
14470
|
activeFileId,
|
|
14471
|
+
activeCodeFile?.fileId,
|
|
14472
|
+
activeCodeFile?.path,
|
|
14473
|
+
activeLeftPanel,
|
|
14372
14474
|
codeLayerOwnerByNodeId.size,
|
|
14373
14475
|
files,
|
|
14374
14476
|
id,
|
|
@@ -16287,7 +16389,14 @@ ${serializedHtml}
|
|
|
16287
16389
|
</div>
|
|
16288
16390
|
);
|
|
16289
16391
|
|
|
16290
|
-
const leftContentWidth =
|
|
16392
|
+
const leftContentWidth =
|
|
16393
|
+
activeLeftPanel === "code"
|
|
16394
|
+
? Math.max(leftSidebarWidth, 640)
|
|
16395
|
+
: Math.max(Math.min(leftSidebarWidth, 420), 320);
|
|
16396
|
+
const routeCodeFileId =
|
|
16397
|
+
activeLeftPanel === "code" ? searchParams.get("fileId") : null;
|
|
16398
|
+
const routeCodeFilename =
|
|
16399
|
+
activeLeftPanel === "code" ? searchParams.get("filename") : null;
|
|
16291
16400
|
return (
|
|
16292
16401
|
// h-full not flex-1: the parent <main> uses overflow-y-auto, not flex,
|
|
16293
16402
|
// so flex-1 on the child doesn't resolve to the available height. h-full
|
|
@@ -16815,6 +16924,32 @@ ${serializedHtml}
|
|
|
16815
16924
|
/>
|
|
16816
16925
|
)}
|
|
16817
16926
|
</div>
|
|
16927
|
+
<div
|
|
16928
|
+
className={cn(
|
|
16929
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
16930
|
+
activeLeftPanel === "code" ? "flex" : "hidden",
|
|
16931
|
+
)}
|
|
16932
|
+
>
|
|
16933
|
+
{id ? (
|
|
16934
|
+
<CodeWorkbenchHost
|
|
16935
|
+
designId={id}
|
|
16936
|
+
activeFileId={
|
|
16937
|
+
activeLeftPanel === "code"
|
|
16938
|
+
? routeCodeFileId
|
|
16939
|
+
: (activeFile?.id ?? activeFileId)
|
|
16940
|
+
}
|
|
16941
|
+
activeFilename={
|
|
16942
|
+
activeLeftPanel === "code"
|
|
16943
|
+
? routeCodeFilename
|
|
16944
|
+
: activeFile?.filename
|
|
16945
|
+
}
|
|
16946
|
+
selectedNodeId={selectedElementLayerId}
|
|
16947
|
+
selectedSelector={selectedCanvasSelector}
|
|
16948
|
+
canEdit={canEditDesign}
|
|
16949
|
+
onActiveFileChange={setActiveCodeFile}
|
|
16950
|
+
/>
|
|
16951
|
+
) : null}
|
|
16952
|
+
</div>
|
|
16818
16953
|
</div>
|
|
16819
16954
|
<div
|
|
16820
16955
|
role="separator"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasCollabState,
|
|
3
|
+
getText,
|
|
4
|
+
applyText,
|
|
5
|
+
seedFromText,
|
|
6
|
+
} from "@agent-native/core/collab";
|
|
7
|
+
import { assertAccess, resolveAccess } from "@agent-native/core/sharing";
|
|
8
|
+
import { eq } from "drizzle-orm";
|
|
9
|
+
|
|
10
|
+
import { isBoardFile } from "../shared/board-file.js";
|
|
11
|
+
import { normalizeDesignSourceType } from "../shared/source-mode.js";
|
|
12
|
+
import type { DesignSourceType } from "../shared/source-mode.js";
|
|
13
|
+
import {
|
|
14
|
+
languageForSourcePath,
|
|
15
|
+
normalizeInlineSourcePath,
|
|
16
|
+
sourceContentHash,
|
|
17
|
+
} from "../shared/source-workspace.js";
|
|
18
|
+
import { getDb, schema } from "./db/index.js";
|
|
19
|
+
import "./db/index.js"; // ensure registerShareableResource runs
|
|
20
|
+
|
|
21
|
+
export interface SourceWorkspaceFile {
|
|
22
|
+
id: string;
|
|
23
|
+
designId: string;
|
|
24
|
+
filename: string;
|
|
25
|
+
fileType: string;
|
|
26
|
+
content?: string | null;
|
|
27
|
+
createdAt: string | null;
|
|
28
|
+
updatedAt: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SourceWorkspaceContext {
|
|
32
|
+
designId: string;
|
|
33
|
+
sourceType: DesignSourceType;
|
|
34
|
+
canEdit: boolean;
|
|
35
|
+
files: SourceWorkspaceFile[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseDesignDataSourceType(value: unknown): DesignSourceType {
|
|
39
|
+
if (typeof value !== "string") return "inline";
|
|
40
|
+
try {
|
|
41
|
+
const parsed: unknown = JSON.parse(value);
|
|
42
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
43
|
+
const raw = (parsed as Record<string, unknown>).sourceType;
|
|
44
|
+
return normalizeDesignSourceType(raw) ?? "inline";
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Invalid design data falls back to inline, matching existing actions.
|
|
48
|
+
}
|
|
49
|
+
return "inline";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function roleCanEdit(role: unknown): boolean {
|
|
53
|
+
return role === "owner" || role === "admin" || role === "editor";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function resolveSourceWorkspace(
|
|
57
|
+
designId: string,
|
|
58
|
+
options: { includeContent?: boolean } = {},
|
|
59
|
+
): Promise<SourceWorkspaceContext> {
|
|
60
|
+
const access = await resolveAccess("design", designId);
|
|
61
|
+
if (!access) throw new Error("Design not found");
|
|
62
|
+
|
|
63
|
+
const db = getDb();
|
|
64
|
+
const files = options.includeContent
|
|
65
|
+
? await db
|
|
66
|
+
.select({
|
|
67
|
+
id: schema.designFiles.id,
|
|
68
|
+
designId: schema.designFiles.designId,
|
|
69
|
+
filename: schema.designFiles.filename,
|
|
70
|
+
fileType: schema.designFiles.fileType,
|
|
71
|
+
content: schema.designFiles.content,
|
|
72
|
+
createdAt: schema.designFiles.createdAt,
|
|
73
|
+
updatedAt: schema.designFiles.updatedAt,
|
|
74
|
+
})
|
|
75
|
+
.from(schema.designFiles)
|
|
76
|
+
.where(eq(schema.designFiles.designId, designId))
|
|
77
|
+
: await db
|
|
78
|
+
.select({
|
|
79
|
+
id: schema.designFiles.id,
|
|
80
|
+
designId: schema.designFiles.designId,
|
|
81
|
+
filename: schema.designFiles.filename,
|
|
82
|
+
fileType: schema.designFiles.fileType,
|
|
83
|
+
createdAt: schema.designFiles.createdAt,
|
|
84
|
+
updatedAt: schema.designFiles.updatedAt,
|
|
85
|
+
})
|
|
86
|
+
.from(schema.designFiles)
|
|
87
|
+
.where(eq(schema.designFiles.designId, designId));
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
designId,
|
|
91
|
+
sourceType: parseDesignDataSourceType(
|
|
92
|
+
(access.resource as { data?: unknown }).data,
|
|
93
|
+
),
|
|
94
|
+
canEdit: roleCanEdit(access.role),
|
|
95
|
+
files: files.filter((file) => !isBoardFile(file.filename)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function findSourceWorkspaceFile(
|
|
100
|
+
files: SourceWorkspaceFile[],
|
|
101
|
+
target: { fileId?: string; path?: string },
|
|
102
|
+
): SourceWorkspaceFile {
|
|
103
|
+
const normalizedPath =
|
|
104
|
+
target.path !== undefined ? normalizeInlineSourcePath(target.path) : null;
|
|
105
|
+
const file = target.fileId
|
|
106
|
+
? files.find((candidate) => candidate.id === target.fileId)
|
|
107
|
+
: files.find((candidate) => candidate.filename === normalizedPath);
|
|
108
|
+
if (!file) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
target.fileId
|
|
111
|
+
? `Source file id "${target.fileId}" not found.`
|
|
112
|
+
: `Source file "${normalizedPath}" not found.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return file;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function readLiveSourceFile(file: SourceWorkspaceFile): Promise<{
|
|
119
|
+
content: string;
|
|
120
|
+
versionHash: string;
|
|
121
|
+
language: string;
|
|
122
|
+
}> {
|
|
123
|
+
let content = file.content ?? "";
|
|
124
|
+
try {
|
|
125
|
+
if (await hasCollabState(file.id)) {
|
|
126
|
+
const live = await getText(file.id, "content");
|
|
127
|
+
if (typeof live === "string") content = live;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Collab reads are best-effort; SQL content is the fallback.
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
content,
|
|
134
|
+
versionHash: sourceContentHash(content),
|
|
135
|
+
language: languageForSourcePath(file.filename),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function writeInlineSourceFile(args: {
|
|
140
|
+
designId: string;
|
|
141
|
+
file: SourceWorkspaceFile;
|
|
142
|
+
content: string;
|
|
143
|
+
expectedVersionHash?: string;
|
|
144
|
+
}): Promise<{ versionHash: string; changed: boolean; updatedAt: string }> {
|
|
145
|
+
await assertAccess("design", args.designId, "editor");
|
|
146
|
+
const db = getDb();
|
|
147
|
+
const [currentFile] = await db
|
|
148
|
+
.select({
|
|
149
|
+
id: schema.designFiles.id,
|
|
150
|
+
designId: schema.designFiles.designId,
|
|
151
|
+
filename: schema.designFiles.filename,
|
|
152
|
+
fileType: schema.designFiles.fileType,
|
|
153
|
+
content: schema.designFiles.content,
|
|
154
|
+
createdAt: schema.designFiles.createdAt,
|
|
155
|
+
updatedAt: schema.designFiles.updatedAt,
|
|
156
|
+
})
|
|
157
|
+
.from(schema.designFiles)
|
|
158
|
+
.where(eq(schema.designFiles.id, args.file.id))
|
|
159
|
+
.limit(1);
|
|
160
|
+
if (!currentFile || currentFile.designId !== args.designId) {
|
|
161
|
+
throw new Error("Source file not found.");
|
|
162
|
+
}
|
|
163
|
+
const current = await readLiveSourceFile(currentFile);
|
|
164
|
+
if (
|
|
165
|
+
args.expectedVersionHash &&
|
|
166
|
+
args.expectedVersionHash !== current.versionHash
|
|
167
|
+
) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
"Source file changed since it was read. Re-read the file and retry.",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const changed = args.content !== current.content;
|
|
174
|
+
const updatedAt = new Date().toISOString();
|
|
175
|
+
if (!changed) {
|
|
176
|
+
return {
|
|
177
|
+
versionHash: current.versionHash,
|
|
178
|
+
changed: false,
|
|
179
|
+
updatedAt: currentFile.updatedAt ?? updatedAt,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (await hasCollabState(args.file.id)) {
|
|
184
|
+
const liveBeforeApply = await getText(args.file.id, "content");
|
|
185
|
+
if (
|
|
186
|
+
args.expectedVersionHash &&
|
|
187
|
+
args.expectedVersionHash !== sourceContentHash(liveBeforeApply)
|
|
188
|
+
) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"Source file changed since it was read. Re-read the file and retry.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (liveBeforeApply !== args.content) {
|
|
194
|
+
await applyText(args.file.id, args.content, "content", "agent");
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
await seedFromText(args.file.id, args.content);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await db
|
|
201
|
+
.update(schema.designFiles)
|
|
202
|
+
.set({ content: args.content, updatedAt })
|
|
203
|
+
.where(eq(schema.designFiles.id, args.file.id));
|
|
204
|
+
|
|
205
|
+
await db
|
|
206
|
+
.update(schema.designs)
|
|
207
|
+
.set({ updatedAt })
|
|
208
|
+
.where(eq(schema.designs.id, args.designId));
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
versionHash: sourceContentHash(args.content),
|
|
212
|
+
changed: true,
|
|
213
|
+
updatedAt,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -147,15 +147,15 @@ export function unavailable(reason?: string): DesignSourceCapabilityEntry {
|
|
|
147
147
|
*
|
|
148
148
|
* - CSS-var token edits and motion are available through the Tweaks loop and
|
|
149
149
|
* the managed `<style data-agent-native-motion>` block respectively.
|
|
150
|
-
* - File-level
|
|
151
|
-
*
|
|
150
|
+
* - File-level ops (`readFile`, `writeFile`, `applyEdit`) are available for
|
|
151
|
+
* inline SQL-backed design_files through the Design source action surface.
|
|
152
152
|
* - Real-app-only capabilities (`indexComponents`, `writeTokens`, `branch`,
|
|
153
153
|
* `deploy*`) are `unavailable` and trigger the "Make it real" CTA.
|
|
154
154
|
*/
|
|
155
155
|
export const INLINE_DEFAULT_CAPABILITIES: DesignSourceCapabilities = {
|
|
156
|
-
readFile:
|
|
157
|
-
writeFile:
|
|
158
|
-
applyEdit:
|
|
156
|
+
readFile: available("Inline design files can be read from Design"),
|
|
157
|
+
writeFile: available("Inline design files can be saved through Design"),
|
|
158
|
+
applyEdit: available("Inline design files can be edited through Design"),
|
|
159
159
|
resolveNodeToFile: available(),
|
|
160
160
|
previewPatch: available(),
|
|
161
161
|
diffPatch: available(),
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export type SourceEdit =
|
|
2
|
+
| {
|
|
3
|
+
kind: "full-replace";
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
| {
|
|
7
|
+
kind: "exact-replace";
|
|
8
|
+
search: string;
|
|
9
|
+
replace: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function sourceContentHash(content: string): string {
|
|
13
|
+
let hash = 2166136261;
|
|
14
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
15
|
+
hash ^= content.charCodeAt(index);
|
|
16
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
return `${content.length}:${hash.toString(36)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeInlineSourcePath(path: string): string {
|
|
22
|
+
const normalized = path.trim().replace(/^\/+/, "");
|
|
23
|
+
if (!normalized) throw new Error("Source path is required.");
|
|
24
|
+
if (
|
|
25
|
+
normalized.includes("..") ||
|
|
26
|
+
normalized.startsWith("~") ||
|
|
27
|
+
normalized.includes("\\")
|
|
28
|
+
) {
|
|
29
|
+
throw new Error("Invalid source path.");
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function languageForSourcePath(path: string): string {
|
|
35
|
+
const lower = path.toLowerCase();
|
|
36
|
+
if (lower.endsWith(".html") || lower.endsWith(".htm")) return "html";
|
|
37
|
+
if (lower.endsWith(".css")) return "css";
|
|
38
|
+
if (lower.endsWith(".jsx")) return "javascriptreact";
|
|
39
|
+
if (lower.endsWith(".tsx")) return "typescriptreact";
|
|
40
|
+
if (lower.endsWith(".ts")) return "typescript";
|
|
41
|
+
if (lower.endsWith(".js")) return "javascript";
|
|
42
|
+
if (lower.endsWith(".json")) return "json";
|
|
43
|
+
if (lower.endsWith(".md") || lower.endsWith(".mdx")) return "markdown";
|
|
44
|
+
return "plaintext";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function applySourceEdit(
|
|
48
|
+
content: string,
|
|
49
|
+
edit: SourceEdit,
|
|
50
|
+
): {
|
|
51
|
+
content: string;
|
|
52
|
+
changed: boolean;
|
|
53
|
+
editsApplied: number;
|
|
54
|
+
} {
|
|
55
|
+
if (edit.kind === "full-replace") {
|
|
56
|
+
return {
|
|
57
|
+
content: edit.content,
|
|
58
|
+
changed: edit.content !== content,
|
|
59
|
+
editsApplied: edit.content === content ? 0 : 1,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!edit.search) {
|
|
64
|
+
throw new Error("exact-replace requires non-empty search text.");
|
|
65
|
+
}
|
|
66
|
+
const firstIndex = content.indexOf(edit.search);
|
|
67
|
+
if (firstIndex === -1) {
|
|
68
|
+
throw new Error("Search text was not found in the source file.");
|
|
69
|
+
}
|
|
70
|
+
const secondIndex = content.indexOf(edit.search, firstIndex + 1);
|
|
71
|
+
if (secondIndex !== -1) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Search text matched more than once. Add more surrounding context.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const nextContent =
|
|
77
|
+
content.slice(0, firstIndex) +
|
|
78
|
+
edit.replace +
|
|
79
|
+
content.slice(firstIndex + edit.search.length);
|
|
80
|
+
return {
|
|
81
|
+
content: nextContent,
|
|
82
|
+
changed: nextContent !== content,
|
|
83
|
+
editsApplied: nextContent === content ? 0 : 1,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lineNumberAtOffset(content: string, offset: number): number {
|
|
88
|
+
let line = 1;
|
|
89
|
+
for (let index = 0; index < offset; index += 1) {
|
|
90
|
+
if (content.charCodeAt(index) === 10) line += 1;
|
|
91
|
+
}
|
|
92
|
+
return line;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function commonPrefixLength(a: string, b: string): number {
|
|
96
|
+
const max = Math.min(a.length, b.length);
|
|
97
|
+
let index = 0;
|
|
98
|
+
while (index < max && a[index] === b[index]) index += 1;
|
|
99
|
+
return index;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function commonSuffixLength(
|
|
103
|
+
a: string,
|
|
104
|
+
b: string,
|
|
105
|
+
prefixLength: number,
|
|
106
|
+
): number {
|
|
107
|
+
const max = Math.min(a.length, b.length) - prefixLength;
|
|
108
|
+
let index = 0;
|
|
109
|
+
while (index < max && a[a.length - 1 - index] === b[b.length - 1 - index]) {
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
112
|
+
return index;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function excerpt(value: string, max = 1200): string {
|
|
116
|
+
if (value.length <= max) return value;
|
|
117
|
+
return `${value.slice(0, max)}\n...`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function previewSourceDiff(before: string, after: string) {
|
|
121
|
+
if (before === after) {
|
|
122
|
+
return {
|
|
123
|
+
changed: false,
|
|
124
|
+
lineStart: null,
|
|
125
|
+
lineEnd: null,
|
|
126
|
+
bytesBefore: before.length,
|
|
127
|
+
bytesAfter: after.length,
|
|
128
|
+
beforeExcerpt: "",
|
|
129
|
+
afterExcerpt: "",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const prefix = commonPrefixLength(before, after);
|
|
134
|
+
const suffix = commonSuffixLength(before, after, prefix);
|
|
135
|
+
const beforeEnd = before.length - suffix;
|
|
136
|
+
const afterEnd = after.length - suffix;
|
|
137
|
+
const lineStart = lineNumberAtOffset(before, prefix);
|
|
138
|
+
const lineEnd = lineNumberAtOffset(before, beforeEnd);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
changed: true,
|
|
142
|
+
lineStart,
|
|
143
|
+
lineEnd,
|
|
144
|
+
bytesBefore: before.length,
|
|
145
|
+
bytesAfter: after.length,
|
|
146
|
+
beforeExcerpt: excerpt(before.slice(prefix, beforeEnd)),
|
|
147
|
+
afterExcerpt: excerpt(after.slice(prefix, afterEnd)),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-replay.d.ts","sourceRoot":"","sources":["../../src/client/session-replay.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,uBAAuB,GAC/B,MAAM,GACN,MAAM,GACN,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"session-replay.d.ts","sourceRoot":"","sources":["../../src/client/session-replay.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,uBAAuB,GAC/B,MAAM,GACN,MAAM,GACN,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;AAoD/B,iFAAiF;AACjF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1D,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,uBAAuB,EAAE,CAAC;IACtC,SAAS,CAAC,EAAE,uBAAuB,EAAE,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,eAAe,CAAC,EACZ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACvB,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EACH,UAAU,GACV,aAAa,GACb,oBAAoB,GACpB,oBAAoB,GACpB,iBAAiB,GACjB,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,eAAe,GACf,eAAe,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAqRD,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,IAAI,SAAwB,GAC3B,MAAM,CAQR;AAED,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,UAAU,SAAI,EACd,IAAI,SAAwB,GAC3B,OAAO,CAKT;AAkaD,wBAAsB,kBAAkB,CAAC,MAAM,SAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzE;AAkDD,wBAAsB,kBAAkB,CACtC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA2DnC;AA2GD,wBAAsB,iBAAiB,CAAC,MAAM,SAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBxE;AAED,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAEnC;AAED,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C"}
|