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

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,11 +860,181 @@ 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"];
857
- var MEDIA_EXTENSIONS = [".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp4", ".mov", ".webm"];
858
- var TEXT_EXTENSIONS = [".json", ".yaml", ".yml", ".md", ".txt", ".css", ".html", ".xml", ".toml"];
1016
+ var MEDIA_EXTENSIONS = [
1017
+ ".svg",
1018
+ ".png",
1019
+ ".jpg",
1020
+ ".jpeg",
1021
+ ".gif",
1022
+ ".webp",
1023
+ ".mp4",
1024
+ ".mov",
1025
+ ".webm"
1026
+ ];
1027
+ var TEXT_EXTENSIONS = [
1028
+ ".json",
1029
+ ".yaml",
1030
+ ".yml",
1031
+ ".md",
1032
+ ".txt",
1033
+ ".css",
1034
+ ".html",
1035
+ ".xml",
1036
+ ".toml"
1037
+ ];
859
1038
  var EXTENSION_TO_LANGUAGE = {
860
1039
  ".tsx": "tsx",
861
1040
  ".jsx": "jsx",
@@ -939,6 +1118,12 @@ function isMediaFile(path) {
939
1118
  function isTextFile(path) {
940
1119
  return TEXT_EXTENSIONS.includes(getExtension(path));
941
1120
  }
1121
+ function isMarkdownFile(path) {
1122
+ return getExtension(path) === ".md";
1123
+ }
1124
+ function isPreviewable(path) {
1125
+ return isCompilable(path) || isMarkdownFile(path);
1126
+ }
942
1127
  function getLanguageFromExt(path) {
943
1128
  const ext = getExtension(path);
944
1129
  return EXTENSION_TO_LANGUAGE[ext] ?? null;
@@ -955,6 +1140,17 @@ function isVideoFile(path) {
955
1140
  const ext = getExtension(path);
956
1141
  return [".mp4", ".mov", ".webm"].includes(ext);
957
1142
  }
1143
+ function sortNodes(nodes) {
1144
+ nodes.sort((a, b) => {
1145
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1146
+ return a.name.localeCompare(b.name);
1147
+ });
1148
+ for (const node of nodes) {
1149
+ if (node.children.length > 0) {
1150
+ sortNodes(node.children);
1151
+ }
1152
+ }
1153
+ }
958
1154
  function buildTree(files) {
959
1155
  const root = { name: "", path: "", isDir: true, children: [] };
960
1156
  for (const file of files) {
@@ -977,14 +1173,26 @@ function buildTree(files) {
977
1173
  current = child;
978
1174
  }
979
1175
  }
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
- });
1176
+ sortNodes(root.children);
984
1177
  return root;
985
1178
  }
986
- function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth = 0 }) {
987
- const [expanded, setExpanded] = useState(true);
1179
+ function TreeNodeComponent({
1180
+ node,
1181
+ activePath,
1182
+ onSelect,
1183
+ onSelectDirectory,
1184
+ onReplaceFile,
1185
+ onOpenInEditor,
1186
+ openInEditorMode = "files",
1187
+ openInEditorIcon,
1188
+ openInEditorTitle = "Open in editor",
1189
+ pinnedPaths,
1190
+ onTogglePin,
1191
+ pageSize = 10,
1192
+ depth = 0
1193
+ }) {
1194
+ const [expanded, setExpanded] = useState(false);
1195
+ const [visibleCount, setVisibleCount] = useState(pageSize);
988
1196
  const [isHovered, setIsHovered] = useState(false);
989
1197
  const fileInputRef = useRef(null);
990
1198
  const handleUploadClick = useCallback((e) => {
@@ -1003,48 +1211,119 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1003
1211
  reader.readAsDataURL(file);
1004
1212
  e.target.value = "";
1005
1213
  }, [node.path, onReplaceFile]);
1214
+ const isPinned = pinnedPaths?.has(node.path) ?? false;
1215
+ const showPin = onTogglePin && isHovered;
1216
+ const handleTogglePin = useCallback((e) => {
1217
+ e.stopPropagation();
1218
+ onTogglePin?.(node.path, node.isDir);
1219
+ }, [node.path, node.isDir, onTogglePin]);
1006
1220
  if (!node.name) {
1007
1221
  return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((child) => /* @__PURE__ */ jsx(
1008
1222
  TreeNodeComponent,
1009
1223
  {
1010
1224
  node: child,
1011
- activeFile,
1225
+ activePath,
1012
1226
  onSelect,
1227
+ onSelectDirectory,
1013
1228
  onReplaceFile,
1229
+ onOpenInEditor,
1230
+ openInEditorMode,
1231
+ openInEditorIcon,
1232
+ openInEditorTitle,
1233
+ pinnedPaths,
1234
+ onTogglePin,
1235
+ pageSize,
1014
1236
  depth
1015
1237
  },
1016
1238
  child.path
1017
1239
  )) });
1018
1240
  }
1019
- const isActive = node.path === activeFile;
1241
+ const isActive = node.path === activePath;
1020
1242
  const isMedia = !node.isDir && isMediaFile(node.path);
1021
1243
  const showUpload = isMedia && isHovered && onReplaceFile;
