Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions docs/content/release/26.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: 26.0.0
---

CodeForge v26.0.0 是一次里程碑式的更新:CodeForge 从"按语言运行代码片段"的工具,进化为以**文件 / 项目为中心**的轻量编辑器,并首次内置 **AI 助手**。本次更新带来多标签编辑、文件树侧栏、就地运行、超大文件只读查看、可自定义快捷键,以及接入 Claude / OpenAI / DeepSeek 的 AI 对话与代码生成能力。

CodeForge v26.0.0 is a milestone release: CodeForge evolves from a "run code snippets by language" tool into a lightweight, **file/project-centric** editor, with a built-in **AI assistant** for the first time. This update brings multi-tab editing, a file-tree sidebar, in-place execution, a read-only viewer for huge files, customizable shortcuts, and AI chat & code generation powered by Claude / OpenAI / DeepSeek.

---

## 📦 版本信息 | Release Information

- **项目地址 | Repository**:https://github.com/devlive-community/codeforge
- **官方网站 | Official Website**:https://codeforge.devlive.org/
- **版本号 | Version**:v26.0.0
- **发布日期 | Release Date**:2026年6月4日 | June 4, 2026

---

## 🗂 文件与项目工作区 | Files & Project Workspace

CodeForge 现在可以像编辑器一样打开文件夹、管理多个文件。
CodeForge can now open folders and manage multiple files like an editor.

- **多标签编辑** - 同时打开多个文件/片段,支持拖拽排序、右键菜单(关闭其他 / 关闭右侧 / 复制路径)
- **左侧文件树侧栏** - 打开文件夹、目录懒加载、当前文件高亮、宽度可拖拽、长文件名横向滚动
- **打开 / 保存本地文件** - 按扩展名自动识别语言;同扩展名多引擎时智能保留当前语言
- **文件树右键操作** - 新建文件/文件夹、重命名、删除、在系统文件管理器中显示
- **文件监听** - 外部改动自动刷新文件树
- **最近文件夹与会话恢复** - 记住并恢复上次打开的文件夹与文件标签
- **快速打开(Cmd/Ctrl+P)** - 模糊查找当前文件夹内的文件

**Multi-tab editing** with drag-to-reorder and a tab context menu (close others / close to the right / copy path)
**File-tree sidebar** — open folder, lazy-loaded directories, active-file highlight, resizable width, horizontal scroll for long names
**Open / save local files** with automatic language detection by extension
**File operations** — create file/folder, rename, delete, reveal in system file manager
**File watching** — auto-refresh the tree on external changes
**Recent folders & session restore** — remember and restore the last folder and open tabs
**Quick open (Cmd/Ctrl+P)** — fuzzy-find files in the current folder

### 超大文件只读查看 | Read-only Viewer for Huge Files

- 打开超过可编辑上限的文本文件时,自动以**只读模式**在编辑器内查看
- 基于**行偏移索引 + 虚拟滚动**,无论文件多大,滚动到任意位置都流畅
- 可编辑打开的文件大小上限可在设置中调整

Open large text files in a **read-only** in-editor view, backed by a **line-offset index + virtual scrolling** for smooth scrolling at any position regardless of file size. The editable size limit is configurable in settings.

---

## 🤖 AI 助手 | AI Assistant

首次内置 AI 能力,请求经本地后端转发以规避跨域,API Key 仅保存在本机。
Built-in AI for the first time. Requests are proxied through the local backend to avoid CORS; API keys are stored only on your machine.

- **多服务商** - 支持 Claude (Anthropic) / OpenAI / DeepSeek,可自定义模型与接口地址
- **流式输出** - 回复逐字呈现,支持随时**停止生成**
- **Markdown 渲染** - 回复与代码块美观呈现,代码块支持**复制 / 一键应用到编辑器**
- **与执行历史绑定** - AI 对话关联到具体的执行记录并持久化;未执行的对话为临时会话不保存
- **一键分析报错** - 运行失败后可直接让 AI 分析报错并给出修复
- **编辑器内自然语言生成(Cmd/Ctrl+K)** - 用自然语言描述需求,生成结果可**编辑确认**后再插入光标处

