Repository: logancyang/obsidian-copilot

Issue:
Project Mode: Support project context preview
**Is your feature request related to a problem? Please describe.**
Currently, I don't know which files to include in the project, when i add the inclusion pattern.

see:

![Image](https://github.com/user-attachments/assets/d17c0c2b-3dde-40eb-bd17-ba5436e5880a)

**Describe the solution you'd like**
Support files preview when i add the inclusion pattern.



Hints from issue comments:

Solution patch:
diff --git a/.gitignore b/.gitignore
index 50ba24a..e95457c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@ data.json
 
 # Test Environment
 .env.test
+
+# Claude configuration
+.claude/settings.local.json
diff --git a/CLAUDE.md b/CLAUDE.md
index f1b3990..41ea8a1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,31 +1,164 @@
-# CLAUDE.md - Copilot for Obsidian Plugin
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Overview
+
+Copilot for Obsidian is an AI-powered assistant plugin that integrates various LLM providers (OpenAI, Anthropic, Google, etc.) with Obsidian. It provides chat interfaces, autocomplete, semantic search, and various AI-powered commands for note-taking and knowledge management.
 
 ## Development Commands
 
-- Build: `npm run build`
-- Development: `npm run dev`
-- Lint: `npm run lint` (fix: `npm run lint:fix`)
-- Format: `npm run format` (check: `npm run format:check`)
-- Test: `npm run test` (single test: `npm test -- -t "test name"`)
-- Before PR: Run `npm run format && npm run lint`
+### Build & Development
+
+- `npm run dev` - Start development server with hot reload (runs Tailwind CSS + esbuild in watch mode)
+- `npm run build` - Production build (TypeScript check + minified output)
+
+### Code Quality
+
+- `npm run lint` - Run ESLint checks
+- `npm run lint:fix` - Auto-fix ESLint issues
+- `npm run format` - Format code with Prettier
+- `npm run format:check` - Check formatting without changing files
+- **Before PR:** Always run `npm run format && npm run lint`
+
+### Testing
+
+- `npm run test` - Run unit tests (excludes integration tests)
+- `npm run test:integration` - Run integration tests (requires API keys)
+- Run single test: `npm test -- -t "test name"`
+
+## High-Level Architecture
+
+### Core Systems
+
+1. **LLM Provider System** (`src/LLMProviders/`)
+
+   - Provider implementations for OpenAI, Anthropic, Google, Azure, local models
+   - `LLMProviderManager` handles provider lifecycle and switching
+   - Stream-based responses with error handling and rate limiting
+   - Custom model configuration support
+
+2. **Chain Factory Pattern** (`src/chainFactory.ts`)
+
+   - Different chain types for various AI operations (chat, copilot, adhoc prompts)
+   - LangChain integration for complex workflows
+   - Memory management for conversation context
+   - Tool integration (search, file operations, time queries)
+
+3. **Vector Store & Search** (`src/search/`)
+
+   - `VectorStoreManager` manages embeddings and semantic search
+   - `ChunkedStorage` for efficient large document handling
+   - Event-driven index updates via `IndexManager`
+   - Multiple embedding providers support
+
+4. **UI Component System** (`src/components/`)
+
+   - React functional components with Radix UI primitives
+   - Tailwind CSS with class variance authority (CVA)
+   - Modal system for user interactions
+   - Chat interface with streaming support
+   - Settings UI with versioned components
+
+5. **State Management**
+
+   - Jotai for atomic state management
+   - React contexts for feature-specific state
+   - Shared state utilities in `src/sharedState.ts`
+
+6. **Plugin Integration**
+   - Main entry: `src/main.ts` extends Obsidian Plugin
+   - Command registration system
+   - Event handling for Obsidian lifecycle
+   - Settings persistence and migration
+
+### Key Patterns
+
+- **Error Handling**: Custom error types with detailed interfaces
+- **Async Operations**: Consistent async/await pattern with proper error boundaries
+- **Caching**: Multi-layer caching for files, PDFs, and API responses
+- **Streaming**: Real-time streaming for LLM responses
+- **Testing**: Unit tests adjacent to implementation, integration tests for API calls
 
 ## Code Style Guidelines
 
-- TypeScript with strict null checks and no implicit any
-- Use absolute imports with `@/` prefix (e.g., `import { ChainType } from "@/chainFactory"`)
-- React functional components with hooks
-- Error handling: Use detailed error objects with type interfaces
-- Consistent naming: PascalCase for components/classes, camelCase for functions/variables
-- Comment complex logic and functions with JSDoc format
-- Use async/await for asynchronous operations
-- Prefer const/let over var
-- Organize imports: React first, then external, then internal
-- Format with Prettier and ESLint before commits
-
-## Code Organization
-
-- UI components in src/components/
-- LLM providers in src/LLMProviders/
-- Utility functions in src/utils.ts
-- Settings in src/settings/
-- Tests adjacent to implementation files
+### TypeScript
+
+- Strict mode enabled (no implicit any, strict null checks)
+- Use absolute imports with `@/` prefix: `import { ChainType } from "@/chainFactory"`
+- Prefer const assertions and type inference where appropriate
+- Use interface for object shapes, type for unions/aliases
+
+### React
+
+- Functional components only (no class components)
+- Custom hooks for reusable logic
+- Props interfaces defined above components
+- Avoid inline styles, use Tailwind classes
+
+### General
+
+- File naming: PascalCase for components, camelCase for utilities
+- Async/await over promises
+- Early returns for error conditions
+- JSDoc for complex functions
+- Organize imports: React → external → internal
+
+## Testing Guidelines
+
+- Unit tests use Jest with TypeScript support
+- Mock Obsidian API for plugin testing
+- Integration tests require API keys in `.env.test`
+- Test files adjacent to implementation (`.test.ts`)
+- Use `@testing-library/react` for component testing
+
+### Manual Test Checklists
+
+**Important**: After each significant change, generate a manual test checklist document that includes:
+
+1. **Overview**: Brief description of what changed
+2. **Test Scenarios**: Specific test cases with steps and expected results
+3. **Verification Checklist**: List of items to verify functionality
+4. **Files Modified**: List of changed files for reference
+
+Example format:
+
+```markdown
+# [Feature] Test Instructions
+
+## Overview
+
+Brief description of the feature/fix
+
+## Test Scenarios
+
+### 1. Test Case Name
+
+1. Step one
+2. Step two
+3. **Expected Result:**
+   - Expected behavior
+   - UI state changes
+   - Data persistence
+
+### 2. Another Test Case
+
+[...]
+
+## Verification Checklist
+
+- [ ] Core functionality works
+- [ ] Edge cases handled
+- [ ] No regressions
+- [ ] Performance acceptable
+```
+
+This helps ensure thorough testing and provides documentation for QA.
+
+## Important Notes
+
+- The plugin supports multiple LLM providers with custom endpoints
+- Vector store requires rebuilding when switching embedding providers
+- Settings are versioned - migrations may be needed
+- Local model support available via Ollama/LM Studio
+- Rate limiting is implemented for all API calls
diff --git a/package-lock.json b/package-lock.json
index 066a984..60d4f18 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,13 +32,14 @@
         "@radix-ui/react-dropdown-menu": "^2.1.4",
         "@radix-ui/react-label": "^2.1.0",
         "@radix-ui/react-popover": "^1.1.4",
-        "@radix-ui/react-scroll-area": "^1.2.2",
+        "@radix-ui/react-scroll-area": "^1.2.9",
         "@radix-ui/react-select": "^2.1.2",
+        "@radix-ui/react-separator": "^1.1.7",
         "@radix-ui/react-slider": "^1.2.1",
-        "@radix-ui/react-slot": "^1.1.0",
+        "@radix-ui/react-slot": "^1.2.3",
         "@radix-ui/react-switch": "^1.1.1",
         "@radix-ui/react-tabs": "^1.1.3",
-        "@radix-ui/react-tooltip": "^1.1.6",
+        "@radix-ui/react-tooltip": "^1.2.7",
         "@tabler/icons-react": "^2.14.0",
         "async-mutex": "^0.5.0",
         "axios": "^1.3.4",
@@ -56,6 +57,7 @@
         "koa": "^2.14.2",
         "koa-proxies": "^0.12.3",
         "langchain": "^0.3.2",
+        "lodash.debounce": "^4.0.8",
         "lucide-react": "^0.462.0",
         "luxon": "^3.5.0",
         "next-i18next": "^13.2.2",
@@ -64,6 +66,7 @@
         "react-dom": "^18.2.0",
         "react-dropzone": "^14.3.5",
         "react-markdown": "^9.0.1",
+        "react-resizable-panels": "^3.0.2",
         "react-syntax-highlighter": "^15.5.0",
         "sse": "github:mpetazzoni/sse.js",
         "tailwind-merge": "^2.5.5",
@@ -81,6 +84,7 @@
         "@types/jest": "^29.5.11",
         "@types/koa": "^2.13.7",
         "@types/koa__cors": "^4.0.0",
+        "@types/lodash.debounce": "^4.0.9",
         "@types/luxon": "^3.4.2",
         "@types/node": "^16.11.6",
         "@types/node-fetch": "^2.6.12",
@@ -5173,6 +5177,24 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+      "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.0"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-compose-refs": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
@@ -6252,6 +6274,24 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+      "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.0"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-roving-focus": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
@@ -6372,20 +6412,20 @@
       }
     },
     "node_modules/@radix-ui/react-scroll-area": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz",
-      "integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz",
+      "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/number": "1.1.0",
-        "@radix-ui/primitive": "1.1.1",
-        "@radix-ui/react-compose-refs": "1.1.1",
-        "@radix-ui/react-context": "1.1.1",
-        "@radix-ui/react-direction": "1.1.0",
-        "@radix-ui/react-presence": "1.1.2",
-        "@radix-ui/react-primitive": "2.0.1",
-        "@radix-ui/react-use-callback-ref": "1.1.0",
-        "@radix-ui/react-use-layout-effect": "1.1.0"
+        "@radix-ui/number": "1.1.1",
+        "@radix-ui/primitive": "1.1.2",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-direction": "1.1.1",
+        "@radix-ui/react-presence": "1.1.4",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-use-callback-ref": "1.1.1",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6402,16 +6442,52 @@
         }
       }
     },
-    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
+    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": {
       "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
-      "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+      "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+      "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+      "license": "MIT"
+    },
+    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+      "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
       "license": "MIT"
     },
     "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+      "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+      "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": {
       "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
-      "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+      "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
       "license": "MIT",
       "peerDependencies": {
         "@types/react": "*",
@@ -6423,13 +6499,37 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+      "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
-      "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+      "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/react-slot": "1.1.1"
+        "@radix-ui/react-slot": "1.2.3"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6446,14 +6546,26 @@
         }
       }
     },
-    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
+    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
       "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
-      "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+      "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
       "license": "MIT",
-      "dependencies": {
-        "@radix-ui/react-compose-refs": "1.1.1"
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
       },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+      "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+      "license": "MIT",
       "peerDependencies": {
         "@types/react": "*",
         "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -6507,6 +6619,24 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+      "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.0"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
@@ -6530,6 +6660,52 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-separator": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+      "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-primitive": "2.1.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+      "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-slot": "1.2.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-slider": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
@@ -6564,12 +6740,12 @@
       }
     },
     "node_modules/@radix-ui/react-slot": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
-      "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+      "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/react-compose-refs": "1.1.0"
+        "@radix-ui/react-compose-refs": "1.1.2"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6581,6 +6757,21 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+      "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-switch": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.1.tgz",
@@ -6760,23 +6951,23 @@
       }
     },
     "node_modules/@radix-ui/react-tooltip": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz",
-      "integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==",
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
+      "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/primitive": "1.1.1",
-        "@radix-ui/react-compose-refs": "1.1.1",
-        "@radix-ui/react-context": "1.1.1",
-        "@radix-ui/react-dismissable-layer": "1.1.3",
-        "@radix-ui/react-id": "1.1.0",
-        "@radix-ui/react-popper": "1.2.1",
-        "@radix-ui/react-portal": "1.1.3",
-        "@radix-ui/react-presence": "1.1.2",
-        "@radix-ui/react-primitive": "2.0.1",
-        "@radix-ui/react-slot": "1.1.1",
-        "@radix-ui/react-use-controllable-state": "1.1.0",
-        "@radix-ui/react-visually-hidden": "1.1.1"
+        "@radix-ui/primitive": "1.1.2",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-dismissable-layer": "1.1.10",
+        "@radix-ui/react-id": "1.1.1",
+        "@radix-ui/react-popper": "1.2.7",
+        "@radix-ui/react-portal": "1.1.9",
+        "@radix-ui/react-presence": "1.1.4",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-slot": "1.2.3",
+        "@radix-ui/react-use-controllable-state": "1.2.2",
+        "@radix-ui/react-visually-hidden": "1.2.3"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6794,18 +6985,18 @@
       }
     },
     "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
-      "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+      "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
       "license": "MIT"
     },
     "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
-      "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+      "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/react-primitive": "2.0.1"
+        "@radix-ui/react-primitive": "2.1.3"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6823,9 +7014,24 @@
       }
     },
     "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
-      "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+      "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+      "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
       "license": "MIT",
       "peerDependencies": {
         "@types/react": "*",
@@ -6838,16 +7044,16 @@
       }
     },
     "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
-      "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
+      "version": "1.1.10",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+      "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/primitive": "1.1.1",
-        "@radix-ui/react-compose-refs": "1.1.1",
-        "@radix-ui/react-primitive": "2.0.1",
-        "@radix-ui/react-use-callback-ref": "1.1.0",
-        "@radix-ui/react-use-escape-keydown": "1.1.0"
+        "@radix-ui/primitive": "1.1.2",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-use-callback-ref": "1.1.1",
+        "@radix-ui/react-use-escape-keydown": "1.1.1"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6864,22 +7070,40 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+      "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
-      "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
+      "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
       "license": "MIT",
       "dependencies": {
         "@floating-ui/react-dom": "^2.0.0",
-        "@radix-ui/react-arrow": "1.1.1",
-        "@radix-ui/react-compose-refs": "1.1.1",
-        "@radix-ui/react-context": "1.1.1",
-        "@radix-ui/react-primitive": "2.0.1",
-        "@radix-ui/react-use-callback-ref": "1.1.0",
-        "@radix-ui/react-use-layout-effect": "1.1.0",
-        "@radix-ui/react-use-rect": "1.1.0",
-        "@radix-ui/react-use-size": "1.1.0",
-        "@radix-ui/rect": "1.1.0"
+        "@radix-ui/react-arrow": "1.1.7",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-use-callback-ref": "1.1.1",
+        "@radix-ui/react-use-layout-effect": "1.1.1",
+        "@radix-ui/react-use-rect": "1.1.1",
+        "@radix-ui/react-use-size": "1.1.1",
+        "@radix-ui/rect": "1.1.1"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6897,13 +7121,37 @@
       }
     },
     "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
-      "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+      "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/react-primitive": "2.0.1",
-        "@radix-ui/react-use-layout-effect": "1.1.0"
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+      "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6921,12 +7169,12 @@
       }
     },
     "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
-      "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+      "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/react-slot": "1.1.1"
+        "@radix-ui/react-slot": "1.2.3"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6943,13 +7191,29 @@
         }
       }
     },
-    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
       "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
-      "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+      "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+      "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/react-compose-refs": "1.1.1"
+        "@radix-ui/react-use-effect-event": "0.0.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -6961,6 +7225,81 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+      "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-callback-ref": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+      "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+      "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/rect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+      "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+      "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+      "license": "MIT"
+    },
     "node_modules/@radix-ui/react-use-callback-ref": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
@@ -6992,6 +7331,39 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-use-effect-event": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+      "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+      "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-use-escape-keydown": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
@@ -7073,12 +7445,12 @@
       }
     },
     "node_modules/@radix-ui/react-visually-hidden": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
-      "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+      "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/react-primitive": "2.0.1"
+        "@radix-ui/react-primitive": "2.1.3"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -7095,28 +7467,13 @@
         }
       }
     },
-    "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
-      "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
-      "license": "MIT",
-      "peerDependencies": {
-        "@types/react": "*",
-        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
-      "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+      "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/react-slot": "1.1.1"
+        "@radix-ui/react-slot": "1.2.3"
       },
       "peerDependencies": {
         "@types/react": "*",
@@ -7133,24 +7490,6 @@
         }
       }
     },
-    "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
-      "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
-      "license": "MIT",
-      "dependencies": {
-        "@radix-ui/react-compose-refs": "1.1.1"
-      },
-      "peerDependencies": {
-        "@types/react": "*",
-        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/@radix-ui/rect": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
@@ -8679,6 +9018,21 @@
         "@types/koa": "*"
       }
     },
+    "node_modules/@types/lodash": {
+      "version": "4.17.17",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz",
+      "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==",
+      "dev": true
+    },
+    "node_modules/@types/lodash.debounce": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
+      "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
     "node_modules/@types/luxon": {
       "version": "3.4.2",
       "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
@@ -15849,6 +16203,11 @@
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
       "devOptional": true
     },
+    "node_modules/lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
+    },
     "node_modules/lodash.includes": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -18666,6 +19025,16 @@
         }
       }
     },
