dsh-input-traffic 0.7.5 → 0.7.7

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
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.7 (2026-10-27)
4
+
5
+ 两条外部 issue(feidky)的修复;无宿主兼容面变更,`engines.dsh` 枚举与 peers 保持 0.7.6。
6
+
7
+ ### 修复:多条排队时自动折叠,与用户抢开关(issue #4)
8
+
9
+ - 多条排队**默认展开**(`listVisible` 仍保留单条直显):排队信息的价值就是看得见。
10
+ - 删除「队列清空 → `setCollapsed(true)`」的自动收回 effect;折叠态只由用户手动决定。
11
+ - 持久化键升 v2(`dsh-input-traffic:collapsed:v2`):v1 的值含自动折叠写入的结果,
12
+ 不是用户选择,不能压过新默认值。
13
+ - `collapseTouched` latch:未手动点过表头前**不写存储**,避免首帧把默认值物化成决定。
14
+
15
+ ### 修复:红档「打断并输入」在多条排队时投了队首行(issue #3)
16
+
17
+ - 根因(确定性,非竞态):宿主唤醒只经 `followup`,而 `followup` 永远追加到 `next-turn`
18
+ 尾部(`agent.ts` `inbox.splice(…, Infinity, …)`),唤醒后 driver 按 FIFO 取**队首**。
19
+ 旧实现 `cancel → remove(目标) → send(目标)` 让目标落在队尾,被消费的是队首。
20
+ 另:`conversation.send()` 的 mode 硬编码为 `queue`,与 `busyEnter` 设置无关。
21
+ - 新流程:`cancel`(宿主 park 队列,收敛期不 claim)→ 摘出全部其它行 →
22
+ `send(目标)`(此时唯一,即队首)→ 其余行按原序重投。
23
+ - 语义:红档只投入当条;其余行(含黄档插入行)统一降级回绿色等待档——
24
+ 被打断的回合不存在「下一个 step」可插入。
25
+ - 顺序:`cancel` 提到最前,所有摘取都发生在 park 之后,去掉「摘到一半被消费」的窗口。
26
+ - `send(目标)` 失败时仍恢复其余排队行(try/finally),不吞队列;
27
+ `queue-item-not-found` 视为已被宿主取走,该行不重投(避免重复投递)。
28
+
29
+ ### 限制:附件行仍锁红档
30
+
31
+ - 重投只有 `send(text)` 一条通道:`file` 块的 `receiptId` 一次性、无反向解析;
32
+ 图片需 `readAttachment` 回读字节(未实现)。
33
+ - 因此待处理行含非文本内容时**整队**红档禁用(不只那一条),文案改为队列口径(9 语)。
34
+ - 根治需宿主提供队列位置操作(`move` / `prepend`),届时红档与重排的附件锁可一并解除。
35
+
36
+ 0.7.6: host enumeration addendum for DSH 0.2.1-rc.3 (declaration-only bump; contract plane zero diff).
37
+
3
38
  ## 0.7.5 (2026-10-18)
4
39
 
