Compare commits

...

10 Commits

Author SHA1 Message Date
bb6a12cd41 add: Replace web fonts with local fonts 2026-04-26 10:15:34 +08:00
xintaofei
af1ca868f7 # Release version 0.10.4
- feat(settings): add a Windows toggle to disable WebView2 hardware acceleration, helping users work around white-screen and rendering glitches on certain GPUs.
- fix(acp): uninstall before reinstall when upgrading npx-managed agents to avoid stale binaries.
- fix(acp): bypass Windows file locks when clearing the binary cache so re-installation no longer fails.
- fix(acp): dedupe slash command results at the session update mapping layer to prevent duplicated entries in the slash menu. Thanks to @MoozLee for contributing this fix in #111.

-----------------------------
# 发布版本 0.10.4

- 新增(设置):Windows 端新增禁用 WebView2 硬件加速的开关,便于规避部分显卡导致的白屏与渲染异常;
- 修复(ACP):通过 npx 升级代理时先卸载再安装,避免残留旧版本二进制;
- 修复(ACP):清理二进制缓存时绕过 Windows 文件锁,避免重新安装失败;
- 修复(ACP):在会话更新映射阶段对斜杠命令结果去重,消除斜杠菜单中的重复项。感谢 @MoozLee 在 #111 中贡献此修复。
2026-04-25 21:38:24 +08:00
xintaofei
7699b1a58c fix(settings): wrap WebView2 env override in unsafe block for Rust 1.82+ 2026-04-25 21:33:39 +08:00
xintaofei
ec07afed50 fix(acp): dedupe slash commands at session update mapping 2026-04-25 17:58:41 +08:00
XinTaoFei
c14104374b Merge pull request #111 from MoozLee/fix/slash-command-search-main
fix(acp): dedupe slash command results
2026-04-25 17:36:10 +08:00
xintaofei
2e8d05ae34 fix(settings): use --disable-gpu for hardware acceleration toggle to align with Tauri 2 ecosystem 2026-04-25 17:20:02 +08:00
lee
8062658e29 fix(acp): dedupe slash command results [T-04-25-fix-slash-command-search] 2026-04-25 16:40:13 +08:00
xintaofei
560b3083f0 fix(settings): use --disable-gpu-compositing to keep webview renderable 2026-04-25 16:32:47 +08:00
xintaofei
acc0cb3f06 fix(settings): drop --disable-software-rasterizer to avoid white-screen webview 2026-04-25 16:04:00 +08:00
xintaofei
f264f560b1 fix(acp): bypass Windows file locks when clearing binary cache
clear_agent_cache falls back to renaming the agent cache directory
to <cache_dir>/.trash/<agent_id>-<nanos>-<counter>/ when
remove_dir_all fails — typically because a running <cmd>.exe or a
Defender scan holds the file. NTFS rename succeeds on directories
whose children are locked, so Upgrade and Uninstall no longer
surface ERROR_ACCESS_DENIED on Windows.

A detached OS thread sweeps .trash/ at startup with all errors
swallowed, panics caught, and no shared state — cannot block app
startup or leak threads. Still-locked entries are left for the
next launch.
2026-04-25 15:56:47 +08:00
13 changed files with 130 additions and 16 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "codeg",
"private": true,
"version": "0.10.3",
"version": "0.10.4",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",

2
src-tauri/Cargo.lock generated
View File

