zcode-pro 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -23,6 +23,7 @@ ZCode Pro enhances the ZCode desktop UI. No client files are modified; just laun
23
23
  | Idle project memory reclaim | Projects unused for a while with no running tasks release their memory automatically, and unused ones are reclaimed once after startup; sessions are untouched and reload when reopened (durations adjustable) |
24
24
  | Plugin card sandbox reclaim | Releases the oldest plugin-card sandboxes once the cap is hit (the one being viewed goes last), so new cards keep opening after the 64-slot pool fills up; released cards come back when their session is reopened (cap adjustable) |
25
25
  | Sidebar menu in the top bar | Turns "New task / Search / Automations / Plugin store" into icon buttons in the top navigation bar: hover shows the name, highlighted while its view is open, shortcuts still work |
26
+ | Plugin panels on the new tab page | The built-in "new tab page" only lists built-in panels and recommended official plugins; this adds cards for third-party marketplace plugin panels (headroom, rtk, …) that open in the side pane |
26
27
  | Collapsible Pinned section | The sidebar "Pinned" header collapses/expands its task list; the state is remembered |
27
28
  | File menu actions | Adds "Open with default app" and "Reveal in file manager" to file links in chat |
28
29
  | Copy image | Right-click an image in chat or the enlarged preview to copy it to the clipboard |
package/README.md CHANGED
@@ -23,6 +23,7 @@ ZCode 桌面版界面增强工具。不修改客户端文件,通过 ZCode Pro
23
23
  | 空闲项目内存回收 | 长时间未使用且无任务运行的项目自动释放其占用的内存,启动后也会自动回收一次未使用的项目;会话记录不受影响,再次打开时自动恢复(时长可调) |
24
24
  | 插件界面沙箱回收 | 插件卡片沙箱数量达到上限时释放最早创建的(正在查看的排最后),避免 64 个名额耗尽后新卡片打不开;被释放的旧卡片重新打开所在会话即恢复(上限可调) |
25
25
  | 侧栏菜单并入顶栏 | “新建任务 / 搜索 / 自动化 / 插件市场”收成顶部导航栏的图标按钮:悬停显示名称,对应界面打开时高亮,快捷键照常可用 |
26
+ | 新标签页插件面板入口 | 官方“新标签页”只列官方面板与推荐的官方插件;本功能把第三方市场插件(headroom、rtk 等)的面板卡片加进去,点击在侧边面板打开 |
26
27
  | 已置顶分区可折叠 | 侧边栏“已置顶”标题可点击折叠/展开其任务列表,状态记住 |
27
28
  | 文件菜单增强 | 会话中文件链接的右键菜单新增“默认应用打开”“打开所在目录” |
28
29
  | 复制图片 | 右键会话中的图片或放大的预览图,复制到剪贴板 |
package/dist/inject.js CHANGED
@@ -255,6 +255,10 @@
255
255
  switcherUnseen: "\u672A\u67E5\u770B",
256
256
  switcherAttentionHint: "\u8FD0\u884C\u4E2D / \u8FD0\u884C\u5B8C\u672A\u67E5\u770B\u7684\u4EFB\u52A1 \xB7 {prev} / {next} \u6216 \u2191 / \u2193 \u9009\u62E9\uFF0CEnter \u6216\u677E\u5F00 alt \u5207\u6362",
257
257
  switcherAttentionEmpty: "\u6682\u65E0\u8FD0\u884C\u4E2D\u6216\u672A\u67E5\u770B\u7684\u4EFB\u52A1",
258
+ featureNewtabPanels: "\u65B0\u6807\u7B7E\u9875\u63D2\u4EF6\u9762\u677F\u5165\u53E3",
259
+ featureNewtabPanelsDesc: "\u5B98\u65B9\u201C\u65B0\u6807\u7B7E\u9875\u201D\u53EA\u5217\u5B98\u65B9\u9762\u677F\u4E0E\u63A8\u8350\u7684\u5B98\u65B9\u63D2\u4EF6\uFF0C\u6B64\u529F\u80FD\u628A\u7B2C\u4E09\u65B9\u5E02\u573A\u63D2\u4EF6\uFF08headroom\u3001rtk \u7B49\uFF09\u7684\u9762\u677F\u5361\u7247\u52A0\u8FDB\u53BB\uFF0C\u70B9\u51FB\u5728\u4FA7\u8FB9\u9762\u677F\u6253\u5F00\u3002",
260
+ ntpDescSuffix: "\u63D2\u4EF6\u9762\u677F",
261
+ ntpNeedSession: "\u5148\u6253\u5F00\u4E00\u4E2A\u4EFB\u52A1\u4F1A\u8BDD\uFF0C\u518D\u4ECE\u65B0\u6807\u7B7E\u9875\u6253\u5F00\u63D2\u4EF6\u9762\u677F",
258
262
  tabShortcuts: "\u5FEB\u6377\u952E",
259
263
  hkTitle: "\u4F1A\u8BDD\u5FEB\u6377\u5207\u6362",
