From 0eddfaf2f19987bce24f3e317da3c11eb73a8d60 Mon Sep 17 00:00:00 2001 From: kuddev Date: Tue, 18 Aug 2026 22:37:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(gpui):=20draw=20the=20tab=20context=20menu?= =?UTF-8?q?=20once=20=E2=80=94=20shadow=20no=20longer=20stacks=20per=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ContextMenuExt::context_menu()` wraps the host element and keys its element state by a hardcoded "context-menu" id. The wrapper's id enters the GlobalElementId stack *before* the wrapped row's own `("sidebar-tab", ix)`, so every sidebar tab row resolved to the same state — the same `Rc>`. Opening one menu set `open = true` for all rows, and each row then re-rendered the same PopupMenu entity at the same anchor. The panel is opaque so the stack was invisible, but the popover shadow is not: its alpha composited once per tab. That is the whole of "the more tabs, the thicker the right-click shadow" — no packaging or config involvement. The tab menu now uses the same carrier as the file tree: rows only record the anchor and index, and a single `deferred(anchored(PopupMenu))` is drawn from the workspace root, so the component's own popover shadow stays single-layer. The menu is dropped when the TABS section folds, and skipped when its captured index no longer exists. Alongside it: - CHANGELOG: 1.1.0 gains this fix and issue #38 (two notify wrappers wrapping each other until Codex's config.toml reached 130MB); release date moves to 2026-08-18. - docs/release-notes/v1.1.0.md: release notes in the 1.0.0 format, with PR credits, reporter credits, and a pointer to the issue templates. - README / THIRD-PARTY-NOTICES: GPUI and gpui-component are credited and declared — both Apache-2.0, compiled as lightly patched forks. - .gitignore: ignore the whole `.probe-*/` family instead of one name at a time. --- .gitignore | 4 +- CHANGELOG.md | 6 +- README.md | 19 +++ THIRD-PARTY-NOTICES | 8 ++ docs/release-notes/v1.1.0.md | 107 +++++++++++++++++ nebula_app/src/gpui_shell/workspace.rs | 22 +++- .../src/gpui_shell/workspace/tab_menu.rs | 109 +++++++++++++----- 7 files changed, 238 insertions(+), 37 deletions(-) create mode 100644 docs/release-notes/v1.1.0.md diff --git a/.gitignore b/.gitignore index acb7981..e41f55e 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,6 @@ impeccable/ # Claude packaging/probe scratch (this branch) dist-pack-*/ -.probe-pack/ +# 每次 A/B 对照都会开一个新的探针目录(.probe-pack、.probe-layout-daily、 +# .probe-tabmenu…),逐个补规则永远漏,所以整族忽略。 +.probe-*/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c973b23..5d1e980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Every release entry is provided in English and Simplified Chinese. 每个版本条目均同时提供英文和简体中文说明。 -## 1.1.0 - 2026-08-17 +## 1.1.0 - 2026-08-18 ### English @@ -39,6 +39,8 @@ Every release entry is provided in English and Simplified Chinese. - **Markdown reader fills the reading column (GPUI shell)** — soft line breaks inside a paragraph (hand-wrapped README prose) rendered as hard line breaks, leaving the right side of the column empty and ragged. They now merge into spaces per CommonMark, with CJK neighbours joining directly. - **Markdown images actually load (GPUI shell)** — the document tab now resolves relative image paths against the document's own directory, loads absolute local paths from disk, and fetches `http(s)` images through a ureq HTTP client on the background executor (gpui ships a null client by default), so README logos, shields badges (SVG included) and screenshots finally display. Animated GIF/WebP (local or remote) are flattened to a still PNG first, because gpui panics when a multi-frame image's frame index is reused on a static image. - **Raw HTML in Markdown renders like GitHub (GPUI shell)** — README-style HTML now matches GitHub's semantics: `align="center"` on `

` / `

