@agent-native/core 0.84.29 → 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 +19 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +50 -8
- package/corpus/core/src/client/session-replay.ts +5 -0
- package/corpus/core/src/client/sse-event-processor.ts +17 -4
- package/corpus/core/src/server/ssr-handler.ts +152 -8
- 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 +189 -11
- package/corpus/templates/design/changelog/2026-07-01-design-code-workspace.md +6 -0
- package/corpus/templates/design/changelog/2026-07-01-design-previews-refresh-immediately-after-agent-screen-edits.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/chat/tool-call-display.d.ts +1 -0
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +22 -5
- package/dist/client/chat/tool-call-display.js.map +1 -1
- 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/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +13 -4
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +111 -8
- package/dist/server/ssr-handler.js.map +1 -1
- package/package.json +1 -1
|
@@ -67,7 +67,10 @@ import {
|
|
|
67
67
|
type TweakSelections,
|
|
68
68
|
} from "@shared/resolve-tweaks";
|
|
69
69
|
import { utilityStem, widthToPrefix } from "@shared/responsive-classes";
|
|
70
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
normalizeDesignSourceType,
|
|
72
|
+
type DesignSourceType,
|
|
73
|
+
} from "@shared/source-mode";
|
|
71
74
|
import {
|
|
72
75
|
IconArrowLeft,
|
|
73
76
|
IconArrowUpRight,
|
|
@@ -146,6 +149,10 @@ import {
|
|
|
146
149
|
CanvasContextMenu,
|
|
147
150
|
type CanvasContextMenuHandle,
|
|
148
151
|
} from "@/components/design/CanvasContextMenu";
|
|
152
|
+
import {
|
|
153
|
+
CodeWorkbenchHost,
|
|
154
|
+
type CodeWorkbenchActiveFile,
|
|
155
|
+
} from "@/components/design/CodeWorkbenchHost";
|
|
149
156
|
import {
|
|
150
157
|
DesignCanvas,
|
|
151
158
|
type IframeContextMenuPayload,
|
|
@@ -347,6 +354,28 @@ function getContentSignature(content: string): string {
|
|
|
347
354
|
return `${content.length}:${hash.toString(36)}`;
|
|
348
355
|
}
|
|
349
356
|
|
|
357
|
+
export function getOverviewScreenRuntimeReplacementKey({
|
|
358
|
+
screenId,
|
|
359
|
+
updatedAt,
|
|
360
|
+
content,
|
|
361
|
+
}: {
|
|
362
|
+
screenId: string;
|
|
363
|
+
updatedAt?: string | null;
|
|
364
|
+
content: string;
|
|
365
|
+
}) {
|
|
366
|
+
return [screenId, updatedAt ?? "", getContentSignature(content)].join(":");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function shouldUseOverviewRuntimeReplacement({
|
|
370
|
+
sourceType,
|
|
371
|
+
externalSnapshotHtml,
|
|
372
|
+
}: {
|
|
373
|
+
sourceType?: DesignSourceType | null;
|
|
374
|
+
externalSnapshotHtml?: string | null;
|
|
375
|
+
}) {
|
|
376
|
+
return sourceType === "inline" && !externalSnapshotHtml;
|
|
377
|
+
}
|
|
378
|
+
|
|
350
379
|
function dedupeStringIds(ids: string[]): string[] {
|
|
351
380
|
return Array.from(new Set(ids.filter(Boolean)));
|
|
352
381
|
}
|
|
@@ -525,18 +554,34 @@ export function getDesignEditorStateUrlSearch(args: {
|
|
|
525
554
|
currentSearch: string;
|
|
526
555
|
viewMode: "single" | "overview";
|
|
527
556
|
screenId?: string | null;
|
|
557
|
+
codeFileId?: string | null;
|
|
558
|
+
codeFilename?: string | null;
|
|
528
559
|
selectionId?: string | null;
|
|
560
|
+
leftPanel?: DesignLeftPanel | null;
|
|
529
561
|
zoom?: number | null;
|
|
530
562
|
}) {
|
|
531
563
|
const params = new URLSearchParams(args.currentSearch);
|
|
532
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
|
+
}
|
|
533
570
|
if (args.screenId) {
|
|
534
571
|
params.set("screen", args.screenId);
|
|
535
572
|
} else {
|
|
536
573
|
params.delete("screen");
|
|
537
574
|
}
|
|
538
|
-
|
|
539
|
-
|
|
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
|
+
}
|
|
540
585
|
if (args.selectionId) {
|
|
541
586
|
params.set("selection", args.selectionId);
|
|
542
587
|
} else {
|
|
@@ -3470,13 +3515,20 @@ function AgentNativeMenuMark({ className }: { className?: string }) {
|
|
|
3470
3515
|
);
|
|
3471
3516
|
}
|
|
3472
3517
|
|
|
3473
|
-
type DesignLeftPanel =
|
|
3518
|
+
type DesignLeftPanel =
|
|
3519
|
+
| "file"
|
|
3520
|
+
| "agent"
|
|
3521
|
+
| "assets"
|
|
3522
|
+
| "tools"
|
|
3523
|
+
| "tokens"
|
|
3524
|
+
| "code";
|
|
3474
3525
|
|
|
3475
3526
|
const INITIAL_GENERATION_DISABLED_LEFT_PANELS = new Set<DesignLeftPanel>([
|
|
3476
3527
|
"file",
|
|
3477
3528
|
"assets",
|
|
3478
3529
|
"tools",
|
|
3479
3530
|
"tokens",
|
|
3531
|
+
"code",
|
|
3480
3532
|
]);
|
|
3481
3533
|
|
|
3482
3534
|
function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
|
|
@@ -3485,7 +3537,8 @@ function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
|
|
|
3485
3537
|
value === "agent" ||
|
|
3486
3538
|
value === "assets" ||
|
|
3487
3539
|
value === "tools" ||
|
|
3488
|
-
value === "tokens"
|
|
3540
|
+
value === "tokens" ||
|
|
3541
|
+
value === "code"
|
|
3489
3542
|
? value
|
|
3490
3543
|
: undefined;
|
|
3491
3544
|
}
|
|
@@ -3539,6 +3592,11 @@ function DesignWorkspaceRail({
|
|
|
3539
3592
|
icon: <IconAssembly className="size-[15px]" />,
|
|
3540
3593
|
},
|
|
3541
3594
|
];
|
|
3595
|
+
const codeItem = {
|
|
3596
|
+
panel: "code" as const,
|
|
3597
|
+
label: "Code" /* i18n-ignore */,
|
|
3598
|
+
icon: <IconCode className="size-[15px]" />,
|
|
3599
|
+
};
|
|
3542
3600
|
|
|
3543
3601
|
return (
|
|
3544
3602
|
<nav
|
|
@@ -3597,6 +3655,51 @@ function DesignWorkspaceRail({
|
|
|
3597
3655
|
</Tooltip>
|
|
3598
3656
|
);
|
|
3599
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>
|
|
3600
3703
|
</div>
|
|
3601
3704
|
{onMotionToggle ? (
|
|
3602
3705
|
<div className="mt-4 flex w-full flex-col items-center border-t border-border/70 pt-3">
|
|
@@ -4583,6 +4686,8 @@ export default function DesignEditor() {
|
|
|
4583
4686
|
useState<InspectorTab>("design");
|
|
4584
4687
|
const [activeLeftPanel, setActiveLeftPanel] =
|
|
4585
4688
|
useState<DesignLeftPanel>("file");
|
|
4689
|
+
const [activeCodeFile, setActiveCodeFile] =
|
|
4690
|
+
useState<CodeWorkbenchActiveFile | null>(null);
|
|
4586
4691
|
const initialSearchCommandAppliedForIdRef = useRef<string | null>(null);
|
|
4587
4692
|
const initialUrlSelectionHydratedForIdRef = useRef<string | null>(null);
|
|
4588
4693
|
const [leftSidebarWidth, setLeftSidebarWidth] = useState(256);
|
|
@@ -4871,11 +4976,17 @@ export default function DesignEditor() {
|
|
|
4871
4976
|
event.stopPropagation();
|
|
4872
4977
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
4873
4978
|
const startX = event.clientX;
|
|
4874
|
-
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;
|
|
4875
4986
|
const setWidth =
|
|
4876
4987
|
side === "left" ? setLeftSidebarWidth : setRightSidebarWidth;
|
|
4877
|
-
const minWidth = side === "left" ? 220 : 240;
|
|
4878
|
-
const maxWidth = side === "left" ? 420 : 390;
|
|
4988
|
+
const minWidth = side === "left" ? (codePanelOpen ? 520 : 220) : 240;
|
|
4989
|
+
const maxWidth = side === "left" ? (codePanelOpen ? 860 : 420) : 390;
|
|
4879
4990
|
const previousCursor = document.body.style.cursor;
|
|
4880
4991
|
const previousUserSelect = document.body.style.userSelect;
|
|
4881
4992
|
const dragShield = document.createElement("div");
|
|
@@ -4914,7 +5025,7 @@ export default function DesignEditor() {
|
|
|
4914
5025
|
window.addEventListener("pointerup", cleanup);
|
|
4915
5026
|
window.addEventListener("pointercancel", cleanup);
|
|
4916
5027
|
},
|
|
4917
|
-
[leftSidebarWidth, rightSidebarWidth],
|
|
5028
|
+
[activeLeftPanel, leftSidebarWidth, rightSidebarWidth],
|
|
4918
5029
|
);
|
|
4919
5030
|
// Undo/redo state driven by Y.UndoManager
|
|
4920
5031
|
const [canUndo, setCanUndo] = useState(false);
|
|
@@ -9109,6 +9220,14 @@ export default function DesignEditor() {
|
|
|
9109
9220
|
activeTool,
|
|
9110
9221
|
inspectorTab: activeInspectorTab,
|
|
9111
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
|
+
},
|
|
9112
9231
|
// §8 DesignNavigationState additions — dock + breakpoint context
|
|
9113
9232
|
dock: { kind: "motion" as const, open: motionDockOpen },
|
|
9114
9233
|
motion: {
|
|
@@ -9177,6 +9296,7 @@ export default function DesignEditor() {
|
|
|
9177
9296
|
activeTool: selection.activeTool,
|
|
9178
9297
|
inspectorTab: selection.inspectorTab,
|
|
9179
9298
|
leftPanel: selection.leftPanel,
|
|
9299
|
+
codeWorkspace: selection.codeWorkspace,
|
|
9180
9300
|
dock: selection.dock,
|
|
9181
9301
|
motion: selection.motion,
|
|
9182
9302
|
breakpoint: selection.breakpoint,
|
|
@@ -9218,6 +9338,7 @@ export default function DesignEditor() {
|
|
|
9218
9338
|
activeTool,
|
|
9219
9339
|
activeInspectorTab,
|
|
9220
9340
|
activeLeftPanel,
|
|
9341
|
+
activeCodeFile,
|
|
9221
9342
|
overviewSelectedScreenIds,
|
|
9222
9343
|
viewMode,
|
|
9223
9344
|
zoom,
|
|
@@ -14327,6 +14448,9 @@ ${serializedHtml}
|
|
|
14327
14448
|
currentSearch: location.search,
|
|
14328
14449
|
viewMode,
|
|
14329
14450
|
screenId: activeFile?.id ?? activeFileId,
|
|
14451
|
+
leftPanel: activeLeftPanel,
|
|
14452
|
+
codeFileId: activeLeftPanel === "code" ? activeCodeFile?.fileId : null,
|
|
14453
|
+
codeFilename: activeLeftPanel === "code" ? activeCodeFile?.path : null,
|
|
14330
14454
|
selectionId:
|
|
14331
14455
|
selectedUrlSelectionId ??
|
|
14332
14456
|
(preserveInitialRouteSelection ? initialRouteSelectionId : null),
|
|
@@ -14344,6 +14468,9 @@ ${serializedHtml}
|
|
|
14344
14468
|
}, [
|
|
14345
14469
|
activeFile?.id,
|
|
14346
14470
|
activeFileId,
|
|
14471
|
+
activeCodeFile?.fileId,
|
|
14472
|
+
activeCodeFile?.path,
|
|
14473
|
+
activeLeftPanel,
|
|
14347
14474
|
codeLayerOwnerByNodeId.size,
|
|
14348
14475
|
files,
|
|
14349
14476
|
id,
|
|
@@ -16262,7 +16389,14 @@ ${serializedHtml}
|
|
|
16262
16389
|
</div>
|
|
16263
16390
|
);
|
|
16264
16391
|
|
|
16265
|
-
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;
|
|
16266
16400
|
return (
|
|
16267
16401
|
// h-full not flex-1: the parent <main> uses overflow-y-auto, not flex,
|
|
16268
16402
|
// so flex-1 on the child doesn't resolve to the available height. h-full
|
|
@@ -16790,6 +16924,32 @@ ${serializedHtml}
|
|
|
16790
16924
|
/>
|
|
16791
16925
|
)}
|
|
16792
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>
|
|
16793
16953
|
</div>
|
|
16794
16954
|
<div
|
|
16795
16955
|
role="separator"
|
|
@@ -17258,12 +17418,26 @@ ${serializedHtml}
|
|
|
17258
17418
|
const screenBridgeUrl = screen.bridgeUrl;
|
|
17259
17419
|
const screenSnapshot =
|
|
17260
17420
|
liveScreenSnapshotsById[screen.id]?.html;
|
|
17421
|
+
const screenContentSignature =
|
|
17422
|
+
getContentSignature(screenContent);
|
|
17423
|
+
const useRuntimeReplacement =
|
|
17424
|
+
shouldUseOverviewRuntimeReplacement({
|
|
17425
|
+
sourceType: screenSourceType,
|
|
17426
|
+
externalSnapshotHtml: screenSnapshot,
|
|
17427
|
+
});
|
|
17428
|
+
const runtimeReplacementKey = useRuntimeReplacement
|
|
17429
|
+
? getOverviewScreenRuntimeReplacementKey({
|
|
17430
|
+
screenId: screen.id,
|
|
17431
|
+
updatedAt: screen.updatedAt,
|
|
17432
|
+
content: screenContent,
|
|
17433
|
+
})
|
|
17434
|
+
: undefined;
|
|
17261
17435
|
const screenContentKey = screenIsActive
|
|
17262
17436
|
? [screen.id, contentRenderRevision].join(":")
|
|
17263
17437
|
: [
|
|
17264
17438
|
screen.id,
|
|
17265
17439
|
screen.updatedAt ?? "",
|
|
17266
|
-
|
|
17440
|
+
screenContentSignature,
|
|
17267
17441
|
0,
|
|
17268
17442
|
].join(":");
|
|
17269
17443
|
|
|
@@ -17271,6 +17445,10 @@ ${serializedHtml}
|
|
|
17271
17445
|
<DesignCanvas
|
|
17272
17446
|
content={screenContent}
|
|
17273
17447
|
contentKey={screenContentKey}
|
|
17448
|
+
runtimeReplacementContent={
|
|
17449
|
+
useRuntimeReplacement ? screenContent : undefined
|
|
17450
|
+
}
|
|
17451
|
+
runtimeReplacementKey={runtimeReplacementKey}
|
|
17274
17452
|
screenId={screen.id}
|
|
17275
17453
|
zoom={100}
|
|
17276
17454
|
deviceFrame="none"
|
|
@@ -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(),
|