Repository navigation
feat: 支持 Windows、macOS 和 Linux 跨平台悬浮窗 - #1
Conversation
Use consistently sized PathIcon controls and refresh the dependency lock files for the macOS release build.
Reference version 0.21.3 explicitly and update the lock file to resolve GHSA-xrw6-gwf8-vvr9 and eliminate NU1903.
审查者指南本 PR 将原有 Windows WPF 监视器拆分为跨平台核心库,并以 Avalonia 提供 Windows、macOS 和 Linux 悬浮面板;同时加入平台适配的配置、自动启动和单实例通信,实现多架构便携构建、快照冒烟验证及自动发布,并扩展日志重写和核心回归测试覆盖。 单实例面板激活时序图sequenceDiagram
participant Launcher as Second launch
participant Instance as SingleInstance
participant Existing as Existing App
participant Panel as MainWindow
Launcher->>Instance: Create
Instance-->>Launcher: IsFirstInstance false
Launcher->>Instance: SignalExistingInstance
Instance->>Existing: Wake request
Existing->>Instance: ShowExistingPanel
Instance->>Panel: ShowPanel
Panel-->>Instance: Acknowledgment
Instance-->>Launcher: Return success
跨平台便携版快照验证时序图sequenceDiagram
participant CI as GitHub Actions
participant Build as Portable build script
participant App as CodexTpsFloat
participant Snapshot as SnapshotWriter
participant Scanner as UsageScanner
CI->>Build: publish runtime
Build-->>CI: portable artifact
CI->>App: --snapshot-json
App->>Snapshot: Write
Snapshot->>Scanner: Refresh
Scanner-->>Snapshot: UsageSnapshot
Snapshot-->>App: JSON file and exit code
App-->>CI: Smoke-test result
增量日志重写检测流程图flowchart TD
Refresh[UsageScanner.Refresh] --> Cursor[Load file cursor]
Cursor --> Match[TailMatches]
Match -->|matches| Append[ReadCompleteLines]
Match -->|mismatch or truncation| Reset[Clear cursors and usage]
Reset --> Rebuild[Refresh from rewritten file]
Append --> Snapshot[Updated UsageSnapshot]
文件变更
提示和命令与 Sourcery 交互
自定义使用体验访问你的控制面板以便:
获取帮助Original review guide in EnglishReviewer's Guide本 PR 将原有 Windows WPF 监视器拆分为跨平台核心库,并以 Avalonia 提供 Windows、macOS 和 Linux 悬浮面板;同时加入平台适配的配置、自动启动和单实例通信,实现多架构便携构建、快照冒烟验证及自动发布,并扩展日志重写和核心回归测试覆盖。 Sequence diagram for single-instance panel activationsequenceDiagram
participant Launcher as Second launch
participant Instance as SingleInstance
participant Existing as Existing App
participant Panel as MainWindow
Launcher->>Instance: Create
Instance-->>Launcher: IsFirstInstance false
Launcher->>Instance: SignalExistingInstance
Instance->>Existing: Wake request
Existing->>Instance: ShowExistingPanel
Instance->>Panel: ShowPanel
Panel-->>Instance: Acknowledgment
Instance-->>Launcher: Return success
Sequence diagram for cross-platform portable snapshot validationsequenceDiagram
participant CI as GitHub Actions
participant Build as Portable build script
participant App as CodexTpsFloat
participant Snapshot as SnapshotWriter
participant Scanner as UsageScanner
CI->>Build: publish runtime
Build-->>CI: portable artifact
CI->>App: --snapshot-json
App->>Snapshot: Write
Snapshot->>Scanner: Refresh
Scanner-->>Snapshot: UsageSnapshot
Snapshot-->>App: JSON file and exit code
App-->>CI: Smoke-test result
Flow diagram for incremental log rewrite detectionflowchart TD
Refresh[UsageScanner.Refresh] --> Cursor[Load file cursor]
Cursor --> Match[TailMatches]
Match -->|matches| Append[ReadCompleteLines]
Match -->|mismatch or truncation| Reset[Clear cursors and usage]
Reset --> Rebuild[Refresh from rewritten file]
Append --> Snapshot[Updated UsageSnapshot]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
你好——我发现了 14 个问题
给 AI 代理的提示
请处理此次代码审查中的评论:
## 各条评论
### 评论 1
<location path="src/Core/Autostart.cs" line_range="110" />
<code_context>
+ [Desktop Entry]
+ Type=Application
+ Name=Codex TPS
+ Exec="{executable}"
+ Terminal=false
+ X-GNOME-Autostart-enabled=true
</code_context>
<issue_to_address>
**登录时启动失败**
如果已安装的可执行文件路径包含桌面条目保留字符(例如引号、反斜杠、百分号或换行符),`LinuxAutostart.SetEnabled` 会将未转义的路径写入 `.desktop` 文件的 `Exec` 值,导致桌面解析器错误解析命令;如果路径中包含精心构造的引号和换行符,还可能在登录时执行注入的命令。
写入之前,请按照桌面条目 `Exec` 语法对可执行文件路径进行编码。
</issue_to_address>
### 评论 2
<location path="src/App/App.axaml.cs" line_range="41" />
<code_context>
+ await window.CaptureAndCloseAsync(Options.CapturePath);
+ // Avalonia can create a tray icon on macOS without macOS displaying it,
+ // so there is no reliable way to detect an unusable menu bar item.
+ else if (OperatingSystem.IsMacOS() || !window.HasTray)
+ window.ShowPanel();
+ };
</code_context>
<issue_to_address>
**面板无法访问**
如果 Linux 桌面没有可用的状态通知托盘宿主,但 Avalonia 仍创建了托盘对象,`HasTray` 会报告存在 `TrayIcon` 对象,因此 `App` 会跳过 `ShowPanel()`,尽管实际上没有显示托盘图标;用户没有可见的方式打开或退出应用。
托盘实际上不可用时,请提供可见的面板作为后备入口。
另请查看 `src/App/MainWindow.axaml.cs:41-42`、`src/App/MainWindow.axaml.cs:51`、`src/App/MainWindow.axaml.cs:62`。
</issue_to_address>
### 评论 3
<location path="src/Core/SingleInstance.cs" line_range="62" />
<code_context>
+ var mutex = new Mutex(true, scopedName, out bool first);
+ try
+ {
+ var wake = new EventWaitHandle(false, EventResetMode.AutoReset, scopedName + ".Show");
+ RegisteredWaitHandle? registration = null;
+ if (first && showExisting != null)
</code_context>
<issue_to_address>
**旧版面板仍保持隐藏**
用户在旧版本仍在运行时启动此 Windows 版本,`CreateWindows` 会共享旧互斥体名称,但发送信号到 `Local\CodexTpsFloat.Panel.v1.Show`,而旧版应用监听的是 `Local\CodexTpsFloat.Show.v1`;新进程随后作为次实例退出,旧面板也不会显示。
请保持唤醒事件名称与旧版 Windows 应用兼容,或以其他方式确保旧实例收到显示请求。
</issue_to_address>
### 评论 4
<location path="src/App/Program.cs" line_range="10" />
<code_context>
+
+public static class Program
+{
+ public static int Main(string[] args)
+ {
+ var options = CommandLineOptions.Parse(args);
</code_context>
<issue_to_address>
**Windows 选择器无法打开**
Windows 用户打开文件夹选择器或报告保存对话框时,`Program.Main` 会在默认的 MTA 线程上启动 Avalonia,因此基于 Windows COM 的选择器无法打开,用户也就无法选择 Codex 目录或导出报告。
请在 STA 线程上启动 Windows UI 线程。
</issue_to_address>
### 评论 5
<location path="src/App/Program.cs" line_range="20" />
<code_context>
+
+ using var singleInstance = SingleInstance.Create(
+ "CodexTpsFloat", () => App.ShowExistingPanel?.Invoke());
+ if (!singleInstance.IsFirstInstance && options.CapturePath == null)
+ {
+ singleInstance.SignalExistingInstance();
</code_context>
<issue_to_address>
**捕获进程覆盖较新的设置**
当捕获任务与普通应用会话同时运行,且普通会话在捕获进程读取设置后更改了设置时,即使已有另一个实例,`--capture` 进程仍会运行,并加载自己的一份设置副本。如果普通应用在捕获进程关闭前保存了更改,捕获窗口的关闭处理程序会将过时的设置副本写回,覆盖用户较新的设置。
请勿从仅用于捕获的窗口持久化设置;或者通过某种机制串行化写入,并拒绝过时的设置副本。
另请查看 `src/App/MainWindow.axaml.cs:491`、`src/Core/SettingsStore.cs:39-40`。
</issue_to_address>
### 评论 6
<location path="src/App/Program.cs" line_range="19-20" />
<code_context>
+ return SnapshotWriter.Write(options.SnapshotPath, options.CodexHome);
+
+ using var singleInstance = SingleInstance.Create(
+ "CodexTpsFloat", () => App.ShowExistingPanel?.Invoke());
+ if (!singleInstance.IsFirstInstance && options.CapturePath == null)
+ {
+ singleInstance.SignalExistingInstance();
</code_context>
<issue_to_address>
**唤醒请求丢失**
如果第二个进程在 `App.ShowExistingPanel` 尚未赋值前向第一个实例发送信号,`SingleInstance.Create` 可能会在回调仍为 null 时处理掉唤醒请求;第二个进程退出时并未打开第一个面板,因此面板仍保持隐藏。
请保留唤醒请求,直到 `ShowExistingPanel` 就绪后再分发。
另请查看 `src/App/App.axaml.cs:31-32`、`src/Core/SingleInstance.cs:79-83`。
</issue_to_address>
### 评论 7
<location path="src/App/Program.cs" line_range="16" />
<code_context>
+ if (options == null) return 2;
+
+ if (options.SnapshotPath != null)
+ return SnapshotWriter.Write(options.SnapshotPath, options.CodexHome);
+
+ using var singleInstance = SingleInstance.Create(
</code_context>
<issue_to_address>
**快照错误绕过 CLI 错误处理**
如果无法创建或写入快照目标,`Program.Main` 会在 `try` 块之前调用 `SnapshotWriter.Write`,导致文件系统异常绕过文档规定的快照错误处理路径,而不是返回退出代码 1。
请通过与其他启动失败相同的错误处理路径处理快照写入异常。
另请查看 `src/Core/SnapshotWriter.cs:22`、`src/App/Program.cs:17`、`src/App/Program.cs:24`。
</issue_to_address>
### 评论 8
<location path="src/Core/SingleInstance.cs" line_range="96" />
<code_context>
+ FileShare.None);
+ owns = true;
+ }
+ catch (IOException)
+ {
+ }
</code_context>
<issue_to_address>
**Unix 启动后退出且没有显示面板**
如果打开 Unix 锁文件时发生了与其他进程持有该文件无关的 I/O 错误,`CreateUnix` 会将所有 `IOException` 都视为另一个实例持有锁的证据。因此,`Program` 会向假定的所有者发送信号并成功退出,即使实际上没有所有者;用户既看不到面板,也收不到错误提示。
请区分正常的锁竞争和其他锁文件 I/O 错误,并报告后者。
另请查看 `src/Core/SingleInstance.cs:171`、`src/App/Program.cs:22-23`。
</issue_to_address>
### 评论 9
<location path="src/Core/SingleInstance.cs" line_range="172" />
<code_context>
+namespace CodexTpsFloat.App;
+
+public partial class App : Application
+{
+ public static CommandLineOptions Options { get; set; } = new(null, null, null);
+ public static SingleInstance? SingleInstance { get; set; }
</code_context>
<issue_to_address>
**第二次启动未能唤醒应用,且没有提示**
如果写入 Unix 唤醒文件或以原子方式替换该文件失败,`SignalExistingInstance` 会吞掉文件写入和替换错误,而 `Program.Main` 随即返回成功;第二次启动看起来成功了,但运行中的面板并未显示。
请返回或传播信号发送结果,并在无法传递唤醒请求时让第二次启动报告失败或使用备用方案。
</issue_to_address>
### 评论 10
<location path="src/App/Program.cs" line_range="18-20" />
<code_context>
+ if (options.SnapshotPath != null)
+ return SnapshotWriter.Write(options.SnapshotPath, options.CodexHome);
+
+ using var singleInstance = SingleInstance.Create(
+ "CodexTpsFloat", () => App.ShowExistingPanel?.Invoke());
+ if (!singleInstance.IsFirstInstance && options.CapturePath == null)
+ {
+ singleInstance.SignalExistingInstance();
</code_context>
<issue_to_address>
**第二次启动请求悄无声息地丢失**
如果第二次启动在第一个实例初始化或关闭期间发出信号,或者 Unix 唤醒文件 I/O 失败,所有者可能会在 `App.ShowExistingPanel` 赋值前处理掉 Unix 唤醒请求、在初始化期间删除请求,或在关闭期间取消监听器;`SignalExistingInstance` 也会忽略 Unix 写入失败。随后 `Program` 会返回成功,却没有打开面板,用户的启动操作因此毫无效果。
请保留唤醒请求,直到 UI 回调就绪,并使用带确认、可重试的交接机制,确保没有实例处理请求时不能报告成功。
另请查看 `src/App/App.axaml.cs:32`、`src/Core/SingleInstance.cs:106`、`src/Core/SingleInstance.cs:171`、`src/Core/SingleInstance.cs:184`。
</issue_to_address>
### 评论 11
<location path="src/App/MainWindow.axaml.cs" line_range="277-280" />
<code_context>
+ // macOS three-finger drag and ordinary mouse drag both end up here, so the whole
+ // panel is a drag surface except for controls that need the press themselves.
+ static bool HasInteractiveAncestor(Visual source) =>
+ source.FindAncestorOfType<Button>() != null
+ || source.FindAncestorOfType<TextBox>() != null
+ || source.FindAncestorOfType<ComboBox>() != null
+ || source.FindAncestorOfType<DataGrid>() != null;
+
+ async void RefreshClick(object? sender, RoutedEventArgs e) => await RefreshAsync();
</code_context>
<issue_to_address>
**点击控件会开始拖动**
用户点击时长选项或设置复选框时,`HasInteractiveAncestor` 会排除按钮、文本框、组合框和数据网格,但没有排除单选按钮或复选框;`DragPanel` 会调用 `BeginMoveDrag`,因此这些控件无法可靠地响应操作。
请将单选按钮和复选框排除在面板拖动区域之外。
</issue_to_address>
### 评论 12
<location path="src/App/MainWindow.axaml.cs" line_range="485-489" />
<code_context>
+ if (closing) return;
+ closing = true;
+ timer.Stop();
+ if (double.IsFinite(Position.X) && double.IsFinite(Position.Y))
+ {
+ double scale = Screens.ScreenFromWindow(this)?.Scaling ?? 1;
+ settings.X = Position.X / scale;
+ settings.Y = Position.Y / scale;
+ }
+ SaveSettings();
</code_context>
<issue_to_address>
**隐藏状态下退出会重置面板位置**
用户在全新启动 Windows 或 Linux 后,若没有先显示面板就从托盘退出,`DisposeWindow` 只要发现 `Position` 是有限值就会持久化该位置,即使最初隐藏的窗口从未打开过也是如此;窗口默认位置 `(0, 0)` 会覆盖已保存的坐标,导致下次启动时面板出现在屏幕左上角。
只有在窗口至少打开并定位过一次之后,才持久化面板坐标。
</issue_to_address>
### 评论 13
<location path="src/App/MainWindow.axaml.cs" line_range="382" />
<code_context>
+
+ async void ChooseHomeClick(object? sender, RoutedEventArgs e)
+ {
+ if (refreshing) return;
+ var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+ {
</code_context>
<issue_to_address>
**所选文件夹显示的是过时数据**
文件夹选择器打开后、返回前,如果定时器启动了刷新,选择器打开期间就可能开始一次刷新。随后 `ChooseHomeClick` 会替换 `scanner` 和 `requestClocks`,但它调用的 `RefreshAsync` 因 `refreshing` 仍为 true 而直接返回;正在执行的任务则会发布旧文件夹的快照,导致所选文件夹的数据不会显示,直到下一次定时器触发。
请为新选定的文件夹排队刷新,并丢弃扫描器或主目录与当前选择不匹配的结果。
另请查看 `src/App/MainWindow.axaml.cs:172`、`src/App/MainWindow.axaml.cs:397-400`。
</issue_to_address>
### 评论 14
<location path=".github/workflows/ci.yml" line_range="66" />
<code_context>
+
+ - name: Run hidden tray lifecycle tests (Windows only)
+ if: runner.os == 'Windows'
+ run: dotnet run --project tests/TrayLifecycleTests.csproj -c Release --nologo -- --never-show
+
+ - name: Build portable application
</code_context>
<issue_to_address>
**托盘测试覆盖的是旧版应用**
Windows CI 或发布验证成功时,工作流会运行 `tests/TrayLifecycleTests.csproj`;该测试引用 `src/CodexTpsFloat.csproj` 并测试 WPF `MainWindow`,而不是新的 Avalonia 发布入口。因此,即使实际发布应用的托盘行为存在问题,所宣传的托盘生命周期检查仍可能通过。
请添加能够启动并验证 Avalonia 应用的生命周期测试,或者将现有步骤标注为仅适用于旧版 WPF,并避免将其视为对发布应用的验证。
另请查看 `.github/workflows/release.yml:73`。
</issue_to_address>Sourcery 评估
需要人工审查。 目前有 14 个问题需要先处理;新跨平台 UI 或打包中的缺陷通常属于常规运行时或发布问题,可以按常规方式修复。如果用户启用了登录时启动,应用会写入可能在代码回滚后仍然存在的操作系统自动启动条目;此影响范围有限,可通过关闭该设置或删除条目来移除。
阻塞性问题:src/Core/Autostart.cs:110、src/App/App.axaml.cs:41、src/Core/SingleInstance.cs:62、src/App/Program.cs:10、src/App/Program.cs:20,以及另外 9 项
Original comment in English
Hey - I've found 14 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/Core/Autostart.cs" line_range="110" />
<code_context>
+ [Desktop Entry]
+ Type=Application
+ Name=Codex TPS
+ Exec="{executable}"
+ Terminal=false
+ X-GNOME-Autostart-enabled=true
</code_context>
<issue_to_address>
**Login startup launches incorrectly**
When the installed executable path contains desktop-entry-reserved characters such as quotes, backslashes, percent signs, or newlines, `LinuxAutostart.SetEnabled` inserts the path unescaped into the `.desktop` `Exec` value, so the desktop parser misreads the command or, for crafted quotes and newlines, launches an injected command at login.
Encode the executable path according to the desktop-entry `Exec` grammar before writing it.
</issue_to_address>
### Comment 2
<location path="src/App/App.axaml.cs" line_range="41" />
<code_context>
+ await window.CaptureAndCloseAsync(Options.CapturePath);
+ // Avalonia can create a tray icon on macOS without macOS displaying it,
+ // so there is no reliable way to detect an unusable menu bar item.
+ else if (OperatingSystem.IsMacOS() || !window.HasTray)
+ window.ShowPanel();
+ };
</code_context>
<issue_to_address>
**Panel becomes inaccessible**
When a Linux desktop has no usable status-notifier host but Avalonia still creates the tray object, `HasTray` reports that a `TrayIcon` object exists, so `App` skips `ShowPanel()` even though no tray icon is displayed; the user has no visible way to open or exit the app.
Provide a visible panel fallback when the tray is not actually available.
Also at `src/App/MainWindow.axaml.cs:41-42`, `src/App/MainWindow.axaml.cs:51`, `src/App/MainWindow.axaml.cs:62`.
</issue_to_address>
### Comment 3
<location path="src/Core/SingleInstance.cs" line_range="62" />
<code_context>
+ var mutex = new Mutex(true, scopedName, out bool first);
+ try
+ {
+ var wake = new EventWaitHandle(false, EventResetMode.AutoReset, scopedName + ".Show");
+ RegisteredWaitHandle? registration = null;
+ if (first && showExisting != null)
</code_context>
<issue_to_address>
**Older panel stays hidden**
When a user launches this Windows version while the previous version is still running, `CreateWindows` shares the old mutex name but signals `Local\CodexTpsFloat.Panel.v1.Show`, while the older app listens on `Local\CodexTpsFloat.Show.v1`; the new process exits as a secondary instance and the old panel is not shown.
Keep the wake event name compatible with the previous Windows version or otherwise ensure the older instance receives the show request.
</issue_to_address>
### Comment 4
<location path="src/App/Program.cs" line_range="10" />
<code_context>
+
+public static class Program
+{
+ public static int Main(string[] args)
+ {
+ var options = CommandLineOptions.Parse(args);
</code_context>
<issue_to_address>
**Windows pickers fail to open**
When a Windows user opens the folder picker or report save dialog, `Program.Main` starts Avalonia on the default MTA thread, so the Windows COM-based pickers fail to open and users cannot choose a Codex directory or export a report.
Start the Windows UI thread in STA.
</issue_to_address>
### Comment 5
<location path="src/App/Program.cs" line_range="20" />
<code_context>
+
+ using var singleInstance = SingleInstance.Create(
+ "CodexTpsFloat", () => App.ShowExistingPanel?.Invoke());
+ if (!singleInstance.IsFirstInstance && options.CapturePath == null)
+ {
+ singleInstance.SignalExistingInstance();
</code_context>
<issue_to_address>
**Capture overwrites newer settings**
When a capture run overlaps a normal app session and the normal session changes settings after capture loads them, a `--capture` process is allowed to run despite another instance and loads its own settings copy. If the regular app saves changed settings before capture closes, the capture window’s closing handler saves its stale copy over them, losing the user’s newer settings.
Do not persist settings from capture-only windows, or serialize writes with a mechanism that rejects stale settings copies.
Also at `src/App/MainWindow.axaml.cs:491`, `src/Core/SettingsStore.cs:39-40`.
</issue_to_address>
### Comment 6
<location path="src/App/Program.cs" line_range="19-20" />
<code_context>
+ return SnapshotWriter.Write(options.SnapshotPath, options.CodexHome);
+
+ using var singleInstance = SingleInstance.Create(
+ "CodexTpsFloat", () => App.ShowExistingPanel?.Invoke());
+ if (!singleInstance.IsFirstInstance && options.CapturePath == null)
+ {
+ singleInstance.SignalExistingInstance();
</code_context>
<issue_to_address>
**Wake request is lost**
When a second process signals the first instance before `App.ShowExistingPanel` is assigned, `SingleInstance.Create` can consume the wake request while the callback is still null; the second process exits without opening the first panel, which remains hidden.
Retain wake requests until `ShowExistingPanel` is ready, then dispatch them.
Also at `src/App/App.axaml.cs:31-32`, `src/Core/SingleInstance.cs:79-83`.
</issue_to_address>
### Comment 7
<location path="src/App/Program.cs" line_range="16" />
<code_context>
+ if (options == null) return 2;
+
+ if (options.SnapshotPath != null)
+ return SnapshotWriter.Write(options.SnapshotPath, options.CodexHome);
+
+ using var singleInstance = SingleInstance.Create(
</code_context>
<issue_to_address>
**Snapshot errors bypass CLI handling**
When the snapshot destination cannot be created or written, `Program.Main` calls `SnapshotWriter.Write` before its `try` block, so a filesystem exception escapes the documented snapshot error path instead of returning exit code 1.
Handle snapshot-writing exceptions through the same error path as other startup failures.
Also at `src/Core/SnapshotWriter.cs:22`, `src/App/Program.cs:17`, `src/App/Program.cs:24`.
</issue_to_address>
### Comment 8
<location path="src/Core/SingleInstance.cs" line_range="96" />
<code_context>
+ FileShare.None);
+ owns = true;
+ }
+ catch (IOException)
+ {
+ }
</code_context>
<issue_to_address>
**Unix launch exits without a panel**
When opening the Unix lock file fails for an I/O reason unrelated to another process holding it, `CreateUnix` treats every `IOException` as proof that another instance owns the lock, so `Program` signals the presumed owner and exits successfully even when no owner exists; the user gets no panel or error.
Distinguish normal lock contention from other lock-file I/O failures and report the latter.
Also at `src/Core/SingleInstance.cs:171`, `src/App/Program.cs:22-23`.
</issue_to_address>
### Comment 9
<location path="src/Core/SingleInstance.cs" line_range="172" />
<code_context>
+namespace CodexTpsFloat.App;
+
+public partial class App : Application
+{
+ public static CommandLineOptions Options { get; set; } = new(null, null, null);
+ public static SingleInstance? SingleInstance { get; set; }
</code_context>
<issue_to_address>
**Second launch silently fails to wake**
When writing or atomically replacing the Unix wake file fails, `SignalExistingInstance` swallows file-write and replacement errors, while `Program.Main` returns success immediately afterward; the second launch appears successful but the running panel is not shown.
Return or propagate a signaling result and make the second launch report failure or use a fallback when the wake request cannot be delivered.
</issue_to_address>
### Comment 10
<location path="src/App/Program.cs" line_range="18-20" />
<code_context>
+ if (options.SnapshotPath != null)
+ return SnapshotWriter.Write(options.SnapshotPath, options.CodexHome);
+
+ using var singleInstance = SingleInstance.Create(
+ "CodexTpsFloat", () => App.ShowExistingPanel?.Invoke());
+ if (!singleInstance.IsFirstInstance && options.CapturePath == null)
+ {
+ singleInstance.SignalExistingInstance();
</code_context>
<issue_to_address>
**Second launch is silently lost**
When a second launch signals while the first instance is initializing or shutting down, or Unix wake-file I/O fails, the owner can consume a Unix wake request before `App.ShowExistingPanel` is assigned, delete a request while initializing, or cancel its listener during shutdown; `SignalExistingInstance` also suppresses Unix write failures. `Program` then returns success without opening a panel, so the user’s launch does nothing.
Keep wake requests pending until the UI callback is ready, and use an acknowledged/retryable handoff that cannot report success when no instance handled the request.
Also at `src/App/App.axaml.cs:32`, `src/Core/SingleInstance.cs:106`, `src/Core/SingleInstance.cs:171`, `src/Core/SingleInstance.cs:184`.
</issue_to_address>
### Comment 11
<location path="src/App/MainWindow.axaml.cs" line_range="277-280" />
<code_context>
+ // macOS three-finger drag and ordinary mouse drag both end up here, so the whole
+ // panel is a drag surface except for controls that need the press themselves.
+ static bool HasInteractiveAncestor(Visual source) =>
+ source.FindAncestorOfType<Button>() != null
+ || source.FindAncestorOfType<TextBox>() != null
+ || source.FindAncestorOfType<ComboBox>() != null
+ || source.FindAncestorOfType<DataGrid>() != null;
+
+ async void RefreshClick(object? sender, RoutedEventArgs e) => await RefreshAsync();
</code_context>
<issue_to_address>
**Controls start dragging**
When a user clicks a duration option or settings checkbox, `HasInteractiveAncestor` excludes buttons, text boxes, combo boxes, and data grids, but not radio buttons or checkboxes; `DragPanel` calls `BeginMoveDrag` instead of reliably activating those controls.
Exclude radio buttons and checkboxes from the panel drag surface.
</issue_to_address>
### Comment 12
<location path="src/App/MainWindow.axaml.cs" line_range="485-489" />
<code_context>
+ if (closing) return;
+ closing = true;
+ timer.Stop();
+ if (double.IsFinite(Position.X) && double.IsFinite(Position.Y))
+ {
+ double scale = Screens.ScreenFromWindow(this)?.Scaling ?? 1;
+ settings.X = Position.X / scale;
+ settings.Y = Position.Y / scale;
+ }
+ SaveSettings();
</code_context>
<issue_to_address>
**Hidden exit resets panel position**
When the user exits a fresh Windows or Linux launch from the tray without showing the panel first, `DisposeWindow` persists `Position` whenever it is finite, even if the initially hidden window was never opened; its default `(0, 0)` overwrites the saved coordinates, so the panel appears at the screen’s top-left on the next launch.
Persist the panel coordinates only after the window has been opened and positioned at least once.
</issue_to_address>
### Comment 13
<location path="src/App/MainWindow.axaml.cs" line_range="382" />
<code_context>
+
+ async void ChooseHomeClick(object? sender, RoutedEventArgs e)
+ {
+ if (refreshing) return;
+ var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+ {
</code_context>
<issue_to_address>
**Selected folder shows stale data**
When the periodic timer starts a refresh after the folder picker opens but before it returns, the timer can start a refresh while the folder picker is open. `ChooseHomeClick` then replaces `scanner` and `requestClocks`, but its `RefreshAsync` call returns because `refreshing` is still true; the in-flight task publishes its old-folder snapshot, leaving the selected folder’s data unshown until a later timer tick.
Queue a refresh for the newly selected folder and discard results whose scanner/home no longer matches the current selection.
Also at `src/App/MainWindow.axaml.cs:172`, `src/App/MainWindow.axaml.cs:397-400`.
</issue_to_address>
### Comment 14
<location path=".github/workflows/ci.yml" line_range="66" />
<code_context>
+
+ - name: Run hidden tray lifecycle tests (Windows only)
+ if: runner.os == 'Windows'
+ run: dotnet run --project tests/TrayLifecycleTests.csproj -c Release --nologo -- --never-show
+
+ - name: Build portable application
</code_context>
<issue_to_address>
**Tray tests exercise the legacy app**
When windows CI or release validation succeeds, the workflow runs `tests/TrayLifecycleTests.csproj`, which references `src/CodexTpsFloat.csproj` and tests WPF `MainWindow`; it does not exercise the new Avalonia release entry point, so the advertised tray lifecycle check passes even if the shipped app’s tray behavior is broken.
Add lifecycle coverage that launches and verifies the Avalonia app, or label the existing step as legacy WPF-only and avoid treating it as validation of the release app.
Also at `.github/workflows/release.yml:73`.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 14 findings to address first, and a bug in the new cross-platform UI or packaging would usually be an ordinary runtime or release issue that can be fixed normally. If a user enables login startup, the app writes an OS autostart entry that can outlive a code revert; it is bounded and can be removed by disabling the setting or deleting the entry.
Blocking findings: src/Core/Autostart.cs:110, src/App/App.axaml.cs:41, src/Core/SingleInstance.cs:62, src/App/Program.cs:10, src/App/Program.cs:20, and 9 more
|
@sourcery-ai review |
There was a problem hiding this comment.
嘿——我发现了 2 个问题
面向 AI 智能体的提示
请处理此次代码审查中的评论:
## 单独评论
### 评论 1
<location path="src/Core/Autostart.cs" line_range="98-99" />
<code_context>
+ // Exec quoting is decoded after desktop-entry string escapes.
+ static string EncodeExecutable(string path)
+ {
+ if (path.Contains('=') || path.Contains('\0'))
+ throw new ArgumentException("登录启动路径不能包含等号或空字符。");
+ var quoted = new StringBuilder();
+ foreach (char c in path)
</code_context>
<issue_to_address>
**登录启动会拒绝有效路径**
当 Linux 可执行文件路径中包含等号时,`EncodeExecutable` 会拒绝 `=`,尽管它在 desktop-entry 的 `Exec` 值中是有效字符。因此,`SetEnabled` 会返回错误,用户无法通过该安装路径启用登录启动。
在对 desktop-entry 的 `Exec` 值进行编码时,允许可执行文件路径包含 `=`。
</issue_to_address>
### 评论 2
<location path="src/App/MainWindow.axaml.cs" line_range="428-431" />
<code_context>
+ var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
+ {
+ Title = "导出报告",
+ SuggestedFileName = $"Codex-TPS-{date:yyyy-MM-dd}.md",
+ DefaultExtension = "md",
+ ShowOverwritePrompt = true,
+ FileTypeChoices = new[]
+ {
+ new FilePickerFileType("中文报告") { Patterns = new[] { "*.md" } },
</code_context>
<issue_to_address>
**JSON 导出可能会写入 Markdown**
当用户选择 JSON 文件类型,但文件选择器仍保留建议的 `.md` 文件名时,`ReportExporter.Write` 会根据返回路径的扩展名选择格式,因此它会将 Markdown 写入 `.md` 文件,而不是生成用户要求的 JSON 报告。
在调用 `ReportExporter.Write` 之前,确保所选文件类型决定返回文件名的扩展名。
另请查看 `src/Core/ReportExporter.cs:35-36`。
</issue_to_address>Sourcery 评估
需要人工审查。 请先处理 2 个发现的问题;此外,Avalonia 移植中的缺陷可能会导致面板无法启动,或在某个平台上无法正常工作;通常可以通过回退更改来修复此类问题。如果启用了登录启动,回退后可能仍会留下格式错误的 LaunchAgent 或 desktop entry,但影响范围有限,可以通过禁用登录启动或删除该条目来清理。
阻塞性发现:src/Core/Autostart.cs:99、src/App/MainWindow.axaml.cs:431
Original comment in English
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/Core/Autostart.cs" line_range="98-99" />
<code_context>
+ // Exec quoting is decoded after desktop-entry string escapes.
+ static string EncodeExecutable(string path)
+ {
+ if (path.Contains('=') || path.Contains('\0'))
+ throw new ArgumentException("登录启动路径不能包含等号或空字符。");
+ var quoted = new StringBuilder();
+ foreach (char c in path)
</code_context>
<issue_to_address>
**Login startup rejects valid paths**
When the Linux executable path contains an equals sign, `EncodeExecutable` rejects `=`, although it is valid in a desktop-entry `Exec` value. `SetEnabled` therefore returns an error and users cannot enable login startup from that installation path.
Allow `=` in the executable path when encoding the desktop-entry `Exec` value.
</issue_to_address>
### Comment 2
<location path="src/App/MainWindow.axaml.cs" line_range="428-431" />
<code_context>
+ var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
+ {
+ Title = "导出报告",
+ SuggestedFileName = $"Codex-TPS-{date:yyyy-MM-dd}.md",
+ DefaultExtension = "md",
+ ShowOverwritePrompt = true,
+ FileTypeChoices = new[]
+ {
+ new FilePickerFileType("中文报告") { Patterns = new[] { "*.md" } },
</code_context>
<issue_to_address>
**JSON export can write Markdown**
When the user chooses the JSON file type but the picker retains the suggested `.md` filename, `ReportExporter.Write` selects the format from the returned path extension, so it writes Markdown to a `.md` file instead of the requested JSON report.
Ensure the selected file type determines the returned filename extension before calling `ReportExporter.Write`.
Also at `src/Core/ReportExporter.cs:35-36`.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and a defect in the Avalonia port could prevent the panel from starting or working correctly on a platform; that behavior is normally fixed by reverting. If login startup is enabled, a malformed LaunchAgent or desktop entry can remain after a revert, but it is bounded and can be removed by disabling login startup or deleting the entry.
Blocking findings: src/Core/Autostart.cs:99, src/App/MainWindow.axaml.cs:431
|
@sourcery-ai review |
There was a problem hiding this comment.
你好——我发现了 1 个问题
给 AI 代理的提示
请处理这次代码审查中的评论:
## 个别评论
### 评论 1
<location path="src/Core/ReportExporter.cs" line_range="16" />
<code_context>
+{
+ public static string BuildMarkdown(UsageSnapshot snapshot, DateTime date, string? model, IReadOnlyList<HourStats> hours)
+ {
+ var text = new StringBuilder($"# Codex TPS 分时统计\n\n日期:{date:yyyy-MM-dd};北京时间 UTC+8;模型:{model ?? "全部模型"}。\n\n快照:{snapshot.GeneratedAt:yyyy-MM-dd HH:mm:ss zzz}。\n\n| 时段 | 样本 | 中位数 | 加权 TPS |\n|---|---:|---:|---:|\n");
+ foreach (var h in hours)
+ text.AppendLine($"| {h.Hour} | {h.Samples} | {h.Median?.ToString("F2", CultureInfo.InvariantCulture) ?? "—"} | {h.Weighted?.ToString("F2", CultureInfo.InvariantCulture) ?? "—"} |");
</code_context>
<issue_to_address>
**不可信的模型文本会被转换为 Markdown**
如果攻击者能够在被扫描的日志中放入经过构造的模型值,而用户随后导出报告,并在会渲染注入内容的 Markdown 查看器中打开报告,那么 `BuildMarkdown` 会在未转义的情况下插入模型值。因此,精心构造的日志数据可能会在导出的报告中加入远程图片或具有欺骗性的链接等 Markdown 内容;会渲染或加载这些内容的查看器将处理攻击者控制的内容。
在将模型值插入报告之前,请先对其进行 Markdown 转义(或将其呈现为不可执行的纯文本)。
</issue_to_address>Sourcery 评估
等待批准。 请先处理 1 个问题。
阻塞问题:src/Core/ReportExporter.cs:16
Original comment in English
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/Core/ReportExporter.cs" line_range="16" />
<code_context>
+{
+ public static string BuildMarkdown(UsageSnapshot snapshot, DateTime date, string? model, IReadOnlyList<HourStats> hours)
+ {
+ var text = new StringBuilder($"# Codex TPS 分时统计\n\n日期:{date:yyyy-MM-dd};北京时间 UTC+8;模型:{model ?? "全部模型"}。\n\n快照:{snapshot.GeneratedAt:yyyy-MM-dd HH:mm:ss zzz}。\n\n| 时段 | 样本 | 中位数 | 加权 TPS |\n|---|---:|---:|---:|\n");
+ foreach (var h in hours)
+ text.AppendLine($"| {h.Hour} | {h.Samples} | {h.Median?.ToString("F2", CultureInfo.InvariantCulture) ?? "—"} | {h.Weighted?.ToString("F2", CultureInfo.InvariantCulture) ?? "—"} |");
</code_context>
<issue_to_address>
**Untrusted model text becomes Markdown**
When an attacker can place a crafted model value in a scanned log and the user exports and opens the report in a Markdown viewer that renders the injected content, `BuildMarkdown` interpolates the model value without escaping it, so crafted log data can add Markdown such as a remote image or deceptive link to an exported report; a viewer that renders or loads it processes attacker-controlled content.
Escape the model value for Markdown (or render it as inert text) before inserting it into the report.
</issue_to_address>Sourcery assessment
Approval pending. 1 finding to address first.
Blocking findings: src/Core/ReportExporter.cs:16
|
@sourcery-ai review |
There was a problem hiding this comment.
嘿——我已经审查了你的更改,整体看起来很棒!
Sourcery 评估
需要人工审查。 如果特定平台的登录启动处理或设置持久化存在问题,用户配置文件中可能会留下启动项或错误的偏好设置;即使回滚,这些残留也可能仍然存在,但影响范围有限,可以将其删除或修正。大多数 UI 或平台集成缺陷则属于普通的运行时错误,回滚即可撤销其影响。
Original comment in English
Hey - I've reviewed your changes and they look great!
Sourcery assessment
Needs a human reviewer. If the platform-specific login-startup handling or settings persistence is wrong, it can leave a startup entry or incorrect preferences in the user’s profile; those artifacts may remain after reverting, but are bounded and can be removed or corrected. Most UI or platform integration defects would otherwise be ordinary runtime bugs that a revert would undo.
There was a problem hiding this comment.
嗨——我已经审阅了你的更改,整体看起来很棒!
Sourcery 评估
需要人工审核。 如果新的登录启动处理逻辑有误,启用它可能会留下 LaunchAgent 或桌面自动启动项;即使回滚此更改,该项仍会继续启动应用。影响范围有限,可通过禁用登录启动或删除该项来消除;除此之外,界面和报告方面的问题只会造成常规且可逆的运行时影响。
Original comment in English
Hey - I've reviewed your changes and they look great!
Sourcery assessment
Needs a human reviewer. If the new login-startup handling is wrong, enabling it can leave a LaunchAgent or desktop autostart entry that continues launching the app after this change is reverted. The effect is bounded and can be removed by disabling login startup or deleting the entry; the UI and reporting defects otherwise have ordinary, reversible runtime impact.
此前程序依赖 Windows WPF,无法直接在 macOS 和 Linux 使用。本 PR 提取平台无关核心库,以 Avalonia 作为跨平台发布入口,并保留原有统计、托盘和报告导出功能。
主要变更
CodexTpsFloat.Core,补充日志重写与单实例唤醒回归测试。验证
dotnet run --project tests/CoreTests.csproj -c Release --nologo,覆盖统计、增量日志、设置、导出、登录启动和单实例唤醒等回归场景。dotnet build src/App/CodexTpsFloat.App.csproj -c Release --nologo --no-restore(本地 macOS,0 错误)。Sourcery 总结
在保留现有监控和报告功能的同时,让 Codex TPS 浮动监视器能够作为便携式应用在 Windows、macOS 和 Linux 上运行。
新功能:
错误修复:
改进:
构建:
CI:
文档:
测试:
杂项:
Original summary in English
Sourcery 摘要
启用 Codex TPS 浮动监视器,使其成为可在 Windows、macOS 和 Linux 上运行的便携式应用,同时保留监控和报告功能。
新功能:
错误修复:
增强功能:
构建:
CI:
文档:
测试:
杂项:
Original summary in English
Sourcery 生成的摘要
启用 Codex TPS 悬浮监控器,使其能够作为便携式应用在 Windows、macOS 和 Linux 上运行,同时保留监控与报告功能。
新功能:
错误修复:
改进:
构建:
CI:
文档:
测试:
杂项:
Original summary in English
Sourcery 摘要
启用 Codex TPS 浮动监视器,使其成为可在 Windows、macOS 和 Linux 上运行的便携式应用,同时保留监控和报告功能。
新功能:
错误修复:
增强功能:
构建:
CI:
文档:
测试:
杂项:
Original summary in English
Sourcery 生成的摘要
启用 Codex TPS Float,使其能够作为可移植的监控应用在 Windows、macOS 和 Linux 上运行,同时保留其统计和报告功能。
新功能:
Bug 修复:
增强:
构建:
CI:
文档:
测试:
Original summary in English
Sourcery 总结
使监控工具能够作为便携式应用在 Windows、macOS 和 Linux 上运行,同时保留现有的统计和报告功能。
新功能:
错误修复:
增强功能:
构建:
CI:
文档:
测试:
Original summary in English
Sourcery 生成的摘要
让监控工具能够作为便携式应用在 Windows、macOS 和 Linux 上运行,同时保留其统计和报告功能。
新功能:
错误修复:
增强:
构建:
CI:
文档:
测试:
Original summary in English
Sourcery 摘要
使 Codex TPS 浮动监视器能够作为便携式应用程序在 Windows、macOS 和 Linux 上运行,同时保留其监控与报告功能。
新功能:
错误修复:
增强功能:
构建:
CI:
文档:
测试:
杂项:
Original summary in English
Sourcery 摘要
启用监控工具,使其能够作为便携式应用在 Windows、macOS 和 Linux 上运行,同时保留统计和报告功能。
新功能:
错误修复:
增强:
构建:
CI:
文档:
测试:
Original summary in English
Sourcery 汇总
使监控工具能够作为便携式跨平台应用在 Windows、macOS 和 Linux 上运行,同时保留其统计和报告功能。
新功能:
错误修复:
改进:
构建:
CI:
文档:
测试:
Original summary in English
Sourcery 摘要
让监控应用能够作为可移植的跨平台 Avalonia 应用在 Windows、macOS 和 Linux 上运行,同时保留统计和报告功能。
新功能:
错误修复:
改进:
构建:
CI:
文档:
测试:
杂项:
Original summary in English
Sourcery 总结
让监控应用能够作为便携式跨平台 Avalonia 应用在 Windows、macOS 和 Linux 上运行,同时保留现有的统计和报告功能。
新功能:
错误修复:
改进:
构建:
CI:
文档:
测试:
Original summary in English
Summary by Sourcery
Enable the monitoring application to run as a portable cross-platform Avalonia app on Windows, macOS, and Linux while retaining its existing statistics and reporting capabilities.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Tests: