dsh-ws-recency 0.7.3 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +16 -3
  2. package/client.js +219 -2
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-ws-recency
2
2
 
3
- DSH 侧栏的三件事:**工作区分组按最近活跃排序**、**过期历史自动折叠**、**运行中/未读/需跟进三色标记**。单独装上就有效,不依赖其它插件。
3
+ DSH 侧栏的四件事:**工作区分组按最近活跃排序**、**过期历史自动折叠**、**运行中/未读/需跟进三色标记**、**所有置顶会话汇总到顶部一个区**。单独装上就有效,不依赖其它插件。
4
4
 
5
5
  ## 一、分组排序
6
6
 
@@ -35,12 +35,25 @@ DSH 已经把**组内**会话按 `updatedAt` 倒序排好了(排序方式 =
35
35
  - **标记只有你手动取消才会消失**:运行中/完成未读期间它只是被更强的状态盖住显示,跑完、读完(没有新未读)之后琥珀自己回来;
36
36
  - 不受打开时长、会话是否继续对话影响——继续对话恰恰常常是还欠一次跟进的原因。
37
37
 
38
+ ## 四、全局置顶区
39
+
40
+ DSH 原生的置顶只把会话顶到**它所属工作区分组的第一行**,分组本身仍在原位;一旦分组排在列表下面,置顶了也看不见。
41
+
42
+ 本插件在**所有工作区分组之上**加一个「置顶」区,把宿主自己的置顶会话(`workspaces.pinnedSessionIds`)集中列出来:
43
+
44
+ - 用的是**宿主原生置顶集合**,不是第二套置顶:会话「…」菜单里的置顶/取消置顶照旧;
45
+ - 置顶区每行显示标题 + 所属工作区名,点行直接打开;hover 出现「取消置顶」;
46
+ - 置顶期间它在原工作区分组里的那一行**隐藏**(不重复出现),取消置顶后原行回到原位;
47
+ - 工作区分组、组内顺序、折叠、颜色全部照旧;
48
+ - 只在按工作区分组时出现;搜索、扁平视图、侧栏收起时自动隐藏。
49
+
38
50
  ## 设置
39
51
 
40
52
  **设置 → 通用**:
41
53
 
42
54
  - **历史折叠天数**:`−` / 数字输入 / `+`(1–90,默认 3),以及“选中 / 运行中 / 等待处理的会话不折叠”;
43
- - **颜色与跟进标记**:颜色标记总开关。
55
+ - **颜色与跟进标记**:颜色标记总开关;
56
+ - **全局置顶区**:显示开关。
44
57
 
45
58
  偏好持久化在 Client store 的 `dsh.ws-recency.prefs.v1`。
46
59
 
@@ -69,7 +82,7 @@ dsh plugin --profile <profile> add github:chyxhtc/dsh-ws-recency
69
82
  或固定版本的预构建包:
70
83
 
71
84
  ```powershell
72
- dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.7.3/dsh-ws-recency-0.7.3.tgz
85
+ dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.8.0/dsh-ws-recency-0.8.0.tgz
73
86
  ```
74
87
 
75
88
  装完把 `dsh-ws-recency` 加进 `~/.hypers-work/profiles/<profile>/package.json` 的 `dsh.profile.bundles`,重启应用。
package/client.js CHANGED
@@ -59,6 +59,8 @@
59
59
  var FOLD_ATTR = "data-wsr-fold";
60
60
  var STATE_ATTR = "data-wsr-state";
61
61
  var DOT_TOKEN = "wsr-dot";
62
+ var PIN_ATTR = "data-wsr-pin";
63
+ var PIN_HIDDEN_ATTR = "data-wsr-pin-hidden";
62
64
  var BUSY_RANK_ATTR = "data-busy-rank";
63
65
  var CSS_ID = "dsh-ws-recency/css";
64
66
  var MENU_ORDER = 460;
@@ -69,7 +71,7 @@
69
71
  var DEFAULT_DAYS = 3;
70
72
  var TICK_MS = 5 * 60 * 1000;
71
73
 
72
- var data = { sessions: null, workspaces: null, settings: null, statuses: null, actions: null, t: null };
74
+ var data = { sessions: null, workspaces: null, settings: null, statuses: null, actions: null, service: null, t: null };
73
75
  var timer = null;
74
76
  var ticker = null;
75
77
  var observer = null;
@@ -79,6 +81,8 @@
79
81
  var labelled = new Set();
80
82
  var toggles = new Set();
81
83
  var dots = new Set();
84
+ var pinHidden = new Set();
85
+ var pinHost = null;
82
86
  var expandedSections = new Set();
83
87
  var clickMarked = new WeakSet();
84
88
  var headerMarked = new WeakSet();
@@ -88,6 +92,16 @@
88
92
  "[" + ROOT_ATTR + '="true"]{display:flex;flex-direction:column}',
89
93
  "[" + ROOT_ATTR + '="true"]>*{flex:0 0 auto}',
90
94
  "[" + HIDDEN_ATTR + '="true"]{display:none !important}',
95
+ "[" + PIN_HIDDEN_ATTR + '="true"]{display:none !important}',
96
+ ".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
+ ".wsr-pin-head{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);padding:4px 12px 2px}",
98
+ ".wsr-pin-row{display:flex;align-items:center;gap:8px;height:30px;padding:0 12px;border-radius:6px;cursor:pointer}",
99
+ ".wsr-pin-row:hover{background:var(--dsw-alias-bg-layer-2)}",
100
+ ".wsr-pin-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;color:var(--dsw-alias-label-primary)}",
101
+ ".wsr-pin-ws{flex:none;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-tertiary)}",
102
+ ".wsr-pin-unpin{flex:none;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:11px;padding:2px 4px;border-radius:4px;visibility:hidden}",
103
+ ".wsr-pin-row:hover .wsr-pin-unpin{visibility:visible}",
104
+ ".wsr-pin-unpin:hover{color:var(--dsw-alias-label-primary)}",
91
105
  ".wsr-toggle{width:100%;height:28px;padding:0 12px 0 calc(28px + var(--dsh-workspace-indent,0px));cursor:pointer;text-align:left;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;font-size:12px}",
92
106
  ".wsr-toggle:hover{color:var(--dsw-alias-label-secondary)}",
93
107
  '[' + STATE_ATTR + '="running"] [class*="_title"]{color:#4f8ef7}',
@@ -114,6 +128,11 @@
114
128
  "color.title": "\u989c\u8272\u4e0e\u8ddf\u8fdb\u6807\u8bb0",
115
129
  "color.description": "\u8fd0\u884c\u4e2d=\u84dd\uff0c\u5b8c\u6210\u672a\u8bfb=\u7eff\uff0c\u624b\u52a8\u6807\u8bb0\u9700\u8981\u8ddf\u8fdb=\u7425\u73c0\u3002\u5728\u4f1a\u8bdd\u300c\u2026\u300d\u83dc\u5355\u91cc\u6807\u8bb0 / \u53d6\u6d88\u3002",
116
130
  "color.enabled": "\u542f\u7528\u989c\u8272\u6807\u8bb0",
131
+ "pin.title": "\u5168\u5c40\u7f6e\u9876\u533a",
132
+ "pin.description": "\u5728\u6240\u6709\u5de5\u4f5c\u533a\u4e4b\u4e0a\u5355\u72ec\u5217\u51fa\u7f6e\u9876\u4f1a\u8bdd\uff1b\u53d6\u6d88\u7f6e\u9876\u540e\u56de\u5230\u5b83\u539f\u6765\u7684\u5de5\u4f5c\u533a\u4f4d\u7f6e\u3002",
133
+ "pin.enabled": "\u663e\u793a\u7f6e\u9876\u533a",
134
+ "pin.head": "\u7f6e\u9876",
135
+ "pin.unpin": "\u53d6\u6d88\u7f6e\u9876",
117
136
  "menu.flag": "\u6807\u8bb0\u9700\u8981\u8ddf\u8fdb",
118
137
  "menu.unflag": "\u53d6\u6d88\u8ddf\u8fdb\u6807\u8bb0",
119
138
  };
