@hydraharness/harness-client-ui-layout 0.1.1-rc.8 → 0.1.1-rc.9

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/README.md CHANGED
@@ -14,9 +14,11 @@ AppFrame always mounts the conversation and details columns; a connected Session
14
14
 
15
15
  In Electron, `Ctrl+P` opens the Files tab and focuses its filter for the current registered Workspace; without one, Files stays empty. The Explorer features a resizable splitter (160px–480px) and three collapsible sections: Open Editors with dirty indicators and Save All/Close All actions, Workspace Files with folders first in natural name order, color-coded extension icons, search filter, inline creation, and drag-and-drop moves, and an Outline section extracting code symbols (functions, classes, interfaces, types, headings) with click-to-jump line navigation.
16
16
 
17
- Selecting an eligible UTF-8 text file opens the in-panel editor with multi-document tabs and breadcrumbs path navigation. The editor provides a line numbers gutter with active line highlighting, Word Wrap toggling (`Alt+Z`), Go to Line (`Ctrl+G`), Diff with disk view, Prettier formatting (`Shift+Alt+F`), atomic version-checked saves (`Ctrl+S`), and a status bar displaying cursor coordinates, selection count, line count, indentation, encoding, line endings, and language mode. Drafts above 50,000 characters stay plain to keep editing responsive, and opening another file, closing a tab, or switching Workspace guards unsaved changes.
17
+ Selecting an eligible UTF-8 text file opens the in-panel editor with multi-document tabs and breadcrumbs path navigation. The editor provides a line numbers gutter with active line highlighting, Word Wrap toggling (`Alt+Z`), Go to Line (`Ctrl+G`), Diff with disk view, Prettier formatting (`Shift+Alt+F`), atomic version-checked saves (`Ctrl+S`), and a status bar displaying cursor coordinates, selection count, line count, indentation, encoding, line endings, and language mode. Drafts above 50,000 characters stay plain to keep editing responsive, and opening another file, closing a tab, or switching Workspace guards unsaved changes. Save All acknowledges each successful file separately, preserves newer typing and closed tabs, and stops on the first failure with remaining drafts intact.
18
18
 
19
- The renderer sends only an opaque Workspace id. Electron re-resolves that id through the Host, validates the IPC sender, canonicalizes and confines every list, search, create, read, format, and save path to the registered root, excludes symlink traversal, accepts only one validated leaf name for creation, and creates without overwriting an existing entry. Editable content must be valid non-binary UTF-8 and no larger than 1 MB. Confinement and version checks reject stable outside paths and stale states; they are not an operating-system sandbox or an atomic compare-and-swap against an external process that replaces a path during the final filesystem operation. This remains a bounded textarea editor: it does not provide Monaco, per-file editor tabs, language diagnostics, or persisted edit buffers.
19
+ Context-menu creation uses the addressed folder. Renaming or moving an entry updates descendant editor paths, expanded folders, and the creation target; deleting an entry removes its cached subtree and returns selection to its parent. A relocated file advances the editor's disk version only when it matches the version immediately before relocation; external edits still cause a save conflict. Rename, move, and delete run one at a time and wait for saving, formatting, and creation to finish. Responses from a previous Workspace cannot update the current tree or errors. A successful rename, move, or delete clears the file filter to display the updated tree.
20
+
21
+ The renderer sends only an opaque Workspace id. Electron re-resolves that id through the Host, validates the IPC sender, canonicalizes and confines every list, search, create, rename, move, delete, read, format, and save path to the registered root, excludes symlink traversal, accepts only one validated leaf name for creation, and creates without overwriting an existing entry. Editable content must be valid non-binary UTF-8 and no larger than 1 MB. Confinement and version checks reject stable outside paths and stale states; they are not an operating-system sandbox or an atomic compare-and-swap against an external process that replaces a path during the final filesystem operation. This remains a bounded textarea editor: it does not provide Monaco, language diagnostics, or persisted edit buffers.
20
22
 
21
23
  The `/client` exports are the plugin body (`apply`/`inject`), `LayoutController`, and the four owner-share interfaces. AppFrame, the panel store, and the concession solver remain package-internal.
22
24
 