1244
+ const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? node.isDir : !node.isDir));
1245
+ const handleOpenInEditor = useCallback(
1246
+ (e) => {
1247
+ e.stopPropagation();
1248
+ onOpenInEditor?.(node.path, node.isDir);
1249
+ },
1250
+ [node.path, node.isDir, onOpenInEditor]
1251
+ );
1022
1252
  if (node.isDir) {
1023
1253
  return /* @__PURE__ */ jsxs("div", { children: [
1024
1254
  /* @__PURE__ */ jsxs(
1025
1255
  "button",
1026
1256
  {
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",
1257
+ onClick: () => {
1258
+ onSelectDirectory?.(node.path);
1259
+ setExpanded(!expanded);
1260
+ },
1261
+ 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" : ""}`,
1262
+ onMouseEnter: () => setIsHovered(true),
1263
+ onMouseLeave: () => setIsHovered(false),
1029
1264
  style: { paddingLeft: `${depth * 12 + 8}px` },
1030
1265
  children: [
1031
1266
  expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
1032
1267
  /* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1033
- /* @__PURE__ */ jsx("span", { className: "truncate", children: node.name })
1268
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
1269
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1270
+ "span",
1271
+ {
1272
+ onClick: handleTogglePin,
1273
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1274
+ title: isPinned ? "Unpin" : "Pin",
1275
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1276
+ }
1277
+ ),
1278
+ showOpenInEditor && /* @__PURE__ */ jsx(
1279
+ "span",
1280
+ {
1281
+ onClick: handleOpenInEditor,
1282
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1283
+ title: openInEditorTitle,
1284
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1285
+ }
1286
+ )
1034
1287
  ]
1035
1288
  }
1036
1289
  ),
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
- )) })
1290
+ expanded && /* @__PURE__ */ jsxs("div", { children: [
1291
+ node.children.slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
1292
+ TreeNodeComponent,
1293
+ {
1294
+ node: child,
1295
+ activePath,
1296
+ onSelect,
1297
+ onSelectDirectory,
1298
+ onReplaceFile,
1299
+ onOpenInEditor,
1300
+ openInEditorMode,
1301
+ openInEditorIcon,
1302
+ openInEditorTitle,
1303
+ pinnedPaths,
1304
+ onTogglePin,
1305
+ pageSize,
1306
+ depth: depth + 1
1307
+ },
1308
+ child.path
1309
+ )),
1310
+ node.children.length > visibleCount && /* @__PURE__ */ jsxs(
1311
+ "button",
1312
+ {
1313
+ onClick: () => setVisibleCount((prev) => prev + pageSize),
1314
+ className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
1315
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1316
+ children: [
1317
+ /* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
1318
+ /* @__PURE__ */ jsxs("span", { children: [
1319
+ "Show ",
1320
+ Math.min(pageSize, node.children.length - visibleCount),
1321
+ " more"
1322
+ ] })
1323
+ ]
1324
+ }
1325
+ )
1326
+ ] })
1048
1327
  ] });
1049
1328
  }
