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 +1 -0
- package/README.md +1 -0
- package/dist/inject.js +223 -21
- package/package.json +1 -1
- package/src/host/helper.mjs +1 -1
- package/src/inject/core.js +8 -0
- package/src/inject/features/newtab-panels.js +224 -0
- package/src/inject/features/settings-dialog.js +5 -0
- package/src/inject/index.js +2 -0
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
|
|
2853
|
+
let enabled7 = true;
|
|
2846
2854
|
const refresh = () => {
|
|
2847
2855
|
void getConfig().then((c) => {
|
|
2848
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
7455
|
-
if (!
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
8190
|
-
if (
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
8320
|
-
if (
|
|
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
package/src/host/helper.mjs
CHANGED
|
@@ -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.
|
|
20
|
+
const VERSION = '1.2.0';
|
|
21
21
|
|
|
22
22
|
// 全局提示词固定在用户主目录:官方加载器按 HOME/USERPROFILE 拼 .zcode/AGENTS.md,
|
|
23
23
|
// 不读 ZCODE_DATA_BASE_DIR(数据根迁走时全局指令仍在原位)。
|
package/src/inject/core.js
CHANGED
|
@@ -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', {
|
package/src/inject/index.js
CHANGED
|
@@ -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 */ }
|