@@ -126,6 +145,11 @@
126
145
  "color.title": "Colors and follow-up marks",
127
146
  "color.description": "Running = blue, finished-unopened = green, a session you marked as needing follow-up = amber. Mark it from the session's \"...\" menu.",
128
147
  "color.enabled": "Enable color marks",
148
+ "pin.title": "Global pinned section",
149
+ "pin.description": "Lists pinned sessions in one section above every workspace; unpinning sends a session back to its own group.",
150
+ "pin.enabled": "Show the pinned section",
151
+ "pin.head": "Pinned",
152
+ "pin.unpin": "Unpin",
129
153
  "menu.flag": "Mark as needs follow-up",
130
154
  "menu.unflag": "Clear follow-up mark",
131
155
  };
@@ -168,6 +192,7 @@
168
192
  foldDays: Math.min(MAX_DAYS, Math.max(MIN_DAYS, Math.round(days))),
169
193
  keepActive: settings.keepActive !== false,
170
194
  colors: settings.colors !== false,
195
+ pinnedSection: settings.pinnedSection !== false,
171
196
  followUps: settings.followUps !== null && typeof settings.followUps === "object" ? settings.followUps : {},
172
197
  };
173
198
  }
@@ -489,6 +514,157 @@
489
514
  }
490
515
  }
