@aprovan/patchwork-editor 0.1.2-dev.03aaf5b → 0.1.2-dev.ba8f277

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/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { Node, textblockTypeInputRule } from '@tiptap/core';
2
2
  import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent, useEditor, EditorContent } from '@tiptap/react';
3
3
  import { useRef, useCallback, useMemo, useState, useEffect } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
- import { AlertCircle, FileImage, FileVideo, Pencil, Loader2, RotateCcw, FileCode, FolderTree, Eye, Code, Save, X, Send, MessageSquare, Cloud, Check, Server, ChevronDown, ChevronRight, Folder, File, Upload } from 'lucide-react';
5
+ import { Folder, Pin, X, Loader2, AlertCircle, FileImage, FileVideo, Pencil, RotateCcw, FileCode, FolderTree, Eye, Code, Send, MessageSquare, Server, ChevronDown, File, PinOff, ChevronRight, ChevronsDown, Upload, AlertTriangle, Save } from 'lucide-react';
6
6
  import { createSingleFileProject, HttpBackend, VFSStore, detectMainFile, createProjectFromFiles } from '@aprovan/patchwork-compiler';
7
7
  import Markdown from 'react-markdown';
8
8
  import remarkGfm from 'remark-gfm';
@@ -371,7 +371,11 @@ async function sendEditRequest(request, options = {}) {
371
371
  }
372
372
  const text = await streamResponse(response, onProgress);
373
373
  if (!hasDiffBlocks(text)) {
374
- throw new Error("No valid diffs in response");
374
+ return {
375
+ newCode: request.code,
376
+ summary: text.trim(),
377
+ progressNotes: []
378
+ };
375
379
  }
376
380
  const parsed = parseEditResponse(text);
377
381
  const result = applyDiffs(request.code, parsed.diffs, { sanitize });
@@ -440,6 +444,7 @@ function useEditSession(options) {
440
444
  const {
441
445
  originalCode,
442
446
  originalProject: providedProject,
447
+ initialActiveFile,
443
448
  compile,
444
449
  apiEndpoint
445
450
  } = options;
@@ -455,7 +460,9 @@ function useEditSession(options) {
455
460
  );
456
461
  const lastSyncedProjectRef = useRef(originalProject);
457
462
  const [project, setProject] = useState(originalProject);
458
- const [activeFile, setActiveFile] = useState(originalProject.entry);
463
+ const [activeFile, setActiveFile] = useState(
464
+ initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
465
+ );
459
466
  const [history, setHistory] = useState([]);
460
467
  const [isApplying, setIsApplying] = useState(false);
461
468
  const [error, setError] = useState(null);
@@ -465,12 +472,14 @@ function useEditSession(options) {
465
472
  if (originalProject !== lastSyncedProjectRef.current) {
466
473
  lastSyncedProjectRef.current = originalProject;
467
474
  setProject(originalProject);
468
- setActiveFile(originalProject.entry);
475
+ setActiveFile(
476
+ initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
477
+ );
469
478
  setHistory([]);
470
479
  setError(null);
471
480
  setStreamingNotes([]);
472
481
  }
473
- }, [originalProject]);
482
+ }, [originalProject, initialActiveFile]);
474
483
  const performEdit = useCallback(
475
484
  async (currentCode2, prompt, isRetry = false) => {
476
485
  const entries = [];
@@ -851,6 +860,156 @@ function MarkdownEditor({
851
860
  }
852
861
  );
853
862
  }
863
+ function parseFrontmatter(content) {
864
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
865
+ if (!match) return { frontmatter: "", body: content };
866
+ return { frontmatter: match[1], body: match[2] };
867
+ }
868
+ function assembleFrontmatter(frontmatter, body) {
869
+ if (!frontmatter.trim()) return body;
870
+ return `---
871
+ ${frontmatter}
872
+ ---
873
+ ${body}`;
874
+ }
875
+ function MarkdownPreview({
876
+ value,
877
+ onChange,
878
+ editable = false,
879
+ className = ""
880
+ }) {
881
+ const { frontmatter, body } = parseFrontmatter(value);
882
+ const [fm, setFm] = useState(frontmatter);
883
+ const fmRef = useRef(frontmatter);
884
+ const bodyRef = useRef(body);
885
+ const textareaRef = useRef(null);
886
+ useEffect(() => {
887
+ const parsed = parseFrontmatter(value);
888
+ fmRef.current = parsed.frontmatter;
889
+ bodyRef.current = parsed.body;
890
+ setFm(parsed.frontmatter);
891
+ }, [value]);
892
+ const emitChange = useCallback(
893
+ (newFm, newBody) => {
894
+ onChange?.(assembleFrontmatter(newFm, newBody));
895
+ },
896
+ [onChange]
897
+ );
898
+ const handleFmChange = useCallback(
899
+ (e) => {
900
+ const newFm = e.target.value;
901
+ setFm(newFm);
902
+ fmRef.current = newFm;
903
+ emitChange(newFm, bodyRef.current);
904
+ },
905
+ [emitChange]
906
+ );
907
+ useEffect(() => {
908
+ if (textareaRef.current) {
909
+ textareaRef.current.style.height = "auto";
910
+ textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
911
+ }
912
+ }, [fm]);
913
+ const editor = useEditor({
914
+ extensions: [
915
+ StarterKit.configure({
916
+ heading: { levels: [1, 2, 3, 4, 5, 6] },
917
+ bulletList: { keepMarks: true, keepAttributes: false },
918
+ orderedList: { keepMarks: true, keepAttributes: false },
919
+ codeBlock: false,
920
+ code: {
921
+ HTMLAttributes: {
922
+ class: "bg-muted rounded px-1 py-0.5 font-mono text-sm"
923
+ }
924
+ },
925
+ blockquote: {
926
+ HTMLAttributes: {
927
+ class: "border-l-4 border-muted-foreground/30 pl-4 italic"
928
+ }
929
+ },
930
+ hardBreak: { keepMarks: false }
931
+ }),
932
+ CodeBlockExtension,
933
+ Typography,
934
+ Markdown$1.configure({
935
+ html: false,
936
+ transformPastedText: true,
937
+ transformCopiedText: true
938
+ })
939
+ ],
940
+ content: body,
941
+ editable,
942
+ editorProps: {
943
+ attributes: {
944
+ class: `outline-none ${className}`
945
+ }
946
+ },
947
+ onUpdate: ({ editor: editor2 }) => {
948
+ const markdownStorage = editor2.storage.markdown;
949
+ const newBody = markdownStorage?.getMarkdown?.() ?? editor2.getText();
950
+ bodyRef.current = newBody;
951
+ emitChange(fmRef.current, newBody);
952
+ }
953
+ });
954
+ useEffect(() => {
955
+ editor?.setEditable(editable);
956
+ }, [editor, editable]);
957
+ useEffect(() => {
958
+ if (!editor) return;
959
+ const parsed = parseFrontmatter(value);
960
+ const markdownStorage = editor.storage.markdown;
961
+ const current = markdownStorage?.getMarkdown?.() ?? editor.getText();
962
+ if (parsed.body !== current) {
963
+ editor.commands.setContent(parsed.body);
964
+ }
965
+ }, [editor, value]);
966
+ return /* @__PURE__ */ jsxs("div", { className: "markdown-preview", children: [
967
+ frontmatter && /* @__PURE__ */ jsxs("div", { className: "mb-4 rounded-md border border-border bg-muted/40 overflow-hidden", children: [
968
+ /* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 text-xs font-mono text-muted-foreground border-b border-border bg-muted/60 select-none", children: "yml" }),
969
+ /* @__PURE__ */ jsx(
970
+ "textarea",
971
+ {
972
+ ref: textareaRef,
973
+ value: fm,
974
+ onChange: handleFmChange,
975
+ readOnly: !editable,
976
+ className: "w-full bg-transparent px-3 py-2 font-mono text-sm outline-none resize-none",
977
+ spellCheck: false
978
+ }
979
+ )
980
+ ] }),
981
+ /* @__PURE__ */ jsx(EditorContent, { editor, className: "markdown-editor" })
982
+ ] });
983
+ }
984
+ function getToneClass(tone, status) {
985
+ if (status === "error") {
986
+ return tone === "muted" ? "text-destructive hover:bg-muted" : "text-destructive hover:bg-destructive/10";
987
+ }
988
+ if (status === "saved") {
989
+ return tone === "muted" ? "text-muted-foreground/50 hover:bg-muted" : "text-primary/50 hover:bg-primary/10";
990
+ }
991
+ return tone === "muted" ? "text-muted-foreground hover:bg-muted" : "text-primary hover:bg-primary/20";
992
+ }
993
+ function SaveStatusButton({
994
+ status,
995
+ onClick,
996
+ disabled = false,
997
+ tone
998
+ }) {
999
+ return /* @__PURE__ */ jsxs(
1000
+ "button",
1001
+ {
1002
+ onClick,
1003
+ disabled,
1004
+ className: `px-2 py-1 text-xs rounded flex items-center gap-1 disabled:opacity-50 ${getToneClass(tone, status)}`,
1005
+ title: "Save",
1006
+ children: [
1007
+ /* @__PURE__ */ jsx("span", { className: "inline-flex h-3 w-3 items-center justify-center shrink-0", children: status === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : status === "error" ? /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Save, { className: `h-3 w-3 ${status === "saved" ? "opacity-60" : "opacity-100"}` }) }),
1008
+ "Save"
1009
+ ]
1010
+ }
1011
+ );
1012
+ }
854
1013
 
