zcode-pro 0.16.0__py3-none-any.whl

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.
Files changed (41) hide show
  1. zcode_pro/__init__.py +3 -0
  2. zcode_pro/__main__.py +4 -0
  3. zcode_pro/app/cli.mjs +9 -0
  4. zcode_pro/app/dist/inject.js +5821 -0
  5. zcode_pro/app/src/host/cdp.mjs +126 -0
  6. zcode_pro/app/src/host/helper.mjs +2134 -0
  7. zcode_pro/app/src/host/main.mjs +262 -0
  8. zcode_pro/app/src/host/paths.mjs +73 -0
  9. zcode_pro/app/src/host/pickFolder.mjs +152 -0
  10. zcode_pro/app/src/host/settings.mjs +61 -0
  11. zcode_pro/app/src/host/taskIndex.mjs +255 -0
  12. zcode_pro/app/src/host/taskOrder.mjs +117 -0
  13. zcode_pro/app/src/host/webdav.mjs +105 -0
  14. zcode_pro/app/src/inject/core.js +751 -0
  15. zcode_pro/app/src/inject/features/alias.js +211 -0
  16. zcode_pro/app/src/inject/features/copy-path.js +78 -0
  17. zcode_pro/app/src/inject/features/file-menu.js +147 -0
  18. zcode_pro/app/src/inject/features/idle-reclaim.js +179 -0
  19. zcode_pro/app/src/inject/features/image-menu.js +215 -0
  20. zcode_pro/app/src/inject/features/open-folder.js +43 -0
  21. zcode_pro/app/src/inject/features/pinned-collapse.js +132 -0
  22. zcode_pro/app/src/inject/features/pinned-expand.js +142 -0
  23. zcode_pro/app/src/inject/features/plugin-updates.js +28 -0
  24. zcode_pro/app/src/inject/features/project-menu.js +58 -0
  25. zcode_pro/app/src/inject/features/relocate-dialog.js +156 -0
  26. zcode_pro/app/src/inject/features/session-switch.js +244 -0
  27. zcode_pro/app/src/inject/features/settings-dialog.js +1503 -0
  28. zcode_pro/app/src/inject/features/settings-entry.js +21 -0
  29. zcode_pro/app/src/inject/features/styles.js +311 -0
  30. zcode_pro/app/src/inject/features/task-order.js +78 -0
  31. zcode_pro/app/src/inject/features/title-lock.js +242 -0
  32. zcode_pro/app/src/inject/features/toolbar-icons.js +243 -0
  33. zcode_pro/app/src/inject/features/ws-running.js +472 -0
  34. zcode_pro/app/src/inject/index.js +55 -0
  35. zcode_pro/app/src/inject/ui.js +734 -0
  36. zcode_pro/cli.py +71 -0
  37. zcode_pro-0.16.0.dist-info/METADATA +45 -0
  38. zcode_pro-0.16.0.dist-info/RECORD +41 -0
  39. zcode_pro-0.16.0.dist-info/WHEEL +4 -0
  40. zcode_pro-0.16.0.dist-info/entry_points.txt +2 -0
  41. zcode_pro-0.16.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,156 @@
