Skip to content

feat(desktop-shell-ball): add bubble region and pinned bubble windows#183

Merged
Blackcloudss merged 20 commits into
1024XEngineer:mainfrom
gdemonc:feat/shell-ball-bubble
Apr 11, 2026
Merged

feat(desktop-shell-ball): add bubble region and pinned bubble windows#183
Blackcloudss merged 20 commits into
1024XEngineer:mainfrom
gdemonc:feat/shell-ball-bubble

Conversation

@gdemonc

@gdemonc gdemonc commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • 把 shell-ball 上方的 bubble 区从占位壳升级成真正的透明消息气泡区域
  • 新增 per-bubble pin/delete 行为,并支持将 pinned bubble 脱离 bubble 区作为独立窗口存在
  • 让 bubble item 对齐协议语义,同时把桌面窗口态保留在 shell-ball 本地扩展层

Details

  • 将 bubble 数据从前端局部 message 结构重构为 BubbleMessage + shell-ball desktop state 的包装模型
  • bubble region window 改成显式存在策略:空时透明且 click-through,有可见未置顶气泡时可交互
  • bubble 区现在渲染真实消息列表:agent 左侧、user 右侧、历史可滚动、滚动条隐藏、上下渐隐、fresh 消息轻微上浮+淡入
  • 每条 bubble 提供 Pin/Delete 控件,delete 立即彻底移除,pin 后从 bubble 区消失
  • 新增 detached pinned bubble window:
    • shell-ball-bubble-pinned-<bubble_id> 生成窗口标签
    • 一窗一气泡
    • 可拖拽
    • 不受 bubble 区消逝机制影响
  • unpin 会把 bubble 按 bubble.created_at,再按 bubble.bubble_id 回到 bubble 区原时间序位置
  • bubble 区整体锚点向下收了一点,更贴近悬浮球但仍保持不重叠
  • 补齐 contract tests,覆盖 bubble existence strategy、pin/unpin/delete round-trip、pinned window 生命周期、排序恢复和样式契约

Test Plan

  • pnpm --dir apps/desktop test:shell-ball
  • pnpm --dir apps/desktop typecheck
  • pnpm --dir apps/desktop build
  • pnpm --dir apps/desktop exec tauri dev

@fennoai fennoai Bot 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.

I found two behavior regressions in the new pinned-bubble flow.


if (payload.action === "pin") {
detachedPinnedBubbleIdsRef.current.delete(payload.bubbleId);
void syncPinnedBubbleWindowAnchor(payload.bubbleId);

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.

setBubbleItems is async, but syncPinnedBubbleWindowAnchor(payload.bubbleId) immediately reads bubbleItemsRef.current, which still contains the pre-pin snapshot. On the first pin this means bubbleItem is still unpinned, so the helper returns early and the detached window is never created until some later move/resize event happens to re-run the sync. This should be driven from the post-update state (or an effect keyed on pinned items) instead of the stale ref.

function handleDetachDrag() {
if (followsShellBallGeometry) {
setFollowsShellBallGeometry(false);
void emitShellBallPinnedWindowDetached(pinnedBubbleId);

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.

The window is marked detached on pointerdown, before any actual drag occurs. A simple click on the drag handle will emit pinnedWindowDetached, after which shell-ball moves stop re-anchoring this bubble even though the user never dragged it. That does not match the documented behavior of detaching only once the user has dragged the pinned window.

@fennoai fennoai Bot 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.

A few behavioral issues need attention in the pinned-bubble flow and its capability scope.

}

function handleBubbleAction(payload: ShellBallBubbleActionPayload) {
setBubbleItems((currentItems) => applyShellBallBubbleAction(currentItems, payload));

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.

pin updates state and then immediately calls syncPinnedBubbleWindowAnchor(), but that helper reads bubbleItemsRef.current, which still holds the pre-update snapshot. On the first pin, the item is not yet marked pinned, so the helper returns early and no detached window is created until some later move/resize re-sync happens.

type="button"
className="shell-ball-bubble-message__drag-handle"
aria-label="Drag pinned bubble"
onPointerDown={handleDetachDrag}

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.

This marks the bubble as detached on pointerdown, not after an actual drag completes. A simple click on the drag handle permanently stops future re-anchoring, which does not match the documented detach after drag behavior.

"windows": ["shell-ball", "shell-ball-bubble", "shell-ball-input", "shell-ball-bubble-pinned-*", "dashboard", "control-panel"],
"permissions": [
"core:default",
"core:window:allow-create",

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.

Adding core:window:allow-create to the shared default capability broadens window-creation rights for every renderer covered by this profile, not just the shell-ball surfaces. This should be scoped to a narrower shell-ball-specific capability/window set so the new pinned-window feature does not expand the app-wide trust boundary.

@gdemonc

gdemonc commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator Author

@xgopilot 先不要扩 scope。只修 pin 后 detached window 可能不会立即创建的竞态。
不要顺手改 protocol model、bubble region strategy、route、hotspot。
修法上优先基于 pin 操作后的 nextItems 做开窗同步,而不是 setState 后立刻读取旧 ref。
改完补一条 contract/runtime test,证明 pin 后无需等待下一次 geometry update 就会立刻出现 pinned window。

@fennoai

fennoai Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Result

已按你限定的 scope 修掉 pin 后 detached window 首次可能不立即创建的竞态,没有改 protocol model、bubble region strategy、route 或 hotspot。

这次只改了两个点:

  • apps/desktop/src/features/shell-ball/useShellBallCoordinator.ts 里,bubbleAction 先基于当前 bubbleItemsRef.current 计算 nextItems,同步写回 ref,再用这份 nextItems 触发 syncPinnedBubbleWindowAnchor,避免 setState 后立刻读到旧快照。
  • apps/desktop/src/features/shell-ball/shellBall.contract.test.ts 新增一条 contract/runtime test,直接断言 pin 事件后会立刻调用 openShellBallPinnedBubbleWindow,不需要等待下一次 onMoved / onResized geometry resync。

已提交并推送到当前 PR 分支,commit 是 f563b43。另外按 fork 流程补了一条配套 PR:gdemonc#1

Verification

本地尝试执行 pnpm --dir apps/desktop test:shell-ballpnpm --dir apps/desktop typecheck,但当前环境里 pnpmcorepack 都不存在,所以这次无法在这里实际重跑脚本;代码和新增测试已完成并提交。

View job run

@Blackcloudss Blackcloudss merged commit bccda4d into 1024XEngineer:main Apr 11, 2026
1 check passed
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.

2 participants