` 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(),
)
}