260
264
  hkAltZ: "\u5728\u5F53\u524D\u4E0E\u4E0A\u6B21\u4F7F\u7528\u7684\u4F1A\u8BDD\u95F4\u6765\u56DE\u5207\u6362",
@@ -675,6 +679,10 @@
675
679
  switcherUnseen: "unseen",
676
680
  switcherAttentionHint: "Running / finished but unseen tasks \xB7 {prev} / {next} or \u2191 / \u2193 to choose, Enter or release alt to switch",
677
681
  switcherAttentionEmpty: "No running or unseen tasks",
682
+ featureNewtabPanels: "Plugin panels on the new tab page",
683
+ featureNewtabPanelsDesc: 'The built-in "new tab page" only lists built-in panels and recommended official plugins; this adds cards for third-party marketplace plugin panels (headroom, rtk, \u2026) that open in the side pane.',
684
+ ntpDescSuffix: "plugin panel",
685
+ ntpNeedSession: "Open a task session first, then open plugin panels from the new tab page",
678
686
  tabShortcuts: "Shortcuts",
679
687
  hkTitle: "Session quick switch",
680
688
  hkAltZ: "Toggle between the current and the last-used session",
@@ -2842,10 +2850,10 @@
2842
2850
  function startImageMenu() {
2843
2851
  if (typeof window === "undefined") return;
2844
2852
  if (typeof navigator.clipboard?.write !== "function" || typeof window.ClipboardItem === "undefined") return;
2845
- let enabled6 = true;
2853
+ let enabled7 = true;
2846
2854
  const refresh = () => {
2847
2855
  void getConfig().then((c) => {
2848
- enabled6 = !(c.features && c.features.imageCopy === false);
2856
+ enabled7 = !(c.features && c.features.imageCopy === false);
2849
2857
  });
2850
2858
  };
2851
2859
  refresh();
@@ -2854,7 +2862,7 @@
2854
2862
  const img = copyTargetOf(e.target, e.clientX, e.clientY);
2855
2863
  if (!img) return;
2856
2864
  e.preventDefault();
2857
- if (enabled6) showMenu(e.clientX, e.clientY, img);
2865
+ if (enabled7) showMenu(e.clientX, e.clientY, img);
2858
2866
  refresh();
2859
2867
  }, true);
2860
2868
  }
@@ -4002,6 +4010,11 @@
4002
4010
  if (await setFeature("wsRunningSpin", next)) f.wsRunningSpin = next;
4003
4011
  refreshRows();
4004
4012
  }),
