docs: update CLAUDE.md and ARCHITECTURE.md to reflect simplified architecture

This commit is contained in:
RAIT-09 2026-03-21 22:04:45 +09:00
parent d2948e20b2
commit ffbfe33304
2 changed files with 417 additions and 314 deletions

View file

@ -2,164 +2,202 @@
## Overview
This plugin uses **React Hooks Architecture** with clear separation of concerns. State and logic are managed by custom hooks, UI by components, and external integrations by adapters.
This plugin uses **React Hooks + ChatPanel Architecture**. A central `ChatPanel` component composes hooks and renders children directly. Services are injected via React Context. ACP protocol changes are isolated in the `acp/` layer.
## Directory Structure
```
src/
├── domain/ # Domain Layer (innermost)
│ ├── models/ # Pure domain models (no dependencies)
│ │ ├── agent-config.ts
│ │ ├── agent-error.ts
│ │ ├── chat-message.ts
│ │ └── chat-session.ts
│ └── ports/ # Interfaces (Dependency Inversion)
│ ├── agent-client.port.ts
│ ├── settings-access.port.ts
│ └── vault-access.port.ts
├── types/ # Type Definitions (no logic)
│ ├── chat.ts # ChatMessage, MessageContent, PromptContent, AttachedFile
│ ├── session.ts # ChatSession, SessionUpdate, 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 # ACP communication, process lifecycle, IAgentClient
│ ├── 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)
│ ├── vault-service.ts # Vault access + fuzzy search + CM6 selection tracking
│ ├── settings-service.ts # Settings persistence, session storage, normalization
│ ├── session-helpers.ts # Agent config building, API key injection (pure functions)
│ ├── 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)
│ ├── useAgentSession.ts # Session lifecycle, agent switching
│ ├── useChat.ts # Message sending, callbacks
│ ├── usePermission.ts # Permission handling
│ ├── useMentions.ts # @[[note]] suggestions
│ ├── useSession.ts # Session lifecycle, create/close/restart, config options
│ ├── useMessages.ts # Message state, send/receive, streaming updates
│ ├── usePermission.ts # Permission request handling
│ ├── useMentions.ts # @[[note]] suggestions + auto-mention active note
│ ├── useSlashCommands.ts # /command suggestions
│ ├── useAutoMention.ts # Auto-mention active note
│ ├── useAutoExport.ts # Auto-export on new/close
│ └── useSettings.ts # Settings subscription
│ ├── useSessionHistory.ts # Session list/load/resume/fork
│ └── useSettings.ts # Settings subscription (useSyncExternalStore)
├── adapters/ # Interface Adapters
│ ├── acp/ # Agent Client Protocol
│ │ ├── acp.adapter.ts # Implements IAgentClient port
│ │ └── acp-type-converter.ts # Converts ACP types to domain
│ └── obsidian/ # Obsidian platform
│ ├── vault.adapter.ts # Implements IVaultAccess port
│ ├── settings-store.adapter.ts # Implements ISettingsAccess port
│ └── mention-service.ts # File indexing, fuzzy search
├── ui/ # React Components
│ ├── ChatContext.ts # React Context (plugin, acpClient, vaultService)
│ ├── ChatPanel.tsx # Hook aggregation + rendering (replaces useChatController)
│ ├── 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 # Message list with auto-scroll
│ ├── MessageBubble.tsx # Single message (content type dispatch)
│ ├── ToolCallBlock.tsx # Tool call display + diff
│ ├── TerminalBlock.tsx # Terminal output polling
│ ├── InputArea.tsx # Textarea, attachments, mentions, history
│ ├── InputToolbar.tsx # 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
│ ├── types.ts # IChatViewHost interface
│ └── shared/
│ ├── IconButton.tsx # Icon button + Lucide icon wrapper
│ ├── MarkdownRenderer.tsx # Obsidian markdown rendering
│ └── AttachmentStrip.tsx # Attachment preview strip
├── components/ # UI Components
│ ├── chat/ # Chat UI (14 components)
│ │ ├── ChatView.tsx # Main view, hook composition
│ │ ├── ChatHeader.tsx
│ │ ├── ChatMessages.tsx
│ │ ├── ChatInput.tsx
│ │ ├── MessageRenderer.tsx
│ │ ├── MessageContentRenderer.tsx
│ │ ├── ToolCallRenderer.tsx
│ │ ├── TerminalRenderer.tsx
│ │ ├── PermissionRequestSection.tsx
│ │ ├── SuggestionDropdown.tsx
│ │ ├── CollapsibleThought.tsx
│ │ ├── MarkdownTextRenderer.tsx
│ │ ├── TextWithMentions.tsx
│ │ └── HeaderButton.tsx
│ └── settings/
│ └── AgentClientSettingTab.ts
├── 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
├── shared/ # Utilities
│ ├── message-service.ts # prepareMessage, sendPreparedMessage
│ ├── terminal-manager.ts # Process spawn, stdout/stderr
│ ├── chat-exporter.ts # Markdown export
│ ├── mention-utils.ts # Mention parsing
│ ├── settings-utils.ts # Settings validation
│ ├── logger.ts # Debug logging
│ ├── path-utils.ts # Path resolution
│ └── wsl-utils.ts # WSL support
├── plugin.ts # Obsidian plugin entry point
└── main.ts # Re-exports plugin
├── plugin.ts # Obsidian plugin lifecycle, commands, view management
└── main.ts # Entry point (re-exports plugin)
```
## Architectural Layers
### 1. Domain Layer (`src/domain/`)
### 1. Types Layer (`src/types/`)
**Purpose**: Pure types and interfaces, zero external dependencies.
**Purpose**: Pure type definitions. No logic, no dependencies.
#### Models (`src/domain/models/`)
- `ChatMessage`: Message structure with content types
- `ChatSession`: Session state, available commands
- `AgentError`: Error types
- `AgentConfig`: Agent configuration
#### Ports (`src/domain/ports/`)
- `IAgentClient`: Agent communication interface
- `IVaultAccess`: File system access interface
- `ISettingsAccess`: Settings management interface
**Dependency Rule**: Zero dependencies on other layers.
| File | Contents |
|------|----------|
| `chat.ts` | ChatMessage, MessageContent (9-type union), Role, ToolCallStatus, ToolKind, AttachedFile, PromptContent |
| `session.ts` | ChatSession, SessionState, SessionUpdate (11-type union), SessionConfigOption, Capabilities, SessionInfo |
| `agent.ts` | AgentEnvVar, BaseAgentSettings, ClaudeAgentSettings, GeminiAgentSettings, CodexAgentSettings |
| `errors.ts` | AcpErrorCode, AcpError, ProcessError, ErrorInfo |
---
### 2. Hooks Layer (`src/hooks/`)
### 2. ACP Layer (`src/acp/`)
**Purpose**: State management and business logic using React hooks.
| Hook | Responsibility |
|------|---------------|
| `useAgentSession` | Session lifecycle, create/close/restart, agent switching |
| `useChat` | Messages state, send/receive, callbacks for AcpAdapter |
| `usePermission` | Permission request handling, auto-approve logic |
| `useMentions` | @[[note]] dropdown state, selection |
| `useSlashCommands` | /command dropdown state, filtering |
| `useAutoMention` | Auto-prepend active note to messages |
| `useAutoExport` | Export chat on new/close |
| `useSettings` | Settings subscription with useSyncExternalStore |
**Dependency Rule**: Hooks depend on Domain (ports/models) and Shared utilities.
---
### 3. Adapters Layer (`src/adapters/`)
**Purpose**: Implement ports, bridge external systems to domain.
#### ACP Adapters (`src/adapters/acp/`)
- `acp.adapter.ts`: Implements `IAgentClient`, manages agent process
- `acp-type-converter.ts`: Converts ACP protocol types to domain types
#### Obsidian Adapters (`src/adapters/obsidian/`)
- `vault.adapter.ts`: Implements `IVaultAccess`
- `settings-store.adapter.ts`: Implements `ISettingsAccess` with observer pattern
- `mention-service.ts`: File indexing, fuzzy search
**Dependency Rule**: Adapters depend on Domain only.
---
### 4. Components Layer (`src/components/`)
**Purpose**: UI rendering, receives state from hooks.
#### ChatView.tsx
- **Hook Composition**: Combines all hooks
- **Adapter Instantiation**: Creates AcpAdapter, VaultAdapter via useMemo
- **Rendering**: Delegates to child components
#### Child Components
- Receive data via props
- Call callbacks from hooks
- No direct business logic
**Dependency Rule**: Components depend on Hooks and Adapters.
---
### 5. Shared Layer (`src/shared/`)
**Purpose**: Cross-cutting utilities.
**Purpose**: Isolate ACP protocol dependency. All `@agentclientprotocol/sdk` imports are confined here.
| File | Purpose |
|------|---------|
| `message-service.ts` | Pure functions: prepareMessage, sendPreparedMessage |
| `terminal-manager.ts` | Process spawn, stdout/stderr capture |
| `chat-exporter.ts` | Export chat to markdown |
| `mention-utils.ts` | Parse @[[note]] syntax |
| `settings-utils.ts` | Validate and normalize settings |
| `logger.ts` | Debug logging (respects debugMode) |
| `acp-client.ts` | Process spawn/kill, JSON-RPC communication, session management. Defines `IAgentClient` and `ITerminalClient` interfaces. |
| `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 |
| `terminal-handler.ts` | Terminal process create/output/kill, stdout/stderr buffering |
**Dependency Rule**: Can be used by any layer.
**Key design**: When the ACP protocol changes, only files in this directory need updating.
---
### 3. Services Layer (`src/services/`)
**Purpose**: Non-React business logic. Classes and pure functions.
| File | Purpose |
|------|---------|
| `vault-service.ts` | `VaultService` class — vault note access, fuzzy search, CM6 selection tracking. Exports `IVaultAccess`, `NoteMetadata`, `EditorPosition`. |
| `settings-service.ts` | `SettingsService` class — settings persistence (data.json), session storage (sessions/*.json), observer pattern. Exports `ISettingsAccess`. |
| `session-helpers.ts` | Pure functions — agent config building, API key injection, agent settings resolution |
| `message-sender.ts` | Pure functions — prompt preparation (mention expansion, context building), 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. Each hook owns a specific domain of state.
| Hook | Responsibility |
|------|---------------|
| `useSession` | Session lifecycle (create/close/restart), mode/model/configOption, optimistic updates |
| `useMessages` | Message state, streaming updates (agent_message_chunk, tool_call, etc.), send/receive |
| `usePermission` | Permission request detection, approve/reject |
| `useMentions` | @[[note]] dropdown + auto-mention active note tracking (merged) |
| `useSlashCommands` | /command dropdown filtering and selection |
| `useSessionHistory` | Session list/load/resume/fork, local session storage, 5-min cache |
| `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: AcpAdapter;
vaultService: VaultService;
settingsService: SettingsService;
}
```
**ChatPanel** is the central orchestrator:
- Calls all hooks (useSession, useMessages, usePermission, useMentions, useSlashCommands, useSessionHistory, useSettings)
- Manages session update routing (ACP → hooks)
- Handles workspace events, message persistence, cleanup
- 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
│ └── MessageBubble (per message)
│ ├── ToolCallBlock → PermissionBanner
│ ├── TerminalBlock
│ └── MarkdownRenderer
├── InputArea
│ ├── SuggestionPopup (mentions / commands)
│ ├── ErrorBanner
│ ├── AttachmentStrip
│ └── InputToolbar (mode/model/usage/send)
└── SessionHistoryModal (imperative, via ref)
```
---
### 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 |
---
@ -167,92 +205,96 @@ src/
```
┌─────────────────────────────────────────────────────────────┐
Components Layer
┌────────────────────────────────────────────────────────┐
│ ChatView.tsx (hook composition + rendering) │
│ ├─ ChatHeader, ChatMessages, ChatInput │
│ └─ MessageRenderer, ToolCallRenderer, etc. │
└────────────────────────────────────────────────────────┘
UI Layer
ChatView / FloatingChatView (Context Providers)
└── ChatPanel (hook composition + rendering)
├── ChatHeader, MessageList, InputArea
└── MessageBubble, ToolCallBlock, etc.
└─────────────────────────────┬───────────────────────────────┘
uses
calls hooks
┌─────────────────────────────┴───────────────────────────────┐
│ Hooks Layer │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ useAgentSession │ │ useChat │ │
│ │ usePermission │ │ useMentions │ │
│ │ useSlashCommands │ │ useAutoExport │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────┬───────────────────────────────┘
↓ uses ports
┌─────────────────────────────┴───────────────────────────────┐
│ Ports (Interfaces) │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ IAgentClient │ │ IVaultAccess │ │
│ │ ISettingsAccess │ └──────────────────┘ │
│ └─────────────────┘ │
└─────────────────────────────┬───────────────────────────────┘
↑ implements
┌─────────────────────────────┴───────────────────────────────┐
│ Adapters (Implementations) │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ AcpAdapter │ │ VaultAdapter │ │
│ │ SettingsStore │ │ MentionService │ │
│ └─────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────┴───────────────────────────────┐
│ Domain Models │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ ChatMessage │ │ ChatSession │ │
│ │ AgentError │ │ AgentConfig │ │
│ └─────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ useSession, useMessages, usePermission, useMentions, │
│ useSlashCommands, useSessionHistory, useSettings │
└───────────┬─────────────────────────────┬───────────────────┘
↓ calls ↓ reads types
┌───────────┴───────────┐ ┌─────────────┴───────────────────┐
│ Services Layer │ │ Types Layer │
│ VaultService │ │ chat.ts, session.ts, │
│ SettingsService │ │ agent.ts, errors.ts │
│ session-helpers │ └─────────────────────────────────┘
│ message-sender │
│ chat-exporter │
│ view-registry │
└───────────┬───────────┘
↓ communicates
┌───────────┴───────────┐
│ ACP Layer │
│ acp-client.ts │
│ type-converter.ts │
│ permission-handler │
│ terminal-handler │
└───────────────────────┘
@agentclientprotocol/sdk
```
---
## Design Patterns
### 1. Custom Hooks Pattern
- State and logic encapsulated in hooks
- Composable and reusable
- Testable in isolation
### 1. ChatPanel Pattern (Component-as-Orchestrator)
- Central component calls all hooks and renders children directly
- Replaces the "god hook" pattern (useChatController)
- No massive return object — props flow directly to JSX
### 2. Dependency Inversion (Ports & Adapters)
- Domain defines interfaces (ports)
- Adapters implement those interfaces
- ACP protocol changes isolated to adapters
### 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. Observer Pattern
- `SettingsStore` notifies subscribers on change
### 3. Custom Hooks for State
- Each hook owns a specific state domain
- Composable and independently testable
- No inter-hook dependencies (communication via ChatPanel)
### 4. ACP Isolation
- All `@agentclientprotocol/sdk` imports confined to `acp/`
- `type-converter.ts` is the change buffer for protocol updates
- Interface types (`IAgentClient`) defined alongside implementation
### 5. Observer Pattern
- `SettingsService` notifies subscribers on change
- React components use `useSyncExternalStore`
### 4. Pure Functions in Shared
- `message-service.ts`: prepareMessage, sendPreparedMessage
- No React dependencies, easy to test
### 6. Ref Pattern for Callbacks
- IChatViewContainer callbacks use refs for latest values
- Prevents stale closures in broadcast command handlers
- Unmount cleanup uses refs to access latest state
---
## Key Benefits
### 1. React-idiomatic
- Hooks for state management
- No ViewModel classes
- Standard React patterns
### 1. Flat and Readable
- 4 layers (types → acp/services → hooks → ui) instead of 5
- No port/adapter indirection
- File names reflect functionality (MessageBubble, ToolCallBlock, ErrorBanner)
### 2. ACP Change Resistance
- `IAgentClient` interface isolates protocol
- Only `adapters/acp/` needs changes for protocol updates
- Only `acp/` directory needs changes for protocol updates
- `type-converter.ts` localizes type mapping changes
### 3. Testability
- Hooks can be tested with React Testing Library
- Pure functions in shared/ easily unit tested
- Adapters mockable via ports
### 3. Easy Feature Addition
- New hook: create in `hooks/`, call in `ChatPanel`, pass to child
- New message type: add to `types/session.ts`, handle in `useMessages`, render in `MessageBubble`
- New agent: add settings in `plugin.ts`, configure in `SettingsTab`
### 4. Maintainability
- Clear file locations by responsibility
- Single Responsibility Principle
- ~9,100 lines across 45 files
- ~20,400 lines across 50 files
- Services testable without React
- Clear dependency direction (no circular dependencies)
---
@ -260,11 +302,12 @@ src/
| Pattern | Example |
|---------|---------|
| Ports | `*.port.ts` |
| Adapters | `*.adapter.ts` |
| Hooks | `use*.ts` |
| Components | `PascalCase.tsx` |
| Utilities | `kebab-case.ts` |
| 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/` |
---
@ -273,23 +316,31 @@ src/
### Adding a New Hook
1. Create `hooks/use[Feature].ts`
2. Define state with useState/useReducer
3. Export state and functions
4. Compose in ChatView.tsx
3. Call the hook in `ui/ChatPanel.tsx`
4. Pass state/callbacks to child components as props
### Adding a New Agent
1. Implement `IAgentClient` in `adapters/[agent]/`
2. Add settings to `plugin.ts`
3. Update AgentClientSettingTab
### Adding a New Session Update Type
1. Add interface to `types/session.ts`, add to `SessionUpdate` union
2. Handle in `hooks/useMessages.ts` `handleSessionUpdate()` (for message-level) or `hooks/useSession.ts` (for session-level)
3. Convert from ACP type in `acp/type-converter.ts`
4. Render in `ui/MessageBubble.tsx` if needed
**No changes needed** in hooks or components!
### 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
Refactored from Clean Architecture (MVVM) to React Hooks Architecture in November 2025:
### March 2026: Simplified Architecture Refactoring
- **Removed**: `core/use-cases/`, `adapters/view-models/`, `infrastructure/`
- **Added**: `hooks/` layer with 8 custom hooks
- **Moved**: `plugin.ts` to src root
- **Result**: Simpler, more React-idiomatic codebase
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

254
CLAUDE.md
View file

@ -1,7 +1,7 @@
# Agent Client Plugin - LLM Developer Guide
## Overview
Obsidian plugin for AI agent interaction (Claude Code, Gemini CLI, custom agents). **React Hooks Architecture**.
Obsidian plugin for AI agent interaction (Claude Code, Codex, Gemini CLI, custom agents). **ChatPanel + React Hooks Architecture**.
**Tech**: React 19, TypeScript, Obsidian API, Agent Client Protocol (ACP)
@ -9,127 +9,164 @@ Obsidian plugin for AI agent interaction (Claude Code, Gemini CLI, custom agents
```
src/
├── domain/ # Pure domain models + ports (interfaces)
│ ├── models/ # agent-config, agent-error, chat-message, chat-session, session-update
│ └── ports/ # IAgentClient, ISettingsAccess, IVaultAccess
├── adapters/ # Interface implementations
│ ├── acp/ # ACP protocol (acp.adapter.ts, acp-type-converter.ts)
│ └── obsidian/ # Platform adapters (vault, settings, mention-service)
├── types/ # Type definitions (no logic, no dependencies)
│ ├── chat.ts # ChatMessage, MessageContent, PromptContent, AttachedFile
│ ├── session.ts # ChatSession, SessionUpdate, SessionInfo, Capabilities
│ ├── agent.ts # AgentConfig, agent settings (Claude/Codex/Gemini/Custom)
│ └── errors.ts # AcpError, ProcessError, ErrorInfo
├── acp/ # ACP protocol (SDK dependency confined here)
│ ├── acp-client.ts # Process lifecycle, JSON-RPC, IAgentClient + ITerminalClient
│ ├── type-converter.ts # ACP SDK ↔ internal type conversion
│ ├── permission-handler.ts # Permission queue, auto-approve, Promise resolution
│ └── terminal-handler.ts # Terminal process create/output/kill
├── services/ # Business logic (non-React)
│ ├── vault-service.ts # Vault access + fuzzy search + CM6 selection tracking
│ ├── settings-service.ts # Settings persistence, session storage, normalization
│ ├── session-helpers.ts # Agent config building, API key injection (pure functions)
│ ├── 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)
│ ├── useAgentSession.ts # Session lifecycle, agent switching
│ ├── useChat.ts # Message sending, session update handling
│ ├── useSession.ts # Session lifecycle, config options, optimistic updates
│ ├── useMessages.ts # Message state, streaming updates, send/receive
│ ├── usePermission.ts # Permission handling
│ ├── useMentions.ts # @[[note]] suggestions
│ ├── useMentions.ts # @[[note]] suggestions + auto-mention active note
│ ├── useSlashCommands.ts # /command suggestions
│ ├── useAutoMention.ts # Auto-mention active note
│ ├── useAutoExport.ts # Auto-export on new/close
│ ├── useSessionHistory.ts # Session list/load/resume/fork
│ └── useSettings.ts # Settings subscription
├── components/ # UI components
│ ├── chat/ # ChatView, ChatHeader, ChatMessages, ChatInput, etc.
│ └── settings/ # AgentClientSettingTab
├── shared/ # Utilities
│ ├── message-service.ts # prepareMessage, sendPreparedMessage (pure functions)
│ ├── terminal-manager.ts # Process spawn, stdout/stderr capture
│ ├── logger.ts, chat-exporter.ts, mention-utils.ts, etc.
├── ui/ # React components
│ ├── ChatContext.ts # React Context (plugin, acpClient, vaultService)
│ ├── ChatPanel.tsx # Hook aggregation + rendering (core orchestrator)
│ ├── ChatView.tsx # Sidebar view (ItemView wrapper)
│ ├── FloatingChatView.tsx # Floating window (position/drag/resize)
│ ├── ChatHeader.tsx # Header (sidebar + floating variants)
│ ├── MessageList.tsx # Message list with auto-scroll
│ ├── MessageBubble.tsx # Single message rendering (content dispatch)
│ ├── ToolCallBlock.tsx # Tool call + diff display
│ ├── TerminalBlock.tsx # Terminal output polling
│ ├── InputArea.tsx # Textarea, attachments, mentions, history
│ ├── InputToolbar.tsx # Mode/model selectors, usage, send button
│ └── ... # SuggestionPopup, PermissionBanner, ErrorBanner, etc.
├── utils/ # Shared utilities
│ ├── 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, settings persistence
└── main.ts # Entry point
```
## Key Components
### ChatView (`components/chat/ChatView.tsx`)
- **Hook Composition**: Combines all hooks (useAgentSession, useChat, usePermission, etc.)
- **Adapter Instantiation**: Creates AcpAdapter, VaultAdapter, MentionService via useMemo
- **Callback Registration**: Registers `onSessionUpdate` for unified event handling
- **Rendering**: Delegates to ChatHeader, ChatMessages, ChatInput
### ChatPanel (`ui/ChatPanel.tsx`)
Central orchestrator component. Replaces the former `useChatController` hook.
- **Hook Composition**: Calls all hooks (useSession, useMessages, usePermission, etc.)
- **Session Update Routing**: Routes ACP updates to useMessages (message-level) or useSession (session-level)
- **Callback Registration**: Provides IChatViewContainer callbacks via ref pattern
- **Workspace Events**: Handles toggle-auto-mention, new-chat-requested, approve/reject-permission, cancel, export
- **Rendering**: Renders ChatHeader, MessageList, InputArea directly (no prop bag)
### ChatView / FloatingChatView (`ui/ChatView.tsx`, `ui/FloatingChatView.tsx`)
Thin wrappers that:
- Create services (AcpClient, VaultService) in lifecycle methods
- Provide ChatContext (plugin, acpClient, vaultService, settingsService)
- Render `<ChatPanel variant="sidebar" | "floating" />`
- Implement IChatViewContainer for broadcast commands
### Hooks (`hooks/`)
**useAgentSession**: Session lifecycle
- `createSession()`: Load config, inject API keys, initialize + newSession
- `switchAgent()`: Change active agent, restart session
**useSession**: Session lifecycle
- `createSession()`: Build config, inject API keys, initialize + newSession
- `restartSession()`: Change active agent, restart session
- `closeSession()`: Cancel session, disconnect
- `updateAvailableCommands()`: Handle slash command updates
- `updateCurrentMode()`: Handle mode change updates
- `setConfigOption()`: Optimistic update + rollback on error
- `handleSessionUpdate()`: Handle session-level updates (commands, config, usage)
**useChat**: Messaging and session update handling
- `sendMessage()`: Prepare (auto-mention, path conversion) → send via IAgentClient
- `handleNewChat()`: Export if enabled, restart session
- `handleSessionUpdate()`: Unified handler for all session updates (agent_message_chunk, tool_call, etc.)
**useMessages**: Messaging and streaming updates
- `sendMessage()`: Prepare (auto-mention, path conversion) → send via AcpClient
- `handleSessionUpdate()`: Handle message-level updates (agent_message_chunk, tool_call, etc.)
- `upsertToolCall()`: Create or update tool call in single `setMessages` callback (avoids race conditions)
- `updateLastMessage()`: Append text/thought chunks to last assistant message
- `updateMessage()`: Update specific message by tool call ID
**usePermission**: Permission handling
- `handlePermissionResponse()`: Respond with selected option
- `approvePermission()` / `rejectPermission()`: Respond with selected option
- Auto-approve logic based on settings
**useMentions / useSlashCommands**: Input suggestions
- Dropdown state management
- Selection handlers
**useMentions**: @mention + auto-mention (merged)
- Dropdown state management + selection
- Active note tracking + toggle for slash commands
### AcpAdapter (`adapters/acp/acp.adapter.ts`)
Implements IAgentClient + IAcpClient (terminal ops)
**useSessionHistory**: Session persistence
- `restoreSession()`: Load/resume with local message fallback
- `forkSession()`: Create new branch from existing session
### ACP Client (`acp/acp-client.ts`)
Implements IAgentClient + ITerminalClient
- **Process**: spawn() with login shell (macOS/Linux -l, Windows shell:true)
- **Protocol**: JSON-RPC over stdin/stdout via ndJsonStream
- **Flow**: initialize() → newSession() → sendMessage() → sessionUpdate via `onSessionUpdate`
- **Updates**: agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update, plan, available_commands_update, current_mode_update
- **Unified Callback**: Single `onSessionUpdate(callback)` replaces legacy `onMessage`, `onError`, `onPermissionRequest`
- **Permissions**: Promise-based Map<requestId, resolver>
- **Terminal**: createTerminal, terminalOutput, killTerminal, releaseTerminal
- **Flow**: initialize() → newSession() → sendPrompt() → sessionUpdate via `onSessionUpdate`
- **Updates**: agent_message_chunk, agent_thought_chunk, user_message_chunk, tool_call, tool_call_update, plan, available_commands_update, current_mode_update, session_info_update, usage_update, config_option_update
- **Unified Callback**: Single `onSessionUpdate(callback)` for all agent events
- **Permissions**: Promise-based via PermissionHandler
- **Terminal**: TerminalHandler manages create/output/kill/release
### Obsidian Adapters (`adapters/obsidian/`)
### Services (`services/`)
**VaultAdapter**: IVaultAccess - searchNotes (fuzzy), getActiveNote, readNote
**SettingsStore**: ISettingsAccess - Observer pattern, getSnapshot(), subscribe()
**MentionService**: File index, fuzzy search (basename, path, aliases)
**VaultService**: Vault access + file index + fuzzy search + CM6 selection tracking
**SettingsService**: Observer pattern settings store, session storage (data.json + sessions/*.json)
**session-helpers**: Pure functions — buildAgentConfigWithApiKey, findAgentSettings, getAvailableAgents
**message-sender**: Pure functions — preparePrompt (auto-mention, context), sendPreparedPrompt (auth retry)
### Message Service (`shared/message-service.ts`)
Pure functions (non-React):
- `prepareMessage()`: Auto-mention, convert @[[note]] → paths
- `sendPreparedMessage()`: Send via IAgentClient, auth retry
## Types
## Domain Models
### SessionUpdate (`domain/models/session-update.ts`)
### SessionUpdate (`types/session.ts`)
Union type for all session update events from the agent:
```typescript
type SessionUpdate =
| AgentMessageChunkUpdate // Text chunk from agent's response
| AgentThoughtChunkUpdate // Text chunk from agent's reasoning
| ToolCallUpdate // New tool call event
| ToolCallUpdateUpdate // Update to existing tool call
| PlanUpdate // Agent's task plan
| AvailableCommandsUpdate // Slash commands changed
| CurrentModeUpdate // Mode changed
| ErrorUpdate; // Error from agent operations
| AgentMessageChunk // Text chunk from agent's response
| AgentThoughtChunk // Text chunk from agent's reasoning
| UserMessageChunk // Text chunk from user message (session/load)
| ToolCall // New tool call event
| ToolCallUpdate // Update to existing tool call
| Plan // Agent's task plan
| AvailableCommandsUpdate // Slash commands changed
| CurrentModeUpdate // Mode changed
| SessionInfoUpdate // Session metadata changed
| UsageUpdate // Context window usage
| ConfigOptionUpdate; // Config options changed
```
This domain type abstracts ACP's `SessionNotification.update.sessionUpdate` values, allowing the application layer to handle events without depending on ACP protocol specifics.
### Key Interfaces
## Ports (Interfaces)
Interfaces are defined in their implementation files (no separate port files):
```typescript
// acp/acp-client.ts
interface IAgentClient {
initialize(config: AgentConfig): Promise<InitializeResult>;
newSession(workingDirectory: string): Promise<NewSessionResult>;
newSession(workingDirectory: string): Promise<SessionResult>;
authenticate(methodId: string): Promise<boolean>;
sendMessage(sessionId: string, message: string): Promise<void>;
sendPrompt(sessionId: string, content: PromptContent[]): Promise<void>;
cancel(sessionId: string): Promise<void>;
disconnect(): Promise<void>;
// Unified callback for all session updates
onSessionUpdate(callback: (update: SessionUpdate) => void): void;
onError(callback: (error: ProcessError) => void): void;
respondToPermission(requestId: string, optionId: string): Promise<void>;
isInitialized(): boolean;
getCurrentAgentId(): string | null;
setSessionMode(sessionId: string, modeId: string): Promise<void>;
setSessionModel(sessionId: string, modelId: string): Promise<void>;
setSessionConfigOption(sessionId: string, configId: string, value: string): Promise<SessionConfigOption[]>;
listSessions(cwd?: string, cursor?: string): Promise<ListSessionsResult>;
loadSession(sessionId: string, cwd: string): Promise<SessionResult>;
resumeSession(sessionId: string, cwd: string): Promise<SessionResult>;
forkSession(sessionId: string, cwd: string): Promise<SessionResult>;
}
// services/vault-service.ts
interface IVaultAccess {
readNote(path: string): Promise<string>;
searchNotes(query: string): Promise<NoteMetadata[]>;
@ -137,21 +174,29 @@ interface IVaultAccess {
listNotes(): Promise<NoteMetadata[]>;
}
// services/settings-service.ts
interface ISettingsAccess {
getSnapshot(): AgentClientPluginSettings;
updateSettings(updates: Partial<AgentClientPluginSettings>): Promise<void>;
subscribe(listener: () => void): () => void;
saveSession(info: SavedSessionInfo): Promise<void>;
getSavedSessions(agentId?: string, cwd?: string): SavedSessionInfo[];
deleteSession(sessionId: string): Promise<void>;
saveSessionMessages(sessionId: string, agentId: string, messages: ChatMessage[]): Promise<void>;
loadSessionMessages(sessionId: string): Promise<ChatMessage[] | null>;
deleteSessionMessages(sessionId: string): Promise<void>;
}
```
## Development Rules
### Architecture
1. **Hooks for state + logic**: No ViewModel, no Use Cases classes
2. **Pure functions in shared/**: Non-React business logic
3. **Ports for ACP resistance**: IAgentClient interface isolates protocol changes
4. **Domain has zero deps**: No `obsidian`, `@agentclientprotocol/sdk`
5. **Unified callbacks**: Use `onSessionUpdate` for all agent events (not multiple callbacks)
1. **ChatPanel as orchestrator**: All hooks called in ChatPanel, renders children directly
2. **Services for non-React logic**: Pure functions and classes in `services/`
3. **ACP isolation**: All `@agentclientprotocol/sdk` imports confined to `acp/`
4. **Types have zero deps**: No `obsidian`, no SDK, no React in `types/`
5. **Unified callbacks**: Use `onSessionUpdate` for all agent events
6. **Context for services**: plugin, acpClient, vaultService via ChatContext
### Obsidian Plugin Review (CRITICAL)
1. No innerHTML/outerHTML - use createEl/createDiv/createSpan
@ -161,19 +206,22 @@ interface ISettingsAccess {
5. Minimize `any` - use proper types
### Naming Conventions
- Ports: `*.port.ts`
- Adapters: `*.adapter.ts`
- Hooks: `use*.ts`
- Components: `PascalCase.tsx`
- Utils/Models: `kebab-case.ts`
- 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/`
- Utils: `kebab-case.ts` in `utils/`
### Code Patterns
1. React hooks for state management
2. useCallback/useMemo for performance
3. useRef for cleanup function access
3. useRef for cleanup function access and stale closure prevention
4. Error handling: try-catch async ops
5. Logging: Logger class (respects debugMode)
6. **Upsert pattern**: Use `setMessages` functional updates to avoid race conditions with tool_call updates
7. **Ref pattern for callbacks**: IChatViewContainer callbacks use refs for latest values
8. **Context value stability**: ChatContext value created once (service instances), wrapped in useMemo
## Common Tasks
@ -181,46 +229,50 @@ interface ISettingsAccess {
1. Create `hooks/use[Feature].ts`
2. Define state with useState/useReducer
3. Export functions and state
4. Compose in ChatView.tsx
4. Call the hook in `ui/ChatPanel.tsx`
5. Pass state/callbacks to child components as props
### Add Agent Type
1. **Optional**: Define config in `domain/models/agent-config.ts`
2. **Adapter**: Implement IAgentClient in `adapters/[agent]/[agent].adapter.ts`
3. **Settings**: Add to AgentClientPluginSettings in plugin.ts
4. **UI**: Update AgentClientSettingTab
1. Add settings type in `types/agent.ts`
2. Add config and defaults in `plugin.ts`
3. Add API key injection in `services/session-helpers.ts`
4. Update `ui/SettingsTab.ts` for configuration UI
### Modify Message Types
1. Update `ChatMessage`/`MessageContent` in `domain/models/chat-message.ts`
1. Update `ChatMessage`/`MessageContent` in `types/chat.ts`
2. If adding new session update type:
- Add to `SessionUpdate` union in `domain/models/session-update.ts`
- Handle in `useChat.handleSessionUpdate()`
3. Update `AcpAdapter.sessionUpdate()` to emit the new type
4. Update `MessageContentRenderer` to render new type
- Add to `SessionUpdate` union in `types/session.ts`
- Handle in `hooks/useMessages.ts` `handleSessionUpdate()`
3. Update `acp/acp-client.ts` `sessionUpdate()` to emit the new type
4. Update `ui/MessageBubble.tsx` `ContentBlock` to render new type
### Add New Session Update Type
1. Define interface in `domain/models/session-update.ts`
1. Define interface in `types/session.ts`
2. Add to `SessionUpdate` union type
3. Handle in `useChat.handleSessionUpdate()` (for message-level updates)
4. Or handle in `ChatView` (for session-level updates like `available_commands_update`)
3. Handle in `hooks/useMessages.ts` `handleSessionUpdate()` (for message-level)
4. Or handle in `hooks/useSession.ts` `handleSessionUpdate()` (for session-level)
5. Route in `ui/ChatPanel.tsx` session update effect
### Debug
1. Settings → Developer Settings → Debug Mode ON
2. Open DevTools (Cmd+Option+I / Ctrl+Shift+I)
3. Filter logs: `[AcpAdapter]`, `[useChat]`, `[NoteMentionService]`
3. Filter logs: `[AcpAdapter]`, `[useMessages]`, `[VaultService]`, `[ChatPanel]`
## ACP Protocol
**Communication**: JSON-RPC 2.0 over stdin/stdout
**Methods**: initialize, newSession, authenticate, prompt, cancel, setSessionMode, setSessionModel
**Notifications**: session/update (agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update, plan, available_commands_update, current_mode_update)
**Methods**: initialize, newSession, authenticate, prompt, cancel, setSessionConfigOption
**Notifications**: session/update (agent_message_chunk, agent_thought_chunk, user_message_chunk, tool_call, tool_call_update, plan, available_commands_update, current_mode_update, session_info_update, usage_update, config_option_update)
**Requests**: requestPermission
**Session Management** (unstable): session/list, session/load, session/resume, session/fork
**Agents**:
- Claude Code: `@zed-industries/claude-agent-acp` (ANTHROPIC_API_KEY)
- Gemini CLI: `@anthropics/gemini-cli-acp` (GOOGLE_API_KEY)
- Codex: `@zed-industries/codex-acp` (OPENAI_API_KEY)
- Gemini CLI: `@anthropics/gemini-cli-acp` (GEMINI_API_KEY)
- Custom: Any ACP-compatible agent
---
**Last Updated**: December 2025 | **Architecture**: React Hooks | **Version**: 0.4.0
**Last Updated**: March 2026 | **Architecture**: ChatPanel + React Hooks | **Version**: 0.9.1