From ffbfe333049f44c082abdc91ec016701900681e4 Mon Sep 17 00:00:00 2001 From: RAIT-09 <51452399+RAIT-09@users.noreply.github.com> Date: Sat, 21 Mar 2026 22:04:45 +0900 Subject: [PATCH] docs: update CLAUDE.md and ARCHITECTURE.md to reflect simplified architecture --- ARCHITECTURE.md | 477 +++++++++++++++++++++++++++--------------------- CLAUDE.md | 254 ++++++++++++++++---------- 2 files changed, 417 insertions(+), 314 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 041b269..b80b603 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index dce5163..9b88bd5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `` +- 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 -- **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; - newSession(workingDirectory: string): Promise; + newSession(workingDirectory: string): Promise; authenticate(methodId: string): Promise; - sendMessage(sessionId: string, message: string): Promise; + sendPrompt(sessionId: string, content: PromptContent[]): Promise; cancel(sessionId: string): Promise; disconnect(): Promise; - - // Unified callback for all session updates onSessionUpdate(callback: (update: SessionUpdate) => void): void; - + onError(callback: (error: ProcessError) => void): void; respondToPermission(requestId: string, optionId: string): Promise; isInitialized(): boolean; getCurrentAgentId(): string | null; - setSessionMode(sessionId: string, modeId: string): Promise; - setSessionModel(sessionId: string, modelId: string): Promise; + setSessionConfigOption(sessionId: string, configId: string, value: string): Promise; + listSessions(cwd?: string, cursor?: string): Promise; + loadSession(sessionId: string, cwd: string): Promise; + resumeSession(sessionId: string, cwd: string): Promise; + forkSession(sessionId: string, cwd: string): Promise; } +// services/vault-service.ts interface IVaultAccess { readNote(path: string): Promise; searchNotes(query: string): Promise; @@ -137,21 +174,29 @@ interface IVaultAccess { listNotes(): Promise; } +// services/settings-service.ts interface ISettingsAccess { getSnapshot(): AgentClientPluginSettings; updateSettings(updates: Partial): Promise; subscribe(listener: () => void): () => void; + saveSession(info: SavedSessionInfo): Promise; + getSavedSessions(agentId?: string, cwd?: string): SavedSessionInfo[]; + deleteSession(sessionId: string): Promise; + saveSessionMessages(sessionId: string, agentId: string, messages: ChatMessage[]): Promise; + loadSessionMessages(sessionId: string): Promise; + deleteSessionMessages(sessionId: string): Promise; } ``` ## 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