dsh-input-traffic 0.1.1 → 0.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.en.md CHANGED
@@ -71,10 +71,15 @@ Each waiting-area message (while not frozen) offers:
71
71
  | Action | Description |
72
72
  |---|---|
73
73
  | Move up / down | Reorder the FIFO queue (the whole queue is rebuilt in the new order; disabled while any image message is queued) |
74
+ | **Drag to reorder** | Drag a row onto its target position (native HTML5 DnD, no extra dependency); same server-side rebuild as the arrow buttons |
74
75
  | Edit in composer | Back-fill the message into the composer draft and remove it from the queue for editing |
75
76
  | Edit / remove | Edit the queued content in a multi-line editor / cancel the message |
76
77
  | Red / yellow / green planning | See "The three tiers" |
77
- | Cancel and clear | Stop the current run and remove every queued message |
78
+ | Cancel and clear | Two-step confirm, then stop the current run and remove every queued message (first click shows "Confirm clear?") |
79
+
80
+ **Concurrency protection** for reordering: if a message was already claimed by the agent during the rebuild (`queue-item-not-found`), the reorder stops immediately and nothing is re-sent — the changed queue is never scrambled.
81
+
82
+ The waiting area's **collapse state is remembered** across sessions.
78
83
 
79
84
  When editing a queued message (inline):
80
85
 
@@ -106,9 +111,36 @@ Local build and tests:
106
111
  npm install --legacy-peer-deps # the @deepseek-ai client package chain is incomplete on npm; toolchain only
107
112
  npm run build # tsc (lib/types) + tsdown (lib/index.js + lib/client.js)
108
113
  node examples/verify-assembly.mjs # 12 assembly assertions
109
- npm test # 32 vitest component tests
114
+ npm test # 36 vitest component tests
115
+ npm run lint # ESLint (src + tests, flat config)
116
+ npm run verify # one-shot gate: lint + test + build + verify-assembly
117
+ ```
118
+
119
+ ## Development (TDD + Lint)
120
+
121
+ This project is maintained with **TDD** (test-driven development): write the failing test first, then implement to green.
122
+
123
+ ```sh
124
+ npm run tdd # vitest watch: re-runs on change, red-to-green loop
110
125
  ```
111
126
 
127
+ Workflow:
128
+
129
+ 1. Add/update a case in `tests/` (red: confirm the new behavior is not implemented yet);
130
+ 2. `npm run tdd` and watch it fail;
131
+ 3. Implement the minimal change in `src/` (green);
132
+ 4. `npm run verify` all green before committing (lint + 36 tests + build + 12 assembly assertions).
133
+
134
+ Lint:
135
+
136
+ ```sh
137
+ npm run lint # ESLint flat config (eslint.config.mjs)
138
+ npm run lint:fix # auto-fix what can be fixed
139
+ ```
140
+
141
+ - Scope: `src/` and `tests/` (TypeScript + React); build output `lib/` is ignored;
142
+ - Rules: `@typescript-eslint/recommended` + `react-hooks` best practices; unused variables are errors (underscore prefix `_` exempts).
143
+
112
144
  ## Usage
113
145
 
114
146
  1. While the agent is busy, type and send — the message enters the **waiting area** (green queue by default);
@@ -193,7 +225,7 @@ End-to-end browser verification on a live `dsh web`, zero application console er
193
225
  | Red now | Interrupted message is processed immediately: the agent replies to it explicitly and continues; no stranded intermediate state |
194
226
  | Yellow next + green revoke | After interjecting, green pulls the message back to the queue |
195
227
  | Freeze / resume | Current turn finishes naturally without interruption, queue frozen with banner; resume drains everything in FIFO order |
196
- | Queue editing (multi-line / fallback) | Covered by component tests (32/32 green); real-environment re-check pending |
228
+ | Queue editing (multi-line / fallback / drag-reorder / confirm) | Covered by component tests (36/36 green); real-environment re-check pending |
197
229
 
198
230
  ## References
199
231
 
package/README.md CHANGED
@@ -71,10 +71,15 @@
71
71
  | 操作 | 说明 |
72
72
  |---|---|
73
73
  | 上移 / 下移 | 调整 FIFO 顺序(整个队列按新顺序重建;含图片消息时禁用) |
74
+ | **拖拽排序** | 按住行直接拖动到目标位置(原生 HTML5 DnD,无额外依赖);与箭头按钮同样走服务端重建 |
74
75
  | 打回输入框编辑 | 消息内容回填 composer 输入框并从队列移除,编辑后重新发送 |
75
76
  | 编辑 / 删除 | 多行文本区直接修改排队内容 / 取消该消息 |
76
77
  | 红 / 黄 / 绿规划 | 见「三档语义」 |
77
- | 取消并清空 | 停止当前执行并清空全部排队消息 |
78
+ | 取消并清空 | 两步确认后停止当前执行并清空全部排队消息(首次点击弹出「确认清空?」) |
79
+
80
+ 排序的**并发保护**:重建期间若某条消息已被 agent 认领(`queue-item-not-found`),本次排序立即中止且不重发,提示「队列已变化,本次排序已取消」——绝不会把变化中的队列排乱。
81
+
82
+ 等待区的**展开状态会记忆**:手动收起/展开后,下次打开插件保持同样状态。
78
83
 
79
84
  编辑排队消息(行内编辑)时:
80
85
 
@@ -106,9 +111,36 @@ dsh web
106
111
  npm install --legacy-peer-deps # @deepseek-ai client 包链在 npm 上不完整,仅装工具链
107
112
  npm run build # tsc(lib/types)+ tsdown(lib/index.js + lib/client.js)
108
113
  node examples/verify-assembly.mjs # 12 项装配断言
109
- npm test # 32 项 vitest 组件测试
114
+ npm test # 36 项 vitest 组件测试
115
+ npm run lint # ESLint(src + tests,flat config)
116
+ npm run verify # 一体化门禁:lint + test + build + verify-assembly
117
+ ```
118
+
119
+ ## 开发(TDD + Lint)
120
+
121
+ 本项目按 **TDD**(测试驱动开发)维护:先写失败用例,再实现到全绿。
122
+
123
+ ```sh
124
+ npm run tdd # vitest watch:改动即重跑,红→绿闭环
110
125
  ```
