Skip to content

fix(desktop): 发送后立即清空输入框,避免与气泡重叠 - #3388

Merged
MagicLizi merged 8 commits into
mainfrom
dash/fix-composer-send-linger
Aug 25, 2026
Merged

fix(desktop): 发送后立即清空输入框,避免与气泡重叠#3388
MagicLizi merged 8 commits into
mainfrom
dash/fix-composer-send-linger

Conversation

@dashhuang

@dashhuang dashhuang commented Aug 25, 2026

Copy link
Copy Markdown
Member

这次改了什么

摘要

本机/SSH 发送一条消息后,聊天气泡已经出来,输入框里同一段字还会停一会儿,看起来像文字叠在一起。

原因是本地发送要等 onSend 整条链路(effort 等待、slash、鉴权、enqueue IPC)结束后才清输入框;而 sendMessage 会先把乐观气泡画进对话。device-link 远程发送早就在点击时清掉了。

这次让本机/SSH 也走同一套:校验通过后立刻清空输入框,发送被拒或抛错再把草稿恢复回来。清空后输入框重新可编辑,失败恢复会保留用户在等待期间新打的字。

变更类型

  • fix 缺陷修复
  • feat 新功能
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护
  • 其他:

范围

  • 关联 Issue / 需求:无
  • 本 PR 包含:
    • ChatInput.dispatchSend 在 await onSend 之前清空当前会话输入框
    • 失败路径(effort 未就绪、onSend 返回 false、抛错)统一恢复点击时草稿
    • 补目录后的延期受理改为只去掉原草稿、保留期间新输入
    • 回归测试与既有源码契约测试同步
  • 明确不包含:
    • 不改 mobile 发送路径
    • 不改 enqueue / 乐观气泡本身
    • 不放开「第一条发送还在 enqueue 时立刻再发第二条」的 in-flight 锁
  • 用户可见变化:本机或 SSH 会话里点发送后,输入框立刻变空,不再和刚发出的气泡叠字
  • 是否存在 breaking change:无

UI 变化

无新控件、无文案、无配色或版式改动,只调整发送后输入框清空时机。

  • 引用的设计规范:DESIGN.md §1 Visual Theme & Atmosphere:工作台里用户内容和 agent 输出才是焦点,每个表面只呈现一个明确想法。发送后同一段字同时出现在气泡和输入框,破坏了这条约束。本次只把清空提前到气泡出现之前,不改视觉语言。

发送后界面时序(HTML 示意,非实机截图):

<!doctype html>
<meta charset="utf-8" />
<title>PR 3388 发送后输入框清空</title>
<style>
  :root { color-scheme: light dark; }
  body { margin: 24px; font: 14px/1.45 ui-sans-serif, system-ui, sans-serif; }
  .row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  figure { margin: 0; padding: 12px; border: 1px solid ButtonBorder; border-radius: 12px; }
  figcaption { margin-bottom: 8px; font-weight: 600; }
  .thread { min-height: 88px; }
  .bubble { display: inline-block; max-width: 80%; padding: 8px 12px; border-radius: 12px; background: Canvas; border: 1px solid ButtonBorder; }
  .composer { margin-top: 12px; min-height: 40px; padding: 8px 12px; border-radius: 10px; border: 1px solid ButtonBorder; background: Field; }
  .ghost { opacity: .45; }
</style>
<div class="row">
  <figure>
    <figcaption>改前:气泡已出现,输入框还留着同一段字</figcaption>
    <div class="thread"><div class="bubble">检查一下这是什么问题?</div></div>
    <div class="composer">检查一下这是什么问题?</div>
  </figure>
  <figure>
    <figcaption>改后:气泡出现时输入框已空</figcaption>
    <div class="thread"><div class="bubble">检查一下这是什么问题?</div></div>
    <div class="composer ghost">输入下一条消息</div>
  </figure>
</div>

怎么验证的

自动验证

pnpm --filter desktop run typecheck
结果:通过

pnpm test:unit:related
结果:PASS apps/desktop unit(相关 5 个文件)