**Multiple providers** — Claude (Anthropic) / OpenAI / DeepSeek with customizable model and endpoint
**Streaming output** — token-by-token responses with a **stop** button
**Markdown rendering** — replies and code blocks render nicely; code blocks support **copy / apply to editor**
**Bound to execution history** — conversations are linked to a specific run and persisted; unrun chats stay temporary
**One-click error analysis** — ask AI to analyze a failed run and suggest a fix
**In-editor natural-language generation (Cmd/Ctrl+K)** — describe what you want, review/edit the result, then insert at the cursor

---

## ▶️ 执行增强 | Execution Enhancements

- **就地运行** - 关联本地文件时直接运行该文件,工作目录设为文件所在目录,多文件/相对路径正确
- **task_id 任务路由** - 执行与事件按任务 id 路由,支持多标签并发运行、互不串扰
- **标准输入与运行参数** - 可为程序提供 stdin,并追加运行参数
- **运行未保存文件策略** - 自动保存 / 每次询问 / 运行临时副本,可在设置中选择
- **执行历史** - 基于 SQLite 持久化,支持分页浏览、查看代码与输出、恢复代码

**In-place execution** — run the associated file directly, with the working directory set to the file's folder
**Task-based routing** — execution and events are routed by task id, enabling concurrent runs across tabs
**Stdin & run arguments** — feed stdin to the program and append run arguments
**Unsaved-run strategy** — auto-save / ask each time / run a temporary copy (configurable)
**Execution history** — persisted in SQLite with pagination, code/output inspection, and restore

---

## 🎨 编辑器与布局 | Editor & Layout

- **多种布局** - 编辑器与控制台支持左右 / 上下 / 仅编辑器三种布局
- **字体缩放** - 编辑器内 Cmd/Ctrl 加减号实时调整字号
- **编辑增强** - 搜索替换、代码折叠、括号匹配
- **统一 Tooltip** - 工具栏图标按钮统一悬停提示

**Multiple layouts** — editor & console in side-by-side / top-bottom / editor-only modes
**Font zoom** — adjust font size with Cmd/Ctrl +/- in the editor
**Editing enhancements** — search & replace, code folding, bracket matching
**Unified tooltips** for toolbar icon buttons

---

## ⌨️ 快捷键 | Keyboard Shortcuts

- **全局快捷键** - 运行、保存、另存为、打开、新建/关闭标签、切换侧栏、快速打开、AI 生成等
- **可自定义** - 新增"快捷键"设置页,所有快捷键可重新绑定

**Global shortcuts** — run, save, save as, open, new/close tab, toggle sidebar, quick open, AI generate, and more
**Customizable** — a new "Shortcuts" settings page lets you rebind any shortcut

---

## 🔧 其它优化与修复 | Other Improvements & Fixes

- **设置整理** - 新增 AI、快捷键设置页;语言列表支持按名称筛选;运行与文件相关设置归入通用
- **切换语言不再清空已写代码** - 各语言独立保留编辑内容
- **构建与工程** - CI 升级 Node 22 并修复依赖安装、对齐 Tauri 相关依赖版本、修复多处 UI 细节与告警

**Settings cleanup** — new AI and Shortcuts pages; language list filtering; run/file settings moved to General
**Language switching no longer clears your code** — each language keeps its own buffer
**Build & engineering** — CI upgraded to Node 22 with dependency fixes, aligned Tauri dependency versions, and numerous UI/lint fixes

---

## 📥 立即下载 | Download Now

