From 791134f354cca418f40fd22746484b7b603e4490 Mon Sep 17 00:00:00 2001 From: Finesssee <90105158+Finesssee@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:26:44 +0700 Subject: [PATCH 1/5] Canonicalize repo URLs and add GitHub community files --- .github/ISSUE_TEMPLATE/config.yml | 12 ++++++++++++ README.md | 4 ++-- SECURITY.md | 21 +++++++++++++++++++++ SUPPORT.md | 24 ++++++++++++++++++++++++ docs/WSL.md | 2 +- 5 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 SECURITY.md create mode 100644 SUPPORT.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..b96e2d0033 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +blank_issues_enabled: false + +contact_links: + - name: Questions & Answers + url: https://github.com/nesszer/Win-CodexBar/discussions + about: Ask usage questions, share tips, and discuss Win-CodexBar with other users. + - name: Configuration docs + url: https://github.com/nesszer/Win-CodexBar/blob/main/docs/CONFIGURATION.md + about: Configuration questions are best answered by the docs first. + - name: Cookies and browser docs + url: https://github.com/nesszer/Win-CodexBar/blob/main/docs/COOKIES.md + about: Browser cookie import questions are covered in the cookies docs. diff --git a/README.md b/README.md index 02c645d1cb..73d926fb28 100755 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Install with Windows Package Manager: winget install Finesssee.Win-CodexBar ``` -Or download the latest installer/portable build from [GitHub Releases](https://github.com/Finesssee/Win-CodexBar/releases). +Or download the latest installer/portable build from [GitHub Releases](https://github.com/nesszer/Win-CodexBar/releases). - Installer: `CodexBar--Setup.exe` - Portable: `CodexBar--portable.exe` @@ -135,7 +135,7 @@ The UI and contributor reporting currently support: ```powershell # Prerequisites: Node.js + pnpm. Rust and MinGW are installed by the script when needed. -git clone https://github.com/Finesssee/Win-CodexBar.git +git clone https://github.com/nesszer/Win-CodexBar.git cd Win-CodexBar .\scripts\dev.ps1 ``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..47e9f5e2bd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security policy + +## Supported versions + +Only the latest release of Win-CodexBar is supported. Security fixes are made +against the current release line; older releases and the historical upstream +macOS project are not patched. + +## Reporting a vulnerability + +Please use GitHub's private vulnerability reporting: open the +[Security tab](https://github.com/nesszer/Win-CodexBar/security) and click +"Report a vulnerability". + +Do **not** open a public GitHub issue for a vulnerability. Public issues and +the bug report template are for non-security problems only. + +Win-CodexBar handles provider cookies, OAuth tokens, and API keys locally, so +reports touching that surface — credential extraction, storage, redaction, or +leakage — are taken seriously. Please keep report details private and do not +paste secrets, cookies, or tokens into any report. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000000..253e30632b --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,24 @@ +# Support + +## Bug reports + +Use the [bug report template](https://github.com/nesszer/Win-CodexBar/issues/new?template=bug_report.yml). +If you have safe diagnostics from the CLI, attach them: +`codexbar diagnose` exports provider diagnostics as JSON with no cookies or +tokens. + +## Questions and discussions + +Ask in [Discussions](https://github.com/nesszer/Win-CodexBar/discussions). + +## Docs + +Configuration, CLI, cookies, providers, and privacy docs live in +[`docs/`](docs): ARCHITECTURE, BUILDING, CLI, CODE_SIGNING, CONFIGURATION, +COOKIES, PRIVACY, PROVIDERS, WINDOWS_PROOF, WSL, and the ADRs under +[`docs/adr/`](docs/adr). + +Feature requests use the +[feature request template](https://github.com/nesszer/Win-CodexBar/issues/new?template=feature_request.yml). +Security vulnerabilities go through the +[Security tab](SECURITY.md), not public issues. diff --git a/docs/WSL.md b/docs/WSL.md index cb30d99097..404bff542d 100644 --- a/docs/WSL.md +++ b/docs/WSL.md @@ -6,7 +6,7 @@ requires [WSLg](https://github.com/microsoft/wslg) (Windows 11, build 22000+). ## Quick Start ```bash -git clone https://github.com/Finesssee/Win-CodexBar.git +git clone https://github.com/nesszer/Win-CodexBar.git cd Win-CodexBar ./scripts/dev.sh ``` From de0b6b052f7c9c41f4bb2366189772d7b3873b6a Mon Sep 17 00:00:00 2001 From: Finesssee <90105158+Finesssee@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:26:55 +0700 Subject: [PATCH 2/5] Add in-app issue reporting funnel --- .../src-tauri/src/commands/diagnostics.rs | 43 ++- .../src-tauri/src/commands/mod.rs | 2 + apps/desktop-tauri/src-tauri/src/main.rs | 2 + apps/desktop-tauri/src/i18n/keys.ts | 3 + apps/desktop-tauri/src/lib/tauri.ts | 4 + .../surfaces/settings/tabs/AboutTab.test.tsx | 7 +- .../src/surfaces/settings/tabs/AboutTab.tsx | 18 +- .../settings/tabs/AdvancedTab.test.tsx | 128 ++++++++ .../surfaces/settings/tabs/AdvancedTab.tsx | 41 ++- rust/src/locale.rs | 3 + rust/src/locale/en-US.ftl | 3 + rust/src/logging.rs | 276 +++++++++++++++++- 12 files changed, 511 insertions(+), 19 deletions(-) create mode 100644 apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx diff --git a/apps/desktop-tauri/src-tauri/src/commands/diagnostics.rs b/apps/desktop-tauri/src-tauri/src/commands/diagnostics.rs index 35b5f1c8d3..beec9c4e56 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/diagnostics.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/diagnostics.rs @@ -1,5 +1,38 @@ -// `get_safe_diagnostics` (the only Tauri command this module exposed) was -// removed as orphaned dead code — the frontend wrappers that invoked it were -// deleted, leaving zero invokers. Its `SafeDiagnostics` payload + the -// `safe_diagnostics_from` builder + the secret-redaction test existed solely -// to support that command and were removed with it. +//! `get_safe_diagnostics` — a copy-friendly, secret-free diagnostics string +//! for bug reports. Contains only: app version/build, OS, update channel, +//! log directory, and the redacted log tail. Never includes provider names, +//! emails, plans, account info, cookies, or tokens. + +use super::*; + +#[tauri::command] +pub fn get_safe_diagnostics() -> String { + let settings = Settings::load(); + let os = format!( + "{} {}", + std::env::consts::OS, + std::env::var("OS").unwrap_or_default() + ); + let log_dir = codexbar::logging::log_file_path() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "unresolvable".to_string()); + let log_tail = + codexbar::logging::read_log_tail().unwrap_or_else(|| "log file unavailable".to_string()); + + format!( + "CodexBar diagnostics\n\ + -------------------\n\ + version: {} (build {})\n\ + os: {}\n\ + channel: {}\n\ + log dir: {}\n\ + --- last log lines (redacted) ---\n\ + {}", + env!("CARGO_PKG_VERSION"), + option_env!("BUILD_NUMBER").unwrap_or("dev"), + os, + update_channel_label(settings.update_channel), + log_dir, + log_tail, + ) +} diff --git a/apps/desktop-tauri/src-tauri/src/commands/mod.rs b/apps/desktop-tauri/src-tauri/src/commands/mod.rs index e0d6f4bd41..cd5420ef51 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/mod.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/mod.rs @@ -36,6 +36,7 @@ mod codex_accounts; mod codex_workspaces; mod credential_detection; mod credentials; +mod diagnostics; mod locale_cmd; mod provider_detail; mod provider_settings; @@ -52,6 +53,7 @@ pub use codex_accounts::*; pub use codex_workspaces::*; pub use credential_detection::*; pub use credentials::*; +pub use diagnostics::*; pub use locale_cmd::*; pub use provider_detail::*; pub use provider_settings::*; diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index 2b6d77fab0..709f324de4 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -110,6 +110,7 @@ fn should_suppress_blur_dismiss(launch: LaunchBehavior, proof_mode: bool) -> boo } fn main() { + codexbar::logging::install_panic_hook(); codexbar::logging::init(false, false).expect("failed to initialize logging"); let proof_config = proof_harness::ProofConfig::from_env(); @@ -200,6 +201,7 @@ fn main() { commands::remove_token_account, commands::set_active_token_account, commands::get_app_info, + commands::get_safe_diagnostics, commands::get_provider_chart_data, commands::get_provider_local_usage_summary, commands::get_usage_spend_summary, diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index fc5dcb8a22..d6359ce013 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -764,6 +764,9 @@ export const ALL_LOCALE_KEYS = [ "AboutLinkGitHub", "AboutLinkWebsite", "AboutLinkOriginalProject", + "DiagnosticsCopyButton", + "DiagnosticsCopied", + "DiagnosticsCopyFailed", // Tauri desktop shell — Cookies tab hints / placeholder "SavedCookiesHint", diff --git a/apps/desktop-tauri/src/lib/tauri.ts b/apps/desktop-tauri/src/lib/tauri.ts index 51f3ca09ff..d255a3815e 100644 --- a/apps/desktop-tauri/src/lib/tauri.ts +++ b/apps/desktop-tauri/src/lib/tauri.ts @@ -528,3 +528,7 @@ export function codexAccountRestartDesktop( export function getCodexAccountsState(): Promise { return invoke("get_codex_accounts_state"); } + +export function getSafeDiagnostics(): Promise { + return invoke("get_safe_diagnostics"); +} diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx index 9837634fde..5c9b8dcbdc 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx @@ -122,10 +122,11 @@ describe("AboutTab", () => { fireEvent.click(await screen.findByRole("button", { name: "AboutLinkGitHub" })); fireEvent.click(screen.getByRole("button", { name: "AboutLinkWebsite" })); fireEvent.click(screen.getByRole("button", { name: "AboutLinkOriginalProject" })); + fireEvent.click(screen.getByRole("button", { name: "SubmitIssue" })); expect(tauriMocks.openExternalUrl).toHaveBeenNthCalledWith( 1, - "https://github.com/Finesssee/Win-CodexBar", + "https://github.com/nesszer/Win-CodexBar", ); expect(tauriMocks.openExternalUrl).toHaveBeenNthCalledWith( 2, @@ -135,6 +136,10 @@ describe("AboutTab", () => { 3, "https://github.com/steipete/CodexBar", ); + expect(tauriMocks.openExternalUrl).toHaveBeenNthCalledWith( + 4, + "https://github.com/nesszer/Win-CodexBar/issues/new?labels=bug&template=bug_report.yml", + ); }); it("shows a link error if the OS browser launch fails", async () => { diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.tsx index 1b971fe801..f1e1962d95 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.tsx @@ -8,10 +8,19 @@ import type { LocaleKey } from "../../../i18n/keys"; import type { TabProps } from "../settingsTabs"; import codexbarIcon from "../../../assets/codexbar-icon.png"; +const REPO_URL = [ + "https:", + "", + "github.com", + "nesszer", + "Win-CodexBar", +].join("/"); +const SUBMIT_ISSUE_URL = `${REPO_URL}/issues/new?labels=bug&template=bug_report.yml`; + const ABOUT_LINKS: ReadonlyArray<{ labelKey: LocaleKey; url: string }> = [ { labelKey: "AboutLinkGitHub", - url: "https://github.com/Finesssee/Win-CodexBar", + url: REPO_URL, }, { labelKey: "AboutLinkWebsite", @@ -90,6 +99,13 @@ export default function AboutTab({ settings, set, saving }: TabProps) { ))} + {linkError && (

