Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/desktop-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Validate desktop changes before they reach the release-only workflow.
name: Desktop CI

on:
pull_request:
paths:
- "desktop/**"
- "src/frontend/**"
- ".github/workflows/desktop-ci.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: desktop-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
scripts:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install desktop toolchain
working-directory: desktop
run: npm ci --no-audit --no-fund

- name: Test release and CE payload helpers
working-directory: desktop
run: npm run test:scripts

platform-tests:
name: ${{ matrix.name }}
env:
NODE_OPTIONS: "--max-old-space-size=4096"
strategy:
fail-fast: false
matrix:
include:
- name: macOS Rust tests
os: macos-latest
prepare_payload: false
- name: Windows CE payload and Rust tests
os: windows-latest
prepare_payload: true

runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build
uses: Swatinem/rust-cache@v2
with:
workspaces: desktop/src-tauri -> target

- name: Install frontend dependencies
working-directory: src/frontend
run: npm install --no-package-lock --no-audit --no-fund

- name: Install desktop toolchain
working-directory: desktop
run: npm ci --no-audit --no-fund

- name: Build frontend for macOS tests
if: ${{ !matrix.prepare_payload }}
working-directory: src/frontend
run: npm run build

- name: Stage the public CE local-server payload on Windows
if: ${{ matrix.prepare_payload }}
working-directory: desktop
run: node scripts/prepare-bundle.mjs

