@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.
Files changed (29) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/session-replay.ts +5 -0
  5. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +13 -1
  6. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-keep-inlined-css-without-live-resource-loa.md +6 -0
  7. package/corpus/templates/design/AGENTS.md +18 -0
  8. package/corpus/templates/design/actions/apply-source-edit.ts +87 -0
  9. package/corpus/templates/design/actions/list-source-files.ts +52 -0
  10. package/corpus/templates/design/actions/navigate.ts +3 -2
  11. package/corpus/templates/design/actions/preview-source-edit.ts +85 -0
  12. package/corpus/templates/design/actions/read-source-file.ts +55 -0
  13. package/corpus/templates/design/actions/resolve-selection-source.ts +101 -0
  14. package/corpus/templates/design/actions/view-screen.ts +43 -1
  15. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +630 -0
  16. package/corpus/templates/design/app/hooks/use-navigation-state.ts +9 -6
  17. package/corpus/templates/design/app/pages/DesignEditor.tsx +144 -9
  18. package/corpus/templates/design/changelog/2026-07-01-design-code-workspace.md +6 -0
  19. package/corpus/templates/design/server/source-workspace.ts +215 -0
  20. package/corpus/templates/design/shared/design-source-capabilities.ts +5 -5
  21. package/corpus/templates/design/shared/source-workspace.ts +149 -0
  22. package/dist/client/session-replay.d.ts +1 -0
  23. package/dist/client/session-replay.d.ts.map +1 -1
  24. package/dist/client/session-replay.js +2 -0
  25. package/dist/client/session-replay.js.map +1 -1
  26. package/dist/progress/routes.d.ts +1 -1
  27. package/dist/resources/handlers.d.ts +1 -1
  28. package/dist/server/transcribe-voice.d.ts +1 -1
  29. 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
- params.delete("fileId");
564
- 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
+ }
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 = "file" | "agent" | "assets" | "tools" | "tokens";
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 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;
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 = 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;
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,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,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(),
@@ -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
+ }
@@ -16,6 +16,7 @@ export interface SessionReplayOptions {
16
16
  maxBatchBytes?: number;
17
17
  checkoutEveryNth?: number;
18
18
  checkoutEveryNms?: number;
19
+ inlineStylesheet?: boolean;
19
20
  blockSelector?: string;
20
21
  ignoreSelector?: string;
21
22
  maskTextClass?: string | RegExp;
@@ -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;AAmD/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,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;AAoRD,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;AAiaD,wBAAsB,kBAAkB,CAAC,MAAM,SAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzE;AAkDD,wBAAsB,kBAAkB,CACtC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA2DnC;AA0GD,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"}
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"}