Skip to content

feat(chat): add overlay mode for chat UI plugin pages#271

Open
Mizushima-Mihane wants to merge 4 commits into
RachelForster:codex/chat-phone-toolbar-entryfrom
Mizushima-Mihane:feat/chat-ui-overlay-slot
Open

feat(chat): add overlay mode for chat UI plugin pages#271
Mizushima-Mihane wants to merge 4 commits into
RachelForster:codex/chat-phone-toolbar-entryfrom
Mizushima-Mihane:feat/chat-ui-overlay-slot

Conversation

@Mizushima-Mihane

@Mizushima-Mihane Mizushima-Mihane commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Stacked on codex/chat-phone-toolbar-entry (extends that PR).

Adds an overlay page mode to the chat-UI plugin slots: 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 (postMessage drag protocol); tap the bar to collapse.

tsc -b passes. No behaviour change for existing navigate contributions.

Summary by Ameath

小爱先把这次核对的重点轻轻收拢,方便快速查看。

该 PR 为聊天插件页面新增 overlay 打开模式:bridge 将 action.mode 序列化为 pageModePluginSlot 将其交给 ChatStagePage,后者以 PluginPageOverlay iframe 浮窗呈现。审查发现浮窗在允许的小窗口尺寸或下拖后可能失去唯一控制条,且 iframe URL 未遵循已配置的 HTTP platform;底栏也无法由键盘关闭。

New Features

  • 聊天 UI 插槽现在可声明 mode: "overlay",在聊天舞台上打开插件前端页。

Enhancements

  • 缺失或未知的 pageMode 会规范化为 navigate,保持既有导航行为。

Tests

  • 现有插件插槽与 bridge payload 的精确断言尚未更新以覆盖新增的 pageMode 协议。
Original summary in English

This PR adds an overlay opening mode for chat plugin pages: the bridge serializes action.mode as pageMode, PluginSlot passes it to ChatStagePage, and that page renders the plugin frontend in a PluginPageOverlay iframe. 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

  • Chat UI slots can now declare mode: "overlay" to open a plugin frontend page over the chat stage.

Enhancements

  • Missing or unknown pageMode values normalize to navigate, preserving existing navigation behavior.

Tests

  • Existing exact assertions for plugin slots and bridge payloads have not been updated for the added pageMode protocol.

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.
@ameath-review

ameath-review Bot commented Jul 24, 2026

Copy link
Copy Markdown

Reviewer's Guide by Ameath

小爱整理了一条清晰的查看路径,顺着读会更省心。

审查链路为 frontend_bridge_core/plugin_ui.py 的贡献序列化、PluginSlot 的目标传递、ChatStagePage 的模式分发,以及 PluginPageOverlay 的 iframe URL、拖拽和关闭生命周期。请优先验证浮窗在最小桌面窗口高度、窗口缩放和向下拖动后仍保留可见的主机控制条;验证独立 Vite/API-base 部署会把 iframe 指向 bridge;并更新既有严格协议断言及新增 overlay 路径的覆盖。

文件级变更

Change Details Files
新增文件 2 个文件,+207 / -0 frontend/src/features/chat-stage/components/PluginPageOverlay.css
frontend/src/features/chat-stage/components/PluginPageOverlay.tsx
修改文件 4 个文件,+28 / -7 frontend/src/features/chat-stage/ChatStagePage.tsx
frontend/src/shared/platform/types.ts
frontend/src/shared/plugin/PluginSlot.tsx
frontend_bridge_core/plugin_ui.py

Tips and commands
  • 重新审查: @Ame4th review
  • 重新生成摘要: @Ame4th summary
  • 重新生成指南: @Ame4th guide
  • 生成标题: @Ame4th title
  • 解决本机器人线程: @Ame4th resolve
  • 清理本机器人审查: @Ame4th dismiss
  • 从行级评论创建 Issue: 回复 @Ame4th issue
  • 继续讨论: 直接回复任一行级评论。
  • 反馈偏好: 对行级评论点 👍 或 👎。
Original review guide in English

The reviewed chain is contribution serialization in frontend_bridge_core/plugin_ui.py, target propagation through PluginSlot, mode dispatch in ChatStagePage, and the iframe URL, drag, and close lifecycle in PluginPageOverlay. Prioritize verifying that the host control remains visible at the minimum desktop height, after resizing, and after a downward drag; that separate Vite/API-base deployments point the iframe at the bridge; and that existing strict protocol assertions plus the new overlay path are covered.

@ameath-review ameath-review Bot changed the title feat(chat): overlay open-mode for chat-UI slot pages feat(chat): add overlay mode for chat UI plugin pages Jul 24, 2026

@ameath-review ameath-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

小爱把需要留意的地方逐一标出来,咱们一起看看。

小爱已完成全部变更审查,共发布 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>

Comment on lines +51 to +80
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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants