diff --git a/package.json b/package.json
index 90e769549..1e84d8ab2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "vmark",
"private": true,
- "version": "0.9.28",
+ "version": "0.9.29",
"license": "ISC",
"description": "The plain-text workspace where humans and AI collaborate",
"type": "module",
diff --git a/server/mcp/package.json b/server/mcp/package.json
index c8fc71b33..ea7dae1da 100644
--- a/server/mcp/package.json
+++ b/server/mcp/package.json
@@ -1,6 +1,6 @@
{
"name": "@vmark/mcp-server",
- "version": "0.9.28",
+ "version": "0.9.29",
"description": "MCP server for VMark — the plain-text workspace where humans and AI collaborate",
"type": "module",
"main": "dist/index.js",
diff --git a/server/mcp/src/cli.ts b/server/mcp/src/cli.ts
index 8c409d518..ff2c53c17 100644
--- a/server/mcp/src/cli.ts
+++ b/server/mcp/src/cli.ts
@@ -23,7 +23,7 @@
* lockstep with the app is the five-file `sed` in the bump procedure
* (`.claude/rules/40-version-bump.md`). Edit it only through that procedure.
*/
-const VERSION = '0.9.28';
+const VERSION = '0.9.29';
/**
* Handle --version flag.
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 068cbd632..3a6622a56 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -6280,7 +6280,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vmark"
-version = "0.9.28"
+version = "0.9.29"
dependencies = [
"base64 0.23.0",
"block2 0.6.2",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index d55784a5e..bafe3dc67 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "vmark"
-version = "0.9.28"
+version = "0.9.29"
description = "The plain-text workspace where humans and AI collaborate"
authors = ["Xiaolai"]
license = "ISC"
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 9063fd954..4473465d5 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "VMark",
- "version": "0.9.28",
+ "version": "0.9.29",
"identifier": "app.vmark",
"build": {
"beforeDevCommand": "pnpm dev",
diff --git a/src/App.tsx b/src/App.tsx
index 451d07130..f706633a0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,7 +3,7 @@ import { FeatureErrorBoundary } from "@/components/FeatureErrorBoundary";
import { useTranslation, withTranslation, type WithTranslation } from "react-i18next";
import { Routes, Route } from "react-router-dom";
import { Toaster } from "sonner";
-import { CheckCircle, XCircle, Info, AlertTriangle, Loader2 } from "lucide-react";
+import { TOAST_ICONS } from "@/components/toastIcons";
import { DocumentSplitContainer } from "@/components/Editor";
import { Sidebar } from "@/components/Sidebar";
import { SidebarResizeHandle } from "@/components/Sidebar/SidebarResizeHandle";
@@ -282,13 +282,7 @@ function App() {
,
- error: ,
- info: ,
- warning: ,
- loading: ,
- }}
+ icons={TOAST_ICONS}
/>
diff --git a/src/components/toastIcons.tsx b/src/components/toastIcons.tsx
new file mode 100644
index 000000000..ff3719b99
--- /dev/null
+++ b/src/components/toastIcons.tsx
@@ -0,0 +1,26 @@
+/**
+ * Purpose: the icon set sonner's `` renders per toast severity.
+ *
+ * Extracted from `App.tsx`, which is the document window's composition root and
+ * sits at the 300-line limit — an explanatory comment on one icon was enough to
+ * push it over. A severity→icon map is data, not composition, so it does not
+ * belong in the root anyway.
+ *
+ * @coordinates-with src/App.tsx — the sole consumer, passed as ``
+ * @module components/toastIcons
+ */
+
+import { CheckCircle, AlertCircle, Info, AlertTriangle, Loader2 } from "lucide-react";
+
+const SIZE = 16;
+
+export const TOAST_ICONS = {
+ success: ,
+ // AlertCircle, not XCircle: an X-in-a-circle reads as a second (and larger)
+ // close button beside sonner's own `closeButton`. Keeping the severity ramp
+ // as i / ! / ⚠ leaves the X shape unique to dismissal.
+ error: ,
+ info: ,
+ warning: ,
+ loading: ,
+};