` / headings centers the block, consecutive `` badges inside one paragraph flow on a single centered row instead of stacking one per line, inline `
` breaks in document order (it used to be hoisted above the text it followed), and `` linked badges are no longer dropped — the image renders and carries its link. +- **Codex `notify` no longer wraps itself into an unusable config** — when another notify wrapper (codex-computer-use) re-registered without recognizing Nebula's helper, it serialized the whole existing array into its own `--previous-notify` argument; Nebula wrapped that again, and the escaped backslashes doubled every round until `config.toml` reached 130 MB and Codex refused to start. The helper mark now counts as "already wired" wherever it appears — including inside a JSON string — so Nebula only heals its own path and never re-wraps. A serialized `notify` over 8 KB is refused outright as a backstop against any other form of inflation, and `nebula setup-ai --remove` persists `ai_hooks=0` so auto-wiring stays off across launches. (#38) +- **Tab context-menu shadow no longer thickens with the tab count (GPUI shell)** — the component library's context-menu extension keys its state by a fixed element id, so every tab row resolved to the same state and re-rendered the same open menu at the same anchor. The menu panel is opaque, but its shadow is not: with eight tabs the drop shadow was composited eight times. The tab menu now draws exactly once from the workspace root, like the file-tree menu. #### Improved @@ -78,6 +80,8 @@ Every release entry is provided in English and Simplified Chinese. - **Markdown 阅读器铺满阅读列(GPUI 壳)** — 段落内的软换行(README 手工折行的正文)此前渲染成硬换行,右侧留出一大片参差空白。现按 CommonMark 合并为空格,中日韩相邻行直接相连不插空格。 - **Markdown 图片真正能加载(GPUI 壳)** — 文档 tab 现在把相对图片路径按文档所在目录解析、绝对路径直接读盘、`http(s)` 图源经后台 ureq 客户端拉取(gpui 默认装的是空客户端),README 的 logo、shields 徽章(含 SVG)与截图终于都能显示。本地和网络的 GIF / 动画 WebP 会先压成单帧 PNG:gpui 在多帧图的帧下标被静态图复用时会直接 panic。 - **Markdown 里的原生 HTML 按 GitHub 语义渲染(GPUI 壳)** — README 常用的 HTML 写法现在与 GitHub 渲染一致:`

` / `