@@ -853,7 +853,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "codeg"
version = "0.10.3"
version = "0.10.4"
dependencies = [
"agent-client-protocol-schema",
"async-trait",

View File

@@ -1,6 +1,6 @@
[package]
name = "codeg"
version = "0.10.3"
version = "0.10.4"
description = "Agent Code Generation App"
authors = ["feitao"]
edition = "2021"

View File

@@ -1,11 +1,18 @@
use std::collections::HashSet;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::acp::error::AcpError;
use crate::acp::registry;
use crate::models::agent::AgentType;
/// Process-local counter appended to rename-aside trash directory names. Guards
/// against the rare case where two `clear_agent_cache` calls land in the same
/// `SystemTime::now()` tick (Windows `GetSystemTimePreciseAsFileTime` has ~100ns
/// resolution) and would otherwise collide on the rename target.
static TRASH_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) fn cache_dir() -> Result<PathBuf, AcpError> {
let base = dirs::cache_dir()
.ok_or_else(|| AcpError::DownloadFailed("cannot determine cache directory".into()))?;
@@ -44,14 +51,56 @@ pub(crate) fn binary_dir(agent_id: &str, version: &str) -> Result<PathBuf, AcpEr
pub fn clear_agent_cache(agent_type: AgentType) -> Result<(), AcpError> {
let agent_id = agent_cache_key(agent_type);
let dir = cache_dir()?.join(agent_id);
if dir.exists() {
std::fs::remove_dir_all(&dir)
.map_err(|e| AcpError::DownloadFailed(format!("failed to clear cache: {e}")))?;
let dir = cache_dir()?.join(&agent_id);
if !dir.exists() {
return Ok(());
}
if std::fs::remove_dir_all(&dir).is_ok() {
return Ok(());
}
// Windows: a running `<cmd>.exe` (ours or anti-virus scanning it) keeps the
// file locked, so `remove_dir_all` returns ERROR_ACCESS_DENIED. NTFS allows
// renaming a directory whose children are locked because rename only
// updates the parent directory entry; the locked file's FILE_OBJECT keeps
// working under the new path. The aside is swept on next startup.
let trash_root = cache_dir()?.join(".trash");
let _ = std::fs::create_dir_all(&trash_root);
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let counter = TRASH_COUNTER.fetch_add(1, Ordering::Relaxed);
let aside = trash_root.join(format!("{agent_id}-{stamp}-{counter}"));
std::fs::rename(&dir, &aside)
.map_err(|e| AcpError::DownloadFailed(format!("failed to clear cache: {e}")))?;
let _ = std::fs::remove_dir_all(&aside);
Ok(())
}
/// Best-effort cleanup of trash directories left behind by
/// `clear_agent_cache`'s rename-aside fallback. Designed to be run from a
/// detached OS thread at startup: every error path is silently swallowed,
/// no logs, no panics escape, no subprocesses spawned. Whatever cannot be
/// removed (e.g. a binary still locked by an external process) is left for
/// the next startup.
///
/// Iterates children rather than nuking the parent so that a concurrent
/// `clear_agent_cache` racing to rename a fresh entry into `.trash/` cannot
/// have its target directory yanked out from under it.
pub fn sweep_trash() {
let Ok(base) = cache_dir() else { return };
let trash = base.join(".trash");
let Ok(entries) = std::fs::read_dir(&trash) else {
return;
};
for entry in entries.flatten() {
let _ = std::fs::remove_dir_all(entry.path());
}
}
fn installed_binary_path(agent_id: &str, version: &str, cmd_name: &str) -> Option<PathBuf> {
let bin_name = if cfg!(target_os = "windows") {
format!("{cmd_name}.exe")

View File

@@ -2892,9 +2892,15 @@ fn emit_conversation_update(
);
}
SessionUpdate::AvailableCommandsUpdate(update) => {
// Some agents (e.g. Claude Code with overlapping user/project slash
// commands) emit duplicate entries sharing the same name. Keep the
// first occurrence so downstream consumers don't render duplicates;
// the frontend reducer also dedupes as a defensive measure.
let mut seen = HashSet::new();
let commands: Vec<AvailableCommandInfo> = update
.available_commands
.iter()
.filter(|cmd| seen.insert(cmd.name.clone()))
.map(|cmd| {
let input_hint = cmd.input.as_ref().map(|input| match input {
sacp::schema::AvailableCommandInput::Unstructured(u) => u.hint.clone(),

View File

@@ -29,6 +29,15 @@ fn main() {
}
async fn async_main() {
// Sweep stale ACP binary cache trash (rename-aside fallback artifacts).
// Detached OS thread: cannot block startup, panics are caught and dropped,
// errors are silenced, no subprocesses spawned.
std::thread::spawn(|| {
let _ = std::panic::catch_unwind(|| {
codeg_lib::sweep_acp_binary_trash();
});
});
let port: u16 = std::env::var("CODEG_PORT")
.ok()
.and_then(|v| v.parse().ok())

View File

@@ -17,6 +17,14 @@ mod terminal;
pub mod web;
pub mod workspace_state;
/// Sweep stale ACP binary cache trash created by the rename-aside fallback in
/// `acp::binary_cache::clear_agent_cache`. Safe to call any time; intended to
/// be invoked once at startup from a detached OS thread. Does not block, does
/// not panic, errors are silently dropped.
pub fn sweep_acp_binary_trash() {
crate::acp::binary_cache::sweep_trash();
}
#[cfg(feature = "tauri-runtime")]
mod tauri_app {
use std::sync::atomic::{AtomicBool, Ordering};
@@ -46,7 +54,10 @@ mod tauri_app {
/// libraries like reqwest/rustls that read `HTTP_PROXY` etc.
#[cfg(target_os = "windows")]
fn apply_webview2_rendering_override() {
const DISABLE_GPU_ARGS: [&str; 2] = ["--disable-gpu", "--disable-software-rasterizer"];
// Matches the dominant pattern across the Tauri 2 ecosystem (Dorion,
// Seelen-UI, and most production Tauri 2 apps that ship a "disable
// hardware acceleration" toggle all use `--disable-gpu`).
const DISABLE_GPU_ARGS: [&str; 1] = ["--disable-gpu"];
const ENV_KEY: &str = "WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS";
let prefs = crate::preferences::load();
@@ -66,8 +77,12 @@ mod tauri_app {
tokens.push(arg.to_string());
}
}
// SAFETY: called before any tokio worker or plugin thread spawns, so
// no concurrent `getenv` can race. `set_var` is `unsafe` since Rust 1.82.
unsafe {
std::env::set_var(ENV_KEY, tokens.join(" "));
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -124,6 +139,16 @@ mod tauri_app {
tauri::async_runtime::block_on(windows::load_saved_zoom(&db.conn));
tauri::async_runtime::block_on(windows::load_saved_appearance_mode(&db.conn));
// Sweep stale ACP binary cache trash (rename-aside fallback
// artifacts). Detached OS thread: cannot block startup, panics
// are caught and dropped, errors are silenced, no subprocesses
// spawned. Anything still locked is left for next startup.
std::thread::spawn(|| {
let _ = std::panic::catch_unwind(|| {
crate::sweep_acp_binary_trash();
});
});
// Install bundled expert skills into the central store
// (`~/.codeg/skills/`). Runs in the background and does
// not block startup; failures are logged but non-fatal.

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "codeg",
"version": "0.10.3",
"version": "0.10.4",
"identifier": "app.codeg",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@@ -1,7 +1,7 @@
import type { Metadata, Viewport } from "next"
import "katex/dist/katex.min.css"
import "./globals.css"
import { JetBrains_Mono } from "next/font/google"
import localFont from "next/font/local"
import { NextIntlClientProvider } from "next-intl"
import { AppI18nProvider } from "@/components/i18n-provider"
import { getMessagesForLocale } from "@/i18n/messages"
@@ -12,8 +12,12 @@ import { APPEARANCE_INIT_SCRIPT } from "@/lib/appearance-script"
import { AppearanceProvider } from "@/components/appearance-provider"
import { OverlayScrollbarsInit } from "@/components/overlay-scrollbars-init"
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
const sourceCodePro = localFont({
src: [
{ path: "../../src/fonts/SourceCodePro-Regular.ttf", weight: "400", style: "normal" },
{ path: "../../src/fonts/SourceCodePro-Medium.ttf", weight: "500", style: "normal" },
{ path: "../../src/fonts/SourceCodePro-Black.ttf", weight: "900", style: "normal" },
],
variable: "--font-sans",
})
@@ -47,7 +51,7 @@ export default async function RootLayout({
return (
<html
lang={initialLocale}
className={jetbrainsMono.variable}
className={sourceCodePro.variable}
suppressHydrationWarning
>
<body>

View File

@@ -551,6 +551,26 @@ function sameCommands(
return true
}
function dedupeCommandsByName(
commands: AvailableCommandInfo[]
): AvailableCommandInfo[] {
const seen = new Set<string>()
let deduped: AvailableCommandInfo[] | null = null
for (let i = 0; i < commands.length; i += 1) {
const command = commands[i]
if (seen.has(command.name)) {
deduped ??= commands.slice(0, i)
continue
}
seen.add(command.name)
deduped?.push(command)
}
return deduped ?? commands
}
function applyStreamingAction(
conn: ConnectionState,
action: StreamingAction
@@ -1194,11 +1214,12 @@ function connectionsReducer(
case "AVAILABLE_COMMANDS": {
const conn = state.get(action.contextKey)
if (!conn) return state
if (sameCommands(conn.availableCommands, action.commands)) return state
const commands = dedupeCommandsByName(action.commands)
if (sameCommands(conn.availableCommands, commands)) return state
const next = new Map(state)
next.set(action.contextKey, {
...conn,
availableCommands: action.commands,
availableCommands: commands,
})
return next
}

Binary file not shown.

Binary file not shown.

Binary file not shown.