5
40
  - hosts: 追加 0.2.1-rc.2 声明面枚举(34/34 @deepseek-ai/* 宿主 peer 逐包 npm 实测 rc.2 在版;本插件不触 ctx.web provider 与 conversation.input.model.label 槽,代码零改动,纯元数据发版;农场 rc.2 格挂账待命)
package/README.en.md CHANGED
@@ -52,7 +52,7 @@ A cordis client plugin assembled via the `dsh plugin` command and a bundle patch
52
52
  > **Suggested pairing for now**: use it together with a **reminder** plugin (e.g. [dsh-notify](https://github.com/zhengjy01/dsh-notify), desktop notifications when it is time to freeze/resume) and a **billing/usage** plugin (e.g. [dsh-deepseek-usage](https://github.com/yyb16yyb-hub/dsh-deepseek-usage), [dsh-cost-tracker](https://github.com/yflmq001/dsh-cost-tracker), [dsh-billing-balance](https://github.com/YZz-S/dsh-billing-balance), to verify actual spend around a freeze) — a "remind → freeze → resume off-peak → verify" saving loop.
53
53
 
54
54
  <!-- host-compat:begin — generated by scripts/sync-hosts.mjs from scripts/hosts.mjs; DO NOT EDIT -->
55
- - **Supported DSH hosts:** `0.1.0-rc.2` `0.1.0-rc.3` `0.1.0-rc.6` `0.1.0-rc.7` `0.1.0-rc.8` `0.1.1-rc.1` `0.1.1-rc.2` `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-rc.1` `0.2.1-rc.2` (17 rc releases across lines 0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1)
55
+ - **Supported DSH hosts:** `0.1.0-rc.2` `0.1.0-rc.3` `0.1.0-rc.6` `0.1.0-rc.7` `0.1.0-rc.8` `0.1.1-rc.1` `0.1.1-rc.2` `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-rc.1` `0.2.1-rc.2` `0.2.1-rc.3` (18 rc releases across lines 0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1)
56
56
  - **Runtime-verified:** `0.1.0-rc.2`, `0.1.0-rc.3`, `0.1.0-rc.6`, `0.1.0-rc.7`, `0.1.0-rc.8`, `0.1.1-rc.1`, `0.1.1-rc.2`, `0.1.2-rc.1`, `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2` — evidence in `.compat-results/`
57
57
  <!-- host-compat:end -->
58
58
 
package/README.ja.md CHANGED
@@ -35,7 +35,7 @@
35
35
  > **推奨セットアップ**:**リマインダー**プラグイン([dsh-notify](https://github.com/zhengjy01/dsh-notify) など、凍結/再開のタイミングをデスクトップ通知)と**課金統計**プラグイン([dsh-deepseek-usage](https://github.com/yyb16yyb-hub/dsh-deepseek-usage)、[dsh-cost-tracker](https://github.com/yflmq001/dsh-cost-tracker)、[dsh-billing-balance](https://github.com/YZz-S/dsh-billing-balance) など、凍結前後の実際の支出を確認)を組み合わせ、「リマインド → 凍結 → オフピーク再開 → 精算」の節約ループを実現します。
36
36
 
37
37
  <!-- host-compat:begin — generated by scripts/sync-hosts.mjs from scripts/hosts.mjs; DO NOT EDIT -->
38
- - **対応する DSH ホスト:** `0.1.0-rc.2` `0.1.0-rc.3` `0.1.0-rc.6` `0.1.0-rc.7` `0.1.0-rc.8` `0.1.1-rc.1` `0.1.1-rc.2` `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-rc.1` `0.2.1-rc.2`(0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1 の 17 rc)
38
+ - **対応する DSH ホスト:** `0.1.0-rc.2` `0.1.0-rc.3` `0.1.0-rc.6` `0.1.0-rc.7` `0.1.0-rc.8` `0.1.1-rc.1` `0.1.1-rc.2` `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-rc.1` `0.2.1-rc.2` `0.2.1-rc.3`(0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1 の 18 rc)
39
39
  - **ランタイム検証済み:** `0.1.0-rc.2`, `0.1.0-rc.3`, `0.1.0-rc.6`, `0.1.0-rc.7`, `0.1.0-rc.8`, `0.1.1-rc.1`, `0.1.1-rc.2`, `0.1.2-rc.1`, `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2`(証跡は `.compat-results/`)
40
40
  <!-- host-compat:end -->
41
41
 
package/README.ko.md CHANGED
@@ -35,7 +35,7 @@
35
35
  > **권장 구성**: **알림** 플러그인([dsh-notify](https://github.com/zhengjy01/dsh-notify), 동결/재개 시점 데스크톱 알림)과 **과금 통계** 플러그인([dsh-deepseek-usage](https://github.com/yyb16yyb-hub/dsh-deepseek-usage), [dsh-cost-tracker](https://github.com/yflmq001/dsh-cost-tracker), [dsh-billing-balance](https://github.com/YZz-S/dsh-billing-balance), 동결 전후 실제 지출 확인)을 함께 사용하여 "알림 → 동결 → 오피크 재개 → 정산" 절약 루ープ를 실현합니다.
36
36
 
37
37
  <!-- host-compat:begin — generated by scripts/sync-hosts.mjs from scripts/hosts.mjs; DO NOT EDIT -->
38
- - **지원하는 DSH 호스트:** `0.1.0-rc.2` `0.1.0-rc.3` `0.1.0-rc.6` `0.1.0-rc.7` `0.1.0-rc.8` `0.1.1-rc.1` `0.1.1-rc.2` `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-rc.1` `0.2.1-rc.2` (0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1 라인의 17개 rc)
38
+ - **지원하는 DSH 호스트:** `0.1.0-rc.2` `0.1.0-rc.3` `0.1.0-rc.6` `0.1.0-rc.7` `0.1.0-rc.8` `0.1.1-rc.1` `0.1.1-rc.2` `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-rc.1` `0.2.1-rc.2` `0.2.1-rc.3` (0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1 라인의 18개 rc)
39
39
  - **런타임 검증됨:** `0.1.0-rc.2`, `0.1.0-rc.3`, `0.1.0-rc.6`, `0.1.0-rc.7`, `0.1.0-rc.8`, `0.1.1-rc.1`, `0.1.1-rc.2`, `0.1.2-rc.1`, `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2` (증적은 `.compat-results/`)
40
40
  <!-- host-compat:end -->
41
41
 
package/README.md CHANGED
@@ -57,7 +57,7 @@
57
57
  > **目前建议搭配**:配合**一般提醒**插件(如 [dsh-notify](https://github.com/zhengjy01/dsh-notify),到点桌面提醒「该冻结/该恢复」)与**计费统计**插件(如 [dsh-deepseek-usage](https://github.com/yyb16yyb-hub/dsh-deepseek-usage)、[dsh-cost-tracker](https://github.com/yflmq001/dsh-cost-tracker)、[dsh-billing-balance](https://github.com/YZz-S/dsh-billing-balance),核对冻结前后的实际花费),形成「提醒 → 冻结 → 错峰恢复 → 对账」的省钱闭环。
58
58
 
59
59
  <!-- host-compat:begin — generated by scripts/sync-hosts.mjs from scripts/hosts.mjs; DO NOT EDIT -->
60
- - **支持的 DSH 宿主:** `0.1.0-rc.2` `0.1.0-rc.3` `0.1.0-rc.6` `0.1.0-rc.7` `0.1.0-rc.8` `0.1.1-rc.1` `0.1.1-rc.2` `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-rc.1` `0.2.1-rc.2`(覆盖 0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1 共 7 条线 17 个 rc)
60
+ - **支持的 DSH 宿主:** `0.1.0-rc.2` `0.1.0-rc.3` `0.1.0-rc.6` `0.1.0-rc.7` `0.1.0-rc.8` `0.1.1-rc.1` `0.1.1-rc.2` `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-rc.1` `0.2.1-rc.2` `0.2.1-rc.3`(覆盖 0.1.0 / 0.1.1 / 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1 共 7 条线 18 个 rc)
61
61
  - **运行时已验证:** `0.1.0-rc.2`, `0.1.0-rc.3`, `0.1.0-rc.6`, `0.1.0-rc.7`, `0.1.0-rc.8`, `0.1.1-rc.1`, `0.1.1-rc.2`, `0.1.2-rc.1`, `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2`(证据见 `.compat-results/`)
62
62
  <!-- host-compat:end -->
63
63
 
package/dsh.plugin.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-input-traffic",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "main": "./lib/index.js",
5
5
  "description": "Busy-time input queue: three-tier planning, drag-to-reorder, interject/interrupt, batch clear, session freeze.",
6
6
  "engines": {
7
- "dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2"
7
+ "dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3"
8
8
  },
9
9
  "contributes": {
10
10
  "tools": [],
package/lib/client.js CHANGED
@@ -29,7 +29,7 @@ window.__ModuleLoader__.load({
29
29
  "steer.inflight": "{n} 条正在插入",
30
30
  "steer.now": "打断并输入",
31
31
  "steer.now.aria": "红色:打断当前动作并立即输入",
32
- "steer.now.unsupported": "包含非文本内容,暂不支持打断",
32
+ "steer.now.unsupported": "队列含非文本内容,暂不支持打断",
33
33
  "steer.nowFailed": "打断失败,请重试。",
34
34
  "steer.next": "插话发送",
35
35
  "steer.next.aria": "黄色:当前动作结束后插入输入",
@@ -83,7 +83,7 @@ window.__ModuleLoader__.load({
83
83
  "steer.inflight": "{n} inserting",
84
84
  "steer.now": "Interrupt and send",
85
85
  "steer.now.aria": "Red: interrupt the running action and send now",
86
- "steer.now.unsupported": "Contains non-text content; interrupting is unsupported",
86
+ "steer.now.unsupported": "The queue contains non-text content; interrupting is unsupported",
87
87
  "steer.nowFailed": "Interrupt failed, please retry.",
88
88
  "steer.next": "Send after action",
89
89
  "steer.next.aria": "Yellow: insert after the current action finishes",
@@ -137,7 +137,7 @@ window.__ModuleLoader__.load({
137
137
  "steer.inflight": "{n} 件挿入中",
138
138
  "steer.now": "中断して送信",
139
139
  "steer.now.aria": "赤:実行中のアクションを中断して即座に送信",
140
- "steer.now.unsupported": "非テキストコンテンツが含まれており、中断はサポートされていません",
140
+ "steer.now.unsupported": "キューに非テキストコンテンツが含まれるため、中断はサポートされていません",
141
141
  "steer.nowFailed": "中断失敗。再試行してください。",
142
142
  "steer.next": "アクション後に送信",
143
143
  "steer.next.aria": "黄:現在のアクション完了後に挿入",
@@ -191,7 +191,7 @@ window.__ModuleLoader__.load({
191
191
  "steer.inflight": "{n}개 삽입 중",
192
192
  "steer.now": "중단 후 전송",
193
193
  "steer.now.aria": "빨간색: 실행 중인 액션을 중단하고 즉시 전송",
194
- "steer.now.unsupported": "비텍스트 콘텐츠가 포함되어 있어 중단이 지원되지 않습니다",
194
+ "steer.now.unsupported": "대기열에 비텍스트 콘텐츠가 있어 중단이 지원되지 않습니다",
195
195
  "steer.nowFailed": "중단 실패. 다시 시도하세요.",
196
196
  "steer.next": "액션 후 전송",
197
197
  "steer.next.aria": "노란색: 현재 액션 완료 후 삽입",
@@ -245,7 +245,7 @@ window.__ModuleLoader__.load({
245
245
  "steer.inflight": "{n} en cours d'insertion",
246
246
  "steer.now": "Interrompre et envoyer",
247
247
  "steer.now.aria": "Rouge : interrompre l'action en cours et envoyer immédiatement",
248
- "steer.now.unsupported": "Contient du contenu non textuel ; interruption non prise en charge",
248
+ "steer.now.unsupported": "La file contient du contenu non textuel ; interruption non prise en charge",
249
249
  "steer.nowFailed": "Échec de l'interruption, veuillez réessayer.",
250
250
  "steer.next": "Envoyer après l'action",
251
251
  "steer.next.aria": "Jaune : insérer après la fin de l'action en cours",
@@ -299,7 +299,7 @@ window.__ModuleLoader__.load({
299
299
  "steer.inflight": "{n} werden eingefügt",
300
300
  "steer.now": "Unterbrechen und senden",
301
301
  "steer.now.aria": "Rot: Laufende Aktion unterbrechen und sofort senden",
302
- "steer.now.unsupported": "Enthält Nicht-Text-Inhalt; Unterbrechen nicht unterstützt",
302
+ "steer.now.unsupported": "Warteschlange enthält Nicht-Text-Inhalt; Unterbrechen nicht unterstützt",
303
303
  "steer.nowFailed": "Unterbrechen fehlgeschlagen, bitte erneut versuchen.",
304
304
  "steer.next": "Nach Aktion senden",
305
305
  "steer.next.aria": "Gelb: Nach Abschluss der aktuellen Aktion einfügen",
@@ -353,7 +353,7 @@ window.__ModuleLoader__.load({
353
353
  "steer.inflight": "{n} in inserimento",
354
354
  "steer.now": "Interrompi e invia",
355
355
  "steer.now.aria": "Rosso: interrompi l'azione in corso e invia immediatamente",
356
- "steer.now.unsupported": "Contiene contenuto non testuale; interruzione non supportata",
356
+ "steer.now.unsupported": "La coda contiene contenuto non testuale; interruzione non supportata",
357
357
  "steer.nowFailed": "Interruzione fallita, riprovare.",
358
358
  "steer.next": "Invia dopo l'azione",
359
359
  "steer.next.aria": "Giallo: inserisci al termine dell'azione corrente",
@@ -407,7 +407,7 @@ window.__ModuleLoader__.load({
407
407
  "steer.inflight": "{n} вставляется",
408
408
  "steer.now": "Прервать и отправить",
409
409
  "steer.now.aria": "Красный: прервать текущее действие и отправить немедленно",
410
- "steer.now.unsupported": "Содержит нетекстовый контент; прерывание не поддерживается",
410
+ "steer.now.unsupported": "Очередь содержит нетекстовый контент; прерывание не поддерживается",
411
411
  "steer.nowFailed": "Не удалось прервать, повторите.",
412
412
  "steer.next": "Отправить после действия",
413
413
  "steer.next.aria": "Жёлтый: вставить после завершения текущего действия",
@@ -461,7 +461,7 @@ window.__ModuleLoader__.load({
461
461
  "steer.inflight": "{n} insertándose",
462
462
  "steer.now": "Interrumpir y enviar",
463
463
  "steer.now.aria": "Rojo: interrumpir la acción en curso y enviar inmediatamente",
464
- "steer.now.unsupported": "Contiene contenido no textual; interrupción no compatible",
464
+ "steer.now.unsupported": "La cola contiene contenido no textual; interrupción no compatible",
465
465
  "steer.nowFailed": "Error al interrumpir, inténtelo de nuevo.",
466
466
  "steer.next": "Enviar después de la acción",
467
467
  "steer.next.aria": "Amarillo: insertar después de que termine la acción actual",
@@ -652,11 +652,14 @@ window.__ModuleLoader__.load({
652
652
  * later (the yellow flow is reversible);
653
653
  * - yellow (next): strict-steer the row into the running turn, consumed at
654
654
  * the next step boundary (after the current action finishes);
655
- * - red (now): cancel the running turn, then remove the row and re-send its
656
- * text as a fresh message —the re-send arms the harness wake latch (the
657
- * converged driver restarts) and the interrupted input is processed as the
658
- * next turn's input. A plain steer after cancel would re-insert a message
659
- * that is already pending in next-turn and the inbox rejects it.
655
+ * - red (now): interrupt the running turn and make that single row the next
656
+ * input. Every other pending row leaves the queue first and comes back
657
+ * afterwards in its original order as a plain queued (green) row — the
658
+ * re-sent target is then the only pending row, hence the one the woken
659
+ * driver claims. A cancelled turn has no next step left for a yellow row to
660
+ * steer into, so falling back to green is the correct outcome. Every touched
661
+ * row must be pure text: the re-send path only carries text (a durable file
662
+ * reference has no way back into `prompt`).
660
663
  *
661
664
  * The toolbar also exposes a queue-level "cancel and clear" that stops the
662
665
  * current run and removes every queued row, plus a session-level
@@ -682,8 +685,13 @@ window.__ModuleLoader__.load({
682
685
  }
683
686
  /** Busy marker for a whole-queue rebuild (reorder); locks every row action. */
684
687
  const REORDER_MARK = "__reorder__";
685
- /** localStorage key for the manual collapse state (dsh-queue-plus parity). */
686
- const COLLAPSE_KEY = "dsh-input-traffic:collapsed";
688
+ /**
689
+ * localStorage key for the MANUAL collapse state. Deliberately versioned v2:
690
+ * v1 was also written by an auto-collapse (empty-queue) path, so a stored v1
691
+ * value is not necessarily a user decision and must not shadow the new
692
+ * expanded-by-default layout.
693
+ */
694
+ const COLLAPSE_KEY = "dsh-input-traffic:collapsed:v2";
687
695
  /**
688
696
  * Project the tier badge of one inbox row from its placement. Pure so the
689
697
  * row rendering and its tests share one truth.
@@ -743,9 +751,9 @@ window.__ModuleLoader__.load({
743
751
  const [collapsed, setCollapsed] = (0, react.useState)(() => {
744
752
  try {
745
753
  const v = localStorage.getItem(COLLAPSE_KEY);
746
- return v === null ? true : v === "1";
754
+ return v === null ? false : v === "1";
747
755
  } catch {
748
- return true;
756
+ return false;
749
757
  }
750
758
  });
751
759
  const { frozen, pending: frozenPending } = (0, react.useSyncExternalStore)(freezeStore.subscribe, () => freezeStore.getSnapshot(sessionId ?? ""));
@@ -753,15 +761,15 @@ window.__ModuleLoader__.load({
753
761
  const listId = (0, react.useId)();
754
762
  const editorRef = (0, react.useRef)(null);
755
763
  (0, react.useEffect)(() => {
756
- if (queue.length === 0 && !collapsed) setCollapsed(true);
757
764
  if (editing !== null && !editing.id.startsWith("frozen:") && (!queueMutable || !queue.some((row) => row.id === editing.id))) setEditing(null);
758
765
  }, [
759
- collapsed,
760
766
  editing,
761
767
  queue,
762
768
  queueMutable
763
769
  ]);
770
+ const collapseTouched = (0, react.useRef)(false);
764
771
  (0, react.useEffect)(() => {
772
+ if (!collapseTouched.current) return;
765
773
  try {
766
774
  localStorage.setItem(COLLAPSE_KEY, collapsed ? "1" : "0");
767
775
  } catch {}
@@ -782,6 +790,7 @@ window.__ModuleLoader__.load({
782
790
  const planDisabled = !queueMutable || !running || frozen;
783
791
  const nothingPending = queue.length === 0 && steering.length === 0;
784
792
  const reorderUnsupported = queue.some((row) => row.text === null);
793
+ const interruptUnsupported = reorderUnsupported || steering.some((row) => row.text === null);
785
794
  const applyAction = async (itemId, action, failure) => {
786
795
  setBusy(itemId);
787
796
  try {
@@ -818,16 +827,71 @@ window.__ModuleLoader__.load({
818
827
  setBusy((current) => current === itemId ? null : current);
819
828
  }
820
829
  };
821
- const steerRow = async (row, tier) => {
830
+ /**
831
+ * Interrupt the running turn and make ONE row the next input; every other
832
+ * pending row falls back to a plain queued (green) row in its original
833
+ * order.
834
+ *
835
+ * The host only ever wakes a turn through `followup`, which appends to the
836
+ * TAIL of next-turn, and the woken driver claims the HEAD. So "submit this
837
+ * row now" has to be built as: interrupt first (the aborted driver parks the
838
+ * queue and claims nothing while it quiesces), take every other row out,
839
+ * re-send this one — it is then the only pending row, hence the head — and
840
+ * finally put the others back in order.
841
+ * @param row - the row to submit, already known to carry pure text.
842
+ */
843
+ const interruptRow = async (row) => {
822
844
  setBusy(row.id);
823
845
  try {
824
- if (tier === "now") {
825
- await cancel();
846
+ await cancel();
847
+ const displaced = [...queue.filter((candidate) => candidate.id !== row.id), ...steering];
848
+ const restored = [];
849
+ let restoreFailed = false;
850
+ for (const candidate of displaced) try {
851
+ await updateQueue(candidate.id, { kind: "remove" });
852
+ restored.push(candidate);
853
+ } catch (error) {
854
+ if (!(error instanceof Error) || !error.message.includes("queue-item-not-found")) restoreFailed = true;
855
+ }
856
+ try {
826
857
  await updateQueue(row.id, { kind: "remove" });
827
- if (row.text !== null) await send(row.text);
828
- } else await updateQueue(row.id, { kind: "steer" });
858
+ } catch {}
859
+ try {
860
+ await send(row.text);
861
+ } finally {
862
+ for (const candidate of restored) {
863
+ if (candidate.text === null) continue;
864
+ try {
865
+ await send(candidate.text);
866
+ } catch {
867
+ restoreFailed = true;
868
+ }
869
+ }
870
+ }
871
+ if (restoreFailed) notify("error", t("steer.nowFailed"));
872
+ } catch {
873
+ notify("error", t("steer.nowFailed"));
874
+ } finally {
875
+ setBusy((current) => current === row.id ? null : current);
876
+ }
877
+ };
878
+ const steerRow = async (row, tier) => {
879
+ if (tier === "now") {
880
+ if (row.text === null || interruptUnsupported) {
881
+ notify("error", t("steer.now.unsupported"));
882
+ return;
883
+ }
884
+ await interruptRow({
885
+ id: row.id,
886
+ text: row.text
887
+ });
888
+ return;
889
+ }
890
+ setBusy(row.id);
891
+ try {
892
+ await updateQueue(row.id, { kind: "steer" });
829
893
  } catch {
830
- notify("error", tier === "now" ? t("steer.nowFailed") : t("steer.nextFailed"));
894
+ notify("error", t("steer.nextFailed"));
831
895
  } finally {
832
896
  setBusy((current) => current === row.id ? null : current);
833
897
  }
@@ -966,6 +1030,7 @@ window.__ModuleLoader__.load({
966
1030
  "aria-expanded": expanded,
967
1031
  disabled: queue.length <= 1 || interactionActive,
968
1032
  onClick: () => {
1033
+ collapseTouched.current = true;
969
1034
  setCollapsed((value) => !value);
970
1035
  },
971
1036
  children: [
@@ -1425,13 +1490,13 @@ window.__ModuleLoader__.load({
1425
1490
  label: t("steer.now"),
1426
1491
  side: "bottom",
1427
1492
  delayMs: 500,
1428
- disabled: planDisabled || row.text === null,
1493
+ disabled: planDisabled || interruptUnsupported,
1429
1494
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1430
1495
  type: "button",
1431
1496
  className: `${steer_queue_dock_module_css_default.tier} ${steer_queue_dock_module_css_default.tierNow}`,
1432
1497
  "aria-label": t("steer.now"),
1433
- title: planDisabled ? t("steer.unavailable.running") : row.text === null ? t("steer.now.unsupported") : void 0,
1434
- disabled: busy !== null || planDisabled || row.text === null,
1498
+ title: planDisabled ? t("steer.unavailable.running") : interruptUnsupported ? t("steer.now.unsupported") : void 0,
1499
+ disabled: busy !== null || planDisabled || interruptUnsupported,
1435
1500
  onClick: () => {
1436
1501
  steerRow(row, "now");
1437
1502
  },