111
126
 
127
+ 流程:
128
+
129
+ 1. 在 `tests/` 新增/修改用例(红:确认新行为尚未实现);
130
+ 2. `npm run tdd` 观察失败;
131
+ 3. 在 `src/` 最小实现(绿);
132
+ 4. `npm run verify` 全绿后提交(lint + 36 测试 + 构建 + 12 项装配断言)。
133
+
134
+ Lint 说明:
135
+
136
+ ```sh
137
+ npm run lint # ESLint flat config(eslint.config.mjs)
138
+ npm run lint:fix # 自动修复可修复项
139
+ ```
140
+
141
+ - 范围:`src/` 与 `tests/`(TypeScript + React);构建产物 `lib/` 忽略;
142
+ - 规则:`@typescript-eslint/recommended` + `react-hooks` 最佳实践;未使用变量报错(下划线前缀 `_` 可豁免)。
143
+
112
144
  ## 使用方式
113
145
 
114
146
  1. 智能体忙碌时直接输入并发送,消息**统一进入等待区**(默认绿色排队);
package/lib/client.js CHANGED
@@ -40,6 +40,8 @@ window.__ModuleLoader__.load({
40
40
  "steer.moveDown": "下移",
41
41
  "steer.reorder.unsupported": "包含非文本内容,暂不支持排序",
42
42
  "steer.reorderFailed": "调整顺序失败,请重试。",
43
+ "steer.reorderStale": "队列已变化,本次排序已取消,请重试。",
44
+ "steer.dragReorder": "拖动调整顺序",
43
45
  "steer.pullBack": "打回输入框编辑",
44
46
  "steer.pullBack.unsupported": "包含非文本内容,暂不支持打回编辑",
45
47
  "steer.pullBack.composerBusy": "主输入框已有内容,请先发送或清空后再打回",
@@ -49,6 +51,7 @@ window.__ModuleLoader__.load({
49
51
  "steer.badge.later": "排队",
50
52
  "steer.unavailable.running": "仅运行中可规划",
51
53
  "steer.clear": "取消并清空",
54
+ "steer.clear.confirm": "确认清空?",
52
55
  "steer.clear.aria": "取消当前执行并清空全部排队消息",
53
56
  "steer.clearFailed": "取消并清空失败,请重试。",
54
57
  "steer.freeze": "冻结会话",
@@ -86,6 +89,8 @@ window.__ModuleLoader__.load({
86
89
  "steer.moveDown": "Move down",
87
90
  "steer.reorder.unsupported": "Contains non-text content; reordering is unsupported",
88
91
  "steer.reorderFailed": "Reorder failed, please retry.",
92
+ "steer.reorderStale": "The queue changed; this reorder was cancelled, please retry.",
93
+ "steer.dragReorder": "Drag to reorder",
89
94
  "steer.pullBack": "Edit in composer",
90
95
  "steer.pullBack.unsupported": "Contains non-text content; pulling back is unsupported",
91
96
  "steer.pullBack.composerBusy": "The composer already has content; send or clear it first",
@@ -95,6 +100,7 @@ window.__ModuleLoader__.load({
95
100
  "steer.badge.later": "queued",
96
101
  "steer.unavailable.running": "Planning requires a running session",
97
102
  "steer.clear": "Cancel and clear",
103
+ "steer.clear.confirm": "Confirm clear?",
98
104
  "steer.clear.aria": "Cancel the current run and clear all queued messages",
99
105
  "steer.clearFailed": "Cancel and clear failed, please retry.",
100
106
  "steer.freeze": "Freeze session",
@@ -128,7 +134,7 @@ window.__ModuleLoader__.load({
128
134
  };
129
135
  //#endregion
130
136
  //#region \0dsh-css:E:\test\rewrite-agently\dsh-input-traffic\src\client\steer-queue-dock.module.css.mjs
131
- const css$1 = ".cev0eq_dock{width:100%}.cev0eq_panel{background:var(--ds-color-surface,#fffc);border:1px solid var(--ds-color-border,#00000014);border-radius:10px;overflow:hidden}.cev0eq_toolbar{border-bottom:1px solid var(--ds-color-border,#0000000f);align-items:center;gap:8px;padding:4px 8px;display:flex}.cev0eq_header{color:var(--ds-color-text-secondary,#6b7280);cursor:pointer;background:0 0;border:0;align-items:center;gap:6px;padding:2px 6px;font-size:12px;display:inline-flex}.cev0eq_header:disabled{cursor:default}.cev0eq_lead{color:var(--ds-color-text-tertiary,#9ca3af);display:inline-flex}.cev0eq_count{font-weight:600}.cev0eq_chevron{color:var(--ds-color-text-tertiary,#9ca3af);display:inline-flex}.cev0eq_toolbarActions{margin-left:auto}.cev0eq_clear{color:var(--ds-color-text-secondary,#6b7280);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;align-items:center;gap:4px;padding:3px 8px;font-size:12px;display:inline-flex}.cev0eq_freeze{border:1px solid var(--ds-color-border,#0000001f);color:var(--ds-color-text-secondary,#6b7280);cursor:pointer;background:0 0;border-radius:6px;align-items:center;gap:4px;padding:3px 8px;font-size:12px;display:inline-flex}.cev0eq_freeze[aria-pressed=true]{border-color:var(--ds-steer-green,#30a46c);color:var(--ds-steer-green,#30a46c)}.cev0eq_freeze:hover:not(:disabled){background:var(--ds-color-surface-hover,#0000000a)}.cev0eq_freeze:disabled{opacity:.5;cursor:default}.cev0eq_frozenBanner{border-bottom:1px solid var(--ds-color-border,#0000000f);background:color-mix(in srgb, var(--ds-steer-green,#30a46c) 8%, transparent);color:var(--ds-steer-green,#30a46c);padding:4px 10px;font-size:12px}.cev0eq_clear:hover:not(:disabled){border-color:var(--ds-color-border,#0000001f);background:var(--ds-color-surface-hover,#0000000a)}.cev0eq_clear:disabled{opacity:.5;cursor:default}.cev0eq_clearLabel{font-size:12px}.cev0eq_list{margin:0;padding:4px;list-style:none}.cev0eq_row{border-radius:6px;align-items:center;gap:8px;padding:4px 6px;display:flex}.cev0eq_row:hover{background:var(--ds-color-surface-hover,#00000008)}.cev0eq_row[data-editing]{align-items:flex-start}.cev0eq_preview{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--ds-color-text,#1f2937);flex:1;font-size:13px;overflow:hidden}.cev0eq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:4px;padding:1px 6px;font-size:11px;line-height:16px;display:inline-flex}.cev0eq_badgeLabel{color:inherit}.cev0eq_badgeNow{color:var(--ds-steer-red,#e5484d);background:color-mix(in srgb, var(--ds-steer-red,#e5484d) 10%, transparent)}.cev0eq_badgeNext{color:var(--ds-steer-yellow,#e8a33d);background:color-mix(in srgb, var(--ds-steer-yellow,#e8a33d) 12%, transparent)}.cev0eq_badgeLater{color:var(--ds-steer-green,#30a46c);background:color-mix(in srgb, var(--ds-steer-green,#30a46c) 12%, transparent)}.cev0eq_badgeNow .cev0eq_dot{background:var(--ds-steer-red,#e5484d)}.cev0eq_badgeNext .cev0eq_dot{background:var(--ds-steer-yellow,#e8a33d)}.cev0eq_badgeLater .cev0eq_dot{background:var(--ds-steer-green,#30a46c)}.cev0eq_steeringList{margin:0;padding:0 4px 4px;list-style:none}.cev0eq_steeringList .cev0eq_row{opacity:.85}.cev0eq_editor{border:1px solid var(--ds-color-border,#00000026);background:var(--ds-color-surface,#fff);resize:none;border-radius:6px;flex:1;min-width:0;max-height:160px;padding:4px 6px;font-size:13px;line-height:20px;overflow-y:auto}.cev0eq_actions{align-items:center;gap:2px;display:inline-flex}.cev0eq_action{width:24px;height:24px;color:var(--ds-color-text-tertiary,#9ca3af);cursor:pointer;background:0 0;border:0;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.cev0eq_action:hover:not(:disabled){background:var(--ds-color-surface-hover,#0000000d);color:var(--ds-color-text,#1f2937)}.cev0eq_action:disabled{opacity:.4;cursor:default}.cev0eq_plan{border-left:1px solid var(--ds-color-border,#00000014);align-items:center;gap:2px;margin-left:4px;padding-left:6px;display:inline-flex}.cev0eq_tier{cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;width:20px;height:20px;padding:0;display:inline-flex}.cev0eq_tier:hover:not(:disabled){border-color:var(--ds-color-border,#0003)}.cev0eq_tier:disabled{cursor:default}.cev0eq_tierLater{border-color:var(--ds-steer-green,#30a46c);background:color-mix(in srgb, var(--ds-steer-green,#30a46c) 12%, transparent)}.cev0eq_dot{border-radius:50%;width:10px;height:10px}.cev0eq_tierNow .cev0eq_dot{background:var(--ds-steer-red,#e5484d)}.cev0eq_tierNext .cev0eq_dot{background:var(--ds-steer-yellow,#e8a33d)}.cev0eq_tierLater .cev0eq_dot{background:var(--ds-steer-green,#30a46c)}";
137
+ const css$1 = ".cev0eq_dock{width:100%}.cev0eq_panel{background:var(--ds-color-surface,#fffc);border:1px solid var(--ds-color-border,#00000014);border-radius:10px;overflow:hidden}.cev0eq_toolbar{border-bottom:1px solid var(--ds-color-border,#0000000f);align-items:center;gap:8px;padding:4px 8px;display:flex}.cev0eq_header{color:var(--ds-color-text-secondary,#6b7280);cursor:pointer;background:0 0;border:0;align-items:center;gap:6px;padding:2px 6px;font-size:12px;display:inline-flex}.cev0eq_header:disabled{cursor:default}.cev0eq_lead{color:var(--ds-color-text-tertiary,#9ca3af);display:inline-flex}.cev0eq_count{font-weight:600}.cev0eq_chevron{color:var(--ds-color-text-tertiary,#9ca3af);display:inline-flex}.cev0eq_toolbarActions{margin-left:auto}.cev0eq_clear{color:var(--ds-color-text-secondary,#6b7280);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;align-items:center;gap:4px;padding:3px 8px;font-size:12px;display:inline-flex}.cev0eq_freeze{border:1px solid var(--ds-color-border,#0000001f);color:var(--ds-color-text-secondary,#6b7280);cursor:pointer;background:0 0;border-radius:6px;align-items:center;gap:4px;padding:3px 8px;font-size:12px;display:inline-flex}.cev0eq_freeze[aria-pressed=true]{border-color:var(--ds-steer-green,#30a46c);color:var(--ds-steer-green,#30a46c)}.cev0eq_freeze:hover:not(:disabled){background:var(--ds-color-surface-hover,#0000000a)}.cev0eq_freeze:disabled{opacity:.5;cursor:default}.cev0eq_frozenBanner{border-bottom:1px solid var(--ds-color-border,#0000000f);background:color-mix(in srgb, var(--ds-steer-green,#30a46c) 8%, transparent);color:var(--ds-steer-green,#30a46c);padding:4px 10px;font-size:12px}.cev0eq_clear:hover:not(:disabled){border-color:var(--ds-color-border,#0000001f);background:var(--ds-color-surface-hover,#0000000a)}.cev0eq_clear:disabled{opacity:.5;cursor:default}.cev0eq_clearConfirm{border-color:var(--ds-steer-red,#e5484d);color:var(--ds-steer-red,#e5484d)}.cev0eq_clearConfirm:hover:not(:disabled){border-color:var(--ds-steer-red,#e5484d);background:color-mix(in srgb, var(--ds-steer-red,#e5484d) 8%, transparent)}.cev0eq_clearLabel{font-size:12px}.cev0eq_list{margin:0;padding:4px;list-style:none}.cev0eq_row{border-radius:6px;align-items:center;gap:8px;padding:4px 6px;display:flex}.cev0eq_row[draggable=true]{cursor:grab}.cev0eq_row[draggable=true]:active{cursor:grabbing}.cev0eq_rowDragOver{outline:2px dashed var(--ds-color-border-strong,#00000040);outline-offset:-2px;background:var(--ds-color-surface-hover,#0000000d)}.cev0eq_row:hover{background:var(--ds-color-surface-hover,#00000008)}.cev0eq_row[data-editing]{align-items:flex-start}.cev0eq_preview{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--ds-color-text,#1f2937);flex:1;font-size:13px;overflow:hidden}.cev0eq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:4px;padding:1px 6px;font-size:11px;line-height:16px;display:inline-flex}.cev0eq_badgeLabel{color:inherit}.cev0eq_badgeNow{color:var(--ds-steer-red,#e5484d);background:color-mix(in srgb, var(--ds-steer-red,#e5484d) 10%, transparent)}.cev0eq_badgeNext{color:var(--ds-steer-yellow,#e8a33d);background:color-mix(in srgb, var(--ds-steer-yellow,#e8a33d) 12%, transparent)}.cev0eq_badgeLater{color:var(--ds-steer-green,#30a46c);background:color-mix(in srgb, var(--ds-steer-green,#30a46c) 12%, transparent)}.cev0eq_badgeNow .cev0eq_dot{background:var(--ds-steer-red,#e5484d)}.cev0eq_badgeNext .cev0eq_dot{background:var(--ds-steer-yellow,#e8a33d)}.cev0eq_badgeLater .cev0eq_dot{background:var(--ds-steer-green,#30a46c)}.cev0eq_steeringList{margin:0;padding:0 4px 4px;list-style:none}.cev0eq_steeringList .cev0eq_row{opacity:.85}.cev0eq_editor{border:1px solid var(--ds-color-border,#00000026);background:var(--ds-color-surface,#fff);resize:none;border-radius:6px;flex:1;min-width:0;max-height:160px;padding:4px 6px;font-size:13px;line-height:20px;overflow-y:auto}.cev0eq_actions{align-items:center;gap:2px;display:inline-flex}.cev0eq_action{width:24px;height:24px;color:var(--ds-color-text-tertiary,#9ca3af);cursor:pointer;background:0 0;border:0;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.cev0eq_action:hover:not(:disabled){background:var(--ds-color-surface-hover,#0000000d);color:var(--ds-color-text,#1f2937)}.cev0eq_action:disabled{opacity:.4;cursor:default}.cev0eq_plan{border-left:1px solid var(--ds-color-border,#00000014);align-items:center;gap:2px;margin-left:4px;padding-left:6px;display:inline-flex}.cev0eq_tier{cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;width:20px;height:20px;padding:0;display:inline-flex}.cev0eq_tier:hover:not(:disabled){border-color:var(--ds-color-border,#0003)}.cev0eq_tier:disabled{cursor:default}.cev0eq_tierLater{border-color:var(--ds-steer-green,#30a46c);background:color-mix(in srgb, var(--ds-steer-green,#30a46c) 12%, transparent)}.cev0eq_dot{border-radius:50%;width:10px;height:10px}.cev0eq_tierNow .cev0eq_dot{background:var(--ds-steer-red,#e5484d)}.cev0eq_tierNext .cev0eq_dot{background:var(--ds-steer-yellow,#e8a33d)}.cev0eq_tierLater .cev0eq_dot{background:var(--ds-steer-green,#30a46c)}";
132
138
  const tagId$1 = "dsh-input-traffic/steer-queue-dock.module.css";
133
139
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
134
140
  const tag = document.createElement("style");
@@ -138,36 +144,38 @@ window.__ModuleLoader__.load({
138
144
  document.head.appendChild(tag);
139
145
  }
140
146
  var steer_queue_dock_module_css_default = {
141
- "row": "cev0eq_row",
142
- "clear": "cev0eq_clear",
143
- "list": "cev0eq_list",
144
- "tierLater": "cev0eq_tierLater",
145
- "toolbar": "cev0eq_toolbar",
146
- "count": "cev0eq_count",
147
- "action": "cev0eq_action",
148
- "tierNext": "cev0eq_tierNext",
147
+ "badgeLabel": "cev0eq_badgeLabel",
148
+ "dot": "cev0eq_dot",
149
149
  "tierNow": "cev0eq_tierNow",
150
- "panel": "cev0eq_panel",
150
+ "actions": "cev0eq_actions",
151
+ "editor": "cev0eq_editor",
152
+ "badge": "cev0eq_badge",
153
+ "toolbarActions": "cev0eq_toolbarActions",
151
154
  "clearLabel": "cev0eq_clearLabel",
152
- "badgeLabel": "cev0eq_badgeLabel",
153
- "badgeNow": "cev0eq_badgeNow",
154
155
  "dock": "cev0eq_dock",
155
- "badgeLater": "cev0eq_badgeLater",
156
- "preview": "cev0eq_preview",
157
- "frozenBanner": "cev0eq_frozenBanner",
156
+ "panel": "cev0eq_panel",
157
+ "row": "cev0eq_row",
158
+ "rowDragOver": "cev0eq_rowDragOver",
158
159
  "plan": "cev0eq_plan",
159
- "toolbarActions": "cev0eq_toolbarActions",
160
- "chevron": "cev0eq_chevron",
160
+ "tierLater": "cev0eq_tierLater",
161
+ "freeze": "cev0eq_freeze",
161
162
  "badgeNext": "cev0eq_badgeNext",
162
- "steeringList": "cev0eq_steeringList",
163
- "lead": "cev0eq_lead",
164
- "header": "cev0eq_header",
165
163
  "tier": "cev0eq_tier",
166
- "badge": "cev0eq_badge",
167
- "editor": "cev0eq_editor",
168
- "actions": "cev0eq_actions",
169
- "freeze": "cev0eq_freeze",
170
- "dot": "cev0eq_dot"
164
+ "count": "cev0eq_count",
165
+ "preview": "cev0eq_preview",
166
+ "badgeNow": "cev0eq_badgeNow",
167
+ "chevron": "cev0eq_chevron",
168
+ "toolbar": "cev0eq_toolbar",
169
+ "list": "cev0eq_list",
170
+ "clear": "cev0eq_clear",
171
+ "header": "cev0eq_header",
172
+ "tierNext": "cev0eq_tierNext",
173
+ "lead": "cev0eq_lead",
174
+ "clearConfirm": "cev0eq_clearConfirm",
175
+ "frozenBanner": "cev0eq_frozenBanner",
176
+ "action": "cev0eq_action",
177
+ "steeringList": "cev0eq_steeringList",
178
+ "badgeLater": "cev0eq_badgeLater"
171
179
  };
172
180
  //#endregion
173
181
  //#region src/client/steer-queue-dock.tsx
@@ -198,6 +206,8 @@ window.__ModuleLoader__.load({
198
206
  */
199
207
  /** Busy marker for a whole-queue rebuild (reorder); locks every row action. */
200
208
  const REORDER_MARK = "__reorder__";
209
+ /** localStorage key for the manual collapse state (dsh-queue-plus parity). */
210
+ const COLLAPSE_KEY = "dsh-input-traffic:collapsed";
201
211
  /**
202
212
  * Project the tier badge of one inbox row from its placement. Pure so the
203
213
  * row rendering and its tests share one truth.
@@ -232,7 +242,18 @@ window.__ModuleLoader__.load({
232
242
  const [editing, setEditing] = (0, react.useState)(null);
233
243
  const [busy, setBusy] = (0, react.useState)(null);
234
244
  const [clearing, setClearing] = (0, react.useState)(false);
235
- const [collapsed, setCollapsed] = (0, react.useState)(true);
245
+ const [confirmClear, setConfirmClear] = (0, react.useState)(false);
246
+ const confirmTimer = (0, react.useRef)(null);
247
+ const dragIndex = (0, react.useRef)(null);
248
+ const [dragOver, setDragOver] = (0, react.useState)(null);
249
+ const [collapsed, setCollapsed] = (0, react.useState)(() => {
250
+ try {
251
+ const v = localStorage.getItem(COLLAPSE_KEY);
252
+ return v === null ? true : v === "1";
253
+ } catch {
254
+ return true;
255
+ }
256
+ });
236
257
  const { frozen } = (0, react.useSyncExternalStore)(freezeStore.subscribe, freezeStore.getSnapshot);
237
258
  const listId = (0, react.useId)();
238
259
  const editorRef = (0, react.useRef)(null);
@@ -246,10 +267,19 @@ window.__ModuleLoader__.load({
246
267
  queueMutable
247
268
  ]);
248
269
  (0, react.useEffect)(() => {
249
- if (editing === null) return;
270
+ try {
271
+ localStorage.setItem(COLLAPSE_KEY, collapsed ? "1" : "0");
272
+ } catch {}
273
+ }, [collapsed]);
274
+ (0, react.useEffect)(() => () => {
275
+ if (confirmTimer.current !== null) clearTimeout(confirmTimer.current);
276
+ }, []);
277
+ const editingId = editing?.id;
278
+ (0, react.useEffect)(() => {
279
+ if (editingId === void 0) return;
250
280
  const el = editorRef.current;
251
281
  if (el !== null) resizeEditor(el);
252
- }, [editing?.id]);
282
+ }, [editingId]);
253
283
  if (queue.length === 0 && steering.length === 0 && !running) return null;
254
284
  const interactionActive = queueMutable && (editing !== null || busy !== null || clearing);
255
285
  const expanded = !collapsed || interactionActive;
@@ -326,10 +356,34 @@ window.__ModuleLoader__.load({
326
356
  }
327
357
  };
328
358
  /**
329
- * Move one queued row up/down in the FIFO order. The queue is rebuilt in
330
- * the new order: every queued row is removed and re-sent sequentially
331
- * (plain-text rows only), so the agent's next-turn list matches the
332
- * reordered view.
359
+ * Rebuild the queue in a new order: remove every queued row, then re-send
360
+ * the texts sequentially so the agent's next-turn list matches the new
361
+ * order. Concurrency protection (dsh-queue-plus parity): if a remove fails
362
+ * with queue-item-not-found, the agent already claimed that row — the
363
+ * rebuild stops immediately and nothing is re-sent, so the changed queue
364
+ * is never scrambled.
365
+ */
366
+ const rebuildQueue = async (rows, next) => {
367
+ setBusy(REORDER_MARK);
368
+ try {
369
+ for (const row of rows) try {
370
+ await updateQueue(row.id, { kind: "remove" });
371
+ } catch (error) {
372
+ if (error instanceof Error && error.message.includes("queue-item-not-found")) {
373
+ notify("error", t("steer.reorderStale"));
374
+ return;
375
+ }
376
+ throw error;
377
+ }
378
+ for (const row of next) if (row.text !== null) await send(row.text);
379
+ } catch {
380
+ notify("error", t("steer.reorderFailed"));
381
+ } finally {
382
+ setBusy(null);
383
+ }
384
+ };
385
+ /**
386
+ * Move one queued row up/down in the FIFO order (arrow buttons).
333
387
  */
334
388
  const reorder = async (rowId, delta) => {
335
389
  const rows = queue;
@@ -344,15 +398,36 @@ window.__ModuleLoader__.load({
344
398
  notify("error", t("steer.reorder.unsupported"));
345
399
  return;
346
400
  }
347
- setBusy(REORDER_MARK);
348
- try {
349
- for (const row of rows) await updateQueue(row.id, { kind: "remove" });
350
- for (const row of next) await send(row.text);
351
- } catch {
352
- notify("error", t("steer.reorderFailed"));
353
- } finally {
354
- setBusy(null);
401
+ await rebuildQueue(rows, next);
402
+ };
403
+ /**
404
+ * Move one row to an absolute position (drag-and-drop drop handler).
405
+ */
406
+ const reorderTo = async (fromIndex, toIndex) => {
407
+ if (fromIndex === toIndex) return;
408
+ const rows = queue;
409
+ if (rows[fromIndex] === void 0 || rows[toIndex] === void 0) return;
410
+ const next = [...rows];
411
+ const [moved] = next.splice(fromIndex, 1);
412
+ if (moved === void 0) return;
413
+ next.splice(toIndex, 0, moved);
414
+ if (next.some((row) => row.text === null)) {
415
+ notify("error", t("steer.reorder.unsupported"));
416
+ return;
355
417
  }
418
+ await rebuildQueue(rows, next);
419
+ };
420
+ /** Arm or execute the two-step clear confirmation. */
421
+ const armClear = () => {
422
+ if (confirmClear) {
423
+ if (confirmTimer.current !== null) clearTimeout(confirmTimer.current);
424
+ setConfirmClear(false);
425
+ clearAll();
426
+ return;
427
+ }
428
+ setConfirmClear(true);
429
+ if (confirmTimer.current !== null) clearTimeout(confirmTimer.current);
430
+ confirmTimer.current = setTimeout(() => setConfirmClear(false), 3e3);
356
431
  };
357
432
  /**
358
433
  * Pull one queued row back into the composer draft for editing: the text
@@ -419,20 +494,18 @@ window.__ModuleLoader__.load({
419
494
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
420
495
  className: steer_queue_dock_module_css_default.toolbarActions,
421
496
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
422
- label: t("steer.clear"),
497
+ label: confirmClear ? t("steer.clear.confirm") : t("steer.clear"),
423
498
  side: "top",
424
499
  delayMs: 500,
425
500
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
426
501
  type: "button",
427
- className: steer_queue_dock_module_css_default.clear,
428
- "aria-label": t("steer.clear"),
502
+ className: `${steer_queue_dock_module_css_default.clear} ${confirmClear ? steer_queue_dock_module_css_default.clearConfirm : ""}`,
503
+ "aria-label": confirmClear ? t("steer.clear.confirm") : t("steer.clear"),
429
504
  disabled: clearing || busy !== null || nothingPending,
430
- onClick: () => {
431
- clearAll();
432
- },
505
+ onClick: armClear,
433
506
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
434
507
  className: steer_queue_dock_module_css_default.clearLabel,
435
- children: t("steer.clear")
508
+ children: confirmClear ? t("steer.clear.confirm") : t("steer.clear")
436
509
  })]
437
510
  })
438
511
  })
@@ -447,10 +520,32 @@ window.__ModuleLoader__.load({
447
520
  id: listId,
448
521
  className: steer_queue_dock_module_css_default.list,
449
522
  hidden: !listVisible,
450
- children: listVisible && queue.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
451
- className: steer_queue_dock_module_css_default.row,
523
+ children: listVisible && queue.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
524
+ className: `${steer_queue_dock_module_css_default.row} ${dragOver === index ? steer_queue_dock_module_css_default.rowDragOver : ""}`,
452
525
  "data-tier": badgeFor("queued") ?? void 0,
453
526
  "data-editing": editing?.id === row.id ? "" : void 0,
527
+ draggable: queueMutable && !reorderUnsupported && busy === null && !frozen,
528
+ title: queueMutable && !reorderUnsupported && busy === null && !frozen ? t("steer.dragReorder") : void 0,
529
+ onDragStart: (event) => {
530
+ dragIndex.current = index;
531
+ event.dataTransfer.effectAllowed = "move";
532
+ },
533
+ onDragOver: (event) => {
534
+ if (dragIndex.current === null) return;
535
+ event.preventDefault();
536
+ setDragOver(index);
537
+ },
538
+ onDrop: (event) => {
539
+ event.preventDefault();
540
+ const from = dragIndex.current;
541
+ dragIndex.current = null;
542
+ setDragOver(null);
543
+ if (from !== null) reorderTo(from, index);
544
+ },
545
+ onDragEnd: () => {
546
+ dragIndex.current = null;
547
+ setDragOver(null);
548
+ },
454
549
  children: [
455
550
  queue.length === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
456
551
  className: steer_queue_dock_module_css_default.lead,