dsh-vscode-workbench 0.1.4 → 0.1.6

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/lib/client.js CHANGED
@@ -115,6 +115,13 @@ window.__ModuleLoader__.load({
115
115
  };
116
116
  //#endregion
117
117
  //#region src/client/api.ts
118
+ var ApiError = class extends Error {
119
+ code;
120
+ constructor(code, message) {
121
+ super(message);
122
+ this.code = code;
123
+ }
124
+ };
118
125
  async function api(method, body, signal) {
119
126
  const response = await fetch(`/dsh-vscode/api/${method}`, {
120
127
  method: "POST",
@@ -123,7 +130,7 @@ window.__ModuleLoader__.load({
123
130
  signal
124
131
  });
125
132
  const answer = await response.json();
126
- if (!response.ok || !answer.ok) throw new Error(answer.error?.message ?? `request failed (${response.status})`);
133
+ if (!response.ok || !answer.ok) throw new ApiError(answer.error?.code ?? "request-failed", answer.error?.message ?? `request failed (${response.status})`);
127
134
  return answer.value;
128
135
  }
129
136
  //#endregion
@@ -4418,22 +4425,21 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4418
4425
  }
4419
4426
  //#endregion
4420
4427
  //#region src/client/DshMark.tsx
4421
- /** Reuse the official DSH whale already rendered by the host brand plugin. */
4428
+ /** Bundled DSH fish logo — self-contained, no dependence on host DOM. */
4429
+ const WHALE_VIEWBOX = "0 0 23.16 17.04";
4430
+ const WHALE_PATH = "M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746L11.1749 14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.643 7.51636 12.39 7.51636C12.2955 7.51636 12.209 7.47486 12.1445 7.44136C12.039 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.277 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z";
4422
4431
  function DshMark({ size = 18 }) {
4423
- const mount = (0, react.useRef)(null);
4424
- (0, react.useLayoutEffect)(() => {
4425
- const source = [...document.querySelectorAll("svg")].find((svg) => svg.getAttribute("viewBox") === "0 0 23.16 17.04" && !mount.current?.contains(svg));
4426
- if (source === void 0 || mount.current === null) return;
4427
- const logo = source.cloneNode(true);
4428
- logo.setAttribute("width", String(size));
4429
- logo.setAttribute("height", String(size * 17.04 / 23.16));
4430
- mount.current.replaceChildren(logo);
4431
- }, [size]);
4432
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4432
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
4433
4433
  className: "dvw-dsh-mark",
4434
- ref: mount,
4434
+ width: size,
4435
+ height: size * 17.04 / 23.16,
4436
+ viewBox: WHALE_VIEWBOX,
4437
+ fill: "none",
4435
4438
  "aria-hidden": "true",
4436
- children: "DSH"
4439
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
4440
+ d: WHALE_PATH,
4441
+ fill: "currentColor"
4442
+ })
4437
4443
  });
4438
4444
  }
4439
4445
  //#endregion
@@ -4469,11 +4475,48 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4469
4475
  };
4470
4476
  //#endregion
4471
4477
  //#region src/client/Workbench.tsx
4478
+ function unloadedTab(path) {
4479
+ return {
4480
+ path,
4481
+ content: "",
4482
+ draft: "",
4483
+ revision: "",
4484
+ size: 0,
4485
+ binary: false,
4486
+ tooLarge: false,
4487
+ dirty: false,
4488
+ loaded: false,
4489
+ preview: /\.md$/i.test(path)
4490
+ };
4491
+ }
4492
+ /** Restore tab headers without reading their file contents. */
4493
+ function restoreTabPaths(paths) {
4494
+ return [...new Set(paths)].map(unloadedTab);
4495
+ }
4496
+ /** Return whether file changes require reloading one visible directory. */
4497
+ function directoryAffected(directory, changedPaths) {
4498
+ return changedPaths.some((changed) => {
4499
+ const normalized = changed.replace(/\\/g, "/");
4500
+ const parent = normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : "";
4501
+ return normalized === directory || parent === directory;
4502
+ });
4503
+ }
4472
4504
  /** Return the basename shown in a tab while preserving its full internal path. */
4473
4505
  function tabLabel(tab) {
4474
4506
  const path = tab.title ?? tab.diffSourcePath ?? tab.path.replace(/^diff:/, "");
4475
4507
  return path.split(/[\\/]/).at(-1) ?? path;
4476
4508
  }
4509
+ /** Invoke `fn` at most once per `ms` after the latest call, always running the freshest callback. */
4510
+ function useDebouncedCallback(fn, ms) {
4511
+ const fnRef = (0, react.useRef)(fn);
4512
+ fnRef.current = fn;
4513
+ const timer = (0, react.useRef)();
4514
+ (0, react.useEffect)(() => () => clearTimeout(timer.current), []);
4515
+ return (0, react.useCallback)(() => {
4516
+ clearTimeout(timer.current);
4517
+ timer.current = setTimeout(() => fnRef.current(), ms);
4518
+ }, [ms]);
4519
+ }
4477
4520
  function useDshDarkTheme() {
4478
4521
  const read = () => document.body.hasAttribute("data-ds-dark-theme");
4479
4522
  const [dark, setDark] = (0, react.useState)(read);
@@ -4487,7 +4530,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4487
4530
  }, []);
