From 23b397a94e2d1978759237bc1c6b60e0321bc3db Mon Sep 17 00:00:00 2001
From: RAIT-09 <51452399+RAIT-09@users.noreply.github.com>
Date: Fri, 3 Apr 2026 17:37:52 +0900
Subject: [PATCH] docs: update AGENTS.md to reflect current architecture after
refactoring
---
AGENTS.md | 249 ++++++++++++++++++++++++++++++++++--------------------
1 file changed, 159 insertions(+), 90 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 0c43c5e..f29140d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,65 +9,99 @@ Obsidian plugin for AI agent interaction (Claude Code, Codex, Gemini CLI, custom
```
src/
-├── 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, UI-facing API (AcpClient class)
-│ ├── acp-handler.ts # SDK event handler (sessionUpdate, permissions, terminals)
-│ ├── 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)
-│ ├── useSession.ts # Session lifecycle, config options, optimistic updates
-│ ├── useMessages.ts # Message state, streaming updates, send/receive
-│ ├── usePermission.ts # Permission handling
-│ ├── useMentions.ts # @[[note]] suggestions + auto-mention active note
-│ ├── useSlashCommands.ts # /command suggestions
-│ ├── useSessionHistory.ts # Session list/load/resume/fork
-│ └── useSettings.ts # Settings subscription
-├── 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
+├── 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/Codex/Gemini/Custom)
+│ └── errors.ts # AcpError, ProcessError, ErrorInfo
+├── acp/ # ACP protocol (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 ↔ 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, 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
+│ ├── 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 wrapper)
+│ ├── FloatingChatView.tsx # Floating window (position/drag/resize)
+│ ├── ChatHeader.tsx # Header (sidebar + floating variants)
+│ ├── MessageList.tsx # Virtualized message list (@tanstack/react-virtual)
+│ ├── MessageBubble.tsx # Single message rendering (content dispatch, copy button)
+│ ├── ToolCallBlock.tsx # Tool call + diff display (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)
+│ ├── FloatingButton.tsx # Draggable launch button
+│ ├── 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, settings persistence
+└── main.ts # Entry point
+```
+
+## Data Flow
+
+### ACP Event Flow (single path)
+```
+Agent Process → ACP SDK → AcpHandler (sessionId filter) → listeners broadcast
+ → useAgentSession (session-level: commands, mode, config, usage, error)
+ → useAgentMessages (message-level: text chunks, tool calls, plan)
+ → useAgent (facade, 1 onSessionUpdate subscription)
+```
+
+All events flow through a single `onSessionUpdate` channel. No special paths for permissions or errors.
+
+### Permission Flow
+```
+Agent requestPermission → PermissionManager.request() → onSessionUpdate (tool_call)
+User clicks approve/reject → PermissionManager.respond() → onSessionUpdate (tool_call_update)
```
## Key Components
### 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)
+Central orchestrator component.
+- **Hook Composition**: Calls useAgent, useSuggestions, useSessionHistory, useChatActions, useHistoryModal, useSettings
+- **Workspace Events**: Handles hotkeys via ref pattern (stable event registration)
+- **Callback Registration**: IChatViewContainer callbacks via refs
+- **Rendering**: Renders ChatHeader, MessageList, InputArea directly
+
+ChatPanel does NOT route session updates — that's handled internally by useAgent.
### ChatView / FloatingChatView (`ui/ChatView.tsx`, `ui/FloatingChatView.tsx`)
Thin wrappers that:
@@ -76,55 +110,76 @@ Thin wrappers that:
- Render ``
- Implement IChatViewContainer for broadcast commands
+FloatingChatView uses `onRegisterExpanded` callback (not CustomEvent) for expand/collapse.
+
### Hooks (`hooks/`)
-**useSession**: Session lifecycle
+**useAgent** (facade): Comp훈oses useAgentSession + useAgentMessages
+- Single `onSessionUpdate` subscription
+- Unified `handleSessionUpdate` dispatches to both sub-hooks
+- Return is `useMemo`-wrapped for referential stability
+
+**useAgentSession**: Session lifecycle + config
- `createSession()`: Build config, inject API keys, initialize + newSession
-- `restartSession()`: Change active agent, restart session
-- `closeSession()`: Cancel session, disconnect
- `setConfigOption()`: Optimistic update + rollback on error
-- `handleSessionUpdate()`: Handle session-level updates (commands, config, usage)
+- `setMode()` / `setModel()`: Legacy API (deprecated, still used by many agents)
+- Session-level update handler (commands, mode, config, usage, process_error)
+- Uses `sessionRef` pattern to stabilize callback deps
-**useMessages**: Messaging and streaming updates
+**useAgentMessages**: Messaging + streaming + permissions
- `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
+- RAF batching: streaming updates accumulated per-frame via `requestAnimationFrame`
+- Tool call index: `Map` for O(1) upsert
+- `ignoreUpdatesRef`: suppresses history replay during session/load
+- Permission: `activePermission` (useMemo derivation), approve/reject callbacks
-**usePermission**: Permission handling
-- `approvePermission()` / `rejectPermission()`: Respond with selected option
-- Auto-approve logic based on settings
+**useSuggestions**: @mention + /command (unified)
+- Mention detection, note searching, dropdown interaction
+- Slash command filtering and selection
+- Auto-mention toggle coordination (slash commands disable auto-mention)
+- Return is `useMemo`-wrapped (mentions + commands objects)
-**useMentions**: @mention + auto-mention (merged)
-- Dropdown state management + selection
-- Active note tracking + toggle for slash commands
+**useChatActions**: Business callbacks
+- handleSendMessage, handleNewChat, handleExportChat, handleRestartAgent, etc.
+- Uses individual method deps (not whole agent object) for stability
+- Owns restoredMessage and agentUpdateNotification state
**useSessionHistory**: Session persistence
- `restoreSession()`: Load/resume with local message fallback
- `forkSession()`: Create new branch from existing session
+- 5-minute cache with invalidation
+- Return is `useMemo`-wrapped
+
+**useHistoryModal**: Modal lifecycle
+- Lazy modal creation, props synchronization
+- Session operation callbacks (restore, fork, delete)
### ACP Client (`acp/acp-client.ts`) + ACP Handler (`acp/acp-handler.ts`)
-Two classes with distinct roles:
**AcpClient** — UI-facing API and process lifecycle:
- spawn() with login shell, JSON-RPC via ndJsonStream
- initialize() → newSession() → sendPrompt() → cancel() → disconnect()
- Session management: listSessions, loadSession, resumeSession, forkSession
- Owns PermissionManager, TerminalManager, AcpHandler
+- `currentSessionId` set before `await` in loadSession/resumeSession to prevent replay filtering
+- Single exit point: `onSessionUpdate` (multiple listeners via Set)
-**AcpHandler** — SDK event receiver (called by ClientSideConnection):
-- sessionUpdate: converts ACP types → domain types → callback
+**AcpHandler** — SDK event receiver:
+- sessionUpdate: converts ACP types → domain types → broadcast to listeners
+- sessionId filter: only emits updates matching `currentSessionId`
- requestPermission → PermissionManager
-- Terminal operations (create/output/kill/release) → TerminalManager
-- Extension notifications (ignored gracefully)
+- Terminal operations → TerminalManager
### Services (`services/`)
**VaultService**: Vault access + file index + fuzzy search + CM6 selection tracking
-**SettingsService**: Observer pattern settings store, session storage (data.json + sessions/*.json)
+**SettingsService**: Reactive settings store (observer pattern for useSyncExternalStore). Session storage delegated to SessionStorage.
+**SessionStorage**: Session metadata CRUD (in plugin settings) + message file I/O (sessions/*.json)
+**settings-normalizer**: Validation helpers (str, bool, num, enumVal, obj, strRecord, xyPoint) + toAgentConfig + parseChatFontSize
**session-helpers**: Pure functions — buildAgentConfigWithApiKey, findAgentSettings, getAvailableAgents
-**message-sender**: Pure functions — preparePrompt (auto-mention, context), sendPreparedPrompt (auth retry)
+**session-state**: Pure functions — applyLegacyValue, tryRestoreConfigOption, restoreLegacyConfig
+**message-state**: Pure functions — applySingleUpdate, applyUpsertToolCall, mergeToolCallContent, findActivePermission, selectOption
+**message-sender**: Pure functions — preparePrompt (embedded context vs XML text, shared helpers), sendPreparedPrompt (auth retry)
## Types
@@ -143,7 +198,8 @@ type SessionUpdate =
| CurrentModeUpdate // Mode changed
| SessionInfoUpdate // Session metadata changed
| UsageUpdate // Context window usage
- | ConfigOptionUpdate; // Config options changed
+ | ConfigOptionUpdate // Config options changed
+ | ProcessErrorUpdate; // Process-level error (spawn failure, command not found)
```
### Key Interfaces
@@ -162,6 +218,7 @@ interface ISettingsAccess {
getSnapshot(): AgentClientPluginSettings;
updateSettings(updates: Partial): Promise;
subscribe(listener: () => void): () => void;
+ // Session storage methods (delegated to SessionStorage internally)
saveSession(info: SavedSessionInfo): Promise;
getSavedSessions(agentId?: string, cwd?: string): SavedSessionInfo[];
deleteSession(sessionId: string): Promise;
@@ -174,12 +231,22 @@ interface ISettingsAccess {
## Development Rules
### Architecture
-1. **ChatPanel as orchestrator**: All hooks called in ChatPanel, renders children directly
-2. **Services for non-React logic**: Pure functions and classes in `services/`
+1. **useAgent as facade**: Composes useAgentSession + useAgentMessages. ChatPanel calls useAgent, not sub-hooks directly.
+2. **Services have zero React imports**: Pure functions and classes in `services/`. No useState, useCallback, React.Dispatch, etc.
3. **ACP isolation**: All `@agentclientprotocol/sdk` imports confined to `acp/`. AcpClient is UI-facing, AcpHandler is SDK-facing.
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
+5. **Single event channel**: All agent events (messages, session updates, permissions, errors) flow through `onSessionUpdate`. No special callback paths.
+6. **Context for services**: plugin, acpClient, vaultService, settingsService via ChatContext
+
+### Performance Patterns
+1. **useMemo for return stability**: useAgent, useSuggestions, useSessionHistory wrap return objects in useMemo to prevent cascading re-renders
+2. **sessionRef pattern**: useAgentSession stores session in useRef for callback access without adding session to deps
+3. **Individual method deps**: useChatActions uses `agent.sendMessage` not `agent` as deps — prevents callback recreation when unrelated state changes
+4. **Workspace event refs**: ChatPanel stores event handler callbacks in refs, keeping useEffect deps minimal
+5. **RAF batching**: useAgentMessages batches streaming updates per animation frame (~60fps) instead of per-chunk
+6. **React.memo**: MessageBubble, ToolCallBlock, TerminalBlock wrapped for skip-render optimization
+7. **Virtual scroll**: MessageList uses @tanstack/react-virtual for large conversations
+8. **O(1) tool call index**: Map for tool call upsert without linear scan
### Obsidian Plugin Review (CRITICAL)
1. No innerHTML/outerHTML - use createEl/createDiv/createSpan
@@ -198,13 +265,14 @@ interface ISettingsAccess {
### Code Patterns
1. React hooks for state management
-2. useCallback/useMemo for performance
+2. useCallback/useMemo for performance (see Performance Patterns above)
3. useRef for cleanup function access and stale closure prevention
4. Error handling: try-catch async ops
-5. Logging: Logger class (respects debugMode)
+5. Logging: Logger class (respects debugMode). Avoid excessive per-keystroke logging.
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
+7. **Ref pattern for callbacks**: IChatViewContainer and workspace event handlers use refs for latest values
8. **Context value stability**: ChatContext value created once (service instances), wrapped in useMemo
+9. **Stable empty arrays**: Use module-level constants (e.g., `EMPTY_COMMANDS`) instead of inline `[]` in hook args
## Common Tasks
@@ -214,6 +282,7 @@ interface ISettingsAccess {
3. Export functions and state
4. Call the hook in `ui/ChatPanel.tsx`
5. Pass state/callbacks to child components as props
+6. Wrap return object in `useMemo` if passed as dependency to other hooks
### Add Agent Type
1. Add settings type in `types/agent.ts`
@@ -225,21 +294,21 @@ interface ISettingsAccess {
1. Update `ChatMessage`/`MessageContent` in `types/chat.ts`
2. If adding new session update type:
- Add to `SessionUpdate` union in `types/session.ts`
- - Handle in `hooks/useMessages.ts` `handleSessionUpdate()`
+ - Handle in `hooks/useAgentMessages.ts` (for message-level) or `hooks/useAgentSession.ts` (for session-level)
3. Update `acp/acp-handler.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 `types/session.ts`
2. Add to `SessionUpdate` union type
-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
+3. Handle in `hooks/useAgentSession.ts` `handleSessionUpdate()` (for session-level)
+4. Or handle via `applySingleUpdate()` in `services/message-state.ts` (for message-level)
+5. No routing needed in ChatPanel — useAgent handles dispatch internally
### Debug
1. Settings → Developer Settings → Debug Mode ON
2. Open DevTools (Cmd+Option+I / Ctrl+Shift+I)
-3. Filter logs: `[AcpClient]`, `[AcpHandler]`, `[useMessages]`, `[VaultService]`, `[ChatPanel]`
+3. Filter logs: `[AcpClient]`, `[AcpHandler]`, `[PermissionManager]`, `[VaultService]`
## ACP Protocol
@@ -258,4 +327,4 @@ interface ISettingsAccess {
---
-**Last Updated**: March 2026 | **Architecture**: ChatPanel + React Hooks | **Version**: 0.10.0-preview.1
+**Last Updated**: April 2026 | **Architecture**: useAgent facade + sub-hooks | **Version**: 0.10.0-preview.1