855
1014
  // src/components/edit/fileTypes.ts
856
1015
  var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
@@ -939,6 +1098,12 @@ function isMediaFile(path) {
939
1098
  function isTextFile(path) {
940
1099
  return TEXT_EXTENSIONS.includes(getExtension(path));
941
1100
  }
1101
+ function isMarkdownFile(path) {
1102
+ return getExtension(path) === ".md";
1103
+ }
1104
+ function isPreviewable(path) {
1105
+ return isCompilable(path) || isMarkdownFile(path);
1106
+ }
942
1107
  function getLanguageFromExt(path) {
943
1108
  const ext = getExtension(path);
944
1109
  return EXTENSION_TO_LANGUAGE[ext] ?? null;
@@ -955,6 +1120,17 @@ function isVideoFile(path) {
955
1120
  const ext = getExtension(path);
956
1121
  return [".mp4", ".mov", ".webm"].includes(ext);
957
1122
  }
1123
+ function sortNodes(nodes) {
1124
+ nodes.sort((a, b) => {
1125
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1126
+ return a.name.localeCompare(b.name);
1127
+ });
1128
+ for (const node of nodes) {
1129
+ if (node.children.length > 0) {
1130
+ sortNodes(node.children);
1131
+ }
1132
+ }
1133
+ }
958
1134
  function buildTree(files) {
959
1135
  const root = { name: "", path: "", isDir: true, children: [] };
960
1136
  for (const file of files) {
@@ -977,14 +1153,26 @@ function buildTree(files) {
977
1153
  current = child;
978
1154
  }
979
1155
  }
980
- root.children.sort((a, b) => {
981
- if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
982
- return a.name.localeCompare(b.name);
983
- });
1156
+ sortNodes(root.children);
984
1157
  return root;
985
1158
  }
986
- function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth = 0 }) {
987
- const [expanded, setExpanded] = useState(true);
1159
+ function TreeNodeComponent({
1160
+ node,
1161
+ activePath,
1162
+ onSelect,
1163
+ onSelectDirectory,
1164
+ onReplaceFile,
1165
+ onOpenInEditor,
1166
+ openInEditorMode = "files",
1167
+ openInEditorIcon,
1168
+ openInEditorTitle = "Open in editor",
1169
+ pinnedPaths,
1170
+ onTogglePin,
1171
+ pageSize = 10,
1172
+ depth = 0
1173
+ }) {
1174
+ const [expanded, setExpanded] = useState(false);
1175
+ const [visibleCount, setVisibleCount] = useState(pageSize);
988
1176
  const [isHovered, setIsHovered] = useState(false);
989
1177
  const fileInputRef = useRef(null);
990
1178
  const handleUploadClick = useCallback((e) => {
@@ -1003,48 +1191,119 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1003
1191
  reader.readAsDataURL(file);
1004
1192
  e.target.value = "";
1005
1193
  }, [node.path, onReplaceFile]);
1194
+ const isPinned = pinnedPaths?.has(node.path) ?? false;
1195
+ const showPin = onTogglePin && isHovered;
1196
+ const handleTogglePin = useCallback((e) => {
1197
+ e.stopPropagation();
1198
+ onTogglePin?.(node.path, node.isDir);
1199
+ }, [node.path, node.isDir, onTogglePin]);
1006
1200
  if (!node.name) {
1007
1201
  return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((child) => /* @__PURE__ */ jsx(
1008
1202
  TreeNodeComponent,
1009
1203
  {
1010
1204
  node: child,
1011
- activeFile,
1205
+ activePath,
1012
1206
  onSelect,
1207
+ onSelectDirectory,
1013
1208
  onReplaceFile,
1209
+ onOpenInEditor,
1210
+ openInEditorMode,
1211
+ openInEditorIcon,
1212
+ openInEditorTitle,
1213
+ pinnedPaths,
1214
+ onTogglePin,
1215
+ pageSize,
1014
1216
  depth
1015
1217
  },
1016
1218
  child.path
1017
1219
  )) });
1018
1220
  }
1019
- const isActive = node.path === activeFile;
1221
+ const isActive = node.path === activePath;
1020
1222
  const isMedia = !node.isDir && isMediaFile(node.path);
1021
1223
  const showUpload = isMedia && isHovered && onReplaceFile;
1224
+ const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? node.isDir : !node.isDir));
1225
+ const handleOpenInEditor = useCallback(
1226
+ (e) => {
1227
+ e.stopPropagation();
1228
+ onOpenInEditor?.(node.path, node.isDir);
1229
+ },
1230
+ [node.path, node.isDir, onOpenInEditor]
1231
+ );
1022
1232
  if (node.isDir) {
1023
1233
  return /* @__PURE__ */ jsxs("div", { children: [
1024
1234
  /* @__PURE__ */ jsxs(
1025
1235
  "button",
1026
1236
  {
1027
- onClick: () => setExpanded(!expanded),
1028
- className: "flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded",
1237
+ onClick: () => {
1238
+ onSelectDirectory?.(node.path);
1239
+ setExpanded(!expanded);
1240
+ },
1241
+ className: `flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded ${isActive ? "bg-primary/10 text-primary" : ""}`,
1242
+ onMouseEnter: () => setIsHovered(true),
1243
+ onMouseLeave: () => setIsHovered(false),
1029
1244
  style: { paddingLeft: `${depth * 12 + 8}px` },
1030
1245
  children: [
1031
1246
  expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
1032
1247
  /* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1033
- /* @__PURE__ */ jsx("span", { className: "truncate", children: node.name })
1248
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
1249
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1250
+ "span",
1251
+ {
1252
+ onClick: handleTogglePin,
1253
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1254
+ title: isPinned ? "Unpin" : "Pin",
1255
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1256
+ }
1257
+ ),
1258
+ showOpenInEditor && /* @__PURE__ */ jsx(
1259
+ "span",
1260
+ {
1261
+ onClick: handleOpenInEditor,
1262
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1263
+ title: openInEditorTitle,
1264
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1265
+ }
1266
+ )
1034
1267
  ]
1035
1268
  }
1036
1269
  ),
1037
- expanded && /* @__PURE__ */ jsx("div", { children: node.children.map((child) => /* @__PURE__ */ jsx(
1038
- TreeNodeComponent,
1039
- {
1040
- node: child,
1041
- activeFile,
1042
- onSelect,
1043
- onReplaceFile,
1044
- depth: depth + 1
1045
- },
1046
- child.path
1047
- )) })
1270
+ expanded && /* @__PURE__ */ jsxs("div", { children: [
1271
+ node.children.slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
1272
+ TreeNodeComponent,
1273
+ {
1274
+ node: child,
1275
+ activePath,
1276
+ onSelect,
1277
+ onSelectDirectory,
1278
+ onReplaceFile,
1279
+ onOpenInEditor,
1280
+ openInEditorMode,
1281
+ openInEditorIcon,
1282
+ openInEditorTitle,
1283
+ pinnedPaths,
1284
+ onTogglePin,
1285
+ pageSize,
1286
+ depth: depth + 1
1287
+ },
1288
+ child.path
1289
+ )),
1290
+ node.children.length > visibleCount && /* @__PURE__ */ jsxs(
1291
+ "button",
1292
+ {
1293
+ onClick: () => setVisibleCount((prev) => prev + pageSize),
1294
+ className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
1295
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1296
+ children: [
1297
+ /* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
1298
+ /* @__PURE__ */ jsxs("span", { children: [
1299
+ "Show ",
1300
+ Math.min(pageSize, node.children.length - visibleCount),
1301
+ " more"
1302
+ ] })
1303
+ ]
1304
+ }
1305
+ )
1306
+ ] })
1048
1307
  ] });