pnpm exec vitest run \
  src/renderer/__tests__/chatInputSessionFocus.test.ts \
  src/renderer/__tests__/voiceInputEnterToSend.test.ts \
  src/renderer/__tests__/remoteSessionSyncInvariants.test.ts \
  src/renderer/__tests__/dispatchSendClear.test.ts \
  src/renderer/components/new-chat/__tests__/composerSendOwnership.test.ts \
  src/renderer/__tests__/effortRuntimeDirtySendGuard.test.ts \
  src/renderer/__tests__/composerDraftMountRace.test.ts \
  src/renderer/__tests__/composerExternalDraftEmptyDoc.test.ts \
  src/renderer/lib/__tests__/composerDraftStore.test.ts \
  src/renderer/lib/__tests__/composerDraftSessionSwitchGuard.test.ts
结果:全部通过

bash run-unit-gate.sh <worktree>
结果:desktop 及相关 packages 通过;apps/mobile 的 startupSplashOverlay.test.ts 在 5s 内超时失败。该测试与本次 composer 改动无关,未并入本 PR。

手工验证

未在本机启动 Desktop 实机点发送。根因是 dispatchSend 明确把本机清空挂在 await onSend 之后,而 sendMessageCore 会先同步插入乐观气泡。

未执行的验证

  • Desktop 实机发送(本机会话 / SSH / device-link / 缺目录补选 / 发送失败恢复)
  • mobile 发送路径(不在范围内)
  • 全量 pnpm test:unit 因无关的 mobile splash 超时未算绿

风险

风险分类

  • 其他:发送失败时的草稿恢复时序
  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 存量插件兼容(批准状态 / 指纹 / manifest 校验 / 安装布局 / 包格式)
  • 原生层 / fingerprint / OTA
  • 跨平台差异

影响与回滚

  • 影响范围:Desktop 本机与 SSH 会话的输入框发送清空时机;device-link 仍走原有乐观清空,只是本地也共用同一套恢复入口
  • 回滚 / 降级方式:回退本 PR。失败发送会先闪空再恢复草稿;New Maker 仍立即 return false,会把刚清掉的草稿立刻填回,行为与改前一致
  • 存量插件影响:无

提交前检查

  • 已 review 完整 diff
  • 每个 commit 都带 DCO 签名(git commit -s,见 DCO
  • UI 改动已在「UI 变化」注明引用的设计规范章节(不涉及 UI 则跳过)
  • 未提交凭证、令牌或授权文件
  • 已补充必要文档
  • 已确认测试结果或说明未执行原因

本机/SSH 原先要等 onSend(含 enqueue)结束后才清 composer,乐观气泡已经出现时输入框里还留着同一段字。现在与 device-link 一样在点击后先清,失败再恢复草稿。

Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com>
@dashhuang
dashhuang requested a review from a team as a code owner August 25, 2026 08:43
@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown

Greptile Summary

此 PR 调整 Desktop 本机、SSH 与 device-link 发送流程,在等待发送链路前清空点击时草稿,并在拒绝或异常时按会话归属恢复。

  • 将发送锁拆分为输入可编辑状态与发送/设置控件锁定状态
  • 为异步发送、会话切换、延期受理和焦点恢复补充回归测试

Confidence Score: 5/5

当前没有仍需阻止合并的已确认问题,PR 看起来可以安全合并。

没有仍然存在的阻塞性故障。

Important Files Changed

Filename Overview
apps/desktop/src/renderer/components/new-chat/ChatInput.tsx 将本机与 SSH 发送改为提前清空,并增加按会话追踪的发送、清空和失败恢复状态。
apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts 焦点恢复现在仅依赖输入是否锁定,使发送仍在处理中但已允许输入时能够恢复焦点。
apps/desktop/src/renderer/tests/dispatchSendClear.test.ts 覆盖发送前立即清空以及返回 false、抛错时恢复草稿的契约。
apps/desktop/src/renderer/components/new-chat/tests/useComposerSendFocusRestore.test.tsx 更新焦点锁定测试,并覆盖发送尚未结束但输入已重新开放的状态。

Sequence Diagram

sequenceDiagram
  participant U as 用户
  participant C as 输入框
  participant S as onSend
  U->>C: 点击发送
  C->>C: 保存点击时草稿并立即清空
  C->>S: 等待发送链路
  alt 发送成功
    S-->>C: 接受
    C->>C: 保留等待期间的新输入
  else 拒绝或异常
    S-->>C: 失败
    C->>C: 恢复原草稿并保留较新内容
  end
Loading

Reviews (8): Last reviewed commit: "fix(desktop): 切回任务时等乐观清空后再放开打字" | Re-trigger Greptile

Comment thread apps/desktop/src/renderer/components/new-chat/ChatInput.tsx
@MagicLizi MagicLizi added the touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) label Aug 25, 2026

