@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
@@ -0,0 +1,630 @@
1
+ import { useActionMutation, useActionQuery } from "@agent-native/core/client";
2
+ import { IconCode, IconDeviceFloppy } from "@tabler/icons-react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { toast } from "sonner";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import { Spinner } from "@/components/ui/spinner";
8
+ import { cn } from "@/lib/utils";
9
+
10
+ interface CodeWorkbenchTheme {
11
+ colorScheme: "light" | "dark";
12
+ values: Record<string, string>;
13
+ }
14
+
15
+ interface CodeWorkbenchDraft {
16
+ content: string;
17
+ baseVersionHash?: string;
18
+ }
19
+
20
+ export interface CodeWorkbenchActiveFile {
21
+ path: string;
22
+ fileId?: string;
23
+ dirty: boolean;
24
+ versionHash?: string;
25
+ backendKind: "virtual-inline";
26
+ }
27
+
28
+ interface CodeWorkbenchHostProps {
29
+ designId: string;
30
+ activeFileId?: string | null;
31
+ activeFilename?: string | null;
32
+ selectedNodeId?: string | null;
33
+ selectedSelector?: string | null;
34
+ canEdit: boolean;
35
+ onActiveFileChange?: (file: CodeWorkbenchActiveFile | null) => void;
36
+ }
37
+
38
+ const WORKBENCH_THEME_VARS: Record<string, string[]> = {
39
+ "--workbench-bg": ["--design-editor-panel-bg", "--background"],
40
+ "--workbench-sidebar-bg": ["--design-editor-panel-bg", "--card"],
41
+ "--workbench-editor-bg": ["--design-editor-panel-bg", "--background"],
42
+ "--workbench-surface-bg": ["--design-editor-control-bg", "--muted"],
43
+ "--workbench-border": ["--design-editor-control-border", "--border"],
44
+ "--workbench-fg": ["--foreground"],
45
+ "--workbench-muted-fg": ["--muted-foreground"],
46
+ "--workbench-hover-bg": ["--design-editor-layer-hover-color", "--accent"],
47
+ "--workbench-active-bg": ["--design-editor-selection-color", "--accent"],
48
+ "--workbench-active-fg": [
49
+ "--design-editor-accent-color",
50
+ "--accent-foreground",
51
+ ],
52
+ "--workbench-accent": ["--design-editor-accent-color", "--primary"],
53
+ "--workbench-button-bg": ["--design-editor-control-bg", "--background"],
54
+ "--workbench-button-fg": ["--foreground"],
55
+ "--workbench-selection-bg": ["--design-editor-selection-color", "--accent"],
56
+ "--workbench-dirty": ["--warning", "--destructive"],
57
+ };
58
+
59
+ function normalizeThemeColorValue(value: string): string {
60
+ const trimmed = value.trim();
61
+ if (!trimmed) return "";
62
+ if (/^-?\d+(\.\d+)?\s+-?\d+(\.\d+)?%/.test(trimmed)) {
63
+ return `hsl(${trimmed})`;
64
+ }
65
+ return trimmed;
66
+ }
67
+
68
+ function readThemeVar(
69
+ elementStyles: CSSStyleDeclaration,
70
+ rootStyles: CSSStyleDeclaration,
71
+ names: string[],
72
+ ): string | undefined {
73
+ for (const name of names) {
74
+ const value =
75
+ elementStyles.getPropertyValue(name) || rootStyles.getPropertyValue(name);
76
+ const normalized = normalizeThemeColorValue(value);
77
+ if (normalized) return normalized;
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ function readCodeWorkbenchTheme(
83
+ element: HTMLElement | null,
84
+ ): CodeWorkbenchTheme {
85
+ if (typeof window === "undefined" || !element) {
86
+ return { colorScheme: "light", values: {} };
87
+ }
88
+ const elementStyles = window.getComputedStyle(element);
89
+ const rootStyles = window.getComputedStyle(document.documentElement);
90
+ const values: Record<string, string> = {};
91
+ for (const [targetVar, sourceVars] of Object.entries(WORKBENCH_THEME_VARS)) {
92
+ const value = readThemeVar(elementStyles, rootStyles, sourceVars);
93
+ if (value) values[targetVar] = value;
94
+ }
95
+ const colorScheme =
96
+ document.documentElement.classList.contains("dark") ||
97
+ elementStyles.colorScheme.includes("dark") ||
98
+ rootStyles.colorScheme.includes("dark")
99
+ ? "dark"
100
+ : "light";
101
+ return { colorScheme, values };
102
+ }
103
+
104
+ export const WORKBENCH_SRC_DOC = `<!DOCTYPE html>
105
+ <html>
106
+ <head>
107
+ <meta charset="utf-8" />
108
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
109
+ <style>
110
+ :root {
111
+ color-scheme: light;
112
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
113
+ --workbench-bg: Canvas;
114
+ --workbench-sidebar-bg: Canvas;
115
+ --workbench-editor-bg: Canvas;
116
+ --workbench-surface-bg: ButtonFace;
117
+ --workbench-border: color-mix(in srgb, CanvasText 16%, transparent);
118
+ --workbench-fg: CanvasText;
119
+ --workbench-muted-fg: color-mix(in srgb, CanvasText 56%, transparent);
120
+ --workbench-hover-bg: color-mix(in srgb, Highlight 10%, transparent);
121
+ --workbench-active-bg: color-mix(in srgb, Highlight 16%, transparent);
122
+ --workbench-active-fg: Highlight;
123
+ --workbench-accent: Highlight;
124
+ --workbench-button-bg: ButtonFace;
125
+ --workbench-button-fg: ButtonText;
126
+ --workbench-selection-bg: color-mix(in srgb, Highlight 28%, transparent);
127
+ --workbench-dirty: Mark;
128
+ background: var(--workbench-bg);
129
+ color: var(--workbench-fg);
130
+ }
131
+ * { box-sizing: border-box; }
132
+ body { margin: 0; height: 100vh; overflow: hidden; background: var(--workbench-bg); }
133
+ button { font: inherit; }
134
+ .shell { display: grid; grid-template-columns: 188px minmax(0, 1fr); height: 100vh; }
135
+ .explorer { border-right: 1px solid var(--workbench-border); background: var(--workbench-sidebar-bg); min-width: 0; display: flex; flex-direction: column; }
136
+ .title { height: 38px; display: flex; align-items: center; padding: 0 12px; gap: 8px; border-bottom: 1px solid var(--workbench-border); font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--workbench-muted-fg); }
137
+ .dot { width: 8px; height: 8px; border-radius: 999px; background: var(--workbench-accent); box-shadow: 0 0 18px color-mix(in srgb, var(--workbench-accent) 55%, transparent); }
138
+ .files { min-height: 0; overflow: auto; padding: 8px 6px; }
139
+ .file { width: 100%; min-width: 0; border: 0; border-radius: 7px; background: transparent; color: var(--workbench-muted-fg); display: flex; align-items: center; gap: 7px; padding: 7px 8px; cursor: pointer; text-align: left; }
140
+ .file:hover { background: var(--workbench-hover-bg); color: var(--workbench-fg); }
141
+ .file.active { background: var(--workbench-active-bg); color: var(--workbench-active-fg); }
142
+ .file .icon { width: 22px; color: var(--workbench-accent); font-size: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
143
+ .file .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
144
+ .status { border-top: 1px solid var(--workbench-border); color: var(--workbench-muted-fg); font-size: 11px; line-height: 1.35; padding: 9px 10px; }
145
+ .editor { min-width: 0; display: flex; flex-direction: column; background: var(--workbench-editor-bg); }
146
+ .tabbar { height: 38px; display: flex; align-items: center; border-bottom: 1px solid var(--workbench-border); background: var(--workbench-surface-bg); }
147
+ .tab { height: 38px; max-width: 260px; display: flex; align-items: center; gap: 8px; padding: 0 13px; border-right: 1px solid var(--workbench-border); color: var(--workbench-fg); font-size: 12px; }
148
+ .dirty { width: 7px; height: 7px; border-radius: 999px; background: var(--workbench-dirty); }
149
+ .toolbar { margin-left: auto; display: flex; align-items: center; gap: 8px; padding: 0 10px; color: var(--workbench-muted-fg); font-size: 11px; }
150
+ .toolbar button { height: 26px; border: 1px solid var(--workbench-border); border-radius: 6px; background: var(--workbench-button-bg); color: var(--workbench-button-fg); padding: 0 9px; cursor: pointer; }
151
+ .toolbar button:disabled { opacity: .4; cursor: default; }
152
+ textarea { flex: 1; width: 100%; min-width: 0; resize: none; border: 0; outline: 0; padding: 18px 20px 28px; background: var(--workbench-editor-bg); color: var(--workbench-fg); font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; tab-size: 2; }
153
+ textarea::selection { background: var(--workbench-selection-bg); }
154
+ .empty { flex: 1; display: grid; place-items: center; color: var(--workbench-muted-fg); font-size: 13px; text-align: center; padding: 24px; }
155
+ </style>
156
+ </head>
157
+ <body>
158
+ <div class="shell">
159
+ <aside class="explorer">
160
+ <div class="title"><span class="dot"></span><span>DesignFS</span></div>
161
+ <div id="files" class="files"></div>
162
+ <div id="status" class="status">Waiting for workspace...</div>
163
+ </aside>
164
+ <main class="editor">
165
+ <div class="tabbar">
166
+ <div id="tab" class="tab">No file</div>
167
+ <div class="toolbar">
168
+ <span id="meta"></span>
169
+ <button id="revert" type="button" disabled>Revert</button>
170
+ <button id="save" type="button" disabled>Save</button>
171
+ </div>
172
+ </div>
173
+ <textarea id="editor" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
174
+ </main>
175
+ </div>
176
+ <script>
177
+ const filesEl = document.getElementById("files");
178
+ const statusEl = document.getElementById("status");
179
+ const tabEl = document.getElementById("tab");
180
+ const metaEl = document.getElementById("meta");
181
+ const editorEl = document.getElementById("editor");
182
+ const saveEl = document.getElementById("save");
183
+ const revertEl = document.getElementById("revert");
184
+ let state = { files: [], activePath: null, content: "", savedContent: "", dirty: false, canEdit: false };
185
+ let lastSelectionKey = "";
186
+
187
+ function applyTheme(theme) {
188
+ const root = document.documentElement;
189
+ if (!theme) return;
190
+ root.style.colorScheme = theme.colorScheme === "dark" ? "dark" : "light";
191
+ const values = theme.values || {};
192
+ for (const name of Object.keys(values)) {
193
+ root.style.setProperty(name, values[name]);
194
+ }
195
+ }
196
+
197
+ function iconFor(path) {
198
+ if (/\\.css$/i.test(path)) return "#";
199
+ if (/\\.jsx?$/i.test(path)) return "JS";
200
+ if (/\\.tsx?$/i.test(path)) return "TS";
201
+ return "<>";
202
+ }
203
+
204
+ function renderFiles() {
205
+ filesEl.innerHTML = "";
206
+ state.files.forEach((file) => {
207
+ const button = document.createElement("button");
208
+ button.type = "button";
209
+ button.className = "file" + (file.path === state.activePath ? " active" : "");
210
+ button.title = file.path;
211
+ const icon = document.createElement("span");
212
+ icon.className = "icon";
213
+ icon.textContent = iconFor(file.path);
214
+ const name = document.createElement("span");
215
+ name.className = "name";
216
+ name.textContent = file.displayName || file.path;
217
+ button.append(icon, name);
218
+ button.addEventListener("click", () => {
219
+ parent.postMessage({ type: "design-code-workbench:select-file", path: file.path }, "*");
220
+ });
221
+ filesEl.appendChild(button);
222
+ });
223
+ }
224
+
225
+ function focusSelection() {
226
+ const selection = state.selection || {};
227
+ const key = [state.activePath, selection.nodeId || "", selection.selector || "", state.versionHash || ""].join(":");
228
+ if (!state.content || key === lastSelectionKey) return;
229
+ lastSelectionKey = key;
230
+ const targets = [];
231
+ if (selection.nodeId) {
232
+ targets.push('data-agent-native-node-id="' + selection.nodeId + '"');
233
+ targets.push('data-code-layer-id="' + selection.nodeId + '"');
234
+ targets.push(selection.nodeId);
235
+ }
236
+ if (selection.selector) targets.push(selection.selector);
237
+ for (const target of targets) {
238
+ const index = state.content.indexOf(target);
239
+ if (index >= 0) {
240
+ editorEl.focus();
241
+ editorEl.setSelectionRange(index, Math.min(state.content.length, index + target.length));
242
+ return;
243
+ }
244
+ }
245
+ }
246
+
247
+ function render() {
248
+ renderFiles();
249
+ const active = state.files.find((file) => file.path === state.activePath);
250
+ tabEl.replaceChildren();
251
+ if (active) {
252
+ const label = document.createElement("span");
253
+ label.textContent = active.path;
254
+ tabEl.appendChild(label);
255
+ if (state.dirty) {
256
+ const dirty = document.createElement("span");
257
+ dirty.className = "dirty";
258
+ tabEl.appendChild(dirty);
259
+ }
260
+ } else {
261
+ tabEl.textContent = "No file";
262
+ }
263
+ metaEl.textContent = state.dirty ? "Unsaved changes" : (state.versionHash ? "Saved " + state.versionHash : "");
264
+ saveEl.disabled = !state.canEdit || !state.dirty || state.saving || !active;
265
+ revertEl.disabled = !state.dirty || !active;
266
+ statusEl.textContent = active
267
+ ? (state.backendKind || "virtual-inline") + " / " + state.workspaceUri
268
+ : state.files.length ? "Choose a file" : "No inline files";
269
+ if (editorEl.value !== state.content) editorEl.value = state.content || "";
270
+ editorEl.readOnly = !state.canEdit || !active;
271
+ focusSelection();
272
+ }
273
+
274
+ window.addEventListener("message", (event) => {
275
+ const message = event.data || {};
276
+ if (message.type !== "design-code-workbench:state") return;
277
+ state = message.state || state;
278
+ applyTheme(state.theme);
279
+ render();
280
+ });
281
+
282
+ editorEl.addEventListener("input", () => {
283
+ parent.postMessage({ type: "design-code-workbench:content-change", content: editorEl.value }, "*");
284
+ });
285
+ saveEl.addEventListener("click", () => {
286
+ parent.postMessage({ type: "design-code-workbench:save" }, "*");
287
+ });
288
+ revertEl.addEventListener("click", () => {
289
+ parent.postMessage({ type: "design-code-workbench:revert" }, "*");
290
+ });
291
+
292
+ parent.postMessage({ type: "design-code-workbench:ready" }, "*");
293
+ </script>
294
+ </body>
295
+ </html>`;
296
+
297
+ export function CodeWorkbenchHost({
298
+ designId,
299
+ activeFileId,
300
+ activeFilename,
301
+ selectedNodeId,
302
+ selectedSelector,
303
+ canEdit,
304
+ onActiveFileChange,
305
+ }: CodeWorkbenchHostProps) {
306
+ const containerRef = useRef<HTMLDivElement | null>(null);
307
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
308
+ const lastExternalTargetKeyRef = useRef<string | null>(null);
309
+ const [activePath, setActivePath] = useState<string | null>(null);
310
+ const [draftsByPath, setDraftsByPath] = useState<
311
+ Record<string, CodeWorkbenchDraft>
312
+ >({});
313
+ const [ready, setReady] = useState(false);
314
+ const [theme, setTheme] = useState<CodeWorkbenchTheme>(() => ({
315
+ colorScheme: "light",
316
+ values: {},
317
+ }));
318
+
319
+ const sourceFilesQuery = useActionQuery("list-source-files", { designId });
320
+ const sourceFiles = (sourceFilesQuery.data as any)?.files ?? [];
321
+ const backend = (sourceFilesQuery.data as any)?.backend;
322
+ const selectedPath =
323
+ activePath ?? activeFilename ?? sourceFiles[0]?.path ?? "";
324
+ const readSourceQuery = useActionQuery(
325
+ "read-source-file",
326
+ { designId, path: selectedPath },
327
+ { enabled: Boolean(selectedPath) },
328
+ );
329
+ const readSource = readSourceQuery.data as any;
330
+ const applySourceEditMutation = useActionMutation("apply-source-edit");
331
+ const savedContent = readSource?.content ?? "";
332
+ const activeDraft = selectedPath ? draftsByPath[selectedPath] : undefined;
333
+ const draftContent =
334
+ activeDraft !== undefined ? activeDraft.content : savedContent;
335
+ const expectedVersionHash =
336
+ activeDraft?.baseVersionHash ?? readSource?.versionHash;
337
+ const dirty = draftContent !== savedContent;
338
+ const activeSourceFile = sourceFiles.find(
339
+ (file: any) =>
340
+ file.path === selectedPath ||
341
+ (activeFileId && file.fileId === activeFileId),
342
+ );
343
+
344
+ useEffect(() => {
345
+ const updateTheme = () => {
346
+ const nextTheme = readCodeWorkbenchTheme(containerRef.current);
347
+ setTheme((current) =>
348
+ current.colorScheme === nextTheme.colorScheme &&
349
+ JSON.stringify(current.values) === JSON.stringify(nextTheme.values)
350
+ ? current
351
+ : nextTheme,
352
+ );
353
+ };
354
+ updateTheme();
355
+ const observer = new MutationObserver(updateTheme);
356
+ observer.observe(document.documentElement, {
357
+ attributes: true,
358
+ attributeFilter: ["class", "style", "data-theme"],
359
+ });
360
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
361
+ media.addEventListener("change", updateTheme);
362
+ return () => {
363
+ observer.disconnect();
364
+ media.removeEventListener("change", updateTheme);
365
+ };
366
+ }, []);
367
+
368
+ useEffect(() => {
369
+ setActivePath(null);
370
+ setDraftsByPath({});
371
+ lastExternalTargetKeyRef.current = null;
372
+ onActiveFileChange?.(null);
373
+ }, [designId, onActiveFileChange]);
374
+
375
+ useEffect(() => {
376
+ const externalTargetKey = [activeFileId ?? "", activeFilename ?? ""].join(
377
+ ":",
378
+ );
379
+ if (!activeFileId && !activeFilename) {
380
+ lastExternalTargetKeyRef.current = null;
381
+ return;
382
+ }
383
+ if (lastExternalTargetKeyRef.current === externalTargetKey) return;
384
+ const match = sourceFiles.find(
385
+ (file: any) =>
386
+ file.fileId === activeFileId || file.path === activeFilename,
387
+ );
388
+ if (match?.path) {
389
+ lastExternalTargetKeyRef.current = externalTargetKey;
390
+ setActivePath(match.path);
391
+ }
392
+ }, [activeFileId, activeFilename, sourceFiles]);
393
+
394
+ const setSelectedDraftContent = useCallback(
395
+ (content: string) => {
396
+ if (!selectedPath) return;
397
+ setDraftsByPath((current) => {
398
+ const next = { ...current };
399
+ if (content === savedContent) {
400
+ delete next[selectedPath];
401
+ } else {
402
+ next[selectedPath] = {
403
+ content,
404
+ baseVersionHash:
405
+ current[selectedPath]?.baseVersionHash ?? readSource?.versionHash,
406
+ };
407
+ }
408
+ return next;
409
+ });
410
+ },
411
+ [readSource?.versionHash, savedContent, selectedPath],
412
+ );
413
+
414
+ useEffect(() => {
415
+ onActiveFileChange?.(
416
+ selectedPath
417
+ ? {
418
+ path: selectedPath,
419
+ fileId: readSource?.fileId ?? activeSourceFile?.fileId,
420
+ dirty,
421
+ versionHash: readSource?.versionHash,
422
+ backendKind: "virtual-inline",
423
+ }
424
+ : null,
425
+ );
426
+ }, [
427
+ activeSourceFile?.fileId,
428
+ dirty,
429
+ onActiveFileChange,
430
+ readSource?.fileId,
431
+ readSource?.versionHash,
432
+ selectedPath,
433
+ ]);
434
+
435
+ const workbenchState = useMemo(
436
+ () => ({
437
+ files: sourceFiles,
438
+ activePath: selectedPath || null,
439
+ content: draftContent,
440
+ savedContent,
441
+ dirty,
442
+ canEdit: canEdit && readSource?.readonly !== true,
443
+ saving: applySourceEditMutation.isPending,
444
+ versionHash: readSource?.versionHash,
445
+ workspaceUri: backend?.workspaceUri ?? `designfs://${designId}/`,
446
+ backendKind: backend?.kind ?? "virtual-inline",
447
+ theme,
448
+ selection: {
449
+ nodeId: selectedNodeId,
450
+ selector: selectedSelector,
451
+ },
452
+ }),
453
+ [
454
+ applySourceEditMutation.isPending,
455
+ backend?.kind,
456
+ backend?.workspaceUri,
457
+ canEdit,
458
+ designId,
459
+ dirty,
460
+ draftContent,
461
+ readSource?.readonly,
462
+ readSource?.versionHash,
463
+ savedContent,
464
+ selectedNodeId,
465
+ selectedPath,
466
+ selectedSelector,
467
+ sourceFiles,
468
+ theme,
469
+ ],
470
+ );
471
+
472
+ useEffect(() => {
473
+ if (!ready) return;
474
+ iframeRef.current?.contentWindow?.postMessage(
475
+ { type: "design-code-workbench:state", state: workbenchState },
476
+ "*",
477
+ );
478
+ }, [ready, workbenchState]);
479
+
480
+ useEffect(() => {
481
+ const handleMessage = (event: MessageEvent) => {
482
+ if (event.source !== iframeRef.current?.contentWindow) return;
483
+ const message = event.data as
484
+ | { type?: string; path?: string; content?: string }
485
+ | undefined;
486
+ if (!message?.type) return;
487
+ if (message.type === "design-code-workbench:ready") {
488
+ setReady(true);
489
+ return;
490
+ }
491
+ if (
492
+ message.type === "design-code-workbench:select-file" &&
493
+ message.path
494
+ ) {
495
+ setActivePath(message.path);
496
+ return;
497
+ }
498
+ if (
499
+ message.type === "design-code-workbench:content-change" &&
500
+ typeof message.content === "string"
501
+ ) {
502
+ setSelectedDraftContent(message.content);
503
+ return;
504
+ }
505
+ if (message.type === "design-code-workbench:revert") {
506
+ setSelectedDraftContent(savedContent);
507
+ return;
508
+ }
509
+ if (message.type === "design-code-workbench:save") {
510
+ if (!selectedPath || !dirty) return;
511
+ applySourceEditMutation.mutate(
512
+ {
513
+ designId,
514
+ path: selectedPath,
515
+ expectedVersionHash,
516
+ edit: { kind: "full-replace", content: draftContent },
517
+ } as any,
518
+ {
519
+ onSuccess: () => {
520
+ setDraftsByPath((current) => {
521
+ const next = { ...current };
522
+ delete next[selectedPath];
523
+ return next;
524
+ });
525
+ toast.success("Source file saved" /* i18n-ignore */);
526
+ },
527
+ onError: (error) => {
528
+ toast.error(
529
+ error instanceof Error
530
+ ? error.message
531
+ : "Could not save source file" /* i18n-ignore */,
532
+ );
533
+ },
534
+ },
535
+ );
536
+ }
537
+ };
538
+ window.addEventListener("message", handleMessage);
539
+ return () => window.removeEventListener("message", handleMessage);
540
+ }, [
541
+ applySourceEditMutation,
542
+ designId,
543
+ dirty,
544
+ draftContent,
545
+ expectedVersionHash,
546
+ readSource?.versionHash,
547
+ savedContent,
548
+ selectedPath,
549
+ setSelectedDraftContent,
550
+ ]);
551
+
552
+ return (
553
+ <div
554
+ ref={containerRef}
555
+ className="flex min-h-0 flex-1 flex-col bg-[var(--design-editor-panel-bg)]"
556
+ >
557
+ <div className="flex h-10 shrink-0 items-center gap-2 border-b border-border/60 px-3">
558
+ <IconCode className="size-4 text-[var(--design-editor-accent-color)]" />
559
+ <div className="min-w-0 flex-1">
560
+ <h3 className="truncate text-xs font-semibold text-foreground">
561
+ {"Code" /* i18n-ignore */}
562
+ </h3>
563
+ <p className="truncate text-[10px] text-muted-foreground">
564
+ {backend?.workspaceUri ?? `designfs://${designId}/`}
565
+ </p>
566
+ </div>
567
+ <Button
568
+ size="sm"
569
+ variant="outline"
570
+ className="h-7 gap-1.5 px-2 text-[11px]"
571
+ disabled={!dirty || !canEdit || applySourceEditMutation.isPending}
572
+ onClick={() => {
573
+ iframeRef.current?.contentWindow?.postMessage(
574
+ { type: "design-code-workbench:state", state: workbenchState },
575
+ "*",
576
+ );
577
+ applySourceEditMutation.mutate(
578
+ {
579
+ designId,
580
+ path: selectedPath,
581
+ expectedVersionHash,
582
+ edit: { kind: "full-replace", content: draftContent },
583
+ } as any,
584
+ {
585
+ onSuccess: () => {
586
+ setDraftsByPath((current) => {
587
+ const next = { ...current };
588
+ delete next[selectedPath];
589
+ return next;
590
+ });
591
+ toast.success("Source file saved" /* i18n-ignore */);
592
+ },
593
+ onError: (error) => {
594
+ toast.error(
595
+ error instanceof Error
596
+ ? error.message
597
+ : "Could not save source file" /* i18n-ignore */,
598
+ );
599
+ },
600
+ },
601
+ );
602
+ }}
603
+ >
604
+ {applySourceEditMutation.isPending ? (
605
+ <Spinner className="size-3" />
606
+ ) : (
607
+ <IconDeviceFloppy className="size-3" />
608
+ )}
609
+ {"Save" /* i18n-ignore */}
610
+ </Button>
611
+ </div>
612
+ <div
613
+ className={cn(
614
+ "min-h-0 flex-1 bg-[var(--design-editor-panel-bg)]",
615
+ (sourceFilesQuery.isLoading || readSourceQuery.isLoading) &&
616
+ "opacity-80",
617
+ )}
618
+ >
619
+ <iframe
620
+ ref={iframeRef}
621
+ title={"Design code workspace" /* i18n-ignore */}
622
+ className="h-full w-full border-0"
623
+ srcDoc={WORKBENCH_SRC_DOC}
624
+ sandbox="allow-scripts"
625
+ allow="clipboard-read; clipboard-write"
626
+ />
627
+ </div>
628
+ </div>
629
+ );
630
+ }
@@ -12,8 +12,8 @@ export interface NavigationState {
12
12
  editorView?: "single" | "overview";
13
13
  inspectorTab?: "design" | "tweaks" | "extensions";
14
14
  inspector?: "design" | "tweaks" | "extensions";
15
- leftPanel?: "file" | "agent" | "assets" | "tools" | "tokens";
16
- panel?: "file" | "agent" | "assets" | "tools" | "tokens";
15
+ leftPanel?: "file" | "agent" | "assets" | "tools" | "tokens" | "code";
16
+ panel?: "file" | "agent" | "assets" | "tools" | "tokens" | "code";
17
17
  fileId?: string;
18
18
  screenId?: string;
19
19
  filename?: string;
@@ -47,8 +47,8 @@ export interface DesignEditorCommand {
47
47
  viewMode?: "single" | "overview";
48
48
  inspectorTab?: "design" | "tweaks" | "extensions";
49
49
  inspector?: "design" | "tweaks" | "extensions";
50
- leftPanel?: "file" | "agent" | "assets" | "tools" | "tokens";
51
- panel?: "file" | "agent" | "assets" | "tools" | "tokens";
50
+ leftPanel?: "file" | "agent" | "assets" | "tools" | "tokens" | "code";
51
+ panel?: "file" | "agent" | "assets" | "tools" | "tokens" | "code";
52
52
  fileId?: string;
53
53
  screenId?: string;
54
54
  filename?: string;
@@ -88,13 +88,14 @@ function normalizeInspectorTab(
88
88
 
89
89
  function normalizeLeftPanel(
90
90
  value: unknown,
91
- ): "file" | "agent" | "assets" | "tools" | "tokens" | undefined {
91
+ ): "file" | "agent" | "assets" | "tools" | "tokens" | "code" | undefined {
92
92
  if (value === "extensions") return "tools";
93
93
  return value === "file" ||
94
94
  value === "agent" ||
95
95
  value === "assets" ||
96
96
  value === "tools" ||
97
- value === "tokens"
97
+ value === "tokens" ||
98
+ value === "code"
98
99
  ? value
99
100
  : undefined;
100
101
  }
@@ -184,6 +185,8 @@ export function useNavigationState(enabled = true) {
184
185
  searchParams.get("inspector"),
185
186
  );
186
187
  if (inspectorTab) state.inspectorTab = inspectorTab;
188
+ const leftPanel = normalizeLeftPanel(searchParams.get("panel"));
189
+ if (leftPanel) state.leftPanel = leftPanel;
187
190
  const screen = searchParams.get("screen");
188
191
  if (screen) state.screen = screen;
189
192
  const fileId = searchParams.get("fileId");