1049
1308
  }
1050
1309
  return /* @__PURE__ */ jsxs(
@@ -1062,7 +1321,25 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1062
1321
  style: { paddingLeft: `${depth * 12 + 20}px` },
1063
1322
  children: [
1064
1323
  /* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1065
- /* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: node.name }),
1324
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
1325
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1326
+ "span",
1327
+ {
1328
+ onClick: handleTogglePin,
1329
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1330
+ title: isPinned ? "Unpin" : "Pin",
1331
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1332
+ }
1333
+ ),
1334
+ showOpenInEditor && /* @__PURE__ */ jsx(
1335
+ "span",
1336
+ {
1337
+ onClick: handleOpenInEditor,
1338
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1339
+ title: openInEditorTitle,
1340
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1341
+ }
1342
+ ),
1066
1343
  showUpload && /* @__PURE__ */ jsx(
1067
1344
  "span",
1068
1345
  {
@@ -1089,17 +1366,309 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1089
1366
  }
1090
1367
  );
1091
1368
  }
1092
- function FileTree({ files, activeFile, onSelectFile, onReplaceFile }) {
1369
+ function LazyTreeNode({
1370
+ entry,
1371
+ activePath,
1372
+ onSelectFile,
1373
+ onSelectDirectory,
1374
+ onOpenInEditor,
1375
+ openInEditorMode = "files",
1376
+ openInEditorIcon,
1377
+ openInEditorTitle = "Open in editor",
1378
+ pinnedPaths,
1379
+ onTogglePin,
1380
+ directoryLoader,
1381
+ pageSize,
1382
+ depth = 0,
1383
+ reloadToken
1384
+ }) {
1385
+ const [expanded, setExpanded] = useState(false);
1386
+ const [loading, setLoading] = useState(false);
1387
+ const [loadError, setLoadError] = useState(null);
1388
+ const [isHovered, setIsHovered] = useState(false);
1389
+ const [children, setChildren] = useState(null);
1390
+ const [visibleCount, setVisibleCount] = useState(pageSize);
1391
+ useEffect(() => {
1392
+ setChildren(null);
1393
+ setVisibleCount(pageSize);
1394
+ if (expanded) {
1395
+ setLoading(true);
1396
+ setLoadError(null);
1397
+ directoryLoader(entry.path).then((loaded) => setChildren(loaded)).catch((err) => setLoadError(err instanceof Error ? err.message : "Failed to load directory")).finally(() => setLoading(false));
1398
+ }
1399
+ }, [reloadToken]);
1400
+ const isActive = entry.path === activePath;
1401
+ const isPinned = pinnedPaths?.has(entry.path) ?? false;
1402
+ const showPin = onTogglePin && isHovered;
1403
+ const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? entry.isDir : !entry.isDir));
1404
+ const handleOpenInEditor = useCallback(
1405
+ (e) => {
1406
+ e.stopPropagation();
1407
+ onOpenInEditor?.(entry.path, entry.isDir);
1408
+ },
1409
+ [entry.path, entry.isDir, onOpenInEditor]
1410
+ );
1411
+ const handleTogglePin = useCallback(
1412
+ (e) => {
1413
+ e.stopPropagation();
1414
+ onTogglePin?.(entry.path, entry.isDir);
1415
+ },
1416
+ [entry.path, entry.isDir, onTogglePin]
1417
+ );
1418
+ const toggleDirectory = useCallback(async () => {
1419
+ if (!entry.isDir) return;
1420
+ onSelectDirectory?.(entry.path);
1421
+ if (!expanded && children === null) {
1422
+ setLoading(true);
1423
+ setLoadError(null);
1424
+ try {
1425
+ const loaded = await directoryLoader(entry.path);
1426
+ setChildren(loaded);
1427
+ } catch (err) {
1428
+ setLoadError(err instanceof Error ? err.message : "Failed to load directory");
1429
+ } finally {
1430
+ setLoading(false);
1431
+ }
1432
+ }
1433
+ setExpanded((prev) => !prev);
1434
+ }, [entry.isDir, entry.path, onSelectDirectory, expanded, children, directoryLoader]);
1435
+ if (!entry.isDir) {
1436
+ return /* @__PURE__ */ jsx(
1437
+ "div",
1438
+ {
1439
+ className: "relative",
1440
+ onMouseEnter: () => setIsHovered(true),
1441
+ onMouseLeave: () => setIsHovered(false),
1442
+ children: /* @__PURE__ */ jsxs(
1443
+ "button",
1444
+ {
1445
+ onClick: () => onSelectFile(entry.path),
1446
+ className: `flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded ${isActive ? "bg-primary/10 text-primary" : ""}`,
1447
+ style: { paddingLeft: `${depth * 12 + 20}px` },
1448
+ children: [
1449
+ /* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1450
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
1451
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1452
+ "span",
1453
+ {
1454
+ onClick: handleTogglePin,
1455
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1456
+ title: isPinned ? "Unpin" : "Pin",
1457
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1458
+ }
1459
+ ),
1460
+ showOpenInEditor && /* @__PURE__ */ jsx(
1461
+ "span",
1462
+ {
1463
+ onClick: handleOpenInEditor,
1464
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1465
+ title: openInEditorTitle,
1466
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1467
+ }
1468
+ )
1469
+ ]
1470
+ }
1471
+ )
1472
+ }
1473
+ );
1474
+ }
1475
+ return /* @__PURE__ */ jsxs("div", { children: [
1476
+ /* @__PURE__ */ jsxs(
1477
+ "button",
1478
+ {
1479
+ onClick: () => void toggleDirectory(),
1480
+ className: `flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded ${isActive ? "bg-primary/10 text-primary" : ""}`,
1481
+ onMouseEnter: () => setIsHovered(true),
1482
+ onMouseLeave: () => setIsHovered(false),
1483
+ style: { paddingLeft: `${depth * 12 + 8}px` },
1484
+ children: [
1485
+ expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
1486
+ /* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1487
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
1488
+ loading && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-muted-foreground" }),
1489
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1490
+ "span",
1491
+ {
1492
+ onClick: handleTogglePin,
1493
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1494
+ title: isPinned ? "Unpin" : "Pin",
1495
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1496
+ }
1497
+ ),
1498
+ showOpenInEditor && /* @__PURE__ */ jsx(
1499
+ "span",
1500
+ {
1501
+ onClick: handleOpenInEditor,
1502
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1503
+ title: openInEditorTitle,
1504
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1505
+ }
1506
+ )
1507
+ ]
1508
+ }
1509
+ ),
1510
+ expanded && /* @__PURE__ */ jsxs("div", { children: [
1511
+ loadError && /* @__PURE__ */ jsx(
1512
+ "div",
1513
+ {
1514
+ className: "px-2 py-1 text-xs text-destructive",
1515
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1516
+ children: loadError
1517
+ }
1518
+ ),
1519
+ (children ?? []).slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
1520
+ LazyTreeNode,
1521
+ {
1522
+ entry: child,
1523
+ activePath,
1524
+ onSelectFile,
1525
+ onSelectDirectory,
1526
+ onOpenInEditor,
1527
+ openInEditorMode,
1528
+ openInEditorIcon,
1529
+ openInEditorTitle,
1530
+ pinnedPaths,
1531
+ onTogglePin,
1532
+ directoryLoader,
1533
+ pageSize,
1534
+ depth: depth + 1,
1535
+ reloadToken
1536
+ },
1537
+ child.path
1538
+ )),
1539
+ (children?.length ?? 0) > visibleCount && /* @__PURE__ */ jsxs(
1540
+ "button",
1541
+ {
1542
+ onClick: () => setVisibleCount((prev) => prev + pageSize),
1543
+ className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
1544
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1545
+ children: [
1546
+ /* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
1547
+ /* @__PURE__ */ jsxs("span", { children: [
1548
+ "Show ",
1549
+ Math.min(pageSize, (children?.length ?? 0) - visibleCount),
1550
+ " more"
1551
+ ] })
1552
+ ]
1553
+ }
1554
+ )
1555
+ ] })
1556
+ ] });
1557
+ }
1558
+ function FileTree({
1559
+ files = [],
1560
+ activeFile,
1561
+ activePath,
1562
+ title = "Files",
1563
+ onSelectFile,
1564
+ onSelectDirectory,
1565
+ onReplaceFile,
1566
+ onOpenInEditor,
1567
+ openInEditorMode,
1568
+ openInEditorIcon,
1569
+ openInEditorTitle,
1570
+ pinnedPaths,
1571
+ onTogglePin,
1572
+ directoryLoader,
1573
+ pageSize = 10,
1574
+ reloadToken
1575
+ }) {
1093
1576
  const tree = useMemo(() => buildTree(files), [files]);
1094
- return /* @__PURE__ */ jsxs("div", { className: "w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
1095
- /* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Files" }),
1096
- /* @__PURE__ */ jsx("div", { className: "p-1", children: /* @__PURE__ */ jsx(
1577
+ const selectedPath = activePath ?? activeFile ?? "";
1578
+ const [rootEntries, setRootEntries] = useState([]);
1579
+ const [rootLoading, setRootLoading] = useState(false);
1580
+ const [rootError, setRootError] = useState(null);
1581
+ useEffect(() => {
1582
+ if (!directoryLoader) return;
1583
+ let cancelled = false;
1584
+ const loadRoot = async () => {
1585
+ setRootLoading(true);
1586
+ setRootError(null);
1587
+ try {
1588
+ const entries = await directoryLoader("");
1589
+ if (!cancelled) {
1590
+ setRootEntries(entries);
1591
+ }
1592
+ } catch (err) {
1593
+ if (!cancelled) {
1594
+ setRootError(err instanceof Error ? err.message : "Failed to load files");
1595
+ }
1596
+ } finally {
1597
+ if (!cancelled) {
1598
+ setRootLoading(false);
1599
+ }
1600
+ }
1601
+ };
1602
+ void loadRoot();
1603
+ return () => {
1604
+ cancelled = true;
1605
+ };
1606
+ }, [directoryLoader, reloadToken]);
1607
+ return /* @__PURE__ */ jsxs("div", { className: "min-w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
1608
+ /* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: title }),
1609
+ pinnedPaths && pinnedPaths.size > 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 border-b flex flex-wrap gap-1", children: Array.from(pinnedPaths).map(([p, isDir]) => /* @__PURE__ */ jsxs(
1610
+ "button",
1611
+ {
1612
+ onClick: () => isDir ? onSelectDirectory?.(p) : onSelectFile(p),
1613
+ className: `inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs hover:bg-muted/50 ${(activePath ?? activeFile ?? "") === p ? "bg-primary/10 text-primary" : "text-muted-foreground"}`,
1614
+ children: [
1615
+ isDir ? /* @__PURE__ */ jsx(Folder, { className: "h-2.5 w-2.5 shrink-0" }) : /* @__PURE__ */ jsx(Pin, { className: "h-2.5 w-2.5 shrink-0" }),
1616
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[120px]", children: p.split("/").pop() }),
1617
+ onTogglePin && /* @__PURE__ */ jsx(
1618
+ "span",
1619
+ {
1620
+ onClick: (e) => {
1621
+ e.stopPropagation();
1622
+ onTogglePin(p, isDir);
1623
+ },
1624
+ className: "hover:text-destructive",
1625
+ children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
1626
+ }
1627
+ )
1628
+ ]
1629
+ },
1630
+ p
1631
+ )) }),
1632
+ /* @__PURE__ */ jsx("div", { className: "p-1", children: directoryLoader ? /* @__PURE__ */ jsxs(Fragment, { children: [
1633
+ rootLoading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2 py-1 text-xs text-muted-foreground", children: [
1634
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
1635
+ /* @__PURE__ */ jsx("span", { children: "Loading..." })
1636
+ ] }),
1637
+ rootError && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs text-destructive", children: rootError }),
1638
+ rootEntries.map((entry) => /* @__PURE__ */ jsx(
1639
+ LazyTreeNode,
1640
+ {
1641
+ entry,
1642
+ activePath: selectedPath,
1643
+ onSelectFile,
1644
+ onSelectDirectory,
1645
+ onOpenInEditor,
1646
+ openInEditorMode,
1647
+ openInEditorIcon,
1648
+ openInEditorTitle,
1649
+ pinnedPaths,
1650
+ onTogglePin,
1651
+ directoryLoader,
1652
+ pageSize,
1653
+ reloadToken
1654
+ },
1655
+ entry.path
1656
+ ))
1657
+ ] }) : /* @__PURE__ */ jsx(
1097
1658
  TreeNodeComponent,
1098
1659
  {
1099
1660
  node: tree,
1100
- activeFile,
1661
+ activePath: selectedPath,
1101
1662
  onSelect: onSelectFile,
1102
- onReplaceFile
1663
+ onSelectDirectory,
1664
+ onReplaceFile,
1665
+ onOpenInEditor,
1666
+ openInEditorMode,
1667
+ openInEditorIcon,
1668
+ openInEditorTitle,
1669
+ pinnedPaths,
1670
+ onTogglePin,
1671
+ pageSize
1103
1672
  }
1104
1673
  ) })
1105
1674
  ] });
@@ -1408,6 +1977,7 @@ function EditModal({
1408
1977
  renderError,
1409
1978
  previewError,
1410
1979
  previewLoading,
1980
+ initialTreePath,
1411
1981
  initialState = {},
1412
1982
  hideFileTree = false,
1413
1983
  ...sessionOptions
@@ -1422,17 +1992,27 @@ function EditModal({
1422
1992
  const [pillContainer, setPillContainer] = useState(null);
1423
1993
  const [showConfirm, setShowConfirm] = useState(false);
1424
1994
  const [isSaving, setIsSaving] = useState(false);
1995
+ const [saveStatus, setSaveStatus] = useState("saved");
1996
+ const [lastSavedSnapshot, setLastSavedSnapshot] = useState("");
1425
1997
  const [saveError, setSaveError] = useState(null);
1426
1998
  const [pendingClose, setPendingClose] = useState(null);
1999
+ const [treePath, setTreePath] = useState(initialTreePath ?? "");
2000
+ const wasOpenRef = useRef(false);
1427
2001
  const currentCodeRef = useRef("");
1428
2002
  const session = useEditSession(sessionOptions);
1429
2003
  const code = getActiveContent(session);
2004
+ const effectiveTreePath = treePath || session.activeFile;
1430
2005
  currentCodeRef.current = code;
1431
2006
  const files = useMemo(() => getFiles(session.project), [session.project]);
2007
+ const projectSnapshot = useMemo(
2008
+ () => Array.from(session.project.files.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([path, file]) => `${path}\0${file.content}`).join(""),
2009
+ [session.project]
2010
+ );
1432
2011
  const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
1433
2012
  const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
1434
2013
  const isCompilableFile = isCompilable(session.activeFile);
1435
- const showPreviewToggle = isCompilableFile;
2014
+ const isMarkdown = isMarkdownFile(session.activeFile);
2015
+ const showPreviewToggle = isCompilableFile || isMarkdown;
1436
2016
  const handleBobbinChanges = useCallback((changes) => {
1437
2017
  setBobbinChanges(changes);
1438
2018
  }, []);
@@ -1454,6 +2034,26 @@ ${bobbinYaml}
1454
2034
  setBobbinChanges([]);
1455
2035
  };
1456
2036
  const hasSaveHandler = onSave || onSaveProject;
2037
+ useEffect(() => {
2038
+ if (isOpen && !wasOpenRef.current) {
2039
+ setLastSavedSnapshot(projectSnapshot);
2040
+ setSaveStatus("saved");
2041
+ setSaveError(null);
2042
+ }
2043
+ wasOpenRef.current = isOpen;
2044
+ }, [isOpen, projectSnapshot]);
2045
+ useEffect(() => {
2046
+ if (!hasSaveHandler) return;
2047
+ if (projectSnapshot === lastSavedSnapshot) {
2048
+ if (saveStatus !== "saving" && saveStatus !== "saved") {
2049
+ setSaveStatus("saved");
2050
+ }
2051
+ return;
2052
+ }
2053
+ if (saveStatus === "saved" || saveStatus === "error") {
2054
+ setSaveStatus("unsaved");
2055
+ }
2056
+ }, [projectSnapshot, lastSavedSnapshot, saveStatus, hasSaveHandler]);
1457
2057
  const handleClose = useCallback(() => {
1458
2058
  const editCount = session.history.length;
1459
2059
  const finalCode = code;
@@ -1470,6 +2070,7 @@ ${bobbinYaml}
1470
2070
  const handleSaveAndClose = useCallback(async () => {
1471
2071
  if (!pendingClose || !hasSaveHandler) return;
1472
2072
  setIsSaving(true);
2073
+ setSaveStatus("saving");
1473
2074
  setSaveError(null);
1474
2075
  try {
1475
2076
  if (onSaveProject) {
@@ -1477,6 +2078,8 @@ ${bobbinYaml}
1477
2078
  } else if (onSave) {
1478
2079
  await onSave(pendingClose.code);
1479
2080
  }
2081
+ setLastSavedSnapshot(projectSnapshot);
2082
+ setSaveStatus("saved");
1480
2083
  setShowConfirm(false);
1481
2084
  setEditInput("");
1482
2085
  session.clearError();
@@ -1484,10 +2087,11 @@ ${bobbinYaml}
1484
2087
  setPendingClose(null);
1485
2088
  } catch (e) {
1486
2089
  setSaveError(e instanceof Error ? e.message : "Save failed");
2090
+ setSaveStatus("error");
1487
2091
  } finally {
1488
2092
  setIsSaving(false);
1489
2093
  }
1490
- }, [pendingClose, onSave, onSaveProject, session, onClose]);
2094
+ }, [pendingClose, onSave, onSaveProject, session, onClose, projectSnapshot, hasSaveHandler]);
1491
2095
  const handleDiscard = useCallback(() => {
1492
2096
  if (!pendingClose) return;
1493
2097
  setShowConfirm(false);
@@ -1504,6 +2108,7 @@ ${bobbinYaml}
1504
2108
  const handleDirectSave = useCallback(async () => {
1505
2109
  if (!hasSaveHandler) return;
1506
2110
  setIsSaving(true);
2111
+ setSaveStatus("saving");
1507
2112
  setSaveError(null);
1508
2113
  try {
1509
2114
  if (onSaveProject) {
@@ -1511,12 +2116,15 @@ ${bobbinYaml}
1511
2116
  } else if (onSave && currentCodeRef.current) {
1512
2117
  await onSave(currentCodeRef.current);
1513
2118
  }
2119
+ setLastSavedSnapshot(projectSnapshot);
2120
+ setSaveStatus("saved");
1514
2121
  } catch (e) {
1515
2122
  setSaveError(e instanceof Error ? e.message : "Save failed");
2123
+ setSaveStatus("error");
1516
2124
  } finally {
1517
2125
  setIsSaving(false);
1518
2126
  }
1519
- }, [onSave, onSaveProject, session.project]);
2127
+ }, [onSave, onSaveProject, session.project, hasSaveHandler, projectSnapshot]);
1520
2128
  if (!isOpen) return null;
1521
2129
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1522
2130
  /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-8", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col bg-background rounded-lg shadow-xl w-full h-full max-w-6xl max-h-[90vh] overflow-hidden", children: [
@@ -1545,30 +2153,26 @@ ${bobbinYaml}
1545
2153
  children: showTree ? /* @__PURE__ */ jsx(FileCode, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FolderTree, { className: "h-3 w-3" })
1546
2154
  }
1547
2155
  ),
2156
+ hasSaveHandler && /* @__PURE__ */ jsx(
2157
+ SaveStatusButton,
2158
+ {
2159
+ status: saveStatus,
2160
+ onClick: handleDirectSave,
2161
+ disabled: isSaving,
2162
+ tone: "primary"
2163
+ }
2164
+ ),
1548
2165
  showPreviewToggle && /* @__PURE__ */ jsxs(
1549
2166
  "button",
1550
2167
  {
1551
2168
  onClick: () => setShowPreview(!showPreview),
1552
- className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
2169
+ className: `w-[5rem] px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
1553
2170
  children: [
1554
2171
  showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
1555
2172
  showPreview ? "Preview" : "Code"
1556
2173
  ]
1557
2174
  }
1558
2175
  ),
1559
- hasSaveHandler && /* @__PURE__ */ jsxs(
1560
- "button",
1561
- {
1562
- onClick: handleDirectSave,
1563
- disabled: isSaving,
1564
- className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary disabled:opacity-50",
1565
- title: "Save changes",
1566
- children: [
1567
- isSaving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3 w-3" }),
1568
- "Save"
1569
- ]
1570
- }
1571
- ),
1572
2176
  /* @__PURE__ */ jsxs(
1573
2177
  "button",
1574
2178
  {
@@ -1589,7 +2193,12 @@ ${bobbinYaml}
1589
2193
  {
1590
2194
  files,
1591
2195
  activeFile: session.activeFile,
1592
- onSelectFile: session.setActiveFile,
2196
+ activePath: effectiveTreePath,
2197
+ onSelectFile: (path) => {
2198
+ setTreePath(path);
2199
+ session.setActiveFile(path);
2200
+ },
2201
+ onSelectDirectory: (path) => setTreePath(path),
1593
2202
  onReplaceFile: session.replaceFile
1594
2203
  }
1595
2204
  ),
@@ -1620,7 +2229,14 @@ ${bobbinYaml}
1620
2229
  editable: true,
1621
2230
  onChange: session.updateActiveFile
1622
2231
  }
1623
- ) : fileType.category === "text" ? /* @__PURE__ */ jsx(
2232
+ ) : isMarkdown && showPreview ? /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none h-full overflow-auto", children: /* @__PURE__ */ jsx(
2233
+ MarkdownPreview,
2234
+ {
2235
+ value: code,
2236
+ editable: true,
2237
+ onChange: session.updateActiveFile
2238
+ }
2239
+ ) }) : fileType.category === "text" ? /* @__PURE__ */ jsx(
1624
2240
  CodeBlockView,
1625
2241
  {
1626
2242
  content: code,
@@ -1635,7 +2251,15 @@ ${bobbinYaml}
1635
2251
  mimeType: getMimeType(session.activeFile),
1636
2252
  fileName: session.activeFile.split("/").pop() ?? session.activeFile
1637
2253
  }
1638
- ) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-muted-foreground", children: /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Preview not available for this file type" }) }) })
2254
+ ) : /* @__PURE__ */ jsx(
2255
+ CodeBlockView,
2256
+ {
2257
+ content: code,
2258
+ language: fileType.language,
2259
+ editable: true,
2260
+ onChange: session.updateActiveFile
2261
+ }
2262
+ ) })
1639
2263
  ] }),
1640
2264
  /* @__PURE__ */ jsx(
1641
2265
  EditHistory,
@@ -1701,6 +2325,77 @@ ${bobbinYaml}
1701
2325
  )
1702
2326
  ] });
1703
2327
  }
2328
+ function createManifest(services) {
2329
+ return {
2330
+ name: "preview",
2331
+ version: "1.0.0",
2332
+ platform: "browser",
2333
+ image: "@aprovan/patchwork-image-shadcn",
2334
+ services
2335
+ };
2336
+ }
2337
+ function WidgetPreview({
2338
+ code,
2339
+ compiler,
2340
+ services,
2341
+ enabled = true
2342
+ }) {
2343
+ const [loading, setLoading] = useState(false);
2344
+ const [error, setError] = useState(null);
2345
+ const containerRef = useRef(null);
2346
+ const mountedRef = useRef(null);
2347
+ useEffect(() => {
2348
+ if (!enabled || !compiler || !containerRef.current) return;
2349
+ let cancelled = false;
2350
+ const compileAndMount = async () => {
2351
+ setLoading(true);
2352
+ setError(null);
2353
+ try {
2354
+ if (mountedRef.current) {
2355
+ compiler.unmount(mountedRef.current);
2356
+ mountedRef.current = null;
2357
+ }
2358
+ const widget = await compiler.compile(code, createManifest(services), {
2359
+ typescript: true
2360
+ });
2361
+ if (cancelled || !containerRef.current) return;
2362
+ const mounted = await compiler.mount(widget, {
2363
+ target: containerRef.current,
2364
+ mode: "embedded"
2365
+ });
2366
+ mountedRef.current = mounted;
2367
+ } catch (err) {
2368
+ if (!cancelled) {
2369
+ setError(err instanceof Error ? err.message : "Failed to render preview");
2370
+ }
2371
+ } finally {
2372
+ if (!cancelled) {
2373
+ setLoading(false);
2374
+ }
2375
+ }
2376
+ };
2377
+ void compileAndMount();
2378
+ return () => {
2379
+ cancelled = true;
2380
+ if (mountedRef.current && compiler) {
2381
+ compiler.unmount(mountedRef.current);
2382
+ mountedRef.current = null;
2383
+ }
2384
+ };
2385
+ }, [code, compiler, enabled, services]);
2386
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2387
+ error && /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive flex items-center gap-2 p-3", children: [
2388
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
2389
+ /* @__PURE__ */ jsx("span", { children: error })
2390
+ ] }),
2391
+ loading && /* @__PURE__ */ jsxs("div", { className: "p-3 flex items-center gap-2 text-muted-foreground", children: [
2392
+ /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
2393
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
2394
+ ] }),
2395
+ !compiler && enabled && !loading && !error && /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }),
2396
+ /* @__PURE__ */ jsx("div", { ref: containerRef, className: "w-full" })
2397
+ ] });
2398
+ }
1704
2399
  var VFS_BASE_URL = "/vfs";
1705
2400
  var vfsConfigCache = null;
1706
2401
  async function getVFSConfig() {
@@ -1764,7 +2459,7 @@ async function isVFSAvailable() {
1764
2459
  return false;
1765
2460
  }
1766
2461
  }
1767
- function createManifest(services) {
2462
+ function createManifest2(services) {
1768
2463
  return {
1769
2464
  name: "preview",
1770
2465
  version: "1.0.0",
@@ -1773,63 +2468,19 @@ function createManifest(services) {
1773
2468
  services
1774
2469
  };
1775
2470
  }
1776
- function useCodeCompiler(compiler, code, enabled, services) {
1777
- const [loading, setLoading] = useState(false);
1778
- const [error, setError] = useState(null);
1779
- const containerRef = useRef(null);
1780
- const mountedRef = useRef(null);
1781
- useEffect(() => {
1782
- if (!enabled || !compiler || !containerRef.current) return;
1783
- let cancelled = false;
1784
- async function compileAndMount() {
1785
- if (!containerRef.current || !compiler) return;
1786
- setLoading(true);
1787
- setError(null);
1788
- try {
1789
- if (mountedRef.current) {
1790
- compiler.unmount(mountedRef.current);
1791
- mountedRef.current = null;
1792
- }
1793
- const widget = await compiler.compile(
1794
- code,
1795
- createManifest(services),
1796
- { typescript: true }
1797
- );
1798
- if (cancelled) {
1799
- return;
1800
- }
1801
- const mounted = await compiler.mount(widget, {
1802
- target: containerRef.current,
1803
- mode: "embedded"
1804
- });
1805
- mountedRef.current = mounted;
1806
- } catch (err) {
1807
- if (!cancelled) {
1808
- setError(err instanceof Error ? err.message : "Failed to render JSX");
1809
- }
1810
- } finally {
1811
- if (!cancelled) {
1812
- setLoading(false);
1813
- }
1814
- }
1815
- }
1816
- compileAndMount();
1817
- return () => {
1818
- cancelled = true;
1819
- if (mountedRef.current && compiler) {
1820
- compiler.unmount(mountedRef.current);
1821
- mountedRef.current = null;
1822
- }
1823
- };
1824
- }, [code, compiler, enabled, services]);
1825
- return { containerRef, loading, error };
1826
- }
1827
- function CodePreview({ code: originalCode, compiler, services, filePath, entrypoint = "index.ts" }) {
2471
+ function CodePreview({
2472
+ code: originalCode,
2473
+ compiler,
2474
+ services,
2475
+ filePath,
2476
+ entrypoint = "index.ts",
2477
+ onOpenEditSession
2478
+ }) {
1828
2479
  const [isEditing, setIsEditing] = useState(false);
1829
2480
  const [showPreview, setShowPreview] = useState(true);
1830
2481
  const [currentCode, setCurrentCode] = useState(originalCode);
1831
2482
  const [editCount, setEditCount] = useState(0);
1832
- const [saveStatus, setSaveStatus] = useState("unsaved");
2483
+ const [saveStatus, setSaveStatus] = useState("saved");
1833
2484
  const [lastSavedCode, setLastSavedCode] = useState(originalCode);
1834
2485
  const [vfsPath, setVfsPath] = useState(null);
1835
2486
  const currentCodeRef = useRef(currentCode);
@@ -1865,6 +2516,14 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1865
2516
  useEffect(() => {
1866
2517
  isEditingRef.current = isEditing;
1867
2518
  }, [isEditing]);
2519
+ useEffect(() => {
2520
+ if (saveStatus === "saving") return;
2521
+ if (currentCode === lastSavedCode) {
2522
+ if (saveStatus !== "saved") setSaveStatus("saved");
2523
+ return;
2524
+ }
2525
+ if (saveStatus === "saved") setSaveStatus("unsaved");
2526
+ }, [currentCode, lastSavedCode, saveStatus]);
1868
2527
  useEffect(() => {
1869
2528
  let active = true;
1870
2529
  void (async () => {
@@ -1917,14 +2576,12 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1917
2576
  setSaveStatus("error");
1918
2577
  }
1919
2578
  }, [currentCode, getProjectId, getEntryFile]);
1920
- const { containerRef, loading, error } = useCodeCompiler(
1921
- compiler,
1922
- currentCode,
1923
- showPreview && !isEditing,
1924
- services
1925
- );
2579
+ const previewPath = filePath ?? entrypoint;
2580
+ const fileType = useMemo(() => getFileType(previewPath), [previewPath]);
2581
+ const canRenderWidget = fileType.category === "compilable";
1926
2582
  const compile = useCallback(
1927
2583
  async (code) => {
2584
+ if (!canRenderWidget) return { success: true };
1928
2585
  if (!compiler) return { success: true };
1929
2586
  const errors = [];
1930
2587
  const originalError = console.error;
@@ -1935,7 +2592,7 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1935
2592
  try {
1936
2593
  await compiler.compile(
1937
2594
  code,
1938
- createManifest(services),
2595
+ createManifest2(services),
1939
2596
  { typescript: true }
1940
2597
  );
1941
2598
  return { success: true };
@@ -1953,15 +2610,64 @@ ${errors.join("\n")}` : "";
1953
2610
  console.error = originalError;
1954
2611
  }
1955
2612
  },
1956
- [compiler, services]
2613
+ [canRenderWidget, compiler, services]
1957
2614
  );
1958
2615
  const handleRevert = () => {
1959
2616
  setCurrentCode(originalCode);
1960
2617
  setEditCount(0);
1961
2618
  };
1962
2619
  const hasChanges = currentCode !== originalCode;
2620
+ const previewBody = useMemo(() => {
2621
+ if (canRenderWidget) {
2622
+ return /* @__PURE__ */ jsx(
2623
+ WidgetPreview,
2624
+ {
2625
+ code: currentCode,
2626
+ compiler,
2627
+ services,
2628
+ enabled: showPreview && !isEditing
2629
+ }
2630
+ );
2631
+ }
2632
+ if (fileType.category === "media") {
2633
+ return /* @__PURE__ */ jsx(
2634
+ MediaPreview,
2635
+ {
2636
+ content: currentCode,
2637
+ mimeType: fileType.mimeType,
2638
+ fileName: previewPath
2639
+ }
2640
+ );
2641
+ }
2642
+ if (fileType.language === "markdown") {
2643
+ return /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none", children: /* @__PURE__ */ jsx(MarkdownPreview, { value: currentCode }) });
2644
+ }
2645
+ return /* @__PURE__ */ jsx(
2646
+ CodeBlockView,
2647
+ {
2648
+ content: currentCode,
2649
+ language: fileType.language
2650
+ }
2651
+ );
2652
+ }, [canRenderWidget, compiler, currentCode, fileType, isEditing, previewPath, services, showPreview]);
2653
+ const handleOpenEditor = useCallback(async () => {
2654
+ if (!onOpenEditSession) {
2655
+ setIsEditing(true);
2656
+ return;
2657
+ }
2658
+ const projectId = await getProjectId();
2659
+ const entryFile = getEntryFile();
2660
+ const initialProject = createSingleFileProject(currentCode, entryFile, projectId);
2661
+ onOpenEditSession({
2662
+ projectId,
2663
+ entryFile,
2664
+ filePath,
2665
+ initialCode: currentCode,
2666
+ initialProject
2667
+ });
2668
+ }, [onOpenEditSession, getProjectId, getEntryFile, currentCode, filePath]);
1963
2669
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1964
- /* @__PURE__ */ jsxs("div", { className: "my-3 border rounded-lg", children: [
2670
+ /* @__PURE__ */ jsxs("div", { className: "border rounded-lg overflow-hidden min-w-0", children: [
1965
2671
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b rounded-t-lg", children: [
1966
2672
  /* @__PURE__ */ jsx(Code, { className: "h-4 w-4 text-muted-foreground" }),
1967
2673
  editCount > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [
@@ -1970,19 +2676,6 @@ ${errors.join("\n")}` : "";
1970
2676
  " edit",
1971
2677
  editCount !== 1 ? "s" : ""
1972
2678
  ] }),
1973
- /* @__PURE__ */ jsx(
1974
- "button",
1975
- {
1976
- onClick: handleSave,
1977
- disabled: saveStatus === "saving",
1978
- className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${saveStatus === "saved" ? "text-green-600" : saveStatus === "error" ? "text-destructive hover:bg-muted" : "text-muted-foreground hover:bg-muted"}`,
1979
- title: saveStatus === "saved" ? "Saved to disk" : saveStatus === "saving" ? "Saving..." : saveStatus === "error" ? "Save failed - click to retry" : "Click to save",
1980
- children: saveStatus === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs("span", { className: "relative", children: [
1981
- /* @__PURE__ */ jsx(Cloud, { className: "h-3 w-3" }),
1982
- saveStatus === "saved" && /* @__PURE__ */ jsx(Check, { className: "h-2 w-2 absolute -bottom-0.5 -right-0.5 stroke-[3]" })
1983
- ] })
1984
- }
1985
- ),
1986
2679
  /* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-1", children: [
1987
2680
  hasChanges && /* @__PURE__ */ jsx(
1988
2681
  "button",
@@ -1996,17 +2689,26 @@ ${errors.join("\n")}` : "";
1996
2689
  /* @__PURE__ */ jsx(
1997
2690
  "button",
1998
2691
  {
1999
- onClick: () => setIsEditing(true),
2692
+ onClick: () => void handleOpenEditor(),
2000
2693
  className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-muted",
2001
2694
  title: "Edit component",
2002
2695
  children: /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3" })
2003
2696
  }
2004
2697
  ),
2698
+ /* @__PURE__ */ jsx(
2699
+ SaveStatusButton,
2700
+ {
2701
+ status: saveStatus,
2702
+ onClick: handleSave,
2703
+ disabled: saveStatus === "saving",
2704
+ tone: "muted"
2705
+ }
2706
+ ),
2005
2707
  /* @__PURE__ */ jsxs(
2006
2708
  "button",
2007
2709
  {
2008
2710
  onClick: () => setShowPreview(!showPreview),
2009
- className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
2711
+ className: `w-[5rem] px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
2010
2712
  children: [
2011
2713
  showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
2012
2714
  showPreview ? "Preview" : "Code"
@@ -2015,16 +2717,13 @@ ${errors.join("\n")}` : "";
2015
2717
  )
2016
2718
  ] })
2017
2719
  ] }),
2018
- showPreview ? /* @__PURE__ */ jsxs("div", { className: "bg-white", children: [
2019
- error ? /* @__PURE__ */ jsxs("div", { className: "p-3 text-sm text-destructive flex items-center gap-2", children: [
2020
- /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
2021
- /* @__PURE__ */ jsx("span", { children: error })
2022
- ] }) : loading ? /* @__PURE__ */ jsxs("div", { className: "p-3 flex items-center gap-2 text-muted-foreground", children: [
2023
- /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
2024
- /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
2025
- ] }) : !compiler ? /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }) : null,
2026
- /* @__PURE__ */ jsx("div", { ref: containerRef })
2027
- ] }) : /* @__PURE__ */ jsx("div", { className: "p-3 bg-muted/30 overflow-auto max-h-96", children: /* @__PURE__ */ jsx("pre", { className: "text-xs whitespace-pre-wrap break-words m-0", children: /* @__PURE__ */ jsx("code", { children: currentCode }) }) })
2720
+ showPreview ? /* @__PURE__ */ jsx("div", { className: "bg-white overflow-y-auto overflow-x-hidden max-h-[60vh]", children: previewBody }) : /* @__PURE__ */ jsx("div", { className: "bg-muted/30 overflow-auto max-h-[60vh]", children: /* @__PURE__ */ jsx(
2721
+ CodeBlockView,
2722
+ {
2723
+ content: currentCode,
2724
+ language: fileType.language
2725
+ }
2726
+ ) })
2028
2727
  ] }),
2029
2728
  /* @__PURE__ */ jsx(
2030
2729
  EditModal,
@@ -2042,6 +2741,7 @@ ${errors.join("\n")}` : "";
2042
2741
  const entryFile = getEntryFile();
2043
2742
  const project = createSingleFileProject(finalCode, entryFile, projectId);
2044
2743
  await saveProject(project);
2744
+ setLastSavedCode(finalCode);
2045
2745
  setSaveStatus("saved");
2046
2746
  } catch (err) {
2047
2747
  console.warn("[VFS] Failed to save project:", err);
@@ -2052,49 +2752,77 @@ ${errors.join("\n")}` : "";
2052
2752
  },
2053
2753
  originalCode: currentCode,
2054
2754
  compile,
2055
- renderPreview: (code) => /* @__PURE__ */ jsx(ModalPreview, { code, compiler, services })
2755
+ renderPreview: (code) => /* @__PURE__ */ jsx(
2756
+ WidgetPreview,
2757
+ {
2758
+ code,
2759
+ compiler,
2760
+ services
2761
+ }
2762
+ )
2056
2763
  }
2057
2764
  )
2058
2765
  ] });
2059
2766
  }
2060
- function ModalPreview({
2061
- code,
2062
- compiler,
2063
- services
2767
+ function DefaultBadge({
2768
+ children,
2769
+ className = ""
2064
2770
  }) {
2065
- const { containerRef, loading, error } = useCodeCompiler(compiler, code, true, services);
2066
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2067
- error && /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive flex items-center gap-2", children: [
2068
- /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
2069
- /* @__PURE__ */ jsx("span", { children: error })
2070
- ] }),
2071
- loading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
2072
- /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
2073
- /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
2074
- ] }),
2075
- !compiler && !loading && !error && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Compiler not initialized" }),
2076
- /* @__PURE__ */ jsx("div", { ref: containerRef })
2077
- ] });
2078
- }
2079
- function DefaultBadge({ children, className = "" }) {
2080
- return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`, children });
2771
+ return /* @__PURE__ */ jsx(
2772
+ "span",
2773
+ {
2774
+ className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`,
2775
+ children
2776
+ }
2777
+ );
2081
2778
  }
2082
- function DefaultDialog({ children, open, onOpenChange }) {
2779
+ function DefaultDialog({
2780
+ children,
2781
+ open,
2782
+ onOpenChange
2783
+ }) {
2083
2784
  if (!open) return null;
2084
- return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 bg-black/50", onClick: () => onOpenChange?.(false), children: /* @__PURE__ */ jsx("div", { className: "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg", onClick: (e) => e.stopPropagation(), children }) });
2785
+ return /* @__PURE__ */ jsx(
2786
+ "div",
2787
+ {
2788
+ className: "fixed inset-0 z-50 bg-black/50",
2789
+ onClick: () => onOpenChange?.(false),
2790
+ children: /* @__PURE__ */ jsx(
2791
+ "div",
2792
+ {
2793
+ className: "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg",
2794
+ onClick: (e) => e.stopPropagation(),
2795
+ children
2796
+ }
2797
+ )
2798
+ }
2799
+ );
2085
2800
  }
2086
2801
  function ServicesInspector({
2087
2802
  namespaces,
2088
2803
  services = [],
2089
2804
  BadgeComponent = DefaultBadge,
2090
- DialogComponent = DefaultDialog
2805
+ DialogComponent = DefaultDialog,
2806
+ DialogHeaderComponent = ({ children }) => /* @__PURE__ */ jsx("div", { className: "flex justify-between items-center mb-4", children }),
2807
+ DialogContentComponent = ({ children, className = "" }) => /* @__PURE__ */ jsx("div", { className, children }),
2808
+ DialogCloseComponent = ({ onClose }) => /* @__PURE__ */ jsx(
2809
+ "button",
2810
+ {
2811
+ onClick: () => onClose?.(),
2812
+ className: "text-muted-foreground hover:text-foreground",
2813
+ children: "\xD7"
2814
+ }
2815
+ )
2091
2816
  }) {
2092
2817
  const [open, setOpen] = useState(false);
2093
2818
  if (namespaces.length === 0) return null;
2094
- const groupedServices = services.reduce((acc, svc) => {
2095
- (acc[svc.namespace] ??= []).push(svc);
2096
- return acc;
2097
- }, {});
2819
+ const groupedServices = services.reduce(
2820
+ (acc, svc) => {
2821
+ (acc[svc.namespace] ??= []).push(svc);
2822
+ return acc;
2823
+ },
2824
+ {}
2825
+ );
2098
2826
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2099
2827
  /* @__PURE__ */ jsxs(
2100
2828
  "button",
@@ -2112,11 +2840,11 @@ function ServicesInspector({
2112
2840
  }
2113
2841
  ),
2114
2842
  /* @__PURE__ */ jsxs(DialogComponent, { open, onOpenChange: setOpen, children: [
2115
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-4", children: [
2843
+ /* @__PURE__ */ jsxs(DialogHeaderComponent, { children: [
2116
2844
  /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Available Services" }),
2117
- /* @__PURE__ */ jsx("button", { onClick: () => setOpen(false), className: "text-muted-foreground hover:text-foreground", children: "\xD7" })
2845
+ /* @__PURE__ */ jsx(DialogCloseComponent, { onClose: () => setOpen(false) })
2118
2846
  ] }),
2119
- /* @__PURE__ */ jsx("div", { className: "space-y-3 max-h-96 overflow-auto", children: namespaces.map((ns) => /* @__PURE__ */ jsxs("details", { open: namespaces.length === 1, children: [
2847
+ /* @__PURE__ */ jsx(DialogContentComponent, { className: "space-y-3 max-h-96 overflow-auto", children: namespaces.map((ns) => /* @__PURE__ */ jsxs("details", { open: namespaces.length === 1, children: [
2120
2848
  /* @__PURE__ */ jsxs("summary", { className: "flex items-center gap-2 w-full p-2 rounded bg-muted/50 hover:bg-muted transition-colors cursor-pointer", children: [
2121
2849
  /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }),
2122
2850
  /* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: ns }),
@@ -2128,11 +2856,11 @@ function ServicesInspector({
2128
2856
  ] }),
2129
2857
  /* @__PURE__ */ jsx("div", { className: "ml-6 mt-2 space-y-2", children: groupedServices[ns]?.map((svc) => /* @__PURE__ */ jsxs("details", { children: [
2130
2858
  /* @__PURE__ */ jsxs("summary", { className: "flex items-center gap-2 w-full text-left text-sm hover:text-foreground text-muted-foreground transition-colors cursor-pointer", children: [
2131
- /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
2859
+ svc.parameters && /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
2132
2860
  /* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: svc.procedure }),
2133
2861
  /* @__PURE__ */ jsx("span", { className: "truncate text-xs opacity-70", children: svc.description })
2134
2862
  ] }),
2135
- /* @__PURE__ */ jsx("div", { className: "ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48", children: /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0", children: JSON.stringify(svc.parameters.jsonSchema, null, 2) }) })
2863
+ svc.parameters && /* @__PURE__ */ jsx("div", { className: "ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48", children: /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0", children: JSON.stringify(svc.parameters, null, 2) }) })
2136
2864
  ] }, svc.name)) ?? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No tool details available" }) })
2137
2865
  ] }, ns)) })
2138
2866
  ] })
@@ -2264,4 +2992,4 @@ function cn(...inputs) {
2264
2992
  return twMerge(clsx(inputs));
2265
2993
  }
2266
2994
 
2267
- export { CodeBlockExtension, CodeBlockView, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, MediaPreview, SaveConfirmDialog, ServicesInspector, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFileType, getFiles, getLanguageFromExt, getMimeType, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isCompilable, isImageFile, isMediaFile, isTextFile, isVFSAvailable, isVideoFile, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession };
2995
+ export { CodeBlockExtension, CodeBlockView, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, MarkdownPreview, MediaPreview, SaveConfirmDialog, ServicesInspector, WidgetPreview, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFileType, getFiles, getLanguageFromExt, getMimeType, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isCompilable, isImageFile, isMarkdownFile, isMediaFile, isPreviewable, isTextFile, isVFSAvailable, isVideoFile, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession };