dsh-easy-archive 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/README.zh.md +67 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +338 -0
- package/lib/index.js +11 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bainianlaoyao
|
|
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
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# easy-archive
|
|
2
|
+
|
|
3
|
+
A DSH (DeepSeek Harness) web plugin that moves session archiving out of the
|
|
4
|
+
kebab (⋮) menu and onto the workspace sidebar row itself, as a two-step
|
|
5
|
+
inline button:
|
|
6
|
+
|
|
7
|
+
1. **Hover a session row** — an archive icon appears right before the ⋮ button.
|
|
8
|
+
2. **Click once** — the button switches to a red **「确认归档 / Confirm archive」** pill.
|
|
9
|
+
3. **Click again** — the session is archived.
|
|
10
|
+
|
|
11
|
+
The **archive item is removed from the ⋮ menu** — archiving lives only on the
|
|
12
|
+
row, never buried in a submenu.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Hover-revealed inline archive button on every session row (next to the ⋮ menu);
|
|
17
|
+
one click arms the red confirm, the second commits.
|
|
18
|
+
- Archive entry automatically hidden from the ⋮ menu whenever it opens
|
|
19
|
+
(rename / fork stay where they are).
|
|
20
|
+
- No slot takeover: the stock workspace browser keeps rendering; rename,
|
|
21
|
+
fork, search, drag-reorder and everything else stay intact.
|
|
22
|
+
- Archive runs through the same wire call the built-in menu used
|
|
23
|
+
(`ctx.workspaces.archiveSession`), so behavior and permissions are identical.
|
|
24
|
+
- Safety nets: the armed confirm auto-disarms after 4s, on mouse-leave, or on
|
|
25
|
+
any click elsewhere; rows with ambiguous identities never get a button
|
|
26
|
+
rather than risk archiving the wrong session.
|
|
27
|
+
- Locale-aware (zh / en), no build step, no React dependency.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
From the DSH plugin market (recommended):
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
dsh plugin --profile web add github:bainianlaoyao/easy-archive # GitHub distribution
|
|
35
|
+
dsh plugin --profile web add dsh-easy-archive # npm distribution (prebuilt; skips allowBuilds)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
…or add it manually to the web profile's `package.json`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"dependencies": { "dsh-easy-archive": "github:bainianlaoyao/easy-archive" },
|
|
43
|
+
"dsh": { "profile": { "bundles": [ "...", "dsh-easy-archive" ] } }
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
then `pnpm install` in the profile directory and restart `dsh web`
|
|
48
|
+
(new plugins enter the `window.__DSH_BOOT__` graph at boot; later edits to
|
|
49
|
+
`lib/client.js` hot-apply on refresh).
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
The host half (`lib/index.js`) is an inert plugin row that carries the
|
|
54
|
+
`dsh.client` declaration into the profile's loader graph; the browser half
|
|
55
|
+
(`lib/client.js`) observes the sidebar DOM and, for every rendered session
|
|
56
|
+
row:
|
|
57
|
+
|
|
58
|
+
- resolves the session id exactly from the `ctx.sessions.list` snapshot by
|
|
59
|
+
display title (disambiguated by the rendered relative-time bucket when
|
|
60
|
+
titles repeat),
|
|
61
|
+
- stamps the row with the id and inserts a 16px archive button into the row's
|
|
62
|
+
action cell, before the kebab,
|
|
63
|
+
- removes the archive item from any ⋮ menu it sees open.
|
|
64
|
+
|
|
65
|
+
Clicking the button never bubbles to the row's "open session" handler.
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
node scripts/smoke.mjs # validate the browser bundle shape
|
|
71
|
+
node scripts/check.mjs # (alias) syntax + smoke
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- Blank "New Session" rows and search-result rows never get the button.
|
|
77
|
+
- Sessions whose display title and time bucket collide are skipped
|
|
78
|
+
(conservative: no wrong-session archives).
|
|
79
|
+
- Uninstall: remove the dependency and bundles entry, `pnpm install`,
|
|
80
|
+
restart `dsh web`.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# easy-archive
|
|
2
|
+
|
|
3
|
+
一个 DSH(DeepSeek Harness)Web 插件:把「归档会话」从 ⋮ 二级菜单里移到工作区侧边栏条目本身上,两击归档:
|
|
4
|
+
|
|
5
|
+
1. **悬停会话条目** — 归档图标出现在 ⋮ 按钮旁边;
|
|
6
|
+
2. **点击一次** — 按钮切换成红色 **「确认归档」** 胶囊;
|
|
7
|
+
3. **再点击一次** — 直接归档该会话。
|
|
8
|
+
|
|
9
|
+
**⋮ 菜单里的「归档会话」项会被自动隐藏** — 归档只存在于条目上,绝不藏在二级菜单里。
|
|
10
|
+
|
|
11
|
+
## 特性
|
|
12
|
+
|
|
13
|
+
- 每个会话条目行内(悬停时)显示归档按钮,两击归档(第一击武装红色确认,第二击提交)。
|
|
14
|
+
- ⋮ 菜单每次打开时自动移除「归档会话」项(重命名/分叉保留原位)。
|
|
15
|
+
- 不接管任何内置插槽,不替换自带工作区浏览器:重命名、分叉、搜索、拖拽排序等全部原样保留。
|
|
16
|
+
- 归档走与内置菜单完全相同的调用(`ctx.workspaces.archiveSession`),行为与权限一致。
|
|
17
|
+
- 安全兜底:确认态在 4 秒、鼠标离开条目、或点击其它位置时自动解除;身份存疑的条目宁可不放按钮,也不冒误归档的风险。
|
|
18
|
+
- 中英双语、零构建、不依赖 React。
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
从 DSH 插件市场安装(推荐):
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
dsh plugin --profile web add github:bainianlaoyao/easy-archive # GitHub 分发
|
|
26
|
+
dsh plugin --profile web add dsh-easy-archive # npm 分发(预构建安装免 allowBuilds)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
或手动加入 web profile 的 `package.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"dependencies": { "dsh-easy-archive": "github:bainianlaoyao/easy-archive" },
|
|
34
|
+
"dsh": { "profile": { "bundles": [ "...", "dsh-easy-archive" ] } }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
然后在 profile 目录执行 `pnpm install` 并重启 `dsh web`(新插件在启动时进入
|
|
39
|
+
`window.__DSH_BOOT__` 图;之后修改 `lib/client.js` 刷新页面即可生效)。
|
|
40
|
+
|
|
41
|
+
## 工作原理
|
|
42
|
+
|
|
43
|
+
宿主半区(`lib/index.js`)是一个空插件行,负责把 `dsh.client` 声明带进 profile 的加载图;
|
|
44
|
+
浏览器半区(`lib/client.js`)观察侧边栏 DOM,对每个渲染出的会话条目:
|
|
45
|
+
|
|
46
|
+
- 从 `ctx.sessions.list` 快照按显示标题精确解析会话 id(标题重复时用渲染出的相对时间分桶消歧);
|
|
47
|
+
- 在条目上盖 id 戳,并把 16px 归档按钮插入条目的操作区、⋮ 按钮之前;
|
|
48
|
+
- 一旦发现打开的 ⋮ 菜单,移除其中的「归档会话」项。
|
|
49
|
+
|
|
50
|
+
点击按钮不会冒泡到条目的「打开会话」事件。
|
|
51
|
+
|
|
52
|
+
## 开发
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
node scripts/smoke.mjs # 校验浏览器 bundle 结构
|
|
56
|
+
node scripts/check.mjs # (别名) 语法 + smoke
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 说明
|
|
60
|
+
|
|
61
|
+
- 空白「新会话」行与搜索结果行不会出现归档按钮。
|
|
62
|
+
- 显示标题与时间分桶完全相同的会话会被跳过(保守策略:绝不误归档)。
|
|
63
|
+
- 卸载:移除依赖与 bundles 条目,`pnpm install`,重启 `dsh web`。
|
|
64
|
+
|
|
65
|
+
## 许可证
|
|
66
|
+
|
|
67
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# easy-archive bundle patch
|
|
2
|
+
#
|
|
3
|
+
# `dsh.bundle.patch` layer: when the plugin is installed into a profile (added
|
|
4
|
+
# to `dsh.profile.bundles` in the profile's package.json, or via
|
|
5
|
+
# `dsh plugin --profile web add github:bainianlaoyao/easy-archive`), profile
|
|
6
|
+
# boot merges this patch — a single `insert` of the plugin row. The row's
|
|
7
|
+
# `name` resolves the installed package, so the plugin's client half (declared
|
|
8
|
+
# via `dsh.client` in package.json) is picked up by client-modules and served
|
|
9
|
+
# to the browser as part of the `window.__DSH_BOOT__` graph.
|
|
10
|
+
- insert:
|
|
11
|
+
- id: easy-archive
|
|
12
|
+
name: 'dsh-easy-archive'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "easy-archive",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
/**
|
|
8
|
+
* easy-archive — browser half.
|
|
9
|
+
*
|
|
10
|
+
* Moves session archiving out of the kebab (secondary) menu onto the row
|
|
11
|
+
* itself, as a two-step inline button:
|
|
12
|
+
*
|
|
13
|
+
* 1. click → the button switches to a red "confirm archive" pill;
|
|
14
|
+
* 2. click again → the session is archived through the workspaces service.
|
|
15
|
+
*
|
|
16
|
+
* The archive entry is simultaneously removed from the kebab menu whenever
|
|
17
|
+
* it opens, so archiving lives only on the row.
|
|
18
|
+
*
|
|
19
|
+
* How it works (no React, no slot takeover — the stock workspace browser
|
|
20
|
+
* keeps rendering): the plugin observes the sidebar DOM and, for every
|
|
21
|
+
* rendered session row, stamps the row with its session id (matched
|
|
22
|
+
* exactly from the sessions snapshot store by display title,
|
|
23
|
+
* disambiguated by the rendered relative-time bucket on duplicate titles)
|
|
24
|
+
* and inserts a small archive button into the row’s trailing action cell,
|
|
25
|
+
* before the kebab — revealed together with the ⋮ menu on row hover. Clicking the button never
|
|
26
|
+
* bubbles to the row's "open session" handler.
|
|
27
|
+
*
|
|
28
|
+
* Services: `sessions` (ctx.sessions.list snapshot) and `workspaces`
|
|
29
|
+
* (ctx.workspaces.list snapshot + ctx.workspaces.archiveSession).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const PLUGIN_ID = "easy-archive";
|
|
33
|
+
const CSS_TAG = "easy-archive/ArchiveButton.module.css";
|
|
34
|
+
// Session rows carry `aria-selected`; workspace group headers carry `aria-expanded` instead.
|
|
35
|
+
const ROW_SELECTOR = 'div[role="treeitem"][aria-selected]';
|
|
36
|
+
|
|
37
|
+
/** Small locale-aware copy (the GUI follows `document.documentElement.lang`). */
|
|
38
|
+
const DICTS = {
|
|
39
|
+
zh: {
|
|
40
|
+
archiveAria: "归档会话",
|
|
41
|
+
confirmAria: "确认归档",
|
|
42
|
+
confirmText: "确认归档",
|
|
43
|
+
menuArchive: ["归档会话"],
|
|
44
|
+
time: {
|
|
45
|
+
now: "刚刚",
|
|
46
|
+
minutes: (n) => `${n}分钟`,
|
|
47
|
+
hours: (n) => `${n}小时`,
|
|
48
|
+
days: (n) => `${n}天`,
|
|
49
|
+
months: (n) => `${n}个月`,
|
|
50
|
+
years: (n) => `${n}年`
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
en: {
|
|
54
|
+
archiveAria: "Archive session",
|
|
55
|
+
confirmAria: "Confirm archive",
|
|
56
|
+
confirmText: "Confirm",
|
|
57
|
+
menuArchive: ["Archive session"],
|
|
58
|
+
time: {
|
|
59
|
+
now: "now",
|
|
60
|
+
minutes: (n) => `${n}min`,
|
|
61
|
+
hours: (n) => `${n}h`,
|
|
62
|
+
days: (n) => `${n}d`,
|
|
63
|
+
months: (n) => `${n}mo`,
|
|
64
|
+
years: (n) => `${n}y`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function currentLang() {
|
|
70
|
+
return String(document.documentElement.lang ?? "zh-CN").toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Archive icon: the DSH-native `IconArchiveOutline20` glyph, extracted
|
|
75
|
+
* verbatim from @deepseek-ai/dsh-client-ui-primitives (the same icon the
|
|
76
|
+
* stock ⋮ menu uses for "归档会话"), rendered at 16px inside its 20-unit
|
|
77
|
+
* viewBox — exactly like the menu item renders it at size 16.
|
|
78
|
+
*/
|
|
79
|
+
const ARCHIVE_ICON =
|
|
80
|
+
'<svg width="16" height="16" viewBox="0 0 20 20" fill="none" aria-hidden="true">' +
|
|
81
|
+
'<path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M15.8659 2.05975C17.2603 2.05995 18.3913 3.19096 18.3914 4.58527V5.4874C18.3914 6.02747 18.2192 6.52672 17.9303 6.93735C17.9336 6.96524 17.9388 6.99318 17.9388 7.02195V12.8884C17.9388 13.6345 17.9395 14.2379 17.8996 14.7254C17.8642 15.1593 17.7936 15.5499 17.6373 15.9141L17.5654 16.0685C17.278 16.6328 16.8405 17.1046 16.3038 17.434L16.0679 17.5661C15.66 17.7739 15.2196 17.8598 14.7237 17.9003C14.2362 17.9401 13.6327 17.9405 12.8867 17.9405H7.11122C6.36511 17.9405 5.76171 17.9401 5.27418 17.9003C4.84051 17.8649 4.44949 17.7952 4.08545 17.6391L3.93104 17.5661C3.36673 17.2785 2.89392 16.8414 2.56465 16.3044L2.43245 16.0685C2.22473 15.6608 2.13878 15.2211 2.09825 14.7254C2.05841 14.2379 2.05912 13.6345 2.05912 12.8884V7.02195C2.05912 6.99284 2.06422 6.96449 2.06758 6.93629C1.77931 6.52592 1.60858 6.02687 1.60858 5.4874V4.58527C1.60876 3.19084 2.73962 2.05975 4.1341 2.05975H15.8659ZM16.4984 7.92936C16.296 7.98169 16.0847 8.01288 15.8659 8.01291H4.1341C3.91478 8.01291 3.70246 7.98194 3.49955 7.92936V12.8884C3.49955 13.6582 3.50053 14.1927 3.53445 14.608C3.56769 15.0146 3.62923 15.244 3.71635 15.415L3.7925 15.5514C3.98339 15.8627 4.25749 16.1165 4.58464 16.2833L4.72529 16.3435C4.88095 16.3993 5.08638 16.4402 5.39158 16.4651C5.80685 16.4991 6.34138 16.5001 7.11122 16.5001H12.8867C13.6564 16.5001 14.1911 16.499 14.6063 16.4651C15.0128 16.432 15.2423 16.3703 15.4133 16.2833L15.5508 16.2061C15.8618 16.0152 16.116 15.7419 16.2827 15.415L16.3429 15.2732C16.3985 15.1177 16.4396 14.9128 16.4645 14.608C16.4985 14.1927 16.4984 13.6583 16.4984 12.8884V7.92936ZM4.1341 3.50019C3.53511 3.50019 3.0492 3.98631 3.04902 4.58527V5.4874C3.04902 6.08649 3.535 6.57248 4.1341 6.57248H15.8659C16.4648 6.57228 16.951 6.08638 16.951 5.4874V4.58527C16.9509 3.98644 16.4647 3.50038 15.8659 3.50019H4.1341Z"/>' +
|
|
82
|
+
"</svg>";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check glyph for the armed confirm state: the DSH-native
|
|
86
|
+
* `IconCheckOutline16` path, rendered at 12px inside its 16-unit viewBox.
|
|
87
|
+
*/
|
|
88
|
+
const CHECK_ICON =
|
|
89
|
+
'<svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
|
|
90
|
+
'<path fill="currentColor" d="M15.0498 3.92579L8.49512 12.3818C8.25774 12.6881 8.04517 12.9645 7.84668 13.1689C7.63957 13.3823 7.38732 13.5841 7.04492 13.6719C6.86373 13.7183 6.6757 13.7346 6.48926 13.7197C6.13666 13.6915 5.8528 13.5355 5.6123 13.3604C5.38201 13.1926 5.12573 12.9567 4.83984 12.6953L1.03125 9.21289L1.96875 8.1875L5.77734 11.6699C6.08684 11.9529 6.27773 12.1249 6.43066 12.2363C6.50183 12.2882 6.54699 12.3135 6.57324 12.3252C6.58525 12.3305 6.59269 12.3322 6.5957 12.333C6.59802 12.3336 6.59961 12.334 6.59961 12.334C6.63317 12.3367 6.66758 12.3335 6.7002 12.3252C6.7002 12.3252 6.70211 12.3251 6.7041 12.3242C6.70698 12.3229 6.71348 12.319 6.72461 12.3115C6.74849 12.2956 6.78843 12.2642 6.84961 12.2012C6.98138 12.0654 7.13957 11.8628 7.39648 11.5313L13.9502 3.07422L15.0498 3.92579Z"/>' +
|
|
91
|
+
"</svg>";
|
|
92
|
+
|
|
93
|
+
/** Replicate the workspace browser's relative-time buckets for label-exact disambiguation. */
|
|
94
|
+
function timeBucket(updatedAt, now) {
|
|
95
|
+
const MIN = 6e4;
|
|
96
|
+
const HOUR = 36e5;
|
|
97
|
+
const DAY = 864e5;
|
|
98
|
+
const diff = Math.max(0, now - updatedAt);
|
|
99
|
+
if (diff < MIN) return { unit: "now", n: 0 };
|
|
100
|
+
if (diff < HOUR) return { unit: "minutes", n: Math.floor(diff / MIN) };
|
|
101
|
+
if (diff < DAY) return { unit: "hours", n: Math.floor(diff / HOUR) };
|
|
102
|
+
if (diff < 30 * DAY) return { unit: "days", n: Math.floor(diff / DAY) };
|
|
103
|
+
if (diff < 365 * DAY) return { unit: "months", n: Math.floor(diff / (30 * DAY)) };
|
|
104
|
+
return { unit: "years", n: Math.floor(diff / (365 * DAY)) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function bucketLabel(bucket, dict) {
|
|
108
|
+
const fn = dict.time[bucket.unit];
|
|
109
|
+
return typeof fn === "function" ? fn(bucket.n) : String(bucket.n);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Style sheet injected with the same data-plugin discipline as platform bundles. */
|
|
113
|
+
function injectStyle() {
|
|
114
|
+
if (typeof document === "undefined") return;
|
|
115
|
+
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(CSS_TAG)}]`) !== null) return;
|
|
116
|
+
const tag = document.createElement("style");
|
|
117
|
+
tag.dataset.plugin = PLUGIN_ID;
|
|
118
|
+
tag.dataset.pluginCss = CSS_TAG;
|
|
119
|
+
tag.textContent = [
|
|
120
|
+
"button.dshAr_button{box-sizing:border-box;cursor:pointer;flex:none;width:16px;height:16px;margin:0 2px 0 0;padding:0;border:none;border-radius:4px;background:transparent;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;opacity:.75;transition:color .12s ease,background-color .12s ease,opacity .12s ease,width .12s ease,padding .12s ease}",
|
|
121
|
+
"button.dshAr_button:hover{opacity:1;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
122
|
+
"button.dshAr_button.dshAr_confirm{width:auto;height:20px;padding:0 8px;gap:4px;opacity:1;color:#fff;background:var(--dsw-alias-state-error-primary,#d64545);border-radius:10px;font-size:12px;line-height:1;font-weight:500}",
|
|
123
|
+
"button.dshAr_button.dshAr_confirm:hover{color:#fff;background:var(--dsw-alias-state-error-primary,#d64545)}",
|
|
124
|
+
"button.dshAr_button.dshAr_confirm svg{width:12px;height:12px}"
|
|
125
|
+
].join("\n");
|
|
126
|
+
document.head.appendChild(tag);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Services required by the client half (service names, not module ids). */
|
|
130
|
+
const inject = ["workspaces", "sessions"];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Activate the inline archive behaviors.
|
|
134
|
+
* @param ctx - client root context with workspaces + sessions injected.
|
|
135
|
+
*/
|
|
136
|
+
function apply(ctx) {
|
|
137
|
+
const workspaces = ctx.workspaces;
|
|
138
|
+
const sessions = ctx.sessions;
|
|
139
|
+
if (typeof document === "undefined" || workspaces === void 0 || sessions === void 0 || workspaces.list === void 0 || sessions.list === void 0) return;
|
|
140
|
+
|
|
141
|
+
let dict = DICTS[currentLang()];
|
|
142
|
+
|
|
143
|
+
// ------------------------------------------------------ state
|
|
144
|
+
let cachedSessions = void 0;
|
|
145
|
+
let cachedWorkspaces = void 0;
|
|
146
|
+
let byTitle = new Map();
|
|
147
|
+
let cachedNow = 0;
|
|
148
|
+
|
|
149
|
+
const rebuildIndex = () => {
|
|
150
|
+
const snapS = sessions.list.getSnapshot();
|
|
151
|
+
const snapW = workspaces.list.getSnapshot();
|
|
152
|
+
dict = DICTS[currentLang()];
|
|
153
|
+
if (snapS === cachedSessions && snapW === cachedWorkspaces && cachedNow !== 0) return; // rows unchanged; keep index
|
|
154
|
+
cachedSessions = snapS;
|
|
155
|
+
cachedWorkspaces = snapW;
|
|
156
|
+
cachedNow = Date.now();
|
|
157
|
+
byTitle = new Map();
|
|
158
|
+
if (!snapS || !snapS.byId) return;
|
|
159
|
+
const archived = new Set(snapW?.archivedSessionIds || []);
|
|
160
|
+
const current = snapS.current;
|
|
161
|
+
for (const id of snapS.ids) {
|
|
162
|
+
const s = snapS.byId[id];
|
|
163
|
+
if (!s) continue;
|
|
164
|
+
if (s.origin === "subagent" || archived.has(id)) continue;
|
|
165
|
+
if (s.blank && s.id !== current) continue;
|
|
166
|
+
const title = s.blank ? "New Session" : (s.displayTitle ?? s.title ?? "");
|
|
167
|
+
if (title === "") continue;
|
|
168
|
+
let list = byTitle.get(title);
|
|
169
|
+
if (list === void 0) {
|
|
170
|
+
list = [];
|
|
171
|
+
byTitle.set(title, list);
|
|
172
|
+
}
|
|
173
|
+
list.push(s);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const resolveId = (title, timeText) => {
|
|
178
|
+
const candidates = byTitle.get(title);
|
|
179
|
+
if (candidates === void 0 || candidates.length === 0) return void 0;
|
|
180
|
+
if (candidates.length === 1) return candidates[0].id;
|
|
181
|
+
// duplicate display titles: whoever also matches the rendered time bucket wins.
|
|
182
|
+
if (timeText !== "") {
|
|
183
|
+
const hits = candidates.filter((c) => bucketLabel(timeBucket(c.updatedAt, cachedNow), dict) === timeText);
|
|
184
|
+
if (hits.length === 1) return hits[0].id;
|
|
185
|
+
}
|
|
186
|
+
return void 0;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const renderIdle = (btn) => {
|
|
190
|
+
btn.classList.remove("dshAr_confirm");
|
|
191
|
+
btn.setAttribute("aria-label", dict.archiveAria);
|
|
192
|
+
btn.innerHTML = ARCHIVE_ICON;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const attachButton = (btn, row) => {
|
|
196
|
+
let timer = null;
|
|
197
|
+
const reset = () => {
|
|
198
|
+
if (timer !== null) {
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
timer = null;
|
|
201
|
+
}
|
|
202
|
+
renderIdle(btn);
|
|
203
|
+
};
|
|
204
|
+
btn.addEventListener("click", (event) => {
|
|
205
|
+
event.stopPropagation();
|
|
206
|
+
if (btn.classList.contains("dshAr_confirm")) {
|
|
207
|
+
const sessionId = row.dataset.dshArSessionId;
|
|
208
|
+
if (sessionId !== void 0 && sessionId !== "") {
|
|
209
|
+
workspaces.archiveSession(sessionId).catch((reason) => {
|
|
210
|
+
console.warn("[easy-archive] archive rejected:", reason);
|
|
211
|
+
reset();
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
console.warn("[easy-archive] no session id stamped on row; archive aborted");
|
|
215
|
+
}
|
|
216
|
+
reset();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
btn.classList.add("dshAr_confirm");
|
|
220
|
+
btn.setAttribute("aria-label", dict.confirmAria);
|
|
221
|
+
btn.innerHTML = CHECK_ICON + `<span>${dict.confirmText}</span>`;
|
|
222
|
+
if (timer !== null) clearTimeout(timer);
|
|
223
|
+
timer = setTimeout(reset, 4000);
|
|
224
|
+
});
|
|
225
|
+
row.addEventListener("mouseleave", reset);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const processRow = (row) => {
|
|
229
|
+
// Session rows end with [.., title, time, actions]; the kebab sits (possibly
|
|
230
|
+
// wrapped) inside the trailing action cell, which the stock browser only
|
|
231
|
+
// reveals on row hover. Our archive button lives in the same cell, right
|
|
232
|
+
// before the kebab, so it shows together with the ⋮ menu on row hover —
|
|
233
|
+
// and stays hidden otherwise, matching the stock reveal behavior.
|
|
234
|
+
// Blank / new-session rows have no action cell and no kebab, so they fall
|
|
235
|
+
// out through the kebab check.
|
|
236
|
+
const children = Array.from(row.children);
|
|
237
|
+
if (children.length < 3) return;
|
|
238
|
+
const actions = children[children.length - 1];
|
|
239
|
+
const timeEl = children[children.length - 2];
|
|
240
|
+
const titleEl = children[children.length - 3];
|
|
241
|
+
let kebab = null;
|
|
242
|
+
for (const candidate of actions.querySelectorAll("button")) {
|
|
243
|
+
if (!candidate.classList.contains("dshAr_button")) {
|
|
244
|
+
kebab = candidate;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (kebab === null) return;
|
|
249
|
+
const title = (titleEl.textContent || "").trim();
|
|
250
|
+
if (title === "") return;
|
|
251
|
+
const sessionId = resolveId(title, timeEl === null ? "" : (timeEl.textContent || "").trim());
|
|
252
|
+
if (sessionId === void 0) return; // ambiguous title — stay conservative, do not risk archiving the wrong session
|
|
253
|
+
if (row.dataset.dshArSessionId !== sessionId) row.dataset.dshArSessionId = sessionId;
|
|
254
|
+
if (row.querySelector("button.dshAr_button") !== null) return;
|
|
255
|
+
const btn = document.createElement("button");
|
|
256
|
+
btn.type = "button";
|
|
257
|
+
btn.className = "dshAr_button";
|
|
258
|
+
btn.setAttribute("aria-label", dict.archiveAria);
|
|
259
|
+
btn.innerHTML = ARCHIVE_ICON;
|
|
260
|
+
// Inside the trailing action cell, before the kebab wrapper, so the row's
|
|
261
|
+
// own child structure (…, title, time, actions) stays stable.
|
|
262
|
+
actions.insertBefore(btn, actions.firstChild);
|
|
263
|
+
attachButton(btn, row);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const syncAll = () => {
|
|
267
|
+
rebuildIndex();
|
|
268
|
+
const rows = document.querySelectorAll(ROW_SELECTOR);
|
|
269
|
+
for (const row of rows) processRow(row);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// ------------------------------------------------------ observers
|
|
273
|
+
let dirty = false;
|
|
274
|
+
const scheduleSync = () => {
|
|
275
|
+
if (dirty) return;
|
|
276
|
+
dirty = true;
|
|
277
|
+
const finish = () => {
|
|
278
|
+
dirty = false;
|
|
279
|
+
syncAll();
|
|
280
|
+
purgeArchiveFromMenus();
|
|
281
|
+
};
|
|
282
|
+
// rAF coalesces per frame; the timeout fallback covers throttled/hidden
|
|
283
|
+
// tabs (background windows starve rAF) so rows always get stamped.
|
|
284
|
+
requestAnimationFrame(finish);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const rowObserver = new MutationObserver(scheduleSync);
|
|
288
|
+
rowObserver.observe(document.body, { childList: true, subtree: true });
|
|
289
|
+
|
|
290
|
+
const doneMenus = new WeakSet();
|
|
291
|
+
const purgeArchiveFromMenus = () => {
|
|
292
|
+
for (const menu of document.querySelectorAll('[role="menu"]')) {
|
|
293
|
+
if (doneMenus.has(menu)) continue;
|
|
294
|
+
doneMenus.add(menu);
|
|
295
|
+
for (const item of menu.querySelectorAll('button[role="menuitem"]')) {
|
|
296
|
+
const text = (item.textContent || "").replace(/s+/g, "");
|
|
297
|
+
if (dict.menuArchive.includes(text)) item.style.display = "none";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const menuObserver = new MutationObserver(purgeArchiveFromMenus);
|
|
302
|
+
menuObserver.observe(document.body, { childList: true, subtree: true });
|
|
303
|
+
|
|
304
|
+
// any click outside an archive button disarms every armed confirm pill.
|
|
305
|
+
const onDocumentClick = (event) => {
|
|
306
|
+
const target = event.target;
|
|
307
|
+
if (target instanceof Element && target.closest("button.dshAr_button") !== null) return;
|
|
308
|
+
for (const btn of document.querySelectorAll("button.dshAr_button.dshAr_confirm")) renderIdle(btn);
|
|
309
|
+
};
|
|
310
|
+
document.addEventListener("click", onDocumentClick, true);
|
|
311
|
+
|
|
312
|
+
const unsubSessions = sessions.list.subscribe(scheduleSync);
|
|
313
|
+
const unsubWorkspaces = workspaces.list.subscribe(scheduleSync);
|
|
314
|
+
|
|
315
|
+
// Low-frequency heartbeat: guarantees stamping even if a mutation was
|
|
316
|
+
// missed or the tab was throttled; idempotent and cheap (the index is
|
|
317
|
+
// reused while the snapshot stores are unchanged).
|
|
318
|
+
const heartbeat = setInterval(syncAll, 3000);
|
|
319
|
+
|
|
320
|
+
syncAll();
|
|
321
|
+
|
|
322
|
+
ctx.effect(() => () => {
|
|
323
|
+
rowObserver.disconnect();
|
|
324
|
+
menuObserver.disconnect();
|
|
325
|
+
document.removeEventListener("click", onDocumentClick, true);
|
|
326
|
+
unsubSessions();
|
|
327
|
+
unsubWorkspaces();
|
|
328
|
+
clearInterval(heartbeat);
|
|
329
|
+
}, "easy-archive: activate");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
injectStyle();
|
|
333
|
+
|
|
334
|
+
exports.apply = apply;
|
|
335
|
+
exports.inject = inject;
|
|
336
|
+
return module.exports;
|
|
337
|
+
}
|
|
338
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* easy-archive — host half.
|
|
3
|
+
*
|
|
4
|
+
* A deliberately inert web-profile plugin row: its only job is to carry the
|
|
5
|
+
* `dsh.client` declaration into the profile's loader graph so client-modules
|
|
6
|
+
* serves the browser bundle at `/plugins/easy-archive/client.js`.
|
|
7
|
+
*/
|
|
8
|
+
export const name = "easy-archive";
|
|
9
|
+
export const inject = [];
|
|
10
|
+
/** No host-side behavior; the client half owns everything. */
|
|
11
|
+
export function apply() {}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-easy-archive",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "DSH web plugin: two-step inline archive on workspace sidebar rows — one click arms a red confirm, the second archives; archive is no longer buried in the ⋮ menu. · 工作区侧边栏行内两击归档:点一次变红确认,再点即归档,归档不再藏在 ⋮ 菜单里。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/bainianlaoyao/easy-archive.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/bainianlaoyao/easy-archive#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/bainianlaoyao/easy-archive/issues"
|
|
13
|
+
},
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./lib/index.js",
|
|
17
|
+
"./client": "./lib/client.js",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"dsh": {
|
|
21
|
+
"bundle": {
|
|
22
|
+
"patch": "./cordis.patch.yml"
|
|
23
|
+
},
|
|
24
|
+
"client": {
|
|
25
|
+
"platform": "web",
|
|
26
|
+
"inject": [
|
|
27
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib",
|
|
33
|
+
"cordis.patch.yml",
|
|
34
|
+
"README.md",
|
|
35
|
+
"README.zh.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"keywords": [
|
|
39
|
+
"dsh",
|
|
40
|
+
"dsh-plugin",
|
|
41
|
+
"deepseek",
|
|
42
|
+
"harness",
|
|
43
|
+
"archive",
|
|
44
|
+
"sidebar",
|
|
45
|
+
"workspace"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"scripts": {
|
|
49
|
+
"smoke": "node scripts/smoke.mjs",
|
|
50
|
+
"check": "node scripts/smoke.mjs && node --check lib/index.js && node --check lib/client.js"
|
|
51
|
+
}
|
|
52
|
+
}
|