@MagicLizi MagicLizi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查未通过(P1×1)。切任务后失败恢复可能把目标任务草稿写进源任务。

  • apps/desktop/src/renderer/components/new-chat/ChatInput.tsx:5175 — Local/SSH 在 await onSend 前新增的 restore snapshot 刷新没有用 editorOwnsSourceDraft。hydration / voice freeze 之后无条件执行 documentBeforeOptimisticClear = editor.getJSON()。复用的 ChatInput 切走任务后 live editor 已是目标任务草稿;onSend === false / throw / effort dirty 会调用 restoreOptimisticallyClearedComposer(),把当前任务正文/附件写进源任务 draft slot。同函数里 lock、serialize、clearSentComposer 都有 ownership 门,只有这段新刷新没有。请仅在 editorOwnsSourceDraft(...) 为 true 时刷新 snapshot;切走后保留 click-time / frozen extras。

Local/SSH 发送在 hydration 后刷新 restore snapshot 时必须仍持有源任务编辑器。复用的 ChatInput 切走后 live editor 已是目标任务,无条件刷新会在 onSend 失败时把目标草稿写进源 slot。

Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com>
@dashhuang

Copy link
Copy Markdown
Member Author

@MagicLizi 已按你的 P1 修了:hydration 后刷新 restore snapshot 现在先过 editorOwnsSourceDraft。切走任务后保留 click-time / frozen extras,失败恢复不会把目标任务草稿写进源 slot。

commit: caf15c2

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: caf15c2d37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/new-chat/ChatInput.tsx Outdated
@MagicLizi

Copy link
Copy Markdown
Contributor

@dashhuang 👋 这个 PR 还有 1 条 review conversation 没 resolve(apps/desktop/src/renderer/components/new-chat/ChatInput.tsx),auto-review 因此暂时跳过、没法继续审查 / 合并。

如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。

@MagicLizi MagicLizi added the status:awaiting-author 等作者修改(review-pr 自动维护,仅展示) label Aug 25, 2026
语音 stop-and-send 可能在切到任务 B 之后才真正 dispatch。此时 live editor 已是 B,无条件 snapshot 会在 A 发送失败时把 B 的正文写进 A。初始快照与 remote 的 click-time serialize 都先过 editorOwnsSourceDraft,切走后改用 frozen extras / 源 draft slot。

Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe4c1228a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/new-chat/ChatInput.tsx Outdated
本机 onSend 未返回时若整段解锁,发送按钮会亮着但 in-flight guard 把下一次发送静默丢掉,权限选择也会提前可改。清空后只允许继续输入,发送和设置控件等到 enqueue 结束再开。

Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 91bbad0b55

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/new-chat/ChatInput.tsx Outdated
Comment thread apps/desktop/src/renderer/components/new-chat/ChatInput.tsx
切走再回来时按 in-flight key 恢复 send 锁,避免按钮亮着点了被静默丢掉。语音 refinement 在切任务后落地时,把定稿文本写进 restore snapshot,失败恢复不再丢掉听写结果。同步 setEditable 源码契约。

Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e33086abb0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/new-chat/ChatInput.tsx Outdated
@dashhuang

Copy link
Copy Markdown
Member Author

收敛检查点(第 5 轮)

离可合并还差什么: 失败族收口 + MagicLizi 对最新 HEAD 复审 + CI 绿。不是再加一层锁。

失败族

当前 composer 的发送锁必须是「这个 storageKey 是否仍有在途 dispatchSend」的投影。
任何一次发送的 finally / 切任务,都不得改写别的任务sendDispatchInFlight / allowTypeDuringSend

