Skip to content

Commit cf8fc96

Browse files
authored
Merge pull request #27 from RAIT-09/RAIT-09/fix/tool-call-update-lost
fix: Prevent tool_call_update data loss with unified SessionUpdate callback
2 parents a2a5b79 + c15858f commit cf8fc96

File tree

14 files changed

+517
-399
lines changed

14 files changed

+517
-399
lines changed

CLAUDE.md

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ Obsidian plugin for AI agent interaction (Claude Code, Gemini CLI, custom agents
1010
```
1111
src/
1212
├── domain/ # Pure domain models + ports (interfaces)
13-
│ ├── models/ # agent-config, agent-error, chat-message, chat-session
13+
│ ├── models/ # agent-config, agent-error, chat-message, chat-session, session-update
1414
│ └── ports/ # IAgentClient, ISettingsAccess, IVaultAccess
1515
├── adapters/ # Interface implementations
1616
│ ├── acp/ # ACP protocol (acp.adapter.ts, acp-type-converter.ts)
1717
│ └── obsidian/ # Platform adapters (vault, settings, mention-service)
1818
├── hooks/ # React custom hooks (state + logic)
1919
│ ├── useAgentSession.ts # Session lifecycle, agent switching
20-
│ ├── useChat.ts # Message sending, callbacks
20+
│ ├── useChat.ts # Message sending, session update handling
2121
│ ├── usePermission.ts # Permission handling
2222
│ ├── useMentions.ts # @[[note]] suggestions
2323
│ ├── useSlashCommands.ts # /command suggestions
@@ -40,6 +40,7 @@ src/
4040
### ChatView (`components/chat/ChatView.tsx`)
4141
- **Hook Composition**: Combines all hooks (useAgentSession, useChat, usePermission, etc.)
4242
- **Adapter Instantiation**: Creates AcpAdapter, VaultAdapter, MentionService via useMemo
43+
- **Callback Registration**: Registers `onSessionUpdate` for unified event handling
4344
- **Rendering**: Delegates to ChatHeader, ChatMessages, ChatInput
4445

4546
### Hooks (`hooks/`)
@@ -48,11 +49,16 @@ src/
4849
- `createSession()`: Load config, inject API keys, initialize + newSession
4950
- `switchAgent()`: Change active agent, restart session
5051
- `closeSession()`: Cancel session, disconnect
52+
- `updateAvailableCommands()`: Handle slash command updates
53+
- `updateCurrentMode()`: Handle mode change updates
5154

52-
**useChat**: Messaging
55+
**useChat**: Messaging and session update handling
5356
- `sendMessage()`: Prepare (auto-mention, path conversion) → send via IAgentClient
5457
- `handleNewChat()`: Export if enabled, restart session
55-
- Callbacks: addMessage, updateLastMessage, updateMessage
58+
- `handleSessionUpdate()`: Unified handler for all session updates (agent_message_chunk, tool_call, etc.)
59+
- `upsertToolCall()`: Create or update tool call in single `setMessages` callback (avoids race conditions)
60+
- `updateLastMessage()`: Append text/thought chunks to last assistant message
61+
- `updateMessage()`: Update specific message by tool call ID
5662

5763
**usePermission**: Permission handling
5864
- `handlePermissionResponse()`: Respond with selected option
@@ -67,8 +73,9 @@ Implements IAgentClient + IAcpClient (terminal ops)
6773

6874
- **Process**: spawn() with login shell (macOS/Linux -l, Windows shell:true)
6975
- **Protocol**: JSON-RPC over stdin/stdout via ndJsonStream
70-
- **Flow**: initialize() → newSession() → sendMessage() → sessionUpdate() callbacks
71-
- **Updates**: agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update, plan, available_commands_update
76+
- **Flow**: initialize() → newSession() → sendMessage() → sessionUpdate via `onSessionUpdate`
77+
- **Updates**: agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update, plan, available_commands_update, current_mode_update
78+
- **Unified Callback**: Single `onSessionUpdate(callback)` replaces legacy `onMessage`, `onError`, `onPermissionRequest`
7279
- **Permissions**: Promise-based Map<requestId, resolver>
7380
- **Terminal**: createTerminal, terminalOutput, killTerminal, releaseTerminal
7481

@@ -83,6 +90,25 @@ Pure functions (non-React):
8390
- `prepareMessage()`: Auto-mention, convert @[[note]] → paths
8491
- `sendPreparedMessage()`: Send via IAgentClient, auth retry
8592

93+
## Domain Models
94+
95+
### SessionUpdate (`domain/models/session-update.ts`)
96+
Union type for all session update events from the agent:
97+
98+
```typescript
99+
type SessionUpdate =
100+
| AgentMessageChunkUpdate // Text chunk from agent's response
101+
| AgentThoughtChunkUpdate // Text chunk from agent's reasoning
102+
| ToolCallUpdate // New tool call event
103+
| ToolCallUpdateUpdate // Update to existing tool call
104+
| PlanUpdate // Agent's task plan
105+
| AvailableCommandsUpdate // Slash commands changed
106+
| CurrentModeUpdate // Mode changed
107+
| ErrorUpdate; // Error from agent operations
108+
```
109+
110+
This domain type abstracts ACP's `SessionNotification.update.sessionUpdate` values, allowing the application layer to handle events without depending on ACP protocol specifics.
111+
86112
## Ports (Interfaces)
87113

88114
```typescript
@@ -93,10 +119,15 @@ interface IAgentClient {
93119
sendMessage(sessionId: string, message: string): Promise<void>;
94120
cancel(sessionId: string): Promise<void>;
95121
disconnect(): Promise<void>;
96-
onMessage(callback: (message: ChatMessage) => void): void;
97-
onError(callback: (error: AgentError) => void): void;
98-
onPermissionRequest(callback: (request: PermissionRequest) => void): void;
122+
123+
// Unified callback for all session updates
124+
onSessionUpdate(callback: (update: SessionUpdate) => void): void;
125+
99126
respondToPermission(requestId: string, optionId: string): Promise<void>;
127+
isInitialized(): boolean;
128+
getCurrentAgentId(): string | null;
129+
setSessionMode(sessionId: string, modeId: string): Promise<void>;
130+
setSessionModel(sessionId: string, modelId: string): Promise<void>;
100131
}
101132

102133
interface IVaultAccess {
@@ -120,6 +151,7 @@ interface ISettingsAccess {
120151
2. **Pure functions in shared/**: Non-React business logic
121152
3. **Ports for ACP resistance**: IAgentClient interface isolates protocol changes
122153
4. **Domain has zero deps**: No `obsidian`, `@agentclientprotocol/sdk`
154+
5. **Unified callbacks**: Use `onSessionUpdate` for all agent events (not multiple callbacks)
123155

124156
### Obsidian Plugin Review (CRITICAL)
125157
1. No innerHTML/outerHTML - use createEl/createDiv/createSpan
@@ -141,6 +173,7 @@ interface ISettingsAccess {
141173
3. useRef for cleanup function access
142174
4. Error handling: try-catch async ops
143175
5. Logging: Logger class (respects debugMode)
176+
6. **Upsert pattern**: Use `setMessages` functional updates to avoid race conditions with tool_call updates
144177

145178
## Common Tasks
146179

@@ -158,8 +191,17 @@ interface ISettingsAccess {
158191

159192
### Modify Message Types
160193
1. Update `ChatMessage`/`MessageContent` in `domain/models/chat-message.ts`
161-
2. Update `AcpAdapter.sessionUpdate()` to handle new type
162-
3. Update `MessageContentRenderer` to render new type
194+
2. If adding new session update type:
195+
- Add to `SessionUpdate` union in `domain/models/session-update.ts`
196+
- Handle in `useChat.handleSessionUpdate()`
197+
3. Update `AcpAdapter.sessionUpdate()` to emit the new type
198+
4. Update `MessageContentRenderer` to render new type
199+
200+
### Add New Session Update Type
201+
1. Define interface in `domain/models/session-update.ts`
202+
2. Add to `SessionUpdate` union type
203+
3. Handle in `useChat.handleSessionUpdate()` (for message-level updates)
204+
4. Or handle in `ChatView` (for session-level updates like `available_commands_update`)
163205

164206
### Debug
165207
1. Settings → Developer Settings → Debug Mode ON
@@ -170,8 +212,8 @@ interface ISettingsAccess {
170212

171213
**Communication**: JSON-RPC 2.0 over stdin/stdout
172214

173-
**Methods**: initialize, newSession, authenticate, prompt, cancel
174-
**Notifications**: session/update (agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update, plan, available_commands_update)
215+
**Methods**: initialize, newSession, authenticate, prompt, cancel, setSessionMode, setSessionModel
216+
**Notifications**: session/update (agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update, plan, available_commands_update, current_mode_update)
175217
**Requests**: requestPermission
176218

177219
**Agents**:
@@ -181,4 +223,4 @@ interface ISettingsAccess {
181223

182224
---
183225

184-
**Last Updated**: November 2025 | **Architecture**: React Hooks | **Version**: 0.3.0
226+
**Last Updated**: December 2025 | **Architecture**: React Hooks | **Version**: 0.4.0

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "agent-client",
33
"name": "Agent Client",
4-
"version": "0.4.0-preview.2",
4+
"version": "0.4.0-preview.3",
55
"minAppVersion": "0.15.0",
66
"description": "Chat with AI agents via the Agent Client Protocol directly from your vault.",
77
"author": "RAIT-09",

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "obsidian-agent-client",
3-
"version": "0.4.0-preview.2",
3+
"version": "0.4.0-preview.3",
44
"description": "Use AI coding agents via the Agent Client Protocol directly inside Obsidian",
55
"main": "main.js",
66
"scripts": {

0 commit comments

Comments
 (0)