llm-api-gateway-cli 1.0.3 → 1.0.4
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.md +36 -16
- package/lib/hub.js +19 -8
- package/lib/launcher.js +1 -1
- package/package.json +1 -1
- package/public/app.js +18 -12
- package/public/index.html +15 -6
- package/public/manual.html +42 -7
- package/public/shell-bridge.js +100 -0
- package/public/shell.css +242 -0
- package/public/shell.html +73 -0
- package/public/shell.js +412 -0
- package/public/styles.css +81 -15
- package/public/task.css +70 -0
- package/public/task.html +19 -1
- package/public/task.js +217 -13
- package/public/theme.js +20 -0
- package/public/tint.js +5 -3
package/public/task.js
CHANGED
|
@@ -16,6 +16,18 @@ const MODE_NOTES = {
|
|
|
16
16
|
plan: '只读调研,计划写入 docs/ 后再执行',
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 懒加载的两个阈值(任务多了 / 单条任务历史长了之后,一次性全建出来会明显卡)。
|
|
21
|
+
*
|
|
22
|
+
* LIST_PAGE 侧边栏一次渲染多少行:默认 30 行,滚到底自动续载,也可以点「加载更多」。
|
|
23
|
+
* MSG_PAGE 一条任务一次渲染最近多少条消息:默认 30 条,往上翻时点「载入更早」前插。
|
|
24
|
+
*
|
|
25
|
+
* 两个数都只影响「已经存在的数据渲染多少」,不改变任何请求与落盘行为:
|
|
26
|
+
* 拿不到的那部分本来就只是 DOM,没有它照样能切任务、能跑、能保存。
|
|
27
|
+
*/
|
|
28
|
+
const LIST_PAGE = 30;
|
|
29
|
+
const MSG_PAGE = 30;
|
|
30
|
+
|
|
19
31
|
function normalizeMode(m) {
|
|
20
32
|
return MODES.includes(m) ? m : 'manual';
|
|
21
33
|
}
|
|
@@ -58,6 +70,9 @@ const state = {
|
|
|
58
70
|
restoredPending: 0, // 刷新后恢复出来的待批准数量
|
|
59
71
|
collapsedDirs: [], // 侧边栏里被折叠的工作目录分组(按目录记,存在浏览器里)
|
|
60
72
|
collapsedTasks: [], // 侧边栏里被折叠的父任务(子任务收起来,按任务 id 记)
|
|
73
|
+
listLimit: LIST_PAGE, // 侧边栏已经渲染多少行(懒加载:滚到底 / 点「加载更多」往上加)
|
|
74
|
+
query: '', // 侧边栏过滤词(匹配任务标题与工作目录)
|
|
75
|
+
renderFrom: null, // 消息区渲染起点(懒加载);null = 下次渲染按 MSG_PAGE 从末尾算
|
|
61
76
|
draftParentId: null, // 草稿要挂在哪条任务下(侧边栏任务行上的「+」派生出的子任务)
|
|
62
77
|
configRows: [], // 服务端白名单行(GET /api/settings 的 rows)——「其他配置」照它渲染,页面不抄第二份
|
|
63
78
|
configControls: {}, // 配置键 → 渲染出来的控件(回填与测试都要按写这个映射)
|
|
@@ -66,6 +81,8 @@ const state = {
|
|
|
66
81
|
const dom = {
|
|
67
82
|
sidebar: document.getElementById('sidebar'),
|
|
68
83
|
taskList: document.getElementById('task-list'),
|
|
84
|
+
taskSearch: document.getElementById('task-search'),
|
|
85
|
+
btnTaskSearchClear: document.getElementById('btn-task-search-clear'),
|
|
69
86
|
tasksCount: document.getElementById('tasks-count'),
|
|
70
87
|
tasksRetention: document.getElementById('tasks-retention'),
|
|
71
88
|
btnPrune: document.getElementById('btn-prune'),
|
|
@@ -136,6 +153,8 @@ function statusHasContent() {
|
|
|
136
153
|
|
|
137
154
|
function setDot(kind) {
|
|
138
155
|
dom.dot.className = `dot${kind ? ' ' + kind : ''}`;
|
|
156
|
+
// 被 Tab 外壳嵌着时(/),本页顶栏是收起的:状态点也报一份给外壳顶栏
|
|
157
|
+
if (window.lgwBridge) window.lgwBridge.dot(kind || '');
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
/**
|
|
@@ -462,6 +481,9 @@ function titleFromMessages(messages) {
|
|
|
462
481
|
|
|
463
482
|
/** 把服务端返回的消息换成界面用的形状 */
|
|
464
483
|
function adoptMessages(list) {
|
|
484
|
+
// 换了任务/重读了历史:渲染窗口跟着重置,否则上一条任务点过「全部展开」之后,
|
|
485
|
+
// 切到一条几百条消息的任务会一次性全建出来 —— 懒加载就白做了
|
|
486
|
+
state.renderFrom = null;
|
|
465
487
|
return (Array.isArray(list) ? list : [])
|
|
466
488
|
.filter((m) => m && (m.content || m.reasoning || m.steps?.length || m.notices?.length))
|
|
467
489
|
.map((m) => ({
|
|
@@ -1207,25 +1229,89 @@ function buildDraftRow(depth = 0) {
|
|
|
1207
1229
|
return item;
|
|
1208
1230
|
}
|
|
1209
1231
|
|
|
1232
|
+
/* ---------- 侧边栏:过滤 + 分批渲染(懒加载) ---------- */
|
|
1233
|
+
|
|
1234
|
+
/** 过滤词是否命中一条任务(标题 / 工作目录,都不区分大小写) */
|
|
1235
|
+
function taskMatches(t, q) {
|
|
1236
|
+
if (!q) return true;
|
|
1237
|
+
const needle = q.toLowerCase();
|
|
1238
|
+
return String(t.title || '').toLowerCase().includes(needle) || String(t.workDir || '').toLowerCase().includes(needle);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/** 过滤后的任务(过滤时不分目录树地全都参与,交给下面照旧分组) */
|
|
1242
|
+
function visibleTasks() {
|
|
1243
|
+
const q = state.query.trim();
|
|
1244
|
+
if (!q) return state.tasks;
|
|
1245
|
+
return state.tasks.filter((t) => taskMatches(t, q));
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/** 侧边栏底部的「加载更多」:只在还有没渲染完的行时出现 */
|
|
1249
|
+
function buildMoreRow(rest) {
|
|
1250
|
+
const box = document.createElement('button');
|
|
1251
|
+
box.type = 'button';
|
|
1252
|
+
box.className = 'task-more';
|
|
1253
|
+
box.textContent = `加载更多(还有 ${rest} 条)`;
|
|
1254
|
+
box.addEventListener('click', (e) => {
|
|
1255
|
+
e.stopPropagation();
|
|
1256
|
+
state.listLimit += LIST_PAGE;
|
|
1257
|
+
renderTasks();
|
|
1258
|
+
// 续载后把新露出来的第一行滚进视野,不然点了像没反应
|
|
1259
|
+
const next = dom.taskList.querySelectorAll('.session-item')[state.listLimit - LIST_PAGE - 1];
|
|
1260
|
+
if (next && next.scrollIntoView) next.scrollIntoView({ block: 'nearest' });
|
|
1261
|
+
});
|
|
1262
|
+
return box;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/** 侧栏滚到接近底部时自动续载(任务多的时候不用一直点按钮) */
|
|
1266
|
+
function wireTaskListScroll() {
|
|
1267
|
+
if (!dom.taskList || dom.taskList.dataset.scrollWired === '1') return;
|
|
1268
|
+
dom.taskList.dataset.scrollWired = '1';
|
|
1269
|
+
dom.taskList.addEventListener('scroll', () => {
|
|
1270
|
+
if (!dom.taskList.querySelector('.task-more')) return;
|
|
1271
|
+
const nearBottom = dom.taskList.scrollHeight - dom.taskList.scrollTop - dom.taskList.clientHeight < 80;
|
|
1272
|
+
if (!nearBottom) return;
|
|
1273
|
+
state.listLimit += LIST_PAGE;
|
|
1274
|
+
renderTasks();
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/** 侧边栏过滤词变了:重设分批窗口并重画(纯客户端过滤,任务列表本来就一次拿全) */
|
|
1279
|
+
function setTaskQuery(q) {
|
|
1280
|
+
state.query = String(q || '');
|
|
1281
|
+
state.listLimit = LIST_PAGE; // 换了条件就重新分批,别把上一次的「加载更多」带过来
|
|
1282
|
+
if (dom.taskSearch && dom.taskSearch.value !== state.query) dom.taskSearch.value = state.query;
|
|
1283
|
+
if (dom.btnTaskSearchClear) dom.btnTaskSearchClear.hidden = !state.query.trim();
|
|
1284
|
+
renderTasks();
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1210
1287
|
function renderTasks() {
|
|
1211
1288
|
dom.taskList.innerHTML = '';
|
|
1212
|
-
|
|
1289
|
+
wireTaskListScroll();
|
|
1290
|
+
const list = visibleTasks();
|
|
1291
|
+
const q = state.query.trim();
|
|
1292
|
+
|
|
1293
|
+
if (!list.length) {
|
|
1213
1294
|
const hint = document.createElement('div');
|
|
1214
1295
|
hint.className = 'sessions-empty';
|
|
1215
|
-
hint.textContent =
|
|
1296
|
+
hint.textContent = q
|
|
1297
|
+
? `没有匹配「${q}」的任务`
|
|
1298
|
+
: state.draft
|
|
1299
|
+
? '新任务:选好目录、发出第一条指令就会保存'
|
|
1300
|
+
: '还没有任务,发出第一条指令就会自动创建';
|
|
1216
1301
|
dom.taskList.appendChild(hint);
|
|
1217
1302
|
updateRetentionInfo();
|
|
1218
1303
|
return;
|
|
1219
1304
|
}
|
|
1220
1305
|
|
|
1221
|
-
const groups = groupTasksByDir(
|
|
1306
|
+
const groups = groupTasksByDir(list);
|
|
1222
1307
|
const draftKey = state.draft ? dirKey(state.draftWorkDir) : null;
|
|
1223
|
-
//
|
|
1224
|
-
|
|
1308
|
+
// 草稿所在的目录可能一条任务都还没有,补一个空组出来,不然「+」点了看不见落点。
|
|
1309
|
+
// 搜索时不给草稿单独开组:过滤出来的结果里混一个空组会让人以为搜到了东西。
|
|
1310
|
+
if (state.draft && !q && !groups.some((g) => g.key === draftKey)) {
|
|
1225
1311
|
groups.push({ key: draftKey, dir: state.draftWorkDir || null, tasks: [] });
|
|
1226
1312
|
}
|
|
1227
1313
|
// 组间按「组里最新的一条」排序;草稿所在的组顶到最前面
|
|
1228
|
-
const rank = (g) => (state.draft && g.key === draftKey ? Number.MAX_SAFE_INTEGER : g.tasks[0]?.updatedAt || 0);
|
|
1314
|
+
const rank = (g) => (state.draft && !q && g.key === draftKey ? Number.MAX_SAFE_INTEGER : g.tasks[0]?.updatedAt || 0);
|
|
1229
1315
|
groups.sort((a, b) => rank(b) - rank(a));
|
|
1230
1316
|
|
|
1231
1317
|
const sameName = new Map();
|
|
@@ -1234,32 +1320,50 @@ function renderTasks() {
|
|
|
1234
1320
|
sameName.set(label, (sameName.get(label) || 0) + 1);
|
|
1235
1321
|
}
|
|
1236
1322
|
|
|
1323
|
+
// 懒加载:只建前 listLimit 行,剩下的用一个「加载更多」收口。
|
|
1324
|
+
// 行数按**真正建出来的任务行**算(分组头不算),这样「还有 N 条」与用户数得出来的数字一致。
|
|
1325
|
+
let built = 0;
|
|
1326
|
+
const total = list.length;
|
|
1237
1327
|
for (const g of groups) {
|
|
1328
|
+
if (built >= state.listLimit) break;
|
|
1238
1329
|
const box = document.createElement('div');
|
|
1239
1330
|
box.className = 'task-group';
|
|
1240
|
-
const isDraftHere = state.draft && g.key === draftKey;
|
|
1331
|
+
const isDraftHere = state.draft && !q && g.key === draftKey;
|
|
1241
1332
|
const label = g.dir && sameName.get(dirLabel(g.dir)) > 1 ? shortTail(g.dir) : dirLabel(g.dir);
|
|
1242
1333
|
|
|
1243
|
-
|
|
1334
|
+
const rows = flattenTree(g.tasks);
|
|
1335
|
+
const budget = Math.max(0, state.listLimit - built);
|
|
1336
|
+
const shown = rows.slice(0, budget);
|
|
1337
|
+
const hidden = rows.length - shown.length;
|
|
1338
|
+
// 分组头上的数字始终是「这个目录下真实有多少条」,不因为分批渲染而缩水
|
|
1339
|
+
box.appendChild(buildGroupHead(g, label, rows.length + (isDraftHere ? 1 : 0)));
|
|
1244
1340
|
|
|
1245
1341
|
const body = document.createElement('div');
|
|
1246
1342
|
body.className = 'task-group-body';
|
|
1247
1343
|
body.hidden = isDirCollapsed(g.dir);
|
|
1248
1344
|
|
|
1249
|
-
// 组内再按派生关系排成树(没有父子关系时就是原来的平铺)
|
|
1250
|
-
const rows = flattenTree(g.tasks);
|
|
1251
1345
|
// 草稿行:派生出来的草稿挂在它的父任务下面,顶层草稿仍放在组内最前面
|
|
1252
1346
|
const draftParent = isDraftHere && state.draftParentId ? state.draftParentId : null;
|
|
1253
|
-
const parentRendered = draftParent &&
|
|
1347
|
+
const parentRendered = draftParent && shown.some((r) => r.task.id === draftParent);
|
|
1254
1348
|
if (isDraftHere && !parentRendered) body.appendChild(buildDraftRow());
|
|
1255
|
-
for (const r of
|
|
1349
|
+
for (const r of shown) {
|
|
1256
1350
|
body.appendChild(buildTaskRow(r.task, { depth: r.depth, childCount: r.childCount }));
|
|
1257
1351
|
if (isDraftHere && draftParent === r.task.id) body.appendChild(buildDraftRow(r.depth + 1));
|
|
1258
1352
|
}
|
|
1353
|
+
if (hidden) {
|
|
1354
|
+
// 这一组被截断了:给一行轻提示,避免用户以为这个目录下就只有这么多
|
|
1355
|
+
const more = document.createElement('div');
|
|
1356
|
+
more.className = 'task-group-more';
|
|
1357
|
+
more.textContent = `本组还有 ${hidden} 条`;
|
|
1358
|
+
body.appendChild(more);
|
|
1359
|
+
}
|
|
1259
1360
|
|
|
1260
1361
|
box.appendChild(body);
|
|
1261
1362
|
dom.taskList.appendChild(box);
|
|
1363
|
+
built += shown.length;
|
|
1262
1364
|
}
|
|
1365
|
+
|
|
1366
|
+
if (built < total) dom.taskList.appendChild(buildMoreRow(total - built));
|
|
1263
1367
|
updateRetentionInfo();
|
|
1264
1368
|
}
|
|
1265
1369
|
|
|
@@ -1504,9 +1608,21 @@ async function startDraft(dir, parentId = null) {
|
|
|
1504
1608
|
}
|
|
1505
1609
|
}
|
|
1506
1610
|
|
|
1611
|
+
/** 侧边栏当前是不是开着:桌面看 .collapsed,窄屏看 .open(口径只写这一处) */
|
|
1612
|
+
function sidebarOpen() {
|
|
1613
|
+
const mobile = window.matchMedia('(max-width: 820px)').matches;
|
|
1614
|
+
return mobile ? dom.sidebar.classList.contains('open') : !dom.sidebar.classList.contains('collapsed');
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/** 被 Tab 外壳嵌着(/)时,外壳顶栏上的 ☰ 要有按下态;单独打开本页时这行是空操作 */
|
|
1618
|
+
function notifySidebar() {
|
|
1619
|
+
if (window.lgwBridge) window.lgwBridge.sidebar(sidebarOpen());
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1507
1622
|
function toggleSidebar() {
|
|
1508
1623
|
if (window.matchMedia('(max-width: 820px)').matches) dom.sidebar.classList.toggle('open');
|
|
1509
1624
|
else dom.sidebar.classList.toggle('collapsed');
|
|
1625
|
+
notifySidebar();
|
|
1510
1626
|
}
|
|
1511
1627
|
|
|
1512
1628
|
/* ---------- 渲染消息 ---------- */
|
|
@@ -1809,12 +1925,76 @@ function appendMessage(msg) {
|
|
|
1809
1925
|
scrollToBottom(true);
|
|
1810
1926
|
}
|
|
1811
1927
|
|
|
1928
|
+
/* ---------- 消息区:只渲染看得见的那一段(懒加载) ----------
|
|
1929
|
+
* 工具调用的输出单条就能到两万字,一条跑久了的任务几百条消息是常事:一次性全建成 DOM
|
|
1930
|
+
* 会明显卡(切任务、开面板都卡)。所以默认只渲染最近 MSG_PAGE 条,往上翻时点「载入更早」**前插** ——
|
|
1931
|
+
* 前插而不是整表重画,是为了不丢掉斜杠指令的输出(那些只活在 DOM 里),也不会让视口跳走。
|
|
1932
|
+
* 页面上的「任务对白」仍然全在 state.messages 里:落盘、发给模型、审批都不受渲染窗口影响。
|
|
1933
|
+
*/
|
|
1934
|
+
|
|
1935
|
+
/** 渲染窗口的起点:null 按 MSG_PAGE 从末尾算;越界一律夹回合法区间 */
|
|
1936
|
+
function renderFromIndex() {
|
|
1937
|
+
const total = state.messages.length;
|
|
1938
|
+
const maxFrom = Math.max(0, total - MSG_PAGE);
|
|
1939
|
+
if (state.renderFrom == null || !Number.isFinite(state.renderFrom)) state.renderFrom = maxFrom;
|
|
1940
|
+
state.renderFrom = Math.max(0, Math.min(state.renderFrom, maxFrom));
|
|
1941
|
+
return state.renderFrom;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function moreBar() {
|
|
1945
|
+
return dom.messages.querySelector('.msg-more-bar');
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/** 顶部那条「载入更早」:还有没渲染的消息时出现,全渲染完就撤掉 */
|
|
1949
|
+
function updateMoreBar() {
|
|
1950
|
+
const from = renderFromIndex();
|
|
1951
|
+
let bar = moreBar();
|
|
1952
|
+
if (from <= 0) {
|
|
1953
|
+
if (bar) bar.remove();
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
if (!bar) {
|
|
1957
|
+
bar = document.createElement('div');
|
|
1958
|
+
bar.className = 'msg-more-bar';
|
|
1959
|
+
const btn = document.createElement('button');
|
|
1960
|
+
btn.type = 'button';
|
|
1961
|
+
btn.className = 'btn ghost tiny msg-more-btn';
|
|
1962
|
+
btn.textContent = '载入更早的消息';
|
|
1963
|
+
btn.addEventListener('click', loadEarlier);
|
|
1964
|
+
bar.appendChild(btn);
|
|
1965
|
+
dom.messages.insertBefore(bar, dom.messages.firstChild);
|
|
1966
|
+
}
|
|
1967
|
+
const btn = bar.querySelector('.msg-more-btn');
|
|
1968
|
+
if (btn) btn.textContent = `↑ 还有 ${from} 条更早的消息 · 载入 ${Math.min(MSG_PAGE, from)} 条`;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/** 往上补一屏:前插 + 把视口钉在原处(不然正在读的那一段会被顶下去) */
|
|
1972
|
+
function loadEarlier() {
|
|
1973
|
+
const from = renderFromIndex();
|
|
1974
|
+
if (from <= 0) return;
|
|
1975
|
+
const next = Math.max(0, from - MSG_PAGE);
|
|
1976
|
+
const before = dom.messages.scrollHeight;
|
|
1977
|
+
const first = dom.messages.querySelector('.msg');
|
|
1978
|
+
for (let i = next; i < from; i++) {
|
|
1979
|
+
const el = buildMsgEl(state.messages[i]);
|
|
1980
|
+
if (first) dom.messages.insertBefore(el, first);
|
|
1981
|
+
else dom.messages.appendChild(el);
|
|
1982
|
+
}
|
|
1983
|
+
state.renderFrom = next;
|
|
1984
|
+
updateMoreBar();
|
|
1985
|
+
dom.messages.scrollTop += dom.messages.scrollHeight - before;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1812
1988
|
function renderAll() {
|
|
1813
1989
|
for (const node of dom.messages.querySelectorAll('.msg')) node.remove();
|
|
1990
|
+
const bar = moreBar();
|
|
1991
|
+
if (bar) bar.remove();
|
|
1814
1992
|
for (const msg of state.messages) msg.el = null;
|
|
1815
1993
|
const hasMessages = state.messages.length > 0;
|
|
1816
1994
|
dom.empty.hidden = hasMessages;
|
|
1817
|
-
|
|
1995
|
+
const from = renderFromIndex();
|
|
1996
|
+
for (let i = from; i < state.messages.length; i++) dom.messages.appendChild(buildMsgEl(state.messages[i]));
|
|
1997
|
+
updateMoreBar();
|
|
1818
1998
|
if (hasMessages) scrollToBottom(true);
|
|
1819
1999
|
}
|
|
1820
2000
|
|
|
@@ -1822,6 +2002,8 @@ function setBusy(busy) {
|
|
|
1822
2002
|
state.streaming = busy;
|
|
1823
2003
|
dom.send.disabled = busy || !state.workDir;
|
|
1824
2004
|
dom.stop.hidden = !busy;
|
|
2005
|
+
// 切到别的 Tab 也能看出这条任务还在跑(外壳 Tab 上的脉冲点)
|
|
2006
|
+
if (window.lgwBridge) window.lgwBridge.busy(busy);
|
|
1825
2007
|
}
|
|
1826
2008
|
|
|
1827
2009
|
/* ---------- 斜杠指令(E 组:计划与审批 / 模型与请求参数 / 会话与用量 / 工作目录 / 手册) ----------
|
|
@@ -2055,6 +2237,10 @@ function setWorkDir(dir, persist = true) {
|
|
|
2055
2237
|
renderTasks();
|
|
2056
2238
|
}
|
|
2057
2239
|
dom.workdirPath.textContent = dir || '未选择';
|
|
2240
|
+
// 被 Tab 外壳嵌着时,工作目录那根路径条在外壳顶栏上(本页顶栏是收起的),同步文案过去
|
|
2241
|
+
if (window.lgwBridge) {
|
|
2242
|
+
window.lgwBridge.workdir(dir || '未选择', dir ? `点击更换工作目录:${dir}` : '点击选择工作目录');
|
|
2243
|
+
}
|
|
2058
2244
|
dom.workdirChip.title = dir ? `点击更换工作目录:${dir}` : '点击选择工作目录';
|
|
2059
2245
|
const ready = Boolean(dir);
|
|
2060
2246
|
dom.input.disabled = !ready;
|
|
@@ -3030,6 +3216,24 @@ async function init() {
|
|
|
3030
3216
|
// 不传参数:沿用当前工作目录(别把 click 事件传进去)
|
|
3031
3217
|
dom.newTask.addEventListener('click', () => startDraft());
|
|
3032
3218
|
dom.btnToggleSidebar.addEventListener('click', toggleSidebar);
|
|
3219
|
+
|
|
3220
|
+
// 侧边栏搜索:任务多了按标题 / 目录找。Escape 清空(与输入框里的直觉一致)
|
|
3221
|
+
if (dom.taskSearch) {
|
|
3222
|
+
dom.taskSearch.addEventListener('input', () => setTaskQuery(dom.taskSearch.value));
|
|
3223
|
+
dom.taskSearch.addEventListener('keydown', (e) => {
|
|
3224
|
+
if (e.key !== 'Escape') return;
|
|
3225
|
+
e.preventDefault();
|
|
3226
|
+
setTaskQuery('');
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
if (dom.btnTaskSearchClear) dom.btnTaskSearchClear.addEventListener('click', () => setTaskQuery(''));
|
|
3230
|
+
|
|
3231
|
+
// 被 Tab 外壳嵌着时,把状态点 / 工作目录 / 侧栏开合的初值报上去,
|
|
3232
|
+
// 外壳顶栏不必等到下一次变化才有内容(单独打开本页时 window.lgwBridge 不存在,整块跳过)
|
|
3233
|
+
if (window.lgwBridge) {
|
|
3234
|
+
window.lgwBridge.workdir(state.workDir || '未选择', state.workDir ? `点击更换工作目录:${state.workDir}` : '点击选择工作目录');
|
|
3235
|
+
notifySidebar();
|
|
3236
|
+
}
|
|
3033
3237
|
dom.btnPrune.addEventListener('click', async () => {
|
|
3034
3238
|
try {
|
|
3035
3239
|
const data = await api('/api/tasks/prune', { method: 'POST' });
|
package/public/theme.js
CHANGED
|
@@ -82,10 +82,30 @@
|
|
|
82
82
|
syncButtons();
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* 别的文档改了主题就跟上。
|
|
87
|
+
*
|
|
88
|
+
* 场景:`/` 是 Tab 外壳,三个面板是它的 iframe(见 public/shell.html / shell.js)。
|
|
89
|
+
* 外壳上点「白天/黑夜」时,面板自己不会知道 —— localStorage 同源共享,而 `storage` 事件
|
|
90
|
+
* 恰好只在「其他文档」里触发(自己改自己不会绕回来),所以这里正好是缺的那一环:
|
|
91
|
+
* 面板、另一个标签页里的同一页面,都能立刻跟着变。
|
|
92
|
+
* 传了底色的页面(?bg=)配色由宿主决定,一律不跟。
|
|
93
|
+
*/
|
|
94
|
+
function watchStorage() {
|
|
95
|
+
if (typeof window.addEventListener !== 'function') return; // node 垫片里没有这个方法
|
|
96
|
+
window.addEventListener('storage', (e) => {
|
|
97
|
+
if (e && e.key && e.key !== KEY) return; // null = 被 clear() 清空,也当主题变化处理
|
|
98
|
+
if (forced()) return;
|
|
99
|
+
apply(effective());
|
|
100
|
+
syncButtons();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
// 页面加载后立刻同步一次,避免内联脚本与这里不一致
|
|
86
105
|
apply(effective());
|
|
87
106
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
|
|
88
107
|
else wire();
|
|
108
|
+
watchStorage();
|
|
89
109
|
|
|
90
110
|
window.lgwTheme = { get: effective, set: setTheme, stored };
|
|
91
111
|
})();
|
package/public/tint.js
CHANGED
|
@@ -219,9 +219,10 @@
|
|
|
219
219
|
root.style.colorScheme = tint.theme;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
/**
|
|
222
|
+
/** 页内跳到外壳/聊天/任务/手册时把底色带上,否则新标签页里会退回默认配色。
|
|
223
|
+
* `/` 自 20260922 起是 Tab 外壳(shell.html),三个面板都有各自的路径,所以这里逐个列全 */
|
|
223
224
|
function patchLinks(hex) {
|
|
224
|
-
const SELF = ['/', '/index.html', '/task', '/task.html'];
|
|
225
|
+
const SELF = ['/', '/index.html', '/shell', '/shell.html', '/chat', '/chat.html', '/task', '/task.html', '/manual', '/manual.html'];
|
|
225
226
|
for (const a of document.querySelectorAll('a[href]')) {
|
|
226
227
|
let url;
|
|
227
228
|
try {
|
|
@@ -230,7 +231,8 @@
|
|
|
230
231
|
continue;
|
|
231
232
|
}
|
|
232
233
|
const sameOrigin = url.origin === window.location.origin;
|
|
233
|
-
|
|
234
|
+
// 任务页顶栏那个「聊天 ↗」是显式的跨页入口:即使被指到别的主机也要带上底色
|
|
235
|
+
const isEntry = a.id === 'btn-open-chat';
|
|
234
236
|
if (!isEntry && !(sameOrigin && SELF.includes(url.pathname))) continue;
|
|
235
237
|
url.searchParams.set('bg', hex);
|
|
236
238
|
a.setAttribute('href', sameOrigin ? `${url.pathname}${url.search}${url.hash}` : url.toString());
|