Skip to content

修复芯片内容显示时会直接消失的问题 - #65

Merged
taekchef merged 6 commits into
omdsh-dev:mainfrom
fengbinmov:main
Sep 30, 2026
Merged

taekchef merged 6 commits into
omdsh-dev:mainfrom
fengbinmov:main

Conversation

@fengbinmov

Copy link
Copy Markdown
Contributor
  • fix 芯片在显示内容后会直接消失导致无法正常观看(当回话页面更新时updateChip会将其抹掉)

  • fix 芯片的 250ms 延迟 hide 会误杀另一个芯片刚显示的面板(快速从 A 滑到 B 时)

宿主 layout mutation 误杀了正在观看的面板
@Ryuu-64

Ryuu-64 commented Sep 24, 2026

Copy link
Copy Markdown

补充一份独立验证,供维护者参考。

我在本机(DSH Desktop 2.0.11 / 内核 0.1.5-rc.2 / Windows 11)复现并修复了同一个问题,结论与本 PR 的两条修复一致:

  1. 宿主 layout mutation 触发 updateChip() 抹掉面板:updateChip() 在「无待发送批注」分支里无条件清空共享容器 tipLayer(if (ui.quotes.length === 0) { …; tipLayer.textContent = '' }),而它由 scroll(capture) / resize / body 级 MutationObserver 经 onLayoutChange(rAF) 高频调用。hover 的是已发送消息上的标签/芯片,此时 ui.quotes 必然为空,所以一 hover 就被抹掉。
  2. 250ms 定时器跨面板误杀:三类面板各持一个私有 grace 定时器(hoverGrace / bubbleGrace / grace)却清同一个共享容器,A 遗留的定时器到点会清掉 B 刚显示的面板。

我用 jsdom 把真实的 client.js 经 ModuleLoader.load → factory → apply(ctx) 完整装载起来(真实 DOM 事件 + 真实 MutationObserver + 可控时钟),让上游原版代码跑出一条可复现的失败:

✖ [e2e] 真实 hover:面板显示后,宿主 DOM 高频变化不会抹掉它
  AssertionError: 宿主高频变化后面板必须还在
  0 !== 1        ← 面板先显示出来(前一条断言通过),随后被宿主变化抹掉

本 PR 的 tipOwner / clearTip(owner) / presentTip(owner, el) 思路在我这边同样成立,并已在实际页面上确认 hover 不再闪退(重启宿主加载后,hover 已发送消息上的「批注 ×N」标签与回复里的 Annotation N 芯片均稳定显示)。

另外两条我单独开了 PR(与本 PR 正交、可各自独立合并):

如果本 PR 想要回归用例,我可以把上面那套 jsdom harness 整理成一个 PR 提上来——它跑在上游代码上能稳定复现本问题(4 条 e2e 中 3 条失败),在修复后全绿。

@taekchef
taekchef merged commit 142c2ea into omdsh-dev:main Sep 30, 2026
@taekchef

Copy link
Copy Markdown
Collaborator

已合入 main,感谢 @fengbinmov 的高质量工作——卡死自激修复、0.1.7 会话识别、半透明浮层与编号图层这套改动是一次完整的适配,CHANGELOG 已随 PR 收录你的条目。

我已在本地跑通 npm run check(构建 + 26 项测试全绿),并对照 deepseek-harness dsh-v0.2.0-rc.2 源码核实:uiSession.adapter.current(packages/client/ui-session/src/client/index.ts:312)、localStorage dsh.sessions.current(ui-workspace/navigation.ts:131)与 composer 结构在 0.2.0 上均未变化,这套方案对新内核依然成立。

接下来我们会在其上直接接手继续维护(改动会推到各贡献者分支、保留原署名):

如对本 PR 中的产品决策(半透明默认、编辑窗删除按钮、芯片匹配放宽)有保留意见,欢迎继续在这里讨论。

