dsh-better-notice 5.1.8 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -37
- package/README.zh.md +81 -38
- package/assets/completed-1560.webp +0 -0
- package/assets/completed-320.webp +0 -0
- package/assets/completed-64.webp +0 -0
- package/assets/completed-780.webp +0 -0
- package/assets/error-1560.webp +0 -0
- package/assets/error-320.webp +0 -0
- package/assets/error-64.webp +0 -0
- package/assets/error-780.webp +0 -0
- package/assets/manifest.json +30 -30
- package/assets/pending-1560.webp +0 -0
- package/assets/pending-320.webp +0 -0
- package/assets/pending-64.webp +0 -0
- package/assets/pending-780.webp +0 -0
- package/client.js +178 -3
- package/icon.svg +10 -0
- package/index.js +142 -4
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +10 -4
- package/tools/attn-ring/README.md +86 -0
- package/tools/attn-ring/Show-AttnRing.ps1 +309 -0
- package/tools/attn-ring/ring.cmd +4 -0
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.
|
|
16
|
+
- Version: `5.2.0` (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
|
|
|
@@ -46,7 +46,7 @@ dsh plugin --profile <profile> add dsh-better-notice
|
|
|
46
46
|
|
|
47
47
|
## What it does
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Five surfaces, one shared decision:
|
|
50
50
|
|
|
51
51
|
1. **Conversation tab row** — each hot session takes one tab seat; click it to switch. The current session does not take a seat (it *is* the “Chat” tab).
|
|
52
52
|
|
|
@@ -60,6 +60,8 @@ Four surfaces, one shared decision:
|
|
|
60
60
|
|
|
61
61
|
4. **On state change** — one sound plus one popout image that slides in from a window edge.
|
|
62
62
|
|
|
63
|
+
5. **The whole screen, opt-in** — with one of the three ring switches on, the amber ring rises above every window for a few seconds. Off by default. See [The full-screen ring](#the-full-screen-ring).
|
|
64
|
+
|
|
63
65
|
The glyph can also be your own square picture instead of an emoji:
|
|
64
66
|
|
|
65
67
|

|
|
@@ -70,15 +72,17 @@ The glyph can also be your own square picture instead of an emoji:
|
|
|
70
72
|
|
|
71
73
|
**Only three states pop a picture: `error`, `pending`, `completed`.** In words: *something broke*, *you have to approve / choose / review*, *it just finished*. `pinned`, `running` and `idle` never pop — an idle session leaning into your screen would be noise, not news. A popout stays for a few seconds and then leaves on its own (`popout.displayMs`, default 7 s, configurable per category).
|
|
72
74
|
|
|
73
|
-
**The bundled artwork follows
|
|
75
|
+
**The bundled artwork is animated and follows the interruption semantics**, and it is **the author's placeholder art**, not a finished design:
|
|
74
76
|
|
|
75
|
-
| State |
|
|
77
|
+
| State | Clip | What moves | Why this pose |
|
|
76
78
|
|---|---|---|---|
|
|
77
|
-
| `error` |
|
|
78
|
-
| `pending` |
|
|
79
|
-
| `completed` |
|
|
79
|
+
| `error` | smack | the character punches the big red button | a hard whack reads as *something broke* |
|
|
80
|
+
| `pending` | smack | same clip | a whack at you: **you** have to act |
|
|
81
|
+
| `completed` | wave | the character waves hello | a wave reads as calm / settled: done |
|
|
82
|
+
|
|
83
|
+
Only those three states ship raster art (4 tiers each: 64 / 320 / 780 / 1560 px, `.webp`), and the art is **animated WebP** — no still frames to swap per tick, the browser plays it. The source clips are 500×500, which is 1:1 with the popout's default 390 CSS px at 1.25 DPR, so the `780` and `1560` slots hold the same 500 px animation (there is no more source resolution to be had; the browser scales it up if you enlarge the popout). The `320` tier is the same clip downscaled, and `64` — used only as a settings thumbnail — is a still from frame 0.
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
`pinned`, `running` and `idle` have no raster art — their popouts fall back to the packaged SVG and then to the emoji glyph, which costs nothing functionally: those three do not pop by default anyway. The animated clips are the reason the artwork is ~1.6 MB: a 10-frame 500 px clip cannot be compressed down to a still's size, and trimming frames only makes the motion look broken.
|
|
82
86
|
|
|
83
87
|
**You are meant to replace all of it.** Settings → the category card → *Popout → upload image*, or `window.__HOTLIST__.assets.add(category, 'popout', { name, dataUrl })`. Any square transparent PNG/JPG/WebP/GIF works; SVG is rejected.
|
|
84
88
|
|
|
@@ -125,10 +129,10 @@ dsh plugin --profile <profile> add dsh-better-notice
|
|
|
125
129
|
dsh --profile <profile>
|
|
126
130
|
```
|
|
127
131
|
|
|
128
|
-
- **Published on npm:** [dsh-better-notice](https://www.npmjs.com/package/dsh-better-notice) (current `5.
|
|
132
|
+
- **Published on npm:** [dsh-better-notice](https://www.npmjs.com/package/dsh-better-notice) (current `5.2.0`). 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.2.0.tgz` gives the same result and the same restart.
|
|
129
133
|
- **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
134
|
- 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
|
-
- 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
|
|
135
|
+
- 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 ships `index.js`, `client.js`, `cordis.patch.yml`, `assets`, `locale`, `icon.svg` and `tools/attn-ring`; `tools/meru-tts` is **not** in it.
|
|
132
136
|
|
|
133
137
|
Uninstall:
|
|
134
138
|
|
|
@@ -150,7 +154,7 @@ The host route is created during process start-up, so a page refresh is not enou
|
|
|
150
154
|
|
|
151
155
|
In the GUI's browser console:
|
|
152
156
|
|
|
153
|
-
1. `window.__HOTLIST_VERSION__` → the version string, e.g. `'5.
|
|
157
|
+
1. `window.__HOTLIST_VERSION__` → the version string, e.g. `'5.2.0'`.
|
|
154
158
|
2. `Object.keys(window.__HOTLIST__)` → `['version','settings','assets']`.
|
|
155
159
|
3. Settings → the left nav has **“Better Notice”**, and the page shows a global block plus **6** collapsible category cards.
|
|
156
160
|
4. Session rows in the sidebar now start with a status glyph.
|
|
@@ -224,6 +228,57 @@ All three modes **strip the emoji already in the session title**, so you never g
|
|
|
224
228
|
|
|
225
229
|
Settings shows this as six chips on the second row of the category card, and the whole row disappears while the mode is `text` or `icon`, because it would do nothing there.
|
|
226
230
|
|
|
231
|
+
## The full-screen ring
|
|
232
|
+
|
|
233
|
+
Every surface above draws inside the page. When a session needs you and the GUI
|
|
234
|
+
is buried behind something else, this plugin can also ring the **whole screen**:
|
|
235
|
+
an amber ring with a glow rises above every window for a few seconds.
|
|
236
|
+
|
|
237
|
+
It is **off by default** — three switches in their own group right below the
|
|
238
|
+
global block, all `false`:
|
|
239
|
+
|
|
240
|
+
| Switch | Rings when | Default |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| `ring.pending` | a session starts waiting for your approval / answer / plan review | `false` |
|
|
243
|
+
| `ring.error` | a session fails its turn | `false` |
|
|
244
|
+
| `ring.completed` | a session just finishes | `false` |
|
|
245
|
+
|
|
246
|
+
- It fires on the **moment a session enters that state**, not on every repaint.
|
|
247
|
+
The judgement is the same edge detector the sounds use: blank sessions and
|
|
248
|
+
subagents are skipped, and the order is `error > pending > completed`. It has
|
|
249
|
+
its own 1500 ms throttle, so a burst of state changes cannot stack windows —
|
|
250
|
+
and the first sync after a page load only arms it, so opening the GUI never
|
|
251
|
+
rings.
|
|
252
|
+
- It stays a few seconds and leaves on its own. **A click or `Esc` closes it at
|
|
253
|
+
once.**
|
|
254
|
+
- It never takes keyboard focus, so typing in the session underneath keeps
|
|
255
|
+
working. The click that dismisses it is consumed by the ring — that one click
|
|
256
|
+
does not reach the window behind it.
|
|
257
|
+
- Turning a switch on is a **user choice**: it is recorded in `choices`, so the
|
|
258
|
+
factory-preset upgrade rule will not switch it back off.
|
|
259
|
+
|
|
260
|
+
**Why it is a separate process.** A page cannot draw outside its own window;
|
|
261
|
+
covering the Windows taskbar and other applications is exactly the thing a DOM
|
|
262
|
+
element cannot do. So the ring is drawn by `tools/attn-ring/Show-AttnRing.ps1` —
|
|
263
|
+
PowerShell + WPF, zero dependencies — and it **ships inside the npm package**
|
|
264
|
+
(`files` includes `tools/attn-ring`), so an installed copy can ring on its own.
|
|
265
|
+
|
|
266
|
+
The page only asks the host half to start it:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
GET /hotlist-ring?seconds=4&scope=Primary&anim=Expand
|
|
270
|
+
x-hotlist-ring: 1 # required; without it the route refuses
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
| Gate | Answer | Why |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| no `x-hotlist-ring: 1` header | `403` | a bare `<img src>` cannot set a header, so no web page can pop a full-screen window |
|
|
276
|
+
| a second request within 2.5 s | `429` | a burst of state changes must not stack processes |
|
|
277
|
+
| the script is missing from the install | `503` | a partial install stays silent instead of throwing |
|
|
278
|
+
|
|
279
|
+
Windows only, and it covers the primary display — see
|
|
280
|
+
[Known limitations](#known-limitations).
|
|
281
|
+
|
|
227
282
|
## Customization index
|
|
228
283
|
|
|
229
284
|
Field reference tables are below; this section is organized by intent. Everything here is editable in **Settings → “Better Notice”** and applies immediately, no reload. The few rows marked *API only* have no control yet — use `window.__HOTLIST_SETTINGS__.set(...)`.
|
|
@@ -314,6 +369,7 @@ Field-name-level documentation for people writing scripts.
|
|
|
314
369
|
- **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
370
|
- **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
371
|
- **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.
|
|
372
|
+
- **Full-screen ring block**, directly below the global block: `ring.pending`, `ring.error`, `ring.completed` — all off. See [The full-screen ring](#the-full-screen-ring).
|
|
317
373
|
- Out-of-range writes are clamped. A failed write (localStorage quota) is reported in the page footer and through `window.__HOTLIST_SETTINGS__.lastError()`.
|
|
318
374
|
|
|
319
375
|
### Global defaults
|
|
@@ -334,6 +390,7 @@ Field-name-level documentation for people writing scripts.
|
|
|
334
390
|
| `topbar.customScope` | all `true` | six booleans, only read in `custom` mode |
|
|
335
391
|
| `choices` | `{}` | keys the user actually picked; written by `writeSettings()`, never by hand |
|
|
336
392
|
| `takeOverStatusDot` | `true` | take over the status dot drawn by other plugins (`dsh-sessions-manager`) |
|
|
393
|
+
| `ring.pending` / `ring.error` / `ring.completed` | all `false` | one ring on top of the screen when that state appears; Windows only, primary display only — see [The full-screen ring](#the-full-screen-ring) |
|
|
337
394
|
| `hideNativeDot` | `true` | hide the host's native status slot (no settings row since 4.4; field kept) |
|
|
338
395
|
| `collect.completedTtlMs` | `420000` (7 min) | 1–1440 minutes |
|
|
339
396
|
| `collect.recentWindowMinutes` | `17` | **minutes**. Settings offers `1`–`120`; API also accepts `0` (= hide `idle`) |
|
|
@@ -412,7 +469,7 @@ Rules for agents:
|
|
|
412
469
|
|
|
413
470
|
The **GitHub repository** contains `tools/meru-tts/`: a single-file Python script that turns one sentence into an mp3 or wav. It is **not part of the running plugin** — it only produces assets.
|
|
414
471
|
|
|
415
|
-
> **Limit up front:** `
|
|
472
|
+
> **Limit up front:** the npm package now ships `tools/attn-ring` — the full-screen ring script has to travel with the plugin, otherwise an installed copy cannot ring — but it still does **not** ship `tools/meru-tts`: 1.6 MB of `pylibs` has no business on every machine that installs a notification plugin. So “install the plugin” and “build your own cues” remain two separate paths, and this one still requires cloning the repo.
|
|
416
473
|
|
|
417
474
|
### 2. Where the API key comes from (there is a free tier)
|
|
418
475
|
|
|
@@ -500,7 +557,7 @@ s.lastError() // last storage write error (string or null)
|
|
|
500
557
|
|
|
501
558
|
// settings + the three asset pools
|
|
502
559
|
const api = window.__HOTLIST__;
|
|
503
|
-
api.version // '5.
|
|
560
|
+
api.version // '5.2.0'
|
|
504
561
|
api.settings.get() / .set(patch) / .reset() / .subscribe(fn)
|
|
505
562
|
api.settings.backups() // → [{ at, version }]
|
|
506
563
|
api.settings.restore(index) // → { ok, at? }
|
|
@@ -541,6 +598,7 @@ More examples (bulk import, loading assets from URLs, the three preview paths) i
|
|
|
541
598
|
10. **Motion is sampled, not always-on.** Each category rolls its own frequency; on a hit, one seat of that category plays one animation. At most 4 animate at once (`MOTION_CONCURRENCY`).
|
|
542
599
|
11. **Sounds have a 1.5 s global throttle.** A cue arriving less than 1500 ms after the previous one is dropped; the first round after a cold start only records. This is verified by source and unit test (group `sound player dedupe`) — on a real machine it is hard to produce two genuine transitions that fast.
|
|
543
600
|
12. **Settings are bound to the page origin.** `localStorage` belongs to the origin, so a different GUI port means a different origin.
|
|
601
|
+
13. **The full-screen ring is Windows-only and covers the primary display.** The script is PowerShell + WPF, and the page can only *ask* the host to run it: on other platforms the switch saves your choice and nothing rises. It also covers the primary screen only, so a second monitor does not flash.
|
|
544
602
|
|
|
545
603
|
## Modifying this plugin
|
|
546
604
|
|
|
@@ -554,8 +612,8 @@ One npm package, two halves:
|
|
|
554
612
|
|
|
555
613
|
| File | Runs in | Does |
|
|
556
614
|
|---|---|---|
|
|
557
|
-
| `index.js` | Host (Node) | **
|
|
558
|
-
| `client.js` | Browser | **All UI logic** (~
|
|
615
|
+
| `index.js` | Host (Node) | **Two prefix routes, nothing else**: `/hotlist-sound` serves `assets/sounds/*.wav` (the host's shared asset route only accepts images, so audio would 404 there) and `/hotlist-ring` starts the full-screen ring script. No UI, no state. |
|
|
616
|
+
| `client.js` | Browser | **All UI logic** (~7,000 lines, single file, no build): tab seats, sidebar glyphs, top bar, popout, sounds, ring triggers, settings page. Registered via `window.__ModuleLoader__.load({ id: '<package name>' })`. |
|
|
559
617
|
| `cordis.patch.yml` | Config | The bundle insert line that makes a profile aware of the package. |
|
|
560
618
|
|
|
561
619
|
`client.js` pulls in no dependencies except `require('react')`; DOM is written with `h()` (an alias of `React.createElement`). No bundling and no `npm install` are needed to change it.
|
|
@@ -569,24 +627,24 @@ ctx.get('uiWorkspace').workspaces.list ← workspace grouping + pinn
|
|
|
569
627
|
ctx.remote.$on('api-session/error', …) ← agent-level errors
|
|
570
628
|
│
|
|
571
629
|
▼
|
|
572
|
-
classify() category + token client.js:
|
|
573
|
-
sortSeats() ordering within a category client.js:
|
|
574
|
-
buildSeatPlan() apply limits, assign order client.js:
|
|
630
|
+
classify() category + token client.js:2082
|
|
631
|
+
sortSeats() ordering within a category client.js:2148
|
|
632
|
+
buildSeatPlan() apply limits, assign order client.js:2171
|
|
575
633
|
│
|
|
576
634
|
┌───────────┴────────────┬──────────────────┐
|
|
577
635
|
▼ ▼ ▼
|
|
578
636
|
mountTabSeats() createSidebarAdapter() HotlistSettingsPage()
|
|
579
637
|
tab seats (conversation.view) sidebar DOM settings page (settings.section)
|
|
580
|
-
client.js:
|
|
638
|
+
client.js:5582 client.js:2993 client.js:3741
|
|
581
639
|
```
|
|
582
640
|
|
|
583
|
-
The three pure functions (`classify` / `sortSeats` / `buildSeatPlan`) never touch the DOM and can be fed fake data — they are the safest entry point for behaviour changes. Their results are also the only debugging handle besides `window.__HOTLIST__`: the export table at the end of `client.js` (`client.js:
|
|
641
|
+
The three pure functions (`classify` / `sortSeats` / `buildSeatPlan`) never touch the DOM and can be fed fake data — they are the safest entry point for behaviour changes. Their results are also the only debugging handle besides `window.__HOTLIST__`: the export table at the end of `client.js` (`client.js:6932` onwards) exposes `classify`, `sortSeats`, `buildSeatPlan`, `readSettings`, `defaultSettings`, `sanitizeSettings`, `sidebarStateOf`, `HotlistSettingsPage` and more, and the unit tests run the real source through it.
|
|
584
642
|
|
|
585
643
|
### Touch points: change one thing, change these
|
|
586
644
|
|
|
587
645
|
**1) Add a category**
|
|
588
646
|
|
|
589
|
-
`CATEGORY_IDS` (`client.js:
|
|
647
|
+
`CATEGORY_IDS` (`client.js:184`) → `CATEGORY_ORDER` (`client.js:185`) → `CATEGORY_GLYPHS` (`client.js:204`) → `CATEGORY_MOTION` (`client.js:205`) → `CATEGORY_SURFACES` (`client.js:258`, value is `{tab, sidebar, topbar}`) → defaults in `defaultCategory()` (`:359`) → accept the key in `sanitizeSettings()` (`:1192`) → one rule in `classify()` (`:2082`) → one entry each in the dictionaries `ZH` (`:497`) / `EN` (`:696`) → one new group in `test/classify.test.mjs`. Missing `CATEGORY_GLYPHS` usually just means a blank icon; missing `CATEGORY_SURFACES` throws inside `defaultCategory()`.
|
|
590
648
|
|
|
591
649
|
**2) Add a setting**
|
|
592
650
|
|
|
@@ -594,7 +652,7 @@ Five places, or you get “writes but does not read” / “Chinese leaking into
|
|
|
594
652
|
|
|
595
653
|
**3) Add a motion**
|
|
596
654
|
|
|
597
|
-
Add the name to `MOTION_NAMES` (`client.js:
|
|
655
|
+
Add the name to `MOTION_NAMES` (`client.js:267`) → a duration to `MOTION_MS` (`client.js:2725`) → a `@keyframes hl-<name>` to `SIDEBAR_STYLE` (`client.js:2747`) (animate `transform` / `opacity` only, never layout-triggering properties) → for the title variant, one entry each in `TEXT_MOTION` (`client.js:2734`) and `TEXT_BURST` (`client.js:2741`) plus `@keyframes hl-text-<name>`. Concurrency is capped by `MOTION_CONCURRENCY` (`client.js:305`).
|
|
598
656
|
|
|
599
657
|
**4) Add a popout-art slot**
|
|
600
658
|
|
|
@@ -602,11 +660,11 @@ Art is `assets/<category>-<tier>.webp` (tiers 64 / 320 / 780 / 1560) for the thr
|
|
|
602
660
|
|
|
603
661
|
**5) Replace a bundled sound**
|
|
604
662
|
|
|
605
|
-
`BUILTIN_SOUND` (`client.js:
|
|
663
|
+
`BUILTIN_SOUND` (`client.js:325`) is the only table mapping category → filename (`SOUND_CATEGORIES` (`client.js:327`) is derived from it). Put the new wav in `assets/sounds/`, edit the table, keep `assets/sounds/manifest.json` in sync. The URL is built from `SOUND_URL` (`client.js:324`) as `/hotlist-sound/<file>`; the actual serving is `serveSound()` in `index.js` — a **GUI restart is mandatory** (host routes register at start-up).
|
|
606
664
|
|
|
607
665
|
**6) Change ordering or priority**
|
|
608
666
|
|
|
609
|
-
- **Display order:** `CATEGORY_ORDER` (`client.js:
|
|
667
|
+
- **Display order:** `CATEGORY_ORDER` (`client.js:185`) plus each category's `order`, consumed by `sortSeats()` (`:2148`). Placement only, never classification.
|
|
610
668
|
- **Classification priority:** the order of the `if`s inside `classify()` (`:1477`). A session belongs to one category; **first match wins**.
|
|
611
669
|
- They are separate on purpose: users may change `order` but cannot decide who wins a session. Editing `classify` changes semantics — always re-run the `category order` and `sidebar priority` test groups.
|
|
612
670
|
|
|
@@ -616,10 +674,10 @@ Sidebar titles can carry a CSS animation; **tab seats cannot** — the tab label
|
|
|
616
674
|
|
|
617
675
|
- Field: `settings.categories.<id>.glyph.motionTarget`
|
|
618
676
|
- Values: `'glyph'` = icon only; `'row'` = icon + title, started and stopped together.
|
|
619
|
-
- **Since 4.6.0 the default is `'row'`** (`defaultGlyph()`, `client.js:
|
|
677
|
+
- **Since 4.6.0 the default is `'row'`** (`defaultGlyph()`, `client.js:346`).
|
|
620
678
|
|
|
621
679
|
```js
|
|
622
|
-
// Option A: change the default for every category (source edit, defaultGlyph() at client.js:
|
|
680
|
+
// Option A: change the default for every category (source edit, defaultGlyph() at client.js:346)
|
|
623
681
|
function defaultGlyph(category) {
|
|
624
682
|
return {
|
|
625
683
|
kind: 'emoji',
|
|
@@ -634,7 +692,7 @@ function defaultGlyph(category) {
|
|
|
634
692
|
```
|
|
635
693
|
|
|
636
694
|
```js
|
|
637
|
-
// Option B: per category (source edit, override inside defaultCategory() at client.js:
|
|
695
|
+
// Option B: per category (source edit, override inside defaultCategory() at client.js:359)
|
|
638
696
|
// e.g. only "pending" animates its title, everything else animates the icon only:
|
|
639
697
|
glyph: { ...defaultGlyph(category), motionTarget: category === 'pending' ? 'row' : 'glyph' },
|
|
640
698
|
```
|
|
@@ -648,16 +706,16 @@ window.__HOTLIST_SETTINGS__.set({
|
|
|
648
706
|
|
|
649
707
|
**Option C has a trap you must know:** `set` merges only two levels, and `glyph` / `popout` / `sound` are replaced as whole blocks — the line above resets `pending.glyph`'s `value`, `motion` and `frequency`. Do `get()`, edit the whole object, `set()`.
|
|
650
708
|
|
|
651
|
-
Adding a new title motion: one entry each in `TEXT_MOTION` (`client.js:
|
|
709
|
+
Adding a new title motion: one entry each in `TEXT_MOTION` (`client.js:2734`, looping) and `TEXT_BURST` (`client.js:2741`, one-shot), a duration in `MOTION_MS` (`client.js:2725`), and `@keyframes hl-text-<name>` in `SIDEBAR_STYLE` (`client.js:2747`). Title keyframes **only animate `opacity` and 1–2 px of translation, never `scale`** (text goes blurry); concurrency is still capped by `MOTION_CONCURRENCY` (`client.js:305`) and the rate is the per-category `glyph.frequency` (0–1, `0` = never).
|
|
652
710
|
|
|
653
711
|
### Upgrades never overwrite user settings
|
|
654
712
|
|
|
655
|
-
**Promise: upgrading the plugin never changes a setting the user already chose.** A value that still equals one of our own earlier factory defaults is not a choice — it follows the current default (the `LEGACY_VALUES` bullet below). Everything else on disk wins. A field the user *did* pick is his even when the value coincides with a preset: `choices` (`client.js:
|
|
713
|
+
**Promise: upgrading the plugin never changes a setting the user already chose.** A value that still equals one of our own earlier factory defaults is not a choice — it follows the current default (the `LEGACY_VALUES` bullet below). Everything else on disk wins. A field the user *did* pick is his even when the value coincides with a preset: `choices` (`client.js:1967`) records those picks and the preset rule reads it before rewriting anything.
|
|
656
714
|
|
|
657
715
|
How that is guaranteed:
|
|
658
716
|
|
|
659
717
|
- **Defaults only fill gaps**; anything on disk wins. `sanitizeSettings()` starts from the defaults and then, field by field, keeps any valid stored value — so changing `CATEGORY_ORDER` or `defaultGlyph()` is invisible to existing users.
|
|
660
|
-
- **Factory presets are ours, not the user's.** A stored value that only ever equalled a default an earlier release shipped — `💤` for the idle glyph, `blink` / `none` motion, `motionTarget: 'glyph'`, frequency `1`, sound off — follows the current default, so an existing install does eventually see the ice cube and the 4.6.3 motion table without the user re-doing anything. The table is `LEGACY_VALUES` (`client.js:
|
|
718
|
+
- **Factory presets are ours, not the user's.** A stored value that only ever equalled a default an earlier release shipped — `💤` for the idle glyph, `blink` / `none` motion, `motionTarget: 'glyph'`, frequency `1`, sound off — follows the current default, so an existing install does eventually see the ice cube and the 4.6.3 motion table without the user re-doing anything. The table is `LEGACY_VALUES` (`client.js:214`); the same boundary already used for a category `order` that equals a historical default (`LEGACY_DEFAULTS`). As soon as the user edits that field the value is theirs and we never touch it again, and a value nobody ever shipped as a default is kept byte for byte. "Edited" is recorded explicitly: `choices` `{ "<path>": true }` (`client.js:1009-1050`) is written by `writeSettings()` from the incoming patch only, so a value the preset rule rewrote can never pass for a choice; `resetSettings()` clears the whole map because a reset is a fresh acceptance of the presets.
|
|
661
719
|
- **Adding a field:** a fallback in `sanitize*` is enough, **do not bump `SETTINGS_VERSION`**. A version bump claims “a migration is required”, and a wrong migration silently wipes user data — no structural change, no version change.
|
|
662
720
|
- **Real structural changes** (dropping a field, changing its meaning) must keep the chain “read old → convert → drop the old field” (today that lives in `sanitizeSettings()`; the v3.1 → v4 migration went through it) and bump `SETTINGS_VERSION` by the smallest step. The `migrate v3 to v4` test group is the acceptance test for that chain.
|
|
663
721
|
|
|
@@ -684,7 +742,7 @@ The same ground as the pitfall list at the bottom of this file, in checklist for
|
|
|
684
742
|
```powershell
|
|
685
743
|
node --check client.js # syntax. client.js lives inside a module loader, so a syntax error only explodes when the real page boots
|
|
686
744
|
node --check index.js
|
|
687
|
-
node test/classify.test.mjs # expect a final line: PASS —
|
|
745
|
+
node test/classify.test.mjs # expect a final line: PASS — 109 groups
|
|
688
746
|
```
|
|
689
747
|
|
|
690
748
|
Plus a **static gate before saving**, available on the maintainer's machine only (not shipped with the repo):
|
|
@@ -700,7 +758,7 @@ cd <the directory you cloned into>
|
|
|
700
758
|
|
|
701
759
|
node --check client.js # client.js lives inside a module loader, so a syntax error only explodes when the real page boots
|
|
702
760
|
node --check index.js
|
|
703
|
-
node test/classify.test.mjs # expect a final line: PASS —
|
|
761
|
+
node test/classify.test.mjs # expect a final line: PASS — 109 groups
|
|
704
762
|
```
|
|
705
763
|
|
|
706
764
|
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,8 +773,9 @@ index.js host side: registers the /hotlist-sound/ audio route
|
|
|
715
773
|
cordis.patch.yml bundle insert line
|
|
716
774
|
assets/ 3 categories with 4 raster tiers of popout art + svg for all six
|
|
717
775
|
assets/sounds/ bundled cues (attention.wav / complete.wav)
|
|
718
|
-
test/classify.test.mjs unit tests (
|
|
776
|
+
test/classify.test.mjs unit tests (109 groups)
|
|
719
777
|
docs/ architecture, testing, extending
|
|
778
|
+
tools/attn-ring/ full-screen ring script (PowerShell + WPF; ships in the package)
|
|
720
779
|
tools/meru-tts/ TTS helper (generates cues; not part of the running plugin)
|
|
721
780
|
```
|
|
722
781
|
|
|
@@ -724,7 +783,7 @@ tools/meru-tts/ TTS helper (generates cues; not part of the running plugi
|
|
|
724
783
|
|
|
725
784
|
Eleven things that have already cost real debugging time — including a broken release. Each one says what happens if you ignore it.
|
|
726
785
|
|
|
727
|
-
1. **Callbacks handed to the host must be thunks, not values.** The host calls `command.label()` itself inside `refreshLabels()` (`dsh-client-shortcuts/lib/client.js:1190`). Pass a string and you get `TypeError: command.label is not a function`, which takes `uiWorkspace` down with it: **the whole Web UI boots to `Failed to load plugins` with roughly twenty plugin entries stuck pending.** That is exactly how v4.3.0 shipped broken. The settings section's `label: () => t('settings.nav')` (`client.js:
|
|
786
|
+
1. **Callbacks handed to the host must be thunks, not values.** The host calls `command.label()` itself inside `refreshLabels()` (`dsh-client-shortcuts/lib/client.js:1190`). Pass a string and you get `TypeError: command.label is not a function`, which takes `uiWorkspace` down with it: **the whole Web UI boots to `Failed to load plugins` with roughly twenty plugin entries stuck pending.** That is exactly how v4.3.0 shipped broken. The settings section's `label: () => t('settings.nav')` (`client.js:5230`) is a thunk for the same reason. The unit test cannot catch it because the stubbed `shortcuts` service never invokes `label()` — **if the host will call a callback you pass in, make the stub actually call it.**
|
|
728
787
|
*If you ignore it:* the plugin does not just fail to work, it prevents the entire page from loading.
|
|
729
788
|
|
|
730
789
|
2. **Package name, module id and bundle entry must all be the same string.** `package.json`'s `name` = the `id` in `window.__ModuleLoader__.load({ id })` = the entry recorded in the profile's `dsh.profile.bundles`.
|
|
@@ -736,7 +795,7 @@ Eleven things that have already cost real debugging time — including a broken
|
|
|
736
795
|
4. **The static routes this plugin registers are unauthenticated.** `/hotlist-sound/*` and `/plugins/**/assets/*` do not check a token — anyone who can reach the port can fetch them.
|
|
737
796
|
*If you ignore it:* you publish a path that leaks whatever you put behind it. Public assets only, never user data.
|
|
738
797
|
|
|
739
|
-
5. **User settings may never be overwritten.** `sanitizeSettings()` (`client.js:
|
|
798
|
+
5. **User settings may never be overwritten.** `sanitizeSettings()` (`client.js:1192`) is pure and falls back **per field, never wholesale**; unknown keys are parked in `PRESERVED_KEY` (`client.js:169`) and come back on the next read; out-of-range values are recorded (`outOfRangeOf`, `client.js:1692`) and restored; the last five writes live in a rolling backup ring (`BACKUP_KEY`, `client.js:171`, `BACKUP_LIMIT = 5`). Never rebuild the settings object from `defaultSettings()` inside a migration — that silently wipes every custom value the user has.
|
|
740
799
|
*If you ignore it:* a user loses their whole configuration and cannot tell when it happened. Recovery is only `window.__HOTLIST__.settings.backups()` / `.restore(i)`.
|
|
741
800
|
|
|
742
801
|
6. **Only bump `SETTINGS_VERSION` when the data structure really changed — and then write the migration in the same commit.** Cosmetic defaults and new optional fields need a fallback in `sanitize*`, not a version bump.
|
|
@@ -745,10 +804,10 @@ Eleven things that have already cost real debugging time — including a broken
|
|
|
745
804
|
7. **A `client.js` change needs a page refresh; everything else needs a GUI restart.** `index.js`, `package.json` and `cordis.patch.yml` are read at host start-up (the `/hotlist-sound` route is created there). Loaded client modules are not hot-replaced either.
|
|
746
805
|
*If you ignore it:* you debug stale code and conclude the change did nothing.
|
|
747
806
|
|
|
748
|
-
8. **Three hard caps are hard-coded, not settings.** `PLAN_MAX_ROWS = 37` (`client.js:
|
|
807
|
+
8. **Three hard caps are hard-coded, not settings.** `PLAN_MAX_ROWS = 37` (`client.js:292`) trims from the tail so the priority head always survives; `MOTION_CONCURRENCY = 4` (`client.js:305`) limits simultaneous animation; `PINNED_SEATS = {min: 0, max: 12}` (`client.js:289`).
|
|
749
808
|
*If you ignore it:* you "fix" a truncated plan by raising `maxSeats`, which cannot exceed the row cap.
|
|
750
809
|
|
|
751
|
-
9. **There is exactly one source of truth for priority, and it is two functions.** `classify()` (`client.js:
|
|
810
|
+
9. **There is exactly one source of truth for priority, and it is two functions.** `classify()` (`client.js:2082-2140`) decides tab seats and popouts; `sidebarStateOf()` (`client.js:2955-2991`) decides the sidebar glyph. They share the order `error > pending > completed > running > pinned > idle` but apply different extra conditions.
|
|
752
811
|
*If you ignore it:* you edit one and the same session shows two different states on two surfaces — that was a real bug before 4.6.0.
|
|
753
812
|
|
|
754
813
|
10. **`dsh plugin add` on an already-installed plugin does not rewrite the bundle entry, and a running GUI holds the files open.** Remove first, then add; and close that profile's GUI before installing over it, otherwise the copy fails with `EPERM`.
|
|
@@ -765,6 +824,7 @@ Eleven things that have already cost real debugging time — including a broken
|
|
|
765
824
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — module structure, classification and ordering semantics, and the constraints that look dirty but would get worse if “cleaned up”
|
|
766
825
|
- [docs/TESTING.md](docs/TESTING.md) — what each unit-test group covers, and the real-machine acceptance checklist
|
|
767
826
|
- [docs/EXTENDING.md](docs/EXTENDING.md) — public API, settings shape, asset limits, bulk import
|
|
827
|
+
- [tools/attn-ring/README.md](tools/attn-ring/README.md) — the ring script's own docs (ships in the package)
|
|
768
828
|
- [tools/meru-tts/README.md](tools/meru-tts/README.md) — the TTS helper's own docs (only when you cloned the repo)
|
|
769
829
|
- [CHANGELOG.md](CHANGELOG.md) — version history
|
|
770
830
|
|