1050
1329
  return /* @__PURE__ */ jsxs(
@@ -1062,7 +1341,25 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1062
1341
  style: { paddingLeft: `${depth * 12 + 20}px` },
1063
1342
  children: [
1064
1343
  /* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1065
- /* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: node.name }),
1344
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
1345
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1346
+ "span",
1347
+ {
1348
+ onClick: handleTogglePin,
1349
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1350
+ title: isPinned ? "Unpin" : "Pin",
1351
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1352
+ }
1353
+ ),
1354
+ showOpenInEditor && /* @__PURE__ */ jsx(
1355
+ "span",
1356
+ {
1357
+ onClick: handleOpenInEditor,
1358
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1359
+ title: openInEditorTitle,
1360
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1361
+ }
1362
+ ),
1066
1363
  showUpload && /* @__PURE__ */ jsx(
1067
1364
  "span",
1068
1365
  {
@@ -1089,17 +1386,309 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
1089
1386
  }
1090
1387
  );
1091
1388
  }
1092
- function FileTree({ files, activeFile, onSelectFile, onReplaceFile }) {
1389
+ function LazyTreeNode({
1390
+ entry,
1391
+ activePath,
1392
+ onSelectFile,
1393
+ onSelectDirectory,
1394
+ onOpenInEditor,
1395
+ openInEditorMode = "files",
1396
+ openInEditorIcon,
1397
+ openInEditorTitle = "Open in editor",
1398
+ pinnedPaths,
1399
+ onTogglePin,
1400
+ directoryLoader,
1401
+ pageSize,
1402
+ depth = 0,
1403
+ reloadToken
1404
+ }) {
1405
+ const [expanded, setExpanded] = useState(false);
1406
+ const [loading, setLoading] = useState(false);
1407
+ const [loadError, setLoadError] = useState(null);
1408
+ const [isHovered, setIsHovered] = useState(false);
1409
+ const [children, setChildren] = useState(null);
1410
+ const [visibleCount, setVisibleCount] = useState(pageSize);
1411
+ useEffect(() => {
1412
+ setChildren(null);
1413
+ setVisibleCount(pageSize);
1414
+ if (expanded) {
1415
+ setLoading(true);
1416
+ setLoadError(null);
1417
+ directoryLoader(entry.path).then((loaded) => setChildren(loaded)).catch((err) => setLoadError(err instanceof Error ? err.message : "Failed to load directory")).finally(() => setLoading(false));
1418
+ }
1419
+ }, [reloadToken]);
1420
+ const isActive = entry.path === activePath;
1421
+ const isPinned = pinnedPaths?.has(entry.path) ?? false;
1422
+ const showPin = onTogglePin && isHovered;
1423
+ const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? entry.isDir : !entry.isDir));
1424
+ const handleOpenInEditor = useCallback(
1425
+ (e) => {
1426
+ e.stopPropagation();
1427
+ onOpenInEditor?.(entry.path, entry.isDir);
1428
+ },
1429
+ [entry.path, entry.isDir, onOpenInEditor]
1430
+ );
1431
+ const handleTogglePin = useCallback(
1432
+ (e) => {
1433
+ e.stopPropagation();
1434
+ onTogglePin?.(entry.path, entry.isDir);
1435
+ },
1436
+ [entry.path, entry.isDir, onTogglePin]
1437
+ );
1438
+ const toggleDirectory = useCallback(async () => {
1439
+ if (!entry.isDir) return;
1440
+ onSelectDirectory?.(entry.path);
1441
+ if (!expanded && children === null) {
1442
+ setLoading(true);
1443
+ setLoadError(null);
1444
+ try {
1445
+ const loaded = await directoryLoader(entry.path);
1446
+ setChildren(loaded);
1447
+ } catch (err) {
1448
+ setLoadError(err instanceof Error ? err.message : "Failed to load directory");
1449
+ } finally {
1450
+ setLoading(false);
1451
+ }
1452
+ }
1453
+ setExpanded((prev) => !prev);
1454
+ }, [entry.isDir, entry.path, onSelectDirectory, expanded, children, directoryLoader]);
1455
+ if (!entry.isDir) {
1456
+ return /* @__PURE__ */ jsx(
1457
+ "div",
1458
+ {
1459
+ className: "relative",
1460
+ onMouseEnter: () => setIsHovered(true),
1461
+ onMouseLeave: () => setIsHovered(false),
1462
+ children: /* @__PURE__ */ jsxs(
1463
+ "button",
1464
+ {
1465
+ onClick: () => onSelectFile(entry.path),
1466
+ 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" : ""}`,
1467
+ style: { paddingLeft: `${depth * 12 + 20}px` },
1468
+ children: [
1469
+ /* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1470
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
1471
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1472
+ "span",
1473
+ {
1474
+ onClick: handleTogglePin,
1475
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1476
+ title: isPinned ? "Unpin" : "Pin",
1477
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1478
+ }
1479
+ ),
1480
+ showOpenInEditor && /* @__PURE__ */ jsx(
1481
+ "span",
1482
+ {
1483
+ onClick: handleOpenInEditor,
1484
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1485
+ title: openInEditorTitle,
1486
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1487
+ }
1488
+ )
1489
+ ]
1490
+ }
1491
+ )
1492
+ }
1493
+ );
1494
+ }
1495
+ return /* @__PURE__ */ jsxs("div", { children: [
1496
+ /* @__PURE__ */ jsxs(
1497
+ "button",
1498
+ {
1499
+ onClick: () => void toggleDirectory(),
1500
+ 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" : ""}`,
1501
+ onMouseEnter: () => setIsHovered(true),
1502
+ onMouseLeave: () => setIsHovered(false),
1503
+ style: { paddingLeft: `${depth * 12 + 8}px` },
1504
+ children: [
1505
+ expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
1506
+ /* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
1507
+ /* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
1508
+ loading && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-muted-foreground" }),
1509
+ (showPin || isPinned) && /* @__PURE__ */ jsx(
1510
+ "span",
1511
+ {
1512
+ onClick: handleTogglePin,
1513
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1514
+ title: isPinned ? "Unpin" : "Pin",
1515
+ children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
1516
+ }
1517
+ ),
1518
+ showOpenInEditor && /* @__PURE__ */ jsx(
1519
+ "span",
1520
+ {
1521
+ onClick: handleOpenInEditor,
1522
+ className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
1523
+ title: openInEditorTitle,
1524
+ children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
1525
+ }
1526
+ )
1527
+ ]
1528
+ }
1529
+ ),
1530
+ expanded && /* @__PURE__ */ jsxs("div", { children: [
1531
+ loadError && /* @__PURE__ */ jsx(
1532
+ "div",
1533
+ {
1534
+ className: "px-2 py-1 text-xs text-destructive",
1535
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1536
+ children: loadError
1537
+ }
1538
+ ),
1539
+ (children ?? []).slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
1540
+ LazyTreeNode,
1541
+ {
1542
+ entry: child,
1543
+ activePath,
1544
+ onSelectFile,
1545
+ onSelectDirectory,
1546
+ onOpenInEditor,
1547
+ openInEditorMode,
1548
+ openInEditorIcon,
1549
+ openInEditorTitle,
1550
+ pinnedPaths,
1551
+ onTogglePin,
1552
+ directoryLoader,
1553
+ pageSize,
1554
+ depth: depth + 1,
1555
+ reloadToken
1556
+ },
1557
+ child.path
1558
+ )),
1559
+ (children?.length ?? 0) > visibleCount && /* @__PURE__ */ jsxs(
1560
+ "button",
1561
+ {
1562
+ onClick: () => setVisibleCount((prev) => prev + pageSize),
1563
+ className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
1564
+ style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
1565
+ children: [
1566
+ /* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
1567
+ /* @__PURE__ */ jsxs("span", { children: [
1568
+ "Show ",
1569
+ Math.min(pageSize, (children?.length ?? 0) - visibleCount),
1570
+ " more"
1571
+ ] })
1572
+ ]
1573
+ }
1574
+ )
1575
+ ] })
1576
+ ] });
1577
+ }
1578
+ function FileTree({
1579
+ files = [],
1580
+ activeFile,
1581
+ activePath,
1582
+ title = "Files",
1583
+ onSelectFile,
1584
+ onSelectDirectory,
1585
+ onReplaceFile,
1586
+ onOpenInEditor,
1587
+ openInEditorMode,
1588
+ openInEditorIcon,
1589
+ openInEditorTitle,
1590
+ pinnedPaths,
1591
+ onTogglePin,
1592
+ directoryLoader,
1593
+ pageSize = 10,
1594
+ reloadToken
1595
+ }) {
1093
1596
  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(
1597
+ const selectedPath = activePath ?? activeFile ?? "";
1598
+ const [rootEntries, setRootEntries] = useState([]);
1599
+ const [rootLoading, setRootLoading] = useState(false);
1600
+ const [rootError, setRootError] = useState(null);
1601
+ useEffect(() => {
1602
+ if (!directoryLoader) return;
1603
+ let cancelled = false;
1604
+ const loadRoot = async () => {
1605
+ setRootLoading(true);
1606
+ setRootError(null);
1607
+ try {
1608
+ const entries = await directoryLoader("");
1609
+ if (!cancelled) {
1610
+ setRootEntries(entries);
1611
+ }
1612
+ } catch (err) {
1613
+ if (!cancelled) {
1614
+ setRootError(err instanceof Error ? err.message : "Failed to load files");
1615
+ }
1616
+ } finally {
1617
+ if (!cancelled) {
1618
+ setRootLoading(false);
1619
+ }
1620
+ }
1621
+ };
1622
+ void loadRoot();
1623
+ return () => {
1624
+ cancelled = true;
1625
+ };
1626
+ }, [directoryLoader, reloadToken]);
1627
+ return /* @__PURE__ */ jsxs("div", { className: "min-w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
1628
+ /* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: title }),
1629
+ 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(
1630
+ "button",
1631
+ {
1632
+ onClick: () => isDir ? onSelectDirectory?.(p) : onSelectFile(p),
1633
+ 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"}`,
1634
+ children: [
1635
+ 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" }),
1636
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[120px]", children: p.split("/").pop() }),
1637
+ onTogglePin && /* @__PURE__ */ jsx(
1638
+ "span",
1639
+ {
1640
+ onClick: (e) => {
1641
+ e.stopPropagation();
1642
+ onTogglePin(p, isDir);
1643
+ },
1644
+ className: "hover:text-destructive",
1645
+ children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
1646
+ }
1647
+ )
1648
+ ]
1649
+ },
1650
+ p
1651
+ )) }),
1652
+ /* @__PURE__ */ jsx("div", { className: "p-1", children: directoryLoader ? /* @__PURE__ */ jsxs(Fragment, { children: [
1653
+ rootLoading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2 py-1 text-xs text-muted-foreground", children: [
1654
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
1655
+ /* @__PURE__ */ jsx("span", { children: "Loading..." })
1656
+ ] }),
1657
+ rootError && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs text-destructive", children: rootError }),
1658
+ rootEntries.map((entry) => /* @__PURE__ */ jsx(
1659
+ LazyTreeNode,
1660
+ {
1661
+ entry,
1662
+ activePath: selectedPath,
1663
+ onSelectFile,
1664
+ onSelectDirectory,
1665
+ onOpenInEditor,
1666
+ openInEditorMode,
1667
+ openInEditorIcon,
1668
+ openInEditorTitle,
1669
+ pinnedPaths,
1670
+ onTogglePin,
1671
+ directoryLoader,
1672
+ pageSize,
1673
+ reloadToken
1674
+ },
1675
+ entry.path
1676
+ ))
1677
+ ] }) : /* @__PURE__ */ jsx(
1097
1678
  TreeNodeComponent,
1098
1679
  {
1099
1680
  node: tree,
1100
- activeFile,
1681
+ activePath: selectedPath,
1101
1682
  onSelect: onSelectFile,
1102
- onReplaceFile
1683
+ onSelectDirectory,
1684
+ onReplaceFile,
1685
+ onOpenInEditor,
1686
+ openInEditorMode,
1687
+ openInEditorIcon,
1688
+ openInEditorTitle,
1689
+ pinnedPaths,
1690
+ onTogglePin,
1691
+ pageSize
1103
1692
  }
1104
1693
  ) })