4488
4531
  return dark;
4489
4532
  }
4490
- function Tree({ sessionId, path, depth, expanded, selected, selectedDirectory, statuses, onToggle, onSelectDirectory, onOpen, onChange, onUpload }) {
4533
+ function Tree({ sessionId, path, depth, expanded, selected, selectedDirectory, statuses, onToggle, onSelectDirectory, onOpen, onChange, onUpload, creating, onCreatingChange, onCreatingCancel, onCreatingSubmit }) {
4491
4534
  const [rows, setRows] = (0, react.useState)([]);
4492
4535
  const [error, setError] = (0, react.useState)();
4493
4536
  const [dropTarget, setDropTarget] = (0, react.useState)(false);
@@ -4502,7 +4545,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4502
4545
  }, [sessionId, path]);
4503
4546
  (0, react.useEffect)(load, [load]);
4504
4547
  (0, react.useEffect)(() => {
4505
- const handler = () => load();
4548
+ const handler = (event) => {
4549
+ const paths = event.detail;
4550
+ if (paths === void 0 || directoryAffected(path, paths)) load();
4551
+ };
4506
4552
  window.addEventListener("dvw-files-changed", handler);
4507
4553
  return () => window.removeEventListener("dvw-files-changed", handler);
4508
4554
  }, [load]);
@@ -4569,6 +4615,41 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4569
4615
  className: "dvw-error",
4570
4616
  children: error
4571
4617
  }),
4618
+ creating?.directory === path && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4619
+ className: "dvw-tree-row dvw-tree-create-row",
4620
+ style: { paddingLeft: 6 + depth * 13 },
4621
+ children: [
4622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dvw-chevron" }),
4623
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4624
+ name: creating.kind === "directory" ? "folder" : "new-file",
4625
+ className: `dvw-file-icon ${creating.kind === "directory" ? "folder" : ""}`
4626
+ }),
4627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4628
+ className: "dvw-tree-create-input",
4629
+ value: creating.value,
4630
+ autoFocus: true,
4631
+ "aria-label": creating.kind === "directory" ? "新建文件夹名称" : "新建文件名称",
4632
+ "aria-invalid": creating.error !== void 0,
4633
+ placeholder: creating.kind === "directory" ? "文件夹名称" : "文件名称",
4634
+ onChange: (event) => onCreatingChange(event.target.value),
4635
+ onKeyDown: (event) => {
4636
+ if (event.key === "Enter") {
4637
+ event.preventDefault();
4638
+ onCreatingSubmit();
4639
+ }
4640
+ if (event.key === "Escape") {
4641
+ event.preventDefault();
4642
+ onCreatingCancel();
4643
+ }
4644
+ },
4645
+ onBlur: onCreatingCancel
4646
+ })
4647
+ ]
4648
+ }), creating.error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4649
+ className: "dvw-tree-create-error",
4650
+ style: { paddingLeft: 42 + depth * 13 },
4651
+ children: creating.error
4652
+ })] }),
4572
4653
  rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4573
4654
  className: "dvw-tree-row",
4574
4655
  "data-dvw-upload-directory": row.kind === "directory" ? row.path : void 0,
@@ -4646,7 +4727,11 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4646
4727
  onSelectDirectory,
4647
4728
  onOpen,
4648
4729
  onChange,
4649
- onUpload
4730
+ onUpload,
4731
+ creating,
4732
+ onCreatingChange,
4733
+ onCreatingCancel,
4734
+ onCreatingSubmit
4650
4735
  })] }, row.path)),
4651
4736
  context && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4652
4737
  className: "dvw-context-menu",
@@ -4722,6 +4807,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4722
4807
  const [statuses, setStatuses] = (0, react.useState)(/* @__PURE__ */ new Map());
4723
4808
  const [dropTarget, setDropTarget] = (0, react.useState)(false);
4724
4809
  const [selectedDirectory, setSelectedDirectory] = (0, react.useState)("");
4810
+ const [creating, setCreating] = (0, react.useState)();
4725
4811
  const picker = (0, react.useRef)(null);
