Skip to content

fix(mobile): 选目录后无法发送——worktree 偏好同步竞态与输入框 IME 组合态卡死 - #3910

Merged
MagicLizi merged 4 commits into
mainfrom
fix/mobile-dir-select-send-blocked
Sep 4, 2026
Merged

fix(mobile): 选目录后无法发送——worktree 偏好同步竞态与输入框 IME 组合态卡死#3910
MagicLizi merged 4 commits into
mainfrom
fix/mobile-dir-select-send-blocked

Conversation

@MagicLizi

@MagicLizi MagicLizi commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

这次改了什么

摘要

修复移动端「选完目录后,输入文字无法发送」的两条独立成因:

  1. 新建页 worktree 偏好同步竞态:选完项目目录后,worktree 资格探测回包落地会翻转 supportsRecoveryKeyDiscard,该值此前在偏好播种 effect 的依赖数组里——cleanup 会把还在飞的 get-new-maker-defaults 请求判为 cancelled 并重发。eligible 目录上创建门禁只剩偏好 ready 这一条,弱网下重发 + 1.5s 重试循环期间创建按钮灰死,且无任何 caption(eligible 无提示、分支 caption 只在出错时出现),表现为「打完字发不出去、无提示」。该门禁是设备级偏好同步,与用户目录是否使用 worktree 无关,所有项目目录建会话都会经过。修复:探测标记改经 ref 读取并移出依赖(探测落地不再作废在飞请求);偏好门真正阻塞时 caption 兜底显示既有文案「正在同步 worktree 设置…」。
  2. 富文本输入框 IME 组合态卡死:拼音组合未结束时点选 @ 目录等 atom,程序化 DOM 替换会打断组合;Android WebView 85 基线下打断不一定派发 compositionend(部分输入法发 compositioncancel,此前未监听)。composing 卡在 true 后 notify() 持续早退,后续输入可见但 change 不再上抛,发送按钮一直禁用。修复:render() / insertAtSelection() 程序化改 DOM 前先清 composing;监听 compositioncancel
  3. 偏好权威冲突提示:宿主回传的权威偏好与用户刚写入的值不一致时,写入已结束并需用户重试开关。此前 caption / 创建 / Goal 错误仍走「正在同步 worktree 设置…」。现改为独立失败文案「无法确认已保存的 worktree 设置,请重试开关」,仅在真正加载或保存期间保留同步中提示。

变更类型

  • fix 缺陷修复

范围

  • 关联 Issue / 需求:用户同事反馈的手机端「有时候选了目录但输入文字无法发送」(未建 issue)
  • 本 PR 包含:新建页偏好播种竞态修复 + 阻塞 caption 兜底;权威冲突态失败/重试提示;composer WebView IME 组合态清理 + compositioncancel 监听;两侧回归测试
  • 明确不包含:不改动 worktree 门禁的 fail-closed 语义(门禁本身是 2026-08-07 裁决的设计,只修取消竞态与提示缺失)
  • 用户可见变化:新建页选目录后创建按钮不再长时间无提示灰死;偏好同步确需等待时显示「正在同步 worktree 设置…」;权威冲突时显示失败/重试提示而非「正在同步」;会话输入框插入目录 chip 后中文输入不再卡住发送按钮
  • 是否存在 breaking change:无

UI 变化

  • 引用的设计规范:DESIGN.md §11.1 Errors = what happened + what to do;无新增视觉样式与布局,仅复用新建页 worktree 行既有 caption 通道(tertiary caption)。同步中仍用 session.new.worktreeSettingsSaving;权威冲突改用 session.new.worktreeSettingsSyncFailed。Light / Dark 均走语义 token,无硬编码色。

权威冲突后的 caption(复用既有 worktree 行,无新样式):

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>PR #3910 worktree caption — authority conflict</title>
  <style>
    :root { color-scheme: light dark; }
    body { margin: 0; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
    .page { padding: 24px; }
    .grid { display: grid; gap: 24px; grid-template-columns: 1fr; max-width: 390px; }
    @media (min-width: 860px) { .grid { grid-template-columns: 1fr 1fr; max-width: 860px; } }
    h1 { font-size: 16px; font-weight: 600; margin: 0 0 8px; }
    p.note { font-size: 13px; line-height: 18px; margin: 0 0 20px; max-width: 860px; }
    .panel { border-radius: 12px; padding: 16px; }
    .panel h2 { font-size: 13px; font-weight: 600; margin: 0 0 12px; }
    .row { display: flex; flex-direction: column; gap: 6px; }
    .pill { position: relative; display: inline-flex; align-items: stretch; height: 44px; max-width: 100%; }
    .pill-bg { position: absolute; inset: 5px 0; border-radius: 9999px; border: 1px solid; }
    .seg { position: relative; display: flex; align-items: center; gap: 6px; padding: 0 10px 0 12px; min-width: 72px; }
    .toggle { position: relative; display: flex; align-items: center; gap: 6px; padding: 0 12px 0 6px; }
    .divider { width: 1px; height: 16px; align-self: center; }
    .check { width: 18px; height: 18px; border-radius: 9999px; border: 1px solid; box-sizing: border-box; }
    .label { font-size: 13px; font-weight: 500; line-height: 16px; }
    .caption { font-size: 12px; line-height: 16px; margin-left: 8px; }
    .light { background: #EDEDED; color: #3C3F43; }
    .light .pill-bg { background: #F1F1F1; border-color: #686B72; }
    .light .divider { background: #C6C9CE; }
    .light .check { border-color: #686B72; }
    .light .muted { color: #686B72; }
    .dark { background: #2A2828; color: #D4D4D4; }
    .dark .pill-bg { background: #2F2D2D; border-color: #BFC1C4; }
    .dark .divider { background: #434343; }
    .dark .check { border-color: #BFC1C4; }
    .dark .muted { color: #BFC1C4; }
  </style>
</head>
<body>
  <div class="page">
    <h1>新建任务 · worktree 行 caption</h1>
    <p class="note">复用既有 caption 通道(tertiary caption,无新样式)。权威偏好冲突时不再显示「正在同步…」,改为失败/重试提示。Light / Dark 均走语义色 token。</p>
    <div class="grid">
      <section class="panel light">
        <h2>Light · 冲突后</h2>
        <div class="row">
          <div class="pill">
            <div class="pill-bg"></div>
            <div class="seg muted"><span class="label">main</span></div>
            <div class="divider"></div>
            <div class="toggle muted"><span class="check"></span><span class="label">worktree</span></div>
          </div>
          <div class="caption muted">无法确认已保存的 worktree 设置,请重试开关</div>
        </div>
      </section>
      <section class="panel dark">
        <h2>Dark · 冲突后</h2>
        <div class="row">
          <div class="pill">
            <div class="pill-bg"></div>
            <div class="seg muted"><span class="label">main</span></div>
            <div class="divider"></div>
            <div class="toggle muted"><span class="check"></span><span class="label">worktree</span></div>
          </div>
          <div class="caption muted">无法确认已保存的 worktree 设置,请重试开关</div>
        </div>
      </section>
    </div>
  </div>
</body>
</html>

怎么验证的

自动验证

pnpm --filter mobile exec vitest run src/__tests__/newSession.test.ts src/__tests__/composerRichInputHtml.test.ts
结果:139 passed (139)

pnpm --filter mobile run --if-present typecheck
结果:tsc --noEmit 通过,无输出

pnpm test:unit:related
结果:PASS apps/mobile unit (5.7s),related 命中 9 个文件

pnpm check:i18n
结果:zh-CN / zh-TW / en / ja / ko 共 8711 个 key 全部一致

pnpm check:i18n-glossary
结果:无新增违规

pnpm check:dco
结果:DCO check passed: 3 commits signed off, 1 exempt (merge commits)

新增回归测试:

  • composerRichInputHtml.test.ts(WebView 85 stub 环境):组合期间 input 不上抛 change(既有行为锁定)→ applyDocument 程序化替换后组合态被清除 → 后续 input 的 change 正常上抛。
  • newSession.test.ts(source lock):播种 effect 依赖数组不再包含 worktreeHostSupportsRecoveryKeyDiscard、仍含 worktreePreferenceSyncKeymissing 分支判定经 ref 读取;caption 链在权威冲突时优先 worktreeSettingsSyncFailed,仅其余阻塞走 worktreeSettingsSaving;create / Goal 错误路径共用同一解析。

手工验证

未执行。两条成因的复现分别依赖弱网 relay + git 项目目录的时序、以及真机中文输入法组合被打断的时机,本环境不具备;已通过上述结构化测试锁定行为。建议真机回归:新建任务选 git 项目目录后立即打字点发送(弱网下按钮应最多短暂灰且有「正在同步 worktree 设置…」提示);权威冲突时应看到失败/重试提示而非「正在同步」;会话内拼音组合中点选 @ 目录后继续输入并发送。

未执行的验证

见「手工验证」:真机弱网时序与 IME 实机路径未执行,原因如上。

风险

风险分类

  • 无已知风险

(纯 JS 改动,不触碰原生配置 / app.json / 依赖,不改变 runtime fingerprint,可随 OTA 热更。)

影响与回滚

  • 影响范围:仅 mobile 新建会话页的 worktree 偏好播种与 composer WebView 输入协议;桌面端与共享包无改动。
  • 回滚 / 降级方式:revert 本 PR 即可,无数据或持久化格式影响。

提交前检查

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

选完项目目录后 worktree 资格探测落地会翻转 supportsRecoveryKeyDiscard,
它此前在偏好播种 effect 的依赖里:cleanup 把还在飞的 get-new-maker-defaults
判为 cancelled,请求重发。eligible 目录上门禁只剩这一条,弱网下重发/重试
循环期间创建按钮灰死且无任何 caption(eligible 无提示,分支 caption 只在
出错时出现),表现为「打完字发不出去」。

- 探测标记改经 ref 读取,移出 effect 依赖,探测落地不再取消在飞请求;
- 偏好门真正阻塞创建时,caption 兜底显示既有文案「正在同步 worktree 设置…」。

Signed-off-by: MagicLizi <jiali@magiclizi.com>
拼音组合未结束时点选 @ 目录等 atom,程序化 DOM 替换会打断组合,而
Android WebView 85 基线下打断不一定派发 compositionend(部分输入法发
compositioncancel,此前未监听)。composing 卡在 true 后,notify 持续早退,
后续输入可见但 change 不再上抛,发送按钮一直禁用。

- render()/insertAtSelection() 程序化改 DOM 前先清 composing;
- 监听 compositioncancel,与 compositionend 同语义。

Signed-off-by: MagicLizi <jiali@magiclizi.com>
@MagicLizi
MagicLizi requested a review from a team as a code owner September 4, 2026 08:52
@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown

Greptile Summary

此 PR 修复移动端新建会话和富文本输入中的两条独立阻塞路径:

  • 将 worktree 宿主能力标记改为通过 ref 读取,避免目录资格探测更新取消正在进行的偏好读取。
  • 在 worktree 偏好门阻塞创建时补充用户可见提示。
  • 在程序化替换或插入富文本 DOM 前清除 IME 组合态,并处理 compositioncancel
  • 增加针对偏好 effect 依赖、阻塞提示和组合态恢复的回归测试。
  • 当前仍有一处非阻塞的提示准确性问题:宿主拒绝偏好写入后仍会显示为“正在同步”。

Confidence Score: 4/5

此 PR 看起来可以合并,但建议修正偏好权威冲突状态下误导性的“正在同步”提示。

两条核心修复均与现有设备代次隔离和富文本变更协议保持一致;唯一确认的问题是新增提示将已结束的宿主偏好冲突描述为仍在同步,影响故障可理解性但不破坏核心流程。

Files Needing Attention: apps/mobile/app/sessions/new.tsx

Important Files Changed

Filename Overview
apps/mobile/app/sessions/new.tsx 修复 worktree 偏好读取被资格探测更新取消的竞态并补充阻塞提示,但 authority-unknown 冲突被误显示为正在同步。
apps/mobile/src/session/composerRichInputHtml.ts 程序化 DOM 修改前清除 IME 组合态,并新增 compositioncancel 恢复路径。
apps/mobile/src/tests/newSession.test.ts 通过源码锁定测试覆盖播种 effect 依赖和新增提示条件。
apps/mobile/src/tests/composerRichInputHtml.test.ts 覆盖程序化应用文档后组合态解除、后续输入重新上报的行为。

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[用户选择项目目录] --> B[并行读取 worktree 偏好]
  A --> C[探测目录资格与宿主能力]
  C --> D[通过 ref 提供最新能力标记]
  B --> E{偏好是否就绪}
  D --> E
  E -->|否| F[阻止创建并显示状态提示]
  E -->|是| G[允许创建会话]
  H[IME 正在组合输入] --> I[插入目录 atom 或应用文档]
  I --> J[清除 composing 并修改 DOM]
  J --> K[后续 input 正常上报]
  L[compositioncancel] --> K
Loading
Prompt To Fix All With AI
### Issue 1
apps/mobile/app/sessions/new.tsx:1220
**冲突状态提示错误**

当宿主回传的权威偏好与用户刚写入的值不一致时,代码会进入 `worktreePreferenceAuthorityUnknown`。此时写入已经结束并需要用户重试,但这里仍显示“正在同步 worktree 设置…”,会让用户无法判断创建为何仍被阻止。请为该冲突状态提供失败或重试提示,或只在实际加载、保存期间使用当前文案。

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(mobile): 富文本输入框程序化改 DOM 时清 IME 组合态,补..." | Re-trigger Greptile

Comment thread apps/mobile/app/sessions/new.tsx
@MagicLizi MagicLizi added the touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) label Sep 4, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor Author

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

宿主回传与刚写入的 worktree 偏好不一致时,写入已结束并需用户重试开关。
caption / 创建 / Goal 错误此前仍走「正在同步 worktree 设置…」,用户无法
判断创建为何仍被阻止。冲突态改用独立失败文案,仅在真正加载或保存期间
保留同步中提示。

Signed-off-by: MagicLizi <jiali@magiclizi.com>
@MagicLizi

Copy link
Copy Markdown
Contributor Author

本轮:合并 origin/main 后补了权威冲突提示。宿主回传与刚写入的 worktree 偏好不一致时,caption / 创建 / Goal 不再显示「正在同步 worktree 设置…」,改为失败/重试文案,并补了 Light/Dark caption 证据。

@MagicLizi MagicLizi added the status:awaiting-bot-review 等外部审查机器人表态(review-pr 自动维护,仅展示) label Sep 4, 2026
@MagicLizi MagicLizi removed the status:awaiting-bot-review 等外部审查机器人表态(review-pr 自动维护,仅展示) label Sep 4, 2026
@MagicLizi
MagicLizi merged commit dfa95b6 into main Sep 4, 2026
20 checks passed
@MagicLizi
MagicLizi deleted the fix/mobile-dir-select-send-blocked branch September 4, 2026 12:11
@MagicLizi

Copy link
Copy Markdown
Contributor Author

合了。选目录后发不出去这件事,一半是 worktree 偏好被取消的 GET 卡住,一半是输入法组合态把发送钉死——现在两条都解开了,手机上选完目录就能接着打字发。

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