491
516
 
517
+ function baseName(path) {
518
+ if (typeof path !== "string" || path === "") return "";
519
+ var parts = path.replace(/[\\/]+$/, "").split(/[\\/]/);
520
+ return parts.length === 0 ? path : parts[parts.length - 1];
521
+ }
522
+
523
+ /** sessionId -> owning workspace label, for the trailing tag in the pinned rows. */
524
+ function workspaceLabels(workspaces) {
525
+ var map = {};
526
+ var items = (workspaces !== null && workspaces !== undefined && workspaces.items) || [];
527
+ for (var i = 0; i < items.length; i += 1) {
528
+ var item = items[i];
529
+ var label = (item.title && String(item.title)) || baseName(item.path);
530
+ var ids = item.sessionIds || [];
531
+ for (var j = 0; j < ids.length; j += 1) if (map[ids[j]] === undefined) map[ids[j]] = label;
532
+ }
533
+ return map;
534
+ }
535
+
536
+ /**
537
+ * The sessions the host itself has pinned, newest first.
538
+ *
539
+ * The pins are the host's own set (`workspaces.pinnedSessionIds`), so this is
540
+ * not a second pinning system: pin / unpin from the normal "..." menu and the
541
+ * section follows.
542
+ */
543
+ function pinnedItemsOf(sessions, workspaces, settings) {
544
+ if (!settings.pinnedSection) return [];
545
+ var ids = (workspaces !== null && workspaces !== undefined && workspaces.pinnedSessionIds) || [];
546
+ if (ids.length === 0) return [];
547
+ var byId = sessions.byId || {};
548
+ var archived = new Set((workspaces && workspaces.archivedSessionIds) || []);
549
+ var labels = workspaceLabels(workspaces);
550
+ var out = [];
551
+ for (var i = 0; i < ids.length; i += 1) {
552
+ var session = byId[ids[i]];
553
+ if (session === undefined || session === null) continue;
554
+ if (session.origin === "subagent" || session.blank === true) continue;
555
+ if (archived.has(ids[i])) continue;
556
+ var title = (session.title || "").trim();
557
+ out.push({
558
+ id: ids[i],
559
+ title: title === "" ? ids[i].slice(0, 8) : title,
560
+ where: labels[ids[i]] || "",
561
+ updatedAt: typeof session.updatedAt === "number" ? session.updatedAt : 0,
562
+ });
563
+ }
564
+ out.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
565
+ return out;
566
+ }
567
+
568
+ function pinnedRowElement(item) {
569
+ var row = document.createElement("div");
570
+ row.className = "wsr-pin-row";
571
+ row.setAttribute("data-row-key", "pin:" + item.id);
572
+ var title = document.createElement("span");
573
+ title.className = "wsr-pin-title";
574
+ title.textContent = item.title;
575
+ title.setAttribute("title", item.title);
576
+ row.appendChild(title);
577
+ if (item.where !== "") {
578
+ var where = document.createElement("span");
579
+ where.className = "wsr-pin-ws";
580
+ where.textContent = item.where;
581
+ row.appendChild(where);
582
+ }
583
+ var unpin = document.createElement("button");
584
+ unpin.type = "button";
585
+ unpin.className = "wsr-pin-unpin";
586
+ unpin.textContent = t("pin.unpin");
587
+ unpin.addEventListener("click", function (event) {
588
+ if (event !== undefined && typeof event.stopPropagation === "function") event.stopPropagation();
589
+ if (data.service !== null && typeof data.service.unpinSession === "function") data.service.unpinSession(item.id);
590
+ schedule();
591
+ });
592
+ row.appendChild(unpin);
593
+ row.addEventListener("click", function () {
594
+ if (data.service !== null && typeof data.service.openSession === "function") data.service.openSession(item.id);
595
+ });
596
+ return row;
597
+ }
598
+
599
+ /** Hide the copies inside their own groups so nothing is listed twice. */
600
+ function hideOriginalRows(entries, pinnedIds) {
601
+ for (var i = 0; i < entries.length; i += 1) {
602
+ var rows = rowElements(entries[i].section);
603
+ for (var j = 0; j < rows.length; j += 1) {
604
+ var id = sessionIdOf(rows[j]);
605
+ if (id !== null && pinnedIds.has(id)) {
606
+ if (rows[j].getAttribute(PIN_HIDDEN_ATTR) !== "true") rows[j].setAttribute(PIN_HIDDEN_ATTR, "true");
607
+ pinHidden.add(rows[j]);
608
+ } else if (rows[j].getAttribute(PIN_HIDDEN_ATTR) !== null) {
609
+ rows[j].removeAttribute(PIN_HIDDEN_ATTR);
610
+ pinHidden.delete(rows[j]);
611
+ }
612
+ }
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Render the "pinned" section above every workspace group.
618
+ *
619
+ * It is a plain element inserted as the first child of the list container
620
+ * (flex order -1), rebuilt only when the pinned set or its labels change. The
621
+ * host keeps owning the list; the workspace grouping below is untouched.
622
+ */
623
+ function renderPinned(entries, sessions, workspaces, settings) {
624
+ if (typeof document === "undefined") return;
625
+ var items = pinnedItemsOf(sessions, workspaces, settings);
626
+
627
+ if (items.length === 0) {
628
+ if (pinHost !== null) {
629
+ pinHost.remove();
630
+ pinHost = null;
631
+ }
632
+ hideOriginalRows(entries, new Set());
633
+ return;
634
+ }
635
+ if (entries.length === 0) return;
636
+ var container = entries[0].section.parentElement;
637
+ if (container === null) return;
638
+
639
+ if (pinHost === null || pinHost.parentElement !== container) {
640
+ var existing = container.querySelector("[" + PIN_ATTR + '="true"]');
641
+ if (existing !== null) existing.remove();
642
+ pinHost = document.createElement("div");
643
+ pinHost.className = "wsr-pin";
644
+ pinHost.setAttribute(PIN_ATTR, "true");
645
+ pinHost.style.order = "-1";
646
+ if (typeof container.insertBefore === "function" && container.firstChild !== undefined && container.firstChild !== null) {
647
+ container.insertBefore(pinHost, container.firstChild);
648
+ } else {
649
+ container.appendChild(pinHost);
650
+ }
651
+ }
652
+ pinHost.style.order = "-1";
653
+
654
+ var signature = items.map(function (item) { return item.id + "\u0000" + item.title + "\u0000" + item.where; }).join("\u0001");
655
+ if (pinHost.dataset.wsrSignature !== signature) {
656
+ pinHost.dataset.wsrSignature = signature;
657
+ while (pinHost.firstChild !== undefined && pinHost.firstChild !== null) pinHost.removeChild(pinHost.firstChild);
658
+ var head = document.createElement("div");
659
+ head.className = "wsr-pin-head";
660
+ head.textContent = t("pin.head");
661
+ pinHost.appendChild(head);
662
+ for (var i = 0; i < items.length; i += 1) pinHost.appendChild(pinnedRowElement(items[i]));
663
+ }
664
+ var pinnedIds = new Set(items.map(function (item) { return item.id; }));
665
+ hideOriginalRows(entries, pinnedIds);
666
+ }
667
+
492
668
  function statusOf(statuses, id) {
493
669
  if (statuses === null || statuses === undefined) return null;
494
670
  if (typeof statuses.get === "function") return statuses.get(id) || null;
@@ -560,6 +736,7 @@
560
736
 
561
737
  orderGroups(entries, latestByWorkspace(sessions, workspaces));
562
738
  foldRows(entries, settings, sessions, Date.now());
739
+ renderPinned(entries, sessions, workspaces, settings);
563
740
  if (settings.colors) decorateRows(entries, sessions, data.statuses, settings);
564
741
  }
565
742
 
@@ -700,6 +877,28 @@
700
877
  );
701
878
  }
