From 25ece39a30adc7a20282ea83b39d3a2fb5922e9d Mon Sep 17 00:00:00 2001 From: donfaquir <1458918806@qq.com> Date: Wed, 15 Jul 2026 17:27:11 +0800 Subject: [PATCH 01/11] fix(chat): open links in system browser instead of crashing webview Co-authored-by: OpenCode --- package-lock.json | 14 ++++++- package.json | 1 + src-tauri/Cargo.lock | 53 ++++++++++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 3 +- src-tauri/src/lib.rs | 1 + src/components/AiAssistantMarkdown.tsx | 29 +++++++++++++- 7 files changed, 98 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f73647f..6a7e143 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "knowforge", - "version": "0.6.1", + "version": "0.7.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "knowforge", - "version": "0.6.1", + "version": "0.7.6", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -21,6 +21,7 @@ "@milkdown/utils": "^7.20.0", "@tauri-apps/api": "~2.11.0", "@tauri-apps/plugin-dialog": "~2.7.0", + "@tauri-apps/plugin-opener": "~2.5.0", "d3-force": "^3.0.0", "github-slugger": "^2.0.0", "i18next": "^26.0.4", @@ -1721,6 +1722,15 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", + "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", diff --git a/package.json b/package.json index 99d67a1..401eb0d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@milkdown/utils": "^7.20.0", "@tauri-apps/api": "~2.11.0", "@tauri-apps/plugin-dialog": "~2.7.0", + "@tauri-apps/plugin-opener": "~2.5.0", "d3-force": "^3.0.0", "github-slugger": "^2.0.0", "i18next": "^26.0.4", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5819f0b..b6fb6c3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2713,6 +2713,25 @@ dependencies = [ "serde", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2898,6 +2917,7 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-notification", + "tauri-plugin-opener", "tempfile", "tokenizers", "tokio", @@ -3789,6 +3809,17 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "open" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -5794,6 +5825,28 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + [[package]] name = "tauri-runtime" version = "2.11.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d266acd..fdf516c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri-build = { version = "2.6", features = [] } tauri = { version = "2.11", features = ["macos-private-api"] } tauri-plugin-dialog = "2.7" tauri-plugin-notification = "2" +tauri-plugin-opener = "2.5" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" url = "2.5" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 5f0ed98..b08a996 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -11,6 +11,7 @@ "core:window:allow-start-dragging", "core:window:allow-toggle-maximize", "dialog:default", - "notification:default" + "notification:default", + "opener:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1cf7811..2b5cfb9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2154,6 +2154,7 @@ pub fn run() { }) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ open_workspace, refresh_md_tree, diff --git a/src/components/AiAssistantMarkdown.tsx b/src/components/AiAssistantMarkdown.tsx index bf29311..f10366d 100644 --- a/src/components/AiAssistantMarkdown.tsx +++ b/src/components/AiAssistantMarkdown.tsx @@ -1,12 +1,34 @@ +import type { Components } from "react-markdown"; import ReactMarkdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; +import { openUrl } from "@tauri-apps/plugin-opener"; type Props = { content: string; className?: string; }; +/** + * Custom anchor component that opens external links in the system browser + * instead of navigating the webview (which would crash the app). + */ +const markdownComponents: Components = { + a({ href, children }) { + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + if (href) { + openUrl(href); + } + }; + return ( + + {children} + + ); + }, +}; + /** * 助手气泡内 Markdown;与编辑器栈共用 remark-gfm / remark-breaks。 */ @@ -16,7 +38,12 @@ export function AiAssistantMarkdown({ content, className }: Props) { } return (
- {content} + + {content} +
); } From ab695ab8e04ed679e6ed94230268a0ecab1ee54d Mon Sep 17 00:00:00 2001 From: donfaquir <1458918806@qq.com> Date: Wed, 15 Jul 2026 20:06:30 +0800 Subject: [PATCH 02/11] feat(chat): render tool calls chronologically and improve visual hierarchy Co-authored-by: OpenCode --- src-tauri/src/ai_conversations.rs | 10 ++++++ src/components/AiConversationPanel.css | 23 +++++++++---- src/components/MessageBubble.tsx | 41 ++++++++++++++++++------ src/hooks/useAgentEventHandlers.ts | 15 ++++++++- src/hooks/useWorkspaceAiConversations.ts | 14 +++++++- src/types/aiConversation.ts | 7 ++++ 6 files changed, 92 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/ai_conversations.rs b/src-tauri/src/ai_conversations.rs index 7dbfcd5..f0d462c 100644 --- a/src-tauri/src/ai_conversations.rs +++ b/src-tauri/src/ai_conversations.rs @@ -124,6 +124,9 @@ struct PersistedMessageDisk { skill_name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] tool_calls: Option, + /// Ordered content blocks for chronological rendering (thinking -> tool call -> thinking -> ...) + #[serde(default, skip_serializing_if = "Option::is_none")] + content_blocks: Option, } /// 会话级「想法聚焦」上下文(磁盘与 LLM 字段对齐) @@ -305,6 +308,9 @@ pub struct PersistedMessageIn { pub skill_name: Option, #[serde(default)] pub tool_calls: Option, + /// Ordered content blocks for chronological rendering (thinking -> tool call -> thinking -> ...) + #[serde(default)] + pub content_blocks: Option, } #[derive(Debug, Clone, Deserialize)] @@ -365,6 +371,7 @@ fn validate_messages_for_save(msgs: &[PersistedMessageIn]) -> Result {m.role === "assistant" ? ( <> - {m.meta?.toolCalls && m.meta.toolCalls.length > 0 ? ( -
- {m.meta.toolCalls.map((tc) => ( - - ))} -
- ) : null} + {m.contentBlocks && m.contentBlocks.length > 0 ? ( + /* Chronological rendering: thinking -> tool call -> thinking -> tool call -> ... */ + <> + {m.contentBlocks.map((block: ContentBlock, idx: number) => { + if (block.type === "text") { + return block.text.trim() ? ( + + ) : null; + } + // block.type === "tool_call" + const tc = m.meta?.toolCalls?.find((t) => t.toolCallId === block.toolCallId); + return tc ? ( +
+ +
+ ) : null; + })} + + ) : ( + /* Fallback for historical messages without contentBlocks */ + <> + {m.meta?.toolCalls && m.meta.toolCalls.length > 0 ? ( +
+ {m.meta.toolCalls.map((tc) => ( + + ))} +
+ ) : null} + + + )} {m.meta?.budgetWarning && (
Agent {m.meta.budgetWarning.used}/{m.meta.budgetWarning.limit} tool calls used
)} - {!m.streaming && m.meta?.thoughtCitation && !m.meta.thoughtCitation.privateOmitted ? (