dsh-input-traffic 0.1.1 → 0.2.1

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,8 @@ window.__ModuleLoader__.load({
49
51
  "steer.badge.later": "排队",
50
52
  "steer.unavailable.running": "仅运行中可规划",
51
53
  "steer.clear": "取消并清空",
54
+ "steer.clear.confirm": "确认清空?",
55
+ "steer.clear.cancel": "取消清空",
52
56
  "steer.clear.aria": "取消当前执行并清空全部排队消息",
53
57
  "steer.clearFailed": "取消并清空失败,请重试。",
54
58
  "steer.freeze": "冻结会话",
@@ -86,6 +90,8 @@ window.__ModuleLoader__.load({
86
90
  "steer.moveDown": "Move down",
87
91
  "steer.reorder.unsupported": "Contains non-text content; reordering is unsupported",
88
92
  "steer.reorderFailed": "Reorder failed, please retry.",
93
+ "steer.reorderStale": "The queue changed; this reorder was cancelled, please retry.",
94
+ "steer.dragReorder": "Drag to reorder",
89
95
  "steer.pullBack": "Edit in composer",
90
96
  "steer.pullBack.unsupported": "Contains non-text content; pulling back is unsupported",
91
97
  "steer.pullBack.composerBusy": "The composer already has content; send or clear it first",
@@ -95,6 +101,8 @@ window.__ModuleLoader__.load({
95
101
  "steer.badge.later": "queued",
96
102
  "steer.unavailable.running": "Planning requires a running session",
97
103
  "steer.clear": "Cancel and clear",
104
+ "steer.clear.confirm": "Confirm clear?",
105
+ "steer.clear.cancel": "Cancel clearing",
98
106
  "steer.clear.aria": "Cancel the current run and clear all queued messages",
99
107
  "steer.clearFailed": "Cancel and clear failed, please retry.",
100
108
  "steer.freeze": "Freeze session",
@@ -128,7 +136,7 @@ window.__ModuleLoader__.load({
128
136
  };
129
137
  //#endregion
130
138
  //#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)}";
139
+ 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_clearCancel{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_clearCancel:hover:not(:disabled){background:var(--ds-color-surface-hover,#0000000a)}.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
140
  const tagId$1 = "dsh-input-traffic/steer-queue-dock.module.css";
133
141
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
134
142
  const tag = document.createElement("style");
@@ -138,36 +146,39 @@ window.__ModuleLoader__.load({
138
146
  document.head.appendChild(tag);
139
147
  }
140
148
  var steer_queue_dock_module_css_default = {
141
- "row": "cev0eq_row",
142
- "clear": "cev0eq_clear",
143
- "list": "cev0eq_list",
144
- "tierLater": "cev0eq_tierLater",
149
+ "tierNext": "cev0eq_tierNext",
145
150
  "toolbar": "cev0eq_toolbar",
151
+ "badgeLabel": "cev0eq_badgeLabel",
152
+ "rowDragOver": "cev0eq_rowDragOver",
146
153
  "count": "cev0eq_count",
154
+ "toolbarActions": "cev0eq_toolbarActions",
155
+ "row": "cev0eq_row",
156
+ "badge": "cev0eq_badge",
147
157
  "action": "cev0eq_action",
148
- "tierNext": "cev0eq_tierNext",
149
- "tierNow": "cev0eq_tierNow",
158
+ "dock": "cev0eq_dock",
150
159
  "panel": "cev0eq_panel",
160
+ "tier": "cev0eq_tier",
161
+ "list": "cev0eq_list",
162
+ "freeze": "cev0eq_freeze",
163
+ "clear": "cev0eq_clear",
164
+ "tierNow": "cev0eq_tierNow",
165
+ "lead": "cev0eq_lead",
166
+ "chevron": "cev0eq_chevron",
167
+ "tierLater": "cev0eq_tierLater",
168
+ "clearCancel": "cev0eq_clearCancel",
169
+ "frozenBanner": "cev0eq_frozenBanner",
170
+ "editor": "cev0eq_editor",
171
+ "header": "cev0eq_header",
151
172
  "clearLabel": "cev0eq_clearLabel",
152
- "badgeLabel": "cev0eq_badgeLabel",
153
173
  "badgeNow": "cev0eq_badgeNow",
154
- "dock": "cev0eq_dock",
155
174
  "badgeLater": "cev0eq_badgeLater",
175
+ "dot": "cev0eq_dot",
176
+ "actions": "cev0eq_actions",
177
+ "clearConfirm": "cev0eq_clearConfirm",
178
+ "steeringList": "cev0eq_steeringList",
156
179
  "preview": "cev0eq_preview",
157
- "frozenBanner": "cev0eq_frozenBanner",
158
- "plan": "cev0eq_plan",
159
- "toolbarActions": "cev0eq_toolbarActions",
160
- "chevron": "cev0eq_chevron",
161
180
  "badgeNext": "cev0eq_badgeNext",
162
- "steeringList": "cev0eq_steeringList",
163
- "lead": "cev0eq_lead",
164
- "header": "cev0eq_header",
165
- "tier": "cev0eq_tier",
166
- "badge": "cev0eq_badge",
167
- "editor": "cev0eq_editor",
168
- "actions": "cev0eq_actions",
169
- "freeze": "cev0eq_freeze",
170
- "dot": "cev0eq_dot"
181
+ "plan": "cev0eq_plan"
171
182
  };
172
183
  //#endregion
173
184
  //#region src/client/steer-queue-dock.tsx
@@ -198,6 +209,8 @@ window.__ModuleLoader__.load({
198
209
  */
199
210
  /** Busy marker for a whole-queue rebuild (reorder); locks every row action. */
200
211
  const REORDER_MARK = "__reorder__";
212
+ /** localStorage key for the manual collapse state (dsh-queue-plus parity). */
213
+ const COLLAPSE_KEY = "dsh-input-traffic:collapsed";
201
214
  /**
202
215
  * Project the tier badge of one inbox row from its placement. Pure so the
203
216
  * row rendering and its tests share one truth.
@@ -232,7 +245,18 @@ window.__ModuleLoader__.load({
232
245
  const [editing, setEditing] = (0, react.useState)(null);
233
246
  const [busy, setBusy] = (0, react.useState)(null);
234
247
  const [clearing, setClearing] = (0, react.useState)(false);
235
- const [collapsed, setCollapsed] = (0, react.useState)(true);
248
+ const [confirmClear, setConfirmClear] = (0, react.useState)(false);
249
+ const confirmTimer = (0, react.useRef)(null);
250
+ const dragIndex = (0, react.useRef)(null);
251
+ const [dragOver, setDragOver] = (0, react.useState)(null);
252
+ const [collapsed, setCollapsed] = (0, react.useState)(() => {
253
+ try {
254
+ const v = localStorage.getItem(COLLAPSE_KEY);
255
+ return v === null ? true : v === "1";
256
+ } catch {
257
+ return true;
258
+ }
259
+ });
236
260
  const { frozen } = (0, react.useSyncExternalStore)(freezeStore.subscribe, freezeStore.getSnapshot);
237
261
  const listId = (0, react.useId)();
238
262
  const editorRef = (0, react.useRef)(null);
@@ -246,10 +270,19 @@ window.__ModuleLoader__.load({
246
270
  queueMutable
247
271
  ]);
248
272
  (0, react.useEffect)(() => {
249
- if (editing === null) return;
273
+ try {
274
+ localStorage.setItem(COLLAPSE_KEY, collapsed ? "1" : "0");
275
+ } catch {}
276
+ }, [collapsed]);
277
+ (0, react.useEffect)(() => () => {
278
+ if (confirmTimer.current !== null) clearTimeout(confirmTimer.current);
279
+ }, []);
280
+ const editingId = editing?.id;
281
+ (0, react.useEffect)(() => {
282
+ if (editingId === void 0) return;
250
283
  const el = editorRef.current;
251
284
  if (el !== null) resizeEditor(el);
252
- }, [editing?.id]);
285
+ }, [editingId]);
253
286
  if (queue.length === 0 && steering.length === 0 && !running) return null;
254
287
  const interactionActive = queueMutable && (editing !== null || busy !== null || clearing);
255
288
  const expanded = !collapsed || interactionActive;
@@ -326,10 +359,34 @@ window.__ModuleLoader__.load({
326
359
  }
327
360
  };
328
361
  /**
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.
362
+ * Rebuild the queue in a new order: remove every queued row, then re-send
363
+ * the texts sequentially so the agent's next-turn list matches the new
364
+ * order. Concurrency protection (dsh-queue-plus parity): if a remove fails
365
+ * with queue-item-not-found, the agent already claimed that row — the
366
+ * rebuild stops immediately and nothing is re-sent, so the changed queue
367
+ * is never scrambled.
368
+ */
369
+ const rebuildQueue = async (rows, next) => {
370
+ setBusy(REORDER_MARK);
371
+ try {
372
+ for (const row of rows) try {
373
+ await updateQueue(row.id, { kind: "remove" });
374
+ } catch (error) {
375
+ if (error instanceof Error && error.message.includes("queue-item-not-found")) {
376
+ notify("error", t("steer.reorderStale"));
377
+ return;
378
+ }
379
+ throw error;
380
+ }
381
+ for (const row of next) if (row.text !== null) await send(row.text);
382
+ } catch {
383
+ notify("error", t("steer.reorderFailed"));
384
+ } finally {
385
+ setBusy(null);
386
+ }
387
+ };
388
+ /**
389
+ * Move one queued row up/down in the FIFO order (arrow buttons).
333
390
  */
334
391
  const reorder = async (rowId, delta) => {
335
392
  const rows = queue;
@@ -344,15 +401,41 @@ window.__ModuleLoader__.load({
344
401
  notify("error", t("steer.reorder.unsupported"));
345
402
  return;
346
403
  }
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);
404
+ await rebuildQueue(rows, next);
405
+ };
406
+ /**
407
+ * Move one row to an absolute position (drag-and-drop drop handler).
408
+ */
409
+ const reorderTo = async (fromIndex, toIndex) => {
410
+ if (fromIndex === toIndex) return;
411
+ const rows = queue;
412
+ if (rows[fromIndex] === void 0 || rows[toIndex] === void 0) return;
413
+ const next = [...rows];
414
+ const [moved] = next.splice(fromIndex, 1);
415
+ if (moved === void 0) return;
416
+ next.splice(toIndex, 0, moved);
417
+ if (next.some((row) => row.text === null)) {
418
+ notify("error", t("steer.reorder.unsupported"));
419
+ return;
420
+ }
421
+ await rebuildQueue(rows, next);
422
+ };
423
+ /** Arm or execute the two-step clear confirmation. */
424
+ const armClear = () => {
425
+ if (confirmClear) {
426
+ if (confirmTimer.current !== null) clearTimeout(confirmTimer.current);
427
+ setConfirmClear(false);
428
+ clearAll();
429
+ return;
355
430
  }
431
+ setConfirmClear(true);
432
+ if (confirmTimer.current !== null) clearTimeout(confirmTimer.current);
433
+ confirmTimer.current = setTimeout(() => setConfirmClear(false), 3e3);
434
+ };
435
+ /** Abort the armed clear confirmation (the explicit cancel button). */
436
+ const cancelClear = () => {
437
+ if (confirmTimer.current !== null) clearTimeout(confirmTimer.current);
438
+ setConfirmClear(false);
356
439
  };
357
440
  /**
358
441
  * Pull one queued row back into the composer draft for editing: the text
@@ -416,26 +499,34 @@ window.__ModuleLoader__.load({
416
499
  children: expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, {})
417
500
  })
418
501
  ]
419
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
502
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
420
503
  className: steer_queue_dock_module_css_default.toolbarActions,
421
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
422
- label: t("steer.clear"),
504
+ children: [confirmClear && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
505
+ type: "button",
506
+ className: steer_queue_dock_module_css_default.clearCancel,
507
+ "aria-label": t("steer.clear.cancel"),
508
+ disabled: clearing || busy !== null || nothingPending,
509
+ onClick: cancelClear,
510
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
511
+ className: steer_queue_dock_module_css_default.clearLabel,
512
+ children: t("steer.clear.cancel")
513
+ })
514
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
515
+ label: confirmClear ? t("steer.clear.confirm") : t("steer.clear"),
423
516
  side: "top",
424
517
  delayMs: 500,
425
518
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
426
519
  type: "button",
427
- className: steer_queue_dock_module_css_default.clear,
428
- "aria-label": t("steer.clear"),
520
+ className: `${steer_queue_dock_module_css_default.clear} ${confirmClear ? steer_queue_dock_module_css_default.clearConfirm : ""}`,
521
+ "aria-label": confirmClear ? t("steer.clear.confirm") : t("steer.clear"),
429
522
  disabled: clearing || busy !== null || nothingPending,
430
- onClick: () => {
431
- clearAll();
432
- },
523
+ onClick: armClear,
433
524
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
434
525
  className: steer_queue_dock_module_css_default.clearLabel,
435
- children: t("steer.clear")
526
+ children: confirmClear ? t("steer.clear.confirm") : t("steer.clear")
436
527
  })]
437
528
  })
438
- })
529
+ })]
439
530
  })]
440
531
  }),
441
532
  frozen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -447,10 +538,32 @@ window.__ModuleLoader__.load({
447
538
  id: listId,
448
539
  className: steer_queue_dock_module_css_default.list,
449
540
  hidden: !listVisible,
450
- children: listVisible && queue.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
451
- className: steer_queue_dock_module_css_default.row,
541
+ children: listVisible && queue.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
542
+ className: `${steer_queue_dock_module_css_default.row} ${dragOver === index ? steer_queue_dock_module_css_default.rowDragOver : ""}`,
452
543
  "data-tier": badgeFor("queued") ?? void 0,
453
544
  "data-editing": editing?.id === row.id ? "" : void 0,
545
+ draggable: queueMutable && !reorderUnsupported && busy === null && !frozen,
546
+ title: queueMutable && !reorderUnsupported && busy === null && !frozen ? t("steer.dragReorder") : void 0,
547
+ onDragStart: (event) => {
548
+ dragIndex.current = index;
549
+ event.dataTransfer.effectAllowed = "move";
550
+ },
551
+ onDragOver: (event) => {
552
+ if (dragIndex.current === null) return;
553
+ event.preventDefault();
554
+ setDragOver(index);
555
+ },
556
+ onDrop: (event) => {
557
+ event.preventDefault();
558
+ const from = dragIndex.current;
559
+ dragIndex.current = null;
560
+ setDragOver(null);
561
+ if (from !== null) reorderTo(from, index);
562
+ },
563
+ onDragEnd: () => {
564
+ dragIndex.current = null;
565
+ setDragOver(null);
566
+ },
454
567
  children: [
455
568
  queue.length === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
456
569
  className: steer_queue_dock_module_css_default.lead,
@@ -766,8 +879,8 @@ window.__ModuleLoader__.load({
766
879
  document.head.appendChild(tag);
767
880
  }
768
881
  var freeze_button_module_css_default = {
769
- "freeze": "Klmgza_freeze",
770
- "label": "Klmgza_label"
882
+ "label": "Klmgza_label",
883
+ "freeze": "Klmgza_freeze"
771
884
  };
772
885
  //#endregion
773
886
  //#region src/client/freeze-button.tsx