zcode-pro 0.20.3 → 1.0.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 +8 -5
- package/README.md +7 -4
- package/dist/inject.js +1413 -440
- package/package.json +1 -1
- package/src/host/helper.mjs +147 -60
- package/src/host/main.mjs +7 -4
- package/src/host/taskIndex.mjs +97 -0
- package/src/host/version.mjs +51 -0
- package/src/inject/core.js +101 -18
- package/src/inject/features/alias-migration.js +470 -0
- package/src/inject/features/alias.js +6 -1
- package/src/inject/features/idle-reclaim.js +23 -48
- package/src/inject/features/pinned-collapse.js +4 -3
- package/src/inject/features/project-menu.js +209 -19
- package/src/inject/features/session-switch.js +6 -2
- package/src/inject/features/settings-dialog.js +88 -59
- package/src/inject/features/styles.js +8 -1
- package/src/inject/features/title-lock.js +20 -2
- package/src/inject/features/toolbar-icons.js +85 -45
- package/src/inject/features/ws-rows.js +62 -0
- package/src/inject/features/ws-running.js +384 -27
- package/src/inject/index.js +3 -2
- package/src/inject/ui.js +20 -11
- package/src/inject/features/pinned-expand.js +0 -142
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
// 功能二:项目“更多”下拉菜单中新增“打开文件夹 / 复制路径”。
|
|
2
|
+
// 旧结构(3.15.x 及以前):菜单内存在带 data-testid(值中含项目绝对路径)且文案为“移除/Remove”
|
|
3
|
+
// 的菜单项,路径从 testid 提取;四项全注入(自定义别名/切换文件夹/打开文件夹/复制路径)。
|
|
4
|
+
// 新结构(3.16.1 起):菜单项带稳定 testid(virtual-project-edit/-delete/-remove-entry),
|
|
5
|
+
// 项目行 section[data-project-id];菜单打开时同步捕获触发行的 project-id,异步按 id 查
|
|
6
|
+
// helper 的 /projects-v2(官方任务索引 projects/project_roots)拿项目名与全部目录——
|
|
7
|
+
// 不依赖项目展开状态或任务行。插在“编辑项目”之后:
|
|
8
|
+
// - 打开文件夹:单目录直接打开主目录;多目录悬停展开二级菜单列各目录(自绘浮层,
|
|
9
|
+
// 官方 Radix 子菜单由 React 管理不便仿造)
|
|
10
|
+
// - 复制路径:复制主目录路径
|
|
11
|
+
// - 自定义别名与切换文件夹不再注入:官方“编辑项目”已支持改名与多目录增删
|
|
12
|
+
// (迁移存量别名见 alias-migration.js;旧结构下两项照旧注入)
|
|
13
|
+
import { rpc, getConfig, itemsOf, itemText, isNewProjectSidebar, t } from '../core.js';
|
|
14
|
+
import { showToast } from '../ui.js';
|
|
15
|
+
import { copyToClipboard } from './copy-path.js';
|
|
5
16
|
import { appendAliasItem } from './alias.js';
|
|
6
17
|
import { appendRelocateItem } from './relocate-dialog.js';
|
|
7
18
|
import { appendOpenFolderItem } from './open-folder.js';
|
|
@@ -18,13 +29,25 @@ function extractPathFromTestId(testid) {
|
|
|
18
29
|
return m ? m[1] : null;
|
|
19
30
|
}
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
// 新结构:菜单打开时找到触发的项目行。下拉按钮(virtual-project-menu)的打开态
|
|
33
|
+
// 实测(3.16.1)只反映在 aria-expanded=true 上,data-state 不变化,两者都查;
|
|
34
|
+
// 右键行(virtual-project-row)按 data-state/aria-expanded 兜底。菜单关闭后状态
|
|
35
|
+
// 即消失,必须同步调用
|
|
36
|
+
function openMenuProjectRow() {
|
|
37
|
+
const trigger = document.querySelector(
|
|
38
|
+
'[data-testid="virtual-project-menu"][aria-expanded="true"],'
|
|
39
|
+
+ ' [data-testid="virtual-project-menu"][data-state="open"],'
|
|
40
|
+
+ ' [data-testid="virtual-project-row"][data-state="open"],'
|
|
41
|
+
+ ' [data-testid="virtual-project-row"][aria-expanded="true"]'
|
|
42
|
+
);
|
|
43
|
+
const row = trigger ? trigger.closest('section[data-project-id]') : null;
|
|
44
|
+
if (!row) return null;
|
|
45
|
+
const host = row.getAttribute('data-project-host');
|
|
46
|
+
return host === null || host === 'local' ? row : null;
|
|
23
47
|
}
|
|
24
48
|
|
|
25
49
|
async function resolveProjectPath(removeItem) {
|
|
26
|
-
const
|
|
27
|
-
const extracted = extractPathFromTestId(testid);
|
|
50
|
+
const extracted = extractPathFromTestId(removeItem.getAttribute('data-testid') || '');
|
|
28
51
|
if (!extracted) return null;
|
|
29
52
|
// 与 helper 的最近项目列表核对(仅用于确认路径有效;任务派生的项目可能不在列表中)
|
|
30
53
|
const res = await rpc('/projects');
|
|
@@ -33,26 +56,193 @@ async function resolveProjectPath(removeItem) {
|
|
|
33
56
|
const hit = res.projects.find((p) => norm(p.path) === norm(extracted));
|
|
34
57
|
if (hit) return hit;
|
|
35
58
|
}
|
|
36
|
-
return { path: extracted, name:
|
|
59
|
+
return { path: extracted, name: String(extracted).replace(/[\\/]+$/, '').split(/[\\/]/).pop() || '' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// —— 新结构注入:编辑项之后插入 ——
|
|
63
|
+
// 官方菜单项结构:[data-radix-collection-item](div,含 svg + 文本)。克隆保证样式一致
|
|
64
|
+
function cloneMenuItem(anchor, testid, label) {
|
|
65
|
+
const item = anchor.cloneNode(true);
|
|
66
|
+
item.removeAttribute('data-testid');
|
|
67
|
+
item.removeAttribute('data-highlighted');
|
|
68
|
+
item.removeAttribute('id');
|
|
69
|
+
item.setAttribute('data-zcodepro-item', testid);
|
|
70
|
+
for (const child of [...item.childNodes]) child.remove();
|
|
71
|
+
const origIcon = anchor.querySelector('svg');
|
|
72
|
+
const iconClass = origIcon ? origIcon.getAttribute('class') : 'h-3.5 w-3.5';
|
|
73
|
+
// 文件夹图标
|
|
74
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
75
|
+
const svg = document.createElementNS(ns, 'svg');
|
|
76
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
77
|
+
svg.setAttribute('fill', 'none');
|
|
78
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
79
|
+
svg.setAttribute('stroke-width', '2');
|
|
80
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
81
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
82
|
+
svg.setAttribute('class', iconClass);
|
|
83
|
+
const p = document.createElementNS(ns, 'path');
|
|
84
|
+
p.setAttribute('d', 'M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z');
|
|
85
|
+
svg.append(p);
|
|
86
|
+
item.append(svg, document.createTextNode(label));
|
|
87
|
+
return item;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function wireHighlight(menu, item) {
|
|
91
|
+
item.addEventListener('mouseenter', () => {
|
|
92
|
+
for (const el of menu.querySelectorAll('[role="menuitem"]')) el.removeAttribute('data-highlighted');
|
|
93
|
+
item.setAttribute('data-highlighted', '');
|
|
94
|
+
});
|
|
95
|
+
item.addEventListener('mouseleave', () => item.removeAttribute('data-highlighted'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 多目录的二级子菜单:悬停“打开文件夹”即在右侧展开,一级菜单保持可见;
|
|
99
|
+
// 点击该项同样展开(触屏可达)。移出项与子菜单 300ms 后收起;一级菜单被
|
|
100
|
+
// 关闭(Esc/外部点击)时子菜单一并收起
|
|
101
|
+
function attachFolderHover(menu, item, roots) {
|
|
102
|
+
const L = t();
|
|
103
|
+
let sub = null;
|
|
104
|
+
let subHovered = false;
|
|
105
|
+
let hideTimer = 0;
|
|
106
|
+
let aliveTimer = 0;
|
|
107
|
+
|
|
108
|
+
const removeSub = () => {
|
|
109
|
+
clearTimeout(hideTimer);
|
|
110
|
+
clearInterval(aliveTimer);
|
|
111
|
+
sub?.remove();
|
|
112
|
+
sub = null;
|
|
113
|
+
subHovered = false;
|
|
114
|
+
};
|
|
115
|
+
const position = () => {
|
|
116
|
+
const rect = item.getBoundingClientRect();
|
|
117
|
+
sub.style.left = rect.right + 'px';
|
|
118
|
+
sub.style.top = rect.top + 'px';
|
|
119
|
+
const r = sub.getBoundingClientRect();
|
|
120
|
+
if (r.right > window.innerWidth - 8) {
|
|
121
|
+
sub.style.left = '';
|
|
122
|
+
sub.style.right = (window.innerWidth - rect.left) + 'px';
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const ensureSub = () => {
|
|
126
|
+
if (sub) { position(); return; }
|
|
127
|
+
sub = document.createElement('div');
|
|
128
|
+
sub.setAttribute('role', 'menu');
|
|
129
|
+
sub.setAttribute('data-zcodepro-item', 'open-folder-sub');
|
|
130
|
+
sub.className = 'zcodepro-card flex flex-col gap-0.5 rounded-lg border border-border bg-menu p-1 text-ui-sm shadow-lg';
|
|
131
|
+
sub.style.cssText = 'position:fixed;z-index:60;min-width:14rem;';
|
|
132
|
+
for (const root of roots) {
|
|
133
|
+
const base = String(root.path).replace(/[\\/]+$/, '').split(/[\\/]/).pop() || root.path;
|
|
134
|
+
const row = document.createElement('div');
|
|
135
|
+
row.setAttribute('role', 'menuitem');
|
|
136
|
+
row.className = 'flex cursor-pointer items-center gap-2 truncate rounded-md px-2 py-1.5 text-foreground transition-colors hover:bg-surface-hover';
|
|
137
|
+
row.title = root.path;
|
|
138
|
+
row.append(document.createTextNode(base + (root.primary ? ` · ${L.openFolderPrimary}` : '')));
|
|
139
|
+
row.addEventListener('click', () => {
|
|
140
|
+
// 关一级菜单(Escape 走 Radix)再打开目录
|
|
141
|
+
try { menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, cancelable: true })); } catch { /* ignore */ }
|
|
142
|
+
removeSub();
|
|
143
|
+
void rpc('/open-folder', { method: 'POST', body: { path: root.path } });
|
|
144
|
+
});
|
|
145
|
+
sub.append(row);
|
|
146
|
+
}
|
|
147
|
+
sub.addEventListener('mouseenter', () => { subHovered = true; clearTimeout(hideTimer); });
|
|
148
|
+
sub.addEventListener('mouseleave', () => { subHovered = false; scheduleHide(); });
|
|
149
|
+
document.body.append(sub);
|
|
150
|
+
position();
|
|
151
|
+
// 子菜单存活期间周期确认一级还在(Radix 被关后不残留)
|
|
152
|
+
aliveTimer = setInterval(() => { if (!menu.isConnected) removeSub(); }, 400);
|
|
153
|
+
};
|
|
154
|
+
const scheduleHide = () => {
|
|
155
|
+
clearTimeout(hideTimer);
|
|
156
|
+
hideTimer = setTimeout(() => { if (!subHovered) removeSub(); }, 300);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
item.addEventListener('mouseenter', () => { clearTimeout(hideTimer); ensureSub(); });
|
|
160
|
+
item.addEventListener('mouseleave', scheduleHide);
|
|
161
|
+
// 点击同样展开且不关一级(项是注入节点,不触发 Radix 的选中即关闭)
|
|
162
|
+
item.addEventListener('click', () => { clearTimeout(hideTimer); ensureSub(); });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function injectNewMenuItems(content, menuRow) {
|
|
166
|
+
const L = t();
|
|
167
|
+
const projectId = menuRow.getAttribute('data-project-id');
|
|
168
|
+
const editItem = content.querySelector('[data-testid="virtual-project-edit"]');
|
|
169
|
+
if (!editItem || !projectId) return;
|
|
170
|
+
if (content.querySelector('[data-zcodepro-item]')) return;
|
|
171
|
+
const config = await getConfig();
|
|
172
|
+
// 同一组开关(旧结构共用 features.projectMenu)
|
|
173
|
+
if (config.features && config.features.projectMenu === false) return;
|
|
174
|
+
const res = await rpc('/projects-v2');
|
|
175
|
+
if (!res.ok || !res.available) return;
|
|
176
|
+
const proj = (res.projects || []).find((p) => p.id === projectId);
|
|
177
|
+
if (!proj) return;
|
|
178
|
+
const roots = (proj.roots || []).slice().sort((a, b) => (b.primary ? 1 : 0) - (a.primary ? 1 : 0));
|
|
179
|
+
if (!roots.length) return;
|
|
180
|
+
const primary = roots.find((r) => r.primary) || roots[0];
|
|
181
|
+
|
|
182
|
+
// 打开文件夹:单目录点击直接打开;多目录悬停/点击展开二级子菜单(带 › 指示)
|
|
183
|
+
const openItem = cloneMenuItem(editItem, 'open-folder', L.openFolderItem);
|
|
184
|
+
wireHighlight(content, openItem);
|
|
185
|
+
if (roots.length === 1) {
|
|
186
|
+
openItem.addEventListener('click', () => {
|
|
187
|
+
try { content.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, cancelable: true })); } catch { /* ignore */ }
|
|
188
|
+
void rpc('/open-folder', { method: 'POST', body: { path: primary.path } });
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
// 行尾 › 子级指示(与官方菜单 chevron 同款样式)
|
|
192
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
193
|
+
const chev = document.createElementNS(ns, 'svg');
|
|
194
|
+
chev.setAttribute('viewBox', '0 0 24 24');
|
|
195
|
+
chev.setAttribute('fill', 'none');
|
|
196
|
+
chev.setAttribute('stroke', 'currentColor');
|
|
197
|
+
chev.setAttribute('stroke-width', '2');
|
|
198
|
+
chev.setAttribute('stroke-linecap', 'round');
|
|
199
|
+
chev.setAttribute('stroke-linejoin', 'round');
|
|
200
|
+
chev.setAttribute('class', 'ml-auto size-3.5 shrink-0 text-foreground-subtle');
|
|
201
|
+
const chevPath = document.createElementNS(ns, 'path');
|
|
202
|
+
chevPath.setAttribute('d', 'm9 18 6-6-6-6');
|
|
203
|
+
chev.append(chevPath);
|
|
204
|
+
openItem.append(chev);
|
|
205
|
+
attachFolderHover(content, openItem, roots);
|
|
206
|
+
}
|
|
207
|
+
editItem.after(openItem);
|
|
208
|
+
|
|
209
|
+
// 复制路径:主目录
|
|
210
|
+
const copyItem = cloneMenuItem(editItem, 'copy-path', L.copyPathItem);
|
|
211
|
+
wireHighlight(content, copyItem);
|
|
212
|
+
copyItem.addEventListener('click', () => {
|
|
213
|
+
try { content.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, cancelable: true })); } catch { /* ignore */ }
|
|
214
|
+
void copyToClipboard(primary.path).then((ok) => showToast(ok ? L.pathCopied : L.pathCopyFailed, ok ? undefined : 'error'));
|
|
215
|
+
});
|
|
216
|
+
openItem.after(copyItem);
|
|
37
217
|
}
|
|
38
218
|
|
|
39
219
|
export function handleProjectMenu(content) {
|
|
40
|
-
if (content.querySelector('[data-zcodepro-item
|
|
220
|
+
if (content.querySelector('[data-zcodepro-item]')) return;
|
|
41
221
|
const items = itemsOf(content);
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
);
|
|
45
|
-
|
|
222
|
+
const newUi = isNewProjectSidebar();
|
|
223
|
+
// 菜单关闭后 data-state 即消失:项目行必须在任何 await 之前捕获
|
|
224
|
+
const menuRow = newUi ? openMenuProjectRow() : null;
|
|
225
|
+
const anchor = newUi
|
|
226
|
+
? items.find((el) => el.getAttribute('data-testid') === 'virtual-project-edit')
|
|
227
|
+
: items.find(
|
|
228
|
+
(el) => el.hasAttribute('data-testid') && REMOVE_TEXTS.some((x) => itemText(el).startsWith(x))
|
|
229
|
+
);
|
|
230
|
+
if (!anchor) return;
|
|
231
|
+
if (newUi) {
|
|
232
|
+
if (!menuRow) return;
|
|
233
|
+
void injectNewMenuItems(content, menuRow);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
46
236
|
void (async () => {
|
|
47
237
|
const config = await getConfig();
|
|
48
238
|
// 一组开关控制整组菜单项(别名 / 切换文件夹 / 打开文件夹 / 复制路径)
|
|
49
239
|
if (config.features && config.features.projectMenu === false) return;
|
|
50
|
-
const project = await resolveProjectPath(
|
|
240
|
+
const project = await resolveProjectPath(anchor);
|
|
51
241
|
if (!project) return;
|
|
52
242
|
// 菜单顺序:自定义别名 → 切换文件夹 → 打开文件夹 → 复制路径 → 移除(均插在“移除”之前)
|
|
53
|
-
appendAliasItem(content,
|
|
54
|
-
appendRelocateItem(content,
|
|
55
|
-
appendOpenFolderItem(content,
|
|
56
|
-
appendCopyPathItem(content,
|
|
243
|
+
appendAliasItem(content, anchor, project);
|
|
244
|
+
appendRelocateItem(content, anchor, project);
|
|
245
|
+
appendOpenFolderItem(content, anchor, project);
|
|
246
|
+
appendCopyPathItem(content, anchor, project);
|
|
57
247
|
})();
|
|
58
248
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// 目标条目在折叠项目下时先展开项目行再点击(展开先例见 pinned-expand.js)。
|
|
8
8
|
import { h, t, getConfig } from '../core.js';
|
|
9
9
|
import { ensureStyle, showToast } from '../ui.js';
|
|
10
|
+
import { findProjectRow } from './ws-rows.js';
|
|
10
11
|
|
|
11
12
|
const MRU_LIMIT = 50;
|
|
12
13
|
const SCAN_DEBOUNCE_MS = 150; // 相关 DOM 变动后的去抖,合并连续变动
|
|
@@ -93,7 +94,9 @@ async function switchTo(key) {
|
|
|
93
94
|
let item = find();
|
|
94
95
|
if (!item) {
|
|
95
96
|
const ws = workspaceOf(key);
|
|
96
|
-
|
|
97
|
+
// 项目行按路径定位:旧结构查 testid,新结构(3.16.1 起)按 data-project-id
|
|
98
|
+
// 分区匹配(分区与路径的关联需该项目本次会话展开过一次,见 ws-rows.js)
|
|
99
|
+
const header = findProjectRow(ws);
|
|
97
100
|
const expander = header && header.querySelector('[aria-expanded]');
|
|
98
101
|
if (expander && expander.getAttribute('aria-expanded') === 'false') {
|
|
99
102
|
expander.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
@@ -231,7 +234,8 @@ export function startSessionSwitch() {
|
|
|
231
234
|
// x/c:弹窗导航(无可导航会话时与 z 一致轻提示)
|
|
232
235
|
if (mru.length < 2) { showToast(t().switcherEmpty); return; }
|
|
233
236
|
if (!popupEl) await openPopup();
|
|
234
|
-
|
|
237
|
+
// 键位对调:alt+x 上移一位(更靠上/更近的键管向上),alt+c 下移
|
|
238
|
+
moveHighlight(k === 'x' ? -1 : 1);
|
|
235
239
|
})();
|
|
236
240
|
}, true);
|
|
237
241
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// “ZCode Pro 增强设置”弹窗:功能开关 + 样式调整 + 运行状态。
|
|
2
2
|
// 顶部标签页切换(视觉参考侧栏“分组/项目”切换);配置写入 helper(~/.zcode/zcodepro.json)。
|
|
3
|
-
import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale } from '../core.js';
|
|
3
|
+
import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale, isNewProjectSidebar } from '../core.js';
|
|
4
4
|
import { openDialog, dialogFooter, btnPrimary, btnSecondary, btnSmall, settingRow, ensureStyle, showToast, numberField, unitField } from '../ui.js';
|
|
5
5
|
import { comboField, providerOptionsOf, modelOptionsOf, modelPlaceholderOf, isManualConn } from '../provider-fields.js';
|
|
6
6
|
import { refreshAliases } from './alias.js';
|
|
7
|
+
import { migrationCard } from './alias-migration.js';
|
|
7
8
|
import { applyStyles, STYLE_DEFAULTS, showStyleHighlight, hideStyleHighlight } from './styles.js';
|
|
8
9
|
import { copyToClipboard, hCopyIcon } from './copy-path.js';
|
|
9
10
|
|
|
@@ -12,6 +13,9 @@ function openExternal(url) {
|
|
|
12
13
|
try { void window.zcode.openExternal(url); } catch { /* ignore */ }
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
// 插件市场仓库(设置页“自动更新”行的仓库名链接指向它)
|
|
17
|
+
const PLUGINS_REPO_URL = 'https://github.com/duanluan/zcode-plugins';
|
|
18
|
+
|
|
15
19
|
// 本体升级交互(Headroom 的 pip 与 rtk 的 GitHub Releases 共用):
|
|
16
20
|
// 检查更新 → 升级(helper 后台任务,轮询进度,输出取最后一行、超长省略中间)→
|
|
17
21
|
// 进行中可停止;关弹窗不中断升级,重开恢复显示。endpoint 为 /headroom 或 /rtk,
|
|
@@ -236,10 +240,51 @@ export function openSettingsDialog() {
|
|
|
236
240
|
);
|
|
237
241
|
|
|
238
242
|
const rows = h('div', { class: 'divide-y divide-border rounded-xl border border-border' });
|
|
243
|
+
const pluginsStatusLine = h('span', { class: 'max-w-40 truncate text-ui-xs/relaxed text-foreground-subtle' }, '…');
|
|
244
|
+
const pluginsBtn = btnSmall(L.pluginsCheckNow, () => { void runPluginsUpdate(); });
|
|
245
|
+
let pluginsUpdating = false;
|
|
246
|
+
const runPluginsUpdate = async () => {
|
|
247
|
+
// 并发守卫:打开弹窗的自动触发与手动点击可能撞在一起,第二个直接忽略
|
|
248
|
+
if (pluginsUpdating) return;
|
|
249
|
+
pluginsUpdating = true;
|
|
250
|
+
pluginsBtn.disabled = true;
|
|
251
|
+
pluginsStatusLine.textContent = L.pluginsUpdating;
|
|
252
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: {} });
|
|
253
|
+
pluginsUpdating = false;
|
|
254
|
+
pluginsBtn.disabled = false;
|
|
255
|
+
if (!res.ok) {
|
|
256
|
+
pluginsStatusLine.textContent = '';
|
|
257
|
+
showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
pluginsStatusLine.textContent = res.updated > 0
|
|
261
|
+
? L.pluginsUpdatedDone.replaceAll('{n}', String(res.updated))
|
|
262
|
+
: L.pluginsUpToDate;
|
|
263
|
+
showToast(pluginsStatusLine.textContent, 'success');
|
|
264
|
+
};
|
|
265
|
+
void (async () => {
|
|
266
|
+
// 打开设置弹窗即自动更新(与启动时同语义):开关开着且有新版时直接更,
|
|
267
|
+
// 状态行如实显示过程与结果;开关关闭时只检查并显示“N 个可更新”
|
|
268
|
+
const cfg = await getConfig(true).catch(() => null);
|
|
269
|
+
const res = await rpc('/plugins/status');
|
|
270
|
+
if (!res.ok) { pluginsStatusLine.textContent = ''; return; }
|
|
271
|
+
const n = (res.updates || []).length;
|
|
272
|
+
if (cfg && (cfg.features || {}).autoUpdatePlugins === true && n > 0) {
|
|
273
|
+
void runPluginsUpdate();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
pluginsStatusLine.textContent = n > 0
|
|
277
|
+
? L.pluginsUpdatesFound.replaceAll('{n}', String(n))
|
|
278
|
+
: L.pluginsUpToDate;
|
|
279
|
+
})();
|
|
280
|
+
|
|
239
281
|
const refreshRows = () => {
|
|
240
282
|
const f = config.features || {};
|
|
241
283
|
rows.replaceChildren(
|
|
242
|
-
|
|
284
|
+
// 3.16.1 起官方“编辑项目”支持修改名称:别名停用,此行只描述其余三项;
|
|
285
|
+
// 仍有存量别名时在最上方放迁移卡片(卡片自管移除,这里每次重建)
|
|
286
|
+
...(isNewProjectSidebar() ? [migrationCard()] : []),
|
|
287
|
+
settingRow(L.featureProjectMenu, isNewProjectSidebar() ? L.featureProjectMenuDescNew : L.featureProjectMenuDesc, f.projectMenu !== false, async () => {
|
|
243
288
|
const next = !(f.projectMenu !== false);
|
|
244
289
|
if (await setFeature('projectMenu', next)) f.projectMenu = next;
|
|
245
290
|
refreshRows();
|
|
@@ -281,16 +326,31 @@ export function openSettingsDialog() {
|
|
|
281
326
|
if (await setFeature('wsRunningSpin', next)) f.wsRunningSpin = next;
|
|
282
327
|
refreshRows();
|
|
283
328
|
}),
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
329
|
+
(() => {
|
|
330
|
+
// 标题 = “自动更新 duanluan/zcode-plugins 插件”,仓库名可点击跳转仓库
|
|
331
|
+
const repoLink = h('span', {
|
|
332
|
+
class: 'cursor-pointer underline-offset-4 hover:underline',
|
|
333
|
+
title: PLUGINS_REPO_URL,
|
|
334
|
+
onClick: (ev) => { ev.stopPropagation(); openExternal(PLUGINS_REPO_URL); },
|
|
335
|
+
}, L.featureAutoUpdatePluginsRepo);
|
|
336
|
+
const row = settingRow('', '', f.autoUpdatePlugins === true, async () => {
|
|
337
|
+
const next = !(f.autoUpdatePlugins === true);
|
|
338
|
+
if (await setFeature('autoUpdatePlugins', next)) f.autoUpdatePlugins = next;
|
|
339
|
+
refreshRows();
|
|
340
|
+
}, h('div', { class: 'flex items-center gap-2' }, pluginsBtn));
|
|
341
|
+
// 标题行:自动更新 <链接> 插件;描述位放状态字(检查结果/更新进度)
|
|
342
|
+
const nameEl = row.querySelector('.text-ui-sm.font-medium');
|
|
343
|
+
if (nameEl) {
|
|
344
|
+
nameEl.replaceChildren(
|
|
345
|
+
document.createTextNode(L.featureAutoUpdatePlugins + ' '),
|
|
346
|
+
repoLink,
|
|
347
|
+
document.createTextNode(' 插件'),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const descEl = row.querySelector('[data-zcodepro-row-desc]');
|
|
351
|
+
if (descEl) descEl.replaceChildren(pluginsStatusLine);
|
|
352
|
+
return row;
|
|
353
|
+
})(),
|
|
294
354
|
);
|
|
295
355
|
};
|
|
296
356
|
refreshRows();
|
|
@@ -333,54 +393,9 @@ export function openSettingsDialog() {
|
|
|
333
393
|
let activeTab = 'features';
|
|
334
394
|
// 插件更新卡片:显示 duanluan-zcode-plugins 市场状态,一键更新(helper /plugins/* 端点);
|
|
335
395
|
// 状态异步加载,打开弹窗不等待
|
|
336
|
-
|
|
337
|
-
const pluginsBtn = btnSmall(L.pluginsCheckNow, () => { void runPluginsUpdate(); });
|
|
338
|
-
const runPluginsUpdate = async () => {
|
|
339
|
-
pluginsBtn.disabled = true;
|
|
340
|
-
pluginsStatusLine.textContent = L.pluginsUpdating;
|
|
341
|
-
const res = await rpc('/plugins/update', { method: 'POST', body: {} });
|
|
342
|
-
pluginsBtn.disabled = false;
|
|
343
|
-
if (!res.ok) {
|
|
344
|
-
pluginsStatusLine.textContent = '';
|
|
345
|
-
showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
pluginsStatusLine.textContent = res.updated > 0
|
|
349
|
-
? L.pluginsUpdatedDone.replaceAll('{n}', String(res.updated))
|
|
350
|
-
: L.pluginsUpToDate;
|
|
351
|
-
showToast(pluginsStatusLine.textContent, 'success');
|
|
352
|
-
};
|
|
353
|
-
void (async () => {
|
|
354
|
-
const res = await rpc('/plugins/status');
|
|
355
|
-
if (!res.ok) { pluginsStatusLine.textContent = ''; return; }
|
|
356
|
-
pluginsStatusLine.textContent = (res.updates || []).length > 0
|
|
357
|
-
? L.pluginsUpdatesFound.replaceAll('{n}', String(res.updates.length))
|
|
358
|
-
: L.pluginsUpToDate;
|
|
359
|
-
})();
|
|
360
|
-
// 标题中的 zcode-plugins 可点击跳转插件市场仓库(依赖宿主 openExternal 打开系统浏览器)
|
|
361
|
-
const PLUGINS_REPO_URL = 'https://github.com/duanluan/zcode-plugins';
|
|
362
|
-
const repoName = 'zcode-plugins';
|
|
363
|
-
const titleParts = L.pluginsUpdateTitle.split(repoName);
|
|
364
|
-
const pluginsTitle = h('div', { class: 'truncate text-ui-sm font-medium text-foreground' });
|
|
365
|
-
if (titleParts.length === 2 && typeof window.zcode?.openExternal === 'function') {
|
|
366
|
-
pluginsTitle.append(titleParts[0],
|
|
367
|
-
h('span', {
|
|
368
|
-
class: 'cursor-pointer underline-offset-4 hover:underline',
|
|
369
|
-
title: PLUGINS_REPO_URL,
|
|
370
|
-
onClick: () => openExternal(PLUGINS_REPO_URL),
|
|
371
|
-
}, repoName),
|
|
372
|
-
titleParts[1]);
|
|
373
|
-
} else {
|
|
374
|
-
pluginsTitle.append(L.pluginsUpdateTitle);
|
|
375
|
-
}
|
|
376
|
-
const pluginsCard = h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
377
|
-
h('div', { class: 'min-w-0' },
|
|
378
|
-
pluginsTitle,
|
|
379
|
-
h('div', { class: 'mt-0.5 truncate text-ui-xs/relaxed text-foreground-subtle' }, pluginsStatusLine)),
|
|
380
|
-
pluginsBtn);
|
|
396
|
+
|
|
381
397
|
const paneFeatures = h('div', { role: 'tabpanel', class: 'mt-4' },
|
|
382
398
|
h('div', {}, rows),
|
|
383
|
-
pluginsCard,
|
|
384
399
|
communityCards,
|
|
385
400
|
);
|
|
386
401
|
const paneStyles = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
@@ -497,6 +512,7 @@ export function openSettingsDialog() {
|
|
|
497
512
|
cellPadH.el, h('span', { class: 'w-3 text-ui-xs text-foreground-subtle' }, 'px'))),
|
|
498
513
|
};
|
|
499
514
|
const cells = [
|
|
515
|
+
// 侧栏间距双结构:3.15.x 旧容器/行 + 3.16.1 新项目行(virtual-project-row)与任务行
|
|
500
516
|
styleCell(L.sidebarProjectSpacingName, L.sidebarProjectSpacingDesc, 'sidebarProjectSpacing', { max: 24 }),
|
|
501
517
|
styleCell(L.sidebarTaskSpacingName, L.sidebarTaskSpacingDesc, 'sidebarTaskSpacing', { max: 24 }),
|
|
502
518
|
widthCell,
|
|
@@ -1853,6 +1869,7 @@ export function openSettingsDialog() {
|
|
|
1853
1869
|
const hrStatusLine = h('div', { class: 'flex items-center gap-2' },
|
|
1854
1870
|
h('span', { class: 'text-ui-sm text-foreground-subtle/70' }, '…'));
|
|
1855
1871
|
const hrFacts = h('div', { class: 'mt-1.5 grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3' });
|
|
1872
|
+
const hrInstList = h('div', { class: 'mt-1.5 space-y-1', style: 'display:none' });
|
|
1856
1873
|
const hrHintLine = h('p', { class: 'mt-1.5 text-ui-xs/relaxed text-amber-500', style: 'display:none' });
|
|
1857
1874
|
const hrRefresh = async () => {
|
|
1858
1875
|
const res = await rpc('/headroom/status');
|
|
@@ -1860,6 +1877,8 @@ export function openSettingsDialog() {
|
|
|
1860
1877
|
hrStatusLine.replaceChildren(
|
|
1861
1878
|
h('span', { class: 'text-ui-sm text-destructive' }, L.headroomStatusLoadFailed + ': ' + errText(res)));
|
|
1862
1879
|
hrFacts.replaceChildren();
|
|
1880
|
+
hrInstList.replaceChildren();
|
|
1881
|
+
hrInstList.style.display = 'none';
|
|
1863
1882
|
hrHintLine.style.display = 'none';
|
|
1864
1883
|
return;
|
|
1865
1884
|
}
|
|
@@ -1876,9 +1895,18 @@ export function openSettingsDialog() {
|
|
|
1876
1895
|
fact(L.headroomCurrentBackend, hrDevLabel(s.backend)),
|
|
1877
1896
|
fact(L.headroomDesiredBackend, hrDevLabel(s.desiredBackend)),
|
|
1878
1897
|
fact(L.headroomPowerState, HR_PM[s.powerMode] || s.powerMode),
|
|
1879
|
-
fact(L.headroomWatcher, s.watcherRunning ? L.headroomWatcherOn : L.headroomWatcherOff),
|
|
1898
|
+
fact(L.headroomWatcher, s.watcherRunning == null ? '—' : (s.watcherRunning ? L.headroomWatcherOn : L.headroomWatcherOff)),
|
|
1880
1899
|
fact(L.headroomSaverDetect, HR_DETECT[s.saverDetect] || s.saverDetect),
|
|
1881
1900
|
);
|
|
1901
|
+
// 多实例模式:列出各压缩实例(端口/格式/上游/供应商)与运行状态
|
|
1902
|
+
const insts = Array.isArray(s.instances) ? s.instances : [];
|
|
1903
|
+
hrInstList.replaceChildren(...insts.map((it) => h('div', { class: 'flex min-w-0 items-center gap-2' },
|
|
1904
|
+
h('span', { class: 'shrink-0 font-mono text-ui-xs text-foreground-subtle' }, `:${it.port}`),
|
|
1905
|
+
h('span', { class: 'min-w-0 flex-1 truncate text-ui-xs text-foreground-subtle', title: `${it.upstream} | ${it.providers}` },
|
|
1906
|
+
`${it.format} → ${it.upstream}`),
|
|
1907
|
+
h('span', { class: 'max-w-[40%] shrink-0 truncate text-ui-xs text-foreground-subtle/70', title: it.providers }, it.providers),
|
|
1908
|
+
h('span', { class: 'shrink-0 text-ui-xs ' + (it.up ? 'text-emerald-600' : 'text-destructive') }, it.up ? L.hrInstRunning : L.hrInstStopped))));
|
|
1909
|
+
hrInstList.style.display = insts.length ? '' : 'none';
|
|
1882
1910
|
hrHintLine.textContent = s.saverHint || '';
|
|
1883
1911
|
hrHintLine.style.display = s.saverHint ? '' : 'none';
|
|
1884
1912
|
};
|
|
@@ -2052,6 +2080,7 @@ export function openSettingsDialog() {
|
|
|
2052
2080
|
h('div', { class: 'flex items-center gap-1.5' }, hrRefreshBtn, hrStartBtn, hrRestartBtn, hrStopBtn)),
|
|
2053
2081
|
hrStatusLine,
|
|
2054
2082
|
hrFacts,
|
|
2083
|
+
hrInstList,
|
|
2055
2084
|
hrHintLine),
|
|
2056
2085
|
h('div', {
|
|
2057
2086
|
class: 'mt-2 rounded-xl border border-border p-1',
|
|
@@ -214,6 +214,9 @@ function buildCss(styles) {
|
|
|
214
214
|
`${LIST} [data-testid^="workspace-item-"] > div:first-child{align-self:stretch !important;}`,
|
|
215
215
|
`${SCROLL} [data-slot="collapsible-trigger"],${SCROLL} .flex.h-7{height:auto !important;}`,
|
|
216
216
|
`${SCROLL} [data-slot="collapsible-trigger"]{min-height:${28 + 2 * pad}px !important;padding-block:${pad}px !important;padding-top:${pad}px !important;padding-bottom:${pad}px !important;}`,
|
|
217
|
+
// 3.16.1 新结构:项目行(virtual-project-row,固定 h-8)按视觉总量放行内留白;
|
|
218
|
+
// 分区间距由新侧栏容器自己的布局管理,不动
|
|
219
|
+
`section[data-project-id] [data-testid="virtual-project-row"]{height:auto !important;min-height:${28 + 2 * pad}px !important;padding-block:${Math.round(pad)}px !important;}`,
|
|
217
220
|
);
|
|
218
221
|
}
|
|
219
222
|
const sts = styles.sidebarTaskSpacing;
|
|
@@ -228,6 +231,9 @@ function buildCss(styles) {
|
|
|
228
231
|
`ul.space-y-0\\.5:has(> li[data-task-item-key]) > li{padding-block:${pad}px !important;padding-top:${pad}px !important;padding-bottom:${pad}px !important;}`,
|
|
229
232
|
`${SCROLL} .flex.flex-col.gap-2{gap:${m}px !important;row-gap:${m}px !important;}`,
|
|
230
233
|
`${SCROLL} div.cursor-pointer[class*="pl-8"]{padding-block:${pad}px !important;padding-top:${pad}px !important;padding-bottom:${pad}px !important;}`,
|
|
234
|
+
// 3.16.1 新结构:任务行(li[data-task-item-key])行内留白 + 相邻行间距
|
|
235
|
+
`li[data-task-item-key]{padding-block:${Math.max(1, Math.round(pad))}px !important;}`,
|
|
236
|
+
`li[data-task-item-key] + li[data-task-item-key]{margin-block-start:${m}px !important;}`,
|
|
231
237
|
);
|
|
232
238
|
}
|
|
233
239
|
return parts.join('');
|
|
@@ -275,7 +281,8 @@ const HL_SELECTORS = {
|
|
|
275
281
|
thinkingLineHeight: `[data-testid^="chat-assistant-history-content"]`,
|
|
276
282
|
userLineHeight: `${CONV} [class*="user-row"] .whitespace-pre-wrap`,
|
|
277
283
|
contentWidth: '[data-v4-timeline-content-column]',
|
|
278
|
-
|
|
284
|
+
// 双结构:新侧栏项目行 / 旧 testid 行
|
|
285
|
+
sidebarProjectSpacing: 'section[data-project-id] [data-testid="virtual-project-row"], [data-testid^="workspace-item-"]',
|
|
279
286
|
sidebarTaskSpacing: 'li[data-task-item-key]',
|
|
280
287
|
};
|
|
281
288
|
|
|
@@ -28,6 +28,7 @@ const REVERIFY_MS = 3000; // 回写后的再复核:纠正数据库写
|
|
|
28
28
|
const MAP_REFRESH_MS = 60000; // 任务行变动后至多一次的延迟表刷新:让新重命名会话进入锁定范围
|
|
29
29
|
|
|
30
30
|
let installed = false;
|
|
31
|
+
const seenRows = new WeakSet(); // 已见过的任务行;新上屏的行立即钉回(不等复核)
|
|
31
32
|
let enabled = true; // titleLock 开关(/pinned-titles 响应带来)
|
|
32
33
|
let pinned = null; // Map(task_id → 重命名标题);null = 尚未取到/功能关闭
|
|
33
34
|
let resolved = false; // 首次成功通信(无论开关状态)后不再重试启动拉取
|
|
@@ -100,7 +101,19 @@ function writeTitle(el, text) {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// 失配 → 排期复核;到期后行可能已被 React 重挂载,按键重新定位
|
|
103
|
-
function scheduleEnforce(key) {
|
|
104
|
+
function scheduleEnforce(key, { immediate = false } = {}) {
|
|
105
|
+
if (immediate) {
|
|
106
|
+
// 新上屏的行(项目展开/滚动复现)带着旧名出现,用户此刻正看着列表——
|
|
107
|
+
// 立即用缓存表钉回,不等复核(编辑中的行不会到这:isMismatch 已排除 input)
|
|
108
|
+
if (enabled && pinned) {
|
|
109
|
+
const want = pinned.get(sessIdOf(key));
|
|
110
|
+
if (want !== undefined) {
|
|
111
|
+
for (const item of findRows(key)) {
|
|
112
|
+
for (const el of titleElsOf(item)) writeTitle(el, want);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
104
117
|
if (pendingKeys.has(key)) return;
|
|
105
118
|
pendingKeys.add(key);
|
|
106
119
|
setTimeout(() => {
|
|
@@ -138,7 +151,12 @@ function scan() {
|
|
|
138
151
|
if (!enabled || !pinned || pinned.size === 0) return;
|
|
139
152
|
for (const item of document.querySelectorAll(TASK_SEL)) {
|
|
140
153
|
const key = isMismatch(item);
|
|
141
|
-
if (key)
|
|
154
|
+
if (!key) { seenRows.add(item); continue; }
|
|
155
|
+
// 新上屏的失配行立即钉回(项目展开时旧名闪现 1 秒+,用户不知道点哪个);
|
|
156
|
+
// 已在屏上的失配走原路径(复核后写,避免把刚提交的重命名改回旧名)
|
|
157
|
+
const fresh = !seenRows.has(item);
|
|
158
|
+
seenRows.add(item);
|
|
159
|
+
scheduleEnforce(key, { immediate: fresh });
|
|
142
160
|
}
|
|
143
161
|
}
|
|
144
162
|
|