1
+ // “切换文件夹”:把项目记录(侧边栏条目/标签页/任务历史)重新指向另一个已存在的文件夹。
2
+ // 不移动、不修改任何目录;本地任务索引与 setting.json 的路径引用同步更新,会话不丢失。
3
+ import { h, t, rpc, getConfig, closeRadixMenu, itemsOf, itemText, errText } from '../core.js';
4
+ import { openDialog, dialogFooter, btnPrimary, btnSecondary, textInput, showToast, ensureStyle } from '../ui.js';
5
+
6
+ export function appendRelocateItem(menu, anchorItem, project) {
7
+ if (menu.querySelector('[data-zcodepro-item="relocate"]')) return;
8
+ const L = t();
9
+ const item = anchorItem.cloneNode(true);
10
+ item.removeAttribute('data-testid');
11
+ item.removeAttribute('data-highlighted');
12
+ item.setAttribute('data-zcodepro-item', 'relocate');
13
+ for (const child of [...item.childNodes]) child.remove();
14
+ const origIcon = anchorItem.querySelector('svg');
15
+ const iconClass = origIcon ? origIcon.getAttribute('class') : 'h-3.5 w-3.5';
16
+ item.append(hMoveIcon(iconClass), document.createTextNode(L.relocateItem));
17
+ item.addEventListener('mouseenter', () => {
18
+ for (const el of menu.querySelectorAll('[role="menuitem"]')) el.removeAttribute('data-highlighted');
19
+ item.setAttribute('data-highlighted', '');
20
+ });
21
+ item.addEventListener('mouseleave', () => item.removeAttribute('data-highlighted'));
22
+ item.addEventListener('click', () => {
23
+ closeRadixMenu(menu);
24
+ setTimeout(() => openRelocateDialog(project), 80);
25
+ });
26
+ anchorItem.before(item);
27
+ }
28
+
29
+ function hMoveIcon(cls) {
30
+ const ns = 'http://www.w3.org/2000/svg';
31
+ const svg = document.createElementNS(ns, 'svg');
32
+ svg.setAttribute('viewBox', '0 0 24 24');
33
+ svg.setAttribute('fill', 'none');
34
+ svg.setAttribute('stroke', 'currentColor');
35
+ svg.setAttribute('stroke-width', '2');
36
+ svg.setAttribute('stroke-linecap', 'round');
37
+ svg.setAttribute('stroke-linejoin', 'round');
38
+ svg.setAttribute('class', cls);
39
+ // 双向箭头(arrow-right-left):位置指向的变更
40
+ for (const d of ['m16 3 4 4-4 4', 'M20 7H4', 'm8 21-4-4 4-4', 'M4 17h16']) {
41
+ const p = document.createElementNS(ns, 'path');
42
+ p.setAttribute('d', d);
43
+ svg.append(p);
44
+ }
45
+ return svg;
46
+ }
47
+
48
+ export function openRelocateDialog(project) {
49
+ ensureStyle();
50
+ const L = t();
51
+ let submitting = false;
52
+
53
+ openDialog({
54
+ title: L.relocateTitle,
55
+ description: L.relocateDesc,
56
+ width: 'sm:max-w-md',
57
+ onMount: ({ body, close }) => {
58
+ const errLine = h('div', { class: 'mt-2 hidden text-ui-sm text-destructive', 'data-zcodepro-error': '1' });
59
+ const showError = (msg) => { errLine.textContent = msg; errLine.classList.remove('hidden'); };
60
+ const input = textInput({ value: project.path, onEnter: () => submit() });
61
+ const submitBtn = btnPrimary(L.relocateConfirm, () => submit(), 'min-w-24');
62
+
63
+ // ZCode 自带的系统文件夹选择器(含“新建文件夹”);不可用时不显示按钮
64
+ const picker = typeof window !== 'undefined' && window.zcode && typeof window.zcode.selectDirectory === 'function'
65
+ ? () => window.zcode.selectDirectory()
66
+ : null;
67
+ // 优先 helper 的系统目录选择器(可指定起始目录 = 当前项目目录);
68
+ // helper 不可用/无原生选择器时回退到 ZCode 自带选择器(无起始目录)。
69
+ const browse = async () => {
70
+ browseBtn.setAttribute('disabled', 'true');
71
+ try {
72
+ let dir = null;
73
+ const res = await rpc('/pick-folder?start=' + encodeURIComponent(project.path) + '&title=' + encodeURIComponent(L.relocateTitle));
74
+ if (res && res.ok) {
75
+ if (res.path) dir = res.path;
76
+ else if (res.unavailable && picker) dir = await picker();
77
+ } else if (picker) {
78
+ dir = await picker(); // 旧版 helper 无此端点
79
+ }
80
+ if (dir) {
81
+ input.value = dir;
82
+ input.focus();
83
+ }
84
+ } catch { /* 忽略 */ }
85
+ browseBtn.removeAttribute('disabled');
86
+ };
87
+ const browseBtn = h('button', {
88
+ type: 'button',
89
+ class: 'h-9 shrink-0 whitespace-nowrap rounded-lg border border-border bg-surface px-3 text-ui-sm font-medium text-foreground transition-colors hover:bg-surface-hover',
90
+ onClick: () => { void browse(); },
91
+ }, L.browse);
92
+
93
+ const submit = async () => {
94
+ if (submitting) return;
95
+ const newPath = input.value.trim();
96
+ if (!newPath || newPath === project.path) { close(); return; }
97
+ // 二次确认:切换成功后会刷新页面,聊天输入框中未发送的内容会丢失
98
+ openDialog({
99
+ title: L.relocateConfirmTitle,
100
+ description: L.relocateConfirmDesc,
101
+ width: 'sm:max-w-md',
102
+ onMount: ({ body, close: closeConfirm }) => {
103
+ body.append(
104
+ dialogFooter(
105
+ btnSecondary(L.cancel, () => closeConfirm()),
106
+ btnPrimary(L.relocateConfirmGo, () => { closeConfirm(); void runRelocate(); }, 'min-w-24'),
107
+ ),
108
+ );
109
+ },
110
+ });
111
+ };
112
+
113
+ const runRelocate = async () => {
114
+ const newPath = input.value.trim();
115
+ submitting = true;
116
+ submitBtn.setAttribute('disabled', 'true');
117
+ errLine.classList.add('hidden');
118
+ const res = await rpc('/project/relocate', {
119
+ method: 'POST',
120
+ body: { path: project.path, newPath },
121
+ });
122
+ submitting = false;
123
+ submitBtn.removeAttribute('disabled');
124
+ if (res.ok) {
125
+ close();
126
+ showToast(res.indexSynced === false ? L.relocateIndexSkipped : L.relocateSuccess);
127
+ setTimeout(() => location.reload(), 900);
128
+ return;
129
+ }
130
+ const byCode = {
131
+ 'same-path': L.relocateSame,
132
+ 'new-not-found': L.relocateNotFound,
133
+ 'protected-path': L.relocateProtected,
134
+ 'index-busy': L.relocateIndexBusy,
135
+ };
136
+ showError(byCode[res.code] || L.failed + ': ' + (errText(res) || L.retryHint));
137
+ };
138
+
139
+ body.append(
140
+ h('div', { class: 'space-y-3' },
141
+ h('div', {},
142
+ h('div', { class: 'mb-1.5 text-ui-sm font-medium text-foreground' }, L.relocateNewPathLabel),
143
+ h('div', { class: 'flex gap-2' },
144
+ h('div', { class: 'min-w-0 flex-1' }, input),
145
+ browseBtn)),
146
+ h('div', {},
147
+ h('div', { class: 'mb-1 text-ui-xs text-foreground-subtle' }, L.pathLabel),
148
+ h('div', { class: 'break-all rounded-lg border border-border bg-surface-hover/50 px-3 py-1.5 font-mono text-ui-xs text-foreground-subtle' }, project.path)),
149
+ h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, L.relocateHint),
150
+ errLine
151
+ ),
152
+ dialogFooter(btnSecondary(L.cancel, () => close()), submitBtn)
153
+ );
154
+ },
155
+ });
156
+ }
@@ -0,0 +1,244 @@
1
+ // 会话快捷切换:alt+z 立即切到上次使用的会话(再按切回);按住 alt 按 x/c 打开
2
+ // MRU 弹窗前后移动高亮(x 更早 / c 更新,两端循环),松开 alt 切换,Esc / 点弹窗外
3
+ // 取消,点行即切。MRU(最近使用序)从注入起记录(不回填历史库,上限 50 条),
4
+ // 与侧栏展示顺序无关;当前会话靠侧栏条目的 bg-selected 类识别(实测标记),
5
+ // 事件驱动扫描(会话行增删/选中标记移动才触发)+ 低频兜底轮询——
6
+ // 点击、新建任务自动跳转等任何方式的切换都能捕捉,聊天区流式输出不产生扫描。
7
+ // 目标条目在折叠项目下时先展开项目行再点击(展开先例见 pinned-expand.js)。
8
+ import { h, t, getConfig } from '../core.js';
9
+ import { ensureStyle, showToast } from '../ui.js';
10
+
11
+ const MRU_LIMIT = 50;
12
+ const SCAN_DEBOUNCE_MS = 150; // 相关 DOM 变动后的去抖,合并连续变动
13
+ const SCAN_FALLBACK_MS = 5000; // 兜底轮询周期:仅补观察器覆盖不到的路径
14
+
15
+ let mru = []; // 会话键(data-task-item-key),最近使用的在最前
16
+ let current = null; // 当前会话键
17
+ let titles = new Map(); // 键 → 会话标题(展示缓存;条目在 DOM 时实时读取)
18
+ let popupEl = null; // 打开的切换弹窗(null = 未打开)
19
+ let aliasesCache = {}; // 项目路径 → 别名(打开弹窗时预取,renderPopup 同步使用)
20
+ let highlight = 0;
21
+ let scanDebounce = 0;
22
+
23
+ function firstLineOf(item) {
24
+ // 条目文本含相对时间等杂项,取首个截断类标题元素,兜底 innerText 首行
25
+ const tEl = item.querySelector('[class*="truncate"]');
26
+ return (tEl ? tEl.textContent : (item.innerText || '').split('\n')[0])?.trim() || '';
27
+ }
28
+
29
+ function workspaceOf(key) {
30
+ // 键格式 <workspace_path>:<sess_id>;sess 段固定以 "sess_" 开头,按其定位分隔符
31
+ // (Windows 盘符路径里也有冒号,不能按第一个 ':' 切)
32
+ const i = key.indexOf(':sess_');
33
+ return i > 0 ? key.slice(0, i) : key;
34
+ }
35
+
36
+ function findCurrent() {
37
+ for (const el of document.querySelectorAll('[data-task-item-key]')) {
38
+ if ((el.className + '').includes('bg-selected')) return el;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ // 记录一次“切到 key”;顺带刷新在场条目的标题缓存
44
+ function record(key) {
45
+ if (key && key !== current) {
46
+ current = key;
47
+ mru = mru.filter((k) => k !== key);
48
+ mru.unshift(key);
49
+ if (mru.length > MRU_LIMIT) mru.length = MRU_LIMIT;
50
+ }
51
+ }
52
+
53
+ // 扫描:bg-selected 标记识别当前会话(新建任务自动跳转等非点击切换也能捕捉);
54
+ // 标记缺失时由点击兜底记录。顺带刷新在场条目的标题缓存
55
+ function scan() {
56
+ const cur = findCurrent();
57
+ if (cur) record(cur.getAttribute('data-task-item-key'));
58
+ for (const el of document.querySelectorAll('[data-task-item-key]')) {
59
+ titles.set(el.getAttribute('data-task-item-key'), firstLineOf(el));
60
+ }
61
+ }
62
+
63
+ // 只有任务条目相关变动才值得重扫:会话行新增(项目展开/新任务)、行内 class 变化
64
+ // (bg-selected 选中标记移动)。聊天区流式输出等全页高频变动在此滤掉——
65
+ // 这是本功能从 800ms 固定轮询改为事件驱动的关键。
66
+ const rowSel = '[data-task-item-key]';
67
+ const inTaskRow = (node) => node instanceof Element && !!node.closest && !!node.closest(rowSel);
68
+ function taskRowMutations(muts) {
69
+ for (const m of muts) {
70
+ if (m.type === 'attributes') {
71
+ if (inTaskRow(m.target)) return true;
72
+ } else if (m.type === 'characterData') {
73
+ if (m.target.parentElement && inTaskRow(m.target.parentElement)) return true;
74
+ } else if (inTaskRow(m.target)) {
75
+ return true;
76
+ } else {
77
+ for (const n of m.addedNodes) {
78
+ if (n instanceof Element && (n.matches(rowSel) || !!n.querySelector(rowSel))) return true;
79
+ }
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function scheduleScan() {
86
+ if (scanDebounce) return;
87
+ scanDebounce = setTimeout(() => { scanDebounce = 0; scan(); }, SCAN_DEBOUNCE_MS);
88
+ }
89
+
90
+ // 切到指定会话:条目不在 DOM(项目折叠)时先展开项目行,等条目出现再点击
91
+ async function switchTo(key) {
92
+ const find = () => document.querySelector(`[data-task-item-key="${CSS.escape(key)}"]`);
93
+ let item = find();
94
+ if (!item) {
95
+ const ws = workspaceOf(key);
96
+ const header = document.querySelector(`[data-testid="workspace-item-${CSS.escape(ws)}"]`);
97
+ const expander = header && header.querySelector('[aria-expanded]');
98
+ if (expander && expander.getAttribute('aria-expanded') === 'false') {
99
+ expander.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
100
+ for (let i = 0; i < 20 && !(item = find()); i++) await new Promise((r) => setTimeout(r, 100));
101
+ }
102
+ }
103
+ if (!item) return false;
104
+ item.scrollIntoView({ block: 'nearest' });
105
+ item.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
106
+ return true;
107
+ }
108
+
109
+ // —— 切换弹窗(类 alt+tab):居中列表卡,x/c 移动高亮,松开 alt / 点击提交 ——
110
+
111
+ function closePopup(commit) {
112
+ if (!popupEl) return;
113
+ const idx = highlight;
114
+ popupEl.remove();
115
+ popupEl = null;
116
+ if (commit && idx > 0 && mru[idx] && mru[idx] !== current) {
117
+ void switchTo(mru[idx]).then((ok) => { if (!ok) showToast(t().switcherFailed, 'error'); });
118
+ }
119
+ }
120
+
121
+ function findTitle(key) {
122
+ const item = document.querySelector(`[data-task-item-key="${CSS.escape(key)}"]`);
123
+ if (item) titles.set(key, firstLineOf(item));
124
+ const id = key.slice(key.indexOf(':sess_') + 1);
125
+ return titles.get(key) || `sess ${id.slice(0, 8)}`;
126
+ }
127
+
128
+ // 项目显示名:优先自定义别名(与侧栏一致),无别名退回目录名
129
+ function projLabelOf(key, aliases) {
130
+ const ws = workspaceOf(key);
131
+ if (!ws) return '';
132
+ return aliases[ws] || ws.replace(/[\\/]+$/, '').split(/[\\/]/).pop();
133
+ }
134
+
135
+ function renderPopup() {
136
+ if (!popupEl) return;
137
+ const L = t();
138
+ const list = popupEl.querySelector('[data-zcodepro-switcher-list]');
139
+ list.replaceChildren(...mru.map((key, i) => {
140
+ const proj = projLabelOf(key, aliasesCache);
141
+ return h('div', {
142
+ class: 'zcodepro-switcher-row',
143
+ 'data-active': i === highlight ? '' : undefined,
144
+ onClick: () => { highlight = i; closePopup(true); },
145
+ },
146
+ // 标题文本包 span:截断省略作用在文本节点上(flex 容器自身 ellipsis 无效)
147
+ h('div', { class: 'zcodepro-switcher-title' },
148
+ i === 0 ? h('span', { class: 'zcodepro-switcher-tag' }, L.switcherCurrent) : null,
149
+ h('span', null, findTitle(key))),
150
+ // 无项目归属(如置顶)时不渲染空行
151
+ proj ? h('div', { class: 'zcodepro-switcher-ws' }, proj) : null);
152
+ }));
153
+ }
154
+
155
+ async function openPopup() {
156
+ if (popupEl) return;
157
+ const cfg = await getConfig().catch(() => null);
158
+ aliasesCache = (cfg && cfg.aliases && typeof cfg.aliases === 'object') ? cfg.aliases : {};
159
+ ensureStyle();
160
+ const L = t();
161
+ highlight = 0;
162
+ popupEl = h('div', {
163
+ class: 'zcodepro-switcher-overlay',
164
+ // 点弹窗外 = 取消(不切)
165
+ onMousedown: (e) => { if (e.target === popupEl) closePopup(false); },
166
+ },
167
+ h('div', { class: 'zcodepro-switcher' },
168
+ h('div', { class: 'zcodepro-switcher-hint' }, L.switcherHint),
169
+ h('div', { 'data-zcodepro-switcher-list': '1', class: 'zcodepro-switcher-list' })));
170
+ document.body.append(popupEl);
171
+ renderPopup();
172
+ }
173
+
174
+ function moveHighlight(delta) {
175
+ if (!popupEl || mru.length < 2) return;
176
+ highlight = (highlight + delta + mru.length) % mru.length;
177
+ renderPopup();
178
+ const row = popupEl.querySelector('[data-active]');
179
+ if (row) row.scrollIntoView({ block: 'nearest' });
180
+ }
181
+
182
+ export function startSessionSwitch() {
183
+ scan();
184
+ // 事件驱动为主:任务条目相关变动(去抖 150ms)立即重扫
185
+ new MutationObserver((muts) => { if (taskRowMutations(muts)) scheduleScan(); })
186
+ .observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
187
+ // 低频兜底轮询 + 页面隐藏时不扫、回到前台立即补一次
188
+ setInterval(() => { if (!document.hidden) scan(); }, SCAN_FALLBACK_MS);
189
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) scan(); });
190
+
191
+ // 点击兜底:bg-selected 标记不可用/未出现时,点条目(含本功能派发的合成点击)即记录
192
+ document.addEventListener('click', (e) => {
193
+ const item = e.target instanceof Element && e.target.closest('[data-task-item-key]');
194
+ if (item) record(item.getAttribute('data-task-item-key'));
195
+ }, true);
196
+
197
+ document.addEventListener('keydown', (e) => {
198
+ // 裸 Esc 也允许取消弹窗(alt 按住期间通常带 alt 修饰,但松开前后瞬间可能没有)
199
+ if (e.key === 'Escape' && popupEl) {
200
+ e.preventDefault();
201
+ e.stopImmediatePropagation();
202
+ closePopup(false);
203
+ return;
204
+ }
205
+ if (!e.altKey || e.ctrlKey || e.metaKey || e.isComposing) return;
206
+ // 我们的设置弹窗开着时不抢键(它有自己的 Esc/焦点逻辑)
207
+ if (document.querySelector('.zcodepro-overlay')) return;
208
+ const k = e.key.toLowerCase();
209
+ if (k !== 'z' && k !== 'x' && k !== 'c') return;
210
+ e.preventDefault();
211
+ e.stopImmediatePropagation();
212
+ void (async () => {
213
+ const cfg = await getConfig(); // 带 3s 缓存
214
+ if (cfg.features && cfg.features.sessionSwitch === false) return;
215
+ if (k === 'z') {
216
+ if (e.repeat) return;
217
+ // 即时切换:关掉可能开着的弹窗,切到最近使用的会话(上次使用的会话)
218
+ closePopup(false);
219
+ // 先按侧栏选中标记实时识别当前会话:新建任务视图里没有选中行,
220
+ // 此时“当前”视为空,直接回最近使用的会话(新开应用点开过一个会话、
221
+ // 进了新建任务再按 alt+z 也能切回去)
222
+ const live = findCurrent();
223
+ if (live) record(live.getAttribute('data-task-item-key'));
224
+ const target = live
225
+ ? (mru[0] === current ? mru[1] : mru.find((mk) => mk !== current))
226
+ : mru[0];
227
+ if (!target) { showToast(t().switcherEmpty); return; }
228
+ void switchTo(target).then((ok) => { if (!ok) showToast(t().switcherFailed, 'error'); });
229
+ return;
230
+ }
231
+ // x/c:弹窗导航(无可导航会话时与 z 一致轻提示)
232
+ if (mru.length < 2) { showToast(t().switcherEmpty); return; }
233
+ if (!popupEl) await openPopup();
234
+ moveHighlight(k === 'x' ? 1 : -1);
235
+ })();
236
+ }, true);
237
+
238
+ document.addEventListener('keyup', (e) => {
239
+ if (e.key === 'Alt' && popupEl) closePopup(true); // 松开 alt:提交高亮项
240
+ }, true);
241
+
242
+ // 窗口失焦(alt+tab 到别的应用等)视同取消
243
+ window.addEventListener('blur', () => closePopup(false));
244
+ }