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.
- zcode_pro/__init__.py +3 -0
- zcode_pro/__main__.py +4 -0
- zcode_pro/app/cli.mjs +9 -0
- zcode_pro/app/dist/inject.js +5821 -0
- zcode_pro/app/src/host/cdp.mjs +126 -0
- zcode_pro/app/src/host/helper.mjs +2134 -0
- zcode_pro/app/src/host/main.mjs +262 -0
- zcode_pro/app/src/host/paths.mjs +73 -0
- zcode_pro/app/src/host/pickFolder.mjs +152 -0
- zcode_pro/app/src/host/settings.mjs +61 -0
- zcode_pro/app/src/host/taskIndex.mjs +255 -0
- zcode_pro/app/src/host/taskOrder.mjs +117 -0
- zcode_pro/app/src/host/webdav.mjs +105 -0
- zcode_pro/app/src/inject/core.js +751 -0
- zcode_pro/app/src/inject/features/alias.js +211 -0
- zcode_pro/app/src/inject/features/copy-path.js +78 -0
- zcode_pro/app/src/inject/features/file-menu.js +147 -0
- zcode_pro/app/src/inject/features/idle-reclaim.js +179 -0
- zcode_pro/app/src/inject/features/image-menu.js +215 -0
- zcode_pro/app/src/inject/features/open-folder.js +43 -0
- zcode_pro/app/src/inject/features/pinned-collapse.js +132 -0
- zcode_pro/app/src/inject/features/pinned-expand.js +142 -0
- zcode_pro/app/src/inject/features/plugin-updates.js +28 -0
- zcode_pro/app/src/inject/features/project-menu.js +58 -0
- zcode_pro/app/src/inject/features/relocate-dialog.js +156 -0
- zcode_pro/app/src/inject/features/session-switch.js +244 -0
- zcode_pro/app/src/inject/features/settings-dialog.js +1503 -0
- zcode_pro/app/src/inject/features/settings-entry.js +21 -0
- zcode_pro/app/src/inject/features/styles.js +311 -0
- zcode_pro/app/src/inject/features/task-order.js +78 -0
- zcode_pro/app/src/inject/features/title-lock.js +242 -0
- zcode_pro/app/src/inject/features/toolbar-icons.js +243 -0
- zcode_pro/app/src/inject/features/ws-running.js +472 -0
- zcode_pro/app/src/inject/index.js +55 -0
- zcode_pro/app/src/inject/ui.js +734 -0
- zcode_pro/cli.py +71 -0
- zcode_pro-0.16.0.dist-info/METADATA +45 -0
- zcode_pro-0.16.0.dist-info/RECORD +41 -0
- zcode_pro-0.16.0.dist-info/WHEEL +4 -0
- zcode_pro-0.16.0.dist-info/entry_points.txt +2 -0
- zcode_pro-0.16.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// 功能零:右键 ZCode 设置按钮(侧边栏底部齿轮)弹出增强设置窗口。
|
|
2
|
+
// 左键保留应用原生行为(打开 ZCode 设置页)。监听委托到 document 捕获阶段,
|
|
3
|
+
// React 重渲染替换按钮节点也不失效;testid 随应用改版变动时需同步此处。
|
|
4
|
+
import { openSettingsDialog } from './settings-dialog.js';
|
|
5
|
+
|
|
6
|
+
const SETTINGS_BUTTON_TESTID = 'task-settings-button';
|
|
7
|
+
|
|
8
|
+
export function handleSettingsContextmenu(e) {
|
|
9
|
+
const target = e.target;
|
|
10
|
+
if (!(target instanceof Element)) return;
|
|
11
|
+
if (!target.closest(`[data-testid="${SETTINGS_BUTTON_TESTID}"]`)) return;
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
e.stopPropagation();
|
|
14
|
+
// 增强弹窗已打开时忽略,避免叠开(openDialog 无防重入)
|
|
15
|
+
if (document.querySelector('.zcodepro-overlay')) return;
|
|
16
|
+
openSettingsDialog();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function startSettingsEntry() {
|
|
20
|
+
document.addEventListener('contextmenu', handleSettingsContextmenu, true);
|
|
21
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// 样式调整(设置弹窗“样式调整”标签页):
|
|
2
|
+
// 把配置里的样式覆盖写入独立 <style>,改动即时生效、无需刷新页面;
|
|
3
|
+
// 另提供聚焦预览高亮(showStyleHighlight):设置弹窗聚焦某个样式输入框时,
|
|
4
|
+
// 把会话/侧栏中受影响的区域描边标出,间距类项再把边距区域涂成淡黄条。
|
|
5
|
+
// 策略:不逐个追应用的间距机制(回合列表/包裹层/行分组/内容容器各有各的写法),
|
|
6
|
+
// 而是定位到会话容器(SECTION[class*="@md/conversation"]),在其范围内统一重映射
|
|
7
|
+
// 相关间距工具类(gap-5/gap-4/pt-5/pb-5/space-y-4)——对应用内部结构调整更稳。
|
|
8
|
+
// 注意 Tailwind v4 的间距工具类落到 CSS 的多是逻辑属性 margin-block(-start),
|
|
9
|
+
// 只盖 margin-top 会在级联中输掉——必须逻辑/物理一起盖。
|
|
10
|
+
import { getConfig } from '../core.js';
|
|
11
|
+
|
|
12
|
+
// 各样式项的应用默认值(用于设置界面显示与“恢复默认”)
|
|
13
|
+
export const STYLE_DEFAULTS = {
|
|
14
|
+
rowGap: 20, // 段落间距:会话内各块之间的垂直间距
|
|
15
|
+
listSpacing: 12, // 列表上下留白(my-3)
|
|
16
|
+
listItemSpacing: 6, // 列表项之间的间距(space-y-1.5)
|
|
17
|
+
quoteCodeSpacing: 16, // 引用块上下留白(my-4;代码块同规则)
|
|
18
|
+
codeLineHeight: 1.65, // 代码块行高(应用默认 12px 字号 × 20px 行盒)
|
|
19
|
+
tableSpacing: 20, // 表格上下边距(未设置时跟随段落间距)
|
|
20
|
+
tableCellPaddingV: 3, // 单元格上下边距(应用默认 3px)
|
|
21
|
+
tableCellPaddingH: 3, // 单元格左右边距(应用默认 3px)
|
|
22
|
+
lineHeight: 1.75, // 回答行高(leading-[1.75],挂在答案内容容器上)
|
|
23
|
+
userLineHeight: 1.5, // 提问行高(用户消息文本容器,默认 normal=1.5)
|
|
24
|
+
contentWidth: null, // 内容宽度:默认 100%(跟随应用,不覆盖)
|
|
25
|
+
sidebarProjectSpacing: 20, // 侧栏项目间距:项目行视觉间距(行内留白 12 + 边距 8)
|
|
26
|
+
sidebarTaskSpacing: 10, // 侧栏任务间距:行内留白加行间边距(py-1 + space-y-0.5)
|
|
27
|
+
uiFont: null, // 界面字体:除终端外所有界面文字(null = 跟随应用默认)
|
|
28
|
+
userFont: null, // 提问字体:会话中提问内容
|
|
29
|
+
assistantFont: null, // 回答字体:会话中回答正文(代码块仍用等宽字体)
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let styleEl = null;
|
|
33
|
+
|
|
34
|
+
// 应用原生的字体变量值:首次应用覆盖前抓取,用于界面字体开启时的终端保护
|
|
35
|
+
let origFontSans = '';
|
|
36
|
+
let origFontMono = '';
|
|
37
|
+
function captureFontVars() {
|
|
38
|
+
try {
|
|
39
|
+
const cs = getComputedStyle(document.documentElement);
|
|
40
|
+
origFontSans = cs.getPropertyValue('--font-sans').trim();
|
|
41
|
+
origFontMono = cs.getPropertyValue('--font-mono').trim();
|
|
42
|
+
} catch { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 字体设置项的取值:非空字符串才算设置(helper 已清洗,这里只做空值防御)
|
|
46
|
+
const fontStack = (v) => (typeof v === 'string' && v.trim() && !/[{};<>\\]/.test(v) ? v.trim() : null);
|
|
47
|
+
|
|
48
|
+
const CONV = '[class*="@md/conversation"]';
|
|
49
|
+
// 答案内容容器里的“特殊块”:段落间距规则跳过它们,由各自的间距项独立控制
|
|
50
|
+
const SPECIAL = ':is(ul, ol, blockquote, pre, table, div:has(table), div:has(pre), [class*="code-block"])';
|
|
51
|
+
// 特殊块的定位选择器:buildCss 的覆盖规则与高亮预览共用一份,避免两处平行维护后漂移
|
|
52
|
+
const SEL_LIST = `${CONV} .space-y-4 > :is(ul, ol)`;
|
|
53
|
+
const SEL_QUOTE = `${CONV} .space-y-4 > :is(blockquote, div:has(pre), [class*="code-block"])`;
|
|
54
|
+
const SEL_TABLE = `${CONV} .space-y-4 > div:has(table)`;
|
|
55
|
+
|
|
56
|
+
function buildCss(styles) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
const n = styles.rowGap;
|
|
59
|
+
const rowGapSet = typeof n === 'number' && Number.isFinite(n) && n >= 0;
|
|
60
|
+
if (rowGapSet) {
|
|
61
|
+
parts.push(
|
|
62
|
+
// 回合内外各级容器(SECTION 自身或后代,二者都覆盖)
|
|
63
|
+
`${CONV}.pb-5,${CONV} .pb-5{padding-bottom:${n}px !important;}`,
|
|
64
|
+
`${CONV}.pt-5,${CONV} .pt-5{padding-top:${n}px !important;}`,
|
|
65
|
+
`${CONV} .flex.flex-col.gap-5{gap:${n}px !important;}`,
|
|
66
|
+
`${CONV} .flex.flex-col.gap-4{gap:${n}px !important;}`,
|
|
67
|
+
// 答案内相邻文本块之间(特殊块除外,走各自间距项)
|
|
68
|
+
`${CONV} .space-y-4 > * + *:not(${SPECIAL}){margin-block-start:${n}px !important;margin-top:${n}px !important;}`,
|
|
69
|
+
// 特殊块后接文本块:去掉文本块的段落间距,避免与特殊块自身留白叠加
|
|
70
|
+
`${CONV} .space-y-4 > ${SPECIAL} + *:not(${SPECIAL}){margin-block-start:0 !important;margin-top:0 !important;}`,
|
|
71
|
+
// 回合内部条目之间(思考触发条 ↔ 正文等)
|
|
72
|
+
`.history-message.flex.flex-col > * + *:not([data-slot="collapsible-content"]){margin-block-start:${n}px !important;margin-top:${n}px !important;}`,
|
|
73
|
+
// 工具/状态卡片(“N 个文件已更改”等)上方间距下限 8px:
|
|
74
|
+
// 段落间距调到 0 时也不至于贴死(gap 与 margin 在弹性布局里相加)
|
|
75
|
+
`${CONV} .flex.flex-col.gap-5 > [data-slot="collapsible"]{margin-block-start:max(0px, calc(8px - ${n}px)) !important;margin-top:max(0px, calc(8px - ${n}px)) !important;}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
// 特殊块(列表/引用块/表格)自身间距项未设置时跟随段落间距,
|
|
79
|
+
// 否则它们带的原生默认边距(12/16px)会与段落间距合并取大值,调小于该值无效
|
|
80
|
+
const own = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : (rowGapSet ? n : null));
|
|
81
|
+
const ls = own(styles.listSpacing);
|
|
82
|
+
const qc = own(styles.quoteCodeSpacing);
|
|
83
|
+
const tsp = own(styles.tableSpacing);
|
|
84
|
+
const anySet = ls !== null || qc !== null || tsp !== null;
|
|
85
|
+
if (ls !== null) parts.push(`${SEL_LIST}{margin-block:${ls}px !important;}`);
|
|
86
|
+
if (qc !== null) parts.push(`${SEL_QUOTE}{margin-block:${qc}px !important;}`);
|
|
87
|
+
// 文本块自带的底边距(应用给段落默认 16px)清零:相邻块的边距合并会取大值,
|
|
88
|
+
// 不清零时段落间距调小于 16 无效,列表也会上边距大、下边距小。
|
|
89
|
+
// 任一间距项设置时启用,间距完全由设置的起始边距决定
|
|
90
|
+
if (rowGapSet || anySet) {
|
|
91
|
+
parts.push(`${CONV} .space-y-4 > :not(${SPECIAL}){margin-block-end:0 !important;margin-bottom:0 !important;}`);
|
|
92
|
+
}
|
|
93
|
+
if (tsp !== null) {
|
|
94
|
+
// 表格包在 div.my-0.flex 里(space-y-4 的直接子块),边距落在包裹层上:
|
|
95
|
+
// 包裹层 = 工具条(复制/下载/预览,常显)+ 8px 内隙 + 表格卡片,应用把
|
|
96
|
+
// 包裹层边距清零(my-0),这里按设置值给上下边距。工具条必须留在文档流里:
|
|
97
|
+
// 表格卡片容器自带 position:relative 且渲染顺序在后,一旦把工具条浮动到
|
|
98
|
+
// 卡片右上角,会被卡片盖住——按钮看得见但点击全被表格截走,还挡住最后一
|
|
99
|
+
// 列列名。表格框架底部隐藏的宽度调节把手(悬停显现,常占 26px)始终不可
|
|
100
|
+
// 见且 pointer-events:none 不挡点击,浮动到卡片底边不占布局
|
|
101
|
+
parts.push(
|
|
102
|
+
`${SEL_TABLE}{margin-block:${tsp}px !important;}`,
|
|
103
|
+
`${SEL_TABLE} [class*="markdown-table-frame"] > .pointer-events-none.py-1{position:absolute !important;left:0;right:0;bottom:0;}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const li = styles.listItemSpacing;
|
|
107
|
+
if (typeof li === 'number' && Number.isFinite(li) && li >= 0) {
|
|
108
|
+
// 应用的 space-y-1.5 把项间距放在上一项的 margin-block-end 上,一并覆盖:
|
|
109
|
+
// 只设下一项的起始边距会被合并取大值(设 0 仍剩 6px)
|
|
110
|
+
parts.push(
|
|
111
|
+
`${CONV} :is(ul, ol) > li{margin-block-end:0 !important;margin-bottom:0 !important;}`,
|
|
112
|
+
`${CONV} :is(ul, ol) > li + li{margin-block-start:${li}px !important;margin-top:${li}px !important;}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const clh = styles.codeLineHeight;
|
|
117
|
+
if (typeof clh === 'number' && Number.isFinite(clh) && clh >= 0.8) {
|
|
118
|
+
// 代码行在 diffs-container 自定义元素的 Shadow DOM 里(pre > code > 行 div),
|
|
119
|
+
// 行高全部继承自宿主自身的显式值(20px)——直接覆盖宿主,倍数按各元素
|
|
120
|
+
// 自身字号计算;保留 pre 规则兜底其他结构
|
|
121
|
+
parts.push(
|
|
122
|
+
`${CONV} diffs-container{line-height:${clh} !important;}`,
|
|
123
|
+
`${CONV} pre,${CONV} pre code,${CONV} pre [class*="line"]{line-height:${clh} !important;}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const tcv = styles.tableCellPaddingV;
|
|
128
|
+
if (typeof tcv === 'number' && Number.isFinite(tcv) && tcv >= 0) {
|
|
129
|
+
parts.push(`${CONV} table :is(td, th){padding-block:${tcv}px !important;padding-top:${tcv}px !important;padding-bottom:${tcv}px !important;}`);
|
|
130
|
+
}
|
|
131
|
+
const tch = styles.tableCellPaddingH;
|
|
132
|
+
if (typeof tch === 'number' && Number.isFinite(tch) && tch >= 0) {
|
|
133
|
+
parts.push(`${CONV} table :is(td, th){padding-inline:${tch}px !important;padding-left:${tch}px !important;padding-right:${tch}px !important;}`);
|
|
134
|
+
}
|
|
135
|
+
const lh = styles.lineHeight;
|
|
136
|
+
if (typeof lh === 'number' && Number.isFinite(lh) && lh >= 0.8) {
|
|
137
|
+
// 行高挂在内容容器上,段落/列表项继承;代码块等自带行高的元素不受影响
|
|
138
|
+
parts.push(`${CONV} .space-y-4{line-height:${lh} !important;}`);
|
|
139
|
+
}
|
|
140
|
+
const ulh = styles.userLineHeight;
|
|
141
|
+
if (typeof ulh === 'number' && Number.isFinite(ulh) && ulh >= 0.8) {
|
|
142
|
+
// 用户提问是独立的 whitespace-pre-wrap 文本容器,行高默认与回答不同(1.5 vs 1.75)
|
|
143
|
+
parts.push(`${CONV} [class*="user-row"] .whitespace-pre-wrap{line-height:${ulh} !important;}`);
|
|
144
|
+
}
|
|
145
|
+
const cw = styles.contentWidth;
|
|
146
|
+
if (cw && (cw.unit === 'px' || cw.unit === '%') && Number.isFinite(cw.value)) {
|
|
147
|
+
// 内容列(data-v4-timeline-content-column)是应用自己的宽度控制器:
|
|
148
|
+
// 宽屏下 w-[calc(100%-24rem)] + max-w-6xl + 位移。盖它的 max-width,
|
|
149
|
+
// % 相对会话区域可用宽度,且沿用应用自带的居中与过渡。
|
|
150
|
+
parts.push(`[data-v4-timeline-content-column]{max-width:${cw.value}${cw.unit} !important;}`);
|
|
151
|
+
}
|
|
152
|
+
// —— 字体 ——
|
|
153
|
+
const uif = fontStack(styles.uiFont);
|
|
154
|
+
if (uif) {
|
|
155
|
+
// 界面字体覆盖根变量:--font-sans 是全界面文字的来源,--font-mono 管
|
|
156
|
+
// 代码块/路径等等宽区域(同属界面字体范围);终端不受影响——xterm 用
|
|
157
|
+
// 自生成样式表写字面量字体栈,不引用这些变量。为防应用把变量传给
|
|
158
|
+
// xterm 的情形,再在终端子树内恢复注入前抓到的原值
|
|
159
|
+
parts.push(`:root{--font-sans:${uif} !important;--font-mono:${uif} !important;}`);
|
|
160
|
+
if (origFontSans && origFontMono) {
|
|
161
|
+
parts.push(`.terminal,.terminal *{--font-sans:${origFontSans};--font-mono:${origFontMono};}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const usf = fontStack(styles.userFont);
|
|
165
|
+
if (usf) {
|
|
166
|
+
parts.push(`${CONV} [class*="user-row"] .whitespace-pre-wrap{font-family:${usf} !important;}`);
|
|
167
|
+
}
|
|
168
|
+
const asf = fontStack(styles.assistantFont);
|
|
169
|
+
if (asf) {
|
|
170
|
+
// 挂在答案内容容器上由段落继承;代码块等自带等宽字体的元素不受影响
|
|
171
|
+
parts.push(`${CONV} .space-y-4{font-family:${asf} !important;}`);
|
|
172
|
+
}
|
|
173
|
+
// —— 侧栏间距(选择器不依赖分区文案,跟随界面语言)——
|
|
174
|
+
const SCROLL = '.flex.flex-1.min-h-0.flex-col.gap-3.overflow-y-auto';
|
|
175
|
+
const sps = styles.sidebarProjectSpacing;
|
|
176
|
+
if (typeof sps === 'number' && Number.isFinite(sps) && sps >= 0) {
|
|
177
|
+
// 项目间距按视觉总量映射(与任务间距同一套):应用默认 = 行内留白约 12px
|
|
178
|
+
// + 块间边距 8px = 20px。边距封顶 8px,余量均分行内;项目行(role=button
|
|
179
|
+
// 的 div,固定 h-8)、分区标题按钮及其 h-7 包装行的固定行高改为按内容+留白。
|
|
180
|
+
// 注意:类名里的点要转义(\.),否则整条选择器非法、规则被丢弃
|
|
181
|
+
const m = Math.min(sps, 8);
|
|
182
|
+
const pad = Math.max(0, Math.round((sps - m) / 2));
|
|
183
|
+
const LIST = '.space-y-2.pb-4:has(div [data-testid^="workspace-item-"])';
|
|
184
|
+
parts.push(
|
|
185
|
+
// 块间边距(应用的 space-y 落在非末尾子块的 margin-block-end 上,同方向覆盖不叠加)
|
|
186
|
+
`${LIST} > :not(:last-child){margin-block-end:${m}px !important;margin-bottom:${m}px !important;}`,
|
|
187
|
+
// 列表底部留白(pb-4)清零:分区间距由 gap 统一给,避免叠加出大空隙
|
|
188
|
+
`${LIST}{padding-bottom:0 !important;}`,
|
|
189
|
+
// 分区间距:外层滚动容器与项目/任务分区所在的 gap-3 容器。
|
|
190
|
+
// 注意不能用嵌套 :has()(:has 里再套 LIST 的 :has)——当前 Chromium 不支持,
|
|
191
|
+
// 整条选择器会被判非法而丢弃;这里用单层 :has 定位 pb-4 列表容器即可
|
|
192
|
+
`${SCROLL}:has(> div .space-y-2.pb-4),.flex.min-h-0.flex-col.gap-3.px-2:has(.space-y-2.pb-4){gap:${m}px !important;row-gap:${m}px !important;}`,
|
|
193
|
+
// 已置顶分区内层 gap-1(标题与列表间)与项目块内层 gap-1(项目行与任务列表间)
|
|
194
|
+
`${SCROLL} .flex.flex-col.gap-1{gap:${m}px !important;row-gap:${m}px !important;}`,
|
|
195
|
+
// 行高压缩:项目行(role=button 的 div,固定 h-8)、分区标题按钮及其 h-7 包装行。
|
|
196
|
+
// 行高随设置压缩,但要留不小于悬停内容的稳定下限(悬停会渲染约 25px 高的
|
|
197
|
+
// 操作按钮组,实际高约 26.2px 含小数,下限需留出余量,行高若随内容伸缩,
|
|
198
|
+
// 行间无空隙时邻居会被推得上下抖动);
|
|
199
|
+
// box-sizing 为 border-box,min-height 已含上下留白。
|
|
200
|
+
// 文字容器占满行高:否则悬停按钮组(比文字高)入场时行内重新居中,
|
|
201
|
+
// 项目名称会被顶起约 2px
|
|
202
|
+
`${LIST} [data-testid^="workspace-item-"]{height:auto !important;min-height:${28 + 2 * pad}px !important;padding-block:${pad}px !important;padding-top:${pad}px !important;padding-bottom:${pad}px !important;}`,
|
|
203
|
+
`${LIST} [data-testid^="workspace-item-"] > div:first-child{align-self:stretch !important;}`,
|
|
204
|
+
`${SCROLL} [data-slot="collapsible-trigger"],${SCROLL} .flex.h-7{height:auto !important;}`,
|
|
205
|
+
`${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;}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const sts = styles.sidebarTaskSpacing;
|
|
209
|
+
if (typeof sts === 'number' && Number.isFinite(sts) && sts >= 0) {
|
|
210
|
+
// 任务行间距按视觉总量映射:应用默认 = 行内上下留白 4px + 行间边距 2px = 10px,
|
|
211
|
+
// 一个数值同时落到两者(边距封顶 2px,余量均分行内),视觉间距与数值一致。
|
|
212
|
+
// “显示更多”行与列表之间的 gap-2 也随任务间距(同属任务区)
|
|
213
|
+
const m = Math.min(sts, 2);
|
|
214
|
+
const pad = Math.max(0, Math.round((sts - m) / 2));
|
|
215
|
+
parts.push(
|
|
216
|
+
`ul.space-y-0\\.5:has(> li[data-task-item-key]) > :not(:last-child){margin-block-end:${m}px !important;margin-bottom:${m}px !important;}`,
|
|
217
|
+
`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;}`,
|
|
218
|
+
`${SCROLL} .flex.flex-col.gap-2{gap:${m}px !important;row-gap:${m}px !important;}`,
|
|
219
|
+
`${SCROLL} div.cursor-pointer[class*="pl-8"]{padding-block:${pad}px !important;padding-top:${pad}px !important;padding-bottom:${pad}px !important;}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return parts.join('');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function applyStyles(styles) {
|
|
226
|
+
if (!styleEl) return;
|
|
227
|
+
styleEl.textContent = styles && typeof styles === 'object' ? buildCss(styles) : '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function startStyleAdjustments() {
|
|
231
|
+
const root = document.head || document.documentElement;
|
|
232
|
+
if (!root) return;
|
|
233
|
+
styleEl = document.getElementById('__zcodepro_styles__');
|
|
234
|
+
if (!styleEl) {
|
|
235
|
+
styleEl = document.createElement('style');
|
|
236
|
+
styleEl.id = '__zcodepro_styles__';
|
|
237
|
+
root.append(styleEl);
|
|
238
|
+
}
|
|
239
|
+
// 先抓应用原生字体变量再应用覆盖(配置里已有界面字体时,抓到的才是原值)
|
|
240
|
+
captureFontVars();
|
|
241
|
+
void getConfig().then((cfg) => applyStyles(cfg.styles)).catch(() => { /* 配置读取失败时保持应用默认 */ });
|
|
242
|
+
// 配置变更(设置弹窗保存等)后即时重应用
|
|
243
|
+
window.addEventListener('zcodepro:config-changed', () => {
|
|
244
|
+
void getConfig(true).then((cfg) => applyStyles(cfg.styles)).catch(() => { /* ignore */ });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// —— 样式预览高亮:设置弹窗里聚焦某个样式输入框时,把会话/侧栏中受影响的
|
|
249
|
+
// 区域标出(类浏览器开发者工具):元素本身 0.5px 内描边(描边间距离即真实
|
|
250
|
+
// 边距,数值直观);间距类项再把实际边距区域用淡黄色涂出,调值时黄条
|
|
251
|
+
// 随设置实时变宽变窄。设置弹窗本身用透明遮罩,正好看得见后面的会话 ——
|
|
252
|
+
const HL_SELECTORS = {
|
|
253
|
+
rowGap: `${CONV} .space-y-4 > *, ${CONV} .flex.flex-col.gap-5 > *`,
|
|
254
|
+
listSpacing: `${CONV} :is(ul, ol)`,
|
|
255
|
+
listItemSpacing: `${CONV} :is(ul, ol) > li`,
|
|
256
|
+
quoteCodeSpacing: SEL_QUOTE,
|
|
257
|
+
tableSpacing: SEL_TABLE,
|
|
258
|
+
tableCellPaddingV: `${CONV} td, ${CONV} th`,
|
|
259
|
+
tableCellPaddingH: `${CONV} td, ${CONV} th`,
|
|
260
|
+
codeLineHeight: `${CONV} diffs-container, ${CONV} pre`,
|
|
261
|
+
lineHeight: `${CONV} .space-y-4`,
|
|
262
|
+
userLineHeight: `${CONV} [class*="user-row"] .whitespace-pre-wrap`,
|
|
263
|
+
contentWidth: '[data-v4-timeline-content-column]',
|
|
264
|
+
sidebarProjectSpacing: '[data-testid^="workspace-item-"]',
|
|
265
|
+
sidebarTaskSpacing: 'li[data-task-item-key]',
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// 间距类项的边距黄条:受影响元素与边距方向(与 buildCss 里的规则选择器保持一致)
|
|
269
|
+
// rowGap 的黄条排除两种边距实际为零的元素:特殊块自身(走各自间距项)、
|
|
270
|
+
// 紧跟特殊块的文本(其起始边距被清零,画了会失真)
|
|
271
|
+
const HL_MARGIN = {
|
|
272
|
+
rowGap: { sel: `${CONV} .space-y-4 > * + *:not(${SPECIAL}):not(${SPECIAL} + *)`, top: true },
|
|
273
|
+
listSpacing: { sel: SEL_LIST, top: true, bottom: true },
|
|
274
|
+
listItemSpacing: { sel: `${CONV} :is(ul, ol) > li + li`, top: true },
|
|
275
|
+
quoteCodeSpacing: { sel: SEL_QUOTE, top: true, bottom: true },
|
|
276
|
+
tableSpacing: { sel: SEL_TABLE, top: true, bottom: true },
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const HL_AMBER = 'rgba(255, 213, 79, 0.5)';
|
|
280
|
+
|
|
281
|
+
let hlEl = null;
|
|
282
|
+
|
|
283
|
+
export function showStyleHighlight(key, value = null) {
|
|
284
|
+
const outlineSel = HL_SELECTORS[key];
|
|
285
|
+
const margin = HL_MARGIN[key];
|
|
286
|
+
if (!outlineSel && !margin) return;
|
|
287
|
+
if (!hlEl || !hlEl.isConnected) {
|
|
288
|
+
hlEl = document.createElement('style');
|
|
289
|
+
hlEl.id = '__zcodepro_hl__';
|
|
290
|
+
(document.head || document.documentElement).append(hlEl);
|
|
291
|
+
}
|
|
292
|
+
const parts = [];
|
|
293
|
+
if (outlineSel) {
|
|
294
|
+
// 0.5px 内描边:画在元素边界内侧、不向外延伸,两条描边之间的可见距离
|
|
295
|
+
// 严格等于实际边距(数值直观),也不挤动内容
|
|
296
|
+
parts.push(`${outlineSel}{box-shadow:inset 0 0 0 0.5px color-mix(in oklab, var(--color-primary, #3b82f6) 65%, transparent) !important;}`);
|
|
297
|
+
}
|
|
298
|
+
if (margin) {
|
|
299
|
+
const v = typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
300
|
+
// 伪元素画在边距所在的区域(元素边界外侧),高度跟随当前设置值;
|
|
301
|
+
// 调值提交时调用方会带新值重进,黄条随之变宽变窄
|
|
302
|
+
parts.push(`${margin.sel}{position:relative !important;}`);
|
|
303
|
+
if (margin.top) parts.push(`${margin.sel}::before{content:'' !important;position:absolute;left:0;right:0;bottom:100%;height:${v}px;background:${HL_AMBER};pointer-events:none;}`);
|
|
304
|
+
if (margin.bottom) parts.push(`${margin.sel}::after{content:'' !important;position:absolute;left:0;right:0;top:100%;height:${v}px;background:${HL_AMBER};pointer-events:none;}`);
|
|
305
|
+
}
|
|
306
|
+
hlEl.textContent = parts.join('');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function hideStyleHighlight() {
|
|
310
|
+
if (hlEl) hlEl.textContent = '';
|
|
311
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// 会话拖动排序:应用自带的列表拖拽只更新视觉、不持久化(逆向确认:
|
|
2
|
+
// 置顶/项目列表按 updated_at DESC 排序、分组会话按 members.sort_order 排序,
|
|
3
|
+
// 均无拖拽写入路径)。本功能在拖放结束时读取列表的最终顺序,
|
|
4
|
+
// 交给 helper 持久化(workspace 列表重盖 updated_at,分组列表重写
|
|
5
|
+
// sort_order),随后刷新界面使新顺序生效。
|
|
6
|
+
import { t, rpc, getConfig } from '../core.js';
|
|
7
|
+
import { showToast, ensureStyle } from '../ui.js';
|
|
8
|
+
import { errText } from '../core.js';
|
|
9
|
+
|
|
10
|
+
let installed = false;
|
|
11
|
+
let dragKey = null; // 拖动源会话键(dragstart 记录,dragend 清除)
|
|
12
|
+
|
|
13
|
+
export function startTaskOrderWatcher() {
|
|
14
|
+
if (installed || typeof document === 'undefined') return;
|
|
15
|
+
installed = true;
|
|
16
|
+
ensureStyle();
|
|
17
|
+
|
|
18
|
+
document.addEventListener('dragstart', (e) => {
|
|
19
|
+
const row = e.target.closest && e.target.closest('[data-task-item-key]');
|
|
20
|
+
dragKey = row ? row.getAttribute('data-task-item-key') : null;
|
|
21
|
+
}, true);
|
|
22
|
+
|
|
23
|
+
document.addEventListener('dragend', () => { dragKey = null; }, true);
|
|
24
|
+
|
|
25
|
+
// 拖拽经过会话行时允许落下(应用自身未必 preventDefault),并做实时预览
|
|
26
|
+
document.addEventListener('dragover', (e) => {
|
|
27
|
+
if (!dragKey) return;
|
|
28
|
+
const row = e.target.closest && e.target.closest('[data-task-item-key]');
|
|
29
|
+
if (!row || row.getAttribute('data-task-item-key') === dragKey) return;
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
32
|
+
// 实时预览:把拖动行移动到目标行前/后(拖动期间 React 不重渲染;落库后刷新重建)
|
|
33
|
+
const rect = row.getBoundingClientRect();
|
|
34
|
+
const before = e.clientY < rect.top + rect.height / 2;
|
|
35
|
+
const src = [...row.parentElement.querySelectorAll('[data-task-item-key]')]
|
|
36
|
+
.find((el) => el.getAttribute('data-task-item-key') === dragKey);
|
|
37
|
+
if (src && src !== row) {
|
|
38
|
+
row.parentNode.insertBefore(src, before ? row : row.nextSibling);
|
|
39
|
+
}
|
|
40
|
+
}, true);
|
|
41
|
+
|
|
42
|
+
document.addEventListener('drop', (e) => {
|
|
43
|
+
if (!dragKey) return;
|
|
44
|
+
const row = e.target.closest && e.target.closest('[data-task-item-key]');
|
|
45
|
+
const list = row ? row.parentElement : null;
|
|
46
|
+
if (!list) return;
|
|
47
|
+
const rows = [...list.querySelectorAll('[data-task-item-key]')];
|
|
48
|
+
if (rows.length < 2) return;
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
dragKey = null;
|
|
52
|
+
void persistOrder(rows.map((el) => el.getAttribute('data-task-item-key')));
|
|
53
|
+
}, true);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let persisting = false;
|
|
57
|
+
async function persistOrder(ordered) {
|
|
58
|
+
if (persisting) return;
|
|
59
|
+
persisting = true;
|
|
60
|
+
try {
|
|
61
|
+
const config = await getConfig();
|
|
62
|
+
if (config.features && config.features.taskOrder === false) return;
|
|
63
|
+
ensureStyle();
|
|
64
|
+
const L = t();
|
|
65
|
+
// 同一工作区的列表 → workspace 作用域(置顶/项目列表);跨工作区 → 分组成员作用域
|
|
66
|
+
const workspaces = new Set(ordered.map((k) => k.slice(0, k.lastIndexOf(':'))));
|
|
67
|
+
const scope = workspaces.size === 1 ? 'workspace' : 'group-members';
|
|
68
|
+
const res = await rpc('/task-order', { method: 'POST', body: { scope, ordered } });
|
|
69
|
+
if (res && res.ok) {
|
|
70
|
+
showToast(L.taskOrderSaved);
|
|
71
|
+
setTimeout(() => location.reload(), 600);
|
|
72
|
+
} else {
|
|
73
|
+
showToast(L.failed + ': ' + (errText(res) || L.retryHint), 'error');
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
persisting = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// 会话名锁定:手动重命名过的会话(任务索引 title_overridden=1),侧栏始终显示
|
|
2
|
+
// 重命名后的名字。背景:ZCode 的任务索引层尊重重命名标记(自动标题不会写库),
|
|
3
|
+
// 但界面层收到运行时推送的自动标题(会话有新回答后生成)会直接覆盖显示——
|
|
4
|
+
// 重启后从库读又恢复重命名名,表现为名字反复变化。本功能把侧栏行标题钉回
|
|
5
|
+
// 索引库中的重命名值:
|
|
6
|
+
// - 标题表来自 helper /pinned-titles(读任务索引,响应带 titleLock 开关),
|
|
7
|
+
// 按需拉取:启动、失配复核、设置开关切换(广播事件)、行内重命名提交,
|
|
8
|
+
// 以及任务行变动后至多一次的 60 秒延迟刷新——兜住首次重命名(此前不在
|
|
9
|
+
// 表里的会话改名对旧表不产生失配,纯按需会漏掉);空闲时零请求零定时器;
|
|
10
|
+
// - 发现行标题与表不符时先延迟复核:重新拉最新表,仍不符才回写 DOM——
|
|
11
|
+
// 用户刚完成的重命名(新名先上屏、写库在毫秒级)此时表已更新成新名,
|
|
12
|
+
// 不会把新名改回旧名;拉表失败则本轮不干预(缓存可能过时);
|
|
13
|
+
// - 行内有输入框(正在重命名)不干预;回写后再复核一次,纠正数据库写入
|
|
14
|
+
// 导致的误回写。
|
|
15
|
+
// 扫描事件驱动(任务行相关变动去抖 150ms;characterData 必须订阅——React
|
|
16
|
+
// 改标题文本走 nodeValue,只产生 characterData 变动)+ 低频兜底轮询。
|
|
17
|
+
import { rpc } from '../core.js';
|
|
18
|
+
|
|
19
|
+
const TASK_SEL = '[data-task-item-key]';
|
|
20
|
+
const SCAN_DEBOUNCE_MS = 150; // 相关 DOM 变动后的去抖,合并连续变动
|
|
21
|
+
const SCAN_FALLBACK_MS = 5000; // 兜底轮询周期:仅补观察器覆盖不到的路径(纯 DOM 扫描,无请求)
|
|
22
|
+
const BOOT_RETRY_MS = 5000; // 启动取表失败时的重试周期(成功后停)
|
|
23
|
+
const RECHECK_DELAY_MS = 1000; // 失配后延迟复核:给刚提交的重命名留写库时间
|
|
24
|
+
const REVERIFY_MS = 3000; // 回写后的再复核:纠正数据库写入极慢导致的误回写
|
|
25
|
+
const MAP_REFRESH_MS = 60000; // 任务行变动后至多一次的延迟表刷新:让新重命名会话进入锁定范围
|
|
26
|
+
|
|
27
|
+
let installed = false;
|
|
28
|
+
let enabled = true; // titleLock 开关(/pinned-titles 响应带来)
|
|
29
|
+
let pinned = null; // Map(task_id → 重命名标题);null = 尚未取到/功能关闭
|
|
30
|
+
let resolved = false; // 首次成功通信(无论开关状态)后不再重试启动拉取
|
|
31
|
+
let fetching = null; // 进行中的取表请求(去重并发:批量失配只发一次)
|
|
32
|
+
const pendingKeys = new Set(); // 已排期复核的会话键
|
|
33
|
+
|
|
34
|
+
function sessIdOf(key) {
|
|
35
|
+
// 会话键格式 <workspace_path>:<sess_id>;sess 段固定以 "sess_" 开头(见 session-switch.js)
|
|
36
|
+
const i = key.indexOf(':sess_');
|
|
37
|
+
return i > 0 ? key.slice(i + 1) : key;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 行标题元素组:当前应用为跑马灯结构(p.task-title-marquee 内若干份相同文本的
|
|
41
|
+
// data-task-title-copy,滚动动画用副本),读第一份、写时全份同改;
|
|
42
|
+
// 旧版/其他区域的截断类结构退回单元素
|
|
43
|
+
function titleElsOf(item) {
|
|
44
|
+
const copies = item.querySelectorAll('[data-task-title-copy]');
|
|
45
|
+
if (copies.length) return [...copies];
|
|
46
|
+
const t = item.querySelector('[class*="truncate"]');
|
|
47
|
+
return t ? [t] : [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 按键找全部匹配行:同一会话理论上可能多处呈现(如置顶区与项目列表),
|
|
51
|
+
// 单查询会漏掉其余行——漏掉的行每次扫描都排期、每次复核都被首行挡回,
|
|
52
|
+
// 永远钉不回;全量匹配逐行执行
|
|
53
|
+
function findRows(key) {
|
|
54
|
+
return [...document.querySelectorAll(`${TASK_SEL}[data-task-item-key="${CSS.escape(key)}"]`)];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 取表(总是最新,rpc 内部已捕获网络错误不会 reject):
|
|
58
|
+
// 失败返回 null = 本轮不干预且保持可重试;成功时顺带更新开关
|
|
59
|
+
async function fetchPinned() {
|
|
60
|
+
if (fetching) return fetching;
|
|
61
|
+
const p = (async () => {
|
|
62
|
+
const res = await rpc('/pinned-titles');
|
|
63
|
+
if (!res || !res.ok) return null;
|
|
64
|
+
resolved = true;
|
|
65
|
+
enabled = res.enabled !== false;
|
|
66
|
+
if (!enabled) { pinned = null; return null; }
|
|
67
|
+
pinned = new Map(Object.entries(res.titles || {}));
|
|
68
|
+
return pinned;
|
|
69
|
+
})();
|
|
70
|
+
fetching = p;
|
|
71
|
+
try { return await p; } finally { if (fetching === p) fetching = null; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 失配判定:返回会话键(失配)或 null(无需干预)。非重命名会话在查表处
|
|
75
|
+
// 即返回,不付 DOM 查询成本
|
|
76
|
+
function isMismatch(item) {
|
|
77
|
+
if (!enabled || !pinned) return null;
|
|
78
|
+
const key = item.getAttribute('data-task-item-key') || '';
|
|
79
|
+
const want = pinned.get(sessIdOf(key));
|
|
80
|
+
if (want === undefined) return null;
|
|
81
|
+
const els = titleElsOf(item);
|
|
82
|
+
if (!els.length) return null;
|
|
83
|
+
if ((els[0].textContent || '').trim() === String(want).trim()) return null;
|
|
84
|
+
// 行内有输入框 = 正在重命名,不干预
|
|
85
|
+
if (item.querySelector('input, textarea, [contenteditable="true"]')) return null;
|
|
86
|
+
return key;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 写标题:每个目标元素优先原位改 nodeValue(React 缓存的正是该文本节点,
|
|
90
|
+
// 其后续标题更新仍走原节点,互不破坏);多文本节点/无文本时整体替换
|
|
91
|
+
// (原位改首个会把残留节点拼在后面,且比较永远失配、越写越长)
|
|
92
|
+
function writeTitle(el, text) {
|
|
93
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
94
|
+
const first = walker.nextNode();
|
|
95
|
+
if (first && !walker.nextNode()) first.nodeValue = text;
|
|
96
|
+
else el.textContent = text;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 失配 → 排期复核;到期后行可能已被 React 重挂载,按键重新定位
|
|
100
|
+
function scheduleEnforce(key) {
|
|
101
|
+
if (pendingKeys.has(key)) return;
|
|
102
|
+
pendingKeys.add(key);
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
pendingKeys.delete(key);
|
|
105
|
+
void (async () => {
|
|
106
|
+
const map = await fetchPinned();
|
|
107
|
+
if (!map) return; // 拉表失败/功能关闭:缓存可能过时,宁可不干预
|
|
108
|
+
const want = map.get(sessIdOf(key));
|
|
109
|
+
if (want === undefined) return;
|
|
110
|
+
let wrote = false;
|
|
111
|
+
for (const item of findRows(key)) {
|
|
112
|
+
if (isMismatch(item) !== key) continue;
|
|
113
|
+
for (const el of titleElsOf(item)) writeTitle(el, want);
|
|
114
|
+
wrote = true;
|
|
115
|
+
}
|
|
116
|
+
// 数据库写入极慢于复核时,上面的回写会把用户新改名改回旧名;写入完成
|
|
117
|
+
// 后再复核一次纠正(届时表为新名、行仍显旧名,重写回新名)
|
|
118
|
+
if (wrote) setTimeout(() => { void enforceKey(key); }, REVERIFY_MS);
|
|
119
|
+
})();
|
|
120
|
+
}, RECHECK_DELAY_MS);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function enforceKey(key) {
|
|
124
|
+
const map = await fetchPinned();
|
|
125
|
+
if (!map) return;
|
|
126
|
+
const want = map.get(sessIdOf(key));
|
|
127
|
+
if (want === undefined) return;
|
|
128
|
+
for (const item of findRows(key)) {
|
|
129
|
+
if (isMismatch(item) !== key) continue;
|
|
130
|
+
for (const el of titleElsOf(item)) writeTitle(el, want);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scan() {
|
|
135
|
+
if (!enabled || !pinned || pinned.size === 0) return;
|
|
136
|
+
for (const item of document.querySelectorAll(TASK_SEL)) {
|
|
137
|
+
const key = isMismatch(item);
|
|
138
|
+
if (key) scheduleEnforce(key);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 只有任务条目相关变动才重扫(选法同 session-switch.js);
|
|
143
|
+
// 本功能自己的回写也会经过这里,回写后文本与表一致,自然空转
|
|
144
|
+
const inTaskRow = (node) => !!(node instanceof Element && node.closest && node.closest(TASK_SEL));
|
|
145
|
+
function taskRowMutations(muts) {
|
|
146
|
+
for (const m of muts) {
|
|
147
|
+
if (m.type === 'attributes') {
|
|
148
|
+
if (inTaskRow(m.target)) return true;
|
|
149
|
+
} else if (m.type === 'characterData') {
|
|
150
|
+
if (m.target.parentElement && inTaskRow(m.target.parentElement)) return true;
|
|
151
|
+
} else if (inTaskRow(m.target)) {
|
|
152
|
+
return true;
|
|
153
|
+
} else {
|
|
154
|
+
for (const n of m.addedNodes) {
|
|
155
|
+
if (n instanceof Element && (n.matches(TASK_SEL) || !!n.querySelector(TASK_SEL))) return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let scanDebounce = 0;
|
|
163
|
+
function scheduleScan() {
|
|
164
|
+
if (scanDebounce) return;
|
|
165
|
+
scanDebounce = setTimeout(() => { scanDebounce = 0; scan(); }, SCAN_DEBOUNCE_MS);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 变动后至多一次的延迟表刷新:重命名提交对旧表不一定产生失配——从未重命名过的
|
|
169
|
+
// 会话第一次改名时不在表里,查表即返回,不会触发按需拉取;没有这一层,新改名
|
|
170
|
+
// 要等到下次失配或页面重载才进入保护。空闲时(无侧栏变动)不挂任何定时器。
|
|
171
|
+
// 每个变动窗口只刷新一次(窗口内后续变动不再重置定时器),页面隐藏时延迟重试。
|
|
172
|
+
let mapRefreshTimer = 0;
|
|
173
|
+
function refreshMapSoon() {
|
|
174
|
+
if (mapRefreshTimer) return;
|
|
175
|
+
const tick = () => {
|
|
176
|
+
if (document.hidden) { mapRefreshTimer = setTimeout(tick, 10000); return; }
|
|
177
|
+
mapRefreshTimer = 0;
|
|
178
|
+
if (enabled && resolved) void fetchPinned().then((m) => { if (m) scan(); });
|
|
179
|
+
};
|
|
180
|
+
mapRefreshTimer = setTimeout(tick, MAP_REFRESH_MS);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 行内重命名提交的即时感知:任务行内编辑器聚焦→失焦 = 一次重命名提交(或取消,
|
|
184
|
+
// 多拉一次表无害),立即刷新让新名即刻进入锁定范围;对话框式重命名(输入框不在
|
|
185
|
+
// 行内)感知不到,由 refreshMapSoon 兜底
|
|
186
|
+
let editingKey = null;
|
|
187
|
+
|
|
188
|
+
// 启动取表失败的重试:间隔逐次翻倍、封顶 60 秒,成功即停——helper 长期异常时
|
|
189
|
+
// 退为约每分钟一次,不维持 5 秒高频
|
|
190
|
+
let bootDelay = 0;
|
|
191
|
+
function scheduleBootRetry() {
|
|
192
|
+
bootDelay = bootDelay ? Math.min(bootDelay * 2, MAP_REFRESH_MS) : BOOT_RETRY_MS;
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
if (resolved) return;
|
|
195
|
+
if (!document.hidden) void bootstrap();
|
|
196
|
+
scheduleBootRetry();
|
|
197
|
+
}, bootDelay);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function bootstrap() {
|
|
201
|
+
const map = await fetchPinned();
|
|
202
|
+
if (map) scan();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function startTitleLock() {
|
|
206
|
+
if (installed || typeof document === 'undefined') return;
|
|
207
|
+
installed = true;
|
|
208
|
+
void bootstrap();
|
|
209
|
+
|
|
210
|
+
new MutationObserver((muts) => {
|
|
211
|
+
if (!taskRowMutations(muts)) return;
|
|
212
|
+
refreshMapSoon();
|
|
213
|
+
scheduleScan();
|
|
214
|
+
})
|
|
215
|
+
.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'], characterData: true });
|
|
216
|
+
|
|
217
|
+
// 启动期取表失败(helper 未就绪/索引暂不可读)时退避重试,成功即停
|
|
218
|
+
scheduleBootRetry();
|
|
219
|
+
|
|
220
|
+
// 低频兜底:纯 DOM 扫描补观察器覆盖不到的路径(无可锁定会话时零成本早退)
|
|
221
|
+
setInterval(() => { if (!document.hidden) scan(); }, SCAN_FALLBACK_MS);
|
|
222
|
+
|
|
223
|
+
// 回到前台补扫(隐藏期间后台推送的自动标题在此追上)
|
|
224
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) scan(); });
|
|
225
|
+
|
|
226
|
+
// 设置弹窗保存开关后立即生效(settings-dialog.js 派发;响应带新开关与标题表)
|
|
227
|
+
window.addEventListener('zcodepro:config-changed', () => { void bootstrap(); });
|
|
228
|
+
|
|
229
|
+
// 行内重命名提交检测(见 editingKey 注释):失焦即拉最新表
|
|
230
|
+
document.addEventListener('focusin', (e) => {
|
|
231
|
+
const el = e.target;
|
|
232
|
+
if (!(el instanceof Element)) return;
|
|
233
|
+
if (!el.matches('input, textarea, [contenteditable="true"]')) return;
|
|
234
|
+
const row = el.closest(TASK_SEL);
|
|
235
|
+
editingKey = row ? (row.getAttribute('data-task-item-key') || '') : null;
|
|
236
|
+
});
|
|
237
|
+
document.addEventListener('focusout', () => {
|
|
238
|
+
if (editingKey === null) return;
|
|
239
|
+
editingKey = null;
|
|
240
|
+
void fetchPinned().then((m) => { if (m) scan(); });
|
|
241
|
+
});
|
|
242
|
+
}
|