dsh-ws-recency 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/client.js +58 -67
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,8 +12,9 @@ DSH 已经把**组内**会话按 `updatedAt` 倒序排好了(排序方式 =
12
12
 
13
13
  会话超过 `foldDays`(默认 **3 天**)没有更新就折叠起来;整个工作区都过期时,它只剩一行工作区标题——需要翻历史时点开就行。
14
14
 
15
- - **每组只有一个控件**:宿主自己还有没渲染的行时,用它的「展开其余 N 条会话」(N 已含被折叠的行);宿主把该组渲染完了时,才出现插件的 `展开历史 N` / `收起历史`;
16
- - 点宿主的「展开其余」会同时放出被折叠的行;它变成「收起」时,折叠也一并复原;
15
+ - **每组一个控件**:宿主的「展开其余 N 条会话」被隐藏,由插件的 `展开历史 N` / `收起历史` 接管。N = 宿主还没渲染的行 + 已折叠的过期行;
16
+ - 点「展开历史」→ 插件逐轮驱动宿主那个隐藏按钮(每轮它会多渲染 5 条,剩下 ≤5 条时一次全出),直到整组都渲染出来,同时展开过期行——**一次点到全展开**;
17
+ - 点「收起历史」→ 重新折叠过期行(宿主已渲染的部分仍由插件按时间折叠,不会丢失);
17
18
  - 选中 / 运行中 / 等待处理的会话**永不折叠**(可在设置里关掉这个豁免);
18
19
  - 空白草稿行(新会话)不参与折叠判断;
19
20
  - 每 5 分钟自动重算一次,所以跨过 3 天边界的会话会自己折叠,不用刷新。
@@ -97,7 +98,7 @@ dsh plugin --profile <profile> add github:chyxhtc/dsh-ws-recency
97
98
  或固定版本的预构建包:
98
99
 
99
100
  ```powershell
100
- dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.9.0/dsh-ws-recency-0.9.0.tgz
101
+ dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.9.2/dsh-ws-recency-0.9.2.tgz
101
102
  ```
102
103
 
103
104
  装完把 `dsh-ws-recency` 加进 `~/.hypers-work/profiles/<profile>/package.json` 的 `dsh.profile.bundles`,重启应用。
package/client.js CHANGED
@@ -69,6 +69,8 @@
69
69
  var MIN_DAYS = 1;
70
70
  var MAX_DAYS = 90;
71
71
  var DEFAULT_DAYS = 3;
72
+ /** Safety stop for driving the host's own "show more" (5 rows per click). */
73
+ var MAX_EXPAND_CLICKS = 40;
72
74
  var TICK_MS = 5 * 60 * 1000;
73
75
 
74
76
  var data = { sessions: null, workspaces: null, settings: null, statuses: null, actions: null, service: null, t: null };
@@ -78,13 +80,12 @@
78
80
  var written = new Set();
79
81
  var roots = new Set();
80
82
  var hidden = new Set();
81
- var labelled = new Set();
82
83
  var toggles = new Set();
83
84
  var dots = new Set();
84
85
  var pinHidden = new Set();
85
86
  var pinHost = null;
87
+ var expandClicks = new Map();
86
88
  var expandedSections = new Set();
87
- var clickMarked = new WeakSet();
88
89
  var headerMarked = new WeakSet();
89
90
  var disposed = false;
90
91
 
@@ -93,6 +94,7 @@
93
94
  "[" + ROOT_ATTR + '="true"]>*{flex:0 0 auto}',
94
95
  "[" + HIDDEN_ATTR + '="true"]{display:none !important}',
95
96
  "[" + PIN_HIDDEN_ATTR + '="true"]{display:none !important}',
97
+ '[class*="' + OVERFLOW_TOKEN + '"]{display:none !important}',
96
98
  ".wsr-pin{display:flex;flex-direction:column;flex:0 0 auto;padding:2px 0 6px;margin-bottom:4px;border-bottom:.5px solid var(--dsw-alias-border-l2)}",
97
99
  ".wsr-pin-head{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);padding:4px 12px 2px}",
98
100
  ".wsr-pin-row{display:flex;align-items:center;gap:8px;height:30px;padding:0 12px;border-radius:6px;cursor:pointer}",
@@ -325,11 +327,6 @@
325
327
  return out;
326
328
  }
327
329
 
