diff --git a/docs/content/release/26.0.0.md b/docs/content/release/26.0.0.md new file mode 100644 index 0000000..8986f26 --- /dev/null +++ b/docs/content/release/26.0.0.md @@ -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. diff --git a/docs/pageforge.yaml b/docs/pageforge.yaml index 495c504..41de199 100644 --- a/docs/pageforge.yaml +++ b/docs/pageforge.yaml @@ -14,7 +14,7 @@ repo: branch: dev banner: - content: 💗 CodeForge 25.0.5 已经发布, 如果喜欢我们的软件,请点击这里支持我们 ❤️ + content: 💗 CodeForge 26.0.0 已经发布, 如果喜欢我们的软件,请点击这里支持我们 ❤️ feature: lucide: @@ -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 diff --git a/src-tauri/src/ai.rs b/src-tauri/src/ai.rs index 996e79b..74746ee 100644 --- a/src-tauri/src/ai.rs +++ b/src-tauri/src/ai.rs @@ -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> = 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, @@ -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)); @@ -223,6 +248,8 @@ pub async fn ai_chat_stream( } } + // 清理可能残留的取消标记 + let _ = take_cancelled(&stream_id); Ok(()) } diff --git a/src-tauri/src/ai_history.rs b/src-tauri/src/ai_history.rs index 61e59aa..916c209 100644 --- a/src-tauri/src/ai_history.rs +++ b/src-tauri/src/ai_history.rs @@ -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, } @@ -20,12 +13,12 @@ impl AiHistory { pub fn new() -> Result { 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 )", @@ -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>, @@ -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, String> { +) -> Result { 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::, _>>() - .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 { +pub async fn list_ai_conversation_ids(history: State<'_, AiHistory>) -> Result, 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::, _>>() + .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(()) } diff --git a/src-tauri/src/execution.rs b/src-tauri/src/execution.rs index 333ba73..54fa985 100644 --- a/src-tauri/src/execution.rs +++ b/src-tauri/src/execution.rs @@ -61,7 +61,7 @@ impl ExecutionHistory { }) } - fn insert(&self, result: &ExecutionResult) -> Result<(), String> { + fn insert(&self, result: &ExecutionResult) -> Result { let conn = self .conn .lock() @@ -83,7 +83,7 @@ impl ExecutionHistory { ) .map_err(|e| format!("保存执行历史失败: {}", e))?; - Ok(()) + Ok(conn.last_insert_rowid()) } fn list(&self) -> Result, String> { @@ -93,7 +93,7 @@ impl ExecutionHistory { .map_err(|_| "执行历史数据库锁错误".to_string())?; let mut statement = conn .prepare( - "SELECT success, code, stdout, stderr, execution_time, timestamp, language + "SELECT id, success, code, stdout, stderr, execution_time, timestamp, language FROM execution_history ORDER BY id ASC", ) @@ -102,13 +102,14 @@ impl ExecutionHistory { let rows = statement .query_map([], |row| { Ok(ExecutionResult { - success: row.get::<_, i64>(0)? != 0, - code: row.get(1)?, - stdout: row.get(2)?, - stderr: row.get(3)?, - execution_time: row.get::<_, i64>(4)? as u128, - timestamp: row.get::<_, i64>(5)? as u64, - language: row.get(6)?, + id: Some(row.get::<_, i64>(0)?), + success: row.get::<_, i64>(1)? != 0, + code: row.get(2)?, + stdout: row.get(3)?, + stderr: row.get(4)?, + execution_time: row.get::<_, i64>(5)? as u128, + timestamp: row.get::<_, i64>(6)? as u64, + language: row.get(7)?, }) }) .map_err(|e| format!("读取执行历史失败: {}", e))?; @@ -132,7 +133,7 @@ impl ExecutionHistory { let mut statement = conn .prepare( - "SELECT success, code, stdout, stderr, execution_time, timestamp, language + "SELECT id, success, code, stdout, stderr, execution_time, timestamp, language FROM execution_history ORDER BY id DESC LIMIT ?1 OFFSET ?2", @@ -142,13 +143,14 @@ impl ExecutionHistory { let rows = statement .query_map(params![limit as i64, offset as i64], |row| { Ok(ExecutionResult { - success: row.get::<_, i64>(0)? != 0, - code: row.get(1)?, - stdout: row.get(2)?, - stderr: row.get(3)?, - execution_time: row.get::<_, i64>(4)? as u128, - timestamp: row.get::<_, i64>(5)? as u64, - language: row.get(6)?, + id: Some(row.get::<_, i64>(0)?), + success: row.get::<_, i64>(1)? != 0, + code: row.get(2)?, + stdout: row.get(3)?, + stderr: row.get(4)?, + execution_time: row.get::<_, i64>(5)? as u128, + timestamp: row.get::<_, i64>(6)? as u64, + language: row.get(7)?, }) }) .map_err(|e| format!("读取执行历史失败: {}", e))?; @@ -575,6 +577,7 @@ pub async fn execute_code( } let mut result = ExecutionResult { + id: None, success: status.success(), code: request.code.clone(), stdout: stdout_lines.join("\n"), @@ -612,7 +615,8 @@ pub async fn execute_code( ); drop(manager); - history.insert(&result)?; + let row_id = history.insert(&result)?; + result.id = Some(row_id); info!("执行代码 -> 调用插件 [ {} ] 完成", request.language); return Ok(result); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 64b8122..a8d43dd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -22,9 +22,9 @@ mod setup; mod update; mod utils; -use crate::ai::{ai_chat, ai_chat_stream}; +use crate::ai::{ai_chat, ai_chat_stream, stop_ai_stream}; use crate::ai_history::{ - AiHistory, delete_ai_conversation, get_ai_conversation, list_ai_conversations, + AiHistory, delete_ai_conversation, get_ai_conversation, list_ai_conversation_ids, save_ai_conversation, }; use crate::cache::{clear_all_cache, clear_plugins_cache, get_cache_info}; @@ -181,9 +181,10 @@ fn main() { // AI 助手 ai_chat, ai_chat_stream, - // AI 对话历史 + stop_ai_stream, + // AI 对话历史(绑定执行记录) save_ai_conversation, - list_ai_conversations, + list_ai_conversation_ids, get_ai_conversation, delete_ai_conversation ]) diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index 99044cc..4853b22 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -7,6 +7,9 @@ use std::path::PathBuf; // 通用结构定义 #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ExecutionResult { + // 执行历史记录 id(运行后由后端填充;用于关联 AI 对话) + #[serde(default)] + pub id: Option, pub success: bool, pub code: String, pub stdout: String, diff --git a/src/App.vue b/src/App.vue index 01caea5..be596d6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,7 +14,7 @@ @open-file="handleOpenFileClick" @save-file="saveFile" @show-history="showHistory = true" - @show-ai="showAi = true" + @show-ai="handleShowAi" @show-settings="showSettings = true" @load-example="loadExample"> @@ -80,12 +80,13 @@ - + + @@ -143,12 +144,13 @@ - + + @@ -169,7 +171,8 @@ + @restore="restoreHistoryItem" + @open-ai="openAiForExecution"/> @@ -186,7 +189,7 @@ - + { } } -// AI 助手抽屉 +// AI 助手抽屉(绑定的执行 id:工具栏打开取最近一次运行,历史面板打开取指定运行) const showAi = ref(false) +const aiExecutionId = ref(null) +// 失败运行的报错上下文,供"分析报错"快捷动作 +const aiErrorContext = ref<{ code: string, error: string } | null>(null) + +const combinedOutput = (item: ExecutionResult) => + [item.stdout?.trim(), item.stderr?.trim()].filter(Boolean).join('\n\n') + +const handleShowAi = () => { + aiExecutionId.value = currentExecutionId.value + // 最近一次运行失败则带上报错 + aiErrorContext.value = currentExecutionId.value != null && !isSuccess.value && output.value + ? {code: code.value, error: output.value} + : null + showAi.value = true +} + +const openAiForExecution = (item: ExecutionResult) => { + aiExecutionId.value = item.id ?? null + aiErrorContext.value = item.success + ? null + : {code: item.code, error: combinedOutput(item) || '(无输出)'} + showAi.value = true +} + +// 把 AI 代码块应用到编辑器(替换当前内容,可撤销) +const applyAiCode = (codeText: string) => { + code.value = codeText +} + +// 当前 CodeMirror view(用于在光标处插入生成的代码) +const editorView = ref(null) + +// AI 自然语言生成 +const showGenerate = ref(false) +const openGenerate = () => { + showGenerate.value = true +} + +// 在光标处插入/替换选区为生成的代码 +const insertGeneratedCode = (text: string) => { + const view = editorView.value + if (view) { + const sel = view.state.selection.main + view.dispatch({ + changes: {from: sel.from, to: sel.to, insert: text}, + selection: {anchor: sel.from + text.length} + }) + view.focus() + } + else { + code.value = text + } +} // 快速打开(Cmd+P) const showQuickOpen = ref(false) @@ -715,7 +773,8 @@ window.addEventListener('contextmenu', (e) => e.preventDefault(), false) // 是否有弹窗/覆盖层打开(打开时不响应全局快捷键) const isOverlayOpen = () => showSettings.value || showAbout.value || showUpdate.value - || showHistory.value || showViewer.value || showRunPrompt.value || showQuickOpen.value + || showHistory.value || showViewer.value || showRunPrompt.value + || showQuickOpen.value || showGenerate.value // 全局快捷键(绑定可在设置中自定义) const {matchAction: matchShortcut, reload: reloadShortcuts} = useShortcuts() @@ -723,6 +782,7 @@ const {matchAction: matchShortcut, reload: reloadShortcuts} = useShortcuts() const shortcutDispatch: Record void> = { run: () => handleRunCode(), quickOpen: () => openQuickOpen(), + generate: () => openGenerate(), save: () => saveFile(), saveAs: () => saveFileAs(), open: () => handleOpenFileClick(), diff --git a/src/components/AiAssistant.vue b/src/components/AiAssistant.vue index cdc3d25..3a7ffd2 100644 --- a/src/components/AiAssistant.vue +++ b/src/components/AiAssistant.vue @@ -8,11 +8,8 @@ {{ active.model }} - - - - - + + @@ -20,33 +17,22 @@ - - - 暂无历史对话 - - - {{ c.title }} - {{ formatTime(c.updated_at) }} - - - - - + + + {{ executionId != null ? `已关联运行 #${executionId},对话随该次运行保存` : '临时会话:运行代码后对话才会保存' }} - + + 分析报错 解释代码 找 Bug 优化 - + 向 AI 提问,或用上方快捷动作处理当前代码 @@ -63,6 +49,11 @@ + + + 停止生成 + + diff --git a/src/composables/useAiHistory.ts b/src/composables/useAiHistory.ts index ed08263..a02ac6f 100644 --- a/src/composables/useAiHistory.ts +++ b/src/composables/useAiHistory.ts @@ -1,4 +1,3 @@ -import {ref} from 'vue' import {invoke} from '@tauri-apps/api/core' export interface AiMsg @@ -7,50 +6,25 @@ export interface AiMsg content: string } -// 列表项(不含完整消息,按需再取) -export interface AiConversationMeta -{ - id: string - title: string - updated_at: number -} - -export interface AiConversation -{ - id: string - title: string - updatedAt: number - messages: AiMsg[] -} - /** - * AI 对话历史,存于与执行历史相同的 SQLite 库(后端命令)。 + * AI 对话与某次执行记录(executionId)绑定,存于与执行历史同一个 SQLite 库。 + * 未关联执行的对话属临时会话,不保存。 */ export function useAiHistory() { - const conversations = ref([]) - - const reload = async () => { - try { - conversations.value = await invoke('list_ai_conversations') - } - catch (error) { - console.error('读取 AI 对话历史失败:', error) - } - } - - const saveConversation = async (conv: AiConversation) => { + const saveConversation = async (executionId: number, messages: AiMsg[]) => { await invoke('save_ai_conversation', { - id: conv.id, - title: conv.title, - messages: JSON.stringify(conv.messages), - updatedAt: conv.updatedAt + executionId, + messages: JSON.stringify(messages), + updatedAt: Date.now() }) - await reload() } - const getMessages = async (id: string): Promise => { - const json = await invoke('get_ai_conversation', {id}) + const getMessages = async (executionId: number): Promise => { + const json = await invoke('get_ai_conversation', {executionId}) + if (!json) { + return [] + } try { return JSON.parse(json) } @@ -59,10 +33,9 @@ export function useAiHistory() } } - const remove = async (id: string) => { - await invoke('delete_ai_conversation', {id}) - await reload() + const deleteConversation = async (executionId: number) => { + await invoke('delete_ai_conversation', {executionId}) } - return {conversations, reload, saveConversation, getMessages, remove} + return {saveConversation, getMessages, deleteConversation} } diff --git a/src/composables/useCodeExecution.ts b/src/composables/useCodeExecution.ts index 2e21031..41b65d7 100644 --- a/src/composables/useCodeExecution.ts +++ b/src/composables/useCodeExecution.ts @@ -12,6 +12,8 @@ export function useCodeExecution(toast: any) // 当前运行任务的唯一标识,用于事件路由(支持多标签并发运行) const currentTaskId = ref(null) + // 最近一次执行记录 id,用于关联 AI 对话 + const currentExecutionId = ref(null) // 实时输出相关 const realTimeOutput = ref('') @@ -61,6 +63,9 @@ export function useCodeExecution(toast: any) lastExecutionTime.value = result.execution_time isSuccess.value = result.success + if (result.id != null) { + currentExecutionId.value = result.id + } if (result.success) { toast.success(`代码执行成功,用时 ${ result.execution_time } 毫秒`) @@ -180,6 +185,7 @@ export function useCodeExecution(toast: any) isSuccess, lastExecutionTime, currentTaskId, + currentExecutionId, runCode, stopCode, clearOutput, diff --git a/src/composables/useShortcuts.ts b/src/composables/useShortcuts.ts index b236fc0..09e54b9 100644 --- a/src/composables/useShortcuts.ts +++ b/src/composables/useShortcuts.ts @@ -14,6 +14,7 @@ export const SHORTCUT_ACTIONS: ShortcutAction[] = [ {id: 'saveAs', label: '另存为', default: 'Mod+Shift+S'}, {id: 'open', label: '打开文件', default: 'Mod+O'}, {id: 'quickOpen', label: '快速打开文件', default: 'Mod+P'}, + {id: 'generate', label: 'AI 生成代码', default: 'Mod+K'}, {id: 'newTab', label: '新建标签', default: 'Mod+N'}, {id: 'closeTab', label: '关闭标签', default: 'Mod+W'}, {id: 'toggleSidebar', label: '切换侧栏', default: 'Mod+B'} diff --git a/src/types/app.ts b/src/types/app.ts index 7027998..01ce9b1 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -1,5 +1,6 @@ export interface ExecutionResult { + id?: number success: boolean code: string stdout: string