dsh-better-notice 5.1.7 → 5.1.9

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.md CHANGED
@@ -13,7 +13,7 @@ dsh plugin --profile <profile> add dsh-better-notice
13
13
  ```
14
14
 
15
15
  - Package: `dsh-better-notice` (module id is the same string)
16
- - Version: `5.1.7` (see [CHANGELOG.md](CHANGELOG.md))
16
+ - Version: `5.1.9` (see [CHANGELOG.md](CHANGELOG.md))
17
17
  - License: MIT
18
18
  - Settings live in **account menu → Settings → “Better Notice”** (a `settings.section` entry, id `hotlist`, order `30`)
19
19
 
@@ -93,7 +93,7 @@ Only those three states ship raster art (4 tiers each: 64 / 320 / 780 / 1560 px,
93
93
  | 🔐 `pending` | Waiting for your approval / answer / plan review (secondary glyph ❓) | — | ✅ | ✅ | ✅ | 🔈 |
94
94
  | ✅ `completed` | This plugin saw it finish, within the last 7 min | — | ✅ | ✅ | ✅ | 🔈 |
95
95
  | ⏳ `running` | Working (includes “a subagent is running, parent idle”) | — | — | ✅ | — | — |
96
- | 🧊 `idle` | Idle, and last activity is inside the *recent window* (default 5 min) | ✅ | — | ✅ | — | — |
96
+ | 🧊 `idle` | Idle, and last activity is inside the *recent window* (default 17 min) | ✅ | — | ✅ | — | — |
97
97
 
98
98
  ✅ = on by default · — = off by default · 🔈 = a bundled sound exists but **stays silent until you enable that category** in settings.
99
99
 
@@ -125,7 +125,7 @@ dsh plugin --profile <profile> add dsh-better-notice
125
125
  dsh --profile <profile>
126
126
  ```
127
127
 
