dsh-session-autarchive 0.1.5 → 0.1.6

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/README.en.md CHANGED
@@ -11,6 +11,7 @@ Archives sessions that have gone unused for longer than a configurable number of
11
11
  | Where | What it looks like | What it does |
12
12
  |---|---|---|
13
13
  | Session context menu | One row, "Do not auto-archive", next to Pin / Rename / Fork / Archive | Click once to mark, again to unmark |
14
+ | Session row trailing | On hover, a shield button next to the archive icon (hollow when unmarked, filled when marked) | Toggle "do not auto-archive" without opening the overflow menu |
14
15
  | Session row start | A small shield on marked sessions | Shows at a glance that the session is protected |
15
16
  | Hover card | One line, "Not auto-archived" | Same, with text |
16
17
  | General settings | A row at the bottom, "Auto-archive after (days)" | Saved on blur or Enter, writes the same host field |
@@ -70,7 +71,7 @@ Writes are temp-file-then-rename, and reads retry — a half-written file is nev
70
71
  ## How it works
71
72
 
72
73
  - The host half registers four local routes: `GET /dsh-session-autarchive/api/state`, `POST .../protect`, `POST .../config`, `POST .../run`. Loopback addresses only (`127.*`, `::1`, `::ffff:127.*`); anything else gets 403, a wrong method gets 405.
73
- - The browser half mounts four slots (menu row, row start, hover card, settings row). Services are probed with `ctx.get`; when one is missing that entry is skipped and the others still mount.
74
+ - The browser half mounts five slots (menu row, row action, row start, hover card, settings row). Services are probed with `ctx.get`; when one is missing that entry is skipped and the others still mount.
74
75
  - No network access, nothing uploaded. Decisions use local file times only.
75
76
 
76
77
  ## Known limits
@@ -85,7 +86,7 @@ Writes are temp-file-then-rename, and reads retry — a half-written file is nev
85
86
  The source has two halves:
86
87
 
87
88
  - `index.js` — host side (decision, archiving, four routes, the tool)
88
- - `src/entry.jsx` — browser side (menu row, row shield, hover line, settings row, the 5-minute fast path)
89
+ - `src/entry.jsx` — browser side (menu row, row action shield, row shield, hover line, settings row, the 5-minute fast path)
89
90
 
90
91
  Editing `src/entry.jsx` requires rebuilding. `lib/client.js` is a build artifact committed to the repository; without it the UI does not load:
91
92
 
package/README.md CHANGED
@@ -6,11 +6,12 @@
6
6
 
7
7
  把长期没动过的会话自动归档。按会话内最后一条对话结束的时间算天数,默认 5 天;已归档、置顶(可关)、标记「不自动归档」的都跳过。侧栏会话右键菜单给单个会话加标记,通用设置里改天数。
8
8
 
9
- ## 界面上的三处
9
+ ## 界面上的四处
10
10
 
11
11
  | 位置 | 样子 | 作用 |
12
12
  |---|---|---|
13
13
  | 会话右键菜单 | 与「置顶会话/重命名/分叉会话/归档会话」并列的一行「不自动归档」 | 点一次标记,再点一次取消 |
14
+ | 会话行尾 | 悬停时归档图标旁边一个盾牌按钮(未标记空心盾、已标记实心盾) | 不用点开「更多」,直接开/关「不自动归档」 |
14
15
  | 会话行首 | 被标记的会话多一个小盾牌 | 一眼看出这条不会被自动归档 |
15
16
  | 悬停卡片 | 一行「不会被自动归档」 | 同上,带文字说明 |
16
17
  | 通用设置 | 最底一行「自动归档天数」 | 失焦或回车即保存,写的是宿主同一个字段 |
@@ -85,7 +86,7 @@ dsh plugin --profile desktop add dsh-session-autarchive
85
86
  源码分两半:
86
87
 
87
88
  - `index.js` —— 宿主半边(判定、归档、四条路由、工具)
88
- - `src/entry.jsx` —— 浏览器半边(菜单行、行首盾牌、悬停说明、设置行、5 分钟快通道)
89
+ - `src/entry.jsx` —— 浏览器半边(菜单行、行尾盾牌按钮、行首盾牌、悬停说明、设置行、5 分钟快通道)
89
90
 
90
91
  改过 `src/entry.jsx` 必须重新打包。`lib/client.js` 是提交进仓库的构建产物,缺了它界面不会加载:
91
92
 
package/lib/client.js CHANGED
@@ -30,6 +30,7 @@ __export(entry_exports, {
30
30
  ProtectHover: () => ProtectHover,
31
31
  ProtectLeading: () => ProtectLeading,
32
32
  ProtectMenuItem: () => ProtectMenuItem,
33
+ ProtectRowAction: () => ProtectRowAction,
33
34
  apply: () => apply,
34
35
  inject: () => inject
35
36
  });
@@ -140,6 +141,35 @@ function ShieldIcon({ size = 14 }) {
140
141
  }
141
142
  );
142
143
  }
144
+ function ShieldFilledIcon({ size = 14 }) {
145
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
146
+ "svg",
147
+ {
148
+ width: size,
149
+ height: size,
150
+ viewBox: "0 0 24 24",
151
+ "aria-hidden": "true",
152
+ focusable: "false",
153
+ fill: "currentColor",
154
+ stroke: "none",
155
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3l7 2.6v5.2c0 4.3-3 8.2-7 9.4-4-1.2-7-5.1-7-9.4V5.6L12 3z" })
156
+ }
157
+ );
158
+ }
159
+ var ROW_ACTION = {
160
+ flex: "none",
161
+ display: "inline-flex",
162
+ alignItems: "center",
163
+ justifyContent: "center",
164
+ width: "16px",
165
+ height: "16px",
166
+ border: "none",
167
+ borderRadius: "var(--dsw-radius-xs)",
168
+ padding: 0,
169
+ background: "transparent",
170
+ cursor: "pointer",
171
+ color: "var(--dsw-alias-label-tertiary)"
172
+ };
143
173
  var ITEM = {
144
174
  display: "flex",
145
175
  alignItems: "center",
@@ -203,6 +233,39 @@ function ProtectMenuItem(props) {
203
233
  }
204
234
  );
205
235
  }
236
+ function ProtectRowAction(props) {
237
+ const { sessionId } = props;
238
+ const state = useStore();
239
+ const [hover, setHover] = (0, import_react.useState)(false);
240
+ const [busy, setBusy] = (0, import_react.useState)(false);
241
+ const on = state.protected.has(sessionId);
242
+ const label = on ? "\u53D6\u6D88\u300C\u4E0D\u81EA\u52A8\u5F52\u6863\u300D" : "\u4E0D\u81EA\u52A8\u5F52\u6863";
243
+ const click = () => {
244
+ if (busy) return;
245
+ setBusy(true);
246
+ void toggleProtect(sessionId, !on).catch(() => {
247
+ }).finally(() => setBusy(false));
248
+ };
249
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
250
+ "button",
251
+ {
252
+ type: "button",
253
+ title: label,
254
+ "aria-label": label,
255
+ "aria-pressed": on,
256
+ disabled: busy,
257
+ onClick: click,
258
+ onMouseEnter: () => setHover(true),
259
+ onMouseLeave: () => setHover(false),
260
+ style: {
261
+ ...ROW_ACTION,
262
+ color: hover && !busy ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)",
263
+ opacity: busy ? 0.4 : 1
264
+ },
265
+ children: on ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShieldFilledIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShieldIcon, {})
266
+ }
267
+ );
268
+ }
206
269
  function ProtectLeading(props) {
207
270
  const state = useStore();
208
271
  if (!state.protected.has(props.sessionId)) return null;
@@ -380,6 +443,7 @@ function apply(ctx) {
380
443
  } catch {
381
444
  }
382
445
  place(ctx, "sidebar.workspaces.session.menu.item", 450, ProtectMenuItem);
446
+ place(ctx, "sidebar.workspaces.session.row.action", 150, ProtectRowAction);
383
447
  place(ctx, "sidebar.session.row.leading", 100, ProtectLeading);
384
448
  place(ctx, "sidebar.session.row.hover", 100, ProtectHover);
385
449
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-autarchive",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "把长期没动过的会话自动归档(默认 5 天),单个会话可以标成不归档。",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "engines": {
24
24
  "node": "^22.19.0 || >=24.0.0",
25
- "dsh": ">=0.2.1-0"
25
+ "dsh": ">=0.2.1-alpha.3"
26
26
  },
27
27
  "dsh": {
28
28
  "client": {
package/scripts/smoke.mjs CHANGED
@@ -256,6 +256,7 @@ const client = readFileSync(join(root, 'lib', 'client.js'), 'utf8')
256
256
  const idMatch = /^\s*id:\s*"([^"]+)"/m.exec(client)
257
257
  const SLOTS = [
258
258
  'sidebar.workspaces.session.menu.item',
259
+ 'sidebar.workspaces.session.row.action',
259
260
  'sidebar.session.row.leading',
260
261
  'sidebar.session.row.hover',
261
262
  'settings.general.item',
package/src/entry.jsx CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * 超期会话自动归档(浏览器半边源码)。
3
3
  *
4
- * 会话列表上三处,都只在「点开菜单」或「被标记」之后才出现:
4
+ * 会话列表上四处:
5
5
  * 1. 会话菜单里平铺一行「不自动归档」——与官方「置顶会话/重命名/分叉会话/归档会话」同层并列,不另造「更多」;
6
- * 2. 被标记的会话行首一个小盾牌;
7
- * 3. 悬停卡片里一行说明。
8
- * 行尾那排悬停图标按钮**不放东西**(不与归档/置顶图标并排)。
6
+ * 2. 行尾悬停图标组里、归档图标紧旁边一个盾牌按钮(order 150,落在归档 100 与置顶 200 之间),
7
+ * 未保护空心盾、已保护实心盾,点它开/关「不自动归档」——不用点开「更多」;
8
+ * 3. 被标记的会话行首一个小盾牌;
9
+ * 4. 悬停卡片里一行说明。
9
10
  * 设置里一行:通用设置最底「自动归档天数」(`settings.general.item`),失焦或回车即落盘。
10
11
  * 外加每 5 分钟拉一次宿主的候选清单:拿得到官方归档通道就顺手归档,拿不到就只刷新名单——
11
12
  * 判定在宿主,宿主自己有兜底定时(窗口没开也照常归档)。
@@ -140,6 +141,28 @@ function ShieldIcon({ size = 14 }) {
140
141
  )
141
142
  }
142
143
 
144
+ /**
145
+ * 「已标记」实心盾:与描边版同一个轮廓,换成填充,被保护的会话一眼可辨。
146
+ * @param props.size - 边长,默认 14。
147
+ * @returns 一个实心盾牌。
148
+ */
149
+ function ShieldFilledIcon({ size = 14 }) {
150
+ return (
151
+ <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true" focusable="false"
152
+ fill="currentColor" stroke="none">
153
+ <path d="M12 3l7 2.6v5.2c0 4.3-3 8.2-7 9.4-4-1.2-7-5.1-7-9.4V5.6L12 3z" />
154
+ </svg>
155
+ )
156
+ }
157
+
158
+ /** 行尾图标按钮:数值照 Rows.module.css 的 `.iconButton`(16px 格、14px 字形、tertiary → hover primary)。 */
159
+ const ROW_ACTION = {
160
+ flex: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
161
+ width: '16px', height: '16px', border: 'none', borderRadius: 'var(--dsw-radius-xs)',
162
+ padding: 0, background: 'transparent', cursor: 'pointer',
163
+ color: 'var(--dsw-alias-label-tertiary)',
164
+ }
165
+
143
166
  /** 菜单行:照 ui-primitives Menu.module.css 的 `.item`。 */
144
167
  const ITEM = {
145
168
  display: 'flex', alignItems: 'center', gap: '6px', width: '100%', minHeight: '34px',
@@ -201,6 +224,48 @@ export function ProtectMenuItem(props) {
201
224
  )
202
225
  }
203
226
 
227
+ /**
228
+ * 行尾悬停按钮(order 150,紧挨归档图标):盾牌一档,点它开/关「不自动归档」。
229
+ * 未保护=空心盾,已保护=实心盾;标题与 aria-label 写明当前状态要发生什么。
230
+ * 与官方置顶/归档按钮同一排同一套样式;点击不打开会话行(宿主 rowActions 已截住冒泡)。
231
+ * @param props - 行身份。
232
+ * @returns 一个图标按钮。
233
+ */
234
+ export function ProtectRowAction(props) {
235
+ const { sessionId } = props
236
+ const state = useStore()
237
+ const [hover, setHover] = useState(false)
238
+ const [busy, setBusy] = useState(false)
239
+ const on = state.protected.has(sessionId)
240
+ const label = on ? '取消「不自动归档」' : '不自动归档'
241
+ const click = () => {
242
+ if (busy) return
243
+ setBusy(true)
244
+ void toggleProtect(sessionId, !on)
245
+ .catch(() => { /* 路由没挂或写盘失败:不改本地状态,界面保持原样 */ })
246
+ .finally(() => setBusy(false))
247
+ }
248
+ return (
249
+ <button
250
+ type="button"
251
+ title={label}
252
+ aria-label={label}
253
+ aria-pressed={on}
254
+ disabled={busy}
255
+ onClick={click}
256
+ onMouseEnter={() => setHover(true)}
257
+ onMouseLeave={() => setHover(false)}
258
+ style={{
259
+ ...ROW_ACTION,
260
+ color: hover && !busy ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary)',
261
+ opacity: busy ? 0.4 : 1,
262
+ }}
263
+ >
264
+ {on ? <ShieldFilledIcon /> : <ShieldIcon />}
265
+ </button>
266
+ )
267
+ }
268
+
204
269
  /**
205
270
  * 会话行首标记:被保护的会话显示盾牌(该格只在行处于空闲态时才渲染)。
206
271
  * @param props - 行身份。
@@ -401,8 +466,10 @@ export function apply(ctx) {
401
466
  } catch { /* 清理不了也只是多一个定时器 */ }
402
467
  } catch { /* 见上 */ }
403
468
 
404
- // 三处:菜单里并列一行 + 行首标记 + 悬停说明。行尾那排图标按钮不放东西。
469
+ // 四处:菜单里并列一行 + 行尾悬停组里归档旁的盾牌按钮 + 行首标记 + 悬停说明。
470
+ // 行尾按钮 order 150:归档是 100、置顶是 200,插在两者之间,紧挨归档。
405
471
  place(ctx, 'sidebar.workspaces.session.menu.item', 450, ProtectMenuItem)
472
+ place(ctx, 'sidebar.workspaces.session.row.action', 150, ProtectRowAction)
406
473
  place(ctx, 'sidebar.session.row.leading', 100, ProtectLeading)
407
474
  place(ctx, 'sidebar.session.row.hover', 100, ProtectHover)
408
475