@idoall/dsh-session-colors 0.1.2 → 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,54 @@ 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
+
11
59
  ## [0.1.2] - 2026-09-22
12
60
 
13
61
  ### Fixed
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,17 @@ stored as HSVA, so alpha round-trips exactly.
114
117
 
115
118
  ## Compatibility
116
119
 
117
- Current release: plugin **`0.1.2`**, 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.2`** | `0.1.6-alpha.2` | `latest` | 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. |
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. |
123
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. |
124
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 |
125
133
 
@@ -128,9 +136,17 @@ Current release: plugin **`0.1.2`**, verified against DeepSeek Harness
128
136
  [`package.json`](package.json) — and a test keeps both READMEs' compatibility
129
137
  sections word-for-word with it and inside the range `peerDependencies`
130
138
  declares. A DSH version that is not listed is **never claimed as compatible**.
131
- - `peerDependencies` declares `>=0.1.6-0 <0.2.0` for `dsh-client-ui-layout` and
132
- `dsh-client-ui-conversation`: that is the range allowed to **load**, which is
133
- 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.
134
150
  - DSH moving faster than this plugin does not break it: chips depend on DSH's row
135
151
  DOM (see [Limitations](#limitations)), so a large change to the row structure
136
152
  can at worst stop chips from appearing — it cannot block clicking.
@@ -184,7 +200,7 @@ document.querySelector('.dsh-sc-layer').dataset
184
200
  // { dshScBuild, dshScStatus, dshScWritable, dshScMarks, dshScError }
185
201
  ```
186
202
 
187
- 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
188
204
  **cached client**: refresh the page (the browser half reloads on a refresh, no DSH
189
205
  restart needed). If `dshScStatus` is not `ready`, the store cannot be read — see
190
206
  the next item.
@@ -201,9 +217,10 @@ pages, because storage uses its own route. If you see the old "settings
201
217
  unavailable" behaviour, check which version is installed.
202
218
 
203
219
  **Chips are in the wrong place, or gone.**
204
- The layer finds rows by ARIA role and reads the Session id from the React fiber.
205
- A large change to DSH's sidebar structure can make it find nothing; chips then
206
- 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.
207
224
 
208
225
  ## Security boundaries
209
226
 
@@ -221,8 +238,8 @@ disappear, but **clicking is unaffected** because the layer is click-through.
221
238
  ## Limitations
222
239
 
223
240
  - **The chip depends on DSH's row DOM.** It reads rows by ARIA role and the
224
- Session id from React internals. A future DSH release can stop chips from
225
- 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.
226
243
  - **Marks are per DSH instance.** They live in one profile's data directory, so
227
244
  two independent DSH servers do not share them.
228
245
  - **No permission model.** Anyone who can reach the Host's authenticated routes
@@ -241,7 +258,7 @@ is not deleted for you; remove it yourself if you do not want it.
241
258
  ## Development
242
259
 
243
260
  ```sh
244
- 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
245
262
  ```
246
263
 
247
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,16 +90,20 @@ dsh plugin --profile <profile> add link:/path/to/dsh-session-colors
88
90
 
89
91
  ## 兼容性
90
92
 
91
- 当前版本:插件 **`0.1.2`** 已针对 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.2`** | `0.1.6-alpha.2` | `latest` | 修掉折叠/展开工作区后色块停在旧位置约 20 秒:同一个 target 上第二次 `MutationObserver.observe()` 把 `childList` 监听顶掉了。 |
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` 监听顶掉了。 |
96
101
  | `0.1.1` | `0.1.6-alpha.2` | 已发布 | 仅文档:打包进 npm 的 README 不再写"尚未发布",并声明了市场截图。 |
97
102
  | `0.1.0` | `0.1.6-alpha.2` | 已发布 | 首个版本:会话颜色标记、宿主侧存储、手机抽屉内可见 |
98
103
 
99
104
  - **已验证的 DeepSeek Harness** 是这个插件**实际跑过**的确切 DSH 版本。这份清单只有一个存放处——[`package.json`](package.json) 的 `dsh.compatibility.dshReleases`——并且有测试保证两个 README 的兼容性段落与它逐字一致、且落在 `peerDependencies` 声明的范围内。未列出的 DSH 版本**不会被宣称为兼容**。
100
- - `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 失败。
101
107
  - DSH 升级快于插件时,插件本身不会因此报错:色块依赖 DSH 的行 DOM(见[限制](#限制)),所以 DSH 大改行结构时最坏的结果是**色块不再显示**,不会挡住点击。
102
108
 
103
109
  ## 跨设备与局域网
@@ -141,7 +147,7 @@ document.querySelector('.dsh-sc-layer').dataset
141
147
  // { dshScBuild, dshScStatus, dshScWritable, dshScMarks, dshScError }
142
148
  ```
143
149
 