这族已经出现过的症状(同一不变量,不同入口):

  1. hydration 后无条件刷新 restore snapshot → 把 B 写进 A
  2. dispatchSend 入口无条件 snapshot live editor
  3. 清空后整段解锁 → 发送按钮亮着却被 in-flight guard 丢掉
  4. 切任务无条件 setSendDispatchInFlight(false) → 切回 A 锁丢了
  5. (本轮)A 的 finally 无条件 setAllowTypeDuringSend(false) → B 正在发送时编辑器突然只读

唯一 owner

  • 权威:dispatchSendInFlightKeysRef(按 send key 登记)
  • React 状态只是当前 storageKey 的投影
  • finally 只在 storageKeyForDraftRef === sourceStorageKey 时写回这两个 flag
  • 切任务按「当前 key 是否仍在 ref 里」重算,不无条件清零

本轮修法

setAllowTypeDuringSend(false) 收进与 setSendDispatchInFlight(false) 同一个 current-key 门。不新增锁/缓存。

A 的发送结算时若已切到 B,无条件 setAllowTypeDuringSend(false) 会在 B 仍在途时把 B 的编辑器锁死。两个 React 锁 flag 都只在 current storageKey 仍是这次发送的源任务时清零。

Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com>
@MagicLizi MagicLizi removed the status:awaiting-author 等作者修改(review-pr 自动维护,仅展示) label Aug 25, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor

命中 UI 路径(apps/desktop/src/renderer/components/new-chat/ChatInput.tsx)但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范

@dashhuang

Copy link
Copy Markdown
Member Author

@MagicLizi 已在 PR 描述「UI 变化」补了发送后时序的 HTML 示意(改前叠字 / 改后输入框立即清空)。这次没有新控件或视觉语言改动,只改清空时机,所以没有另附实机截图。

原先那条 P1(hydration 后无条件刷新 restore snapshot)已在后续 commit 用 editorOwnsSourceDraft 挡住,请按最新 HEAD 9fb4c8148 复审。

@MagicLizi MagicLizi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

发送后虽然立刻清空了输入框,但焦点恢复仍卡在旧的发送锁上,用户没法马上接着打下一句。

  • [P1] apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts:69
    Local/SSH 路径会先 captureSendFocusForRestore()setSendDispatchInFlight(true)composerMutationLocked 也因此为 true。清空后 allowTypeDuringSend 已经用 composerTypingLocked 重新允许输入,但 useComposerSendFocusRestore 仍在 sendDispatchInFlight || composerMutationLocked 时直接 return。这两个旗标要等到 onSend 结束才落下,空输入框会暂时失焦,立刻打字进不去。
    请在「允许打字」时恢复焦点(看 !composerTypingLocked),并补一条 sendDispatchInFlight && allowTypeDuringSend 的 hook 测试。用较慢的 local/SSH 发送路径、不清点输入框再打字验证。

乐观清空后发送锁仍在,焦点恢复却还等 sendDispatchInFlight。
改成只看 composerTypingLocked,慢路径发送后也能马上接着打字。

Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com>
@dashhuang

Copy link
Copy Markdown
Member Author

@MagicLizi 已修:焦点恢复只看 composerTypingLocked,不再等 sendDispatchInFlight / composerMutationLocked。乐观清空并 allowTypeDuringSend 后立刻把光标拉回输入框;语音仍锁打字时仍不抢焦点。

commit: 476811a

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 476811ab03

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/renderer/components/new-chat/ChatInput.tsx Outdated
发送在途但尚未 clearSentComposer 时,切走再切回不能把
allowTypeDuringSend 直接打开,否则下一句会被并进第一次发送。

Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com>
@MagicLizi
MagicLizi dismissed stale reviews from themself August 25, 2026 16:05

这些问题已在当前 head 的后续 commit 中修复(格式门/安全门已通过),自动 dismiss 旧的 CHANGES_REQUESTED 以解除合并阻塞。

@MagicLizi
MagicLizi merged commit 432b465 into main Aug 25, 2026
19 checks passed
@MagicLizi
MagicLizi deleted the dash/fix-composer-send-linger branch August 25, 2026 16:16
@MagicLizi

Copy link
Copy Markdown
Contributor

合了。发送后输入框立刻空出来,气泡和草稿不会再叠在一起,这个每天都会碰到的顿挫终于消掉了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants