@agent-native/core 0.84.37 → 0.84.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +2 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +20 -0
- package/corpus/core/src/client/sse-event-processor.ts +31 -6
- package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
- package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +486 -341
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +13 -0
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +76 -58
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +6 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +80 -8
- package/corpus/templates/design/app/i18n/zh-TW.ts +2 -2
- package/corpus/templates/design/app/i18n-data.ts +22 -30
- package/corpus/templates/design/app/lib/design-import.ts +42 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +180 -109
- package/corpus/templates/design/changelog/2026-07-01-code-workspace-now-opens-a-vs-code-like-editor-with-real-cod.md +6 -0
- package/corpus/templates/design/changelog/2026-07-01-import-is-more-compact-local-app-setup-is-clearer-and-figma.md +6 -0
- package/corpus/templates/design/package.json +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +2 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +17 -2
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +11 -2
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +15 -10
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +2 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { useActionMutation, useActionQuery } from "@agent-native/core/client";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
IconCode,
|
|
4
|
+
IconDeviceFloppy,
|
|
5
|
+
IconFileCode,
|
|
6
|
+
IconFolder,
|
|
7
|
+
IconRefresh,
|
|
8
|
+
IconSearch,
|
|
9
|
+
} from "@tabler/icons-react";
|
|
10
|
+
import * as monaco from "monaco-editor";
|
|
11
|
+
|
|
12
|
+
import "monaco-editor/min/vs/editor/editor.main.css";
|
|
13
|
+
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
|
14
|
+
import CssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
|
|
15
|
+
import HtmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
|
|
16
|
+
import JsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
|
|
17
|
+
import TypeScriptWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
|
|
18
|
+
import {
|
|
19
|
+
type CSSProperties,
|
|
20
|
+
useCallback,
|
|
21
|
+
useEffect,
|
|
22
|
+
useRef,
|
|
23
|
+
useState,
|
|
24
|
+
} from "react";
|
|
4
25
|
import { toast } from "sonner";
|
|
5
26
|
|
|
6
27
|
import { Button } from "@/components/ui/button";
|
|
@@ -65,6 +86,21 @@ function normalizeThemeColorValue(value: string): string {
|
|
|
65
86
|
return trimmed;
|
|
66
87
|
}
|
|
67
88
|
|
|
89
|
+
function resolveCssColorValue(value: string): string {
|
|
90
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
const probe = document.createElement("span");
|
|
94
|
+
probe.style.color = value;
|
|
95
|
+
probe.style.position = "fixed";
|
|
96
|
+
probe.style.pointerEvents = "none";
|
|
97
|
+
probe.style.visibility = "hidden";
|
|
98
|
+
document.body.appendChild(probe);
|
|
99
|
+
const resolved = window.getComputedStyle(probe).color;
|
|
100
|
+
probe.remove();
|
|
101
|
+
return resolved || value;
|
|
102
|
+
}
|
|
103
|
+
|
|
68
104
|
function readThemeVar(
|
|
69
105
|
elementStyles: CSSStyleDeclaration,
|
|
70
106
|
rootStyles: CSSStyleDeclaration,
|
|
@@ -74,7 +110,7 @@ function readThemeVar(
|
|
|
74
110
|
const value =
|
|
75
111
|
elementStyles.getPropertyValue(name) || rootStyles.getPropertyValue(name);
|
|
76
112
|
const normalized = normalizeThemeColorValue(value);
|
|
77
|
-
if (normalized) return normalized;
|
|
113
|
+
if (normalized) return resolveCssColorValue(normalized);
|
|
78
114
|
}
|
|
79
115
|
return undefined;
|
|
80
116
|
}
|
|
@@ -101,198 +137,95 @@ function readCodeWorkbenchTheme(
|
|
|
101
137
|
return { colorScheme, values };
|
|
102
138
|
}
|
|
103
139
|
|
|
104
|
-
|
|
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
|
-
}
|
|
140
|
+
let monacoEnvironmentInstalled = false;
|
|
203
141
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
|
142
|
+
function ensureMonacoEnvironment() {
|
|
143
|
+
if (monacoEnvironmentInstalled || typeof window === "undefined") return;
|
|
144
|
+
(
|
|
145
|
+
globalThis as typeof globalThis & { MonacoEnvironment?: unknown }
|
|
146
|
+
).MonacoEnvironment = {
|
|
147
|
+
getWorker(_moduleId: string, label: string) {
|
|
148
|
+
if (label === "css" || label === "scss" || label === "less") {
|
|
149
|
+
return new CssWorker();
|
|
243
150
|
}
|
|
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);
|
|
151
|
+
if (label === "html" || label === "handlebars" || label === "razor") {
|
|
152
|
+
return new HtmlWorker();
|
|
259
153
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (editorEl.value !== state.content) editorEl.value = state.content || "";
|
|
270
|
-
editorEl.readOnly = !state.canEdit || !active;
|
|
271
|
-
focusSelection();
|
|
272
|
-
}
|
|
154
|
+
if (label === "json") return new JsonWorker();
|
|
155
|
+
if (label === "typescript" || label === "javascript") {
|
|
156
|
+
return new TypeScriptWorker();
|
|
157
|
+
}
|
|
158
|
+
return new EditorWorker();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
monacoEnvironmentInstalled = true;
|
|
162
|
+
}
|
|
273
163
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
164
|
+
function languageForPath(path: string, language?: string): string {
|
|
165
|
+
if (language === "html" || /\.html?$/i.test(path)) return "html";
|
|
166
|
+
if (language === "css" || /\.css$/i.test(path)) return "css";
|
|
167
|
+
if (language === "json" || /\.json$/i.test(path)) return "json";
|
|
168
|
+
if (language === "typescript" || /\.tsx?$/i.test(path)) return "typescript";
|
|
169
|
+
if (language === "javascript" || /\.jsx?$/i.test(path)) return "javascript";
|
|
170
|
+
return language || "plaintext";
|
|
171
|
+
}
|
|
281
172
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
173
|
+
function extensionBadgeForPath(path: string): string {
|
|
174
|
+
if (/\.css$/i.test(path)) return "CSS";
|
|
175
|
+
if (/\.tsx$/i.test(path)) return "TSX";
|
|
176
|
+
if (/\.ts$/i.test(path)) return "TS";
|
|
177
|
+
if (/\.jsx$/i.test(path)) return "JSX";
|
|
178
|
+
if (/\.js$/i.test(path)) return "JS";
|
|
179
|
+
if (/\.json$/i.test(path)) return "{}";
|
|
180
|
+
if (/\.html?$/i.test(path)) return "<>";
|
|
181
|
+
return "TXT";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function editorUriForPath(designId: string, path: string) {
|
|
185
|
+
return monaco.Uri.from({
|
|
186
|
+
scheme: "designfs",
|
|
187
|
+
authority: designId,
|
|
188
|
+
path: `/${path.replace(/^\/+/, "")}`,
|
|
287
189
|
});
|
|
288
|
-
|
|
289
|
-
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function defineMonacoTheme(theme: CodeWorkbenchTheme): string {
|
|
193
|
+
const dark = theme.colorScheme === "dark";
|
|
194
|
+
const values = theme.values;
|
|
195
|
+
const name = dark
|
|
196
|
+
? "design-code-workbench-dark"
|
|
197
|
+
: "design-code-workbench-light";
|
|
198
|
+
const colors = Object.fromEntries(
|
|
199
|
+
Object.entries({
|
|
200
|
+
"editor.background": values["--workbench-editor-bg"],
|
|
201
|
+
"editor.foreground": values["--workbench-fg"],
|
|
202
|
+
"editor.lineHighlightBackground": values["--workbench-hover-bg"],
|
|
203
|
+
"editor.selectionBackground": values["--workbench-selection-bg"],
|
|
204
|
+
"editor.inactiveSelectionBackground": values["--workbench-active-bg"],
|
|
205
|
+
"editorCursor.foreground": values["--workbench-accent"],
|
|
206
|
+
"editorLineNumber.foreground": values["--workbench-muted-fg"],
|
|
207
|
+
"editorLineNumber.activeForeground": values["--workbench-fg"],
|
|
208
|
+
"editorIndentGuide.background1": values["--workbench-border"],
|
|
209
|
+
"editorIndentGuide.activeBackground1": values["--workbench-muted-fg"],
|
|
210
|
+
"editorWidget.background": values["--workbench-surface-bg"],
|
|
211
|
+
"editorWidget.border": values["--workbench-border"],
|
|
212
|
+
focusBorder: values["--workbench-accent"],
|
|
213
|
+
}).filter(
|
|
214
|
+
(entry): entry is [string, string] =>
|
|
215
|
+
typeof entry[1] === "string" && entry[1].length > 0,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
monaco.editor.defineTheme(name, {
|
|
219
|
+
base: dark ? "vs-dark" : "vs",
|
|
220
|
+
inherit: true,
|
|
221
|
+
rules: [],
|
|
222
|
+
colors,
|
|
290
223
|
});
|
|
224
|
+
return name;
|
|
225
|
+
}
|
|
291
226
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
</body>
|
|
295
|
-
</html>`;
|
|
227
|
+
const MONACO_FONT_FAMILY =
|
|
228
|
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace';
|
|
296
229
|
|
|
297
230
|
export function CodeWorkbenchHost({
|
|
298
231
|
designId,
|
|
@@ -304,13 +237,21 @@ export function CodeWorkbenchHost({
|
|
|
304
237
|
onActiveFileChange,
|
|
305
238
|
}: CodeWorkbenchHostProps) {
|
|
306
239
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
307
|
-
const
|
|
240
|
+
const editorHostRef = useRef<HTMLDivElement | null>(null);
|
|
241
|
+
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
|
242
|
+
const modelRef = useRef<monaco.editor.ITextModel | null>(null);
|
|
243
|
+
const applyingModelContentRef = useRef(false);
|
|
244
|
+
const contentChangeRef = useRef<(content: string) => void>(() => {});
|
|
245
|
+
const saveCommandRef = useRef<(() => void) | null>(null);
|
|
308
246
|
const lastExternalTargetKeyRef = useRef<string | null>(null);
|
|
247
|
+
const lastSelectionKeyRef = useRef<string | null>(null);
|
|
309
248
|
const [activePath, setActivePath] = useState<string | null>(null);
|
|
310
249
|
const [draftsByPath, setDraftsByPath] = useState<
|
|
311
250
|
Record<string, CodeWorkbenchDraft>
|
|
312
251
|
>({});
|
|
313
|
-
const [
|
|
252
|
+
const [editorReady, setEditorReady] = useState(false);
|
|
253
|
+
const [cursorLabel, setCursorLabel] = useState("Ln 1, Col 1");
|
|
254
|
+
const [previewSummary, setPreviewSummary] = useState<string | null>(null);
|
|
314
255
|
const [theme, setTheme] = useState<CodeWorkbenchTheme>(() => ({
|
|
315
256
|
colorScheme: "light",
|
|
316
257
|
values: {},
|
|
@@ -327,6 +268,7 @@ export function CodeWorkbenchHost({
|
|
|
327
268
|
{ enabled: Boolean(selectedPath) },
|
|
328
269
|
);
|
|
329
270
|
const readSource = readSourceQuery.data as any;
|
|
271
|
+
const previewSourceEditMutation = useActionMutation("preview-source-edit");
|
|
330
272
|
const applySourceEditMutation = useActionMutation("apply-source-edit");
|
|
331
273
|
const savedContent = readSource?.content ?? "";
|
|
332
274
|
const activeDraft = selectedPath ? draftsByPath[selectedPath] : undefined;
|
|
@@ -340,6 +282,14 @@ export function CodeWorkbenchHost({
|
|
|
340
282
|
file.path === selectedPath ||
|
|
341
283
|
(activeFileId && file.fileId === activeFileId),
|
|
342
284
|
);
|
|
285
|
+
const activeDisplayName =
|
|
286
|
+
activeSourceFile?.displayName ?? readSource?.displayName ?? selectedPath;
|
|
287
|
+
const activeLanguage = languageForPath(selectedPath, readSource?.language);
|
|
288
|
+
const canEditSource = canEdit && readSource?.readonly !== true;
|
|
289
|
+
const saving =
|
|
290
|
+
previewSourceEditMutation.isPending || applySourceEditMutation.isPending;
|
|
291
|
+
const workspaceUri = backend?.workspaceUri ?? `designfs://${designId}/`;
|
|
292
|
+
const backendKind = backend?.kind ?? "virtual-inline";
|
|
343
293
|
|
|
344
294
|
useEffect(() => {
|
|
345
295
|
const updateTheme = () => {
|
|
@@ -369,6 +319,7 @@ export function CodeWorkbenchHost({
|
|
|
369
319
|
setActivePath(null);
|
|
370
320
|
setDraftsByPath({});
|
|
371
321
|
lastExternalTargetKeyRef.current = null;
|
|
322
|
+
lastSelectionKeyRef.current = null;
|
|
372
323
|
onActiveFileChange?.(null);
|
|
373
324
|
}, [designId, onActiveFileChange]);
|
|
374
325
|
|
|
@@ -411,6 +362,97 @@ export function CodeWorkbenchHost({
|
|
|
411
362
|
[readSource?.versionHash, savedContent, selectedPath],
|
|
412
363
|
);
|
|
413
364
|
|
|
365
|
+
const revertSelectedFile = useCallback(() => {
|
|
366
|
+
setSelectedDraftContent(savedContent);
|
|
367
|
+
setPreviewSummary(null);
|
|
368
|
+
}, [savedContent, setSelectedDraftContent]);
|
|
369
|
+
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
contentChangeRef.current = (content: string) => {
|
|
372
|
+
setSelectedDraftContent(content);
|
|
373
|
+
setPreviewSummary(null);
|
|
374
|
+
};
|
|
375
|
+
}, [setSelectedDraftContent]);
|
|
376
|
+
|
|
377
|
+
const saveSelectedFile = useCallback(() => {
|
|
378
|
+
if (!selectedPath || !dirty || !canEditSource || saving) return;
|
|
379
|
+
const edit = { kind: "full-replace" as const, content: draftContent };
|
|
380
|
+
previewSourceEditMutation.mutate(
|
|
381
|
+
{
|
|
382
|
+
designId,
|
|
383
|
+
path: selectedPath,
|
|
384
|
+
expectedVersionHash,
|
|
385
|
+
edit,
|
|
386
|
+
} as any,
|
|
387
|
+
{
|
|
388
|
+
onSuccess: (preview: any) => {
|
|
389
|
+
if (preview?.okToApply === false) {
|
|
390
|
+
toast.error(
|
|
391
|
+
preview.message ||
|
|
392
|
+
"Source file changed since it was read" /* i18n-ignore */,
|
|
393
|
+
);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
setPreviewSummary(
|
|
397
|
+
preview?.diff?.summary ||
|
|
398
|
+
`${preview?.editsApplied ?? 1} edit previewed` /* i18n-ignore */,
|
|
399
|
+
);
|
|
400
|
+
applySourceEditMutation.mutate(
|
|
401
|
+
{
|
|
402
|
+
designId,
|
|
403
|
+
path: selectedPath,
|
|
404
|
+
expectedVersionHash:
|
|
405
|
+
preview?.currentVersionHash ?? expectedVersionHash,
|
|
406
|
+
edit,
|
|
407
|
+
} as any,
|
|
408
|
+
{
|
|
409
|
+
onSuccess: (result: any) => {
|
|
410
|
+
setDraftsByPath((current) => {
|
|
411
|
+
const next = { ...current };
|
|
412
|
+
delete next[selectedPath];
|
|
413
|
+
return next;
|
|
414
|
+
});
|
|
415
|
+
setPreviewSummary(
|
|
416
|
+
result?.diff?.summary ||
|
|
417
|
+
"Saved through source actions" /* i18n-ignore */,
|
|
418
|
+
);
|
|
419
|
+
toast.success("Source file saved" /* i18n-ignore */);
|
|
420
|
+
},
|
|
421
|
+
onError: (error) => {
|
|
422
|
+
toast.error(
|
|
423
|
+
error instanceof Error
|
|
424
|
+
? error.message
|
|
425
|
+
: "Could not save source file" /* i18n-ignore */,
|
|
426
|
+
);
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
},
|
|
431
|
+
onError: (error) => {
|
|
432
|
+
toast.error(
|
|
433
|
+
error instanceof Error
|
|
434
|
+
? error.message
|
|
435
|
+
: "Could not preview source edit" /* i18n-ignore */,
|
|
436
|
+
);
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
}, [
|
|
441
|
+
applySourceEditMutation,
|
|
442
|
+
canEditSource,
|
|
443
|
+
designId,
|
|
444
|
+
dirty,
|
|
445
|
+
draftContent,
|
|
446
|
+
expectedVersionHash,
|
|
447
|
+
previewSourceEditMutation,
|
|
448
|
+
saving,
|
|
449
|
+
selectedPath,
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
saveCommandRef.current = saveSelectedFile;
|
|
454
|
+
}, [saveSelectedFile]);
|
|
455
|
+
|
|
414
456
|
useEffect(() => {
|
|
415
457
|
onActiveFileChange?.(
|
|
416
458
|
selectedPath
|
|
@@ -432,176 +474,179 @@ export function CodeWorkbenchHost({
|
|
|
432
474
|
selectedPath,
|
|
433
475
|
]);
|
|
434
476
|
|
|
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
477
|
useEffect(() => {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
"
|
|
478
|
+
ensureMonacoEnvironment();
|
|
479
|
+
if (!editorHostRef.current || editorRef.current) return;
|
|
480
|
+
const editor = monaco.editor.create(editorHostRef.current, {
|
|
481
|
+
value: "",
|
|
482
|
+
language: "html",
|
|
483
|
+
theme: defineMonacoTheme(theme),
|
|
484
|
+
automaticLayout: true,
|
|
485
|
+
contextmenu: true,
|
|
486
|
+
fontFamily: MONACO_FONT_FAMILY,
|
|
487
|
+
fontSize: 12,
|
|
488
|
+
lineHeight: 20,
|
|
489
|
+
minimap: { enabled: true, scale: 0.75, showSlider: "mouseover" },
|
|
490
|
+
bracketPairColorization: { enabled: true },
|
|
491
|
+
guides: { bracketPairs: true, indentation: true },
|
|
492
|
+
lineNumbers: "on",
|
|
493
|
+
renderLineHighlight: "all",
|
|
494
|
+
renderWhitespace: "selection",
|
|
495
|
+
scrollBeyondLastLine: false,
|
|
496
|
+
stickyScroll: { enabled: true },
|
|
497
|
+
tabSize: 2,
|
|
498
|
+
insertSpaces: true,
|
|
499
|
+
wordWrap: "off",
|
|
500
|
+
});
|
|
501
|
+
editorRef.current = editor;
|
|
502
|
+
setEditorReady(true);
|
|
503
|
+
|
|
504
|
+
const updateCursorLabel = () => {
|
|
505
|
+
const position = editor.getPosition();
|
|
506
|
+
setCursorLabel(
|
|
507
|
+
position
|
|
508
|
+
? `Ln ${position.lineNumber}, Col ${position.column}` /* i18n-ignore */
|
|
509
|
+
: "Ln 1, Col 1" /* i18n-ignore */,
|
|
510
|
+
);
|
|
511
|
+
};
|
|
512
|
+
const disposables = [
|
|
513
|
+
editor.onDidChangeModelContent(() => {
|
|
514
|
+
if (applyingModelContentRef.current) return;
|
|
515
|
+
contentChangeRef.current(editor.getValue());
|
|
516
|
+
}),
|
|
517
|
+
editor.onDidChangeCursorPosition(updateCursorLabel),
|
|
518
|
+
];
|
|
519
|
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () =>
|
|
520
|
+
saveCommandRef.current?.(),
|
|
477
521
|
);
|
|
478
|
-
|
|
522
|
+
updateCursorLabel();
|
|
523
|
+
|
|
524
|
+
return () => {
|
|
525
|
+
disposables.forEach((disposable) => disposable?.dispose?.());
|
|
526
|
+
editor.dispose();
|
|
527
|
+
modelRef.current?.dispose();
|
|
528
|
+
modelRef.current = null;
|
|
529
|
+
editorRef.current = null;
|
|
530
|
+
};
|
|
531
|
+
}, []);
|
|
479
532
|
|
|
480
533
|
useEffect(() => {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
);
|
|
534
|
+
if (!editorRef.current) return;
|
|
535
|
+
monaco.editor.setTheme(defineMonacoTheme(theme));
|
|
536
|
+
}, [theme]);
|
|
537
|
+
|
|
538
|
+
useEffect(() => {
|
|
539
|
+
const editor = editorRef.current;
|
|
540
|
+
if (!editor || !selectedPath) return;
|
|
541
|
+
const uri = editorUriForPath(designId, selectedPath);
|
|
542
|
+
const language = activeLanguage;
|
|
543
|
+
const currentModel = modelRef.current;
|
|
544
|
+
if (currentModel?.uri.toString() !== uri.toString()) {
|
|
545
|
+
currentModel?.dispose();
|
|
546
|
+
const nextModel = monaco.editor.createModel(draftContent, language, uri);
|
|
547
|
+
modelRef.current = nextModel;
|
|
548
|
+
editor.setModel(nextModel);
|
|
549
|
+
} else if (currentModel) {
|
|
550
|
+
monaco.editor.setModelLanguage(currentModel, language);
|
|
551
|
+
if (currentModel.getValue() !== draftContent) {
|
|
552
|
+
applyingModelContentRef.current = true;
|
|
553
|
+
currentModel.setValue(draftContent);
|
|
554
|
+
applyingModelContentRef.current = false;
|
|
536
555
|
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
556
|
+
}
|
|
557
|
+
editor.updateOptions({
|
|
558
|
+
readOnly: !canEditSource,
|
|
559
|
+
readOnlyMessage: {
|
|
560
|
+
value: canEdit
|
|
561
|
+
? "This source backend is read-only in the current workspace." /* i18n-ignore */
|
|
562
|
+
: "Ask an owner for edit access before changing this file." /* i18n-ignore */,
|
|
563
|
+
},
|
|
564
|
+
});
|
|
540
565
|
}, [
|
|
541
|
-
|
|
566
|
+
activeLanguage,
|
|
567
|
+
canEdit,
|
|
568
|
+
canEditSource,
|
|
542
569
|
designId,
|
|
543
|
-
dirty,
|
|
544
570
|
draftContent,
|
|
545
|
-
|
|
571
|
+
selectedPath,
|
|
572
|
+
]);
|
|
573
|
+
|
|
574
|
+
useEffect(() => {
|
|
575
|
+
const editor = editorRef.current;
|
|
576
|
+
const model = modelRef.current;
|
|
577
|
+
if (!editor || !model || !selectedPath) return;
|
|
578
|
+
const key = [
|
|
579
|
+
selectedPath,
|
|
580
|
+
selectedNodeId ?? "",
|
|
581
|
+
selectedSelector ?? "",
|
|
582
|
+
readSource?.versionHash ?? "",
|
|
583
|
+
].join(":");
|
|
584
|
+
if (lastSelectionKeyRef.current === key) return;
|
|
585
|
+
lastSelectionKeyRef.current = key;
|
|
586
|
+
const targets: string[] = [];
|
|
587
|
+
if (selectedNodeId) {
|
|
588
|
+
targets.push(`data-agent-native-node-id="${selectedNodeId}"`);
|
|
589
|
+
targets.push(`data-code-layer-id="${selectedNodeId}"`);
|
|
590
|
+
targets.push(selectedNodeId);
|
|
591
|
+
}
|
|
592
|
+
if (selectedSelector) targets.push(selectedSelector);
|
|
593
|
+
for (const target of targets) {
|
|
594
|
+
const index = draftContent.indexOf(target);
|
|
595
|
+
if (index < 0) continue;
|
|
596
|
+
const start = model.getPositionAt(index);
|
|
597
|
+
const end = model.getPositionAt(index + target.length);
|
|
598
|
+
const range = new monaco.Range(
|
|
599
|
+
start.lineNumber,
|
|
600
|
+
start.column,
|
|
601
|
+
end.lineNumber,
|
|
602
|
+
end.column,
|
|
603
|
+
);
|
|
604
|
+
editor.setSelection(range);
|
|
605
|
+
editor.revealRangeInCenter(range, monaco.editor.ScrollType.Smooth);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}, [
|
|
609
|
+
draftContent,
|
|
546
610
|
readSource?.versionHash,
|
|
547
|
-
|
|
611
|
+
selectedNodeId,
|
|
548
612
|
selectedPath,
|
|
549
|
-
|
|
613
|
+
selectedSelector,
|
|
550
614
|
]);
|
|
551
615
|
|
|
552
616
|
return (
|
|
553
617
|
<div
|
|
554
618
|
ref={containerRef}
|
|
555
|
-
className="flex min-h-0 flex-1 flex-col bg-[var(--
|
|
619
|
+
className="flex min-h-0 flex-1 flex-col bg-[var(--workbench-bg)] text-[var(--workbench-fg)]"
|
|
620
|
+
style={theme.values as CSSProperties}
|
|
556
621
|
>
|
|
557
|
-
<div className="flex h-10 shrink-0 items-center gap-2 border-b border-border
|
|
622
|
+
<div className="flex h-10 shrink-0 items-center gap-2 border-b border-[var(--workbench-border)] bg-[var(--workbench-surface-bg)] px-3">
|
|
558
623
|
<IconCode className="size-4 text-[var(--design-editor-accent-color)]" />
|
|
559
624
|
<div className="min-w-0 flex-1">
|
|
560
|
-
<h3 className="truncate text-xs font-semibold text-
|
|
625
|
+
<h3 className="truncate text-xs font-semibold text-[var(--workbench-fg)]">
|
|
561
626
|
{"Code" /* i18n-ignore */}
|
|
562
627
|
</h3>
|
|
563
|
-
<p className="truncate text-[10px] text-muted-
|
|
564
|
-
{
|
|
628
|
+
<p className="truncate text-[10px] text-[var(--workbench-muted-fg)]">
|
|
629
|
+
{workspaceUri}
|
|
565
630
|
</p>
|
|
566
631
|
</div>
|
|
632
|
+
<Button
|
|
633
|
+
size="sm"
|
|
634
|
+
variant="ghost"
|
|
635
|
+
className="h-7 gap-1.5 px-2 text-[11px]"
|
|
636
|
+
disabled={!dirty || saving}
|
|
637
|
+
onClick={revertSelectedFile}
|
|
638
|
+
>
|
|
639
|
+
<IconRefresh className="size-3" />
|
|
640
|
+
{"Revert" /* i18n-ignore */}
|
|
641
|
+
</Button>
|
|
567
642
|
<Button
|
|
568
643
|
size="sm"
|
|
569
644
|
variant="outline"
|
|
570
645
|
className="h-7 gap-1.5 px-2 text-[11px]"
|
|
571
|
-
disabled={!dirty || !
|
|
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
|
-
}}
|
|
646
|
+
disabled={!dirty || !canEditSource || saving}
|
|
647
|
+
onClick={saveSelectedFile}
|
|
603
648
|
>
|
|
604
|
-
{
|
|
649
|
+
{saving ? (
|
|
605
650
|
<Spinner className="size-3" />
|
|
606
651
|
) : (
|
|
607
652
|
<IconDeviceFloppy className="size-3" />
|
|
@@ -611,19 +656,119 @@ export function CodeWorkbenchHost({
|
|
|
611
656
|
</div>
|
|
612
657
|
<div
|
|
613
658
|
className={cn(
|
|
614
|
-
"min-h-0 flex-1 bg-[var(--
|
|
659
|
+
"grid min-h-0 flex-1 grid-cols-[210px_minmax(0,1fr)] bg-[var(--workbench-editor-bg)]",
|
|
615
660
|
(sourceFilesQuery.isLoading || readSourceQuery.isLoading) &&
|
|
616
661
|
"opacity-80",
|
|
617
662
|
)}
|
|
618
663
|
>
|
|
619
|
-
<
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
664
|
+
<aside className="flex min-h-0 flex-col border-r border-[var(--workbench-border)] bg-[var(--workbench-sidebar-bg)]">
|
|
665
|
+
<div className="flex h-9 shrink-0 items-center gap-2 border-b border-[var(--workbench-border)] px-3 text-[11px] font-semibold uppercase tracking-[0.11em] text-[var(--workbench-muted-fg)]">
|
|
666
|
+
<IconFolder className="size-3.5" />
|
|
667
|
+
{"Explorer" /* i18n-ignore */}
|
|
668
|
+
</div>
|
|
669
|
+
<div className="flex h-8 shrink-0 items-center gap-2 border-b border-[var(--workbench-border)] px-3 text-[11px] text-[var(--workbench-muted-fg)]">
|
|
670
|
+
<IconSearch className="size-3.5" />
|
|
671
|
+
<span className="truncate">
|
|
672
|
+
{"Use Cmd/Ctrl+F in editor" /* i18n-ignore */}
|
|
673
|
+
</span>
|
|
674
|
+
</div>
|
|
675
|
+
<div className="min-h-0 flex-1 overflow-auto py-1">
|
|
676
|
+
{sourceFiles.length > 0 ? (
|
|
677
|
+
sourceFiles.map((file: any) => {
|
|
678
|
+
const active = file.path === selectedPath;
|
|
679
|
+
const disabled = file.readonly && !canEdit;
|
|
680
|
+
return (
|
|
681
|
+
<button
|
|
682
|
+
key={file.path}
|
|
683
|
+
type="button"
|
|
684
|
+
title={file.path}
|
|
685
|
+
aria-current={active ? "page" : undefined}
|
|
686
|
+
className={cn(
|
|
687
|
+
"flex h-7 w-full min-w-0 cursor-pointer items-center gap-2 px-2 text-left text-xs text-[var(--workbench-muted-fg)] outline-none hover:bg-[var(--workbench-hover-bg)] hover:text-[var(--workbench-fg)] focus-visible:ring-1 focus-visible:ring-[var(--workbench-accent)]",
|
|
688
|
+
active &&
|
|
689
|
+
"bg-[var(--workbench-active-bg)] text-[var(--workbench-active-fg)]",
|
|
690
|
+
disabled && "opacity-60",
|
|
691
|
+
)}
|
|
692
|
+
onClick={() => {
|
|
693
|
+
setActivePath(file.path);
|
|
694
|
+
setPreviewSummary(null);
|
|
695
|
+
}}
|
|
696
|
+
>
|
|
697
|
+
<span className="w-7 shrink-0 font-mono text-[10px] text-[var(--workbench-accent)]">
|
|
698
|
+
{extensionBadgeForPath(file.path)}
|
|
699
|
+
</span>
|
|
700
|
+
<span className="min-w-0 flex-1 truncate">
|
|
701
|
+
{file.displayName || file.path}
|
|
702
|
+
</span>
|
|
703
|
+
{draftsByPath[file.path] ? (
|
|
704
|
+
<span className="size-1.5 shrink-0 rounded-full bg-[var(--workbench-dirty)]" />
|
|
705
|
+
) : null}
|
|
706
|
+
</button>
|
|
707
|
+
);
|
|
708
|
+
})
|
|
709
|
+
) : (
|
|
710
|
+
<p className="px-3 py-4 text-xs text-[var(--workbench-muted-fg)]">
|
|
711
|
+
{"No inline source files" /* i18n-ignore */}
|
|
712
|
+
</p>
|
|
713
|
+
)}
|
|
714
|
+
</div>
|
|
715
|
+
<div className="border-t border-[var(--workbench-border)] px-3 py-2 text-[11px] leading-4 text-[var(--workbench-muted-fg)]">
|
|
716
|
+
<div className="truncate">{backendKind}</div>
|
|
717
|
+
<div className="truncate">{workspaceUri}</div>
|
|
718
|
+
</div>
|
|
719
|
+
</aside>
|
|
720
|
+
|
|
721
|
+
<main className="flex min-h-0 min-w-0 flex-col bg-[var(--workbench-editor-bg)]">
|
|
722
|
+
<div className="flex h-9 shrink-0 items-center border-b border-[var(--workbench-border)] bg-[var(--workbench-surface-bg)]">
|
|
723
|
+
<div className="flex h-full max-w-[360px] items-center gap-2 border-r border-[var(--workbench-border)] px-3 text-xs text-[var(--workbench-fg)]">
|
|
724
|
+
<IconFileCode className="size-3.5 text-[var(--workbench-accent)]" />
|
|
725
|
+
<span className="min-w-0 truncate">
|
|
726
|
+
{activeDisplayName || "No file" /* i18n-ignore */}
|
|
727
|
+
</span>
|
|
728
|
+
{dirty ? (
|
|
729
|
+
<span className="size-1.5 shrink-0 rounded-full bg-[var(--workbench-dirty)]" />
|
|
730
|
+
) : null}
|
|
731
|
+
</div>
|
|
732
|
+
<div className="ml-auto flex min-w-0 items-center gap-3 px-3 text-[11px] text-[var(--workbench-muted-fg)]">
|
|
733
|
+
<span className="truncate">
|
|
734
|
+
{dirty
|
|
735
|
+
? "Unsaved changes" /* i18n-ignore */
|
|
736
|
+
: readSource?.versionHash
|
|
737
|
+
? `Saved ${readSource.versionHash}` /* i18n-ignore */
|
|
738
|
+
: previewSummary || ""}
|
|
739
|
+
</span>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
<div className="relative min-h-0 flex-1">
|
|
743
|
+
<div
|
|
744
|
+
ref={editorHostRef}
|
|
745
|
+
data-testid="design-code-monaco-editor"
|
|
746
|
+
className="absolute inset-0"
|
|
747
|
+
/>
|
|
748
|
+
{!selectedPath && !sourceFilesQuery.isLoading ? (
|
|
749
|
+
<div className="absolute inset-0 grid place-items-center px-6 text-center text-sm text-[var(--workbench-muted-fg)]">
|
|
750
|
+
{"Choose a source file to start editing." /* i18n-ignore */}
|
|
751
|
+
</div>
|
|
752
|
+
) : null}
|
|
753
|
+
{!editorReady ? (
|
|
754
|
+
<div className="absolute inset-0 grid place-items-center bg-[var(--workbench-editor-bg)] text-[var(--workbench-muted-fg)]">
|
|
755
|
+
<Spinner className="size-4" />
|
|
756
|
+
</div>
|
|
757
|
+
) : null}
|
|
758
|
+
</div>
|
|
759
|
+
<div className="flex h-6 shrink-0 items-center gap-4 border-t border-[var(--workbench-border)] bg-[var(--workbench-surface-bg)] px-3 text-[11px] text-[var(--workbench-muted-fg)]">
|
|
760
|
+
<span>{activeLanguage}</span>
|
|
761
|
+
<span>{cursorLabel}</span>
|
|
762
|
+
<span className="ml-auto truncate">
|
|
763
|
+
{
|
|
764
|
+
previewSummary ??
|
|
765
|
+
(canEditSource
|
|
766
|
+
? "Preview on save, apply with version check" /* i18n-ignore */
|
|
767
|
+
: "Read-only source") /* i18n-ignore */
|
|
768
|
+
}
|
|
769
|
+
</span>
|
|
770
|
+
</div>
|
|
771
|
+
</main>
|
|
627
772
|
</div>
|
|
628
773
|
</div>
|
|
629
774
|
);
|