13 KiB
Agent Client Plugin - LLM Developer Guide
Overview
Obsidian plugin for AI agent interaction (Claude Code, Codex, Gemini CLI, custom agents) via ACP.
Tech: React 19, TypeScript, Obsidian API, Agent Client Protocol (ACP)
Architecture
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
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)
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/)
useSession: Session lifecycle
createSession(): Build config, inject API keys, initialize + newSessionrestartSession(): Change active agent, restart sessioncloseSession(): Cancel session, disconnectsetConfigOption(): Optimistic update + rollback on errorhandleSessionUpdate(): Handle session-level updates (commands, config, usage)
useMessages: Messaging and streaming updates
sendMessage(): Prepare (auto-mention, path conversion) → send via AcpClienthandleSessionUpdate(): Handle message-level updates (agent_message_chunk, tool_call, etc.)upsertToolCall(): Create or update tool call in singlesetMessagescallback (avoids race conditions)updateLastMessage(): Append text/thought chunks to last assistant messageupdateMessage(): Update specific message by tool call ID
usePermission: Permission handling
approvePermission()/rejectPermission(): Respond with selected option- Auto-approve logic based on settings
useMentions: @mention + auto-mention (merged)
- Dropdown state management + selection
- Active note tracking + toggle for slash commands
useSessionHistory: Session persistence
restoreSession(): Load/resume with local message fallbackforkSession(): Create new branch from existing session
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
AcpHandler — SDK event receiver (called by ClientSideConnection):
- sessionUpdate: converts ACP types → domain types → callback
- requestPermission → PermissionManager
- Terminal operations (create/output/kill/release) → TerminalManager
- Extension notifications (ignored gracefully)
Services (services/)
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)
Types
SessionUpdate (types/session.ts)
Union type for all session update events from the agent:
type SessionUpdate =
| 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
Key Interfaces
// services/vault-service.ts
interface IVaultAccess {
readNote(path: string): Promise<string>;
searchNotes(query: string): Promise<NoteMetadata[]>;
getActiveNote(): Promise<NoteMetadata | null>;
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
- ChatPanel as orchestrator: All hooks called in ChatPanel, renders children directly
- Services for non-React logic: Pure functions and classes in
services/ - ACP isolation: All
@agentclientprotocol/sdkimports confined toacp/. AcpClient is UI-facing, AcpHandler is SDK-facing. - Types have zero deps: No
obsidian, no SDK, no React intypes/ - Unified callbacks: Use
onSessionUpdatefor all agent events - Context for services: plugin, acpClient, vaultService via ChatContext
Obsidian Plugin Review (CRITICAL)
- No innerHTML/outerHTML - use createEl/createDiv/createSpan
- NO detach leaves in onunload (antipattern)
- Styles in CSS only - no JS style manipulation
- Use Platform interface - not process.platform
- Minimize
any- use proper types
Naming Conventions
- Types:
kebab-case.tsintypes/ - ACP:
kebab-case.tsinacp/ - Services:
kebab-case.tsinservices/ - Hooks:
use*.tsinhooks/ - Components:
PascalCase.tsxinui/ - Utils:
kebab-case.tsinutils/
Code Patterns
- React hooks for state management
- useCallback/useMemo for performance
- useRef for cleanup function access and stale closure prevention
- Error handling: try-catch async ops
- Logging: Logger class (respects debugMode)
- Upsert pattern: Use
setMessagesfunctional updates to avoid race conditions with tool_call updates - Ref pattern for callbacks: IChatViewContainer callbacks use refs for latest values
- Context value stability: ChatContext value created once (service instances), wrapped in useMemo
Common Tasks
Add New Feature Hook
- Create
hooks/use[Feature].ts - Define state with useState/useReducer
- Export functions and state
- Call the hook in
ui/ChatPanel.tsx - Pass state/callbacks to child components as props
Add Agent Type
- Add settings type in
types/agent.ts - Add config and defaults in
plugin.ts - Add API key injection in
services/session-helpers.ts - Update
ui/SettingsTab.tsfor configuration UI
Modify Message Types
- Update
ChatMessage/MessageContentintypes/chat.ts - If adding new session update type:
- Add to
SessionUpdateunion intypes/session.ts - Handle in
hooks/useMessages.tshandleSessionUpdate()
- Add to
- Update
acp/acp-handler.tssessionUpdate()to emit the new type - Update
ui/MessageBubble.tsxContentBlockto render new type
Add New Session Update Type
- Define interface in
types/session.ts - Add to
SessionUpdateunion type - Handle in
hooks/useMessages.tshandleSessionUpdate()(for message-level) - Or handle in
hooks/useSession.tshandleSessionUpdate()(for session-level) - Route in
ui/ChatPanel.tsxsession update effect
Debug
- Settings → Developer Settings → Debug Mode ON
- Open DevTools (Cmd+Option+I / Ctrl+Shift+I)
- Filter logs:
[AcpClient],[AcpHandler],[useMessages],[VaultService],[ChatPanel]
ACP Protocol
Communication: JSON-RPC 2.0 over stdin/stdout
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:
@agentclientprotocol/claude-agent-acp(ANTHROPIC_API_KEY) - Codex:
@zed-industries/codex-acp(OPENAI_API_KEY) - Gemini CLI:
@google/gemini-cli(GEMINI_API_KEY) - Custom: Any ACP-compatible agent
Last Updated: March 2026 | Architecture: ChatPanel + React Hooks | Version: 0.10.0-preview.1