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