dsh-ws-recency 0.0.0-stage → 0.7.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.
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/client.js +847 -0
- package/cordis.patch.yml +6 -0
- package/index.js +14 -0
- package/package.json +52 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the dsh-ws-recency contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-ws-recency
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH 侧栏的三件事:**工作区分组按最近活跃排序**、**过期历史自动折叠**、**运行中/未读/需跟进三色标记**。单独装上就有效,不依赖其它插件。
|
|
4
|
+
|
|
5
|
+
## 一、分组排序
|
|
6
|
+
|
|
7
|
+
DSH 已经把**组内**会话按 `updatedAt` 倒序排好了(排序方式 = 最近更新),但**分组自身**用的是工作区注册表的顺序(创建工作区的先后),所以一个刚跑完一堆活的工程可能仍排在侧栏很下面。
|
|
8
|
+
|
|
9
|
+
做法:从 Client store 读每个工作区自己的会话,取组内最大的 `updatedAt`;空工作区回落到该工作区的 `createdAt`;把结果写成各分组元素的 flex `order`。不碰会话行、不碰原生置顶、不碰归档、不改 store。
|
|
10
|
+
|
|
11
|
+
## 二、历史折叠
|
|
12
|
+
|
|
13
|
+
会话超过 `foldDays`(默认 **3 天**)没有更新就折叠起来;整个工作区都过期时,它只剩一行工作区标题——需要翻历史时点开就行。
|
|
14
|
+
|
|
15
|
+
- **每组只有一个控件**:宿主自己还有没渲染的行时,用它的「展开其余 N 条会话」(N 已含被折叠的行);宿主把该组渲染完了时,才出现插件的 `展开历史 N` / `收起历史`;
|
|
16
|
+
- 点宿主的「展开其余」会同时放出被折叠的行;它变成「收起」时,折叠也一并复原;
|
|
17
|
+
- 选中 / 运行中 / 等待处理的会话**永不折叠**(可在设置里关掉这个豁免);
|
|
18
|
+
- 空白草稿行(新会话)不参与折叠判断;
|
|
19
|
+
- 每 5 分钟自动重算一次,所以跨过 3 天边界的会话会自己折叠,不用刷新。
|
|
20
|
+
|
|
21
|
+
## 三、颜色与“需要跟进”
|
|
22
|
+
|
|
23
|
+
自动那部分只做能读准的:运行中(蓝)、完成未读(绿)。**“看过但没决策”不猜**——DSH 只有 `completionUnread` 一个布尔,点开就清零,猜不准反而制造噪音。
|
|
24
|
+
|
|
25
|
+
改成反向标记,由你自己打:
|
|
26
|
+
|
|
27
|
+
| 状态 | 颜色 | 怎么产生 |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| 运行中 | 蓝 | 宿主状态 |
|
|
30
|
+
| 完成未读 | 绿 | 宿主状态 |
|
|
31
|
+
| **需要跟进** | 琥珀 | 你在会话「…」菜单点「标记需要跟进」 |
|
|
32
|
+
|
|
33
|
+
- 标记存在 Client store 的 `followUps`(随 `dsh.ws-recency.prefs.v1` 持久化),重启后还在;
|
|
34
|
+
- 琥珀圆点渲染在宿主自己的 `sidebar.session.row.leading` 槽里,**不往行里塞子元素**——塞进去会让后面的行整体下移一格;点圆点即取消标记;
|
|
35
|
+
- 标记后该会话**又有新活动**(`updatedAt` 前进)→ 自动视为已跟进,标记消失;
|
|
36
|
+
- 不受 20 秒、不受打开时长影响——你标记就一直是琥珀,直到你取消或它又有新活动。
|
|
37
|
+
|
|
38
|
+
## 设置
|
|
39
|
+
|
|
40
|
+
**设置 → 通用**:
|
|
41
|
+
|
|
42
|
+
- **历史折叠天数**:`−` / 数字输入 / `+`(1–90,默认 3),以及“选中 / 运行中 / 等待处理的会话不折叠”;
|
|
43
|
+
- **颜色与跟进标记**:颜色标记总开关。
|
|
44
|
+
|
|
45
|
+
偏好持久化在 Client store 的 `dsh.ws-recency.prefs.v1`。
|
|
46
|
+
|
|
47
|
+
## 已知边界
|
|
48
|
+
|
|
49
|
+
- 依赖宿主 DOM:分组容器带 `_groupSection`、分组头带 `data-row-key="workspace:<id>"`、行带 `data-row-key="session:<id>"`、溢出按钮类名含 `sessionOverflowButton`。宿主改结构就要跟着改。
|
|
50
|
+
- 折叠只能作用于宿主**已经渲染**的行;更早的行仍在宿主自己的「展开其余」之后。
|
|
51
|
+
- “未分组”桶永远排在真实工作区之后。
|
|
52
|
+
- 只在按工作区分组(含工作区树顶层)时生效。
|
|
53
|
+
- **可选互操作**:如果你另外装着 `dsh-plugin-busy-workspace`,本插件会尊重它写在分组上的 `data-busy-rank`,把它的紧急度当主键、档位内部再按活跃度排。没装就是纯最近活跃排序——本插件不依赖它。
|
|
54
|
+
|
|
55
|
+
## 安装
|
|
56
|
+
|
|
57
|
+
npm(推荐,升级最简单):
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
dsh plugin --profile <profile> add dsh-ws-recency
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
或从 GitHub 直装:
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
dsh plugin --profile <profile> add github:chyxhtc/dsh-ws-recency
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
或固定版本的预构建包:
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
dsh plugin --profile <profile> add https://github.com/chyxhtc/dsh-ws-recency/releases/download/v0.7.2/dsh-ws-recency-0.7.2.tgz
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
装完把 `dsh-ws-recency` 加进 `~/.hypers-work/profiles/<profile>/package.json` 的 `dsh.profile.bundles`,重启应用。
|
|
76
|
+
|
|
77
|
+
本地开发可以直接 `link:`:
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
dsh plugin --profile desktop add "link:C:\Users\Konka\Hypers Work\workspace\dsh-ws-recency"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
自检:`node tests/selfcheck.cjs`(桩 DOM,验证排序、按天数折叠、分组控件展开、宿主按钮计数、三色与跟进标记)。
|
package/client.js
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ws-recency - client half.
|
|
3
|
+
*
|
|
4
|
+
* Three sidebar behaviors, nothing else:
|
|
5
|
+
*
|
|
6
|
+
* 1. GROUP ORDER - DSH already sorts the sessions INSIDE a workspace group by
|
|
7
|
+
* `updatedAt` (orderBy: "updated"), but the groups themselves follow the
|
|
8
|
+
* workspace registry order (creation order). This plugin ranks every group
|
|
9
|
+
* by the freshest `updatedAt` among its own sessions and writes a flex
|
|
10
|
+
* `order` onto it. Native pins, archive state and the store are untouched.
|
|
11
|
+
* OPTIONAL interop: when dsh-plugin-busy-workspace happens to be installed
|
|
12
|
+
* it writes `data-busy-rank` (its urgency band) on the same elements; that
|
|
13
|
+
* band is then kept as the primary key and recency only decides the order
|
|
14
|
+
* inside one band. This plugin never depends on it.
|
|
15
|
+
*
|
|
16
|
+
* 2. HISTORY FOLD - a session that has not been updated for `foldDays` (default
|
|
17
|
+
* 3) is folded away, and a workspace whose own freshest session is older than
|
|
18
|
+
* that shows as a bare header. A small "show history (n)" control on the
|
|
19
|
+
* group reveals the folded rows, and the host's own "show more" button keeps
|
|
20
|
+
* working for rows it did not render. The selected, running and waiting rows
|
|
21
|
+
* are never folded.
|
|
22
|
+
*
|
|
23
|
+
* 3. COLORS - running = blue, finished-unopened = green, and a MANUAL
|
|
24
|
+
* "needs follow-up" flag = amber. DSH only exposes `completionUnread`, which
|
|
25
|
+
* clears the moment a session is opened, so "I looked but never decided" is
|
|
26
|
+
* not readable anywhere: this plugin does not guess it. You mark it yourself
|
|
27
|
+
* from the session's "..." menu (or click the amber dot to drop it), exactly
|
|
28
|
+
* the way you would remember a conversation you still owe an answer to.
|
|
29
|
+
*
|
|
30
|
+
* Settings (Settings -> General) persist in the shared Client store.
|
|
31
|
+
*/
|
|
32
|
+
(() => {
|
|
33
|
+
try {
|
|
34
|
+
window.__ModuleLoader__.load({
|
|
35
|
+
id: "dsh-ws-recency",
|
|
36
|
+
factory: (require) => {
|
|
37
|
+
var module = { exports: {} };
|
|
38
|
+
var exports = module.exports;
|
|
39
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
40
|
+
|
|
41
|
+
var react = require("react");
|
|
42
|
+
var clientStore = (function () {
|
|
43
|
+
try { return require("@deepseek-ai/dsh-client-store"); } catch (error) { return {}; }
|
|
44
|
+
})();
|
|
45
|
+
var ui = (function () {
|
|
46
|
+
try { return require("@deepseek-ai/dsh-client-ui-primitives"); } catch (error) { return {}; }
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
var NS = "ws-recency";
|
|
50
|
+
var h = react.createElement;
|
|
51
|
+
|
|
52
|
+
var HEADER_PREFIX = "workspace:";
|
|
53
|
+
var SESSION_PREFIX = "session:";
|
|
54
|
+
var SECTION_TOKEN = "_groupSection";
|
|
55
|
+
var OVERFLOW_TOKEN = "sessionOverflowButton";
|
|
56
|
+
var ROOT_ATTR = "data-wsr-root";
|
|
57
|
+
var RANK_ATTR = "data-wsr-order";
|
|
58
|
+
var HIDDEN_ATTR = "data-wsr-hidden";
|
|
59
|
+
var FOLD_ATTR = "data-wsr-fold";
|
|
60
|
+
var STATE_ATTR = "data-wsr-state";
|
|
61
|
+
var DOT_TOKEN = "wsr-dot";
|
|
62
|
+
var BUSY_RANK_ATTR = "data-busy-rank";
|
|
63
|
+
var CSS_ID = "dsh-ws-recency/css";
|
|
64
|
+
var MENU_ORDER = 460;
|
|
65
|
+
|
|
66
|
+
var DAY = 24 * 60 * 60 * 1000;
|
|
67
|
+
var MIN_DAYS = 1;
|
|
68
|
+
var MAX_DAYS = 90;
|
|
69
|
+
var DEFAULT_DAYS = 3;
|
|
70
|
+
var TICK_MS = 5 * 60 * 1000;
|
|
71
|
+
|
|
72
|
+
var data = { sessions: null, workspaces: null, settings: null, statuses: null, actions: null, t: null };
|
|
73
|
+
var timer = null;
|
|
74
|
+
var ticker = null;
|
|
75
|
+
var observer = null;
|
|
76
|
+
var written = new Set();
|
|
77
|
+
var roots = new Set();
|
|
78
|
+
var hidden = new Set();
|
|
79
|
+
var labelled = new Set();
|
|
80
|
+
var toggles = new Set();
|
|
81
|
+
var dots = new Set();
|
|
82
|
+
var expandedSections = new Set();
|
|
83
|
+
var clickMarked = new WeakSet();
|
|
84
|
+
var headerMarked = new WeakSet();
|
|
85
|
+
var disposed = false;
|
|
86
|
+
|
|
87
|
+
var CSS = [
|
|
88
|
+
"[" + ROOT_ATTR + '="true"]{display:flex;flex-direction:column}',
|
|
89
|
+
"[" + ROOT_ATTR + '="true"]>*{flex:0 0 auto}',
|
|
90
|
+
"[" + HIDDEN_ATTR + '="true"]{display:none !important}',
|
|
91
|
+
".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
|
+
".wsr-toggle:hover{color:var(--dsw-alias-label-secondary)}",
|
|
93
|
+
'[' + STATE_ATTR + '="running"] [class*="_title"]{color:#4f8ef7}',
|
|
94
|
+
'[' + STATE_ATTR + '="unread"] [class*="_title"]{color:#3fae6b}',
|
|
95
|
+
'[' + STATE_ATTR + '="followup"] [class*="_title"]{color:#d99a2b}',
|
|
96
|
+
".wsr-dot{display:block;width:7px;height:7px;border-radius:50%;margin:0 auto;background:#d99a2b;cursor:pointer}",
|
|
97
|
+
".wsr-dot:hover{outline:2px solid var(--dsw-alias-border-l2)}",
|
|
98
|
+
".wsr-setRow{display:flex;align-items:center;gap:16px;justify-content:space-between;padding:12px 0}",
|
|
99
|
+
".wsr-setText{min-width:0}",
|
|
100
|
+
".wsr-setTitle{font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary)}",
|
|
101
|
+
".wsr-setDesc{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);margin-top:2px}",
|
|
102
|
+
".wsr-controls{display:flex;align-items:center;gap:8px;flex:none}",
|
|
103
|
+
".wsr-step{width:24px;height:24px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:inherit;cursor:pointer;font-size:14px;line-height:1}",
|
|
104
|
+
".wsr-input{width:48px;height:24px;text-align:center;border-radius:6px;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:inherit;font-size:13px}",
|
|
105
|
+
".wsr-exempt{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);margin-left:8px}",
|
|
106
|
+
].join("");
|
|
107
|
+
|
|
108
|
+
var ZH = {
|
|
109
|
+
"settings.title": "\u5386\u53f2\u6298\u53e0\u5929\u6570",
|
|
110
|
+
"settings.description": "\u4f1a\u8bdd\u8d85\u8fc7\u8fd9\u4e48\u591a\u5929\u6ca1\u6709\u66f4\u65b0\u5c31\u6298\u53e0\uff1b\u6574\u4e2a\u5de5\u4f5c\u533a\u90fd\u8fc7\u671f\u65f6\u53ea\u5269\u5de5\u4f5c\u533a\u6807\u9898\u3002",
|
|
111
|
+
"settings.exempt": "\u9009\u4e2d / \u8fd0\u884c\u4e2d / \u7b49\u5f85\u5904\u7406\u7684\u4f1a\u8bdd\u4e0d\u6298\u53e0",
|
|
112
|
+
"fold.show": "\u5c55\u5f00\u5386\u53f2 {n}",
|
|
113
|
+
"fold.hide": "\u6536\u8d77\u5386\u53f2",
|
|
114
|
+
"color.title": "\u989c\u8272\u4e0e\u8ddf\u8fdb\u6807\u8bb0",
|
|
115
|
+
"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
|
+
"color.enabled": "\u542f\u7528\u989c\u8272\u6807\u8bb0",
|
|
117
|
+
"menu.flag": "\u6807\u8bb0\u9700\u8981\u8ddf\u8fdb",
|
|
118
|
+
"menu.unflag": "\u53d6\u6d88\u8ddf\u8fdb\u6807\u8bb0",
|
|
119
|
+
};
|
|
120
|
+
var EN = {
|
|
121
|
+
"settings.title": "Fold history after (days)",
|
|
122
|
+
"settings.description": "Sessions with no update for this many days are folded; a workspace older than that shows as a bare header.",
|
|
123
|
+
"settings.exempt": "Never fold the selected / running / waiting session",
|
|
124
|
+
"fold.show": "Show history ({n})",
|
|
125
|
+
"fold.hide": "Hide history",
|
|
126
|
+
"color.title": "Colors and follow-up marks",
|
|
127
|
+
"color.description": "Running = blue, finished-unopened = green, a session you marked as needing follow-up = amber. Mark it from the session's \"...\" menu.",
|
|
128
|
+
"color.enabled": "Enable color marks",
|
|
129
|
+
"menu.flag": "Mark as needs follow-up",
|
|
130
|
+
"menu.unflag": "Clear follow-up mark",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
var FALLBACK = { "fold.show": "Show history ({n})", "fold.hide": "Hide history" };
|
|
134
|
+
|
|
135
|
+
function t(key, params) {
|
|
136
|
+
if (typeof data.t === "function") {
|
|
137
|
+
try { return data.t(key, params); } catch (error) { /* fall back below */ }
|
|
138
|
+
}
|
|
139
|
+
var text = FALLBACK[key] || key;
|
|
140
|
+
if (params !== undefined && params.n !== undefined) text = text.replace("{n}", String(params.n));
|
|
141
|
+
return text;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function top() {
|
|
145
|
+
return typeof document !== "undefined" && document.body !== null ? document.body : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function log(message, detail) {
|
|
149
|
+
if (window.__DSH_WSR_DEBUG__ === true) console.debug("[dsh-ws-recency] " + message, detail === undefined ? "" : detail);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function installCss() {
|
|
153
|
+
if (typeof document === "undefined") return;
|
|
154
|
+
if (document.querySelector('style[data-plugin-css="' + CSS_ID + '"]') !== null) return;
|
|
155
|
+
var tag = document.createElement("style");
|
|
156
|
+
tag.dataset.plugin = "dsh-ws-recency";
|
|
157
|
+
tag.dataset.pluginCss = CSS_ID;
|
|
158
|
+
tag.textContent = CSS;
|
|
159
|
+
document.head.appendChild(tag);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function settingsOf() {
|
|
163
|
+
var settings = data.settings || {};
|
|
164
|
+
var days = Number(settings.foldDays);
|
|
165
|
+
if (!Number.isFinite(days)) days = DEFAULT_DAYS;
|
|
166
|
+
return {
|
|
167
|
+
enabled: settings.enabled !== false,
|
|
168
|
+
foldDays: Math.min(MAX_DAYS, Math.max(MIN_DAYS, Math.round(days))),
|
|
169
|
+
keepActive: settings.keepActive !== false,
|
|
170
|
+
colors: settings.colors !== false,
|
|
171
|
+
followUps: settings.followUps !== null && typeof settings.followUps === "object" ? settings.followUps : {},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function latestByWorkspace(sessions, workspaces) {
|
|
176
|
+
var map = new Map();
|
|
177
|
+
if (sessions === null || workspaces === null) return map;
|
|
178
|
+
var byId = sessions.byId || {};
|
|
179
|
+
var items = workspaces.items || [];
|
|
180
|
+
var archived = new Set(workspaces.archivedSessionIds || []);
|
|
181
|
+
for (var i = 0; i < items.length; i += 1) {
|
|
182
|
+
var workspace = items[i];
|
|
183
|
+
var ids = workspace.sessionIds || [];
|
|
184
|
+
var latest = 0;
|
|
185
|
+
for (var j = 0; j < ids.length; j += 1) {
|
|
186
|
+
var session = byId[ids[j]];
|
|
187
|
+
if (session === undefined || session === null) continue;
|
|
188
|
+
if (session.blank === true) continue;
|
|
189
|
+
if (session.origin === "subagent") continue;
|
|
190
|
+
if (archived.has(session.id)) continue;
|
|
191
|
+
var updated = typeof session.updatedAt === "number" ? session.updatedAt : 0;
|
|
192
|
+
if (updated > latest) latest = updated;
|
|
193
|
+
}
|
|
194
|
+
map.set(workspace.workspaceId, latest);
|
|
195
|
+
}
|
|
196
|
+
return map;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sectionOf(header) {
|
|
200
|
+
var current = header.parentElement;
|
|
201
|
+
while (current !== null) {
|
|
202
|
+
var className = typeof current.className === "string" ? current.className : "";
|
|
203
|
+
if (className.indexOf(SECTION_TOKEN) !== -1) return current;
|
|
204
|
+
current = current.parentElement;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function collect() {
|
|
210
|
+
var out = [];
|
|
211
|
+
var headers = document.querySelectorAll('[data-row-key^="' + HEADER_PREFIX + '"]');
|
|
212
|
+
for (var i = 0; i < headers.length; i += 1) {
|
|
213
|
+
var header = headers[i];
|
|
214
|
+
var section = sectionOf(header);
|
|
215
|
+
if (section === null) continue;
|
|
216
|
+
var key = header.getAttribute("data-row-key") || "";
|
|
217
|
+
out.push({
|
|
218
|
+
header: header,
|
|
219
|
+
section: section,
|
|
220
|
+
key: key,
|
|
221
|
+
id: key.slice(HEADER_PREFIX.length),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function busyRankOf(section) {
|
|
228
|
+
var raw = section.getAttribute(BUSY_RANK_ATTR);
|
|
229
|
+
if (raw === null || raw === "") return 0;
|
|
230
|
+
var value = Number(raw);
|
|
231
|
+
return Number.isFinite(value) ? value : 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function ready(snapshot) {
|
|
235
|
+
if (snapshot === null || snapshot === undefined) return false;
|
|
236
|
+
return snapshot.phase === undefined || snapshot.phase === "ready";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function orderGroups(entries, latest) {
|
|
240
|
+
var byParent = new Map();
|
|
241
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
242
|
+
var parent = entries[i].section.parentElement;
|
|
243
|
+
if (parent === null) continue;
|
|
244
|
+
var list = byParent.get(parent);
|
|
245
|
+
if (list === undefined) {
|
|
246
|
+
list = [];
|
|
247
|
+
byParent.set(parent, list);
|
|
248
|
+
}
|
|
249
|
+
list.push(entries[i]);
|
|
250
|
+
}
|
|
251
|
+
byParent.forEach(function (list, parent) {
|
|
252
|
+
if (list.length < 2) return;
|
|
253
|
+
if (parent.getAttribute(ROOT_ATTR) !== "true") {
|
|
254
|
+
parent.setAttribute(ROOT_ATTR, "true");
|
|
255
|
+
roots.add(parent);
|
|
256
|
+
}
|
|
257
|
+
list.sort(function (a, b) {
|
|
258
|
+
var ua = a.id === "" ? 1 : 0;
|
|
259
|
+
var ub = b.id === "" ? 1 : 0;
|
|
260
|
+
if (ua !== ub) return ua - ub;
|
|
261
|
+
var ra = busyRankOf(a.section);
|
|
262
|
+
var rb = busyRankOf(b.section);
|
|
263
|
+
if (ra !== rb) return rb - ra;
|
|
264
|
+
var la = a.id === "" ? -1 : latest.get(a.id) || 0;
|
|
265
|
+
var lb = b.id === "" ? -1 : latest.get(b.id) || 0;
|
|
266
|
+
return lb - la;
|
|
267
|
+
});
|
|
268
|
+
for (var k = 0; k < list.length; k += 1) {
|
|
269
|
+
var section = list[k].section;
|
|
270
|
+
var value = String(k);
|
|
271
|
+
if (section.style.order !== value) section.style.order = value;
|
|
272
|
+
if (section.getAttribute(RANK_ATTR) !== value) section.setAttribute(RANK_ATTR, value);
|
|
273
|
+
written.add(section);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Direct children of a group that carry a Session row (the host wraps some). */
|
|
279
|
+
function rowElements(section) {
|
|
280
|
+
var out = [];
|
|
281
|
+
var children = section.children || [];
|
|
282
|
+
for (var i = 0; i < children.length; i += 1) {
|
|
283
|
+
var child = children[i];
|
|
284
|
+
if (child.getAttribute !== undefined && child.getAttribute("role") === "group") continue;
|
|
285
|
+
if (child.tagName === "BUTTON") continue;
|
|
286
|
+
if (child.dataset !== undefined && child.dataset !== null && child.dataset.dshUxPlus !== undefined) continue;
|
|
287
|
+
var own = child.getAttribute !== undefined ? child.getAttribute("data-row-key") : null;
|
|
288
|
+
if (typeof own === "string" && own.indexOf(SESSION_PREFIX) === 0) {
|
|
289
|
+
out.push(child);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (typeof child.querySelector === "function" && child.querySelector('[data-row-key^="' + SESSION_PREFIX + '"]') !== null) out.push(child);
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function overflowButtonOf(section) {
|
|
298
|
+
if (typeof section.querySelector !== "function") return null;
|
|
299
|
+
return section.querySelector('button[class*="' + OVERFLOW_TOKEN + '"]');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isActiveRow(row) {
|
|
303
|
+
if (typeof row.querySelector !== "function") return false;
|
|
304
|
+
return row.querySelector('[data-state="ongoing"], [data-state="warning"]') !== null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isSelectedRow(row) {
|
|
308
|
+
if (typeof row.getAttribute !== "function") return false;
|
|
309
|
+
return row.getAttribute("aria-selected") === "true";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function sessionIdOf(row) {
|
|
313
|
+
var key = typeof row.getAttribute === "function" ? row.getAttribute("data-row-key") : null;
|
|
314
|
+
if (typeof key !== "string" || key.indexOf(SESSION_PREFIX) !== 0) {
|
|
315
|
+
if (typeof row.querySelector === "function") {
|
|
316
|
+
var inner = row.querySelector('[data-row-key^="' + SESSION_PREFIX + '"]');
|
|
317
|
+
if (inner !== null) key = inner.getAttribute("data-row-key");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return typeof key === "string" && key.indexOf(SESSION_PREFIX) === 0 ? key.slice(SESSION_PREFIX.length) : null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function updatedAtOf(row, sessions) {
|
|
324
|
+
var id = sessionIdOf(row);
|
|
325
|
+
if (id === null || sessions === null) return null;
|
|
326
|
+
var session = (sessions.byId || {})[id];
|
|
327
|
+
if (session === undefined || session === null) return null;
|
|
328
|
+
return typeof session.updatedAt === "number" ? session.updatedAt : null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The provisional "New session" row. The host always renders it, and folding
|
|
333
|
+
* it would remove the very input row you are typing into.
|
|
334
|
+
*/
|
|
335
|
+
function isBlankRow(row, sessions) {
|
|
336
|
+
if (sessions === null || sessions === undefined) return false;
|
|
337
|
+
var id = sessionIdOf(row);
|
|
338
|
+
if (id === null) return false;
|
|
339
|
+
var session = (sessions.byId || {})[id];
|
|
340
|
+
return session !== undefined && session !== null && session.blank === true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Rewrite the host button's count so it reports what is really hidden: the
|
|
345
|
+
* host counts only the rows it did not render, and the fold hides a few more.
|
|
346
|
+
*/
|
|
347
|
+
function rewriteLabel(button, extraHidden) {
|
|
348
|
+
if (button === null || button === undefined) return;
|
|
349
|
+
if (typeof button.textContent !== "string" || button.dataset === undefined) return;
|
|
350
|
+
var base = button.dataset.wsrBase;
|
|
351
|
+
if (typeof base !== "string") {
|
|
352
|
+
base = button.textContent;
|
|
353
|
+
button.dataset.wsrBase = base;
|
|
354
|
+
labelled.add(button);
|
|
355
|
+
}
|
|
356
|
+
var match = /\d+/.exec(base);
|
|
357
|
+
if (match === null) return;
|
|
358
|
+
var hostCount = Number(match[0]);
|
|
359
|
+
if (!Number.isFinite(hostCount)) return;
|
|
360
|
+
var total = hostCount + extraHidden;
|
|
361
|
+
var text = total <= 0 ? base : base.slice(0, match.index) + String(total) + base.slice(match.index + match[0].length);
|
|
362
|
+
if (button.textContent !== text) button.textContent = text;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function releaseLabel(button) {
|
|
366
|
+
if (button === null || button === undefined || button.dataset === undefined) return;
|
|
367
|
+
var base = button.dataset.wsrBase;
|
|
368
|
+
if (typeof base === "string") {
|
|
369
|
+
if (button.textContent !== base) button.textContent = base;
|
|
370
|
+
delete button.dataset.wsrBase;
|
|
371
|
+
}
|
|
372
|
+
labelled.delete(button);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function markButton(button, key) {
|
|
376
|
+
if (typeof button.addEventListener !== "function" || clickMarked.has(button)) return;
|
|
377
|
+
clickMarked.add(button);
|
|
378
|
+
button.addEventListener("click", function () {
|
|
379
|
+
if (disposed) return;
|
|
380
|
+
/* The host's own control is the only one while it exists, so it has to
|
|
381
|
+
carry the fold too: `aria-expanded` still reads the pre-click state
|
|
382
|
+
(React delegates at the root, this listener runs first), which is
|
|
383
|
+
exactly what decides whether the click reveals or hides. */
|
|
384
|
+
var expanded = typeof button.getAttribute === "function"
|
|
385
|
+
&& button.getAttribute("aria-expanded") === "true";
|
|
386
|
+
if (expanded) expandedSections.delete(key);
|
|
387
|
+
else expandedSections.add(key);
|
|
388
|
+
schedule();
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function markHeader(header, key) {
|
|
393
|
+
if (typeof header.addEventListener !== "function" || headerMarked.has(header)) return;
|
|
394
|
+
headerMarked.add(header);
|
|
395
|
+
header.addEventListener("click", function () {
|
|
396
|
+
if (disposed) return;
|
|
397
|
+
expandedSections.delete(key);
|
|
398
|
+
schedule();
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** The group's own "show history" control, created only when something is folded. */
|
|
403
|
+
function ensureToggle(section, key, label, onClick) {
|
|
404
|
+
var button = section.querySelector('[data-wsr-fold="true"]');
|
|
405
|
+
if (button === null) {
|
|
406
|
+
button = document.createElement("button");
|
|
407
|
+
button.type = "button";
|
|
408
|
+
button.className = "wsr-toggle";
|
|
409
|
+
button.setAttribute(FOLD_ATTR, "true");
|
|
410
|
+
button.addEventListener("click", function (event) {
|
|
411
|
+
if (disposed) return;
|
|
412
|
+
if (event !== undefined && typeof event.stopPropagation === "function") event.stopPropagation();
|
|
413
|
+
onClick();
|
|
414
|
+
});
|
|
415
|
+
section.appendChild(button);
|
|
416
|
+
toggles.add(button);
|
|
417
|
+
}
|
|
418
|
+
if (button.textContent !== label) button.textContent = label;
|
|
419
|
+
return button;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function dropToggle(section) {
|
|
423
|
+
var button = section.querySelector('[data-wsr-fold="true"]');
|
|
424
|
+
if (button !== null) {
|
|
425
|
+
toggles.delete(button);
|
|
426
|
+
button.remove();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Fold the rows the host rendered that have not changed for `foldDays`, and
|
|
432
|
+
* offer the group's own control to bring them back. A workspace whose every
|
|
433
|
+
* session is that old therefore renders as a bare header.
|
|
434
|
+
*/
|
|
435
|
+
function foldRows(entries, settings, sessions, now) {
|
|
436
|
+
var cutoff = now - settings.foldDays * DAY;
|
|
437
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
438
|
+
var entry = entries[i];
|
|
439
|
+
var section = entry.section;
|
|
440
|
+
var rows = rowElements(section);
|
|
441
|
+
var hostButton = overflowButtonOf(section);
|
|
442
|
+
markHeader(entry.header, entry.key);
|
|
443
|
+
if (hostButton !== null) markButton(hostButton, entry.key);
|
|
444
|
+
releaseLabel(hostButton);
|
|
445
|
+
|
|
446
|
+
var expanded = expandedSections.has(entry.key);
|
|
447
|
+
var folded = 0;
|
|
448
|
+
for (var j = 0; j < rows.length; j += 1) {
|
|
449
|
+
var row = rows[j];
|
|
450
|
+
var updated = updatedAtOf(row, sessions);
|
|
451
|
+
var stale = updated !== null && updated < cutoff;
|
|
452
|
+
var blank = isBlankRow(row, sessions);
|
|
453
|
+
var exempt = settings.keepActive && (isActiveRow(row) || isSelectedRow(row));
|
|
454
|
+
var hide = !expanded && stale && !exempt && !blank;
|
|
455
|
+
if (hide) {
|
|
456
|
+
if (row.getAttribute(HIDDEN_ATTR) !== "true") row.setAttribute(HIDDEN_ATTR, "true");
|
|
457
|
+
hidden.add(row);
|
|
458
|
+
folded += 1;
|
|
459
|
+
} else {
|
|
460
|
+
if (row.getAttribute(HIDDEN_ATTR) !== null) row.removeAttribute(HIDDEN_ATTR);
|
|
461
|
+
hidden.delete(row);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* ONE control per group: while the host offers its own "show more"
|
|
466
|
+
button, that button is the control (its count already carries the
|
|
467
|
+
folded rows below). Only a group the host finished rendering gets
|
|
468
|
+
this plugin's own toggle. */
|
|
469
|
+
if (hostButton !== null) {
|
|
470
|
+
dropToggle(section);
|
|
471
|
+
rewriteLabel(hostButton, expanded ? 0 : folded);
|
|
472
|
+
} else if (expanded) {
|
|
473
|
+
(function (entryKey) {
|
|
474
|
+
ensureToggle(section, entryKey, t("fold.hide"), function () {
|
|
475
|
+
expandedSections.delete(entryKey);
|
|
476
|
+
schedule();
|
|
477
|
+
});
|
|
478
|
+
})(entry.key);
|
|
479
|
+
} else if (folded > 0) {
|
|
480
|
+
(function (entryKey) {
|
|
481
|
+
ensureToggle(section, entryKey, t("fold.show", { n: folded }), function () {
|
|
482
|
+
expandedSections.add(entryKey);
|
|
483
|
+
schedule();
|
|
484
|
+
});
|
|
485
|
+
})(entry.key);
|
|
486
|
+
} else {
|
|
487
|
+
dropToggle(section);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function statusOf(statuses, id) {
|
|
493
|
+
if (statuses === null || statuses === undefined) return null;
|
|
494
|
+
if (typeof statuses.get === "function") return statuses.get(id) || null;
|
|
495
|
+
return statuses[id] || null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Color one rendered row. The state lives on the row element only: the
|
|
500
|
+
* follow-up dot is NOT injected here. A row is a flex/grid layout the host
|
|
501
|
+
* owns, and an extra child shifted every following row down by one, so the
|
|
502
|
+
* dot is rendered through the host's own `sidebar.session.row.leading`
|
|
503
|
+
* slot instead (see FollowUpDot).
|
|
504
|
+
*/
|
|
505
|
+
function setRowState(row, state) {
|
|
506
|
+
if (state === null || state === undefined) {
|
|
507
|
+
if (row.getAttribute(STATE_ATTR) !== null) row.removeAttribute(STATE_ATTR);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (row.getAttribute(STATE_ATTR) !== state) row.setAttribute(STATE_ATTR, state);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Color the rows: running (blue), finished-unopened (green), and the manual
|
|
515
|
+
* "needs follow-up" flag (amber). The flag is the user's own judgement - it
|
|
516
|
+
* is never inferred, and it is dropped automatically once the session has
|
|
517
|
+
* produced newer activity than the moment it was flagged.
|
|
518
|
+
*/
|
|
519
|
+
function decorateRows(entries, sessions, statuses, settings) {
|
|
520
|
+
var flags = settings.followUps;
|
|
521
|
+
var byId = sessions.byId || {};
|
|
522
|
+
var states = {};
|
|
523
|
+
|
|
524
|
+
for (var id in byId) {
|
|
525
|
+
if (!Object.prototype.hasOwnProperty.call(byId, id)) continue;
|
|
526
|
+
var session = byId[id];
|
|
527
|
+
if (session === null || session === undefined) continue;
|
|
528
|
+
if (session.blank === true || session.origin === "subagent") continue;
|
|
529
|
+
var status = statusOf(statuses, id);
|
|
530
|
+
var running = status !== null ? status.running === true : session.running === true;
|
|
531
|
+
var unread = status !== null ? status.completionUnread === true : session.completed === true;
|
|
532
|
+
var flaggedAt = flags[id];
|
|
533
|
+
if (typeof flaggedAt === "number" && typeof session.updatedAt === "number" && session.updatedAt > flaggedAt) {
|
|
534
|
+
/* The session moved on after the mark: the follow-up happened. */
|
|
535
|
+
if (data.actions !== null && typeof data.actions.clearFollowUp === "function") data.actions.clearFollowUp(id);
|
|
536
|
+
flaggedAt = undefined;
|
|
537
|
+
}
|
|
538
|
+
states[id] = running ? "running" : unread ? "unread" : flaggedAt !== undefined ? "followup" : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
542
|
+
var rows = rowElements(entries[i].section);
|
|
543
|
+
for (var j = 0; j < rows.length; j += 1) {
|
|
544
|
+
var row = rows[j];
|
|
545
|
+
var sessionId = sessionIdOf(row);
|
|
546
|
+
setRowState(row, sessionId === null ? null : states[sessionId] || null);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function apply() {
|
|
552
|
+
if (disposed) return;
|
|
553
|
+
var sessions = data.sessions;
|
|
554
|
+
var workspaces = data.workspaces;
|
|
555
|
+
if (!ready(sessions) || !ready(workspaces)) return;
|
|
556
|
+
|
|
557
|
+
var settings = settingsOf();
|
|
558
|
+
var entries = collect();
|
|
559
|
+
if (entries.length === 0) return;
|
|
560
|
+
|
|
561
|
+
orderGroups(entries, latestByWorkspace(sessions, workspaces));
|
|
562
|
+
foldRows(entries, settings, sessions, Date.now());
|
|
563
|
+
if (settings.colors) decorateRows(entries, sessions, data.statuses, settings);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function schedule() {
|
|
567
|
+
if (disposed || timer !== null) return;
|
|
568
|
+
timer = window.setTimeout(function () {
|
|
569
|
+
timer = null;
|
|
570
|
+
try {
|
|
571
|
+
apply();
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.warn("[dsh-ws-recency] pass failed:", error);
|
|
574
|
+
}
|
|
575
|
+
}, 0);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** `shell.overlay` occupant; it renders no chrome of its own. */
|
|
579
|
+
function Sorter(props) {
|
|
580
|
+
if (typeof props.useSessions === "function") {
|
|
581
|
+
data.sessions = props.useSessions(function (state) { return state; });
|
|
582
|
+
}
|
|
583
|
+
if (typeof props.useWorkspaces === "function") {
|
|
584
|
+
data.workspaces = props.useWorkspaces(function (state) { return state; });
|
|
585
|
+
}
|
|
586
|
+
if (typeof props.useSessionStatus === "function") {
|
|
587
|
+
data.statuses = props.useSessionStatus(function (state) { return state; });
|
|
588
|
+
}
|
|
589
|
+
if (typeof props.useStore === "function") {
|
|
590
|
+
data.settings = props.useStore(function (state) { return state; });
|
|
591
|
+
}
|
|
592
|
+
if (props.actions !== undefined && props.actions !== null) data.actions = props.actions;
|
|
593
|
+
if (typeof props.t === "function") data.t = props.t;
|
|
594
|
+
react.useEffect(function () { schedule(); });
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Session "..." menu row: mark / unmark this conversation as needing follow-up. */
|
|
599
|
+
function FollowUpMenuItem(props) {
|
|
600
|
+
var flags = props.useStore(function (state) { return state.followUps; }) || {};
|
|
601
|
+
var flagged = flags[props.sessionId] !== undefined;
|
|
602
|
+
var Item = ui.MenuItemButton;
|
|
603
|
+
if (typeof Item !== "function") return null;
|
|
604
|
+
/* Every shipped row in this menu carries an icon; without one the text
|
|
605
|
+
column starts where the others start after their icon, so the row
|
|
606
|
+
would look shifted. The checklist mark reads as "still to do". */
|
|
607
|
+
var Icon = flagged ? ui.IconCheckOutlineRegular : ui.IconChecklistOutlineRegular;
|
|
608
|
+
return h(Item, {
|
|
609
|
+
icon: typeof Icon === "function" ? h(Icon, {}) : undefined,
|
|
610
|
+
onSelect: function () { props.actions.toggleFollowUp(props.sessionId); },
|
|
611
|
+
}, props.t(flagged ? "menu.unflag" : "menu.flag"));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The host's leading slot for one row: the amber dot of a follow-up mark.
|
|
616
|
+
*
|
|
617
|
+
* Rendering here (instead of injecting a child into the row) is what keeps
|
|
618
|
+
* the list's own layout intact - the host reserves this slot, so a dot in it
|
|
619
|
+
* costs no space and cannot push the following rows down.
|
|
620
|
+
*/
|
|
621
|
+
function FollowUpDot(props) {
|
|
622
|
+
var flags = props.useStore(function (state) { return state.followUps; }) || {};
|
|
623
|
+
if (flags[props.sessionId] === undefined) return null;
|
|
624
|
+
return h("span", {
|
|
625
|
+
className: DOT_TOKEN,
|
|
626
|
+
role: "img",
|
|
627
|
+
"aria-label": "needs follow-up",
|
|
628
|
+
title: "needs follow-up",
|
|
629
|
+
onClick: function (event) {
|
|
630
|
+
if (event !== undefined && typeof event.stopPropagation === "function") event.stopPropagation();
|
|
631
|
+
props.actions.clearFollowUp(props.sessionId);
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Settings -> General row: how old a session must be before it is folded. */
|
|
637
|
+
function SettingsRow(props) {
|
|
638
|
+
var settings = props.useStore(function (state) { return state; });
|
|
639
|
+
var days = Number(settings.foldDays);
|
|
640
|
+
if (!Number.isFinite(days)) days = DEFAULT_DAYS;
|
|
641
|
+
var keep = settings.keepActive !== false;
|
|
642
|
+
var text = props.t("settings.title");
|
|
643
|
+
return h("div", { className: "wsr-setRow" },
|
|
644
|
+
h("div", { className: "wsr-setText" },
|
|
645
|
+
h("div", { className: "wsr-setTitle" }, text),
|
|
646
|
+
h("div", { className: "wsr-setDesc" }, props.t("settings.description")),
|
|
647
|
+
),
|
|
648
|
+
h("div", { className: "wsr-controls" },
|
|
649
|
+
h("button", {
|
|
650
|
+
type: "button",
|
|
651
|
+
className: "wsr-step",
|
|
652
|
+
"aria-label": text + " -1",
|
|
653
|
+
onClick: function () { props.actions.setFoldDays(days - 1); },
|
|
654
|
+
}, "-"),
|
|
655
|
+
h("input", {
|
|
656
|
+
type: "number",
|
|
657
|
+
className: "wsr-input",
|
|
658
|
+
min: MIN_DAYS,
|
|
659
|
+
max: MAX_DAYS,
|
|
660
|
+
value: String(days),
|
|
661
|
+
"aria-label": text,
|
|
662
|
+
onChange: function (event) { props.actions.setFoldDays(event.target.value); },
|
|
663
|
+
}),
|
|
664
|
+
h("button", {
|
|
665
|
+
type: "button",
|
|
666
|
+
className: "wsr-step",
|
|
667
|
+
"aria-label": text + " +1",
|
|
668
|
+
onClick: function () { props.actions.setFoldDays(days + 1); },
|
|
669
|
+
}, "+"),
|
|
670
|
+
h("label", { className: "wsr-exempt" },
|
|
671
|
+
h("input", {
|
|
672
|
+
type: "checkbox",
|
|
673
|
+
checked: keep,
|
|
674
|
+
onChange: function (event) { props.actions.setKeepActive(event.target.checked); },
|
|
675
|
+
}),
|
|
676
|
+
props.t("settings.exempt")),
|
|
677
|
+
),
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Settings -> General row: color marks on/off. */
|
|
682
|
+
function ColorsRow(props) {
|
|
683
|
+
var settings = props.useStore(function (state) { return state; });
|
|
684
|
+
var enabled = settings.colors !== false;
|
|
685
|
+
var text = props.t("color.title");
|
|
686
|
+
return h("div", { className: "wsr-setRow" },
|
|
687
|
+
h("div", { className: "wsr-setText" },
|
|
688
|
+
h("div", { className: "wsr-setTitle" }, text),
|
|
689
|
+
h("div", { className: "wsr-setDesc" }, props.t("color.description")),
|
|
690
|
+
),
|
|
691
|
+
h("div", { className: "wsr-controls" },
|
|
692
|
+
h("label", { className: "wsr-exempt" },
|
|
693
|
+
h("input", {
|
|
694
|
+
type: "checkbox",
|
|
695
|
+
checked: enabled,
|
|
696
|
+
onChange: function (event) { props.actions.setColors(event.target.checked); },
|
|
697
|
+
}),
|
|
698
|
+
props.t("color.enabled")),
|
|
699
|
+
),
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function shutdown() {
|
|
704
|
+
disposed = true;
|
|
705
|
+
if (timer !== null) window.clearTimeout(timer);
|
|
706
|
+
timer = null;
|
|
707
|
+
if (ticker !== null) window.clearInterval(ticker);
|
|
708
|
+
ticker = null;
|
|
709
|
+
if (observer !== null) observer.disconnect();
|
|
710
|
+
observer = null;
|
|
711
|
+
written.forEach(function (section) {
|
|
712
|
+
if (section.style !== undefined) section.style.order = "";
|
|
713
|
+
if (section.getAttribute(RANK_ATTR) !== null) section.removeAttribute(RANK_ATTR);
|
|
714
|
+
});
|
|
715
|
+
written.clear();
|
|
716
|
+
hidden.forEach(function (element) {
|
|
717
|
+
if (element.removeAttribute !== undefined) element.removeAttribute(HIDDEN_ATTR);
|
|
718
|
+
});
|
|
719
|
+
hidden.clear();
|
|
720
|
+
labelled.forEach(function (button) { releaseLabel(button); });
|
|
721
|
+
labelled.clear();
|
|
722
|
+
toggles.forEach(function (button) { button.remove(); });
|
|
723
|
+
toggles.clear();
|
|
724
|
+
dots.forEach(function (dot) { dot.remove(); });
|
|
725
|
+
dots.clear();
|
|
726
|
+
roots.forEach(function (root) { root.removeAttribute(ROOT_ATTR); });
|
|
727
|
+
roots.clear();
|
|
728
|
+
var marked = document.querySelectorAll("[" + STATE_ATTR + "]");
|
|
729
|
+
for (var m = 0; m < marked.length; m += 1) marked[m].removeAttribute(STATE_ATTR);
|
|
730
|
+
expandedSections.clear();
|
|
731
|
+
var tag = document.querySelector('style[data-plugin-css="' + CSS_ID + '"]');
|
|
732
|
+
if (tag !== null) tag.remove();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
var inject = ["slots", "locale"];
|
|
736
|
+
|
|
737
|
+
function apply_(ctx) {
|
|
738
|
+
installCss();
|
|
739
|
+
var prefs = clientStore.defineStore({
|
|
740
|
+
init: function () {
|
|
741
|
+
return { enabled: true, foldDays: DEFAULT_DAYS, keepActive: true, colors: true, followUps: {} };
|
|
742
|
+
},
|
|
743
|
+
persist: "dsh.ws-recency.prefs.v1",
|
|
744
|
+
actions: {
|
|
745
|
+
setEnabled: function (draft, value) { draft.enabled = value === true; },
|
|
746
|
+
setKeepActive: function (draft, value) { draft.keepActive = value === true; },
|
|
747
|
+
setColors: function (draft, value) { draft.colors = value === true; },
|
|
748
|
+
setFoldDays: function (draft, value) {
|
|
749
|
+
var next = Number(value);
|
|
750
|
+
if (!Number.isFinite(next)) return;
|
|
751
|
+
draft.foldDays = Math.min(MAX_DAYS, Math.max(MIN_DAYS, Math.round(next)));
|
|
752
|
+
},
|
|
753
|
+
toggleFollowUp: function (draft, sessionId) {
|
|
754
|
+
if (typeof sessionId !== "string" || sessionId === "") return;
|
|
755
|
+
if (draft.followUps === null || typeof draft.followUps !== "object") draft.followUps = {};
|
|
756
|
+
if (draft.followUps[sessionId] === undefined) draft.followUps[sessionId] = Date.now();
|
|
757
|
+
else delete draft.followUps[sessionId];
|
|
758
|
+
},
|
|
759
|
+
clearFollowUp: function (draft, sessionId) {
|
|
760
|
+
if (draft.followUps === null || typeof draft.followUps !== "object") return;
|
|
761
|
+
if (draft.followUps[sessionId] !== undefined) delete draft.followUps[sessionId];
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
ctx.effect(function () {
|
|
767
|
+
return ctx.locale.register(NS, { zh: ZH, en: EN });
|
|
768
|
+
}, "dsh-ws-recency: dictionaries");
|
|
769
|
+
|
|
770
|
+
if (typeof MutationObserver === "function" && top() !== null) {
|
|
771
|
+
observer = new MutationObserver(schedule);
|
|
772
|
+
observer.observe(top(), {
|
|
773
|
+
childList: true,
|
|
774
|
+
subtree: true,
|
|
775
|
+
attributes: true,
|
|
776
|
+
attributeFilter: [BUSY_RANK_ATTR, "data-row-key", "aria-expanded", "aria-selected"],
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
ctx.slots.inject("shell.overlay", function () {
|
|
781
|
+
return ctx.slots.register({
|
|
782
|
+
name: "shell.overlay",
|
|
783
|
+
id: "ws-recency",
|
|
784
|
+
order: 5,
|
|
785
|
+
store: prefs,
|
|
786
|
+
locale: NS,
|
|
787
|
+
}, Sorter);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
ctx.slots.inject("settings.general.item", function () {
|
|
791
|
+
return ctx.slots.register({
|
|
792
|
+
name: "settings.general.item",
|
|
793
|
+
id: "ws-recency",
|
|
794
|
+
order: 45,
|
|
795
|
+
store: prefs,
|
|
796
|
+
locale: NS,
|
|
797
|
+
}, SettingsRow);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
ctx.slots.inject("settings.general.item", function () {
|
|
801
|
+
return ctx.slots.register({
|
|
802
|
+
name: "settings.general.item",
|
|
803
|
+
id: "ws-recency-colors",
|
|
804
|
+
order: 46,
|
|
805
|
+
store: prefs,
|
|
806
|
+
locale: NS,
|
|
807
|
+
}, ColorsRow);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
ctx.slots.inject("sidebar.workspaces.session.menu.item", function () {
|
|
811
|
+
return ctx.slots.register({
|
|
812
|
+
name: "sidebar.workspaces.session.menu.item",
|
|
813
|
+
id: "ws-recency",
|
|
814
|
+
order: MENU_ORDER,
|
|
815
|
+
store: prefs,
|
|
816
|
+
locale: NS,
|
|
817
|
+
}, FollowUpMenuItem);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
ctx.slots.inject("sidebar.session.row.leading", function () {
|
|
821
|
+
return ctx.slots.register({
|
|
822
|
+
name: "sidebar.session.row.leading",
|
|
823
|
+
id: "ws-recency",
|
|
824
|
+
order: 400,
|
|
825
|
+
store: prefs,
|
|
826
|
+
locale: NS,
|
|
827
|
+
}, FollowUpDot);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
ctx.effect(function () {
|
|
831
|
+
return function () { shutdown(); };
|
|
832
|
+
}, "dsh-ws-recency: list observer");
|
|
833
|
+
|
|
834
|
+
if (typeof window.setInterval === "function") ticker = window.setInterval(schedule, TICK_MS);
|
|
835
|
+
schedule();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
exports.name = "dsh-ws-recency";
|
|
839
|
+
exports.inject = inject;
|
|
840
|
+
exports.apply = apply_;
|
|
841
|
+
return module.exports;
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.warn("[dsh-ws-recency] client half failed to load:", error);
|
|
846
|
+
}
|
|
847
|
+
})();
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# dsh-ws-recency bundle patch: one host row whose client half carries the whole
|
|
2
|
+
# feature. The row itself does nothing on the Host plane; it exists so the
|
|
3
|
+
# Client registry can deliver ./client.js to the browser.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: ws-recency
|
|
6
|
+
name: dsh-ws-recency
|
package/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of dsh-ws-recency.
|
|
3
|
+
*
|
|
4
|
+
* The whole feature is client-side: it reads Session and Workspace state through
|
|
5
|
+
* the Client's root-standard hooks and writes a flex `order` onto the sidebar's
|
|
6
|
+
* workspace groups. This row only carries the package into the plugin graph so
|
|
7
|
+
* the Client registry can serve ./client.js beside it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Plugin name shown by the loader. */
|
|
11
|
+
export const name = 'ws-recency'
|
|
12
|
+
|
|
13
|
+
/** Host-plane entry point; this plugin owns no Host behavior. */
|
|
14
|
+
export function apply() {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ws-recency",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.7.2",
|
|
4
|
+
"description": "Sidebar session management for DeepSeek Harness: workspace groups sorted by latest activity, stale history folded, manual follow-up marks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "chyxhtc",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/chyxhtc/dsh-ws-recency.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/chyxhtc/dsh-ws-recency#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/chyxhtc/dsh-ws-recency/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"dsh",
|
|
17
|
+
"dsh-plugin",
|
|
18
|
+
"deepseek-harness",
|
|
19
|
+
"sidebar",
|
|
20
|
+
"session",
|
|
21
|
+
"workspace"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./index.js",
|
|
27
|
+
"./client": "./client.js",
|
|
28
|
+
"./client.js": "./client.js",
|
|
29
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"index.js",
|
|
34
|
+
"client.js",
|
|
35
|
+
"cordis.patch.yml",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
40
|
+
},
|
|
41
|
+
"dsh": {
|
|
42
|
+
"id": "dsh-ws-recency",
|
|
43
|
+
"name": "Workspace recency order",
|
|
44
|
+
"bundle": {
|
|
45
|
+
"patch": "./cordis.patch.yml"
|
|
46
|
+
},
|
|
47
|
+
"client": {
|
|
48
|
+
"platform": "web",
|
|
49
|
+
"inject": [
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|