zcode-pro 0.17.2 → 0.19.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 +1368 -216
- package/package.json +1 -1
- package/src/host/helper.mjs +1248 -156
- package/src/host/main.mjs +11 -0
- package/src/host/pickFolder.mjs +48 -3
- package/src/host/setup.mjs +34 -9
- package/src/host/taskIndex.mjs +83 -0
- package/src/host/webdav.mjs +128 -12
- package/src/inject/core.js +190 -42
- package/src/inject/features/file-menu.js +9 -2
- package/src/inject/features/plugin-updates.js +1 -1
- package/src/inject/features/settings-dialog.js +821 -118
- package/src/inject/features/styles.js +15 -1
- package/src/inject/features/title-gen.js +169 -0
- package/src/inject/features/title-lock.js +14 -0
- package/src/inject/features/toolbar-icons.js +38 -4
- package/src/inject/index.js +14 -2
- package/src/inject/provider-fields.js +71 -0
- package/src/inject/ui.js +23 -7
|
@@ -20,6 +20,7 @@ export const STYLE_DEFAULTS = {
|
|
|
20
20
|
tableCellPaddingV: 3, // 单元格上下边距(应用默认 3px)
|
|
21
21
|
tableCellPaddingH: 3, // 单元格左右边距(应用默认 3px)
|
|
22
22
|
lineHeight: 1.75, // 回答行高(leading-[1.75],挂在答案内容容器上)
|
|
23
|
+
thinkingLineHeight: 1.5, // 思考行高:“已工作/工作中”过程视图的行距(实测应用默认 1.5;范围见 buildCss 注释)
|
|
23
24
|
userLineHeight: 1.5, // 提问行高(用户消息文本容器,默认 normal=1.5)
|
|
24
25
|
contentWidth: null, // 内容宽度:默认 100%(跟随应用,不覆盖)
|
|
25
26
|
sidebarProjectSpacing: 20, // 侧栏项目间距:项目行视觉间距(行内留白 12 + 边距 8)
|
|
@@ -137,6 +138,16 @@ function buildCss(styles) {
|
|
|
137
138
|
// 行高挂在内容容器上,段落/列表项继承;代码块等自带行高的元素不受影响
|
|
138
139
|
parts.push(`${CONV} .space-y-4{line-height:${lh} !important;}`);
|
|
139
140
|
}
|
|
141
|
+
const tlh = styles.thinkingLineHeight;
|
|
142
|
+
if (typeof tlh === 'number' && Number.isFinite(tlh) && tlh >= 0.8) {
|
|
143
|
+
// 「思考行高」管“已工作/工作中 N 秒”过程视图的行距(读取/编辑/查阅/终端等
|
|
144
|
+
// 条目行、流式思考预览,默认 1.5):行高挂在历史内容容器上由各行继承;
|
|
145
|
+
// 过程里的叙述正文走 .space-y-4(回答行高,选择器更具体压过这里的继承)。
|
|
146
|
+
// 单独展开的“思考·持续了 N 秒”块内内容不在此设置范围内
|
|
147
|
+
parts.push(
|
|
148
|
+
`[data-testid^="chat-assistant-history-content"]{line-height:${tlh} !important;}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
140
151
|
const ulh = styles.userLineHeight;
|
|
141
152
|
if (typeof ulh === 'number' && Number.isFinite(ulh) && ulh >= 0.8) {
|
|
142
153
|
// 用户提问是独立的 whitespace-pre-wrap 文本容器,行高默认与回答不同(1.5 vs 1.75)
|
|
@@ -258,7 +269,10 @@ const HL_SELECTORS = {
|
|
|
258
269
|
tableCellPaddingV: `${CONV} td, ${CONV} th`,
|
|
259
270
|
tableCellPaddingH: `${CONV} td, ${CONV} th`,
|
|
260
271
|
codeLineHeight: `${CONV} diffs-container, ${CONV} pre`,
|
|
261
|
-
lineHeight: `${CONV} .space-y-4`,
|
|
272
|
+
lineHeight: `${CONV} .space-y-4:not([data-reasoning-content] *)`,
|
|
273
|
+
// 思考行高作用在“已工作/工作中”过程视图容器上;视图折叠未挂载时不标
|
|
274
|
+
// (触发行不属此设置范围,不额外描边)
|
|
275
|
+
thinkingLineHeight: `[data-testid^="chat-assistant-history-content"]`,
|
|
262
276
|
userLineHeight: `${CONV} [class*="user-row"] .whitespace-pre-wrap`,
|
|
263
277
|
contentWidth: '[data-v4-timeline-content-column]',
|
|
264
278
|
sidebarProjectSpacing: '[data-testid^="workspace-item-"]',
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// 任务名提示词(自动起名):官方在新会话首轮后自动起名的提示词写死在运行时
|
|
2
|
+
// 里、不可配置,起出的名字常不合适。本功能在“新会话”出现后,把会话区的
|
|
3
|
+
// 用户消息交给 helper(/title-gen/run),用设置弹窗里选择的供应商与提示词
|
|
4
|
+
// 重新生成标题,并按“手动重命名”写入任务索引(title_overridden=1):
|
|
5
|
+
// - 触发信号 = 侧栏出现一个全新的任务行(本页面生命周期内没见过的
|
|
6
|
+
// data-task-item-key)且它是当前选中会话——项目展开/滚动复现的旧行、切换
|
|
7
|
+
// 到老会话、后台建的任务都不会触发;
|
|
8
|
+
// - 生成时机 = 模型开始作答(会话区出现用户消息以外的内容变动)。提问阶段
|
|
9
|
+
// 的全部用户消息都会进入标题——新建任务连问两次、第一次还没得到回答的
|
|
10
|
+
// 场景,两次提问都算数;迟迟没有作答时按静默时长兜底生成;
|
|
11
|
+
// - 官方自动起名从此不会再覆盖索引里的名字(索引层尊重重命名标记),界面
|
|
12
|
+
// 层偶发的覆盖显示由 title-lock 按锁定表钉回;写库成功后立即更新侧栏行
|
|
13
|
+
// 并刷新锁定表,新名即刻上屏;
|
|
14
|
+
// - 失败静默(一次生成失败不打扰用户),每个会话只尝试一次。
|
|
15
|
+
// 扫描事件驱动(任务行相关变动去抖 300ms)+ 低频兜底轮询,选法同 title-lock。
|
|
16
|
+
import { rpc, getConfig } from '../core.js';
|
|
17
|
+
import { setRowTitle, refreshPinnedTitles } from './title-lock.js';
|
|
18
|
+
|
|
19
|
+
const CONV = '[class*="@md/conversation"]';
|
|
20
|
+
const USER_ROW = '[class*="user-row"]';
|
|
21
|
+
const SCAN_DEBOUNCE_MS = 300; // 任务行相关 DOM 变动后的去抖
|
|
22
|
+
const SCAN_FALLBACK_MS = 5000; // 兜底轮询周期(补观察器覆盖不到的路径)
|
|
23
|
+
const SELECT_WAIT_MS = 2000; // 新行出现后等它成为当前会话的时间
|
|
24
|
+
const MSG_WAIT_MS = 10000; // 任务行先于消息上屏时,等首条消息的最长时间
|
|
25
|
+
const QUIET_FALLBACK_MS = 120000; // 迟迟没有作答时的静默兜底(期间追加的提问也会带上)
|
|
26
|
+
const COLLECT_MAX_MS = 600000; // 收集期上限(10 分钟内始终没等到作答就放弃)
|
|
27
|
+
|
|
28
|
+
let installed = false;
|
|
29
|
+
const knownKeys = new Set(); // 页面生命周期内见过的任务键(含启动时已在侧栏的)
|
|
30
|
+
const attempted = new Set(); // 已处理过(成功或失败)的会话 id,防重复
|
|
31
|
+
|
|
32
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
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) : '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function currentKey() {
|
|
41
|
+
for (const el of document.querySelectorAll('[data-task-item-key]')) {
|
|
42
|
+
if ((el.className + '').includes('bg-selected')) return el.getAttribute('data-task-item-key') || '';
|
|
43
|
+
}
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 会话区全部用户消息文本(按 DOM 顺序);没有用户消息时返回空数组
|
|
48
|
+
function userTexts() {
|
|
49
|
+
return [...document.querySelectorAll(`${CONV} ${USER_ROW}`)]
|
|
50
|
+
.map((r) => {
|
|
51
|
+
const el = r.querySelector('.whitespace-pre-wrap');
|
|
52
|
+
return ((el ? el.textContent : r.innerText) || '').trim();
|
|
53
|
+
})
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function onNewTaskRow(key) {
|
|
58
|
+
// 新任务行创建时选中标记可能晚一拍;短暂等待它成为当前会话,
|
|
59
|
+
// 期间没选中(其他入口建的任务)就不处理
|
|
60
|
+
const selDeadline = Date.now() + SELECT_WAIT_MS;
|
|
61
|
+
while (currentKey() !== key && Date.now() < selDeadline) await sleep(200);
|
|
62
|
+
if (currentKey() !== key) return;
|
|
63
|
+
|
|
64
|
+
const cfg = await getConfig().catch(() => null);
|
|
65
|
+
if (!cfg || !cfg.titleGen || cfg.titleGen.enabled !== true) return;
|
|
66
|
+
|
|
67
|
+
const sessId = sessIdOf(key);
|
|
68
|
+
if (!sessId || attempted.has(sessId)) return;
|
|
69
|
+
attempted.add(sessId);
|
|
70
|
+
|
|
71
|
+
// 任务行可能先于消息渲染出现:等首条用户消息上屏
|
|
72
|
+
let texts = [];
|
|
73
|
+
const msgDeadline = Date.now() + MSG_WAIT_MS;
|
|
74
|
+
while (Date.now() < msgDeadline) {
|
|
75
|
+
texts = userTexts();
|
|
76
|
+
if (texts.length) break;
|
|
77
|
+
await sleep(400);
|
|
78
|
+
}
|
|
79
|
+
if (!texts.length) return;
|
|
80
|
+
|
|
81
|
+
// 收集期:会话区出现用户消息以外的内容变动 = 模型开始作答,提问阶段结束,
|
|
82
|
+
// 立即生成;期间追加的用户消息会刷新收集与静默计时。切到别的会话时暂停
|
|
83
|
+
// 收集(不把别的会话的内容混进来),切回来继续
|
|
84
|
+
let assistantSeen = false;
|
|
85
|
+
const convObserver = new MutationObserver((muts) => {
|
|
86
|
+
if (currentKey() !== key) return;
|
|
87
|
+
for (const m of muts) {
|
|
88
|
+
const t = m.type === 'characterData' ? m.target.parentElement : m.target;
|
|
89
|
+
if (t instanceof Element && t.closest(USER_ROW)) continue; // 用户消息自身的变动不算作答
|
|
90
|
+
for (const n of [t, ...m.addedNodes]) {
|
|
91
|
+
if (n instanceof Element && n.closest && n.closest(CONV)) { assistantSeen = true; return; }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
convObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
96
|
+
const startAt = Date.now();
|
|
97
|
+
let lastCount = texts.length;
|
|
98
|
+
let lastChange = Date.now();
|
|
99
|
+
try {
|
|
100
|
+
while (Date.now() - startAt < COLLECT_MAX_MS) {
|
|
101
|
+
await sleep(500);
|
|
102
|
+
if (currentKey() !== key) { lastChange = Date.now(); continue; } // 切走:暂停收集
|
|
103
|
+
texts = userTexts();
|
|
104
|
+
if (texts.length !== lastCount) {
|
|
105
|
+
lastCount = texts.length;
|
|
106
|
+
lastChange = Date.now();
|
|
107
|
+
}
|
|
108
|
+
if (assistantSeen) break;
|
|
109
|
+
if (Date.now() - lastChange >= QUIET_FALLBACK_MS) break;
|
|
110
|
+
}
|
|
111
|
+
} finally { convObserver.disconnect(); }
|
|
112
|
+
if (!texts.length) return;
|
|
113
|
+
|
|
114
|
+
const res = await rpc('/title-gen/run', { method: 'POST', body: { sessId, text: texts.join('\n') } }).catch(() => null);
|
|
115
|
+
// written=false 常见于用户已抢先手动重命名——尊重手动命名,不提示
|
|
116
|
+
if (res && res.ok && res.written && res.title) {
|
|
117
|
+
setRowTitle(key, res.title);
|
|
118
|
+
void refreshPinnedTitles();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scanForNewRows() {
|
|
123
|
+
for (const el of document.querySelectorAll('[data-task-item-key]')) {
|
|
124
|
+
const key = el.getAttribute('data-task-item-key') || '';
|
|
125
|
+
if (!key || knownKeys.has(key)) continue;
|
|
126
|
+
knownKeys.add(key);
|
|
127
|
+
void onNewTaskRow(key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 只有任务条目相关变动才重扫(选法同 title-lock.js);聊天区流式输出等
|
|
132
|
+
// 高频变动在此滤掉
|
|
133
|
+
const TASK_SEL = '[data-task-item-key]';
|
|
134
|
+
const inTaskRow = (node) => !!(node instanceof Element && node.closest && node.closest(TASK_SEL));
|
|
135
|
+
function taskRowMutations(muts) {
|
|
136
|
+
for (const m of muts) {
|
|
137
|
+
if (m.type === 'attributes') {
|
|
138
|
+
if (inTaskRow(m.target)) return true;
|
|
139
|
+
} else if (m.type === 'characterData') {
|
|
140
|
+
if (m.target.parentElement && inTaskRow(m.target.parentElement)) return true;
|
|
141
|
+
} else if (inTaskRow(m.target)) {
|
|
142
|
+
return true;
|
|
143
|
+
} else {
|
|
144
|
+
for (const n of m.addedNodes) {
|
|
145
|
+
if (n instanceof Element && (n.matches(TASK_SEL) || !!n.querySelector(TASK_SEL))) return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let scanDebounce = 0;
|
|
153
|
+
function scheduleScan() {
|
|
154
|
+
if (scanDebounce) return;
|
|
155
|
+
scanDebounce = setTimeout(() => { scanDebounce = 0; scanForNewRows(); }, SCAN_DEBOUNCE_MS);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function startTitleGen() {
|
|
159
|
+
if (installed || typeof document === 'undefined') return;
|
|
160
|
+
installed = true;
|
|
161
|
+
// 启动先记下已在侧栏的任务键:此后出现的才是新会话
|
|
162
|
+
scanForNewRows();
|
|
163
|
+
|
|
164
|
+
new MutationObserver((muts) => { if (taskRowMutations(muts)) scheduleScan(); })
|
|
165
|
+
.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'], characterData: true });
|
|
166
|
+
|
|
167
|
+
setInterval(() => { if (!document.hidden) scanForNewRows(); }, SCAN_FALLBACK_MS);
|
|
168
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) scanForNewRows(); });
|
|
169
|
+
}
|
|
@@ -202,6 +202,20 @@ async function bootstrap() {
|
|
|
202
202
|
if (map) scan();
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// 供智能起名(title-gen.js)复用:把指定会话键的所有侧栏行标题改为 text
|
|
206
|
+
// (同一会话可能多行呈现,与失配回写共用同一套元素定位与写法)
|
|
207
|
+
export function setRowTitle(key, text) {
|
|
208
|
+
for (const item of findRows(key)) {
|
|
209
|
+
for (const el of titleElsOf(item)) writeTitle(el, text);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 供智能起名在写库成功后立即刷新锁定表(拉最新标题表并全量复核一次),
|
|
214
|
+
// 之后官方推送的自动标题由本功能按新表钉回
|
|
215
|
+
export function refreshPinnedTitles() {
|
|
216
|
+
return fetchPinned().then((m) => { if (m) scan(); });
|
|
217
|
+
}
|
|
218
|
+
|
|
205
219
|
export function startTitleLock() {
|
|
206
220
|
if (installed || typeof document === 'undefined') return;
|
|
207
221
|
installed = true;
|
|
@@ -103,6 +103,9 @@ function rebuild() {
|
|
|
103
103
|
menu.classList.add(HIDE_CLASS);
|
|
104
104
|
origs = next;
|
|
105
105
|
rowEl?.remove();
|
|
106
|
+
// 原生顶栏新建按钮(文件子页会展开的那个)钉死收起:新建入口统一由我们
|
|
107
|
+
// 的行提供,省去随子页进出显隐自己按钮的同步
|
|
108
|
+
navRow.classList.add('zcodepro-tb-hidden-native');
|
|
106
109
|
const L = t();
|
|
107
110
|
const mk = (kind, orig, nameSel, fallback) => h('button', {
|
|
108
111
|
type: 'button',
|
|
@@ -129,7 +132,15 @@ function rebuild() {
|
|
|
129
132
|
const pIcon = iconOf(next.plugin);
|
|
130
133
|
if (pIcon) pluginBtn.append(pIcon);
|
|
131
134
|
rowEl = h('div', { class: 'zcodepro-tb-row' }, newBtn, searchBtn, autoBtn, pluginBtn);
|
|
132
|
-
|
|
135
|
+
// 插在容器内“更新”按钮之前(实测容器子序:侧栏切换/后退/前进/原生新建
|
|
136
|
+
// 容器/更新按钮)。更新按钮无 testid,按绿色胶囊的 bg-success 样式类定位
|
|
137
|
+
//(实测当前主题;老主题为 bg-emerald,一并匹配),找不到退回末尾追加——
|
|
138
|
+
// 更新按钮只在有新版本时出现。容器内追加/前插都是框架长期容忍的位置,
|
|
139
|
+
// 不要插到容器外侧(React 重渲染会把外侧插入点挤漂移)
|
|
140
|
+
const updBtn = [...navRow.querySelectorAll('button')]
|
|
141
|
+
.find((b) => /bg-(success|emerald)/.test(b.className || ''));
|
|
142
|
+
if (updBtn) updBtn.before(rowEl);
|
|
143
|
+
else navRow.append(rowEl);
|
|
133
144
|
return true;
|
|
134
145
|
}
|
|
135
146
|
|
|
@@ -171,14 +182,32 @@ function sync() {
|
|
|
171
182
|
if (sidebarCollapsed() || sidebarTooNarrow()) {
|
|
172
183
|
if (rowEl) rowEl.style.display = 'none';
|
|
173
184
|
menu.classList.remove(HIDE_CLASS);
|
|
185
|
+
// 这两种形态藏起我们的整行:原生新建按钮要放出来(去掉钉死类),
|
|
186
|
+
// 否则收起侧栏时顶栏没有任何新建入口
|
|
187
|
+
document.querySelectorAll('.zcodepro-tb-hidden-native').forEach((el) => el.classList.remove('zcodepro-tb-hidden-native'));
|
|
174
188
|
return;
|
|
175
189
|
}
|
|
190
|
+
// 旧引用是否全部仍可用;不可用时先试重建,重建未就绪(React 分批挂载,
|
|
191
|
+
// 菜单/导航容器还没上屏)则保留现有行原样等待重试——若在这里提前把旧行
|
|
192
|
+
// 拆掉,重试成功前图标会整行消失,只能等兜底轮询补上(表现为返回会话后
|
|
193
|
+
// 图标迟两三秒才回来)
|
|
176
194
|
const alive = origs && origs.menu === menu
|
|
177
195
|
&& origs.newTask.isConnected && origs.search.isConnected
|
|
178
196
|
&& origs.auto.isConnected && origs.plugin.isConnected
|
|
179
197
|
&& rowEl && rowEl.isConnected;
|
|
180
|
-
if (!alive && !rebuild())
|
|
181
|
-
|
|
198
|
+
if (!alive && !rebuild()) {
|
|
199
|
+
// 菜单在而原节点断连:界面正在换节点,行先藏着等重试。观察器监听菜单
|
|
200
|
+
// 挂载、命中即恢复(150ms 去抖);万一变动被滤掉,这里再排短程重试
|
|
201
|
+
// 兜底,不依赖 5 秒轮询
|
|
202
|
+
if (rowEl) rowEl.style.display = 'none';
|
|
203
|
+
setTimeout(scheduleSync, 400);
|
|
204
|
+
setTimeout(scheduleSync, 1200);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// 界面重渲染可能整体重写容器的 class,隐藏类兜底重加;钉死原生新建按钮的
|
|
208
|
+
// 类同理(收起分支会摘掉它,展开回来若节点仍活着不会触发 rebuild,须在此补)
|
|
209
|
+
const navRow = document.querySelector('[data-testid="desktop-top-nav-back"]')?.parentElement;
|
|
210
|
+
if (navRow) navRow.classList.add('zcodepro-tb-hidden-native');
|
|
182
211
|
menu.classList.add(HIDE_CLASS);
|
|
183
212
|
if (rowEl) rowEl.style.display = '';
|
|
184
213
|
syncActive();
|
|
@@ -219,7 +248,12 @@ function relevantMutations(muts) {
|
|
|
219
248
|
function teardown() {
|
|
220
249
|
rowEl?.remove();
|
|
221
250
|
rowEl = null;
|
|
222
|
-
if (origs) {
|
|
251
|
+
if (origs) {
|
|
252
|
+
origs.menu.classList.remove(HIDE_CLASS);
|
|
253
|
+
// 恢复原生子页新建按钮(功能关闭时不再钉死)
|
|
254
|
+
document.querySelectorAll('.zcodepro-tb-hidden-native').forEach((el) => el.classList.remove('zcodepro-tb-hidden-native'));
|
|
255
|
+
origs = null;
|
|
256
|
+
}
|
|
223
257
|
}
|
|
224
258
|
|
|
225
259
|
async function refreshConfig() {
|
package/src/inject/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// ZCode Pro 注入脚本入口(由 esbuild 打包为 dist/inject.js,launcher 拼上启动参数后注入页面)。
|
|
2
|
-
//
|
|
2
|
+
// 幂等守卫:同一启动器的重复注入(CDP 重连)只初始化一次;换了启动器(开发重载,
|
|
3
|
+
// token 每次启动随机生成)则让页面整页刷新,由新启动器注入唯一一份新代码。
|
|
3
4
|
import { observeRadixPopups, startLocaleResolver } from './core.js';
|
|
4
5
|
import { handleProjectMenu } from './features/project-menu.js';
|
|
5
6
|
import { handleFileMenu } from './features/file-menu.js';
|
|
@@ -9,6 +10,7 @@ import { startAliasWatcher } from './features/alias.js';
|
|
|
9
10
|
import { startTaskOrderWatcher } from './features/task-order.js';
|
|
10
11
|
import { startSessionSwitch } from './features/session-switch.js';
|
|
11
12
|
import { startTitleLock } from './features/title-lock.js';
|
|
13
|
+
import { startTitleGen } from './features/title-gen.js';
|
|
12
14
|
import { startPinnedExpandSuppression } from './features/pinned-expand.js';
|
|
13
15
|
import { startWsRunningSpin } from './features/ws-running.js';
|
|
14
16
|
import { startIdleReclaim } from './features/idle-reclaim.js';
|
|
@@ -20,8 +22,17 @@ import { ensureStyle } from './ui.js';
|
|
|
20
22
|
|
|
21
23
|
(function zcodeproInject() {
|
|
22
24
|
if (typeof window === 'undefined') return;
|
|
23
|
-
if (window.__zcodeproInjected)
|
|
25
|
+
if (window.__zcodeproInjected) {
|
|
26
|
+
// 已有实例在跑:bootstrap 里 token 相同 = 同一启动器的重复注入,跳过;
|
|
27
|
+
// token 不同(或旧版脚本没记 token)= 开发重载换了个启动器 → 刷新页面接管
|
|
28
|
+
const boot = window.__ZCODEPRO__ || {};
|
|
29
|
+
const sameGeneration = window.__zcodeproBootedToken && boot.token
|
|
30
|
+
&& window.__zcodeproBootedToken === boot.token;
|
|
31
|
+
if (!sameGeneration) location.reload();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
24
34
|
window.__zcodeproInjected = true;
|
|
35
|
+
window.__zcodeproBootedToken = (window.__ZCODEPRO__ || {}).token || '';
|
|
25
36
|
|
|
26
37
|
// 尽早取回系统语言(异步 IPC):system 偏好下 macOS 的 navigator.language 可能与
|
|
27
38
|
// 实际不符;start() 里会再调一次,覆盖 preload 桥未就绪的情况
|
|
@@ -40,6 +51,7 @@ import { ensureStyle } from './ui.js';
|
|
|
40
51
|
try { startTaskOrderWatcher(); } catch { /* ignore */ }
|
|
41
52
|
try { startSessionSwitch(); } catch { /* ignore */ }
|
|
42
53
|
try { startTitleLock(); } catch { /* ignore */ }
|
|
54
|
+
try { startTitleGen(); } catch { /* ignore */ }
|
|
43
55
|
try { startImageMenu(); } catch { /* ignore */ }
|
|
44
56
|
try { startPinnedExpandSuppression(); } catch { /* ignore */ }
|
|
45
57
|
try { startWsRunningSpin(); } catch { /* ignore */ }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// 供应商 / 模型 / 接口地址 / API Key 的共用选择逻辑:智能起名与视觉代理两个
|
|
2
|
+
// 设置面板共用,后续其他面板直接复用这里的组件与规则。供应商取值约定:
|
|
3
|
+
// - '':不跟随(自填接口地址 + API Key + 协议直连);
|
|
4
|
+
// - 'session':跟随当前会话(连接由会话侧解析,不存地址密钥);
|
|
5
|
+
// - 其余:供应商 id;已删的 id 保留为「已删除」选项,调用方靠保存的地址/
|
|
6
|
+
// 密钥副本继续直连。
|
|
7
|
+
import { h } from './core.js';
|
|
8
|
+
|
|
9
|
+
export const PROVIDER_FIELD_CLS = 'h-8 w-full rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
|
|
10
|
+
|
|
11
|
+
// 「下拉 + 手输」组合(供应商与模型选择共用):下拉选择列表项,也可以直接
|
|
12
|
+
// 输入;当前值不在列表里(条目被删或手输 id)时保留为带 staleLabel 标注的
|
|
13
|
+
// 选项,不清空、不跳值;当前值为空时插入一个 emptyLabel 空选项,避免下拉框
|
|
14
|
+
// 假装选中第一项而配置里其实没选
|
|
15
|
+
export function comboField(current, options, onChange, { disabled = false, placeholder = '', staleLabel = '(手动输入)', emptyLabel = '' } = {}) {
|
|
16
|
+
const sel = h('select', { class: PROVIDER_FIELD_CLS, disabled: disabled || undefined });
|
|
17
|
+
const input = h('input', { type: 'text', value: current, placeholder, class: PROVIDER_FIELD_CLS, disabled: disabled || undefined });
|
|
18
|
+
const sync = () => {
|
|
19
|
+
const cur = input.value;
|
|
20
|
+
const opts = [...options];
|
|
21
|
+
if (cur && !opts.some((o) => o.value === cur)) opts.push({ value: cur, label: `${cur}${staleLabel}` });
|
|
22
|
+
if (!cur && !opts.some((o) => o.value === '')) opts.unshift({ value: '', label: emptyLabel });
|
|
23
|
+
sel.replaceChildren(...opts.map((o) => h('option', { value: o.value, title: o.value }, o.label)));
|
|
24
|
+
sel.value = cur;
|
|
25
|
+
};
|
|
26
|
+
sel.addEventListener('change', () => { input.value = sel.value; onChange(sel.value, 'select'); });
|
|
27
|
+
input.addEventListener('input', () => onChange(input.value, 'type'));
|
|
28
|
+
input.addEventListener('blur', () => sync());
|
|
29
|
+
sync();
|
|
30
|
+
return h('div', { class: 'flex flex-col gap-1' }, sel, input);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 供应商下拉选项:不跟随(自填地址)/ 跟随当前会话 / 供应商列表
|
|
34
|
+
export function providerOptionsOf(providers, L) {
|
|
35
|
+
return [
|
|
36
|
+
{ value: '', label: L.titleGenProviderNone },
|
|
37
|
+
{ value: 'session', label: L.titleGenProviderSession },
|
|
38
|
+
...providers.map((p) => ({ value: p.id, label: p.name })),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 模型下拉选项:具体供应商 = 该供应商的模型;跟随会话 = 汇总全部供应商的
|
|
43
|
+
// 模型(去重,方便快速选一个);不跟随 / 已删供应商 = 空列表(纯手输)。
|
|
44
|
+
// followSession = true 时,跟随会话的选项表头部多一个“跟随当前会话当前模型”
|
|
45
|
+
//(值为空):任务名提示词与视觉代理(zcode-vision ≥1.6.1)都在运行时把它
|
|
46
|
+
// 解析为会话当前模型;不需要该能力的场景不传即可
|
|
47
|
+
export function modelOptionsOf(providerValue, providers, { followSession = false, followSessionLabel = '' } = {}) {
|
|
48
|
+
const v = (providerValue || '').trim();
|
|
49
|
+
const cur = providers.find((p) => p.id === v);
|
|
50
|
+
if (cur) return cur.models.map((m) => ({ value: m, label: m }));
|
|
51
|
+
if (v === 'session') {
|
|
52
|
+
const head = followSession && followSessionLabel ? [{ value: '', label: followSessionLabel }] : [];
|
|
53
|
+
return [...head, ...[...new Set(providers.flatMap((p) => p.models))].map((m) => ({ value: m, label: m }))];
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 模型输入框的提示文案(按供应商模式选)
|
|
59
|
+
export function modelPlaceholderOf(providerValue, L) {
|
|
60
|
+
const v = (providerValue || '').trim();
|
|
61
|
+
if (v === 'session') return L.titleGenModelSessionHint;
|
|
62
|
+
if (v === '') return L.titleGenModelManualHint;
|
|
63
|
+
return L.titleGenModelHint;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 是否手动连接(不跟随,或供应商 id 已不在列表里):调用方据此决定接口
|
|
67
|
+
// 地址 / API Key / 协议字段是否显示
|
|
68
|
+
export function isManualConn(providerValue, providers) {
|
|
69
|
+
const v = (providerValue || '').trim();
|
|
70
|
+
return v === '' || (v !== 'session' && !providers.some((p) => p.id === v));
|
|
71
|
+
}
|
package/src/inject/ui.js
CHANGED
|
@@ -9,9 +9,10 @@ export function showToast(text, kind = 'info') {
|
|
|
9
9
|
document.getElementById('__zcodepro_toast__')?.remove();
|
|
10
10
|
const el = h('div', {
|
|
11
11
|
id: '__zcodepro_toast__',
|
|
12
|
-
class: 'fixed bottom-6
|
|
12
|
+
class: 'fixed bottom-6 z-[100] flex max-w-md items-center gap-2 rounded-lg border px-4 py-3 text-ui-sm shadow-lg ' +
|
|
13
13
|
(kind === 'error' ? 'border-destructive/40 bg-menu text-foreground' : 'border-border bg-menu text-foreground'),
|
|
14
|
-
|
|
14
|
+
// left 用内联样式:ZCode 的 Tailwind 未编译 left-6/right-6 工具类(bottom-6 有)
|
|
15
|
+
style: 'animation:zcodepro-fade-in .18s ease; left: 1.5rem;',
|
|
15
16
|
},
|
|
16
17
|
h('span', { class: 'truncate' }, text));
|
|
17
18
|
document.body.append(el);
|
|
@@ -37,7 +38,7 @@ const contentClass = 'w-full sm:max-w-md rounded-2xl border-none bg-popover/98 p
|
|
|
37
38
|
// Esc 只关最上层,避免一次 Esc 把两层一起关掉
|
|
38
39
|
let dialogStack = [];
|
|
39
40
|
|
|
40
|
-
export function openDialog({ title, description, onMount, onClose, width = 'sm:max-w-md', overlay = 'dim', draggable = false, posKey = '', dismissOnOutside = true }) {
|
|
41
|
+
export function openDialog({ title, description, onMount, onClose, width = 'sm:max-w-md', overlay = 'dim', draggable = false, posKey = '', dismissOnOutside = true, bodyClass = '' }) {
|
|
41
42
|
const L = t();
|
|
42
43
|
const prevActive = document.activeElement;
|
|
43
44
|
let cleanupDrag = null;
|
|
@@ -76,7 +77,9 @@ export function openDialog({ title, description, onMount, onClose, width = 'sm:m
|
|
|
76
77
|
// 注意不能把 null 直接传给 append:DOM 会把 null 渲染成字面量 "null" 文本
|
|
77
78
|
...(description ? [h('p', { class: 'mt-2 text-ui-sm/relaxed text-foreground-subtle' }, description)] : [])
|
|
78
79
|
);
|
|
79
|
-
|
|
80
|
+
// bodyClass 覆盖默认的 mt-4(标题与内容间距):设置弹窗标题下还有状态行和
|
|
81
|
+
// 标签栏,需要更紧凑时传更小的间距类
|
|
82
|
+
const body = h('div', { class: 'zcodepro-dialog-body ' + (bodyClass || 'mt-4') });
|
|
80
83
|
content.append(body);
|
|
81
84
|
let restorePos = null;
|
|
82
85
|
if (draggable) {
|
|
@@ -328,14 +331,16 @@ export function settingRow(name, desc, checked, onToggle, extra) {
|
|
|
328
331
|
}, knob);
|
|
329
332
|
const extraEl = extra
|
|
330
333
|
? h('div', {
|
|
331
|
-
class: 'flex shrink-0 items-center
|
|
334
|
+
class: 'flex shrink-0 items-center',
|
|
332
335
|
onClick: (e) => { e.stopPropagation(); },
|
|
333
336
|
onMousedown: (e) => { e.stopPropagation(); },
|
|
334
337
|
onWheel: (e) => { e.stopPropagation(); },
|
|
335
338
|
}, extra)
|
|
336
339
|
: null;
|
|
340
|
+
// items-center:简介换行成多行时,开关与整个“标题+简介”块垂直居中
|
|
341
|
+
//(顶对齐会把开关吊在第一行的高度上)
|
|
337
342
|
const row = h('div', {
|
|
338
|
-
class: `flex cursor-pointer items-
|
|
343
|
+
class: `flex cursor-pointer items-center gap-2 rounded-lg ${desc ? 'p-2' : 'p-1.5'} transition-colors hover:bg-surface-hover`,
|
|
339
344
|
onClick: () => { onToggle(); },
|
|
340
345
|
},
|
|
341
346
|
h('div', { class: 'min-w-0 flex-1' },
|
|
@@ -419,13 +424,14 @@ export function ensureStyle() {
|
|
|
419
424
|
align-items: center;
|
|
420
425
|
justify-content: center;
|
|
421
426
|
height: 24px;
|
|
422
|
-
padding: 0
|
|
427
|
+
padding: 0 8px;
|
|
423
428
|
border: none;
|
|
424
429
|
border-radius: 9999px;
|
|
425
430
|
background: transparent;
|
|
426
431
|
cursor: pointer;
|
|
427
432
|
font-size: 12px;
|
|
428
433
|
line-height: 1;
|
|
434
|
+
white-space: nowrap;
|
|
429
435
|
color: var(--color-muted-foreground, #888);
|
|
430
436
|
transition: color 0.15s ease, background-color 0.15s ease;
|
|
431
437
|
}
|
|
@@ -514,6 +520,8 @@ export function ensureStyle() {
|
|
|
514
520
|
.zcodepro-textarea:disabled { opacity: 0.5; }
|
|
515
521
|
/* 小型文本域(视觉代理提示词等):11rem 最小高度只属于全局提示词大编辑框,这里按 rows 定高 */
|
|
516
522
|
.zcodepro-textarea.zcodepro-textarea-sm { min-height: 0; }
|
|
523
|
+
/* 全局提示词大编辑框:默认/最大高度都比默认文本域更高 */
|
|
524
|
+
.zcodepro-textarea.zcodepro-textarea-lg { min-height: 16rem; max-height: 40rem; }
|
|
517
525
|
/* 行高滑杆(样式调整标签页) */
|
|
518
526
|
.zcodepro-range {
|
|
519
527
|
-webkit-appearance: none;
|
|
@@ -699,6 +707,14 @@ export function ensureStyle() {
|
|
|
699
707
|
}
|
|
700
708
|
/* 原侧栏菜单整体隐藏(toolbar-icons 开启时) */
|
|
701
709
|
.zcodepro-tb-hidden { display: none !important; }
|
|
710
|
+
/* 原生顶栏新建任务按钮(文件查看等子页展开的那个):常驻 DOM、靠动画容器
|
|
711
|
+
在 w-7/w-0 间切换显隐。功能开启期间整体钉死收起态——新建入口统一由我们
|
|
712
|
+
的按钮行提供,也就不再需要随子页进出动态显隐自己的新建按钮 */
|
|
713
|
+
.zcodepro-tb-hidden-native div[class*="transition-[opacity,width]"] {
|
|
714
|
+
width: 0 !important;
|
|
715
|
+
opacity: 0 !important;
|
|
716
|
+
pointer-events: none !important;
|
|
717
|
+
}
|
|
702
718
|
/* 已置顶分区可折叠(pinned-collapse.js):标题整行可点,折叠时藏起任务列表,
|
|
703
719
|
chevron 旋向与“项目”分区一致(收起指向右侧) */
|
|
704
720
|
.zcodepro-pin-head {
|