feat(chat): add overlay mode for chat UI plugin pages#271
feat(chat): add overlay mode for chat UI plugin pages#271Mizushima-Mihane wants to merge 4 commits into
Conversation
Extends the chat-UI plugin slots with an "overlay" page mode: a chat-top-toolbar contribution can pop its plugin page as a draggable floating window over the chat stage, instead of navigating to the plugin settings route. - FrontendChatUIContribution action gains an optional "mode": "overlay" (default "navigate"), serialized as pageMode. - PluginSlot threads pageMode into the open-plugin-page target. - ChatStagePage opens overlay-mode pages in a new PluginPageOverlay instead of navigating away. - PluginPageOverlay: a plugin-neutral floating window hosting the plugin page iframe; drag by the bottom bar or, for a cooperating page, from any blank area (a postMessage drag protocol); tap the bar to collapse.
Reviewer's Guide by Ameath小爱整理了一条清晰的查看路径,顺着读会更省心。 审查链路为 文件级变更
Tips and commands
Original review guide in EnglishThe reviewed chain is contribution serialization in |
There was a problem hiding this comment.
小爱把需要留意的地方逐一标出来,咱们一起看看。
小爱已完成全部变更审查,共发布 3 条行级建议(高风险 0,中风险 2)。
合并前关注点(风险 2 · 建议测试 1)
风险
docs/PLUGIN_DEVELOPER_GUIDE.md仍说明open-plugin-page只接受page_id,尚未说明mode: "overlay"协议。- PR 元数据的
linked_issues为空;基线CONTRIBUTING.md要求每个 PR 链接已有 issue,维护者需补充链接或确认豁免。
建议测试
- 更新并扩展插件 slot 协议测试:
test/unit/frontend_bridge_core/test_frontend_plugin_ui.py的两个严格 payload 断言现在缺少默认pageMode: "navigate",frontend/src/test/shared/plugin/pluginSlots.test.tsx仍期待不含mode的回调目标;同时覆盖mode: "overlay"打开 iframe 的路径。
Prompt for AI Agents
请逐项处理以下审查问题;修改前先核实上下文,修改后运行相关测试。
1. 浮窗可失去唯一的主机控制条
frontend/src/features/chat-stage/components/PluginPageOverlay.tsx:80 (RIGHT)
<issue_to_address>HEIGHT 固定为 680,但桌面聊天窗允许的最小内部高度是 620;初始 top: 84 时底栏从 y=718 开始,完全位于视口外。这里的拖拽上界也忽略了 HEIGHT,因此向下拖动同样能把唯一的主机拖拽/关闭控件移出屏幕。未实现私有 postMessage 关闭路径的插件页将无法通过主机 UI 移动或关闭该浮窗。请按视口限制浮窗尺寸和位置,并始终保留底栏可见。
HEIGHT is fixed at 680 while the desktop chat window permits a 620px inner height; with the initial top: 84, the bar starts at y=718 and is entirely outside the viewport. This drag bound also ignores HEIGHT, so dragging down can likewise strand the only host drag/close control. Plugin pages without a private postMessage close path cannot then move or dismiss the overlay through the host UI. Bound its size and position against the viewport while always keeping the bar visible.</issue_to_address>
2. Overlay iframe 绕过已配置的 HTTP bridge 基址
frontend/src/features/chat-stage/components/PluginPageOverlay.tsx:25 (RIGHT)
<issue_to_address>平台选择器支持 VITE_SHINSEKAI_API_BASE,且开发指南将其作为 Vite 连接 bridge 的方式;这里却只读取 URL 中的 shinsekai_bridge。在该受支持配置下,插件插槽列表会从 bridge 获取,但 iframe 会请求 Vite 源站的相对 /api/plugins/... 路径;vite.config.ts 没有该路径的 proxy,因此 overlay 会加载应用回退页或 404,而不是插件页面。应使用与 platform 相同的 bridge 基址和认证来源,或使用 host 已解析的 frontend URL。
The platform selector supports VITE_SHINSEKAI_API_BASE, and the developer guide uses it for connecting Vite to the bridge, but this code reads only the shinsekai_bridge URL parameter. In that supported setup, slot contributions come from the bridge while the iframe requests relative /api/plugins/... from the Vite origin. vite.config.ts has no proxy for that path, so the overlay loads the app fallback or a 404 instead of the plugin page. Use the same bridge base and auth source as the platform, or a host-resolved frontend URL.</issue_to_address>
3. 键盘激活会推进对话而不能关闭浮窗
frontend/src/features/chat-stage/components/PluginPageOverlay.tsx:137 (RIGHT)
<issue_to_address>此按钮只处理 onPointerDown,键盘的 Enter/Space 激活不会触发该处理器。与此同时,overlayTarget 未计入 modalOpen,所以全局聊天快捷键会拦截这两个按键并推进对话;带有 “Collapse” 标签的唯一主机关闭控件既不能关闭浮窗,还会意外推进聊天。请为键盘关闭提供显式处理,并阻止或在 overlay 打开时禁用舞台快捷键。
This button handles only onPointerDown, which keyboard Enter/Space activation does not trigger. Because overlayTarget is not included in modalOpen, the global chat shortcut intercepts those keys and advances the dialog; the only host control labelled “Collapse” neither closes the overlay nor avoids an unintended chat advance. Provide explicit keyboard close handling and stop or disable stage shortcuts while the overlay is open.</issue_to_address>
| const [pos, setPos] = useState<Pos>(() => ({ | ||
| x: clamp(window.innerWidth - WIDTH - 24, 8, Math.max(8, window.innerWidth - WIDTH - 8)), | ||
| y: 84, | ||
| })); | ||
| const posRef = useRef(pos); | ||
| posRef.current = pos; | ||
| const dragBase = useRef<Pos | null>(null); | ||
| const frameRef = useRef<HTMLIFrameElement | null>(null); | ||
|
|
||
| // Host-owned drag surface: the bottom bar. A press that never moves is a tap → close. | ||
| const onBarPointerDown = (event: ReactPointerEvent) => { | ||
| if (event.button !== 0) { | ||
| return; | ||
| } | ||
| dragBase.current = posRef.current; | ||
| const startX = event.screenX; | ||
| const startY = event.screenY; | ||
| let moved = false; | ||
| const move = (e: PointerEvent) => { | ||
| if (!dragBase.current) { | ||
| return; | ||
| } | ||
| const dx = e.screenX - startX; | ||
| const dy = e.screenY - startY; | ||
| if (Math.abs(dx) > 3 || Math.abs(dy) > 3) { | ||
| moved = true; | ||
| } | ||
| setPos({ | ||
| x: clamp(dragBase.current.x + dx, 8, window.innerWidth - WIDTH - 8), | ||
| y: clamp(dragBase.current.y + dy, 8, window.innerHeight - 56), |
There was a problem hiding this comment.
[MEDIUM · bug] 浮窗可失去唯一的主机控制条
HEIGHT 固定为 680,但桌面聊天窗允许的最小内部高度是 620;初始 top: 84 时底栏从 y=718 开始,完全位于视口外。这里的拖拽上界也忽略了 HEIGHT,因此向下拖动同样能把唯一的主机拖拽/关闭控件移出屏幕。未实现私有 postMessage 关闭路径的插件页将无法通过主机 UI 移动或关闭该浮窗。请按视口限制浮窗尺寸和位置,并始终保留底栏可见。
HEIGHT is fixed at 680 while the desktop chat window permits a 620px inner height; with the initial top: 84, the bar starts at y=718 and is entirely outside the viewport. This drag bound also ignores HEIGHT, so dragging down can likewise strand the only host drag/close control. Plugin pages without a private postMessage close path cannot then move or dismiss the overlay through the host UI. Bound its size and position against the viewport while always keeping the bar visible.
置信度 99% · 可直接回复讨论 · 👍/👎 会影响后续审查
| return path; | ||
| } | ||
| const params = new URLSearchParams(window.location.search); | ||
| const bridgeBase = params.get("shinsekai_bridge")?.trim(); |
There was a problem hiding this comment.
[MEDIUM · bug] Overlay iframe 绕过已配置的 HTTP bridge 基址
平台选择器支持 VITE_SHINSEKAI_API_BASE,且开发指南将其作为 Vite 连接 bridge 的方式;这里却只读取 URL 中的 shinsekai_bridge。在该受支持配置下,插件插槽列表会从 bridge 获取,但 iframe 会请求 Vite 源站的相对 /api/plugins/... 路径;vite.config.ts 没有该路径的 proxy,因此 overlay 会加载应用回退页或 404,而不是插件页面。应使用与 platform 相同的 bridge 基址和认证来源,或使用 host 已解析的 frontend URL。
Original in English
The platform selector supports VITE_SHINSEKAI_API_BASE, and the developer guide uses it for connecting Vite to the bridge, but this code reads only the shinsekai_bridge URL parameter. In that supported setup, slot contributions come from the bridge while the iframe requests relative /api/plugins/... from the Vite origin. vite.config.ts has no proxy for that path, so the overlay loads the app fallback or a 404 instead of the plugin page. Use the same bridge base and auth source as the platform, or a host-resolved frontend URL.
置信度 96% · 可直接回复讨论 · 👍/👎 会影响后续审查
| src={pluginPageSrc(target.pluginId, target.pageId, nonce)} | ||
| title="Plugin" | ||
| /> | ||
| <button aria-label="Collapse" className="plugin-overlay__bar" onPointerDown={onBarPointerDown} type="button"> |
There was a problem hiding this comment.
[LOW · bug] 键盘激活会推进对话而不能关闭浮窗
此按钮只处理 onPointerDown,键盘的 Enter/Space 激活不会触发该处理器。与此同时,overlayTarget 未计入 modalOpen,所以全局聊天快捷键会拦截这两个按键并推进对话;带有 “Collapse” 标签的唯一主机关闭控件既不能关闭浮窗,还会意外推进聊天。请为键盘关闭提供显式处理,并阻止或在 overlay 打开时禁用舞台快捷键。
Original in English
This button handles only onPointerDown, which keyboard Enter/Space activation does not trigger. Because overlayTarget is not included in modalOpen, the global chat shortcut intercepts those keys and advances the dialog; the only host control labelled “Collapse” neither closes the overlay nor avoids an unintended chat advance. Provide explicit keyboard close handling and stop or disable stage shortcuts while the overlay is open.
置信度 98% · 可直接回复讨论 · 👍/👎 会影响后续审查
…ming Lets a plugin (the phone) ring the player. The runtime emits a call.incoming / call.ended stream event, folded into the snapshot's incomingCall field; the chat stage auto-pops the plugin's overlay page with call params (call_name/call_type/ call_dir) so it renders the ring screen, and closes that overlay on call.ended. - main.py: get_stream_sink() accessor so plugins can emit stream events. - event_sink.py: incomingCall snapshot default + call.incoming/ended fold branches. - types.ts / initialState.ts / events.ts: IncomingCall type, snapshot field, event union members, reducer cases. - PluginPageTarget gains params; PluginPageOverlay forwards them into the iframe URL. - ChatStagePage: pop overlay on a fresh incomingCall, close on call.ended. - frontend_bridge.py: get_bridge_state() accessor (phone SMS/call triggers).
…stroy
The overlay used to unmount (destroying the iframe, cache-busting a fresh nonce)
whenever it closed, so every reopen fully reloaded the plugin page. It now stays
mounted after the first open and just toggles visibility; 'shown' tracks the last
non-null target so the iframe src stays stable across open/close and only changes
when the page or its params actually change (an incoming call passes new call_*
params -> src changes -> reload, as before). On re-show it posts
{__doki:'phone', type:'show'} so a cooperating page (the phone) refreshes its data.
The overlay open-mode work added a pageMode field to serialized chat-UI slot contributions; the two payload assertions were never updated. Add pageMode=navigate (the default) to both expected dicts.
Stacked on
codex/chat-phone-toolbar-entry(extends that PR).Adds an overlay page mode to the chat-UI plugin slots: a
chat-top-toolbarcontribution can pop its plugin page as a draggable floating window over the chat stage, instead of navigating to the plugin settings route.FrontendChatUIContributionaction gains an optional"mode": "overlay"(defaultnavigate), serialized aspageMode.PluginSlotthreadspageModeinto the open-plugin-page target.ChatStagePageopens overlay-mode pages in a newPluginPageOverlayinstead of navigating away.PluginPageOverlay: a plugin-neutral floating window hosting the plugin page iframe; drag by the bottom bar or, for a cooperating page, from any blank area (postMessage drag protocol); tap the bar to collapse.tsc -bpasses. No behaviour change for existingnavigatecontributions.Summary by Ameath
小爱先把这次核对的重点轻轻收拢,方便快速查看。
该 PR 为聊天插件页面新增
overlay打开模式:bridge 将action.mode序列化为pageMode,PluginSlot将其交给ChatStagePage,后者以PluginPageOverlayiframe 浮窗呈现。审查发现浮窗在允许的小窗口尺寸或下拖后可能失去唯一控制条,且 iframe URL 未遵循已配置的 HTTP platform;底栏也无法由键盘关闭。New Features
mode: "overlay",在聊天舞台上打开插件前端页。Enhancements
pageMode会规范化为navigate,保持既有导航行为。Tests
pageMode协议。Original summary in English
This PR adds an
overlayopening mode for chat plugin pages: the bridge serializesaction.modeaspageMode,PluginSlotpasses it toChatStagePage, and that page renders the plugin frontend in aPluginPageOverlayiframe. The review found that the overlay can lose its only host control at supported small window sizes or after a downward drag, its iframe URL does not follow the configured HTTP platform, and its bar cannot be closed with a keyboard.New Features
mode: "overlay"to open a plugin frontend page over the chat stage.Enhancements
pageModevalues normalize tonavigate, preserving existing navigation behavior.Tests
pageModeprotocol.