1105
1694
  ] });
@@ -1408,6 +1997,7 @@ function EditModal({
1408
1997
  renderError,
1409
1998
  previewError,
1410
1999
  previewLoading,
2000
+ initialTreePath,
1411
2001
  initialState = {},
1412
2002
  hideFileTree = false,
1413
2003
  ...sessionOptions
@@ -1422,17 +2012,27 @@ function EditModal({
1422
2012
  const [pillContainer, setPillContainer] = useState(null);
1423
2013
  const [showConfirm, setShowConfirm] = useState(false);
1424
2014
  const [isSaving, setIsSaving] = useState(false);
2015
+ const [saveStatus, setSaveStatus] = useState("saved");
2016
+ const [lastSavedSnapshot, setLastSavedSnapshot] = useState("");
1425
2017
  const [saveError, setSaveError] = useState(null);
1426
2018
  const [pendingClose, setPendingClose] = useState(null);
2019
+ const [treePath, setTreePath] = useState(initialTreePath ?? "");
2020
+ const wasOpenRef = useRef(false);
1427
2021
  const currentCodeRef = useRef("");
1428
2022
  const session = useEditSession(sessionOptions);
1429
2023
  const code = getActiveContent(session);
2024
+ const effectiveTreePath = treePath || session.activeFile;
1430
2025
  currentCodeRef.current = code;
1431
2026
  const files = useMemo(() => getFiles(session.project), [session.project]);
2027
+ const projectSnapshot = useMemo(
2028
+ () => Array.from(session.project.files.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([path, file]) => `${path}\0${file.content}`).join(""),
2029
+ [session.project]
2030
+ );
1432
2031
  const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
1433
2032
  const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
1434
2033
  const isCompilableFile = isCompilable(session.activeFile);
1435
- const showPreviewToggle = isCompilableFile;
2034
+ const isMarkdown = isMarkdownFile(session.activeFile);
2035
+ const showPreviewToggle = isCompilableFile || isMarkdown;
1436
2036
  const handleBobbinChanges = useCallback((changes) => {
1437
2037
  setBobbinChanges(changes);
1438
2038
  }, []);
@@ -1454,6 +2054,26 @@ ${bobbinYaml}
1454
2054
  setBobbinChanges([]);
1455
2055
  };
1456
2056
  const hasSaveHandler = onSave || onSaveProject;
2057
+ useEffect(() => {
2058
+ if (isOpen && !wasOpenRef.current) {
2059
+ setLastSavedSnapshot(projectSnapshot);
2060
+ setSaveStatus("saved");
2061
+ setSaveError(null);
2062
+ }
2063
+ wasOpenRef.current = isOpen;
2064
+ }, [isOpen, projectSnapshot]);
2065
+ useEffect(() => {
2066
+ if (!hasSaveHandler) return;
2067
+ if (projectSnapshot === lastSavedSnapshot) {
2068
+ if (saveStatus !== "saving" && saveStatus !== "saved") {
2069
+ setSaveStatus("saved");
2070
+ }
2071
+ return;
2072
+ }
2073
+ if (saveStatus === "saved" || saveStatus === "error") {
2074
+ setSaveStatus("unsaved");
2075
+ }
2076
+ }, [projectSnapshot, lastSavedSnapshot, saveStatus, hasSaveHandler]);
1457
2077
  const handleClose = useCallback(() => {
1458
2078
  const editCount = session.history.length;
1459
2079
  const finalCode = code;
@@ -1470,6 +2090,7 @@ ${bobbinYaml}
1470
2090
  const handleSaveAndClose = useCallback(async () => {
1471
2091
  if (!pendingClose || !hasSaveHandler) return;
1472
2092
  setIsSaving(true);
2093
+ setSaveStatus("saving");
1473
2094
  setSaveError(null);
1474
2095
  try {
1475
2096
  if (onSaveProject) {
@@ -1477,6 +2098,8 @@ ${bobbinYaml}
1477
2098
  } else if (onSave) {
1478
2099
  await onSave(pendingClose.code);
1479
2100
  }
2101
+ setLastSavedSnapshot(projectSnapshot);
2102
+ setSaveStatus("saved");
1480
2103
  setShowConfirm(false);
1481
2104
  setEditInput("");
1482
2105
  session.clearError();
@@ -1484,10 +2107,11 @@ ${bobbinYaml}
1484
2107
  setPendingClose(null);
1485
2108
  } catch (e) {
1486
2109
  setSaveError(e instanceof Error ? e.message : "Save failed");
2110
+ setSaveStatus("error");
1487
2111
  } finally {
1488
2112
  setIsSaving(false);
1489
2113
  }
1490
- }, [pendingClose, onSave, onSaveProject, session, onClose]);
2114
+ }, [pendingClose, onSave, onSaveProject, session, onClose, projectSnapshot, hasSaveHandler]);
1491
2115
  const handleDiscard = useCallback(() => {
1492
2116
  if (!pendingClose) return;
1493
2117
  setShowConfirm(false);
@@ -1504,6 +2128,7 @@ ${bobbinYaml}
1504
2128
  const handleDirectSave = useCallback(async () => {
1505
2129
  if (!hasSaveHandler) return;
1506
2130
  setIsSaving(true);
2131
+ setSaveStatus("saving");
1507
2132
  setSaveError(null);
1508
2133
  try {
1509
2134
  if (onSaveProject) {
@@ -1511,12 +2136,15 @@ ${bobbinYaml}
1511
2136
  } else if (onSave && currentCodeRef.current) {
1512
2137
  await onSave(currentCodeRef.current);
1513
2138
  }
2139
+ setLastSavedSnapshot(projectSnapshot);
2140
+ setSaveStatus("saved");
1514
2141
  } catch (e) {
1515
2142
  setSaveError(e instanceof Error ? e.message : "Save failed");
2143
+ setSaveStatus("error");
1516
2144
  } finally {
1517
2145
  setIsSaving(false);
1518
2146
  }
1519
- }, [onSave, onSaveProject, session.project]);
2147
+ }, [onSave, onSaveProject, session.project, hasSaveHandler, projectSnapshot]);
1520
2148
  if (!isOpen) return null;
1521
2149
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1522
2150
  /* @__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 +2173,26 @@ ${bobbinYaml}
1545
2173
  children: showTree ? /* @__PURE__ */ jsx(FileCode, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FolderTree, { className: "h-3 w-3" })
1546
2174
  }
1547
2175
  ),
2176
+ hasSaveHandler && /* @__PURE__ */ jsx(
2177
+ SaveStatusButton,
2178
+ {
2179
+ status: saveStatus,
2180
+ onClick: handleDirectSave,
2181
+ disabled: isSaving,
2182
+ tone: "primary"
2183
+ }
2184
+ ),
1548
2185
  showPreviewToggle && /* @__PURE__ */ jsxs(
1549
2186
  "button",
1550
2187
  {
1551
2188
  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"}`,
2189
+ 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
2190
  children: [
1554
2191
  showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
1555
2192
  showPreview ? "Preview" : "Code"
1556
2193
  ]
1557
2194
  }
1558
2195
  ),
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
2196
  /* @__PURE__ */ jsxs(
1573
2197
  "button",
1574
2198
  {
@@ -1589,7 +2213,12 @@ ${bobbinYaml}
1589
2213
  {
1590
2214
  files,
1591
2215
  activeFile: session.activeFile,
1592
- onSelectFile: session.setActiveFile,
2216
+ activePath: effectiveTreePath,
2217
+ onSelectFile: (path) => {
2218
+ setTreePath(path);
2219
+ session.setActiveFile(path);
2220
+ },
2221
+ onSelectDirectory: (path) => setTreePath(path),
1593
2222
  onReplaceFile: session.replaceFile
1594
2223
  }
1595
2224
  ),
@@ -1620,7 +2249,14 @@ ${bobbinYaml}
1620
2249
  editable: true,
1621
2250
  onChange: session.updateActiveFile
1622
2251
  }
1623
- ) : fileType.category === "text" ? /* @__PURE__ */ jsx(
2252
+ ) : isMarkdown && showPreview ? /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none h-full overflow-auto", children: /* @__PURE__ */ jsx(
2253
+ MarkdownPreview,
2254
+ {
2255
+ value: code,
2256
+ editable: true,
2257
+ onChange: session.updateActiveFile
2258
+ }
2259
+ ) }) : fileType.category === "text" ? /* @__PURE__ */ jsx(
1624
2260
  CodeBlockView,
1625
2261
  {
1626
2262
  content: code,
@@ -1635,7 +2271,15 @@ ${bobbinYaml}
1635
2271
  mimeType: getMimeType(session.activeFile),
1636
2272
  fileName: session.activeFile.split("/").pop() ?? session.activeFile
1637
2273
  }
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" }) }) })
2274
+ ) : /* @__PURE__ */ jsx(
2275
+ CodeBlockView,
2276
+ {
2277
+ content: code,
2278
+ language: fileType.language,
2279
+ editable: true,
2280
+ onChange: session.updateActiveFile
2281
+ }
2282
+ ) })
1639
2283
  ] }),
1640
2284
  /* @__PURE__ */ jsx(
1641
2285
  EditHistory,
@@ -1701,6 +2345,77 @@ ${bobbinYaml}
1701
2345
  )
1702
2346
  ] });
1703
2347
  }
2348
+ function createManifest(services) {
2349
+ return {
2350
+ name: "preview",
2351
+ version: "1.0.0",
2352
+ platform: "browser",
2353
+ image: "@aprovan/patchwork-image-shadcn",
2354
+ services
2355
+ };
2356
+ }
2357
+ function WidgetPreview({
2358
+ code,
2359
+ compiler,
2360
+ services,
2361
+ enabled = true
2362
+ }) {
2363
+ const [loading, setLoading] = useState(false);
2364
+ const [error, setError] = useState(null);
2365
+ const containerRef = useRef(null);
2366
+ const mountedRef = useRef(null);
2367
+ useEffect(() => {
2368
+ if (!enabled || !compiler || !containerRef.current) return;
2369
+ let cancelled = false;
2370
+ const compileAndMount = async () => {
2371
+ setLoading(true);
2372
+ setError(null);
2373
+ try {
2374
+ if (mountedRef.current) {
2375
+ compiler.unmount(mountedRef.current);
2376
+ mountedRef.current = null;
2377
+ }
2378
+ const widget = await compiler.compile(code, createManifest(services), {
2379
+ typescript: true
2380
+ });
2381
+ if (cancelled || !containerRef.current) return;
2382
+ const mounted = await compiler.mount(widget, {
2383
+ target: containerRef.current,
2384
+ mode: "embedded"
2385
+ });
2386
+ mountedRef.current = mounted;
2387
+ } catch (err) {
2388
+ if (!cancelled) {
2389
+ setError(err instanceof Error ? err.message : "Failed to render preview");
2390
+ }
2391
+ } finally {
2392
+ if (!cancelled) {
2393
+ setLoading(false);
2394
+ }
2395
+ }
2396
+ };
2397
+ void compileAndMount();
2398
+ return () => {
2399
+ cancelled = true;
2400
+ if (mountedRef.current && compiler) {
2401
+ compiler.unmount(mountedRef.current);
2402
+ mountedRef.current = null;
2403
+ }
2404
+ };
2405
+ }, [code, compiler, enabled, services]);
2406
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2407
+ error && /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive flex items-center gap-2 p-3", children: [
2408
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
2409
+ /* @__PURE__ */ jsx("span", { children: error })
2410
+ ] }),
2411
+ loading && /* @__PURE__ */ jsxs("div", { className: "p-3 flex items-center gap-2 text-muted-foreground", children: [
2412
+ /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
2413
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
2414
+ ] }),
2415
+ !compiler && enabled && !loading && !error && /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }),
2416
+ /* @__PURE__ */ jsx("div", { ref: containerRef, className: "w-full" })
2417
+ ] });
2418
+ }
1704
2419
  var VFS_BASE_URL = "/vfs";