taekchef added a commit to pinzza/dsh-annotation that referenced this pull request Sep 30, 2026
Merges the uiSession.adapter.current read face (omdsh-dev#65) with the
localStorage / retainedBy fallback chain and the noSession toast +
pending-retry UX (omdsh-dev#68). Session-switch detection now layers the
uiSession source subscription, the legacy list subscription, and a 1s
poll. Verified against dsh-v0.2.0-rc.2 kernel sources; npm run check
passes 33/33.
taekchef pushed a commit that referenced this pull request Sep 30, 2026
`tipLayer` 是长期存活的 body 级单例(只在插件销毁时 remove),而气泡标签
(attachBubbleTag)与回复芯片(makeReplyChip)会被反复重建 —— 消息重渲染、语言
切换、切会话回来都会让它们换一代。改动前每个面板都在自己的 mouseenter 里往
`tipLayer` 追加一对监听器,却从不移除:

    tag.addEventListener('mouseleave', bubbleHide)
    tipLayer.addEventListener('mouseenter', bubbleKeep)   // ← 每代 +1
    tipLayer.addEventListener('mouseleave', bubbleHide)   // ← 每代 +1

回复芯片同理(keep / hide)。于是每次重建净增两个闭包,它们持有已废弃的 grace
定时器与已被移出 DOM 的触发元素;关闭决策不再由「当前面板」决定,而是由历史上
任意一代的定时器决定 —— 这既让面板的关闭时机变得不可预测,也随会话变长持续泄漏。

改动:
- 共享容器上只注册两处固定监听器 sharedTipMouseEnter / sharedTipMouseLeave;
- 新增 `tipActiveHide` 指针:由面板在 mouseenter 时登记 `{ keep, hide }`,
  共享监听器按它派发;
- 新增 releaseActiveTip():面板真正关闭时解除登记(三处宽限到点的关闭都调用),
  避免残留钩子仍被派发;
- 移除三类面板中各自的 tipLayer.addEventListener。

对外行为保持不变:指针进入面板仍会取消关闭,离开仍会按各自的 250ms 宽限关闭。

回归测试 test/shared-listeners.test.mjs(8 条):结构断言「tipLayer 上恰好两处
监听器」+ 把真实的 sharedTipMouseEnter / sharedTipMouseLeave / releaseActiveTip
抽到 vm 沙箱里按行为验证派发目标、面板换代、无登记空操作、解除登记后不再派发。

同一套用例跑上游原版 client.js:8/8 失败;本分支 8/8 通过。

注:本 PR 与 #65、以及「跨间隙保活」那个 PR 均正交。#65 引入归属模型解决「宿主
layout mutation 用 updateChip 抹掉面板」与「定时器跨面板误杀」;本 PR 只处理
「监听器挂在共享容器上、随面板重建累积」这一条。
taekchef added a commit that referenced this pull request Sep 30, 2026
The listener-leak fix (#67) and the tipOwner attributed-clear mechanism
(#65) are orthogonal: tipActiveHide removes the per-rebuild listener
accumulation on the shared tipLayer, tipOwner stops unattributed clears
from killing a visible panel. All clearTip/presentTip ownership pairs
kept; releaseActiveTip fires on every real close path.
taekchef pushed a commit that referenced this pull request Sep 30, 2026
面板定位用 r.bottom + 6 / r.top - h - 6,与触发元素之间必然留 6px 间隙。鼠标从
触发元素移向面板时会先触发 mouseleave,进入「谁都不在」的空档;当前唯一阻止关闭的
是固定 250ms 宽限(官方 HoverCard 的 pointer-grace 思路)。于是:

- 指针停在间隙里把 250ms 走满 → 面板消失,想点「删除」按钮常常点不中;
- 触摸板细调或系统卡顿更容易命中;
- 面板被放到上方时(top < 8 的分支)间隙还更大。

改动:
- 用 pointermove(capture)持续记录实时指针坐标;
- 新增 pointerInsideRect / pointerWithinTipLayer / shouldKeepTipOpen:宽限到点时
  复查指针是否仍在「面板 ∪ 触发元素」的并集内(容差 10px,覆盖 6px 间隙);
- scheduleHide / hide / bubbleHide 接收触发元素,统一用 shouldKeepTipOpen 判定;
- teardown 移除 pointermove 监听。

为什么必须用实时坐标:mouseleave 事件里的 clientX/clientY 是离开那一刻的位置,
250ms 后再拿它判断会读到过期值,等于没判。

回归测试 test/pointer-gap.test.mjs(7 条):把 client.js 里真实的
pointerInsideRect / pointerWithinTipLayer / shouldKeepTipOpen / scheduleHide
抽到 vm 沙箱里按行为验证 —— 覆盖指针停在触发元素上 / 停在 6px 间隙里 / 移进面板
/ 真正远离(仍会正常关闭)/ 宽限期内移回 / cancelHide / 生命周期注册。

注:本 PR 与 #65 正交。#65 解决「宿主 layout mutation 触发 updateChip 抹掉面板」
和「250ms 定时器跨面板误杀」;本 PR 解决「指针跨间隙时被固定宽限关掉」。两者互不
依赖,可各自独立合并。
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.

3 participants