- name: Run desktop Rust tests
run: cargo test --manifest-path desktop/src-tauri/Cargo.toml
11 changes: 6 additions & 5 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ HUGAGENT_SERVER_BASE=https://你的后端 npm run dev
| `src-tauri/src/auth.rs` | token 落盘 + handoff 票据 redeem |
| `src-tauri/src/config.rs` | server.json / 环境变量 / 默认值;`save_server_base` 写回 |
| `src-tauri/src/local_server.rs` | 本机服务安装、版本检测、进程托管、健康检查、进度与日志状态 |
| `src-tauri/src/menu.rs` | 顶部原生菜单栏(文件/编辑/视图/帮助)构建 + 事件分发 |
| `src-tauri/src/menu.rs` | 平台菜单构建 + 事件分发;macOS 使用系统菜单栏,Windows/Linux 使用窗口内菜单 |
| `src-tauri/src/notify.rs` | **A1** 后台通知轮询 → 原生系统通知(接后端 `automations/notifications/list`) |
| `src-tauri/src/update.rs` | **A3** 一键自动更新:拉后端 manifest → 验签 → 安装 → 重启 |
| `src-tauri/tauri.conf.json` | 窗口/打包/deep-link scheme/资源/**updater 配置(pubkey + endpoints)** |
Expand All @@ -151,14 +151,15 @@ HUGAGENT_SERVER_BASE=https://你的后端 npm run dev
`chatStream.ts` 全部对话能力,零重复实现)。未登录时退化为唤起主窗。
- **A3 一键自动更新**(`update.rs`):菜单「帮助 → 检查更新…」或托盘触发,见下方《自动更新》。
- **A4 托盘增强**:托盘菜单新增「新建对话」「检查更新…」。
- **原生菜单栏**(`menu.rs`):顶部「文件 / 编辑 / 视图 / 帮助」。文件=新建对话·设置服务器地址·退出;
编辑=撤销/剪切/复制/粘贴/全选(系统预定义,直接作用于焦点输入框);视图=重新加载·全屏;
帮助=检查更新·访问官网·关于
- **平台化菜单与标题栏**(`menu.rs` + `proxy.rs`):Windows/Linux 保留紧凑的一体化窗口菜单;macOS
使用系统菜单栏和左侧原生交通灯,窗口内只保留半透明工具栏及「新建对话」「服务设置」两个 Mac
风格图标按钮,不显示 Windows 式右侧最小化、最大化和关闭按钮
- **设置服务器地址 UI**(菜单「文件 → 设置服务器地址…」):填后端地址→写回 server.json→重启生效,
不再必须手改 JSON。
- **本机服务一键安装**(菜单「文件 → 本机服务…」):后端不可达时也会自动显示。安装过程提供
阶段进度、实时日志、失败重试和健康检查;客户端更新携带新 CE 资源时自动升级服务代码,
`data/` 目录保持不变。
`data/` 目录保持不变。远程模式下点击安装会先在当前页面完成安装,服务通过健康检查后才切换
`server.json` 并重启,避免提前重启造成按钮无响应或安装状态不可见。

> 交互全部走「原生菜单/托盘 → Rust」或「导航到 `/__desktop/*` 哨兵 → 导航守卫」,**不依赖 Tauri
> IPC**——因为前端跑在本地反代这个「远程源」上,`window.__TAURI__`/invoke 不保证注入。这是本壳
Expand Down
32 changes: 27 additions & 5 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,13 +573,25 @@ fn build_window(app: &tauri::AppHandle, url: &str) -> tauri::Result<()> {
let app_for_nav = app.clone();
let (width, height, min_width, min_height) = main_window_dimensions(app);

let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::External(parsed))
let builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::External(parsed))
.title(brand::NAME)
// 用反代注入的一体化标题栏替代「系统标题栏 + 原生菜单栏」两行。
.decorations(false)
.additional_browser_args(WEBVIEW_BROWSER_ARGS)
.inner_size(width, height)
.min_inner_size(min_width, min_height)
.min_inner_size(min_width, min_height);

// Windows/Linux keep the compact custom chrome. macOS uses native window
// decorations and traffic lights, with content extending into a translucent
// toolbar in the same visual hierarchy as Codex and other modern Mac apps.
#[cfg(target_os = "macos")]
let builder = builder
.decorations(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true)
.traffic_light_position(tauri::LogicalPosition::new(18.0, 18.0));
#[cfg(not(target_os = "macos"))]
let builder = builder.decorations(false);

let window = builder
.on_navigation(move |u| {
let scheme = u.scheme();
// Tauri 内部 / 数据类源放行。
Expand Down Expand Up @@ -733,7 +745,17 @@ fn build_window(app: &tauri::AppHandle, url: &str) -> tauri::Result<()> {

apply_display_zoom(&window);

// 主窗口不再挂原生菜单;文件/编辑/视图/帮助与产品名共用一体化标题栏。
// macOS application menus belong in the system menu bar. Windows/Linux use
// the in-window menu injected by proxy.rs to avoid a second chrome row.
#[cfg(target_os = "macos")]
match menu::build(app) {
Ok(menu) => {
if let Err(error) = window.set_menu(menu) {
eprintln!("[menu] 挂载 macOS 原生菜单失败: {error}");
}
}
Err(error) => eprintln!("[menu] 构建 macOS 原生菜单失败: {error}"),
}

Ok(())
}
Expand Down
132 changes: 95 additions & 37 deletions desktop/src-tauri/src/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,103 @@ use tauri_plugin_opener::OpenerExt;
use crate::brand;
use crate::Shared;

/// 原生菜单保留为平台回退;Windows 主窗口使用与标题同一行的 WebView 标题栏
/// macOS 使用系统应用菜单;Windows/Linux 主窗口使用与标题同一行的 WebView 菜单
#[allow(dead_code)]
pub fn build<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let file = SubmenuBuilder::new(app, "文件")
.text("new_chat", "新建对话")
.text("server_config", "设置服务器地址…")
.text("local_server", "本机服务…")
.separator()
.quit()
.build()?;

// 编辑:交给系统预定义项,直接作用于焦点输入框。
let edit = SubmenuBuilder::new(app, "编辑")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.select_all()
.build()?;

let view = SubmenuBuilder::new(app, "视图")
.text("reload", "重新加载")
.separator()
.fullscreen()
.build()?;

let about = AboutMetadataBuilder::new()
.name(Some(brand::NAME.to_string()))
.build();
let help = SubmenuBuilder::new(app, "帮助")
.text("check_update", "检查更新…")
.text("website", "访问官网")
.separator()
.about(Some(about))
.build()?;

Menu::with_items(app, &[&file, &edit, &view, &help])
#[cfg(target_os = "macos")]
{
let about = AboutMetadataBuilder::new()
.name(Some(brand::NAME.to_string()))
.build();
let application = SubmenuBuilder::new(app, brand::NAME)
.about(Some(about))
.separator()
.text("server_config", "设置…")
.text("check_update", "检查更新…")
.separator()
.services()
.separator()
.hide()
.hide_others()
.show_all()
.separator()
.quit()
.build()?;

let file = SubmenuBuilder::new(app, "文件")
.text("new_chat", "新建对话")
.text("local_server", "本机服务…")
.separator()
.close_window()
.build()?;

let edit = SubmenuBuilder::new(app, "编辑")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.select_all()
.build()?;

let view = SubmenuBuilder::new(app, "显示")
.text("reload", "重新加载")
.separator()
.fullscreen()
.build()?;

let window = SubmenuBuilder::new(app, "窗口")
.minimize()
.maximize()
.build()?;

let help = SubmenuBuilder::new(app, "帮助")
.text("website", "访问官网")
.build()?;

return Menu::with_items(app, &[&application, &file, &edit, &view, &window, &help]);
}

#[cfg(not(target_os = "macos"))]
{
let file = SubmenuBuilder::new(app, "文件")
.text("new_chat", "新建对话")
.text("server_config", "设置服务器地址…")
.text("local_server", "本机服务…")
.separator()
.quit()
.build()?;

// 编辑:交给系统预定义项,直接作用于焦点输入框。
let edit = SubmenuBuilder::new(app, "编辑")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.select_all()
.build()?;

let view = SubmenuBuilder::new(app, "视图")
.text("reload", "重新加载")
.separator()
.fullscreen()
.build()?;

let about = AboutMetadataBuilder::new()
.name(Some(brand::NAME.to_string()))
.build();
let help = SubmenuBuilder::new(app, "帮助")
.text("check_update", "检查更新…")
.text("website", "访问官网")
.separator()
.about(Some(about))
.build()?;

Menu::with_items(app, &[&file, &edit, &view, &help])
}
}

/// 菜单事件分发。托盘的同名动作也复用这里(见 `build_tray`)。
Expand Down
Loading
Loading