Compare commits
10 Commits
f0bd2a28a2
...
bb6a12cd41
| Author | SHA1 | Date | |
|---|---|---|---|
| bb6a12cd41 | |||
|
|
af1ca868f7 | ||
|
|
7699b1a58c | ||
|
|
ec07afed50 | ||
|
|
c14104374b | ||
|
|
2e8d05ae34 | ||
|
|
8062658e29 | ||
|
|
560b3083f0 | ||
|
|
acc0cb3f06 | ||
|
|
f264f560b1 |
@@ -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
2
src-tauri/Cargo.lock
generated
@@ -853,7 +853,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||
|
||||
[[package]]
|
||||
name = "codeg"
|
||||
version = "0.10.3"
|
||||
version = "0.10.4"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"async-trait",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codeg"
|
||||
version = "0.10.3"
|
||||
version = "0.10.4"
|
||||
description = "Agent Code Generation App"
|
||||
authors = ["feitao"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
BIN
src/fonts/SourceCodePro-Black.ttf
Normal file
BIN
src/fonts/SourceCodePro-Black.ttf
Normal file
Binary file not shown.
BIN
src/fonts/SourceCodePro-Medium.ttf
Normal file
BIN
src/fonts/SourceCodePro-Medium.ttf
Normal file
Binary file not shown.
BIN
src/fonts/SourceCodePro-Regular.ttf
Normal file
BIN
src/fonts/SourceCodePro-Regular.ttf
Normal file
Binary file not shown.
Reference in New Issue
Block a user