dsh-console-sentinel 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -163,7 +163,13 @@ window.__ModuleLoader__.load({
163
163
  busy: false,
164
164
  message: "",
165
165
  tone: "muted",
166
- desktop: null
166
+ desktop: null,
167
+ taskbar: null,
168
+ taskbarLoading: false,
169
+ taskbarQueued: false,
170
+ taskbarView: "icon", // "icon" (compact icon grid, DEFAULT) | "detail" (grouped cards)
171
+ autoTimer: null,
172
+ visHooked: false
167
173
  };
168
174
 
169
175
  function el(id) {
@@ -240,6 +246,378 @@ window.__ModuleLoader__.load({
240
246
  renderWindows();
241
247
  renderWhitelist();
242
248
  renderDesktopResult();
249
+ renderTaskbar();
250
+ }
251
+
252
+ // One refresh for everything: status/counts (via /status) AND the taskbar
253
+ // window list (via /taskbar-windows). Replaces the old separate "刷新" +
254
+ // "刷新任务栏" pair that confused users.
255
+ function refreshAll() {
256
+ setStatus("正在刷新…", "muted");
257
+ loadStatus();
258
+ loadTaskbar({ silent: true });
259
+ }
260
+
261
+ // ---- compact tab switching (任务栏 / 控制台 / 白名单) --------------------
262
+ var TAB = { active: "taskbar" };
263
+ function setActiveTab(name) {
264
+ TAB.active = name;
265
+ var panels = { taskbar: "dcs-panel-taskbar", console: "dcs-panel-console", whitelist: "dcs-panel-whitelist" };
266
+ for (var k in panels) {
267
+ if (!panels.hasOwnProperty(k)) continue;
268
+ var p = el(panels[k]);
269
+ if (p) p.style.display = (k === name) ? "block" : "none";
270
+ }
271
+ var tabs = { taskbar: "dcs-tab-taskbar", console: "dcs-tab-console", whitelist: "dcs-tab-whitelist" };
272
+ for (var j in tabs) {
273
+ if (!tabs.hasOwnProperty(j)) continue;
274
+ var b = el(tabs[j]);
275
+ if (b) {
276
+ var on = (j === name);
277
+ b.style.color = on ? TOKEN.accent : TOKEN.textDim;
278
+ b.style.borderBottom = "2px solid " + (on ? TOKEN.accent : "transparent");
279
+ b.style.fontWeight = on ? 600 : 500;
280
+ }
281
+ }
282
+ }
283
+
284
+ // ---- taskbar window whitelist (icon + checkbox list) --------------------
285
+
286
+ // Queue-safe loader: if a load is in flight, remember that another is needed
287
+ // and run it as soon as the current one finishes (a plain early `return`
288
+ // used to silently DROP the refresh after closing windows → list looked stale).
289
+ function loadTaskbar(opts) {
290
+ opts = opts || {};
291
+ if (cache.taskbarLoading) { cache.taskbarQueued = true; return; }
292
+ cache.taskbarLoading = true;
293
+ if (!opts.silent) {
294
+ var box0 = el("dcs-taskbar");
295
+ if (box0 && !(cache.taskbar && cache.taskbar.windows && cache.taskbar.windows.length)) {
296
+ clear(box0); box0.appendChild(mk("div", S.muted, "正在读取任务栏窗口…"));
297
+ }
298
+ }
299
+ api("/taskbar-windows").then(function (body) {
300
+ cache.taskbar = body && body.ok ? body : { ok: false, reason: (body && body.reason) || "读取失败", windows: [] };
301
+ }).catch(function (error) {
302
+ cache.taskbar = { ok: false, reason: (error && error.message ? error.message : String(error)), windows: [] };
303
+ }).then(function () {
304
+ cache.taskbarLoading = false;
305
+ renderTaskbar();
306
+ if (cache.taskbarQueued) { cache.taskbarQueued = false; loadTaskbar({ silent: true }); }
307
+ });
308
+ }
309
+
310
+ // Optimistically drop windows from the cached taskbar list so the UI updates
311
+ // the instant a close action returns, before the real refresh reconciles.
312
+ function pruneTaskbar(pred) {
313
+ if (!cache.taskbar || !cache.taskbar.windows) return;
314
+ var before = cache.taskbar.windows.length;
315
+ cache.taskbar.windows = cache.taskbar.windows.filter(function (w) { return !pred(w); });
316
+ if (cache.taskbar.windows.length !== before) renderTaskbar();
317
+ }
318
+
319
+ // One call after any mutating action: optimistic prune + refresh counters +
320
+ // refresh the taskbar list (queue-safe). Fixes "关了却不更新".
321
+ function afterMutation(opt) {
322
+ opt = opt || {};
323
+ var rmHwnds = (opt.removeHwnds || []).map(Number);
324
+ var rmProc = String(opt.removeProcess || "").toLowerCase();
325
+ if (rmHwnds.length || rmProc) {
326
+ pruneTaskbar(function (w) {
327
+ if (rmHwnds.length && rmHwnds.indexOf(Number(w.hwnd)) >= 0) return true;
328
+ if (rmProc && String(w.process || "").toLowerCase().replace(/\.exe$/, "") === rmProc.replace(/\.exe$/, "")) return true;
329
+ return false;
330
+ });
331
+ }
332
+ loadStatus(true);
333
+ loadTaskbar({ silent: true });
334
+ }
335
+
336
+ function iconImg(icon, label) {
337
+ if (icon && typeof icon === "string" && icon.indexOf("data:image") === 0) {
338
+ var img = document.createElement("img");
339
+ img.src = icon;
340
+ img.alt = label || "";
341
+ img.style.width = "18px";
342
+ img.style.height = "18px";
343
+ img.style.flex = "0 0 auto";
344
+ img.style.objectFit = "contain";
345
+ img.style.borderRadius = "3px";
346
+ return img;
347
+ }
348
+ // Fallback: a small neutral placeholder block keeps rows aligned.
349
+ var ph = mk("span", {
350
+ display: "inline-block", width: "18px", height: "18px", flex: "0 0 auto",
351
+ borderRadius: "3px", background: TOKEN.bgCardSoft,
352
+ border: "1px solid " + TOKEN.borderSoft
353
+ });
354
+ return ph;
355
+ }
356
+
357
+ function _badge(text, color, dashed) {
358
+ return mk("span", {
359
+ fontSize: "10.5px", fontWeight: "600", color: color,
360
+ border: (dashed ? "1px dashed " : "1px solid ") + color,
361
+ borderRadius: "6px", padding: "0 5px", whiteSpace: "nowrap", flex: "0 0 auto"
362
+ }, text);
363
+ }
364
+
365
+ // System-level protection: host + compute jobs. These can NEVER be closed
366
+ // from the panel (closing them kills the harness or an in-progress Fluent run).
367
+ function isSystemLocked(proc, hostLocked) {
368
+ if (hostLocked) return true;
369
+ return /^dsh|electron|deepseek|mpi|fluent|ansys|^cx\d|solver|cae|launcher/i.test(String(proc || ""));
370
+ }
371
+
372
+ function toggleTaskbarView() {
373
+ cache.taskbarView = cache.taskbarView === "icon" ? "detail" : "icon";
374
+ renderTaskbar();
375
+ }
376
+
377
+ function renderTaskbar() {
378
+ var box = el("dcs-taskbar");
379
+ if (!box) return;
380
+ clear(box);
381
+ var vb = el("dcs-taskbar-view");
382
+ if (vb) vb.textContent = cache.taskbarView === "icon" ? "详细视图" : "仅图标视图"; var data = cache.taskbar;
383
+ if (!data) { box.appendChild(mk("div", S.muted, "读取中…")); return; }
384
+ if (!data.ok) {
385
+ box.appendChild(mk("div", { fontSize: "12px", color: TOKEN.danger, padding: "4px 0" },
386
+ "读取任务栏失败:" + (data.reason || "未知")));
387
+ return;
388
+ }
389
+ var list = data.windows || [];
390
+ if (!list.length) { box.appendChild(mk("div", S.muted, "任务栏上没有可列出的窗口。")); return; }
391
+
392
+ // ---- Icon-only mode: a compact grid, one icon per window, row by row ----
393
+ if (cache.taskbarView === "icon") {
394
+ var grid = mk("div", { display: "flex", flexWrap: "wrap", gap: "6px", alignContent: "flex-start" });
395
+ for (var k = 0; k < list.length; k++) {
396
+ (function (w) {
397
+ var sysLocked = isSystemLocked(w.process, w.hostLocked);
398
+ var closable = !w.whitelisted && !sysLocked;
399
+ var cell = mk("div", {
400
+ display: "flex", alignItems: "center", justifyContent: "center",
401
+ width: "30px", height: "30px", borderRadius: "8px",
402
+ border: "1px solid " + (closable ? TOKEN.borderSoft : TOKEN.borderSoft),
403
+ background: TOKEN.bgCardSoft,
404
+ opacity: closable ? "1" : "0.45",
405
+ cursor: closable ? "pointer" : "default", flex: "0 0 auto"
406
+ });
407
+ var ic = iconImg(w.icon, w.process);
408
+ ic.style.width = "20px"; ic.style.height = "20px";
409
+ cell.appendChild(ic);
410
+ cell.title = (w.process || "?") + (w.title ? (" — " + w.title) : "") +
411
+ (closable ? "(点击关闭此窗口)" : (w.whitelisted ? "(白名单,不可关闭)" : "(系统保护,不可关闭)"));
412
+ if (closable) {
413
+ cell.addEventListener("click", function (ev) {
414
+ try { ev.preventDefault(); } catch (e) {}
415
+ doWindow(w.hwnd, "close", Boolean(w.elevated), w.process);
416
+ });
417
+ }
418
+ grid.appendChild(cell);
419
+ })(list[k]);
420
+ }
421
+ box.appendChild(grid);
422
+ return;
423
+ }
424
+
425
+ // Group by software (process). Same process → same pid / protection / elevation.
426
+ var order = [];
427
+ var groups = {};
428
+ for (var i = 0; i < list.length; i++) {
429
+ var w = list[i];
430
+ var key = String(w.process || "?").toLowerCase().replace(/\.exe$/, "");
431
+ if (!groups[key]) { groups[key] = { key: key, meta: w, windows: [] }; order.push(key); }
432
+ groups[key].windows.push(w);
433
+ }
434
+
435
+ for (var gi = 0; gi < order.length; gi++) {
436
+ (function () {
437
+ var g = groups[order[gi]];
438
+ var m = g.meta;
439
+ var proc = m.process || "?";
440
+ var pid = m.pid;
441
+ var wl = Boolean(m.whitelisted);
442
+ var sysLocked = isSystemLocked(proc, Boolean(m.hostLocked));
443
+ var elevated = Boolean(m.elevated);
444
+ var closable = !wl && !sysLocked; // only these can be closed
445
+ var n = g.windows.length;
446
+
447
+ var card = mk("div", {
448
+ border: "1px solid " + TOKEN.borderSoft, borderRadius: "8px",
449
+ background: TOKEN.bgCardSoft, padding: "7px 9px", marginBottom: "7px"
450
+ });
451
+
452
+ // ---- group header ----
453
+ var head = mk("div", { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" });
454
+
455
+ var cb = document.createElement("input");
456
+ cb.type = "checkbox";
457
+ cb.checked = wl;
458
+ cb.disabled = sysLocked;
459
+ cb.title = sysLocked ? "系统保护程序,不可加入白名单" : "勾选=加入白名单(该软件永不被关闭)";
460
+ cb.style.flex = "0 0 auto";
461
+ cb.style.cursor = sysLocked ? "default" : "pointer";
462
+ (function (el2, procName, locked) {
463
+ el2.addEventListener("change", function () {
464
+ if (locked) return;
465
+ setTaskbarWhitelist(procName, "", el2.checked);
466
+ });
467
+ })(cb, proc, sysLocked);
468
+ head.appendChild(cb);
469
+ head.appendChild(iconImg(m.icon, proc));
470
+
471
+ var nameWrap = mk("div", { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap", minWidth: "0" });
472
+ nameWrap.appendChild(mk("span", { fontSize: "13px", fontWeight: 600, color: TOKEN.text },
473
+ proc + (pid ? " (pid " + pid + ")" : "")));
474
+ if (n > 1) nameWrap.appendChild(mk("span", Object.assign({}, S.muted, { fontSize: "11px" }), "· " + n + " 个窗口"));
475
+ if (wl) nameWrap.appendChild(_badge("白名单", TOKEN.success, false));
476
+ if (sysLocked) nameWrap.appendChild(_badge("系统保护", TOKEN.danger, false));
477
+ if (elevated && !sysLocked) nameWrap.appendChild(_badge("管理员", TOKEN.textDim, true));
478
+ head.appendChild(nameWrap);
479
+
480
+ var acts = mk("div", { display: "flex", gap: "6px", alignItems: "center", marginLeft: "auto", flex: "0 0 auto" });
481
+ if (closable) {
482
+ (function (procName, cnt) {
483
+ acts.appendChild(button("关掉该软件" + (cnt > 1 ? ("(" + cnt + ")") : ""), S.btn, function () { closeSoftware(procName, cnt); }));
484
+ })(proc, n);
485
+ }
486
+ head.appendChild(acts);
487
+ card.appendChild(head);
488
+
489
+ // ---- per-window rows, each with its own 关闭 button ----
490
+ var winsBox = mk("div", { marginTop: "5px", paddingLeft: "22px", display: "flex", flexDirection: "column", gap: "2px" });
491
+ for (var wi = 0; wi < g.windows.length; wi++) {
492
+ (function (winRow) {
493
+ var line = mk("div", { display: "flex", alignItems: "center", gap: "6px" });
494
+ line.appendChild(mk("span", { color: TOKEN.textDim, flex: "0 0 auto", fontSize: "12px" }, "•"));
495
+ line.appendChild(mk("span", {
496
+ fontSize: "11.5px", color: TOKEN.textDim, minWidth: "0", flex: "1 1 auto",
497
+ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"
498
+ }, winRow.title ? String(winRow.title) : "(无标题)"));
499
+ // 关闭此窗口: disabled (ineffective) when whitelisted or system-protected.
500
+ var closeBtn = mk("button", {
501
+ appearance: "none", border: "1px solid " + (closable ? TOKEN.border : TOKEN.borderSoft),
502
+ background: "transparent", color: closable ? TOKEN.text : TOKEN.textDim,
503
+ borderRadius: "6px", padding: "1px 8px", fontSize: "11px",
504
+ flex: "0 0 auto", cursor: closable ? "pointer" : "not-allowed",
505
+ opacity: closable ? "1" : "0.45"
506
+ }, "关闭");
507
+ closeBtn.type = "button";
508
+ if (closable) {
509
+ closeBtn.title = elevated ? "关闭此窗口(管理员窗口会弹一次 UAC)" : "关闭此窗口(优雅关闭,等同 Alt+F4)";
510
+ closeBtn.addEventListener("click", function (ev) {
511
+ try { ev.preventDefault(); } catch (e) {}
512
+ doWindow(winRow.hwnd, "close", elevated, proc);
513
+ });
514
+ } else {
515
+ closeBtn.title = wl ? "白名单程序,关闭无效" : "系统保护程序,关闭无效";
516
+ }
517
+ line.appendChild(closeBtn);
518
+ winsBox.appendChild(line);
519
+ })(g.windows[wi]);
520
+ }
521
+ card.appendChild(winsBox);
522
+ box.appendChild(card);
523
+ })();
524
+ }
525
+ }
526
+
527
+ // 取消 / 恢复 内置保护(写入持久 whitelist.unprotect 列表)
528
+ function setTaskbarProtection(proc, lift) {
529
+ var name = String(proc || "").trim();
530
+ if (!name) return;
531
+ var msg = lift ? ("确认取消 " + name + " 的保护?取消后它会被「一键关闭其他窗口」关闭。") : ("恢复 " + name + " 的保护?");
532
+ if (lift) { var yes = true; try { yes = window.confirm(msg); } catch (e) {} if (!yes) return; }
533
+ cache.busy = true;
534
+ setStatus(lift ? ("正在取消 " + name + " 的保护…") : ("正在恢复 " + name + " 的保护…"), "muted");
535
+ api("/whitelist", { method: "POST", body: JSON.stringify({ action: lift ? "add" : "remove", kind: "unprotect", items: [name] }) })
536
+ .then(function (body) {
537
+ cache.busy = false;
538
+ if (cache.status) cache.status.whitelist = { processes: body.processes || [], titles: body.titles || [], unprotect: body.unprotect || [] };
539
+ setStatus(lift ? (name + " 的保护已取消") : (name + " 的保护已恢复"), "ok");
540
+ loadTaskbar();
541
+ })
542
+ .catch(function (error) {
543
+ cache.busy = false;
544
+ setStatus("操作失败:" + (error && error.message ? error.message : error), "error");
545
+ });
546
+ }
547
+ function unprotectProcess(proc) { setTaskbarProtection(proc, true); }
548
+ function reprotectProcess(proc) { setTaskbarProtection(proc, false); }
549
+
550
+ // 强制关闭(管理员): elevated taskkill. One UAC prompt; answering No changes
551
+ // nothing. Used for windows whose owner runs elevated — an unelevated
552
+ // WM_CLOSE is silently dropped by Windows UIPI, so the normal 关闭 never works.
553
+ function forceCloseElevated(pid, procName) {
554
+ var name = String(procName || "").trim();
555
+ if (!pid) return;
556
+ var yes = true;
557
+ try {
558
+ yes = window.confirm(
559
+ "将以管理员权限强制结束 " + (name || ("PID " + pid)) + "(PID " + pid + ")。\n\n" +
560
+ "接下来会弹出 Windows UAC 授权框,点「是」才会执行。\n" +
561
+ "强制结束会丢弃该程序未保存的内容。确定继续?");
562
+ } catch (e) { yes = true; }
563
+ if (!yes) return;
564
+ cache.busy = true;
565
+ setStatus("正在请求管理员权限…(请在弹出的 UAC 窗口点「是」)", "muted");
566
+ api("/force-close", {
567
+ method: "POST",
568
+ body: JSON.stringify({ pids: [pid], allowedProcesses: name ? [name] : [] })
569
+ })
570
+ .then(function (body) {
571
+ cache.busy = false;
572
+ if (body && body.ok) {
573
+ setStatus("已强制结束 " + (name || ("PID " + pid)) + "(" + (body.killedCount || 0) + " 个)", "ok");
574
+ } else {
575
+ var why = (body && body.reason) || "未知";
576
+ if (why === "uac-denied-or-cancelled" || why.indexOf("elevated-helper-failed") === 0 || why === "no-marker-output") {
577
+ setStatus("未执行:UAC 被取消或被系统策略阻止(" + why + ")", "error");
578
+ } else if (why === "all-rejected") {
579
+ setStatus("未执行:目标进程被安全规则拒绝", "error");
580
+ } else {
581
+ setStatus("强制关闭失败:" + why, "error");
582
+ }
583
+ }
584
+ loadTaskbar();
585
+ })
586
+ .catch(function (error) {
587
+ cache.busy = false;
588
+ setStatus("强制关闭失败:" + (error && error.message ? error.message : error), "error");
589
+ loadTaskbar();
590
+ });
591
+ }
592
+
593
+ function setTaskbarWhitelist(proc, title, checked) {
594
+ var name = String(proc || "").trim();
595
+ if (!name) return;
596
+ cache.busy = true;
597
+ setStatus(checked ? ("正在把 " + name + " 加入白名单…") : ("正在把 " + name + " 移出白名单…"), "muted");
598
+ api("/whitelist", { method: "POST", body: JSON.stringify({ action: checked ? "add" : "remove", kind: "process", items: [name] }) })
599
+ .then(function (body) {
600
+ cache.busy = false;
601
+ if (cache.status) cache.status.whitelist = { processes: body.processes || [], titles: body.titles || [] };
602
+ setStatus(checked ? (name + " 已加入白名单") : (name + " 已移出白名单"), "ok");
603
+ // Reflect new state on every row for this process without a full reload.
604
+ if (cache.taskbar && cache.taskbar.windows) {
605
+ for (var i = 0; i < cache.taskbar.windows.length; i++) {
606
+ var row = cache.taskbar.windows[i];
607
+ if (String(row.process || "").toLowerCase() === name.toLowerCase()) {
608
+ row.whitelisted = checked;
609
+ row.whitelistedBy = checked ? "process" : "";
610
+ }
611
+ }
612
+ }
613
+ renderTaskbar();
614
+ renderWhitelist();
615
+ })
616
+ .catch(function (error) {
617
+ cache.busy = false;
618
+ setStatus("白名单更新失败:" + (error && error.message ? error.message : error), "error");
619
+ renderTaskbar();
620
+ });
243
621
  }
244
622
 
245
623
  function renderHeader() {
@@ -251,8 +629,8 @@ window.__ModuleLoader__.load({
251
629
  pill.textContent = win ? "Windows · 运行中" : ("非 Windows(" + st.platform + ")· 不生效");
252
630
  pill.style.color = win ? TOKEN.success : TOKEN.danger;
253
631
  var cfg = st.config || {};
254
- setText("dcs-cfg", "扫描间隔 " + cfg.scanIntervalSec + "s · 空闲阈值 " + cfg.idleThresholdSec +
255
- "s · 自动动作 " + (cfg.autoAction || "none") + " · 空壳动作 " + (cfg.emptyConsoleAction || "hide"));
632
+ setText("dcs-cfg", "扫描 " + cfg.scanIntervalSec + "s · 空闲 " + cfg.idleThresholdSec +
633
+ "s · 空壳 " + (cfg.emptyConsoleAction || "hide"));
256
634
  }
257
635
 
258
636
  function renderCounts() {
@@ -262,22 +640,25 @@ window.__ModuleLoader__.load({
262
640
  clear(box);
263
641
  var counts = st.counts || {};
264
642
  var items = [
265
- ["控制台窗口", counts.total || 0],
266
- ["空壳 PowerShell", counts.empty || 0],
643
+ ["控制台", counts.total || 0],
644
+ ["空壳", counts.empty || 0],
267
645
  ["可见空壳", counts.visibleEmpty || 0],
268
646
  ["已隐藏", counts.hidden || 0]
269
647
  ];
270
648
  for (var i = 0; i < items.length; i++) {
649
+ // Compact inline pill (auto width, does NOT stretch across the row).
271
650
  var cell = mk("div", {
272
- flex: "1 1 0",
273
- minWidth: "96px",
651
+ flex: "0 0 auto",
652
+ display: "inline-flex",
653
+ alignItems: "baseline",
654
+ gap: "4px",
274
655
  background: TOKEN.bgCardSoft,
275
656
  border: "1px solid " + TOKEN.borderSoft,
276
- borderRadius: "8px",
277
- padding: "8px 10px"
657
+ borderRadius: "999px",
658
+ padding: "2px 9px"
278
659
  });
279
- cell.appendChild(mk("div", { fontSize: "20px", fontWeight: "600", color: TOKEN.text }, items[i][1]));
280
- cell.appendChild(mk("div", S.muted, items[i][0]));
660
+ cell.appendChild(mk("span", { fontSize: "13px", fontWeight: "600", color: TOKEN.text }, items[i][1]));
661
+ cell.appendChild(mk("span", Object.assign({}, S.muted, { fontSize: "11px" }), items[i][0]));
281
662
  box.appendChild(cell);
282
663
  }
283
664
  }
@@ -386,34 +767,68 @@ window.__ModuleLoader__.load({
386
767
 
387
768
  function renderDesktopResult() {
388
769
  var box = el("dcs-desktop-result");
770
+ // Enable「执行清理」only after a preview (dryRun) with plans exists.
771
+ var execBtn = el("dcs-exec-cleanup");
772
+ var d = cache.desktop;
773
+ var canExec = Boolean(d && d.dryRun && (d.plannedCount || 0) > 0);
774
+ if (execBtn) {
775
+ execBtn.disabled = !canExec;
776
+ execBtn.style.opacity = canExec ? "1" : "0.5";
777
+ execBtn.style.cursor = canExec ? "pointer" : "not-allowed";
778
+ execBtn.title = canExec ? "执行清理上面预演的窗口" : "请先点「预演」核对清单";
779
+ }
389
780
  if (!box) return;
390
781
  clear(box);
391
- var d = cache.desktop;
392
- if (!d) { box.appendChild(mk("div", S.muted, "点「预演」列出将被关闭的窗口;点「执行」真正关闭(保护项与白名单自动跳过)。")); return; }
782
+ if (!d) return; // no preview yet → render nothing (stay compact)
393
783
  var head = (d.dryRun ? "预演" : "已执行") + ":可关闭 " + (d.plannedCount || 0) + " 个" +
394
784
  (d.dryRun ? "" : (",已关闭 " + (d.closedCount || 0) + " 个")) +
395
- (d.kept && d.kept.length ? (",保护跳过 " + d.kept.length + " 个") : "");
785
+ (d.elevatedCount ? (";其中管理员窗口 " + d.elevatedCount + " 个(执行时弹一次 UAC)") : "") +
786
+ (d.kept && d.kept.length ? (";保护跳过 " + d.kept.length + " 个") : "") +
787
+ (d.elevatedUacCancelled ? ";管理员窗口的 UAC 被取消" : "");
396
788
  box.appendChild(mk("div", { fontSize: "12px", fontWeight: "600", color: d.dryRun ? TOKEN.text : TOKEN.success }, head));
397
789
  var ul = mk("div", { marginTop: "6px" });
398
790
  var planned = d.planned || [];
791
+ var detailed = d.plannedDetailed || [];
399
792
  if (!planned.length) ul.appendChild(mk("div", S.muted, "没有可关闭的窗口。"));
400
793
  for (var i = 0; i < planned.length && i < 40; i++) {
401
- ul.appendChild(mk("div", Object.assign({}, S.mono, { color: TOKEN.textDim, padding: "1px 0" }), planned[i]));
794
+ var det = detailed[i];
795
+ if (det) {
796
+ var row = mk("div", { display: "flex", alignItems: "center", gap: "8px", padding: "2px 0" });
797
+ row.appendChild(iconImg(det.icon, det.process));
798
+ row.appendChild(mk("span", Object.assign({}, S.mono, { color: TOKEN.textDim, wordBreak: "break-all" }), planned[i]));
799
+ ul.appendChild(row);
800
+ } else {
801
+ ul.appendChild(mk("div", Object.assign({}, S.mono, { color: TOKEN.textDim, padding: "1px 0" }), planned[i]));
802
+ }
402
803
  }
403
804
  box.appendChild(ul);
404
805
  }
405
806
 
406
807
  // ---- actions -------------------------------------------------------------
407
808
 
408
- function doWindow(hwnd, action) {
809
+ function doWindow(hwnd, action, elevated, process) {
409
810
  if (!hwnd) return;
410
811
  cache.busy = true;
411
- setStatus("正在 " + action + " …", "muted");
412
- api("/window", { method: "POST", body: JSON.stringify({ hwnd: hwnd, action: action }) })
812
+ setStatus(action === "close" && elevated ? "正在请求管理员权限关闭该窗口(请点 UAC「是」)…" : ("正在 " + action + " …"), "muted");
813
+ api("/window", { method: "POST", body: JSON.stringify({ hwnd: hwnd, action: action, elevated: Boolean(elevated), process: process || "" }) })
413
814
  .then(function (body) {
414
815
  cache.busy = false;
415
- setStatus(action + " 完成", "ok");
416
- applyStatus(body);
816
+ var refused = body && body.reason === "host-protected";
817
+ if (refused) {
818
+ setStatus("已拒绝:这是 DSH 宿主相关窗口,不能 " + action, "error");
819
+ } else if (body && body.ok === false) {
820
+ var r = body.reason || "未知";
821
+ if (r === "uac-denied-or-cancelled") setStatus("未关闭:UAC 被取消", "error");
822
+ else setStatus(action + " 未成功:" + r, "error");
823
+ } else {
824
+ setStatus(action + " 完成" + (body && body.elevated ? "(管理员提权)" : ""), "ok");
825
+ }
826
+ if (body && body.status) { cache.status = body.status; renderWindows(); renderHeader(); renderCounts(); renderWhitelist(); }
827
+ if (!refused && action === "close") {
828
+ afterMutation({ removeHwnds: [hwnd] });
829
+ } else if (!refused) {
830
+ afterMutation({});
831
+ }
417
832
  })
418
833
  .catch(function (error) {
419
834
  cache.busy = false;
@@ -428,7 +843,9 @@ window.__ModuleLoader__.load({
428
843
  .then(function (body) {
429
844
  cache.busy = false;
430
845
  setStatus("已隐藏 " + (body.hidden || 0) + " 个空壳", "ok");
431
- applyStatus(body);
846
+ if (body && body.status) cache.status = body.status;
847
+ renderWindows(); renderHeader(); renderCounts();
848
+ afterMutation({});
432
849
  })
433
850
  .catch(function (error) {
434
851
  cache.busy = false;
@@ -436,20 +853,91 @@ window.__ModuleLoader__.load({
436
853
  });
437
854
  }
438
855
 
856
+ // 按软件一键关闭:优雅关闭该软件的全部窗口(等同对每个窗口按 Alt+F4)。
857
+ function closeSoftware(processName, count) {
858
+ var name = String(processName || "").trim();
859
+ if (!name) return;
860
+ var yes = true;
861
+ try {
862
+ yes = window.confirm(
863
+ "关闭「" + name + "」的全部窗口" + (count > 1 ? ("(共 " + count + " 个)") : "") + "?\n\n" +
864
+ "这是优雅关闭(等同对每个窗口按 Alt+F4),不是强杀。\n" +
865
+ "若该软件是管理员程序,会弹一次 UAC,请点「是」。");
866
+ } catch (e) { yes = true; }
867
+ if (!yes) return;
868
+ cache.busy = true;
869
+ setStatus("正在关闭 " + name + " 的全部窗口…", "muted");
870
+ api("/close-process", { method: "POST", body: JSON.stringify({ process: name }) })
871
+ .then(function (body) {
872
+ cache.busy = false;
873
+ if (body && body.ok) {
874
+ var m = "已关闭 " + name + " " + (body.closedCount || 0) + "/" + (body.total || 0) + " 个窗口";
875
+ if (body.elevated) m += "(含管理员窗口 " + body.elevated + " 个)";
876
+ if (body.uacCancelled) m += " · UAC 被取消,管理员窗口未关";
877
+ setStatus(m, "ok");
878
+ } else {
879
+ setStatus("关闭失败:" + ((body && body.reason) || "未知"), "error");
880
+ }
881
+ if (body && body.status) cache.status = body.status;
882
+ renderWindows(); renderHeader(); renderCounts();
883
+ if (body && body.ok !== false && !body.uacCancelled) {
884
+ afterMutation({ removeProcess: name });
885
+ } else {
886
+ afterMutation({});
887
+ }
888
+ })
889
+ .catch(function (error) {
890
+ cache.busy = false;
891
+ setStatus("关闭失败:" + (error && error.message ? error.message : error), "error");
892
+ });
893
+ }
894
+
895
+ // 关闭空壳窗口:真正结束空壳控制台的进程(不是隐藏),释放资源。
896
+ function closeEmpty() {
897
+ var yes = true;
898
+ try { yes = window.confirm("关闭所有空壳控制台窗口?\n只关闭「裸解释器空壳」(cmd / powershell / pwsh / python 且没有正在运行的任务);\n宿主程序、Fluent/MPI 等一律不动。确定的请按「确定」。"); } catch (e) { yes = true; }
899
+ if (!yes) return;
900
+ cache.busy = true;
901
+ setStatus("正在关闭空壳窗口…", "muted");
902
+ api("/close-empty", { method: "POST", body: "{}" })
903
+ .then(function (body) {
904
+ cache.busy = false;
905
+ var msg = "已关闭 " + (body.closed || 0) + " 个空壳窗口";
906
+ if (body.skipped) msg += "(跳过 " + body.skipped + " 个受保护/非裸解释器)";
907
+ setStatus(msg, "ok");
908
+ if (body && body.status) cache.status = body.status;
909
+ renderWindows(); renderHeader(); renderCounts();
910
+ afterMutation({});
911
+ })
912
+ .catch(function (error) {
913
+ cache.busy = false;
914
+ setStatus("关闭空壳失败:" + (error && error.message ? error.message : error), "error");
915
+ });
916
+ }
917
+
439
918
  function desktopCleanup(dryRun) {
440
919
  if (!dryRun) {
441
920
  var yes = true;
442
- try { yes = window.confirm("将关闭所有可关闭的窗口(任务栏窗口),受保护的程序和白名单会自动跳过。确定执行?"); } catch (e) {}
921
+ try { yes = window.confirm("将清理任务栏窗口(关闭其他程序的窗口)。受保护的进程和白名单会自动跳过,前台窗口不会被关。\n\n执行前建议先「预演」核对清单。确定执行?"); } catch (e) {}
443
922
  if (!yes) return;
444
923
  }
445
924
  cache.busy = true;
446
- setStatus(dryRun ? "正在预演…" : "正在关闭其他窗口…", "muted");
925
+ setStatus(dryRun ? "正在预演…" : "正在清理任务栏窗口…", "muted");
447
926
  api("/desktop-cleanup", { method: "POST", body: JSON.stringify({ dryRun: Boolean(dryRun) }) })
448
927
  .then(function (body) {
449
928
  cache.busy = false;
450
929
  cache.desktop = body;
451
- setStatus(dryRun ? "预演完成" : ("已关闭 " + (body.closedCount || 0) + " 个窗口"), "ok");
452
930
  renderDesktopResult();
931
+ if (dryRun) {
932
+ setStatus("预演完成:可关闭 " + (body.plannedCount || 0) + " 个,查看清单后点「执行清理」", "ok");
933
+ } else {
934
+ var mm = "已清理 " + (body.closedCount || 0) + " 个窗口";
935
+ if (body.elevatedUacCancelled) mm += "(管理员窗口的 UAC 被取消)";
936
+ setStatus(mm, "ok");
937
+ if (body && body.status) cache.status = body.status;
938
+ renderWindows(); renderHeader(); renderCounts();
939
+ afterMutation({});
940
+ }
453
941
  })
454
942
  .catch(function (error) {
455
943
  cache.busy = false;
@@ -490,6 +978,63 @@ window.__ModuleLoader__.load({
490
978
  whitelistAction("reset", "process", []);
491
979
  }
492
980
 
981
+ // ---- Auto refresh while the section is VISIBLE --------------------------
982
+ // 🔴 只在面板真正可见时定时刷新;关闭设置 / 切走 / 隐藏后立刻停表。
983
+ // 判定三重条件,任一不满足即视为不可见(保守,宁可少刷不可空转):
984
+ // ① document.visibilityState === 'visible'(窗口最小化/切到别的应用时停)
985
+ // ② 我们的容器在文档里(document.contains)——设置页关掉后容器被卸载
986
+ // ③ 容器 offsetParent 非空 或 有非零尺寸(display:none / 被隐藏时停)
987
+ // 另监听 visibilitychange 与设置页消失,即时停表(不只等定时器)。
988
+ var AUTO_MS = 3000;
989
+ function panelAlive() {
990
+ try {
991
+ if (typeof document === "undefined") return false;
992
+ if (document.visibilityState && document.visibilityState !== "visible") return false;
993
+ var box = el("dcs-taskbar") || el("dcs-counts");
994
+ if (!box) return false;
995
+ if (document.contains && !document.contains(box)) return false;
996
+ // offsetParent 为空通常意味着某级祖先 display:none(关闭/隐藏)
997
+ if ("offsetParent" in box && box.offsetParent === null) {
998
+ var r = box.getBoundingClientRect ? box.getBoundingClientRect() : null;
999
+ if (!r || (r.width === 0 && r.height === 0)) return false;
1000
+ }
1001
+ return true;
1002
+ } catch (e) { return false; }
1003
+ }
1004
+
1005
+ function startAutoRefresh() {
1006
+ if (cache.autoTimer) return;
1007
+ cache.autoTimer = setInterval(function () {
1008
+ if (!panelAlive()) { stopAutoRefresh(); return; }
1009
+ if (cache.busy || cache.taskbarLoading) return; // 正在操作/加载时跳过,避免打断
1010
+ loadTaskbar({ silent: true });
1011
+ loadStatus(true);
1012
+ }, AUTO_MS);
1013
+ }
1014
+
1015
+ function stopAutoRefresh() {
1016
+ if (cache.autoTimer) { try { clearInterval(cache.autoTimer); } catch (e) {} cache.autoTimer = null; }
1017
+ }
1018
+
1019
+ // 挂一次性全局监听:窗口可见性变化 / 页面卸载 → 停表;重新可见 → 视情况恢复。
1020
+ function installVisibilityHooks() {
1021
+ if (cache.visHooked) return;
1022
+ cache.visHooked = true;
1023
+ try {
1024
+ document.addEventListener("visibilitychange", function () {
1025
+ if (document.visibilityState === "visible") {
1026
+ if (panelAlive()) { loadTaskbar({ silent: true }); loadStatus(true); startAutoRefresh(); }
1027
+ } else {
1028
+ stopAutoRefresh();
1029
+ }
1030
+ });
1031
+ } catch (e) {}
1032
+ try {
1033
+ window.addEventListener("beforeunload", function () { stopAutoRefresh(); });
1034
+ window.addEventListener("pagehide", function () { stopAutoRefresh(); });
1035
+ } catch (e) {}
1036
+ }
1037
+
493
1038
  // ---- React component -----------------------------------------------------
494
1039
 
495
1040
  function SettingsSection() {
@@ -500,94 +1045,172 @@ window.__ModuleLoader__.load({
500
1045
  setTimeout(function () {
501
1046
  if (cache.status) { renderAll(); }
502
1047
  else { loadStatus(); }
1048
+ loadTaskbar({ silent: true });
1049
+ installVisibilityHooks();
1050
+ startAutoRefresh();
503
1051
  }, 0);
504
1052
  } catch (e) {}
505
1053
 
1054
+ var TABS = [
1055
+ { key: "taskbar", label: "任务栏" },
1056
+ { key: "console", label: "控制台" },
1057
+ { key: "whitelist", label: "白名单" }
1058
+ ];
1059
+ var activeTab = TAB.active || "taskbar";
1060
+ var tabBtn = function (t) {
1061
+ var on = activeTab === t.key;
1062
+ return React.createElement("button", {
1063
+ id: "dcs-tab-" + t.key,
1064
+ type: "button",
1065
+ onClick: function () { setActiveTab(t.key); },
1066
+ style: {
1067
+ background: "transparent", border: "none",
1068
+ borderBottom: "2px solid " + (on ? TOKEN.accent : "transparent"),
1069
+ color: on ? TOKEN.accent : TOKEN.textDim,
1070
+ fontWeight: on ? 600 : 500,
1071
+ fontSize: "12.5px", padding: "6px 10px", cursor: "pointer", lineHeight: 1.3
1072
+ }
1073
+ }, t.label);
1074
+ };
1075
+ var panel = function (t, children) {
1076
+ return React.createElement("div", {
1077
+ id: "dcs-panel-" + t.key,
1078
+ style: { display: (activeTab === t.key) ? "block" : "none" }
1079
+ }, children);
1080
+ };
1081
+ var hint = function (text) {
1082
+ return React.createElement("div", {
1083
+ style: Object.assign({}, S.muted, { fontSize: "11px", marginBottom: "6px", lineHeight: 1.45 })
1084
+ }, text);
1085
+ };
1086
+ var scrollBox = { maxHeight: "270px", overflowY: "auto", paddingRight: "2px" };
1087
+
506
1088
  return React.createElement(
507
1089
  "div",
508
- { style: { display: "flex", flexDirection: "column", gap: "0px" } },
509
-
510
- // Header
511
- React.createElement("div", { style: S.card },
512
- React.createElement("div", { style: S.cardHead },
513
- React.createElement("div", null,
514
- React.createElement("div", { style: S.title }, "🛡️ 控制台哨兵"),
515
- React.createElement("div", { id: "dcs-cfg", style: S.muted }, "读取中…")
1090
+ { style: { display: "flex", flexDirection: "column", gap: "8px" } },
1091
+
1092
+ // Header (compact: title + platform pill + refresh, then cfg + status)
1093
+ React.createElement("div", { style: Object.assign({}, S.card, { padding: "9px 12px", marginBottom: "2px" }) },
1094
+ React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px" } },
1095
+ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" } },
1096
+ React.createElement("span", { style: S.title }, "控制台哨兵"),
1097
+ React.createElement("span", { id: "dcs-platform", style: { fontSize: "11.5px", fontWeight: 600 } }, "读取中…")
516
1098
  ),
517
- React.createElement("div", { style: { display: "flex", alignItems: "center", gap: "10px" } },
518
- React.createElement("span", { id: "dcs-platform", style: { fontSize: "12px", fontWeight: 600 } }, "读取中…"),
519
- React.createElement("button", {
520
- type: "button", style: S.btnPrimary,
521
- onClick: function () { loadStatus(); }
522
- }, "刷新")
523
- )
1099
+ React.createElement("button", { type: "button", style: S.btnPrimary, onClick: function () { refreshAll(); } }, "刷新")
524
1100
  ),
525
- React.createElement("div", { id: "dcs-status", style: { fontSize: "12px", color: TOKEN.textDim, minHeight: "16px" } }, "")
1101
+ React.createElement("div", { id: "dcs-cfg", style: Object.assign({}, S.muted, { marginTop: "4px", fontSize: "11.5px" }) }, "读取中…"),
1102
+ React.createElement("div", { id: "dcs-status", style: { fontSize: "11.5px", color: TOKEN.textDim, minHeight: "14px", marginTop: "2px" } }, "")
526
1103
  ),
527
1104
 
528
- // Counts
529
- React.createElement("div", { style: S.card },
530
- React.createElement("div", { style: S.cardHead },
531
- React.createElement("div", { style: S.title }, "概览"),
532
- React.createElement("button", { type: "button", style: S.btn, onClick: function () { hideEmpty(); } }, "隐藏可见空壳")
533
- ),
534
- React.createElement("div", { id: "dcs-counts", style: { display: "flex", gap: "8px", flexWrap: "wrap" } })
1105
+ // Counts — slim inline strip (no card chrome)
1106
+ React.createElement("div", { id: "dcs-counts", style: { display: "flex", gap: "6px", flexWrap: "wrap" } }),
1107
+
1108
+ // 操作栏 —— 全部动作压成一行(说明走 title 悬浮提示,不占版面)
1109
+ React.createElement("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap", alignItems: "center" } },
1110
+ React.createElement("button", {
1111
+ type: "button", style: S.btn,
1112
+ title: "把「空壳」控制台窗口隐藏起来(进程不杀,可恢复)",
1113
+ onClick: function () { hideEmpty(); }
1114
+ }, "隐藏可见空壳"),
1115
+ React.createElement("button", {
1116
+ type: "button", style: S.btnDanger,
1117
+ title: "真正结束「空壳」控制台进程(只关 cmd/powershell/pwsh/python 且无任务在跑;宿主与 Fluent 不动)",
1118
+ onClick: function () { closeEmpty(); }
1119
+ }, "关闭空壳窗口"),
1120
+ React.createElement("span", { style: { width: "1px", height: "18px", background: TOKEN.borderSoft, flex: "0 0 auto" } }),
1121
+ React.createElement("button", {
1122
+ type: "button", style: S.btn,
1123
+ title: "预演:列出将被关闭的窗口(保护项与白名单自动跳过)",
1124
+ onClick: function () { desktopCleanup(true); }
1125
+ }, "预演清理"),
1126
+ React.createElement("button", {
1127
+ id: "dcs-exec-cleanup", type: "button", style: S.btnPrimary,
1128
+ title: "执行清理:关闭其他程序的窗口(需先预演;管理员窗口会弹一次 UAC)",
1129
+ onClick: function () { desktopCleanup(false); }
1130
+ }, "执行清理")
535
1131
  ),
536
1132
 
537
- // Taskbar window cleanup (the "一键关闭其他窗口" feature)
538
- React.createElement("div", { style: S.card },
539
- React.createElement("div", { style: S.cardHead },
540
- React.createElement("div", null,
541
- React.createElement("div", { style: S.title }, "任务栏窗口清理"),
542
- React.createElement("div", { style: S.muted }, "关闭其它程序的窗口;受保护进程与白名单自动跳过,前台窗口不会被关。")
1133
+ // 预演/执行结果(有内容才显示,平时只是一行小字,不占高度)
1134
+ React.createElement("div", { id: "dcs-desktop-result", style: { maxHeight: "160px", overflowY: "auto", paddingRight: "2px" } }),
1135
+
1136
+ // Tabs
1137
+ React.createElement("div", { style: { display: "flex", gap: "2px", borderBottom: "1px solid " + TOKEN.borderSoft } },
1138
+ tabBtn(TABS[0]), tabBtn(TABS[1]), tabBtn(TABS[2])
1139
+ ),
1140
+
1141
+ // Panel 1: 任务栏(图标网格 / 详细分组 · 每窗口可单独关闭 / 关掉整个软件 / 白名单)
1142
+ panel(TABS[0],
1143
+ React.createElement("div", null,
1144
+ React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", marginBottom: "6px" } },
1145
+ React.createElement("span", {
1146
+ style: Object.assign({}, S.muted, { fontSize: "11px" }),
1147
+ title: "图标=点一下关闭该窗口;详细视图=按软件分组,可勾选加入白名单、可关掉整个软件。管理员窗口会弹一次 UAC;「系统保护」程序(宿主/Fluent/MPI)不可关闭。"
1148
+ }, cache.taskbarView === "icon" ? "点击图标关闭对应窗口(悬停看详情)" : "勾选=加入白名单;每窗口后可单独关闭"),
1149
+ React.createElement("button", { id: "dcs-taskbar-view", type: "button", style: S.btn, onClick: function () { toggleTaskbarView(); } }, "详细视图")
543
1150
  ),
544
- React.createElement("div", { style: { display: "flex", gap: "8px" } },
545
- React.createElement("button", { type: "button", style: S.btn, onClick: function () { desktopCleanup(true); } }, "预演"),
546
- React.createElement("button", { type: "button", style: S.btnPrimary, onClick: function () { desktopCleanup(false); } }, "一键关闭其他窗口")
547
- )
548
- ),
549
- React.createElement("div", { id: "dcs-desktop-result" })
1151
+ React.createElement("div", { id: "dcs-taskbar", style: scrollBox })
1152
+ )
550
1153
  ),
551
1154
 
552
- // Tracked windows
553
- React.createElement("div", { style: S.card },
554
- React.createElement("div", { style: S.cardHead },
555
- React.createElement("div", { style: S.title }, "控制台窗口"),
556
- React.createElement("div", { style: S.muted }, "空壳 = 解释器无业务子进程(只剩 conhost)")
557
- ),
558
- React.createElement("div", { id: "dcs-windows" })
1155
+ // Panel 2: 控制台窗口
1156
+ panel(TABS[1],
1157
+ React.createElement("div", null,
1158
+ hint("空壳 = 解释器无业务子进程(只剩 conhost)。点上方「隐藏可见空壳 / 关闭空壳窗口」一键处理。"),
1159
+ React.createElement("div", { id: "dcs-windows", style: { maxHeight: "260px", overflowY: "auto", paddingRight: "2px" } })
1160
+ )
559
1161
  ),
560
1162
 
561
- // Whitelist
562
- React.createElement("div", { style: S.card },
563
- React.createElement("div", { style: S.cardHead },
564
- React.createElement("div", null,
565
- React.createElement("div", { style: S.title }, "豁免白名单"),
566
- React.createElement("div", { style: S.muted }, "这些进程/窗口标题永远不被隐藏或关闭。")
567
- ),
568
- React.createElement("button", { type: "button", style: S.btnDanger, onClick: function () { whitelistReset(); } }, "清空运行时白名单")
569
- ),
570
- React.createElement("div", { style: { marginTop: "6px" } },
571
- React.createElement("div", { style: Object.assign({}, S.muted, { marginBottom: "4px" }) }, "进程名"),
1163
+ // Panel 3: 白名单
1164
+ panel(TABS[2],
1165
+ React.createElement("div", null,
1166
+ hint("这些进程 / 标题永远不被隐藏或关闭。"),
1167
+ React.createElement("div", { style: Object.assign({}, S.muted, { marginBottom: "3px", fontSize: "11px" }) }, "进程名"),
572
1168
  React.createElement("div", { id: "dcs-wl-proc", style: { marginBottom: "4px" } }),
573
- React.createElement("div", { style: { display: "flex", gap: "8px", alignItems: "center", marginTop: "4px" } },
1169
+ React.createElement("div", { style: { display: "flex", gap: "6px", alignItems: "center", marginTop: "3px" } },
574
1170
  React.createElement("input", { id: "dcs-wl-proc-input", type: "text", placeholder: "如 chrome / code / excel", style: S.input }),
575
- React.createElement("button", { type: "button", style: S.btn, onClick: function () { whitelistAdd("process", "dcs-wl-proc-input"); } }, "加入进程白名单")
576
- )
577
- ),
578
- React.createElement("div", { style: { marginTop: "12px" } },
579
- React.createElement("div", { style: Object.assign({}, S.muted, { marginBottom: "4px" }) }, "窗口标题包含"),
1171
+ React.createElement("button", { type: "button", style: S.btn, onClick: function () { whitelistAdd("process", "dcs-wl-proc-input"); } }, "加入")
1172
+ ),
1173
+ React.createElement("div", { style: Object.assign({}, S.muted, { marginTop: "10px", marginBottom: "3px", fontSize: "11px" }) }, "窗口标题包含"),
580
1174
  React.createElement("div", { id: "dcs-wl-title", style: { marginBottom: "4px" } }),
581
- React.createElement("div", { style: { display: "flex", gap: "8px", alignItems: "center", marginTop: "4px" } },
1175
+ React.createElement("div", { style: { display: "flex", gap: "6px", alignItems: "center", marginTop: "3px" } },
582
1176
  React.createElement("input", { id: "dcs-wl-title-input", type: "text", placeholder: "如 Visual Studio Code / 记事本", style: S.input }),
583
- React.createElement("button", { type: "button", style: S.btn, onClick: function () { whitelistAdd("title", "dcs-wl-title-input"); } }, "加入标题白名单")
584
- )
585
- ),
586
- React.createElement("div", { id: "dcs-wl-path", style: Object.assign({}, S.muted, S.mono, { marginTop: "10px" }) })
1177
+ React.createElement("button", { type: "button", style: S.btn, onClick: function () { whitelistAdd("title", "dcs-wl-title-input"); } }, "加入")
1178
+ ),
1179
+ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: "10px" } },
1180
+ React.createElement("button", { type: "button", style: S.btnDanger, onClick: function () { whitelistReset(); } }, "清空运行时白名单")
1181
+ ),
1182
+ React.createElement("div", { id: "dcs-wl-path", style: Object.assign({}, S.muted, S.mono, { marginTop: "8px", fontSize: "10.5px", wordBreak: "break-all" }) })
1183
+ )
587
1184
  )
588
1185
  );
589
1186
  }
590
1187
 
1188
+ // 分区导航图标(与插件内容相关:终端窗口 + 盾牌「哨兵」)。
1189
+ // 🔴 宿主设置页对未注册 icon 的分区会 fallback 到通用的齿轮图标
1190
+ // (navIcon(row.id) 的默认分支) —— 视觉上像"没有图标/缺省图标"。
1191
+ // 必须注册 settings.section.icon 槽并用 only 限定到本分区 id。
1192
+ function SettingsIcon(props) {
1193
+ var size = (props && props.size) || 16;
1194
+ var active = Boolean(props && props.active);
1195
+ var color = active ? TOKEN.accent : "currentColor";
1196
+ return React.createElement(
1197
+ "svg",
1198
+ {
1199
+ width: size, height: size, viewBox: "0 0 16 16",
1200
+ fill: "none", stroke: color, strokeWidth: 1.4,
1201
+ strokeLinecap: "round", strokeLinejoin: "round",
1202
+ "aria-hidden": "true", focusable: "false"
1203
+ },
1204
+ // 终端窗口外框
1205
+ React.createElement("rect", { x: "1.6", y: "2.6", width: "12.8", height: "10.8", rx: "1.9" }),
1206
+ // 标题栏分隔线
1207
+ React.createElement("path", { d: "M1.6 5.4h12.8" }),
1208
+ // 提示符 >_ (控制台的标志性元素)
1209
+ React.createElement("path", { d: "M4.3 8.1l1.7 1.5-1.7 1.5" }),
1210
+ React.createElement("path", { d: "M7.9 11.1h3.4" })
1211
+ );
1212
+ }
1213
+
591
1214
  function registerSettings(ctx) {
592
1215
  if (!ctx || !ctx.slots || !React) return false;
593
1216
  try {
@@ -603,11 +1226,30 @@ window.__ModuleLoader__.load({
603
1226
  SettingsSection
604
1227
  );
605
1228
  });
606
- return true;
607
1229
  } catch (e) {
608
- try { console.warn("[dsh-console-sentinel] settings registration failed", e); } catch (e2) {}
609
- return false;
1230
+ try { console.warn("[dsh-console-sentinel] settings.section registration failed", e); } catch (e2) {}
1231
+ }
1232
+ // 导航图标:独立注册,失败不影响分区注册本身。
1233
+ // 🔴 宿主契约(dsh-cordis-client-runner / slots.ts:65):
1234
+ // 「settings.section.icon」的 registration id **必须等于目标分区的 id**,
1235
+ // 不匹配的 id 永不渲染(instead the shell keeps its DEFAULT GLYPH = 齿轮)。
1236
+ // 所以这里必须写 "dsh-console-sentinel",不能加 -icon 后缀。
1237
+ try {
1238
+ ctx.slots.inject("settings.section.icon", function () {
1239
+ return ctx.slots.register(
1240
+ {
1241
+ name: "settings.section.icon",
1242
+ id: "dsh-console-sentinel",
1243
+ order: 28,
1244
+ inject: function () { return {}; }
1245
+ },
1246
+ SettingsIcon
1247
+ );
1248
+ });
1249
+ } catch (e) {
1250
+ try { console.warn("[dsh-console-sentinel] settings icon registration failed", e); } catch (e2) {}
610
1251
  }
1252
+ return true;
611
1253
  }
612
1254
 
613
1255
  // MUST be a plain array (see the header comment).