{t("ErrorPrefix")} {linkError} diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx new file mode 100644 index 0000000000..195ab7e89a --- /dev/null +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx @@ -0,0 +1,128 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const tauriMocks = vi.hoisted(() => ({ + getSafeDiagnostics: vi.fn(), + registerGlobalShortcut: vi.fn().mockResolvedValue(undefined), + unregisterGlobalShortcut: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("../../../lib/tauri", () => tauriMocks); +vi.mock("../../../hooks/useLocale", () => ({ + useLocale: () => ({ t: (key: string) => key }), +})); + +import AdvancedTab from "./AdvancedTab"; +import type { SettingsSnapshot } from "../../../types/bridge"; + +const settings: SettingsSnapshot = { + enabledProviders: [], + refreshIntervalSecs: 300, + adaptiveRefresh: false, + refreshAllProvidersOnMenuOpen: false, + lowPowerMode: false, + startAtLogin: false, + startMinimized: false, + showNotifications: true, + soundEnabled: true, + notificationSoundTheme: "windows", + highUsageThreshold: 70, + criticalUsageThreshold: 90, + predictivePaceWarningEnabled: false, + trayIconMode: "single", + switcherShowsIcons: true, + menuBarShowsHighestUsage: true, + menuBarShowsPercent: true, + showAsUsed: false, + showAllTokenAccountsInMenu: true, + enableAnimations: true, + resetTimeRelative: true, + showResetWhenExhausted: false, + menuBarDisplayMode: "compact", + notificationSoundPaths: { + predictiveWarning: null, + highUsage: null, + criticalUsage: null, + exhausted: null, + statusIssue: null, + sessionDepleted: null, + sessionRestored: null, + }, + hidePersonalInfo: false, + autoDownloadUpdates: false, + installUpdatesOnQuit: false, + globalShortcut: "", + codexCustomSessionsDirs: [], + updateChannel: "stable", + uiLanguage: "english", + theme: "dark", + windowScalePercent: 125, + trayScalePercent: 100, + powertoysStatusPipeEnabled: false, + claudeAvoidKeychainPrompts: true, + codexSparkUsageVisible: true, + disableKeychainAccess: false, + providerMetrics: {}, + floatBarEnabled: false, + floatBarOpacity: 0.9, + floatBarScale: 100, + floatBarOrientation: "horizontal", + floatBarStyle: "floating", + floatBarClickThrough: false, + floatBarProviderIds: [], + floatBarDarkText: false, + floatBarShowResetInline: false, + floatBarShowCost: false, + claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, + alibabaTokenPlanRegion: "cn", + weeklyProgressWorkDays: null, + costSummaryDisplayStyle: "compact", + providerAccentColors: {}, +}; + +describe("AdvancedTab", () => { + beforeEach(() => { + vi.clearAllMocks(); + tauriMocks.getSafeDiagnostics.mockResolvedValue("diagnostics text"); + }); + + it("copies safe diagnostics to the clipboard", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.assign(navigator, { clipboard: { writeText } }); + + render(); + + const copyButtons = screen.getAllByRole("button", { + name: "DiagnosticsCopyButton", + }); + fireEvent.click(copyButtons[copyButtons.length - 1]); + + await waitFor(() => { + expect(tauriMocks.getSafeDiagnostics).toHaveBeenCalled(); + }); + await waitFor(() => { + expect(writeText).toHaveBeenCalledWith("diagnostics text"); + }); + await waitFor(() => { + expect(screen.getAllByText("DiagnosticsCopied").length).toBeGreaterThan(0); + }); + }); + + it("shows an error when copying diagnostics fails", async () => { + tauriMocks.getSafeDiagnostics.mockRejectedValue(new Error("invoke failed")); + + render(); + + const copyButtons = screen.getAllByRole("button", { + name: "DiagnosticsCopyButton", + }); + fireEvent.click(copyButtons[copyButtons.length - 1]); + + await waitFor(() => { + expect( + screen.getAllByText(/DiagnosticsCopyFailed/).length, + ).toBeGreaterThan(0); + }); + }); +}); diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx index ec3a96742c..09e8ed563a 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { useLocale } from "../../../hooks/useLocale"; import { + getSafeDiagnostics, registerGlobalShortcut, unregisterGlobalShortcut, } from "../../../lib/tauri"; @@ -22,10 +23,12 @@ function parseCodexSessionsDirs(value: string): string[] { function parseSshHosts(value: string): string[] { return value.split(/[,\n]/).map((host) => host.trim()).filter(Boolean); } - export default function AdvancedTab({ settings, set, saving }: TabProps) { const { t } = useLocale(); const [shortcutError, setShortcutError] = useState(null); + const [diagnosticsStatus, setDiagnosticsStatus] = useState( + null, + ); const [codexDirsDraft, setCodexDirsDraft] = useState(() => formatCodexSessionsDirs(settings.codexCustomSessionsDirs), ); @@ -33,6 +36,20 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) { (settings.agentSessionSshHosts ?? []).join(", "), ); + const copyDiagnostics = useCallback(async () => { + try { + const text = await getSafeDiagnostics(); + await navigator.clipboard.writeText(text); + setDiagnosticsStatus(t("DiagnosticsCopied")); + } catch (error) { + setDiagnosticsStatus(`${t("DiagnosticsCopyFailed")} ${String(error)}`); + } + }, [t]); + + const commitCodexDirs = useCallback(() => { + set({ codexCustomSessionsDirs: parseCodexSessionsDirs(codexDirsDraft) }); + }, [codexDirsDraft, set]); + useEffect(() => { if (!saving) { setCodexDirsDraft(formatCodexSessionsDirs(settings.codexCustomSessionsDirs)); @@ -66,9 +83,6 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) { } }, [set]); - const commitCodexDirs = useCallback(() => { - set({ codexCustomSessionsDirs: parseCodexSessionsDirs(codexDirsDraft) }); - }, [codexDirsDraft, set]); return ( <> @@ -314,6 +328,25 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) { + + {/* ── Diagnostics ──────────────────────────────────────────── */} +

+

+ {t("DiagnosticsCopyButton")} +

+
+ + {diagnosticsStatus && ( +

{diagnosticsStatus}

+ )} +
+
); } diff --git a/rust/src/locale.rs b/rust/src/locale.rs index 6eb2e3a40f..84892747e9 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -1044,6 +1044,9 @@ locale_keys! { AboutLinkGitHub, AboutLinkWebsite, AboutLinkOriginalProject, + DiagnosticsCopyButton, + DiagnosticsCopied, + DiagnosticsCopyFailed, // Tauri desktop shell — Cookies tab hints / placeholder SavedCookiesHint, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index a29757dfd3..8c0a095fbf 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -725,6 +725,9 @@ AboutCopyrightAfter = by steipete. MIT License. AboutLinkGitHub = GitHub AboutLinkWebsite = Website AboutLinkOriginalProject = Original Project +DiagnosticsCopyButton = Copy diagnostics +DiagnosticsCopied = Diagnostics copied to clipboard +DiagnosticsCopyFailed = Could not copy diagnostics SavedCookiesHint = Manual cookie overrides for browser-authenticated providers. These are used when automatic browser cookie extraction is unavailable. ImportFromBrowserHint = Extract cookies automatically from a signed-in browser. The browser must be installed on this machine and you must be signed in to the provider in that browser. NoBrowsersDetectedHint = No supported browsers detected on this machine, or automatic cookie extraction is unavailable (requires Windows with Chrome, Edge, Brave, or Firefox installed). Use the manual paste form below instead. diff --git a/rust/src/logging.rs b/rust/src/logging.rs index 76572a98b8..a5631b5536 100755 --- a/rust/src/logging.rs +++ b/rust/src/logging.rs @@ -1,5 +1,13 @@ //! Logging configuration using tracing +//! +//! Logging writes to stderr always and additionally to a size-capped file +//! under the app logs directory when that directory is writable. Any +//! filesystem error degrades to stderr-only logging; startup must never fail +//! because logs cannot be written. +use std::io::Write as _; +use std::path::PathBuf; +use std::sync::LazyLock; use tracing_subscriber::{EnvFilter, fmt, prelude::*}; /// Convert a displayable error into a frontend/log-safe message. @@ -7,6 +15,112 @@ pub fn safe_error_message(err: impl std::fmt::Display) -> String { crate::core::SecretRedactor::redact(&err.to_string()) } +/// Settings directory that hosts the app settings file (also the log root). +/// +/// Mirrors 's base without +/// importing the settings module (keeps logging self-contained). +pub fn settings_dir() -> Option { + dirs::config_dir().map(|p| p.join("CodexBar")) +} + +/// Path of the current in-app log file, if a settings dir is resolvable. +pub fn log_file_path() -> Option { + settings_dir().map(|p| p.join("logs").join("codexbar.log")) +} + +/// Maximum size of the current log file before rotation to the single backup file. +pub const LOG_MAX_BYTES: u64 = 1024 * 1024; + +/// Lines returned by the log-tail helper. +pub const LOG_TAIL_LINES: usize = 200; + +// -- Size-capped file writer ------------------------------------------------- + +struct CappedFileWriter { + inner: std::sync::Mutex>, + path: PathBuf, + max_bytes: u64, +} + +impl CappedFileWriter { + fn new(path: PathBuf, max_bytes: u64) -> Option { + // Best-effort creation; a failure here degrades to stderr-only. + let dir = path.parent()?; + if std::fs::create_dir_all(dir).is_err() { + return None; + } + Some(Self { + inner: std::sync::Mutex::new(None), + path, + max_bytes, + }) + } + + fn open_or_replace(&self) -> Option { + std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.path) + .ok() + } + + /// Rotates to the single backup file once the current file exceeds the cap; + /// every fs error degrades to a no-op so the tracing layer never fails on logging. + fn append(&self, line: &[u8]) { + let mut guard = match self.inner.lock() { + Ok(g) => g, + Err(_) => return, + }; + let over_cap = self.path.metadata().map(|m| m.len()).unwrap_or(0) > self.max_bytes; + if over_cap { + // Close the cached handle first: on Windows an open handle blocks + // the rename, and a handle left open after a successful rename + // would keep writing into the backup file. + drop(guard.take()); + let backup = self.path.with_extension("log.1"); + // Renaming replaces an existing destination on Windows and Unix, so + // the old backup is overwritten in one call. + if std::fs::rename(&self.path, &backup).is_err() { + // Rotation is best-effort; keep appending to the current file. + } + } + if guard.is_none() { + *guard = self.open_or_replace(); + } + if let Some(file) = guard.as_mut() { + let _ignored = file.write_all(line); + let _ignored2 = file.flush(); + } + } +} +impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for &'a CappedFileWriter { + type Writer = &'a CappedFileWriter; + + fn make_writer(&'a self) -> Self::Writer { + self + } +} + +impl std::io::Write for &'_ CappedFileWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.append(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +// -- Wiring ------------------------------------------------------------------- + +static WRITER: LazyLock> = + LazyLock::new(|| log_file_path().and_then(|p| CappedFileWriter::new(p, LOG_MAX_BYTES))); + +fn file_writer() -> Option<&'static CappedFileWriter> { + WRITER.as_ref() +} + /// Initialize the logging system pub fn init(verbose: bool, json: bool) -> anyhow::Result<()> { let filter = if verbose { @@ -15,17 +129,163 @@ pub fn init(verbose: bool, json: bool) -> anyhow::Result<()> { EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")) }; - if json { - tracing_subscriber::registry() - .with(filter) - .with(fmt::layer().json().with_writer(std::io::stderr)) - .init(); + let file_writer = file_writer(); + let stderr_layer = if json { + fmt::layer().json().with_writer(std::io::stderr).boxed() } else { - tracing_subscriber::registry() + fmt::layer().with_writer(std::io::stderr).boxed() + }; + + match file_writer { + Some(w) => tracing_subscriber::registry() + .with(filter) + .with(stderr_layer) + .with(fmt::layer().with_writer(move || w)) + .init(), + None => tracing_subscriber::registry() .with(filter) - .with(fmt::layer().with_writer(std::io::stderr)) - .init(); + .with(stderr_layer) + .init(), } Ok(()) } + +/// Install a panic hook that best-effort logs panics to the app log file and +/// then chains to the previous default hook. The hook itself never panics. +pub fn install_panic_hook() { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + let hook_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let message = if let Some(s) = info.payload().downcast_ref::<&str>() { + (*s).to_string() + } else if let Some(s) = info.payload().downcast_ref::() { + s.clone() + } else { + "panic payload of non-string type".to_string() + }; + let location = match info.location() { + Some(loc) => format!("{}:{}:{}", loc.file(), loc.line(), loc.column()), + None => "unknown location".to_string(), + }; + let backtrace = std::backtrace::Backtrace::force_capture().to_string(); + if let Some(writer) = file_writer() { + writer.append( + safe_error_message(format!( + "panic at {location}: {message}\nbacktrace:\n{backtrace}\n" + )) + .as_bytes(), + ); + } + })); + let _ignored_hook = hook_result; + previous(info); + })); +} + +/// Read the last LOG_TAIL_LINES lines of the current log file, redacted. +pub fn read_log_tail() -> Option { + let path = log_file_path()?; + let content = std::fs::read_to_string(path).ok()?; + let tail: Vec<&str> = content.lines().rev().take(LOG_TAIL_LINES).collect(); + let tail: Vec<&str> = tail.into_iter().rev().collect(); + Some(crate::core::SecretRedactor::redact(&tail.join("\n"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rotation_moves_oversized_file_to_backup() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("codexbar.log"); + std::fs::write(&path, "x".repeat(2048)).expect("seed file"); + let writer = CappedFileWriter::new(path.clone(), 1024).expect("writer"); + writer.append(b"trigger rotation\n"); + let backup = path.with_extension("log.1"); + assert!(backup.exists(), "backup should exist after rotation"); + assert_eq!(std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0), 17); + assert_eq!( + std::fs::metadata(&backup).map(|m| m.len()).unwrap_or(0), + 2048 + ); + } + + #[test] + fn rotation_keeps_small_file_in_place() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("codexbar.log"); + std::fs::write(&path, "small\n").expect("seed file"); + let writer = CappedFileWriter::new(path.clone(), 1024).expect("writer"); + writer.append(b"still small\n"); + assert!( + !path.with_extension("log.1").exists(), + "no rotation below cap" + ); + } + + #[test] + fn rotation_with_warm_handle_recreates_current_file() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("codexbar.log"); + // 60-byte seed + the 11-byte first line = 71 bytes, so the metadata + // check before the second append sees the file over the 64-byte cap. + std::fs::write(&path, "seed\n".repeat(12)).expect("seed file"); + let writer = CappedFileWriter::new(path.clone(), 64).expect("writer"); + // First append opens and caches the handle (warm). + writer.append(b"first line\n"); + // Blow past the cap while the handle is cached. + writer.append(&[b'x'; 128]); + let backup = path.with_extension("log.1"); + assert!( + backup.exists(), + "rotation must move the oversized file to the backup" + ); + // The current file must be recreated and contain only post-rotation + // bytes: the warm handle was dropped before the rename, so nothing + // leaks into the backup or the reopened file. + let current = std::fs::read_to_string(&path).expect("current file recreated"); + assert!( + !current.contains("seed"), + "seed bytes must have been rotated away" + ); + assert_eq!(current, "x".repeat(128)); + } + + #[test] + fn read_log_tail_returns_up_to_max_lines() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("logs").join("codexbar.log"); + std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir"); + let body: String = (0..(LOG_TAIL_LINES + 50)) + .map(|i| format!("line {i}\n")) + .collect(); + std::fs::write(&path, body).expect("seed file"); + let writer = CappedFileWriter::new(path, LOG_MAX_BYTES).expect("writer"); + // read_log_tail reads from log_file_path(); simulate by reading from + // the temp dir file directly through the same line-count logic. + let content = std::fs::read_to_string(writer.path.clone()).expect("read"); + let tail: Vec<&str> = content.lines().rev().take(LOG_TAIL_LINES).collect(); + assert_eq!(tail.len(), LOG_TAIL_LINES); + } + + #[test] + fn panic_hook_returns_cleanly_when_log_path_unwritable() { + let dir = tempfile::tempdir().expect("tempdir"); + // Seed a regular file where a directory would need to be created. + let blocker = dir.path().join("blocker"); + std::fs::write(&blocker, "not a directory").expect("write blocker"); + let bad_path = blocker.join("logs").join("codexbar.log"); + // CappedFileWriter::new fails because create_dir_all cannot succeed. + assert!(CappedFileWriter::new(bad_path, LOG_MAX_BYTES).is_none()); + // The hook body must never panic even with no writer available. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + // Simulate the hook's fallible path with file_writer() == None. + if let Some(writer) = file_writer() { + writer.append(b"should not happen\n"); + } + })); + assert!(result.is_ok()); + } +} From 4182339b364f0df3c332af99f63ac8a9c689885f Mon Sep 17 00:00:00 2001 From: Finesssee <90105158+Finesssee@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:28:30 +0700 Subject: [PATCH 3/5] Apply rustfmt to command_runner test --- rust/src/host/command_runner.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/src/host/command_runner.rs b/rust/src/host/command_runner.rs index 017c396014..0fb7d38181 100755 --- a/rust/src/host/command_runner.rs +++ b/rust/src/host/command_runner.rs @@ -646,7 +646,11 @@ mod tests { let result = runner.run("powershell.exe", None, &options).unwrap(); assert_eq!(result.exit_code, Some(1)); - assert!(result.text.contains("stdout line"), "stdout: {}", result.text); + assert!( + result.text.contains("stdout line"), + "stdout: {}", + result.text + ); assert!( result.stderr.contains("boom diagnostics"), "stderr: {}", From 4fe264c7f61da212a4f9018d78ebd6e0391b5a75 Mon Sep 17 00:00:00 2001 From: Finesssee <90105158+Finesssee@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:53:49 +0700 Subject: [PATCH 4/5] fix(locale): restore AboutLinkOriginalProject clobbered by heading insert --- apps/desktop-tauri/src-tauri/src/main.rs | 4 + apps/desktop-tauri/src/i18n/keys.ts | 1 + .../src/surfaces/settings/tabs/AboutTab.tsx | 8 +- .../settings/tabs/AdvancedTab.test.tsx | 18 +-- .../surfaces/settings/tabs/AdvancedTab.tsx | 3 +- rust/src/locale.rs | 1 + rust/src/locale/en-US.ftl | 1 + rust/src/logging.rs | 152 +++++++++++++----- rust/src/settings.rs | 4 +- 9 files changed, 128 insertions(+), 64 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index 709f324de4..e7e1cfebda 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -110,6 +110,10 @@ fn should_suppress_blur_dismiss(launch: LaunchBehavior, proof_mode: bool) -> boo } fn main() { + // Per-process log file names: the shell writes codexbar-desktop.log so + // its cached handle never blocks the CLI's rotation on Windows. + // SAFETY: runs before any thread spawns; no concurrent env access exists. + unsafe { std::env::set_var("CODEXBAR_PROCESS", "desktop") }; codexbar::logging::install_panic_hook(); codexbar::logging::init(false, false).expect("failed to initialize logging"); diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index d6359ce013..63ff983b4b 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -764,6 +764,7 @@ export const ALL_LOCALE_KEYS = [ "AboutLinkGitHub", "AboutLinkWebsite", "AboutLinkOriginalProject", + "DiagnosticsSectionHeading", "DiagnosticsCopyButton", "DiagnosticsCopied", "DiagnosticsCopyFailed", diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.tsx index f1e1962d95..c6e62e429c 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.tsx @@ -8,13 +8,7 @@ import type { LocaleKey } from "../../../i18n/keys"; import type { TabProps } from "../settingsTabs"; import codexbarIcon from "../../../assets/codexbar-icon.png"; -const REPO_URL = [ - "https:", - "", - "github.com", - "nesszer", - "Win-CodexBar", -].join("/"); +const REPO_URL = "https://github.com/nesszer/Win-CodexBar"; const SUBMIT_ISSUE_URL = `${REPO_URL}/issues/new?labels=bug&template=bug_report.yml`; const ABOUT_LINKS: ReadonlyArray<{ labelKey: LocaleKey; url: string }> = [ diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx index 195ab7e89a..e3ac4dd2a6 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx @@ -93,10 +93,10 @@ describe("AdvancedTab", () => { render(); - const copyButtons = screen.getAllByRole("button", { - name: "DiagnosticsCopyButton", - }); - fireEvent.click(copyButtons[copyButtons.length - 1]); + screen.getByRole("heading", { name: "DiagnosticsSectionHeading" }); + fireEvent.click( + screen.getByRole("button", { name: "DiagnosticsCopyButton" }), + ); await waitFor(() => { expect(tauriMocks.getSafeDiagnostics).toHaveBeenCalled(); @@ -111,14 +111,12 @@ describe("AdvancedTab", () => { it("shows an error when copying diagnostics fails", async () => { tauriMocks.getSafeDiagnostics.mockRejectedValue(new Error("invoke failed")); - render(); - const copyButtons = screen.getAllByRole("button", { - name: "DiagnosticsCopyButton", - }); - fireEvent.click(copyButtons[copyButtons.length - 1]); - + screen.getByRole("heading", { name: "DiagnosticsSectionHeading" }); + fireEvent.click( + screen.getByRole("button", { name: "DiagnosticsCopyButton" }), + ); await waitFor(() => { expect( screen.getAllByText(/DiagnosticsCopyFailed/).length, diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx index 09e8ed563a..bcae463719 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.tsx @@ -23,6 +23,7 @@ function parseCodexSessionsDirs(value: string): string[] { function parseSshHosts(value: string): string[] { return value.split(/[,\n]/).map((host) => host.trim()).filter(Boolean); } + export default function AdvancedTab({ settings, set, saving }: TabProps) { const { t } = useLocale(); const [shortcutError, setShortcutError] = useState(null); @@ -332,7 +333,7 @@ export default function AdvancedTab({ settings, set, saving }: TabProps) { {/* ── Diagnostics ──────────────────────────────────────────── */}

- {t("DiagnosticsCopyButton")} + {t("DiagnosticsSectionHeading")}