@idoall/dsh-session-colors 0.1.1 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -8,6 +8,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.1.3] - 2026-09-23
12
+
13
+ Verified against DeepSeek Harness `0.1.7-alpha.2`. The first fix below is what
14
+ made the previous build fail to load on that release; the other two are a
15
+ declaration correctness fix and a visible behaviour fix.
16
+
17
+ ### Fixed
18
+
19
+ - **The plugin could not load from a `link:` install on DSH 0.1.7.** From 0.1.7,
20
+ only a linked plugin's **peer** dependencies resolve from the running
21
+ installation; a plain dependency is looked up under the plugin's own
22
+ `node_modules`, which a link install does not populate. `@deepseek-ai/schemastery`
23
+ was a plain dependency and this repository ships no `node_modules`, so the Host
24
+ half failed to import and the plugin never loaded. It is a peer now (kept in
25
+ `devDependencies` for this repository's own tests).
26
+ - **The declared peer range excluded the release it was running on.**
27
+ `>=0.1.6-0 <0.2.0` does **not** admit `0.1.7-alpha.2` under node-semver's
28
+ default prerelease rule, so pnpm reports the peer as unmet (the plugin market's
29
+ discovery check passes `includePrerelease`, which is why it did not surface as
30
+ an outright incompatibility). The range is now `>=0.1.7-alpha.2 <0.2.0`, with a
31
+ `dsh.engines.dsh` requirement and `dsh.compatibility.dshReleases` recording
32
+ `0.1.7-alpha.2`.
33
+ - **Chips no longer stayed behind while rows glided.** DSH 0.1.7 moves sidebar
34
+ rows with the Web Animations API (`element.animate`), which fires neither
35
+ `transitionrun` nor `transitionend` and mutates no DOM. The layer only opened
36
+ its bounded follow window on a transition event, so a collapse, expand or
37
+ reorder left every chip at the position the glide started from. A row-list
38
+ mutation and a window resize now re-measure once and enter the same 40-frame
39
+ follow window; an unrelated body mutation re-measures once but opens no window,
40
+ so a streaming transcript cannot keep a measuring loop alive.
41
+
42
+ ### Changed
43
+
44
+ - The Session id is read from the row's own `data-row-key` (`session:<id>`, added
45
+ in DSH 0.1.7) before falling back to the React fiber, so the common case no
46
+ longer depends on React internals. The fiber fallback remains for older builds
47
+ and search-result rows.
48
+ - The client build marker is `host-routes+animated-rows`.
49
+ - The docs-consistency test now evaluates the peer range with the prerelease
50
+ rule instead of a substring check — the check that let the old, excluding range
51
+ pass.
52
+
53
+ ### Notes
54
+
55
+ - `0.1.3` supports DSH `0.1.7-alpha.2` only and declares
56
+ `>=0.1.7-alpha.2 <0.2.0` as its Host requirement. Installations on an older DSH,
57
+ including `0.1.6-alpha.2`, should stay on plugin `0.1.2`.
58
+
59
+ ## [0.1.2] - 2026-09-22
60
+
61
+ ### Fixed
62
+
63
+ - Chips no longer stay at their old coordinates for up to 20 seconds when a
64
+ workspace is collapsed or expanded. Each mutation option set now gets its own
65
+ `MutationObserver`: a second `observe()` call on the same target **replaces**
66
+ the first one's options rather than merging them, so sharing one observer for
67
+ `childList` and for the `body` class had silently stopped row additions and
68
+ removals from being watched at all. The chips only moved when the 20-second peer
69
+ poll happened to re-render the layer.
70
+
71
+ ### Added
72
+
73
+ - A regression test that fails if two option sets ever share one observer again.
74
+
11
75
  ## [0.1.1] - 2026-09-22
12
76
 
13
77
  Documentation only — no code change.
package/README.md CHANGED
@@ -69,12 +69,15 @@ Requirements:
69
69
 
70
70
  - DeepSeek Harness with a Web profile
71
71
  - Node.js 20 or newer
72
- - **Verified DSH version: `0.1.6-alpha.2`** (see [Compatibility](#compatibility))
72
+ - **Verified DSH version: `0.1.7-alpha.2`** (see [Compatibility](#compatibility))
73
73
 
74
74
  ```sh
75
- dsh plugin --profile <profile> add @idoall/dsh-session-colors@0.1.0
75
+ dsh plugin --profile <profile> add @idoall/dsh-session-colors@latest
76
76
  ```
77
77
 
78
+ The [compatibility table](#compatibility) lists the exact versions, if you need to
79
+ pin one to a specific DSH release.
80
+
78
81
  Working on the plugin itself? Install the directory instead:
79
82
 
80
83
  ```sh
@@ -114,12 +117,18 @@ stored as HSVA, so alpha round-trips exactly.
114
117
 
115
118
  ## Compatibility
116
119
 
117
- Current release: plugin **`0.1.1`**, verified against DeepSeek Harness
118
- **`0.1.6-alpha.2`**.
120
+ Current release: plugin **`0.1.3`**, verified against DeepSeek Harness
121
+ **`0.1.7-alpha.2`**.
122
+
123
+ **`0.1.3` supports DSH `0.1.7-alpha.2` only.** On an older DSH — including
124
+ `0.1.6-alpha.2` — keep plugin **`0.1.2`**: `0.1.3` declares its Host requirement
125
+ as `>=0.1.7-alpha.2 <0.2.0` and is not verified below it.
119
126
 
120
127
  | Plugin version | Verified DeepSeek Harness | npm status | What it is |
121
128
  | --- | --- | --- | --- |
122
- | **`0.1.1`** | `0.1.6-alpha.2` | `latest` | Documentation only: the packaged README no longer says "not published", and the market screenshots are declared. No code change. |
129
+ | **`0.1.3`** | `0.1.7-alpha.2` | `latest` | Adapts to DSH 0.1.7: the Session id now comes from the row's own `data-row-key` (fiber fallback kept), and chips follow the Web-Animations row gliding `0.1.7` introduced. The Host dependency is declared the way `0.1.7` resolves a linked plugin, and the peer range admits the release. |
130
+ | `0.1.2` | `0.1.6-alpha.2` | published | Fixes chips staying at their old coordinates for ~20s when a workspace is collapsed or expanded: a second `MutationObserver.observe()` on the same target had silently replaced the `childList` watch. |
131
+ | `0.1.1` | `0.1.6-alpha.2` | published | Documentation only: the packaged README no longer says "not published", and the market screenshots are declared. |
123
132
  | `0.1.0` | `0.1.6-alpha.2` | published | First release: Session colour marks, Host-side storage, visible inside a phone's sidebar drawer |
124
133
 
125
134
  - **Verified DeepSeek Harness** is the exact DSH version this plugin was actually
@@ -127,9 +136,17 @@ Current release: plugin **`0.1.1`**, verified against DeepSeek Harness
127
136
  [`package.json`](package.json) — and a test keeps both READMEs' compatibility
128
137
  sections word-for-word with it and inside the range `peerDependencies`
129
138
  declares. A DSH version that is not listed is **never claimed as compatible**.
130
- - `peerDependencies` declares `>=0.1.6-0 <0.2.0` for `dsh-client-ui-layout` and
131
- `dsh-client-ui-conversation`: that is the range allowed to **load**, which is
132
- not the same as verified.
139
+ - `peerDependencies` declares `>=0.1.7-alpha.2 <0.2.0` for `dsh-client-ui-layout`
140
+ and `dsh-client-ui-conversation`, and `dsh.engines.dsh` declares the same range
141
+ as the Host requirement: that is the range allowed to **load**, which is not the
142
+ same as verified. The lower bound names the alpha on purpose — under
143
+ node-semver's default prerelease rule a range like `>=0.1.6-0 <0.2.0` does
144
+ **not** admit `0.1.7-alpha.2`, so pnpm reports the peer as unmet even though the
145
+ plugin runs.
146
+ - `@deepseek-ai/schemastery` is a **peer**, not a plain dependency: DSH 0.1.7
147
+ resolves only a linked plugin's peer dependencies from the running
148
+ installation, so a `link:` install of this directory would otherwise fail to
149
+ import the Host half.
133
150
  - DSH moving faster than this plugin does not break it: chips depend on DSH's row
134
151
  DOM (see [Limitations](#limitations)), so a large change to the row structure
135
152
  can at worst stop chips from appearing — it cannot block clicking.
@@ -183,7 +200,7 @@ document.querySelector('.dsh-sc-layer').dataset
183
200
  // { dshScBuild, dshScStatus, dshScWritable, dshScMarks, dshScError }
184
201
  ```
185
202
 
186
- If `dshScBuild` is not `host-routes+drawer-aware`, the device is running a
203
+ If `dshScBuild` is not `host-routes+animated-rows`, the device is running a
187
204
  **cached client**: refresh the page (the browser half reloads on a refresh, no DSH
188
205
  restart needed). If `dshScStatus` is not `ready`, the store cannot be read — see
189
206
  the next item.
@@ -200,9 +217,10 @@ pages, because storage uses its own route. If you see the old "settings
200
217
  unavailable" behaviour, check which version is installed.
201
218
 
202
219
  **Chips are in the wrong place, or gone.**
203
- The layer finds rows by ARIA role and reads the Session id from the React fiber.
204
- A large change to DSH's sidebar structure can make it find nothing; chips then
205
- disappear, but **clicking is unaffected** because the layer is click-through.
220
+ The layer finds rows by ARIA role and reads the Session id from the row's
221
+ `data-row-key` (falling back to React internals). A large change to DSH's sidebar
222
+ structure can make it find nothing; chips then disappear, but **clicking is
223
+ unaffected** because the layer is click-through.
206
224
 
207
225
  ## Security boundaries
208
226
 
@@ -220,8 +238,8 @@ disappear, but **clicking is unaffected** because the layer is click-through.
220
238
  ## Limitations
221
239
 
222
240
  - **The chip depends on DSH's row DOM.** It reads rows by ARIA role and the
223
- Session id from React internals. A future DSH release can stop chips from
224
- appearing; it cannot break clicking.
241
+ Session id from the row's `data-row-key`, falling back to React internals. A
242
+ future DSH release can stop chips from appearing; it cannot break clicking.
225
243
  - **Marks are per DSH instance.** They live in one profile's data directory, so
226
244
  two independent DSH servers do not share them.
227
245
  - **No permission model.** Anyone who can reach the Host's authenticated routes
@@ -240,7 +258,7 @@ is not deleted for you; remove it yourself if you do not want it.
240
258
  ## Development
241
259
 
242
260
  ```sh
243
- npm test # 34 tests: bundle shape, store, route, cross-device, geometry, docs
261
+ npm test # 38 tests: bundle shape, store, route, cross-device, geometry, docs
244
262
  ```
245
263
 
246
264
  The browser half is a hand-authored `__ModuleLoader__` bundle, which is the only
package/README.zh.md CHANGED
@@ -51,12 +51,14 @@
51
51
 
52
52
  - 带 Web profile 的 DeepSeek Harness
53
53
  - Node.js 20 或更新版本
54
- - **已验证的 DSH 版本:`0.1.6-alpha.2`**(见[兼容性](#兼容性))
54
+ - **已验证的 DSH 版本:`0.1.7-alpha.2`**(见[兼容性](#兼容性))
55
55
 
56
56
  ```sh
57
- dsh plugin --profile <profile> add @idoall/dsh-session-colors@0.1.0
57
+ dsh plugin --profile <profile> add @idoall/dsh-session-colors@latest
58
58
  ```
59
59
 
60
+ 要按 DSH 版本精确对应,用[兼容性](#兼容性)表里的具体版本号。
61
+
60
62
  要改插件本身就用目录安装:
61
63
 
62
64
  ```sh
@@ -88,15 +90,20 @@ dsh plugin --profile <profile> add link:/path/to/dsh-session-colors
88
90
 
89
91
  ## 兼容性
90
92
 
91
- 当前版本:插件 **`0.1.1`** 已针对 DeepSeek Harness **`0.1.6-alpha.2`** 验证。
93
+ 当前版本:插件 **`0.1.3`** 已针对 DeepSeek Harness **`0.1.7-alpha.2`** 验证。
94
+
95
+ **`0.1.3` 只支持 DSH `0.1.7-alpha.2`。** 更旧的 DSH(包括 `0.1.6-alpha.2`)请继续用插件 **`0.1.2`**:`0.1.3` 声明的宿主要求是 `>=0.1.7-alpha.2 <0.2.0`,低于它的版本未经本轮验证。
92
96
 
93
97
  | 插件版本 | 已验证的 DeepSeek Harness | npm 发布状态 | 该版本是什么 |
94
98
  | --- | --- | --- | --- |
95
- | **`0.1.1`** | `0.1.6-alpha.2` | `latest` | 仅文档:打包进 npm 的 README 不再写"尚未发布",并声明了市场截图。无代码改动。 |
99
+ | **`0.1.3`** | `0.1.7-alpha.2` | `latest` | 适配 DSH 0.1.7:会话 id 改从行自身的 `data-row-key` 读取(保留 fiber 回退),色块跟随 0.1.7 引入的 Web Animations 行滑动;宿主依赖按 0.1.7 对 link 插件的解析方式声明,peer 范围也真正覆盖该版本。 |
100
+ | `0.1.2` | `0.1.6-alpha.2` | 已发布 | 修掉折叠/展开工作区后色块停在旧位置约 20 秒:同一个 target 上第二次 `MutationObserver.observe()` 把 `childList` 监听顶掉了。 |
101
+ | `0.1.1` | `0.1.6-alpha.2` | 已发布 | 仅文档:打包进 npm 的 README 不再写"尚未发布",并声明了市场截图。 |
96
102
  | `0.1.0` | `0.1.6-alpha.2` | 已发布 | 首个版本:会话颜色标记、宿主侧存储、手机抽屉内可见 |
97
103
 
98
104
  - **已验证的 DeepSeek Harness** 是这个插件**实际跑过**的确切 DSH 版本。这份清单只有一个存放处——[`package.json`](package.json) 的 `dsh.compatibility.dshReleases`——并且有测试保证两个 README 的兼容性段落与它逐字一致、且落在 `peerDependencies` 声明的范围内。未列出的 DSH 版本**不会被宣称为兼容**。
99
- - `peerDependencies` 声明的范围是 `>=0.1.6-0 <0.2.0`(`dsh-client-ui-layout` 与 `dsh-client-ui-conversation`):这是**允许加载**的范围,不等于已验证。
105
+ - `peerDependencies` 声明的范围是 `>=0.1.7-alpha.2 <0.2.0`(`dsh-client-ui-layout` 与 `dsh-client-ui-conversation`),`dsh.engines.dsh` 把同一范围声明为宿主要求:这是**允许加载**的范围,不等于已验证。下界特意写成 alpha 版本——按 node-semver 的默认预发布规则,`>=0.1.6-0 <0.2.0` 这样的范围**并不包含** `0.1.7-alpha.2`,pnpm 会把 peer 报成 unmet,即使插件实际跑得起来。
106
+ - `@deepseek-ai/schemastery` 声明为 **peer**,不是普通依赖:DSH 0.1.7 只把 link 插件的 **peer** 依赖解析到运行实例,所以用 `link:` 装入本目录时,普通依赖会让宿主半边 import 失败。
100
107
  - DSH 升级快于插件时,插件本身不会因此报错:色块依赖 DSH 的行 DOM(见[限制](#限制)),所以 DSH 大改行结构时最坏的结果是**色块不再显示**,不会挡住点击。
101
108
 
102
109
  ## 跨设备与局域网
@@ -140,7 +147,7 @@ document.querySelector('.dsh-sc-layer').dataset
140
147
  // { dshScBuild, dshScStatus, dshScWritable, dshScMarks, dshScError }
141
148
  ```
142
149
 
143
- `dshScBuild` 不是 `host-routes+drawer-aware` 就是**客户端缓存**,刷新页面即可(客户端半边刷新即热重载,不需要重启 DSH)。`dshScStatus` 不是 `ready` 则是存储读不到,见下一条。
150
+ `dshScBuild` 不是 `host-routes+animated-rows` 就是**客户端缓存**,刷新页面即可(客户端半边刷新即热重载,不需要重启 DSH)。`dshScStatus` 不是 `ready` 则是存储读不到,见下一条。
144
151
 
145
152
  **头部控制显示 ⚠。**
146
153
  表示读不到存储:`dshScError` 里有原因,`dshScWritable` 说明能不能写。宿主路由未注册(插件没被 profile 加载)或数据目录不可写时会这样。注意**这不是**"这个会话没有颜色"。
@@ -149,7 +156,7 @@ document.querySelector('.dsh-sc-layer').dataset
149
156
  从 `0.1.0` 起本插件在局域网/隧道页面与回环页面行为一致(存储走自有路由)。如果你看到的是旧版那种"设置读不到"的表现,先确认装的是哪个版本。
150
157
 
151
158
  **色块位置不对或消失。**
152
- 色块浮层靠 ARIA role 找会话行、靠 React fiber 取会话 ID。DSH 大改侧栏结构后可能找不到行;此时色块会消失,但**不会影响点击**,因为浮层是点击穿透的。
159
+ 色块浮层靠 ARIA role 找会话行、靠行自身的 `data-row-key` 取会话 ID(取不到时回退到 React fiber)。DSH 大改侧栏结构后可能找不到行;此时色块会消失,但**不会影响点击**,因为浮层是点击穿透的。
153
160
 
154
161
  ## 安全边界
155
162
 
@@ -161,7 +168,7 @@ document.querySelector('.dsh-sc-layer').dataset
161
168
 
162
169
  ## 限制
163
170
 
164
- - **色块依赖 DSH 的行 DOM。** 它靠 ARIA role 找行、靠 React 内部结构取会话 ID。DSH 未来改版可能让色块不再显示;但**不会影响点击**。
171
+ - **色块依赖 DSH 的行 DOM。** 它靠 ARIA role 找行、靠行自身的 `data-row-key` 取会话 ID(回退到 React 内部结构)。DSH 未来改版可能让色块不再显示;但**不会影响点击**。
165
172
  - **标记属于单个 DSH 实例。** 它存在某个 profile 的数据目录里,两个独立的 DSH 服务之间不共享。
166
173
  - **没有权限模型。** 凡是能访问该 profile 已鉴权路由的人,都能读写标记。
167
174
  - **只有颜色。** 没有文字标签、图标或 emoji。
@@ -177,7 +184,7 @@ dsh plugin --profile <profile> remove @idoall/dsh-session-colors
177
184
  ## 开发
178
185
 
179
186
  ```sh
180
- npm test # 34 项:bundle 结构、存储、路由、跨设备、几何、文档一致性
187
+ npm test # 38 项:bundle 结构、存储、路由、跨设备、几何、文档一致性
181
188
  ```
182
189
 
183
190
  浏览器半边是**手写的** `__ModuleLoader__` bundle——这是 DSH 客户端加载器接受的唯一格式。手写是刻意的:**没有构建步骤**,就不会有产物与目标 DSH 版本漂移的问题。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idoall/dsh-session-colors",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "DSH web plugin: give any Session a colour mark, shown as a chip in the sidebar and picked from the conversation header. Marks are stored on the Host, so a colour chosen on one device is visible on every other one — including over LAN and tunnels.",
6
6
  "main": "./src/index.js",
@@ -20,9 +20,12 @@
20
20
  ],
21
21
  "dsh": {
22
22
  "manifestVersion": 1,
23
+ "engines": {
24
+ "dsh": ">=0.1.7-alpha.2 <0.2.0"
25
+ },
23
26
  "compatibility": {
24
27
  "dshReleases": {
25
- "0.1.6-alpha.2": "compatible"
28
+ "0.1.7-alpha.2": "compatible"
26
29
  }
27
30
  },
28
31
  "bundle": {
@@ -42,12 +45,10 @@
42
45
  "publishConfig": {
43
46
  "access": "public"
44
47
  },
45
- "dependencies": {
46
- "@deepseek-ai/schemastery": "^3.18.2"
47
- },
48
48
  "peerDependencies": {
49
- "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.6-0 <0.2.0",
50
- "@deepseek-ai/dsh-client-ui-layout": ">=0.1.6-0 <0.2.0",
49
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.7-alpha.2 <0.2.0",
50
+ "@deepseek-ai/dsh-client-ui-layout": ">=0.1.7-alpha.2 <0.2.0",
51
+ "@deepseek-ai/schemastery": "^3.18.2",
51
52
  "react": "^18.2.0"
52
53
  },
53
54
  "peerDependenciesMeta": {
@@ -58,6 +59,11 @@
58
59
  "optional": true
59
60
  }
60
61
  },
62
+ "devDependencies": {
63
+ "@deepseek-ai/cordis": "^4.0.4",
64
+ "@deepseek-ai/schemastery": "^3.18.2",
65
+ "react": "^18.3.1"
66
+ },
61
67
  "license": "MIT",
62
68
  "engines": {
63
69
  "node": ">=20"
package/src/client.js CHANGED
@@ -15,12 +15,13 @@
15
15
  * `ChipsLayer` and `layerZIndexOf`.
16
16
  *
17
17
  * There is no Session-row slot, so a chip cannot be rendered inside a row. The
18
- * layer finds rows by ARIA role and reads each row's Session id from the React
19
- * fiber the renderer already attached — the only place the id exists, since the
20
- * row markup carries no id attribute. That coupling is deliberate and is kept
21
- * harmless: every step is guarded, an unreadable row is skipped, and the layer
22
- * never accepts pointer events, so a future DSH change can at worst stop the
23
- * chips from appearing.
18
+ * layer finds rows by ARIA role and reads each row's Session id from the
19
+ * `data-row-key` attribute DSH 0.1.7 puts on every row (`session:<id>`),
20
+ * falling back to the React fiber the renderer attaches — the only source on a
21
+ * build without that attribute, and still the source for search-result rows.
22
+ * That coupling is deliberate and is kept harmless: every step is guarded, an
23
+ * unreadable row is skipped, and the layer never accepts pointer events, so a
24
+ * future DSH change can at worst stop the chips from appearing.
24
25
  *
25
26
  * Written as a hand-authored `__ModuleLoader__` bundle (the only format the
26
27
  * client loader accepts for a package client half), so it needs no build step.
@@ -46,13 +47,13 @@ window.__ModuleLoader__.load({
46
47
 
47
48
  const NS = 'sessionColor'
48
49
  /**
49
- * Build marker. Stored marks are Host-side as of this build, and the chip
50
- * layer paints just above the sidebar as of the `drawer-aware` build — the
51
- * change that makes a chip visible inside a phone's sidebar drawer. A device
52
- * that still shows an older marker is running a cached client and must be
53
- * hard-refreshed.
50
+ * Build marker. Stored marks are Host-side as of the `host-routes` build,
51
+ * the chip layer paints just above the sidebar as of `drawer-aware`, and as
52
+ * of `animated-rows` it follows the Web-Animations row gliding DSH 0.1.7
53
+ * introduced. A device that still shows an older marker is running a cached
54
+ * client and must be hard-refreshed.
54
55
  */
55
- const BUILD = 'host-routes+drawer-aware'
56
+ const BUILD = 'host-routes+animated-rows'
56
57
 
57
58
  /**
58
59
  * This plugin's own Host route. Kept in sync with the Host half's
@@ -429,13 +430,28 @@ window.__ModuleLoader__.load({
429
430
 
430
431
  //#region row discovery
431
432
  /**
432
- * Session id of a rendered row, read from the React fiber the renderer
433
- * already attached. The row markup deliberately carries no id, so the fiber
434
- * is the only exact source; the walk is bounded and every failure returns
435
- * undefined, which makes the caller skip that row.
433
+ * Prefix DSH 0.1.7 uses for a Session row's `data-row-key`. It is the
434
+ * exact, stable DOM source of a row's Session id; older builds and
435
+ * search-result rows carry no such attribute.
436
+ */
437
+ const SESSION_ROW_KEY = 'session:'
438
+
439
+ /**
440
+ * Session id of a rendered row. The `data-row-key` attribute is read first
441
+ * because DSH 0.1.7 marks every row with it; the React fiber the renderer
442
+ * already attached remains the fallback, and the walk is bounded so an
443
+ * unexpected shape only costs that row its chip.
436
444
  * @param node - a Session row element.
437
445
  */
438
446
  function sessionIdOf(node) {
447
+ try {
448
+ const key = node === null || node === undefined || typeof node.getAttribute !== 'function'
449
+ ? undefined
450
+ : node.getAttribute('data-row-key')
451
+ if (typeof key === 'string' && key.startsWith(SESSION_ROW_KEY)) return key.slice(SESSION_ROW_KEY.length)
452
+ } catch {
453
+ // An unreadable attribute only sends this row to the fiber fallback.
454
+ }
439
455
  try {
440
456
  const key = Object.keys(node).find((name) => name.startsWith('__reactFiber$') || name.startsWith('__reactInternalInstance$'))
441
457
  if (key === undefined) return undefined
@@ -603,10 +619,23 @@ window.__ModuleLoader__.load({
603
619
  setLayer({ items, clip, zIndex: rows.length === 0 ? 1 : layerZIndexOf(rows[0]) })
604
620
  }
605
621
  sync()
622
+ /**
623
+ * Re-measure now, then keep re-measuring for a bounded window.
624
+ *
625
+ * DSH 0.1.7 glides rows with the Web Animations API
626
+ * (`element.animate`): that fires neither `transitionrun` nor
627
+ * `transitionend` and mutates no DOM, so the one MutationObserver
628
+ * callback for a collapse lands on the animation's first frame — every
629
+ * row still painted at its old coordinates — and nothing would ever
630
+ * report the movement again. Following each mutation for the same
631
+ * bounded window as a transition keeps the chips on their rows.
632
+ */
633
+ const kick = () => { sync(); follow() }
606
634
  // Rows move with their scroll container and re-render on every list
607
- // change, so follow both.
635
+ // change, so follow both. A scroll reports its own frames continuously,
636
+ // so it needs no follow window of its own.
608
637
  document.addEventListener('scroll', sync, true)
609
- window.addEventListener('resize', sync)
638
+ window.addEventListener('resize', kick)
610
639
  // A CSS transition moves every row without any scroll, resize or DOM
611
640
  // mutation: the phone adapter slides the whole sidebar in with a
612
641
  // transform on the drawer while the rows themselves never change. Only
@@ -621,24 +650,53 @@ window.__ModuleLoader__.load({
621
650
  }
622
651
  document.addEventListener('transitionrun', onTransition, true)
623
652
  document.addEventListener('transitionend', onTransition, true)
624
- let observer
653
+ // Each option set needs its OWN observer. A second `observe()` call on
654
+ // the same target REPLACES the first one's options rather than merging
655
+ // them (DOM spec), so sharing one observer silently stopped childList
656
+ // from being watched at all: collapsing a workspace removed its rows
657
+ // without any re-sync, and the chips sat at their old coordinates until
658
+ // the peer poll happened to re-render the layer ~20s later.
659
+ const observers = []
625
660
  try {
626
- observer = new MutationObserver(sync)
627
- observer.observe(document.body, { childList: true, subtree: true })
661
+ /**
662
+ * Whether a mutation could have changed the row list, and so started a
663
+ * row glide. The childList watch is body-wide because rows live deep
664
+ * inside the sidebar, so a mutation unrelated to the list — a
665
+ * streaming transcript, a tooltip — re-measures once but must not open
666
+ * a follow window for every frame it produces.
667
+ */
668
+ const rowsChanged = (records) => records.some((record) => {
669
+ const target = record.target
670
+ return target !== null && target !== undefined
671
+ && typeof target.closest === 'function'
672
+ && target.closest('[role="tree"]') !== null
673
+ })
674
+ const onRowsMutation = (records) => { if (rowsChanged(records)) kick(); else sync() }
675
+ const rowsObserver = new MutationObserver(onRowsMutation)
676
+ rowsObserver.observe(document.body, { childList: true, subtree: true })
677
+ observers.push(rowsObserver)
628
678
  // The phone adapter opens its drawer by toggling a class on `body`.
629
679
  // With transitions disabled the geometry still changes, just without a
630
680
  // transition event to follow.
631
- observer.observe(document.body, { attributes: true, attributeFilter: ['class'] })
681
+ const drawerObserver = new MutationObserver(kick)
682
+ drawerObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] })
683
+ observers.push(drawerObserver)
684
+ // A workspace collapse that hides rows instead of removing them
685
+ // produces no childList mutation, so follow the ARIA state and
686
+ // `hidden` as well.
687
+ const stateObserver = new MutationObserver(kick)
688
+ stateObserver.observe(document.body, { attributes: true, attributeFilter: ['aria-expanded', 'hidden'], subtree: true })
689
+ observers.push(stateObserver)
632
690
  } catch {
633
- observer = undefined
691
+ // Without observers the layer still follows scroll, resize and transitions.
634
692
  }
635
693
  return () => {
636
694
  cancelFrame(frame)
637
695
  document.removeEventListener('scroll', sync, true)
638
- window.removeEventListener('resize', sync)
696
+ window.removeEventListener('resize', kick)
639
697
  document.removeEventListener('transitionrun', onTransition, true)
640
698
  document.removeEventListener('transitionend', onTransition, true)
641
- if (observer !== undefined) observer.disconnect()
699
+ for (const watcher of observers) watcher.disconnect()
642
700
  }
643
701
  }, [snapshot])
644
702