Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
76bd8f9
chore(linux-egui): advance the package revision to 103
aeoform Sep 27, 2026
b20c027
docs: add the Beta.3-103 device acceptance checklist
aeoform Sep 27, 2026
96ac68e
fix(linux): stop the fcitx5 startup barrier failing on a session-mana…
aeoform Sep 27, 2026
6baa1e0
docs: record the session-managed fcitx5 case in the acceptance checklist
aeoform Sep 27, 2026
8b97599
chore(linux-egui): advance the package revision to 104
aeoform Sep 27, 2026
ec07236
chore(linux-egui): keep clippy quiet on the declared toolchains
aeoform Sep 27, 2026
c6a2a59
fix(linux-egui): degrade the UI snapshot instead of blinking the window
aeoform Sep 27, 2026
29c48fa
feat(linux-egui): page large frontend collections
aeoform Sep 27, 2026
3700ac0
ci: align Linux egui workflow name
aeoform Sep 27, 2026
ad9f60e
fix: degrade gracefully when fcitx5 is unavailable
aeoform Sep 27, 2026
cfa12b4
chore: remove unused Linux desktop helpers
aeoform Sep 27, 2026
38b4bdd
fix: keep Linux popup panels inside X11 work area
aeoform Sep 27, 2026
ed3b000
fix: match Linux capsule styles to Tauri geometry
aeoform Sep 27, 2026
00fcf4d
fix: keep typeless capsule at the OpenLess size
aeoform Sep 27, 2026
9869466
refactor: remove Linux egui OpenGL rendering
aeoform Sep 27, 2026
2a5f529
fix(linux-egui): remove standalone startup error window
aeoform Sep 27, 2026
4932d88
fix(linux-egui): pad Siri WGPU uniform buffer
aeoform Sep 27, 2026
e2f3951
fix(linux-egui): restore transparent Siri rendering and previews
aeoform Sep 27, 2026
a3a2de0
fix(linux-egui): match Siri stage size and spectral colors
aeoform Sep 27, 2026
7e02f2a
fix(linux-egui): animate Siri preview and show thinking label
aeoform Sep 27, 2026
5fe0e3a
perf(linux-egui): stop wgpu reserving ~180 MiB of VRAM per process
aeoform Sep 27, 2026
6a646a4
fix(linux-egui): assert the Vulkan runtime deps in the release contract
aeoform Sep 27, 2026
99661c6
fix(linux-egui): keep the device descriptor test clippy-clean
aeoform Sep 27, 2026
61420b3
fix(core): prune history with the injected clock, not the wall clock
aeoform Sep 27, 2026
23275d5
Merge remote-tracking branch 'origin/beta' into feat/linux-egui-follo…
aeoform Sep 28, 2026
2387882
fix(linux-egui): bound the UI snapshot inbox, stop the 5.8 GB freeze
aeoform Sep 28, 2026
9f2a0c9
feat(linux-egui): match the Tauri AI-services page
aeoform Sep 28, 2026
402ce6e
feat(linux-egui): embedded channel editor, flush settings rail, right…
aeoform Sep 28, 2026
7d569f0
feat(linux-egui): auto-save the channel editor and add channels throu…
aeoform Sep 28, 2026
4a08a8f
fix(linux-egui): the channel editor must not hijack the settings sheet
aeoform Sep 28, 2026
2c0d28b
feat(linux-egui): 思考光点的转速跟着流式输出速率走
aeoform Sep 28, 2026
d35540e
feat(linux-egui): 撤掉转速功能,逐项对齐 Siri 胶囊的入场/收尾节奏
aeoform Sep 28, 2026
1529eba
test(linux-egui): 把「窗口不排空 + 保活快照」的 5.8 GB 事故钉成回归测试
aeoform Sep 30, 2026
a34f57c
fix(linux-egui): 首选麦克风不再把音响/HDMI 监听列进来
aeoform Sep 30, 2026
fb59afd
Merge remote-tracking branch 'origin/beta' into feat/linux-egui-follo…
aeoform Sep 30, 2026
c546be6
chore(linux-egui): 跟上 Beta.4 的版本线与文案
aeoform Sep 30, 2026
bafc10d
feat(linux-egui): 对齐 Beta.4 的确认式词库学习(胶囊里的「要记住这个词吗」卡片)
aeoform Sep 30, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/check-linux-egui.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Linux egui build and backend checks
name: Linux Core, egui and deb/rpm checks

