@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,1162 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
const { useState, useRef, useEffect, useMemo, useCallback } = React;
|
|
3
|
+
import {
|
|
4
|
+
Notice,
|
|
5
|
+
FileSystemAdapter,
|
|
6
|
+
Platform,
|
|
7
|
+
Menu,
|
|
8
|
+
setIcon,
|
|
9
|
+
type MenuItem,
|
|
10
|
+
} from "obsidian";
|
|
11
|
+
|
|
12
|
+
import type { AttachedFile, ChatInputState } from "../types/chat";
|
|
13
|
+
import { isSameDirectory } from "../utils/platform";
|
|
14
|
+
import { useHistoryModal } from "../hooks/useHistoryModal";
|
|
15
|
+
import { useChatActions } from "../hooks/useChatActions";
|
|
16
|
+
import { ChangeDirectoryModal } from "./ChangeDirectoryModal";
|
|
17
|
+
|
|
18
|
+
// Service imports
|
|
19
|
+
import { getLogger } from "../utils/logger";
|
|
20
|
+
|
|
21
|
+
// Adapter imports
|
|
22
|
+
import type { AcpClient } from "../acp/acp-client";
|
|
23
|
+
|
|
24
|
+
// Context imports
|
|
25
|
+
import { useChatContext } from "./ChatContext";
|
|
26
|
+
|
|
27
|
+
// Hooks imports
|
|
28
|
+
import { useSettings } from "../hooks/useSettings";
|
|
29
|
+
import { useSuggestions } from "../hooks/useSuggestions";
|
|
30
|
+
import { useAgent } from "../hooks/useAgent";
|
|
31
|
+
import { useSessionHistory } from "../hooks/useSessionHistory";
|
|
32
|
+
|
|
33
|
+
// Domain model imports
|
|
34
|
+
import {
|
|
35
|
+
flattenConfigSelectOptions,
|
|
36
|
+
type SlashCommand,
|
|
37
|
+
type SessionModeState,
|
|
38
|
+
type SessionModelState,
|
|
39
|
+
type SessionConfigOption,
|
|
40
|
+
} from "../types/session";
|
|
41
|
+
import { checkAgentUpdate } from "../services/update-checker";
|
|
42
|
+
import { buildGeminiDeprecationNotice } from "../services/session-helpers";
|
|
43
|
+
|
|
44
|
+
/** Stable empty array for useSuggestions when no commands available */
|
|
45
|
+
const EMPTY_COMMANDS: SlashCommand[] = [];
|
|
46
|
+
|
|
47
|
+
// Component imports
|
|
48
|
+
import { ChatHeader } from "./ChatHeader";
|
|
49
|
+
import { MessageList } from "./MessageList";
|
|
50
|
+
import { InputArea } from "./InputArea";
|
|
51
|
+
import type { IChatViewHost } from "./view-host";
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// ChatPanelCallbacks - interface for class-level delegation
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Callbacks that ChatPanel registers with its parent container class.
|
|
59
|
+
* Used by ChatView / FloatingViewContainer to implement IChatViewContainer
|
|
60
|
+
* by delegating to the React component's state and handlers.
|
|
61
|
+
*/
|
|
62
|
+
export interface ChatPanelCallbacks {
|
|
63
|
+
getDisplayName: () => string;
|
|
64
|
+
getInputState: () => ChatInputState | null;
|
|
65
|
+
setInputState: (state: ChatInputState) => void;
|
|
66
|
+
canSend: () => boolean;
|
|
67
|
+
sendMessage: () => Promise<boolean>;
|
|
68
|
+
cancelOperation: () => Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// ChatPanelProps
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
export interface ChatPanelProps {
|
|
76
|
+
variant: "sidebar" | "floating";
|
|
77
|
+
viewId: string;
|
|
78
|
+
workingDirectory?: string;
|
|
79
|
+
initialAgentId?: string;
|
|
80
|
+
config?: { agent?: string; model?: string };
|
|
81
|
+
onRegisterCallbacks?: (callbacks: ChatPanelCallbacks) => void;
|
|
82
|
+
/** Called when agent ID changes (sidebar only — persists in Obsidian state) */
|
|
83
|
+
onAgentIdChanged?: (agentId: string) => void;
|
|
84
|
+
// Floating-specific
|
|
85
|
+
onMinimize?: () => void;
|
|
86
|
+
onClose?: () => void;
|
|
87
|
+
onOpenNewWindow?: () => void;
|
|
88
|
+
/** Mouse down handler for floating header drag area */
|
|
89
|
+
onFloatingHeaderMouseDown?: (e: React.MouseEvent) => void;
|
|
90
|
+
// Sidebar-specific: Obsidian view host for DOM event registration
|
|
91
|
+
viewHost?: IChatViewHost;
|
|
92
|
+
/** External container element for focus tracking (floating uses parent's container) */
|
|
93
|
+
containerEl?: HTMLElement | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// State Definitions
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
// Type definitions for Obsidian internal APIs (sidebar menu)
|
|
101
|
+
interface AppWithSettings {
|
|
102
|
+
setting: {
|
|
103
|
+
open: () => void;
|
|
104
|
+
openTabById: (id: string) => void;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// ChatPanel Component
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Core chat panel component that encapsulates all chat logic.
|
|
114
|
+
*
|
|
115
|
+
* This is the single source of truth for chat state and behavior,
|
|
116
|
+
* shared between sidebar (ChatView) and floating (FloatingChatView) variants.
|
|
117
|
+
* It is a 1:1 migration of useChatController into a React component,
|
|
118
|
+
* with workspace event handlers moved from ChatComponent/FloatingChatComponent.
|
|
119
|
+
*/
|
|
120
|
+
export function ChatPanel({
|
|
121
|
+
variant,
|
|
122
|
+
viewId,
|
|
123
|
+
workingDirectory,
|
|
124
|
+
initialAgentId,
|
|
125
|
+
config,
|
|
126
|
+
onRegisterCallbacks,
|
|
127
|
+
onAgentIdChanged,
|
|
128
|
+
onMinimize,
|
|
129
|
+
onClose,
|
|
130
|
+
onOpenNewWindow,
|
|
131
|
+
onFloatingHeaderMouseDown,
|
|
132
|
+
viewHost: viewHostProp,
|
|
133
|
+
containerEl: containerElProp,
|
|
134
|
+
}: ChatPanelProps) {
|
|
135
|
+
// ============================================================
|
|
136
|
+
// Platform Check
|
|
137
|
+
// ============================================================
|
|
138
|
+
if (!Platform.isDesktopApp) {
|
|
139
|
+
throw new Error("Agent Client is only available on desktop");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================
|
|
143
|
+
// Context
|
|
144
|
+
// ============================================================
|
|
145
|
+
const { plugin, acpClient, vaultService } = useChatContext();
|
|
146
|
+
|
|
147
|
+
// ============================================================
|
|
148
|
+
// Memoized Services & Adapters
|
|
149
|
+
// ============================================================
|
|
150
|
+
const logger = getLogger();
|
|
151
|
+
|
|
152
|
+
const vaultPath = useMemo(() => {
|
|
153
|
+
if (workingDirectory) {
|
|
154
|
+
return workingDirectory;
|
|
155
|
+
}
|
|
156
|
+
const adapter = plugin.app.vault.adapter;
|
|
157
|
+
if (adapter instanceof FileSystemAdapter) {
|
|
158
|
+
return adapter.getBasePath();
|
|
159
|
+
}
|
|
160
|
+
// Fallback for non-FileSystemAdapter (e.g., mobile)
|
|
161
|
+
return process.cwd();
|
|
162
|
+
}, [plugin, workingDirectory]);
|
|
163
|
+
|
|
164
|
+
// Agent working directory — defaults to vault path.
|
|
165
|
+
// Can be changed independently via "New chat in directory..." action.
|
|
166
|
+
const [agentCwd, setAgentCwd] = useState(vaultPath);
|
|
167
|
+
|
|
168
|
+
// ============================================================
|
|
169
|
+
// Custom Hooks
|
|
170
|
+
// ============================================================
|
|
171
|
+
const settings = useSettings(plugin);
|
|
172
|
+
|
|
173
|
+
const agent = useAgent(
|
|
174
|
+
acpClient,
|
|
175
|
+
plugin.settingsService,
|
|
176
|
+
vaultService,
|
|
177
|
+
agentCwd,
|
|
178
|
+
initialAgentId,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const {
|
|
182
|
+
session,
|
|
183
|
+
isReady: isSessionReady,
|
|
184
|
+
messages,
|
|
185
|
+
isSending,
|
|
186
|
+
errorInfo,
|
|
187
|
+
} = agent;
|
|
188
|
+
|
|
189
|
+
const suggestions = useSuggestions(
|
|
190
|
+
vaultService,
|
|
191
|
+
plugin,
|
|
192
|
+
session.availableCommands || EMPTY_COMMANDS,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Session history hook with callback for session load
|
|
196
|
+
const handleSessionLoad = useCallback(
|
|
197
|
+
(
|
|
198
|
+
sessionId: string,
|
|
199
|
+
modes?: SessionModeState,
|
|
200
|
+
models?: SessionModelState,
|
|
201
|
+
configOptions?: SessionConfigOption[],
|
|
202
|
+
) => {
|
|
203
|
+
logger.log(
|
|
204
|
+
`[ChatPanel] Session loaded/resumed/forked: ${sessionId}`,
|
|
205
|
+
{
|
|
206
|
+
modes,
|
|
207
|
+
models,
|
|
208
|
+
configOptions,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
void agent.updateSessionFromLoad(
|
|
212
|
+
sessionId,
|
|
213
|
+
modes,
|
|
214
|
+
models,
|
|
215
|
+
configOptions,
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
[logger, agent.updateSessionFromLoad],
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const sessionHistory = useSessionHistory({
|
|
222
|
+
agentClient: acpClient,
|
|
223
|
+
session,
|
|
224
|
+
settingsAccess: plugin.settingsService,
|
|
225
|
+
cwd: vaultPath,
|
|
226
|
+
agentCwd,
|
|
227
|
+
onSessionLoad: handleSessionLoad,
|
|
228
|
+
onMessagesRestore: agent.setMessagesFromLocal,
|
|
229
|
+
onIgnoreUpdates: agent.setIgnoreUpdates,
|
|
230
|
+
onClearMessages: agent.clearMessages,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ============================================================
|
|
234
|
+
// Local State
|
|
235
|
+
// ============================================================
|
|
236
|
+
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
|
|
237
|
+
|
|
238
|
+
// Input state (for broadcast commands)
|
|
239
|
+
const [inputValue, setInputValue] = useState("");
|
|
240
|
+
const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
|
|
241
|
+
|
|
242
|
+
// ============================================================
|
|
243
|
+
// Refs
|
|
244
|
+
// ============================================================
|
|
245
|
+
const terminalClientRef = useRef<AcpClient>(acpClient);
|
|
246
|
+
|
|
247
|
+
// ============================================================
|
|
248
|
+
// Computed Values
|
|
249
|
+
// ============================================================
|
|
250
|
+
const activeAgentLabel = useMemo(() => {
|
|
251
|
+
const activeId = session.agentId;
|
|
252
|
+
if (activeId === plugin.settings.claude.id) {
|
|
253
|
+
return (
|
|
254
|
+
plugin.settings.claude.displayName || plugin.settings.claude.id
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (activeId === plugin.settings.codex.id) {
|
|
258
|
+
return (
|
|
259
|
+
plugin.settings.codex.displayName || plugin.settings.codex.id
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (activeId === plugin.settings.gemini.id) {
|
|
263
|
+
return (
|
|
264
|
+
plugin.settings.gemini.displayName || plugin.settings.gemini.id
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const custom = plugin.settings.customAgents.find(
|
|
268
|
+
(agent) => agent.id === activeId,
|
|
269
|
+
);
|
|
270
|
+
return custom?.displayName || custom?.id || activeId;
|
|
271
|
+
}, [session.agentId, plugin.settings]);
|
|
272
|
+
|
|
273
|
+
const availableAgents = useMemo(() => {
|
|
274
|
+
return plugin.getAvailableAgents();
|
|
275
|
+
}, [plugin]);
|
|
276
|
+
|
|
277
|
+
// ============================================================
|
|
278
|
+
// Chat Actions
|
|
279
|
+
// ============================================================
|
|
280
|
+
const actions = useChatActions(
|
|
281
|
+
plugin,
|
|
282
|
+
agent,
|
|
283
|
+
sessionHistory,
|
|
284
|
+
suggestions,
|
|
285
|
+
session,
|
|
286
|
+
messages,
|
|
287
|
+
settings,
|
|
288
|
+
vaultPath,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const {
|
|
292
|
+
handleSendMessage,
|
|
293
|
+
handleStopGeneration,
|
|
294
|
+
handleNewChat,
|
|
295
|
+
handleExportChat,
|
|
296
|
+
handleSwitchAgent,
|
|
297
|
+
handleRestartAgent,
|
|
298
|
+
handleSetMode,
|
|
299
|
+
handleSetModel,
|
|
300
|
+
handleSetConfigOption,
|
|
301
|
+
handleClearError,
|
|
302
|
+
handleClearAgentUpdate,
|
|
303
|
+
handleRestoredMessageConsumed,
|
|
304
|
+
restoredMessage,
|
|
305
|
+
agentUpdateNotification,
|
|
306
|
+
setAgentUpdateNotification,
|
|
307
|
+
autoExportIfEnabled,
|
|
308
|
+
} = actions;
|
|
309
|
+
|
|
310
|
+
// ============================================================
|
|
311
|
+
// Gemini CLI deprecation notice (static, agent-id driven)
|
|
312
|
+
// ============================================================
|
|
313
|
+
// Independent channel from the npm-backed agentUpdateNotification:
|
|
314
|
+
// derived synchronously from the active agent id (no network).
|
|
315
|
+
const geminiNotice = useMemo(
|
|
316
|
+
() =>
|
|
317
|
+
session.agentId === plugin.settings.gemini.id
|
|
318
|
+
? buildGeminiDeprecationNotice()
|
|
319
|
+
: null,
|
|
320
|
+
[session.agentId, plugin.settings.gemini.id],
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Dismiss state lives locally in ChatPanel so it never races with the
|
|
324
|
+
// async setAgentUpdateNotification owned by useChatActions.
|
|
325
|
+
const [geminiNoticeDismissed, setGeminiNoticeDismissed] = useState(false);
|
|
326
|
+
|
|
327
|
+
// Re-show the notice when switching agents (e.g. away and back to Gemini).
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
setGeminiNoticeDismissed(false);
|
|
330
|
+
}, [session.agentId]);
|
|
331
|
+
|
|
332
|
+
const effectiveGeminiNotice =
|
|
333
|
+
geminiNotice && !geminiNoticeDismissed ? geminiNotice : null;
|
|
334
|
+
|
|
335
|
+
const handleClearGeminiNotice = useCallback(
|
|
336
|
+
() => setGeminiNoticeDismissed(true),
|
|
337
|
+
[],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Wrap send so the Gemini notice also dismisses on send, mirroring how
|
|
341
|
+
// useChatActions clears agentUpdateNotification inside handleSendMessage.
|
|
342
|
+
const handleSendMessageWithGeminiDismiss = useCallback(
|
|
343
|
+
(content: string, attachments?: AttachedFile[]) => {
|
|
344
|
+
setGeminiNoticeDismissed(true);
|
|
345
|
+
return handleSendMessage(content, attachments);
|
|
346
|
+
},
|
|
347
|
+
[handleSendMessage],
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const { handleOpenHistory } = useHistoryModal(
|
|
351
|
+
plugin,
|
|
352
|
+
agent,
|
|
353
|
+
sessionHistory,
|
|
354
|
+
vaultPath,
|
|
355
|
+
isSessionReady,
|
|
356
|
+
settings.debugMode,
|
|
357
|
+
setAgentCwd,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// ============================================================
|
|
361
|
+
// Sidebar-specific: handleNewChat wrapper that persists agent ID
|
|
362
|
+
// ============================================================
|
|
363
|
+
const handleNewChatWithPersist = useCallback(
|
|
364
|
+
async (requestedAgentId?: string) => {
|
|
365
|
+
await handleNewChat(requestedAgentId);
|
|
366
|
+
// Persist agent ID for this view (survives Obsidian restart)
|
|
367
|
+
if (requestedAgentId) {
|
|
368
|
+
onAgentIdChanged?.(requestedAgentId);
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
[handleNewChat, onAgentIdChanged],
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// ============================================================
|
|
375
|
+
// Sidebar-specific: Header Menu (Obsidian native Menu API)
|
|
376
|
+
// ============================================================
|
|
377
|
+
const handleOpenSettings = useCallback(() => {
|
|
378
|
+
const appWithSettings = plugin.app as unknown as AppWithSettings;
|
|
379
|
+
appWithSettings.setting.open();
|
|
380
|
+
appWithSettings.setting.openTabById(plugin.manifest.id);
|
|
381
|
+
}, [plugin]);
|
|
382
|
+
|
|
383
|
+
const handleNewChatInDirectory = useCallback(
|
|
384
|
+
async (directory: string) => {
|
|
385
|
+
// Auto-export current chat before switching
|
|
386
|
+
if (messages.length > 0) {
|
|
387
|
+
await autoExportIfEnabled("newChat", messages, session);
|
|
388
|
+
}
|
|
389
|
+
agent.clearMessages();
|
|
390
|
+
setAgentCwd(directory);
|
|
391
|
+
await agent.restartSession(undefined, directory);
|
|
392
|
+
sessionHistory.invalidateCache();
|
|
393
|
+
},
|
|
394
|
+
[
|
|
395
|
+
messages,
|
|
396
|
+
session,
|
|
397
|
+
autoExportIfEnabled,
|
|
398
|
+
agent.clearMessages,
|
|
399
|
+
agent.restartSession,
|
|
400
|
+
sessionHistory.invalidateCache,
|
|
401
|
+
],
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const handleShowSidebarMenu = useCallback(
|
|
405
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
406
|
+
const menu = new Menu();
|
|
407
|
+
|
|
408
|
+
// -- Switch agent section --
|
|
409
|
+
menu.addItem((item: MenuItem) => {
|
|
410
|
+
item.setTitle("Switch agent").setIsLabel(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
for (const agent of availableAgents) {
|
|
414
|
+
menu.addItem((item: MenuItem) => {
|
|
415
|
+
item.setTitle(agent.displayName)
|
|
416
|
+
.setChecked(agent.id === (session.agentId || ""))
|
|
417
|
+
.onClick(() => {
|
|
418
|
+
void handleNewChatWithPersist(agent.id);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
menu.addSeparator();
|
|
424
|
+
|
|
425
|
+
// -- Actions section --
|
|
426
|
+
menu.addItem((item: MenuItem) => {
|
|
427
|
+
item.setTitle("Open new view")
|
|
428
|
+
.setIcon("copy-plus")
|
|
429
|
+
.onClick(() => {
|
|
430
|
+
void plugin.openNewChatViewWithAgent(
|
|
431
|
+
plugin.settings.defaultAgentId,
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
menu.addItem((item: MenuItem) => {
|
|
437
|
+
item.setTitle("Restart agent")
|
|
438
|
+
.setIcon("refresh-cw")
|
|
439
|
+
.onClick(() => {
|
|
440
|
+
void handleRestartAgent();
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
menu.addItem((item: MenuItem) => {
|
|
445
|
+
item.setTitle("New chat in directory...")
|
|
446
|
+
.setIcon("folder-open")
|
|
447
|
+
.onClick(() => {
|
|
448
|
+
const modal = new ChangeDirectoryModal(
|
|
449
|
+
plugin.app,
|
|
450
|
+
agentCwd,
|
|
451
|
+
(directory) => {
|
|
452
|
+
void handleNewChatInDirectory(directory);
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
modal.open();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
menu.addSeparator();
|
|
460
|
+
|
|
461
|
+
menu.addItem((item: MenuItem) => {
|
|
462
|
+
item.setTitle("Plugin settings")
|
|
463
|
+
.setIcon("settings")
|
|
464
|
+
.onClick(() => {
|
|
465
|
+
handleOpenSettings();
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
menu.showAtMouseEvent(e.nativeEvent);
|
|
470
|
+
},
|
|
471
|
+
[
|
|
472
|
+
availableAgents,
|
|
473
|
+
session.agentId,
|
|
474
|
+
handleNewChatWithPersist,
|
|
475
|
+
plugin,
|
|
476
|
+
handleRestartAgent,
|
|
477
|
+
agentCwd,
|
|
478
|
+
handleNewChatInDirectory,
|
|
479
|
+
handleOpenSettings,
|
|
480
|
+
],
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const handleShowFloatingMenu = useCallback(
|
|
484
|
+
(e: React.MouseEvent<HTMLElement>) => {
|
|
485
|
+
const menu = new Menu();
|
|
486
|
+
|
|
487
|
+
menu.addItem((item: MenuItem) => {
|
|
488
|
+
item.setTitle("New chat")
|
|
489
|
+
.setIcon("plus")
|
|
490
|
+
.onClick(() => {
|
|
491
|
+
void handleNewChat();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
menu.addItem((item: MenuItem) => {
|
|
496
|
+
item.setTitle("Session history")
|
|
497
|
+
.setIcon("history")
|
|
498
|
+
.onClick(() => {
|
|
499
|
+
void handleOpenHistory();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
menu.addItem((item: MenuItem) => {
|
|
504
|
+
item.setTitle("Export chat to Markdown")
|
|
505
|
+
.setIcon("save")
|
|
506
|
+
.onClick(() => {
|
|
507
|
+
void handleExportChat();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
menu.addSeparator();
|
|
512
|
+
|
|
513
|
+
if (onOpenNewWindow) {
|
|
514
|
+
menu.addItem((item: MenuItem) => {
|
|
515
|
+
item.setTitle("Open new floating chat")
|
|
516
|
+
.setIcon("copy-plus")
|
|
517
|
+
.onClick(() => {
|
|
518
|
+
onOpenNewWindow();
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
menu.addItem((item: MenuItem) => {
|
|
524
|
+
item.setTitle("Restart agent")
|
|
525
|
+
.setIcon("refresh-cw")
|
|
526
|
+
.onClick(() => {
|
|
527
|
+
void handleRestartAgent();
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
menu.addItem((item: MenuItem) => {
|
|
532
|
+
item.setTitle("New chat in directory...")
|
|
533
|
+
.setIcon("folder-open")
|
|
534
|
+
.onClick(() => {
|
|
535
|
+
const modal = new ChangeDirectoryModal(
|
|
536
|
+
plugin.app,
|
|
537
|
+
agentCwd,
|
|
538
|
+
(directory) => {
|
|
539
|
+
void handleNewChatInDirectory(directory);
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
modal.open();
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
menu.addSeparator();
|
|
547
|
+
|
|
548
|
+
menu.addItem((item: MenuItem) => {
|
|
549
|
+
item.setTitle("Plugin settings")
|
|
550
|
+
.setIcon("settings")
|
|
551
|
+
.onClick(() => {
|
|
552
|
+
handleOpenSettings();
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
menu.showAtMouseEvent(e.nativeEvent);
|
|
557
|
+
},
|
|
558
|
+
[
|
|
559
|
+
handleNewChat,
|
|
560
|
+
handleOpenHistory,
|
|
561
|
+
handleExportChat,
|
|
562
|
+
onOpenNewWindow,
|
|
563
|
+
handleRestartAgent,
|
|
564
|
+
agentCwd,
|
|
565
|
+
handleNewChatInDirectory,
|
|
566
|
+
handleOpenSettings,
|
|
567
|
+
],
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// ============================================================
|
|
571
|
+
// viewHost creation for child components
|
|
572
|
+
// ============================================================
|
|
573
|
+
// Track registered listeners for cleanup (floating variant)
|
|
574
|
+
const registeredListenersRef = useRef<
|
|
575
|
+
{
|
|
576
|
+
target: Window | Document | HTMLElement;
|
|
577
|
+
type: string;
|
|
578
|
+
callback: EventListenerOrEventListenerObject;
|
|
579
|
+
}[]
|
|
580
|
+
>([]);
|
|
581
|
+
|
|
582
|
+
const viewHost: IChatViewHost = useMemo(() => {
|
|
583
|
+
// Sidebar: use the provided viewHost from the ChatView class
|
|
584
|
+
if (viewHostProp) {
|
|
585
|
+
return viewHostProp;
|
|
586
|
+
}
|
|
587
|
+
// Floating: create a shim with listener tracking
|
|
588
|
+
return {
|
|
589
|
+
app: plugin.app,
|
|
590
|
+
registerDomEvent: ((
|
|
591
|
+
target: Window | Document | HTMLElement,
|
|
592
|
+
type: string,
|
|
593
|
+
callback: EventListenerOrEventListenerObject,
|
|
594
|
+
) => {
|
|
595
|
+
target.addEventListener(type, callback);
|
|
596
|
+
registeredListenersRef.current.push({ target, type, callback });
|
|
597
|
+
}),
|
|
598
|
+
};
|
|
599
|
+
}, [viewHostProp, plugin.app]);
|
|
600
|
+
|
|
601
|
+
// Cleanup registered listeners on unmount (floating variant)
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
return () => {
|
|
604
|
+
for (const {
|
|
605
|
+
target,
|
|
606
|
+
type,
|
|
607
|
+
callback,
|
|
608
|
+
} of registeredListenersRef.current) {
|
|
609
|
+
target.removeEventListener(type, callback);
|
|
610
|
+
}
|
|
611
|
+
registeredListenersRef.current = [];
|
|
612
|
+
};
|
|
613
|
+
}, []);
|
|
614
|
+
|
|
615
|
+
// ============================================================
|
|
616
|
+
// Effects - Session Lifecycle
|
|
617
|
+
// ============================================================
|
|
618
|
+
// Initialize session on mount
|
|
619
|
+
useEffect(() => {
|
|
620
|
+
logger.log("[Debug] Starting connection setup via useSession...");
|
|
621
|
+
void agent.createSession(config?.agent || initialAgentId);
|
|
622
|
+
}, [agent.createSession, config?.agent, initialAgentId]);
|
|
623
|
+
|
|
624
|
+
// Apply configured model when session is ready
|
|
625
|
+
useEffect(() => {
|
|
626
|
+
if (!config?.model || !isSessionReady) return;
|
|
627
|
+
|
|
628
|
+
// Prefer configOptions if available
|
|
629
|
+
if (session.configOptions) {
|
|
630
|
+
const modelOption = session.configOptions.find(
|
|
631
|
+
(o) => o.category === "model",
|
|
632
|
+
);
|
|
633
|
+
if (modelOption && modelOption.currentValue !== config.model) {
|
|
634
|
+
const valueExists = flattenConfigSelectOptions(
|
|
635
|
+
modelOption.options,
|
|
636
|
+
).some((o) => o.value === config.model);
|
|
637
|
+
if (valueExists) {
|
|
638
|
+
logger.log(
|
|
639
|
+
"[ChatPanel] Applying configured model via configOptions:",
|
|
640
|
+
config.model,
|
|
641
|
+
);
|
|
642
|
+
void agent.setConfigOption(modelOption.id, config.model);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Fallback to legacy models
|
|
649
|
+
if (session.models) {
|
|
650
|
+
const modelExists = session.models.availableModels.some(
|
|
651
|
+
(m) => m.modelId === config.model,
|
|
652
|
+
);
|
|
653
|
+
if (modelExists && session.models.currentModelId !== config.model) {
|
|
654
|
+
logger.log(
|
|
655
|
+
"[ChatPanel] Applying configured model:",
|
|
656
|
+
config.model,
|
|
657
|
+
);
|
|
658
|
+
void agent.setModel(config.model);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}, [
|
|
662
|
+
config?.model,
|
|
663
|
+
isSessionReady,
|
|
664
|
+
session.configOptions,
|
|
665
|
+
session.models,
|
|
666
|
+
agent.setConfigOption,
|
|
667
|
+
agent.setModel,
|
|
668
|
+
logger,
|
|
669
|
+
]);
|
|
670
|
+
|
|
671
|
+
// Refs for cleanup (to access latest values in cleanup function)
|
|
672
|
+
const messagesRef = useRef(messages);
|
|
673
|
+
const sessionRef = useRef(session);
|
|
674
|
+
const autoExportRef = useRef(autoExportIfEnabled);
|
|
675
|
+
const closeSessionRef = useRef(agent.closeSession);
|
|
676
|
+
messagesRef.current = messages;
|
|
677
|
+
sessionRef.current = session;
|
|
678
|
+
autoExportRef.current = autoExportIfEnabled;
|
|
679
|
+
closeSessionRef.current = agent.closeSession;
|
|
680
|
+
|
|
681
|
+
// Cleanup on unmount only - auto-export and close session
|
|
682
|
+
useEffect(() => {
|
|
683
|
+
return () => {
|
|
684
|
+
logger.log("[ChatPanel] Cleanup: auto-export and close session");
|
|
685
|
+
void (async () => {
|
|
686
|
+
await autoExportRef.current(
|
|
687
|
+
"closeChat",
|
|
688
|
+
messagesRef.current,
|
|
689
|
+
sessionRef.current,
|
|
690
|
+
);
|
|
691
|
+
await closeSessionRef.current();
|
|
692
|
+
})();
|
|
693
|
+
};
|
|
694
|
+
}, [logger]);
|
|
695
|
+
|
|
696
|
+
// ============================================================
|
|
697
|
+
// Effects - Update Check
|
|
698
|
+
// ============================================================
|
|
699
|
+
useEffect(() => {
|
|
700
|
+
plugin
|
|
701
|
+
.checkForUpdates()
|
|
702
|
+
.then(setIsUpdateAvailable)
|
|
703
|
+
.catch((error) => {
|
|
704
|
+
logger.error("Failed to check for updates:", error);
|
|
705
|
+
});
|
|
706
|
+
}, [plugin, logger]);
|
|
707
|
+
|
|
708
|
+
// ============================================================
|
|
709
|
+
// Effects - Agent Update Check
|
|
710
|
+
// ============================================================
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
if (!isSessionReady || !session.agentInfo?.name) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
checkAgentUpdate(
|
|
717
|
+
session.agentInfo as { name: string; version?: string },
|
|
718
|
+
)
|
|
719
|
+
.then(setAgentUpdateNotification)
|
|
720
|
+
.catch((error) => {
|
|
721
|
+
logger.error("Failed to check agent update:", error);
|
|
722
|
+
});
|
|
723
|
+
}, [isSessionReady, session.agentInfo, logger]);
|
|
724
|
+
|
|
725
|
+
// ============================================================
|
|
726
|
+
// Effects - Save Session Messages on Turn End
|
|
727
|
+
// ============================================================
|
|
728
|
+
const prevIsSendingRef = useRef<boolean>(false);
|
|
729
|
+
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
const wasSending = prevIsSendingRef.current;
|
|
732
|
+
prevIsSendingRef.current = isSending;
|
|
733
|
+
|
|
734
|
+
// Save when turn ends (isSending: true -> false) and has messages
|
|
735
|
+
if (
|
|
736
|
+
wasSending &&
|
|
737
|
+
!isSending &&
|
|
738
|
+
session.sessionId &&
|
|
739
|
+
messages.length > 0
|
|
740
|
+
) {
|
|
741
|
+
sessionHistory.saveSessionMessages(session.sessionId, messages);
|
|
742
|
+
logger.log(
|
|
743
|
+
`[ChatPanel] Session messages saved: ${session.sessionId}`,
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
// System notification on response completion
|
|
747
|
+
if (settings.enableSystemNotifications && !activeDocument.hasFocus()) {
|
|
748
|
+
new Notification("Agent Client", {
|
|
749
|
+
body: `${activeAgentLabel} has completed the response.`,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}, [
|
|
754
|
+
isSending,
|
|
755
|
+
session.sessionId,
|
|
756
|
+
messages,
|
|
757
|
+
sessionHistory.saveSessionMessages,
|
|
758
|
+
settings.enableSystemNotifications,
|
|
759
|
+
activeAgentLabel,
|
|
760
|
+
logger,
|
|
761
|
+
]);
|
|
762
|
+
|
|
763
|
+
// ============================================================
|
|
764
|
+
// Effects - System Notification on Permission Request
|
|
765
|
+
// ============================================================
|
|
766
|
+
const prevHasActivePermissionRef = useRef<boolean>(false);
|
|
767
|
+
|
|
768
|
+
useEffect(() => {
|
|
769
|
+
const wasActive = prevHasActivePermissionRef.current;
|
|
770
|
+
prevHasActivePermissionRef.current = agent.hasActivePermission;
|
|
771
|
+
|
|
772
|
+
// Notify when permission transitions from inactive to active
|
|
773
|
+
if (
|
|
774
|
+
!wasActive &&
|
|
775
|
+
agent.hasActivePermission &&
|
|
776
|
+
settings.enableSystemNotifications &&
|
|
777
|
+
!activeDocument.hasFocus()
|
|
778
|
+
) {
|
|
779
|
+
new Notification("Agent Client", {
|
|
780
|
+
body: `${activeAgentLabel} is requesting permission.`,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}, [
|
|
784
|
+
agent.hasActivePermission,
|
|
785
|
+
settings.enableSystemNotifications,
|
|
786
|
+
activeAgentLabel,
|
|
787
|
+
]);
|
|
788
|
+
|
|
789
|
+
// ============================================================
|
|
790
|
+
// Effects - Auto-mention Active Note Tracking
|
|
791
|
+
// ============================================================
|
|
792
|
+
useEffect(() => {
|
|
793
|
+
let isMounted = true;
|
|
794
|
+
|
|
795
|
+
const refreshActiveNote = async () => {
|
|
796
|
+
if (!isMounted) return;
|
|
797
|
+
await suggestions.mentions.updateActiveNote();
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
const unsubscribe = vaultService.subscribeSelectionChanges(() => {
|
|
801
|
+
void refreshActiveNote();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
void refreshActiveNote();
|
|
805
|
+
|
|
806
|
+
return () => {
|
|
807
|
+
isMounted = false;
|
|
808
|
+
unsubscribe();
|
|
809
|
+
};
|
|
810
|
+
}, [suggestions.mentions.updateActiveNote, vaultService]);
|
|
811
|
+
|
|
812
|
+
// ============================================================
|
|
813
|
+
// Effects - Workspace Events (Hotkeys)
|
|
814
|
+
// ============================================================
|
|
815
|
+
|
|
816
|
+
// Refs for workspace event handlers (avoids re-registering on every render)
|
|
817
|
+
const handleNewChatWithPersistRef = useRef(handleNewChatWithPersist);
|
|
818
|
+
const handleNewChatRef = useRef(handleNewChat);
|
|
819
|
+
const approveActivePermissionRef = useRef(agent.approveActivePermission);
|
|
820
|
+
const rejectActivePermissionRef = useRef(agent.rejectActivePermission);
|
|
821
|
+
const handleStopGenerationRef = useRef(handleStopGeneration);
|
|
822
|
+
const handleExportChatRef = useRef(handleExportChat);
|
|
823
|
+
handleNewChatWithPersistRef.current = handleNewChatWithPersist;
|
|
824
|
+
handleNewChatRef.current = handleNewChat;
|
|
825
|
+
approveActivePermissionRef.current = agent.approveActivePermission;
|
|
826
|
+
rejectActivePermissionRef.current = agent.rejectActivePermission;
|
|
827
|
+
handleStopGenerationRef.current = handleStopGeneration;
|
|
828
|
+
handleExportChatRef.current = handleExportChat;
|
|
829
|
+
|
|
830
|
+
useEffect(() => {
|
|
831
|
+
const workspace = plugin.app.workspace;
|
|
832
|
+
const ws = workspace as unknown as {
|
|
833
|
+
on: (
|
|
834
|
+
name: string,
|
|
835
|
+
callback: (...args: never[]) => void,
|
|
836
|
+
) => ReturnType<typeof workspace.on>;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
const refs = [
|
|
840
|
+
// Toggle auto-mention
|
|
841
|
+
ws.on(
|
|
842
|
+
"agent-client:toggle-auto-mention",
|
|
843
|
+
(targetViewId?: string) => {
|
|
844
|
+
if (targetViewId && targetViewId !== viewId) return;
|
|
845
|
+
suggestions.mentions.toggleAutoMention();
|
|
846
|
+
},
|
|
847
|
+
),
|
|
848
|
+
|
|
849
|
+
// New chat requested (from "New chat" or "Switch agent to" commands)
|
|
850
|
+
ws.on(
|
|
851
|
+
"agent-client:new-chat-requested",
|
|
852
|
+
(targetViewId?: string, agentId?: string) => {
|
|
853
|
+
if (targetViewId && targetViewId !== viewId) return;
|
|
854
|
+
if (variant === "sidebar") {
|
|
855
|
+
void handleNewChatWithPersistRef.current(agentId);
|
|
856
|
+
} else {
|
|
857
|
+
void handleNewChatRef.current(agentId);
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
),
|
|
861
|
+
|
|
862
|
+
// Approve active permission
|
|
863
|
+
ws.on(
|
|
864
|
+
"agent-client:approve-active-permission",
|
|
865
|
+
(targetViewId?: string) => {
|
|
866
|
+
if (targetViewId && targetViewId !== viewId) return;
|
|
867
|
+
void (async () => {
|
|
868
|
+
const success =
|
|
869
|
+
await approveActivePermissionRef.current();
|
|
870
|
+
if (!success) {
|
|
871
|
+
new Notice(
|
|
872
|
+
"[Agent Client] No active permission request",
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
})();
|
|
876
|
+
},
|
|
877
|
+
),
|
|
878
|
+
|
|
879
|
+
// Reject active permission
|
|
880
|
+
ws.on(
|
|
881
|
+
"agent-client:reject-active-permission",
|
|
882
|
+
(targetViewId?: string) => {
|
|
883
|
+
if (targetViewId && targetViewId !== viewId) return;
|
|
884
|
+
void (async () => {
|
|
885
|
+
const success =
|
|
886
|
+
await rejectActivePermissionRef.current();
|
|
887
|
+
if (!success) {
|
|
888
|
+
new Notice(
|
|
889
|
+
"[Agent Client] No active permission request",
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
})();
|
|
893
|
+
},
|
|
894
|
+
),
|
|
895
|
+
|
|
896
|
+
// Cancel current message
|
|
897
|
+
ws.on("agent-client:cancel-message", (targetViewId?: string) => {
|
|
898
|
+
if (targetViewId && targetViewId !== viewId) return;
|
|
899
|
+
void handleStopGenerationRef.current();
|
|
900
|
+
}),
|
|
901
|
+
|
|
902
|
+
// Export chat
|
|
903
|
+
ws.on("agent-client:export-chat", (targetViewId?: string) => {
|
|
904
|
+
if (targetViewId && targetViewId !== viewId) return;
|
|
905
|
+
void handleExportChatRef.current();
|
|
906
|
+
}),
|
|
907
|
+
];
|
|
908
|
+
|
|
909
|
+
return () => {
|
|
910
|
+
for (const ref of refs) {
|
|
911
|
+
workspace.offref(ref);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
}, [
|
|
915
|
+
plugin.app.workspace,
|
|
916
|
+
plugin.lastActiveChatViewId,
|
|
917
|
+
viewId,
|
|
918
|
+
variant,
|
|
919
|
+
suggestions.mentions.toggleAutoMention,
|
|
920
|
+
]);
|
|
921
|
+
|
|
922
|
+
// ============================================================
|
|
923
|
+
// Effects - Focus Tracking
|
|
924
|
+
// ============================================================
|
|
925
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
926
|
+
useEffect(() => {
|
|
927
|
+
const handleFocus = () => {
|
|
928
|
+
plugin.setLastActiveChatViewId(viewId);
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const container = containerElProp ?? containerRef.current;
|
|
932
|
+
if (!container) return;
|
|
933
|
+
|
|
934
|
+
container.addEventListener("focus", handleFocus, true);
|
|
935
|
+
container.addEventListener("click", handleFocus);
|
|
936
|
+
|
|
937
|
+
// Set as active on mount (first opened view becomes active)
|
|
938
|
+
plugin.setLastActiveChatViewId(viewId);
|
|
939
|
+
|
|
940
|
+
return () => {
|
|
941
|
+
container.removeEventListener("focus", handleFocus, true);
|
|
942
|
+
container.removeEventListener("click", handleFocus);
|
|
943
|
+
};
|
|
944
|
+
}, [plugin, viewId, containerElProp]);
|
|
945
|
+
|
|
946
|
+
// ============================================================
|
|
947
|
+
// Callback Registration for IChatViewContainer
|
|
948
|
+
// ============================================================
|
|
949
|
+
// Use refs so callbacks always access latest values
|
|
950
|
+
const inputValueRef = useRef(inputValue);
|
|
951
|
+
const attachedFilesRef = useRef(attachedFiles);
|
|
952
|
+
const isSessionReadyRef = useRef(isSessionReady);
|
|
953
|
+
const isSendingRef = useRef(isSending);
|
|
954
|
+
const sessionHistoryLoadingRef = useRef(sessionHistory.loading);
|
|
955
|
+
const handleSendMessageRef = useRef(handleSendMessage);
|
|
956
|
+
inputValueRef.current = inputValue;
|
|
957
|
+
attachedFilesRef.current = attachedFiles;
|
|
958
|
+
isSessionReadyRef.current = isSessionReady;
|
|
959
|
+
isSendingRef.current = isSending;
|
|
960
|
+
sessionHistoryLoadingRef.current = sessionHistory.loading;
|
|
961
|
+
handleSendMessageRef.current = handleSendMessage;
|
|
962
|
+
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
onRegisterCallbacks?.({
|
|
965
|
+
getDisplayName: () => activeAgentLabel,
|
|
966
|
+
getInputState: () => ({
|
|
967
|
+
text: inputValueRef.current,
|
|
968
|
+
files: attachedFilesRef.current,
|
|
969
|
+
}),
|
|
970
|
+
setInputState: (state) => {
|
|
971
|
+
setInputValue(state.text);
|
|
972
|
+
setAttachedFiles(state.files);
|
|
973
|
+
},
|
|
974
|
+
canSend: () => {
|
|
975
|
+
const hasContent =
|
|
976
|
+
inputValueRef.current.trim() !== "" ||
|
|
977
|
+
attachedFilesRef.current.length > 0;
|
|
978
|
+
return (
|
|
979
|
+
hasContent &&
|
|
980
|
+
isSessionReadyRef.current &&
|
|
981
|
+
!sessionHistoryLoadingRef.current &&
|
|
982
|
+
!isSendingRef.current
|
|
983
|
+
);
|
|
984
|
+
},
|
|
985
|
+
sendMessage: async () => {
|
|
986
|
+
const currentInput = inputValueRef.current;
|
|
987
|
+
const currentFiles = attachedFilesRef.current;
|
|
988
|
+
// Allow sending if there's text OR attachments
|
|
989
|
+
if (!currentInput.trim() && currentFiles.length === 0) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
if (
|
|
993
|
+
!isSessionReadyRef.current ||
|
|
994
|
+
sessionHistoryLoadingRef.current
|
|
995
|
+
) {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
if (isSendingRef.current) {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Clear input before sending
|
|
1003
|
+
const messageToSend = currentInput.trim();
|
|
1004
|
+
const filesToSend =
|
|
1005
|
+
currentFiles.length > 0 ? [...currentFiles] : undefined;
|
|
1006
|
+
setInputValue("");
|
|
1007
|
+
setAttachedFiles([]);
|
|
1008
|
+
|
|
1009
|
+
await handleSendMessageRef.current(messageToSend, filesToSend);
|
|
1010
|
+
return true;
|
|
1011
|
+
},
|
|
1012
|
+
cancelOperation: async () => {
|
|
1013
|
+
if (isSendingRef.current) {
|
|
1014
|
+
await handleStopGenerationRef.current();
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
}, [onRegisterCallbacks, activeAgentLabel]);
|
|
1019
|
+
|
|
1020
|
+
// ============================================================
|
|
1021
|
+
// Render
|
|
1022
|
+
// ============================================================
|
|
1023
|
+
const chatFontSizeStyle =
|
|
1024
|
+
settings.displaySettings.fontSize !== null
|
|
1025
|
+
? ({
|
|
1026
|
+
"--ac-chat-font-size": `${settings.displaySettings.fontSize}px`,
|
|
1027
|
+
} as React.CSSProperties)
|
|
1028
|
+
: undefined;
|
|
1029
|
+
|
|
1030
|
+
const headerElement =
|
|
1031
|
+
variant === "sidebar" ? (
|
|
1032
|
+
<ChatHeader
|
|
1033
|
+
variant="sidebar"
|
|
1034
|
+
agentLabel={activeAgentLabel}
|
|
1035
|
+
isUpdateAvailable={isUpdateAvailable}
|
|
1036
|
+
onNewChat={() => void handleNewChatWithPersist()}
|
|
1037
|
+
onExportChat={() => void handleExportChat()}
|
|
1038
|
+
onShowMenu={handleShowSidebarMenu}
|
|
1039
|
+
onOpenHistory={handleOpenHistory}
|
|
1040
|
+
/>
|
|
1041
|
+
) : (
|
|
1042
|
+
<ChatHeader
|
|
1043
|
+
variant="floating"
|
|
1044
|
+
agentLabel={activeAgentLabel}
|
|
1045
|
+
availableAgents={availableAgents}
|
|
1046
|
+
currentAgentId={session.agentId}
|
|
1047
|
+
isUpdateAvailable={isUpdateAvailable}
|
|
1048
|
+
onAgentChange={(agentId) => void handleSwitchAgent(agentId)}
|
|
1049
|
+
onShowMenu={handleShowFloatingMenu}
|
|
1050
|
+
onMinimize={onMinimize}
|
|
1051
|
+
onClose={onClose}
|
|
1052
|
+
/>
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
const cwdBanner =
|
|
1056
|
+
agentCwd !== vaultPath && !isSameDirectory(agentCwd, vaultPath) ? (
|
|
1057
|
+
<div className="agent-client-cwd-banner" title={agentCwd}>
|
|
1058
|
+
<span
|
|
1059
|
+
className="agent-client-cwd-banner-icon"
|
|
1060
|
+
ref={(el) => {
|
|
1061
|
+
if (el) setIcon(el, "folder-open");
|
|
1062
|
+
}}
|
|
1063
|
+
/>
|
|
1064
|
+
<span className="agent-client-cwd-banner-path">{agentCwd}</span>
|
|
1065
|
+
</div>
|
|
1066
|
+
) : null;
|
|
1067
|
+
|
|
1068
|
+
const messageListElement = (
|
|
1069
|
+
<MessageList
|
|
1070
|
+
messages={messages}
|
|
1071
|
+
isSending={isSending}
|
|
1072
|
+
isSessionReady={isSessionReady}
|
|
1073
|
+
isRestoringSession={sessionHistory.loading}
|
|
1074
|
+
agentLabel={activeAgentLabel}
|
|
1075
|
+
plugin={plugin}
|
|
1076
|
+
view={viewHost}
|
|
1077
|
+
terminalClient={terminalClientRef.current}
|
|
1078
|
+
onApprovePermission={agent.approvePermission}
|
|
1079
|
+
hasActivePermission={agent.hasActivePermission}
|
|
1080
|
+
/>
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
const inputAreaElement = (
|
|
1084
|
+
<InputArea
|
|
1085
|
+
isSending={isSending}
|
|
1086
|
+
isSessionReady={isSessionReady}
|
|
1087
|
+
isRestoringSession={sessionHistory.loading}
|
|
1088
|
+
agentLabel={activeAgentLabel}
|
|
1089
|
+
availableCommands={session.availableCommands || []}
|
|
1090
|
+
autoMentionEnabled={settings.autoMentionActiveNote}
|
|
1091
|
+
restoredMessage={restoredMessage}
|
|
1092
|
+
suggestions={suggestions}
|
|
1093
|
+
plugin={plugin}
|
|
1094
|
+
view={viewHost}
|
|
1095
|
+
onSendMessage={handleSendMessageWithGeminiDismiss}
|
|
1096
|
+
onStopGeneration={handleStopGeneration}
|
|
1097
|
+
onRestoredMessageConsumed={handleRestoredMessageConsumed}
|
|
1098
|
+
modes={session.modes}
|
|
1099
|
+
onModeChange={(modeId) => void handleSetMode(modeId)}
|
|
1100
|
+
models={session.models}
|
|
1101
|
+
onModelChange={(modelId) => void handleSetModel(modelId)}
|
|
1102
|
+
configOptions={session.configOptions}
|
|
1103
|
+
onConfigOptionChange={(configId, value) =>
|
|
1104
|
+
void handleSetConfigOption(configId, value)
|
|
1105
|
+
}
|
|
1106
|
+
usage={session.usage}
|
|
1107
|
+
supportsImages={session.promptCapabilities?.image ?? false}
|
|
1108
|
+
agentId={session.agentId}
|
|
1109
|
+
// Controlled component props (for broadcast commands)
|
|
1110
|
+
inputValue={inputValue}
|
|
1111
|
+
onInputChange={setInputValue}
|
|
1112
|
+
attachedFiles={attachedFiles}
|
|
1113
|
+
onAttachedFilesChange={setAttachedFiles}
|
|
1114
|
+
// Error overlay props
|
|
1115
|
+
errorInfo={errorInfo}
|
|
1116
|
+
onClearError={handleClearError}
|
|
1117
|
+
// Agent update notification props
|
|
1118
|
+
agentUpdateNotification={agentUpdateNotification}
|
|
1119
|
+
onClearAgentUpdate={handleClearAgentUpdate}
|
|
1120
|
+
// Gemini CLI deprecation notice props
|
|
1121
|
+
geminiNotice={effectiveGeminiNotice}
|
|
1122
|
+
onClearGeminiNotice={handleClearGeminiNotice}
|
|
1123
|
+
messages={messages}
|
|
1124
|
+
/>
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
if (variant === "floating") {
|
|
1128
|
+
// Floating layout: no wrapper div. Parent agent-client-floating-window is the flex container.
|
|
1129
|
+
// Focus tracking uses containerElProp (from FloatingChatView's containerRef).
|
|
1130
|
+
return (
|
|
1131
|
+
<>
|
|
1132
|
+
<div
|
|
1133
|
+
className="agent-client-floating-header"
|
|
1134
|
+
onMouseDown={onFloatingHeaderMouseDown}
|
|
1135
|
+
>
|
|
1136
|
+
{headerElement}
|
|
1137
|
+
</div>
|
|
1138
|
+
{cwdBanner}
|
|
1139
|
+
<div className="agent-client-floating-content">
|
|
1140
|
+
<div className="agent-client-floating-messages-container">
|
|
1141
|
+
{messageListElement}
|
|
1142
|
+
</div>
|
|
1143
|
+
{inputAreaElement}
|
|
1144
|
+
</div>
|
|
1145
|
+
</>
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Sidebar layout
|
|
1150
|
+
return (
|
|
1151
|
+
<div
|
|
1152
|
+
ref={containerRef}
|
|
1153
|
+
className="agent-client-chat-view-container"
|
|
1154
|
+
style={chatFontSizeStyle}
|
|
1155
|
+
>
|
|
1156
|
+
{headerElement}
|
|
1157
|
+
{cwdBanner}
|
|
1158
|
+
{messageListElement}
|
|
1159
|
+
{inputAreaElement}
|
|
1160
|
+
</div>
|
|
1161
|
+
);
|
|
1162
|
+
}
|