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",
|
"name": "codeg",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.10.3",
|
"version": "0.10.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -853,7 +853,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codeg"
|
name = "codeg"
|
||||||
version = "0.10.3"
|
version = "0.10.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-client-protocol-schema",
|
"agent-client-protocol-schema",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "codeg"
|
name = "codeg"
|
||||||
version = "0.10.3"
|
version = "0.10.4"
|
||||||
description = "Agent Code Generation App"
|
description = "Agent Code Generation App"
|
||||||
authors = ["feitao"]
|
authors = ["feitao"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use crate::acp::error::AcpError;
|
use crate::acp::error::AcpError;
|
||||||
use crate::acp::registry;
|
use crate::acp::registry;
|
||||||
use crate::models::agent::AgentType;
|
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> {
|
pub(crate) fn cache_dir() -> Result<PathBuf, AcpError> {
|
||||||
let base = dirs::cache_dir()
|
let base = dirs::cache_dir()
|
||||||
.ok_or_else(|| AcpError::DownloadFailed("cannot determine cache directory".into()))?;
|
.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> {
|
pub fn clear_agent_cache(agent_type: AgentType) -> Result<(), AcpError> {
|
||||||
let agent_id = agent_cache_key(agent_type);
|
let agent_id = agent_cache_key(agent_type);
|
||||||
let dir = cache_dir()?.join(agent_id);
|
let dir = cache_dir()?.join(&agent_id);
|
||||||
if dir.exists() {
|
if !dir.exists() {
|
||||||
std::fs::remove_dir_all(&dir)
|
return Ok(());
|
||||||
.map_err(|e| AcpError::DownloadFailed(format!("failed to clear cache: {e}")))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(())
|
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> {
|
fn installed_binary_path(agent_id: &str, version: &str, cmd_name: &str) -> Option<PathBuf> {
|
||||||
let bin_name = if cfg!(target_os = "windows") {
|
let bin_name = if cfg!(target_os = "windows") {
|
||||||
format!("{cmd_name}.exe")
|
format!("{cmd_name}.exe")
|
||||||
|
|||||||
@@ -2892,9 +2892,15 @@ fn emit_conversation_update(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
SessionUpdate::AvailableCommandsUpdate(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
|
let commands: Vec<AvailableCommandInfo> = update
|
||||||
.available_commands
|
.available_commands
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|cmd| seen.insert(cmd.name.clone()))
|
||||||
.map(|cmd| {
|
.map(|cmd| {
|
||||||
let input_hint = cmd.input.as_ref().map(|input| match input {
|
let input_hint = cmd.input.as_ref().map(|input| match input {
|
||||||
sacp::schema::AvailableCommandInput::Unstructured(u) => u.hint.clone(),
|
sacp::schema::AvailableCommandInput::Unstructured(u) => u.hint.clone(),
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn async_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")
|
let port: u16 = std::env::var("CODEG_PORT")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ mod terminal;
|
|||||||
pub mod web;
|
pub mod web;
|
||||||
pub mod workspace_state;
|
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")]
|
#[cfg(feature = "tauri-runtime")]
|
||||||
mod tauri_app {
|
mod tauri_app {
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
@@ -46,7 +54,10 @@ mod tauri_app {
|
|||||||
/// libraries like reqwest/rustls that read `HTTP_PROXY` etc.
|
/// libraries like reqwest/rustls that read `HTTP_PROXY` etc.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn apply_webview2_rendering_override() {
|
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";
|
const ENV_KEY: &str = "WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS";
|
||||||
|
|
||||||
let prefs = crate::preferences::load();
|
let prefs = crate::preferences::load();
|
||||||
@@ -66,7 +77,11 @@ mod tauri_app {
|
|||||||
tokens.push(arg.to_string());
|
tokens.push(arg.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::env::set_var(ENV_KEY, tokens.join(" "));
|
// 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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -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_zoom(&db.conn));
|
||||||
tauri::async_runtime::block_on(windows::load_saved_appearance_mode(&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
|
// Install bundled expert skills into the central store
|
||||||
// (`~/.codeg/skills/`). Runs in the background and does
|
// (`~/.codeg/skills/`). Runs in the background and does
|
||||||
// not block startup; failures are logged but non-fatal.
|
// not block startup; failures are logged but non-fatal.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "codeg",
|
"productName": "codeg",
|
||||||
"version": "0.10.3",
|
"version": "0.10.4",
|
||||||
"identifier": "app.codeg",
|
"identifier": "app.codeg",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Metadata, Viewport } from "next"
|
import type { Metadata, Viewport } from "next"
|
||||||
import "katex/dist/katex.min.css"
|
import "katex/dist/katex.min.css"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
import { JetBrains_Mono } from "next/font/google"
|
import localFont from "next/font/local"
|
||||||
import { NextIntlClientProvider } from "next-intl"
|
import { NextIntlClientProvider } from "next-intl"
|
||||||
import { AppI18nProvider } from "@/components/i18n-provider"
|
import { AppI18nProvider } from "@/components/i18n-provider"
|
||||||
import { getMessagesForLocale } from "@/i18n/messages"
|
import { getMessagesForLocale } from "@/i18n/messages"
|
||||||
@@ -12,8 +12,12 @@ import { APPEARANCE_INIT_SCRIPT } from "@/lib/appearance-script"
|
|||||||
import { AppearanceProvider } from "@/components/appearance-provider"
|
import { AppearanceProvider } from "@/components/appearance-provider"
|
||||||
import { OverlayScrollbarsInit } from "@/components/overlay-scrollbars-init"
|
import { OverlayScrollbarsInit } from "@/components/overlay-scrollbars-init"
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
const sourceCodePro = localFont({
|
||||||
subsets: ["latin"],
|
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",
|
variable: "--font-sans",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -47,7 +51,7 @@ export default async function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang={initialLocale}
|
lang={initialLocale}
|
||||||
className={jetbrainsMono.variable}
|
className={sourceCodePro.variable}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -551,6 +551,26 @@ function sameCommands(
|
|||||||
return true
|
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(
|
function applyStreamingAction(
|
||||||
conn: ConnectionState,
|
conn: ConnectionState,
|
||||||
action: StreamingAction
|
action: StreamingAction
|
||||||
@@ -1194,11 +1214,12 @@ function connectionsReducer(
|
|||||||
case "AVAILABLE_COMMANDS": {
|
case "AVAILABLE_COMMANDS": {
|
||||||
const conn = state.get(action.contextKey)
|
const conn = state.get(action.contextKey)
|
||||||
if (!conn) return state
|
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)
|
const next = new Map(state)
|
||||||
next.set(action.contextKey, {
|
next.set(action.contextKey, {
|
||||||
...conn,
|
...conn,
|
||||||
availableCommands: action.commands,
|
availableCommands: commands,
|
||||||
})
|
})
|
||||||
return next
|
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