22 KiB
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:
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
variantprop - 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
useAgentcomposesuseAgentSession+useAgentMessages- Single
onSessionUpdatesubscription, dispatches to both sub-hooks - ChatPanel calls useAgent, not sub-hooks directly
- Return is
useMemo-wrapped for referential stability
2. React Context for Services
ChatContextprovides 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
currentSessionIdbefore broadcasting
4. ACP Isolation
- All
@agentclientprotocol/sdkimports confined toacp/ AcpClient(UI-facing) andAcpHandler(SDK-facing) separate concernstype-converter.tsis 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.sendMessagenotagentobject 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
SettingsServicenotifies 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.tslocalizes type mapping changes
3. Easy Feature Addition
- New hook: create in
hooks/, call inChatPanel, wrap return inuseMemo - New message type: add to
types/session.ts, handle inuseAgentMessagesormessage-state.ts, render inMessageBubble - New agent: add settings in
plugin.ts, configure inSettingsTab
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
- Create
hooks/use[Feature].ts - Define state with useState/useReducer
- Call the hook in
ui/ChatPanel.tsx - Pass state/callbacks to child components as props
- Wrap return object in
useMemoif passed as dependency to other hooks
Adding a New Session Update Type
- Add interface to
types/session.ts, add toSessionUpdateunion - Handle in
acp/acp-handler.tssessionUpdate()switch - Convert from ACP type in
acp/type-converter.tsif needed - Handle in
hooks/useAgentSession.tshandleSessionUpdate()(for session-level) - Or handle via
applySingleUpdate()inservices/message-state.ts(for message-level) - No routing needed in ChatPanel — useAgent handles dispatch internally
Adding a New Agent Type
- Add settings type to
types/agent.ts - Add config in
plugin.tssettings - Add API key injection in
services/session-helpers.ts - Update
ui/SettingsTab.tsfor 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.