328
- function overflowButtonOf(section) {
329
- if (typeof section.querySelector !== "function") return null;
330
- return section.querySelector('button[class*="' + OVERFLOW_TOKEN + '"]');
331
- }
332
-
333
330
  function isActiveRow(row) {
334
331
  if (typeof row.querySelector !== "function") return false;
335
332
  return row.querySelector('[data-state="ongoing"], [data-state="warning"]') !== null;
@@ -371,55 +368,23 @@
371
368
  return session !== undefined && session !== null && session.blank === true;
372
369
  }
373
370
 
374
- /**
375
- * Rewrite the host button's count so it reports what is really hidden: the
376
- * host counts only the rows it did not render, and the fold hides a few more.
377
- */
378
- function rewriteLabel(button, extraHidden) {
379
- if (button === null || button === undefined) return;
380
- if (typeof button.textContent !== "string" || button.dataset === undefined) return;
381
- var base = button.dataset.wsrBase;
382
- if (typeof base !== "string") {
383
- base = button.textContent;
384
- button.dataset.wsrBase = base;
385
- labelled.add(button);
386
- }
387
- var match = /\d+/.exec(base);
388
- if (match === null) return;
389
- var hostCount = Number(match[0]);
390
- if (!Number.isFinite(hostCount)) return;
391
- var total = hostCount + extraHidden;
392
- var text = total <= 0 ? base : base.slice(0, match.index) + String(total) + base.slice(match.index + match[0].length);
393
- if (button.textContent !== text) button.textContent = text;
394
- }
395
-
396
- function releaseLabel(button) {
397
- if (button === null || button === undefined || button.dataset === undefined) return;
398
- var base = button.dataset.wsrBase;
399
- if (typeof base === "string") {
400
- if (button.textContent !== base) button.textContent = base;
401
- delete button.dataset.wsrBase;
402
- }
403
- labelled.delete(button);
371
+ /** The host's own "show more" button, hidden by CSS but still clickable. */
372
+ function overflowButtonOf(section) {
373
+ if (typeof section.querySelector !== "function") return null;
374
+ return section.querySelector('button[class*="' + OVERFLOW_TOKEN + '"]');
404
375
  }
405
376
 
406
- function markButton(button, key) {
407
- if (typeof button.addEventListener !== "function" || clickMarked.has(button)) return;
408
- clickMarked.add(button);
409
- button.addEventListener("click", function () {
410
- if (disposed) return;
411
- /* The host's own control is the only one while it exists, so it has to
412
- carry the fold too: `aria-expanded` still reads the pre-click state
413
- (React delegates at the root, this listener runs first), which is
414
- exactly what decides whether the click reveals or hides. */
415
- var expanded = typeof button.getAttribute === "function"
416
- && button.getAttribute("aria-expanded") === "true";
417
- if (expanded) expandedSections.delete(key);
418
- else expandedSections.add(key);
419
- schedule();
420
- });
377
+ /** The count the host reports on its own button, 0 when there is none. */
378
+ function labelNumber(button) {
379
+ if (button === null || button === undefined) return 0;
380
+ var text = typeof button.textContent === "string" ? button.textContent : "";
381
+ var match = /\d+/.exec(text);
382
+ if (match === null) return 0;
383
+ var value = Number(match[0]);
384
+ return Number.isFinite(value) && value > 0 ? value : 0;
421
385
  }
422
386
 
387
+ /** Collapsing a group resets its fold state, so the click is observed. */
423
388
  function markHeader(header, key) {
424
389
  if (typeof header.addEventListener !== "function" || headerMarked.has(header)) return;
425
390
  headerMarked.add(header);
@@ -441,11 +406,15 @@
441
406
  button.addEventListener("click", function (event) {
442
407
  if (disposed) return;
443
408
  if (event !== undefined && typeof event.stopPropagation === "function") event.stopPropagation();
444
- onClick();
409
+ var current = button.wsrOnClick;
410
+ if (typeof current === "function") current();
445
411
  });
446
412
  section.appendChild(button);
447
413
  toggles.add(button);
448
414
  }
415
+ /* The handler is refreshed every pass: the button toggles between "show"
416
+ and "hide", and a captured first callback would only ever show. */
417
+ button.wsrOnClick = onClick;
449
418
  if (button.textContent !== label) button.textContent = label;
450
419
  return button;
451
420
  }
@@ -462,6 +431,13 @@
462
431
  * Fold the rows the host rendered that have not changed for `foldDays`, and
463
432
  * offer the group's own control to bring them back. A workspace whose every
464
433
  * session is that old therefore renders as a bare header.
434
+ *
435
+ * The host's own "show more" button is hidden (CSS) and driven from here: it
436
+ * only renders five rows at a time and cannot collapse once everything is
437
+ * rendered, which made "show more" look broken next to a fold. Expanding the
438
+ * group now clicks that hidden button once per pass until the host has
439
+ * rendered every session, so one click reveals everything; collapsing just
440
+ * folds the stale rows again.
465
441
  */
466
442
  function foldRows(entries, settings, sessions, now) {
467
443
  var cutoff = now - settings.foldDays * DAY;
@@ -471,10 +447,27 @@
471
447
  var rows = rowElements(section);
472
448
  var hostButton = overflowButtonOf(section);
473
449
  markHeader(entry.header, entry.key);
474
- if (hostButton !== null) markButton(hostButton, entry.key);
475
- releaseLabel(hostButton);
476
450
 
477
451
  var expanded = expandedSections.has(entry.key);
452
+ var hostHidden = hostButton === null ? 0 : labelNumber(hostButton);
453
+ var hostExpanded = hostButton !== null
454
+ && typeof hostButton.getAttribute === "function"
455
+ && hostButton.getAttribute("aria-expanded") === "true";
456
+ if (expanded && hostButton !== null && !hostExpanded) {
457
+ /* The host renders five more per click (or everything when five or
458
+ fewer remain) and reports `aria-expanded` once nothing is hidden.
459
+ Stopping on that flag is what keeps the loop from collapsing the
460
+ very thing it just expanded - a click on an expanded host button
461
+ resets the watermark. */
462
+ var used = expandClicks.get(entry.key) || 0;
463
+ if (used < MAX_EXPAND_CLICKS) {
464
+ expandClicks.set(entry.key, used + 1);
465
+ hostButton.click();
466
+ }
467
+ } else if (!expanded) {
468
+ expandClicks.delete(entry.key);
469
+ }
470
+
478
471
  var folded = 0;
479
472
  for (var j = 0; j < rows.length; j += 1) {
480
473
  var row = rows[j];
@@ -493,23 +486,22 @@
493
486
  }
494
487
  }
495
488
 
496
- /* ONE control per group: while the host offers its own "show more"
497
- button, that button is the control (its count already carries the
498
- folded rows below). Only a group the host finished rendering gets
499
- this plugin's own toggle. */
500
- if (hostButton !== null) {
501
- dropToggle(section);
502
- rewriteLabel(hostButton, expanded ? 0 : folded);
503
- } else if (expanded) {
489
+ /* ONE control per group: its count is everything the group still hides,
490
+ both the rows the host did not render and the stale rows this plugin
491
+ folded. Expanding drives the hidden host button until nothing is left
492
+ unrendered, so one click reveals the whole group. */
493
+ var remaining = folded + hostHidden;
494
+ if (expanded) {
504
495
  (function (entryKey) {
505
496
  ensureToggle(section, entryKey, t("fold.hide"), function () {
506
497
  expandedSections.delete(entryKey);
498
+ expandClicks.delete(entryKey);
507
499
  schedule();
508
500
  });
509
501
  })(entry.key);
510
- } else if (folded > 0) {
502
+ } else if (remaining > 0) {
511
503
  (function (entryKey) {
512
- ensureToggle(section, entryKey, t("fold.show", { n: folded }), function () {
504
+ ensureToggle(section, entryKey, t("fold.show", { n: remaining }), function () {
513
505
  expandedSections.add(entryKey);
514
506
  schedule();
515
507
  });
@@ -958,8 +950,6 @@
958
950
  if (element.removeAttribute !== undefined) element.removeAttribute(HIDDEN_ATTR);
959
951
  });
960
952
  hidden.clear();
961
- labelled.forEach(function (button) { releaseLabel(button); });
962
- labelled.clear();
963
953
  toggles.forEach(function (button) { button.remove(); });
964
954
  toggles.clear();
965
955
  dots.forEach(function (dot) { dot.remove(); });
@@ -970,6 +960,7 @@
970
960
  pinHidden.clear();
971
961
  if (pinHost !== null && pinHost.parentElement !== null) pinHost.remove();
972
962
  pinHost = null;
963
+ expandClicks.clear();
973
964
  roots.forEach(function (root) { root.removeAttribute(ROOT_ATTR); });
974
965
  roots.clear();
975
966
  var marked = document.querySelectorAll("[" + STATE_ATTR + "]");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ws-recency",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Sidebar session management for DeepSeek Harness: workspace groups sorted by latest activity, stale history folded, a global pinned section, follow-up marks, and session delete into a recoverable trash.",
5
5
  "license": "MIT",
6
6
  "author": "chyxhtc",