@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.
Files changed (44) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/chat/tool-call-display.tsx +50 -8
  5. package/corpus/core/src/client/session-replay.ts +5 -0
  6. package/corpus/core/src/client/sse-event-processor.ts +17 -4
  7. package/corpus/core/src/server/ssr-handler.ts +152 -8
  8. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +13 -1
  9. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-keep-inlined-css-without-live-resource-loa.md +6 -0
  10. package/corpus/templates/design/AGENTS.md +18 -0
  11. package/corpus/templates/design/actions/apply-source-edit.ts +87 -0
  12. package/corpus/templates/design/actions/list-source-files.ts +52 -0
  13. package/corpus/templates/design/actions/navigate.ts +3 -2
  14. package/corpus/templates/design/actions/preview-source-edit.ts +85 -0
  15. package/corpus/templates/design/actions/read-source-file.ts +55 -0
  16. package/corpus/templates/design/actions/resolve-selection-source.ts +101 -0
  17. package/corpus/templates/design/actions/view-screen.ts +43 -1
  18. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +630 -0
  19. package/corpus/templates/design/app/hooks/use-navigation-state.ts +9 -6
  20. package/corpus/templates/design/app/pages/DesignEditor.tsx +189 -11
  21. package/corpus/templates/design/changelog/2026-07-01-design-code-workspace.md +6 -0
  22. package/corpus/templates/design/changelog/2026-07-01-design-previews-refresh-immediately-after-agent-screen-edits.md +6 -0
  23. package/corpus/templates/design/server/source-workspace.ts +215 -0
  24. package/corpus/templates/design/shared/design-source-capabilities.ts +5 -5
  25. package/corpus/templates/design/shared/source-workspace.ts +149 -0
  26. package/dist/client/chat/tool-call-display.d.ts +1 -0
  27. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  28. package/dist/client/chat/tool-call-display.js +22 -5
  29. package/dist/client/chat/tool-call-display.js.map +1 -1
  30. package/dist/client/session-replay.d.ts +1 -0
  31. package/dist/client/session-replay.d.ts.map +1 -1
  32. package/dist/client/session-replay.js +2 -0
  33. package/dist/client/session-replay.js.map +1 -1
  34. package/dist/client/sse-event-processor.d.ts.map +1 -1
  35. package/dist/client/sse-event-processor.js +13 -4
  36. package/dist/client/sse-event-processor.js.map +1 -1
  37. package/dist/collab/routes.d.ts +1 -1
  38. package/dist/notifications/routes.d.ts +1 -1
  39. package/dist/observability/routes.d.ts +5 -5
  40. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  41. package/dist/server/ssr-handler.d.ts.map +1 -1
  42. package/dist/server/ssr-handler.js +111 -8
  43. package/dist/server/ssr-handler.js.map +1 -1
  44. 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 { normalizeDesignSourceType } from "@shared/source-mode";
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
- params.delete("fileId");
539
- params.delete("filename");
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 = "file" | "agent" | "assets" | "tools" | "tokens";
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 startWidth = side === "left" ? leftSidebarWidth : rightSidebarWidth;
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 = Math.max(leftSidebarWidth, 320);
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
- getContentSignature(screenContent),
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,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Designs can be edited in a new Code workspace from the left rail.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Design previews refresh immediately after agent screen edits.
@@ -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 write ops (`readFile`, `writeFile`, `applyEdit`) are `planned`
151
- * pending bridge write hardening.
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: planned("Local file reads require bridge hardening"),
157
- writeFile: planned("Local file writes require bridge hardening"),
158
- applyEdit: planned("Source edits require bridge hardening"),
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(),