+    "node_modules/react-resizable-panels": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.2.tgz",
+      "integrity": "sha512-j4RNII75fnHkLnbsTb5G5YsDvJsSEZrJK2XSF2z0Tc2jIonYlIVir/Yh/5LvcUFCfs1HqrMAoiBFmIrRjC4XnA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+        "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+      }
+    },
     "node_modules/react-style-singleton": {
       "version": "2.2.3",
       "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
diff --git a/package.json b/package.json
index 91477f6..e7edf34 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
     "@types/jest": "^29.5.11",
     "@types/koa": "^2.13.7",
     "@types/koa__cors": "^4.0.0",
+    "@types/lodash.debounce": "^4.0.9",
     "@types/luxon": "^3.4.2",
     "@types/node": "^16.11.6",
     "@types/node-fetch": "^2.6.12",
@@ -98,13 +99,14 @@
     "@radix-ui/react-dropdown-menu": "^2.1.4",
     "@radix-ui/react-label": "^2.1.0",
     "@radix-ui/react-popover": "^1.1.4",
-    "@radix-ui/react-scroll-area": "^1.2.2",
+    "@radix-ui/react-scroll-area": "^1.2.9",
     "@radix-ui/react-select": "^2.1.2",
+    "@radix-ui/react-separator": "^1.1.7",
     "@radix-ui/react-slider": "^1.2.1",
-    "@radix-ui/react-slot": "^1.1.0",
+    "@radix-ui/react-slot": "^1.2.3",
     "@radix-ui/react-switch": "^1.1.1",
     "@radix-ui/react-tabs": "^1.1.3",
-    "@radix-ui/react-tooltip": "^1.1.6",
+    "@radix-ui/react-tooltip": "^1.2.7",
     "@tabler/icons-react": "^2.14.0",
     "async-mutex": "^0.5.0",
     "axios": "^1.3.4",
@@ -122,6 +124,7 @@
     "koa": "^2.14.2",
     "koa-proxies": "^0.12.3",
     "langchain": "^0.3.2",
+    "lodash.debounce": "^4.0.8",
     "lucide-react": "^0.462.0",
     "luxon": "^3.5.0",
     "next-i18next": "^13.2.2",
@@ -130,6 +133,7 @@
     "react-dom": "^18.2.0",
     "react-dropzone": "^14.3.5",
     "react-markdown": "^9.0.1",
+    "react-resizable-panels": "^3.0.2",
     "react-syntax-highlighter": "^15.5.0",
     "sse": "github:mpetazzoni/sse.js",
     "tailwind-merge": "^2.5.5",
diff --git a/src/LLMProviders/chainManager.ts b/src/LLMProviders/chainManager.ts
index 3e1c14d..50f12e9 100644
--- a/src/LLMProviders/chainManager.ts
+++ b/src/LLMProviders/chainManager.ts
@@ -32,6 +32,8 @@ import MemoryManager from "./memoryManager";
 import PromptManager from "./promptManager";
 
 export default class ChainManager {
+  // TODO: These chains are deprecated since we now use direct chat model calls in chain runners
+  // Consider removing after verifying no dependencies remain
   private chain: RunnableSequence;
   private retrievalChain: RunnableSequence;
   private retrievedDocuments: Document[] = [];
@@ -72,6 +74,8 @@ export default class ChainManager {
     await this.createChainWithNewModel();
   }
 
+  // TODO: These methods are deprecated - chain runners now use direct chat model calls
+  // Remove after confirming no usage remains
   public getChain(): RunnableSequence {
     return this.chain;
   }
@@ -93,6 +97,8 @@ export default class ChainManager {
     }
   }
 
+  // TODO: This method is deprecated - chain validation no longer needed
+  // Remove after confirming no dependencies
   private validateChainInitialization() {
     if (!this.chain || !isSupportedChain(this.chain)) {
       console.error("Chain is not initialized properly, re-initializing chain: ", getChainType());
@@ -175,6 +181,8 @@ export default class ChainManager {
     }
   }
 
+  // TODO: This method is deprecated - chain runners now handle chain logic directly
+  // Remove after confirming no usage remains
   async setChain(chainType: ChainType, options: SetChainOptions = {}): Promise<void> {
     if (!this.chatModelManager.validateChatModel(this.chatModelManager.getChatModel())) {
       console.error("setChain failed: No chat model set.");
@@ -190,6 +198,7 @@ export default class ChainManager {
 
     switch (chainType) {
       case ChainType.LLM_CHAIN: {
+        // TODO: LLMChainRunner now handles this directly without chains
         this.chain = ChainFactory.createNewLLMChain({
           llm: chatModel,
           memory: memory,
@@ -202,6 +211,7 @@ export default class ChainManager {
       }
 
       case ChainType.VAULT_QA_CHAIN: {
+        // TODO: VaultQAChainRunner now handles this directly without chains
         await this.initializeQAChain(options);
 
         const retriever = new HybridRetriever({
diff --git a/src/LLMProviders/chainRunner.ts b/src/LLMProviders/chainRunner.ts
index 3a3e662..4cc8b06 100644
--- a/src/LLMProviders/chainRunner.ts
+++ b/src/LLMProviders/chainRunner.ts
@@ -15,7 +15,8 @@ import {
   MessageContent,
 } from "@/imageProcessing/imageProcessor";
 import { BrevilabsClient } from "@/LLMProviders/brevilabsClient";
-import { logInfo } from "@/logger";
+import { logError, logInfo, logWarn } from "@/logger";
+import { HybridRetriever } from "@/search/hybridRetriever";
 import { getSettings, getSystemPrompt } from "@/settings/model";
 import { ChatMessage } from "@/sharedState";
 import { ToolManager } from "@/tools/toolManager";
@@ -151,10 +152,14 @@ abstract class BaseChainRunner implements ChainRunner {
     abortController: AbortController,
     addMessage: (message: ChatMessage) => void,
     updateCurrentAiMessage: (message: string) => void,
-    debug: boolean,
     sources?: { title: string; score: number }[]
   ) {
-    if (fullAIResponse && abortController.signal.reason !== ABORT_REASON.NEW_CHAT) {
+    // Save to memory and add message if we have a response
+    // Skip only if it's a NEW_CHAT abort (clearing everything)
+    if (
+      fullAIResponse &&
+      !(abortController.signal.aborted && abortController.signal.reason === ABORT_REASON.NEW_CHAT)
+    ) {
       await this.chainManager.memoryManager
         .getMemory()
         .saveContext({ input: userMessage.message }, { output: fullAIResponse });
@@ -166,28 +171,30 @@ abstract class BaseChainRunner implements ChainRunner {
         timestamp: formatDateTime(new Date()),
         sources: sources,
       });
+
+      // Clear the streaming message since it's now in chat history
+      updateCurrentAiMessage("");
+    } else if (abortController.signal.reason === ABORT_REASON.NEW_CHAT) {
+      // Also clear if it's a new chat
+      updateCurrentAiMessage("");
     }
-    updateCurrentAiMessage("");
-    if (debug) {
-      console.log(
-        "==== Chat Memory ====\n",
-        (this.chainManager.memoryManager.getMemory().chatHistory as any).messages.map(
-          (m: any) => m.content
-        )
-      );
-      console.log("==== Final AI Response ====\n", fullAIResponse);
-    }
+    logInfo(
+      "==== Chat Memory ====\n",
+      (this.chainManager.memoryManager.getMemory().chatHistory as any).messages.map(
+        (m: any) => m.content
+      )
+    );
+    logInfo("==== Final AI Response ====\n", fullAIResponse);
     return fullAIResponse;
   }
 
   protected async handleError(
     error: any,
-    debug: boolean,
     addMessage?: (message: ChatMessage) => void,
     updateCurrentAiMessage?: (message: string) => void
   ) {
     const msg = err2String(error);
-    if (debug) console.error("Error during LLM invocation:", msg);
+    logError("Error during LLM invocation:", msg);
     const errorData = error?.response?.data?.error || msg;
     const errorCode = errorData?.code || msg;
     let errorMessage = "";
@@ -202,7 +209,7 @@ abstract class BaseChainRunner implements ChainRunner {
       errorMessage = `${errorCode}`;
     }
 
-    console.error(errorData);
+    logError(errorData);
 
     if (addMessage && updateCurrentAiMessage) {
       updateCurrentAiMessage("");
@@ -231,7 +238,7 @@ abstract class BaseChainRunner implements ChainRunner {
     } else {
       // Fallback to Notice if message handlers aren't provided
       new Notice(errorMessage);
-      console.error(errorData);
+      logError(errorData);
     }
   }
 }
@@ -248,30 +255,80 @@ class LLMChainRunner extends BaseChainRunner {
       updateLoading?: (loading: boolean) => void;
     }
   ): Promise<string> {
-    const { debug = false } = options;
     const streamer = new ThinkBlockStreamer(updateCurrentAiMessage);
 
     try {
-      const chain = this.chainManager.getChain();
-      const chatStream = await chain.stream({
-        input: userMessage.message,
-      } as any);
+      // Get chat history from memory
+      const memory = this.chainManager.memoryManager.getMemory();
+      const memoryVariables = await memory.loadMemoryVariables({});
+      const chatHistory = extractChatHistory(memoryVariables);
+
+      // Create messages array starting with system message
+      const messages: any[] = [];
+
+      // Add system message if available
+      const systemPrompt = getSystemPrompt();
+      const chatModel = this.chainManager.chatModelManager.getChatModel();
+
+      if (systemPrompt) {
+        messages.push({
+          role: getMessageRole(chatModel),
+          content: systemPrompt,
+        });
+      }
+
+      // Add chat history
+      for (const entry of chatHistory) {
+        messages.push({ role: entry.role, content: entry.content });
+      }
+
+      // Add current user message
+      messages.push({
+        role: "user",
+        content: userMessage.message,
+      });
+
+      logInfo("==== Final Request to AI ====\n", messages);
+
+      // Stream with abort signal
+      const chatStream = await withSuppressedTokenWarnings(() =>
+        this.chainManager.chatModelManager.getChatModel().stream(messages, {
+          signal: abortController.signal,
+        })
+      );
 
       for await (const chunk of chatStream) {
-        if (abortController.signal.aborted) break;
+        if (abortController.signal.aborted) {
+          logInfo("Stream iteration aborted", { reason: abortController.signal.reason });
+          break;
+        }
         streamer.processChunk(chunk);
       }
-    } catch (error) {
-      await this.handleError(error, debug, addMessage, updateCurrentAiMessage);
+    } catch (error: any) {
+      // Check if the error is due to abort signal
+      if (error.name === "AbortError" || abortController.signal.aborted) {
+        logInfo("Stream aborted by user", { reason: abortController.signal.reason });
+        // Don't show error message for user-initiated aborts
+      } else {
+        await this.handleError(error, addMessage, updateCurrentAiMessage);
+      }
+    }
+
+    // Always return the response, even if partial
+    const response = streamer.close();
+
+    // Only skip saving if it's a new chat (clearing everything)
+    if (abortController.signal.aborted && abortController.signal.reason === ABORT_REASON.NEW_CHAT) {
+      updateCurrentAiMessage("");
+      return "";
     }
 
     return this.handleResponse(
-      streamer.close(),
+      response,
       userMessage,
       abortController,
       addMessage,
-      updateCurrentAiMessage,
-      debug
+      updateCurrentAiMessage
     );
   }
 }
@@ -288,8 +345,7 @@ class VaultQAChainRunner extends BaseChainRunner {
       updateLoading?: (loading: boolean) => void;
     }
   ): Promise<string> {
-    const { debug = false } = options;
-    let fullAIResponse = "";
+    const streamer = new ThinkBlockStreamer(updateCurrentAiMessage);
 
     try {
       // Add check for empty index
@@ -300,37 +356,113 @@ class VaultQAChainRunner extends BaseChainRunner {
           userMessage,
           abortController,
           addMessage,
-          updateCurrentAiMessage,
-          debug
+          updateCurrentAiMessage
         );
       }
 
+      // Get chat history from memory
       const memory = this.chainManager.memoryManager.getMemory();
       const memoryVariables = await memory.loadMemoryVariables({});
       const chatHistory = extractChatHistory(memoryVariables);
-      const qaStream = await this.chainManager.getRetrievalChain().stream({
-        question: userMessage.message,
-        chat_history: chatHistory,
-      } as any);
-
-      for await (const chunk of qaStream) {
-        if (abortController.signal.aborted) break;
-        fullAIResponse += chunk.content;
-        updateCurrentAiMessage(fullAIResponse);
+
+      // Generate standalone question from user message + chat history
+      // This is similar to what the conversational retrieval chain does
+      let standaloneQuestion = userMessage.message;
+      if (chatHistory.length > 0) {
+        // For simplicity, we'll use the original question directly
+        // The original chain would rephrase it, but this approach should work for most cases
+        standaloneQuestion = userMessage.message;
+      }
+
+      // Create retriever (similar to how it's done in chainManager)
+      const retriever = new HybridRetriever({
+        minSimilarityScore: 0.01,
+        maxK: getSettings().maxSourceChunks,
+        salientTerms: [],
+      });
+
+      // Retrieve relevant documents
+      const retrievedDocs = await retriever.getRelevantDocuments(standaloneQuestion);
+
+      // Store retrieved documents for sources
+      this.chainManager.storeRetrieverDocuments(retrievedDocs);
+
+      // Format documents as context
+      const context = retrievedDocs.map((doc: any) => doc.pageContent).join("\n\n");
+
+      // Create messages array
+      const messages: any[] = [];
+
+      // Add system message with QA instruction
+      const systemPrompt = getSystemPrompt();
+      const qaInstructions =
+        "\n\nAnswer the question with as detailed as possible based only on the following context:\n" +
+        context;
+      const fullSystemMessage = systemPrompt + qaInstructions;
+
+      const chatModel = this.chainManager.chatModelManager.getChatModel();
+      if (fullSystemMessage) {
+        messages.push({
+          role: getMessageRole(chatModel),
+          content: fullSystemMessage,
+        });
       }
 
-      fullAIResponse = this.addSourcestoResponse(fullAIResponse);
-    } catch (error) {
-      await this.handleError(error, debug, addMessage, updateCurrentAiMessage);
+      // Add chat history
+      for (const entry of chatHistory) {
+        messages.push({ role: entry.role, content: entry.content });
+      }
+
+      // Add current user question
+      messages.push({
+        role: "user",
+        content: userMessage.message,
+      });
+
+      logInfo("==== Final Request to AI ====\n", messages);
+
+      // Stream with abort signal
+      const chatStream = await withSuppressedTokenWarnings(() =>
+        this.chainManager.chatModelManager.getChatModel().stream(messages, {
+          signal: abortController.signal,
+        })
+      );
+
+      for await (const chunk of chatStream) {
+        if (abortController.signal.aborted) {
+          logInfo("VaultQA stream iteration aborted", { reason: abortController.signal.reason });
+          break;
+        }
+        streamer.processChunk(chunk);
+      }
+    } catch (error: any) {
+      // Check if the error is due to abort signal
+      if (error.name === "AbortError" || abortController.signal.aborted) {
+        logInfo("VaultQA stream aborted by user", { reason: abortController.signal.reason });
+        // Don't show error message for user-initiated aborts
+      } else {
+        await this.handleError(error, addMessage, updateCurrentAiMessage);
+      }
     }
 
+    // Always get the response, even if partial
+    let fullAIResponse = streamer.close();
+
+    // Only skip saving if it's a new chat (clearing everything)
+    if (abortController.signal.aborted && abortController.signal.reason === ABORT_REASON.NEW_CHAT) {
+      updateCurrentAiMessage("");
+      return "";
+    }
+
+    // Add sources to the response
+    fullAIResponse = this.addSourcestoResponse(fullAIResponse);
+
     return this.handleResponse(
       fullAIResponse,
       userMessage,
       abortController,
       addMessage,
-      updateCurrentAiMessage,
-      debug
+      updateCurrentAiMessage
     );
   }
 
@@ -462,8 +594,7 @@ class CopilotPlusChainRunner extends BaseChainRunner {
     textContent: string,
     userMessage: ChatMessage,
     abortController: AbortController,
-    updateCurrentAiMessage: (message: string) => void,
-    debug: boolean
+    updateCurrentAiMessage: (message: string) => void
   ): Promise<string> {
     // Get chat history
     const memory = this.chainManager.memoryManager.getMemory();
@@ -520,11 +651,18 @@ class CopilotPlusChainRunner extends BaseChainRunner {
 
     // Wrap the stream call with warning suppression
     const chatStream = await withSuppressedTokenWarnings(() =>
-      this.chainManager.chatModelManager.getChatModel().stream(messages)
+      this.chainManager.chatModelManager.getChatModel().stream(messages, {
+        signal: abortController.signal,
+      })
     );
 
     for await (const chunk of chatStream) {
-      if (abortController.signal.aborted) break;
+      if (abortController.signal.aborted) {
+        logInfo("CopilotPlus multimodal stream iteration aborted", {
+          reason: abortController.signal.reason,
+        });
+        break;
+      }
       streamer.processChunk(chunk);
     }
 
@@ -543,9 +681,16 @@ class CopilotPlusChainRunner extends BaseChainRunner {
       updateLoadingMessage?: (message: string) => void;
     }
   ): Promise<string> {
-    const { debug = false, updateLoadingMessage } = options;
+    const { updateLoadingMessage } = options;
     let fullAIResponse = "";
     let sources: { title: string; score: number }[] = [];
+    let currentPartialResponse = "";
+
+    // Wrapper to track partial response
+    const trackAndUpdateAiMessage = (message: string) => {
+      currentPartialResponse = message;
+      updateCurrentAiMessage(message);
+    };
 
     try {
       // Check if this is a YouTube-only message
@@ -562,8 +707,7 @@ class CopilotPlusChainRunner extends BaseChainRunner {
                 userMessage,
                 abortController,
                 addMessage,
-                updateCurrentAiMessage,
-                debug
+                updateCurrentAiMessage
               );
             }
             return this.handleResponse(
@@ -571,24 +715,22 @@ class CopilotPlusChainRunner extends BaseChainRunner {
               userMessage,
               abortController,
               addMessage,
-              updateCurrentAiMessage,
-              debug
+              updateCurrentAiMessage
             );
           } catch (error) {
-            console.error("Error processing YouTube video:", error);
+            logError("Error processing YouTube video:", error);
             return this.handleResponse(
               failMessage,
               userMessage,
               abortController,
               addMessage,
-              updateCurrentAiMessage,
-              debug
+              updateCurrentAiMessage
             );
           }
         }
       }
 
-      if (debug) console.log("==== Step 1: Analyzing intent ====");
+      logInfo("==== Step 1: Analyzing intent ====");
       let toolCalls;
       // Use the original message for intent analysis
       const messageForAnalysis = userMessage.originalMessage || userMessage.message;
@@ -600,8 +742,7 @@ class CopilotPlusChainRunner extends BaseChainRunner {
           userMessage,
           abortController,
           addMessage,
-          updateCurrentAiMessage,
-          debug
+          updateCurrentAiMessage
         );
       }
 
@@ -612,7 +753,7 @@ class CopilotPlusChainRunner extends BaseChainRunner {
         .join(" ")
         .trim();
 
-      const toolOutputs = await this.executeToolCalls(toolCalls, debug, updateLoadingMessage);
+      const toolOutputs = await this.executeToolCalls(toolCalls, updateLoadingMessage);
       const localSearchResult = toolOutputs.find(
         (output) => output.tool === "localSearch" && output.output && output.output.length > 0
       );
@@ -623,14 +764,14 @@ class CopilotPlusChainRunner extends BaseChainRunner {
       const chatHistory = extractChatHistory(memoryVariables);
 
       if (localSearchResult) {
-        if (debug) console.log("==== Step 2: Processing local search results ====");
+        logInfo("==== Step 2: Processing local search results ====");
         const documents = JSON.parse(localSearchResult.output);
 
-        if (debug) console.log("==== Step 3: Condensing Question ====");
+        logInfo("==== Step 3: Condensing Question ====");
         const standaloneQuestion = await getStandaloneQuestion(cleanedUserMessage, chatHistory);
-        if (debug) console.log("Condensed standalone question: ", standaloneQuestion);
+        logInfo("Condensed standalone question: ", standaloneQuestion);
 
-        if (debug) console.log("==== Step 4: Preparing context ====");
+        logInfo("==== Step 4: Preparing context ====");
         const timeExpression = this.getTimeExpression(toolCalls);
         const context = this.prepareLocalSearchResult(documents, timeExpression);
 
@@ -640,8 +781,8 @@ class CopilotPlusChainRunner extends BaseChainRunner {
           currentTimeOutputs
         );
 
-        if (debug) console.log(context);
-        if (debug) console.log("==== Step 5: Invoking QA Chain ====");
+        logInfo(context);
+        logInfo("==== Step 5: Invoking QA Chain ====");
         const qaPrompt = await this.chainManager.promptManager.getQAPrompt({
           question: enhancedQuestion,
           context,
@@ -652,8 +793,7 @@ class CopilotPlusChainRunner extends BaseChainRunner {
           qaPrompt,
           userMessage,
           abortController,
-          updateCurrentAiMessage,
-          debug
+          trackAndUpdateAiMessage
         );
 
         // Append sources to the response
@@ -671,14 +811,31 @@ class CopilotPlusChainRunner extends BaseChainRunner {
           enhancedUserMessage,
           userMessage,
           abortController,
-          updateCurrentAiMessage,
-          debug
+          trackAndUpdateAiMessage
         );
       }
-    } catch (error) {
+    } catch (error: any) {
       // Reset loading message to default
       updateLoadingMessage?.(LOADING_MESSAGES.DEFAULT);
-      await this.handleError(error, debug, addMessage, updateCurrentAiMessage);
+
+      // Check if the error is due to abort signal
+      if (error.name === "AbortError" || abortController.signal.aborted) {
+        logInfo("CopilotPlus stream aborted by user", { reason: abortController.signal.reason });
+        // Don't show error message for user-initiated aborts
+      } else {
+        await this.handleError(error, addMessage, updateCurrentAiMessage);
+      }
+    }
+
+    // Only skip saving if it's a new chat (clearing everything)
+    if (abortController.signal.aborted && abortController.signal.reason === ABORT_REASON.NEW_CHAT) {
+      updateCurrentAiMessage("");
+      return "";
+    }
+
+    // If aborted but not a new chat, use the partial response
+    if (abortController.signal.aborted && currentPartialResponse) {
+      fullAIResponse = currentPartialResponse;
     }
 
     return this.handleResponse(
@@ -687,14 +844,13 @@ class CopilotPlusChainRunner extends BaseChainRunner {
       abortController,
       addMessage,
       updateCurrentAiMessage,
-      debug,
       sources
     );
   }
 
   private getSources(documents: any): { title: string; score: number }[] {
     if (!documents || !Array.isArray(documents)) {
-      console.warn("No valid documents provided to getSources");
+      logWarn("No valid documents provided to getSources");
       return [];
     }
     return this.sortUniqueDocsByScore(documents);
@@ -706,7 +862,7 @@ class CopilotPlusChainRunner extends BaseChainRunner {
     // Iterate through all documents
     for (const doc of documents) {
       if (!doc.title || (!doc?.score && !doc?.rerank_score)) {
-        console.warn("Invalid document structure:", doc);
+        logWarn("Invalid document structure:", doc);
         continue;
       }
 
@@ -730,14 +886,11 @@ class CopilotPlusChainRunner extends BaseChainRunner {
 
   private async executeToolCalls(
     toolCalls: any[],
-    debug: boolean,
     updateLoadingMessage?: (message: string) => void
   ) {
     const toolOutputs = [];
     for (const toolCall of toolCalls) {
-      if (debug) {
-        console.log(`==== Step 2: Calling tool: ${toolCall.tool.name} ====`);
-      }
+      logInfo(`==== Step 2: Calling tool: ${toolCall.tool.name} ====`);
       if (toolCall.tool.name === "localSearch") {
         updateLoadingMessage?.(LOADING_MESSAGES.READING_FILES);
       } else if (toolCall.tool.name === "webSearch") {
@@ -785,7 +938,7 @@ class CopilotPlusChainRunner extends BaseChainRunner {
     let truncatedDocs = includedDocs;
     if (totalLength > MAX_CHARS_FOR_LOCAL_SEARCH_CONTEXT) {
       const truncationRatio = MAX_CHARS_FOR_LOCAL_SEARCH_CONTEXT / totalLength;
-      console.log("Truncating documents to fit context length. Truncation ratio:", truncationRatio);
+      logInfo("Truncating documents to fit context length. Truncation ratio:", truncationRatio);
       truncatedDocs = includedDocs.map((doc) => ({
         ...doc,
         content: doc.content.slice(0, Math.floor(doc.content.length * truncationRatio)),
diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts
index ba0a246..f3ce548 100644
--- a/src/LLMProviders/chatModelManager.ts
+++ b/src/LLMProviders/chatModelManager.ts
@@ -433,13 +433,13 @@ export default class ChatModelManager {
       // eslint-disable-next-line @typescript-eslint/no-unused-vars
       const { streaming, maxTokens, maxCompletionTokens, ...pingConfig } = modelConfig;
       const isOSeries = isOSeriesModel(modelToTest.name);
-      const tokenConfig = this.handleOpenAIExtraArgs(isOSeries, 10, modelConfig.temperature);
+      const tokenConfig = this.handleOpenAIExtraArgs(isOSeries, 30, modelConfig.temperature);
       const testModel = new (this.getProviderConstructor(modelToTest))({
         ...pingConfig,
         ...tokenConfig,
       });
       await testModel.invoke([{ role: "user", content: "hello" }], {
-        timeout: 3000,
+        timeout: 8000,
       });
     };
 
diff --git a/src/LLMProviders/embeddingManager.ts b/src/LLMProviders/embeddingManager.ts
index 91772d8..bd9a535 100644
--- a/src/LLMProviders/embeddingManager.ts
+++ b/src/LLMProviders/embeddingManager.ts
@@ -52,7 +52,7 @@ export default class EmbeddingManager {
     [EmbeddingModelProviders.AZURE_OPENAI]: () => getSettings().azureOpenAIApiKey,
     [EmbeddingModelProviders.OLLAMA]: () => "default-key",
     [EmbeddingModelProviders.LM_STUDIO]: () => "default-key",
-    [EmbeddingModelProviders.OPENAI_FORMAT]: () => "",
+    [EmbeddingModelProviders.OPENAI_FORMAT]: () => "default-key",
   };
 
   private constructor() {
diff --git a/src/autocomplete/codemirrorIntegration.ts b/src/autocomplete/codemirrorIntegration.ts
index 5763dac..e32f702 100644
--- a/src/autocomplete/codemirrorIntegration.ts
+++ b/src/autocomplete/codemirrorIntegration.ts
@@ -612,6 +612,19 @@ export class CodeMirrorIntegration {
       return false;
     }
 
+    // Don't suggest for Obsidian wiki links that just started
+    if (prefix.endsWith("[[")) {
+      return false;
+    }
+
+    // Check if we're in the middle of writing a note link
+    // This regex matches if there's an open [[ without a closing ]] yet
+    const wikiLinkRegex = /\[\[[^\]]*$/;
+    if (wikiLinkRegex.test(prefix)) {
+      // We're inside a wiki link, let Obsidian's native note completion handle this
+      return false;
+    }
+
     // Check if we're in the middle of typing a word
     const trimmedPrefix = prefix.trim();
     const lastChar = trimmedPrefix[trimmedPrefix.length - 1];
@@ -621,6 +634,25 @@ export class CodeMirrorIntegration {
       // Check if there's a current word of sufficient length
       const currentWord = this.getCurrentWordFromPrefix(prefix);
       if (currentWord && currentWord.length >= 2) {
+        // Check if word is already complete and has no significant completions
+        const trieSuggestions = this.wordCompletionManager.getSuggestions(currentWord);
+        if (trieSuggestions.length === 0) {
+          return false; // No suggestions available
+        }
+
+        const isCurrentWordComplete = trieSuggestions.some(
+          (suggestion) => suggestion.word === currentWord
+        );
+
+        const hasSignificantCompletions = trieSuggestions.some(
+          (suggestion) => suggestion.word.length - currentWord.length >= 2
+        );
+
+        // If current word is complete and no significant completions exist, don't trigger
+        if (isCurrentWordComplete && !hasSignificantCompletions) {
+          return false; // No meaningful completion needed
+        }
+
         return true;
       }
     }
diff --git a/src/cache/projectContextCache.ts b/src/cache/projectContextCache.ts
index 6785e0e..cc21f8d 100644
--- a/src/cache/projectContextCache.ts
+++ b/src/cache/projectContextCache.ts
@@ -5,8 +5,7 @@ import { getMatchingPatterns, shouldIndexFile } from "@/search/searchUtils";
 import { getSettings } from "@/settings/model";
 import { MD5 } from "crypto-js";
 import { TAbstractFile, TFile, Vault } from "obsidian";
-
-const DEBOUNCE_DELAY = 5000; // 5 seconds
+import debounce from "lodash.debounce";
 
 export interface ContextCache {
   // Markdown context
@@ -37,8 +36,8 @@ export class ProjectContextCache {
   private cacheDir: string = ".copilot/project-context-cache";
   private memoryCache: Map<string, ContextCache> = new Map();
   private vault: Vault;
-  private debounceTimer: number | null = null;
   private fileCache: FileCache<string>;
+  private static readonly DEBOUNCE_DELAY = 5000; // 5 seconds
 
   private constructor() {
     this.vault = app.vault;
@@ -61,10 +60,7 @@ export class ProjectContextCache {
    * Clean up resources used by the cache
    */
   public cleanup() {
-    if (this.debounceTimer !== null) {
-      window.clearTimeout(this.debounceTimer);
-    }
-
+    this.debouncedHandleFileChange.cancel();
     this.vault.off("create", this.handleFileEvent);
     this.vault.off("modify", this.handleFileEvent);
     this.vault.off("delete", this.handleFileEvent);
@@ -85,18 +81,7 @@ export class ProjectContextCache {
     }
   };
 
-  private debouncedHandleFileChange = (file: TFile) => {
-    if (this.debounceTimer !== null) {
-      window.clearTimeout(this.debounceTimer);
-    }
-
-    this.debounceTimer = window.setTimeout(() => {
-      this.handleFileChange(file);
-      this.debounceTimer = null;
-    }, DEBOUNCE_DELAY);
-  };
-
-  private async handleFileChange(file: TFile) {
+  private handleFileChange = async (file: TFile) => {
     try {
       // Only process markdown files
       if (file.extension !== "md") {
@@ -125,7 +110,18 @@ export class ProjectContextCache {
     } catch (error) {
       logError("Error handling file change for project context cache:", error);
     }
-  }
+  };
+
+  private debouncedHandleFileChange = debounce(
+    (file: TFile) => {
+      void this.handleFileChange(file);
+    },
+    ProjectContextCache.DEBOUNCE_DELAY,
+    {
+      leading: true,
+      trailing: true,
+    }
+  );
 
   //===========================================================================
   // BASE CACHE OPERATIONS
diff --git a/src/chainFactory.ts b/src/chainFactory.ts
index 81e556a..8f16cea 100644
--- a/src/chainFactory.ts
+++ b/src/chainFactory.ts
@@ -1,3 +1,6 @@
+// TODO(logan): This entire file is deprecated since we moved to direct chat model calls in chain runners
+// Consider removing after verifying no dependencies remain
+
 import { BaseLanguageModel } from "@langchain/core/language_models/base";
 import { StringOutputParser } from "@langchain/core/output_parsers";
 import { ChatPromptTemplate, PromptTemplate } from "@langchain/core/prompts";
diff --git a/src/components/modals/InlineEditModal.tsx b/src/commands/CustomCommandChatModal.tsx
similarity index 94%
rename from src/components/modals/InlineEditModal.tsx
rename to src/commands/CustomCommandChatModal.tsx
index e917379..ba10f8b 100644
--- a/src/components/modals/InlineEditModal.tsx
+++ b/src/commands/CustomCommandChatModal.tsx
@@ -1,10 +1,9 @@
 import { CustomModel, useModelKey } from "@/aiParams";
-import { processCommandPrompt } from "@/commands/inlineEditCommandUtils";
+import { processCommandPrompt } from "@/commands/customCommandUtils";
 import { Button } from "@/components/ui/button";
 import { getModelDisplayText } from "@/components/ui/model-display";
 import ChatModelManager from "@/LLMProviders/chatModelManager";
 import { logError } from "@/logger";
-import { InlineEditCommandSettings, useSettingsValue } from "@/settings/model";
 import { findCustomModel, insertIntoEditor } from "@/utils";
 import {
   ChatPromptTemplate,
@@ -18,6 +17,8 @@ import { ArrowBigUp, Bot, Command, Copy, CornerDownLeft, PenLine } from "lucide-
 import { App, Modal, Notice, Platform } from "obsidian";
 import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import { createRoot, Root } from "react-dom/client";
+import { CustomCommand } from "@/commands/type";
+import { useSettingsValue } from "@/settings/model";
 
 // Custom hook for managing chat chain
 function useChatChain(selectedModel: CustomModel) {
@@ -61,19 +62,19 @@ function useChatChain(selectedModel: CustomModel) {
   return { chatChain, chatMemory };
 }
 
-interface InlineEditModalContentProps {
+interface CustomCommandChatModalContentProps {
   originalText: string;
-  command: InlineEditCommandSettings;
+  command: CustomCommand;
   onInsert: (message: string) => void;
   onReplace: (message: string) => void;
 }
 
-function InlineEditModalContent({
+function CustomCommandChatModalContent({
   originalText,
   command,
   onInsert,
   onReplace,
-}: InlineEditModalContentProps) {
+}: CustomCommandChatModalContentProps) {
   const [aiCurrentMessage, setAiCurrentMessage] = useState<string | null>(null);
   const [processedMessage, setProcessedMessage] = useState<string | null>(null);
   const [followupInstruction, setFollowupInstruction] = useState<string>("");
@@ -89,7 +90,7 @@ function InlineEditModalContent({
 
   const { chatChain, chatMemory } = useChatChain(selectedModel);
 
-  const commandName = command.name;
+  const commandTitle = command.title;
 
   // Reusable function to handle streaming responses wrapped in useCallback
   const streamResponse = useCallback(
@@ -150,7 +151,7 @@ function InlineEditModalContent({
       }
 
       try {
-        const prompt = await processCommandPrompt(command.prompt, originalText);
+        const prompt = await processCommandPrompt(command.content, originalText);
         await streamResponse(prompt, abortController);
       } catch (error) {
         logError("Error in initial response:", error);
@@ -162,7 +163,7 @@ function InlineEditModalContent({
     return () => {
       abortController.abort();
     };
-  }, [command.prompt, originalText, chatChain, streamResponse]);
+  }, [command.content, originalText, chatChain, streamResponse]);
 
   const abortControllerRef = useRef<AbortController | null>(null);
 
@@ -248,10 +249,10 @@ function InlineEditModalContent({
         {originalText}
       </div>
       <div className="tw-flex tw-flex-col tw-gap-2">
-        {commandName && (
+        {commandTitle && (
           <div className="tw-flex tw-items-center tw-gap-2 tw-font-bold tw-text-normal">
             <PenLine className="tw-size-4" />
-            {commandName}
+            {commandTitle}
           </div>
         )}
       </div>
@@ -314,7 +315,7 @@ function InlineEditModalContent({
                 className="tw-flex tw-items-center tw-gap-1"
               >
                 <span>Insert</span>
-                <div className="tw-flex tw-items-center tw-text-xs tw-text-muted">
+                <div className="tw-flex tw-items-center tw-text-xs tw-text-normal">
                   {Platform.isMacOS ? (
                     <>
                       <Command className="tw-size-3" />
@@ -335,7 +336,7 @@ function InlineEditModalContent({
                 className="tw-flex tw-items-center tw-gap-1"
               >
                 <span>Replace</span>
-                <div className="tw-flex tw-items-center tw-text-xs tw-text-muted">
+                <div className="tw-flex tw-items-center tw-text-xs tw-text-normal">
                   {Platform.isMacOS ? (
                     <>
                       <Command className="tw-size-3" />
@@ -357,14 +358,14 @@ function InlineEditModalContent({
   );
 }
 
-export class InlineEditModal extends Modal {
+export class CustomCommandChatModal extends Modal {
   private root: Root;
 
   constructor(
     app: App,
     private configs: {
       selectedText: string;
-      command: InlineEditCommandSettings;
+      command: CustomCommand;
     }
   ) {
     super(app);
@@ -386,7 +387,7 @@ export class InlineEditModal extends Modal {
     };
 
     this.root.render(
-      <InlineEditModalContent
+      <CustomCommandChatModalContent
         originalText={selectedText}
         command={command}
         onInsert={handleInsert}
diff --git a/src/components/modals/InlineEditCommandSettingsModal.tsx b/src/commands/CustomCommandSettingsModal.tsx
similarity index 72%
rename from src/components/modals/InlineEditCommandSettingsModal.tsx
rename to src/commands/CustomCommandSettingsModal.tsx
index 0c6142d..87bac53 100644
--- a/src/components/modals/InlineEditCommandSettingsModal.tsx
+++ b/src/commands/CustomCommandSettingsModal.tsx
@@ -6,33 +6,30 @@ import { Checkbox } from "@/components/ui/checkbox";
 import { Textarea } from "@/components/ui/textarea";
 import React, { useState } from "react";
 import { createRoot, Root } from "react-dom/client";
-import {
-  getModelKeyFromModel,
-  InlineEditCommandSettings,
-  useSettingsValue,
-} from "@/settings/model";
-import { validateCommandName } from "@/commands/inlineEditCommandUtils";
+import { getModelKeyFromModel, useSettingsValue } from "@/settings/model";
 import { getModelDisplayText } from "@/components/ui/model-display";
 import { cn } from "@/lib/utils";
 import { ChevronDown } from "lucide-react";
 import { logError } from "@/logger";
 import { CustomPromptSyntaxInstruction } from "@/components/CustomPromptSyntaxInstruction";
+import { CustomCommand } from "@/commands/type";
+import { validateCommandName } from "@/commands/customCommandUtils";
 
 type FormErrors = {
-  name?: string;
-  prompt?: string;
+  title?: string;
+  content?: string;
 };
 
-function InlineEditCommandSettingsModalContent({
+function CustomCommandSettingsModalContent({
+  commands,
   command: initialCommand,
   onConfirm,
   onCancel,
-  onRemove,
 }: {
-  command: InlineEditCommandSettings;
-  onConfirm: (command: InlineEditCommandSettings) => void;
+  commands: CustomCommand[];
+  command: CustomCommand;
+  onConfirm: (command: CustomCommand) => void;
   onCancel: () => void;
-  onRemove?: () => void;
 }) {
   const settings = useSettingsValue();
   const activeModels = settings.activeModels
@@ -44,7 +41,7 @@ function InlineEditCommandSettingsModalContent({
   const [command, setCommand] = useState(initialCommand);
   const [errors, setErrors] = useState<FormErrors>({});
 
-  const handleUpdate = (field: keyof InlineEditCommandSettings, value: any) => {
+  const handleUpdate = (field: keyof CustomCommand, value: any) => {
     setCommand((prev) => ({
       ...prev,
       [field]: value,
@@ -58,14 +55,13 @@ function InlineEditCommandSettingsModalContent({
   const handleSubmit = () => {
     const newErrors: FormErrors = {};
 
-    try {
-      validateCommandName(command.name, initialCommand.name);
-    } catch (e) {
-      newErrors.name = e.message;
+    const nameError = validateCommandName(command.title, commands, initialCommand.title);
+    if (nameError) {
+      newErrors.title = nameError;
     }
 
-    if (!command.prompt.trim()) {
-      newErrors.prompt = "Prompt is required";
+    if (!command.content.trim()) {
+      newErrors.content = "Prompt is required";
     }
 
     if (Object.keys(newErrors).length > 0) {
@@ -79,27 +75,27 @@ function InlineEditCommandSettingsModalContent({
   return (
     <div className="tw-flex tw-flex-col tw-gap-4 tw-p-4">
       <div className="tw-flex tw-flex-col tw-gap-2">
-        <Label htmlFor="name">Name</Label>
+        <Label htmlFor="title">Name</Label>
         <Input
-          id="name"
-          value={command.name}
-          onChange={(e) => handleUpdate("name", e.target.value)}
+          id="title"
+          value={command.title}
+          onChange={(e) => handleUpdate("title", e.target.value)}
           placeholder="Enter command name"
         />
-        {errors.name && <div className="tw-text-sm tw-text-error">{errors.name}</div>}
+        {errors.title && <div className="tw-text-sm tw-text-error">{errors.title}</div>}
       </div>
 
       <div className="tw-flex tw-flex-col tw-gap-2">
-        <Label htmlFor="prompt">Prompt</Label>
+        <Label htmlFor="content">Prompt</Label>
         <CustomPromptSyntaxInstruction />
         <Textarea
-          id="prompt"
-          value={command.prompt}
-          onChange={(e) => handleUpdate("prompt", e.target.value)}
+          id="content"
+          value={command.content}
+          onChange={(e) => handleUpdate("content", e.target.value)}
           placeholder="Enter command prompt"
           className="tw-min-h-[200px]"
         />
-        {errors.prompt && <div className="tw-text-sm tw-text-error">{errors.prompt}</div>}
+        {errors.content && <div className="tw-text-sm tw-text-error">{errors.content}</div>}
       </div>
 
       <div className="tw-flex tw-flex-col tw-gap-2">
@@ -157,6 +153,15 @@ function InlineEditCommandSettingsModalContent({
         <Label htmlFor="showInContextMenu">Show in context menu</Label>
       </div>
 
+      <div className="tw-flex tw-items-center tw-gap-2">
+        <Checkbox
+          id="showInSlashMenu"
+          checked={command.showInSlashMenu}
+          onCheckedChange={(checked) => handleUpdate("showInSlashMenu", checked)}
+        />
+        <Label htmlFor="showInSlashMenu">Show in slash menu</Label>
+      </div>
+
       <div className="tw-flex tw-justify-end tw-gap-2">
         <Button variant="secondary" onClick={onCancel}>
           Cancel
@@ -169,14 +174,14 @@ function InlineEditCommandSettingsModalContent({
   );
 }
 
-export class InlineEditCommandSettingsModal extends Modal {
+export class CustomCommandSettingsModal extends Modal {
   private root: Root;
 
   constructor(
     app: App,
-    private command: InlineEditCommandSettings,
-    private onUpdate: (command: InlineEditCommandSettings) => void,
-    private onRemove?: () => void
+    private commands: CustomCommand[],
+    private command: CustomCommand,
+    private onUpdate: (command: CustomCommand) => void
   ) {
     super(app);
     // https://docs.obsidian.md/Reference/TypeScript+API/Modal/setTitle
@@ -188,22 +193,17 @@ export class InlineEditCommandSettingsModal extends Modal {
     const { contentEl } = this;
     this.root = createRoot(contentEl);
 
-    const handleConfirm = (command: InlineEditCommandSettings) => {
+    const handleConfirm = (command: CustomCommand) => {
       this.onUpdate(command);
       this.close();
     };
 
-    const handleRemove = () => {
-      this.onRemove?.();
-      this.close();
-    };
-
     this.root.render(
-      <InlineEditCommandSettingsModalContent
+      <CustomCommandSettingsModalContent
+        commands={this.commands}
         command={this.command}
         onConfirm={handleConfirm}
         onCancel={() => this.close()}
-        onRemove={this.onRemove ? handleRemove : undefined}
       />
     );
   }
diff --git a/src/commands/constants.ts b/src/commands/constants.ts
index 66be1cb..55ef7e8 100644
--- a/src/commands/constants.ts
+++ b/src/commands/constants.ts
@@ -1,88 +1,160 @@
-import { InlineEditCommandSettings } from "@/settings/model";
+import { CustomCommand } from "@/commands/type";
 
-export const SELECTED_TEXT_PLACEHOLDER = "{copilot-selection}";
+export const LEGACY_SELECTED_TEXT_PLACEHOLDER = "{copilot-selection}";
 export const COMMAND_NAME_MAX_LENGTH = 50;
+export const EMPTY_COMMAND: CustomCommand = {
+  title: "",
+  content: "",
+  showInContextMenu: false,
+  showInSlashMenu: false,
+  order: Number.MAX_SAFE_INTEGER,
+  modelKey: "",
+  lastUsedMs: 0,
+};
 
-export const DEFAULT_INLINE_EDIT_COMMANDS: InlineEditCommandSettings[] = [
+// Custom command frontmatter property constants
+export const COPILOT_COMMAND_CONTEXT_MENU_ENABLED = "copilot-command-context-menu-enabled";
+export const COPILOT_COMMAND_SLASH_ENABLED = "copilot-command-slash-enabled";
+export const COPILOT_COMMAND_CONTEXT_MENU_ORDER = "copilot-command-context-menu-order";
+export const COPILOT_COMMAND_MODEL_KEY = "copilot-command-model-key";
+export const COPILOT_COMMAND_LAST_USED = "copilot-command-last-used";
+export const DEFAULT_COMMANDS: CustomCommand[] = [
   {
-    name: "Fix grammar and spelling",
-    prompt: `Fix the grammar and spelling of {}. Preserve all formatting, line breaks, and special characters. Do not add or remove any content. Return only the corrected text.`,
+    title: "Fix grammar and spelling",
+    content: `Fix the grammar and spelling of {}. Preserve all formatting, line breaks, and special characters. Do not add or remove any content. Return only the corrected text.`,
     showInContextMenu: true,
+    showInSlashMenu: true,
+    order: 1000,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Translate to Chinese",
-    prompt: `Translate {} into Chinese:
+    title: "Translate to Chinese",
+    content: `Translate {} into Chinese:
     1. Preserve the meaning and tone
     2. Maintain appropriate cultural context
     3. Keep formatting and structure
     Return only the translated text.`,
     showInContextMenu: true,
+    showInSlashMenu: true,
+    order: 1010,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Summarize",
-    prompt: `Create a bullet-point summary of {}. Each bullet point should capture a key point. Return only the bullet-point summary.`,
+    title: "Summarize",
+    content: `Create a bullet-point summary of {}. Each bullet point should capture a key point. Return only the bullet-point summary.`,
     showInContextMenu: true,
+    showInSlashMenu: true,
+    order: 1020,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Simplify",
-    prompt: `Simplify {} to a 6th-grade reading level (ages 11-12). Use simple sentences, common words, and clear explanations. Maintain the original key concepts. Return only the simplified text.`,
+    title: "Simplify",
+    content: `Simplify {} to a 6th-grade reading level (ages 11-12). Use simple sentences, common words, and clear explanations. Maintain the original key concepts. Return only the simplified text.`,
     showInContextMenu: true,
+    showInSlashMenu: true,
+    order: 1030,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Emojify",
-    prompt: `Add relevant emojis to enhance {}. Follow these rules:
+    title: "Explain like I am 5",
+    content: `Explain {} in simple terms that a 5-year-old would understand:
+    1. Use basic vocabulary
+    2. Include simple analogies
+    3. Break down complex concepts
+    Return only the simplified explanation.`,
+    showInContextMenu: true,
+    showInSlashMenu: true,
+    order: 1040,
+    modelKey: "",
+    lastUsedMs: 0,
+  },
+  {
+    title: "Emojify",
+    content: `Add relevant emojis to enhance {}. Follow these rules:
     1. Insert emojis at natural breaks in the text
     2. Never place two emojis next to each other
     3. Keep all original text unchanged
     4. Choose emojis that match the context and tone
     Return only the emojified text.`,
     showInContextMenu: true,
+    showInSlashMenu: true,
+    order: 1050,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Make shorter",
-    prompt: `Reduce {} to half its length while preserving these elements:
+    title: "Make shorter",
+    content: `Reduce {} to half its length while preserving these elements:
     1. Main ideas and key points
     2. Essential details
     3. Original tone and style
     Return only the shortened text.`,
     showInContextMenu: true,
+    showInSlashMenu: true,
+    order: 1060,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Make longer",
-    prompt: `Expand {} to twice its length by:
+    title: "Make longer",
+    content: `Expand {} to twice its length by:
     1. Adding relevant details and examples
     2. Elaborating on key points
     3. Maintaining the original tone and style
     Return only the expanded text.`,
     showInContextMenu: true,
+    showInSlashMenu: true,
+    order: 1070,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Generate table of contents",
-    prompt: `Generate a hierarchical table of contents for {}. Use appropriate heading levels (H1, H2, H3, etc.). Include page numbers if present. Return only the table of contents.`,
+    title: "Generate table of contents",
+    content: `Generate a hierarchical table of contents for {}. Use appropriate heading levels (H1, H2, H3, etc.). Include page numbers if present. Return only the table of contents.`,
     showInContextMenu: false,
+    showInSlashMenu: false,
+    order: 1080,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Generate glossary",
-    prompt: `Create a glossary of important terms, concepts, and phrases from {}. Format each entry as "Term: Definition". Sort entries alphabetically. Return only the glossary.`,
+    title: "Generate glossary",
+    content: `Create a glossary of important terms, concepts, and phrases from {}. Format each entry as "Term: Definition". Sort entries alphabetically. Return only the glossary.`,
     showInContextMenu: false,
+    showInSlashMenu: false,
+    order: 1090,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Remove URLs",
-    prompt: `Remove all URLs from {}. Preserve all other content and formatting. URLs may be in various formats (http, https, www). Return only the text with URLs removed.`,
+    title: "Remove URLs",
+    content: `Remove all URLs from {}. Preserve all other content and formatting. URLs may be in various formats (http, https, www). Return only the text with URLs removed.`,
     showInContextMenu: false,
+    showInSlashMenu: false,
+    order: 1100,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Rewrite as tweet",
-    prompt: `Rewrite {} as a single tweet with these requirements:
+    title: "Rewrite as tweet",
+    content: `Rewrite {} as a single tweet with these requirements:
     1. Maximum 280 characters
     2. Use concise, impactful language
     3. Maintain the core message
     Return only the tweet text.`,
     showInContextMenu: false,
+    showInSlashMenu: false,
+    order: 1110,
+    modelKey: "",
+    lastUsedMs: 0,
   },
   {
-    name: "Rewrite as tweet thread",
-    prompt: `Convert {} into a Twitter thread following these rules:
+    title: "Rewrite as tweet thread",
+    content: `Convert {} into a Twitter thread following these rules:
     1. Each tweet must be under 240 characters
     2. Start with "THREAD START" on its own line
     3. Separate tweets with "\n\n---\n\n"
@@ -90,23 +162,9 @@ export const DEFAULT_INLINE_EDIT_COMMANDS: InlineEditCommandSettings[] = [
     5. Make content engaging and clear
     Return only the formatted thread.`,
     showInContextMenu: false,
-  },
-  {
-    name: "Explain like I am 5",
-    prompt: `Explain {} in simple terms that a 5-year-old would understand:
-    1. Use basic vocabulary
-    2. Include simple analogies
-    3. Break down complex concepts
-    Return only the simplified explanation.`,
-    showInContextMenu: false,
-  },
-  {
-    name: "Rewrite as press release",
-    prompt: `Transform {} into a professional press release:
-    1. Use formal, journalistic style
-    2. Include headline and dateline
-    3. Follow inverted pyramid structure
-    Return only the press release format.`,
-    showInContextMenu: false,
+    showInSlashMenu: false,
+    order: 1120,
+    modelKey: "",
+    lastUsedMs: 0,
   },
 ];
diff --git a/src/commands/contextMenu.ts b/src/commands/contextMenu.ts
new file mode 100644
index 0000000..940555e
--- /dev/null
+++ b/src/commands/contextMenu.ts
@@ -0,0 +1,18 @@
+import { getCommandId, sortCommandsByOrder } from "@/commands/customCommandUtils";
+import { Menu } from "obsidian";
+import { CustomCommand } from "./type";
+import { getCachedCustomCommands } from "@/commands/state";
+
+export function registerContextMenu(menu: Menu) {
+  const commands = getCachedCustomCommands();
+
+  sortCommandsByOrder(
+    commands.filter((command: CustomCommand) => command.showInContextMenu)
+  ).forEach((command: CustomCommand) => {
+    menu.addItem((item) => {
+      item.setTitle(`Copilot: ${command.title}`).onClick(() => {
+        (app as any).commands.executeCommandById(`copilot:${getCommandId(command.title)}`);
+      });
+    });
+  });
+}
diff --git a/src/commands/customCommandManager.ts b/src/commands/customCommandManager.ts
new file mode 100644
index 0000000..d20b142
--- /dev/null
+++ b/src/commands/customCommandManager.ts
@@ -0,0 +1,108 @@
+import { TFile } from "obsidian";
+import { getCommandFilePath, getCustomCommandsFolder } from "@/commands/customCommandUtils";
+import { CustomCommand } from "@/commands/type";
+import { CustomError } from "@/error";
+import {
+  COPILOT_COMMAND_CONTEXT_MENU_ENABLED,
+  COPILOT_COMMAND_CONTEXT_MENU_ORDER,
+  COPILOT_COMMAND_LAST_USED,
+  COPILOT_COMMAND_MODEL_KEY,
+  COPILOT_COMMAND_SLASH_ENABLED,
+} from "@/commands/constants";
+import {
+  createCachedCommand,
+  deleteCachedCommand,
+  updateCachedCommand,
+  updateCachedCommands,
+} from "./state";
+
+export class CustomCommandManager {
+  private static instance: CustomCommandManager;
+
+  static getInstance(): CustomCommandManager {
+    if (!CustomCommandManager.instance) {
+      CustomCommandManager.instance = new CustomCommandManager();
+    }
+    return CustomCommandManager.instance;
+  }
+
+  async createCommand(title: string, content: string, skipStoreUpdate = false): Promise<void> {
+    if (!skipStoreUpdate) {
+      createCachedCommand(title);
+    }
+    const folderPath = getCustomCommandsFolder();
+    const filePath = getCommandFilePath(title);
+
+    // Check if the folder exists and create it if it doesn't
+    const folderExists = await app.vault.adapter.exists(folderPath);
+    if (!folderExists) {
+      await app.vault.createFolder(folderPath);
+    }
+
+    const file = await app.vault.create(filePath, content);
+    await app.fileManager.processFrontMatter(file, (frontmatter) => {
+      frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = false;
+      frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = false;
+      frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = Number.MAX_SAFE_INTEGER;
+      frontmatter[COPILOT_COMMAND_MODEL_KEY] = "";
+      frontmatter[COPILOT_COMMAND_LAST_USED] = 0;
+    });
+  }
+
+  async recordUsage(command: CustomCommand) {
+    this.updateCommand({ ...command, lastUsedMs: Date.now() }, command.title);
+  }
+
+  async updateCommand(command: CustomCommand, prevCommandTitle: string, skipStoreUpdate = false) {
+    if (!skipStoreUpdate) {
+      updateCachedCommand(command, prevCommandTitle);
+    }
+
+    let commandFile = app.vault.getAbstractFileByPath(getCommandFilePath(command.title));
+    // Verify whether the title has changed to decide whether to rename the file
+    if (command.title !== prevCommandTitle) {
+      const newFilePath = getCommandFilePath(command.title);
+      const newFileExists = app.vault.getAbstractFileByPath(newFilePath);
+      if (newFileExists) {
+        throw new CustomError(
+          "Error saving custom prompt. Please check if the title already exists."
+        );
+      }
+      const prevFilePath = getCommandFilePath(prevCommandTitle);
+      const prevCommandFile = app.vault.getAbstractFileByPath(prevFilePath);
+      if (prevCommandFile instanceof TFile) {
+        await app.vault.rename(prevCommandFile, newFilePath);
+        commandFile = prevCommandFile;
+      }
+    }
+
+    if (!commandFile) {
+      await this.createCommand(command.title, command.content, skipStoreUpdate);
+      commandFile = app.vault.getAbstractFileByPath(getCommandFilePath(command.title));
+    }
+
+    if (commandFile instanceof TFile) {
+      await app.vault.modify(commandFile, command.content);
+      await app.fileManager.processFrontMatter(commandFile, (frontmatter) => {
+        frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
+        frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
+        frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
+        frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
+        frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
+      });
+    }
+  }
+
+  async updateCommands(commands: CustomCommand[]) {
+    updateCachedCommands(commands);
+    await Promise.all(commands.map((command) => this.updateCommand(command, command.title, true)));
+  }
+
+  async deleteCommand(command: CustomCommand) {
+    deleteCachedCommand(command.title);
+    const file = app.vault.getAbstractFileByPath(getCommandFilePath(command.title));
+    if (file instanceof TFile) {
+      await app.vault.delete(file);
+    }
+  }
+}
diff --git a/src/commands/customCommandRegister.ts b/src/commands/customCommandRegister.ts
new file mode 100644
index 0000000..3b5d681
--- /dev/null
+++ b/src/commands/customCommandRegister.ts
@@ -0,0 +1,126 @@
+import {
+  getCommandId,
+  isCustomCommandFile,
+  loadAllCustomCommands,
+  parseCustomCommandFile,
+} from "@/commands/customCommandUtils";
+import { Editor, Plugin, TFile, Vault } from "obsidian";
+import { CustomCommandChatModal } from "@/commands/CustomCommandChatModal";
+import debounce from "lodash.debounce";
+import { CustomCommand } from "@/commands/type";
+import {
+  createCachedCommand,
+  deleteCachedCommand,
+  getCachedCustomCommands,
+  updateCachedCommand,
+} from "@/commands/state";
+import { CustomCommandManager } from "@/commands/customCommandManager";
+
+/** This manager is used to register custom commands as obsidian commands */
+export class CustomCommandRegister {
+  private plugin: Plugin;
+  private vault: Vault;
+
+  constructor(plugin: Plugin, vault: Vault) {
+    this.plugin = plugin;
+    this.vault = vault;
+    this.initializeEventListeners();
+  }
+
+  async initialize() {
+    await loadAllCustomCommands();
+    this.registerCommands();
+  }
+
+  /**
+   * Register all custom commands found in the custom commands folder.
+   */
+  private async registerCommands() {
+    const commands = getCachedCustomCommands();
+    commands.forEach((command) => {
+      this.registerCommand(command);
+    });
+  }
+
+  /**
+   * Clean up resources used by the cache
+   */
+  cleanup() {
+    this.vault.off("create", this.handleFileCreation);
+    this.vault.off("delete", this.handleFileDeletion);
+    this.vault.off("rename", this.handleFileRename);
+    this.vault.off("modify", this.handleFileModify);
+  }
+
+  private initializeEventListeners() {
+    this.vault.on("create", this.handleFileCreation);
+    this.vault.on("delete", this.handleFileDeletion);
+    this.vault.on("rename", this.handleFileRename);
+    this.vault.on("modify", this.handleFileModify);
+  }
+
+  private handleFileModify = debounce(
+    async (file: TFile) => {
+      if (isCustomCommandFile(file)) {
+        const customCommand = await parseCustomCommandFile(file);
+        this.registerCommand(customCommand);
+        updateCachedCommand(customCommand, customCommand.title);
+      }
+    },
+    3000,
+    {
+      // We cannot use leading: true because frontmatter is not updated
+      // immediately when modify event is triggered.
+      leading: false,
+      trailing: true,
+    }
+  );
+
+  private handleFileCreation = async (file: TFile) => {
+    if (isCustomCommandFile(file)) {
+      const customCommand = await parseCustomCommandFile(file);
+      this.registerCommand(customCommand);
+      createCachedCommand(customCommand.title);
+    }
+  };
+
+  private handleFileDeletion = async (file: TFile) => {
+    if (isCustomCommandFile(file)) {
+      const commandId = getCommandId(file.basename);
+      (this.plugin as any).removeCommand(commandId);
+      deleteCachedCommand(file.basename);
+    }
+  };
+
+  private handleFileRename = async (file: TFile, oldPath: string) => {
+    // Remove the old command
+    const oldFilename = oldPath.split("/").pop()?.replace(/\.md$/, "");
+    if (oldFilename) {
+      const oldCommandId = getCommandId(oldFilename);
+      (this.plugin as any).removeCommand(oldCommandId);
+      deleteCachedCommand(oldFilename);
+    }
+    // Register the new command if it's still a custom command file
+    if (isCustomCommandFile(file)) {
+      const customCommand = await parseCustomCommandFile(file);
+      this.registerCommand(customCommand);
+      updateCachedCommand(customCommand, customCommand.title);
+    }
+  };
+
+  private registerCommand(customCommand: CustomCommand) {
+    const commandId = getCommandId(customCommand.title);
+    (this.plugin as any).removeCommand(commandId);
+    this.plugin.addCommand({
+      id: commandId,
+      name: customCommand.title,
+      editorCallback: (editor: Editor) => {
+        new CustomCommandChatModal(this.plugin.app, {
+          selectedText: editor.getSelection(),
+          command: customCommand,
+        }).open();
+        CustomCommandManager.getInstance().recordUsage(customCommand);
+      },
+    });
+  }
+}
diff --git a/src/customPromptProcessor.test.ts b/src/commands/customCommandUtils.test.ts
similarity index 62%
rename from src/customPromptProcessor.test.ts
rename to src/commands/customCommandUtils.test.ts
index 19d7863..988c635 100644
--- a/src/customPromptProcessor.test.ts
+++ b/src/commands/customCommandUtils.test.ts
@@ -1,4 +1,6 @@
-import { CustomPrompt, CustomPromptProcessor } from "@/customPromptProcessor";
+import { processPrompt } from "@/commands/customCommandUtils";
+import { validateCommandName } from "@/commands/customCommandUtils";
+import { CustomCommand } from "@/commands/type";
 import {
   extractNoteFiles,
   getFileContent,
@@ -7,6 +9,10 @@ import {
   getNotesFromTags,
 } from "@/utils";
 import { Notice, TFile, Vault } from "obsidian";
+import * as settingsModelModule from "@/settings/model";
+import { PromptSortStrategy } from "@/types";
+import { sortSlashCommands } from "@/commands/customCommandUtils";
+import { DEFAULT_SETTINGS } from "@/constants";
 
 // Mock Obsidian
 jest.mock("obsidian", () => ({
@@ -25,18 +31,7 @@ jest.mock("@/utils", () => ({
   processVariableNameForNotePath: jest.fn(),
 }));
 
-// Mock the specific function from the same module
-jest.mock("@/customPromptProcessor", () => {
-  // Keep original implementations for things we don't want to mock
-  const originalModule = jest.requireActual("@/customPromptProcessor");
-  return {
-    ...originalModule,
-    extractVariablesFromPrompt: jest.fn(), // Mock the specific function
-  };
-});
-
-describe("CustomPromptProcessor", () => {
-  let processor: CustomPromptProcessor;
+describe("processedPrompt()", () => {
   let mockVault: Vault;
   let mockActiveNote: TFile;
   let originalConsoleWarn: typeof console.warn;
@@ -60,9 +55,6 @@ describe("CustomPromptProcessor", () => {
       path: "path/to/active/note.md",
       basename: "Active Note",
     } as TFile;
-
-    // Create an instance of CustomPromptProcessor with mocked dependencies
-    processor = CustomPromptProcessor.getInstance(mockVault);
   });
 
   afterEach(() => {
@@ -71,28 +63,22 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should add 1 context and selectedText", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "This is a {variable} and {}.",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "here is some selected text 12345";
 
-    // Mock getFileContent to return content for {variable}
     (getFileContent as jest.Mock).mockResolvedValueOnce("here is the note content for note0");
     (getFileName as jest.Mock).mockReturnValueOnce("Variable Note");
     (getNotesFromPath as jest.Mock).mockResolvedValueOnce([mockActiveNote]);
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([
-        ["variable", "## Variable Note\n\nhere is the note content for note0"],
-      ]),
-      includedFiles: new Set([mockActiveNote]),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "This is a {variable} and {selectedText}.\n\nselectedText:\n\nhere is some selected text 12345\n\nvariable:\n\n## Variable Note\n\nhere is the note content for note0"
@@ -101,40 +87,31 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should add 2 context and no selectedText", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "This is a {variable1} and {variable2}.",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "";
 
-    // Mock getFileContent to return content for {variable1} and {variable2}
     (getFileContent as jest.Mock)
       .mockResolvedValueOnce("here is the note content for note0")
       .mockResolvedValueOnce("note content for note1");
 
-    // Mock getFileName to return appropriate names
     const { getFileName } = jest.requireMock("@/utils") as any;
     getFileName.mockReturnValueOnce("Variable1 Note").mockReturnValueOnce("Variable2 Note");
 
-    // Mock getNotesFromPath to return two mock files, one for each variable
     const mockNote1 = { path: "path/to/note1.md", basename: "Variable1 Note" } as TFile;
     const mockNote2 = { path: "path/to/note2.md", basename: "Variable2 Note" } as TFile;
     (getNotesFromPath as jest.Mock)
       .mockResolvedValueOnce([mockNote1])
       .mockResolvedValueOnce([mockNote2]);
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([
-        ["variable1", "## Variable1 Note\n\nhere is the note content for note0"],
-        ["variable2", "## Variable2 Note\n\nnote content for note1"],
-      ]),
-      includedFiles: new Set([mockNote1, mockNote2]),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "This is a {variable1} and {variable2}.\n\nvariable1:\n\n## Variable1 Note\n\nhere is the note content for note0\n\nvariable2:\n\n## Variable2 Note\n\nnote content for note1"
@@ -144,21 +121,18 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should add 1 selectedText and no context", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "Rewrite the following text {}",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "here is some selected text 12345";
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map(),
-      includedFiles: new Set(),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "Rewrite the following text {selectedText}\n\nselectedText:\n\nhere is some selected text 12345"
@@ -167,26 +141,22 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should process {activeNote} correctly", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "This is the active note: {activenote}",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "";
 
-    // Mock the getFileContent function to return a predefined content for the active note
     (getFileContent as jest.Mock).mockResolvedValue("Content of the active note");
     const { getFileName } = jest.requireMock("@/utils") as any;
     getFileName.mockReturnValue("Active Note");
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([["activenote", "## Active Note\n\nContent of the active note"]]),
-      includedFiles: new Set([mockActiveNote]),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "This is the active note: {activenote}\n\nactivenote:\n\n## Active Note\n\nContent of the active note"
@@ -196,21 +166,18 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should handle {activeNote} when no active note is provided", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "This is the active note: {activeNote}",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "";
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map(), // ActiveNote variable won't be found
-      includedFiles: new Set(),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, undefined);
+    const result = await processPrompt(doc.content, selectedText, mockVault, undefined);
 
     expect(result.processedPrompt).toBe("This is the active note: {activeNote}\n\n");
     expect(result.includedFiles).toEqual([]);
@@ -218,21 +185,18 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should handle prompts without variables", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "This is a test prompt with no variables.",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "selected text";
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map(),
-      includedFiles: new Set(),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe("This is a test prompt with no variables.\n\n");
     expect(result.includedFiles).toEqual([]);
@@ -258,15 +222,7 @@ describe("CustomPromptProcessor", () => {
     // Mock getFileContent to return content for the note
     (getFileContent as jest.Mock).mockResolvedValue("Note content for #tag");
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([["#tag", "## Tagged Note\n\nNote content for #tag"]]),
-      includedFiles: new Set([mockNoteForTag]),
-    });
-
-    const result = await processor.processCustomPrompt(customPrompt, selectedText, mockActiveNote);
+    const result = await processPrompt(customPrompt, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "Notes related to {#tag} are:\n\n#tag:\n\n## Tagged Note\n\nNote content for #tag"
@@ -305,20 +261,7 @@ describe("CustomPromptProcessor", () => {
       return "";
     });
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([
-        [
-          "#tag1,#tag2,#tag3",
-          "## Tagged Note 1\n\nNote content for #tag1\n\n## Tagged Note 2\n\nNote content for #tag2",
-        ],
-      ]),
-      includedFiles: new Set([mockNoteForTag1, mockNoteForTag2]),
-    });
-
-    const result = await processor.processCustomPrompt(customPrompt, selectedText, mockActiveNote);
+    const result = await processPrompt(customPrompt, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "Notes related to {#tag1,#tag2,#tag3} are:\n\n#tag1,#tag2,#tag3:\n\n## Tagged Note 1\n\nNote content for #tag1\n\n## Tagged Note 2\n\nNote content for #tag2"
@@ -336,15 +279,7 @@ describe("CustomPromptProcessor", () => {
     (extractNoteFiles as jest.Mock).mockReturnValue([mockTestNote]);
     (getFileContent as jest.Mock).mockResolvedValue("Test note content");
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map(),
-      includedFiles: new Set(),
-    });
-
-    const result = await processor.processCustomPrompt(customPrompt, selectedText, mockActiveNote);
+    const result = await processPrompt(customPrompt, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toContain("Content of [[Test Note]] is important");
     expect(result.processedPrompt).toContain(
@@ -373,15 +308,7 @@ describe("CustomPromptProcessor", () => {
     // Mock getNotesFromPath to return our mock note
     (getNotesFromPath as jest.Mock).mockResolvedValue([mockNoteFile]);
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([["[[Test Note]]", "## Test Note\n\nTest note content"]]),
-      includedFiles: new Set([mockNoteFile]),
-    });
-
-    const result = await processor.processCustomPrompt(customPrompt, selectedText, mockActiveNote);
+    const result = await processPrompt(customPrompt, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "Content of {[[Test Note]]} is important. Look at [[Test Note]].\n\n[[Test Note]]:\n\n## Test Note\n\nTest note content"
@@ -423,15 +350,7 @@ describe("CustomPromptProcessor", () => {
     // Mock getNotesFromPath to return our mock note
     (getNotesFromPath as jest.Mock).mockResolvedValue([mockNote1]);
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([["[[Note1]]", "## Note1\n\nNote1 content"]]),
-      includedFiles: new Set([mockNote1]), // Only Note1 comes from variables
-    });
-
-    const result = await processor.processCustomPrompt(customPrompt, selectedText, mockActiveNote);
+    const result = await processPrompt(customPrompt, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toContain(
       "{[[Note1]]} content and [[Note2]] are both important"
@@ -463,15 +382,7 @@ describe("CustomPromptProcessor", () => {
       return "";
     });
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map(),
-      includedFiles: new Set(),
-    });
-
-    const result = await processor.processCustomPrompt(customPrompt, selectedText, mockActiveNote);
+    const result = await processPrompt(customPrompt, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toContain("[[Note1]] is related to [[Note2]] and [[Note3]].");
     expect(result.processedPrompt).toContain("Title: [[Note1]]\nPath: Note1.md\n\nNote1 content");
@@ -488,24 +399,21 @@ describe("CustomPromptProcessor", () => {
     // Mock the necessary functions
     (extractNoteFiles as jest.Mock).mockReturnValue([]); // Assume it returns empty if note doesn't exist
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map(),
-      includedFiles: new Set(),
-    });
-
-    const result = await processor.processCustomPrompt(customPrompt, selectedText, mockActiveNote);
+    const result = await processPrompt(customPrompt, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe("[[Non-existent Note]] should not cause errors.\n\n");
     expect(result.includedFiles).toEqual([]);
   });
 
   it("should process {activenote} only once when it appears multiple times", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "This is the active note: {activeNote}. And again: {activeNote}",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "";
 
@@ -515,16 +423,7 @@ describe("CustomPromptProcessor", () => {
 
     (getFileContent as jest.Mock).mockResolvedValue("Content of the active note");
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    // Even if the regex matches twice, extractVariablesFromPrompt should handle it
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([["activeNote", "## Active Note\n\nContent of the active note"]]),
-      includedFiles: new Set([mockActiveNote]),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     // Check that getFileContent was called with the active note at least once
     expect(getFileContent).toHaveBeenCalledWith(mockActiveNote, mockVault);
@@ -535,23 +434,20 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should use active note content when {} is present and no selected text", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "Summarize this: {}",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "";
 
     (getFileContent as jest.Mock).mockResolvedValue("Content of the active note");
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map(),
-      includedFiles: new Set(),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "Summarize this: {selectedText}\n\nselectedText (entire active note):\n\nContent of the active note"
@@ -561,9 +457,14 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should not duplicate active note content when both {} and {activeNote} are present", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "Summarize this: {}. Additional info: {activeNote}",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "";
 
@@ -572,21 +473,11 @@ describe("CustomPromptProcessor", () => {
     const { getFileName } = jest.requireMock("@/utils") as any;
     getFileName.mockReturnValue("Active Note");
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([["activeNote", "## Active Note\n\nContent of the active note"]]),
-      includedFiles: new Set([mockActiveNote]),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "Summarize this: {selectedText}. Additional info: {activeNote}\n\nselectedText (entire active note):\n\nContent of the active note"
     );
-    // Ensure extractVariablesFromPrompt was NOT necessarily called if active note was handled by {}
-    // expect(mockExtractVariables).toHaveBeenCalled(); // Removed this check
     // Ensure getFileContent was called for the {} replacement
     expect(getFileContent).toHaveBeenCalledWith(mockActiveNote, mockVault);
     // Active note should be included only once
@@ -594,23 +485,20 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should prioritize selected text over active note when both are available", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "Analyze this: {}",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
     const selectedText = "This is the selected text";
 
     (getFileContent as jest.Mock).mockResolvedValue("Content of the active note");
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map(),
-      includedFiles: new Set(),
-    });
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "Analyze this: {selectedText}\n\nselectedText:\n\nThis is the selected text"
@@ -620,9 +508,14 @@ describe("CustomPromptProcessor", () => {
   });
 
   it("should handle invalid variable names correctly", async () => {
-    const doc: CustomPrompt = {
+    const doc: CustomCommand = {
       title: "test-prompt",
       content: "This is a test prompt with {invalidVariable} name and {activeNote}",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
     };
 
     const selectedText = "";
@@ -643,19 +536,7 @@ describe("CustomPromptProcessor", () => {
       return "";
     });
 
-    // Mock the module-level extractVariablesFromPrompt function
-    const mockExtractVariables = jest.requireMock("@/customPromptProcessor")
-      .extractVariablesFromPrompt as jest.Mock;
-    // Simulate that only activeNote is found
-    mockExtractVariables.mockResolvedValue({
-      variablesMap: new Map([["activeNote", "## Active Note\n\nActive Note Content"]]),
-      includedFiles: new Set([mockActiveNote]),
-    });
-    // We also need to mock processVariableNameForNotePath for the "activeNote" variable
-    const { processVariableNameForNotePath } = jest.requireMock("@/utils") as any;
-    processVariableNameForNotePath.mockImplementation((name: string) => name); // Simple passthrough for test
-
-    const result = await processor.processCustomPrompt(doc.content, selectedText, mockActiveNote);
+    const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
 
     expect(result.processedPrompt).toBe(
       "This is a test prompt with {invalidVariable} name and {activeNote}\n\nactiveNote:\n\n## Active Note\n\nActive Note Content"
@@ -665,3 +546,222 @@ describe("CustomPromptProcessor", () => {
     expect(console.warn).toHaveBeenCalledWith("No notes found for variable: invalidVariable");
   });
 });
+
+describe("sortSlashCommands", () => {
+  const sampleCommands = [
+    {
+      title: "Beta",
+      content: "",
+      showInContextMenu: false,
+      showInSlashMenu: true,
+      order: 2,
+      modelKey: "",
+      lastUsedMs: 100,
+    },
+    {
+      title: "Alpha",
+      content: "",
+      showInContextMenu: false,
+      showInSlashMenu: true,
+      order: 1,
+      modelKey: "",
+      lastUsedMs: 150,
+    },
+    {
+      title: "Gamma",
+      content: "",
+      showInContextMenu: false,
+      showInSlashMenu: true,
+      order: 3,
+      modelKey: "",
+      lastUsedMs: 200,
+    },
+  ];
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it("sorts by recency (TIMESTAMP)", () => {
+    jest.spyOn(settingsModelModule, "getSettings").mockReturnValue({
+      ...DEFAULT_SETTINGS,
+      promptSortStrategy: PromptSortStrategy.TIMESTAMP,
+    });
+
+    const sorted = sortSlashCommands(sampleCommands);
+    expect(sorted.map((c: any) => c.title)).toEqual(["Gamma", "Alpha", "Beta"]);
+  });
+
+  it("sorts alphabetically (ALPHABETICAL)", () => {
+    jest.spyOn(settingsModelModule, "getSettings").mockReturnValue({
+      ...DEFAULT_SETTINGS,
+      promptSortStrategy: PromptSortStrategy.ALPHABETICAL,
+    });
+
+    const sorted = sortSlashCommands(sampleCommands);
+    expect(sorted.map((c: any) => c.title)).toEqual(["Alpha", "Beta", "Gamma"]);
+  });
+
+  it("sorts by manual order (MANUAL)", () => {
+    jest.spyOn(settingsModelModule, "getSettings").mockReturnValue({
+      ...DEFAULT_SETTINGS,
+      promptSortStrategy: PromptSortStrategy.MANUAL,
+    });
+    const sorted = sortSlashCommands(sampleCommands);
+    expect(sorted.map((c: any) => c.title)).toEqual(["Alpha", "Beta", "Gamma"]);
+  });
+
+  it("returns original order for unknown strategy", () => {
+    jest.spyOn(settingsModelModule, "getSettings").mockReturnValue({
+      ...DEFAULT_SETTINGS,
+      promptSortStrategy: "UNKNOWN",
+    });
+    const sorted = sortSlashCommands(sampleCommands);
+    expect(sorted).toEqual(sampleCommands);
+  });
+});
+
+describe("parseCustomCommandFile", () => {
+  let originalApp: any;
+  let mockFile: any;
+  let mockFrontmatter: any;
+  let mockMetadata: any;
+
+  beforeEach(() => {
+    // Save and mock global app
+    originalApp = global.app;
+    mockFrontmatter = {
+      "copilot-command-context-menu-enabled": true,
+      "copilot-command-slash-enabled": false,
+      "copilot-command-context-menu-order": 42,
+      "copilot-command-model-key": "gpt-4",
+      "copilot-command-last-used": 1234567890,
+    };
+    mockMetadata = { frontmatter: mockFrontmatter };
+    global.app = {
+      vault: {
+        read: jest
+          .fn()
+          .mockResolvedValue(
+            "---\ncopilot-command-context-menu-enabled: true\ncopilot-command-slash-enabled: false\ncopilot-command-context-menu-order: 42\ncopilot-command-model-key: gpt-4\ncopilot-command-last-used: 1234567890\n---\nPrompt content here."
+          ) as any,
+      } as any,
+      metadataCache: {
+        getFileCache: jest.fn().mockReturnValue(mockMetadata) as any,
+      } as any,
+    } as any;
+    mockFile = {
+      basename: "Test Command",
+      extension: "md",
+      path: "CustomCommands/Test Command.md",
+    };
+  });
+
+  afterEach(() => {
+    global.app = originalApp;
+  });
+
+  it("parses a custom command file with frontmatter and content", async () => {
+    const { parseCustomCommandFile } = await import("@/commands/customCommandUtils");
+    const result = await parseCustomCommandFile(mockFile);
+    expect(result).toEqual({
+      title: "Test Command",
+      modelKey: "gpt-4",
+      content: "Prompt content here.",
+      showInContextMenu: true,
+      showInSlashMenu: false,
+      order: 42,
+      lastUsedMs: 1234567890,
+    });
+    expect(app.vault.read).toHaveBeenCalledWith(mockFile);
+    expect(app.metadataCache.getFileCache).toHaveBeenCalledWith(mockFile);
+  });
+
+  it("uses EMPTY_COMMAND defaults if frontmatter is missing", async () => {
+    (global.app.vault as any).read.mockResolvedValue("Prompt content only, no frontmatter.");
+    (global.app.metadataCache as any).getFileCache.mockReturnValue({});
+    const { parseCustomCommandFile } = await import("@/commands/customCommandUtils");
+    const result = await parseCustomCommandFile(mockFile);
+    expect(result).toEqual({
+      title: "Test Command",
+      modelKey: "",
+      content: "Prompt content only, no frontmatter.",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: Number.MAX_SAFE_INTEGER,
+      lastUsedMs: 0,
+    });
+  });
+});
+
+describe("validateCommandName", () => {
+  const baseCommands = [
+    {
+      title: "Command One",
+      content: "",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
+    },
+    {
+      title: "Command Two",
+      content: "",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
+    },
+    {
+      title: "Another Command",
+      content: "",
+      showInContextMenu: false,
+      showInSlashMenu: false,
+      order: 0,
+      modelKey: "",
+      lastUsedMs: 0,
+    },
+  ];
+
+  it("returns null for a unique, valid name", () => {
+    expect(validateCommandName("New Command", baseCommands)).toBeNull();
+  });
+
+  it("returns error for duplicate name (case-insensitive)", () => {
+    expect(validateCommandName("command one", baseCommands)).toBe(
+      "A command with this name already exists"
+    );
+    expect(validateCommandName("COMMAND TWO", baseCommands)).toBe(
+      "A command with this name already exists"
+    );
+  });
+
+  it("returns error for invalid characters", () => {
+    const invalids = [
+      "Invalid#Name",
+      "Invalid<Name>",
+      'Invalid:"Name"',
+      "Invalid/Name",
+      "Invalid\\Name",
+      "Invalid|Name",
+      "Invalid?Name",
+      "Invalid*Name",
+      "Invalid[Name]",
+      "Invalid^Name",
+      "Invalid\x00Name",
+      "Invalid\x1FName",
+    ];
+    for (const name of invalids) {
+      expect(validateCommandName(name, baseCommands)).toMatch(
+        /Command name contains invalid characters/
+      );
+    }
+  });
+
+  it("returns null if unchanged currentCommandName", () => {
+    expect(validateCommandName("Command One", baseCommands, "Command One")).toBeNull();
+    expect(validateCommandName("  Command One  ", baseCommands, "Command One")).toBeNull();
+  });
+});
diff --git a/src/commands/customCommandUtils.ts b/src/commands/customCommandUtils.ts
new file mode 100644
index 0000000..eb3c009
--- /dev/null
+++ b/src/commands/customCommandUtils.ts
@@ -0,0 +1,393 @@
+import {
+  COPILOT_COMMAND_CONTEXT_MENU_ENABLED,
+  COPILOT_COMMAND_CONTEXT_MENU_ORDER,
+  COPILOT_COMMAND_LAST_USED,
+  COPILOT_COMMAND_MODEL_KEY,
+  COPILOT_COMMAND_SLASH_ENABLED,
+  EMPTY_COMMAND,
+  LEGACY_SELECTED_TEXT_PLACEHOLDER,
+} from "@/commands/constants";
+import { CustomCommand } from "@/commands/type";
+import { normalizePath, Notice, TAbstractFile, TFile, Vault } from "obsidian";
+import { getSettings } from "@/settings/model";
+import { updateCachedCommands } from "./state";
+import { PromptSortStrategy } from "@/types";
+import {
+  extractNoteFiles,
+  getFileContent,
+  getFileName,
+  getNotesFromPath,
+  getNotesFromTags,
+  processVariableNameForNotePath,
+} from "@/utils";
+import { NOTE_CONTEXT_PROMPT_TAG } from "@/constants";
+
+export function validateCommandName(
+  name: string,
+  commands: CustomCommand[],
+  currentCommandName?: string
+): string | null {
+  const trimmedName = name.trim();
+
+  if (currentCommandName && trimmedName === currentCommandName) {
+    return null; // No change is allowed
+  }
+
+  // eslint-disable-next-line no-control-regex
+  const invalidChars = /[#<>:"/\\|?*[\]^\x00-\x1F]/g;
+  if (invalidChars.test(trimmedName)) {
+    return 'Command name contains invalid characters. Avoid using: < > : " / \\ | ? * [ ] ^';
+  }
+
+  if (commands.some((cmd) => cmd.title.toLowerCase() === trimmedName.toLowerCase())) {
+    return "A command with this name already exists";
+  }
+
+  return null;
+}
+
+/**
+ * Converts a custom command name to a command id. Encodes the name to avoid
+ * special characters.
+ */
+export function getCommandId(commandName: string) {
+  return encodeURIComponent(commandName.toLowerCase());
+}
+
+export function getCustomCommandsFolder(): string {
+  return normalizePath(getSettings().customPromptsFolder);
+}
+
+export function getCommandFilePath(title: string): string {
+  return `${getCustomCommandsFolder()}/${title}.md`;
+}
+
+/**
+ * Check if a file is a markdown file in the custom commands folder.
+ */
+export function isCustomCommandFile(file: TAbstractFile): boolean {
+  if (!(file instanceof TFile)) return false;
+  if (file.extension !== "md") return false;
+  const folder = getCustomCommandsFolder();
+  if (!file.path.startsWith(folder + "/")) return false;
+  // Only include direct children (no slashes in relative path)
+  const relativePath = file.path.slice(folder.length + 1);
+  if (relativePath.includes("/")) return false;
+  return true;
+}
+
+/**
+ * Utility to strip YAML frontmatter from markdown content.
+ */
+function stripFrontmatter(content: string): string {
+  if (content.startsWith("---")) {
+    const end = content.indexOf("---", 3);
+    if (end !== -1) {
+      return content.slice(end + 3).trimStart();
+    }
+  }
+  return content;
+}
+
+/**
+ * Parse a TFile as a CustomCommand by reading its content and extracting frontmatter.
+ */
+export async function parseCustomCommandFile(file: TFile): Promise<CustomCommand> {
+  const rawContent = await app.vault.read(file);
+  const content = stripFrontmatter(rawContent);
+  const metadata = app.metadataCache.getFileCache(file);
+  const showInContextMenu =
+    metadata?.frontmatter?.[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] ??
+    EMPTY_COMMAND.showInContextMenu;
+  const showInSlashMenu =
+    metadata?.frontmatter?.[COPILOT_COMMAND_SLASH_ENABLED] ?? EMPTY_COMMAND.showInSlashMenu;
+  const lastUsedMs = metadata?.frontmatter?.[COPILOT_COMMAND_LAST_USED] ?? EMPTY_COMMAND.lastUsedMs;
+  const order = metadata?.frontmatter?.[COPILOT_COMMAND_CONTEXT_MENU_ORDER] ?? EMPTY_COMMAND.order;
+  const modelKey = metadata?.frontmatter?.[COPILOT_COMMAND_MODEL_KEY] ?? EMPTY_COMMAND.modelKey;
+
+  return {
+    title: file.basename,
+    modelKey,
+    content,
+    showInContextMenu,
+    showInSlashMenu,
+    order,
+    lastUsedMs,
+  };
+}
+
+export async function loadAllCustomCommands(): Promise<CustomCommand[]> {
+  const files = app.vault.getFiles().filter((file) => isCustomCommandFile(file));
+  const commands: CustomCommand[] = await Promise.all(files.map(parseCustomCommandFile));
+  updateCachedCommands(commands);
+  return commands;
+}
+
+export function sortCommandsByOrder(commands: CustomCommand[]): CustomCommand[] {
+  return [...commands].sort((a, b) => {
+    if (a.order === b.order) {
+      return a.title.localeCompare(b.title);
+    }
+    return a.order - b.order;
+  });
+}
+
+export function sortCommandsByRecency(commands: CustomCommand[]): CustomCommand[] {
+  return [...commands].sort((a, b) => {
+    if (a.lastUsedMs === b.lastUsedMs) {
+      return a.title.localeCompare(b.title);
+    }
+    return b.lastUsedMs - a.lastUsedMs;
+  });
+}
+
+export function sortCommandsByAlphabetical(commands: CustomCommand[]): CustomCommand[] {
+  return [...commands].sort((a, b) => a.title.localeCompare(b.title));
+}
+
+/**
+ * Sort prompts of the slash commands based on the sort strategy.
+ */
+export function sortSlashCommands(commands: CustomCommand[]): CustomCommand[] {
+  const sortStrategy = getSettings().promptSortStrategy;
+  switch (sortStrategy) {
+    case PromptSortStrategy.TIMESTAMP:
+      return sortCommandsByRecency(commands);
+    case PromptSortStrategy.ALPHABETICAL:
+      return sortCommandsByAlphabetical(commands);
+    case PromptSortStrategy.MANUAL:
+      return sortCommandsByOrder(commands);
+    default:
+      return commands;
+  }
+}
+
+/**
+ * Process the custom command prompt. In addition to the regular prompt processing,
+ * it handles legacy logic such as auto appending the selected text to the prompt
+ * if it's not already present.
+ */
+export async function processCommandPrompt(
+  prompt: string,
+  selectedText: string,
+  skipAppendingSelectedText = false
+) {
+  const result = await processPrompt(
+    prompt,
+    selectedText,
+    app.vault,
+    app.workspace.getActiveFile()
+  );
+
+  const processedPrompt = result.processedPrompt;
+
+  if (processedPrompt.includes("{selectedText}") || skipAppendingSelectedText) {
+    // Containing {selectedText} means the prompt was using the custom prompt
+    // processor way of handling the selected text. No need to go through the
+    // legacy placeholder.
+    return processedPrompt;
+  }
+
+  // This is the legacy custom command selected text placeholder. It replaced
+  // {copilot-selection} in the prompt with the selected text. This is different
+  // from the custom prompt processor which uses {} in the prompt and appends
+  // the selected text to the prompt. We cannot change user's custom commands
+  // that have the old placeholder, so we need to support both.
+  // Also, selected text is required for custom commands. If neither `{}` nor
+  // `{copilot-selection}` is found, append the selected text to the prompt.
+  const index = processedPrompt.indexOf(LEGACY_SELECTED_TEXT_PLACEHOLDER);
+  if (index === -1 && selectedText.trim()) {
+    return processedPrompt + "\n\n<selectedText>" + selectedText + "</selectedText>";
+  }
+  return (
+    processedPrompt.slice(0, index) +
+    selectedText +
+    processedPrompt.slice(index + LEGACY_SELECTED_TEXT_PLACEHOLDER.length)
+  );
+}
+
+/**
+ * Find all variables between {} in a custom command prompt.
+ * {copilot-selection} is the legacy custom command special placeholder. It must
+ * be skipped when processing custom prompts because it's handled differently
+ * by the custom command prompt processor.
+ */
+const VARIABLE_REGEX = /\{(?!copilot-selection\})([^}]+)\}/g;
+
+/**
+ * Represents the result of processing a custom prompt variable.
+ */
+interface VariableProcessingResult {
+  content: string;
+  files: TFile[];
+}
+
+/**
+ * Extract variables from a custom prompt and get their content and associated
+ * files.
+ */
+async function extractVariablesFromPrompt(
+  customPrompt: string,
+  vault: Vault,
+  activeNote?: TFile | null
+): Promise<{ variablesMap: Map<string, string>; includedFiles: Set<TFile> }> {
+  const variablesMap = new Map<string, string>();
+  const includedFiles = new Set<TFile>();
+  let match: RegExpExecArray | null;
+
+  while ((match = VARIABLE_REGEX.exec(customPrompt)) !== null) {
+    const variableName = match[1].trim();
+    const variableResult: VariableProcessingResult = { content: "", files: [] };
+
+    if (variableName.toLowerCase() === "activenote") {
+      if (activeNote) {
+        const content = await getFileContent(activeNote, vault);
+        if (content) {
+          variableResult.content = `## ${getFileName(activeNote)}\n\n${content}`;
+          variableResult.files.push(activeNote);
+        }
+      } else {
+        new Notice("No active note found.");
+      }
+    } else if (variableName.startsWith("#")) {
+      // Handle tag-based variable for multiple tags
+      const tagNames = variableName
+        .slice(1)
+        .split(",")
+        .map((tag) => tag.trim());
+      const noteFiles = await getNotesFromTags(vault, tagNames);
+      const notesContent: string[] = [];
+      for (const file of noteFiles) {
+        const content = await getFileContent(file, vault);
+        if (content) {
+          notesContent.push(`## ${getFileName(file)}\n\n${content}`);
+          variableResult.files.push(file);
+        }
+      }
+      variableResult.content = notesContent.join("\n\n");
+    } else {
+      const processedVariableName = processVariableNameForNotePath(variableName);
+      const noteFiles = await getNotesFromPath(vault, processedVariableName);
+      const notesContent: string[] = [];
+      for (const file of noteFiles) {
+        const content = await getFileContent(file, vault);
+        if (content) {
+          notesContent.push(`## ${getFileName(file)}\n\n${content}`);
+          variableResult.files.push(file);
+        }
+      }
+      variableResult.content = notesContent.join("\n\n");
+    }
+
+    if (variableResult.content) {
+      variablesMap.set(variableName, variableResult.content);
+      variableResult.files.forEach((file) => includedFiles.add(file));
+    } else if (variableName.toLowerCase() !== "activenote") {
+      if (variableName.startsWith('"')) {
+        // DO NOTHING as the user probably wants to write a JSON object
+      } else {
+        console.warn(`No notes found for variable: ${variableName}`);
+      }
+    }
+  }
+
+  return { variablesMap, includedFiles };
+}
+
+/**
+ * Represents the result of processing a custom prompt.
+ */
+export interface ProcessedPromptResult {
+  processedPrompt: string;
+  includedFiles: TFile[];
+}
+
+/**
+ * Process a custom prompt by replacing variables and adding note contents.
+ * Returns the processed prompt string and a list of files included in the processing.
+ */
+export async function processPrompt(
+  customPrompt: string,
+  selectedText: string,
+  vault: Vault,
+  activeNote?: TFile | null
+): Promise<ProcessedPromptResult> {
+  const settings = getSettings();
+  const includedFiles = new Set<TFile>();
+
+  if (!settings.enableCustomPromptTemplating) {
+    // If templating is disabled, check if activeNote should be included for {}
+    if (customPrompt.includes("{}") && !selectedText && activeNote) {
+      includedFiles.add(activeNote);
+    }
+    return {
+      processedPrompt: customPrompt + "\n\n",
+      includedFiles: Array.from(includedFiles),
+    };
+  }
+
+  // Extract variables and track files included through them
+  const { variablesMap, includedFiles: variableFiles } = await extractVariablesFromPrompt(
+    customPrompt,
+    vault,
+    activeNote
+  );
+  variableFiles.forEach((file) => includedFiles.add(file));
+
+  let processedPrompt = customPrompt;
+  let additionalInfo = "";
+  let activeNoteContent: string | null = null;
+
+  if (processedPrompt.includes("{}")) {
+    processedPrompt = processedPrompt.replace(/\{\}/g, "{selectedText}");
+    if (selectedText) {
+      additionalInfo += `selectedText:\n\n${selectedText}`;
+      // Note: selectedText doesn't directly correspond to a file inclusion here
+    } else if (activeNote) {
+      activeNoteContent = await getFileContent(activeNote, vault);
+      additionalInfo += `selectedText (entire active note):\n\n${activeNoteContent}`;
+      includedFiles.add(activeNote); // Ensure active note is tracked if used for {}
+    } else {
+      additionalInfo += `selectedText:\n\n(No selected text or active note available)`;
+    }
+  }
+
+  // Add variable contents to the additional info
+  // The files are already tracked via includedFiles set
+  for (const [varName, content] of variablesMap.entries()) {
+    if (varName.toLowerCase() === "activenote" && activeNoteContent !== null) {
+      // Content already added via {} handling, but file tracking is done.
+      continue;
+    }
+    if (additionalInfo) {
+      additionalInfo += `\n\n${varName}:\n\n${content}`;
+    } else {
+      additionalInfo += `${varName}:\n\n${content}`;
+    }
+  }
+
+  // Process [[note title]] syntax
+  const noteLinkFiles = extractNoteFiles(processedPrompt, vault);
+  for (const noteFile of noteLinkFiles) {
+    // Check if this note wasn't already included via a variable
+    // We use the Set's reference equality which works for TFile objects
+    if (!includedFiles.has(noteFile)) {
+      const noteContent = await getFileContent(noteFile, vault);
+      if (noteContent) {
+        const noteContext = `<${NOTE_CONTEXT_PROMPT_TAG}> \n Title: [[${noteFile.basename}]]\nPath: ${noteFile.path}\n\n${noteContent}\n</${NOTE_CONTEXT_PROMPT_TAG}>`;
+        if (additionalInfo) {
+          additionalInfo += `\n\n`;
+        }
+        additionalInfo += `${noteContext}`;
+        includedFiles.add(noteFile); // Track files included via [[links]]
+      }
+    }
+  }
+
+  return {
+    processedPrompt: additionalInfo
+      ? `${processedPrompt}\n\n${additionalInfo}`
+      : `${processedPrompt}\n\n`,
+    includedFiles: Array.from(includedFiles),
+  };
+}
diff --git a/src/commands/index.ts b/src/commands/index.ts
index d349b96..b5ef8b2 100644
--- a/src/commands/index.ts
+++ b/src/commands/index.ts
@@ -1,28 +1,12 @@
 import { FileCache } from "@/cache/fileCache";
 import { ProjectContextCache } from "@/cache/projectContextCache";
-import {
-  getCommandById,
-  getCommandId,
-  getInlineEditCommands,
-} from "@/commands/inlineEditCommandUtils";
-import { AddPromptModal } from "@/components/modals/AddPromptModal";
 import { AdhocPromptModal } from "@/components/modals/AdhocPromptModal";
 import { DebugSearchModal } from "@/components/modals/DebugSearchModal";
-import { InlineEditModal } from "@/components/modals/InlineEditModal";
-import { ListPromptModal } from "@/components/modals/ListPromptModal";
 import { OramaSearchModal } from "@/components/modals/OramaSearchModal";
 import { RemoveFromIndexModal } from "@/components/modals/RemoveFromIndexModal";
-import { CustomPromptProcessor } from "@/customPromptProcessor";
-import { logError } from "@/logger";
 import CopilotPlugin from "@/main";
 import { getAllQAMarkdownContent } from "@/search/searchUtils";
-import {
-  CopilotSettings,
-  InlineEditCommandSettings,
-  getSettings,
-  updateSetting,
-} from "@/settings/model";
-import { err2String } from "@/utils";
+import { CopilotSettings, getSettings, updateSetting } from "@/settings/model";
 import { Editor, Notice, TFile } from "obsidian";
 import { COMMAND_IDS, COMMAND_NAMES, CommandId } from "../constants";
 
@@ -67,67 +51,11 @@ export function addCheckCommand(
   });
 }
 
-/**
- * Process an inline edit command and display a modal with the processed prompt.
- */
-async function processInlineEditCommand(editor: Editor, commandId: string) {
-  const selectedText = editor.getSelection().trim();
-  if (!selectedText) {
-    return;
-  }
-
-  const command = getCommandById(commandId);
-  if (!command) {
-    logError(`Command not found for id ${commandId}`);
-    return;
-  }
-
-  new InlineEditModal(app, {
-    selectedText,
-    command,
-  }).open();
-}
-
-export function registerInlineEditCommands(
-  plugin: CopilotPlugin,
-  prevCommands: InlineEditCommandSettings[],
-  nextCommands: InlineEditCommandSettings[]
-) {
-  prevCommands.forEach((command) => {
-    const id = getCommandId(command.name);
-    if (id) {
-      // removeCommand is not available in TypeScript for some reasons
-      // https://docs.obsidian.md/Reference/TypeScript+API/Plugin/removeCommand
-      (plugin as any).removeCommand(id);
-    }
-  });
-
-  nextCommands.forEach((command) => {
-    const id = getCommandId(command.name);
-    plugin.addCommand({
-      id,
-      name: command.name,
-      editorCallback: (editor) => {
-        processInlineEditCommand(editor, id);
-      },
-    });
-  });
-}
-
 export function registerCommands(
   plugin: CopilotPlugin,
   prev: CopilotSettings | undefined,
   next: CopilotSettings
 ) {
-  registerInlineEditCommands(
-    plugin,
-    prev?.inlineEditCommands ?? [],
-    // If a user comes from a legacy version and doesn't have inlineEditCommands
-    // in settings, we use the default commands.
-    next.inlineEditCommands ?? getInlineEditCommands()
-  );
-  const promptProcessor = CustomPromptProcessor.getInstance(plugin.app.vault);
-
   addEditorCommand(plugin, COMMAND_IDS.COUNT_WORD_AND_TOKENS_SELECTION, async (editor: Editor) => {
     const selectedText = await editor.getSelection();
     const wordCount = selectedText.split(" ").length;
@@ -162,41 +90,6 @@ export function registerCommands(
     plugin.newChat();
   });
 
-  addCommand(plugin, COMMAND_IDS.ADD_CUSTOM_PROMPT, () => {
-    new AddPromptModal(plugin.app, async (title: string, prompt: string) => {
-      try {
-        await promptProcessor.savePrompt(title, prompt);
-        new Notice("Custom prompt saved successfully.");
-      } catch (e) {
-        const msg = "An error occurred while saving the custom prompt: " + err2String(e);
-        console.error(msg);
-        throw new Error(msg);
-      }
-    }).open();
-  });
-
-  addCommand(plugin, COMMAND_IDS.APPLY_CUSTOM_PROMPT, async () => {
-    const prompts = await promptProcessor.getAllPrompts();
-    const promptTitles = prompts.map((p) => p.title);
-    new ListPromptModal(plugin.app, promptTitles, async (promptTitle: string) => {
-      if (!promptTitle) {
-        new Notice("Please select a prompt title.");
-        return;
-      }
-      try {
-        const prompt = await promptProcessor.getPrompt(promptTitle);
-        if (!prompt) {
-          new Notice(`No prompt found with the title "${promptTitle}".`);
-          return;
-        }
-        plugin.processCustomPrompt(COMMAND_IDS.APPLY_CUSTOM_PROMPT, prompt.content);
-      } catch (err) {
-        console.error(err);
-        new Notice("An error occurred.");
-      }
-    }).open();
-  });
-
   addCommand(plugin, COMMAND_IDS.APPLY_ADHOC_PROMPT, async () => {
     const modal = new AdhocPromptModal(plugin.app, async (adhocPrompt: string) => {
       try {
@@ -210,76 +103,6 @@ export function registerCommands(
     modal.open();
   });
 
-  addCheckCommand(plugin, COMMAND_IDS.DELETE_CUSTOM_PROMPT, (checking: boolean) => {
-    if (checking) {
-      return true;
-    }
-
-    promptProcessor.getAllPrompts().then((prompts) => {
-      const promptTitles = prompts.map((p) => p.title);
-      new ListPromptModal(plugin.app, promptTitles, async (promptTitle: string) => {
-        if (!promptTitle) {
-          new Notice("Please select a prompt title.");
-          return;
-        }
-
-        try {
-          await promptProcessor.deletePrompt(promptTitle);
-          new Notice(`Prompt "${promptTitle}" has been deleted.`);
-        } catch (err) {
-          console.error(err);
-          new Notice("An error occurred while deleting the prompt.");
-        }
-      }).open();
-    });
-    return true;
-  });
-
-  addCheckCommand(plugin, COMMAND_IDS.EDIT_CUSTOM_PROMPT, (checking: boolean) => {
-    if (checking) {
-      return true;
-    }
-
-    promptProcessor.getAllPrompts().then((prompts) => {
-      const promptTitles = prompts.map((p) => p.title);
-      new ListPromptModal(plugin.app, promptTitles, async (promptTitle: string) => {
-        if (!promptTitle) {
-          new Notice("Please select a prompt title.");
-          return;
-        }
-
-        try {
-          const prompt = await promptProcessor.getPrompt(promptTitle);
-          if (prompt) {
-            new AddPromptModal(
-              plugin.app,
-              async (title: string, newPrompt: string) => {
-                try {
-                  await promptProcessor.updatePrompt(promptTitle, title, newPrompt);
-                  new Notice(`Prompt "${title}" has been updated.`);
-                } catch (err) {
-                  const msg =
-                    "An error occurred while updating the custom prompt: " + err2String(err);
-                  console.error(msg);
-                  throw new Error(msg);
-                }
-              },
-              prompt.title,
-              prompt.content,
-              false
-            ).open();
-          } else {
-            new Notice(`No prompt found with the title "${promptTitle}".`);
-          }
-        } catch (err) {
-          console.error(err);
-          new Notice("An error occurred.");
-        }
-      }).open();
-    });
-    return true;
-  });
-
   addCommand(plugin, COMMAND_IDS.CLEAR_LOCAL_COPILOT_INDEX, async () => {
     await plugin.vectorStoreManager.clearIndex();
   });
diff --git a/src/commands/inlineEditCommandUtils.ts b/src/commands/inlineEditCommandUtils.ts
deleted file mode 100644
index 6d9eca0..0000000
--- a/src/commands/inlineEditCommandUtils.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import {
-  COMMAND_NAME_MAX_LENGTH,
-  DEFAULT_INLINE_EDIT_COMMANDS,
-  SELECTED_TEXT_PLACEHOLDER as LEGACY_SELECTED_TEXT_PLACEHOLDER,
-} from "@/commands/constants";
-import { processPrompt } from "@/customPromptProcessor";
-import { InlineEditCommandSettings, getSettings, useSettingsValue } from "@/settings/model";
-
-export function getCommandId(commandName: string) {
-  return encodeURIComponent(commandName.toLowerCase());
-}
-
-/**
- * Validate the command name. A command name must be:
- * - less than 50 characters
- * - not empty
- * - not duplicate an existing command name (except when editing the same command)
- * @param commandName - The name of the command.
- * @param currentCommandName - Optional. The current name of the command being edited.
- */
-export function validateCommandName(commandName: string, currentCommandName?: string) {
-  if (!commandName) {
-    throw new Error("Command name is required");
-  }
-
-  if (commandName.length > COMMAND_NAME_MAX_LENGTH) {
-    throw new Error(`Command name must be less than ${COMMAND_NAME_MAX_LENGTH} characters`);
-  }
-
-  // Check for duplicate command names, but allow keeping the same name when editing
-  const existingCommands = getInlineEditCommands();
-  if (
-    existingCommands.some(
-      (cmd) =>
-        cmd.name.toLowerCase() === commandName.toLowerCase() &&
-        cmd.name.toLowerCase() !== currentCommandName?.toLowerCase()
-    )
-  ) {
-    throw new Error("A command with this name already exists");
-  }
-}
-
-export function getCommandById(commandId: string): InlineEditCommandSettings | undefined {
-  const commandSettings = getInlineEditCommands();
-  return commandSettings.find((command) => getCommandId(command.name) === commandId);
-}
-
-export function getInlineEditCommands(): InlineEditCommandSettings[] {
-  return getSettings().inlineEditCommands ?? DEFAULT_INLINE_EDIT_COMMANDS;
-}
-
-export function useInlineEditCommands(): InlineEditCommandSettings[] {
-  const settings = useSettingsValue();
-  return settings.inlineEditCommands ?? DEFAULT_INLINE_EDIT_COMMANDS;
-}
-
-/**
- * Process the command prompt.
- */
-export async function processCommandPrompt(
-  prompt: string,
-  selectedText: string,
-  skipAppendingSelectedText = false
-) {
-  const result = await processPrompt(
-    prompt,
-    selectedText,
-    app.vault,
-    app.workspace.getActiveFile()
-  );
-
-  const processedPrompt = result.processedPrompt;
-
-  if (processedPrompt.includes("{selectedText}") || skipAppendingSelectedText) {
-    // Containing {selectedText} means the prompt was using the custom prompt
-    // processor way of handling the selected text. No need to go through the
-    // legacy placeholder.
-    return processedPrompt;
-  }
-
-  // This is the legacy custom command selected text placeholder. It replaced
-  // {copilot-selection} in the prompt with the selected text. This is different
-  // from the custom prompt processor which uses {} in the prompt and appends
-  // the selected text to the prompt. We cannot change user's custom commands
-  // that have the old placeholder, so we need to support both.
-  // Also, selected text is required for custom commands. If neither `{}` nor
-  // `{copilot-selection}` is found, append the selected text to the prompt.
-  const index = processedPrompt.indexOf(LEGACY_SELECTED_TEXT_PLACEHOLDER);
-  if (index === -1) {
-    return processedPrompt + "\n\n" + selectedText;
-  }
-  return (
-    processedPrompt.slice(0, index) +
-    selectedText +
-    processedPrompt.slice(index + LEGACY_SELECTED_TEXT_PLACEHOLDER.length)
-  );
-}
-
-export function hasModifiedCommand() {
-  const commands = getInlineEditCommands();
-  const defaultCommands = DEFAULT_INLINE_EDIT_COMMANDS;
-  return commands !== defaultCommands;
-}
diff --git a/src/commands/migrator.ts b/src/commands/migrator.ts
new file mode 100644
index 0000000..9c62f2e
--- /dev/null
+++ b/src/commands/migrator.ts
@@ -0,0 +1,121 @@
+import { CustomCommandManager } from "@/commands/customCommandManager";
+import { getCustomCommandsFolder, validateCommandName } from "@/commands/customCommandUtils";
+import { CustomCommand } from "@/commands/type";
+import { getSettings, updateSetting } from "@/settings/model";
+import {
+  COPILOT_COMMAND_CONTEXT_MENU_ORDER,
+  COPILOT_COMMAND_LAST_USED,
+  COPILOT_COMMAND_MODEL_KEY,
+  COPILOT_COMMAND_SLASH_ENABLED,
+  DEFAULT_COMMANDS,
+} from "@/commands/constants";
+import { COPILOT_COMMAND_CONTEXT_MENU_ENABLED } from "@/commands/constants";
+import { ConfirmModal } from "@/components/modals/ConfirmModal";
+import { getCachedCustomCommands } from "@/commands/state";
+
+function saveUnsupportedCommands(commands: CustomCommand[]) {
+  const folderPath = getCustomCommandsFolder();
+  const unsupportedFolderPath = `${folderPath}/unsupported`;
+  const unsupportedFolder = app.vault.getAbstractFileByPath(unsupportedFolderPath);
+  if (!unsupportedFolder) {
+    app.vault.createFolder(unsupportedFolderPath);
+  }
+  return Promise.all(
+    commands.map(async (command) => {
+      const filePath = `${unsupportedFolderPath}/${command.title}.md`;
+      const file = await app.vault.create(filePath, command.content);
+      await app.fileManager.processFrontMatter(file, (frontmatter) => {
+        frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
+        frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
+        frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
+        frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
+        frontmatter[COPILOT_COMMAND_LAST_USED] = 0;
+      });
+    })
+  );
+}
+
+/** Migrates the legacy commands in data.json to the new note format. */
+export async function migrateCommands() {
+  const legacyCommands = getSettings().inlineEditCommands;
+  if (!legacyCommands || legacyCommands.length === 0) {
+    return;
+  }
+  const commandsToMigrate: CustomCommand[] = [];
+  const unsupportedCommands: CustomCommand[] = [];
+  const existingCommands = getCachedCustomCommands();
+
+  const commands = legacyCommands.map((command, index) => ({
+    title: command.name,
+    content: command.prompt,
+    showInContextMenu: command.showInContextMenu,
+    showInSlashMenu: false,
+    order: index * 10,
+    modelKey: command.modelKey ?? "",
+    lastUsedMs: 0,
+  }));
+
+  for (const command of commands) {
+    const error = validateCommandName(command.title, existingCommands);
+    if (error) {
+      unsupportedCommands.push({
+        ...command,
+        title: encodeURIComponent(command.title),
+        content: `> ${error} \n\nOriginal name: ${command.title} \n\n${command.content}`,
+      });
+    } else {
+      commandsToMigrate.push(command);
+    }
+  }
+
+  await CustomCommandManager.getInstance().updateCommands([
+    ...existingCommands.map((command) => ({
+      ...command,
+      showInSlashMenu: true,
+    })),
+    ...commandsToMigrate,
+  ]);
+
+  let message = `We have upgraded your commands to the new format. They are now also stored as notes in ${getCustomCommandsFolder()}.`;
+  if (unsupportedCommands.length > 0) {
+    await saveUnsupportedCommands(unsupportedCommands);
+    message += `\n\nWe found ${unsupportedCommands.length} unsupported commands. They are saved in ${getCustomCommandsFolder()}/unsupported. To fix them, please resolve the errors and move the note file out of the unsupported folder.`;
+  }
+
+  updateSetting("inlineEditCommands", []);
+
+  new ConfirmModal(app, () => {}, message, "🚀 New Copilot Custom Commands", "OK", "").open();
+}
+
+/** Generates the default commands. */
+export async function generateDefaultCommands(): Promise<void> {
+  const existingCommands = getCachedCustomCommands();
+  const defaultCommands = DEFAULT_COMMANDS.filter(
+    (command) => !existingCommands.some((c) => c.title === command.title)
+  );
+  const newCommands = [...existingCommands, ...defaultCommands];
+  CustomCommandManager.getInstance().updateCommands(newCommands);
+}
+
+/** Suggests the default commands if the user has not created any commands yet. */
+export async function suggestDefaultCommands(): Promise<void> {
+  const suggestedCommand = getSettings().suggestedDefaultCommands;
+  if (suggestedCommand) {
+    // We only show the modal once
+    return;
+  }
+  const existingCommands = getCachedCustomCommands();
+  if (existingCommands.length === 0) {
+    new ConfirmModal(
+      app,
+      () => {
+        generateDefaultCommands();
+      },
+      "Would you like to add Copilot recommended commands in your custom prompts folder? These commands will be available through the right-click context menu and slash commands in chat.",
+      "Welcome to Copilot",
+      "Confirm",
+      "Skip"
+    ).open();
+    updateSetting("suggestedDefaultCommands", true);
+  }
+}
diff --git a/src/commands/state.ts b/src/commands/state.ts
new file mode 100644
index 0000000..b58827a
--- /dev/null
+++ b/src/commands/state.ts
@@ -0,0 +1,48 @@
+import { atom, createStore } from "jotai";
+import { useAtomValue } from "jotai";
+import { CustomCommand } from "./type";
+import { EMPTY_COMMAND } from "@/commands/constants";
+
+const customCommandsStore = createStore();
+const customCommandsAtom = atom<CustomCommand[]>([]);
+
+export function createCachedCommand(title: string) {
+  const commands = customCommandsStore.get(customCommandsAtom);
+  if (commands.some((command) => command.title === title)) {
+    return;
+  }
+  customCommandsStore.set(customCommandsAtom, [...commands, { ...EMPTY_COMMAND, title }]);
+}
+
+export function deleteCachedCommand(title: string) {
+  const commands = customCommandsStore.get(customCommandsAtom);
+  customCommandsStore.set(
+    customCommandsAtom,
+    commands.filter((command) => command.title !== title)
+  );
+}
+
+export function updateCachedCommand(command: CustomCommand, prevCommandTitle: string) {
+  const commands = customCommandsStore.get(customCommandsAtom);
+  let updatedCommands: CustomCommand[];
+  const prevIndex = prevCommandTitle ? commands.findIndex((c) => c.title === prevCommandTitle) : -1;
+  if (prevIndex !== -1) {
+    updatedCommands = commands.map((c) => (c.title === prevCommandTitle ? command : c));
+  } else {
+    // Create a new command
+    updatedCommands = [...commands, command];
+  }
+  customCommandsStore.set(customCommandsAtom, updatedCommands);
+}
+
+export function updateCachedCommands(commands: CustomCommand[]) {
+  customCommandsStore.set(customCommandsAtom, commands);
+}
+
+export function useCustomCommands(): CustomCommand[] {
+  return useAtomValue(customCommandsAtom, { store: customCommandsStore });
+}
+
+export function getCachedCustomCommands(): CustomCommand[] {
+  return customCommandsStore.get(customCommandsAtom);
+}
diff --git a/src/commands/type.ts b/src/commands/type.ts
new file mode 100644
index 0000000..15ffbe0
--- /dev/null
+++ b/src/commands/type.ts
@@ -0,0 +1,36 @@
+export interface CustomCommand {
+  /**
+   * Title of the custom command
+   */
+  title: string;
+
+  /**
+   * Content of the custom command without frontmatter
+   */
+  content: string;
+
+  /**
+   * Whether the custom command is enabled as a context menu item
+   */
+  showInContextMenu: boolean;
+
+  /**
+   * Whether the custom command is enabled as a slash menu item
+   */
+  showInSlashMenu: boolean;
+
+  /**
+   * Order for display in context menu and settings
+   */
+  order: number;
+
+  /**
+   * Model key to use for the custom command
+   */
+  modelKey: string;
+
+  /**
+   * Timestamp of the last time the custom command was used
+   */
+  lastUsedMs: number;
+}
diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx
index 4a65b81..04c165a 100644
--- a/src/components/Chat.tsx
+++ b/src/components/Chat.tsx
@@ -15,7 +15,6 @@ import { ProjectList } from "@/components/chat-components/ProjectList";
 import { ABORT_REASON, COMMAND_IDS, EVENT_NAMES, LOADING_MESSAGES, USER_SENDER } from "@/constants";
 import { AppContext, EventTargetContext } from "@/context";
 import { ContextProcessor } from "@/contextProcessor";
-import { CustomPromptProcessor } from "@/customPromptProcessor";
 import { getAIResponse } from "@/langchainStream";
 import ChainManager from "@/LLMProviders/chainManager";
 import CopilotPlugin from "@/main";
@@ -33,6 +32,7 @@ import { err2String, formatDateTime } from "@/utils";
 import { Buffer } from "buffer";
 import { Notice, TFile } from "obsidian";
 import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
+import { processPrompt } from "@/commands/customCommandUtils";
 
 type ChatMode = "default" | "project";
 
@@ -173,13 +173,12 @@ const Chat: React.FC<ChatProps> = ({
         inputMessage + "\n\n<output_format>\n" + composerPrompt + "\n</output_format>";
     }
     // process the original user message for custom prompts
-    const customPromptProcessor = CustomPromptProcessor.getInstance(app.vault);
-    const { processedPrompt: processedUserMessage, includedFiles } =
-      await customPromptProcessor.processCustomPrompt(
-        processedInputMessage || "",
-        "",
-        app.workspace.getActiveFile() as TFile | undefined
-      );
+    const { processedPrompt: processedUserMessage, includedFiles } = await processPrompt(
+      processedInputMessage || "",
+      "",
+      app.vault,
+      app.workspace.getActiveFile()
+    );
 
     // Extract Mentions (such as URLs) from original input message only if using Copilot Plus chain
     const urlContextAddition =
@@ -356,6 +355,9 @@ ${chatContent}`;
         }
         abortController.abort(reason);
         setLoading(false);
+        setLoadingMessage(LOADING_MESSAGES.DEFAULT);
+        // Keep the partial AI message visible
+        // Don't clear setCurrentAiMessage here
       }
     },
     [abortController, settings.debug]
@@ -496,32 +498,17 @@ ${chatContent}`;
     };
   };
 
-  const customPromptProcessor = CustomPromptProcessor.getInstance(app.vault);
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  useEffect(
-    createEffect(COMMAND_IDS.APPLY_CUSTOM_PROMPT, async (selectedText, customPrompt) => {
-      if (!customPrompt) {
-        return selectedText;
-      }
-      const result = await customPromptProcessor.processCustomPrompt(
-        customPrompt,
-        selectedText,
-        app.workspace.getActiveFile() ?? undefined
-      );
-      return result.processedPrompt; // Extract just the processed prompt string
-    }),
-    []
-  );
   // eslint-disable-next-line react-hooks/exhaustive-deps
   useEffect(
     createEffect(COMMAND_IDS.APPLY_ADHOC_PROMPT, async (selectedText, customPrompt) => {
       if (!customPrompt) {
         return selectedText;
       }
-      const result = await customPromptProcessor.processCustomPrompt(
+      const result = await processPrompt(
         customPrompt,
         selectedText,
-        app.workspace.getActiveFile() as TFile | undefined
+        app.vault,
+        app.workspace.getActiveFile()
       );
       return result.processedPrompt; // Extract just the processed prompt string
     }),
@@ -641,6 +628,21 @@ ${chatContent}`;
     plugin.loadCopilotChatHistory();
   }, [plugin]);
 
+  // Event listener for abort stream events
+  useEffect(() => {
+    const handleAbortStream = (event: CustomEvent) => {
+      const reason = event.detail?.reason || ABORT_REASON.NEW_CHAT;
+      handleStopGenerating(reason);
+    };
+
+    eventTarget?.addEventListener(EVENT_NAMES.ABORT_STREAM, handleAbortStream);
+
+    // Cleanup function
+    return () => {
+      eventTarget?.removeEventListener(EVENT_NAMES.ABORT_STREAM, handleAbortStream);
+    };
+  }, [eventTarget, handleStopGenerating]);
+
   // Use the includeActiveNoteAsContext setting
   useEffect(() => {
     if (settings.includeActiveNoteAsContext !== undefined) {
diff --git a/src/components/chat-components/ChatInput.tsx b/src/components/chat-components/ChatInput.tsx
index 1757be2..242462e 100644
--- a/src/components/chat-components/ChatInput.tsx
+++ b/src/components/chat-components/ChatInput.tsx
@@ -19,7 +19,7 @@ import {
 } from "@/components/ui/dropdown-menu";
 import { ModelDisplay } from "@/components/ui/model-display";
 import { ContextProcessor } from "@/contextProcessor";
-import { CustomPromptProcessor } from "@/customPromptProcessor";
+import { CustomCommandManager } from "@/commands/customCommandManager";
 import { COPILOT_TOOL_NAMES } from "@/LLMProviders/intentAnalyzer";
 import { Mention } from "@/mentions/Mention";
 import { getModelKeyFromModel, useSettingsValue } from "@/settings/model";
@@ -47,6 +47,8 @@ import React, {
 } from "react";
 import { useDropzone } from "react-dropzone";
 import ContextControl from "./ContextControl";
+import { getCachedCustomCommands } from "@/commands/state";
+import { sortSlashCommands } from "@/commands/customCommandUtils";
 
 interface ChatInputProps {
   inputMessage: string;
@@ -250,18 +252,22 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
       fetchNoteTitles();
     };
 
-    const showCustomPromptModal = async () => {
-      const customPromptProcessor = CustomPromptProcessor.getInstance(app.vault);
-      const prompts = await customPromptProcessor.getAllPrompts();
-      const promptTitles = prompts.map((prompt) => prompt.title);
+    const showCustomPromptModal = () => {
+      const commandManager = CustomCommandManager.getInstance();
+      const commands = getCachedCustomCommands();
+      const slashCommands = sortSlashCommands(
+        commands.filter((command) => command.showInSlashMenu)
+      );
+      const commandTitles = slashCommands.map((command) => command.title);
 
-      new ListPromptModal(app, promptTitles, async (promptTitle: string) => {
-        const selectedPrompt = prompts.find((prompt) => prompt.title === promptTitle);
-        if (selectedPrompt) {
-          customPromptProcessor.recordPromptUsage(selectedPrompt.title);
-          setInputMessage(selectedPrompt.content);
+      const modal = new ListPromptModal(app, commandTitles, (commandTitle: string) => {
+        const selectedCommand = slashCommands.find((command) => command.title === commandTitle);
+        if (selectedCommand) {
+          commandManager.recordUsage(selectedCommand);
+          setInputMessage(selectedCommand.content);
         }
-      }).open();
+      });
+      modal.open();
     };
 
     const showCopilotPlusOptionsModal = () => {
diff --git a/src/components/chat-components/ProjectList.tsx b/src/components/chat-components/ProjectList.tsx
index 50f89d3..8f4d982 100644
--- a/src/components/chat-components/ProjectList.tsx
+++ b/src/components/chat-components/ProjectList.tsx
@@ -1,5 +1,5 @@
 import { ProjectConfig, setCurrentProject } from "@/aiParams";
-import { AddProjectModal } from "@/components/modals/AddProjectModal";
+import { AddProjectModal } from "@/components/modals/project/AddProjectModal";
 import { ConfirmModal } from "@/components/modals/ConfirmModal";
 import { Button } from "@/components/ui/button";
 import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
@@ -11,6 +11,7 @@ import {
   SelectValue,
 } from "@/components/ui/select";
 import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { SearchBar } from "@/components/ui/SearchBar";
 import { cn } from "@/lib/utils";
 import { logError } from "@/logger";
 import { updateSetting } from "@/settings/model";
@@ -25,12 +26,10 @@ import {
   Search,
   Trash2,
   X,
-  XCircle,
 } from "lucide-react";
 import { App, Notice } from "obsidian";
 import React, { memo, useEffect, useMemo, useState } from "react";
 import { filterProjects } from "@/utils/projectUtils";
-import { Input } from "../ui/input";
 
 function ProjectItem({
   project,
@@ -356,23 +355,11 @@ export const ProjectList = memo(
                     {projects.length > 0 && (
                       <div className="tw-px-4 tw-pb-2 tw-pt-3">
                         <div className="tw-relative">
-                          <Input
-                            type="text"
-                            placeholder="Search projects..."
+                          <SearchBar
                             value={searchQuery}
-                            onChange={(e) => setSearchQuery(e.target.value)}
+                            onChange={setSearchQuery}
+                            placeholder="Search projects..."
                           />
-                          {searchQuery && (
-                            <Button
-                              variant={"secondary"}
-                              onClick={() => setSearchQuery("")}
-                              className="tw-absolute tw-right-8 tw-top-1/2 tw-size-4 -tw-translate-y-1/2 tw-transform tw-rounded-full tw-p-0 tw-transition-colors"
-                              aria-label="Clear search"
-                            >
-                              <XCircle className="tw-size-4 tw-text-muted/60 hover:tw-text-accent-hover" />
-                            </Button>
-                          )}
-                          <Search className="tw-absolute tw-right-3 tw-top-1/2 tw-size-4 -tw-translate-y-1/2 tw-transform tw-text-muted" />
                         </div>
                       </div>
                     )}
diff --git a/src/components/composer/ApplyView.tsx b/src/components/composer/ApplyView.tsx
index 0548754..22a084f 100644
--- a/src/components/composer/ApplyView.tsx
+++ b/src/components/composer/ApplyView.tsx
@@ -1,9 +1,9 @@
 import { cn } from "@/lib/utils";
 import { logError } from "@/logger";
-import { Change } from "diff";
+import { Change, diffWords } from "diff";
 import { Check, X as XIcon } from "lucide-react";
 import { App, ItemView, Notice, TFile, WorkspaceLeaf } from "obsidian";
-import React, { useRef } from "react";
+import React, { useRef, memo } from "react";
 import { createRoot } from "react-dom/client";
 import { Button } from "../ui/button";
 import { useState } from "react";
@@ -78,6 +78,34 @@ interface ApplyViewRootProps {
   close: () => void;
 }
 
+// Convert renderWordDiff to a React component
+const WordDiff = memo(({ oldLine, newLine }: { oldLine: string; newLine: string }) => {
+  const wordDiff = diffWords(oldLine, newLine);
+  return (
+    <>
+      {wordDiff.map((part, idx) => {
+        if (part.added) {
+          return (
+            <span key={idx} className="tw-text-success">
+              {part.value}
+            </span>
+          );
+        }
+        if (part.removed) {
+          return (
+            <span key={idx} className="tw-text-error tw-line-through">
+              {part.value}
+            </span>
+          );
+        }
+        return <span key={idx}>{part.value}</span>;
+      })}
+    </>
+  );
+});
+
+WordDiff.displayName = "WordDiff";
+
 const ApplyViewRoot: React.FC<ApplyViewRootProps> = ({ app, state, close }) => {
   const [diff, setDiff] = useState<ExtendedChange[]>(() => {
     return state.changes.map((change) => ({
@@ -277,12 +305,7 @@ const ApplyViewRoot: React.FC<ApplyViewRootProps> = ({ app, state, close }) => {
             <div
               key={blockIndex}
               ref={(el) => (blockRefs.current[blockIndex] = el)}
-              className={cn(
-                "tw-mb-4 tw-overflow-hidden tw-rounded-md tw-border tw-border-solid tw-border-border",
-                {
-                  "tw-border-solid": blockStatus !== "unchanged",
-                }
-              )}
+              className={cn("tw-mb-4 tw-overflow-hidden tw-rounded-md")}
             >
               {blockStatus === "accepted" ? (
                 // Show only the accepted version
@@ -303,20 +326,49 @@ const ApplyViewRoot: React.FC<ApplyViewRootProps> = ({ app, state, close }) => {
                     ))}
                 </div>
               ) : (
-                // Show the diff view for undecided blocks
-                block.map((change, changeIndex) => (
-                  <div
-                    key={`${blockIndex}-${changeIndex}`}
-                    className={cn("tw-relative", {
-                      "tw-bg-success tw-border-l-green": change.added,
-                      "tw-bg-error tw-border-l-red": change.removed,
-                    })}
-                  >
-                    <div className="tw-flex-1 tw-whitespace-pre-wrap tw-px-2 tw-py-1 tw-font-mono tw-text-sm tw-text-normal">
-                      {change.value}
+                // Render the block
+                block.map((change, changeIndex) => {
+                  // Try to find a corresponding added/removed pair for word-level diff
+                  if (change.added) {
+                    const removedIdx = block.findIndex((c, i) => c.removed && i !== changeIndex);
+                    if (removedIdx !== -1) {
+                      const removedLine = block[removedIdx].value;
+                      return (
+                        <div key={`${blockIndex}-${changeIndex}`} className="tw-relative">
+                          <div className="tw-flex-1 tw-whitespace-pre-wrap tw-px-2 tw-py-1 tw-font-mono tw-text-sm">
+                            <WordDiff oldLine={removedLine} newLine={change.value} />
+                          </div>
+                        </div>
+                      );
+                    }
+                  }
+                  // Skip rendering removed line if it is already paired with an added line.
+                  if (change.removed) {
+                    const addedIdx = block.findIndex((c, i) => c.added && i !== changeIndex);
+                    if (addedIdx !== -1) {
+                      // Skip rendering removed line, since it's shown in the added line
+                      return null;
+                    }
+                  }
+                  // No pair found, render the line as is.
+                  return (
+                    <div key={`${blockIndex}-${changeIndex}`} className="tw-relative">
+                      <div
+                        className={cn(
+                          "tw-flex-1 tw-whitespace-pre-wrap tw-px-2 tw-py-1 tw-font-mono tw-text-sm",
+                          {
+                            "tw-text-success": change.added,
+                            "tw-text-error": change.removed,
+                            "tw-text-normal": !change.added && !change.removed,
+                            "tw-line-through": change.removed,
+                          }
+                        )}
+                      >
+                        {change.value}
+                      </div>
                     </div>
-                  </div>
-                ))
+                  );
+                })
               )}
 
               {/* Only show accept/reject buttons for blocks with changes that are undecided */}
diff --git a/src/components/modals/AddPromptModal.tsx b/src/components/modals/AddPromptModal.tsx
deleted file mode 100644
index 33c054e..0000000
--- a/src/components/modals/AddPromptModal.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import { App, Modal, Notice } from "obsidian";
-import React, { useState } from "react";
-import { createRoot, Root } from "react-dom/client";
-import { err2String } from "@/utils";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import { Button } from "@/components/ui/button";
-import { CustomPromptSyntaxInstruction } from "@/components/CustomPromptSyntaxInstruction";
-
-interface AddPromptModalContentProps {
-  initialTitle?: string;
-  initialPrompt?: string;
-  disabledTitle?: boolean;
-  onSave: (title: string, prompt: string) => Promise<void>;
-  onCancel: () => void;
-}
-
-function AddPromptModalContent({
-  initialTitle = "",
-  initialPrompt = "",
-  disabledTitle = false,
-  onSave,
-  onCancel,
-}: AddPromptModalContentProps) {
-  const [title, setTitle] = useState(initialTitle);
-  const [prompt, setPrompt] = useState(initialPrompt);
-  const [touched, setTouched] = useState({ title: false, prompt: false });
-  const [isSubmitting, setIsSubmitting] = useState(false);
-
-  // Invalid characters for filename
-  // eslint-disable-next-line no-control-regex
-  const invalidChars = /[<>:"/\\|?*\x00-\x1F]/g;
-  const hasInvalidChars = title && invalidChars.test(title);
-
-  const handleSave = async () => {
-    if (hasInvalidChars) {
-      new Notice("Title contains invalid characters. Please remove them before saving.");
-      return;
-    }
-
-    if (title && prompt) {
-      try {
-        setIsSubmitting(true);
-        await onSave(title, prompt);
-      } catch (e) {
-        new Notice(err2String(e));
-      } finally {
-        setIsSubmitting(false);
-      }
-    } else {
-      setTouched({ title: true, prompt: true });
-      new Notice("Please fill in both fields: Title and Prompt.");
-    }
-  };
-
-  const showTitleError = touched.title && !title;
-  const showPromptError = touched.prompt && !prompt;
-  const isValid = title.trim() !== "" && prompt.trim() !== "" && !hasInvalidChars;
-
-  return (
-    <div className="tw-flex tw-flex-col tw-gap-4 tw-p-4">
-      <div className="tw-mb-2 tw-text-xl tw-font-bold tw-text-normal">User Custom Prompt</div>
-
-      <div className="tw-flex tw-flex-col">
-        <div className="tw-flex tw-items-center tw-gap-1">
-          <div className="tw-text-base tw-font-medium tw-text-normal">Title</div>
-          <span className="tw-text-error">*</span>
-        </div>
-        <div className="tw-flex tw-flex-col tw-gap-1">
-          <div className="tw-text-sm tw-text-muted">The title of the prompt, must be unique.</div>
-          <div className="tw-text-xs tw-text-warning">
-            Note: Title will be used as filename. Avoid using: {'< > : " / \\ | ? *'}
-          </div>
-        </div>
-        <Input
-          type="text"
-          value={title}
-          onChange={(e) => {
-            setTitle(e.target.value);
-            if (!touched.title) setTouched((prev) => ({ ...prev, title: true }));
-          }}
-          onBlur={() => setTouched((prev) => ({ ...prev, title: true }))}
-          disabled={disabledTitle}
-          className="tw-mt-1 tw-w-full"
-          required
-        />
-        {showTitleError && (
-          <div className="tw-mt-1 tw-text-xs tw-text-error">Title is required</div>
-        )}
-        {hasInvalidChars && (
-          <div className="tw-mt-1 tw-text-xs tw-text-error">Title contains invalid characters</div>
-        )}
-      </div>
-
-      <div className="tw-flex tw-flex-col tw-gap-1">
-        <div className="tw-space-y-2">
-          <div className="tw-flex tw-items-center tw-gap-1">
-            <div className="tw-text-base tw-font-medium tw-text-normal">Prompt</div>
-            <span className="tw-text-error">*</span>
-          </div>
-          <div className="tw--mt-1 tw-text-sm tw-text-muted">
-            Use the following syntax in your prompt:
-          </div>
-        </div>
-        <CustomPromptSyntaxInstruction />
-
-        <Textarea
-          value={prompt}
-          onChange={(e) => {
-            setPrompt(e.target.value);
-            if (!touched.prompt) setTouched((prev) => ({ ...prev, prompt: true }));
-          }}
-          onBlur={() => setTouched((prev) => ({ ...prev, prompt: true }))}
-          className="tw-mt-1 !tw-min-h-32"
-          required
-        />
-        {showPromptError && (
-          <div className="tw-mt-1 tw-text-xs tw-text-error">Prompt is required</div>
-        )}
-
-        <div className="tw-mt-2 tw-flex tw-flex-col tw-gap-2 tw-text-xs tw-text-muted">
-          <div>
-            Save the prompt to the local prompt library. You can then use it with the Copilot
-            command: <strong>Apply custom prompt to selection.</strong>
-          </div>
-          <div>
-            Check out the{" "}
-            <a
-              href="https://github.com/f/awesome-chatgpt-prompts"
-              target="_blank"
-              className="tw-text-accent hover:tw-text-accent-hover"
-              rel="noreferrer"
-            >
-              awesome chatGPT prompts
-            </a>{" "}
-            for inspiration.
-          </div>
-        </div>
-      </div>
-
-      <div className="tw-flex tw-items-center tw-justify-end tw-gap-2">
-        <Button variant="secondary" onClick={onCancel} disabled={isSubmitting}>
-          Cancel
-        </Button>
-        <Button onClick={handleSave} disabled={!isValid || isSubmitting}>
-          {isSubmitting ? "Saving..." : "Save"}
-        </Button>
-      </div>
-    </div>
-  );
-}
-
-export class AddPromptModal extends Modal {
-  private root: Root;
-
-  constructor(
-    app: App,
-    private onSave: (title: string, prompt: string) => Promise<void>,
-    private initialTitle = "",
-    private initialPrompt = "",
-    private disabledTitle?: boolean
-  ) {
-    super(app);
-  }
-
-  onOpen() {
-    const { contentEl } = this;
-    this.root = createRoot(contentEl);
-
-    const handleSave = async (title: string, prompt: string) => {
-      await this.onSave(title, prompt);
-      this.close();
-    };
-
-    const handleCancel = () => {
-      this.close();
-    };
-
-    this.root.render(
-      <AddPromptModalContent
-        initialTitle={this.initialTitle}
-        initialPrompt={this.initialPrompt}
-        disabledTitle={this.disabledTitle}
-        onSave={handleSave}
-        onCancel={handleCancel}
-      />
-    );
-  }
-
-  onClose() {
-    this.root.unmount();
-  }
-}
diff --git a/src/components/modals/ConfirmModal.tsx b/src/components/modals/ConfirmModal.tsx
index 4e75ecc..8d93ed1 100644
--- a/src/components/modals/ConfirmModal.tsx
+++ b/src/components/modals/ConfirmModal.tsx
@@ -1,3 +1,4 @@
+import { Button } from "@/components/ui/button";
 import { App, Modal } from "obsidian";
 import React from "react";
 import { createRoot, Root } from "react-dom/client";
@@ -16,20 +17,19 @@ function ConfirmModalContent({
   cancelButtonText: string;
 }) {
   return (
-    <div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
-      <div>{content}</div>
-      <div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
-        <button onClick={onCancel}>{cancelButtonText}</button>
-        <button
-          style={{
-            backgroundColor: "var(--interactive-accent)",
-            color: "var(--text-on-accent)",
-            cursor: "pointer",
-          }}
-          onClick={onConfirm}
-        >
-          {confirmButtonText}
-        </button>
+    <div className="tw-flex tw-flex-col tw-gap-5">
+      <div className="tw-whitespace-pre-wrap">{content}</div>
+      <div className="tw-flex tw-justify-end tw-gap-2">
+        {cancelButtonText && (
+          <Button variant="secondary" onClick={onCancel}>
+            {cancelButtonText}
+          </Button>
+        )}
+        {confirmButtonText && (
+          <Button variant="default" onClick={onConfirm}>
+            {confirmButtonText}
+          </Button>
+        )}
       </div>
     </div>
   );
diff --git a/src/components/modals/ExtensionInputModal.tsx b/src/components/modals/ExtensionInputModal.tsx
index c82a2b0..316dbcf 100644
--- a/src/components/modals/ExtensionInputModal.tsx
+++ b/src/components/modals/ExtensionInputModal.tsx
@@ -25,6 +25,8 @@ function ExtensionInputModalContent({
 
   const handleKeyDown = (e: React.KeyboardEvent) => {
     if (e.key === "Enter") {
+      e.preventDefault();
+      e.stopPropagation();
       validateAndConfirm(extension);
     }
   };
diff --git a/src/components/modals/PatternMatchingModal.tsx b/src/components/modals/PatternMatchingModal.tsx
index ce3d208..782fd91 100644
--- a/src/components/modals/PatternMatchingModal.tsx
+++ b/src/components/modals/PatternMatchingModal.tsx
@@ -4,8 +4,11 @@ import React, { useState } from "react";
 import { createRoot, Root } from "react-dom/client";
 import {
   categorizePatterns,
-  getDecodedPatterns,
   createPatternSettingsValue,
+  getDecodedPatterns,
+  getExtensionPattern,
+  getFilePattern,
+  getTagPattern,
 } from "@/search/searchUtils";
 import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react";
 import {
@@ -148,7 +151,7 @@ function PatternMatchingModalContent({
             <DropdownMenuItem
               onSelect={() => {
                 new TagSearchModal(app, (tag) => {
-                  const tagPattern = `#${tag}`;
+                  const tagPattern = getTagPattern(tag);
                   if (tagPatterns.includes(tagPattern)) {
                     return;
                   }
@@ -185,7 +188,7 @@ function PatternMatchingModalContent({
                 new AddContextNoteModal({
                   app,
                   onNoteSelect: (note) => {
-                    const notePattern = `[[${note.basename}]]`;
+                    const notePattern = getFilePattern(note);
                     if (notePatterns.includes(notePattern)) {
                       return;
                     }
@@ -206,7 +209,7 @@ function PatternMatchingModalContent({
             <DropdownMenuItem
               onSelect={() => {
                 new ExtensionInputModal(app, (extension) => {
-                  const extensionPattern = `*.${extension}`;
+                  const extensionPattern = getExtensionPattern(extension);
                   if (extensionPatterns.includes(extensionPattern)) {
                     return;
                   }
diff --git a/src/components/modals/ProjectPatternMatchingModal.tsx b/src/components/modals/ProjectPatternMatchingModal.tsx
index e525fd0..288c43b 100644
--- a/src/components/modals/ProjectPatternMatchingModal.tsx
+++ b/src/components/modals/ProjectPatternMatchingModal.tsx
@@ -10,6 +10,9 @@ import {
   categorizePatterns,
   createPatternSettingsValue,
   getDecodedPatterns,
+  getExtensionPattern,
+  getFilePattern,
+  getTagPattern,
 } from "@/search/searchUtils";
 import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react";
 import { App, Modal, TFile } from "obsidian";
@@ -148,7 +151,7 @@ function ProjectPatternMatchingModalContent({
             <DropdownMenuItem
               onSelect={() => {
                 new TagSearchModal(app, (tag) => {
-                  const tagPattern = `#${tag}`;
+                  const tagPattern = getTagPattern(tag);
                   if (tagPatterns.includes(tagPattern)) {
                     return;
                   }
@@ -185,7 +188,7 @@ function ProjectPatternMatchingModalContent({
                 new ProjectFileSelectModal({
                   app,
                   onFileSelect: (file: TFile) => {
-                    const filePattern = `[[${file.basename}]]`;
+                    const filePattern = getFilePattern(file);
                     if (notePatterns.includes(filePattern)) {
                       return;
                     }
@@ -206,7 +209,7 @@ function ProjectPatternMatchingModalContent({
             <DropdownMenuItem
               onSelect={() => {
                 new ExtensionInputModal(app, (extension) => {
-                  const extensionPattern = `*.${extension}`;
+                  const extensionPattern = getExtensionPattern(extension);
                   if (extensionPatterns.includes(extensionPattern)) {
                     return;
                   }
diff --git a/src/components/modals/AddProjectModal.tsx b/src/components/modals/project/AddProjectModal.tsx
similarity index 86%
rename from src/components/modals/AddProjectModal.tsx
rename to src/components/modals/project/AddProjectModal.tsx
index c1cb355..898ed11 100644
--- a/src/components/modals/AddProjectModal.tsx
+++ b/src/components/modals/project/AddProjectModal.tsx
@@ -1,5 +1,4 @@
 import { ProjectConfig } from "@/aiParams";
-import { ProjectPatternMatchingModal } from "@/components/modals/ProjectPatternMatchingModal";
 import { Button } from "@/components/ui/button";
 import { FormField } from "@/components/ui/form-field";
 import { Input } from "@/components/ui/input";
@@ -14,6 +13,9 @@ import { HelpCircle } from "lucide-react";
 import { App, Modal, Notice } from "obsidian";
 import React, { useState } from "react";
 import { createRoot, Root } from "react-dom/client";
+import { ContextManageModal } from "@/components/modals/project/context-manage-modal";
+import { TruncatedText } from "@/components/TruncatedText";
+import { getDecodedPatterns } from "@/search/searchUtils";
 
 interface AddProjectModalContentProps {
   initialProject?: ProjectConfig;
@@ -31,7 +33,7 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
     inclusions: false,
   });
 
-  const [formData, setFormData] = useState<Partial<ProjectConfig>>(
+  const [formData, setFormData] = useState<ProjectConfig>(
     initialProject || {
       id: randomUUID(),
       name: "",
@@ -53,6 +55,23 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
     }
   );
 
+  const showContext = getDecodedPatterns(
+    formData.contextSource.inclusions || formData.contextSource.exclusions || "nothing"
+  )
+    .reverse()
+    .join(",");
+
+  const handleEditProjectContext = (originP: ProjectConfig) => {
+    const modal = new ContextManageModal(
+      app,
+      async (updatedProject: ProjectConfig) => {
+        setFormData(updatedProject);
+      },
+      originP
+    );
+    modal.open();
+  };
+
   const isFormValid = () => {
     return formData.name && formData.projectModelKey;
   };
@@ -130,7 +149,9 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
 
   return (
     <div className="tw-flex tw-flex-col tw-gap-2 tw-p-4">
-      <div className="tw-mb-2 tw-text-xl tw-font-bold tw-text-normal">Add New Project</div>
+      <div className="tw-mb-2 tw-text-xl tw-font-bold tw-text-normal">
+        {initialProject ? "Edit Project" : "New Project"}
+      </div>
 
       <div className="tw-flex tw-flex-col tw-gap-2">
         <FormField
@@ -236,7 +257,7 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
           <FormField
             label={
               <div className="tw-flex tw-items-center tw-gap-2">
-                <span>Inclusions</span>
+                <span>Project Context</span>
                 <TooltipProvider delayDuration={0}>
                   <Tooltip>
                     <TooltipTrigger asChild>
@@ -261,53 +282,19 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
             description="Define patterns to include specific files or folders in the project context"
           >
             <div className="tw-flex tw-items-center tw-gap-2">
-              <div className="tw-flex-1 tw-text-xs tw-text-muted">
-                {formData.contextSource?.inclusions?.trim()
-                  ? "Patterns configured"
-                  : "No patterns configured"}
-              </div>
-              <Button
-                variant="secondary"
-                onClick={() =>
-                  new ProjectPatternMatchingModal(
-                    app,
-                    (value: string) => {
-                      handleInputChange("contextSource.inclusions", value);
-                    },
-                    formData.contextSource?.inclusions || "",
-                    "Manage Inclusions"
-                  ).open()
-                }
-              >
-                Manage Patterns
-              </Button>
-            </div>
-          </FormField>
-
-          <FormField
-            label="Exclusions"
-            description="Exclude specific files or patterns from the included folders above"
-          >
-            <div className="tw-flex tw-items-center tw-gap-2">
-              <div className="tw-flex-1 tw-text-xs tw-text-muted">
-                {formData.contextSource?.exclusions?.trim()
-                  ? "Patterns configured"
-                  : "No patterns configured"}
+              <div className="tw-flex tw-flex-1 tw-flex-row">
+                <span className="tw-text-sm">Patterns configure：</span>
+                <TruncatedText className="tw-max-w-[100px] tw-text-sm tw-text-accent">
+                  {showContext}
+                </TruncatedText>
               </div>
               <Button
                 variant="secondary"
-                onClick={() =>
-                  new ProjectPatternMatchingModal(
-                    app,
-                    (value: string) => {
-                      handleInputChange("contextSource.exclusions", value);
-                    },
-                    formData.contextSource?.exclusions || "",
-                    "Manage Exclusions"
-                  ).open()
-                }
+                onClick={() => {
+                  handleEditProjectContext(formData);
+                }}
               >
-                Manage Patterns
+                Manage Context
               </Button>
             </div>
           </FormField>
diff --git a/src/components/modals/project/context-manage-modal.tsx b/src/components/modals/project/context-manage-modal.tsx
new file mode 100644
index 0000000..c51507f
--- /dev/null
+++ b/src/components/modals/project/context-manage-modal.tsx
@@ -0,0 +1,1059 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+  FileAudio,
+  FileImage,
+  FileText,
+  FileVideo,
+  FolderIcon,
+  Plus,
+  PlusCircle,
+  TagIcon,
+  XIcon,
+} from "lucide-react";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+import { App, Modal, Notice, TFile } from "obsidian";
+import { createRoot, Root } from "react-dom/client";
+import { ProjectConfig } from "@/aiParams";
+import { Button } from "@/components/ui/button";
+import { SearchBar } from "@/components/ui/SearchBar";
+import { TagSearchModal } from "@/components/modals/TagSearchModal";
+import { FolderSearchModal } from "@/components/modals/FolderSearchModal";
+import { ProjectFileSelectModal } from "@/components/modals/ProjectFileSelectModal";
+import {
+  createPatternSettingsValue,
+  getFilePattern,
+  getMatchingPatterns,
+  getTagPattern,
+  PatternCategory,
+  shouldIndexFile,
+} from "@/search/searchUtils";
+import { getTagsFromNote } from "@/utils";
+import { TruncatedText } from "@/components/TruncatedText";
+
+function FileIcon({ extension, size = "tw-size-4" }: { extension: string; size?: string }) {
+  const ext = extension.toLowerCase().replace("*.", "");
+  if (["jpg", "jpeg", "png", "gif", "svg"].includes(ext))
+    return <FileImage className={`tw-text-context-manager-blue ${size}`} />;
+  if (["mp3", "wav", "ogg"].includes(ext))
+    return <FileAudio className={`tw-text-context-manager-purple ${size}`} />;
+  if (["mp4", "mov", "avi"].includes(ext))
+    return <FileVideo className={`tw-text-context-manager-orange ${size}`} />;
+  if (["md", "txt", "mdx", "markdown"].includes(ext)) return <FileText className={`${size}`} />;
+
+  return <FileText className={`tw-text-faint ${size}`} />;
+}
+
+interface ParsedQuery {
+  tags: string[];
+  titles: string[];
+  extensions: string[];
+}
+
+type ActiveSection = "tags" | "folders" | "files" | "extensions" | "ignoreFiles" | "search" | null;
+type ActiveItem = string | null;
+
+interface SectionHeaderProps {
+  IconComponent: React.ComponentType<any>;
+  title: string;
+  iconColorClassName: string;
+  onAddClick: () => void;
+}
+
+const SectionHeader: React.FC<SectionHeaderProps> = ({
+  IconComponent,
+  title,
+  iconColorClassName,
+  onAddClick,
+}) => {
+  return (
+    <div className="tw-mb-3 tw-flex tw-items-center tw-justify-between">
+      <div className="tw-flex tw-items-center">
+        <IconComponent className={`tw-mr-2 tw-size-4 ${iconColorClassName}`} />
+        <h3 className={`tw-text-sm tw-font-semibold ${iconColorClassName}`}>{title}</h3>
+      </div>
+      <Button
+        variant="ghost"
+        size="fit"
+        className="tw-text-muted hover:tw-bg-secondary"
+        title={`Add new ${title.toLowerCase()}`}
+        onClick={onAddClick}
+      >
+        <PlusCircle className={`tw-size-4 ${iconColorClassName}`} />
+      </Button>
+    </div>
+  );
+};
+
+interface SectionItem {
+  id: string;
+  name: string;
+  count?: number;
+}
+
+interface SectionListProps {
+  title: string;
+  IconComponent: React.ComponentType<any>;
+  iconColorClassName: string;
+  items: SectionItem[];
+  activeItem: string | null;
+  activeSection: string | null;
+  sectionType: string;
+  renderIcon?: (item: SectionItem) => React.ReactNode;
+  onItemClick: (itemId: string, itemName?: string) => void;
+  onAddClick: () => void;
+  onDeleteItem: (e: React.MouseEvent, item: SectionItem) => void;
+}
+
+const SectionList: React.FC<SectionListProps> = ({
+  title,
+  IconComponent,
+  iconColorClassName,
+  items,
+  activeItem,
+  activeSection,
+  sectionType,
+  renderIcon,
+  onItemClick,
+  onAddClick,
+  onDeleteItem,
+}) => {
+  return (
+    <div>
+      <SectionHeader
+        IconComponent={IconComponent}
+        title={title}
+        iconColorClassName={iconColorClassName}
+        onAddClick={onAddClick}
+      />
+      <div className="tw-space-y-1">
+        {items.map((item) => (
+          <div
+            key={item.id}
+            className={cn(
+              "tw-group tw-flex tw-cursor-pointer tw-items-center tw-justify-between tw-rounded-md tw-p-2 hover:tw-bg-secondary/50",
+              activeSection === sectionType &&
+                activeItem === item.id &&
+                "tw-bg-secondary tw-text-normal"
+            )}
+            onClick={() => onItemClick(item.id, item.name)}
+          >
+            <div className="tw-flex tw-min-w-0 tw-flex-1 tw-items-center">
+              {renderIcon ? (
+                renderIcon(item)
+              ) : (
+                <IconComponent className={`tw-mr-2 tw-size-4 ${iconColorClassName}`} />
+              )}
+              <TruncatedText className="tw-flex-1 tw-text-sm">{item.name}</TruncatedText>
+            </div>
+            <div className="tw-flex tw-items-center tw-gap-2">
+              <span className="tw-text-xs tw-text-muted group-hover:tw-hidden">
+                {item.count || 0}
+              </span>
+              <XIcon
+                className="tw-hidden tw-size-4 tw-shrink-0 tw-text-muted hover:tw-text-warning group-hover:tw-block group-hover:tw-flex-none"
+                onClick={(e) => onDeleteItem(e, item)}
+              />
+            </div>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+interface ItemCardProps {
+  item: GroupItem;
+  viewMode: "list";
+  onDelete: (e: React.MouseEvent, item: GroupItem) => void;
+}
+
+function ItemCard({ item, viewMode, onDelete }: ItemCardProps) {
+  const extension = item.id.split(".").pop() || "";
+
+  // add or remove
+  const IconComponent = item.isIgnored ? Plus : XIcon;
+
+  return (
+    <div className="tw-group tw-flex tw-cursor-pointer tw-items-center tw-rounded-lg tw-border tw-border-solid tw-border-border tw-p-2 tw-transition-shadow hover:tw-shadow-md">
+      <div className="tw-mr-2 tw-shrink-0">
+        <FileIcon extension={extension} size={"tw-size-8"} />
+      </div>
+      <div className="tw-flex tw-min-w-0 tw-flex-1 tw-flex-col">
+        <TruncatedText className="tw-flex-1 tw-text-sm tw-font-medium">
+          {item.isIgnored && <span className="tw-text-sm tw-text-error"> (Ignored files)</span>}
+          {item.name}
+        </TruncatedText>
+        {item.id && (
+          <TruncatedText className="tw-flex-1 tw-text-xs tw-text-faint">{item.id}</TruncatedText>
+        )}
+      </div>
+
+      <div className="tw-ml-auto tw-flex tw-min-w-[24px] tw-items-center tw-justify-end tw-gap-2">
+        <IconComponent
+          className="tw-hidden tw-size-4 tw-shrink-0 tw-text-muted hover:tw-text-warning group-hover:tw-block group-hover:tw-flex-none"
+          onClick={(e) => onDelete(e, item)}
+        />
+      </div>
+    </div>
+  );
+}
+
+interface ContextManageProps {
+  initialProject: ProjectConfig;
+  onSave: (project: ProjectConfig) => void;
+  onCancel: () => void;
+  app: App;
+}
+
+interface GroupItem {
+  id: string;
+  name: string;
+  isIgnored?: boolean;
+}
+
+interface GroupListItem {
+  tags: Record<string, Array<GroupItem>>;
+  folders: Record<string, Array<GroupItem>>;
+  extensions: Record<string, Array<GroupItem>>;
+  notes: Array<GroupItem>;
+}
+
+interface IgnoreItems {
+  files: Set<TFile>;
+}
+
+function ContextManage({ initialProject, onSave, onCancel, app }: ContextManageProps) {
+  const { inclusions: inclusionPatterns, exclusions: exclusionPatterns } = useMemo(() => {
+    return getMatchingPatterns({
+      inclusions: initialProject?.contextSource.inclusions,
+      exclusions: initialProject?.contextSource.exclusions,
+      isProject: true,
+    });
+  }, [initialProject.contextSource]);
+
+  const appAllFiles = useMemo(() => {
+    return app.vault.getFiles();
+  }, [app.vault]);
+
+  // init groupList
+  const createAndPopulateGroupList = useCallback(
+    (
+      appFiles: TFile[],
+      inclusionPatterns: PatternCategory | null,
+      exclusionPatterns: PatternCategory | null
+    ): GroupListItem => {
+      const projectAllFiles = appFiles.filter((file) =>
+        shouldIndexFile(file, inclusionPatterns, exclusionPatterns)
+      );
+
+      const processPatternGroup = (
+        file: TFile,
+        patterns: string[] | undefined,
+        patternType: "tagPatterns" | "folderPatterns" | "extensionPatterns",
+        targetGroup: Record<string, Array<GroupItem>>
+      ) => {
+        if (patterns) {
+          patterns.forEach((pattern) => {
+            const singlePatternConfig = { [patternType]: [pattern] };
+            if (
+              shouldIndexFile(file, singlePatternConfig, null) &&
+              !targetGroup[pattern].some((item) => item.id === file.path)
+            ) {
+              targetGroup[pattern].push({
+                id: file.path,
+                name: file.basename,
+              });
+            }
+          });
+        }
+      };
+
+      // initialize groups
+      const tags: Record<string, Array<GroupItem>> = {};
+      const folders: Record<string, Array<GroupItem>> = {};
+      const extensions: Record<string, Array<GroupItem>> = {};
+      const notes: Array<GroupItem> = [];
+
+      (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => {
+        tags[tag] = [];
+      });
+      (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => {
+        folders[folder] = [];
+      });
+      (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => {
+        extensions[extension] = [];
+      });
+
+      // Traverse the files and populate them into corresponding groups
+      projectAllFiles.forEach((file) => {
+        // tag
+        processPatternGroup(file, inclusionPatterns?.tagPatterns, "tagPatterns", tags);
+
+        // folder
+        processPatternGroup(file, inclusionPatterns?.folderPatterns, "folderPatterns", folders);
+
+        // extension
+        processPatternGroup(
+          file,
+          inclusionPatterns?.extensionPatterns,
+          "extensionPatterns",
+          extensions
+        );
+
+        // note/file
+        if (
+          inclusionPatterns?.notePatterns &&
+          shouldIndexFile(file, { notePatterns: inclusionPatterns.notePatterns }, null) &&
+          !notes.some((item) => item.id === file.path)
+        ) {
+          notes.push({
+            id: file.path,
+            name: file.basename,
+          });
+        }
+      });
+
+      return {
+        tags,
+        folders,
+        extensions,
+        notes,
+      };
+    },
+    []
+  );
+
+  const [groupList, setGroupList] = useState<GroupListItem>(() => {
+    // init include files
+    return createAndPopulateGroupList(appAllFiles, inclusionPatterns, exclusionPatterns);
+  });
+  const [ignoreItems, setIgnoreItems] = useState<IgnoreItems>(() => {
+    // init exclude files
+    const excludeFiles = appAllFiles.filter(
+      (file) => exclusionPatterns && shouldIndexFile(file, exclusionPatterns, null)
+    );
+    return {
+      files: new Set<TFile>(excludeFiles),
+    };
+  });
+
+  const latestGroupList = useRef(groupList);
+  const latestIgnoreItems = useRef(ignoreItems);
+
+  const [searchTerm, setSearchTerm] = useState("");
+  const [activeSection, setActiveSection] = useState<ActiveSection>(null);
+  const [activeItem, setActiveItem] = useState<ActiveItem>(null);
+
+  //  groupList convert to inclusions format
+  const convertGroupListToInclusions = useCallback(
+    (list: GroupListItem, appFiles: TFile[]): string => {
+      const tagPatterns = Object.keys(list.tags);
+      const folderPatterns = Object.keys(list.folders);
+      const extensionPatterns = Object.keys(list.extensions);
+      const notePatterns = list.notes
+        .map((note) => {
+          const file = appFiles.find((file) => file.path === note.id);
+          if (file) {
+            return getFilePattern(file);
+          }
+        })
+        .filter(Boolean) as string[];
+
+      return createPatternSettingsValue({
+        tagPatterns,
+        folderPatterns,
+        extensionPatterns,
+        notePatterns,
+      });
+    },
+    []
+  );
+
+  // ignore file items convert to exclusions format
+  const convertDeletedItemsToExclusions = useCallback((items: IgnoreItems): string => {
+    const notePatterns = new Array(...items.files).map((file) => getFilePattern(file));
+
+    return createPatternSettingsValue({ notePatterns: notePatterns }) || "";
+  }, []);
+
+  useEffect(() => {
+    latestGroupList.current = groupList;
+    latestIgnoreItems.current = ignoreItems;
+  }, [groupList, ignoreItems]);
+
+  const allItems: Array<{ id: string; name: string }> = useMemo(() => {
+    const items: Array<{ id: string; name: string }> = [];
+
+    const addFilesToItems = (
+      items: Array<{ id: string; name: string }>,
+      groupItems: GroupItem[]
+    ): void => {
+      groupItems.forEach((groupItem) => {
+        if (!items.some((item) => item.id === groupItem.id)) {
+          items.push({
+            id: groupItem.id,
+            name: groupItem.name,
+          });
+        }
+      });
+    };
+
+    const arr = [
+      groupList.tags,
+      groupList.folders,
+      groupList.extensions,
+      { notes: groupList.notes },
+    ];
+
+    arr.forEach((item) => {
+      Object.values(item).forEach((groupItems) => {
+        addFilesToItems(items, groupItems);
+      });
+    });
+
+    return items;
+  }, [groupList]);
+
+  const parseSearchQuery = useCallback((query: string): ParsedQuery => {
+    const tags: string[] = [];
+    const titles: string[] = [];
+    const extensions: string[] = [];
+
+    const parts = query
+      .split(",")
+      .map((p) => p.trim())
+      .filter((p) => p);
+
+    parts.forEach((part) => {
+      if (part.startsWith("#")) {
+        // tag
+        tags.push(part.substring(1));
+      } else if (part.startsWith(".") || part.startsWith("*.")) {
+        extensions.push(part.replace("*", ""));
+      } else {
+        // All other content is matched as title.
+        titles.push(part);
+      }
+    });
+    return { tags, titles, extensions };
+  }, []);
+
+  const getDisplayItems = useMemo(() => {
+    if (searchTerm) {
+      // Custom search
+      const parsedQuery = parseSearchQuery(searchTerm);
+      return allItems
+        .filter((item) => {
+          const fileObj = appAllFiles.find((f) => f.path === item.id);
+          if (!fileObj) return false;
+
+          const isNote = fileObj.extension === "md";
+
+          const matchesTag =
+            parsedQuery.tags.length > 0 &&
+            isNote &&
+            parsedQuery.tags.some((queryTag) => {
+              const fileTags = getTagsFromNote(fileObj);
+              return fileTags.some((tag) => {
+                const cleanTag = tag.startsWith("#") ? tag.substring(1) : tag;
+                return cleanTag.toLowerCase().includes(queryTag.toLowerCase());
+              });
+            });
+
+          const matchesTitle =
+            parsedQuery.titles.length > 0 &&
+            parsedQuery.titles.some((t) =>
+              fileObj.basename.toLowerCase().includes(t.toLowerCase())
+            );
+
+          const matchesExtension =
+            parsedQuery.extensions.length > 0 &&
+            parsedQuery.extensions.some(
+              (ext) => `.${fileObj.extension}`.toLowerCase() === ext.toLowerCase()
+            );
+
+          const hasSpecificFilters =
+            parsedQuery.tags.length > 0 ||
+            parsedQuery.titles.length > 0 ||
+            parsedQuery.extensions.length > 0;
+
+          if (hasSpecificFilters) {
+            return matchesTag || matchesTitle || matchesExtension;
+          }
+          return false;
+        })
+        .map((item) => {
+          return {
+            id: item.id,
+            name: item.name,
+          };
+        });
+    }
+
+    if (activeSection === "tags" && activeItem) {
+      const tagFiles = groupList.tags[activeItem];
+      if (tagFiles) {
+        return tagFiles;
+      }
+      return [];
+    }
+
+    if (activeSection === "folders" && activeItem) {
+      const folderFiles = groupList.folders[activeItem];
+      if (folderFiles) {
+        return folderFiles;
+      }
+      return [];
+    }
+
+    if (activeSection === "files") {
+      return groupList.notes;
+    }
+
+    if (activeSection === "extensions" && activeItem) {
+      const extensionFiles = groupList.extensions[activeItem];
+      if (extensionFiles) {
+        return extensionFiles;
+      }
+      return [];
+    }
+
+    if (activeSection === "ignoreFiles") {
+      return Array.from(ignoreItems.files).map((file) => ({
+        id: file.path,
+        name: file.basename,
+      }));
+    }
+
+    return [];
+  }, [
+    searchTerm,
+    activeSection,
+    activeItem,
+    parseSearchQuery,
+    allItems,
+    appAllFiles,
+    groupList.tags,
+    groupList.folders,
+    groupList.notes,
+    groupList.extensions,
+    ignoreItems.files,
+  ]);
+
+  const sortItems = useCallback((items: GroupItem[]) => {
+    return [...items].sort((a, b) => {
+      return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+    });
+  }, []);
+
+  const makeSectionItem = useCallback(
+    (
+      groupData: Record<string, Array<GroupItem>>,
+      nameTransform?: (name: string) => string
+    ): SectionItem[] => {
+      return Object.entries(groupData)
+        .sort(([a], [b]) => a.localeCompare(b))
+        .map(([itemName, itemFiles]) => ({
+          id: itemName,
+          name: nameTransform ? nameTransform(itemName) : itemName,
+          count: itemFiles.length,
+        }));
+    },
+    []
+  );
+
+  const addPatternToGroup = useCallback(
+    (
+      groupType: "tags" | "folders" | "extensions",
+      pattern: string,
+      patternConfig: PatternCategory
+    ) => {
+      const getMatchingFilesFromApp = (patterns: PatternCategory): GroupItem[] => {
+        return appAllFiles
+          .filter((file) => shouldIndexFile(file, patterns, null))
+          .map((file) => ({
+            id: file.path,
+            name: file.basename,
+          }));
+      };
+
+      const ignoreFiles = [...latestIgnoreItems.current.files];
+      const matchingFiles: GroupItem[] = getMatchingFilesFromApp(patternConfig).map((v) => ({
+        ...v,
+        // add flag if file is ignored
+        isIgnored: ignoreFiles.some((f) => f.path === v.id),
+      }));
+
+      setGroupList((prev) => ({
+        ...prev,
+        [groupType]: {
+          ...prev[groupType],
+          [pattern]: matchingFiles,
+        },
+      }));
+    },
+    [appAllFiles]
+  );
+
+  const removeFileFromGroupList = useCallback(
+    (groupList: GroupListItem, filePath: string): GroupListItem => {
+      const newGroupList: GroupListItem = {
+        tags: { ...groupList.tags },
+        folders: { ...groupList.folders },
+        extensions: { ...groupList.extensions },
+        notes: [...groupList.notes],
+      };
+
+      const removeFileFromGroupObject = (groupObject: Record<string, Array<GroupItem>>) => {
+        Object.keys(groupObject).forEach((key) => {
+          groupObject[key] = groupObject[key].filter((item) => item.id !== filePath);
+        });
+      };
+
+      removeFileFromGroupObject(newGroupList.tags);
+      removeFileFromGroupObject(newGroupList.folders);
+      removeFileFromGroupObject(newGroupList.extensions);
+
+      // Remove file from notes
+      newGroupList.notes = newGroupList.notes.filter((item) => item.id !== filePath);
+
+      return newGroupList;
+    },
+    []
+  );
+
+  const setActiveState = useCallback(
+    (section: ActiveSection, item: ActiveItem = null, clearSearch: boolean = true) => {
+      setActiveSection(section);
+      setActiveItem(item);
+      if (clearSearch) {
+        setSearchTerm("");
+      }
+    },
+    []
+  );
+
+  // Unified processor
+  const groupHandlers = useMemo(() => {
+    const createDeleteHandler = (groupType: keyof Omit<GroupListItem, "notes">) => {
+      return (e: React.MouseEvent, item: SectionItem) => {
+        e.stopPropagation();
+
+        setGroupList((prev) => {
+          const newGroup = { ...prev[groupType] };
+          delete newGroup[item.id];
+          return {
+            ...prev,
+            [groupType]: newGroup,
+          };
+        });
+      };
+    };
+
+    return {
+      delete: {
+        tag: createDeleteHandler("tags"),
+        folder: createDeleteHandler("folders"),
+        extension: createDeleteHandler("extensions"),
+      },
+
+      add: {
+        tag: () => {
+          new TagSearchModal(app, (tagName) => {
+            const tagPattern = getTagPattern(tagName);
+            addPatternToGroup("tags", tagPattern, { tagPatterns: [tagPattern] });
+          }).open();
+        },
+
+        folder: () => {
+          new FolderSearchModal(app, (folderPath) => {
+            addPatternToGroup("folders", folderPath, { folderPatterns: [folderPath] });
+          }).open();
+        },
+
+        file: () => {
+          new ProjectFileSelectModal({
+            app,
+            onFileSelect: (file: TFile) => {
+              const existingNote = groupList.notes.find((note) => note.id === file.path);
+              if (existingNote) return;
+
+              // remove file from ignore
+              setIgnoreItems((prev) => {
+                const newFiles = new Set(prev.files);
+                newFiles.delete(file);
+                return { ...prev, files: newFiles };
+              });
+
+              setGroupList((prev) => ({
+                ...prev,
+                notes: [...prev.notes, { id: file.path, name: file.basename }],
+              }));
+            },
+            excludeFilePaths: [],
+            titleOnly: false,
+          }).open();
+        },
+
+        extension: () => {
+          // todo(emt-lin)：maybe use this in the future
+          new Notice("Adding extension is temporarily not supported.");
+          return;
+          /*new ExtensionInputModal(app, (extension: string) => {
+            if (extension.trim() === "") return;
+            const extensionPattern = getExtensionPattern(extension);
+            addPatternToGroup("extensions", extensionPattern, {
+              extensionPatterns: [extensionPattern],
+            });
+          }).open();*/
+        },
+
+        ignoreFile: () => {
+          new ProjectFileSelectModal({
+            app,
+            onFileSelect: (file: TFile) => {
+              const isAlreadyIgnored = ignoreItems.files.has(file);
+              if (isAlreadyIgnored) return;
+
+              setIgnoreItems((prev) => {
+                const newFiles = new Set(prev.files);
+                newFiles.add(file);
+                return { ...prev, files: newFiles };
+              });
+
+              // Remove related files from the groupList
+              setGroupList((prev) => removeFileFromGroupList(prev, file.path));
+            },
+            excludeFilePaths: [],
+            titleOnly: false,
+          }).open();
+        },
+      },
+
+      click: {
+        tag: (tagId: string) => {
+          setActiveState("tags", tagId);
+        },
+
+        folder: (folderId: string) => {
+          setActiveState("folders", folderId);
+        },
+
+        extension: (extension: string) => {
+          setActiveState("extensions", extension);
+        },
+
+        files: () => {
+          setActiveState("files");
+        },
+
+        ignoreFiles: () => {
+          setActiveState("ignoreFiles");
+        },
+      },
+    };
+  }, [
+    app,
+    groupList.notes,
+    ignoreItems.files,
+    addPatternToGroup,
+    removeFileFromGroupList,
+    setActiveState,
+  ]);
+
+  const getDisplayTitle = () => {
+    if (searchTerm) return `Search Results for: "${searchTerm}"`;
+    if (activeSection === "tags" && activeItem) {
+      return `Tag: ${activeItem}`;
+    }
+    if (activeSection === "folders" && activeItem) {
+      return `Folder: ${activeItem}`;
+    }
+    if (activeSection === "files") return "Files";
+    if (activeSection === "extensions" && activeItem) {
+      return `Extension: ${activeItem}`;
+    }
+    if (activeSection === "ignoreFiles") return "Ignore Files";
+    return "Select a category to view items";
+  };
+
+  const handleDeleteItem = (e: React.MouseEvent, item: GroupItem) => {
+    e.stopPropagation();
+
+    const file = appAllFiles.find((file) => file.path === item.id);
+    if (file) {
+      // add file to ignore
+      setIgnoreItems((prev) => {
+        const newFiles = new Set(prev.files);
+        newFiles.add(file);
+        return { ...prev, files: newFiles };
+      });
+
+      setGroupList((prev) => removeFileFromGroupList(prev, item.id));
+    }
+  };
+
+  const refreshGroupListFromCurrentPatterns = useCallback(() => {
+    const currentInclude = convertGroupListToInclusions(latestGroupList.current, appAllFiles);
+    const currentExclude = convertDeletedItemsToExclusions(latestIgnoreItems.current);
+
+    const { inclusions, exclusions } = getMatchingPatterns({
+      inclusions: currentInclude,
+      exclusions: currentExclude,
+      isProject: true,
+    });
+
+    const newGroupList = createAndPopulateGroupList(appAllFiles, inclusions, exclusions);
+    setGroupList(newGroupList);
+  }, [
+    appAllFiles,
+    convertDeletedItemsToExclusions,
+    convertGroupListToInclusions,
+    createAndPopulateGroupList,
+  ]);
+
+  const handleDeleteIgnoreItem = (e: React.MouseEvent, item: GroupItem) => {
+    e.stopPropagation();
+
+    const file = appAllFiles.find((file) => file.path === item.id);
+
+    if (file) {
+      setIgnoreItems((prev) => {
+        const newFiles = new Set(prev.files);
+        newFiles.delete(file);
+        return { ...prev, files: newFiles };
+      });
+
+      // refresh groupList
+      refreshGroupListFromCurrentPatterns();
+    }
+  };
+
+  const handleSave = () => {
+    const include = convertGroupListToInclusions(groupList, appAllFiles);
+    const exclude = convertDeletedItemsToExclusions(ignoreItems);
+    onSave({
+      ...initialProject,
+      contextSource: {
+        ...initialProject.contextSource,
+        inclusions: include,
+        exclusions: exclude,
+      },
+    });
+  };
+
+  return (
+    <div className="tw-flex tw-h-full tw-flex-col">
+      <ResizablePanelGroup direction="horizontal" className="tw-flex-1">
+        {/* Left Sidebar - Navigation */}
+        <ResizablePanel defaultSize={30} minSize={20} maxSize={40}>
+          <div className="tw-flex tw-h-full tw-flex-col">
+            {/* Header */}
+            <div className="tw-border-b tw-p-4">
+              <h2 className="tw-text-lg tw-font-semibold">Project Context</h2>
+            </div>
+
+            <ScrollArea className="tw-max-h-[500px] tw-flex-1">
+              <div className="tw-space-y-6 tw-p-4">
+                {/* Tags Section */}
+                <SectionList
+                  title="Tags"
+                  IconComponent={TagIcon}
+                  iconColorClassName="tw-text-context-manager-orange"
+                  items={makeSectionItem(groupList.tags, (name) => name.slice(1))}
+                  activeItem={activeItem}
+                  activeSection={activeSection}
+                  sectionType="tags"
+                  renderIcon={(_item) => <span className="tw-mr-2 tw-text-faint">#</span>}
+                  onItemClick={groupHandlers.click.tag}
+                  onAddClick={groupHandlers.add.tag}
+                  onDeleteItem={(e, item) => groupHandlers.delete.tag(e, item)}
+                />
+
+                <Separator />
+
+                {/* Folders Section */}
+                <SectionList
+                  title="Folders"
+                  IconComponent={FolderIcon}
+                  iconColorClassName="tw-text-context-manager-yellow"
+                  items={makeSectionItem(groupList.folders)}
+                  activeItem={activeItem}
+                  activeSection={activeSection}
+                  sectionType="folders"
+                  onItemClick={groupHandlers.click.folder}
+                  onAddClick={groupHandlers.add.folder}
+                  onDeleteItem={(e, item) => groupHandlers.delete.folder(e, item)}
+                />
+
+                <Separator />
+
+                {/* Files Section */}
+                <div>
+                  <SectionHeader
+                    IconComponent={FileText}
+                    title="Files"
+                    iconColorClassName="tw-text-context-manager-blue"
+                    onAddClick={groupHandlers.add.file}
+                  />
+                  <div
+                    className={cn(
+                      "tw-cursor-pointer tw-rounded-md tw-p-2 tw-text-sm hover:tw-bg-secondary/50",
+                      activeSection === "files" && "tw-bg-secondary tw-text-normal"
+                    )}
+                    onClick={groupHandlers.click.files}
+                  >
+                    Files ({groupList.notes.length})
+                  </div>
+                </div>
+
+                <Separator />
+
+                {/* todo(emt-lin)：maybe use this in the future */}
+                {/* Extensions Section */}
+                {/*<SectionList
+                  title="Extensions"
+                  IconComponent={Hash}
+                  iconColorClassName="tw-text-context-manager-green"
+                  items={makeSectionItem(groupList.extensions)}
+                  activeItem={activeItem}
+                  activeSection={activeSection}
+                  sectionType="extensions"
+                  renderIcon={(item) => <FileIcon extension={item.name} />}
+                  onItemClick={groupHandlers.click.extension}
+                  onAddClick={groupHandlers.add.extension}
+                  onDeleteItem={(e, item) => groupHandlers.delete.extension(e, item)}
+                />
+
+                <Separator />*/}
+
+                {/* Ignore Files Section */}
+                <div>
+                  <SectionHeader
+                    IconComponent={XIcon}
+                    title="Ignore Files"
+                    iconColorClassName="tw-text-context-manager-red"
+                    onAddClick={groupHandlers.add.ignoreFile}
+                  />
+                  <div
+                    className={cn(
+                      "tw-cursor-pointer tw-rounded-md tw-p-2 tw-text-sm hover:tw-bg-secondary/50",
+                      activeSection === "ignoreFiles" && "tw-bg-secondary tw-text-normal"
+                    )}
+                    onClick={groupHandlers.click.ignoreFiles}
+                  >
+                    Ignore Files ({ignoreItems.files.size})
+                  </div>
+                </div>
+              </div>
+            </ScrollArea>
+          </div>
+        </ResizablePanel>
+
+        <ResizableHandle withHandle />
+
+        {/* Right Content Area */}
+        <ResizablePanel defaultSize={70}>
+          <div className="tw-flex tw-h-full tw-flex-col">
+            {/* Header */}
+            <div className="tw-border-b tw-p-4">
+              <SearchBar
+                placeholder="Custom search: title, #tag1, .jpg"
+                value={searchTerm}
+                onChange={(v) => {
+                  setSearchTerm(v);
+                  if (v) {
+                    setActiveState("search", null, false);
+                  }
+                }}
+              />
+            </div>
+
+            {/* Content Title */}
+            <div className="tw-p-4">
+              <h3 className="tw-text-lg tw-font-medium tw-text-muted">{getDisplayTitle()}</h3>
+            </div>
+
+            {/* Content Area */}
+            <ScrollArea className="tw-max-h-[400px] tw-flex-1 tw-p-4 tw-pt-0">
+              {getDisplayItems.length === 0 ? (
+                <div className="tw-mt-10 tw-text-center tw-text-muted">
+                  {activeSection
+                    ? "No items found."
+                    : "Select a category from the sidebar to view items."}
+                </div>
+              ) : (
+                <div className="tw-space-y-2" style={{ display: "block" }}>
+                  {sortItems(getDisplayItems).map((item) => (
+                    <ItemCard
+                      key={item.id}
+                      item={item}
+                      viewMode="list"
+                      onDelete={
+                        activeSection === "ignoreFiles" || item.isIgnored
+                          ? handleDeleteIgnoreItem
+                          : handleDeleteItem
+                      }
+                    />
+                  ))}
+                </div>
+              )}
+            </ScrollArea>
+          </div>
+        </ResizablePanel>
+      </ResizablePanelGroup>
+      <div className="tw-flex tw-justify-end tw-gap-2 tw-border-t tw-p-1">
+        <Button variant="ghost" onClick={onCancel}>
+          Cancel
+        </Button>
+        <Button onClick={handleSave}>Save</Button>
+      </div>
+    </div>
+  );
+}
+
+export class ContextManageModal extends Modal {
+  private root: Root;
+
+  constructor(
+    app: App,
+    private onSave: (project: ProjectConfig) => void,
+    private initialProject: ProjectConfig
+  ) {
+    super(app);
+  }
+
+  onOpen() {
+    const { contentEl, modalEl } = this;
+    this.root = createRoot(contentEl);
+
+    modalEl.style.minWidth = "50vw";
+
+    const handleSave = (project: ProjectConfig) => {
+      this.onSave(project);
+      this.close();
+    };
+
+    const handleCancel = () => {
+      this.close();
+    };
+
+    this.root.render(
+      <ContextManage
+        initialProject={this.initialProject}
+        onSave={handleSave}
+        onCancel={handleCancel}
+        app={this.app}
+      />
+    );
+  }
+
+  onClose() {
+    if (this.root) {
+      this.root.unmount();
+    }
+  }
+}
diff --git a/src/components/ui/SearchBar.tsx b/src/components/ui/SearchBar.tsx
new file mode 100644
index 0000000..c504cfe
--- /dev/null
+++ b/src/components/ui/SearchBar.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { XCircle, Search } from "lucide-react";
+
+interface SearchBarProps {
+  value: string;
+  onChange: (value: string) => void;
+  placeholder?: string;
+}
+
+export const SearchBar: React.FC<SearchBarProps> = ({
+  value,
+  onChange,
+  placeholder = "Search...",
+}) => {
+  return (
+    <div className="tw-relative">
+      <Input
+        type="text"
+        placeholder={placeholder}
+        value={value}
+        onChange={(e) => onChange(e.target.value)}
+        className="tw-pr-10" // Add padding to prevent text overlap with icons
+      />
+      {value && (
+        <Button
+          variant={"secondary"}
+          onClick={() => onChange("")}
+          className="tw-absolute tw-right-8 tw-top-1/2 tw-size-4 -tw-translate-y-1/2 tw-transform tw-rounded-full tw-p-0 tw-transition-colors"
+          aria-label="Clear search"
+        >
+          <XCircle className="tw-size-4 tw-text-muted/60 hover:tw-text-accent-hover" />
+        </Button>
+      )}
+      <Search className="tw-absolute tw-right-3 tw-top-1/2 tw-size-4 -tw-translate-y-1/2 tw-transform tw-text-muted" />
+    </div>
+  );
+};
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx
index 35b68b0..6277007 100644
--- a/src/components/ui/popover.tsx
+++ b/src/components/ui/popover.tsx
@@ -21,7 +21,7 @@ const PopoverContent = React.forwardRef<
       align={align}
       sideOffset={sideOffset}
       className={cn(
-        "tw-z-popover tw-w-72 tw-rounded-md tw-border tw-border-border tw-bg-primary tw-p-4 tw-text-normal tw-shadow-md tw-outline-none data-[state=open]:tw-animate-in data-[state=closed]:tw-animate-out data-[state=closed]:tw-fade-out-0 data-[state=open]:tw-fade-in-0 data-[state=closed]:tw-zoom-out-95 data-[state=open]:tw-zoom-in-95 data-[side=bottom]:tw-slide-in-from-top-2 data-[side=left]:tw-slide-in-from-right-2 data-[side=right]:tw-slide-in-from-left-2 data-[side=top]:tw-slide-in-from-bottom-2",
+        "tw-z-popover tw-w-72 tw-rounded-md tw-border tw-border-solid tw-border-border tw-bg-primary tw-p-4 tw-text-normal tw-shadow-md tw-outline-none data-[state=open]:tw-animate-in data-[state=closed]:tw-animate-out data-[state=closed]:tw-fade-out-0 data-[state=open]:tw-fade-in-0 data-[state=closed]:tw-zoom-out-95 data-[state=open]:tw-zoom-in-95 data-[side=bottom]:tw-slide-in-from-top-2 data-[side=left]:tw-slide-in-from-right-2 data-[side=right]:tw-slide-in-from-left-2 data-[side=top]:tw-slide-in-from-bottom-2",
         className
       )}
       {...props}
@@ -30,4 +30,6 @@ const PopoverContent = React.forwardRef<
 ));
 PopoverContent.displayName = PopoverPrimitive.Content.displayName;
 
-export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
+const PopoverClose = PopoverPrimitive.Close;
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose };
diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx
new file mode 100644
index 0000000..ba4e754
--- /dev/null
+++ b/src/components/ui/resizable.tsx
@@ -0,0 +1,45 @@
+import * as React from "react";
+import { GripVertical } from "lucide-react";
+import * as ResizablePrimitive from "react-resizable-panels";
+
+import { cn } from "@/lib/utils";
+
+const ResizablePanelGroup = ({
+  className,
+  ...props
+}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
+  <ResizablePrimitive.PanelGroup
+    className={cn(
+      "tw-flex tw-size-full data-[panel-group-direction=vertical]:tw-flex-col",
+      className
+    )}
+    {...props}
+  />
+);
+
+const ResizablePanel = ResizablePrimitive.Panel;
+
+const ResizableHandle = ({
+  withHandle,
+  className,
+  ...props
+}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
+  withHandle?: boolean;
+}) => (
+  // tw-bg-border -> --divider-color: var(--background-modifier-border);
+  <ResizablePrimitive.PanelResizeHandle
+    className={cn(
+      "tw-relative tw-flex tw-w-px tw-items-center tw-justify-center tw-bg-[var(--background-modifier-border)] after:tw-absolute after:tw-inset-y-0 after:tw-left-1/2 after:tw-w-1 after:tw--translate-x-1/2 focus-visible:tw-outline-none focus-visible:tw-ring-1 focus-visible:tw-ring-ring focus-visible:tw-ring-offset-1 data-[panel-group-direction=vertical]:tw-h-px data-[panel-group-direction=vertical]:tw-w-full data-[panel-group-direction=vertical]:after:tw-left-0 data-[panel-group-direction=vertical]:after:tw-h-1 data-[panel-group-direction=vertical]:after:tw-w-full data-[panel-group-direction=vertical]:after:tw--translate-y-1/2 data-[panel-group-direction=vertical]:after:tw-translate-x-0 [&[data-panel-group-direction=vertical]>div]:tw-rotate-90",
+      className
+    )}
+    {...props}
+  >
+    {withHandle && (
+      <div className="tw-z-sidedock tw-flex tw-h-4 tw-w-3 tw-items-center tw-justify-center tw-rounded-sm tw-border tw-border-solid tw-border-border">
+        <GripVertical className="tw-size-2.5" />
+      </div>
+    )}
+  </ResizablePrimitive.PanelResizeHandle>
+);
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..253a9f9
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+import * as React from "react";
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+
+import { cn } from "@/lib/utils";
+
+const ScrollArea = React.forwardRef<
+  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
+>(({ className, children, ...props }, ref) => (
+  <ScrollAreaPrimitive.Root
+    ref={ref}
+    className={cn("tw-relative tw-overflow-hidden", className)}
+    {...props}
+  >
+    {/*[&>div:first-child]:!tw-block：for override the first child div style's (display: table)*/}
+    <ScrollAreaPrimitive.Viewport className="tw-size-full tw-rounded-[inherit] [&>div:first-child]:!tw-block">
+      {children}
+    </ScrollAreaPrimitive.Viewport>
+    <ScrollBar />
+    <ScrollAreaPrimitive.Corner />
+  </ScrollAreaPrimitive.Root>
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
+  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
+>(({ className, orientation = "vertical", ...props }, ref) => (
+  <ScrollAreaPrimitive.ScrollAreaScrollbar
+    ref={ref}
+    orientation={orientation}
+    className={cn(
+      "tw-flex tw-touch-none tw-select-none tw-transition-colors",
+      orientation === "vertical" &&
+        "tw-h-full tw-w-2.5 tw-border-l tw-border-l-transparent tw-p-[1px]",
+      orientation === "horizontal" &&
+        "tw-h-2.5 tw-flex-col tw-border-t tw-border-t-transparent tw-p-[1px]",
+      className
+    )}
+    {...props}
+  >
+    {/* tw-bg-border -> --divider-color: var(--background-modifier-border); */}
+    <ScrollAreaPrimitive.ScrollAreaThumb className="tw-relative tw-flex-1 tw-rounded-full tw-bg-[var(--background-modifier-border)]" />
+  </ScrollAreaPrimitive.ScrollAreaScrollbar>
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000..14a4a35
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+
+import { cn } from "@/lib/utils";
+
+const Separator = React.forwardRef<
+  React.ElementRef<typeof SeparatorPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
+>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
+  // tw-bg-border -> --divider-color: var(--background-modifier-border);
+  <SeparatorPrimitive.Root
+    ref={ref}
+    decorative={decorative}
+    orientation={orientation}
+    className={cn(
+      "tw-shrink-0 tw-bg-[var(--background-modifier-border)]",
+      orientation === "horizontal" ? "tw-h-[1px] tw-w-full" : "tw-h-full tw-w-[1px]",
+      className
+    )}
+    {...props}
+  />
+));
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/src/constants.ts b/src/constants.ts
index 2269c5d..354f29c 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,6 +1,5 @@
 import { CustomModel } from "@/aiParams";
 import { AcceptKeyOption } from "@/autocomplete/codemirrorIntegration";
-import { DEFAULT_INLINE_EDIT_COMMANDS } from "@/commands/constants";
 import { type CopilotSettings } from "@/settings/model";
 import { v4 as uuidv4 } from "uuid";
 import { ChainType } from "./chainFactory";
@@ -549,17 +548,12 @@ export enum DEFAULT_OPEN_AREA {
 }
 
 export const COMMAND_IDS = {
-  ADD_CUSTOM_PROMPT: "add-custom-prompt",
   APPLY_ADHOC_PROMPT: "apply-adhoc-prompt",
-  APPLY_CUSTOM_PROMPT: "apply-custom-prompt",
   CLEAR_LOCAL_COPILOT_INDEX: "clear-local-copilot-index",
   CLEAR_COPILOT_CACHE: "clear-copilot-cache",
   COUNT_WORD_AND_TOKENS_SELECTION: "count-word-and-tokens-selection",
   COUNT_TOTAL_VAULT_TOKENS: "count-total-vault-tokens",
   DEBUG_WORD_COMPLETION: "debug-word-completion",
-  DELETE_CUSTOM_PROMPT: "delete-custom-prompt",
-  EDIT_CUSTOM_PROMPT: "edit-custom-prompt",
-  FIND_RELEVANT_NOTES: "find-relevant-notes",
   FORCE_REINDEX_VAULT_TO_COPILOT_INDEX: "force-reindex-vault-to-copilot-index",
   GARBAGE_COLLECT_COPILOT_INDEX: "garbage-collect-copilot-index",
   INDEX_VAULT_TO_COPILOT_INDEX: "index-vault-to-copilot-index",
@@ -575,17 +569,12 @@ export const COMMAND_IDS = {
 } as const;
 
 export const COMMAND_NAMES: Record<CommandId, string> = {
-  [COMMAND_IDS.ADD_CUSTOM_PROMPT]: "Add custom prompt",
   [COMMAND_IDS.APPLY_ADHOC_PROMPT]: "Apply ad-hoc custom prompt",
-  [COMMAND_IDS.APPLY_CUSTOM_PROMPT]: "Apply custom prompt",
   [COMMAND_IDS.CLEAR_LOCAL_COPILOT_INDEX]: "Clear local Copilot index",
   [COMMAND_IDS.CLEAR_COPILOT_CACHE]: "Clear Copilot cache",
   [COMMAND_IDS.COUNT_TOTAL_VAULT_TOKENS]: "Count total tokens in your vault",
   [COMMAND_IDS.COUNT_WORD_AND_TOKENS_SELECTION]: "Count words and tokens in selection",
   [COMMAND_IDS.DEBUG_WORD_COMPLETION]: "Word completion: Debug",
-  [COMMAND_IDS.DELETE_CUSTOM_PROMPT]: "Delete custom prompt",
-  [COMMAND_IDS.EDIT_CUSTOM_PROMPT]: "Edit custom prompt",
-  [COMMAND_IDS.FIND_RELEVANT_NOTES]: "Find relevant notes",
   [COMMAND_IDS.FORCE_REINDEX_VAULT_TO_COPILOT_INDEX]: "Force reindex vault",
   [COMMAND_IDS.GARBAGE_COLLECT_COPILOT_INDEX]:
     "Garbage collect Copilot index (remove files that no longer exist in vault)",
@@ -666,7 +655,8 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
   promptUsageTimestamps: {},
   promptSortStrategy: PromptSortStrategy.TIMESTAMP,
   defaultConversationNoteName: "{$topic}@{$date}_{$time}",
-  inlineEditCommands: DEFAULT_INLINE_EDIT_COMMANDS,
+  /** @deprecated */
+  inlineEditCommands: [],
   projectList: [],
   enableAutocomplete: true,
   autocompleteAcceptKey: AUTOCOMPLETE_CONFIG.KEYBIND,
@@ -675,11 +665,13 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
   lastDismissedVersion: null,
   passMarkdownImages: true,
   enableCustomPromptTemplating: true,
+  suggestedDefaultCommands: false,
 };
 
 export const EVENT_NAMES = {
   CHAT_IS_VISIBLE: "chat-is-visible",
   ACTIVE_LEAF_CHANGE: "active-leaf-change",
+  ABORT_STREAM: "abort-stream",
 };
 
 export enum ABORT_REASON {
diff --git a/src/contextMenu.ts b/src/contextMenu.ts
deleted file mode 100644
index 6e4bd7f..0000000
--- a/src/contextMenu.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { getCommandId, getInlineEditCommands } from "@/commands/inlineEditCommandUtils";
-import CopilotPlugin from "@/main";
-import { Editor, Menu } from "obsidian";
-
-export function registerContextMenu(menu: Menu, editor: Editor, plugin: CopilotPlugin) {
-  const commands = getInlineEditCommands();
-
-  commands
-    .filter((command) => command.showInContextMenu)
-    .forEach((command) => {
-      menu.addItem((item) => {
-        item.setTitle(`Copilot: ${command.name}`).onClick(async (e) => {
-          (plugin.app as any).commands.executeCommandById(`copilot:${getCommandId(command.name)}`);
-        });
-      });
-    });
-}
diff --git a/src/customPromptProcessor.ts b/src/customPromptProcessor.ts
deleted file mode 100644
index 7058e96..0000000
--- a/src/customPromptProcessor.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-import { CustomError } from "@/error";
-import { createPromptUsageStrategy, PromptUsageStrategy } from "@/promptUsageStrategy";
-import { getSettings, subscribeToSettingsChange } from "@/settings/model";
-import {
-  extractNoteFiles,
-  getFileContent,
-  getFileName,
-  getNotesFromPath,
-  getNotesFromTags,
-  processVariableNameForNotePath,
-} from "@/utils";
-import { normalizePath, Notice, TFile, Vault } from "obsidian";
-import { NOTE_CONTEXT_PROMPT_TAG } from "./constants";
-
-export interface CustomPrompt {
-  title: string;
-  content: string;
-}
-
-/**
- * {copilot-selection} is the legacy custom command special placeholder. It must
- * be skipped when processing custom prompts because it's handled differently
- * by the custom command prompt processor.
- */
-const VARIABLE_REGEX = /\{(?!copilot-selection\})([^}]+)\}/g;
-
-/**
- * Represents the result of processing a custom prompt variable.
- */
-interface VariableProcessingResult {
-  content: string;
-  files: TFile[];
-}
-
-/**
- * Extract variables from a custom prompt and get their content and associated files.
- */
-async function extractVariablesFromPrompt(
-  customPrompt: string,
-  vault: Vault,
-  activeNote?: TFile | null
-): Promise<{ variablesMap: Map<string, string>; includedFiles: Set<TFile> }> {
-  const variablesMap = new Map<string, string>();
-  const includedFiles = new Set<TFile>();
-  let match;
-
-  while ((match = VARIABLE_REGEX.exec(customPrompt)) !== null) {
-    const variableName = match[1].trim();
-    const variableResult: VariableProcessingResult = { content: "", files: [] };
-
-    if (variableName.toLowerCase() === "activenote") {
-      if (activeNote) {
-        const content = await getFileContent(activeNote, vault);
-        if (content) {
-          variableResult.content = `## ${getFileName(activeNote)}\n\n${content}`;
-          variableResult.files.push(activeNote);
-        }
-      } else {
-        new Notice("No active note found.");
-      }
-    } else if (variableName.startsWith("#")) {
-      // Handle tag-based variable for multiple tags
-      const tagNames = variableName
-        .slice(1)
-        .split(",")
-        .map((tag) => tag.trim());
-      const noteFiles = await getNotesFromTags(vault, tagNames);
-      const notesContent: string[] = [];
-      for (const file of noteFiles) {
-        const content = await getFileContent(file, vault);
-        if (content) {
-          notesContent.push(`## ${getFileName(file)}\n\n${content}`);
-          variableResult.files.push(file);
-        }
-      }
-      variableResult.content = notesContent.join("\n\n");
-    } else {
-      const processedVariableName = processVariableNameForNotePath(variableName);
-      const noteFiles = await getNotesFromPath(vault, processedVariableName);
-      const notesContent: string[] = [];
-      for (const file of noteFiles) {
-        const content = await getFileContent(file, vault);
-        if (content) {
-          notesContent.push(`## ${getFileName(file)}\n\n${content}`);
-          variableResult.files.push(file);
-        }
-      }
-      variableResult.content = notesContent.join("\n\n");
-    }
-
-    if (variableResult.content) {
-      variablesMap.set(variableName, variableResult.content);
-      variableResult.files.forEach((file) => includedFiles.add(file));
-    } else if (variableName.toLowerCase() !== "activenote") {
-      if (variableName.startsWith('"')) {
-        // DO NOTHING as the user probably wants to write a JSON object
-      } else {
-        console.warn(`No notes found for variable: ${variableName}`);
-      }
-    }
-  }
-
-  return { variablesMap, includedFiles };
-}
-
-/**
- * Represents the result of processing a custom prompt.
- */
-export interface ProcessedPromptResult {
-  processedPrompt: string;
-  includedFiles: TFile[];
-}
-
-/**
- * Process a custom prompt by replacing variables and adding note contents.
- * Returns the processed prompt string and a list of files included in the processing.
- */
-export async function processPrompt(
-  customPrompt: string,
-  selectedText: string,
-  vault: Vault,
-  activeNote?: TFile | null
-): Promise<ProcessedPromptResult> {
-  const settings = getSettings();
-  const includedFiles = new Set<TFile>();
-
-  if (!settings.enableCustomPromptTemplating) {
-    // If templating is disabled, check if activeNote should be included for {}
-    if (customPrompt.includes("{}") && !selectedText && activeNote) {
-      includedFiles.add(activeNote);
-    }
-    return {
-      processedPrompt: customPrompt + "\n\n",
-      includedFiles: Array.from(includedFiles),
-    };
-  }
-
-  // Extract variables and track files included through them
-  const { variablesMap, includedFiles: variableFiles } = await extractVariablesFromPrompt(
-    customPrompt,
-    vault,
-    activeNote
-  );
-  variableFiles.forEach((file) => includedFiles.add(file));
-
-  let processedPrompt = customPrompt;
-  let additionalInfo = "";
-  let activeNoteContent: string | null = null;
-
-  if (processedPrompt.includes("{}")) {
-    processedPrompt = processedPrompt.replace(/\{\}/g, "{selectedText}");
-    if (selectedText) {
-      additionalInfo += `selectedText:\n\n${selectedText}`;
-      // Note: selectedText doesn't directly correspond to a file inclusion here
-    } else if (activeNote) {
-      activeNoteContent = await getFileContent(activeNote, vault);
-      additionalInfo += `selectedText (entire active note):\n\n${activeNoteContent}`;
-      includedFiles.add(activeNote); // Ensure active note is tracked if used for {}
-    } else {
-      additionalInfo += `selectedText:\n\n(No selected text or active note available)`;
-    }
-  }
-
-  // Add variable contents to the additional info
-  // The files are already tracked via includedFiles set
-  for (const [varName, content] of variablesMap.entries()) {
-    if (varName.toLowerCase() === "activenote" && activeNoteContent !== null) {
-      // Content already added via {} handling, but file tracking is done.
-      continue;
-    }
-    if (additionalInfo) {
-      additionalInfo += `\n\n${varName}:\n\n${content}`;
-    } else {
-      additionalInfo += `${varName}:\n\n${content}`;
-    }
-  }
-
-  // Process [[note title]] syntax
-  const noteLinkFiles = extractNoteFiles(processedPrompt, vault);
-  for (const noteFile of noteLinkFiles) {
-    // Check if this note wasn't already included via a variable
-    // We use the Set's reference equality which works for TFile objects
-    if (!includedFiles.has(noteFile)) {
-      const noteContent = await getFileContent(noteFile, vault);
-      if (noteContent) {
-        const noteContext = `<${NOTE_CONTEXT_PROMPT_TAG}> \n Title: [[${noteFile.basename}]]\nPath: ${noteFile.path}\n\n${noteContent}\n</${NOTE_CONTEXT_PROMPT_TAG}>`;
-        if (additionalInfo) {
-          additionalInfo += `\n\n`;
-        }
-        additionalInfo += `${noteContext}`;
-        includedFiles.add(noteFile); // Track files included via [[links]]
-      }
-    }
-  }
-
-  return {
-    processedPrompt: additionalInfo
-      ? `${processedPrompt}\n\n${additionalInfo}`
-      : `${processedPrompt}\n\n`,
-    includedFiles: Array.from(includedFiles),
-  };
-}
-
-export class CustomPromptProcessor {
-  private static instance: CustomPromptProcessor;
-  private usageStrategy: PromptUsageStrategy;
-
-  private constructor(private vault: Vault) {
-    this.usageStrategy = createPromptUsageStrategy();
-
-    subscribeToSettingsChange(() => {
-      this.usageStrategy = createPromptUsageStrategy();
-    });
-  }
-
-  get customPromptsFolder(): string {
-    return getSettings().customPromptsFolder;
-  }
-
-  static getInstance(vault: Vault): CustomPromptProcessor {
-    if (!CustomPromptProcessor.instance) {
-      CustomPromptProcessor.instance = new CustomPromptProcessor(vault);
-    }
-    return CustomPromptProcessor.instance;
-  }
-
-  recordPromptUsage(title: string) {
-    this.usageStrategy.recordUsage(title);
-  }
-
-  async getAllPrompts(): Promise<CustomPrompt[]> {
-    const folder = this.customPromptsFolder;
-    const files = this.vault
-      .getFiles()
-      .filter((file) => file.path.startsWith(folder) && file.extension === "md");
-
-    const prompts: CustomPrompt[] = [];
-    for (const file of files) {
-      const content = await this.vault.read(file);
-      prompts.push({
-        title: file.basename,
-        content: content,
-      });
-    }
-
-    // Clean up promptUsageTimestamps
-    this.usageStrategy.removeUnusedPrompts(prompts.map((prompt) => prompt.title));
-
-    return prompts.sort((a, b) => this.usageStrategy.compare(b.title, a.title) || 0);
-  }
-
-  async getPrompt(title: string): Promise<CustomPrompt | null> {
-    const filePath = `${this.customPromptsFolder}/${title}.md`;
-    const file = this.vault.getAbstractFileByPath(filePath);
-    if (file instanceof TFile) {
-      const content = await this.vault.read(file);
-      return { title, content };
-    }
-    return null;
-  }
-
-  async savePrompt(title: string, content: string): Promise<void> {
-    const folderPath = normalizePath(this.customPromptsFolder);
-    const filePath = `${folderPath}/${title}.md`;
-
-    // Check if the folder exists and create it if it doesn't
-    const folderExists = await this.vault.adapter.exists(folderPath);
-    if (!folderExists) {
-      await this.vault.createFolder(folderPath);
-    }
-
-    // Create the file
-    await this.vault.create(filePath, content);
-  }
-
-  async updatePrompt(originTitle: string, newTitle: string, content: string): Promise<void> {
-    const filePath = `${this.customPromptsFolder}/${originTitle}.md`;
-    const file = this.vault.getAbstractFileByPath(filePath);
-
-    if (file instanceof TFile) {
-      if (originTitle !== newTitle) {
-        const newFilePath = `${this.customPromptsFolder}/${newTitle}.md`;
-        const newFileExists = this.vault.getAbstractFileByPath(newFilePath);
-
-        if (newFileExists) {
-          throw new CustomError(
-            "Error saving custom prompt. Please check if the title already exists."
-          );
-        }
-
-        this.usageStrategy.updateUsage(originTitle, newTitle);
-        await this.vault.rename(file, newFilePath);
-      }
-      await this.vault.modify(file, content);
-    }
-  }
-
-  async deletePrompt(title: string): Promise<void> {
-    const filePath = `${this.customPromptsFolder}/${title}.md`;
-    const file = this.vault.getAbstractFileByPath(filePath);
-    if (file instanceof TFile) {
-      this.usageStrategy.removeUnusedPrompts([title]);
-      await this.vault.delete(file);
-    }
-  }
-
-  /**
-   * Process a custom prompt by replacing variables, adding note contents,
-   * and tracking which files were included.
-   *
-   * @param {string} customPrompt - the custom prompt template
-   * @param {string} selectedText - the text selected by the user
-   * @param {TFile} [activeNote] - the currently active note (optional)
-   * @return {Promise<ProcessedPromptResult>} An object containing the processed prompt string
-   *                                         and an array of TFile objects included.
-   */
-  async processCustomPrompt(
-    customPrompt: string,
-    selectedText: string,
-    activeNote?: TFile
-  ): Promise<ProcessedPromptResult> {
-    // Remove dependency on lastProcessedPrompt state
-    return processPrompt(customPrompt, selectedText, this.vault, activeNote);
-  }
-}
diff --git a/src/main.ts b/src/main.ts
index f7f94fc..b2005d6 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -8,8 +8,8 @@ import CopilotView from "@/components/CopilotView";
 import { APPLY_VIEW_TYPE, ApplyView } from "@/components/composer/ApplyView";
 import { LoadChatHistoryModal } from "@/components/modals/LoadChatHistoryModal";
 
-import { CHAT_VIEWTYPE, DEFAULT_OPEN_AREA, EVENT_NAMES } from "@/constants";
-import { registerContextMenu } from "@/contextMenu";
+import { ABORT_REASON, CHAT_VIEWTYPE, DEFAULT_OPEN_AREA, EVENT_NAMES } from "@/constants";
+import { registerContextMenu } from "@/commands/contextMenu";
 import { encryptAllKeys } from "@/encryptionService";
 import { logInfo } from "@/logger";
 import { checkIsPlusUser } from "@/plusUtils";
@@ -36,6 +36,8 @@ import {
   WorkspaceLeaf,
 } from "obsidian";
 import { IntentAnalyzer } from "./LLMProviders/intentAnalyzer";
+import { CustomCommandRegister } from "@/commands/customCommandRegister";
+import { migrateCommands, suggestDefaultCommands } from "@/commands/migrator";
 
 export default class CopilotPlugin extends Plugin {
   // A chat history that stores the messages sent and received
@@ -46,6 +48,7 @@ export default class CopilotPlugin extends Plugin {
   userMessageHistory: string[] = [];
   vectorStoreManager: VectorStoreManager;
   fileParserManager: FileParserManager;
+  customCommandRegister: CustomCommandRegister;
   settingsUnsubscriber?: () => void;
   private autocompleteService: AutocompleteService;
 
@@ -91,11 +94,8 @@ export default class CopilotPlugin extends Plugin {
     IntentAnalyzer.initTools(this.app.vault);
 
     this.registerEvent(
-      this.app.workspace.on("editor-menu", (menu: Menu, editor: Editor) => {
-        const selectedText = editor.getSelection().trim();
-        if (selectedText) {
-          this.handleContextMenu(menu, editor);
-        }
+      this.app.workspace.on("editor-menu", (menu: Menu) => {
+        return registerContextMenu(menu);
       })
     );
 
@@ -119,10 +119,13 @@ export default class CopilotPlugin extends Plugin {
 
     // Initialize autocomplete service
     this.autocompleteService = AutocompleteService.getInstance(this);
+    this.customCommandRegister = new CustomCommandRegister(this, this.app.vault);
+    this.app.workspace.onLayoutReady(() => {
+      this.customCommandRegister.initialize().then(migrateCommands).then(suggestDefaultCommands);
+    });
   }
 
   async onunload() {
-    // Clean up VectorStoreManager
     if (this.vectorStoreManager) {
       this.vectorStoreManager.onunload();
     }
@@ -131,6 +134,7 @@ export default class CopilotPlugin extends Plugin {
       this.projectManager.onunload();
     }
 
+    this.customCommandRegister.cleanup();
     this.settingsUnsubscriber?.();
     this.autocompleteService?.destroy();
 
@@ -288,10 +292,6 @@ export default class CopilotPlugin extends Plugin {
     return Array.from(modelMap.values());
   }
 
-  handleContextMenu = (menu: Menu, editor: Editor): void => {
-    registerContextMenu(menu, editor, this);
-  };
-
   async loadCopilotChatHistory() {
     const chatFiles = await this.getChatHistoryFiles();
     if (chatFiles.length === 0) {
@@ -356,6 +356,17 @@ export default class CopilotPlugin extends Plugin {
     // First autosave the current chat if the setting is enabled
     await this.autosaveCurrentChat();
 
+    // Abort any ongoing streams before clearing chat
+    const existingView = this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0];
+    if (existingView) {
+      const copilotView = existingView.view as CopilotView;
+      // Dispatch abort event to stop any ongoing streams
+      const abortEvent = new CustomEvent(EVENT_NAMES.ABORT_STREAM, {
+        detail: { reason: ABORT_REASON.NEW_CHAT },
+      });
+      copilotView.eventTarget.dispatchEvent(abortEvent);
+    }
+
     // Clear chat history
     this.sharedState.clearChatHistory();
 
@@ -363,7 +374,6 @@ export default class CopilotPlugin extends Plugin {
     this.projectManager.getCurrentChainManager().memoryManager.clearChatMemory();
 
     // Update view if it exists
-    const existingView = this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0];
     if (existingView) {
       const copilotView = existingView.view as CopilotView;
       copilotView.updateView();
diff --git a/src/promptUsageStrategy.ts b/src/promptUsageStrategy.ts
deleted file mode 100644
index 779d34c..0000000
--- a/src/promptUsageStrategy.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { getSettings, updateSetting } from "@/settings/model";
-import { PromptSortStrategy } from "@/types";
-
-export interface PromptUsageStrategy {
-  recordUsage: (promptTitle: string) => void;
-
-  updateUsage: (oldTitle: string, newTitle: string) => void;
-
-  removeUnusedPrompts: (existingPromptTitles: Array<string>) => void;
-
-  compare: (aKey: string, bKey: string) => number;
-}
-
-export class TimestampUsageStrategy implements PromptUsageStrategy {
-  get usageData(): Readonly<Record<string, number>> {
-    return getSettings().promptUsageTimestamps;
-  }
-
-  recordUsage(promptTitle: string) {
-    updateSetting("promptUsageTimestamps", { ...this.usageData, [promptTitle]: Date.now() });
-  }
-
-  updateUsage(oldTitle: string, newTitle: string) {
-    const newUsageData = { ...this.usageData };
-    newUsageData[newTitle] = newUsageData[oldTitle];
-    delete newUsageData[oldTitle];
-    updateSetting("promptUsageTimestamps", newUsageData);
-  }
-
-  removeUnusedPrompts(existingPromptTitles: Array<string>) {
-    const newUsageData = { ...this.usageData };
-    for (const key of Object.keys(newUsageData)) {
-      if (!existingPromptTitles.includes(key)) {
-        delete newUsageData[key];
-      }
-    }
-    updateSetting("promptUsageTimestamps", newUsageData);
-  }
-
-  compare(aKey: string, bKey: string): number {
-    return (this.usageData[aKey] || 0) - (this.usageData[bKey] || 0);
-  }
-}
-
-export class AlphabeticalSortStrategy implements PromptUsageStrategy {
-  get usageData(): Readonly<Record<string, number>> {
-    return getSettings().promptUsageTimestamps;
-  }
-
-  recordUsage(promptTitle: string) {
-    // Alphabetical sorting doesn't need to record usage, but keeping this method for interface consistency
-    // Reusing timestamp strategy implementation to maintain state synchronization
-    updateSetting("promptUsageTimestamps", { ...this.usageData, [promptTitle]: Date.now() });
-  }
-
-  updateUsage(oldTitle: string, newTitle: string) {
-    const newUsageData = { ...this.usageData };
-    newUsageData[newTitle] = newUsageData[oldTitle];
-    delete newUsageData[oldTitle];
-    updateSetting("promptUsageTimestamps", newUsageData);
-  }
-
-  removeUnusedPrompts(existingPromptTitles: Array<string>) {
-    const newUsageData = { ...this.usageData };
-    for (const key of Object.keys(newUsageData)) {
-      if (!existingPromptTitles.includes(key)) {
-        delete newUsageData[key];
-      }
-    }
-    updateSetting("promptUsageTimestamps", newUsageData);
-  }
-
-  compare(aKey: string, bKey: string): number {
-    return bKey.localeCompare(aKey);
-  }
-}
-
-export function createPromptUsageStrategy(): PromptUsageStrategy {
-  const settings = getSettings();
-  const strategy = settings.promptSortStrategy || PromptSortStrategy.TIMESTAMP;
-
-  switch (strategy) {
-    case PromptSortStrategy.ALPHABETICAL:
-      return new AlphabeticalSortStrategy();
-    case PromptSortStrategy.TIMESTAMP:
-    default:
-      return new TimestampUsageStrategy();
-  }
-}
diff --git a/src/search/searchUtils.ts b/src/search/searchUtils.ts
index c4275b3..4111fc7 100644
--- a/src/search/searchUtils.ts
+++ b/src/search/searchUtils.ts
@@ -5,7 +5,7 @@ import { getTagsFromNote, stripHash } from "@/utils";
 import { Embeddings } from "@langchain/core/embeddings";
 import { App, TFile } from "obsidian";
 
-interface PatternCategory {
+export interface PatternCategory {
   tagPatterns?: string[];
   extensionPatterns?: string[];
   folderPatterns?: string[];
@@ -331,3 +331,15 @@ export function extractAppIgnoreSettings(app: App): string[] {
 
   return appIgnoreFolders;
 }
+
+export function getTagPattern(tag: string): string {
+  return `#${tag}`;
+}
+
+export function getFilePattern(file: TFile): string {
+  return `[[${file.basename}]]`;
+}
+
+export function getExtensionPattern(extension: string): string {
+  return `*.${extension}`;
+}
diff --git a/src/settings/model.ts b/src/settings/model.ts
index f342d95..01c8c80 100644
--- a/src/settings/model.ts
+++ b/src/settings/model.ts
@@ -14,7 +14,12 @@ import {
   EmbeddingModelProviders,
 } from "@/constants";
 
-export interface InlineEditCommandSettings {
+/**
+ * We used to store commands in the settings file with the following interface.
+ * It has been migrated to CustomCommand. This interface is needed to migrate
+ * the legacy commands to the new format.
+ */
+export interface LegacyCommandSettings {
   /**
    * The name of the command. The name will be turned into id by replacing
    * spaces with underscores.
@@ -97,7 +102,7 @@ export interface CopilotSettings {
   defaultConversationNoteName: string;
   // undefined means never checked
   isPlusUser: boolean | undefined;
-  inlineEditCommands: InlineEditCommandSettings[] | undefined;
+  inlineEditCommands: LegacyCommandSettings[] | undefined;
   enableAutocomplete: boolean;
   autocompleteAcceptKey: AcceptKeyOption;
   allowAdditionalContext: boolean;
@@ -105,6 +110,8 @@ export interface CopilotSettings {
   projectList: Array<ProjectConfig>;
   passMarkdownImages: boolean;
   enableCustomPromptTemplating: boolean;
+  /** Whether we have suggested built-in default commands to the user once. */
+  suggestedDefaultCommands: boolean;
 }
 
 export const settingsStore = createStore();
diff --git a/src/settings/v2/SettingsMainV2.tsx b/src/settings/v2/SettingsMainV2.tsx
index bcc13a9..6a2d071 100644
--- a/src/settings/v2/SettingsMainV2.tsx
+++ b/src/settings/v2/SettingsMainV2.tsx
@@ -44,7 +44,7 @@ const tabs: TabItemType[] = TAB_IDS.map((id) => ({
   label: id.charAt(0).toUpperCase() + id.slice(1),
 }));
 
-const SettingsContent: React.FC<{ plugin: CopilotPlugin }> = ({ plugin }) => {
+const SettingsContent: React.FC = () => {
   const { selectedTab, setSelectedTab } = useTab();
 
   return (
@@ -130,7 +130,7 @@ const SettingsMainV2: React.FC<SettingsMainV2Props> = ({ plugin }) => {
           </h1>
         </div>
         {/* Add the key prop to force re-render */}
-        <SettingsContent key={resetKey} plugin={plugin} />
+        <SettingsContent key={resetKey} />
       </div>
     </TabProvider>
   );
diff --git a/src/settings/v2/components/AdvancedSettings.tsx b/src/settings/v2/components/AdvancedSettings.tsx
index 5051247..3fe46ad 100644
--- a/src/settings/v2/components/AdvancedSettings.tsx
+++ b/src/settings/v2/components/AdvancedSettings.tsx
@@ -1,6 +1,5 @@
 import { SettingItem } from "@/components/ui/setting-item";
 import { updateSetting, useSettingsValue } from "@/settings/model";
-import { PromptSortStrategy } from "@/types";
 import React from "react";
 
 export const AdvancedSettings: React.FC = () => {
@@ -20,28 +19,6 @@ export const AdvancedSettings: React.FC = () => {
         />
 
         <div className="tw-space-y-4">
-          <SettingItem
-            type="switch"
-            title="Custom Prompt Templating"
-            description="Enable templating to process variables like {activenote}, {foldername} or {#tag} in prompts. Disable to use raw prompts without any processing."
-            checked={settings.enableCustomPromptTemplating}
-            onCheckedChange={(checked) => {
-              updateSetting("enableCustomPromptTemplating", checked);
-            }}
-          />
-
-          <SettingItem
-            type="select"
-            title="Custom Prompts Sort Strategy"
-            description="Choose how to sort custom prompts (by recent usage or alphabetically)"
-            value={settings.promptSortStrategy}
-            onChange={(value) => updateSetting("promptSortStrategy", value)}
-            options={[
-              { label: "Recency", value: PromptSortStrategy.TIMESTAMP },
-              { label: "Alphabetical", value: PromptSortStrategy.ALPHABETICAL },
-            ]}
-          />
-
           <SettingItem
             type="switch"
             title="Enable Encryption"
diff --git a/src/settings/v2/components/BasicSettings.tsx b/src/settings/v2/components/BasicSettings.tsx
index d24db17..04639d7 100644
--- a/src/settings/v2/components/BasicSettings.tsx
+++ b/src/settings/v2/components/BasicSettings.tsx
@@ -282,15 +282,6 @@ export const BasicSettings: React.FC = () => {
             placeholder="copilot-conversations"
           />
 
-          <SettingItem
-            type="text"
-            title="Custom Prompts Folder Name"
-            description="The default folder name where custom prompts will be saved. Default is 'copilot-custom-prompts'"
-            value={settings.customPromptsFolder}
-            onChange={(value) => updateSetting("customPromptsFolder", value)}
-            placeholder="copilot-custom-prompts"
-          />
-
           <SettingItem
             type="text"
             title="Default Conversation Tag"
diff --git a/src/settings/v2/components/CommandSettings.tsx b/src/settings/v2/components/CommandSettings.tsx
index fa9dc20..b6661b6 100644
--- a/src/settings/v2/components/CommandSettings.tsx
+++ b/src/settings/v2/components/CommandSettings.tsx
@@ -1,17 +1,8 @@
-import React from "react";
-import { InlineEditCommandSettings, updateSetting } from "@/settings/model";
+import React, { useMemo } from "react";
 import { Button } from "@/components/ui/button";
-import { InlineEditCommandSettingsModal } from "@/components/modals/InlineEditCommandSettingsModal";
-import { hasModifiedCommand, useInlineEditCommands } from "@/commands/inlineEditCommandUtils";
-import {
-  Lightbulb,
-  PencilLine,
-  Plus,
-  GripVertical,
-  Copy,
-  MoreVertical,
-  Trash2,
-} from "lucide-react";
+import { useCustomCommands } from "@/commands/state";
+import { Lightbulb, GripVertical, Trash2, Plus, Info, PenLine } from "lucide-react";
+
 import {
   Table,
   TableBody,
@@ -31,7 +22,6 @@ import {
   DragEndEvent,
 } from "@dnd-kit/core";
 import {
-  arrayMove,
   SortableContext,
   sortableKeyboardCoordinates,
   useSortable,
@@ -39,22 +29,34 @@ import {
 } from "@dnd-kit/sortable";
 import { CSS } from "@dnd-kit/utilities";
 import { cn } from "@/lib/utils";
-import {
-  DropdownMenu,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+import { useSettingsValue } from "@/settings/model";
+import { updateSetting } from "@/settings/model";
+import { PromptSortStrategy } from "@/types";
 import { useContainerContext } from "@/settings/v2/components/ContainerContext";
+import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Input } from "@/components/ui/input";
+import { Notice } from "obsidian";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { CustomCommand } from "@/commands/type";
+import {
+  loadAllCustomCommands,
+  sortCommandsByOrder,
+  validateCommandName,
+} from "@/commands/customCommandUtils";
+import { CustomCommandSettingsModal } from "@/commands/CustomCommandSettingsModal";
+import { SettingItem } from "@/components/ui/setting-item";
+import { CustomCommandManager } from "@/commands/customCommandManager";
+import { ConfirmModal } from "@/components/modals/ConfirmModal";
+import { generateDefaultCommands } from "@/commands/migrator";
 
 const SortableTableRow: React.FC<{
-  command: InlineEditCommandSettings;
-  onUpdate: (prevCommand: InlineEditCommandSettings, newCommand: InlineEditCommandSettings) => void;
-  onRemove: (command: InlineEditCommandSettings) => void;
-  onDuplicate: (command: InlineEditCommandSettings) => void;
-}> = ({ command, onUpdate, onRemove, onDuplicate }) => {
+  command: CustomCommand;
+  commands: CustomCommand[];
+  onUpdate: (newCommand: CustomCommand, prevCommandTitle: string) => void;
+  onRemove: (command: CustomCommand) => void;
+}> = ({ command, commands, onUpdate, onRemove }) => {
   const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
-    id: command.name,
+    id: command.title,
   });
 
   const style = {
@@ -62,7 +64,9 @@ const SortableTableRow: React.FC<{
     transition,
   };
 
-  const container = useContainerContext();
+  const handleDelete = () => {
+    onRemove(command);
+  };
 
   return (
     <TableRow
@@ -83,15 +87,33 @@ const SortableTableRow: React.FC<{
           <GripVertical className="tw-size-4" />
         </div>
       </TableCell>
-      <TableCell>{command.name}</TableCell>
+      <TableCell>{command.title}</TableCell>
       <TableCell className="tw-text-center">
         <Checkbox
           checked={command.showInContextMenu}
+          onCheckedChange={(checked) => {
+            onUpdate(
+              {
+                ...command,
+                showInContextMenu: checked === true,
+              },
+              command.title
+            );
+          }}
+          className="tw-mx-auto"
+        />
+      </TableCell>
+      <TableCell className="tw-text-center">
+        <Checkbox
+          checked={command.showInSlashMenu}
           onCheckedChange={(checked) =>
-            onUpdate(command, {
-              ...command,
-              showInContextMenu: checked === true,
-            })
+            onUpdate(
+              {
+                ...command,
+                showInSlashMenu: checked === true,
+              },
+              command.title
+            )
           }
           className="tw-mx-auto"
         />
@@ -101,47 +123,36 @@ const SortableTableRow: React.FC<{
           <Button
             variant="ghost"
             size="icon"
-            onClick={() =>
-              new InlineEditCommandSettingsModal(
+            onClick={() => {
+              const modal = new CustomCommandSettingsModal(
                 app,
+                commands,
                 command,
-                (newCommand) => onUpdate(command, newCommand),
-                () => onRemove(command)
-              ).open()
-            }
+                async (updatedCommand) => {
+                  await onUpdate(updatedCommand, command.title);
+                }
+              );
+              modal.open();
+            }}
           >
-            <PencilLine className="tw-size-4" />
+            <PenLine className="tw-size-4" />
+          </Button>
+          <Button
+            variant="ghost"
+            size="icon"
+            onClick={() => {
+              new ConfirmModal(
+                app,
+                handleDelete,
+                `Are you sure you want to delete the command "${command.title}"? This will permanently remove the command file and cannot be undone.`,
+                "Delete Command",
+                "Delete",
+                "Cancel"
+              ).open();
+            }}
+          >
+            <Trash2 className="tw-size-4" />
           </Button>
-          <DropdownMenu>
-            <DropdownMenuTrigger asChild>
-              <Button variant="ghost" size="icon">
-                <MoreVertical className="tw-size-4" />
-              </Button>
-            </DropdownMenuTrigger>
-            <DropdownMenuContent align="end" container={container}>
-              <DropdownMenuItem
-                onClick={() =>
-                  new InlineEditCommandSettingsModal(
-                    app,
-                    command,
-                    (newCommand) => onUpdate(command, newCommand),
-                    () => onRemove(command)
-                  ).open()
-                }
-              >
-                <PencilLine className="tw-mr-2 tw-size-4" />
-                Edit
-              </DropdownMenuItem>
-              <DropdownMenuItem onClick={() => onDuplicate(command)}>
-                <Copy className="tw-mr-2 tw-size-4" />
-                Copy
-              </DropdownMenuItem>
-              <DropdownMenuItem onClick={() => onRemove(command)} className="tw-text-error">
-                <Trash2 className="tw-mr-2 tw-size-4" />
-                Delete
-              </DropdownMenuItem>
-            </DropdownMenuContent>
-          </DropdownMenu>
         </div>
       </TableCell>
     </TableRow>
@@ -149,7 +160,14 @@ const SortableTableRow: React.FC<{
 };
 
 export const CommandSettings: React.FC = () => {
-  const commands = useInlineEditCommands();
+  const rawCommands = useCustomCommands();
+  const commands = useMemo(() => {
+    return sortCommandsByOrder([...rawCommands]);
+  }, [rawCommands]);
+
+  const [newCommandName, setNewCommandName] = React.useState("");
+
+  const settings = useSettingsValue();
   const sensors = useSensors(
     useSensor(PointerSensor, {
       activationConstraint: {
@@ -161,74 +179,119 @@ export const CommandSettings: React.FC = () => {
     })
   );
 
-  const handleUpdate = (
-    prevCommand: InlineEditCommandSettings,
-    newCommand: InlineEditCommandSettings
-  ) => {
-    const index = commands.findIndex((c) => c === prevCommand);
-    if (index === -1) {
-      updateSetting("inlineEditCommands", [...commands, newCommand]);
-    } else {
-      updateSetting("inlineEditCommands", [
-        ...commands.slice(0, index),
-        newCommand,
-        ...commands.slice(index + 1),
-      ]);
+  const validationError = validateCommandName(newCommandName, commands);
+  const canCreate = !validationError && newCommandName.trim() !== "";
+
+  const handleCreateCommand = async () => {
+    if (!canCreate) return;
+
+    try {
+      await CustomCommandManager.getInstance().createCommand(newCommandName.trim(), "");
+      setNewCommandName("");
+      new Notice(`Command "${newCommandName.trim()}" created successfully!`);
+    } catch (error) {
+      console.error("Failed to create command:", error);
+      new Notice("Failed to create command. Please try again.");
     }
   };
 
-  const handleDuplicate = (command: InlineEditCommandSettings) => {
-    const duplicatedCommand = {
-      ...command,
-      name: `${command.name} (copy)`,
-    };
-    const index = commands.findIndex((c) => c === command);
-    if (index !== -1) {
-      updateSetting("inlineEditCommands", [
-        ...commands.slice(0, index + 1),
-        duplicatedCommand,
-        ...commands.slice(index + 1),
-      ]);
-    }
+  const handleUpdate = async (newCommand: CustomCommand, prevCommandTitle: string) => {
+    await CustomCommandManager.getInstance().updateCommand(newCommand, prevCommandTitle);
   };
 
-  const handleRemove = (command: InlineEditCommandSettings) => {
-    updateSetting(
-      "inlineEditCommands",
-      commands.filter((c) => c !== command)
-    );
+  const handleRemove = async (command: CustomCommand) => {
+    try {
+      await CustomCommandManager.getInstance().deleteCommand(command);
+
+      new Notice(`Command "${command.title}" deleted successfully!`);
+    } catch (error) {
+      console.error("Failed to delete command:", error);
+      new Notice("Failed to delete command. Please try again.");
+      throw error;
+    }
   };
 
-  const handleDragEnd = (event: DragEndEvent) => {
+  const handleDragEnd = async (event: DragEndEvent) => {
     const { active, over } = event;
 
-    if (over && active.id !== over.id) {
-      const oldIndex = commands.findIndex((command) => command.name === active.id);
-      const newIndex = commands.findIndex((command) => command.name === over.id);
+    if (!over || active.id === over.id) {
+      return;
+    }
+
+    const activeIndex = commands.findIndex((command) => command.title === active.id);
+    const overIndex = commands.findIndex((command) => command.title === over.id);
+
+    if (activeIndex === -1 || overIndex === -1) {
+      return;
+    }
+
+    // Create new order
+    const newCommands = [...commands];
+    const [movedCommand] = newCommands.splice(activeIndex, 1);
+    newCommands.splice(overIndex, 0, movedCommand);
 
-      if (oldIndex !== -1 && newIndex !== -1) {
-        const newCommands = arrayMove(commands, oldIndex, newIndex);
-        updateSetting("inlineEditCommands", newCommands);
-      }
+    for (let i = 0; i < newCommands.length; i++) {
+      newCommands[i] = { ...newCommands[i], order: i * 10 };
     }
+
+    await CustomCommandManager.getInstance().updateCommands(newCommands);
   };
 
+  const container = useContainerContext();
+
   return (
     <div className="tw-space-y-4">
       <section>
         <div className="tw-mb-4 tw-flex tw-flex-col tw-gap-2">
           <div className="tw-text-xl tw-font-bold">Custom Commands</div>
           <div className="tw-text-sm tw-text-muted">
-            To trigger a custom command, highlight text in the editor and select it from the command
-            palette, or right-click and choose it from the context menu if configured.
+            Custom commands are preset prompts that you can trigger in the editor by right-clicking
+            and selecting them from the context menu or by using a <code>/</code> command in the
+            chat to load them into your chat input.
           </div>
         </div>
-        {!hasModifiedCommand() && (
-          <div className="tw-flex tw-items-start tw-gap-2 tw-rounded-md tw-border tw-border-solid tw-border-border tw-p-4 tw-text-muted">
-            <Lightbulb className="tw-size-5" /> Take control of your inline edit commands! You can
-            now create your own or edit built-in ones to tailor functionality to your needs.
+
+        <SettingItem
+          type="text"
+          title="Custom Prompts Folder Name"
+          description="Folder where custom prompts are stored"
+          value={settings.customPromptsFolder}
+          onChange={(value) => {
+            updateSetting("customPromptsFolder", value);
+            loadAllCustomCommands();
+          }}
+          placeholder="copilot-custom-prompts"
+        />
+        <SettingItem
+          type="switch"
+          title="Custom Prompt Templating"
+          description="Process variables like {activenote}, {foldername}, or {#tag} in prompts. Disable for raw prompts."
+          checked={settings.enableCustomPromptTemplating}
+          onCheckedChange={(checked) => {
+            updateSetting("enableCustomPromptTemplating", checked);
+          }}
+        />
+        <SettingItem
+          type="select"
+          title="Custom Prompts Sort Strategy"
+          description="Sort order for slash command menu prompts"
+          value={settings.promptSortStrategy}
+          onChange={(value) => updateSetting("promptSortStrategy", value)}
+          options={[
+            { label: "Recency", value: PromptSortStrategy.TIMESTAMP },
+            { label: "Alphabetical", value: PromptSortStrategy.ALPHABETICAL },
+            { label: "Manual", value: PromptSortStrategy.MANUAL },
+          ]}
+        />
+
+        <div className="tw-flex tw-items-start tw-gap-2 tw-rounded-md tw-border tw-border-solid tw-border-border tw-p-4 tw-text-muted">
+          <Lightbulb className="tw-size-5" />{" "}
+          <div>
+            Commands are automatically loaded from .md files in your custom prompts folder{" "}
+            <strong>{settings.customPromptsFolder}</strong>. Modifying the files will also update
+            the command settings.
           </div>
-        )}
+        </div>
 
         <div className="tw-flex tw-flex-col tw-gap-4">
           <DndContext
@@ -241,45 +304,137 @@ export const CommandSettings: React.FC = () => {
                 <TableRow>
                   <TableHead className="tw-w-10"></TableHead>
                   <TableHead>Name</TableHead>
-                  <TableHead className="tw-w-20 tw-text-center">In Menu</TableHead>
-                  <TableHead className="tw-w-10"></TableHead>
+                  <TableHead className="tw-w-24 tw-text-center">
+                    <div className="tw-flex tw-items-center tw-justify-center tw-gap-1">
+                      In Menu
+                      <TooltipProvider>
+                        <Tooltip>
+                          <TooltipTrigger asChild>
+                            <Info className="tw-size-4" />
+                          </TooltipTrigger>
+                          <TooltipContent className="tw-max-w-xs tw-text-xs">
+                            If enabled, the command will be available in the context menu when you
+                            right-click in the editor.
+                          </TooltipContent>
+                        </Tooltip>
+                      </TooltipProvider>
+                    </div>
+                  </TableHead>
+                  <TableHead className="tw-w-28 tw-text-center">
+                    <div className="tw-flex tw-items-center tw-justify-center tw-gap-1">
+                      Slash Cmd
+                      <TooltipProvider>
+                        <Tooltip>
+                          <TooltipTrigger asChild>
+                            <Info className="tw-size-4" />
+                          </TooltipTrigger>
+                          <TooltipContent className="tw-max-w-xs tw-text-xs">
+                            If enabled, the command will be available as a slash command in the
+                            chat.
+                          </TooltipContent>
+                        </Tooltip>
+                      </TooltipProvider>
+                    </div>
+                  </TableHead>
+                  <TableHead className="tw-w-32 tw-text-center">Actions</TableHead>
                 </TableRow>
               </TableHeader>
               <SortableContext
-                items={commands.map((command) => command.name)}
+                items={commands.map((command) => command.title)}
                 strategy={verticalListSortingStrategy}
               >
                 <TableBody>
-                  {commands.map((command) => (
-                    <SortableTableRow
-                      key={command.name}
-                      command={command}
-                      onUpdate={handleUpdate}
-                      onRemove={handleRemove}
-                      onDuplicate={handleDuplicate}
-                    />
-                  ))}
+                  {commands.length === 0 ? (
+                    <TableRow>
+                      <TableCell colSpan={5} className="tw-py-8 tw-text-center tw-text-muted">
+                        No custom prompt files found.
+                      </TableCell>
+                    </TableRow>
+                  ) : (
+                    commands.map((command) => (
+                      <SortableTableRow
+                        key={command.title}
+                        command={command}
+                        commands={commands}
+                        onUpdate={handleUpdate}
+                        onRemove={handleRemove}
+                      />
+                    ))
+                  )}
                 </TableBody>
               </SortableContext>
             </Table>
           </DndContext>
-          <div className="tw-flex tw-w-full tw-justify-end">
-            <Button
-              variant="secondary"
-              onClick={() =>
-                new InlineEditCommandSettingsModal(
-                  app,
-                  {
-                    name: "",
-                    prompt: "",
-                    showInContextMenu: false,
-                  },
-                  (command) => handleUpdate(command, command)
-                ).open()
-              }
-            >
-              <Plus className="tw-size-4" /> Add Command
-            </Button>
+          <div className="tw-flex tw-w-full tw-justify-between">
+            <div>
+              <Button
+                variant="secondary"
+                onClick={() =>
+                  new ConfirmModal(
+                    app,
+                    generateDefaultCommands,
+                    "This will add default commands to your custom prompts folder. Do you want to continue?",
+                    "Generate Default Commands"
+                  ).open()
+                }
+              >
+                Generate Default Commands
+              </Button>
+            </div>
+            <Popover>
+              <PopoverTrigger asChild>
+                <Button variant="default" className="tw-gap-2">
+                  <Plus className="tw-size-4" />
+                  Add Command
+                </Button>
+              </PopoverTrigger>
+              <PopoverContent container={container} className="tw-w-80" align="end">
+                <div className="tw-flex tw-flex-col tw-gap-4">
+                  <div className="tw-space-y-2">
+                    <div className="tw-text-lg tw-font-medium tw-leading-none">
+                      Create New Command
+                    </div>
+                    <p className="tw-text-sm tw-text-muted">
+                      Enter a name for your new custom command. A markdown file will be created in
+                      your custom prompts folder.
+                    </p>
+                  </div>
+                  <div className="tw-space-y-2">
+                    <Input
+                      placeholder="Command name"
+                      value={newCommandName}
+                      onChange={(e) => setNewCommandName(e.target.value)}
+                      onKeyDown={(e) => {
+                        if (e.key === "Enter" && canCreate) {
+                          handleCreateCommand();
+                        }
+                      }}
+                    />
+                    {validationError && (
+                      <p className="tw-text-sm tw-text-error">{validationError}</p>
+                    )}
+                  </div>
+                  <div className="tw-flex tw-justify-end tw-gap-2">
+                    <PopoverClose asChild>
+                      <Button
+                        variant="secondary"
+                        size="sm"
+                        onClick={() => {
+                          setNewCommandName("");
+                        }}
+                      >
+                        Cancel
+                      </Button>
+                    </PopoverClose>
+                    <PopoverClose asChild>
+                      <Button size="sm" onClick={handleCreateCommand} disabled={!canCreate}>
+                        Create
+                      </Button>
+                    </PopoverClose>
+                  </div>
+                </div>
+              </PopoverContent>
+            </Popover>
           </div>
         </div>
       </section>
diff --git a/src/types.ts b/src/types.ts
index 9e37b2b..baf75df 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -13,4 +13,5 @@ declare module "obsidian" {
 export enum PromptSortStrategy {
   TIMESTAMP = "timestamp",
   ALPHABETICAL = "alphabetical",
+  MANUAL = "manual",
 }
diff --git a/src/utils.test.ts b/src/utils.test.ts
index ab280f3..6b50e83 100644
--- a/src/utils.test.ts
+++ b/src/utils.test.ts
@@ -6,6 +6,7 @@ import {
   getNotesFromTags,
   isFolderMatch,
   processVariableNameForNotePath,
+  removeThinkTags,
 } from "./utils";
 
 // Mock Obsidian's TFile class
@@ -378,3 +379,76 @@ describe("extractNoteFiles", () => {
     expect(resultPaths).toEqual(["Note-1.md", "Note_2.md", "Note#3.md"]);
   });
 });
+
+describe("removeThinkTags", () => {
+  it("should remove complete think tags and their content", () => {
+    const input = "Before <think>This is thinking content</think> After";
+    const expected = "Before  After";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should handle multiple think tags", () => {
+    const input = "Text <think>First thought</think> middle <think>Second thought</think> end";
+    const expected = "Text  middle  end";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should handle multiline think content", () => {
+    const input = `Start
+<think>
+Line 1
+Line 2
+Line 3
+</think>
+End`;
+    const expected = "Start\n\nEnd";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should handle unclosed think tags (streaming scenario)", () => {
+    const input = "Before content <think>Partial thought that is still being";
+    const expected = "Before content";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should handle empty think tags", () => {
+    const input = "Text <think></think> more text";
+    const expected = "Text  more text";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should handle text without think tags", () => {
+    const input = "This is regular text without any think tags";
+    const expected = "This is regular text without any think tags";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should handle nested content within think tags", () => {
+    const input = `Main text <think>
+I need to consider:
+- Point 1
+- Point 2
+<inner>nested content</inner>
+</think> Final text`;
+    const expected = "Main text  Final text";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should trim whitespace from the result", () => {
+    const input = "  <think>content</think>  ";
+    const expected = "";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should handle think tags at the beginning of text", () => {
+    const input = "<think>Initial thoughts</think>Main content here";
+    const expected = "Main content here";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+
+  it("should handle think tags at the end of text", () => {
+    const input = "Main content here<think>Final thoughts</think>";
+    const expected = "Main content here";
+    expect(removeThinkTags(input)).toBe(expected);
+  });
+});
diff --git a/src/utils.ts b/src/utils.ts
index 132f656..84f2c03 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -184,6 +184,8 @@ export function getNotesFromTags(vault: Vault, tags: string[], noteFiles?: TFile
   return filesWithTag;
 }
 
+// TODO: Chain type conversion still needed for chain runner selection
+// This function is still used but the underlying chain infrastructure is deprecated
 export const stringToChainType = (chain: string): ChainType => {
   switch (chain) {
     case "llm_chain":
@@ -197,6 +199,8 @@ export const stringToChainType = (chain: string): ChainType => {
   }
 };
 
+// TODO: These chain validation functions are deprecated
+// Remove after confirming chainManager no longer uses them
 export const isLLMChain = (chain: RunnableSequence): chain is RunnableSequence => {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   return (chain as any).last.bound.modelName || (chain as any).last.bound.model;
@@ -662,10 +666,14 @@ export async function insertIntoEditor(message: string, replace: boolean = false
   const editor = leaf.view.editor;
   const cursorFrom = editor.getCursor("from");
   const cursorTo = editor.getCursor("to");
+
+  // Remove think tags before inserting
+  const cleanedMessage = removeThinkTags(message);
+
   if (replace) {
-    editor.replaceRange(message, cursorFrom, cursorTo);
+    editor.replaceRange(cleanedMessage, cursorFrom, cursorTo);
   } else {
-    editor.replaceRange(message, cursorTo);
+    editor.replaceRange(cleanedMessage, cursorTo);
   }
   new Notice("Message inserted into the active note.");
 }
@@ -804,8 +812,11 @@ export function extractTextFromChunk(content: any): string {
 export function removeThinkTags(text: any): string {
   // First convert any content format to plain text
   const plainText = extractTextFromChunk(text);
-  // Then remove think tags
-  return plainText.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
+  // Remove complete think tags and their content
+  let cleanedText = plainText.replace(/<think>[\s\S]*?<\/think>/g, "");
+  // Remove any remaining unclosed think tags (for streaming scenarios)
+  cleanedText = cleanedText.replace(/<think>[\s\S]*$/g, "");
+  return cleanedText.trim();
 }
 
 export function randomUUID() {
diff --git a/tailwind.config.js b/tailwind.config.js
index 8d8b7ed..e58ec7e 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -40,6 +40,14 @@ module.exports = {
         green: "var(--color-green)",
         blue: "var(--color-blue)",
       },
+      "context-manager": {
+        blue: "var(--color-blue)",
+        green: "var(--color-green)",
+        red: "var(--color-red)",
+        yellow: "var(--color-yellow)",
+        orange: "var(--color-orange)",
+        purple: "var(--color-purple)",
+      },
     },
     backgroundColor: {
       inherit: colors.inherit,



Test patch:
diff --git a/src/tests/context-manage-modal.test.ts b/src/tests/context-manage-modal.test.ts
new file mode 100644
index 0000000..359cd0e
--- /dev/null
+++ b/src/tests/context-manage-modal.test.ts
@@ -0,0 +1,546 @@
+import { TFile } from "obsidian";
+import {
+  createPatternSettingsValue,
+  getFilePattern,
+  shouldIndexFile,
+  PatternCategory,
+} from "@/search/searchUtils";
+
+// Mock dependencies
+jest.mock("obsidian", () => ({
+  TFile: class TFile {
+    path: string;
+    basename: string;
+    extension: string;
+
+    constructor(path: string = "") {
+      this.path = path;
+      const parts = path.split("/");
+      const filename = parts[parts.length - 1];
+      this.basename = filename.replace(/\.[^/.]+$/, "");
+      this.extension = filename.split(".").pop() || "";
+    }
+  },
+}));
+
+jest.mock("@/search/searchUtils", () => ({
+  getMatchingPatterns: jest.fn(),
+  shouldIndexFile: jest.fn(),
+  getFilePattern: jest.fn(),
+  createPatternSettingsValue: jest.fn(),
+  categorizePatterns: jest.fn(),
+  getDecodedPatterns: jest.fn(),
+}));
+
+// Mock types and interfaces
+interface GroupItem {
+  id: string;
+  name: string;
+  isIgnored?: boolean;
+}
+
+interface GroupListItem {
+  tags: Record<string, Array<GroupItem>>;
+  folders: Record<string, Array<GroupItem>>;
+  extensions: Record<string, Array<GroupItem>>;
+  notes: Array<GroupItem>;
+}
+
+interface IgnoreItems {
+  files: Set<TFile>;
+}
+
+// Test helper functions
+const createTestFile = (path: string): TFile => {
+  const file = new TFile();
+  file.path = path;
+  const parts = path.split("/");
+  const filename = parts[parts.length - 1];
+  file.basename = filename.replace(/\.[^/.]+$/, "");
+  file.extension = filename.split(".").pop() || "";
+  return file;
+};
+
+const createMockPatternCategory = (overrides: Partial<PatternCategory> = {}): PatternCategory => ({
+  tagPatterns: [],
+  folderPatterns: [],
+  extensionPatterns: [],
+  notePatterns: [],
+  ...overrides,
+});
+
+// Implementation of the functions to test
+const createAndPopulateGroupList = (
+  appFiles: TFile[],
+  inclusionPatterns: PatternCategory | null,
+  exclusionPatterns: PatternCategory | null
+): GroupListItem => {
+  const projectAllFiles = appFiles.filter((file) =>
+    shouldIndexFile(file, inclusionPatterns, exclusionPatterns)
+  );
+
+  // Initialize groups
+  const tags: Record<string, Array<GroupItem>> = {};
+  const folders: Record<string, Array<GroupItem>> = {};
+  const extensions: Record<string, Array<GroupItem>> = {};
+  const notes: Array<GroupItem> = [];
+
+  (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => {
+    tags[tag] = [];
+  });
+  (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => {
+    folders[folder] = [];
+  });
+  (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => {
+    extensions[extension] = [];
+  });
+
+  // Populate with matching files
+  projectAllFiles.forEach((file) => {
+    const groupItem: GroupItem = {
+      id: file.path,
+      name: file.basename,
+    };
+
+    // Add to notes array
+    notes.push(groupItem);
+
+    // Add to appropriate groups based on patterns
+    (inclusionPatterns?.tagPatterns ?? []).forEach((tag) => {
+      if (tags[tag]) {
+        tags[tag].push(groupItem);
+      }
+    });
+
+    (inclusionPatterns?.folderPatterns ?? []).forEach((folder) => {
+      if (folders[folder] && file.path.includes(folder)) {
+        folders[folder].push(groupItem);
+      }
+    });
+
+    (inclusionPatterns?.extensionPatterns ?? []).forEach((extension) => {
+      if (extensions[extension] && file.extension === extension.replace("*.", "")) {
+        extensions[extension].push(groupItem);
+      }
+    });
+  });
+
+  return { tags, folders, extensions, notes };
+};
+
+const convertGroupListToInclusions = (list: GroupListItem, appFiles: TFile[]): string => {
+  const tagPatterns = Object.keys(list.tags);
+  const folderPatterns = Object.keys(list.folders);
+  const extensionPatterns = Object.keys(list.extensions);
+  const notePatterns = list.notes
+    .map((note) => {
+      const file = appFiles.find((file) => file.path === note.id);
+      if (file) {
+        return getFilePattern(file);
+      }
+    })
+    .filter(Boolean) as string[];
+
+  return createPatternSettingsValue({
+    tagPatterns,
+    folderPatterns,
+    extensionPatterns,
+    notePatterns,
+  });
+};
+
+const convertDeletedItemsToExclusions = (items: IgnoreItems): string => {
+  const notePatterns = Array.from(items.files).map((file) => getFilePattern(file));
+  return createPatternSettingsValue({ notePatterns }) || "";
+};
+
+describe("Context Manage Modal Functions", () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    // Setup default mocks
+    (shouldIndexFile as jest.Mock).mockReturnValue(true);
+    (getFilePattern as jest.Mock).mockImplementation((file: TFile) => `[[${file.basename}]]`);
+    (createPatternSettingsValue as jest.Mock).mockImplementation(
+      ({ tagPatterns = [], folderPatterns = [], extensionPatterns = [], notePatterns = [] }) => {
+        const patterns = [...tagPatterns, ...folderPatterns, ...extensionPatterns, ...notePatterns];
+        return patterns.join(",");
+      }
+    );
+  });
+
+  describe("createAndPopulateGroupList", () => {
+    it("Should create empty GroupList when there are no files", () => {
+      const result = createAndPopulateGroupList([], null, null);
+
+      expect(result).toEqual({
+        tags: {},
+        folders: {},
+        extensions: {},
+        notes: [],
+      });
+    });
+
+    it("Should correctly filter files based on inclusion and exclusion patterns", () => {
+      const files = [
+        createTestFile("test1.md"),
+        createTestFile("test2.pdf"),
+        createTestFile("test3.txt"),
+      ];
+
+      (shouldIndexFile as jest.Mock)
+        .mockReturnValueOnce(true)
+        .mockReturnValueOnce(false)
+        .mockReturnValueOnce(true);
+
+      const inclusionPatterns = createMockPatternCategory();
+      const exclusionPatterns = createMockPatternCategory();
+
+      const result = createAndPopulateGroupList(files, inclusionPatterns, exclusionPatterns);
+
+      expect(shouldIndexFile).toHaveBeenCalledTimes(3);
+      expect(result.notes).toHaveLength(2);
+      expect(result.notes[0].id).toBe("test1.md");
+      expect(result.notes[1].id).toBe("test3.txt");
+    });
+
+    it("Should initialize groups based on inclusion patterns", () => {
+      const files = [createTestFile("test.md")];
+      const inclusionPatterns = createMockPatternCategory({
+        tagPatterns: ["#important", "#work"],
+        folderPatterns: ["docs", "projects"],
+        extensionPatterns: ["*.md", "*.pdf"],
+      });
+
+      const result = createAndPopulateGroupList(files, inclusionPatterns, null);
+
+      expect(Object.keys(result.tags)).toEqual(["#important", "#work"]);
+      expect(Object.keys(result.folders)).toEqual(["docs", "projects"]);
+      expect(Object.keys(result.extensions)).toEqual(["*.md", "*.pdf"]);
+    });
+
+    it("Should add files to appropriate groups", () => {
+      const files = [createTestFile("docs/test.md"), createTestFile("projects/work.pdf")];
+
+      const inclusionPatterns = createMockPatternCategory({
+        folderPatterns: ["docs", "projects"],
+        extensionPatterns: ["*.md", "*.pdf"],
+      });
+
+      const result = createAndPopulateGroupList(files, inclusionPatterns, null);
+
+      expect(result.folders["docs"]).toHaveLength(1);
+      expect(result.folders["docs"][0].name).toBe("test");
+      expect(result.folders["projects"]).toHaveLength(1);
+      expect(result.folders["projects"][0].name).toBe("work");
+    });
+
+    it("Should handle duplicate patterns", () => {
+      const files = [createTestFile("test.md")];
+      const inclusionPatterns = createMockPatternCategory({
+        tagPatterns: ["#tag", "#tag", "#tag"],
+        folderPatterns: ["folder", "folder"],
+      });
+
+      const result = createAndPopulateGroupList(files, inclusionPatterns, null);
+
+      expect(Object.keys(result.tags)).toEqual(["#tag"]);
+      expect(Object.keys(result.folders)).toEqual(["folder"]);
+    });
+  });
+
+  describe("convertGroupListToInclusions", () => {
+    it("Should convert empty GroupList to empty string", () => {
+      const emptyGroupList: GroupListItem = {
+        tags: {},
+        folders: {},
+        extensions: {},
+        notes: [],
+      };
+
+      const result = convertGroupListToInclusions(emptyGroupList, []);
+
+      expect(createPatternSettingsValue).toHaveBeenCalledWith({
+        tagPatterns: [],
+        folderPatterns: [],
+        extensionPatterns: [],
+        notePatterns: [],
+      });
+      expect(result).toBe("");
+    });
+
+    it("Should correctly extract all pattern types", () => {
+      const groupList: GroupListItem = {
+        tags: { "#important": [], "#work": [] },
+        folders: { docs: [], projects: [] },
+        extensions: { "*.md": [], "*.pdf": [] },
+        notes: [
+          { id: "note1.md", name: "note1" },
+          { id: "note2.md", name: "note2" },
+        ],
+      };
+
+      const appFiles = [createTestFile("note1.md"), createTestFile("note2.md")];
+
+      convertGroupListToInclusions(groupList, appFiles);
+
+      expect(createPatternSettingsValue).toHaveBeenCalledWith({
+        tagPatterns: ["#important", "#work"],
+        folderPatterns: ["docs", "projects"],
+        extensionPatterns: ["*.md", "*.pdf"],
+        notePatterns: ["[[note1]]", "[[note2]]"],
+      });
+    });
+
+    it("Should handle notes without corresponding files", () => {
+      const groupList: GroupListItem = {
+        tags: {},
+        folders: {},
+        extensions: {},
+        notes: [
+          { id: "missing.md", name: "missing" },
+          { id: "existing.md", name: "existing" },
+        ],
+      };
+
+      const appFiles = [createTestFile("existing.md")];
+
+      convertGroupListToInclusions(groupList, appFiles);
+
+      expect(createPatternSettingsValue).toHaveBeenCalledWith({
+        tagPatterns: [],
+        folderPatterns: [],
+        extensionPatterns: [],
+        notePatterns: ["[[existing]]"],
+      });
+    });
+  });
+
+  describe("convertDeletedItemsToExclusions", () => {
+    it("Should convert empty IgnoreItems to empty string", () => {
+      const emptyIgnoreItems: IgnoreItems = {
+        files: new Set(),
+      };
+
+      const result = convertDeletedItemsToExclusions(emptyIgnoreItems);
+
+      expect(createPatternSettingsValue).toHaveBeenCalledWith({
+        notePatterns: [],
+      });
+      expect(result).toBe("");
+    });
+
+    it("Should correctly convert ignored files to exclusion patterns", () => {
+      const file1 = createTestFile("ignore1.md");
+      const file2 = createTestFile("ignore2.pdf");
+
+      const ignoreItems: IgnoreItems = {
+        files: new Set([file1, file2]),
+      };
+
+      convertDeletedItemsToExclusions(ignoreItems);
+
+      expect(getFilePattern).toHaveBeenCalledWith(file1);
+      expect(getFilePattern).toHaveBeenCalledWith(file2);
+      expect(createPatternSettingsValue).toHaveBeenCalledWith({
+        notePatterns: ["[[ignore1]]", "[[ignore2]]"],
+      });
+    });
+
+    it("Should handle a single ignored file", () => {
+      const file = createTestFile("single.md");
+      const ignoreItems: IgnoreItems = {
+        files: new Set([file]),
+      };
+
+      convertDeletedItemsToExclusions(ignoreItems);
+
+      expect(getFilePattern).toHaveBeenCalledWith(file);
+      expect(createPatternSettingsValue).toHaveBeenCalledWith({
+        notePatterns: ["[[single]]"],
+      });
+    });
+
+    it("Should handle case when createPatternSettingsValue returns undefined", () => {
+      (createPatternSettingsValue as jest.Mock).mockReturnValueOnce(undefined);
+
+      const file = createTestFile("test.md");
+      const ignoreItems: IgnoreItems = {
+        files: new Set([file]),
+      };
+
+      const result = convertDeletedItemsToExclusions(ignoreItems);
+
+      expect(result).toBe("");
+    });
+  });
+
+  // Boundary conditions and error handling tests
+  describe("Boundary Condition Tests", () => {
+    describe("createAndPopulateGroupList", () => {
+      it("Should handle null inclusion patterns", () => {
+        const files = [createTestFile("test.md")];
+        const result = createAndPopulateGroupList(files, null, null);
+
+        expect(result.tags).toEqual({});
+        expect(result.folders).toEqual({});
+        expect(result.extensions).toEqual({});
+        expect(result.notes).toHaveLength(1);
+      });
+
+      it("Should handle file paths with special characters", () => {
+        const files = [
+          createTestFile("folder with spaces/file-with-dashes.md"),
+          createTestFile("中文文件夹/中文文件.md"),
+          createTestFile("folder/file@#$%.md"),
+        ];
+
+        const result = createAndPopulateGroupList(files, null, null);
+
+        expect(result.notes).toHaveLength(3);
+        expect(result.notes[0].name).toBe("file-with-dashes");
+        expect(result.notes[1].name).toBe("中文文件");
+        expect(result.notes[2].name).toBe("file@#$%");
+      });
+    });
+
+    describe("convertGroupListToInclusions", () => {
+      it("Should handle large number of patterns", () => {
+        const tags: Record<string, Array<GroupItem>> = {};
+        const folders: Record<string, Array<GroupItem>> = {};
+        const extensions: Record<string, Array<GroupItem>> = {};
+
+        // Create many patterns
+        for (let i = 0; i < 100; i++) {
+          tags[`#tag${i}`] = [];
+          folders[`folder${i}`] = [];
+          extensions[`*.ext${i}`] = [];
+        }
+
+        const groupList: GroupListItem = { tags, folders, extensions, notes: [] };
+        convertGroupListToInclusions(groupList, []);
+
+        expect(createPatternSettingsValue).toHaveBeenCalledWith({
+          tagPatterns: expect.arrayContaining([`#tag0`, `#tag99`]),
+          folderPatterns: expect.arrayContaining([`folder0`, `folder99`]),
+          extensionPatterns: expect.arrayContaining([`*.ext0`, `*.ext99`]),
+          notePatterns: [],
+        });
+      });
+
+      it("Should handle invalid file paths in notes", () => {
+        const groupList: GroupListItem = {
+          tags: {},
+          folders: {},
+          extensions: {},
+          notes: [
+            { id: "", name: "" },
+            { id: "   ", name: "   " },
+            { id: "valid.md", name: "valid" },
+          ],
+        };
+
+        const appFiles = [createTestFile("valid.md")];
+        convertGroupListToInclusions(groupList, appFiles);
+
+        expect(createPatternSettingsValue).toHaveBeenCalledWith({
+          tagPatterns: [],
+          folderPatterns: [],
+          extensionPatterns: [],
+          notePatterns: ["[[valid]]"],
+        });
+      });
+    });
+
+    describe("convertDeletedItemsToExclusions", () => {
+      it("Should handle large number of ignored files", () => {
+        const files = new Set<TFile>();
+        for (let i = 0; i < 1000; i++) {
+          files.add(createTestFile(`file${i}.md`));
+        }
+
+        const ignoreItems: IgnoreItems = { files };
+        convertDeletedItemsToExclusions(ignoreItems);
+
+        expect(getFilePattern).toHaveBeenCalledTimes(1000);
+        expect(createPatternSettingsValue).toHaveBeenCalledWith({
+          notePatterns: expect.arrayContaining(["[[file0]]", "[[file999]]"]),
+        });
+      });
+
+      it("Should handle filenames with special characters", () => {
+        const files = [
+          createTestFile("file with spaces.md"),
+          createTestFile("file-with-dashes.md"),
+          createTestFile("file_with_underscores.md"),
+          createTestFile("file@#$%.md"),
+        ];
+
+        const ignoreItems: IgnoreItems = { files: new Set(files) };
+        convertDeletedItemsToExclusions(ignoreItems);
+
+        expect(getFilePattern).toHaveBeenCalledTimes(4);
+        files.forEach((file) => {
+          expect(getFilePattern).toHaveBeenCalledWith(file);
+        });
+      });
+    });
+  });
+
+  // Integration tests
+  describe("Integration Tests", () => {
+    it("Should correctly handle the complete workflow", () => {
+      // 1. Create initial file list
+      const appFiles = [
+        createTestFile("docs/readme.md"),
+        createTestFile("src/main.ts"),
+        createTestFile("tests/test.spec.ts"),
+        createTestFile("config.json"),
+      ];
+
+      // 2. Set inclusion patterns
+      const inclusionPatterns = createMockPatternCategory({
+        folderPatterns: ["docs", "src"],
+        extensionPatterns: ["*.md", "*.ts"],
+      });
+
+      // 3. Create GroupList
+      const groupList = createAndPopulateGroupList(appFiles, inclusionPatterns, null);
+
+      // 4. Convert to inclusions
+      const inclusions = convertGroupListToInclusions(groupList, appFiles);
+
+      // 5. Create ignore items and convert to exclusions
+      const ignoreItems: IgnoreItems = {
+        files: new Set([createTestFile("config.json")]),
+      };
+      const exclusions = convertDeletedItemsToExclusions(ignoreItems);
+
+      // Verify results
+      expect(groupList.folders).toHaveProperty("docs");
+      expect(groupList.folders).toHaveProperty("src");
+      expect(Object.prototype.hasOwnProperty.call(groupList.extensions, "*.md")).toBe(true);
+      expect(Object.prototype.hasOwnProperty.call(groupList.extensions, "*.ts")).toBe(true);
+
+      expect(createPatternSettingsValue).toHaveBeenCalledWith({
+        tagPatterns: [],
+        folderPatterns: ["docs", "src"],
+        extensionPatterns: ["*.md", "*.ts"],
+        notePatterns: expect.any(Array),
+      });
+
+      expect(createPatternSettingsValue).toHaveBeenCalledWith({
+        notePatterns: ["[[config]]"],
+      });
+
+      // Verify inclusions and exclusions variables
+      // Since createPatternSettingsValue is mocked to return all patterns as comma-separated list
+      expect(inclusions).toContain("docs");
+      expect(inclusions).toContain("src");
+      expect(inclusions).toContain("*.md");
+      expect(inclusions).toContain("*.ts");
+
+      expect(exclusions).toBe("[[config]]");
+    });
+  });
+});