702
879
 
880
+ /** Settings -> General row: the global pinned section on/off. */
881
+ function PinSectionRow(props) {
882
+ var settings = props.useStore(function (state) { return state; });
883
+ var enabled = settings.pinnedSection !== false;
884
+ var text = props.t("pin.title");
885
+ return h("div", { className: "wsr-setRow" },
886
+ h("div", { className: "wsr-setText" },
887
+ h("div", { className: "wsr-setTitle" }, text),
888
+ h("div", { className: "wsr-setDesc" }, props.t("pin.description")),
889
+ ),
890
+ h("div", { className: "wsr-controls" },
891
+ h("label", { className: "wsr-exempt" },
892
+ h("input", {
893
+ type: "checkbox",
894
+ checked: enabled,
895
+ onChange: function (event) { props.actions.setPinnedSection(event.target.checked); },
896
+ }),
897
+ props.t("pin.enabled")),
898
+ ),
899
+ );
900
+ }
901
+
703
902
  function shutdown() {
704
903
  disposed = true;
705
904
  if (timer !== null) window.clearTimeout(timer);
@@ -723,6 +922,12 @@
723
922
  toggles.clear();
724
923
  dots.forEach(function (dot) { dot.remove(); });
725
924
  dots.clear();
925
+ pinHidden.forEach(function (element) {
926
+ if (element.removeAttribute !== undefined) element.removeAttribute(PIN_HIDDEN_ATTR);
927
+ });
928
+ pinHidden.clear();
929
+ if (pinHost !== null && pinHost.parentElement !== null) pinHost.remove();
930
+ pinHost = null;
726
931
  roots.forEach(function (root) { root.removeAttribute(ROOT_ATTR); });
727
932
  roots.clear();
728
933
  var marked = document.querySelectorAll("[" + STATE_ATTR + "]");
@@ -736,15 +941,17 @@
736
941
 
737
942
  function apply_(ctx) {
738
943
  installCss();
944
+ try { data.service = ctx.get("uiWorkspace") || null; } catch (error) { data.service = null; }
739
945
  var prefs = clientStore.defineStore({
740
946
  init: function () {
741
- return { enabled: true, foldDays: DEFAULT_DAYS, keepActive: true, colors: true, followUps: {} };
947
+ return { enabled: true, foldDays: DEFAULT_DAYS, keepActive: true, colors: true, pinnedSection: true, followUps: {} };
742
948
  },
743
949
  persist: "dsh.ws-recency.prefs.v1",
744
950
  actions: {
745
951
  setEnabled: function (draft, value) { draft.enabled = value === true; },
746
952
  setKeepActive: function (draft, value) { draft.keepActive = value === true; },
747
953
  setColors: function (draft, value) { draft.colors = value === true; },
954
+ setPinnedSection: function (draft, value) { draft.pinnedSection = value === true; },
748
955
  setFoldDays: function (draft, value) {
749
956
  var next = Number(value);
750
957
  if (!Number.isFinite(next)) return;
@@ -807,6 +1014,16 @@
807
1014
  }, ColorsRow);
808
1015
  });
809
1016
 
1017
+ ctx.slots.inject("settings.general.item", function () {
1018
+ return ctx.slots.register({
1019
+ name: "settings.general.item",
1020
+ id: "ws-recency-pins",
1021
+ order: 47,
1022
+ store: prefs,
1023
+ locale: NS,
1024
+ }, PinSectionRow);
1025
+ });
1026
+
810
1027
  ctx.slots.inject("sidebar.workspaces.session.menu.item", function () {
811
1028
  return ctx.slots.register({
812
1029
  name: "sidebar.workspaces.session.menu.item",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-ws-recency",
3
- "version": "0.7.3",
4
- "description": "Sidebar session management for DeepSeek Harness: workspace groups sorted by latest activity, stale history folded, manual follow-up marks.",
3
+ "version": "0.8.0",
4
+ "description": "Sidebar session management for DeepSeek Harness: workspace groups sorted by latest activity, stale history folded, a global pinned section, and manual follow-up marks.",
5
5
  "license": "MIT",
6
6
  "author": "chyxhtc",
7
7
  "repository": {