4013
+ settingRow(L.featureNewtabPanels, L.featureNewtabPanelsDesc, f.newtabPanels !== false, async () => {
4014
+ const next = !(f.newtabPanels !== false);
4015
+ if (await setFeature("newtabPanels", next)) f.newtabPanels = next;
4016
+ refreshRows();
4017
+ }),
4005
4018
  (() => {
4006
4019
  const repoLink = h("span", {
4007
4020
  class: "cursor-pointer underline-offset-4 hover:underline",
@@ -7417,6 +7430,191 @@
7417
7430
  window.addEventListener("blur", () => closePopup(false));
7418
7431
  }
7419
7432
 
7433
+ // src/inject/features/newtab-panels.js
7434
+ var GRID_HINTS = ["\u8FD0\u884C\u547D\u4EE4\u548C\u67E5\u770B\u8F93\u51FA", "Run commands and view output"];
7435
+ var OFFICIAL_SUFFIX = "@zcode-plugins-official";
7436
+ var OPEN_FN_MARK = "\u6253\u5F00\u63D2\u4EF6 UI";
7437
+ var enabled3 = true;
7438
+ var injectTimer = 0;
7439
+ function eachFiber(visit) {
7440
+ const rootEl = document.getElementById("root");
7441
+ const key = rootEl && Object.keys(rootEl).find((k) => k.startsWith("__reactContainer$"));
7442
+ if (!key) return;
7443
+ let visited = 0;
7444
+ const walk = (f) => {
7445
+ if (!f || visited > 2e5) return;
7446
+ visited++;
7447
+ if (visit(f)) return true;
7448
+ return walk(f.child) || walk(f.sibling);
7449
+ };
7450
+ walk(rootEl[key]);
7451
+ }
7452
+ function findOpenSidePane() {
7453
+ let found = null;
7454
+ eachFiber((f) => {
7455
+ const et = f.elementType;
7456
+ if (et && typeof et === "object" && typeof et.$$typeof === "symbol" && String(et.$$typeof).includes("react.context")) {
7457
+ const val = f.memoizedProps && f.memoizedProps.value;
7458
+ if (typeof val === "function" && val.toString().includes(OPEN_FN_MARK)) {
7459
+ found = val;
7460
+ return true;
7461
+ }
7462
+ }
7463
+ return false;
7464
+ });
7465
+ return found;
7466
+ }
7467
+ function findBridge() {
7468
+ let holder = null;
7469
+ const seen = /* @__PURE__ */ new Set();
7470
+ const scan4 = (o) => {
7471
+ if (!o || typeof o !== "object" || seen.has(o) || holder) return;
7472
+ seen.add(o);
7473
+ if (typeof o.pluginUiBridgeService === "object" && o.pluginUiBridgeService) {
7474
+ holder = o.pluginUiBridgeService;
7475
+ return;
7476
+ }
7477
+ for (const v of Object.values(o)) if (v && typeof v === "object") scan4(v);
7478
+ };
7479
+ eachFiber((f) => {
7480
+ for (const h2 of [f.memoizedProps, f.memoizedState]) {
7481
+ if (!h2 || typeof h2 !== "object") continue;
7482
+ scan4(h2);
7483
+ if (holder) return true;
7484
+ }
7485
+ return false;
7486
+ });
7487
+ return holder;
7488
+ }
7489
+ function sessionScope() {
7490
+ const rows = [...document.querySelectorAll("[data-task-item-key]")];
7491
+ const cur = rows.find((e) => (e.className + "").includes("bg-selected")) || rows[rows.length - 1];
7492
+ const key = cur ? cur.getAttribute("data-task-item-key") : "";
7493
+ if (!key) return null;
7494
+ const i = key.indexOf(":sess_");
7495
+ return i > 0 ? { workspacePath: key.slice(0, i), sessionId: key.slice(i + 1) } : null;
7496
+ }
7497
+ async function thirdPartySurfaces(bridge, workspacePath) {
7498
+ try {
7499
+ const list = await bridge.listSurfaces({ workspacePath });
7500
+ return (list || []).filter((s) => !String(s.pluginId || "").endsWith(OFFICIAL_SUFFIX));
7501
+ } catch {
7502
+ return [];
7503
+ }
7504
+ }
7505
+ function surfaceTitle(s, locale) {
7506
+ const title = s.title;
7507
+ const pick = (obj) => {
7508
+ const lang = (locale || "").split("-")[0];
7509
+ return obj[locale] || lang && obj[lang] || Object.entries(obj).find(([k]) => k.toLowerCase().startsWith(lang || "\0"))?.[1] || obj.en || Object.values(obj)[0] || "";
7510
+ };
7511
+ if (typeof title === "string") return title;
7512
+ if (title && typeof title === "object") {
7513
+ return pick(title) || s.pluginName || s.id;
7514
+ }
7515
+ return s.pluginName || s.id;
7516
+ }
7517
+ var PANEL_ICON_PATHS = ['<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>'];
7518
+ function findNewTabGrid() {
7519
+ const item = document.querySelector("[data-side-pane-open-tab-item]");
7520
+ if (!item || item.offsetParent === null) return null;
7521
+ const grid = item.parentElement;
7522
+ if (!grid || grid === document.body) return null;
7523
+ const text = grid.textContent || "";
7524
+ return GRID_HINTS.some((h2) => text.includes(h2)) ? grid : null;
7525
+ }
7526
+ async function injectPanels(grid) {
7527
+ if (grid.dataset.zcodeproPanels) return;
7528
+ const template = grid.querySelector("[data-side-pane-open-tab-item]");
7529
+ if (!template) return;
7530
+ const scope = sessionScope();
7531
+ if (!scope) return;
7532
+ const bridge = findBridge();
7533
+ if (!bridge) return;
7534
+ const surfaces = await thirdPartySurfaces(bridge, scope.workspacePath);
7535
+ if (!surfaces.length) {
7536
+ grid.dataset.zcodeproPanels = "1";
7537
+ return;
7538
+ }
7539
+ grid.dataset.zcodeproPanels = "1";
7540
+ const L = t();
7541
+ const locale = isZhLocale() ? "zh-CN" : "en-US";
7542
+ for (const s of surfaces) {
7543
+ const card = template.cloneNode(true);
7544
+ card.removeAttribute("data-side-pane-open-tab-item");
7545
+ card.setAttribute("data-zcodepro-panel", s.id);
7546
+ card.setAttribute("data-zcodepro-panel-name", s.pluginName || "");
7547
+ const title = surfaceTitle(s, locale);
7548
+ const desc = (s.pluginName || "") + " \xB7 " + L.ntpDescSuffix;
7549
+ const titleEl = card.querySelector("span.font-semibold");
7550
+ if (titleEl) titleEl.textContent = title;
7551
+ const descEl = card.querySelector("span[title]");
7552
+ if (descEl) {
7553
+ descEl.textContent = desc;
7554
+ descEl.setAttribute("title", desc);
7555
+ }
7556
+ const svg = card.querySelector("svg");
7557
+ if (svg) {
7558
+ svg.innerHTML = PANEL_ICON_PATHS[0];
7559
+ svg.setAttribute("viewBox", "0 0 24 24");
7560
+ }
7561
+ card.addEventListener("click", (ev) => {
7562
+ ev.stopPropagation();
7563
+ const sc = sessionScope();
7564
+ const open = findOpenSidePane();
7565
+ if (!sc || !open) {
7566
+ showToast(L.ntpNeedSession, "error");
7567
+ return;
7568
+ }
7569
+ try {
7570
+ open({
7571
+ parentSessionId: sc.sessionId,
7572
+ pluginId: s.pluginId,
7573
+ surfaceId: s.id,
7574
+ serverName: s.server,
7575
+ resourceUri: s.resourceUri,
7576
+ remoteSessionId: null,
7577
+ title,
7578
+ workspacePath: sc.workspacePath
7579
+ });
7580
+ } catch (e) {
7581
+ console.warn("[zcodepro] \u6253\u5F00\u63D2\u4EF6\u9762\u677F\u5931\u8D25", e);
7582
+ showToast(L.ntpNeedSession, "error");
7583
+ }
7584
+ });
7585
+ grid.append(card);
7586
+ }
7587
+ }
7588
+ function tryInject() {
7589
+ if (!enabled3 || document.hidden) return;
7590
+ const grid = findNewTabGrid();
7591
+ if (!grid) return;
7592
+ if (grid.dataset.zcodeproPanels && !grid.querySelector("[data-zcodepro-panel]")) {
7593
+ delete grid.dataset.zcodeproPanels;
7594
+ }
7595
+ void injectPanels(grid);
7596
+ }
7597
+ function startNewtabPanels() {
7598
+ ensureStyle();
7599
+ const refreshEnabled = () => {
7600
+ void getConfig().then((cfg) => {
7601
+ enabled3 = cfg.features ? cfg.features.newtabPanels !== false : true;
7602
+ }).catch(() => {
7603
+ });
7604
+ };
7605
+ refreshEnabled();
7606
+ window.addEventListener("zcodepro:config-changed", refreshEnabled);
7607
+ const observer2 = new MutationObserver(() => {
7608
+ if (injectTimer) return;
7609
+ injectTimer = setTimeout(() => {
7610
+ injectTimer = 0;
7611
+ tryInject();
7612
+ }, 300);
7613
+ });
7614
+ observer2.observe(document.body, { childList: true, subtree: true });
7615
+ setInterval(tryInject, 5e3);
7616
+ }
7617
+
7420
7618
  // src/inject/features/title-lock.js
7421
7619
  var TASK_SEL2 = "[data-task-item-key]";
7422
7620
  var SCAN_DEBOUNCE_MS3 = 150;
@@ -7427,7 +7625,7 @@
7427
7625
  var MAP_REFRESH_MS = 6e4;
7428
7626
  var installed3 = false;
7429
7627
  var seenRows = /* @__PURE__ */ new WeakSet();
7430
- var enabled3 = true;
7628
+ var enabled4 = true;
7431
7629
  var pinned = null;
7432
7630
  var resolved = false;
7433
7631
  var fetching = null;
@@ -7451,8 +7649,8 @@
7451
7649
  const res = await rpc("/pinned-titles");
7452
7650
  if (!res || !res.ok) return null;
7453
7651
  resolved = true;
7454
- enabled3 = res.enabled !== false;
7455
- if (!enabled3) {
7652
+ enabled4 = res.enabled !== false;
7653
+ if (!enabled4) {
7456
7654
  pinned = null;
7457
7655
  return null;
7458
7656
  }
@@ -7467,7 +7665,7 @@
7467
7665
  }
7468
7666
  }
7469
7667
  function isMismatch(item) {
7470
- if (!enabled3 || !pinned) return null;
7668
+ if (!enabled4 || !pinned) return null;
7471
7669
  const key = item.getAttribute("data-task-item-key") || "";
7472
7670
  const want = pinned.get(sessIdOf(key));
7473
7671
  if (want === void 0) return null;
@@ -7485,7 +7683,7 @@
7485
7683
  }
7486
7684
  function scheduleEnforce(key, { immediate = false } = {}) {
7487
7685
  if (immediate) {
7488
- if (enabled3 && pinned) {
7686
+ if (enabled4 && pinned) {
7489
7687
  const want = pinned.get(sessIdOf(key));
7490
7688
  if (want !== void 0) {
7491
7689
  for (const item of findRows(key)) {
@@ -7526,7 +7724,7 @@
7526
7724
  }
7527
7725
  }
7528
7726
  function scan3() {
7529
- if (!enabled3 || !pinned || pinned.size === 0) return;
7727
+ if (!enabled4 || !pinned || pinned.size === 0) return;
7530
7728
  for (const item of document.querySelectorAll(TASK_SEL2)) {
7531
7729
  const key = isMismatch(item);
7532
7730
  if (!key) {
@@ -7572,7 +7770,7 @@
7572
7770
  return;
7573
7771
  }
7574
7772
  mapRefreshTimer = 0;
7575
- if (enabled3 && resolved) void fetchPinned().then((m) => {
7773
+ if (enabled4 && resolved) void fetchPinned().then((m) => {
7576
7774
  if (m) scan3();
7577
7775
  });
7578
7776
  };
@@ -7618,7 +7816,7 @@
7618
7816
  renameCtx = null;
7619
7817
  return;
7620
7818
  }
7621
- if (!enabled3 || !pinned) return;
7819
+ if (!enabled4 || !pinned) return;
7622
7820
  const want = pinned.get(sessIdOf(renameCtx.key));
7623
7821
  if (want === void 0) return;
7624
7822
  for (const el of document.querySelectorAll('[role="dialog"] input, [role="dialog"] textarea, [role="dialog"] [contenteditable="true"]')) {
@@ -7981,7 +8179,7 @@
7981
8179
  var SCAN_FALLBACK_MS5 = 5e3;
7982
8180
  var ROW_SEL = "[data-task-item-key]";
7983
8181
  var MENU_SEL = '[data-testid="task-new-button"], [data-testid="automations-open"], [data-testid="plugin-store-sidebar-open"], [data-testid="sidebar-search-open"], [data-slot="command"], [data-testid="desktop-top-nav-back"]';
7984
- var enabled4 = true;
8182
+ var enabled5 = true;
7985
8183
  var rowEl = null;
7986
8184
  var origs = null;
7987
8185
  var syncTimer = 0;
@@ -8124,7 +8322,7 @@
8124
8322
  return scroll.getBoundingClientRect().width < NARROW_SIDEBAR_PX;
8125
8323
  }
8126
8324
  function sync() {
8127
- if (!enabled4) return;
8325
+ if (!enabled5) return;
8128
8326
  const parts = findParts();
8129
8327
  if (!parts) {
8130
8328
  if (rowEl) rowEl.style.display = "none";
@@ -8186,8 +8384,8 @@
8186
8384
  }
8187
8385
  async function refreshConfig2() {
8188
8386
  const cfg = await getConfig(true).catch(() => null);
8189
- enabled4 = !cfg || !cfg.features || cfg.features.toolbarIcons !== false;
8190
- if (enabled4) sync();
8387
+ enabled5 = !cfg || !cfg.features || cfg.features.toolbarIcons !== false;
8388
+ if (enabled5) sync();
8191
8389
  else teardown();
8192
8390
  }
8193
8391
  function startToolbarIcons() {
@@ -8213,7 +8411,7 @@
8213
8411
  var STORE_KEY = "zcodepro:pinned-collapsed";
8214
8412
  var SYNC_DEBOUNCE_MS2 = 300;
8215
8413
  var SCAN_FALLBACK_MS6 = 5e3;
8216
- var enabled5 = true;
8414
+ var enabled6 = true;
8217
8415
  var syncTimer2 = 0;
8218
8416
  function findPinnedHeader() {
8219
8417
  for (const h3 of document.querySelectorAll("h3")) {
@@ -8270,7 +8468,7 @@
8270
8468
  }
8271
8469
  h3.classList.add(HEAD_CLASS);
8272
8470
  h3.addEventListener("click", () => {
8273
- if (enabled5) toggle();
8471
+ if (enabled6) toggle();
8274
8472
  });
8275
8473
  const btn = h("button", {
8276
8474
  type: "button",
@@ -8279,7 +8477,7 @@
8279
8477
  "aria-expanded": collapsed ? "false" : "true",
8280
8478
  onClick: (e) => {
8281
8479
  e.stopPropagation();
8282
- if (enabled5) toggle();
8480
+ if (enabled6) toggle();
8283
8481
  }
8284
8482
  });
8285
8483
  btn.append(chevronSvg());
@@ -8287,7 +8485,7 @@
8287
8485
  return true;
8288
8486
  }
8289
8487
  function sync2() {
8290
- if (enabled5) apply2();
8488
+ if (enabled6) apply2();
8291
8489
  }
8292
8490
  function scheduleSync2() {
8293
8491
  if (syncTimer2) return;
@@ -8316,8 +8514,8 @@
8316
8514
  }
8317
8515
  async function refreshConfig3() {
8318
8516
  const cfg = await getConfig(true).catch(() => null);
8319
- enabled5 = !cfg || !cfg.features || cfg.features.pinnedCollapse !== false;
8320
- if (enabled5) sync2();
8517
+ enabled6 = !cfg || !cfg.features || cfg.features.pinnedCollapse !== false;
8518
+ if (enabled6) sync2();
8321
8519
  else teardown2();
8322
8520
  }
8323
8521
  function startPinnedCollapse() {
@@ -8436,6 +8634,10 @@
8436
8634
  startToolbarIcons();
8437
8635
  } catch {
8438
8636
  }
8637
+ try {
8638
+ startNewtabPanels();
8639
+ } catch {
8640
+ }
8439
8641
  try {
8440
8642
  startPinnedCollapse();
8441
8643
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zcode-pro",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "ZCode Pro — 为 ZCode 桌面版增加界面增强(不修改客户端文件,通过 ZCode Pro 快捷方式启动即可),核心功能:项目自定义别名",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,7 @@ import { reorderWorkspaceTasks, reorderGroupMembers } from './taskOrder.mjs';
17
17
  import { webdavRequest, webdavPutWithMkcol, webdavHttpText, parseWebdavListing, findCurl } from './webdav.mjs';
18
18
  import { resolveZcodeExecutable } from './paths.mjs';
19
19
 
20
- const VERSION = '1.1.0';
20
+ const VERSION = '1.2.0';
21
21
 
22
22
  // 全局提示词固定在用户主目录:官方加载器按 HOME/USERPROFILE 拼 .zcode/AGENTS.md,
23
23
  // 不读 ZCODE_DATA_BASE_DIR(数据根迁走时全局指令仍在原位)。
@@ -265,6 +265,10 @@ const zh = {
265
265
  switcherUnseen: '未查看',
266
266
  switcherAttentionHint: '运行中 / 运行完未查看的任务 · {prev} / {next} 或 ↑ / ↓ 选择,Enter 或松开 alt 切换',
267
267
  switcherAttentionEmpty: '暂无运行中或未查看的任务',
268
+ featureNewtabPanels: '新标签页插件面板入口',
269
+ featureNewtabPanelsDesc: '官方“新标签页”只列官方面板与推荐的官方插件,此功能把第三方市场插件(headroom、rtk 等)的面板卡片加进去,点击在侧边面板打开。',
270
+ ntpDescSuffix: '插件面板',
271
+ ntpNeedSession: '先打开一个任务会话,再从新标签页打开插件面板',
268
272
  tabShortcuts: '快捷键',
269
273
  hkTitle: '会话快捷切换',
270
274
  hkAltZ: '在当前与上次使用的会话间来回切换',
@@ -686,6 +690,10 @@ const en = {
686
690
  switcherUnseen: 'unseen',
687
691
  switcherAttentionHint: 'Running / finished but unseen tasks · {prev} / {next} or ↑ / ↓ to choose, Enter or release alt to switch',
688
692
  switcherAttentionEmpty: 'No running or unseen tasks',
693
+ featureNewtabPanels: 'Plugin panels on the new tab page',
694
+ featureNewtabPanelsDesc: "The built-in \"new tab page\" only lists built-in panels and recommended official plugins; this adds cards for third-party marketplace plugin panels (headroom, rtk, …) that open in the side pane.",
695
+ ntpDescSuffix: 'plugin panel',
696
+ ntpNeedSession: 'Open a task session first, then open plugin panels from the new tab page',
689
697
  tabShortcuts: 'Shortcuts',
690
698
  hkTitle: 'Session quick switch',
691
699
  hkAltZ: 'Toggle between the current and the last-used session',
@@ -0,0 +1,224 @@
1
+ // 新标签页插件面板入口:ZCode 3.16.1 的“新标签页”只列官方面板(文件/终端/
2
+ // 辅助对话/浏览器/审查)与云端推荐的官方插件,第三方市场(duanluan/zcode-plugins
3
+ // 等)的面板(headroom、rtk……)注册正常(listSurfaces 有返回)却没有任何入口。
4
+ // 本功能检测新标签页的模块网格,把非官方市场的面板注入成同款卡片;点击走官方
5
+ // openSidePane(App 层注入 Context Provider 的 handleOpenPluginUi,从 React
6
+ // fiber 里按函数源码特征串挖出)在侧边面板打开 tab——与插件市场页“打开”同一条
7
+ // 调用路径,不绕开官方逻辑。
8
+ import { t, getConfig, isZhLocale } from '../core.js';
9
+ import { ensureStyle, showToast } from '../ui.js';
10
+
11
+ // 官方“新标签页”终端模块的描述片段(界面中英文),作为网格识别特征
12
+ const GRID_HINTS = ['运行命令和查看输出', 'Run commands and view output'];
13
+ // 官方市场的 pluginId 后缀:这些面板已有云端推荐入口,不注入
14
+ const OFFICIAL_SUFFIX = '@zcode-plugins-official';
15
+ // handleOpenPluginUi(useCallback 产物)源码里的 logger 字符串,压缩不改动
16
+ // 字面量,以此从多个 Context Provider 中精确挑出打开面板的函数
17
+ const OPEN_FN_MARK = '打开插件 UI';
18
+
19
+ let enabled = true;
20
+ let injectTimer = 0;
21
+
22
+ // —— 从 React fiber 取应用内部对象(ws-running.js findServices 同款遍历) ——
23
+
24
+ function eachFiber(visit) {
25
+ const rootEl = document.getElementById('root');
26
+ const key = rootEl && Object.keys(rootEl).find((k) => k.startsWith('__reactContainer$'));
27
+ if (!key) return;
28
+ let visited = 0;
29
+ const walk = (f) => {
30
+ if (!f || visited > 200000) return;
31
+ visited++;
32
+ if (visit(f)) return true;
33
+ return walk(f.child) || walk(f.sibling);
34
+ };
35
+ walk(rootEl[key]);
36
+ }
37
+
38
+ // Context Provider 的 value 里找打开插件面板的函数(特征:源码含 logger 字符串)
39
+ function findOpenSidePane() {
40
+ let found = null;
41
+ eachFiber((f) => {
42
+ const et = f.elementType;
43
+ if (et && typeof et === 'object' && typeof et.$$typeof === 'symbol'
44
+ && String(et.$$typeof).includes('react.context')) {
45
+ const val = f.memoizedProps && f.memoizedProps.value;
46
+ if (typeof val === 'function' && val.toString().includes(OPEN_FN_MARK)) {
47
+ found = val;
48
+ return true;
49
+ }
50
+ }
51
+ return false;
52
+ });
53
+ return found;
54
+ }
55
+
56
+ // pluginUiBridgeService(listSurfaces 数据源)
57
+ function findBridge() {
58
+ let holder = null;
59
+ const seen = new Set();
60
+ const scan = (o) => {
61
+ if (!o || typeof o !== 'object' || seen.has(o) || holder) return;
62
+ seen.add(o);
63
+ if (typeof o.pluginUiBridgeService === 'object' && o.pluginUiBridgeService) {
64
+ holder = o.pluginUiBridgeService;
65
+ return;
66
+ }
67
+ for (const v of Object.values(o)) if (v && typeof v === 'object') scan(v);
68
+ };
69
+ eachFiber((f) => {
70
+ for (const h of [f.memoizedProps, f.memoizedState]) {
71
+ if (!h || typeof h !== 'object') continue;
72
+ scan(h);
73
+ if (holder) return true;
74
+ }
75
+ return false;
76
+ });
77
+ return holder;
78
+ }
79
+
80
+ // —— 会话与面板清单 ——
81
+
82
+ // 打开面板必须挂在某个会话上(availability: session):优先当前选中会话,
83
+ // 其次侧栏最新一行(新会话更可能已随启动拉起插件的 UI server,老会话的
84
+ // server 目录里没有该插件时会卡在“正在加载插件界面”);一个都没有返回 null
85
+ function sessionScope() {
86
+ const rows = [...document.querySelectorAll('[data-task-item-key]')];
87
+ const cur = rows.find((e) => (e.className + '').includes('bg-selected')) || rows[rows.length - 1];
88
+ const key = cur ? cur.getAttribute('data-task-item-key') : '';
89
+ if (!key) return null;
90
+ const i = key.indexOf(':sess_');
91
+ return i > 0 ? { workspacePath: key.slice(0, i), sessionId: key.slice(i + 1) } : null;
92
+ }
93
+
94
+ async function thirdPartySurfaces(bridge, workspacePath) {
95
+ try {
96
+ const list = await bridge.listSurfaces({ workspacePath });
97
+ return (list || []).filter((s) => !String(s.pluginId || '').endsWith(OFFICIAL_SUFFIX));
98
+ } catch {
99
+ return [];
100
+ }
101
+ }
102
+
103
+ function surfaceTitle(s, locale) {
104
+ const title = s.title;
105
+ const pick = (obj) => {
106
+ const lang = (locale || '').split('-')[0];
107
+ return obj[locale] || (lang && obj[lang])
108
+ || Object.entries(obj).find(([k]) => k.toLowerCase().startsWith(lang || '\0'))?.[1]
109
+ || obj.en || Object.values(obj)[0] || '';
110
+ };
111
+ if (typeof title === 'string') return title;
112
+ if (title && typeof title === 'object') {
113
+ return pick(title) || s.pluginName || s.id;
114
+ }
115
+ return s.pluginName || s.id;
116
+ }
117
+
118
+ // —— 卡片注入 ——
119
+
120
+ // 通用面板图标(lucide layout-dashboard 同款路径),替换模板卡片原图标
121
+ const PANEL_ICON_PATHS = ['<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>'];
122
+
123
+ // 找“新标签页”的模块网格:官方卡片按钮带 data-side-pane-open-tab-item
124
+ // (3.16.1 实测标识),其父容器即网格;文本特征兜底校验(终端模块描述)
125
+ function findNewTabGrid() {
126
+ const item = document.querySelector('[data-side-pane-open-tab-item]');
127
+ if (!item || item.offsetParent === null) return null;
128
+ const grid = item.parentElement;
129
+ if (!grid || grid === document.body) return null;
130
+ const text = grid.textContent || '';
131
+ return GRID_HINTS.some((h) => text.includes(h)) ? grid : null;
132
+ }
133
+
134
+ async function injectPanels(grid) {
135
+ if (grid.dataset.zcodeproPanels) return;
136
+ const template = grid.querySelector('[data-side-pane-open-tab-item]');
137
+ if (!template) return;
138
+ const scope = sessionScope();
139
+ if (!scope) return; // 没有任何会话时暂不注入,下次 DOM 变动再试
140
+ const bridge = findBridge();
141
+ if (!bridge) return;
142
+ const surfaces = await thirdPartySurfaces(bridge, scope.workspacePath);
143
+ if (!surfaces.length) { grid.dataset.zcodeproPanels = '1'; return; }
144
+ grid.dataset.zcodeproPanels = '1';
145
+ const L = t();
146
+ const locale = isZhLocale() ? 'zh-CN' : 'en-US';
147
+ for (const s of surfaces) {
148
+ // 官方卡片整卡即 button:克隆同款,去掉官方标识,挂我们的标识与点击
149
+ const card = template.cloneNode(true);
150
+ card.removeAttribute('data-side-pane-open-tab-item');
151
+ card.setAttribute('data-zcodepro-panel', s.id);
152
+ card.setAttribute('data-zcodepro-panel-name', s.pluginName || '');
153
+ const title = surfaceTitle(s, locale);
154
+ const desc = (s.pluginName || '') + ' · ' + L.ntpDescSuffix;
155
+ const titleEl = card.querySelector('span.font-semibold');
156
+ if (titleEl) titleEl.textContent = title;
157
+ const descEl = card.querySelector('span[title]');
158
+ if (descEl) { descEl.textContent = desc; descEl.setAttribute('title', desc); }
159
+ // 图标换通用面板图形(模板 svg 的描边样式保留)
160
+ const svg = card.querySelector('svg');
161
+ if (svg) {
162
+ svg.innerHTML = PANEL_ICON_PATHS[0];
163
+ svg.setAttribute('viewBox', '0 0 24 24');
164
+ }
165
+ card.addEventListener('click', (ev) => {
166
+ // 克隆卡片会冒泡到官方“新标签页”容器的点击处理(被当成再开一个新标签页),
167
+ // 这里拦下冒泡只走面板打开
168
+ ev.stopPropagation();
169
+ const sc = sessionScope();
170
+ const open = findOpenSidePane();
171
+ if (!sc || !open) {
172
+ showToast(L.ntpNeedSession, 'error');
173
+ return;
174
+ }
175
+ try {
176
+ // 字段与官方市场页 selectSurface 的请求一致;serverName(资源的 MCP
177
+ // server 归属)缺失会让沙箱准备一直停在“正在加载插件界面…”
178
+ open({
179
+ parentSessionId: sc.sessionId,
180
+ pluginId: s.pluginId,
181
+ surfaceId: s.id,
182
+ serverName: s.server,
183
+ resourceUri: s.resourceUri,
184
+ remoteSessionId: null,
185
+ title,
186
+ workspacePath: sc.workspacePath,
187
+ });
188
+ } catch (e) {
189
+ console.warn('[zcodepro] 打开插件面板失败', e);
190
+ showToast(L.ntpNeedSession, 'error');
191
+ }
192
+ });
193
+ grid.append(card);
194
+ }
195
+ }
196
+
197
+ function tryInject() {
198
+ if (!enabled || document.hidden) return;
199
+ const grid = findNewTabGrid();
200
+ if (!grid) return;
201
+ // React 重渲染可能冲掉注入的卡片:标记还在但卡片没了时清标记重注
202
+ if (grid.dataset.zcodeproPanels && !grid.querySelector('[data-zcodepro-panel]')) {
203
+ delete grid.dataset.zcodeproPanels;
204
+ }
205
+ void injectPanels(grid);
206
+ }
207
+
208
+ export function startNewtabPanels() {
209
+ ensureStyle();
210
+ const refreshEnabled = () => {
211
+ void getConfig().then((cfg) => {
212
+ enabled = cfg.features ? cfg.features.newtabPanels !== false : true;
213
+ }).catch(() => {});
214
+ };
215
+ refreshEnabled();
216
+ // 设置里开关变化即时生效(保存时会派发 config-changed)
217
+ window.addEventListener('zcodepro:config-changed', refreshEnabled);
218
+ const observer = new MutationObserver(() => {
219
+ if (injectTimer) return;
220
+ injectTimer = setTimeout(() => { injectTimer = 0; tryInject(); }, 300);
221
+ });
222
+ observer.observe(document.body, { childList: true, subtree: true });
223
+ setInterval(tryInject, 5000);
224
+ }
@@ -360,6 +360,11 @@ export function openSettingsDialog() {
360
360
  if (await setFeature('wsRunningSpin', next)) f.wsRunningSpin = next;
361
361
  refreshRows();
362
362
  }),
363
+ settingRow(L.featureNewtabPanels, L.featureNewtabPanelsDesc, f.newtabPanels !== false, async () => {
364
+ const next = !(f.newtabPanels !== false);
365
+ if (await setFeature('newtabPanels', next)) f.newtabPanels = next;
366
+ refreshRows();
367
+ }),
363
368
  (() => {
364
369
  // 标题 = “自动更新 duanluan/zcode-plugins 插件”,仓库名可点击跳转仓库
365
370
  const repoLink = h('span', {
@@ -10,6 +10,7 @@ import { startAliasWatcher } from './features/alias.js';
10
10
  import { startAliasMigrationWatcher } from './features/alias-migration.js';
11
11
  import { startTaskOrderWatcher } from './features/task-order.js';
12
12
  import { startSessionSwitch } from './features/session-switch.js';
13
+ import { startNewtabPanels } from './features/newtab-panels.js';
13
14
  import { startTitleLock } from './features/title-lock.js';
14
15
  import { startTitleGen } from './features/title-gen.js';
15
16
  import { startWsRunningSpin } from './features/ws-running.js';
@@ -58,6 +59,7 @@ import { ensureStyle } from './ui.js';
58
59
  try { startWsRunningSpin(); } catch { /* ignore */ }
59
60
  try { startIdleReclaim(); } catch { /* ignore */ }
60
61
  try { startToolbarIcons(); } catch { /* ignore */ }
62
+ try { startNewtabPanels(); } catch { /* ignore */ }
61
63
  try { startPinnedCollapse(); } catch { /* ignore */ }
62
64
  try { startStyleAdjustments(); } catch { /* ignore */ }
63
65
  try { startSettingsEntry(); } catch { /* ignore */ }