package/lib/client.js CHANGED
@@ -420,7 +420,7 @@ window.__ModuleLoader__.load({
420
420
  const savingRef = (0, react.useRef)(false);
421
421
  const formattingRef = (0, react.useRef)(false);
422
422
  const creatingRef = (0, react.useRef)(false);
423
- const movingRef = (0, react.useRef)(false);
423
+ const mutatingRef = (0, react.useRef)(false);
424
424
  const mountedRef = (0, react.useRef)(true);
425
425
  const rootWorkspaceRef = (0, react.useRef)();
426
426
  const documentRef = (0, react.useRef)();
@@ -475,11 +475,19 @@ window.__ModuleLoader__.load({
475
475
  const [creating, setCreating] = (0, react.useState)(false);
476
476
  const [saving, setSaving] = (0, react.useState)(false);
477
477
  const [formatting, setFormatting] = (0, react.useState)(false);
478
- const [moving, setMoving] = (0, react.useState)(false);
478
+ const [mutating, setMutating] = (0, react.useState)(false);
479
479
  const [draggedPath, setDraggedPath] = (0, react.useState)();
480
480
  const [dropTargetPath, setDropTargetPath] = (0, react.useState)();
481
481
  const deferredDraft = (0, react.useDeferredValue)(document?.draft);
482
482
  const dirty = documentIsDirty(document);
483
+ const publishDocuments = (0, react.useCallback)((nextList) => {
484
+ openDocumentsRef.current = nextList;
485
+ setOpenDocuments(nextList);
486
+ const nextActiveDoc = nextList.find((d) => d.path === activePathRef.current);
487
+ documentRef.current = nextActiveDoc;
488
+ setDocument(nextActiveDoc);
489
+ props.onDirtyChange?.(nextList.some((d) => documentIsDirty(d)));
490
+ }, [props.onDirtyChange]);
483
491
  const updateDocument = (0, react.useCallback)((update) => {
484
492
  if (!mountedRef.current) return;
485
493
  const next = update(openDocumentsRef.current.find((d) => d.path === activePathRef.current));
@@ -498,13 +506,32 @@ window.__ModuleLoader__.load({
498
506
  activePathRef.current = next.path;
499
507
  setActivePath(next.path);
500
508
  }
501
- openDocumentsRef.current = nextList;
502
- setOpenDocuments(nextList);
503
- const nextActiveDoc = nextList.find((d) => d.path === activePathRef.current);
504
- documentRef.current = nextActiveDoc;
505
- setDocument(nextActiveDoc);
506
- props.onDirtyChange?.(nextList.some((d) => documentIsDirty(d)));
507
- }, [props.onDirtyChange]);
509
+ publishDocuments(nextList);
510
+ }, [publishDocuments]);
511
+ const remapEntryPaths = (oldPath, newPath, version) => {
512
+ const remap = (path) => samePath(path, oldPath) || pathContains(oldPath, path) ? `${newPath}${path.slice(oldPath.length)}` : path;
513
+ if (activePathRef.current !== void 0) {
514
+ activePathRef.current = remap(activePathRef.current);
515
+ setActivePath(activePathRef.current);
516
+ }
517
+ publishDocuments(openDocumentsRef.current.map((d) => ({
518
+ ...d,
519
+ path: remap(d.path),
520
+ version: samePath(d.path, oldPath) && version !== void 0 && d.version === version.before ? version.after : d.version
521
+ })));
522
+ setChildren((current) => Object.fromEntries(Object.entries(current).map(([path, entries]) => [remap(path), entries.map((entry) => ({
523
+ ...entry,
524
+ path: remap(entry.path),
525
+ name: fileName(remap(entry.path))
526
+ }))])));
527
+ setExpanded((current) => new Set([...current].map(remap)));
528
+ setSelectedDirectory((current) => current === void 0 ? current : remap(current));
529
+ setCreateDraft((current) => current === void 0 ? current : {
530
+ ...current,
531
+ parent: remap(current.parent)
532
+ });
533
+ setQuery("");
534
+ };
508
535
  const loadDirectory = (0, react.useCallback)(async (path) => {
509
536
  if (api === void 0 || rootPath === void 0 || workspaceId === void 0 || rootWorkspaceRef.current !== workspaceId || !mountedRef.current) return;
510
537
  const generation = listGeneration.current;
@@ -668,7 +695,7 @@ window.__ModuleLoader__.load({
668
695
  if (opening && children[path] === void 0) loadDirectory(path);
669
696
  };
670
697
  const openFile = (0, react.useCallback)(async (path) => {
671
- if (api === void 0 || rootPath === void 0 || workspaceId === void 0 || savingRef.current || !mountedRef.current) return;
698
+ if (api === void 0 || rootPath === void 0 || workspaceId === void 0 || savingRef.current || mutatingRef.current || !mountedRef.current) return;
672
699
  const previous = documentRef.current;
673
700
  if (previous?.path === path) return;
674
701
  if (documentIsDirty(previous) && !window.confirm("Discard unsaved changes and open another file?")) return;
@@ -748,18 +775,18 @@ window.__ModuleLoader__.load({
748
775
  document,
749
776
  updateDocument
750
777
  ]);
751
- const startCreate = (kind) => {
752
- if (rootPath === void 0 || creatingRef.current) return;
778
+ const startCreate = (kind, parent = selectedDirectory ?? rootPath) => {
779
+ if (parent === void 0 || creatingRef.current || mutatingRef.current) return;
753
780
  setQuery("");
754
781
  setError(void 0);
755
782
  setCreateName("");
756
783
  setCreateDraft({
757
- parent: selectedDirectory ?? rootPath,
784
+ parent,
758
785
  kind
759
786
  });
760
787
  };
761
788
  const submitCreate = async () => {
762
- if (api === void 0 || createDraft === void 0 || workspaceId === void 0 || createName.trim() === "" || creatingRef.current) return;
789
+ if (api === void 0 || createDraft === void 0 || workspaceId === void 0 || createName.trim() === "" || creatingRef.current || mutatingRef.current) return;
763
790
  const draft = createDraft;
764
791
  const name = createName.trim();
765
792
  const generation = listGeneration.current;
@@ -786,103 +813,89 @@ window.__ModuleLoader__.load({
786
813
  }
787
814
  };
788
815
  const startRename = (entry) => {
816
+ if (mutatingRef.current || savingRef.current || formattingRef.current || creatingRef.current) return;
789
817
  setContextMenu(void 0);
790
818
  setRenamingPath(entry.path);
791
819
  setRenameName(entry.name);
792
820
  };
793
821
  const submitRename = async (entry) => {
822
+ if (mutatingRef.current || savingRef.current || formattingRef.current || creatingRef.current) return;
794
823
  if (api?.rename === void 0 || workspaceId === void 0 || rootPath === void 0 || renameName.trim() === "" || renameName.trim() === entry.name) {
795
824
  setRenamingPath(void 0);
796
825
  return;
797
826
  }
798
827
  const newName = renameName.trim();
828
+ const generation = listGeneration.current;
829
+ mutatingRef.current = true;
830
+ setMutating(true);
831
+ setError(void 0);
832
+ readRequest.current += 1;
833
+ setReading(false);
799
834
  try {
800
835
  const result = await api.rename(entry.path, newName, workspaceId);
801
- const nextList = openDocumentsRef.current.map((d) => {
802
- if (d.path === entry.path) return {
803
- ...d,
804
- path: result.path
805
- };
806
- if (entry.directory && (d.path.startsWith(`${entry.path}/`) || d.path.startsWith(`${entry.path}\\`))) {
807
- const sub = d.path.slice(entry.path.length);
808
- return {
809
- ...d,
810
- path: `${result.path}${sub}`
811
- };
812
- }
813
- return d;
814
- });
815
- openDocumentsRef.current = nextList;
816
- setOpenDocuments(nextList);
817
- if (activePathRef.current === entry.path) {
818
- activePathRef.current = result.path;
819
- setActivePath(result.path);
820
- const nextDoc = nextList.find((d) => d.path === result.path);
821
- documentRef.current = nextDoc;
822
- setDocument(nextDoc);
823
- }
836
+ if (!mountedRef.current || listGeneration.current !== generation) return;
837
+ remapEntryPaths(entry.path, result.path, result.version);
824
838
  await loadDirectory(parentDirectory(rootPath, entry.path));
825
839
  } catch (reason) {
826
- setError(String(reason));
840
+ if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
827
841
  } finally {
828
- setRenamingPath(void 0);
842
+ mutatingRef.current = false;
843
+ if (mountedRef.current) {
844
+ setMutating(false);
845
+ if (listGeneration.current === generation) setRenamingPath(void 0);
846
+ }
829
847
  }
830
848
  };
831
849
  const handleMove = async (sourcePath, destinationPath) => {
832
850
  setDropTargetPath(void 0);
833
851
  setDraggedPath(void 0);
834
- if (api?.move === void 0 || workspaceId === void 0 || rootPath === void 0 || movingRef.current || samePath(sourcePath, destinationPath) || pathContains(sourcePath, destinationPath)) return;
835
- movingRef.current = true;
836
- setMoving(true);
852
+ if (api?.move === void 0 || workspaceId === void 0 || rootPath === void 0 || mutatingRef.current || savingRef.current || formattingRef.current || creatingRef.current || samePath(parentDirectory(rootPath, sourcePath), destinationPath) || samePath(sourcePath, destinationPath) || pathContains(sourcePath, destinationPath)) return;
853
+ const generation = listGeneration.current;
854
+ mutatingRef.current = true;
855
+ setMutating(true);
837
856
  setError(void 0);
857
+ readRequest.current += 1;
858
+ setReading(false);
838
859
  try {
839
860
  const result = await api.move(sourcePath, destinationPath, workspaceId);
840
- const activePath = activePathRef.current;
841
- const activeNextPath = activePath !== void 0 && (samePath(activePath, sourcePath) || pathContains(sourcePath, activePath)) ? `${result.path}${activePath.slice(sourcePath.length)}` : activePath;
842
- const nextList = openDocumentsRef.current.map((d) => {
843
- if (d.path === sourcePath) return {
844
- ...d,
845
- path: result.path
846
- };
847
- if (pathContains(sourcePath, d.path)) {
848
- const suffix = d.path.slice(sourcePath.length);
849
- return {
850
- ...d,
851
- path: `${result.path}${suffix}`
852
- };
853
- }
854
- return d;
855
- });
856
- openDocumentsRef.current = nextList;
857
- setOpenDocuments(nextList);
858
- if (activeNextPath !== activePath) {
859
- activePathRef.current = activeNextPath;
860
- setActivePath(activeNextPath);
861
- const nextDocument = nextList.find((d) => d.path === activeNextPath);
862
- documentRef.current = nextDocument;
863
- setDocument(nextDocument);
864
- }
861
+ if (!mountedRef.current || listGeneration.current !== generation) return;
862
+ remapEntryPaths(sourcePath, result.path, result.version);
865
863
  await Promise.all([loadDirectory(parentDirectory(rootPath, sourcePath)), loadDirectory(destinationPath)]);
866
864
  } catch (reason) {
867
- setError(String(reason));
865
+ if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
868
866
  } finally {
869
- movingRef.current = false;
870
- if (mountedRef.current) setMoving(false);
867
+ mutatingRef.current = false;
868
+ if (mountedRef.current) setMutating(false);
871
869
  }
872
870
  };
873
871
  const handleDelete = async (entry) => {
874
872
  setContextMenu(void 0);
875
- if (api?.delete === void 0 || workspaceId === void 0 || rootPath === void 0) return;
873
+ if (api?.delete === void 0 || workspaceId === void 0 || rootPath === void 0 || mutatingRef.current || savingRef.current || formattingRef.current || creatingRef.current) return;
876
874
  if (!window.confirm(`Are you sure you want to delete "${entry.name}"?`)) return;
875
+ const generation = listGeneration.current;
876
+ mutatingRef.current = true;
877
+ setMutating(true);
878
+ setError(void 0);
879
+ readRequest.current += 1;
880
+ setReading(false);
877
881
  try {
878
882
  await api.delete(entry.path, workspaceId);
883
+ if (!mountedRef.current || listGeneration.current !== generation) return;
879
884
  if (entry.directory) {
880
885
  const inside = openDocumentsRef.current.filter((d) => d.path.startsWith(`${entry.path}/`) || d.path.startsWith(`${entry.path}\\`));
881
886
  for (const d of inside) closeTab(d.path, true);
882
887
  } else closeTab(entry.path, true);
883
- await loadDirectory(parentDirectory(rootPath, entry.path));
888
+ const parent = parentDirectory(rootPath, entry.path);
889
+ setSelectedDirectory((current) => current !== void 0 && (samePath(current, entry.path) || pathContains(entry.path, current)) ? parent : current);
890
+ setExpanded((current) => new Set([...current].filter((path) => !samePath(path, entry.path) && !pathContains(entry.path, path))));
891
+ setChildren((current) => Object.fromEntries(Object.entries(current).filter(([path]) => !samePath(path, entry.path) && !pathContains(entry.path, path))));
892
+ setQuery("");
893
+ await loadDirectory(parent);
884
894
  } catch (reason) {
885
- setError(String(reason));
895
+ if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
896
+ } finally {
897
+ mutatingRef.current = false;
898
+ if (mountedRef.current) setMutating(false);
886
899
  }
887
900
  };
888
901
  const handleReveal = async (path) => {
@@ -982,33 +995,28 @@ window.__ModuleLoader__.load({
982
995
  props.onDirtyChange?.(false);
983
996
  }, [props.onDirtyChange]);
984
997
  const saveAllFiles = (0, react.useCallback)(async () => {
985
- if (api === void 0 || workspaceId === void 0 || savingRef.current) return;
986
- if (openDocumentsRef.current.filter((d) => documentIsDirty(d)).length === 0) return;
998
+ if (api === void 0 || workspaceId === void 0 || savingRef.current || formattingRef.current || mutatingRef.current) return;
999
+ const dirtyDocs = openDocumentsRef.current.filter((d) => documentIsDirty(d));
1000
+ if (dirtyDocs.length === 0) return;
1001
+ const generation = listGeneration.current;
987
1002
  savingRef.current = true;
988
1003
  setSaving(true);
989
1004
  props.onSavingChange?.(true);
990
1005
  setError(void 0);
991
1006
  try {
992
- const updatedList = [...openDocumentsRef.current];
993
- for (let i = 0; i < updatedList.length; i += 1) {
994
- const doc = updatedList[i];
995
- if (doc && documentIsDirty(doc)) {
996
- const saved = await api.save(doc.path, doc.draft, doc.version, workspaceId);
997
- updatedList[i] = {
998
- ...doc,
999
- savedContent: doc.draft,
1000
- version: saved.version
1001
- };
1002
- }
1007
+ for (const doc of dirtyDocs) {
1008
+ if (!mountedRef.current || listGeneration.current !== generation) return;
1009
+ if (!openDocumentsRef.current.some((current) => current.path === doc.path)) continue;
1010
+ const saved = await api.save(doc.path, doc.draft, doc.version, workspaceId);
1011
+ if (!mountedRef.current || listGeneration.current !== generation) return;
1012
+ publishDocuments(openDocumentsRef.current.map((current) => current.path === doc.path ? {
1013
+ ...current,
1014
+ savedContent: doc.draft,
1015
+ version: saved.version
1016
+ } : current));
1003
1017
  }
1004
- openDocumentsRef.current = updatedList;
1005
- setOpenDocuments(updatedList);
1006
- const currentActive = updatedList.find((d) => d.path === activePathRef.current);
1007
- documentRef.current = currentActive;
1008
- setDocument(currentActive);
1009
- props.onDirtyChange?.(false);
1010
1018
  } catch (reason) {
1011
- if (mountedRef.current) setError(String(reason));
1019
+ if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
1012
1020
  } finally {
1013
1021
  savingRef.current = false;
1014
1022
  if (mountedRef.current) {
@@ -1018,26 +1026,28 @@ window.__ModuleLoader__.load({
1018
1026
  }
1019
1027
  }, [
1020
1028
  api,
1021
- props.onDirtyChange,
1029
+ publishDocuments,
1022
1030
  props.onSavingChange,
1023
1031
  workspaceId
1024
1032
  ]);
1025
1033
  const saveFile = (0, react.useCallback)(async () => {
1026
- if (api === void 0 || document === void 0 || workspaceId === void 0 || !dirty || savingRef.current || formattingRef.current) return;
1034
+ if (api === void 0 || document === void 0 || workspaceId === void 0 || !dirty || savingRef.current || formattingRef.current || mutatingRef.current) return;
1027
1035
  const snapshot = document;
1036
+ const generation = listGeneration.current;
1028
1037
  savingRef.current = true;
1029
1038
  setSaving(true);
1030
1039
  props.onSavingChange?.(true);
1031
1040
  setError(void 0);
1032
1041
  try {
1033
1042
  const saved = await api.save(snapshot.path, snapshot.draft, snapshot.version, workspaceId);
1034
- updateDocument((current) => current?.path === snapshot.path ? {
1043
+ if (!mountedRef.current || listGeneration.current !== generation) return;
1044
+ publishDocuments(openDocumentsRef.current.map((current) => current.path === snapshot.path ? {
1035
1045
  ...current,
1036
1046
  savedContent: snapshot.draft,
1037
1047
  version: saved.version
1038
- } : current);
1048
+ } : current));
1039
1049
  } catch (reason) {
1040
- if (mountedRef.current && documentRef.current?.path === snapshot.path) setError(String(reason));
1050
+ if (mountedRef.current && listGeneration.current === generation) setError(String(reason));
1041
1051
  } finally {
1042
1052
  savingRef.current = false;
1043
1053
  if (mountedRef.current) {
@@ -1050,11 +1060,11 @@ window.__ModuleLoader__.load({
1050
1060
  dirty,
1051
1061
  document,
1052
1062
  props.onSavingChange,
1053
- updateDocument,
1063
+ publishDocuments,
1054
1064
  workspaceId
1055
1065
  ]);
1056
1066
  const formatFile = (0, react.useCallback)(async () => {
1057
- if (api === void 0 || document === void 0 || workspaceId === void 0 || formattingRef.current || savingRef.current) return;
1067
+ if (api === void 0 || document === void 0 || workspaceId === void 0 || formattingRef.current || savingRef.current || mutatingRef.current) return;
1058
1068
  const snapshot = document;
1059
1069
  formattingRef.current = true;
1060
1070
  setFormatting(true);
@@ -1220,6 +1230,7 @@ window.__ModuleLoader__.load({
1220
1230
  autoFocus: true,
1221
1231
  className: DesktopFilesPanel_module_css_default.renameInput,
1222
1232
  "aria-label": `Rename ${entry.name}`,
1233
+ disabled: mutating,
1223
1234
  value: renameName,
1224
1235
  onChange: (e) => {
1225
1236
  setRenameName(e.currentTarget.value);
@@ -1232,11 +1243,13 @@ window.__ModuleLoader__.load({
1232
1243
  type: "submit",
1233
1244
  "aria-label": "Confirm rename",
1234
1245
  className: DesktopFilesPanel_module_css_default.renameBtn,
1246
+ disabled: mutating,
1235
1247
  children: (0, react_jsx_runtime.jsx)(_hydraharness_harness_client_ui_primitives.IconCheckOutline14, {})
1236
1248
  }),
1237
1249
  (0, react_jsx_runtime.jsx)("button", {
1238
1250
  type: "button",
1239
1251
  "aria-label": "Cancel rename",
1252
+ disabled: mutating,
1240
1253
  className: DesktopFilesPanel_module_css_default.renameBtn,
1241
1254
  onClick: () => {
1242
1255
  setRenamingPath(void 0);
@@ -1247,7 +1260,7 @@ window.__ModuleLoader__.load({
1247
1260
  }) : (0, react_jsx_runtime.jsxs)("button", {
1248
1261
  type: "button",
1249
1262
  className: DesktopFilesPanel_module_css_default.row,
1250
- draggable: !moving,
1263
+ draggable: !mutating && !saving && !formatting && !creating,
1251
1264
  "data-drop-target": dropTargetPath === entry.path ? "true" : void 0,
1252
1265
  style: { paddingLeft: 8 + depth * 14 },
1253
1266
  "data-selected": entry.directory ? selectedDirectory === entry.path : document?.path === entry.path || void 0,
@@ -1265,7 +1278,7 @@ window.__ModuleLoader__.load({
1265
1278
  });
1266
1279
  },
1267
1280
  onDragStart: (event) => {
1268
- if (moving) return;
1281
+ if (mutating || saving || formatting || creating) return;
1269
1282
  setDraggedPath(entry.path);
1270
1283
  event.dataTransfer.effectAllowed = "move";
1271
1284
  event.dataTransfer.setData("text/plain", entry.path);
@@ -1275,7 +1288,7 @@ window.__ModuleLoader__.load({
1275
1288
  setDropTargetPath(void 0);
1276
1289
  },
1277
1290
  onDragOver: (event) => {
1278
- if (!entry.directory || draggedPath === void 0 || samePath(draggedPath, entry.path) || pathContains(draggedPath, entry.path) || moving) return;
1291
+ if (!entry.directory || draggedPath === void 0 || samePath(draggedPath, entry.path) || pathContains(draggedPath, entry.path) || mutating) return;
1279
1292
  event.preventDefault();
1280
1293
  event.dataTransfer.dropEffect = "move";
1281
1294
  setDropTargetPath(entry.path);
@@ -1309,7 +1322,7 @@ window.__ModuleLoader__.load({
1309
1322
  role: error === void 0 ? "status" : "alert",
1310
1323
  children: error ?? (workspaceId === void 0 ? "Open a workspace to browse its files." : rootLoading ? "Loading workspace files…" : "Workspace files are unavailable.")
1311
1324
  });
1312
- const status = saving ? "Saving…" : formatting ? "Formatting…" : moving ? "Moving…" : creating ? "Creating…" : searching ? "Searching…" : loading || reading ? "Loading…" : void 0;
1325
+ const status = saving ? "Saving…" : formatting ? "Formatting…" : mutating ? "Updating files…" : creating ? "Creating…" : searching ? "Searching…" : loading || reading ? "Loading…" : void 0;
1313
1326
  const highlightedDraft = document !== void 0 && deferredDraft === document.draft && document.draft.length <= HIGHLIGHT_MAX_CHARS ? deferredDraft : void 0;
1314
1327
  const lineCount = document ? document.draft.split("\n").length : 1;
1315
1328
  return (0, react_jsx_runtime.jsxs)("div", {
@@ -1356,7 +1369,7 @@ window.__ModuleLoader__.load({
1356
1369
  className: DesktopFilesPanel_module_css_default.sectionActionBtn,
1357
1370
  title: "Save all files",
1358
1371
  "aria-label": "Save all files",
1359
- disabled: saving || !openDocuments.some((d) => documentIsDirty(d)),
1372
+ disabled: saving || formatting || mutating || !openDocuments.some((d) => documentIsDirty(d)),
1360
1373
  onClick: () => {
1361
1374
  saveAllFiles();
1362
1375
  },
@@ -1464,7 +1477,7 @@ window.__ModuleLoader__.load({
1464
1477
  className: DesktopFilesPanel_module_css_default.sectionActionBtn,
1465
1478
  "aria-label": "New file",
1466
1479
  title: "New file",
1467
- disabled: creating,
1480
+ disabled: creating || mutating,
1468
1481
  onClick: () => {
1469
1482
  startCreate("file");
1470
1483
  },
@@ -1475,7 +1488,7 @@ window.__ModuleLoader__.load({
1475
1488
  className: DesktopFilesPanel_module_css_default.sectionActionBtn,
1476
1489
  "aria-label": "New folder",
1477
1490
  title: "New folder",
1478
- disabled: creating,
1491
+ disabled: creating || mutating,
1479
1492
  onClick: () => {
1480
1493
  startCreate("directory");
1481
1494
  },
@@ -1496,7 +1509,7 @@ window.__ModuleLoader__.load({
1496
1509
  className: DesktopFilesPanel_module_css_default.sectionActionBtn,
1497
1510
  "aria-label": "Refresh files",
1498
1511
  title: "Refresh files",
1499
- disabled: creating,
1512
+ disabled: creating || mutating,
1500
1513
  onClick: () => {
1501
1514
  loadDirectory(rootPath);
1502
1515
  },
@@ -1523,10 +1536,10 @@ window.__ModuleLoader__.load({
1523
1536
  className: DesktopFilesPanel_module_css_default.tree,
1524
1537
  role: "tree",
1525
1538
  "aria-label": "Workspace files",
1526
- "aria-busy": loading || searching || creating || moving || void 0,
1539
+ "aria-busy": loading || searching || creating || mutating || void 0,
1527
1540
  "data-drop-target": dropTargetPath === rootPath ? "true" : void 0,
1528
1541
  onDragOver: (event) => {
1529
- if (event.target !== event.currentTarget || draggedPath === void 0 || moving) return;
1542
+ if (event.target !== event.currentTarget || draggedPath === void 0 || mutating) return;
1530
1543
  event.preventDefault();
1531
1544
  event.dataTransfer.dropEffect = "move";
1532
1545
  setDropTargetPath(rootPath);
@@ -1826,7 +1839,7 @@ window.__ModuleLoader__.load({
1826
1839
  className: DesktopFilesPanel_module_css_default.editorActionBtn,
1827
1840
  "aria-label": "Format",
1828
1841
  "aria-keyshortcuts": "Shift+Alt+F",
1829
- disabled: formatting || saving,
1842
+ disabled: formatting || saving || mutating,
1830
1843
  title: "Format document (Shift+Alt+F)",
1831
1844
  onMouseDown: (event) => {
1832
1845
  event.preventDefault();
@@ -1841,7 +1854,7 @@ window.__ModuleLoader__.load({
1841
1854
  className: `${DesktopFilesPanel_module_css_default.editorActionBtn} ${dirty ? DesktopFilesPanel_module_css_default.editorActionBtnDirty : ""}`,
1842
1855
  "aria-label": "Save",
1843
1856
  "aria-keyshortcuts": "Control+S",
1844
- disabled: !dirty || saving || formatting,
1857
+ disabled: !dirty || saving || formatting || mutating,
1845
1858
  title: "Save (Ctrl+S)",
1846
1859
  onMouseDown: (event) => {
1847
1860
  event.preventDefault();
@@ -2199,7 +2212,7 @@ window.__ModuleLoader__.load({
2199
2212
  onClick: () => {
2200
2213
  setContextMenu(void 0);
2201
2214
  setSelectedDirectory(contextMenu.entry.path);
2202
- startCreate("file");
2215
+ startCreate("file", contextMenu.entry.path);
2203
2216
  },
2204
2217
  children: [(0, react_jsx_runtime.jsx)(_hydraharness_harness_client_ui_primitives.IconPlusOutline16, { size: 13 }), "New File"]
2205
2218
  }),
@@ -2210,7 +2223,7 @@ window.__ModuleLoader__.load({
2210
2223
  onClick: () => {
2211
2224
  setContextMenu(void 0);
2212
2225
  setSelectedDirectory(contextMenu.entry.path);
2213
- startCreate("directory");
2226
+ startCreate("directory", contextMenu.entry.path);
2214
2227
  },
2215
2228
  children: [(0, react_jsx_runtime.jsx)(_hydraharness_harness_client_ui_primitives.IconProjectAddOutline16, { size: 13 }), "New Folder"]
2216
2229
  }),
@@ -2220,6 +2233,7 @@ window.__ModuleLoader__.load({
2220
2233
  type: "button",
2221
2234
  className: DesktopFilesPanel_module_css_default.contextMenuItem,
2222
2235
  role: "menuitem",
2236
+ disabled: api?.rename === void 0 || mutating || saving || formatting || creating,
2223
2237
  onClick: () => {
2224
2238
  startRename(contextMenu.entry);
2225
2239
  },
@@ -2229,6 +2243,7 @@ window.__ModuleLoader__.load({
2229
2243
  type: "button",
2230
2244
  className: DesktopFilesPanel_module_css_default.contextMenuItem,
2231
2245
  role: "menuitem",
2246
+ disabled: api?.delete === void 0 || mutating || saving || formatting || creating,
2232
2247
  onClick: () => {
2233
2248
  handleDelete(contextMenu.entry);
2234
2249
  },
@@ -18,12 +18,20 @@ export interface DesktopFilesApi {
18
18
  path: string;
19
19
  name: string;
20
20
  directory?: boolean;
21
+ version?: {
22
+ before: string;
23
+ after: string;
24
+ };
21
25
  }>;
22
26
  move?(path: string, destinationDirectory: string, workspaceId: string): Promise<{
23
27
  oldPath: string;
24
28
  path: string;
25
29
  name: string;
26
30
  directory?: boolean;
31
+ version?: {
32
+ before: string;
33
+ after: string;
34
+ };
27
35
  }>;
28
36
  delete?(path: string, workspaceId: string): Promise<{
29
37
  path: string;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@hydraharness/harness-client-ui-layout",
3
3
  "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
4
- "version": "0.1.1-rc.8",
4
+ "version": "0.1.1-rc.9",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
10
+ "url": "git+https://github.com/MaiHongPhong2010/Hydra-Harness.git",
11
11
  "directory": "packages/client/ui-layout"
12
12
  },
13
13
  "type": "module",
@@ -49,25 +49,25 @@
49
49
  "@xterm/xterm": "^6.0.0"
50
50
  },
51
51
  "peerDependencies": {
52
- "@hydraharness/harness-client-locale": "^0.1.1-rc.8",
53
- "@hydraharness/harness-client-ui-settings": "^0.1.1-rc.8",
54
- "@hydraharness/harness-client-ui-theme": "^0.1.1-rc.8",
55
- "@hydraharness/harness-invariants": "^0.1.1-rc.8",
56
- "@hydraharness/cordis": "^4.0.2",
57
- "@hydraharness/harness-client-runtime": "^0.1.1-rc.8"
52
+ "@hydraharness/harness-client-locale": "^0.1.1-rc.9",
53
+ "@hydraharness/harness-client-ui-settings": "^0.1.1-rc.9",
54
+ "@hydraharness/harness-client-runtime": "^0.1.1-rc.9",
55
+ "@hydraharness/harness-client-ui-theme": "^0.1.1-rc.9",
56
+ "@hydraharness/harness-invariants": "^0.1.1-rc.9",
57
+ "@hydraharness/cordis": "^4.0.2"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/react": "~18.3.1",
61
61
  "react": "^18.2.0",
62
- "@hydraharness/harness-client-locale": "^0.1.1-rc.8",
63
- "@hydraharness/harness-client-test-runtime": "^0.1.1-rc.8",
64
- "@hydraharness/harness-client-ui-slots": "^0.1.1-rc.8",
65
- "@hydraharness/harness-client-ui-settings": "^0.1.1-rc.8",
66
- "@hydraharness/harness-client-ui-theme": "^0.1.1-rc.8",
67
- "@hydraharness/harness-client-runtime": "^0.1.1-rc.8",
68
- "@hydraharness/cordis": "^4.0.2",
69
- "@hydraharness/harness-client-ui-primitives": "^0.1.1-rc.8",
70
- "@hydraharness/harness-invariants": "^0.1.1-rc.8"
62
+ "@hydraharness/harness-client-test-runtime": "^0.1.1-rc.9",
63
+ "@hydraharness/harness-client-ui-settings": "^0.1.1-rc.9",
64
+ "@hydraharness/harness-client-locale": "^0.1.1-rc.9",
65
+ "@hydraharness/harness-client-ui-slots": "^0.1.1-rc.9",
66
+ "@hydraharness/harness-client-ui-theme": "^0.1.1-rc.9",
67
+ "@hydraharness/harness-client-runtime": "^0.1.1-rc.9",
68
+ "@hydraharness/harness-invariants": "^0.1.1-rc.9",
69
+ "@hydraharness/harness-client-ui-primitives": "^0.1.1-rc.9",
70
+ "@hydraharness/cordis": "^4.0.2"
71
71
  },
72
72
  "files": [
73
73
  "lib/index.js",