dsh-vscode-workbench 0.1.11 → 0.1.12

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
@@ -7471,6 +7471,54 @@ window.__ModuleLoader__.load({
7471
7471
  return "file";
7472
7472
  }
7473
7473
  //#endregion
7474
+ //#region src/client/websocket.ts
7475
+ const WEB_PAGE_PROTOCOLS = /* @__PURE__ */ new Set([
7476
+ "http:",
7477
+ "https:",
7478
+ "blob:",
7479
+ "data:",
7480
+ "about:",
7481
+ "filesystem:"
7482
+ ]);
7483
+ /** Parse a shell-published base only when it is a credential-free HTTP(S) origin. */
7484
+ function networkBase(value) {
7485
+ if (typeof value !== "string" || value === "") return void 0;
7486
+ let url;
7487
+ try {
7488
+ url = new URL(value);
7489
+ } catch {
7490
+ return;
7491
+ }
7492
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
7493
+ if (url.username !== "" || url.password !== "" || url.host === "") return void 0;
7494
+ return {
7495
+ protocol: url.protocol,
7496
+ host: url.host
7497
+ };
7498
+ }
7499
+ /** Read the Host origin published by the DSH Desktop shell before application boot. */
7500
+ function transportStreamBaseUrl(globals = globalThis) {
7501
+ const transport = globals.__DSH_TRANSPORT__;
7502
+ return typeof transport?.streamBaseUrl === "string" ? transport.streamBaseUrl : void 0;
7503
+ }
7504
+ /**
7505
+ * Resolve an absolute workbench route to a WebSocket URL.
7506
+ *
7507
+ * Network pages keep their own origin. Application-delivered pages such as
7508
+ * `dsh-app://app/` have no WebSocket transport of their own, so they must use
7509
+ * the local Host origin published by the desktop shell.
7510
+ */
7511
+ function websocketUrl(path, location = globalThis.location, streamBaseUrl = transportStreamBaseUrl()) {
7512
+ const route = path.startsWith("/") ? path : `/${path}`;
7513
+ if (WEB_PAGE_PROTOCOLS.has(location.protocol)) {
7514
+ if (location.host === "") return void 0;
7515
+ return `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${route}`;
7516
+ }
7517
+ const base = networkBase(streamBaseUrl);
7518
+ if (base === void 0) return void 0;
7519
+ return `${base.protocol === "https:" ? "wss" : "ws"}://${base.host}${route}`;
7520
+ }
7521
+ //#endregion
7474
7522
  //#region src/client/terminal.ts
7475
7523
  async function request(method, body) {
7476
7524
  const response = await fetch(`/dsh-vscode-terminal/api/${method}`, {
@@ -7491,14 +7539,23 @@ window.__ModuleLoader__.load({
7491
7539
  var SocketConnection = class {
7492
7540
  socket;
7493
7541
  listeners = /* @__PURE__ */ new Set();
7542
+ errorListeners = /* @__PURE__ */ new Set();
7543
+ closed = false;
7494
7544
  queue = [];
7495
7545
  constructor(sessionId, id) {
7496
- const protocol = location.protocol === "https:" ? "wss" : "ws";
7497
- this.socket = new WebSocket(`${protocol}://${location.host}/dsh-vscode-terminal/ws?sessionId=${encodeURIComponent(sessionId)}&id=${encodeURIComponent(id)}`);
7546
+ const url = websocketUrl(`/dsh-vscode-terminal/ws?sessionId=${encodeURIComponent(sessionId)}&id=${encodeURIComponent(id)}`);
7547
+ if (url === void 0) throw new Error("无法连接终端:桌面端没有发布本地 Host 地址");
7548
+ this.socket = new WebSocket(url);
7498
7549
  this.socket.onopen = () => {
7499
7550
  for (const value of this.queue) this.socket.send(value);
7500
7551
  this.queue = [];
7501
7552
  };
7553
+ this.socket.onerror = () => {
7554
+ for (const listener of this.errorListeners) listener("终端 WebSocket 连接失败");
7555
+ };
7556
+ this.socket.onclose = () => {
7557
+ if (!this.closed) for (const listener of this.errorListeners) listener("终端 WebSocket 连接已断开");
7558
+ };
7502
7559
  this.socket.onmessage = (event) => {
7503
7560
  try {
7504
7561
  const value = JSON.parse(String(event.data));
@@ -7530,12 +7587,17 @@ window.__ModuleLoader__.load({
7530
7587
  });
7531
7588
  }
7532
7589
  close() {
7590
+ this.closed = true;
7533
7591
  this.socket.close();
7534
7592
  }
7535
7593
  subscribe(listener) {
7536
7594
  this.listeners.add(listener);
7537
7595
  return () => this.listeners.delete(listener);
7538
7596
  }
7597
+ subscribeError(listener) {
7598
+ this.errorListeners.add(listener);
7599
+ return () => this.errorListeners.delete(listener);
7600
+ }
7539
7601
  };
7540
7602
  /** Identify automatic terminal-emulator replies that must not be echoed during replay. */
7541
7603
  function isTerminalProtocolReply(data) {
@@ -7572,12 +7634,21 @@ window.__ModuleLoader__.load({
7572
7634
  }
7573
7635
  }
7574
7636
  async create(title) {
7575
- const terminal = await request("create", {
7576
- sessionId: this.sessionId,
7577
- ...title === void 0 ? {} : { title }
7578
- });
7579
- await this.refresh();
7580
- return terminal;
7637
+ try {
7638
+ const terminal = await request("create", {
7639
+ sessionId: this.sessionId,
7640
+ ...title === void 0 ? {} : { title }
7641
+ });
7642
+ await this.refresh();
7643
+ return terminal;
7644
+ } catch (error) {
7645
+ this.set({
7646
+ ...this.snapshot,
7647
+ loading: false,
7648
+ error: error instanceof Error ? error.message : String(error)
7649
+ });
7650
+ throw error;
7651
+ }
7581
7652
  }
7582
7653
  async close(id) {
7583
7654
  await request("close", {
@@ -7643,7 +7714,14 @@ window.__ModuleLoader__.load({
7643
7714
  const fit = new import_addon_fit.FitAddon();
7644
7715
  terminal.loadAddon(fit);
7645
7716
  terminal.open(root.current);
7646
- const connection = workspace.connect(id);
7717
+ let connection;
7718
+ try {
7719
+ connection = workspace.connect(id);
7720
+ } catch (exception) {
7721
+ setError(exception instanceof Error ? exception.message : String(exception));
7722
+ terminal.dispose();
7723
+ return;
7724
+ }
7647
7725
  const output = connection.subscribe((value) => {
7648
7726
  if (value.replay) {
7649
7727
  replaying.current = true;
@@ -7655,6 +7733,7 @@ window.__ModuleLoader__.load({
7655
7733
  }, 250);
7656
7734
  });
7657
7735
  });
7736
+ const connectionError = connection.subscribeError(setError);
7658
7737
  const input = terminal.onData((data) => {
7659
7738
  if (!replaying.current || !isTerminalProtocolReply(data)) connection.write(data);
7660
7739
  });
@@ -7675,6 +7754,7 @@ window.__ModuleLoader__.load({
7675
7754
  terminalRef.current = void 0;
7676
7755
  clearTimeout(replayGuard.current);
7677
7756
  observer.disconnect();
7757
+ connectionError();
7678
7758
  input.dispose();
7679
7759
  resize.dispose();
7680
7760
  output();
@@ -7712,8 +7792,8 @@ window.__ModuleLoader__.load({
7712
7792
  workspace.refresh().then(() => {
7713
7793
  if (!cancelled && workspace.getSnapshot().terminals.length === 0) workspace.create().then((value) => {
7714
7794
  if (!cancelled) setActive(value.id);
7715
- });
7716
- });
7795
+ }).catch(() => void 0);
7796
+ }).catch(() => void 0);
7717
7797
  return () => {
7718
7798
  cancelled = true;
7719
7799
  };
@@ -7723,7 +7803,7 @@ window.__ModuleLoader__.load({
7723
7803
  }, [snapshot.terminals, active]);
7724
7804
  (0, react.useEffect)(() => {
7725
7805
  const create = () => {
7726
- workspace.create().then((value) => setActive(value.id));
7806
+ workspace.create().then((value) => setActive(value.id)).catch(() => void 0);
7727
7807
  };
7728
7808
  window.addEventListener("dvw-terminal-new", create);
7729
7809
  return () => window.removeEventListener("dvw-terminal-new", create);
@@ -7756,7 +7836,7 @@ window.__ModuleLoader__.load({
7756
7836
  workspace.create().then((value) => {
7757
7837
  setSplit((current) => [...current?.includes(first) ? current : [first], value.id]);
7758
7838
  setActive(value.id);
7759
- });
7839
+ }).catch(() => void 0);
7760
7840
  };
7761
7841
  const shown = (split ?? (active === void 0 ? [] : [active])).filter((id) => snapshot.terminals.some((value) => value.id === id));
7762
7842
  const selectTerminal = (id) => {
@@ -12086,6 +12166,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
12086
12166
  attached?.style.setProperty("--dvw-status-height", `${statusHeight}px`);
12087
12167
  };
12088
12168
  attach();
12169
+ document.body.setAttribute("data-dvw-conversation-active", "");
12089
12170
  const observer = new MutationObserver(attach);
12090
12171
  observer.observe(document.body, {
12091
12172
  childList: true,
@@ -12093,6 +12174,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
12093
12174
  });
12094
12175
  return () => {
12095
12176
  observer.disconnect();
12177
+ document.body.removeAttribute("data-dvw-conversation-active");
12096
12178
  attached?.removeAttribute("data-dvw-conversation-panel");
12097
12179
  attached?.style.removeProperty("--dvw-chat-width");
12098
12180
  attached?.style.removeProperty("--dvw-status-height");
@@ -46216,6 +46298,11 @@ section.${c}>footer { z-index: 1; }
46216
46298
  function resizedPanelWidth(startWidth, startX, nextX, direction, min, max) {
46217
46299
  return Math.max(min, Math.min(max, Math.round(startWidth + (nextX - startX) * direction)));
46218
46300
  }
46301
+ /** Retry a workspace lookup that raced the Host publishing its session after startup. */
46302
+ function workspaceRetryDelay(error, attempt) {
46303
+ if (!(error instanceof ApiError) || error.code !== "session-not-found" || attempt >= 10) return void 0;
46304
+ return Math.min(250 * (attempt + 1), 1e3);
46305
+ }
46219
46306
  function PanelResizer({ className, label, width, min, max, defaultWidth, direction, onWidth }) {
46220
46307
  const [dragging, setDragging] = (0, react.useState)(false);
46221
46308
  const stopDragging = (0, react.useRef)(() => {});
@@ -46281,6 +46368,7 @@ section.${c}>footer { z-index: 1; }
46281
46368
  }
46282
46369
  function unloadedTab(path) {
46283
46370
  return {
46371
+ instance: Symbol(path),
46284
46372
  path,
46285
46373
  content: "",
46286
46374
  draft: "",
@@ -46293,6 +46381,12 @@ section.${c}>footer { z-index: 1; }
46293
46381
  preview: /\.md$/i.test(path)
46294
46382
  };
46295
46383
  }
46384
+ function WorkbenchTitlebar({ name }) {
46385
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
46386
+ className: "dvw-titlebar",
46387
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "DSH Workbench" }), name !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name })]
46388
+ });
46389
+ }
46296
46390
  /** Restore tab headers without reading their file contents. */
46297
46391
  function restoreTabPaths(paths) {
46298
46392
  return [...new Set(paths)].map(unloadedTab);
@@ -46345,13 +46439,16 @@ section.${c}>footer { z-index: 1; }
46345
46439
  api("fs.list", {
46346
46440
  sessionId,
46347
46441
  path
46348
- }).then(setRows).catch((error) => setError(String(error)));
46442
+ }).then((value) => {
46443
+ setRows(value);
46444
+ setError(void 0);
46445
+ }).catch((error) => setError(String(error)));
46349
46446
  }, [sessionId, path]);
46350
46447
  (0, react.useEffect)(load, [load]);
46351
46448
  (0, react.useEffect)(() => {
46352
46449
  const handler = (event) => {
46353
46450
  const paths = event.detail;
46354
- if (paths === void 0 || directoryAffected(path, paths)) load();
46451
+ if (!Array.isArray(paths) || directoryAffected(path, paths)) load();
46355
46452
  };
46356
46453
  window.addEventListener("dvw-files-changed", handler);
46357
46454
  return () => window.removeEventListener("dvw-files-changed", handler);
@@ -47515,7 +47612,7 @@ section.${c}>footer { z-index: 1; }
47515
47612
  const tabsRef = (0, react.useRef)(tabs);
47516
47613
  tabsRef.current = tabs;
47517
47614
  const workspaceRequests = (0, react.useRef)(new WorkspaceRequestScope());
47518
- const loadingTabs = (0, react.useRef)(/* @__PURE__ */ new Set());
47615
+ const loadingTabs = (0, react.useRef)(/* @__PURE__ */ new Map());
47519
47616
  const dshDark = useDshDarkTheme();
47520
47617
  const [theme, setTheme] = (0, react.useState)(() => {
47521
47618
  const saved = localStorage.getItem("dvw:theme");
@@ -47528,6 +47625,11 @@ section.${c}>footer { z-index: 1; }
47528
47625
  (0, react.useEffect)(() => localStorage.setItem("dvw:dsh-chat-width", String(Math.round(chatWidth))), [chatWidth]);
47529
47626
  (0, react.useEffect)(() => localStorage.setItem("dvw:primary-side-visible", primarySideVisible ? "open" : "closed"), [primarySideVisible]);
47530
47627
  (0, react.useEffect)(() => localStorage.setItem("dvw:status-visible", statusVisible ? "open" : "closed"), [statusVisible]);
47628
+ (0, react.useEffect)(() => {
47629
+ if (!snapshot.visible) return;
47630
+ document.body.setAttribute("data-dvw-workbench-active", "");
47631
+ return () => document.body.removeAttribute("data-dvw-workbench-active");
47632
+ }, [snapshot.visible]);
47531
47633
  (0, react.useEffect)(() => {
47532
47634
  if (!snapshot.visible) return;
47533
47635
  const onKey = (event) => {
@@ -47552,7 +47654,7 @@ section.${c}>footer { z-index: 1; }
47552
47654
  setActive(path);
47553
47655
  };
47554
47656
  (0, react.useEffect)(() => {
47555
- if (!snapshot.visible || sessionId === void 0) return;
47657
+ if (sessionId === void 0) return;
47556
47658
  const epoch = workspaceRequests.current.begin();
47557
47659
  const cleared = resetWorkspaceDocuments();
47558
47660
  loadingTabs.current.clear();
@@ -47562,113 +47664,177 @@ section.${c}>footer { z-index: 1; }
47562
47664
  setActive(cleared.active);
47563
47665
  setError(cleared.error);
47564
47666
  const controller = new AbortController();
47565
- api("workspace", { sessionId }, controller.signal).then((value) => {
47566
- if (!workspaceRequests.current.isCurrent(epoch)) return;
47567
- setWorkspace(value);
47568
- const saved = localStorage.getItem(`dvw:${value.cwd}`);
47569
- if (saved) try {
47570
- const parsed = JSON.parse(saved);
47571
- if (parsed.view) service.show(parsed.view);
47572
- if (typeof parsed.panelHeight === "number") setPanelHeight(parsed.panelHeight);
47573
- if (typeof parsed.sideWidth === "number") setSideWidth(Math.max(210, Math.min(640, Math.round(parsed.sideWidth))));
47574
- const restored = restoreTabPaths(parsed.paths ?? []);
47667
+ const load = async () => {
47668
+ for (let attempt = 0;; attempt++) try {
47669
+ const value = await api("workspace", { sessionId }, controller.signal);
47670
+ if (!workspaceRequests.current.isCurrent(epoch)) return;
47671
+ setWorkspace(value);
47672
+ let restored = [];
47673
+ let selected;
47674
+ try {
47675
+ const parsed = JSON.parse(localStorage.getItem(`dvw:${value.cwd}`) ?? "{}");
47676
+ if (parsed.view && service.getSnapshot().visible) service.show(parsed.view);
47677
+ if (typeof parsed.panelHeight === "number") setPanelHeight(parsed.panelHeight);
47678
+ if (typeof parsed.sideWidth === "number") setSideWidth(Math.max(210, Math.min(640, Math.round(parsed.sideWidth))));
47679
+ restored = restoreTabPaths(parsed.paths ?? []);
47680
+ selected = parsed.active;
47681
+ const drafts = JSON.parse(localStorage.getItem(`dvw:drafts:${sessionId}:${value.cwd}`) ?? "[]");
47682
+ if (Array.isArray(drafts)) for (const draft of drafts) {
47683
+ if (!draft || typeof draft.path !== "string" || typeof draft.content !== "string" || typeof draft.draft !== "string" || typeof draft.revision !== "string" || draft.path.startsWith("diff:")) continue;
47684
+ const tab = {
47685
+ ...unloadedTab(draft.path),
47686
+ content: draft.content,
47687
+ draft: draft.draft,
47688
+ revision: draft.revision,
47689
+ dirty: draft.draft !== draft.content,
47690
+ loaded: true
47691
+ };
47692
+ restored = [...restored.filter((row) => row.path !== tab.path), tab];
47693
+ }
47694
+ } catch {}
47575
47695
  setTabs(restored);
47576
- setActive(restored.some((tab) => tab.path === parsed.active) ? parsed.active : restored.at(-1)?.path);
47577
- } catch {}
47578
- }).catch((error) => {
47579
- if (!controller.signal.aborted && workspaceRequests.current.isCurrent(epoch)) setError(String(error));
47580
- });
47696
+ setActive(restored.some((tab) => tab.path === selected) ? selected : restored.at(-1)?.path);
47697
+ return;
47698
+ } catch (error) {
47699
+ if (controller.signal.aborted || !workspaceRequests.current.isCurrent(epoch)) return;
47700
+ const delay = workspaceRetryDelay(error, attempt);
47701
+ if (delay === void 0) {
47702
+ setError(String(error));
47703
+ return;
47704
+ }
47705
+ await new Promise((resolve) => setTimeout(resolve, delay));
47706
+ if (controller.signal.aborted || !workspaceRequests.current.isCurrent(epoch)) return;
47707
+ }
47708
+ };
47709
+ load();
47581
47710
  return () => {
47582
47711
  controller.abort();
47583
47712
  loadingTabs.current.clear();
47584
47713
  workspaceRequests.current.invalidate(epoch);
47585
47714
  };
47586
- }, [snapshot.visible, sessionId]);
47715
+ }, [sessionId, cwd]);
47587
47716
  (0, react.useEffect)(() => {
47588
- if (workspace) localStorage.setItem(`dvw:${workspace.cwd}`, JSON.stringify({
47589
- view: snapshot.view,
47590
- paths: tabs.filter((t) => !t.path.startsWith("diff:")).map((t) => t.path),
47591
- active,
47592
- panelHeight,
47593
- sideWidth
47594
- }));
47717
+ if (!workspace || sessionId === void 0) return;
47718
+ try {
47719
+ localStorage.setItem(`dvw:${workspace.cwd}`, JSON.stringify({
47720
+ view: snapshot.view,
47721
+ paths: tabs.filter((t) => !t.path.startsWith("diff:")).map((t) => t.path),
47722
+ active,
47723
+ panelHeight,
47724
+ sideWidth
47725
+ }));
47726
+ localStorage.setItem(`dvw:drafts:${sessionId}:${workspace.cwd}`, JSON.stringify(tabs.filter((tab) => tab.dirty).map(({ path, content, draft, revision }) => ({
47727
+ path,
47728
+ content,
47729
+ draft,
47730
+ revision
47731
+ }))));
47732
+ } catch {
47733
+ setError("无法保存工作区草稿,请在离开页面前保存文件。");
47734
+ }
47595
47735
  }, [
47596
47736
  workspace,
47737
+ sessionId,
47597
47738
  snapshot.view,
47598
47739
  tabs,
47599
47740
  active,
47600
47741
  panelHeight,
47601
47742
  sideWidth
47602
47743
  ]);
47603
- (0, react.useEffect)(() => {
47604
- if (!snapshot.visible || sessionId === void 0) return;
47605
- const protocol = location.protocol === "https:" ? "wss" : "ws";
47606
- const ws = new WebSocket(`${protocol}://${location.host}/dsh-vscode/ws/files?sessionId=${encodeURIComponent(sessionId)}`);
47607
- ws.onmessage = (event) => {
47608
- let paths = [];
47609
- try {
47610
- paths = JSON.parse(String(event.data)).paths ?? [];
47611
- } catch {}
47612
- window.dispatchEvent(new CustomEvent("dvw-files-changed", { detail: paths }));
47613
- setTabs((current) => current.map((tab) => tab.dirty ? {
47614
- ...tab,
47615
- external: true
47616
- } : tab));
47617
- };
47618
- return () => ws.close();
47619
- }, [snapshot.visible, sessionId]);
47620
- (0, react.useEffect)(() => {
47621
- if (sessionId === void 0 || active === void 0) return;
47622
- const tab = tabsRef.current.find((row) => row.path === active);
47623
- if (tab === void 0 || tab.loaded !== false || loadingTabs.current.has(active)) return;
47744
+ const loadTab = (0, react.useCallback)((path) => {
47745
+ if (sessionId === void 0) return;
47624
47746
  const epoch = workspaceRequests.current.current();
47625
- loadingTabs.current.add(active);
47626
- const documentPreview = documentPreviewKind(active);
47627
- if (documentPreview !== void 0) {
47628
- const documentUrl = `/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(active)}`;
47629
- setTabs((rows) => rows.map((row) => row.path === active ? {
47747
+ const token = Symbol(path);
47748
+ loadingTabs.current.set(path, token);
47749
+ const current = () => workspaceRequests.current.isCurrent(epoch) && loadingTabs.current.get(path) === token;
47750
+ const documentPreview = documentPreviewKind(path);
47751
+ const image = /\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(path);
47752
+ if (documentPreview !== void 0 || image) {
47753
+ const url = `/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(path)}&v=${Date.now()}`;
47754
+ setTabs((rows) => rows.map((row) => row.path === path ? {
47630
47755
  ...row,
47631
47756
  loaded: true,
47632
- documentPreview,
47633
- documentUrl
47634
- } : row));
47635
- loadingTabs.current.delete(active);
47636
- return;
47637
- }
47638
- if (/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(active)) {
47639
- const imageUrl = `/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(active)}`;
47640
- setTabs((rows) => rows.map((row) => row.path === active ? {
47641
- ...row,
47642
- loaded: true,
47643
- imageUrl
47757
+ ...image ? { imageUrl: url } : {
47758
+ documentPreview,
47759
+ documentUrl: url
47760
+ }
47644
47761
  } : row));
47645
- loadingTabs.current.delete(active);
47762
+ loadingTabs.current.delete(path);
47646
47763
  return;
47647
47764
  }
47648
47765
  api("fs.read", {
47649
47766
  sessionId,
47650
- path: active
47767
+ path
47651
47768
  }).then((doc) => {
47652
- if (!workspaceRequests.current.isCurrent(epoch)) return;
47769
+ if (!current()) return;
47653
47770
  if (doc.binary || doc.tooLarge) {
47654
- setTabs((rows) => rows.filter((row) => row.path !== active));
47655
- setActive((current) => current === active ? void 0 : current);
47656
- setError(doc.binary ? "二进制文件不能在文本编辑器中打开" : `文件超过 5 MiB:${active}`);
47771
+ setTabs((rows) => rows.map((row) => row.path === path ? {
47772
+ ...row,
47773
+ loaded: true,
47774
+ ...row.dirty ? {} : {
47775
+ binary: doc.binary,
47776
+ tooLarge: doc.tooLarge
47777
+ },
47778
+ external: true
47779
+ } : row));
47657
47780
  return;
47658
47781
  }
47659
- setTabs((rows) => rows.map((row) => row.path === active ? {
47782
+ setTabs((rows) => rows.map((row) => row.path !== path ? row : row.dirty ? {
47783
+ ...row,
47784
+ external: row.revision !== doc.revision
47785
+ } : {
47660
47786
  ...row,
47661
47787
  ...doc,
47662
47788
  draft: doc.content,
47663
- loaded: true
47664
- } : row));
47789
+ loaded: true,
47790
+ external: false
47791
+ }));
47665
47792
  }).catch((error) => {
47666
- if (workspaceRequests.current.isCurrent(epoch)) setError(String(error));
47667
- }).finally(() => loadingTabs.current.delete(active));
47793
+ if (current()) setError(String(error));
47794
+ }).finally(() => {
47795
+ if (current()) loadingTabs.current.delete(path);
47796
+ });
47797
+ }, [sessionId]);
47798
+ (0, react.useEffect)(() => {
47799
+ if (!snapshot.visible || sessionId === void 0 || workspace === void 0) return;
47800
+ const url = websocketUrl(`/dsh-vscode/ws/files?sessionId=${encodeURIComponent(sessionId)}`);
47801
+ if (url === void 0) return;
47802
+ const ws = new WebSocket(url);
47803
+ let closed = false;
47804
+ const refresh = (paths) => {
47805
+ if (closed) return;
47806
+ window.dispatchEvent(new CustomEvent("dvw-files-changed", { detail: paths }));
47807
+ for (const tab of tabsRef.current) {
47808
+ if (tab.path.startsWith("diff:") || tab.loaded === false) continue;
47809
+ if (paths === void 0 || paths.some((path) => tab.path === path || tab.path.startsWith(`${path}/`))) loadTab(tab.path);
47810
+ }
47811
+ };
47812
+ ws.onopen = () => refresh();
47813
+ ws.onmessage = (event) => {
47814
+ try {
47815
+ const value = JSON.parse(String(event.data));
47816
+ if (Array.isArray(value.paths) && value.paths.every((path) => typeof path === "string")) refresh(value.paths);
47817
+ } catch {}
47818
+ };
47819
+ return () => {
47820
+ closed = true;
47821
+ ws.close();
47822
+ };
47823
+ }, [
47824
+ snapshot.visible,
47825
+ sessionId,
47826
+ workspace,
47827
+ loadTab
47828
+ ]);
47829
+ (0, react.useEffect)(() => {
47830
+ if (sessionId === void 0 || active === void 0 || workspace === void 0) return;
47831
+ if (tabsRef.current.find((row) => row.path === active)?.loaded === false && !loadingTabs.current.has(active)) loadTab(active);
47668
47832
  }, [
47669
47833
  active,
47670
47834
  sessionId,
47671
- tabs
47835
+ workspace,
47836
+ tabs,
47837
+ loadTab
47672
47838
  ]);
47673
47839
  const open = (path, line, column) => {
47674
47840
  if (sessionId !== void 0) openForWorkspace(path, line, column);
@@ -47678,8 +47844,12 @@ section.${c}>footer { z-index: 1; }
47678
47844
  window.addEventListener("dvw-open-workspace-path", listener);
47679
47845
  return () => window.removeEventListener("dvw-open-workspace-path", listener);
47680
47846
  }, [sessionId, tabs]);
47847
+ const savingTabs = (0, react.useRef)(/* @__PURE__ */ new Set());
47681
47848
  const save = async (tab) => {
47682
- if (sessionId === void 0 || !tab.dirty) return;
47849
+ if (sessionId === void 0 || !tab.dirty || savingTabs.current.has(tab.path)) return;
47850
+ const epoch = workspaceRequests.current.current();
47851
+ savingTabs.current.add(tab.path);
47852
+ loadingTabs.current.delete(tab.path);
47683
47853
  try {
47684
47854
  const doc = await api("fs.save", {
47685
47855
  sessionId,
@@ -47687,27 +47857,34 @@ section.${c}>footer { z-index: 1; }
47687
47857
  content: tab.draft,
47688
47858
  revision: tab.revision
47689
47859
  });
47690
- setTabs((rows) => rows.map((row) => row.path === tab.path ? {
47860
+ if (!workspaceRequests.current.isCurrent(epoch)) return;
47861
+ loadingTabs.current.delete(tab.path);
47862
+ setTabs((rows) => rows.map((row) => row.path === tab.path && row.instance === tab.instance ? {
47691
47863
  ...row,
47692
47864
  ...doc,
47693
- draft: doc.content,
47694
- dirty: false,
47865
+ draft: row.draft,
47866
+ dirty: row.draft !== doc.content,
47695
47867
  external: false
47696
47868
  } : row));
47697
47869
  } catch (error) {
47698
- setError(String(error));
47870
+ if (workspaceRequests.current.isCurrent(epoch)) setError(String(error));
47871
+ } finally {
47872
+ savingTabs.current.delete(tab.path);
47699
47873
  }
47700
47874
  };
47701
47875
  const close = (tab) => {
47702
47876
  if (tab.dirty && !confirm(`${tab.path} 有未保存更改,确定关闭?`)) return;
47877
+ loadingTabs.current.delete(tab.path);
47703
47878
  setTabs((rows) => rows.filter((row) => row.path !== tab.path));
47704
47879
  if (active === tab.path) setActive(tabs.filter((row) => row.path !== tab.path).at(-1)?.path);
47705
47880
  };
47706
47881
  const closeAll = () => {
47707
47882
  const dirty = tabs.filter((tab) => tab.dirty);
47708
47883
  if (dirty.length > 0 && !confirm(`有 ${dirty.length} 个文件包含未保存更改,确定全部关闭?`)) return;
47884
+ loadingTabs.current.clear();
47709
47885
  setTabs([]);
47710
47886
  setActive(void 0);
47887
+ setError(void 0);
47711
47888
  };
47712
47889
  const activeTab = tabs.find((tab) => tab.path === active);
47713
47890
  const Panel = panels.find((panel) => panel.id === snapshot.bottomPanel)?.component;
@@ -47737,10 +47914,10 @@ section.${c}>footer { z-index: 1; }
47737
47914
  }
47738
47915
  };
47739
47916
  if (!snapshot.visible) return null;
47740
- if (sessionId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47917
+ if (sessionId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47741
47918
  className: "dvw-overlay",
47742
47919
  "data-dvw-theme": theme === "dsh" ? void 0 : theme,
47743
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47920
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkbenchTitlebar, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
47744
47921
  className: "dvw-empty",
47745
47922
  style: { gridColumn: "1/4" },
47746
47923
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
@@ -47751,7 +47928,7 @@ section.${c}>footer { z-index: 1; }
47751
47928
  children: "返回 DSH"
47752
47929
  })
47753
47930
  ] })
47754
- })
47931
+ })]
47755
47932
  });
47756
47933
  const markdown = activeTab !== void 0 && !activeTab.path.startsWith("diff:") && /\.md$/i.test(activeTab.path);
47757
47934
  const diffMarkdown = activeTab?.diff !== void 0 && /\.md$/i.test(activeTab.diffSourcePath ?? "");
@@ -47773,6 +47950,7 @@ section.${c}>footer { z-index: 1; }
47773
47950
  onDragOverCapture: suppressAttachmentDrop,
47774
47951
  onDropCapture: suppressAttachmentDrop,
47775
47952
  children: [
47953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkbenchTitlebar, { name: workspace?.name }),
47776
47954
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DshConversationPanel, {
47777
47955
  visible: chatVisible && !settingsOpen,
47778
47956
  width: chatWidth,
@@ -47961,9 +48139,9 @@ section.${c}>footer { z-index: 1; }
47961
48139
  className: "dvw-side",
47962
48140
  "data-side-width": sideWidth,
47963
48141
  children: [
47964
- snapshot.view === "explorer" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Explorer, {
48142
+ snapshot.view === "explorer" && workspace !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Explorer, {
47965
48143
  sessionId,
47966
- cwd: workspace?.cwd,
48144
+ cwd: workspace.cwd,
47967
48145
  selected: active,
47968
48146
  onOpen: (path) => void open(path)
47969
48147
  }),
@@ -48164,6 +48342,12 @@ section.${c}>footer { z-index: 1; }
48164
48342
  source: activeTab.draft,
48165
48343
  sessionId,
48166
48344
  path: activeTab.path
48345
+ }) : activeTab.binary || activeTab.tooLarge ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48346
+ className: "dvw-message",
48347
+ children: activeTab.binary ? "二进制文件不能在文本编辑器中打开" : `文件超过 5 MiB:${activeTab.path}`
48348
+ }) : activeTab.loaded === false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48349
+ className: "dvw-message",
48350
+ children: "正在加载文件…"
48167
48351
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MonacoEditor, {
48168
48352
  uri: `${workspace?.cwd ?? ""}/${activeTab.path}`,
48169
48353
  value: activeTab.draft,
@@ -48257,11 +48441,13 @@ section.${c}>footer { z-index: 1; }
48257
48441
  //#region src/client/styles.ts
48258
48442
  const CSS$1 = String.raw`
48259
48443
  @font-face{font-family:codicon;font-display:block;src:url('/dsh-vscode/codicons/codicon.ttf') format('truetype')}
48444
+ :root{--dvw-titlebar-height:0px}html[data-platform=darwin]{--dvw-titlebar-height:var(--dsh-frame-top-clearance,48px)}
48260
48445
  .codicon{display:inline-block;font:normal normal normal 16px/1 codicon;text-align:center;text-decoration:none;text-rendering:auto;-webkit-font-smoothing:antialiased;user-select:none}
48261
48446
  .codicon-add:before{content:"\ea60"}.codicon-git-branch:before,.codicon-source-control:before{content:"\ea68"}.codicon-search:before{content:"\ea6d"}.codicon-edit:before{content:"\ea73"}.codicon-close:before{content:"\ea76"}.codicon-close-all:before{content:"\eac1"}.codicon-cloud-upload:before{content:"\eac3"}.codicon-file:before{content:"\ea7b"}.codicon-new-file:before{content:"\ea7f"}.codicon-new-folder:before{content:"\ea80"}.codicon-folder:before{content:"\ea83"}.codicon-trash:before{content:"\ea81"}.codicon-code:before{content:"\eac4"}.codicon-check:before{content:"\eab2"}.codicon-chevron-down:before{content:"\eab4"}.codicon-chevron-right:before{content:"\eab6"}.codicon-color-mode:before{content:"\eac6"}.codicon-diff:before,.codicon-diff-sidebyside:before{content:"\eae1"}.codicon-diff-single:before{content:"\ec22"}.codicon-discard:before{content:"\eae2"}.codicon-file-code:before{content:"\eae9"}.codicon-file-media:before{content:"\eaea"}.codicon-file-pdf:before{content:"\eaeb"}.codicon-file-zip:before{content:"\eaef"}.codicon-files:before{content:"\eaf0"}.codicon-folder-opened:before{content:"\eaf7"}.codicon-git-commit:before{content:"\eafc"}.codicon-graph:before{content:"\eb03"}.codicon-home:before{content:"\eb06"}.codicon-list-tree:before{content:"\eb86"}.codicon-preview:before{content:"\eb2f"}.codicon-refresh:before{content:"\eb37"}.codicon-remove:before{content:"\eb3b"}.codicon-check-all:before{content:"\ebb1"}
48262
48447
  .codicon-layout-sidebar-right:before{content:"\ebf4"}.codicon-layout-sidebar-right-off:before{content:"\ec00"}.codicon-settings:before{content:"\eb51"}.codicon-sparkle:before{content:"\ec10"}.codicon-split-horizontal:before{content:"\eb56"}.codicon-terminal:before{content:"\ea85"}
48263
48448
  .dvw-launch{display:flex;align-items:center;gap:10px;width:100%;height:36px;padding:0 9px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#aaa);cursor:pointer;font:inherit}.dvw-launch:hover{background:var(--dsw-alias-interactive-bg-hover,#292929);color:var(--dsw-alias-label-primary,#fff)}.dvw-launcher-mount{position:fixed;z-index:20;right:16px;bottom:16px}.dvw-launcher-mount .dvw-launch{width:auto;height:32px;padding:0 12px;justify-content:center;border:1px solid var(--dsw-alias-border-l3,#e5e7eb);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 2px rgba(0,0,0,.06);color:var(--dsw-alias-label-primary,#333)}body[data-ds-dark-theme] .dvw-launcher-mount .dvw-launch{color:var(--dsw-alias-label-primary,#ddd)}@media(max-width:640px){.dvw-launcher-mount{right:8px;bottom:8px}.dvw-launcher-mount .dvw-launch{height:30px;padding:0 10px;font-size:12px}}
48264
- .dvw-overlay{--dvw-side-width:210px;--dvw-base:var(--dsw-alias-bg-base,#181818);--dvw-surface-1:var(--dsw-alias-bg-layer-1,#1e1e1e);--dvw-surface-2:var(--dsw-alias-bg-layer-2,#252526);--dvw-surface-3:var(--dsw-alias-bg-layer-3,#2d2d30);--dvw-text:var(--dsw-alias-label-primary,#ccc);--dvw-text-2:var(--dsw-alias-label-secondary,#aaa);--dvw-text-3:var(--dsw-alias-label-tertiary,#858585);--dvw-border:var(--dsw-alias-border-l2,#333);--dvw-border-soft:var(--dsw-alias-border-l3,#292929);--dvw-hover:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));--dvw-active:var(--dsw-alias-interactive-bg-active,rgba(255,255,255,.14));--dvw-accent:var(--dsw-alias-state-business-primary,#3b82f6);--dvw-error:var(--dsw-alias-state-error-primary,#f48771);--dvw-success:var(--dsw-alias-state-success-primary,#73c991);--dvw-warn:var(--dsw-alias-state-warn-primary,#d7ba7d);position:absolute;inset:0;z-index:100;display:grid;grid-template-columns:48px var(--dvw-side-width) minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto 24px;background:var(--dvw-base);color:var(--dvw-text);pointer-events:auto;font:13px/1.4 var(--dsw-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif);overflow:hidden;color-scheme:light}
48449
+ .dvw-overlay{--dvw-side-width:210px;--dvw-base:var(--dsw-alias-bg-base,#181818);--dvw-surface-1:var(--dsw-alias-bg-layer-1,#1e1e1e);--dvw-surface-2:var(--dsw-alias-bg-layer-2,#252526);--dvw-surface-3:var(--dsw-alias-bg-layer-3,#2d2d30);--dvw-text:var(--dsw-alias-label-primary,#ccc);--dvw-text-2:var(--dsw-alias-label-secondary,#aaa);--dvw-text-3:var(--dsw-alias-label-tertiary,#858585);--dvw-border:var(--dsw-alias-border-l2,#333);--dvw-border-soft:var(--dsw-alias-border-l3,#292929);--dvw-hover:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));--dvw-active:var(--dsw-alias-interactive-bg-active,rgba(255,255,255,.14));--dvw-accent:var(--dsw-alias-state-business-primary,#3b82f6);--dvw-error:var(--dsw-alias-state-error-primary,#f48771);--dvw-success:var(--dsw-alias-state-success-primary,#73c991);--dvw-warn:var(--dsw-alias-state-warn-primary,#d7ba7d);position:absolute;inset:0;z-index:100;display:grid;grid-template-columns:48px var(--dvw-side-width) minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto 24px;box-sizing:border-box;padding-top:var(--dvw-titlebar-height);background:var(--dvw-base);color:var(--dvw-text);pointer-events:auto;font:13px/1.4 var(--dsw-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif);overflow:hidden;color-scheme:light}
48450
+ .dvw-titlebar{display:none}html[data-platform=darwin] .dvw-titlebar{position:absolute;top:0;right:0;left:0;z-index:6;display:flex;align-items:center;gap:8px;height:var(--dvw-titlebar-height);padding:0 12px 0 84px;border-bottom:1px solid var(--dvw-border-soft);background:var(--dvw-base);color:var(--dvw-text-3);font-size:12px;-webkit-app-region:drag;user-select:none}.dvw-titlebar strong{flex:none;font-weight:600;color:var(--dvw-text-2)}.dvw-titlebar span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
48265
48451
  body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-scheme:dark}
48266
48452
  .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}
48267
48453
  .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}
@@ -48285,11 +48471,11 @@ body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-sc
48285
48471
  .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}
48286
48472
  .dvw-chat-head>span{display:flex;align-items:center;gap:7px}.dvw-chat-body{height:calc(100% - 36px);background:var(--dvw-base)}
48287
48473
  .dvw-dsh-mark{display:block}
48288
- .dvw-chat-resizer{position:fixed;top:36px;right:calc(var(--dvw-chat-width) - 2px);bottom:0;width:4px;z-index:4;cursor:ew-resize;touch-action:none}
48474
+ .dvw-chat-resizer{position:fixed;top:var(--dvw-titlebar-height);right:calc(var(--dvw-chat-width) - 2px);bottom:0;width:4px;z-index:4;cursor:ew-resize;touch-action:none}
48289
48475
  .dvw-chat-resizer:hover,.dvw-chat-resizer:focus-visible,.dvw-chat-resizer[data-dragging=true]{background:var(--dvw-accent)}
48290
48476
  .dvw-side-resizer,.dvw-chat-resizer{width:10px;cursor:ew-resize;touch-action:none;background:transparent}
48291
48477
  .dvw-side-resizer{position:relative;transform:translateX(5px);z-index:22}
48292
- .dvw-chat-resizer{position:fixed;top:36px;right:calc(var(--dvw-chat-width) - 5px);z-index:22}
48478
+ .dvw-chat-resizer{position:fixed;top:var(--dvw-titlebar-height);right:calc(var(--dvw-chat-width) - 5px);z-index:22}
48293
48479
  .dvw-side-resizer::after,.dvw-chat-resizer::after{content:"";position:absolute;top:0;bottom:0;left:4px;width:2px;background:var(--dvw-border)}
48294
48480
  .dvw-chat-resizer::after{left:1px;width:6px}
48295
48481
  .dvw-side-resizer:hover,.dvw-side-resizer:focus-visible,.dvw-side-resizer[data-dragging=true],.dvw-chat-resizer:hover,.dvw-chat-resizer:focus-visible,.dvw-chat-resizer[data-dragging=true]{background:transparent}
@@ -48299,7 +48485,10 @@ body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-sc
48299
48485
  .dvw-side-resizer:hover::after,.dvw-side-resizer:focus-visible::after,.dvw-side-resizer[data-dragging=true]::after,.dvw-chat-resizer:hover::after,.dvw-chat-resizer:focus-visible::after,.dvw-chat-resizer[data-dragging=true]::after{background:var(--dvw-accent)}
48300
48486
  .dvw-chat-toggle{position:absolute;z-index:5;top:6px;right:6px;border:1px solid var(--dvw-border);background:var(--dvw-base)}
48301
48487
  .dvw-overlay:not([data-dvw-chat=true]) .dvw-tabs{padding-right:36px}
48302
- [data-dvw-conversation-panel]{position:fixed!important;top:36px!important;right:0!important;bottom:var(--dvw-status-height,24px)!important;left:auto!important;width:var(--dvw-chat-width)!important;height:auto!important;min-width:0!important;z-index:21!important;border-left:1px solid var(--dsw-alias-border-l2)!important;box-shadow:none!important}
48488
+ [data-dvw-conversation-panel]{position:fixed!important;top:var(--dvw-titlebar-height)!important;right:0!important;bottom:var(--dvw-status-height,24px)!important;left:auto!important;width:var(--dvw-chat-width)!important;height:auto!important;min-width:0!important;z-index:21!important;border-left:1px solid var(--dsw-alias-border-l2)!important;box-shadow:none!important}
48489
+ body[data-dvw-workbench-active] [data-sidebar-right-expand],body[data-dvw-conversation-active] [data-sidebar-right-expand]{display:none!important}
48490
+ body[data-dvw-workbench-active] [data-shell-leading]{display:none!important}
48491
+ body[data-dvw-workbench-active] [data-sidebar-toggle]{display:none!important}
48303
48492
  .dvw-confirm-backdrop{position:fixed;z-index:50;inset:0;display:grid;place-items:center;padding:24px;background:rgba(0,0,0,.38)}.dvw-confirm{width:min(380px,calc(100vw - 48px));border:1px solid var(--dvw-border);border-radius:7px;background:var(--dvw-surface-1);box-shadow:0 12px 36px rgba(0,0,0,.34);padding:18px}.dvw-confirm h2{margin:0 0 12px;font-size:15px}.dvw-confirm p{margin:0;color:var(--dvw-text)}.dvw-confirm-note{margin-top:8px!important;color:var(--dvw-text-3)!important;font-size:12px}.dvw-confirm footer{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.dvw-confirm footer button{height:28px;border:1px solid var(--dvw-border);border-radius:4px;background:var(--dvw-surface-2);color:var(--dvw-text);padding:0 12px;font:inherit;cursor:pointer}.dvw-confirm footer button:hover{background:var(--dvw-hover)}.dvw-confirm footer .dvw-danger-button{border-color:color-mix(in srgb,var(--dvw-error) 65%,var(--dvw-border));background:var(--dvw-error);color:#fff}.dvw-confirm footer .dvw-danger-button:hover{background:color-mix(in srgb,var(--dvw-error) 82%,#000)}
48304
48493
  .dvw-tree[data-dvw-native-drop-target=true]{outline:1px dashed var(--dvw-accent);outline-offset:-3px}.dvw-tree-row[data-dvw-native-drop-target=true]{background:var(--dvw-active)}
48305
48494
  .dvw-act:disabled{opacity:.4;cursor:not-allowed}