# CI and independent manual Linux builds share this deb/rpm verification path.
# No standalone push trigger: a Linux PR must show one combined Linux check.
Expand Down
12 changes: 7 additions & 5 deletions docs/linux-egui-dependency-upgrade.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Linux egui 依赖升级与 Vulkan 渲染

更新:2026-09-24。以 `linux-egui/Cargo.toml`、Cargo.lock 和当前源码为准;旧 Pi 记录和此前暂缓计划只作为线索。
更新:2026-09-27。以 `linux-egui/Cargo.toml`、Cargo.lock 和当前源码为准;旧 Pi 记录和此前暂缓计划只作为线索。

## 已完成

Expand All @@ -9,12 +9,14 @@
- Siri 波形、圆点和选区助手环改用 egui 原生图形绘制,避免 Glow shader callback 在 WGPU 窗口中被忽略后退化成普通音量条。
- 0.36 破坏性 API 已适配:`App::ui`、`Context::run_ui`、`CentralPanel::show(&mut Ui)`、文本编辑框 frame、UI 样式按主题设置,以及 Glow layer-shell 纹理增量可变传递。
- Cargo.lock 已解析升级 62 个兼容依赖;包括 eframe/egui 图形栈、accesskit、wgpu/Naga 等。其余直接依赖未为追新而跨主版本改动。
- Wayland wlr-layer-shell 胶囊面已从 `glutin`/EGL + `egui_glow::Painter` 迁到 wgpu/Vulkan(`popup_layer.rs` 自建 surface、presentation 与帧同步),OpenGL 渲染路径整体移除;`glutin`、`egui_glow` 及其 EGL 依赖不再出现在二进制里。
- 包依赖随之从 `libegl1` / `libwayland-egl1`(rpm: `libglvnd-egl` / `libwayland-egl.so.1`)换成 `libvulkan1`(rpm: `vulkan-loader`),并 `Recommends: mesa-vulkan-drivers`。契约测试 `linux-egui-release-contract.test.mjs` 会同时断言 Vulkan loader 在依赖里、EGL 依赖不再回来。

## 仍存在的 GL 路径
## 渲染路径:全部 Vulkan

自建的 Wayland wlr-layer-shell 胶囊面仍通过 `glutin`/EGL 和 `egui_glow::Painter` 提交 epaint 图元。这是独立于 eframe 主窗/普通弹窗的 surface 实现;Siri 效果本身已不再依赖 GL shader。X11 overlay 也保留现有窗口管理器路径。
主窗、普通弹窗、layer-shell 胶囊现在共用 `src/wgpu_device.rs::device_descriptor()`:Vulkan-only 后端、`max_texture_dimension_2d = 8192`、`MemoryHints::MemoryUsage`。

若要让 layer-shell surface 也由 WGPU/Vulkan 提交,需要将 `popup_layer.rs` 的手工 Wayland surface、presentation 和帧同步整体改成 wgpu surface;这不是 egui 升级的 API 适配,需另行实现与设备验证。
最后一项是必须的:wgpu 28 起 `MemoryHints` 默认 `Performance`,wgpu-hal 会按 device 128–256 MB 的内存块向驱动申请,每个渲染进程因此固定多占约 180 MiB(宿主、主窗、每个弹窗各一份)。本机 GTX 1660 SUPER 实测:胶囊 460×180 从 198 MiB 降到 18 MiB,开设置面板的主窗从 209 MiB 降到 47 MiB,帧率不变(29.8 fps / 72.7 fps)。

## 当前验证与限制

Expand All @@ -28,5 +30,5 @@

1. 在带 Vulkan 硬件驱动的 Wayland 与 X11 主机启动主窗及三个弹窗,确认日志枚举到硬件 Vulkan adapter。
2. 检查 Siri 录音波形、处理动画和 QA 录音环在透明 surface 上的颜色、裁剪、帧率与显存/CPU 占用。
3. 检查 LayerShell 胶囊的 EGL surface、窗口位置、焦点与音频动画;当前这条 surface 仍走 GL。
3. 检查 LayerShell 胶囊的位置、透明背景、焦点与音频动画,确认 Vulkan surface 换帧与 `PreMultiplied` alpha 正常。
4. 在无 Vulkan loader 或 adapter 时确认故障提示清晰。目前选择的是 Vulkan-only,启动时没有自动回退 GL。
49 changes: 48 additions & 1 deletion docs/linux-egui-handoff/07-acceptance.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 07:验收与证据

状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-08。
状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-27。

## 1. 两个不同的完成门

Expand Down Expand Up @@ -28,3 +28,50 @@
## 4. 记录格式

`ID / owner / commit / 已完成效果 / 自动证据 / 设备证据 / 剩余限制`。“待实测”不是“未实现”,也不是“已完成”;验收报告按证据分级表述(见[范围](../2.0-requirements.md)第 4 节)。

## 5. 本轮(Beta.3-103)设备验收清单

本轮改动集中在启动/运行时/保存三条链(`fcitx5.rs`、`ui/bridge.rs`、`linux_app/{window,settings_save,history}.rs`、`preference_patch.rs`),以及上一轮的 Tauri 视觉对齐项。逐项结果按第 4 节格式记录,未实测的不要写成已验证。

### 5.1 fcitx5 成为启动硬依赖(`fcitx5.rs::prepare_fcitx5`)

就绪的判据是**插件接口能应答**,不是 fcitx5 进程被替换。桌面会话(KWin 拉起 `fcitx5-wayland-launcher --reopen`)会让 fcitx5 报告 `CanRestart=false`:此时 `Restart` 调用会“成功”但进程不替换,103 包因此每次启动都卡满 15 秒并报错。104 起该状态只记警告并继续使用正在运行的副本,安装的副本等 fcitx5 下次启动时生效。

| 场景 | 期望 |
| --- | --- |
| 插件已装、fcitx5 运行 | 启动不阻塞(15 秒就绪预算内),热键可用 |
| 插件缺失/未启用 | 弹出错误窗口(`startup.fcitx_help` 文案);**关闭后进程以非零状态退出并释放单实例锁**,可立即重新启动 |
| 启动后才 `fcitx5 -r` 重启 | 插件重载完成后热键仍可用(验证就绪等待是否覆盖慢重启) |
| 刚升级过插件、且 fcitx5 由会话/合成器托管(`CanRestart=false`) | 启动**不得**因重启失败而卡满 15 秒或弹错;日志写明保留正在运行的副本,热键仍可用(旧 103 包在此状态下每次启动都失败,见下) |
| 无会话 D-Bus(ssh/`systemd-run` 启动) | 明确失败并给出修复指引,不得静默降级为全局热键 |

### 5.2 UI 子进程协议 v2(`ui/bridge.rs`)

| 场景 | 期望 |
| --- | --- |
| 关闭 UI 主窗后从托盘/启动器重开 | 宿主与后台能力(热键、录音、托盘)持续存活;重开后重新握手并收到全量快照,历史/设置不丢 |
| UI 进程被 `kill -9` | 宿主不崩;下次开窗握手正常 |
| 状态高频变化(历史、录音电平)时 UI 较慢 | 显示的是最新状态,不出现回退到旧快照 |
| 误接协议不匹配的 UI 二进制 | 回 `Rejected` 并断开,而不是渲染半懂数据 |

### 5.3 设置保存改为字段补丁(`linux_app/settings_save.rs`、`preference_patch.rs`)

| 场景 | 期望 |
| --- | --- |
| 快速连改多个开关/滑条 | 不出现“改 A 丢 B”;最终值与最后一次操作一致 |
| 制造 revision 冲突(另一端同时改设置) | 冲突被重试(≤3 次)后正确落盘;仍失败时保留草稿并显式提供重试,不打开设置页 |

### 5.4 历史缓存与删除身份(`linux_app/history.rs`)

| 场景 | 期望 |
| --- | --- |
| 删除历史条目 | 删除的是点中的那条(不再因刷新错位);列表与概览计数同步 |
| 历史条目很多时打开概览/设置 | 首屏不卡顿(读取与 WAV 探测在后台任务,两条页面共享缓存) |

### 5.5 回归:上一轮 Tauri 视觉对齐项

OAuth 弹窗(加宽、标题字号、居中状态行、整行验证码框、关闭按钮悬停不缩放)、风格页卡片(图标选择器贴右、铅笔/重置徽标、名称/描述/标签行)、市场“我的发布”上传/更新/下架流程。

### 5.6 安装与升级

`dpkg -i` 用 103 覆盖 102(apt 视为升级,不回退);rpm 同理;`apt remove` 后无残留(fcitx5 postinst/postrm 行为);核对 `openless --version` 自报 `OpenLess 2.0.0-Beta.3-103`。
3 changes: 0 additions & 3 deletions openless-all/app/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions openless-all/app/crates/openless-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2522,6 +2522,10 @@ impl OpenLessBackend {
));
}
let events = Arc::new(EventBus::new(256));
// 保留裁剪与「最近 N 分钟」必须跟着注入的时钟走:同一个后端里
// `created_at` 已经用 `clock` 生成,裁剪却偷读系统时钟的话,测试/
// 回放的固定时钟下刚写入的条目会在写入瞬间被判超期。
repositories.history.attach_clock(Arc::clone(&clock));
let preferences_revision = Arc::new(AtomicU64::new(0));
let style_pack_revision = Arc::new(AtomicU64::new(0));
let history_revision = Arc::new(AtomicU64::new(0));
Expand Down
75 changes: 70 additions & 5 deletions openless-all/app/crates/openless-core/src/history.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! Newest-first dictation history with retention and count caps.

use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::{Arc, Mutex};

use crate::config::{Clock, SystemClock};
use crate::errors::{BackendError, BackendErrorCode};
use crate::persistence::{atomic_write, persistence_error};
use crate::types::{DictationSession, HistorySource};
Expand All @@ -12,6 +13,12 @@ pub const HISTORY_CAP: usize = 200;
pub struct HistoryStore {
path: PathBuf,
lock: Mutex<()>,
/// 保留裁剪和「最近 N 分钟」都只认这一个时钟,而不是各自去读
/// `chrono::Utc::now()`:后者会让注入固定时钟的后端(测试、回放)里
/// `created_at` 与裁剪依据分成两套时间,条目可能在写入的瞬间就超期。
/// `BackendRepositories::open` 时还不知道时钟来源,所以由后端随后用
/// `attach_clock` 补上;没补就是系统时钟。
clock: Mutex<Arc<dyn Clock>>,
}

impl HistoryStore {
Expand All @@ -23,9 +30,19 @@ impl HistoryStore {
Self {
path,
lock: Mutex::new(()),
clock: Mutex::new(Arc::new(SystemClock)),
}
}

/// 把后端注入的时钟交给历史仓。同一个仓只会被后端组装一次。
pub(crate) fn attach_clock(&self, clock: Arc<dyn Clock>) {
*self.clock.lock().expect("history clock lock") = clock;
}

fn now_utc(&self) -> chrono::DateTime<chrono::Utc> {
self.clock.lock().expect("history clock lock").now_utc()
}

pub fn list(&self) -> Result<Vec<DictationSession>, BackendError> {
let _guard = self.lock_store()?;
self.read_locked()
Expand Down Expand Up @@ -66,7 +83,7 @@ impl HistoryStore {
let _guard = self.lock_store()?;
let mut sessions = self.read_locked()?;
sessions.insert(0, session);
retain_with_policy(&mut sessions, retention_days, max_entries);
retain_with_policy(&mut sessions, retention_days, max_entries, self.now_utc());
self.write_locked(&sessions)
},
)
Expand Down Expand Up @@ -95,7 +112,7 @@ impl HistoryStore {
} else {
sessions.insert(0, session);
}
retain_with_policy(&mut sessions, retention_days, max_entries);
retain_with_policy(&mut sessions, retention_days, max_entries, self.now_utc());
self.write_locked(&sessions)
},
)
Expand Down Expand Up @@ -123,7 +140,7 @@ impl HistoryStore {
}
let _guard = self.lock_store()?;
let sessions = self.read_locked()?;
let cutoff = chrono::Utc::now() - chrono::Duration::minutes(i64::from(minutes));
let cutoff = self.now_utc() - chrono::Duration::minutes(i64::from(minutes));
Ok(sessions
.into_iter()
.take_while(|session| {
Expand Down Expand Up @@ -206,13 +223,14 @@ fn retain_with_policy(
sessions: &mut Vec<DictationSession>,
retention_days: u32,
max_entries: Option<u32>,
now: chrono::DateTime<chrono::Utc>,
) {
// Quick notes are intentionally outside the ordinary history retention
// policy. A recording-start draft is also protected until it reaches a
// terminal state, because its audio may be the only recoverable artifact
// after a process crash.
if retention_days > 0 {
let cutoff = chrono::Utc::now() - chrono::Duration::days(i64::from(retention_days));
let cutoff = now - chrono::Duration::days(i64::from(retention_days));
sessions.retain(|session| {
session.source == HistorySource::QuickNote
|| session.error_code.as_deref() == Some("recording")
Expand Down Expand Up @@ -329,6 +347,53 @@ mod tests {
let _ = std::fs::remove_file(path);
}

#[test]
fn retention_follows_the_injected_clock_instead_of_the_wall_clock() {
// CI 2026-09-27 之后这个场景一直是红的:裁剪偷读 `chrono::Utc::now()`,
// 于是一个 injected 固定时钟(2026-08-28)+ retention 30 天的后端,会把
// 刚写入的条目当成过期档删掉。
let path = std::env::temp_dir().join(format!(
"openless-history-clock-{}.json",
uuid::Uuid::new_v4().simple()
));
let store = HistoryStore::at_path(path.clone());
let fixed = chrono::DateTime::parse_from_rfc3339("2026-08-28T12:34:56+00:00")
.unwrap()
.with_timezone(&chrono::Utc);
store.attach_clock(Arc::new(crate::testing::FixedClock::new(
fixed,
fixed.date_naive(),
)));

store
.append_with_retention(session("kept", fixed.to_rfc3339()), 30, Some(20))
.unwrap();
assert_eq!(
store.list().unwrap().len(),
1,
"a fresh entry is never stale"
);
assert_eq!(
store.recent_within_minutes(5).unwrap().len(),
1,
"the polish context window reads the same clock"
);

// 功能本身不能被放宽:同一个固定时钟下超期的条目照旧被裁掉。
let stale = (fixed - chrono::Duration::days(31)).to_rfc3339();
store
.append_with_retention(session("stale", stale), 30, None)
.unwrap();
let ids: Vec<String> = store
.list()
.unwrap()
.into_iter()
.map(|entry| entry.id)
.collect();
assert_eq!(ids, vec!["kept".to_string()]);
let _ = std::fs::remove_file(path);
}

#[test]
fn update_delete_clear_and_recent_queries_are_stable() {
let path = std::env::temp_dir().join(format!(
Expand Down
3 changes: 0 additions & 3 deletions openless-all/app/linux-egui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ egui = "=0.36.2"
# 「贴底居中 + 不抢键盘焦点」的 overlay surface;不支持时回退到 X11 叠加层。
wayland-client = "0.31"
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
glutin = { version = "0.32", default-features = false, features = ["egl", "wayland"] }
egui_glow = "0.36.2"
glow = "0.17"
raw-window-handle = "0.6"
eframe = { version = "=0.36.2", default-features = false, features = ["accesskit", "default_fonts", "wgpu_no_default_features", "wayland", "x11"] }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "vulkan", "wgsl"] }
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/linux-egui/assets/lucide/ChevronLeft.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions openless-all/app/linux-egui/assets/lucide/Grip.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion openless-all/app/linux-egui/package-revision
Original file line number Diff line number Diff line change
@@ -1 +1 @@
102
129
Loading
Loading