` / 标题上的 `align="center"` 让整块居中;同一段落里连续的 `` 徽章在同一行居中横排(此前一枚一行竖着摞);行内 `
` 按文档顺序断行(此前会被提到所跟文本之前);`` 链接徽章不再整个丢失——图片正常显示且带链接。 +- **Codex `notify` 不再自我包装成起不来的配置** — 另一个 notify 包装器(codex-computer-use)重新注册时若认不出 Nebula 的 helper,会把整个旧数组序列化进自己的 `--previous-notify` 参数;Nebula 再包一层,转义反斜杠每轮翻倍,最终把 `config.toml` 撑到 130 MB,Codex 直接起不来。现在只要 helper 标记出现在**任何位置**(含 JSON 字符串内部)就算已接线,Nebula 只自愈自己的路径、绝不再包。序列化后超过 8 KB 的 `notify` 一律拒写,兜住其他形态的膨胀;`nebula setup-ai --remove` 会持久化 `ai_hooks=0`,重启后也不再自动装回。(#38) +- **Tab 右键菜单阴影不再随标签数变厚(GPUI 壳)** — 组件库的右键菜单扩展用固定元素 id 存状态,于是每个标签行都解析到同一份状态、把同一个已打开的菜单重复画在同一锚点上。菜单面板不透明,阴影不是:8 个标签就叠 8 层投影。现在 Tab 菜单与文件树菜单一样,由 workspace 根只画一次。 #### 改进 diff --git a/README.md b/README.md index d3dae03..0f04a0f 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,16 @@ Windows/Linux paths, arrays, modules, and reload behavior. Questions and feedback: [fickleheartedkeys@163.com](mailto:fickleheartedkeys@163.com) +### 🙏 Acknowledgements + +Nebula's shell is built on **[GPUI](https://github.com/zed-industries/zed)** — the +GPU-accelerated UI framework developed by the [Zed](https://zed.dev) team — together with +**[gpui-component](https://github.com/longbridge/gpui-component)** from +[Longbridge](https://github.com/longbridge), which supplies the widget library. Both are +Apache-2.0 licensed and are used through lightly patched forks pinned in the workspace +manifest. Thank you for releasing this work as open source — Nebula's interface would not +look the way it does without it. + ### 📜 License Released under the [GNU General Public License v3.0](LICENSE). @@ -420,6 +430,15 @@ Nebula 内置 Lua 5.4,使用 `require 'nebula'` 与 问题与反馈:[fickleheartedkeys@163.com](mailto:fickleheartedkeys@163.com) +### 🙏 致谢 + +Nebula 的界面层构建在 **[GPUI](https://github.com/zed-industries/zed)** 之上——由 +[Zed](https://zed.dev) 团队开发的 GPU 加速 UI 框架——并使用 +[Longbridge](https://github.com/longbridge) 的 +**[gpui-component](https://github.com/longbridge/gpui-component)** 作为组件库。两者均以 +Apache-2.0 许可发布,Nebula 通过带少量补丁的 fork 使用,版本在 workspace 清单中精确锁定。 +感谢把这些工作开源——没有它,Nebula 现在的界面不会是这个样子。 + ### 📜 许可证 基于 [GNU 通用公共许可证 v3.0(GPL-3.0)](LICENSE) 发布。 diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 743ecb5..b302b51 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -29,6 +29,14 @@ The bundled conpty.dll and OpenConsole.exe come from the Windows Terminal project (github.com/microsoft/terminal), distributed under the MIT License. +The GPUI interface layer is built with gpui 0.2.2, developed by Zed +Industries (github.com/zed-industries/zed), and gpui-component 0.5.1, +developed by Longbridge (github.com/longbridge/gpui-component). Both are +distributed under the Apache License 2.0, whose text is reproduced below. +Nebula compiles modified forks of both crates; the fork baselines and the +applied patches are recorded in the workspace Cargo.toml under +[patch.crates-io]. + --------------------------------------------------------------------- Apache License diff --git a/docs/release-notes/v1.1.0.md b/docs/release-notes/v1.1.0.md new file mode 100644 index 0000000..eac5d5c --- /dev/null +++ b/docs/release-notes/v1.1.0.md @@ -0,0 +1,107 @@ +# Nebula Terminal 1.1.0 + +## English + +### Added + +- Added remote backup destinations — Push the password-protected archive to a local or network folder, WebDAV, S3-compatible storage, or SFTP, and restore the newest one. +- Added a popup completion list — Choose between inline ghost text and a popup list of history, directories, PATH commands, and file matches. +- Added a real color picker for the custom background — Pick continuously on a saturation/value plane with a hue bar and live preview. +- Added clipboard screenshot paste — Pasting an image-only clipboard writes a PNG and pastes its path, uploading it first on SSH panes. +- Added configurable new-tab placement — Place new tabs after the active tab or at the end of the list. Contributed by @Sakyvo in #25. +- Added configurable terminal cell width — Choose Compact or Relaxed cell width, applied immediately. Contributed by @Sakyvo in #26. +- Added ordered font fallback chains — `font_family` accepts comma-separated families, and later families fill missing glyphs in order. (#33) +- Added middle-click to close tabs. +- Added an audible terminal bell — BEL plays the system notification sound, throttled and optional. (#37) +- Added AI conversation resume across restarts — Restored panes retype the exact claude or codex resume command. +- Added a system tray icon with agent attention — The tray shows an amber dot when an AI CLI waits for input and jumps to that pane. +- Added line numbers for plain-text files — `.txt`, `.log`, `.json`, and `.jsonl` now open in the code viewer. +- Added clickable file and URL links — Hovering shows a preview, and Ctrl+click opens the path or URI. +- Added a configurable terminal bell — Choose Off, Flash, Sound, or Flash + sound. +- Added a WYSIWYG font picker and a folder-picker startup directory. +- Added the GPUI window as the default in 1.1.0 packages — Pass `--legacy-shell` for the previous UI. + +### Fixed + +- Fixed "Open in Nebula" always starting a separate window instead of joining the running instance. +- Fixed imported terminals not being selectable in the default-shell dropdown. +- Fixed the Git panel blanking on repositories owned by another user. +- Fixed visible windows getting stuck without repainting. (#21, #32) +- Fixed "Open in Nebula" failing on drive roots such as `D:\`. (#36) +- Fixed multi-line paste interrupting codex and other bracketed-paste applications. (#35) +- Fixed formulas not rendering when an AI CLI removed the delimiter backslashes. +- Fixed the Markdown reader leaving the right side of the reading column ragged. +- Fixed Markdown images not loading, including remote badges and screenshots. +- Fixed raw HTML in Markdown not matching GitHub's centering, badge rows, and linked images. +- Fixed Codex `notify` growing `config.toml` to 130 MB when two notify wrappers wrapped each other. (#38) +- Fixed the tab context-menu shadow getting thicker with each open tab. + +### Improved + +- Improved File/Git side-panel refreshes — Snapshots are built on a worker thread, so the view stays usable. +- Improved tab disclosure and file-tree menus — Linear chevrons replace Nerd Font glyphs, and menus no longer pick up the drawer shadow. + +## 中文 + +### 新增 + +- 新增远程备份目的地 — 把口令保护的归档推送到本地或网络目录、WebDAV、S3 兼容存储或 SFTP,并可恢复最新一份。 +- 新增弹窗补全列表 — 在行内灰字与弹窗列表之间选择,弹窗汇总历史命令、常用目录、PATH 命令与文件匹配。 +- 新增真正的背景色调色盘 — 在饱和度/明度取色面与色相条上连续取色,终端实时预览。 +- 新增剪贴板截图粘贴 — 剪贴板只有图片时,粘贴会写出 PNG 并粘出路径;SSH 会话先上传再敲入远端路径。 +- 新增可配置的新标签页插入位置 — 新标签可以插在当前标签之后或列表末尾。由 @Sakyvo 在 #25 中贡献。 +- 新增可配置的终端单元格宽度 — 可选“紧凑”或“宽松”,切换后立即生效。由 @Sakyvo 在 #26 中贡献。 +- 新增有序字体 fallback 链 — `font_family` 支持逗号分隔的字体族,后续字体族按顺序补齐缺失字形。(#33) +- 新增中键关闭标签页。 +- 新增终端铃声 — BEL 会播放系统提示音,带节流且可关闭。(#37) +- 新增 AI 对话跨重启接续 — 恢复出的终端会自动重敲 claude 或 codex 的 resume 命令。 +- 新增托盘图标 agent 提醒 — AI CLI 等待输入时托盘显示橙点,点击直达该终端。 +- 新增纯文本文件行号 — `.txt`、`.log`、`.json`、`.jsonl` 现在用代码查看器打开。 +- 新增可点击的文件与 URL 链接 — 悬停显示预览,Ctrl+点击打开路径或链接。 +- 新增可配置的终端铃声 — 可选关、闪烁、声音、闪烁 + 声音。 +- 新增所见即所得字体选择器与文件夹式启动目录选择。 +- 新增 1.1.0 安装包默认进入 GPUI 主窗 — 旧界面用 `--legacy-shell` 启动。 + +### 修复 + +- 修复“在 Nebula 中打开”总是新开独立窗口、不并入已运行实例的问题。 +- 修复导入的终端在默认 Shell 下拉中无法选中的问题。 +- 修复 Git 面板在他人所有的仓库上空白的问题。 +- 修复可见窗口卡住不重绘的问题。(#21、#32) +- 修复在 `D:\` 这类盘符根目录下“在 Nebula 中打开”报错的问题。(#36) +- 修复多行粘贴打断 codex 等 bracketed paste 应用的问题。(#35) +- 修复 AI CLI 去掉定界符反斜杠后公式不渲染的问题。 +- 修复 Markdown 阅读器右侧参差、铺不满阅读列的问题。 +- 修复 Markdown 图片无法加载的问题,包括网络徽章与截图。 +- 修复 Markdown 中原生 HTML 的居中、徽章横排与链接图片与 GitHub 不一致的问题。 +- 修复两个 notify 包装器互相包装、把 Codex 的 `config.toml` 撑到 130 MB 的问题。(#38) +- 修复标签右键菜单阴影随打开的标签数逐层变厚的问题。 + +### 改进 + +- 改进文件/Git 侧栏刷新 — 快照在工作线程中生成,刷新期间视图仍可使用。 +- 改进标签折叠控件与文件树菜单 — 改用线性箭头替代 Nerd Font 字形,菜单不再叠上抽屉阴影。 + +## Contributors + +- @Kuddev +- @Sakyvo:可配置新标签页插入位置(#25)、可配置终端单元格宽度(#26)。 + +Thanks for the reports / 感谢反馈:@Lawwaikit (#21)、@yellatiamo (#32)、 +@Mr-Second (#35)、@Mikachu2333 (#36)、@Andyear (#37)、@Pancakefog (#38)。 + +## Reporting issues / 反馈问题 + +Bug reports and feature ideas are welcome at +[Issues → New issue](https://github.com/Kuddev/nebula/issues/new/choose) — +the templates ask for the version, Windows build, and the shell or AI CLI involved. + +欢迎通过 [Issues → New issue](https://github.com/Kuddev/nebula/issues/new/choose) +提交问题与建议,模板会询问版本、Windows 版本和涉及的 shell 或 AI CLI。 + +--- + +**SHA256** + +- `NebulaTerminal-v1.1.0-windows-x64.zip`: `1213FB61F5DEAD02A0442E77801D9A2E92382F18E26B1E77E0A0BE366F8B6FD7` +- `NebulaTerminal-1.1.0-windows-x64-setup.exe`: `492A79808042FE88D054366A5586B0E44AD205B3E3BEC024FB6C44FC78AE823F` diff --git a/nebula_app/src/gpui_shell/workspace.rs b/nebula_app/src/gpui_shell/workspace.rs index 7458a20..ba4f6b6 100644 --- a/nebula_app/src/gpui_shell/workspace.rs +++ b/nebula_app/src/gpui_shell/workspace.rs @@ -767,6 +767,9 @@ pub struct NebulaWorkspace { side_panel_anim_armed: bool, /// 文件树右键:画在 workspace 根上,不进抽屉子孙树。见 `file_tree.rs`。 file_tree_menu: Option, + /// 标签右键:同样画在根上。挂进标签行会让每一行都渲染同一份菜单, + /// popover 阴影按标签数叠厚。见 `tab_menu.rs` 模块头。 + tab_menu: Option, /// 复用旧壳随包分发的 AI 品牌图,不用近似字体图标替代。 sidebar_logo_images: HashMap<(crate::display::AiLogo, bool), Arc>, /// 品牌图缓存对应的整数物理像素边长;窗口跨 DPI 显示器时据此重建。 @@ -908,6 +911,7 @@ impl NebulaWorkspace { side_panel_polling: false, side_panel_anim_armed: false, file_tree_menu: None, + tab_menu: None, sidebar_logo_images: sidebar_logo_images(sidebar_logo_target_px), sidebar_logo_target_px, _appearance_sub: appearance_sub, @@ -3898,8 +3902,6 @@ impl NebulaWorkspace { }, _ => None, }); - let workspace = cx.entity().downgrade(); - let context_workspace = workspace.clone(); let hover_group: SharedString = format!("sidebar-tab-hover-{ix}").into(); let shell_tag = (is_terminal && activity == SidebarActivity::Idle) .then(|| self.meta(ix).shell_tag) @@ -4152,9 +4154,16 @@ impl NebulaWorkspace { ), ), ) - .context_menu(move |menu, window, cx| { - Self::tab_context_menu(menu, context_workspace.clone(), ix, window, cx) - }); + // 右键只记锚点:菜单由 workspace 根上唯一一份宿主画。挂 + // `.context_menu()` 会让每个标签行都渲染同一个 PopupMenu, + // 阴影按标签数叠厚(见 `tab_menu.rs` 模块头)。 + .on_mouse_down( + MouseButton::Right, + cx.listener(move |this, event: &MouseDownEvent, window, cx| { + cx.stop_propagation(); + this.open_tab_context_menu(ix, event.position, window, cx); + }), + ); if dragged { // 骑指针 + 提到最上层画(deferred 只延后绘制、不动布局), // 阴影给"拿起来"的抬升感。 @@ -4350,6 +4359,8 @@ impl NebulaWorkspace { /// 列表锁成一行滚动区。 fn toggle_tabs_section(&mut self, cx: &mut Context) { self.tabs_section_collapsed = !self.tabs_section_collapsed; + // 卷帘一动,菜单锚定的那一行就不在原处了。 + self.tab_menu = None; self.tabs_fold_armed = true; self.tabs_fold_seq = self.tabs_fold_seq.wrapping_add(1).max(1); let seq = self.tabs_fold_seq; @@ -5143,6 +5154,7 @@ impl Render for NebulaWorkspace { .when_some(self.render_file_tree_context_menu(), |root, menu| { root.child(menu) }) + .when_some(self.render_tab_context_menu(), |root, menu| root.child(menu)) // 组件库的模态/通知层不会自己上屏:`Root::render` 只画宿主视图, // dialog/notification 两层由宿主显式挂。挂在最外层链尾=盖住命令 // 面板和所有拖拽罩层;dialog 在下、notification 在上,确认框弹着 diff --git a/nebula_app/src/gpui_shell/workspace/tab_menu.rs b/nebula_app/src/gpui_shell/workspace/tab_menu.rs index 4e80dc3..79826ec 100644 --- a/nebula_app/src/gpui_shell/workspace/tab_menu.rs +++ b/nebula_app/src/gpui_shell/workspace/tab_menu.rs @@ -3,10 +3,27 @@ //! 「分叉 AI 会话」必须在菜单打开当下按 hook 身份 + 官方 fork 语法重算, //! 不能吃侧栏上一帧的快照——否则刚接到 session_id、或冷恢复种下身份后 //! 右键仍看不到这一行。 +//! +//! # 为什么不用 `ContextMenuExt::context_menu()` +//! +//! 2026-08-18:那个扩展会把 `ContextMenu` 包在被挂载元素的**外面**,而它的 +//! `ElementId` 在 fork 里是硬编码的 `"context-menu"` +//! (`crates/ui/src/menu/context_menu.rs`)。包裹层的 id 先于内层行 id 入 +//! `GlobalElementId` 栈,于是每个标签行的 `ContextMenu` 算出来的 id 路径完全 +//! 相同、共享同一份 element state(含那个 `Rc>`)。 +//! 菜单一打开,`open == true` 对**所有**标签行同时成立,每一行都渲染同一个 +//! `PopupMenu` entity、落在同一个锚点上。面板底不透明,叠 N 次看不出来; +//! popover 阴影是半透明的,alpha 叠 N 层——这就是「标签越多,右键菜单阴影 +//! 越厚」的全部原因,与打包资源和用户配置都无关。 +//! +//! 所以这里跟 `file_tree.rs` 用同一套载体:标签行只记锚点和下标,菜单由 +//! workspace 根上唯一一份 `deferred(anchored)` 画,组件自带的 popover 阴影 +//! 因此保持单层。 use gpui::{ - App, InteractiveElement as _, MouseButton, ParentElement as _, Styled as _, Window, div, - prelude::FluentBuilder as _, px, + AnyElement, App, Context, Corner, DismissEvent, Entity, Focusable as _, + InteractiveElement as _, IntoElement as _, MouseButton, ParentElement as _, Pixels, Point, + Styled as _, Subscription, Window, anchored, deferred, div, prelude::FluentBuilder as _, px, }; use gpui_component::menu::PopupMenuItem; @@ -19,6 +36,17 @@ use super::{ CloseActiveTerminal, NebulaWorkspace, RenameActiveTab, SplitDown, SplitRight, WorkspaceTab, }; +/// 标签右键菜单的宿主。画在 workspace 根上,不进标签行的子孙树——理由见 +/// 模块头。 +pub(super) struct TabContextMenu { + menu: Entity, + position: Point, + /// 菜单里每条命令捕获的都是下标。标签集合在菜单挂着的时候变了(远端 + /// 或 AI 侧自动关标签),这一份就不该再画。 + ix: usize, + _subscription: Subscription, +} + /// 旧壳 `sync_chrome_tabs`:只有 Default / 检测 Shell 的 tab 才允许分叉。 /// Profile 可能直接启动 agent;SSH 会把命令打进认证提示。 pub(super) fn tab_launch_allows_ai_fork(launch: Option<&LaunchSession>) -> bool { @@ -38,36 +66,57 @@ pub(super) fn tab_ai_fork_enabled(workspace: &NebulaWorkspace, ix: usize, cx: &A } impl NebulaWorkspace { - /// 右键打开当下现查 fork 资格,并先选中该行(旧壳 chrome 同惯例)。 - pub(super) fn tab_context_menu( - menu: PopupMenu, - workspace: gpui::WeakEntity, + /// 右键:先选中该行(旧壳 chrome 同惯例),再按当下的 hook 身份现查 + /// fork 资格,最后把菜单交给根上那一份宿主。 + pub(super) fn open_tab_context_menu( + &mut self, ix: usize, + position: Point, window: &mut Window, - cx: &mut App, - ) -> PopupMenu { - let (terminal, ai_fork, color) = workspace - .upgrade() - .map(|entity| { - entity.update(cx, |this, cx| { - if ix < this.tabs.len() { - this.activate_tab(ix, window, cx); - } - ( - this.tabs.get(ix).is_some_and(WorkspaceTab::is_terminal), - tab_ai_fork_enabled(this, ix, cx), - this.meta(ix).color, - ) - }) - }) - .unwrap_or((false, false, None)); - Self::tab_popup_menu( - menu.external_link_icon(false), - workspace, - ix, - terminal, - ai_fork, - color, + cx: &mut Context, + ) { + if ix >= self.tabs.len() { + return; + } + self.activate_tab(ix, window, cx); + let terminal = self.tabs.get(ix).is_some_and(WorkspaceTab::is_terminal); + let ai_fork = tab_ai_fork_enabled(self, ix, cx); + let color = self.meta(ix).color; + let workspace = cx.entity().downgrade(); + let menu = PopupMenu::build(window, cx, move |menu, _window, _cx| { + Self::tab_popup_menu( + menu.external_link_icon(false), + workspace, + ix, + terminal, + ai_fork, + color, + ) + }); + menu.focus_handle(cx).focus(window); + let subscription = cx.subscribe_in(&menu, window, |this, _, _: &DismissEvent, _, cx| { + this.tab_menu = None; + cx.notify(); + }); + self.tab_menu = Some(TabContextMenu { menu, position, ix, _subscription: subscription }); + cx.notify(); + } + + pub(super) fn render_tab_context_menu(&self) -> Option { + let state = self.tab_menu.as_ref()?; + if state.ix >= self.tabs.len() { + return None; + } + Some( + deferred( + anchored() + .position(state.position) + .snap_to_window_with_margin(px(8.0)) + .anchor(Corner::TopLeft) + .child(state.menu.clone()), + ) + .with_priority(1) + .into_any_element(), ) }