128
- - **Published on npm:** [dsh-better-notice](https://www.npmjs.com/package/dsh-better-notice) (current `5.1.7`). If npm is unreachable, the same tarball is attached to the [latest release](https://github.com/MeruSu/better-notice/releases) — `dsh plugin --profile <profile> add <absolute path>/dsh-better-notice-5.1.7.tgz` gives the same result and the same restart.
128
+ - **Published on npm:** [dsh-better-notice](https://www.npmjs.com/package/dsh-better-notice) (current `5.1.9`). If npm is unreachable, the same tarball is attached to the [latest release](https://github.com/MeruSu/better-notice/releases) — `dsh plugin --profile <profile> add <absolute path>/dsh-better-notice-5.1.9.tgz` gives the same result and the same restart.
129
129
  - **Use an absolute path, forward slashes** when you install from a local directory instead of the registry: `dsh plugin` forwards the argument to pnpm *inside the profile directory*, so a relative path points into the profile.
130
130
  - Installing from a directory uses `link:` — code edits need no reinstall. `client.js` changes apply on page refresh; `index.js` / `cordis.patch.yml` / `package.json` changes need a GUI restart.
131
131
  - Install from a git checkout when you also want the TTS helper (see [Sounds](#sounds-and-the-bundled-tts-tool)). The npm package's `files` field only ships `index.js`, `client.js`, `cordis.patch.yml` and `assets`, so `tools/` is **not** in it.
@@ -150,7 +150,7 @@ The host route is created during process start-up, so a page refresh is not enou
150
150
 
151
151
  In the GUI's browser console:
152
152
 
153
- 1. `window.__HOTLIST_VERSION__` → the version string, e.g. `'5.1.7'`.
153
+ 1. `window.__HOTLIST_VERSION__` → the version string, e.g. `'5.1.9'`.
154
154
  2. `Object.keys(window.__HOTLIST__)` → `['version','settings','assets']`.
155
155
  3. Settings → the left nav has **“Better Notice”**, and the page shows a global block plus **6** collapsible category cards.
156
156
  4. Session rows in the sidebar now start with a status glyph.
@@ -258,9 +258,9 @@ Per-category placement is a second layer: `categories.<id>.surfaces.{tab,sidebar
258
258
  | I want… | Field | Values | Default |
259
259
  |---|---|---|---|
260
260
  | Keep a just-finished session in ✅ longer | `collect.completedTtlMs` | `1`–`1440` **minutes** | `7` |
261
- | Decide how stale a session may be and still count as `idle` | `collect.recentWindowMinutes` | **minutes**; settings exposes `1`–`36`, the API accepts `0`–`120` (`0` = never show idle) | `5` |
261
+ | Decide how stale a session may be and still count as `idle` | `collect.recentWindowMinutes` | **minutes**; settings offers `1`–`120`, the API also accepts `0` (= never show idle) | `17` |
262
262
 
263
- > The recent window has **two layers on purpose**: storage clamps `0`–`120` (so scripts can set 90), the settings control only offers `1`–`36`. Don't mistake the UI bound for the data bound.
263
+ > The recent window has **two layers on purpose**: the settings control offers `1`–`120`; storage clamps `0`–`120` and keeps `0` for a caller that wants "never show idle". Don't mistake the UI bound for the data bound.
264
264
 
265
265
  ### The six categories — each one is configured separately
266
266
 
@@ -313,7 +313,7 @@ Field-name-level documentation for people writing scripts.
313
313
 
314
314
  - **Collapsed card:** glyph preview, category name, `· order N`, five read-only chips (Tab / Bar / Top / Pop / Snd — highlighted = on), caret. Click the row to expand; **several cards may be open at once**. Open state lives in `sessionStorage` (`dsh.hotlist.open`), never in settings.
315
315
  - **Expanded area** (compact multi-column): ① glyph (source / upload / value / frequency slider); ② motion (`motion` + target `glyph`/`row`); ③ popout (enabled / image / entrance·ambient·exit / side·display time); ④ sound (enabled / source / preview); ⑤ order.
316
- - **Global block:** scope (workspace / all / smart), seat limit (with “auto”), **pinned seats**, completed TTL in minutes, global volume, **recent window** (minutes), `positions.tab`, `positions.topbar`, `positions.sidebar` (API only), `takeOverStatusDot`. The band block (style / label display / pill count / pill width / web preview) only exists while `positions.topbar` is on.
316
+ - **Global block:** scope (workspace / all / smart), seat limit, **pinned seats**, completed TTL in minutes, global volume, **recent window** (minutes), `positions.tab`, `positions.topbar`, `positions.sidebar` (API only), `takeOverStatusDot`. The band block (style / label display / pill count / pill width / web preview) only exists while `positions.topbar` is on.
317
317
  - Out-of-range writes are clamped. A failed write (localStorage quota) is reported in the page footer and through `window.__HOTLIST_SETTINGS__.lastError()`.
318
318
 
319
319
  ### Global defaults
@@ -336,7 +336,7 @@ Field-name-level documentation for people writing scripts.
336
336
  | `takeOverStatusDot` | `true` | take over the status dot drawn by other plugins (`dsh-sessions-manager`) |
337
337
  | `hideNativeDot` | `true` | hide the host's native status slot (no settings row since 4.4; field kept) |
338
338
  | `collect.completedTtlMs` | `420000` (7 min) | 1–1440 minutes |
339
- | `collect.recentWindowMinutes` | `5` | **minutes**. Settings offers `1`–`36`; API accepts `0`–`120` (`0` = hide `idle`) |
339
+ | `collect.recentWindowMinutes` | `17` | **minutes**. Settings offers `1`–`120`; API also accepts `0` (= hide `idle`) |
340
340
  | `sound.volume` | `60` | 0–100, global |
341
341
  | `overlay.side` | `'auto'` | `auto` / `left` / `right` |
342
342
  | `overlay.sizeMin` / `sizeMax` | `260` / `520` | 120–900 px (no settings row since 4.5; midpoint, narrowed to the viewport) |
@@ -500,7 +500,7 @@ s.lastError() // last storage write error (string or null)
500
500
 
501
501
  // settings + the three asset pools
502
502
  const api = window.__HOTLIST__;
503
- api.version // '5.1.7'
503
+ api.version // '5.1.9'
504
504
  api.settings.get() / .set(patch) / .reset() / .subscribe(fn)
505
505
  api.settings.backups() // → [{ at, version }]
506
506
  api.settings.restore(index) // → { ok, at? }
@@ -684,7 +684,7 @@ The same ground as the pitfall list at the bottom of this file, in checklist for
684
684
  ```powershell
685
685
  node --check client.js # syntax. client.js lives inside a module loader, so a syntax error only explodes when the real page boots
686
686
  node --check index.js
687
- node test/classify.test.mjs # expect a final line: PASS — 93 groups
687
+ node test/classify.test.mjs # expect a final line: PASS — 107 groups
688
688
  ```
689
689
 
690
690
  Plus a **static gate before saving**, available on the maintainer's machine only (not shipped with the repo):
@@ -700,7 +700,7 @@ cd <the directory you cloned into>
700
700
 
701
701
  node --check client.js # client.js lives inside a module loader, so a syntax error only explodes when the real page boots
702
702
  node --check index.js
703
- node test/classify.test.mjs # expect a final line: PASS — 93 groups
703
+ node test/classify.test.mjs # expect a final line: PASS — 107 groups
704
704
  ```
705
705
 
706
706
  The unit tests read the real `client.js` source, wrap it in a stub `window.__ModuleLoader__` and run it: the fake browser is hand-written (`window` / `document`, no jsdom, `React` is a stub too), the code under test is the real source. They also include a few static scans: undeclared constants, CSS selector legality, and “shortcut labels must be thunks”.
@@ -715,7 +715,7 @@ index.js host side: registers the /hotlist-sound/ audio route
715
715
  cordis.patch.yml bundle insert line
716
716
  assets/ 3 categories with 4 raster tiers of popout art + svg for all six
717
717
  assets/sounds/ bundled cues (attention.wav / complete.wav)
718
- test/classify.test.mjs unit tests (93 groups)
718
+ test/classify.test.mjs unit tests (107 groups)
719
719
  docs/ architecture, testing, extending
720
720
  tools/meru-tts/ TTS helper (generates cues; not part of the running plugin)
721
721
  ```
package/README.zh.md CHANGED
@@ -13,7 +13,7 @@ dsh plugin --profile <profile> add dsh-better-notice
13
13
  ```
14
14
 
15
15
  - 包名:`dsh-better-notice`(模块 id 与包名相同)
16
- - 版本:`5.1.7`(见 [CHANGELOG.md](CHANGELOG.md))
16
+ - 版本:`5.1.9`(见 [CHANGELOG.md](CHANGELOG.md))
17
17
  - 许可:MIT
18
18
  - 设置位置:**账号菜单 →「设置」→ 左导航「热表」**(`settings.section`,id `hotlist`,order `30`)
19
19
 
@@ -125,7 +125,7 @@ dsh plugin --profile <profile> add dsh-better-notice
125
125
  dsh --profile <profile>
126
126
  ```
127
127
 
128
- - **已发布到 npm:**[dsh-better-notice](https://www.npmjs.com/package/dsh-better-notice)(当前 `5.1.7`)。如果 npm 拉不动,同样的 tarball 也附在[最新 Release](https://github.com/MeruSu/better-notice/releases) 里——`dsh plugin --profile <profile> add <绝对路径>/dsh-better-notice-5.1.7.tgz`,效果一样,同样要重启。
128
+ - **已发布到 npm:**[dsh-better-notice](https://www.npmjs.com/package/dsh-better-notice)(当前 `5.1.9`)。如果 npm 拉不动,同样的 tarball 也附在[最新 Release](https://github.com/MeruSu/better-notice/releases) 里——`dsh plugin --profile <profile> add <绝对路径>/dsh-better-notice-5.1.9.tgz`,效果一样,同样要重启。
129
129
  - 从**本地目录**安装时要用**绝对路径、正斜杠**:`dsh plugin` 是把参数在 profile 目录里转交给 pnpm 的,相对路径会指到 profile 里去。
130
130
  - 装目录用的是 `link:`,之后改代码不用重装:`client.js` 改完刷新页面即生效;`index.js` / `cordis.patch.yml` / `package.json` 改完要重启 GUI。
131
131
  - 想连 TTS 小工具一起用就装 git 仓库(见[提示音](#提示音与仓库自带的-tts-工具))。npm 包的 `files` 只有 `index.js`、`client.js`、`cordis.patch.yml`、`assets`,`tools/` **不在包里**。
@@ -150,7 +150,7 @@ host 路由在进程启动时创建,只刷新页面不够。
150
150
 
151
151
  在 GUI 的浏览器控制台里依次确认:
152
152
 
153
- 1. `window.__HOTLIST_VERSION__` → 版本号,例如 `'5.1.7'`;
153
+ 1. `window.__HOTLIST_VERSION__` → 版本号,例如 `'5.1.9'`;
154
154
  2. `Object.keys(window.__HOTLIST__)` → `['version','settings','assets']`;
155
155
  3. 打开设置,左导航有**「热表」**,页面里是一块全局区 + **6** 张可折叠的类别卡;
156
156
  4. 侧栏的会话行前面出现了状态图标;
@@ -258,9 +258,9 @@ Windows 上的 GUI 顶部有一条 40px 标题带。插件可以往里面画一
258
258
  | 我想… | 字段 | 取值 | 默认 |
259
259
  |---|---|---|---|
260
260
  | 让刚跑完的会话在 ✅ 里多待一会儿 | `collect.completedTtlMs` | `1`–`1440` **分钟** | `7` |
261
- | 决定多久没动过的会话还算 `idle` | `collect.recentWindowMinutes` | **分钟**;设置页给 `1`–`36`,走 API 可写 `0`–`120`(`0` = 不显示闲置) | `5` |
261
+ | 决定多久没动过的会话还算 `idle` | `collect.recentWindowMinutes` | **分钟**;设置页给 `1`–`120`,走 API 也可写 `0`(= 不显示闲置) | `17` |
262
262
 
263
- > 最近窗口**故意分两层**:存储层夹 `0`–`120`(所以脚本能写 90),设置页控件只给 `1`–`36`。别把界面上的上限当成数据上限。
263
+ > 最近窗口**故意分两层**:设置页控件给 `1`–`120`;存储层夹 `0`–`120`,`0` 只留给 API 表示「不显示闲置」。别把界面上的范围当成全部。
264
264
 
265
265
  ### 六个类别:每一类都能单独设
266
266
 
@@ -313,7 +313,7 @@ Windows 上的 GUI 顶部有一条 40px 标题带。插件可以往里面画一
313
313
 
314
314
  - **折叠卡**:图标预览、类别名、`· 顺序 N`、五个只读小标签(Tab / 栏 / 顶 / 弹 / 音,高亮 = 开)、右端 caret。点整行展开,**可以同时开多张卡**。展开状态存 `sessionStorage`(`dsh.hotlist.open`),不写进设置。
315
315
  - **展开区**(紧凑多列):① 图标(来源 / 上传 / 图标值 / 频率滑块);② 动效(`motion` + 作用于 `glyph`/`row`);③ 弹出(开关 / 弹出图 / 入场·待机·退出 / 出现侧·停留时长);④ 音效(启用 / 来源 / 试听);⑤ 顺序。
316
- - **全局区**:范围(当前工作区 / 全部 / 智能)、座位上限(含「自动」)、**置顶最多显示**、刚完成保持时长(分钟)、全局音量、**最近窗口**(分钟)、`positions.tab`、`positions.topbar`、`positions.sidebar`(仅 API)、`takeOverStatusDot`。顶栏那一段(样式 / 标签显示 / 胶囊数 / 胶囊宽度 / 网页预览)只在 `positions.topbar` 打开时才存在。
316
+ - **全局区**:范围(当前工作区 / 全部 / 智能)、座位上限、**置顶最多显示**、刚完成保持时长(分钟)、全局音量、**最近窗口**(分钟)、`positions.tab`、`positions.topbar`、`positions.sidebar`(仅 API)、`takeOverStatusDot`。顶栏那一段(样式 / 标签显示 / 胶囊数 / 胶囊宽度 / 网页预览)只在 `positions.topbar` 打开时才存在。
317
317
  - 越界写入会被夹回范围。写盘失败(localStorage 配额满)会在页脚提示,也能从 `window.__HOTLIST_SETTINGS__.lastError()` 拿到原因。
318
318
 
319
319
  ### 全局默认值
@@ -500,7 +500,7 @@ s.lastError() // 最近一次写盘错误(字符串或 null)
500
500
 
501
501
  // 设置 + 三个素材池
502
502
  const api = window.__HOTLIST__;
503
- api.version // '5.1.7'
503
+ api.version // '5.1.9'
504
504
  api.settings.get() / .set(patch) / .reset() / .subscribe(fn)
505
505
  api.settings.backups() // → [{ at, version }]
506
506
  api.settings.restore(index) // → { ok, at? }
@@ -684,7 +684,7 @@ window.__HOTLIST_SETTINGS__.set({
684
684
  ```powershell
685
685
  node --check client.js # 语法。client.js 包在模块加载器里,语法错只在真机 boot 时炸
686
686
  node --check index.js
687
- node test/classify.test.mjs # 期望最后一行:PASS — 93 groups
687
+ node test/classify.test.mjs # 期望最后一行:PASS — 107 groups
688
688
  ```
689
689
 
690
690
  外加一道**保存前的静态闸门**(只在维护者机器上有,不随仓库分发):
@@ -700,7 +700,7 @@ cd <你 clone 下来的那个目录>
700
700
 
701
701
  node --check client.js # client.js 包在模块加载器里,语法错只在真机 boot 时炸
702
702
  node --check index.js
703
- node test/classify.test.mjs # 期望最后一行:PASS — 93 groups
703
+ node test/classify.test.mjs # 期望最后一行:PASS — 107 groups
704
704
  ```
705
705
 
706
706
  单测把真实的 `client.js` 源文件读进来,塞进 `window.__ModuleLoader__` 的桩里执行:假的是浏览器(手写的极简 `window` / `document`,不引 jsdom,`React` 也是桩),跑的是真源码。里面还带几道静态扫描:引用未声明的常量、注入样式选择器合法性、快捷键 label 必须是 thunk。
@@ -715,7 +715,7 @@ index.js 宿主侧:注册 /hotlist-sound/ 音频路由
715
715
  cordis.patch.yml bundle 插入行
716
716
  assets/ 3 类各 4 档弹出图素材 + 六类各一份 svg
717
717
  assets/sounds/ 内置音效(attention.wav / complete.wav)
718
- test/classify.test.mjs 单测(93 组)
718
+ test/classify.test.mjs 单测(107 组)
719
719
  docs/ 架构、测试、扩展三份维护者文档
720
720
  tools/meru-tts/ TTS 小工具(生成提示音用,不参与插件运行)
721
721
  ```
package/client.js CHANGED
@@ -79,7 +79,7 @@ window.__ModuleLoader__.load({
79
79
 
80
80
  const SLOT = 'conversation.view';
81
81
  const NS = 'hotlist';
82
- const VERSION = '5.1.7';
82
+ const VERSION = '5.1.9';
83
83
  /** Seat view ids are namespaced so a persisted leftover can be recognised and repaired. */
84
84
  const SEAT_PREFIX = 'hotlist-seat-';
85
85
  /** Every prefix ever written for a seat id; a persisted one is repaired back to `chat` on load. */
@@ -224,6 +224,11 @@ window.__ModuleLoader__.load({
224
224
  * band switched off forever.
225
225
  */
226
226
  'positions.topbar': [false],
227
+ /**
228
+ * v4.6 shipped 5 minutes as the window default. A stored 5 with no marker is that default,
229
+ * not a pick, so the current 17 replaces it; anyone who typed 5 gets a marker and keeps it.
230
+ */
231
+ 'collect.recentWindowMinutes': [5],
227
232
  };
228
233
  /**
229
234
  * Which picks the page can offer, and where their marker lives. Writing one of those fields
@@ -231,7 +236,7 @@ window.__ModuleLoader__.load({
231
236
  */
232
237
  const CHOICE_SLOTS = ['glyph', 'glyphSecondary'];
233
238
  const CHOICE_FIELDS = ['value', 'motion', 'motionTarget', 'frequency'];
234
- const CHOICE_KEY = /^(?:categories\.[a-z]+\.[a-zA-Z]+(?:\.[a-zA-Z]+)?|positions\.topbar)$/;
239
+ const CHOICE_KEY = /^(?:categories\.[a-z]+\.[a-zA-Z]+(?:\.[a-zA-Z]+)?|positions\.topbar|collect\.recentWindowMinutes)$/;
235
240
  const CHOICE_LIMIT = 256;
236
241
  /**
237
242
  * Which surface each class belongs to. The split is the point of the feature: the band is
@@ -278,13 +283,12 @@ window.__ModuleLoader__.load({
278
283
  const ALWAYS_OVERFLOW = 4;
279
284
  const TTL_MINUTES = { min: 1, max: 1440 };
280
285
  /** One global window: how recently a session moved to enter the idle tier. */
281
- const RECENT_WINDOW_MINUTES = { min: 0, max: 120, fallback: 5 };
286
+ const RECENT_WINDOW_MINUTES = { min: 0, max: 120, fallback: 17 };
282
287
  /**
283
- * What the settings page offers for that window. Storage and the API stay on
284
- * `RECENT_WINDOW_MINUTES` (0–120), so a caller may still ask for a wider window; the row only
285
- * shows the range a person actually wants, and 0 is not one of them.
288
+ * What the settings page offers for that window: the same 1–120 a person may want. Storage and
289
+ * the API keep 0 as "no window" for a caller that asks for it explicitly; the row never shows 0.
286
290
  */
287
- const RECENT_WINDOW_UI = { min: 1, max: 36 };
291
+ const RECENT_WINDOW_UI = { min: 1, max: 120 };
288
292
  /** One-slot animation bursts: never more than this many glyphs animating at once. */
289
293
  // four titles moving at once is the readability ceiling; frequency decides how often.
290
294
  const MOTION_CONCURRENCY = 4;
@@ -485,7 +489,6 @@ window.__ModuleLoader__.load({
485
489
  'settings.group.motion': '动效',
486
490
  'settings.maxSeats': '座位上限',
487
491
  'settings.maxSeatsHint': '对话区 tab 行最多显示几个会话(1–12)。',
488
- 'settings.maxSeatsAuto': '自动',
489
492
  'settings.scope': '显示范围',
490
493
  'settings.scopeHint': '哪些工作区的会话进列表(顶栏、tab 行与侧栏一致)。',
491
494
  'settings.scope.workspace': '仅当前工作区',
@@ -496,7 +499,7 @@ window.__ModuleLoader__.load({
496
499
  'settings.ttl': '刚完成保持时长(分钟)',
497
500
  'settings.ttlHint': '会话跑完后仍留在列表里的时间。',
498
501
  'settings.recentWindow': '最近对话展示时间(分钟)',
499
- 'settings.recentWindowHint': '1–36 分钟。最后活动在这个时间内的会话会进第 3 级(闲置)并出现在对话区 tab 行。API 可写 0–120。',
502
+ 'settings.recentWindowHint': '1–120 分钟。最后活动在这个时间内的会话会进第 3 级(闲置)并出现在对话区 tab 行。',
500
503
  'settings.motion': '动效',
501
504
  'settings.motion.none': '不动',
502
505
  'settings.motion.spin': '旋转',
@@ -677,7 +680,6 @@ window.__ModuleLoader__.load({
677
680
  'settings.group.motion': 'Motion',
678
681
  'settings.maxSeats': 'Maximum seats',
679
682
  'settings.maxSeatsHint': 'How many sessions the tab row may show (1–12).',
680
- 'settings.maxSeatsAuto': 'Automatic',
681
683
  'settings.scope': 'Scope',
682
684
  'settings.scopeHint': 'Which workspaces feed the list (top bar, tab row and sidebar alike).',
683
685
  'settings.scope.workspace': 'Current workspace only',
@@ -688,7 +690,7 @@ window.__ModuleLoader__.load({
688
690
  'settings.ttl': 'Keep finished seats for (minutes)',
689
691
  'settings.ttlHint': 'How long a finished session stays in the list.',
690
692
  'settings.recentWindow': 'Recent conversation window (minutes)',
691
- 'settings.recentWindowHint': '1–36 minutes. Sessions active within this window enter the third tier (idle) and show in the conversation tab row. The API accepts 0–120.',
693
+ 'settings.recentWindowHint': '1–120 minutes. Sessions active within this window enter the third tier (idle) and show in the conversation tab row.',
692
694
  'settings.motion': 'Motion',
693
695
  'settings.motion.none': 'Still',
694
696
  'settings.motion.spin': 'Spin',
@@ -992,6 +994,9 @@ const FULL_WIDTH = /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-
992
994
  const next = { ...(isPlainObject(base.choices) ? base.choices : {}) };
993
995
  if (isPlainObject(patch) === false) return next;
994
996
  if (isPlainObject(patch.positions) && typeof patch.positions.topbar === 'boolean') next['positions.topbar'] = true;
997
+ if (isPlainObject(patch.collect) && typeof patch.collect.recentWindowMinutes === 'number') {
998
+ next['collect.recentWindowMinutes'] = true;
999
+ }
995
1000
  const categories = isPlainObject(patch.categories) ? patch.categories : {};
996
1001
  for (const [category, change] of Object.entries(categories)) {
997
1002
  if (CATEGORY_IDS.includes(category) === false || isPlainObject(change) === false) continue;
@@ -1234,9 +1239,16 @@ const FULL_WIDTH = /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-
1234
1239
  ? clamp(Math.round(legacyIdle.within), RECENT_WINDOW_MINUTES.min, RECENT_WINDOW_MINUTES.max)
1235
1240
  : null;
1236
1241
  if (isPlainObject(raw.collect) && typeof raw.collect.recentWindowMinutes === 'number' && Number.isFinite(raw.collect.recentWindowMinutes)) {
1237
- next.collect.recentWindowMinutes = clamp(Math.round(raw.collect.recentWindowMinutes), RECENT_WINDOW_MINUTES.min, RECENT_WINDOW_MINUTES.max);
1242
+ const minutes = clamp(Math.round(raw.collect.recentWindowMinutes), RECENT_WINDOW_MINUTES.min, RECENT_WINDOW_MINUTES.max);
1243
+ /** Same boundary as the presets: an unmarked value equal to the old default follows the default. */
1244
+ const stale = isLegacyValue(legacyValuesOf('collect', 'recentWindowMinutes'), minutes);
1245
+ const chosen = next.choices['collect.recentWindowMinutes'] === true;
1246
+ next.collect.recentWindowMinutes = stale === true && chosen === false ? next.collect.recentWindowMinutes : minutes;
1238
1247
  } else if (legacyWithin !== null) {
1239
- next.collect.recentWindowMinutes = legacyWithin;
1248
+ /** The class field had the same 5-minute factory default, so an unmarked 5 follows the default too. */
1249
+ const stale = isLegacyValue(legacyValuesOf('collect', 'recentWindowMinutes'), legacyWithin);
1250
+ const chosen = next.choices['collect.recentWindowMinutes'] === true;
1251
+ if (stale === false || chosen === true) next.collect.recentWindowMinutes = legacyWithin;
1240
1252
  }
1241
1253
  /** v4.6 dropped the hand-written pin list: pins live in the workspace store, this only caps the row. */
1242
1254
  if (isPlainObject(raw.pinned) && typeof raw.pinned.maxSeats === 'number' && Number.isFinite(raw.pinned.maxSeats)) {
@@ -4083,19 +4095,17 @@ const FULL_WIDTH = /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-
4083
4095
  h('span', { key: 'title', className: 'hl-rowTitle' }, t('settings.maxSeats')),
4084
4096
  h('span', { key: 'hint', className: 'hl-rowHint' }, t('settings.maxSeatsHint')),
4085
4097
  ]),
4098
+ /**
4099
+ * No "automatic" option: it was a second, differently-shaped control on a row that is
4100
+ * otherwise a plain stepper, and every other seat count here is a plain stepper too. A
4101
+ * stored `'auto'` is still honoured (`buildSeatPlan` reads it), the stepper just starts
4102
+ * from the fallback and the first click writes a number.
4103
+ */
4086
4104
  h('div', { key: 'control', className: 'hl-fieldControl' }, numberStepper(
4087
4105
  settings.maxSeats === 'auto' ? MAX_SEATS.fallback : settings.maxSeats,
4088
4106
  MAX_SEATS.min,
4089
4107
  MAX_SEATS.max,
4090
4108
  (value) => patch({ maxSeats: value }),
4091
- h('button', {
4092
- key: 'auto',
4093
- type: 'button',
4094
- className: 'hl-link',
4095
- 'aria-pressed': settings.maxSeats === 'auto' ? 'true' : 'false',
4096
- onClick: () => patch({ maxSeats: settings.maxSeats === 'auto' ? MAX_SEATS.fallback : 'auto' }),
4097
- }, t('settings.maxSeatsAuto')),
4098
- settings.maxSeats === 'auto',
4099
4109
  )),
4100
4110
  ]),
4101
4111
  h('div', { key: 'pinnedSeats', className: 'hl-row', 'data-hotlist-row': 'pinned.maxSeats' }, [
package/icon.svg ADDED
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
+ <rect x="3.5" y="7" width="25" height="18" rx="4" stroke="#5B6B7C" stroke-width="2"/>
3
+ <path d="M3.5 12.5h25" stroke="#8FA3B5" stroke-width="2"/>
4
+ <circle cx="8" cy="9.75" r="1.15" fill="#B7C4CF"/>
5
+ <rect x="7" y="15.5" width="7" height="2.4" rx="1.2" fill="#5B6B7C"/>
6
+ <rect x="16.5" y="15.5" width="7" height="2.4" rx="1.2" fill="#8FA3B5"/>
7
+ <rect x="7" y="20" width="4.5" height="2.4" rx="1.2" fill="#C3CED8"/>
8
+ <path d="M22.5 21.6c0-1.4 1.1-2.4 2.4-2.4" stroke="#E0A03A" stroke-width="2" stroke-linecap="round"/>
9
+ <circle cx="24.9" cy="21.6" r="1.5" fill="#E0A03A"/>
10
+ </svg>
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Better Notice",
4
+ "description": "Turn session state into something you can see and hear: a hot-session table page, top-strip chips, sidebar state glyphs, six per-state sounds and popout artwork, with an export API for your own tooling."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Better Notice · 会话热表",
4
+ "description": "把会话状态变成看得见、听得到的提醒:热表页、顶栏胶囊、侧栏状态图标、六类音效与弹出图,还可以导出成表。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-notice",
3
- "version": "5.1.7",
3
+ "version": "5.1.9",
4
4
  "private": false,
5
5
  "description": "Better Notice — unified visual and audible session notices for DeepSeek Harness: hot-session tab seats, sidebar state glyphs, per-state sounds and popout artwork",
6
6
  "type": "module",
@@ -9,13 +9,17 @@
9
9
  ".": "./index.js",
10
10
  "./client": "./client.js",
11
11
  "./assets/*": "./assets/*",
12
- "./package.json": "./package.json"
12
+ "./package.json": "./package.json",
13
+ "./locale/*.json": "./locale/*.json",
14
+ "./icon": "./icon.svg"
13
15
  },
14
16
  "files": [
15
17
  "index.js",
16
18
  "client.js",
17
19
  "cordis.patch.yml",
18
- "assets"
20
+ "assets",
21
+ "locale",
22
+ "icon.svg"
19
23
  ],
20
24
  "keywords": [
21
25
  "dsh",
@@ -39,5 +43,6 @@
39
43
  "@deepseek-ai/dsh-client-ui-conversation"
40
44
  ]
41
45
  }
42
- }
46
+ },
47
+ "icon": "./icon.svg"
43
48
  }