mirror of
https://github.com/rait-09/obsidian-agent-client.git
synced 2026-07-22 06:43:37 +00:00
390 lines
22 KiB
Markdown
390 lines
22 KiB
Markdown
# Architecture Documentation
|
|
|
|
## Overview
|
|
|
|
Obsidian plugin for AI agent interaction via ACP. `useAgent` facade hook composes sub-hooks (`useAgentSession` + `useAgentMessages`) and subscribes to a single `onSessionUpdate` channel. `ChatPanel` orchestrates hooks and renders children directly. Services are injected via React Context. ACP protocol details are isolated in the `acp/` layer.
|
|
|
|
## Directory Structure
|
|
|
|
```
|
|
src/
|
|
├── types/ # Type Definitions (no logic, no dependencies)
|
|
│ ├── chat.ts # ChatMessage, MessageContent, PromptContent, AttachedFile, ActivePermission
|
|
│ ├── session.ts # ChatSession, SessionUpdate (12-type union), SessionInfo, Capabilities
|
|
│ ├── agent.ts # AgentConfig, agent settings (Claude/Gemini/Codex/Custom)
|
|
│ └── errors.ts # AcpError, ProcessError, ErrorInfo
|
|
│
|
|
├── acp/ # ACP Protocol Layer (SDK dependency confined here)
|
|
│ ├── acp-client.ts # Process lifecycle, UI-facing API (AcpClient class)
|
|
│ ├── acp-handler.ts # SDK event handler + sessionId filter + listener broadcast
|
|
│ ├── type-converter.ts # ACP SDK types ↔ internal types
|
|
│ ├── permission-handler.ts # Permission queue, auto-approve, Promise resolution
|
|
│ └── terminal-handler.ts # Terminal process create/output/kill
|
|
│
|
|
├── services/ # Business Logic (non-React, no React imports)
|
|
│ ├── vault-service.ts # Vault access + fuzzy search + CM6 selection tracking
|
|
│ ├── settings-service.ts # Reactive settings store (observer pattern only)
|
|
│ ├── session-storage.ts # Session metadata + message file I/O (sessions/*.json)
|
|
│ ├── settings-normalizer.ts # Settings validation helpers (str, bool, num, enumVal, etc.)
|
|
│ ├── session-helpers.ts # Agent config building, API key injection (pure functions)
|
|
│ ├── session-state.ts # Session state updates (legacy mode/model, config restore)
|
|
│ ├── message-state.ts # Message array transforms (upsert, merge, streaming apply)
|
|
│ ├── message-sender.ts # Prompt preparation + sending (pure functions)
|
|
│ ├── chat-exporter.ts # Markdown export with frontmatter
|
|
│ ├── view-registry.ts # Multi-view management, focus, broadcast
|
|
│ └── update-checker.ts # Agent/plugin version checking
|
|
│
|
|
├── hooks/ # React Custom Hooks (state + logic)
|
|
│ ├── useAgent.ts # Facade: composes useAgentSession + useAgentMessages
|
|
│ ├── useAgentSession.ts # Session lifecycle, config options, optimistic updates
|
|
│ ├── useAgentMessages.ts # Message state, streaming (RAF batch), permissions
|
|
│ ├── useSuggestions.ts # @[[note]] mentions + /command suggestions (unified)
|
|
│ ├── useSessionHistory.ts # Session list/load/resume/fork, 5-min cache
|
|
│ ├── useChatActions.ts # Business callbacks (send, newChat, export, restart, etc.)
|
|
│ ├── useHistoryModal.ts # Session history modal lifecycle
|
|
│ └── useSettings.ts # Settings subscription (useSyncExternalStore)
|
|
│
|
|
├── ui/ # React Components
|
|
│ ├── ChatContext.ts # React Context (plugin, acpClient, vaultService, settingsService)
|
|
│ ├── ChatPanel.tsx # Orchestrator: calls hooks, workspace events, rendering
|
|
│ ├── ChatView.tsx # Sidebar view (ItemView + Context Provider)
|
|
│ ├── FloatingChatView.tsx # Floating window (position/drag/resize + Context Provider)
|
|
│ ├── FloatingButton.tsx # Draggable launch button
|
|
│ ├── ChatHeader.tsx # Header (sidebar + floating variants)
|
|
│ ├── MessageList.tsx # Virtualized message list (@tanstack/react-virtual)
|
|
│ ├── MessageBubble.tsx # Single message (content dispatch, copy button)
|
|
│ ├── ToolCallBlock.tsx # Tool call display + diff (word-level highlighting)
|
|
│ ├── TerminalBlock.tsx # Terminal output polling
|
|
│ ├── InputArea.tsx # Textarea, attachments, mentions, history
|
|
│ ├── InputToolbar.tsx # Config/mode/model selectors, usage, send button
|
|
│ ├── SuggestionPopup.tsx # Mention/command dropdown
|
|
│ ├── PermissionBanner.tsx # Permission request buttons
|
|
│ ├── ErrorBanner.tsx # Error/notification overlay
|
|
│ ├── SessionHistoryModal.tsx # Session history modal (list + confirm delete)
|
|
│ ├── SettingsTab.ts # Plugin settings UI
|
|
│ ├── view-host.ts # IChatViewHost interface
|
|
│ └── shared/
|
|
│ ├── IconButton.tsx # Icon button + Lucide icon wrapper
|
|
│ ├── MarkdownRenderer.tsx # Obsidian markdown rendering
|
|
│ └── AttachmentStrip.tsx # Attachment preview strip
|
|
│
|
|
├── utils/ # Shared Utilities (pure functions)
|
|
│ ├── platform.ts # Shell, WSL, Windows env, command building
|
|
│ ├── paths.ts # Path resolution, file:// URI
|
|
│ ├── error-utils.ts # ACP error conversion
|
|
│ ├── mention-parser.ts # @[[note]] detection/extraction
|
|
│ └── logger.ts # Debug-mode logger
|
|
│
|
|
├── plugin.ts # Obsidian plugin lifecycle, commands, view management
|
|
└── main.ts # Entry point (re-exports plugin)
|
|
```
|
|
|
|
## Architectural Layers
|
|
|
|
### 1. Types Layer (`src/types/`)
|
|
|
|
**Purpose**: Pure type definitions. No logic, no dependencies.
|
|
|
|
| File | Contents |
|
|
|------|----------|
|
|
| `chat.ts` | ChatMessage, MessageContent (8+ type union), Role, ToolCallStatus, ToolKind, AttachedFile, ActivePermission, PromptContent |
|
|
| `session.ts` | ChatSession, SessionState, SessionUpdate (12-type union incl. ProcessErrorUpdate), SessionConfigOption, Capabilities, SessionInfo |
|
|
| `agent.ts` | AgentEnvVar, BaseAgentSettings, ClaudeAgentSettings, GeminiAgentSettings, CodexAgentSettings |
|
|
| `errors.ts` | AcpErrorCode, AcpError, ProcessError, ErrorInfo |
|
|
|
|
---
|
|
|
|
### 2. ACP Layer (`src/acp/`)
|
|
|
|
**Purpose**: Isolate ACP protocol dependency. All `@agentclientprotocol/sdk` imports are confined here.
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `acp-client.ts` | UI-facing API: process spawn/kill, JSON-RPC communication, session management. Owns AcpHandler + managers. Single exit point: `onSessionUpdate` (multiple listeners via Set). |
|
|
| `acp-handler.ts` | SDK-facing: receives sessionUpdate, requestPermission, terminal ops. Filters by `currentSessionId`. Broadcasts to all listeners. |
|
|
| `type-converter.ts` | Converts ACP SDK types to internal types (change buffer for protocol updates) |
|
|
| `permission-handler.ts` | Permission request queue, auto-approve, Promise-based resolution. All UI updates via `onSessionUpdate` (no separate callback path). |
|
|
| `terminal-handler.ts` | Terminal process create/output/kill, stdout/stderr buffering |
|
|
|
|
**Key design**: All agent events (messages, session updates, permissions, errors) flow through a single `onSessionUpdate` channel. No special paths.
|
|
|
|
---
|
|
|
|
### 3. Services Layer (`src/services/`)
|
|
|
|
**Purpose**: Non-React business logic. Classes and pure functions. **No React imports.**
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `vault-service.ts` | `VaultService` class — vault note access, fuzzy search, CM6 selection tracking. Exports `IVaultAccess`, `NoteMetadata`. |
|
|
| `settings-service.ts` | `SettingsService` class — reactive settings store (observer pattern). Delegates session storage to `SessionStorage`. Exports `ISettingsAccess`. |
|
|
| `session-storage.ts` | `SessionStorage` class — session metadata CRUD (in plugin settings) + message file I/O (sessions/*.json). |
|
|
| `settings-normalizer.ts` | Pure functions — settings validation helpers (`str`, `bool`, `num`, `enumVal`, `obj`, `strRecord`, `xyPoint`), `toAgentConfig`, `parseChatFontSize`. |
|
|
| `session-helpers.ts` | Pure functions — agent config building, API key injection, agent settings resolution |
|
|
| `session-state.ts` | Pure functions — legacy mode/model application, config option restoration |
|
|
| `message-state.ts` | Pure functions — message array transforms (streaming apply, tool call upsert with O(1) index, permission scanning) |
|
|
| `message-sender.ts` | Pure functions — prompt preparation (embedded context vs XML text, shared helpers), sending with auth retry |
|
|
| `chat-exporter.ts` | `ChatExporter` class — markdown export with frontmatter, image handling |
|
|
| `view-registry.ts` | `ChatViewRegistry` class — multi-view focus tracking, broadcast commands. Exports `IChatViewContainer`. |
|
|
| `update-checker.ts` | Agent version checking via npm registry |
|
|
|
|
---
|
|
|
|
### 4. Hooks Layer (`src/hooks/`)
|
|
|
|
**Purpose**: React state management. Hook composition via useAgent facade.
|
|
|
|
| Hook | Responsibility |
|
|
|------|---------------|
|
|
| `useAgent` | Facade: composes useAgentSession + useAgentMessages. Single `onSessionUpdate` subscription. Return is `useMemo`-wrapped. |
|
|
| `useAgentSession` | Session lifecycle (create/close/restart), mode/model/configOption with optimistic updates. Uses `sessionRef` pattern. |
|
|
| `useAgentMessages` | Message state, RAF-batched streaming, permissions (activePermission derivation, approve/reject) |
|
|
| `useSuggestions` | @[[note]] mentions + /command suggestions (unified). Return is `useMemo`-wrapped. |
|
|
| `useSessionHistory` | Session list/load/resume/fork, local session storage, 5-min cache. Return is `useMemo`-wrapped. |
|
|
| `useChatActions` | Business callbacks (send, newChat, export, restart, config changes). Individual method deps for stability. |
|
|
| `useHistoryModal` | Session history modal lifecycle (lazy creation, props sync) |
|
|
| `useSettings` | Settings subscription via useSyncExternalStore |
|
|
|
|
**Dependency Rule**: Hooks import from `types/`, `acp/`, `services/`, `utils/`. Never from `ui/`.
|
|
|
|
---
|
|
|
|
### 5. UI Layer (`src/ui/`)
|
|
|
|
**Purpose**: React components. Rendering and user interaction.
|
|
|
|
#### Core Architecture
|
|
|
|
**ChatContext** provides shared services to the component tree:
|
|
```typescript
|
|
interface ChatContextValue {
|
|
plugin: AgentClientPlugin;
|
|
acpClient: AcpClient;
|
|
vaultService: VaultService;
|
|
settingsService: SettingsService;
|
|
}
|
|
```
|
|
|
|
**ChatPanel** is the central orchestrator:
|
|
- Calls hooks: useAgent, useSuggestions, useSessionHistory, useChatActions, useHistoryModal, useSettings
|
|
- Does NOT route session updates (useAgent handles that internally)
|
|
- Handles workspace events via ref pattern (stable event registration)
|
|
- Renders ChatHeader, MessageList, InputArea directly
|
|
|
|
**ChatView** (sidebar) and **FloatingChatView** (floating window) are thin wrappers:
|
|
- Create services (AcpClient, VaultService) in lifecycle methods
|
|
- Provide ChatContext
|
|
- Render ChatPanel with `variant` prop
|
|
- Implement IChatViewContainer for broadcast commands
|
|
|
|
#### Component Tree
|
|
|
|
```
|
|
ChatView / FloatingChatView
|
|
└── ChatContextProvider
|
|
└── ChatPanel (variant="sidebar" | "floating")
|
|
├── ChatHeader (variant-based rendering)
|
|
├── MessageList (virtualized via @tanstack/react-virtual)
|
|
│ └── MessageBubble (per message, React.memo)
|
|
│ ├── ToolCallBlock (React.memo) → PermissionBanner
|
|
│ ├── TerminalBlock (React.memo)
|
|
│ └── MarkdownRenderer
|
|
├── InputArea
|
|
│ ├── SuggestionPopup (mentions / commands)
|
|
│ ├── ErrorBanner
|
|
│ ├── AttachmentStrip
|
|
│ └── InputToolbar (config/mode/model/usage/send)
|
|
└── SessionHistoryModal (imperative, via useHistoryModal)
|
|
```
|
|
|
|
---
|
|
|
|
### 6. Utils Layer (`src/utils/`)
|
|
|
|
**Purpose**: Pure utility functions. No React, no Obsidian dependencies (except `platform.ts`).
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `platform.ts` | Shell detection, WSL path conversion, Windows PATH from registry, platform-specific command preparation |
|
|
| `paths.ts` | Path resolution (which/where), file:// URI building, relative path conversion |
|
|
| `error-utils.ts` | ACP error code → user-friendly title/suggestion conversion |
|
|
| `mention-parser.ts` | @[[note]] detection, replacement, extraction from text |
|
|
| `logger.ts` | Singleton logger respecting debugMode setting |
|
|
|
|
---
|
|
|
|
## Dependency Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ UI Layer │
|
|
│ │
|
|
│ ChatView / FloatingChatView (Context Providers) │
|
|
│ └── ChatPanel (hook composition + rendering) │
|
|
│ ├── ChatHeader, MessageList, InputArea │
|
|
│ └── MessageBubble, ToolCallBlock, etc. │
|
|
└─────────────────────────────┬───────────────────────────────┘
|
|
↓ calls hooks
|
|
┌─────────────────────────────┴───────────────────────────────┐
|
|
│ Hooks Layer │
|
|
│ useAgent (facade) → useAgentSession + useAgentMessages │
|
|
│ useSuggestions, useSessionHistory, useChatActions, │
|
|
│ useHistoryModal, useSettings │
|
|
└───────────┬─────────────────────────────┬───────────────────┘
|
|
↓ calls ↓ reads types
|
|
┌───────────┴───────────┐ ┌─────────────┴───────────────────┐
|
|
│ Services Layer │ │ Types Layer │
|
|
│ VaultService │ │ chat.ts, session.ts, │
|
|
│ SettingsService │ │ agent.ts, errors.ts │
|
|
│ SessionStorage │ └─────────────────────────────────┘
|
|
│ settings-normalizer │
|
|
│ session-helpers │
|
|
│ session-state │
|
|
│ message-state │
|
|
│ message-sender │
|
|
│ chat-exporter │
|
|
│ view-registry │
|
|
└───────────┬───────────┘
|
|
↓ communicates
|
|
┌───────────┴───────────┐
|
|
│ ACP Layer │
|
|
│ acp-client.ts │
|
|
│ acp-handler.ts │
|
|
│ type-converter.ts │
|
|
│ permission-handler │
|
|
│ terminal-handler │
|
|
└───────────────────────┘
|
|
↑
|
|
@agentclientprotocol/sdk
|
|
```
|
|
|
|
---
|
|
|
|
## Design Patterns
|
|
|
|
### 1. useAgent Facade Pattern
|
|
- `useAgent` composes `useAgentSession` + `useAgentMessages`
|
|
- Single `onSessionUpdate` subscription, dispatches to both sub-hooks
|
|
- ChatPanel calls useAgent, not sub-hooks directly
|
|
- Return is `useMemo`-wrapped for referential stability
|
|
|
|
### 2. React Context for Services
|
|
- `ChatContext` provides plugin, acpClient, vaultService, settingsService
|
|
- Value is stable (service instances don't change)
|
|
- Eliminates prop drilling for shared dependencies
|
|
|
|
### 3. Single Event Channel
|
|
- All agent events flow through `onSessionUpdate` (messages, session updates, permissions, errors)
|
|
- No special callback paths (onUpdateMessage, onError removed)
|
|
- AcpHandler filters by `currentSessionId` before broadcasting
|
|
|
|
### 4. ACP Isolation
|
|
- All `@agentclientprotocol/sdk` imports confined to `acp/`
|
|
- `AcpClient` (UI-facing) and `AcpHandler` (SDK-facing) separate concerns
|
|
- `type-converter.ts` is the change buffer for protocol updates
|
|
|
|
### 5. Performance Patterns
|
|
- **useMemo for return stability**: useAgent, useSuggestions, useSessionHistory wrap returns in useMemo
|
|
- **sessionRef pattern**: useAgentSession stores session in useRef, reads in callbacks without adding to deps
|
|
- **Individual method deps**: useChatActions uses `agent.sendMessage` not `agent` object in deps
|
|
- **Workspace event refs**: ChatPanel stores handler callbacks in refs, keeping useEffect deps minimal
|
|
- **RAF batching**: useAgentMessages batches streaming updates per animation frame
|
|
- **React.memo**: MessageBubble, ToolCallBlock, TerminalBlock for skip-render optimization
|
|
- **Virtual scroll**: MessageList uses @tanstack/react-virtual
|
|
- **O(1) tool call index**: Map<string, number> for tool call upsert
|
|
|
|
### 6. Observer Pattern
|
|
- `SettingsService` notifies subscribers on change
|
|
- React components use `useSyncExternalStore`
|
|
|
|
### 7. Ref Pattern for Callbacks
|
|
- IChatViewContainer callbacks use refs for latest values
|
|
- Workspace event handlers use refs to avoid re-registration
|
|
- Unmount cleanup uses refs to access latest state
|
|
|
|
---
|
|
|
|
## Key Benefits
|
|
|
|
### 1. Flat and Readable
|
|
- 4 layers (types → acp/services → hooks → ui)
|
|
- No port/adapter indirection
|
|
- File names reflect functionality
|
|
|
|
### 2. ACP Change Resistance
|
|
- Only `acp/` directory needs changes for protocol updates
|
|
- `type-converter.ts` localizes type mapping changes
|
|
|
|
### 3. Easy Feature Addition
|
|
- New hook: create in `hooks/`, call in `ChatPanel`, wrap return in `useMemo`
|
|
- New message type: add to `types/session.ts`, handle in `useAgentMessages` or `message-state.ts`, render in `MessageBubble`
|
|
- New agent: add settings in `plugin.ts`, configure in `SettingsTab`
|
|
|
|
### 4. Maintainability
|
|
- ~19,800 lines across 56 files
|
|
- Services testable without React (zero React imports)
|
|
- Clear dependency direction (no circular dependencies)
|
|
|
|
---
|
|
|
|
## File Naming Conventions
|
|
|
|
| Pattern | Example |
|
|
|---------|---------|
|
|
| Types | `kebab-case.ts` in `types/` |
|
|
| ACP | `kebab-case.ts` in `acp/` |
|
|
| Services | `kebab-case.ts` in `services/` |
|
|
| Hooks | `use*.ts` in `hooks/` |
|
|
| Components | `PascalCase.tsx` in `ui/` |
|
|
| Utilities | `kebab-case.ts` in `utils/` |
|
|
|
|
---
|
|
|
|
## Adding New Features
|
|
|
|
### Adding a New Hook
|
|
1. Create `hooks/use[Feature].ts`
|
|
2. Define state with useState/useReducer
|
|
3. Call the hook in `ui/ChatPanel.tsx`
|
|
4. Pass state/callbacks to child components as props
|
|
5. Wrap return object in `useMemo` if passed as dependency to other hooks
|
|
|
|
### Adding a New Session Update Type
|
|
1. Add interface to `types/session.ts`, add to `SessionUpdate` union
|
|
2. Handle in `acp/acp-handler.ts` `sessionUpdate()` switch
|
|
3. Convert from ACP type in `acp/type-converter.ts` if needed
|
|
4. Handle in `hooks/useAgentSession.ts` `handleSessionUpdate()` (for session-level)
|
|
5. Or handle via `applySingleUpdate()` in `services/message-state.ts` (for message-level)
|
|
6. No routing needed in ChatPanel — useAgent handles dispatch internally
|
|
|
|
### Adding a New Agent Type
|
|
1. Add settings type to `types/agent.ts`
|
|
2. Add config in `plugin.ts` settings
|
|
3. Add API key injection in `services/session-helpers.ts`
|
|
4. Update `ui/SettingsTab.ts` for configuration UI
|
|
|
|
---
|
|
|
|
## Migration Notes
|
|
|
|
### March 2026: Simplified Architecture Refactoring
|
|
|
|
Refactored from Port/Adapter Architecture to simplified layered architecture:
|
|
|
|
- **Removed**: `domain/models/` (9 files → `types/` 4 files), `domain/ports/` (5 files → interfaces moved to implementation files), `adapters/` directory, `components/` directory, `shared/` directory
|
|
- **Added**: `types/`, `acp/`, `services/`, `ui/`, `utils/` flat directories, `ChatPanel` + `ChatContext`
|
|
- **Merged**: VaultAdapter + MentionService → VaultService, useMentions + useAutoMention → useMentions
|
|
- **Removed**: useChatController (god hook → ChatPanel component), Port files (no implementation swapping planned)
|
|
- **Result**: 76 → 50 files, 5 → 4 layers, flat directory structure
|
|
|
|
### April 2026: Simplification & Performance Refactoring
|
|
|
|
Refactored data flow, hooks, services, and performance:
|
|
|
|
- **ACP wiring**: 3 exit points (onSessionUpdate, onError, setUpdateMessageCallback) → 1 (onSessionUpdate only). Multiple listeners via Set. SessionId filter in AcpHandler.
|
|
- **Hook consolidation**: 7 hooks → 4 public hooks. useSession + useMessages + usePermission → useAgent (facade) + useAgentSession + useAgentMessages. useMentions + useSlashCommands → useSuggestions. New: useChatActions, useHistoryModal.
|
|
- **ChatPanel slimmed**: 1,483 → 936 lines. Session update routing removed (moved to useAgent). Business callbacks extracted to useChatActions. History modal extracted to useHistoryModal. Workspace events stabilized with refs.
|
|
- **Services split**: settings-service.ts (722 lines) → settings-service (285) + session-storage (267) + settings-normalizer (264). Pure functions extracted: message-state.ts, session-state.ts.
|
|
- **plugin.ts cleaned**: loadSettings compressed with helper functions (370 → 120 lines). Legacy floatingChatInstances removed. Double-save fixed.
|
|
- **Performance**: useMemo on hook returns (useAgent, useSuggestions, useSessionHistory). sessionRef pattern in useAgentSession. Individual method deps in useChatActions. Workspace event handler refs in ChatPanel.
|
|
- **Result**: 50 → 56 files, ~19,800 lines. Single event channel. All hooks stabilized.
|