4726
4812
  (0, react.useEffect)(() => {
4727
4813
  if (props.cwd) try {
@@ -4736,25 +4822,49 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4736
4822
  const refreshStatus = (0, react.useCallback)(() => {
4737
4823
  api("git.status", { sessionId: props.sessionId }).then((status) => setStatuses(new Map(status.changes.map((change) => [change.path, gitStatusCode(change)])))).catch(() => setStatuses(/* @__PURE__ */ new Map()));
4738
4824
  }, [props.sessionId]);
4825
+ const refreshStatusDebounced = useDebouncedCallback(refreshStatus, 500);
4739
4826
  (0, react.useEffect)(() => {
4740
4827
  refreshStatus();
4741
- const handler = () => refreshStatus();
4828
+ const handler = () => refreshStatusDebounced();
4742
4829
  window.addEventListener("dvw-files-changed", handler);
4743
4830
  return () => window.removeEventListener("dvw-files-changed", handler);
4744
- }, [refreshStatus]);
4745
- const create = async (kind) => {
4746
- const path = prompt(kind === "file" ? "新文件路径" : "新目录路径");
4747
- if (!path) return;
4831
+ }, [refreshStatus, refreshStatusDebounced]);
4832
+ const startCreating = (kind) => {
4833
+ const directory = selectedDirectory;
4834
+ if (directory) setExpanded((current) => new Set(current).add(directory));
4835
+ setCreating({
4836
+ kind,
4837
+ directory,
4838
+ value: ""
4839
+ });
4840
+ };
4841
+ const submitCreating = async () => {
4842
+ if (creating === void 0) return;
4843
+ const value = creating.value.trim();
4844
+ if (value === "") {
4845
+ setCreating((current) => current && {
4846
+ ...current,
4847
+ error: "请输入名称"
4848
+ });
4849
+ return;
4850
+ }
4851
+ const path = createdEntryPath(creating.directory, value);
4748
4852
  try {
4749
4853
  await api("fs.create", {
4750
4854
  sessionId: props.sessionId,
4751
4855
  path,
4752
- kind
4856
+ kind: creating.kind
4753
4857
  });
4858
+ const created = path;
4859
+ setCreating(void 0);
4860
+ setSelectedDirectory(creating.kind === "directory" ? created : creating.directory);
4754
4861
  setRevision((x) => x + 1);
4755
4862
  window.dispatchEvent(new Event("dvw-files-changed"));
4756
4863
  } catch (error) {
4757
- alert(String(error));
4864
+ setCreating((current) => current && {
4865
+ ...current,
4866
+ error: error instanceof Error ? error.message : String(error)
4867
+ });
4758
4868
  }
4759
4869
  };
4760
4870
  const upload = (0, react.useCallback)(async (directory, files) => {
@@ -4865,13 +4975,13 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4865
4975
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4866
4976
  className: "dvw-icon",
4867
4977
  title: "新建文件",
4868
- onClick: () => void create("file"),
4978
+ onClick: () => startCreating("file"),
4869
4979
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "new-file" })
4870
4980
  }),
4871
4981
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4872
4982
  className: "dvw-icon",
4873
- title: "新建目录",
4874
- onClick: () => void create("directory"),
4983
+ title: "新建文件夹",
4984
+ onClick: () => startCreating("directory"),
4875
4985
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { name: "new-folder" })
4876
4986
  }),
4877
4987
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -4909,7 +5019,15 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4909
5019
  onSelectDirectory: setSelectedDirectory,
4910
5020
  onOpen: props.onOpen,
4911
5021
  onChange: () => setRevision((x) => x + 1),
4912
- onUpload: (directory, files) => void upload(directory, files)
5022
+ onUpload: (directory, files) => void upload(directory, files),
5023
+ creating,
5024
+ onCreatingChange: (value) => setCreating((current) => current && {
5025
+ ...current,
5026
+ value,
5027
+ error: void 0
5028
+ }),
5029
+ onCreatingCancel: () => setCreating(void 0),
5030
+ onCreatingSubmit: () => void submitCreating()
4913
5031
  })
4914
5032
  }, revision)] });
4915
5033
  }
@@ -4917,6 +5035,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4917
5035
  function uploadDestination(selectedDirectory) {
4918
5036
  return selectedDirectory ?? "";
4919
5037
  }
5038
+ /** Join an inline-created entry name to the currently selected Explorer directory. */
5039
+ function createdEntryPath(directory, name) {
5040
+ return directory ? `${directory}/${name}` : name;
5041
+ }
4920
5042
  /** Replace the final path component while preserving its workspace-relative directory. */
4921
5043
  function renamedPath(path, name) {
4922
5044
  return [...path.split("/").slice(0, -1), name].filter(Boolean).join("/");
@@ -5318,14 +5440,19 @@ Please report this to https://github.com/markedjs/marked.`, e) {
5318
5440
  setStatus(nextStatus);
5319
5441
  setCommits(nextCommits);
5320
5442
  setError(void 0);
5321
- }).catch((error) => setError(String(error)));
5443
+ }).catch((error) => {
5444
+ setStatus(void 0);
5445
+ setCommits([]);
5446
+ setError(error instanceof ApiError && error.code === "not-repository" ? void 0 : String(error));
5447
+ });
5322
5448
  }, [props.sessionId]);
5449
+ const refreshDebounced = useDebouncedCallback(refresh, 500);
5323
5450
  (0, react.useEffect)(refresh, [refresh]);
5324
5451
  (0, react.useEffect)(() => {
5325
- const fn = () => refresh();
5452
+ const fn = () => refreshDebounced();
5326
5453
  window.addEventListener("dvw-files-changed", fn);
5327
5454
  return () => window.removeEventListener("dvw-files-changed", fn);
5328
- }, [refresh]);
5455
+ }, [refreshDebounced]);
5329
5456
  const action = async (name, path, value) => {
5330
5457
  setBusy(true);
5331
5458
  try {
@@ -5586,6 +5713,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
5586
5713
  const tabsRef = (0, react.useRef)(tabs);
5587
5714
  tabsRef.current = tabs;
5588
5715
  const workspaceRequests = (0, react.useRef)(new WorkspaceRequestScope());
5716
+ const loadingTabs = (0, react.useRef)(/* @__PURE__ */ new Set());
5589
5717
  const dshDark = useDshDarkTheme();
5590
5718
  const [theme, setTheme] = (0, react.useState)(() => {
5591
5719
  const saved = localStorage.getItem("dvw:theme");
@@ -5608,61 +5736,24 @@ Please report this to https://github.com/markedjs/marked.`, e) {
5608
5736
  window.addEventListener("keydown", onKey, true);
5609
5737
  return () => window.removeEventListener("keydown", onKey, true);
5610
5738
  }, [snapshot.visible]);
5611
- const openForWorkspace = async (targetSessionId, epoch, path, line, column) => {
5612
- if (!workspaceRequests.current.isCurrent(epoch)) return;
5613
- if (tabsRef.current.find((tab) => tab.path === path)) {
5614
- setActive(path);
5615
- setTabs((rows) => rows.map((tab) => tab.path === path ? {
5616
- ...tab,
5617
- line,
5618
- column
5619
- } : tab));
5620
- return;
5621
- }
5622
- if (/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(path)) {
5623
- const imageUrl = `/dsh-vscode/file?sessionId=${encodeURIComponent(targetSessionId)}&path=${encodeURIComponent(path)}`;
5624
- if (!workspaceRequests.current.isCurrent(epoch)) return;
5625
- setTabs((rows) => workspaceRequests.current.isCurrent(epoch) ? [...rows.filter((tab) => tab.path !== path), {
5626
- path,
5627
- content: "",
5628
- draft: "",
5629
- revision: "",
5630
- size: 0,
5631
- binary: false,
5632
- tooLarge: false,
5633
- dirty: false,
5634
- imageUrl
5635
- }] : rows);
5636
- setActive(path);
5637
- return;
5638
- }
5639
- try {
5640
- const doc = await api("fs.read", {
5641
- sessionId: targetSessionId,
5642
- path
5643
- });
5644
- if (!workspaceRequests.current.isCurrent(epoch)) return;
5645
- if (doc.binary || doc.tooLarge) {
5646
- setError(doc.binary ? "二进制文件不能在文本编辑器中打开" : `文件超过 5 MiB:${path}`);
5647
- return;
5648
- }
5649
- setTabs((rows) => workspaceRequests.current.isCurrent(epoch) ? [...rows.filter((tab) => tab.path !== path), {
5650
- ...doc,
5651
- draft: doc.content,
5652
- dirty: false,
5653
- line,
5654
- column,
5655
- preview: /\.md$/i.test(path)
5656
- }] : rows);
5657
- setActive(path);
5658
- } catch (error) {
5659
- if (workspaceRequests.current.isCurrent(epoch)) setError(String(error));
5660
- }
5739
+ const openForWorkspace = (path, line, column) => {
5740
+ if (tabsRef.current.find((tab) => tab.path === path)) setTabs((rows) => rows.map((tab) => tab.path === path ? {
5741
+ ...tab,
5742
+ line,
5743
+ column
5744
+ } : tab));
5745
+ else setTabs((rows) => [...rows, {
5746
+ ...unloadedTab(path),
5747
+ line,
5748
+ column
5749
+ }]);
5750
+ setActive(path);
5661
5751
  };
5662
5752
  (0, react.useEffect)(() => {
5663
5753
  if (!snapshot.visible || sessionId === void 0) return;
5664
5754
  const epoch = workspaceRequests.current.begin();
5665
5755
  const cleared = resetWorkspaceDocuments();
5756
+ loadingTabs.current.clear();
5666
5757
  setWorkspace(void 0);
5667
5758
  setTabs(cleared.tabs);
5668
5759
  tabsRef.current = cleared.tabs;
@@ -5678,13 +5769,16 @@ Please report this to https://github.com/markedjs/marked.`, e) {
5678
5769
  if (parsed.view) service.show(parsed.view);
5679
5770
  if (typeof parsed.panelHeight === "number") setPanelHeight(parsed.panelHeight);
5680
5771
  if (typeof parsed.sideWidth === "number") setSideWidth(Math.max(210, Math.min(640, Math.round(parsed.sideWidth))));
5681
- for (const path of parsed.paths ?? []) openForWorkspace(sessionId, epoch, path);
5772
+ const restored = restoreTabPaths(parsed.paths ?? []);
5773
+ setTabs(restored);
5774
+ setActive(restored.some((tab) => tab.path === parsed.active) ? parsed.active : restored.at(-1)?.path);
5682
5775
  } catch {}
5683
5776
  }).catch((error) => {
5684
5777
  if (!controller.signal.aborted && workspaceRequests.current.isCurrent(epoch)) setError(String(error));
5685
5778
  });
5686
5779
  return () => {
5687
5780
  controller.abort();
5781
+ loadingTabs.current.clear();
5688
5782
  workspaceRequests.current.invalidate(epoch);
5689
5783
  };
5690
5784
  }, [snapshot.visible, sessionId]);
@@ -5692,6 +5786,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
5692
5786
  if (workspace) localStorage.setItem(`dvw:${workspace.cwd}`, JSON.stringify({
5693
5787
  view: snapshot.view,
5694
5788
  paths: tabs.filter((t) => !t.path.startsWith("diff:")).map((t) => t.path),
5789
+ active,
5695
5790
  panelHeight,
5696
5791
  sideWidth
5697
5792
  }));
@@ -5699,6 +5794,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
5699
5794
  workspace,
5700
5795
  snapshot.view,
5701
5796
  tabs,
5797
+ active,
5702
5798
  panelHeight,
5703
5799
  sideWidth
5704
5800
  ]);
@@ -5706,8 +5802,12 @@ Please report this to https://github.com/markedjs/marked.`, e) {
5706
5802
  if (!snapshot.visible || sessionId === void 0) return;
5707
5803
  const protocol = location.protocol === "https:" ? "wss" : "ws";
5708
5804
  const ws = new WebSocket(`${protocol}://${location.host}/dsh-vscode/ws/files?sessionId=${encodeURIComponent(sessionId)}`);
5709
- ws.onmessage = () => {
5710
- window.dispatchEvent(new Event("dvw-files-changed"));
5805
+ ws.onmessage = (event) => {
5806
+ let paths = [];
5807
+ try {
5808
+ paths = JSON.parse(String(event.data)).paths ?? [];
5809
+ } catch {}
5810
+ window.dispatchEvent(new CustomEvent("dvw-files-changed", { detail: paths }));
5711
5811
  setTabs((current) => current.map((tab) => tab.dirty ? {
5712
5812
  ...tab,
5713
5813
  external: true
@@ -5715,9 +5815,49 @@ Please report this to https://github.com/markedjs/marked.`, e) {
5715
5815
  };
5716
5816
  return () => ws.close();
5717
5817
  }, [snapshot.visible, sessionId]);
5718
- const open = async (path, line, column) => {
5719
- if (sessionId === void 0) return;
5720
- await openForWorkspace(sessionId, workspaceRequests.current.current(), path, line, column);
5818
+ (0, react.useEffect)(() => {
5819
+ if (sessionId === void 0 || active === void 0) return;
5820
+ const tab = tabsRef.current.find((row) => row.path === active);
5821
+ if (tab === void 0 || tab.loaded !== false || loadingTabs.current.has(active)) return;
5822
+ const epoch = workspaceRequests.current.current();
5823
+ loadingTabs.current.add(active);
5824
+ if (/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(active)) {
5825
+ const imageUrl = `/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(active)}`;
5826
+ setTabs((rows) => rows.map((row) => row.path === active ? {
5827
+ ...row,
5828
+ loaded: true,
5829
+ imageUrl
5830
+ } : row));
5831
+ loadingTabs.current.delete(active);
5832
+ return;
5833
+ }
5834
+ api("fs.read", {
5835
+ sessionId,
5836
+ path: active
5837
+ }).then((doc) => {
5838
+ if (!workspaceRequests.current.isCurrent(epoch)) return;
5839
+ if (doc.binary || doc.tooLarge) {
5840
+ setTabs((rows) => rows.filter((row) => row.path !== active));
5841
+ setActive((current) => current === active ? void 0 : current);
5842
+ setError(doc.binary ? "二进制文件不能在文本编辑器中打开" : `文件超过 5 MiB:${active}`);
5843
+ return;
5844
+ }
5845
+ setTabs((rows) => rows.map((row) => row.path === active ? {
5846
+ ...row,
5847
+ ...doc,
5848
+ draft: doc.content,
5849
+ loaded: true
5850
+ } : row));
5851
+ }).catch((error) => {
5852
+ if (workspaceRequests.current.isCurrent(epoch)) setError(String(error));
5853
+ }).finally(() => loadingTabs.current.delete(active));
5854
+ }, [
5855
+ active,
5856
+ sessionId,
5857
+ tabs
5858
+ ]);
5859
+ const open = (path, line, column) => {
5860
+ if (sessionId !== void 0) openForWorkspace(path, line, column);
5721
5861
  };
5722
5862
  (0, react.useEffect)(() => {
5723
5863
  const listener = (event) => void open(event.detail);
@@ -6301,7 +6441,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
6301
6441
  body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-scheme:dark}
6302
6442
  .dvw-overlay[data-dvw-theme=dark]{--dvw-base:#151517;--dvw-surface-1:#232324;--dvw-surface-2:#2c2c2e;--dvw-surface-3:#353638;--dvw-text:#f9fafb;--dvw-text-2:#cfd3d6;--dvw-text-3:#adb2b8;--dvw-border:#ffffff1f;--dvw-border-soft:#ffffff29;--dvw-hover:#ffffff14;--dvw-active:#ffffff24;--dvw-accent:#679efe;--dvw-error:#f25a5a;--dvw-success:#22c55e;--dvw-warn:#f59e0b}
6303
6443
  .dvw-overlay[data-dvw-theme=light]{--dvw-base:#fff;--dvw-surface-1:#fff;--dvw-surface-2:#fff;--dvw-surface-3:#fff;--dvw-text:#0f1115;--dvw-text-2:#61666b;--dvw-text-3:#81858c;--dvw-border:#0000001a;--dvw-border-soft:#0000001f;--dvw-hover:#2631480f;--dvw-active:#2631481a;--dvw-accent:#4176e6;--dvw-error:#ec1313;--dvw-success:#22c55e;--dvw-warn:#f59e0b;color-scheme:light}
6304
- .dvw-overlay *{box-sizing:border-box}.dvw-activity{grid-row:1/3;background:var(--dvw-base);border-right:1px solid var(--dvw-border-soft);display:flex;flex-direction:column;align-items:center}.dvw-act{width:48px;height:48px;border:0;border-left:2px solid transparent;background:transparent;color:var(--dvw-text-3);font-size:22px;cursor:pointer}.dvw-act:hover,.dvw-act[data-active=true]{color:var(--dvw-text)}.dvw-act[data-active=true]{border-left-color:var(--dvw-accent)}.dvw-act.dsh{margin-top:auto}.dvw-side{grid-column:2;grid-row:1/3;min-width:0;width:100%;background:var(--dvw-surface-1);border-right:1px solid var(--dvw-border);display:flex;flex-direction:column}.dvw-side-resizer{grid-column:2;grid-row:1/3;justify-self:end;width:7px;height:100%;z-index:4;cursor:ew-resize;transform:translateX(3px);touch-action:none}.dvw-side-resizer:hover,.dvw-side-resizer:active{background:var(--dvw-accent)}.dvw-side-head{height:36px;display:flex;align-items:center;padding:0 12px;text-transform:uppercase;font-size:11px;letter-spacing:.4px}.dvw-toolbar{margin-left:auto;display:flex;gap:2px}.dvw-icon{border:0;background:transparent;color:var(--dvw-text-2);border-radius:4px;min-width:24px;height:24px;display:inline-grid;place-items:center;cursor:pointer}.dvw-icon:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-icon:disabled{opacity:.45;cursor:default}.dvw-file-picker{display:none}.dvw-tree,.dvw-results,.dvw-git{overflow:auto;flex:1}.dvw-tree[data-drop-target=true]{outline:1px dashed var(--dvw-accent);outline-offset:-3px}.dvw-tree-row{display:flex;align-items:center;height:24px;padding-right:6px;cursor:pointer;white-space:nowrap}.dvw-tree-row:hover{background:var(--dvw-hover)}.dvw-tree-row[data-selected=true],.dvw-tree-row[data-drop-target=true]{background:var(--dvw-active)}.dvw-chevron{width:16px;text-align:center;color:var(--dvw-text-3)}.dvw-file-icon{width:18px;color:var(--dvw-text-2)}.dvw-file-icon.folder{color:var(--dvw-accent)}.dvw-tree-name{overflow:hidden;text-overflow:ellipsis}.dvw-tree-rename{min-width:0;flex:1;height:20px;border:1px solid var(--dvw-accent);background:var(--dvw-surface-1);color:var(--dvw-text);font:inherit;outline:0}.dvw-tree-status{margin-left:auto;padding-left:6px;font-size:11px;font-weight:600;color:var(--dvw-warn)}.dvw-tree-status[data-status=M]{color:var(--dvw-warn)}.dvw-tree-status[data-status=A]{color:var(--dvw-success)}.dvw-tree-status[data-status=D]{color:var(--dvw-error)}.dvw-context-menu{position:fixed;z-index:40;min-width:148px;padding:4px;border:1px solid var(--dvw-border);border-radius:5px;background:var(--dvw-surface-3);box-shadow:0 6px 20px rgba(0,0,0,.3)}.dvw-context-menu button{width:100%;height:28px;border:0;border-radius:3px;background:transparent;color:var(--dvw-text);display:flex;align-items:center;gap:8px;padding:0 8px;text-align:left;font:inherit;cursor:pointer}.dvw-context-menu button:hover{background:var(--dvw-hover)}.dvw-context-menu .dvw-context-delete{color:var(--dvw-error)}
6444
+ .dvw-overlay *{box-sizing:border-box}.dvw-activity{grid-row:1/3;background:var(--dvw-base);border-right:1px solid var(--dvw-border-soft);display:flex;flex-direction:column;align-items:center}.dvw-act{width:48px;height:48px;border:0;border-left:2px solid transparent;background:transparent;color:var(--dvw-text-3);font-size:22px;cursor:pointer}.dvw-act:hover,.dvw-act[data-active=true]{color:var(--dvw-text)}.dvw-act[data-active=true]{border-left-color:var(--dvw-accent)}.dvw-act.dsh{margin-top:auto}.dvw-side{grid-column:2;grid-row:1/3;min-width:0;width:100%;background:var(--dvw-surface-1);border-right:1px solid var(--dvw-border);display:flex;flex-direction:column}.dvw-side-resizer{grid-column:2;grid-row:1/3;justify-self:end;width:7px;height:100%;z-index:4;cursor:ew-resize;transform:translateX(3px);touch-action:none}.dvw-side-resizer:hover,.dvw-side-resizer:active{background:var(--dvw-accent)}.dvw-side-head{height:36px;display:flex;align-items:center;padding:0 12px;text-transform:uppercase;font-size:11px;letter-spacing:.4px}.dvw-toolbar{margin-left:auto;display:flex;gap:2px}.dvw-icon{border:0;background:transparent;color:var(--dvw-text-2);border-radius:4px;min-width:24px;height:24px;display:inline-grid;place-items:center;cursor:pointer}.dvw-icon:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-icon:disabled{opacity:.45;cursor:default}.dvw-file-picker{display:none}.dvw-tree,.dvw-results,.dvw-git{overflow:auto;flex:1}.dvw-tree[data-drop-target=true]{outline:1px dashed var(--dvw-accent);outline-offset:-3px}.dvw-tree-row{display:flex;align-items:center;height:24px;padding-right:6px;cursor:pointer;white-space:nowrap}.dvw-tree-row:hover{background:var(--dvw-hover)}.dvw-tree-row[data-selected=true],.dvw-tree-row[data-drop-target=true]{background:var(--dvw-active)}.dvw-chevron{width:16px;flex:none;text-align:center;color:var(--dvw-text-3)}.dvw-file-icon{width:18px;flex:none;color:var(--dvw-text-2)}.dvw-file-icon.folder{color:var(--dvw-accent)}.dvw-tree-name{overflow:hidden;text-overflow:ellipsis}.dvw-tree-rename,.dvw-tree-create-input{min-width:0;flex:1;height:20px;border:1px solid var(--dvw-accent);border-radius:0;background:var(--dvw-surface-1);color:var(--dvw-text);padding:0 3px;font:inherit;letter-spacing:0;outline:0}.dvw-tree-create-row{cursor:text}.dvw-tree-create-row:hover{background:transparent}.dvw-tree-create-input::placeholder{color:var(--dvw-text-3)}.dvw-tree-create-input[aria-invalid=true]{border-color:var(--dvw-error)}.dvw-tree-create-error{min-height:20px;padding-top:2px;padding-right:6px;padding-bottom:3px;color:var(--dvw-error);font-size:11px;line-height:15px;white-space:normal;overflow-wrap:anywhere}.dvw-tree-status{margin-left:auto;padding-left:6px;font-size:11px;font-weight:600;color:var(--dvw-warn)}.dvw-tree-status[data-status=M]{color:var(--dvw-warn)}.dvw-tree-status[data-status=A]{color:var(--dvw-success)}.dvw-tree-status[data-status=D]{color:var(--dvw-error)}.dvw-context-menu{position:fixed;z-index:40;min-width:148px;padding:4px;border:1px solid var(--dvw-border);border-radius:5px;background:var(--dvw-surface-3);box-shadow:0 6px 20px rgba(0,0,0,.3)}.dvw-context-menu button{width:100%;height:28px;border:0;border-radius:3px;background:transparent;color:var(--dvw-text);display:flex;align-items:center;gap:8px;padding:0 8px;text-align:left;font:inherit;cursor:pointer}.dvw-context-menu button:hover{background:var(--dvw-hover)}.dvw-context-menu .dvw-context-delete{color:var(--dvw-error)}
6305
6445
  .dvw-main{grid-column:3;grid-row:1;display:flex;min-width:0;min-height:0;flex-direction:column;background:var(--dvw-surface-1)}.dvw-tabs{height:36px;background:var(--dvw-base);display:flex;overflow-x:auto}.dvw-tab{height:36px;min-width:110px;max-width:260px;border:0;border-right:1px solid var(--dvw-border-soft);background:var(--dvw-base);color:var(--dvw-text-2);padding:0 8px 0 12px;display:flex;align-items:center;gap:8px;cursor:pointer}.dvw-tab[data-active=true]{background:var(--dvw-surface-1);color:var(--dvw-text);border-top:1px solid var(--dvw-accent)}.dvw-tab-name{overflow:hidden;text-overflow:ellipsis;flex:1;text-align:left}.dvw-editor{position:relative;flex:1;min-height:0}.dvw-editor-toolbar{position:absolute;z-index:3;top:0;right:0;left:0;height:36px;display:flex;align-items:center;justify-content:flex-end;gap:4px;padding:0 10px;border-bottom:1px solid var(--dvw-border-soft);background:var(--dvw-surface-1)}.dvw-editor-body{position:absolute;inset:0;min-height:0}.dvw-editor-body[data-toolbar=true]{top:36px}.dvw-mode-button{height:27px;padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--dvw-text-2);cursor:pointer}.dvw-mode-button:hover{background:var(--dvw-hover)}.dvw-mode-button[data-active=true]{border-color:var(--dvw-border);background:var(--dvw-active);color:var(--dvw-text)}.dvw-empty{height:100%;display:grid;place-items:center;color:var(--dvw-text-3);text-align:center}.dvw-empty strong{font-size:24px;color:var(--dvw-text-2)}.dvw-message{padding:12px;color:var(--dvw-warn)}
6306
6446
  .dvw-bottom{grid-column:3;grid-row:2;height:260px;min-height:120px;border-top:1px solid var(--dvw-border);background:var(--dvw-base);display:flex;flex-direction:column}.dvw-bottom-head{height:35px;display:flex;align-items:center;gap:14px;padding:0 12px}.dvw-bottom-title{border:0;border-bottom:1px solid var(--dvw-text);background:transparent;color:var(--dvw-text);height:35px}.dvw-bottom-body{flex:1;min-height:0}.dvw-status{grid-column:1/4;grid-row:3;background:var(--dvw-accent);color:#fff;display:flex;align-items:center;padding:0 8px;gap:18px;font-size:12px}.dvw-theme-control{position:relative;margin-left:auto}.dvw-theme-trigger{height:22px;min-width:112px;display:flex;align-items:center;gap:7px;padding:0 8px;border:0;border-radius:5px;background:color-mix(in srgb,#000 17%,transparent);color:#fff;cursor:pointer}.dvw-theme-trigger:hover{background:color-mix(in srgb,#000 27%,transparent)}.dvw-theme-orb{width:13px;height:13px;border:2px solid #fff;border-radius:50%;background:linear-gradient(90deg,#17181c 50%,#fff 50%)}.dvw-theme-orb.dark{background:#17181c}.dvw-theme-orb.light{background:#fff}.dvw-theme-menu{position:absolute;right:0;bottom:29px;width:224px;padding:6px;border:1px solid var(--dvw-border);border-radius:10px;background:var(--dvw-surface-3);box-shadow:0 10px 35px rgba(0,0,0,.28);color:var(--dvw-text)}.dvw-theme-menu button{width:100%;min-height:48px;border:0;border-radius:7px;background:transparent;color:inherit;display:grid;grid-template-columns:30px 1fr 20px;align-items:center;text-align:left;padding:6px;cursor:pointer}.dvw-theme-menu button:hover,.dvw-theme-menu button[aria-checked=true]{background:var(--dvw-hover)}.dvw-theme-menu strong,.dvw-theme-menu small{display:block}.dvw-theme-menu small{color:var(--dvw-text-3);margin-top:2px}.dvw-theme-swatch{width:22px;height:22px;border-radius:6px;border:1px solid var(--dvw-border);background:linear-gradient(135deg,#17181c 0 50%,#fff 50%)}.dvw-theme-swatch.dark{background:#17181c}.dvw-theme-swatch.light{background:#fff}
6307
6447
  .dvw-search-form{padding:8px;display:grid;gap:6px}.dvw-input,.dvw-textarea,.dvw-select{width:100%;border:1px solid var(--dvw-border);background:var(--dvw-surface-2);color:var(--dvw-text);padding:5px 7px;outline:none}.dvw-input:focus,.dvw-textarea:focus,.dvw-select:focus{border-color:var(--dvw-accent)}.dvw-options{display:flex;align-items:center;gap:8px;font-size:11px}.dvw-result-file{padding:5px 8px;font-weight:600}.dvw-result{padding:4px 8px 4px 20px;cursor:pointer;display:flex;gap:8px}.dvw-result:hover{background:var(--dvw-hover)}.dvw-line{color:var(--dvw-text-3);min-width:30px;text-align:right}.dvw-preview{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -6319,7 +6459,7 @@ body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-sc
6319
6459
  .dvw-chat{grid-column:4;grid-row:1/3;min-width:0;background:var(--dvw-base);border-left:1px solid var(--dvw-border);position:relative}
6320
6460
  .dvw-chat-head{height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 6px 0 12px;border-bottom:1px solid var(--dvw-border);background:var(--dvw-base);font-weight:600}
6321
6461
  .dvw-chat-head>span{display:flex;align-items:center;gap:7px}.dvw-chat-body{height:calc(100% - 36px);background:var(--dvw-base)}
6322
- .dvw-dsh-mark{min-width:18px;height:18px;display:inline-grid;place-items:center;color:currentColor;font-size:0}.dvw-dsh-mark svg{display:block}
6462
+ .dvw-dsh-mark{display:block}
6323
6463
  .dvw-chat-resizer{position:fixed;top:0;right:calc(var(--dvw-chat-width) - 4px);bottom:0;width:8px;z-index:4;cursor:ew-resize;touch-action:none}
6324
6464
  .dvw-chat-resizer:hover,.dvw-chat-resizer:active{background:var(--dvw-accent)}
6325
6465
  .dvw-chat-toggle{position:absolute;z-index:5;top:6px;right:6px;border:1px solid var(--dvw-border);background:var(--dvw-base)}