Skip to content

fix(startup): 打开一个 md,屏幕上就该是那一篇——启动时不再有两段代码抢焦点 - #258

Open
oratis wants to merge 1 commit into
mainfrom
claude/markup-file-switch-issue-5b55f4
Open

fix(startup): 打开一个 md,屏幕上就该是那一篇——启动时不再有两段代码抢焦点#258
oratis wants to merge 1 commit into
mainfrom
claude/markup-file-switch-issue-5b55f4

Conversation

@oratis

@oratis oratis commented Sep 7, 2026

Copy link
Copy Markdown
Owner

双击一个 .md,Markup 起来了、标签页也在,屏幕上却还是上次那篇。

根因:两个 effect 在抢 activeTabId

App.tsx 里有两段互不相识的启动 effect:

做什么 IPC 跳数
A 恢复会话 readSession() → 并发读 N 个文件 → 无条件 setActiveTab(sess.active) 1
B OS 打开 takePendingFiles() → 读 1 个文件 → openLoadedFile()(顺带设焦点) 2

谁后落地谁赢。B 是「取 + 读」两跳 IPC,A 是一跳并发读——文件都小的时候 B 后落地,看起来一切正常;只要会话里有一篇大文件(这仓库里一本书动辄几百 KB),A 就慢到最后才落地,那句无条件的 setActiveTab 把焦点拽回上一次的文档。所以它对轻用户偶发,对天天开着一本书的人「总是」发生。

方案:把「谁该在屏幕上」变成一条有序流程

新的 useStartupTabs 接管这两段 effect:

  1. 先注册 open-files 监听——启动期间来的 live open 不会漏;
  2. 再打开 OS 交给我们的文件,带焦点。用户在等这一篇,不是在等上次的标签页;
  3. 最后在后台恢复会话openLoadedFile(loaded, { activate: false })),并且只有在没人认领焦点时才 setActiveTab(sess.active)

「没人认领」在开标签页之前取快照判定:activeTabId 还是启动时那个 welcome 草稿才算;已经是别的东西(第 2 步开的文件、侧边栏点开的、拖进来的、⌘N 新建的)→ 恢复流程闭嘴。

同源的三个问题一并修掉

  • setActiveTab 不校验 id —— 会话里记的文件被删/改名后,activeTabId 指向一个不存在的标签页,getActiveTab() 返回 null,编辑器直接白屏。现在忽略这种 id。
  • RunEvent::Opened 既 emit 又缓冲 —— 应用已在跑时那份缓冲没人取,直到下一个窗口(⌘N)挂载才被 take_pending_files 取走,新窗口莫名打开十分钟前双击过的文件。新增 FrontendReady:前端就绪后只 emit,不再缓冲。顺带 unminimize + show + set_focus——「切换到最新打开的」也包括把窗口本身端到用户面前。
  • 标签条从不滚到激活项 —— overflow-x-auto 但没有 scroll-into-view,标签一多,新开的那页在可视区外。

怎么知道它真的修好了

把修好的代码临时改回旧逻辑跑新测试,失败信息就是用户的原话:

AssertionError: expected '/v/old.md' to be '/v/new.md'
AssertionError: expected '/v/gone.md' to be '/v/a.md'     ← 白屏那条
AssertionError: expected '/v/old.md' to be '/v/live.md'

改回修复版后 3 条全绿。

验证

检查 结果
pnpm lint ✅ 259 文件干净
pnpm tsc --noEmit
pnpm test ✅ 109 文件 / 1073 用例全过(新增 9 条)
pnpm test:coverage ✅ 52.6 / 50.0 / 52.1 / 53.3,均高于 CI 下限
cargo check + cargo test ✅ 干净 / 9 passed,新增块 rustfmt-clean

取舍与不在本轮

两处取舍(恢复会话该不该完全不设焦点;被双击的文件排标签条左边还是右边)的正反方辩论,以及三条「不在本轮」(重开已开文件不刷新内容、.canvas/.html 不能从 Finder 打开、会话恢复失败无提示),见 docs/design/11-open-file-focus.md

一处已知的行为变化:只有「冷启动 + 双击文件」这一种情况下标签顺序会变(被双击的那篇在最左,恢复的标签排它右边)——换来的是不必等整个会话读完才看到自己点的文件。普通启动的顺序不变。

🤖 Generated with Claude Code

双击一个 .md,Markup 起来了、标签页也在,屏幕上却还是上次那篇。

根因是 App.tsx 里两段互不相识的启动 effect 在抢 activeTabId:

  A 恢复会话  readSession() → 并发读 N 个文件 → 无条件 setActiveTab(sess.active)
  B OS 打开   takePendingFiles() → 读 1 个文件 → openLoadedFile()(顺带设焦点)

谁后落地谁赢。B 是「取 + 读」两跳 IPC,A 是一跳并发读——文件都小的时候
B 后落地,看起来正常;只要会话里有一篇大文件(这仓库一本书动辄几百 KB),
A 就慢到最后才落地,那句无条件的 setActiveTab 把焦点拽回上一次的文档。
所以它对轻用户偶发,对天天开着一本书的人「总是」发生。

useStartupTabs 把这两段合成一条有序流程:先注册监听(启动期间来的 live
open 不丢),再打开 OS 交给我们的文件并带焦点(用户在等这一篇),最后在
后台恢复会话(openLoadedFile 的新 activate: false),且只有在没人认领焦点
时才去 setActiveTab(sess.active)。「没人认领」在开标签页之前取快照判定:
activeTabId 还是启动时的 welcome 草稿才算。

同源的三个问题一并修掉:

- setActiveTab 不校验 id。会话里记的文件被删/改名后 activeTabId 指向一个
  不存在的标签页,getActiveTab() 返回 null,编辑器直接白屏。现在忽略。
- RunEvent::Opened 既 emit 又缓冲。应用已在跑时那份缓冲没人取,直到下一个
  窗口(⌘N)挂载才被 take_pending_files 取走——新窗口莫名打开十分钟前双击
  过的文件。新增 FrontendReady:前端就绪后只 emit,不再缓冲。顺带
  unminimize + show + set_focus,最小化的窗口也会被端到用户面前。
- 标签条 overflow-x-auto 但从不滚到激活项。标签一多,新开的那页在可视区外。

把修好的代码临时改回旧逻辑,新测试如实报出用户的原话:
expected '/v/old.md' to be '/v/new.md'。

设计与两处取舍的辩论见 docs/design/11-open-file-focus.md。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant