@artenx/dshb 0.1.2 → 0.1.4

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/cordis.patch.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  - insert:
2
2
  - id: dshb-auth
3
3
  name: "@artenx/dshb/auth"
4
+ - id: web-startup
5
+ disabled: true
4
6
  - insert:
5
7
  - id: dshb-web-startup
6
8
  name: "@artenx/dshb/auth/startup"
package/lib/client.js CHANGED
@@ -530,6 +530,8 @@ window.__ModuleLoader__.load({
530
530
  const [error, setError] = (0, react.useState)(void 0);
531
531
  const [busy, setBusy] = (0, react.useState)(false);
532
532
  const [newDirName, setNewDirName] = (0, react.useState)("");
533
+ const [pendingInstruction, setPendingInstruction] = (0, react.useState)(void 0);
534
+ const [instructionDialogOpen, setInstructionDialogOpen] = (0, react.useState)(false);
533
535
  (0, react.useEffect)(() => {
534
536
  if (!props.open) return;
535
537
  (async () => {
@@ -602,24 +604,25 @@ window.__ModuleLoader__.load({
602
604
  try {
603
605
  const node = nodes.find((n) => n.id === nodeId);
604
606
  if (!node) return;
605
- if (node.type === "local-host") {
606
- props.onPicked(path);
607
- return;
608
- }
609
- const res = await fetch("/api/dshb/workspaces/bind", {
610
- method: "POST",
611
- headers: { "content-type": "application/json" },
612
- body: JSON.stringify({
613
- nodeId,
614
- remotePath: path
615
- })
616
- });
617
- const data = await res.json();
618
- if (!res.ok || !data.ok || !data.mirrorPath) {
619
- setError(data.error ?? "工作区注册失败");
620
- return;
607
+ let pickedPath = path;
608
+ if (node.type !== "local-host") {
609
+ const res = await fetch("/api/dshb/workspaces/bind", {
610
+ method: "POST",
611
+ headers: { "content-type": "application/json" },
612
+ body: JSON.stringify({
613
+ nodeId,
614
+ remotePath: path
615
+ })
616
+ });
617
+ const data = await res.json();
618
+ if (!res.ok || !data.ok || !data.mirrorPath) {
619
+ setError(data.error ?? "工作区注册失败");
620
+ return;
621
+ }
622
+ pickedPath = data.mirrorPath;
621
623
  }
622
- props.onPicked(data.mirrorPath);
624
+ props.onPicked(pickedPath);
625
+ if (pendingInstruction !== void 0 && pendingInstruction.trim()) savePendingInstruction(pickedPath, pendingInstruction);
623
626
  } catch {
624
627
  setError("网络错误");
625
628
  } finally {
@@ -629,15 +632,16 @@ window.__ModuleLoader__.load({
629
632
  nodeId,
630
633
  path,
631
634
  nodes,
632
- props
635
+ props,
636
+ pendingInstruction
633
637
  ]);
634
638
  if (!props.open) return null;
635
639
  const node = nodes.find((n) => n.id === nodeId);
636
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
640
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
637
641
  style: overlayStyle,
638
642
  role: "dialog",
639
643
  "aria-label": "添加工作区",
640
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
644
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
641
645
  style: dialogStyle,
642
646
  children: [
643
647
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -858,28 +862,177 @@ window.__ModuleLoader__.load({
858
862
  justifyContent: "flex-end",
859
863
  gap: 10
860
864
  },
865
+ children: [
866
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
867
+ type: "button",
868
+ onClick: props.onCancel,
869
+ style: secondaryButtonStyle$1,
870
+ children: "取消"
871
+ }),
872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
873
+ type: "button",
874
+ onClick: () => setInstructionDialogOpen(true),
875
+ disabled: busy || props.busy || !nodeId || !path,
876
+ style: {
877
+ ...secondaryButtonStyle$1,
878
+ borderColor: pendingInstruction !== void 0 && pendingInstruction.trim() ? "var(--dsw-alias-state-business-primary)" : "var(--dsw-alias-border-l2)",
879
+ color: pendingInstruction !== void 0 && pendingInstruction.trim() ? "var(--dsw-alias-state-business-primary)" : "var(--dsw-alias-label-primary)"
880
+ },
881
+ children: ["指令", pendingInstruction !== void 0 && pendingInstruction.trim() ? " ●" : ""]
882
+ }),
883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
884
+ type: "button",
885
+ onClick: () => void confirm(),
886
+ disabled: busy || props.busy || !nodeId || !path,
887
+ style: primaryButtonStyle$1,
888
+ children: "设为工作区"
889
+ })
890
+ ]
891
+ })
892
+ ]
893
+ }), instructionDialogOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstructionPreviewDialog, {
894
+ text: pendingInstruction ?? "",
895
+ onClose: () => setInstructionDialogOpen(false),
896
+ onSave: (t) => {
897
+ setPendingInstruction(t);
898
+ setInstructionDialogOpen(false);
899
+ }
900
+ })]
901
+ });
902
+ }
903
+ function dirnameOf(path) {
904
+ const parts = path.split("/").filter(Boolean);
905
+ parts.pop();
906
+ return parts.length === 0 ? "/" : `/${parts.join("/")}`;
907
+ }
908
+ const pendingInstructions = /* @__PURE__ */ new Map();
909
+ function savePendingInstruction(path, text) {
910
+ pendingInstructions.set(path, text);
911
+ (async () => {
912
+ try {
913
+ let attempts = 0;
914
+ const tryResolve = async () => {
915
+ if (attempts++ > 10) return;
916
+ const ws = ((window.workspaces?.list?.getSnapshot?.())?.items ?? []).find((w) => w.path === path);
917
+ if (!ws) {
918
+ await new Promise((r) => setTimeout(r, 300));
919
+ return tryResolve();
920
+ }
921
+ await fetch(`/api/dshb/instructions/${ws.workspaceId}`, {
922
+ method: "PUT",
923
+ headers: { "content-type": "application/json" },
924
+ body: JSON.stringify({
925
+ text,
926
+ expectedRevision: 0
927
+ })
928
+ });
929
+ pendingInstructions.delete(path);
930
+ };
931
+ tryResolve();
932
+ } catch {}
933
+ })();
934
+ }
935
+ function InstructionPreviewDialog(props) {
936
+ const [text, setText] = (0, react.useState)(props.text);
937
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
938
+ style: {
939
+ position: "fixed",
940
+ inset: 0,
941
+ background: "var(--dsw-alias-overlay-mask, rgba(0,0,0,0.45))",
942
+ display: "flex",
943
+ alignItems: "center",
944
+ justifyContent: "center",
945
+ zIndex: 1100
946
+ },
947
+ onClick: props.onClose,
948
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
949
+ style: {
950
+ width: "min(92vw, 600px)",
951
+ maxWidth: "100%",
952
+ maxHeight: "85vh",
953
+ overflow: "auto",
954
+ overflowX: "hidden",
955
+ background: "var(--dsw-alias-bg-layer-1)",
956
+ border: "1px solid var(--dsw-alias-border-l2)",
957
+ borderRadius: 12,
958
+ padding: 20,
959
+ boxSizing: "border-box",
960
+ color: "var(--dsw-alias-label-primary)"
961
+ },
962
+ onClick: (e) => e.stopPropagation(),
963
+ children: [
964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
965
+ style: {
966
+ display: "flex",
967
+ justifyContent: "space-between",
968
+ alignItems: "center",
969
+ marginBottom: 14
970
+ },
971
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
972
+ style: {
973
+ margin: 0,
974
+ fontSize: 16,
975
+ color: "var(--dsw-alias-label-primary)"
976
+ },
977
+ children: "项目指令"
978
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
979
+ type: "button",
980
+ onClick: props.onClose,
981
+ style: secondaryButtonStyle$1,
982
+ children: "关闭"
983
+ })]
984
+ }),
985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
986
+ style: {
987
+ fontSize: 12,
988
+ color: "var(--dsw-alias-label-secondary)",
989
+ marginBottom: 10
990
+ },
991
+ children: "工作区创建后,此指令将自动保存为新工作区的项目指令。新对话首次交互时注入。"
992
+ }),
993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
994
+ style: {
995
+ width: "100%",
996
+ minHeight: "280px",
997
+ fontFamily: "monospace",
998
+ fontSize: 13,
999
+ padding: "12px",
1000
+ border: "1px solid var(--dsw-alias-border-l2)",
1001
+ borderRadius: 8,
1002
+ background: "var(--dsw-alias-bg-layer-2)",
1003
+ color: "var(--dsw-alias-label-primary)",
1004
+ resize: "vertical",
1005
+ boxSizing: "border-box",
1006
+ outline: "none"
1007
+ },
1008
+ value: text,
1009
+ onChange: (e) => setText(e.target.value),
1010
+ placeholder: "留空则不注入指令...",
1011
+ spellCheck: false
1012
+ }),
1013
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1014
+ style: {
1015
+ display: "flex",
1016
+ justifyContent: "flex-end",
1017
+ gap: 10,
1018
+ marginTop: 14
1019
+ },
861
1020
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
862
1021
  type: "button",
863
- onClick: props.onCancel,
1022
+ onClick: props.onClose,
864
1023
  style: secondaryButtonStyle$1,
865
1024
  children: "取消"
866
1025
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
867
1026
  type: "button",
868
- onClick: () => void confirm(),
869
- disabled: busy || props.busy || !nodeId || !path,
1027
+ onClick: () => props.onSave(text),
870
1028
  style: primaryButtonStyle$1,
871
- children: "设为工作区"
1029
+ children: "确定"
872
1030
  })]
873
1031
  })
874
1032
  ]
875
1033
  })
876
1034
  });
877
1035
  }
878
- function dirnameOf(path) {
879
- const parts = path.split("/").filter(Boolean);
880
- parts.pop();
881
- return parts.length === 0 ? "/" : `/${parts.join("/")}`;
882
- }
883
1036
  //#endregion
884
1037
  //#region src/client/mobile.tsx
885
1038
  /**
@@ -1762,6 +1915,215 @@ window.__ModuleLoader__.load({
1762
1915
  }, DrawerToggle));
1763
1916
  }
1764
1917
  //#endregion
1918
+ //#region src/client/workspace-instruction-menu.tsx
1919
+ let injectedRegistry;
1920
+ function setWorkspaceRegistry(reg) {
1921
+ injectedRegistry = reg;
1922
+ }
1923
+ function resolveWorkspaceByTitle(title) {
1924
+ const wks = injectedRegistry;
1925
+ if (!wks) return void 0;
1926
+ return wks.list.getSnapshot().items.find((w) => w.title === title);
1927
+ }
1928
+ const ROW_INJECT_FLAG = "data-dshb-instr-row-bound";
1929
+ const MENU_INJECT_FLAG = "data-dshb-instr-menu-bound";
1930
+ const MENU_ITEM_FLAG = "data-dshb-instr-menu-item";
1931
+ function mountInstructionDialog(workspaceId, workspaceTitle) {
1932
+ if (!workspaceId) return;
1933
+ const overlay = document.createElement("div");
1934
+ overlay.style.cssText = "position:fixed;inset:0;z-index:1100;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-overlay-mask,rgba(0,0,0,.45))";
1935
+ const dialog = document.createElement("div");
1936
+ dialog.setAttribute("role", "dialog");
1937
+ dialog.setAttribute("aria-label", `修改 ${workspaceTitle} 的项目指令`);
1938
+ dialog.style.cssText = "box-sizing:border-box;width:min(92vw,600px);max-height:85vh;overflow:auto;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)";
1939
+ const title = document.createElement("h2");
1940
+ title.textContent = `项目指令 - ${workspaceTitle}`;
1941
+ title.style.cssText = "margin:0 0 10px;font-size:16px";
1942
+ const description = document.createElement("div");
1943
+ description.textContent = "此内容会注入该工作区的新对话。已有对话继续使用创建时的指令快照。";
1944
+ description.style.cssText = "margin-bottom:10px;font-size:12px;color:var(--dsw-alias-label-secondary)";
1945
+ const textarea = document.createElement("textarea");
1946
+ textarea.placeholder = "留空则不注入指令...";
1947
+ textarea.spellcheck = false;
1948
+ textarea.style.cssText = "box-sizing:border-box;width:100%;min-height:280px;resize:vertical;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:13px monospace";
1949
+ const status = document.createElement("div");
1950
+ status.style.cssText = "min-height:18px;margin-top:6px;font-size:12px;color:var(--dsw-alias-label-secondary)";
1951
+ const actions = document.createElement("div");
1952
+ actions.style.cssText = "display:flex;justify-content:flex-end;gap:10px;margin-top:14px";
1953
+ const cancel = document.createElement("button");
1954
+ cancel.type = "button";
1955
+ cancel.textContent = "取消";
1956
+ cancel.style.cssText = "height:36px;padding:0 16px;border:1px solid var(--dsw-alias-border-l2);border-radius:18px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer";
1957
+ const save = document.createElement("button");
1958
+ save.type = "button";
1959
+ save.textContent = "保存";
1960
+ save.style.cssText = "height:36px;padding:0 16px;border:0;border-radius:18px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);cursor:pointer";
1961
+ const close = () => overlay.remove();
1962
+ cancel.addEventListener("click", close);
1963
+ overlay.addEventListener("click", (event) => {
1964
+ if (event.target === overlay) close();
1965
+ });
1966
+ dialog.addEventListener("click", (event) => event.stopPropagation());
1967
+ textarea.addEventListener("input", () => {
1968
+ const bytes = new TextEncoder().encode(textarea.value).length;
1969
+ status.textContent = `${bytes} / 32768 字节`;
1970
+ status.style.color = bytes > 32768 ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-label-secondary)";
1971
+ });
1972
+ save.addEventListener("click", () => {
1973
+ (async () => {
1974
+ const bytes = new TextEncoder().encode(textarea.value).length;
1975
+ if (bytes > 32768) {
1976
+ status.textContent = `指令超过 32768 字节限制(当前 ${bytes} 字节)`;
1977
+ status.style.color = "var(--dsw-alias-state-error-primary)";
1978
+ return;
1979
+ }
1980
+ save.disabled = true;
1981
+ save.textContent = "保存中...";
1982
+ try {
1983
+ const revision = Number(overlay.dataset.revision ?? "0");
1984
+ const response = await fetch(`/api/dshb/instructions/${workspaceId}`, {
1985
+ method: "PUT",
1986
+ headers: { "content-type": "application/json" },
1987
+ body: JSON.stringify({
1988
+ text: textarea.value,
1989
+ expectedRevision: revision
1990
+ })
1991
+ });
1992
+ if (!response.ok) {
1993
+ const body = await response.json().catch(() => ({}));
1994
+ status.textContent = body.error ?? `保存失败 (${response.status})`;
1995
+ status.style.color = "var(--dsw-alias-state-error-primary)";
1996
+ return;
1997
+ }
1998
+ close();
1999
+ } catch {
2000
+ status.textContent = "保存失败";
2001
+ status.style.color = "var(--dsw-alias-state-error-primary)";
2002
+ } finally {
2003
+ save.disabled = false;
2004
+ save.textContent = "保存";
2005
+ }
2006
+ })();
2007
+ });
2008
+ actions.append(cancel, save);
2009
+ dialog.append(title, description, textarea, status, actions);
2010
+ overlay.append(dialog);
2011
+ document.body.append(overlay);
2012
+ status.textContent = "加载中...";
2013
+ fetch(`/api/dshb/instructions/${workspaceId}`).then(async (response) => {
2014
+ if (!response.ok) throw new Error(`读取失败 (${response.status})`);
2015
+ return response.json();
2016
+ }).then((body) => {
2017
+ textarea.value = body.text ?? "";
2018
+ overlay.dataset.revision = String(body.revision ?? 0);
2019
+ textarea.dispatchEvent(new Event("input"));
2020
+ textarea.focus();
2021
+ }).catch((error) => {
2022
+ status.textContent = error instanceof Error ? error.message : "读取失败";
2023
+ status.style.color = "var(--dsw-alias-state-error-primary)";
2024
+ });
2025
+ }
2026
+ function registerWorkspaceInstructionMenu(ctx) {
2027
+ const wks = ctx.workspaces;
2028
+ if (wks) setWorkspaceRegistry(wks);
2029
+ let activeWorkspaceTitle = "";
2030
+ const findWorkspaceRow = (el) => {
2031
+ let node = el instanceof HTMLElement ? el : null;
2032
+ while (node) {
2033
+ if (node.className && typeof node.className === "string" && node.className.includes("projectRow")) return node;
2034
+ node = node.parentElement;
2035
+ }
2036
+ return null;
2037
+ };
2038
+ const getRowTitle = (row) => {
2039
+ const titleEl = row.querySelector("[class*=\"title\"]");
2040
+ if (!titleEl) return null;
2041
+ return titleEl.textContent ?? null;
2042
+ };
2043
+ const injectMenuItem = (menuContainer) => {
2044
+ if (menuContainer.getAttribute(MENU_INJECT_FLAG) || menuContainer.querySelector(`[${MENU_ITEM_FLAG}]`)) return;
2045
+ menuContainer.setAttribute(MENU_INJECT_FLAG, "1");
2046
+ const existingItems = menuContainer.querySelectorAll("button, [role=\"menuitem\"]");
2047
+ let refItem = null;
2048
+ for (const item of Array.from(existingItems)) {
2049
+ const text = item.textContent ?? "";
2050
+ if (text.includes("重命名") || text.includes("Rename")) {
2051
+ refItem = item;
2052
+ break;
2053
+ }
2054
+ }
2055
+ if (!refItem) refItem = existingItems[existingItems.length - 1];
2056
+ if (!refItem) return;
2057
+ const btn = document.createElement("button");
2058
+ btn.type = "button";
2059
+ btn.setAttribute("role", "menuitem");
2060
+ btn.setAttribute(MENU_ITEM_FLAG, "1");
2061
+ if (refItem instanceof HTMLElement) {
2062
+ btn.className = refItem.className || "";
2063
+ btn.style.cssText = refItem.style.cssText;
2064
+ }
2065
+ for (const className of Array.from(btn.classList)) if (/danger|delete/i.test(className)) btn.classList.remove(className);
2066
+ btn.style.color = "var(--dsw-alias-label-primary)";
2067
+ btn.style.display = "flex";
2068
+ btn.style.alignItems = "center";
2069
+ btn.style.gap = "8px";
2070
+ const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2071
+ icon.setAttribute("width", "16");
2072
+ icon.setAttribute("height", "16");
2073
+ icon.setAttribute("viewBox", "0 0 16 16");
2074
+ icon.setAttribute("fill", "none");
2075
+ icon.setAttribute("aria-hidden", "true");
2076
+ icon.style.flex = "none";
2077
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2078
+ path.setAttribute("d", "M3 11.75V13h1.25l7.37-7.37-1.25-1.25L3 11.75Zm9.59-7.34a.66.66 0 0 0 0-.94l-1.06-1.06a.66.66 0 0 0-.94 0l-.83.83 1.25 1.25.83-.83.75.75Z");
2079
+ path.setAttribute("fill", "currentColor");
2080
+ icon.append(path);
2081
+ const label = document.createElement("span");
2082
+ label.textContent = "修改指令";
2083
+ btn.append(icon, label);
2084
+ btn.addEventListener("click", (e) => {
2085
+ e.stopPropagation();
2086
+ e.preventDefault();
2087
+ mountInstructionDialog((activeWorkspaceTitle ? resolveWorkspaceByTitle(activeWorkspaceTitle) : void 0)?.workspaceId ?? "", activeWorkspaceTitle || "Unknown");
2088
+ });
2089
+ refItem.parentElement?.insertBefore(btn, refItem.nextSibling);
2090
+ };
2091
+ new MutationObserver(() => {
2092
+ const projectRows = document.querySelectorAll("[class*=\"projectRow\"]");
2093
+ for (const row of Array.from(projectRows)) {
2094
+ const rowEl = row;
2095
+ if (rowEl.getAttribute(ROW_INJECT_FLAG)) continue;
2096
+ rowEl.setAttribute(ROW_INJECT_FLAG, "1");
2097
+ const iconBtns = rowEl.querySelectorAll("button[class*=\"iconButton\"]");
2098
+ for (const btn of Array.from(iconBtns)) {
2099
+ const label = btn.getAttribute("aria-label") ?? "";
2100
+ if (label.includes("workspace") || label.includes("操作") || label.includes("actions")) btn.addEventListener("click", () => {
2101
+ const title = getRowTitle(rowEl);
2102
+ if (title) activeWorkspaceTitle = title;
2103
+ });
2104
+ }
2105
+ }
2106
+ const menuItems = document.querySelectorAll("[role=\"menuitem\"]");
2107
+ for (const item of Array.from(menuItems)) {
2108
+ const text = item.textContent ?? "";
2109
+ if (text.includes("删除") || text.includes("Delete") || text.includes("重命名") || text.includes("Rename")) {
2110
+ const container = item.closest("[role=\"menu\"]") ?? item.parentElement;
2111
+ if (container instanceof HTMLElement && !container.getAttribute(MENU_INJECT_FLAG)) {
2112
+ const row = findWorkspaceRow(container);
2113
+ if (row) {
2114
+ const title = getRowTitle(row);
2115
+ if (title) activeWorkspaceTitle = title;
2116
+ }
2117
+ injectMenuItem(container);
2118
+ }
2119
+ }
2120
+ }
2121
+ }).observe(document.body, {
2122
+ childList: true,
2123
+ subtree: true
2124
+ });
2125
+ }
2126
+ //#endregion
1765
2127
  //#region src/client/ui.tsx
1766
2128
  const SECTION_ID = "dshb-nodes";
1767
2129
  const EMPTY_FORM = {
@@ -2678,6 +3040,7 @@ window.__ModuleLoader__.load({
2678
3040
  `;
2679
3041
  document.head.appendChild(globalStyle);
2680
3042
  installMobile(ctx);
3043
+ registerWorkspaceInstructionMenu(ctx);
2681
3044
  ctx.plugin({
2682
3045
  inject: ["slots", "settingsScope"],
2683
3046
  apply: (sub) => {