1705
2420
  var vfsConfigCache = null;
1706
2421
  async function getVFSConfig() {
@@ -1764,7 +2479,7 @@ async function isVFSAvailable() {
1764
2479
  return false;
1765
2480
  }
1766
2481
  }
1767
- function createManifest(services) {
2482
+ function createManifest2(services) {
1768
2483
  return {
1769
2484
  name: "preview",
1770
2485
  version: "1.0.0",
@@ -1773,63 +2488,19 @@ function createManifest(services) {
1773
2488
  services
1774
2489
  };
1775
2490
  }
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" }) {
2491
+ function CodePreview({
2492
+ code: originalCode,
2493
+ compiler,
2494
+ services,
2495
+ filePath,
2496
+ entrypoint = "index.ts",
2497
+ onOpenEditSession
2498
+ }) {
1828
2499
  const [isEditing, setIsEditing] = useState(false);
1829
2500
  const [showPreview, setShowPreview] = useState(true);
1830
2501
  const [currentCode, setCurrentCode] = useState(originalCode);
1831
2502
  const [editCount, setEditCount] = useState(0);
1832
- const [saveStatus, setSaveStatus] = useState("unsaved");
2503
+ const [saveStatus, setSaveStatus] = useState("saved");
1833
2504
  const [lastSavedCode, setLastSavedCode] = useState(originalCode);
1834
2505
  const [vfsPath, setVfsPath] = useState(null);
1835
2506
  const currentCodeRef = useRef(currentCode);
@@ -1865,6 +2536,14 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1865
2536
  useEffect(() => {
1866
2537
  isEditingRef.current = isEditing;
1867
2538
  }, [isEditing]);
2539
+ useEffect(() => {
2540
+ if (saveStatus === "saving") return;
2541
+ if (currentCode === lastSavedCode) {
2542
+ if (saveStatus !== "saved") setSaveStatus("saved");
2543
+ return;
2544
+ }
2545
+ if (saveStatus === "saved") setSaveStatus("unsaved");
2546
+ }, [currentCode, lastSavedCode, saveStatus]);
1868
2547
  useEffect(() => {
1869
2548
  let active = true;
1870
2549
  void (async () => {
@@ -1917,14 +2596,12 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1917
2596
  setSaveStatus("error");
1918
2597
  }
1919
2598
  }, [currentCode, getProjectId, getEntryFile]);
1920
- const { containerRef, loading, error } = useCodeCompiler(
1921
- compiler,
1922
- currentCode,
1923
- showPreview && !isEditing,
1924
- services
1925
- );
2599
+ const previewPath = filePath ?? entrypoint;
2600
+ const fileType = useMemo(() => getFileType(previewPath), [previewPath]);
2601
+ const canRenderWidget = fileType.category === "compilable";
1926
2602
  const compile = useCallback(
1927
2603
  async (code) => {
2604
+ if (!canRenderWidget) return { success: true };
1928
2605
  if (!compiler) return { success: true };
1929
2606
  const errors = [];
1930
2607
  const originalError = console.error;
@@ -1935,7 +2612,7 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
1935
2612
  try {
1936
2613
  await compiler.compile(
1937
2614
  code,
1938
- createManifest(services),
2615
+ createManifest2(services),
1939
2616
  { typescript: true }
1940
2617
  );
1941
2618
  return { success: true };
@@ -1953,15 +2630,64 @@ ${errors.join("\n")}` : "";
1953
2630
  console.error = originalError;
1954
2631
  }
1955
2632
  },
1956
- [compiler, services]
2633
+ [canRenderWidget, compiler, services]
1957
2634
  );
1958
2635
  const handleRevert = () => {
1959
2636
  setCurrentCode(originalCode);
1960
2637
  setEditCount(0);
1961
2638
  };
1962
2639
  const hasChanges = currentCode !== originalCode;
2640
+ const previewBody = useMemo(() => {
2641
+ if (canRenderWidget) {
2642
+ return /* @__PURE__ */ jsx(
2643
+ WidgetPreview,
2644
+ {
2645
+ code: currentCode,
2646
+ compiler,
2647
+ services,
2648
+ enabled: showPreview && !isEditing
2649
+ }
2650
+ );
2651
+ }
2652
+ if (fileType.category === "media") {
2653
+ return /* @__PURE__ */ jsx(
2654
+ MediaPreview,
2655
+ {
2656
+ content: currentCode,
2657
+ mimeType: fileType.mimeType,
2658
+ fileName: previewPath
2659
+ }
2660
+ );
2661
+ }
2662
+ if (fileType.language === "markdown") {
2663
+ return /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none", children: /* @__PURE__ */ jsx(MarkdownPreview, { value: currentCode }) });
2664
+ }
2665
+ return /* @__PURE__ */ jsx(
2666
+ CodeBlockView,
2667
+ {
2668
+ content: currentCode,
2669
+ language: fileType.language
2670
+ }
2671
+ );
2672
+ }, [canRenderWidget, compiler, currentCode, fileType, isEditing, previewPath, services, showPreview]);
2673
+ const handleOpenEditor = useCallback(async () => {
2674
+ if (!onOpenEditSession) {
2675
+ setIsEditing(true);
2676
+ return;
2677
+ }
2678
+ const projectId = await getProjectId();
2679
+ const entryFile = getEntryFile();
2680
+ const initialProject = createSingleFileProject(currentCode, entryFile, projectId);
2681
+ onOpenEditSession({
2682
+ projectId,
2683
+ entryFile,
2684
+ filePath,
2685
+ initialCode: currentCode,
2686
+ initialProject
2687
+ });
2688
+ }, [onOpenEditSession, getProjectId, getEntryFile, currentCode, filePath]);
1963
2689
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1964
- /* @__PURE__ */ jsxs("div", { className: "my-3 border rounded-lg", children: [
2690
+ /* @__PURE__ */ jsxs("div", { className: "border rounded-lg overflow-hidden min-w-0", children: [
1965
2691
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b rounded-t-lg", children: [
1966
2692
  /* @__PURE__ */ jsx(Code, { className: "h-4 w-4 text-muted-foreground" }),
1967
2693
  editCount > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [
@@ -1970,19 +2696,6 @@ ${errors.join("\n")}` : "";
1970
2696
  " edit",
1971
2697
  editCount !== 1 ? "s" : ""
1972
2698
  ] }),
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
2699
  /* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-1", children: [
1987
2700
  hasChanges && /* @__PURE__ */ jsx(
1988
2701
  "button",
@@ -1996,17 +2709,26 @@ ${errors.join("\n")}` : "";
1996
2709
  /* @__PURE__ */ jsx(
1997
2710
  "button",
1998
2711
  {
1999
- onClick: () => setIsEditing(true),
2712
+ onClick: () => void handleOpenEditor(),
2000
2713
  className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-muted",
2001
2714
  title: "Edit component",
2002
2715
  children: /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3" })
2003
2716
  }
2004
2717
  ),
2718
+ /* @__PURE__ */ jsx(
2719
+ SaveStatusButton,
2720
+ {
2721
+ status: saveStatus,
2722
+ onClick: handleSave,
2723
+ disabled: saveStatus === "saving",
2724
+ tone: "muted"
2725
+ }
2726
+ ),
2005
2727
  /* @__PURE__ */ jsxs(
2006
2728
  "button",
2007
2729
  {
2008
2730
  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"}`,
2731
+ 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
2732
  children: [
2011
2733
  showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
2012
2734
  showPreview ? "Preview" : "Code"
@@ -2015,16 +2737,13 @@ ${errors.join("\n")}` : "";
2015
2737
  )
2016
2738
  ] })
2017
2739
  ] }),
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 }) }) })
2740
+ 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(
2741
+ CodeBlockView,
2742
+ {
2743
+ content: currentCode,
2744
+ language: fileType.language
2745
+ }
2746
+ ) })
2028
2747
  ] }),
2029
2748
  /* @__PURE__ */ jsx(
2030
2749
  EditModal,
@@ -2042,6 +2761,7 @@ ${errors.join("\n")}` : "";
2042
2761
  const entryFile = getEntryFile();
2043
2762
  const project = createSingleFileProject(finalCode, entryFile, projectId);
2044
2763
  await saveProject(project);
2764
+ setLastSavedCode(finalCode);
2045
2765
  setSaveStatus("saved");
2046
2766
  } catch (err) {
2047
2767
  console.warn("[VFS] Failed to save project:", err);
@@ -2052,49 +2772,77 @@ ${errors.join("\n")}` : "";
2052
2772
  },
2053
2773
  originalCode: currentCode,
2054
2774
  compile,
2055
- renderPreview: (code) => /* @__PURE__ */ jsx(ModalPreview, { code, compiler, services })
2775
+ renderPreview: (code) => /* @__PURE__ */ jsx(
2776
+ WidgetPreview,
2777
+ {
2778
+ code,
2779
+ compiler,
2780
+ services
2781
+ }
2782
+ )
2056
2783
  }
2057
2784
  )
2058
2785
  ] });
2059
2786
  }
2060
- function ModalPreview({
2061
- code,
2062
- compiler,
2063
- services
2787
+ function DefaultBadge({
2788
+ children,
2789
+ className = ""
2064
2790
  }) {
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 });
2791
+ return /* @__PURE__ */ jsx(
2792
+ "span",
2793
+ {
2794
+ 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}`,
2795
+ children
2796
+ }
2797
+ );
2081
2798
  }
2082
- function DefaultDialog({ children, open, onOpenChange }) {
2799
+ function DefaultDialog({
2800
+ children,
2801
+ open,
2802
+ onOpenChange
2803
+ }) {
2083
2804
  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 }) });
2805
+ return /* @__PURE__ */ jsx(
2806
+ "div",
2807
+ {
2808
+ className: "fixed inset-0 z-50 bg-black/50",
2809
+ onClick: () => onOpenChange?.(false),
2810
+ children: /* @__PURE__ */ jsx(
2811
+ "div",
2812
+ {
2813
+ 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",
2814
+ onClick: (e) => e.stopPropagation(),
2815
+ children
2816
+ }
2817
+ )
2818
+ }
2819
+ );
2085
2820
  }