在 [GitHub Releases](https://github.com/devlive-community/codeforge/releases) 下载最新版本,或访问[官方网站](https://codeforge.devlive.org/)了解更多信息。

Download the latest version from [GitHub Releases](https://github.com/devlive-community/codeforge/releases), or visit the [Official Website](https://codeforge.devlive.org/) for more information.
3 changes: 2 additions & 1 deletion docs/pageforge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repo:
branch: dev

banner:
content: 💗 <a href="https://github.com/devlive-community/codeforge" target="_blank">CodeForge <em>25.0.5</em> 已经发布, 如果喜欢我们的软件,请点击这里支持我们</a> ❤️
content: 💗 <a href="https://github.com/devlive-community/codeforge" target="_blank">CodeForge <em>26.0.0</em> 已经发布, 如果喜欢我们的软件,请点击这里支持我们</a> ❤️

feature:
lucide:
Expand Down Expand Up @@ -43,6 +43,7 @@ footer:

nav:
- 发布日志:
- /release/26.0.0.md
- /release/25.0.5.md
- /release/25.0.4.md
- /release/25.0.3.md
Expand Down
27 changes: 27 additions & 0 deletions src-tauri/src/ai.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::BTreeSet;
use std::sync::Mutex;
use tauri::{AppHandle, Emitter};

// 被请求停止的流式任务 id 集合
static AI_CANCELLED: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());

/// 请求停止指定的流式生成
#[tauri::command]
pub fn stop_ai_stream(stream_id: String) {
if let Ok(mut set) = AI_CANCELLED.lock() {
set.insert(stream_id);
}
}

// 取出并清除取消标记,返回是否被取消
fn take_cancelled(stream_id: &str) -> bool {
AI_CANCELLED
.lock()
.map(|mut s| s.remove(stream_id))
.unwrap_or(false)
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: String,
Expand Down Expand Up @@ -187,6 +208,10 @@ pub async fn ai_chat_stream(
let mut buf = String::new();

while let Some(chunk) = stream.next().await {
// 收到停止请求则中断
if take_cancelled(&stream_id) {
return Ok(());
}
let chunk = chunk.map_err(|e| format!("读取流失败: {}", e))?;
buf.push_str(&String::from_utf8_lossy(&chunk));

Expand Down Expand Up @@ -223,6 +248,8 @@ pub async fn ai_chat_stream(
}
}

// 清理可能残留的取消标记
let _ = take_cancelled(&stream_id);
Ok(())
}

Expand Down
91 changes: 42 additions & 49 deletions src-tauri/src/ai_history.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
use crate::execution::get_codeforge_db_path;
use rusqlite::{Connection, params};
use serde::Serialize;
use std::sync::Mutex as StdMutex;
use tauri::State;

#[derive(Serialize)]
pub struct AiConversationMeta {
pub id: String,
pub title: String,
pub updated_at: i64,
}

/// AI 对话历史,存于与执行历史相同的 codeforge.sqlite 库
/// AI 对话历史,绑定到某次执行记录(execution_id),存于同一个 codeforge.sqlite 库。
/// 未关联执行的对话属临时会话,不落库。
pub struct AiHistory {
conn: StdMutex<Connection>,
}
Expand All @@ -20,12 +13,12 @@ impl AiHistory {
pub fn new() -> Result<Self, String> {
let db_path = get_codeforge_db_path()?;
let conn = Connection::open(&db_path).map_err(|e| format!("打开数据库失败: {}", e))?;
// 并发读写更稳
let _ = conn.pragma_update(None, "journal_mode", "WAL");
// 清理早期错误结构的旧表
let _ = conn.execute("DROP TABLE IF EXISTS ai_conversations", []);
conn.execute(
"CREATE TABLE IF NOT EXISTS ai_conversations (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
"CREATE TABLE IF NOT EXISTS ai_execution_chats (
execution_id INTEGER PRIMARY KEY,
messages TEXT NOT NULL,
updated_at INTEGER NOT NULL
)",
Expand All @@ -39,10 +32,10 @@ impl AiHistory {
}
}

/// 保存/更新某次执行对应的 AI 对话
#[tauri::command]
pub async fn save_ai_conversation(
id: String,
title: String,
execution_id: i64,
messages: String,
updated_at: i64,
history: State<'_, AiHistory>,
Expand All @@ -52,67 +45,67 @@ pub async fn save_ai_conversation(
.lock()
.map_err(|_| "数据库锁错误".to_string())?;
conn.execute(
"INSERT INTO ai_conversations (id, title, messages, updated_at)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(id) DO UPDATE SET title=?2, messages=?3, updated_at=?4",
params![id, title, messages, updated_at],
"INSERT INTO ai_execution_chats (execution_id, messages, updated_at)
VALUES (?1, ?2, ?3)
ON CONFLICT(execution_id) DO UPDATE SET messages=?2, updated_at=?3",
params![execution_id, messages, updated_at],
)
.map_err(|e| format!("保存 AI 对话失败: {}", e))?;
Ok(())
}

/// 读取某次执行的 AI 对话(messages JSON);无则返回空串
#[tauri::command]
pub async fn list_ai_conversations(
pub async fn get_ai_conversation(
execution_id: i64,
history: State<'_, AiHistory>,
) -> Result<Vec<AiConversationMeta>, String> {
) -> Result<String, String> {
let conn = history
.conn
.lock()
.map_err(|_| "数据库锁错误".to_string())?;
let mut stmt = conn
.prepare("SELECT id, title, updated_at FROM ai_conversations ORDER BY updated_at DESC")
.map_err(|e| format!("读取 AI 对话失败: {}", e))?;
let rows = stmt
.query_map([], |row| {
Ok(AiConversationMeta {
id: row.get(0)?,
title: row.get(1)?,
updated_at: row.get(2)?,
})
})
.map_err(|e| format!("读取 AI 对话失败: {}", e))?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("读取 AI 对话失败: {}", e))
let result = conn.query_row(
"SELECT messages FROM ai_execution_chats WHERE execution_id = ?1",
params![execution_id],
|row| row.get::<_, String>(0),
);
match result {
Ok(s) => Ok(s),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(String::new()),
Err(e) => Err(format!("读取 AI 对话失败: {}", e)),
}
}

/// 返回该会话的 messages JSON 字符串
/// 返回所有有 AI 对话的执行 id(供历史面板标记)
#[tauri::command]
pub async fn get_ai_conversation(
id: String,
history: State<'_, AiHistory>,
) -> Result<String, String> {
pub async fn list_ai_conversation_ids(history: State<'_, AiHistory>) -> Result<Vec<i64>, String> {
let conn = history
.conn
.lock()
.map_err(|_| "数据库锁错误".to_string())?;
conn.query_row(
"SELECT messages FROM ai_conversations WHERE id = ?1",
params![id],
|row| row.get::<_, String>(0),
)
.map_err(|e| format!("读取 AI 对话失败: {}", e))
let mut stmt = conn
.prepare("SELECT execution_id FROM ai_execution_chats")
.map_err(|e| format!("读取失败: {}", e))?;
let rows = stmt
.query_map([], |row| row.get::<_, i64>(0))
.map_err(|e| format!("读取失败: {}", e))?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("读取失败: {}", e))
}

#[tauri::command]
pub async fn delete_ai_conversation(
id: String,
execution_id: i64,
history: State<'_, AiHistory>,
) -> Result<(), String> {
let conn = history
.conn
.lock()
.map_err(|_| "数据库锁错误".to_string())?;
conn.execute("DELETE FROM ai_conversations WHERE id = ?1", params![id])
.map_err(|e| format!("删除 AI 对话失败: {}", e))?;
conn.execute(
"DELETE FROM ai_execution_chats WHERE execution_id = ?1",
params![execution_id],
)
.map_err(|e| format!("删除 AI 对话失败: {}", e))?;
Ok(())
}
Loading
Loading