144
- `dshScBuild` 不是 `host-routes+drawer-aware` 就是**客户端缓存**,刷新页面即可(客户端半边刷新即热重载,不需要重启 DSH)。`dshScStatus` 不是 `ready` 则是存储读不到,见下一条。
150
+ `dshScBuild` 不是 `host-routes+animated-rows` 就是**客户端缓存**,刷新页面即可(客户端半边刷新即热重载,不需要重启 DSH)。`dshScStatus` 不是 `ready` 则是存储读不到,见下一条。
145
151
 
146
152
  **头部控制显示 ⚠。**
147
153
  表示读不到存储:`dshScError` 里有原因,`dshScWritable` 说明能不能写。宿主路由未注册(插件没被 profile 加载)或数据目录不可写时会这样。注意**这不是**"这个会话没有颜色"。
@@ -150,7 +156,7 @@ document.querySelector('.dsh-sc-layer').dataset
150
156
  从 `0.1.0` 起本插件在局域网/隧道页面与回环页面行为一致(存储走自有路由)。如果你看到的是旧版那种"设置读不到"的表现,先确认装的是哪个版本。
151
157
 
152
158
  **色块位置不对或消失。**
153
- 色块浮层靠 ARIA role 找会话行、靠 React fiber 取会话 ID。DSH 大改侧栏结构后可能找不到行;此时色块会消失,但**不会影响点击**,因为浮层是点击穿透的。
159
+ 色块浮层靠 ARIA role 找会话行、靠行自身的 `data-row-key` 取会话 ID(取不到时回退到 React fiber)。DSH 大改侧栏结构后可能找不到行;此时色块会消失,但**不会影响点击**,因为浮层是点击穿透的。
154
160
 
155
161
  ## 安全边界
156
162
 
@@ -162,7 +168,7 @@ document.querySelector('.dsh-sc-layer').dataset
162
168
 
163
169
  ## 限制
164
170
 
165
- - **色块依赖 DSH 的行 DOM。** 它靠 ARIA role 找行、靠 React 内部结构取会话 ID。DSH 未来改版可能让色块不再显示;但**不会影响点击**。
171
+ - **色块依赖 DSH 的行 DOM。** 它靠 ARIA role 找行、靠行自身的 `data-row-key` 取会话 ID(回退到 React 内部结构)。DSH 未来改版可能让色块不再显示;但**不会影响点击**。
166
172
  - **标记属于单个 DSH 实例。** 它存在某个 profile 的数据目录里,两个独立的 DSH 服务之间不共享。
167
173
  - **没有权限模型。** 凡是能访问该 profile 已鉴权路由的人,都能读写标记。
168
174
  - **只有颜色。** 没有文字标签、图标或 emoji。
@@ -178,7 +184,7 @@ dsh plugin --profile <profile> remove @idoall/dsh-session-colors
178
184
  ## 开发
179
185
 
180
186
  ```sh
181
- npm test # 34 项:bundle 结构、存储、路由、跨设备、几何、文档一致性
187
+ npm test # 38 项:bundle 结构、存储、路由、跨设备、几何、文档一致性
182
188
  ```
183
189
 
184
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.2",
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
@@ -629,19 +658,33 @@ window.__ModuleLoader__.load({
629
658
  // the peer poll happened to re-render the layer ~20s later.
630
659
  const observers = []
631
660
  try {
632
- const rowsObserver = new MutationObserver(sync)
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)
633
676
  rowsObserver.observe(document.body, { childList: true, subtree: true })
634
677
  observers.push(rowsObserver)
635
678
  // The phone adapter opens its drawer by toggling a class on `body`.
636
679
  // With transitions disabled the geometry still changes, just without a
637
680
  // transition event to follow.
638
- const drawerObserver = new MutationObserver(sync)
681
+ const drawerObserver = new MutationObserver(kick)
639
682
  drawerObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] })
640
683
  observers.push(drawerObserver)
641
684
  // A workspace collapse that hides rows instead of removing them
642
685
  // produces no childList mutation, so follow the ARIA state and
643
686
  // `hidden` as well.
644
- const stateObserver = new MutationObserver(sync)
687
+ const stateObserver = new MutationObserver(kick)
645
688
  stateObserver.observe(document.body, { attributes: true, attributeFilter: ['aria-expanded', 'hidden'], subtree: true })
646
689
  observers.push(stateObserver)
647
690
  } catch {
@@ -650,7 +693,7 @@ window.__ModuleLoader__.load({
650
693
  return () => {
651
694
  cancelFrame(frame)
652
695
  document.removeEventListener('scroll', sync, true)
653
- window.removeEventListener('resize', sync)
696
+ window.removeEventListener('resize', kick)
654
697
  document.removeEventListener('transitionrun', onTransition, true)
655
698
  document.removeEventListener('transitionend', onTransition, true)
656
699
  for (const watcher of observers) watcher.disconnect()