@mseep/obsidian-agent-client 0.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/gh-setup.sh +49 -0
- package/.claude/settings.json +15 -0
- package/.claude/skills/release-notes/SKILL.md +331 -0
- package/.editorconfig +10 -0
- package/.github/FUNDING.yml +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
- package/.github/copilot-instructions.md +45 -0
- package/.github/pull_request_template.md +32 -0
- package/.github/workflows/ci.yaml +25 -0
- package/.github/workflows/docs.yml +58 -0
- package/.github/workflows/relay_to_openclaw.yml +59 -0
- package/.github/workflows/release.yaml +45 -0
- package/.prettierignore +10 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +37 -0
- package/.zed/settings.json +42 -0
- package/AGENTS.md +330 -0
- package/ARCHITECTURE.md +390 -0
- package/CONTRIBUTING.md +216 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.ja.md +121 -0
- package/README.md +125 -0
- package/docs/.vitepress/config.mts +124 -0
- package/docs/.vitepress/theme/custom.css +111 -0
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/agent-setup/claude-code.md +84 -0
- package/docs/agent-setup/codex.md +76 -0
- package/docs/agent-setup/custom-agents.md +67 -0
- package/docs/agent-setup/gemini-cli.md +99 -0
- package/docs/agent-setup/index.md +34 -0
- package/docs/announcements/gemini-cli-deprecation.md +73 -0
- package/docs/getting-started/index.md +78 -0
- package/docs/getting-started/quick-start.md +38 -0
- package/docs/help/faq.md +181 -0
- package/docs/help/troubleshooting.md +221 -0
- package/docs/index.md +63 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/demo.mp4 +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/public/images/editing.webp +0 -0
- package/docs/public/images/export.webp +0 -0
- package/docs/public/images/floating-chat-button.webp +0 -0
- package/docs/public/images/floating-chat-instance-menu.webp +0 -0
- package/docs/public/images/floating-chat-view.webp +0 -0
- package/docs/public/images/mode-selection.webp +0 -0
- package/docs/public/images/model-selection.webp +0 -0
- package/docs/public/images/multi-session.webp +0 -0
- package/docs/public/images/remove-image.webp +0 -0
- package/docs/public/images/ribbon-icon.webp +0 -0
- package/docs/public/images/selection-context.gif +0 -0
- package/docs/public/images/sending-images.webp +0 -0
- package/docs/public/images/sending-messages.webp +0 -0
- package/docs/public/images/session-history-button.webp +0 -0
- package/docs/public/images/slash-commands-1.webp +0 -0
- package/docs/public/images/slash-commands-2.webp +0 -0
- package/docs/public/images/switch-agent.webp +0 -0
- package/docs/public/images/switch-default-agent.webp +0 -0
- package/docs/public/images/temporary-disable.gif +0 -0
- package/docs/reference/acp-support.md +110 -0
- package/docs/usage/chat-export.md +80 -0
- package/docs/usage/commands.md +51 -0
- package/docs/usage/context-files.md +57 -0
- package/docs/usage/editing.md +69 -0
- package/docs/usage/floating-chat.md +84 -0
- package/docs/usage/index.md +97 -0
- package/docs/usage/mcp-tools.md +33 -0
- package/docs/usage/mentions.md +70 -0
- package/docs/usage/mode-selection.md +28 -0
- package/docs/usage/model-selection.md +32 -0
- package/docs/usage/multi-session.md +68 -0
- package/docs/usage/sending-images.md +64 -0
- package/docs/usage/session-history.md +91 -0
- package/docs/usage/slash-commands.md +44 -0
- package/esbuild.config.mjs +49 -0
- package/eslint.config.mjs +25 -0
- package/main.js +228 -0
- package/manifest.json +11 -0
- package/package.json +52 -0
- package/src/acp/acp-client.ts +921 -0
- package/src/acp/acp-handler.ts +252 -0
- package/src/acp/permission-handler.ts +282 -0
- package/src/acp/terminal-handler.ts +264 -0
- package/src/acp/type-converter.ts +272 -0
- package/src/hooks/useAgent.ts +250 -0
- package/src/hooks/useAgentMessages.ts +470 -0
- package/src/hooks/useAgentSession.ts +544 -0
- package/src/hooks/useChatActions.ts +400 -0
- package/src/hooks/useHistoryModal.ts +219 -0
- package/src/hooks/useSessionHistory.ts +863 -0
- package/src/hooks/useSettings.ts +19 -0
- package/src/hooks/useSuggestions.ts +342 -0
- package/src/main.ts +9 -0
- package/src/plugin.ts +1126 -0
- package/src/services/chat-exporter.ts +552 -0
- package/src/services/message-sender.ts +755 -0
- package/src/services/message-state.ts +375 -0
- package/src/services/session-helpers.ts +211 -0
- package/src/services/session-state.ts +130 -0
- package/src/services/session-storage.ts +267 -0
- package/src/services/settings-normalizer.ts +255 -0
- package/src/services/settings-service.ts +285 -0
- package/src/services/update-checker.ts +128 -0
- package/src/services/vault-service.ts +558 -0
- package/src/services/view-registry.ts +345 -0
- package/src/types/agent.ts +92 -0
- package/src/types/chat.ts +351 -0
- package/src/types/errors.ts +136 -0
- package/src/types/obsidian-internals.d.ts +14 -0
- package/src/types/session.ts +731 -0
- package/src/ui/ChangeDirectoryModal.ts +137 -0
- package/src/ui/ChatContext.ts +25 -0
- package/src/ui/ChatHeader.tsx +295 -0
- package/src/ui/ChatPanel.tsx +1162 -0
- package/src/ui/ChatView.tsx +348 -0
- package/src/ui/ErrorBanner.tsx +104 -0
- package/src/ui/FloatingButton.tsx +351 -0
- package/src/ui/FloatingChatView.tsx +531 -0
- package/src/ui/InputArea.tsx +1107 -0
- package/src/ui/InputToolbar.tsx +371 -0
- package/src/ui/MessageBubble.tsx +442 -0
- package/src/ui/MessageList.tsx +265 -0
- package/src/ui/PermissionBanner.tsx +61 -0
- package/src/ui/SessionHistoryModal.tsx +821 -0
- package/src/ui/SettingsTab.ts +1337 -0
- package/src/ui/SuggestionPopup.tsx +138 -0
- package/src/ui/TerminalBlock.tsx +107 -0
- package/src/ui/ToolCallBlock.tsx +456 -0
- package/src/ui/shared/AttachmentStrip.tsx +57 -0
- package/src/ui/shared/IconButton.tsx +55 -0
- package/src/ui/shared/MarkdownRenderer.tsx +103 -0
- package/src/ui/view-host.ts +56 -0
- package/src/utils/error-utils.ts +274 -0
- package/src/utils/logger.ts +44 -0
- package/src/utils/mention-parser.ts +129 -0
- package/src/utils/paths.ts +246 -0
- package/src/utils/platform.ts +425 -0
- package/styles.css +2322 -0
- package/tsconfig.json +18 -0
- package/version-bump.mjs +18 -0
- package/versions.json +3 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for managing the complete agent interaction lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* This is a facade that composes useAgentSession and useAgentMessages,
|
|
5
|
+
* providing a unified API to ChatPanel.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
const { useState, useCallback, useEffect, useMemo } = React;
|
|
10
|
+
|
|
11
|
+
import type { SessionUpdate } from "../types/session";
|
|
12
|
+
import type { AcpClient } from "../acp/acp-client";
|
|
13
|
+
import type { IVaultAccess } from "../services/vault-service";
|
|
14
|
+
import type { ISettingsAccess } from "../services/settings-service";
|
|
15
|
+
import type { ErrorInfo } from "../types/errors";
|
|
16
|
+
import type { IMentionService } from "../utils/mention-parser";
|
|
17
|
+
import { useAgentSession } from "./useAgentSession";
|
|
18
|
+
import { useAgentMessages, type SendMessageOptions } from "./useAgentMessages";
|
|
19
|
+
|
|
20
|
+
// Re-export types that ChatPanel uses
|
|
21
|
+
export type { SendMessageOptions } from "./useAgentMessages";
|
|
22
|
+
export type { AgentDisplayInfo } from "../services/session-helpers";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
import type { ChatMessage, ActivePermission } from "../types/chat";
|
|
29
|
+
import type {
|
|
30
|
+
ChatSession,
|
|
31
|
+
SessionModeState,
|
|
32
|
+
SessionModelState,
|
|
33
|
+
SessionConfigOption,
|
|
34
|
+
} from "../types/session";
|
|
35
|
+
import type { AgentDisplayInfo } from "../services/session-helpers";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return type for useAgent hook.
|
|
39
|
+
*/
|
|
40
|
+
export interface UseAgentReturn {
|
|
41
|
+
// Session state
|
|
42
|
+
session: ChatSession;
|
|
43
|
+
isReady: boolean;
|
|
44
|
+
|
|
45
|
+
// Message state
|
|
46
|
+
messages: ChatMessage[];
|
|
47
|
+
isSending: boolean;
|
|
48
|
+
lastUserMessage: string | null;
|
|
49
|
+
|
|
50
|
+
// Combined error
|
|
51
|
+
errorInfo: ErrorInfo | null;
|
|
52
|
+
|
|
53
|
+
// Session lifecycle
|
|
54
|
+
createSession: (
|
|
55
|
+
overrideAgentId?: string,
|
|
56
|
+
overrideCwd?: string,
|
|
57
|
+
) => Promise<void>;
|
|
58
|
+
restartSession: (
|
|
59
|
+
newAgentId?: string,
|
|
60
|
+
overrideCwd?: string,
|
|
61
|
+
) => Promise<void>;
|
|
62
|
+
closeSession: () => Promise<void>;
|
|
63
|
+
forceRestartAgent: () => Promise<void>;
|
|
64
|
+
cancelOperation: () => Promise<void>;
|
|
65
|
+
getAvailableAgents: () => AgentDisplayInfo[];
|
|
66
|
+
updateSessionFromLoad: (
|
|
67
|
+
sessionId: string,
|
|
68
|
+
modes?: SessionModeState,
|
|
69
|
+
models?: SessionModelState,
|
|
70
|
+
configOptions?: SessionConfigOption[],
|
|
71
|
+
) => Promise<void>;
|
|
72
|
+
|
|
73
|
+
// Config
|
|
74
|
+
setMode: (modeId: string) => Promise<void>;
|
|
75
|
+
setModel: (modelId: string) => Promise<void>;
|
|
76
|
+
setConfigOption: (configId: string, value: string) => Promise<void>;
|
|
77
|
+
|
|
78
|
+
// Message operations
|
|
79
|
+
sendMessage: (
|
|
80
|
+
content: string,
|
|
81
|
+
options: SendMessageOptions,
|
|
82
|
+
) => Promise<void>;
|
|
83
|
+
clearMessages: () => void;
|
|
84
|
+
setInitialMessages: (
|
|
85
|
+
history: Array<{
|
|
86
|
+
role: string;
|
|
87
|
+
content: Array<{ type: string; text: string }>;
|
|
88
|
+
timestamp?: string;
|
|
89
|
+
}>,
|
|
90
|
+
) => void;
|
|
91
|
+
setMessagesFromLocal: (localMessages: ChatMessage[]) => void;
|
|
92
|
+
clearError: () => void;
|
|
93
|
+
setIgnoreUpdates: (ignore: boolean) => void;
|
|
94
|
+
// Permission
|
|
95
|
+
activePermission: ActivePermission | null;
|
|
96
|
+
hasActivePermission: boolean;
|
|
97
|
+
approvePermission: (requestId: string, optionId: string) => Promise<void>;
|
|
98
|
+
approveActivePermission: () => Promise<boolean>;
|
|
99
|
+
rejectActivePermission: () => Promise<boolean>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Hook Implementation
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param agentClient - Agent client for communication
|
|
108
|
+
* @param settingsAccess - Settings access for agent configuration
|
|
109
|
+
* @param vaultAccess - Vault access for reading notes (also serves as IMentionService)
|
|
110
|
+
* @param workingDirectory - Working directory for the session
|
|
111
|
+
* @param initialAgentId - Optional initial agent ID (from view persistence)
|
|
112
|
+
*/
|
|
113
|
+
export function useAgent(
|
|
114
|
+
agentClient: AcpClient,
|
|
115
|
+
settingsAccess: ISettingsAccess,
|
|
116
|
+
vaultAccess: IVaultAccess & IMentionService,
|
|
117
|
+
workingDirectory: string,
|
|
118
|
+
initialAgentId?: string,
|
|
119
|
+
): UseAgentReturn {
|
|
120
|
+
// ============================================================
|
|
121
|
+
// Shared Error State
|
|
122
|
+
// ============================================================
|
|
123
|
+
|
|
124
|
+
const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null);
|
|
125
|
+
|
|
126
|
+
// ============================================================
|
|
127
|
+
// Sub-hooks
|
|
128
|
+
// ============================================================
|
|
129
|
+
|
|
130
|
+
const agentSession = useAgentSession(
|
|
131
|
+
agentClient,
|
|
132
|
+
settingsAccess,
|
|
133
|
+
workingDirectory,
|
|
134
|
+
setErrorInfo,
|
|
135
|
+
initialAgentId,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const agentMessages = useAgentMessages(
|
|
139
|
+
agentClient,
|
|
140
|
+
settingsAccess,
|
|
141
|
+
vaultAccess,
|
|
142
|
+
agentSession.session,
|
|
143
|
+
setErrorInfo,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// ============================================================
|
|
147
|
+
// Unified Session Update Handler
|
|
148
|
+
// ============================================================
|
|
149
|
+
|
|
150
|
+
const handleSessionUpdate = useCallback(
|
|
151
|
+
(update: SessionUpdate) => {
|
|
152
|
+
// Session-level updates (commands, mode, config, usage, error)
|
|
153
|
+
agentSession.handleSessionUpdate(update);
|
|
154
|
+
|
|
155
|
+
// Message-level updates (batched via RAF, ignoreUpdates checked internally)
|
|
156
|
+
agentMessages.enqueueUpdate(update);
|
|
157
|
+
},
|
|
158
|
+
[agentSession.handleSessionUpdate, agentMessages.enqueueUpdate],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Composed cancel: session-level cancel + message-level RAF cleanup
|
|
162
|
+
const cancelOperation = useCallback(async () => {
|
|
163
|
+
await agentSession.cancelOperation();
|
|
164
|
+
agentMessages.clearPendingUpdates();
|
|
165
|
+
}, [agentSession.cancelOperation, agentMessages.clearPendingUpdates]);
|
|
166
|
+
|
|
167
|
+
// Subscribe to all updates from agent
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const unsubscribe = agentClient.onSessionUpdate(handleSessionUpdate);
|
|
170
|
+
return unsubscribe;
|
|
171
|
+
}, [agentClient, handleSessionUpdate]);
|
|
172
|
+
|
|
173
|
+
// ============================================================
|
|
174
|
+
// Return
|
|
175
|
+
// ============================================================
|
|
176
|
+
|
|
177
|
+
return useMemo(
|
|
178
|
+
() => ({
|
|
179
|
+
// Session state
|
|
180
|
+
session: agentSession.session,
|
|
181
|
+
isReady: agentSession.isReady,
|
|
182
|
+
|
|
183
|
+
// Message state
|
|
184
|
+
messages: agentMessages.messages,
|
|
185
|
+
isSending: agentMessages.isSending,
|
|
186
|
+
lastUserMessage: agentMessages.lastUserMessage,
|
|
187
|
+
|
|
188
|
+
// Combined error
|
|
189
|
+
errorInfo,
|
|
190
|
+
|
|
191
|
+
// Session lifecycle
|
|
192
|
+
createSession: agentSession.createSession,
|
|
193
|
+
restartSession: agentSession.restartSession,
|
|
194
|
+
closeSession: agentSession.closeSession,
|
|
195
|
+
forceRestartAgent: agentSession.forceRestartAgent,
|
|
196
|
+
cancelOperation,
|
|
197
|
+
getAvailableAgents: agentSession.getAvailableAgents,
|
|
198
|
+
updateSessionFromLoad: agentSession.updateSessionFromLoad,
|
|
199
|
+
|
|
200
|
+
// Config
|
|
201
|
+
setMode: agentSession.setMode,
|
|
202
|
+
setModel: agentSession.setModel,
|
|
203
|
+
setConfigOption: agentSession.setConfigOption,
|
|
204
|
+
|
|
205
|
+
// Message operations
|
|
206
|
+
sendMessage: agentMessages.sendMessage,
|
|
207
|
+
clearMessages: agentMessages.clearMessages,
|
|
208
|
+
setInitialMessages: agentMessages.setInitialMessages,
|
|
209
|
+
setMessagesFromLocal: agentMessages.setMessagesFromLocal,
|
|
210
|
+
clearError: agentMessages.clearError,
|
|
211
|
+
setIgnoreUpdates: agentMessages.setIgnoreUpdates,
|
|
212
|
+
|
|
213
|
+
// Permission
|
|
214
|
+
activePermission: agentMessages.activePermission,
|
|
215
|
+
hasActivePermission: agentMessages.hasActivePermission,
|
|
216
|
+
approvePermission: agentMessages.approvePermission,
|
|
217
|
+
approveActivePermission: agentMessages.approveActivePermission,
|
|
218
|
+
rejectActivePermission: agentMessages.rejectActivePermission,
|
|
219
|
+
}),
|
|
220
|
+
[
|
|
221
|
+
agentSession.session,
|
|
222
|
+
agentSession.isReady,
|
|
223
|
+
agentMessages.messages,
|
|
224
|
+
agentMessages.isSending,
|
|
225
|
+
agentMessages.lastUserMessage,
|
|
226
|
+
errorInfo,
|
|
227
|
+
agentSession.createSession,
|
|
228
|
+
agentSession.restartSession,
|
|
229
|
+
agentSession.closeSession,
|
|
230
|
+
agentSession.forceRestartAgent,
|
|
231
|
+
cancelOperation,
|
|
232
|
+
agentSession.getAvailableAgents,
|
|
233
|
+
agentSession.updateSessionFromLoad,
|
|
234
|
+
agentSession.setMode,
|
|
235
|
+
agentSession.setModel,
|
|
236
|
+
agentSession.setConfigOption,
|
|
237
|
+
agentMessages.sendMessage,
|
|
238
|
+
agentMessages.clearMessages,
|
|
239
|
+
agentMessages.setInitialMessages,
|
|
240
|
+
agentMessages.setMessagesFromLocal,
|
|
241
|
+
agentMessages.clearError,
|
|
242
|
+
agentMessages.setIgnoreUpdates,
|
|
243
|
+
agentMessages.activePermission,
|
|
244
|
+
agentMessages.hasActivePermission,
|
|
245
|
+
agentMessages.approvePermission,
|
|
246
|
+
agentMessages.approveActivePermission,
|
|
247
|
+
agentMessages.rejectActivePermission,
|
|
248
|
+
],
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-hook for managing chat messages, streaming, and permissions.
|
|
3
|
+
*
|
|
4
|
+
* Handles message state, RAF batching for streaming updates,
|
|
5
|
+
* send/receive operations, and permission approve/reject.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
const { useState, useCallback, useMemo, useRef, useEffect } = React;
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ChatMessage,
|
|
13
|
+
MessageContent,
|
|
14
|
+
ActivePermission,
|
|
15
|
+
ImagePromptContent,
|
|
16
|
+
ResourceLinkPromptContent,
|
|
17
|
+
} from "../types/chat";
|
|
18
|
+
import type { ChatSession, SessionUpdate } from "../types/session";
|
|
19
|
+
import type { AcpClient } from "../acp/acp-client";
|
|
20
|
+
import type { IVaultAccess, NoteMetadata } from "../services/vault-service";
|
|
21
|
+
import type { ISettingsAccess } from "../services/settings-service";
|
|
22
|
+
import type { ErrorInfo } from "../types/errors";
|
|
23
|
+
import type { IMentionService } from "../utils/mention-parser";
|
|
24
|
+
import { preparePrompt, sendPreparedPrompt } from "../services/message-sender";
|
|
25
|
+
import { extractErrorMessage } from "../utils/error-utils";
|
|
26
|
+
import { Platform } from "obsidian";
|
|
27
|
+
import {
|
|
28
|
+
rebuildToolCallIndex,
|
|
29
|
+
applySingleUpdate,
|
|
30
|
+
findActivePermission,
|
|
31
|
+
selectOption,
|
|
32
|
+
} from "../services/message-state";
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Types
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Options for sending a message.
|
|
40
|
+
*/
|
|
41
|
+
export interface SendMessageOptions {
|
|
42
|
+
/** Currently active note for auto-mention */
|
|
43
|
+
activeNote: NoteMetadata | null;
|
|
44
|
+
/** Vault base path for mention resolution */
|
|
45
|
+
vaultBasePath: string;
|
|
46
|
+
/** Whether auto-mention is temporarily disabled */
|
|
47
|
+
isAutoMentionDisabled?: boolean;
|
|
48
|
+
/** Attached images (Base64 embedded) */
|
|
49
|
+
images?: ImagePromptContent[];
|
|
50
|
+
/** Attached file references (resource links) */
|
|
51
|
+
resourceLinks?: ResourceLinkPromptContent[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface UseAgentMessagesReturn {
|
|
55
|
+
// Message state
|
|
56
|
+
messages: ChatMessage[];
|
|
57
|
+
isSending: boolean;
|
|
58
|
+
lastUserMessage: string | null;
|
|
59
|
+
|
|
60
|
+
// Message operations
|
|
61
|
+
sendMessage: (
|
|
62
|
+
content: string,
|
|
63
|
+
options: SendMessageOptions,
|
|
64
|
+
) => Promise<void>;
|
|
65
|
+
clearMessages: () => void;
|
|
66
|
+
setInitialMessages: (
|
|
67
|
+
history: Array<{
|
|
68
|
+
role: string;
|
|
69
|
+
content: Array<{ type: string; text: string }>;
|
|
70
|
+
timestamp?: string;
|
|
71
|
+
}>,
|
|
72
|
+
) => void;
|
|
73
|
+
setMessagesFromLocal: (localMessages: ChatMessage[]) => void;
|
|
74
|
+
clearError: () => void;
|
|
75
|
+
setIgnoreUpdates: (ignore: boolean) => void;
|
|
76
|
+
/** Discard any pending RAF updates and reset streaming state (call after stop/cancel). */
|
|
77
|
+
clearPendingUpdates: () => void;
|
|
78
|
+
|
|
79
|
+
// Permission
|
|
80
|
+
activePermission: ActivePermission | null;
|
|
81
|
+
hasActivePermission: boolean;
|
|
82
|
+
approvePermission: (requestId: string, optionId: string) => Promise<void>;
|
|
83
|
+
approveActivePermission: () => Promise<boolean>;
|
|
84
|
+
rejectActivePermission: () => Promise<boolean>;
|
|
85
|
+
|
|
86
|
+
/** Enqueue a message-level update (used by useAgent for unified handler) */
|
|
87
|
+
enqueueUpdate: (update: SessionUpdate) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Hook Implementation
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export function useAgentMessages(
|
|
95
|
+
agentClient: AcpClient,
|
|
96
|
+
settingsAccess: ISettingsAccess,
|
|
97
|
+
vaultAccess: IVaultAccess & IMentionService,
|
|
98
|
+
session: ChatSession,
|
|
99
|
+
setErrorInfo: (error: ErrorInfo | null) => void,
|
|
100
|
+
): UseAgentMessagesReturn {
|
|
101
|
+
// ============================================================
|
|
102
|
+
// Message State
|
|
103
|
+
// ============================================================
|
|
104
|
+
|
|
105
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
106
|
+
const [isSending, setIsSending] = useState(false);
|
|
107
|
+
const [lastUserMessage, setLastUserMessage] = useState<string | null>(null);
|
|
108
|
+
|
|
109
|
+
// Tool call index: toolCallId → message index for O(1) lookup
|
|
110
|
+
const toolCallIndexRef = useRef<Map<string, number>>(new Map());
|
|
111
|
+
|
|
112
|
+
// Ignore updates flag (used during session/load to skip history replay)
|
|
113
|
+
const ignoreUpdatesRef = useRef(false);
|
|
114
|
+
|
|
115
|
+
// Generation counter to prevent stale async callbacks from overwriting
|
|
116
|
+
// state after cancel/stop followed by a new send. Each sendMessage()
|
|
117
|
+
// increments this; completion handlers only update state if the
|
|
118
|
+
// generation hasn't changed (fixes Issue #200).
|
|
119
|
+
const generationRef = useRef(0);
|
|
120
|
+
|
|
121
|
+
// Track the current send promise so a new sendMessage() can wait for
|
|
122
|
+
// the previous one to settle before starting (avoids interleaved sends).
|
|
123
|
+
const sendPromiseRef = useRef<Promise<void> | null>(null);
|
|
124
|
+
|
|
125
|
+
// ============================================================
|
|
126
|
+
// Streaming Update Batching
|
|
127
|
+
// ============================================================
|
|
128
|
+
|
|
129
|
+
const pendingUpdatesRef = useRef<SessionUpdate[]>([]);
|
|
130
|
+
const flushScheduledRef = useRef(false);
|
|
131
|
+
|
|
132
|
+
const flushPendingUpdates = useCallback(() => {
|
|
133
|
+
flushScheduledRef.current = false;
|
|
134
|
+
const updates = pendingUpdatesRef.current;
|
|
135
|
+
if (updates.length === 0) return;
|
|
136
|
+
pendingUpdatesRef.current = [];
|
|
137
|
+
|
|
138
|
+
setMessages((prev) => {
|
|
139
|
+
let result = prev;
|
|
140
|
+
for (const update of updates) {
|
|
141
|
+
result = applySingleUpdate(
|
|
142
|
+
result,
|
|
143
|
+
update,
|
|
144
|
+
toolCallIndexRef.current,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
});
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
const enqueueUpdate = useCallback(
|
|
152
|
+
(update: SessionUpdate) => {
|
|
153
|
+
if (ignoreUpdatesRef.current) return;
|
|
154
|
+
pendingUpdatesRef.current.push(update);
|
|
155
|
+
if (!flushScheduledRef.current) {
|
|
156
|
+
flushScheduledRef.current = true;
|
|
157
|
+
window.requestAnimationFrame(flushPendingUpdates);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
[flushPendingUpdates],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Clean up on unmount
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
return () => {
|
|
166
|
+
pendingUpdatesRef.current = [];
|
|
167
|
+
flushScheduledRef.current = false;
|
|
168
|
+
toolCallIndexRef.current.clear();
|
|
169
|
+
};
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
// ============================================================
|
|
173
|
+
// Message Operations
|
|
174
|
+
// ============================================================
|
|
175
|
+
|
|
176
|
+
const addMessage = useCallback((message: ChatMessage): void => {
|
|
177
|
+
setMessages((prev) => [...prev, message]);
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
const setIgnoreUpdates = useCallback((ignore: boolean): void => {
|
|
181
|
+
ignoreUpdatesRef.current = ignore;
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
/** Discard any pending RAF updates and reset the streaming flag. */
|
|
185
|
+
const clearPendingUpdates = useCallback((): void => {
|
|
186
|
+
pendingUpdatesRef.current = [];
|
|
187
|
+
flushScheduledRef.current = false;
|
|
188
|
+
setIsSending(false);
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const clearMessages = useCallback((): void => {
|
|
192
|
+
setMessages([]);
|
|
193
|
+
toolCallIndexRef.current.clear();
|
|
194
|
+
setLastUserMessage(null);
|
|
195
|
+
setIsSending(false);
|
|
196
|
+
setErrorInfo(null);
|
|
197
|
+
}, [setErrorInfo]);
|
|
198
|
+
|
|
199
|
+
const setInitialMessages = useCallback(
|
|
200
|
+
(
|
|
201
|
+
history: Array<{
|
|
202
|
+
role: string;
|
|
203
|
+
content: Array<{ type: string; text: string }>;
|
|
204
|
+
timestamp?: string;
|
|
205
|
+
}>,
|
|
206
|
+
): void => {
|
|
207
|
+
const chatMessages: ChatMessage[] = history.map((msg) => ({
|
|
208
|
+
id: crypto.randomUUID(),
|
|
209
|
+
role: msg.role as "user" | "assistant",
|
|
210
|
+
content: msg.content.map((c) => ({
|
|
211
|
+
type: c.type as "text",
|
|
212
|
+
text: c.text,
|
|
213
|
+
})),
|
|
214
|
+
timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
setMessages(chatMessages);
|
|
218
|
+
rebuildToolCallIndex(chatMessages, toolCallIndexRef.current);
|
|
219
|
+
setIsSending(false);
|
|
220
|
+
setErrorInfo(null);
|
|
221
|
+
},
|
|
222
|
+
[setErrorInfo],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const setMessagesFromLocal = useCallback(
|
|
226
|
+
(localMessages: ChatMessage[]): void => {
|
|
227
|
+
setMessages(localMessages);
|
|
228
|
+
rebuildToolCallIndex(localMessages, toolCallIndexRef.current);
|
|
229
|
+
setIsSending(false);
|
|
230
|
+
setErrorInfo(null);
|
|
231
|
+
},
|
|
232
|
+
[setErrorInfo],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const clearError = useCallback((): void => {
|
|
236
|
+
setErrorInfo(null);
|
|
237
|
+
}, [setErrorInfo]);
|
|
238
|
+
|
|
239
|
+
const shouldConvertToWsl = useMemo(() => {
|
|
240
|
+
const settings = settingsAccess.getSnapshot();
|
|
241
|
+
return Platform.isWin && settings.windowsWslMode;
|
|
242
|
+
}, [settingsAccess]);
|
|
243
|
+
|
|
244
|
+
const sendMessage = useCallback(
|
|
245
|
+
async (content: string, options: SendMessageOptions): Promise<void> => {
|
|
246
|
+
if (!session.sessionId) {
|
|
247
|
+
setErrorInfo({
|
|
248
|
+
title: "Cannot Send Message",
|
|
249
|
+
message: "No active session. Please wait for connection.",
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Wait for any in-flight send to settle (e.g. after cancel/stop)
|
|
255
|
+
// before starting a new one to avoid interleaved state updates.
|
|
256
|
+
if (sendPromiseRef.current) {
|
|
257
|
+
try { await sendPromiseRef.current; } catch { /* ignore */ }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const currentSessionId = session.sessionId;
|
|
261
|
+
const generation = ++generationRef.current;
|
|
262
|
+
const settings = settingsAccess.getSnapshot();
|
|
263
|
+
|
|
264
|
+
const prepared = await preparePrompt(
|
|
265
|
+
{
|
|
266
|
+
message: content,
|
|
267
|
+
images: options.images,
|
|
268
|
+
resourceLinks: options.resourceLinks,
|
|
269
|
+
activeNote: options.activeNote,
|
|
270
|
+
vaultBasePath: options.vaultBasePath,
|
|
271
|
+
isAutoMentionDisabled: options.isAutoMentionDisabled,
|
|
272
|
+
convertToWsl: shouldConvertToWsl,
|
|
273
|
+
supportsEmbeddedContext:
|
|
274
|
+
session.promptCapabilities?.embeddedContext ?? false,
|
|
275
|
+
maxNoteLength: settings.displaySettings.maxNoteLength,
|
|
276
|
+
maxSelectionLength:
|
|
277
|
+
settings.displaySettings.maxSelectionLength,
|
|
278
|
+
},
|
|
279
|
+
vaultAccess,
|
|
280
|
+
vaultAccess, // IMentionService (same object)
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const userMessageContent: MessageContent[] = [];
|
|
284
|
+
|
|
285
|
+
if (prepared.autoMentionContext) {
|
|
286
|
+
userMessageContent.push({
|
|
287
|
+
type: "text_with_context",
|
|
288
|
+
text: content,
|
|
289
|
+
autoMentionContext: prepared.autoMentionContext,
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
userMessageContent.push({
|
|
293
|
+
type: "text",
|
|
294
|
+
text: content,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (options.images && options.images.length > 0) {
|
|
299
|
+
for (const img of options.images) {
|
|
300
|
+
userMessageContent.push({
|
|
301
|
+
type: "image",
|
|
302
|
+
data: img.data,
|
|
303
|
+
mimeType: img.mimeType,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (options.resourceLinks && options.resourceLinks.length > 0) {
|
|
309
|
+
for (const link of options.resourceLinks) {
|
|
310
|
+
userMessageContent.push({
|
|
311
|
+
type: "resource_link",
|
|
312
|
+
uri: link.uri,
|
|
313
|
+
name: link.name,
|
|
314
|
+
mimeType: link.mimeType,
|
|
315
|
+
size: link.size,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const userMessage: ChatMessage = {
|
|
321
|
+
id: crypto.randomUUID(),
|
|
322
|
+
role: "user",
|
|
323
|
+
content: userMessageContent,
|
|
324
|
+
timestamp: new Date(),
|
|
325
|
+
};
|
|
326
|
+
addMessage(userMessage);
|
|
327
|
+
|
|
328
|
+
setIsSending(true);
|
|
329
|
+
setLastUserMessage(content);
|
|
330
|
+
|
|
331
|
+
const sendPromise = (async () => {
|
|
332
|
+
try {
|
|
333
|
+
const result = await sendPreparedPrompt(
|
|
334
|
+
{
|
|
335
|
+
sessionId: currentSessionId,
|
|
336
|
+
agentContent: prepared.agentContent,
|
|
337
|
+
displayContent: prepared.displayContent,
|
|
338
|
+
authMethods: session.authMethods,
|
|
339
|
+
},
|
|
340
|
+
agentClient,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Discard results if a newer send has started
|
|
344
|
+
if (generationRef.current !== generation) return;
|
|
345
|
+
|
|
346
|
+
if (result.success) {
|
|
347
|
+
setIsSending(false);
|
|
348
|
+
setLastUserMessage(null);
|
|
349
|
+
} else {
|
|
350
|
+
setIsSending(false);
|
|
351
|
+
setErrorInfo(
|
|
352
|
+
result.error
|
|
353
|
+
? {
|
|
354
|
+
title: result.error.title,
|
|
355
|
+
message: result.error.message,
|
|
356
|
+
suggestion: result.error.suggestion,
|
|
357
|
+
}
|
|
358
|
+
: {
|
|
359
|
+
title: "Send Message Failed",
|
|
360
|
+
message: "Failed to send message",
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (generationRef.current !== generation) return;
|
|
366
|
+
setIsSending(false);
|
|
367
|
+
setErrorInfo({
|
|
368
|
+
title: "Send Message Failed",
|
|
369
|
+
message: `Failed to send message: ${extractErrorMessage(error)}`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
|
|
374
|
+
sendPromiseRef.current = sendPromise;
|
|
375
|
+
try {
|
|
376
|
+
await sendPromise;
|
|
377
|
+
} catch {
|
|
378
|
+
// Error already handled inside sendPromise
|
|
379
|
+
} finally {
|
|
380
|
+
sendPromiseRef.current = null;
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
[
|
|
384
|
+
agentClient,
|
|
385
|
+
vaultAccess,
|
|
386
|
+
settingsAccess,
|
|
387
|
+
session.sessionId,
|
|
388
|
+
session.authMethods,
|
|
389
|
+
session.promptCapabilities,
|
|
390
|
+
shouldConvertToWsl,
|
|
391
|
+
addMessage,
|
|
392
|
+
setErrorInfo,
|
|
393
|
+
],
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// ============================================================
|
|
397
|
+
// Permission State & Operations
|
|
398
|
+
// ============================================================
|
|
399
|
+
|
|
400
|
+
const activePermission = useMemo(
|
|
401
|
+
() => findActivePermission(messages),
|
|
402
|
+
[messages],
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const hasActivePermission = activePermission !== null;
|
|
406
|
+
|
|
407
|
+
const approvePermission = useCallback(
|
|
408
|
+
async (requestId: string, optionId: string): Promise<void> => {
|
|
409
|
+
try {
|
|
410
|
+
await agentClient.respondToPermission(requestId, optionId);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
setErrorInfo({
|
|
413
|
+
title: "Permission Error",
|
|
414
|
+
message: `Failed to respond to permission request: ${extractErrorMessage(error)}`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
[agentClient, setErrorInfo],
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const approveActivePermission = useCallback(async (): Promise<boolean> => {
|
|
422
|
+
if (!activePermission || activePermission.options.length === 0)
|
|
423
|
+
return false;
|
|
424
|
+
const option = selectOption(activePermission.options, [
|
|
425
|
+
"allow_once",
|
|
426
|
+
"allow_always",
|
|
427
|
+
]);
|
|
428
|
+
if (!option) return false;
|
|
429
|
+
await approvePermission(activePermission.requestId, option.optionId);
|
|
430
|
+
return true;
|
|
431
|
+
}, [activePermission, approvePermission]);
|
|
432
|
+
|
|
433
|
+
const rejectActivePermission = useCallback(async (): Promise<boolean> => {
|
|
434
|
+
if (!activePermission || activePermission.options.length === 0)
|
|
435
|
+
return false;
|
|
436
|
+
const option = selectOption(
|
|
437
|
+
activePermission.options,
|
|
438
|
+
["reject_once", "reject_always"],
|
|
439
|
+
(opt) =>
|
|
440
|
+
opt.name.toLowerCase().includes("reject") ||
|
|
441
|
+
opt.name.toLowerCase().includes("deny"),
|
|
442
|
+
);
|
|
443
|
+
if (!option) return false;
|
|
444
|
+
await approvePermission(activePermission.requestId, option.optionId);
|
|
445
|
+
return true;
|
|
446
|
+
}, [activePermission, approvePermission]);
|
|
447
|
+
|
|
448
|
+
// ============================================================
|
|
449
|
+
// Return
|
|
450
|
+
// ============================================================
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
messages,
|
|
454
|
+
isSending,
|
|
455
|
+
lastUserMessage,
|
|
456
|
+
sendMessage,
|
|
457
|
+
clearMessages,
|
|
458
|
+
setInitialMessages,
|
|
459
|
+
setMessagesFromLocal,
|
|
460
|
+
clearError,
|
|
461
|
+
setIgnoreUpdates,
|
|
462
|
+
clearPendingUpdates,
|
|
463
|
+
activePermission,
|
|
464
|
+
hasActivePermission,
|
|
465
|
+
approvePermission,
|
|
466
|
+
approveActivePermission,
|
|
467
|
+
rejectActivePermission,
|
|
468
|
+
enqueueUpdate,
|
|
469
|
+
};
|
|
470
|
+
}
|