2086
2821
  function ServicesInspector({
2087
2822
  namespaces,
2088
2823
  services = [],
2089
2824
  BadgeComponent = DefaultBadge,
2090
- DialogComponent = DefaultDialog
2825
+ DialogComponent = DefaultDialog,
2826
+ DialogHeaderComponent = ({ children }) => /* @__PURE__ */ jsx("div", { className: "flex justify-between items-center mb-4", children }),
2827
+ DialogContentComponent = ({ children, className = "" }) => /* @__PURE__ */ jsx("div", { className, children }),
2828
+ DialogCloseComponent = ({ onClose }) => /* @__PURE__ */ jsx(
2829
+ "button",
2830
+ {
2831
+ onClick: () => onClose?.(),
2832
+ className: "text-muted-foreground hover:text-foreground",
2833
+ children: "\xD7"
2834
+ }
2835
+ )
2091
2836
  }) {
2092
2837
  const [open, setOpen] = useState(false);
2093
2838
  if (namespaces.length === 0) return null;
2094
- const groupedServices = services.reduce((acc, svc) => {
2095
- (acc[svc.namespace] ??= []).push(svc);
2096
- return acc;
2097
- }, {});
2839
+ const groupedServices = services.reduce(
2840
+ (acc, svc) => {
2841
+ (acc[svc.namespace] ??= []).push(svc);
2842
+ return acc;
2843
+ },
2844
+ {}
2845
+ );
2098
2846
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2099
2847
  /* @__PURE__ */ jsxs(
2100
2848
  "button",
@@ -2112,11 +2860,11 @@ function ServicesInspector({
2112
2860
  }
2113
2861
  ),
2114
2862
  /* @__PURE__ */ jsxs(DialogComponent, { open, onOpenChange: setOpen, children: [
2115
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-4", children: [
2116
- /* @__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" })
2863
+ /* @__PURE__ */ jsxs(DialogHeaderComponent, { children: [
2864
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Services" }),
2865
+ /* @__PURE__ */ jsx(DialogCloseComponent, { onClose: () => setOpen(false) })
2118
2866
  ] }),
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: [
2867
+ /* @__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
2868
  /* @__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
2869
  /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }),
2122
2870
  /* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: ns }),
@@ -2128,11 +2876,11 @@ function ServicesInspector({
2128
2876
  ] }),
2129
2877
  /* @__PURE__ */ jsx("div", { className: "ml-6 mt-2 space-y-2", children: groupedServices[ns]?.map((svc) => /* @__PURE__ */ jsxs("details", { children: [
2130
2878
  /* @__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" }),
2879
+ svc.parameters && /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
2132
2880
  /* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: svc.procedure }),
2133
2881
  /* @__PURE__ */ jsx("span", { className: "truncate text-xs opacity-70", children: svc.description })
2134
2882
  ] }),
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) }) })
2883
+ 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
2884
  ] }, svc.name)) ?? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No tool details available" }) })
2137
2885
  ] }, ns)) })
2138
2886
  ] })
@@ -2264,4 +3012,4 @@ function cn(...inputs) {
2264
3012
  return twMerge(clsx(inputs));
2265
3013
  }
2266
3014
 
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 };
3015
+ 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 };