dsh-sidenote 0.3.4 → 0.3.5

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/lib/client.js CHANGED
@@ -2181,92 +2181,92 @@ window.__ModuleLoader__.load({
2181
2181
  document.head.appendChild(tag);
2182
2182
  }
2183
2183
  var sidechat_module_css_default = {
2184
- "pendingBar": "_5KEEsG_pendingBar",
2185
- "composerCard": "_5KEEsG_composerCard",
2186
- "attachRail": "_5KEEsG_attachRail",
2187
- "reasoningBody": "_5KEEsG_reasoningBody",
2188
- "empty": "_5KEEsG_empty",
2189
- "dotRunning": "_5KEEsG_dotRunning",
2190
- "toolBody": "_5KEEsG_toolBody",
2184
+ "todoBody": "_5KEEsG_todoBody",
2185
+ "attachButton": "_5KEEsG_attachButton",
2186
+ "writeNoticeClose": "_5KEEsG_writeNoticeClose",
2187
+ "pendingBtnGhost": "_5KEEsG_pendingBtnGhost",
2191
2188
  "attachImg": "_5KEEsG_attachImg",
2192
- "slashItem": "_5KEEsG_slashItem",
2193
- "stateDetail": "_5KEEsG_stateDetail",
2194
- "sentChipRow": "_5KEEsG_sentChipRow",
2189
+ "composer": "_5KEEsG_composer",
2190
+ "headerSide": "_5KEEsG_headerSide",
2191
+ "toolGenericBody": "_5KEEsG_toolGenericBody",
2192
+ "toolHead": "_5KEEsG_toolHead",
2193
+ "assistantBody": "_5KEEsG_assistantBody",
2194
+ "sendButton": "_5KEEsG_sendButton",
2195
2195
  "userBubble": "_5KEEsG_userBubble",
2196
2196
  "rowPreview": "_5KEEsG_rowPreview",
2197
- "reflowButton": "_5KEEsG_reflowButton",
2198
- "toolLocations": "_5KEEsG_toolLocations",
2199
- "toolGenericBody": "_5KEEsG_toolGenericBody",
2200
- "jumpBottom": "_5KEEsG_jumpBottom",
2201
- "streamingHint": "_5KEEsG_streamingHint",
2202
- "pendingTitle": "_5KEEsG_pendingTitle",
2203
- "inheritedBody": "_5KEEsG_inheritedBody",
2204
- "pendingQHeader": "_5KEEsG_pendingQHeader",
2197
+ "empty": "_5KEEsG_empty",
2198
+ "densityToggle": "_5KEEsG_densityToggle",
2205
2199
  "pendingOption": "_5KEEsG_pendingOption",
2206
- "pendingOptionActive": "_5KEEsG_pendingOptionActive",
2207
- "rowActions": "_5KEEsG_rowActions",
2208
- "stateScreen": "_5KEEsG_stateScreen",
2209
- "pendingActions": "_5KEEsG_pendingActions",
2210
- "pendingBtnGhost": "_5KEEsG_pendingBtnGhost",
2200
+ "emptyIcon": "_5KEEsG_emptyIcon",
2201
+ "errorRow": "_5KEEsG_errorRow",
2202
+ "todoDot": "_5KEEsG_todoDot",
2203
+ "pendingHead": "_5KEEsG_pendingHead",
2204
+ "flowRowError": "_5KEEsG_flowRowError",
2205
+ "pendingReason": "_5KEEsG_pendingReason",
2206
+ "pendingQuestion": "_5KEEsG_pendingQuestion",
2211
2207
  "actionGroup": "_5KEEsG_actionGroup",
2212
- "toolHead": "_5KEEsG_toolHead",
2213
- "toolBodyText": "_5KEEsG_toolBodyText",
2208
+ "jumpBottom": "_5KEEsG_jumpBottom",
2214
2209
  "sidechat-pulse": "_5KEEsG_sidechat-pulse",
2215
- "assistantBody": "_5KEEsG_assistantBody",
2216
- "todoDot": "_5KEEsG_todoDot",
2217
- "todoBody": "_5KEEsG_todoBody",
2218
- "toolError": "_5KEEsG_toolError",
2219
- "reasoning": "_5KEEsG_reasoning",
2220
- "attachThumb": "_5KEEsG_attachThumb",
2221
- "attachButton": "_5KEEsG_attachButton",
2222
- "densityToggle": "_5KEEsG_densityToggle",
2223
- "composerFoot": "_5KEEsG_composerFoot",
2210
+ "pendingBar": "_5KEEsG_pendingBar",
2211
+ "userRow": "_5KEEsG_userRow",
2212
+ "transcript": "_5KEEsG_transcript",
2213
+ "assistantRow": "_5KEEsG_assistantRow",
2214
+ "flowRow": "_5KEEsG_flowRow",
2215
+ "pendingCustom": "_5KEEsG_pendingCustom",
2216
+ "pendingCard": "_5KEEsG_pendingCard",
2217
+ "dotPending": "_5KEEsG_dotPending",
2218
+ "reasoningBody": "_5KEEsG_reasoningBody",
2219
+ "todoText": "_5KEEsG_todoText",
2220
+ "slashItemName": "_5KEEsG_slashItemName",
2224
2221
  "stopButton": "_5KEEsG_stopButton",
2225
- "slashItemActive": "_5KEEsG_slashItemActive",
2226
- "slashMenu": "_5KEEsG_slashMenu",
2227
- "writeNotice": "_5KEEsG_writeNotice",
2228
- "input": "_5KEEsG_input",
2222
+ "slashItem": "_5KEEsG_slashItem",
2229
2223
  "densityRow": "_5KEEsG_densityRow",
2230
- "composerChips": "_5KEEsG_composerChips",
2224
+ "root": "_5KEEsG_root",
2225
+ "slashMenu": "_5KEEsG_slashMenu",
2226
+ "stateScreen": "_5KEEsG_stateScreen",
2227
+ "pendingActions": "_5KEEsG_pendingActions",
2228
+ "rowActions": "_5KEEsG_rowActions",
2229
+ "pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
2230
+ "streamingHint": "_5KEEsG_streamingHint",
2231
+ "attachRail": "_5KEEsG_attachRail",
2232
+ "pendingQText": "_5KEEsG_pendingQText",
2233
+ "dotIdle": "_5KEEsG_dotIdle",
2234
+ "slashItemActive": "_5KEEsG_slashItemActive",
2235
+ "toolBody": "_5KEEsG_toolBody",
2236
+ "toolError": "_5KEEsG_toolError",
2237
+ "toolRunning": "_5KEEsG_toolRunning",
2231
2238
  "sentChip": "_5KEEsG_sentChip",
2232
- "pendingQuestion": "_5KEEsG_pendingQuestion",
2233
- "todoText": "_5KEEsG_todoText",
2234
- "pendingReason": "_5KEEsG_pendingReason",
2239
+ "pendingQHeader": "_5KEEsG_pendingQHeader",
2240
+ "body": "_5KEEsG_body",
2241
+ "emptyTitle": "_5KEEsG_emptyTitle",
2242
+ "toolBodyText": "_5KEEsG_toolBodyText",
2243
+ "pendingOptionActive": "_5KEEsG_pendingOptionActive",
2244
+ "inheritedBody": "_5KEEsG_inheritedBody",
2245
+ "dotRunning": "_5KEEsG_dotRunning",
2246
+ "reflowButton": "_5KEEsG_reflowButton",
2247
+ "composerChips": "_5KEEsG_composerChips",
2248
+ "pendingTitle": "_5KEEsG_pendingTitle",
2235
2249
  "pendingBtnPrimary": "_5KEEsG_pendingBtnPrimary",
2236
- "dotIdle": "_5KEEsG_dotIdle",
2237
- "assistantRow": "_5KEEsG_assistantRow",
2238
- "flowRow": "_5KEEsG_flowRow",
2239
- "userRow": "_5KEEsG_userRow",
2250
+ "composerCard": "_5KEEsG_composerCard",
2240
2251
  "toolCard": "_5KEEsG_toolCard",
2241
- "errorRow": "_5KEEsG_errorRow",
2252
+ "reasoning": "_5KEEsG_reasoning",
2242
2253
  "noticeRow": "_5KEEsG_noticeRow",
2254
+ "stateDetail": "_5KEEsG_stateDetail",
2243
2255
  "slashItemDetail": "_5KEEsG_slashItemDetail",
2244
- "transcript": "_5KEEsG_transcript",
2245
- "toolRunning": "_5KEEsG_toolRunning",
2246
- "emptyIcon": "_5KEEsG_emptyIcon",
2247
- "pendingCustom": "_5KEEsG_pendingCustom",
2248
- "headerSide": "_5KEEsG_headerSide",
2249
- "parentStrip": "_5KEEsG_parentStrip",
2250
- "modelSelect": "_5KEEsG_modelSelect",
2251
- "pendingHead": "_5KEEsG_pendingHead",
2252
- "root": "_5KEEsG_root",
2253
- "flowRowError": "_5KEEsG_flowRowError",
2254
- "writeNoticeClose": "_5KEEsG_writeNoticeClose",
2255
- "slashGroupTitle": "_5KEEsG_slashGroupTitle",
2256
- "sendButton": "_5KEEsG_sendButton",
2257
- "slashItemName": "_5KEEsG_slashItemName",
2258
- "body": "_5KEEsG_body",
2259
- "emptyText": "_5KEEsG_emptyText",
2260
- "composer": "_5KEEsG_composer",
2261
- "emptyTitle": "_5KEEsG_emptyTitle",
2262
2256
  "attachRemove": "_5KEEsG_attachRemove",
2263
- "dotPending": "_5KEEsG_dotPending",
2264
- "todoItem": "_5KEEsG_todoItem",
2265
- "pendingQText": "_5KEEsG_pendingQText",
2266
- "pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
2267
2257
  "pendingDesc": "_5KEEsG_pendingDesc",
2258
+ "sentChipRow": "_5KEEsG_sentChipRow",
2259
+ "writeNotice": "_5KEEsG_writeNotice",
2260
+ "attachThumb": "_5KEEsG_attachThumb",
2261
+ "modelSelect": "_5KEEsG_modelSelect",
2262
+ "slashGroupTitle": "_5KEEsG_slashGroupTitle",
2268
2263
  "modelLabel": "_5KEEsG_modelLabel",
2269
- "pendingCard": "_5KEEsG_pendingCard"
2264
+ "emptyText": "_5KEEsG_emptyText",
2265
+ "parentStrip": "_5KEEsG_parentStrip",
2266
+ "input": "_5KEEsG_input",
2267
+ "composerFoot": "_5KEEsG_composerFoot",
2268
+ "toolLocations": "_5KEEsG_toolLocations",
2269
+ "todoItem": "_5KEEsG_todoItem"
2270
2270
  };
2271
2271
  //#endregion
2272
2272
  //#region src/client/chat/ToolCard.tsx
@@ -4709,43 +4709,43 @@ window.__ModuleLoader__.load({
4709
4709
  document.head.appendChild(tag);
4710
4710
  }
4711
4711
  var annotate_module_css_default = {
4712
- "badgeSent": "_SVViG_badgeSent",
4713
- "chipClearAll": "_SVViG_chipClearAll",
4714
- "highlight": "_SVViG_highlight",
4715
- "chipRemove": "_SVViG_chipRemove",
4716
- "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
4717
- "editorNew": "_SVViG_editorNew",
4718
- "chipRow": "_SVViG_chipRow",
4719
- "editorTextarea": "_SVViG_editorTextarea",
4712
+ "editorTarget": "_SVViG_editorTarget",
4713
+ "cancelButton": "_SVViG_cancelButton",
4714
+ "hintClose": "_SVViG_hintClose",
4715
+ "chipWrap": "_SVViG_chipWrap",
4720
4716
  "editorAsk": "_SVViG_editorAsk",
4721
- "chipNote": "_SVViG_chipNote",
4722
- "chip": "_SVViG_chip",
4723
- "chipText": "_SVViG_chipText",
4724
- "chipPanel": "_SVViG_chipPanel",
4717
+ "editorAskRow": "_SVViG_editorAskRow",
4725
4718
  "editorError": "_SVViG_editorError",
4726
- "toolbarBelow": "_SVViG_toolbarBelow",
4727
- "sentCardQuote": "_SVViG_sentCardQuote",
4728
- "hint": "_SVViG_hint",
4729
- "sentChipRow": "_SVViG_sentChipRow",
4730
- "deleteButton": "_SVViG_deleteButton",
4731
- "sentCardNote": "_SVViG_sentCardNote",
4732
4719
  "confirmButton": "_SVViG_confirmButton",
4733
- "cancelButton": "_SVViG_cancelButton",
4720
+ "toolbar": "_SVViG_toolbar",
4721
+ "editorInput": "_SVViG_editorInput",
4722
+ "toolbarButton": "_SVViG_toolbarButton",
4723
+ "chip": "_SVViG_chip",
4724
+ "chipNumber": "_SVViG_chipNumber",
4725
+ "sentCardQuote": "_SVViG_sentCardQuote",
4734
4726
  "editorFooter": "_SVViG_editorFooter",
4727
+ "sentCardNote": "_SVViG_sentCardNote",
4728
+ "hint": "_SVViG_hint",
4735
4729
  "sentCardTitle": "_SVViG_sentCardTitle",
4736
- "badge": "_SVViG_badge",
4737
- "editorTarget": "_SVViG_editorTarget",
4738
- "chipNumber": "_SVViG_chipNumber",
4739
- "editorAskRow": "_SVViG_editorAskRow",
4730
+ "chipPanel": "_SVViG_chipPanel",
4731
+ "sentChip": "_SVViG_sentChip",
4732
+ "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
4733
+ "editorTextarea": "_SVViG_editorTextarea",
4734
+ "editorEdit": "_SVViG_editorEdit",
4740
4735
  "editorSpacer": "_SVViG_editorSpacer",
4741
- "hintClose": "_SVViG_hintClose",
4742
- "editorInput": "_SVViG_editorInput",
4736
+ "badgeSent": "_SVViG_badgeSent",
4737
+ "chipNote": "_SVViG_chipNote",
4738
+ "chipRow": "_SVViG_chipRow",
4739
+ "highlight": "_SVViG_highlight",
4743
4740
  "saveButton": "_SVViG_saveButton",
4744
- "toolbarButton": "_SVViG_toolbarButton",
4745
- "editorEdit": "_SVViG_editorEdit",
4746
- "chipWrap": "_SVViG_chipWrap",
4747
- "sentChip": "_SVViG_sentChip",
4748
- "toolbar": "_SVViG_toolbar"
4741
+ "chipText": "_SVViG_chipText",
4742
+ "sentChipRow": "_SVViG_sentChipRow",
4743
+ "toolbarBelow": "_SVViG_toolbarBelow",
4744
+ "deleteButton": "_SVViG_deleteButton",
4745
+ "chipRemove": "_SVViG_chipRemove",
4746
+ "editorNew": "_SVViG_editorNew",
4747
+ "chipClearAll": "_SVViG_chipClearAll",
4748
+ "badge": "_SVViG_badge"
4749
4749
  };
4750
4750
  //#endregion
4751
4751
  //#region src/client/annotate/hint.tsx
@@ -5584,6 +5584,23 @@ window.__ModuleLoader__.load({
5584
5584
  function installSendInterceptor(ctx, store, reflow) {
5585
5585
  /** 重入/连按护栏:事务进行中吞掉命中识别面的 Enter/点击(不重复驱动)。 */
5586
5586
  let committing = false;
5587
+ /**
5588
+ * 手势尾随 click 的一次性吞并。pointerdown 提交后,同一次点击的 click 仍会
5589
+ * 到达,而此时宿主主按钮已按 `primaryStops = running && empty` 变身「停止」
5590
+ * ——放行它会立刻 `stop()` 掉刚发起的这一轮(表现:assistant/attempt 空流、
5591
+ * 缺 turn/end、且没有任何错误提示)。只吞这一发,并带兜底超时。
5592
+ */
5593
+ let swallowNextClick = false;
5594
+ let swallowTimer = 0;
5595
+ const clearSwallow = () => {
5596
+ swallowNextClick = false;
5597
+ window.clearTimeout(swallowTimer);
5598
+ };
5599
+ const armSwallow = () => {
5600
+ swallowNextClick = true;
5601
+ window.clearTimeout(swallowTimer);
5602
+ swallowTimer = window.setTimeout(clearSwallow, 2e3);
5603
+ };
5587
5604
  const currentSessionId = () => {
5588
5605
  try {
5589
5606
  return ctx.sessions.list.getSnapshot().current ?? "";
@@ -5663,6 +5680,12 @@ window.__ModuleLoader__.load({
5663
5680
  const target = event.target;
5664
5681
  if (!(target instanceof Element)) return;
5665
5682
  if (target.closest("[data-composer-seat]") === null) return;
5683
+ if (swallowNextClick) {
5684
+ clearSwallow();
5685
+ event.preventDefault();
5686
+ event.stopImmediatePropagation();
5687
+ return;
5688
+ }
5666
5689
  const button = target.closest("button");
5667
5690
  if (button === null) return;
5668
5691
  if (committing) {
@@ -5675,11 +5698,35 @@ window.__ModuleLoader__.load({
5675
5698
  event.preventDefault();
5676
5699
  event.stopImmediatePropagation();
5677
5700
  };
5701
+ /**
5702
+ * 空草稿补位:只接管「因空草稿而 disabled」的主按钮。enabled 时立即放行,
5703
+ * 正常路径仍由上面的 click 拦截负责 —— 二者以 button.disabled 互斥。
5704
+ */
5705
+ const onPointerDown = (event) => {
5706
+ if (event.button !== 0) return;
5707
+ const target = event.target;
5708
+ if (!(target instanceof Element)) return;
5709
+ const button = target.closest("button");
5710
+ if (!(button instanceof HTMLButtonElement)) return;
5711
+ if (!button.disabled) return;
5712
+ if (button !== findSendButtonInCard()) return;
5713
+ if (committing) return;
5714
+ const sessionId = currentSessionId();
5715
+ if (sessionId === "") return;
5716
+ const input = resolveInput(ctx, sessionId);
5717
+ if (input === void 0 || input.state.getSnapshot().draft.trim() !== "") return;
5718
+ if (!hijack()) return;
5719
+ armSwallow();
5720
+ event.preventDefault();
5721
+ event.stopImmediatePropagation();
5722
+ };
5678
5723
  document.addEventListener("keydown", onKeyDown, true);
5679
5724
  document.addEventListener("click", onClick, true);
5725
+ document.addEventListener("pointerdown", onPointerDown, true);
5680
5726
  return () => {
5681
5727
  document.removeEventListener("keydown", onKeyDown, true);
5682
5728
  document.removeEventListener("click", onClick, true);
5729
+ document.removeEventListener("pointerdown", onPointerDown, true);
5683
5730
  };
5684
5731
  }
5685
5732
  //#endregion
@@ -19,6 +19,17 @@
19
19
  * plain 且草稿清空 = 发出(markSent 只翻当时拼进去的那批 id,C2 P1-3);
20
20
  * 回到 plain 而草稿未清 = 发送失败(宿主 notice + 留稿)→ 仅在草稿仍以
21
21
  * 我们拼的前缀开头时剥离回滚(C2 P2-3);相位未到终态前绝不回滚。
22
+ *
23
+ * 空草稿补位:宿主主按钮在 `draft.trim()==="" && attachments.length===0` 时
24
+ * 被置为原生 `disabled`(dsh-client-ui-conversation 的 `empty`),而注释/回流
25
+ * 按设计不进草稿 —— 于是此时**没有任何可达的发送手势**:浏览器不为 disabled
26
+ * 的按钮派发 `click`(`mousedown` 同样不发),只有 `pointerdown` 仍会到达它
27
+ * (Chromium / Firefox / WebKit 实测一致)。因此对「空草稿导致 disabled 的
28
+ * 主按钮」补一条 pointerdown 拦截;enabled 时一切照旧走 click,两条路径以
29
+ * `button.disabled` 互斥,不会双驱动。**该手势尾随的 click 必须一并吞掉**:
30
+ * 提交瞬间草稿变空,宿主主按钮随即按 `primaryStops = running && empty` 变身
31
+ * 「停止」,放行那一发 click 会立刻 `stop()` 掉刚发起的这一轮 —— 症状是
32
+ * `assistant/attempt` 空流、缺 `turn/end`、界面上却没有任何错误提示。
22
33
  */
23
34
  import type { Context, SessionId, SessionInput } from '../host/contracts.ts';
24
35
  import type { AnnotationStore } from './model.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidenote",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "DSH web plugin: Codex-style side chat (fork the current session into a persistent side panel) + selection annotations with numbered badges and a composer chip. A thin consumer of dsh-better-sidebar. / Codex 风格侧边聊天 + 划选注释的 DSH 插件。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,6 +19,17 @@
19
19
  * plain 且草稿清空 = 发出(markSent 只翻当时拼进去的那批 id,C2 P1-3);
20
20
  * 回到 plain 而草稿未清 = 发送失败(宿主 notice + 留稿)→ 仅在草稿仍以
21
21
  * 我们拼的前缀开头时剥离回滚(C2 P2-3);相位未到终态前绝不回滚。
22
+ *
23
+ * 空草稿补位:宿主主按钮在 `draft.trim()==="" && attachments.length===0` 时
24
+ * 被置为原生 `disabled`(dsh-client-ui-conversation 的 `empty`),而注释/回流
25
+ * 按设计不进草稿 —— 于是此时**没有任何可达的发送手势**:浏览器不为 disabled
26
+ * 的按钮派发 `click`(`mousedown` 同样不发),只有 `pointerdown` 仍会到达它
27
+ * (Chromium / Firefox / WebKit 实测一致)。因此对「空草稿导致 disabled 的
28
+ * 主按钮」补一条 pointerdown 拦截;enabled 时一切照旧走 click,两条路径以
29
+ * `button.disabled` 互斥,不会双驱动。**该手势尾随的 click 必须一并吞掉**:
30
+ * 提交瞬间草稿变空,宿主主按钮随即按 `primaryStops = running && empty` 变身
31
+ * 「停止」,放行那一发 click 会立刻 `stop()` 掉刚发起的这一轮 —— 症状是
32
+ * `assistant/attempt` 空流、缺 `turn/end`、界面上却没有任何错误提示。
22
33
  */
23
34
  import type { Context, ConversationService, SessionId, SessionInput } from '../host/contracts.ts'
24
35
  import { buildProtocolBlock } from './format.ts'
@@ -56,6 +67,25 @@ export function installSendInterceptor(ctx: Context, store: AnnotationStore, ref
56
67
  /** 重入/连按护栏:事务进行中吞掉命中识别面的 Enter/点击(不重复驱动)。 */
57
68
  let committing = false
58
69
 
70
+ /**
71
+ * 手势尾随 click 的一次性吞并。pointerdown 提交后,同一次点击的 click 仍会
72
+ * 到达,而此时宿主主按钮已按 `primaryStops = running && empty` 变身「停止」
73
+ * ——放行它会立刻 `stop()` 掉刚发起的这一轮(表现:assistant/attempt 空流、
74
+ * 缺 turn/end、且没有任何错误提示)。只吞这一发,并带兜底超时。
75
+ */
76
+ let swallowNextClick = false
77
+ let swallowTimer = 0
78
+ const clearSwallow = (): void => {
79
+ swallowNextClick = false
80
+ window.clearTimeout(swallowTimer)
81
+ }
82
+ const armSwallow = (): void => {
83
+ swallowNextClick = true
84
+ window.clearTimeout(swallowTimer)
85
+ // 兜底:手势没有产生 click(指针移出按钮等)时不至于吞掉后续无关点击。
86
+ swallowTimer = window.setTimeout(clearSwallow, 2_000)
87
+ }
88
+
59
89
  const currentSessionId = (): string => {
60
90
  try {
61
91
  return ctx.sessions.list.getSnapshot().current ?? ''
@@ -159,6 +189,13 @@ export function installSendInterceptor(ctx: Context, store: AnnotationStore, ref
159
189
  if (!(target instanceof Element)) return
160
190
  const seat = target.closest('[data-composer-seat]')
161
191
  if (seat === null) return
192
+ // 尾随 click:必须先于按钮识别吞掉(见 armSwallow),否则宿主按「停止」处理。
193
+ if (swallowNextClick) {
194
+ clearSwallow()
195
+ event.preventDefault()
196
+ event.stopImmediatePropagation()
197
+ return
198
+ }
162
199
  const button = target.closest('button')
163
200
  if (button === null) return
164
201
  if (committing) {
@@ -175,10 +212,39 @@ export function installSendInterceptor(ctx: Context, store: AnnotationStore, ref
175
212
  event.stopImmediatePropagation()
176
213
  }
177
214
 
215
+ /**
216
+ * 空草稿补位:只接管「因空草稿而 disabled」的主按钮。enabled 时立即放行,
217
+ * 正常路径仍由上面的 click 拦截负责 —— 二者以 button.disabled 互斥。
218
+ */
219
+ const onPointerDown = (event: PointerEvent): void => {
220
+ // 只认主键:右键/中键的 pointerdown 同样到达 disabled 按钮(已实证),
221
+ // 不检查会把「打开上下文菜单」误当发送手势。
222
+ if (event.button !== 0) return
223
+ const target = event.target
224
+ if (!(target instanceof Element)) return
225
+ const button = target.closest('button')
226
+ if (!(button instanceof HTMLButtonElement)) return
227
+ if (!button.disabled) return
228
+ if (button !== findSendButtonInCard()) return
229
+ if (committing) return
230
+ // disabled 的成因必须是空草稿:离线/无模型等其它成因不属本拦截器授权范围。
231
+ const sessionId = currentSessionId()
232
+ if (sessionId === '') return
233
+ const input = resolveInput(ctx, sessionId)
234
+ if (input === undefined || input.state.getSnapshot().draft.trim() !== '') return
235
+ if (!hijack()) return
236
+ // 同一次手势的 click 紧随其后,而主按钮此刻已变身「停止」——吞掉这一发。
237
+ armSwallow()
238
+ event.preventDefault()
239
+ event.stopImmediatePropagation()
240
+ }
241
+
178
242
  document.addEventListener('keydown', onKeyDown, true)
179
243
  document.addEventListener('click', onClick, true)
244
+ document.addEventListener('pointerdown', onPointerDown, true)
180
245
  return () => {
181
246
  document.removeEventListener('keydown', onKeyDown, true)
182
247
  document.removeEventListener('click', onClick, true)
248
+ document.removeEventListener('pointerdown', onPointerDown, true)
183
249
  }
184
250
  }