@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
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Plugin,
|
|
3
|
+
WorkspaceLeaf,
|
|
4
|
+
Notice,
|
|
5
|
+
requestUrl,
|
|
6
|
+
} from "obsidian";
|
|
7
|
+
import * as semver from "semver";
|
|
8
|
+
import { ChatView, VIEW_TYPE_CHAT } from "./ui/ChatView";
|
|
9
|
+
import {
|
|
10
|
+
createFloatingChat,
|
|
11
|
+
FloatingViewContainer,
|
|
12
|
+
} from "./ui/FloatingChatView";
|
|
13
|
+
import { FloatingButtonContainer } from "./ui/FloatingButton";
|
|
14
|
+
import { ChatViewRegistry } from "./services/view-registry";
|
|
15
|
+
import {
|
|
16
|
+
createSettingsService,
|
|
17
|
+
type SettingsService,
|
|
18
|
+
} from "./services/settings-service";
|
|
19
|
+
import { AgentClientSettingTab } from "./ui/SettingsTab";
|
|
20
|
+
import { AcpClient } from "./acp/acp-client";
|
|
21
|
+
import {
|
|
22
|
+
sanitizeArgs,
|
|
23
|
+
normalizeEnvVars,
|
|
24
|
+
normalizeCustomAgent,
|
|
25
|
+
ensureUniqueCustomAgentIds,
|
|
26
|
+
parseChatFontSize,
|
|
27
|
+
str,
|
|
28
|
+
bool,
|
|
29
|
+
num,
|
|
30
|
+
enumVal,
|
|
31
|
+
obj,
|
|
32
|
+
strRecord,
|
|
33
|
+
xyPoint,
|
|
34
|
+
} from "./services/settings-normalizer";
|
|
35
|
+
import {
|
|
36
|
+
AgentEnvVar,
|
|
37
|
+
GeminiAgentSettings,
|
|
38
|
+
ClaudeAgentSettings,
|
|
39
|
+
CodexAgentSettings,
|
|
40
|
+
CustomAgentSettings,
|
|
41
|
+
} from "./types/agent";
|
|
42
|
+
import type { SavedSessionInfo } from "./types/session";
|
|
43
|
+
import { initializeLogger } from "./utils/logger";
|
|
44
|
+
|
|
45
|
+
// Re-export for backward compatibility
|
|
46
|
+
export type { AgentEnvVar, CustomAgentSettings };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Send message shortcut configuration.
|
|
50
|
+
* - 'enter': Enter to send, Shift+Enter for newline (default)
|
|
51
|
+
* - 'cmd-enter': Cmd/Ctrl+Enter to send, Enter for newline
|
|
52
|
+
*/
|
|
53
|
+
export type SendMessageShortcut = "enter" | "cmd-enter";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Chat view location configuration.
|
|
57
|
+
* - 'right-tab': Open in right pane as tabs (default)
|
|
58
|
+
* - 'right-split': Open in right pane with vertical split
|
|
59
|
+
* - 'editor-tab': Open in editor area as tabs
|
|
60
|
+
* - 'editor-split': Open in editor area with right split
|
|
61
|
+
*/
|
|
62
|
+
export type ChatViewLocation =
|
|
63
|
+
| "right-tab"
|
|
64
|
+
| "right-split"
|
|
65
|
+
| "editor-tab"
|
|
66
|
+
| "editor-split";
|
|
67
|
+
|
|
68
|
+
export interface AgentClientPluginSettings {
|
|
69
|
+
gemini: GeminiAgentSettings;
|
|
70
|
+
claude: ClaudeAgentSettings;
|
|
71
|
+
codex: CodexAgentSettings;
|
|
72
|
+
customAgents: CustomAgentSettings[];
|
|
73
|
+
/** Default agent ID for new views (renamed from activeAgentId for multi-session) */
|
|
74
|
+
defaultAgentId: string;
|
|
75
|
+
autoAllowPermissions: boolean;
|
|
76
|
+
autoMentionActiveNote: boolean;
|
|
77
|
+
/** Show OS system notifications on response completion and permission requests */
|
|
78
|
+
enableSystemNotifications: boolean;
|
|
79
|
+
debugMode: boolean;
|
|
80
|
+
nodePath: string;
|
|
81
|
+
exportSettings: {
|
|
82
|
+
defaultFolder: string;
|
|
83
|
+
filenameTemplate: string;
|
|
84
|
+
autoExportOnNewChat: boolean;
|
|
85
|
+
autoExportOnCloseChat: boolean;
|
|
86
|
+
openFileAfterExport: boolean;
|
|
87
|
+
includeImages: boolean;
|
|
88
|
+
imageLocation: "obsidian" | "custom" | "base64";
|
|
89
|
+
imageCustomFolder: string;
|
|
90
|
+
frontmatterTag: string;
|
|
91
|
+
};
|
|
92
|
+
// WSL settings (Windows only)
|
|
93
|
+
windowsWslMode: boolean;
|
|
94
|
+
windowsWslDistribution?: string;
|
|
95
|
+
// Input behavior
|
|
96
|
+
sendMessageShortcut: SendMessageShortcut;
|
|
97
|
+
// View settings
|
|
98
|
+
chatViewLocation: ChatViewLocation;
|
|
99
|
+
// Display settings
|
|
100
|
+
displaySettings: {
|
|
101
|
+
autoCollapseDiffs: boolean;
|
|
102
|
+
diffCollapseThreshold: number;
|
|
103
|
+
maxNoteLength: number;
|
|
104
|
+
maxSelectionLength: number;
|
|
105
|
+
showEmojis: boolean;
|
|
106
|
+
fontSize: number | null;
|
|
107
|
+
};
|
|
108
|
+
// Locally saved session metadata (for agents without session/list support)
|
|
109
|
+
savedSessions: SavedSessionInfo[];
|
|
110
|
+
// Last used model per agent (agentId → modelId)
|
|
111
|
+
lastUsedModels: Record<string, string>;
|
|
112
|
+
// Last used mode per agent (agentId → modeId)
|
|
113
|
+
lastUsedModes: Record<string, string>;
|
|
114
|
+
// Floating chat settings
|
|
115
|
+
enableFloatingChat: boolean;
|
|
116
|
+
floatingButtonImage: string;
|
|
117
|
+
floatingWindowSize: { width: number; height: number };
|
|
118
|
+
floatingWindowPosition: { x: number; y: number } | null;
|
|
119
|
+
floatingButtonPosition: { x: number; y: number } | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const DEFAULT_SETTINGS: AgentClientPluginSettings = {
|
|
123
|
+
claude: {
|
|
124
|
+
id: "claude-code-acp",
|
|
125
|
+
displayName: "Claude Code",
|
|
126
|
+
apiKey: "",
|
|
127
|
+
command: "claude-agent-acp",
|
|
128
|
+
args: [],
|
|
129
|
+
env: [],
|
|
130
|
+
},
|
|
131
|
+
codex: {
|
|
132
|
+
id: "codex-acp",
|
|
133
|
+
displayName: "Codex",
|
|
134
|
+
apiKey: "",
|
|
135
|
+
command: "codex-acp",
|
|
136
|
+
args: [],
|
|
137
|
+
env: [],
|
|
138
|
+
},
|
|
139
|
+
gemini: {
|
|
140
|
+
id: "gemini-cli",
|
|
141
|
+
displayName: "Gemini CLI",
|
|
142
|
+
apiKey: "",
|
|
143
|
+
command: "gemini",
|
|
144
|
+
args: ["--experimental-acp"],
|
|
145
|
+
env: [],
|
|
146
|
+
},
|
|
147
|
+
customAgents: [],
|
|
148
|
+
defaultAgentId: "claude-code-acp",
|
|
149
|
+
autoAllowPermissions: false,
|
|
150
|
+
autoMentionActiveNote: true,
|
|
151
|
+
enableSystemNotifications: true,
|
|
152
|
+
debugMode: false,
|
|
153
|
+
nodePath: "",
|
|
154
|
+
exportSettings: {
|
|
155
|
+
defaultFolder: "Agent Client",
|
|
156
|
+
filenameTemplate: "agent_client_{date}_{time}",
|
|
157
|
+
autoExportOnNewChat: false,
|
|
158
|
+
autoExportOnCloseChat: false,
|
|
159
|
+
openFileAfterExport: true,
|
|
160
|
+
includeImages: true,
|
|
161
|
+
imageLocation: "obsidian",
|
|
162
|
+
imageCustomFolder: "Agent Client",
|
|
163
|
+
frontmatterTag: "agent-client",
|
|
164
|
+
},
|
|
165
|
+
windowsWslMode: false,
|
|
166
|
+
windowsWslDistribution: undefined,
|
|
167
|
+
sendMessageShortcut: "enter",
|
|
168
|
+
chatViewLocation: "right-tab",
|
|
169
|
+
displaySettings: {
|
|
170
|
+
autoCollapseDiffs: false,
|
|
171
|
+
diffCollapseThreshold: 10,
|
|
172
|
+
maxNoteLength: 10000,
|
|
173
|
+
maxSelectionLength: 10000,
|
|
174
|
+
showEmojis: true,
|
|
175
|
+
fontSize: null,
|
|
176
|
+
},
|
|
177
|
+
savedSessions: [],
|
|
178
|
+
lastUsedModels: {},
|
|
179
|
+
lastUsedModes: {},
|
|
180
|
+
enableFloatingChat: false,
|
|
181
|
+
floatingButtonImage: "",
|
|
182
|
+
floatingWindowSize: { width: 400, height: 500 },
|
|
183
|
+
floatingWindowPosition: null,
|
|
184
|
+
floatingButtonPosition: null,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export default class AgentClientPlugin extends Plugin {
|
|
188
|
+
settings: AgentClientPluginSettings;
|
|
189
|
+
settingsService!: SettingsService;
|
|
190
|
+
|
|
191
|
+
/** Registry for all chat view containers (sidebar + floating) */
|
|
192
|
+
viewRegistry = new ChatViewRegistry();
|
|
193
|
+
|
|
194
|
+
/** Map of viewId to AcpClient for multi-session support */
|
|
195
|
+
private _acpClients: Map<string, AcpClient> = new Map();
|
|
196
|
+
/** Floating button container (independent from chat view instances) */
|
|
197
|
+
private floatingButton: FloatingButtonContainer | null = null;
|
|
198
|
+
/** Counter for generating unique floating chat instance IDs */
|
|
199
|
+
private floatingChatCounter = 0;
|
|
200
|
+
|
|
201
|
+
async onload() {
|
|
202
|
+
await this.loadSettings();
|
|
203
|
+
|
|
204
|
+
initializeLogger(this.settings);
|
|
205
|
+
|
|
206
|
+
// Initialize settings store
|
|
207
|
+
this.settingsService = createSettingsService(this.settings, this);
|
|
208
|
+
|
|
209
|
+
this.registerView(VIEW_TYPE_CHAT, (leaf) => new ChatView(leaf, this));
|
|
210
|
+
|
|
211
|
+
const ribbonIconEl = this.addRibbonIcon(
|
|
212
|
+
"bot-message-square",
|
|
213
|
+
"Open agent client",
|
|
214
|
+
(_evt: MouseEvent) => {
|
|
215
|
+
void this.activateView();
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
ribbonIconEl.addClass("agent-client-ribbon-icon");
|
|
219
|
+
|
|
220
|
+
this.addCommand({
|
|
221
|
+
id: "open-chat-view",
|
|
222
|
+
name: "Open chat view",
|
|
223
|
+
callback: () => {
|
|
224
|
+
void this.activateView();
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
this.addCommand({
|
|
229
|
+
id: "focus-next-chat-view",
|
|
230
|
+
name: "Focus next chat view",
|
|
231
|
+
callback: () => {
|
|
232
|
+
this.focusChatView("next");
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
this.addCommand({
|
|
237
|
+
id: "focus-previous-chat-view",
|
|
238
|
+
name: "Focus previous chat view",
|
|
239
|
+
callback: () => {
|
|
240
|
+
this.focusChatView("previous");
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
this.addCommand({
|
|
245
|
+
id: "open-new-chat-view",
|
|
246
|
+
name: "Open new chat view",
|
|
247
|
+
callback: () => {
|
|
248
|
+
void this.openNewChatViewWithAgent(
|
|
249
|
+
this.settings.defaultAgentId,
|
|
250
|
+
);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Register agent-specific commands
|
|
255
|
+
this.registerAgentCommands();
|
|
256
|
+
this.registerPermissionCommands();
|
|
257
|
+
this.registerBroadcastCommands();
|
|
258
|
+
|
|
259
|
+
// Floating chat window commands
|
|
260
|
+
this.addCommand({
|
|
261
|
+
id: "open-floating-chat-view",
|
|
262
|
+
name: "Open floating chat view",
|
|
263
|
+
checkCallback: (checking) => {
|
|
264
|
+
if (!this.settings.enableFloatingChat) return false;
|
|
265
|
+
if (checking) return true;
|
|
266
|
+
const instances = this.getFloatingChatInstances();
|
|
267
|
+
if (instances.length === 0) {
|
|
268
|
+
this.openNewFloatingChat(true);
|
|
269
|
+
} else if (instances.length === 1) {
|
|
270
|
+
this.expandFloatingChat(instances[0]);
|
|
271
|
+
} else {
|
|
272
|
+
const focused = this.viewRegistry.getFocused();
|
|
273
|
+
if (focused && focused.viewType === "floating") {
|
|
274
|
+
focused.expand();
|
|
275
|
+
} else {
|
|
276
|
+
this.expandFloatingChat(
|
|
277
|
+
instances[instances.length - 1],
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
this.addCommand({
|
|
285
|
+
id: "open-new-floating-chat-view",
|
|
286
|
+
name: "Open new floating chat view",
|
|
287
|
+
checkCallback: (checking) => {
|
|
288
|
+
if (!this.settings.enableFloatingChat) return false;
|
|
289
|
+
if (checking) return true;
|
|
290
|
+
this.openNewFloatingChat(true);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
this.addCommand({
|
|
295
|
+
id: "minimize-floating-chat-view",
|
|
296
|
+
name: "Minimize floating chat view",
|
|
297
|
+
checkCallback: (checking) => {
|
|
298
|
+
if (!this.settings.enableFloatingChat) return false;
|
|
299
|
+
const focused = this.viewRegistry.getFocused();
|
|
300
|
+
if (!(focused && focused.viewType === "floating")) return false;
|
|
301
|
+
if (checking) return true;
|
|
302
|
+
focused.collapse();
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
this.addCommand({
|
|
307
|
+
id: "close-floating-chat-view",
|
|
308
|
+
name: "Close floating chat view",
|
|
309
|
+
checkCallback: (checking) => {
|
|
310
|
+
if (!this.settings.enableFloatingChat) return false;
|
|
311
|
+
const focused = this.viewRegistry.getFocused();
|
|
312
|
+
if (!(focused && focused.viewType === "floating")) return false;
|
|
313
|
+
if (checking) return true;
|
|
314
|
+
this.closeFloatingChat(focused.viewId);
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
this.addSettingTab(new AgentClientSettingTab(this.app, this));
|
|
319
|
+
|
|
320
|
+
// Mount floating button (always present; visibility controlled by settings inside component)
|
|
321
|
+
this.floatingButton = new FloatingButtonContainer(this);
|
|
322
|
+
this.floatingButton.mount();
|
|
323
|
+
|
|
324
|
+
// Mount initial floating chat instance only if enabled
|
|
325
|
+
if (this.settings.enableFloatingChat) {
|
|
326
|
+
this.openNewFloatingChat();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Clean up all ACP sessions when Obsidian quits
|
|
330
|
+
// Note: We don't wait for disconnect to complete to avoid blocking quit
|
|
331
|
+
this.registerEvent(
|
|
332
|
+
this.app.workspace.on("quit", () => {
|
|
333
|
+
// Fire and forget - don't block Obsidian from quitting
|
|
334
|
+
for (const [viewId, client] of this._acpClients) {
|
|
335
|
+
client.disconnect().catch((error) => {
|
|
336
|
+
console.warn(
|
|
337
|
+
`[AgentClient] Quit cleanup error for view ${viewId}:`,
|
|
338
|
+
error,
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
this._acpClients.clear();
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
onunload() {
|
|
348
|
+
// Unmount floating button
|
|
349
|
+
this.floatingButton?.unmount();
|
|
350
|
+
this.floatingButton = null;
|
|
351
|
+
|
|
352
|
+
// Unmount all floating chat instances via registry
|
|
353
|
+
for (const container of this.viewRegistry.getByType("floating")) {
|
|
354
|
+
if (container instanceof FloatingViewContainer) {
|
|
355
|
+
container.unmount();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Clear registry (sidebar views are managed by Obsidian workspace)
|
|
360
|
+
this.viewRegistry.clear();
|
|
361
|
+
|
|
362
|
+
// Disconnect all ACP clients (kill agent processes)
|
|
363
|
+
for (const [, client] of this._acpClients) {
|
|
364
|
+
client.disconnect().catch(() => {});
|
|
365
|
+
}
|
|
366
|
+
this._acpClients.clear();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get or create an AcpClient for a specific view.
|
|
371
|
+
* Each ChatView has its own AcpClient for independent sessions.
|
|
372
|
+
*/
|
|
373
|
+
getOrCreateAcpClient(viewId: string): AcpClient {
|
|
374
|
+
let client = this._acpClients.get(viewId);
|
|
375
|
+
if (!client) {
|
|
376
|
+
client = new AcpClient(this);
|
|
377
|
+
this._acpClients.set(viewId, client);
|
|
378
|
+
}
|
|
379
|
+
return client;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Remove and disconnect the AcpClient for a specific view.
|
|
384
|
+
* Called when a ChatView is closed.
|
|
385
|
+
*/
|
|
386
|
+
async removeAcpClient(viewId: string): Promise<void> {
|
|
387
|
+
const client = this._acpClients.get(viewId);
|
|
388
|
+
if (client) {
|
|
389
|
+
try {
|
|
390
|
+
await client.disconnect();
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.warn(
|
|
393
|
+
`[AgentClient] Failed to disconnect client for view ${viewId}:`,
|
|
394
|
+
error,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
this._acpClients.delete(viewId);
|
|
398
|
+
}
|
|
399
|
+
// Note: lastActiveChatViewId is now managed by viewRegistry
|
|
400
|
+
// Clearing happens automatically when view is unregistered
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get the last active ChatView ID for keybind targeting.
|
|
405
|
+
*/
|
|
406
|
+
get lastActiveChatViewId(): string | null {
|
|
407
|
+
return this.viewRegistry.getFocusedId();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Set the last active ChatView ID.
|
|
412
|
+
* Called when a ChatView receives focus or interaction.
|
|
413
|
+
*/
|
|
414
|
+
setLastActiveChatViewId(viewId: string | null): void {
|
|
415
|
+
if (viewId) {
|
|
416
|
+
this.viewRegistry.setFocused(viewId);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async activateView() {
|
|
421
|
+
const { workspace } = this.app;
|
|
422
|
+
|
|
423
|
+
let leaf: WorkspaceLeaf | null = null;
|
|
424
|
+
const leaves = workspace.getLeavesOfType(VIEW_TYPE_CHAT);
|
|
425
|
+
|
|
426
|
+
if (leaves.length > 0) {
|
|
427
|
+
// Find the leaf matching lastActiveChatViewId, or fall back to first leaf
|
|
428
|
+
const focusedId = this.lastActiveChatViewId;
|
|
429
|
+
if (focusedId) {
|
|
430
|
+
leaf =
|
|
431
|
+
leaves.find(
|
|
432
|
+
(l) => (l.view as ChatView)?.viewId === focusedId,
|
|
433
|
+
) || leaves[0];
|
|
434
|
+
} else {
|
|
435
|
+
leaf = leaves[0];
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
leaf = this.createNewChatLeaf(false);
|
|
439
|
+
if (leaf) {
|
|
440
|
+
await leaf.setViewState({
|
|
441
|
+
type: VIEW_TYPE_CHAT,
|
|
442
|
+
active: true,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (leaf) {
|
|
448
|
+
await workspace.revealLeaf(leaf);
|
|
449
|
+
this.focusTextarea(leaf);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Focus the textarea in a ChatView leaf.
|
|
455
|
+
*/
|
|
456
|
+
private focusTextarea(leaf: WorkspaceLeaf): void {
|
|
457
|
+
const viewContainerEl = leaf.view?.containerEl;
|
|
458
|
+
if (viewContainerEl) {
|
|
459
|
+
window.setTimeout(() => {
|
|
460
|
+
const textarea = viewContainerEl.querySelector(
|
|
461
|
+
"textarea.agent-client-chat-input-textarea",
|
|
462
|
+
);
|
|
463
|
+
if (textarea instanceof HTMLTextAreaElement) {
|
|
464
|
+
textarea.focus();
|
|
465
|
+
}
|
|
466
|
+
}, 50);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Focus the next or previous ChatView in the list.
|
|
472
|
+
* Uses ChatViewRegistry which includes both sidebar and floating views.
|
|
473
|
+
*/
|
|
474
|
+
private focusChatView(direction: "next" | "previous"): void {
|
|
475
|
+
if (direction === "next") {
|
|
476
|
+
this.viewRegistry.focusNext();
|
|
477
|
+
} else {
|
|
478
|
+
this.viewRegistry.focusPrevious();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Create a new leaf for ChatView based on the configured location setting.
|
|
484
|
+
* @param isAdditional - true when opening additional views (e.g., Open New View)
|
|
485
|
+
*/
|
|
486
|
+
private createNewChatLeaf(isAdditional: boolean): WorkspaceLeaf | null {
|
|
487
|
+
const { workspace } = this.app;
|
|
488
|
+
const location = this.settings.chatViewLocation;
|
|
489
|
+
|
|
490
|
+
switch (location) {
|
|
491
|
+
case "right-tab":
|
|
492
|
+
if (isAdditional) {
|
|
493
|
+
return this.createSidebarTab("right");
|
|
494
|
+
}
|
|
495
|
+
return workspace.getRightLeaf(false);
|
|
496
|
+
case "right-split":
|
|
497
|
+
return workspace.getRightLeaf(isAdditional);
|
|
498
|
+
case "editor-tab":
|
|
499
|
+
return workspace.getLeaf("tab");
|
|
500
|
+
case "editor-split":
|
|
501
|
+
return workspace.getLeaf("split");
|
|
502
|
+
default:
|
|
503
|
+
return workspace.getRightLeaf(false);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Create a new tab within an existing sidebar tab group.
|
|
509
|
+
* Uses the parent of an existing chat leaf to add a sibling tab,
|
|
510
|
+
* avoiding the vertical split caused by getRightLeaf(true).
|
|
511
|
+
*/
|
|
512
|
+
private createSidebarTab(side: "right" | "left"): WorkspaceLeaf | null {
|
|
513
|
+
const { workspace } = this.app;
|
|
514
|
+
const split =
|
|
515
|
+
side === "right" ? workspace.rightSplit : workspace.leftSplit;
|
|
516
|
+
|
|
517
|
+
// Find an existing chat leaf in this sidebar to get its tab group
|
|
518
|
+
const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_CHAT);
|
|
519
|
+
const sidebarLeaf = existingLeaves.find(
|
|
520
|
+
(leaf) => leaf.getRoot() === split,
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
if (sidebarLeaf) {
|
|
524
|
+
const tabGroup = sidebarLeaf.parent;
|
|
525
|
+
// Index is clamped by Obsidian, so a large value appends to the end
|
|
526
|
+
return workspace.createLeafInParent(
|
|
527
|
+
tabGroup,
|
|
528
|
+
Number.MAX_SAFE_INTEGER,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Fallback: no existing chat leaf in sidebar, create first one
|
|
533
|
+
return side === "right"
|
|
534
|
+
? workspace.getRightLeaf(false)
|
|
535
|
+
: workspace.getLeftLeaf(false);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Open a new chat view with a specific agent.
|
|
540
|
+
* Always creates a new view (doesn't reuse existing).
|
|
541
|
+
*/
|
|
542
|
+
async openNewChatViewWithAgent(agentId: string): Promise<void> {
|
|
543
|
+
const leaf = this.createNewChatLeaf(true);
|
|
544
|
+
if (!leaf) {
|
|
545
|
+
console.warn("[AgentClient] Failed to create new leaf");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
await leaf.setViewState({
|
|
550
|
+
type: VIEW_TYPE_CHAT,
|
|
551
|
+
active: true,
|
|
552
|
+
state: { initialAgentId: agentId },
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
await this.app.workspace.revealLeaf(leaf);
|
|
556
|
+
|
|
557
|
+
// Focus textarea after revealing the leaf
|
|
558
|
+
const viewContainerEl = leaf.view?.containerEl;
|
|
559
|
+
if (viewContainerEl) {
|
|
560
|
+
window.setTimeout(() => {
|
|
561
|
+
const textarea = viewContainerEl.querySelector(
|
|
562
|
+
"textarea.agent-client-chat-input-textarea",
|
|
563
|
+
);
|
|
564
|
+
if (textarea instanceof HTMLTextAreaElement) {
|
|
565
|
+
textarea.focus();
|
|
566
|
+
}
|
|
567
|
+
}, 0);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Open a new floating chat window.
|
|
573
|
+
* Each window is independent with its own session.
|
|
574
|
+
*/
|
|
575
|
+
openNewFloatingChat(
|
|
576
|
+
initialExpanded = false,
|
|
577
|
+
initialPosition?: { x: number; y: number },
|
|
578
|
+
): void {
|
|
579
|
+
// instanceId is just the counter (e.g., "0", "1", "2")
|
|
580
|
+
// FloatingViewContainer will create viewId as "floating-chat-{instanceId}"
|
|
581
|
+
const instanceId = String(this.floatingChatCounter++);
|
|
582
|
+
createFloatingChat(this, instanceId, initialExpanded, initialPosition);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Close a specific floating chat window.
|
|
587
|
+
* @param viewId - The viewId in "floating-chat-{id}" format (from getFloatingChatInstances())
|
|
588
|
+
*/
|
|
589
|
+
closeFloatingChat(viewId: string): void {
|
|
590
|
+
const container = this.viewRegistry.get(viewId);
|
|
591
|
+
if (container && container instanceof FloatingViewContainer) {
|
|
592
|
+
container.unmount();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Get all floating chat instance viewIds.
|
|
598
|
+
* @returns Array of viewIds in "floating-chat-{id}" format
|
|
599
|
+
*/
|
|
600
|
+
getFloatingChatInstances(): string[] {
|
|
601
|
+
return this.viewRegistry.getByType("floating").map((v) => v.viewId);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Expand a specific floating chat window by triggering a custom event.
|
|
606
|
+
* @param viewId - The viewId in "floating-chat-{id}" format (from getFloatingChatInstances())
|
|
607
|
+
*/
|
|
608
|
+
expandFloatingChat(viewId: string): void {
|
|
609
|
+
const view = this.viewRegistry.get(viewId);
|
|
610
|
+
if (view) {
|
|
611
|
+
view.expand();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get all available agents (claude, codex, gemini, custom)
|
|
617
|
+
*/
|
|
618
|
+
getAvailableAgents(): Array<{ id: string; displayName: string }> {
|
|
619
|
+
return [
|
|
620
|
+
{
|
|
621
|
+
id: this.settings.claude.id,
|
|
622
|
+
displayName:
|
|
623
|
+
this.settings.claude.displayName || this.settings.claude.id,
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
id: this.settings.codex.id,
|
|
627
|
+
displayName:
|
|
628
|
+
this.settings.codex.displayName || this.settings.codex.id,
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
id: this.settings.gemini.id,
|
|
632
|
+
displayName:
|
|
633
|
+
this.settings.gemini.displayName || this.settings.gemini.id,
|
|
634
|
+
},
|
|
635
|
+
...this.settings.customAgents.map((agent) => ({
|
|
636
|
+
id: agent.id,
|
|
637
|
+
displayName: agent.displayName || agent.id,
|
|
638
|
+
})),
|
|
639
|
+
];
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Register commands for each configured agent
|
|
644
|
+
*/
|
|
645
|
+
private registerAgentCommands(): void {
|
|
646
|
+
const agents = this.getAvailableAgents();
|
|
647
|
+
|
|
648
|
+
for (const agent of agents) {
|
|
649
|
+
this.addCommand({
|
|
650
|
+
id: `switch-agent-to-${agent.id}`,
|
|
651
|
+
name: `Switch agent to ${agent.displayName}`,
|
|
652
|
+
callback: () => {
|
|
653
|
+
this.app.workspace.trigger(
|
|
654
|
+
"agent-client:new-chat-requested",
|
|
655
|
+
this.lastActiveChatViewId,
|
|
656
|
+
agent.id,
|
|
657
|
+
);
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private registerPermissionCommands(): void {
|
|
664
|
+
this.addCommand({
|
|
665
|
+
id: "approve-active-permission",
|
|
666
|
+
name: "Approve active permission",
|
|
667
|
+
callback: () => {
|
|
668
|
+
this.app.workspace.trigger(
|
|
669
|
+
"agent-client:approve-active-permission",
|
|
670
|
+
this.lastActiveChatViewId,
|
|
671
|
+
);
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
this.addCommand({
|
|
676
|
+
id: "reject-active-permission",
|
|
677
|
+
name: "Reject active permission",
|
|
678
|
+
callback: () => {
|
|
679
|
+
this.app.workspace.trigger(
|
|
680
|
+
"agent-client:reject-active-permission",
|
|
681
|
+
this.lastActiveChatViewId,
|
|
682
|
+
);
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
this.addCommand({
|
|
687
|
+
id: "toggle-auto-mention",
|
|
688
|
+
name: "Toggle auto-mention",
|
|
689
|
+
callback: () => {
|
|
690
|
+
this.app.workspace.trigger(
|
|
691
|
+
"agent-client:toggle-auto-mention",
|
|
692
|
+
this.lastActiveChatViewId,
|
|
693
|
+
);
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
this.addCommand({
|
|
698
|
+
id: "new-chat",
|
|
699
|
+
name: "New chat",
|
|
700
|
+
callback: () => {
|
|
701
|
+
this.app.workspace.trigger(
|
|
702
|
+
"agent-client:new-chat-requested",
|
|
703
|
+
this.lastActiveChatViewId,
|
|
704
|
+
);
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
this.addCommand({
|
|
709
|
+
id: "cancel-current-message",
|
|
710
|
+
name: "Cancel current message",
|
|
711
|
+
callback: () => {
|
|
712
|
+
this.app.workspace.trigger(
|
|
713
|
+
"agent-client:cancel-message",
|
|
714
|
+
this.lastActiveChatViewId,
|
|
715
|
+
);
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
this.addCommand({
|
|
720
|
+
id: "export-chat",
|
|
721
|
+
name: "Export chat",
|
|
722
|
+
callback: () => {
|
|
723
|
+
this.app.workspace.trigger(
|
|
724
|
+
"agent-client:export-chat",
|
|
725
|
+
this.lastActiveChatViewId,
|
|
726
|
+
);
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Register broadcast commands for multi-view operations
|
|
733
|
+
*/
|
|
734
|
+
private registerBroadcastCommands(): void {
|
|
735
|
+
// Broadcast prompt: Copy prompt from active view to all other views
|
|
736
|
+
this.addCommand({
|
|
737
|
+
id: "broadcast-prompt",
|
|
738
|
+
name: "Broadcast prompt",
|
|
739
|
+
callback: () => {
|
|
740
|
+
this.broadcastPrompt();
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// Broadcast send: Send message in all views that can send
|
|
745
|
+
this.addCommand({
|
|
746
|
+
id: "broadcast-send",
|
|
747
|
+
name: "Broadcast send",
|
|
748
|
+
callback: () => {
|
|
749
|
+
void this.broadcastSend();
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Broadcast cancel: Cancel operation in all views
|
|
754
|
+
this.addCommand({
|
|
755
|
+
id: "broadcast-cancel",
|
|
756
|
+
name: "Broadcast cancel",
|
|
757
|
+
callback: () => {
|
|
758
|
+
void this.broadcastCancel();
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Copy prompt from active view to all other views
|
|
765
|
+
*/
|
|
766
|
+
private broadcastPrompt(): void {
|
|
767
|
+
const allViews = this.viewRegistry.getAll();
|
|
768
|
+
if (allViews.length === 0) {
|
|
769
|
+
new Notice("[Agent Client] No chat views open");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const inputState = this.viewRegistry.toFocused((v) =>
|
|
774
|
+
v.getInputState(),
|
|
775
|
+
);
|
|
776
|
+
if (
|
|
777
|
+
!inputState ||
|
|
778
|
+
(inputState.text.trim() === "" && inputState.files.length === 0)
|
|
779
|
+
) {
|
|
780
|
+
new Notice("[Agent Client] No prompt to broadcast");
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const focusedId = this.viewRegistry.getFocusedId();
|
|
785
|
+
const targetViews = allViews.filter((v) => v.viewId !== focusedId);
|
|
786
|
+
if (targetViews.length === 0) {
|
|
787
|
+
new Notice("[Agent Client] No other chat views to broadcast to");
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
for (const view of targetViews) {
|
|
792
|
+
view.setInputState(inputState);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Send message in all views that can send
|
|
798
|
+
*/
|
|
799
|
+
private async broadcastSend(): Promise<void> {
|
|
800
|
+
const allViews = this.viewRegistry.getAll();
|
|
801
|
+
if (allViews.length === 0) {
|
|
802
|
+
new Notice("[Agent Client] No chat views open");
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const sendableViews = allViews.filter((v) => v.canSend());
|
|
807
|
+
if (sendableViews.length === 0) {
|
|
808
|
+
new Notice("[Agent Client] No views ready to send");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
await Promise.allSettled(sendableViews.map((v) => v.sendMessage()));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Cancel operation in all views
|
|
817
|
+
*/
|
|
818
|
+
private async broadcastCancel(): Promise<void> {
|
|
819
|
+
const allViews = this.viewRegistry.getAll();
|
|
820
|
+
if (allViews.length === 0) {
|
|
821
|
+
new Notice("[Agent Client] No chat views open");
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
await Promise.allSettled(allViews.map((v) => v.cancelOperation()));
|
|
826
|
+
new Notice("[Agent Client] Cancel broadcast to all views");
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async loadSettings() {
|
|
830
|
+
const raw = ((await this.loadData()) ?? {}) as Record<string, unknown>;
|
|
831
|
+
const D = DEFAULT_SETTINGS;
|
|
832
|
+
|
|
833
|
+
// Extract agent sub-objects
|
|
834
|
+
const rc = obj(raw.claude) ?? {};
|
|
835
|
+
const rk = obj(raw.codex) ?? {};
|
|
836
|
+
const rg = obj(raw.gemini) ?? {};
|
|
837
|
+
const re = obj(raw.exportSettings) ?? {};
|
|
838
|
+
const rd = obj(raw.displaySettings) ?? {};
|
|
839
|
+
|
|
840
|
+
// Normalize custom agents
|
|
841
|
+
const customAgents = Array.isArray(raw.customAgents)
|
|
842
|
+
? ensureUniqueCustomAgentIds(
|
|
843
|
+
raw.customAgents.map((a: unknown) =>
|
|
844
|
+
normalizeCustomAgent(obj(a) ?? {}),
|
|
845
|
+
),
|
|
846
|
+
)
|
|
847
|
+
: [];
|
|
848
|
+
|
|
849
|
+
// Migration: defaultAgentId ← activeAgentId (old name)
|
|
850
|
+
const availableAgentIds = [
|
|
851
|
+
D.claude.id,
|
|
852
|
+
D.codex.id,
|
|
853
|
+
D.gemini.id,
|
|
854
|
+
...customAgents.map((a) => a.id),
|
|
855
|
+
];
|
|
856
|
+
const rawDefaultId =
|
|
857
|
+
str(raw.defaultAgentId, "") || str(raw.activeAgentId, "");
|
|
858
|
+
const defaultAgentId =
|
|
859
|
+
rawDefaultId && availableAgentIds.includes(rawDefaultId)
|
|
860
|
+
? rawDefaultId
|
|
861
|
+
: availableAgentIds[0] || D.claude.id;
|
|
862
|
+
|
|
863
|
+
this.settings = {
|
|
864
|
+
claude: {
|
|
865
|
+
id: D.claude.id, // Fixed — never from raw
|
|
866
|
+
displayName: str(rc.displayName, D.claude.displayName),
|
|
867
|
+
apiKey: str(rc.apiKey, D.claude.apiKey),
|
|
868
|
+
// Migration: claude.command ← claudeCodeAcpCommandPath (old name)
|
|
869
|
+
command:
|
|
870
|
+
str(rc.command, "") ||
|
|
871
|
+
str(raw.claudeCodeAcpCommandPath, "") ||
|
|
872
|
+
D.claude.command,
|
|
873
|
+
args: sanitizeArgs(rc.args),
|
|
874
|
+
env: normalizeEnvVars(rc.env),
|
|
875
|
+
},
|
|
876
|
+
codex: {
|
|
877
|
+
id: D.codex.id,
|
|
878
|
+
displayName: str(rk.displayName, D.codex.displayName),
|
|
879
|
+
apiKey: str(rk.apiKey, D.codex.apiKey),
|
|
880
|
+
command: str(rk.command, "") || D.codex.command,
|
|
881
|
+
args: sanitizeArgs(rk.args),
|
|
882
|
+
env: normalizeEnvVars(rk.env),
|
|
883
|
+
},
|
|
884
|
+
gemini: {
|
|
885
|
+
id: D.gemini.id,
|
|
886
|
+
displayName: str(rg.displayName, D.gemini.displayName),
|
|
887
|
+
apiKey: str(rg.apiKey, D.gemini.apiKey),
|
|
888
|
+
// Migration: gemini.command ← geminiCommandPath (old name)
|
|
889
|
+
command:
|
|
890
|
+
str(rg.command, "") ||
|
|
891
|
+
str(raw.geminiCommandPath, "") ||
|
|
892
|
+
D.gemini.command,
|
|
893
|
+
args:
|
|
894
|
+
sanitizeArgs(rg.args).length > 0
|
|
895
|
+
? sanitizeArgs(rg.args)
|
|
896
|
+
: D.gemini.args,
|
|
897
|
+
env: normalizeEnvVars(rg.env),
|
|
898
|
+
},
|
|
899
|
+
customAgents,
|
|
900
|
+
defaultAgentId,
|
|
901
|
+
autoAllowPermissions: bool(
|
|
902
|
+
raw.autoAllowPermissions,
|
|
903
|
+
D.autoAllowPermissions,
|
|
904
|
+
),
|
|
905
|
+
autoMentionActiveNote: bool(
|
|
906
|
+
raw.autoMentionActiveNote,
|
|
907
|
+
D.autoMentionActiveNote,
|
|
908
|
+
),
|
|
909
|
+
enableSystemNotifications: bool(
|
|
910
|
+
raw.enableSystemNotifications,
|
|
911
|
+
D.enableSystemNotifications,
|
|
912
|
+
),
|
|
913
|
+
debugMode: bool(raw.debugMode, D.debugMode),
|
|
914
|
+
nodePath: str(raw.nodePath, D.nodePath),
|
|
915
|
+
exportSettings: {
|
|
916
|
+
defaultFolder: str(
|
|
917
|
+
re.defaultFolder,
|
|
918
|
+
D.exportSettings.defaultFolder,
|
|
919
|
+
),
|
|
920
|
+
filenameTemplate: str(
|
|
921
|
+
re.filenameTemplate,
|
|
922
|
+
D.exportSettings.filenameTemplate,
|
|
923
|
+
),
|
|
924
|
+
autoExportOnNewChat: bool(
|
|
925
|
+
re.autoExportOnNewChat,
|
|
926
|
+
D.exportSettings.autoExportOnNewChat,
|
|
927
|
+
),
|
|
928
|
+
autoExportOnCloseChat: bool(
|
|
929
|
+
re.autoExportOnCloseChat,
|
|
930
|
+
D.exportSettings.autoExportOnCloseChat,
|
|
931
|
+
),
|
|
932
|
+
openFileAfterExport: bool(
|
|
933
|
+
re.openFileAfterExport,
|
|
934
|
+
D.exportSettings.openFileAfterExport,
|
|
935
|
+
),
|
|
936
|
+
includeImages: bool(
|
|
937
|
+
re.includeImages,
|
|
938
|
+
D.exportSettings.includeImages,
|
|
939
|
+
),
|
|
940
|
+
imageLocation: enumVal(
|
|
941
|
+
re.imageLocation,
|
|
942
|
+
["obsidian", "custom", "base64"],
|
|
943
|
+
D.exportSettings.imageLocation,
|
|
944
|
+
),
|
|
945
|
+
imageCustomFolder: str(
|
|
946
|
+
re.imageCustomFolder,
|
|
947
|
+
D.exportSettings.imageCustomFolder,
|
|
948
|
+
),
|
|
949
|
+
frontmatterTag: str(
|
|
950
|
+
re.frontmatterTag,
|
|
951
|
+
D.exportSettings.frontmatterTag,
|
|
952
|
+
),
|
|
953
|
+
},
|
|
954
|
+
windowsWslMode: bool(raw.windowsWslMode, D.windowsWslMode),
|
|
955
|
+
windowsWslDistribution: str(
|
|
956
|
+
raw.windowsWslDistribution,
|
|
957
|
+
D.windowsWslDistribution as string,
|
|
958
|
+
),
|
|
959
|
+
sendMessageShortcut: enumVal(
|
|
960
|
+
raw.sendMessageShortcut,
|
|
961
|
+
["enter", "cmd-enter"],
|
|
962
|
+
D.sendMessageShortcut,
|
|
963
|
+
),
|
|
964
|
+
chatViewLocation: enumVal(
|
|
965
|
+
raw.chatViewLocation,
|
|
966
|
+
["right-tab", "right-split", "editor-tab", "editor-split"],
|
|
967
|
+
D.chatViewLocation,
|
|
968
|
+
),
|
|
969
|
+
displaySettings: {
|
|
970
|
+
autoCollapseDiffs: bool(
|
|
971
|
+
rd.autoCollapseDiffs,
|
|
972
|
+
D.displaySettings.autoCollapseDiffs,
|
|
973
|
+
),
|
|
974
|
+
diffCollapseThreshold: num(
|
|
975
|
+
rd.diffCollapseThreshold,
|
|
976
|
+
D.displaySettings.diffCollapseThreshold,
|
|
977
|
+
1,
|
|
978
|
+
),
|
|
979
|
+
maxNoteLength: num(
|
|
980
|
+
rd.maxNoteLength,
|
|
981
|
+
D.displaySettings.maxNoteLength,
|
|
982
|
+
1,
|
|
983
|
+
),
|
|
984
|
+
maxSelectionLength: num(
|
|
985
|
+
rd.maxSelectionLength,
|
|
986
|
+
D.displaySettings.maxSelectionLength,
|
|
987
|
+
1,
|
|
988
|
+
),
|
|
989
|
+
showEmojis: bool(rd.showEmojis, D.displaySettings.showEmojis),
|
|
990
|
+
fontSize: parseChatFontSize(rd.fontSize),
|
|
991
|
+
},
|
|
992
|
+
savedSessions: Array.isArray(raw.savedSessions)
|
|
993
|
+
? (raw.savedSessions as SavedSessionInfo[])
|
|
994
|
+
: D.savedSessions,
|
|
995
|
+
lastUsedModels: strRecord(raw.lastUsedModels),
|
|
996
|
+
lastUsedModes: strRecord(raw.lastUsedModes),
|
|
997
|
+
// Migration: enableFloatingChat ← showFloatingButton (old name)
|
|
998
|
+
enableFloatingChat: bool(
|
|
999
|
+
raw.enableFloatingChat,
|
|
1000
|
+
bool(raw.showFloatingButton, D.enableFloatingChat),
|
|
1001
|
+
),
|
|
1002
|
+
floatingButtonImage: str(
|
|
1003
|
+
raw.floatingButtonImage,
|
|
1004
|
+
D.floatingButtonImage,
|
|
1005
|
+
),
|
|
1006
|
+
floatingWindowSize: (() => {
|
|
1007
|
+
const s = obj(raw.floatingWindowSize);
|
|
1008
|
+
return s &&
|
|
1009
|
+
typeof s.width === "number" &&
|
|
1010
|
+
typeof s.height === "number"
|
|
1011
|
+
? { width: s.width, height: s.height }
|
|
1012
|
+
: D.floatingWindowSize;
|
|
1013
|
+
})(),
|
|
1014
|
+
floatingWindowPosition: xyPoint(raw.floatingWindowPosition),
|
|
1015
|
+
floatingButtonPosition: xyPoint(raw.floatingButtonPosition),
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
this.ensureDefaultAgentId();
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async saveSettings() {
|
|
1022
|
+
await this.saveData(this.settings);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async saveSettingsAndNotify(nextSettings: AgentClientPluginSettings) {
|
|
1026
|
+
await this.settingsService.updateSettings(nextSettings);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Fetch the latest stable release version from GitHub.
|
|
1031
|
+
*/
|
|
1032
|
+
private async fetchLatestStable(): Promise<string | null> {
|
|
1033
|
+
const response = await requestUrl({
|
|
1034
|
+
url: "https://api.github.com/repos/RAIT-09/obsidian-agent-client/releases/latest",
|
|
1035
|
+
});
|
|
1036
|
+
const data = response.json as { tag_name?: string };
|
|
1037
|
+
return data.tag_name ? semver.clean(data.tag_name) : null;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Fetch the latest prerelease version from GitHub.
|
|
1042
|
+
*/
|
|
1043
|
+
private async fetchLatestPrerelease(): Promise<string | null> {
|
|
1044
|
+
const response = await requestUrl({
|
|
1045
|
+
url: "https://api.github.com/repos/RAIT-09/obsidian-agent-client/releases",
|
|
1046
|
+
});
|
|
1047
|
+
const releases = response.json as Array<{
|
|
1048
|
+
tag_name: string;
|
|
1049
|
+
prerelease: boolean;
|
|
1050
|
+
}>;
|
|
1051
|
+
|
|
1052
|
+
// Find the first prerelease (releases are sorted by date descending)
|
|
1053
|
+
const latestPrerelease = releases.find((r) => r.prerelease);
|
|
1054
|
+
return latestPrerelease
|
|
1055
|
+
? semver.clean(latestPrerelease.tag_name)
|
|
1056
|
+
: null;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Check for plugin updates.
|
|
1061
|
+
* - Stable version users: compare with latest stable release
|
|
1062
|
+
* - Prerelease users: compare with both latest stable and latest prerelease
|
|
1063
|
+
*/
|
|
1064
|
+
async checkForUpdates(): Promise<boolean> {
|
|
1065
|
+
const currentVersion =
|
|
1066
|
+
semver.clean(this.manifest.version) || this.manifest.version;
|
|
1067
|
+
const isCurrentPrerelease = semver.prerelease(currentVersion) !== null;
|
|
1068
|
+
|
|
1069
|
+
if (isCurrentPrerelease) {
|
|
1070
|
+
// Prerelease user: check both stable and prerelease
|
|
1071
|
+
const [latestStable, latestPrerelease] = await Promise.all([
|
|
1072
|
+
this.fetchLatestStable(),
|
|
1073
|
+
this.fetchLatestPrerelease(),
|
|
1074
|
+
]);
|
|
1075
|
+
|
|
1076
|
+
const hasNewerStable =
|
|
1077
|
+
latestStable && semver.gt(latestStable, currentVersion);
|
|
1078
|
+
const hasNewerPrerelease =
|
|
1079
|
+
latestPrerelease && semver.gt(latestPrerelease, currentVersion);
|
|
1080
|
+
|
|
1081
|
+
if (hasNewerStable || hasNewerPrerelease) {
|
|
1082
|
+
// Prefer stable version notification if available
|
|
1083
|
+
const newestVersion = hasNewerStable
|
|
1084
|
+
? latestStable
|
|
1085
|
+
: latestPrerelease;
|
|
1086
|
+
new Notice(
|
|
1087
|
+
`[Agent Client] Update available: v${newestVersion}`,
|
|
1088
|
+
);
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
// Stable version user: check stable only
|
|
1093
|
+
const latestStable = await this.fetchLatestStable();
|
|
1094
|
+
if (latestStable && semver.gt(latestStable, currentVersion)) {
|
|
1095
|
+
new Notice(`[Agent Client] Update available: v${latestStable}`);
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
ensureDefaultAgentId(): void {
|
|
1104
|
+
const availableIds = this.collectAvailableAgentIds();
|
|
1105
|
+
if (availableIds.length === 0) {
|
|
1106
|
+
this.settings.defaultAgentId = DEFAULT_SETTINGS.claude.id;
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (!availableIds.includes(this.settings.defaultAgentId)) {
|
|
1110
|
+
this.settings.defaultAgentId = availableIds[0];
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
private collectAvailableAgentIds(): string[] {
|
|
1115
|
+
const ids = new Set<string>();
|
|
1116
|
+
ids.add(this.settings.claude.id);
|
|
1117
|
+
ids.add(this.settings.codex.id);
|
|
1118
|
+
ids.add(this.settings.gemini.id);
|
|
1119
|
+
for (const agent of this.settings.customAgents) {
|
|
1120
|
+
if (agent.id && agent.id.length > 0) {
|
|
1121
|
+
ids.add(agent.id);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return Array.from(ids);
|
|
1125
|
+
}
|
|
1126
|
+
}
|