@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,1337 @@
|
|
|
1
|
+
import {
|
|
2
|
+
App,
|
|
3
|
+
PluginSettingTab,
|
|
4
|
+
Setting,
|
|
5
|
+
DropdownComponent,
|
|
6
|
+
Platform,
|
|
7
|
+
} from "obsidian";
|
|
8
|
+
import type AgentClientPlugin from "../plugin";
|
|
9
|
+
import type {
|
|
10
|
+
CustomAgentSettings,
|
|
11
|
+
AgentEnvVar,
|
|
12
|
+
ChatViewLocation,
|
|
13
|
+
} from "../plugin";
|
|
14
|
+
import { resolveCommandPath, resolveCommandPathInWsl } from "../utils/paths";
|
|
15
|
+
import {
|
|
16
|
+
normalizeEnvVars,
|
|
17
|
+
CHAT_FONT_SIZE_MAX,
|
|
18
|
+
CHAT_FONT_SIZE_MIN,
|
|
19
|
+
parseChatFontSize,
|
|
20
|
+
} from "../services/settings-normalizer";
|
|
21
|
+
|
|
22
|
+
export class AgentClientSettingTab extends PluginSettingTab {
|
|
23
|
+
plugin: AgentClientPlugin;
|
|
24
|
+
private agentSelector: DropdownComponent | null = null;
|
|
25
|
+
private unsubscribe: (() => void) | null = null;
|
|
26
|
+
|
|
27
|
+
constructor(app: App, plugin: AgentClientPlugin) {
|
|
28
|
+
super(app, plugin);
|
|
29
|
+
this.plugin = plugin;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
display(): void {
|
|
33
|
+
const { containerEl } = this;
|
|
34
|
+
|
|
35
|
+
containerEl.empty();
|
|
36
|
+
this.agentSelector = null;
|
|
37
|
+
|
|
38
|
+
// Cleanup previous subscription if exists
|
|
39
|
+
if (this.unsubscribe) {
|
|
40
|
+
this.unsubscribe();
|
|
41
|
+
this.unsubscribe = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Documentation link
|
|
45
|
+
const docContainer = containerEl.createDiv({
|
|
46
|
+
cls: "agent-client-doc-link",
|
|
47
|
+
});
|
|
48
|
+
docContainer.createSpan({ text: "Need help? Check out the " });
|
|
49
|
+
docContainer.createEl("a", {
|
|
50
|
+
text: "documentation",
|
|
51
|
+
href: "https://rait-09.github.io/obsidian-agent-client/",
|
|
52
|
+
attr: { target: "_blank" },
|
|
53
|
+
});
|
|
54
|
+
docContainer.createSpan({ text: "." });
|
|
55
|
+
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
57
|
+
// Top-level settings (no header)
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
this.renderAgentSelector(containerEl);
|
|
61
|
+
|
|
62
|
+
// Subscribe to settings changes to update agent dropdown
|
|
63
|
+
this.unsubscribe = this.plugin.settingsService.subscribe(() => {
|
|
64
|
+
this.updateAgentDropdown();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Also update immediately on display to sync with current settings
|
|
68
|
+
this.updateAgentDropdown();
|
|
69
|
+
|
|
70
|
+
const nodePathSetting = new Setting(containerEl)
|
|
71
|
+
.setName("Node.js path")
|
|
72
|
+
.setDesc(
|
|
73
|
+
"Path to Node.js. Usually leave blank. Only needed if node is in a non-standard location (enter absolute path, e.g. /usr/local/bin/node).",
|
|
74
|
+
)
|
|
75
|
+
.addText((text) => {
|
|
76
|
+
text.setPlaceholder("Leave blank (login shell auto-resolves)")
|
|
77
|
+
.setValue(this.plugin.settings.nodePath)
|
|
78
|
+
.onChange(async (value) => {
|
|
79
|
+
this.plugin.settings.nodePath = value.trim();
|
|
80
|
+
await this.plugin.saveSettings();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
this.addAutoDetectButton(nodePathSetting, "node", async (path) => {
|
|
84
|
+
this.plugin.settings.nodePath = path;
|
|
85
|
+
await this.plugin.saveSettings();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
new Setting(containerEl)
|
|
89
|
+
.setName("Send message shortcut")
|
|
90
|
+
.setDesc(
|
|
91
|
+
"Choose the keyboard shortcut to send messages. Note: If using Cmd/Ctrl+Enter, you may need to remove any hotkeys assigned to Cmd/Ctrl+Enter (Settings → Hotkeys).",
|
|
92
|
+
)
|
|
93
|
+
.addDropdown((dropdown) =>
|
|
94
|
+
dropdown
|
|
95
|
+
.addOption(
|
|
96
|
+
"enter",
|
|
97
|
+
"Enter to send, Shift+Enter for newline",
|
|
98
|
+
)
|
|
99
|
+
.addOption(
|
|
100
|
+
"cmd-enter",
|
|
101
|
+
"Cmd/Ctrl+Enter to send, Enter for newline",
|
|
102
|
+
)
|
|
103
|
+
.setValue(this.plugin.settings.sendMessageShortcut)
|
|
104
|
+
.onChange(async (value) => {
|
|
105
|
+
this.plugin.settings.sendMessageShortcut = value as
|
|
106
|
+
| "enter"
|
|
107
|
+
| "cmd-enter";
|
|
108
|
+
await this.plugin.saveSettings();
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
113
|
+
// Mentions
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
new Setting(containerEl).setName("Mentions").setHeading();
|
|
117
|
+
|
|
118
|
+
new Setting(containerEl)
|
|
119
|
+
.setName("Auto-mention active note")
|
|
120
|
+
.setDesc(
|
|
121
|
+
"Include the current note in your messages automatically. The agent will have access to its content without typing @notename.",
|
|
122
|
+
)
|
|
123
|
+
.addToggle((toggle) =>
|
|
124
|
+
toggle
|
|
125
|
+
.setValue(this.plugin.settings.autoMentionActiveNote)
|
|
126
|
+
.onChange(async (value) => {
|
|
127
|
+
this.plugin.settings.autoMentionActiveNote = value;
|
|
128
|
+
await this.plugin.saveSettings();
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
new Setting(containerEl)
|
|
133
|
+
.setName("Max note length")
|
|
134
|
+
.setDesc(
|
|
135
|
+
"Maximum characters per mentioned note. Notes longer than this will be truncated.",
|
|
136
|
+
)
|
|
137
|
+
.addText((text) =>
|
|
138
|
+
text
|
|
139
|
+
.setPlaceholder("10000")
|
|
140
|
+
.setValue(
|
|
141
|
+
String(
|
|
142
|
+
this.plugin.settings.displaySettings.maxNoteLength,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
.onChange(async (value) => {
|
|
146
|
+
const num = parseInt(value, 10);
|
|
147
|
+
if (!isNaN(num) && num >= 1) {
|
|
148
|
+
this.plugin.settings.displaySettings.maxNoteLength =
|
|
149
|
+
num;
|
|
150
|
+
await this.plugin.saveSettings();
|
|
151
|
+
}
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
new Setting(containerEl)
|
|
156
|
+
.setName("Max selection length")
|
|
157
|
+
.setDesc(
|
|
158
|
+
"Maximum characters for text selection in auto-mention. Selections longer than this will be truncated.",
|
|
159
|
+
)
|
|
160
|
+
.addText((text) =>
|
|
161
|
+
text
|
|
162
|
+
.setPlaceholder("10000")
|
|
163
|
+
.setValue(
|
|
164
|
+
String(
|
|
165
|
+
this.plugin.settings.displaySettings
|
|
166
|
+
.maxSelectionLength,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
.onChange(async (value) => {
|
|
170
|
+
const num = parseInt(value, 10);
|
|
171
|
+
if (!isNaN(num) && num >= 1) {
|
|
172
|
+
this.plugin.settings.displaySettings.maxSelectionLength =
|
|
173
|
+
num;
|
|
174
|
+
await this.plugin.saveSettings();
|
|
175
|
+
}
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
180
|
+
// Display
|
|
181
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
new Setting(containerEl).setName("Display").setHeading();
|
|
184
|
+
|
|
185
|
+
new Setting(containerEl)
|
|
186
|
+
.setName("Chat view location")
|
|
187
|
+
.setDesc("Where to open new chat views")
|
|
188
|
+
.addDropdown((dropdown) =>
|
|
189
|
+
dropdown
|
|
190
|
+
.addOption("right-tab", "Right pane (tabs)")
|
|
191
|
+
.addOption("right-split", "Right pane (split)")
|
|
192
|
+
.addOption("editor-tab", "Editor area (tabs)")
|
|
193
|
+
.addOption("editor-split", "Editor area (split)")
|
|
194
|
+
.setValue(this.plugin.settings.chatViewLocation)
|
|
195
|
+
.onChange(async (value) => {
|
|
196
|
+
this.plugin.settings.chatViewLocation =
|
|
197
|
+
value as ChatViewLocation;
|
|
198
|
+
await this.plugin.saveSettings();
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
new Setting(containerEl)
|
|
203
|
+
.setName("Chat font size")
|
|
204
|
+
.setDesc(
|
|
205
|
+
`Adjust the font size of the chat message area (${CHAT_FONT_SIZE_MIN}-${CHAT_FONT_SIZE_MAX}px).`,
|
|
206
|
+
)
|
|
207
|
+
.addText((text) => {
|
|
208
|
+
const getCurrentDisplayValue = (): string => {
|
|
209
|
+
const currentFontSize =
|
|
210
|
+
this.plugin.settings.displaySettings.fontSize;
|
|
211
|
+
return currentFontSize === null
|
|
212
|
+
? ""
|
|
213
|
+
: String(currentFontSize);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const persistChatFontSize = async (
|
|
217
|
+
fontSize: number | null,
|
|
218
|
+
): Promise<void> => {
|
|
219
|
+
if (
|
|
220
|
+
this.plugin.settings.displaySettings.fontSize ===
|
|
221
|
+
fontSize
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const nextSettings = {
|
|
227
|
+
...this.plugin.settings,
|
|
228
|
+
displaySettings: {
|
|
229
|
+
...this.plugin.settings.displaySettings,
|
|
230
|
+
fontSize,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
await this.plugin.saveSettingsAndNotify(nextSettings);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
text.setPlaceholder(
|
|
237
|
+
`${CHAT_FONT_SIZE_MIN}-${CHAT_FONT_SIZE_MAX}`,
|
|
238
|
+
)
|
|
239
|
+
.setValue(getCurrentDisplayValue())
|
|
240
|
+
.onChange(async (value) => {
|
|
241
|
+
if (value.trim().length === 0) {
|
|
242
|
+
await persistChatFontSize(null);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const trimmedValue = value.trim();
|
|
247
|
+
if (!/^-?\d+$/.test(trimmedValue)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const numericValue = Number.parseInt(trimmedValue, 10);
|
|
252
|
+
if (
|
|
253
|
+
numericValue < CHAT_FONT_SIZE_MIN ||
|
|
254
|
+
numericValue > CHAT_FONT_SIZE_MAX
|
|
255
|
+
) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const parsedFontSize = parseChatFontSize(numericValue);
|
|
260
|
+
if (parsedFontSize === null) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const hasChanged =
|
|
265
|
+
this.plugin.settings.displaySettings.fontSize !==
|
|
266
|
+
parsedFontSize;
|
|
267
|
+
if (hasChanged) {
|
|
268
|
+
await persistChatFontSize(parsedFontSize);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
text.inputEl.addEventListener("blur", () => {
|
|
273
|
+
const currentInputValue = text.getValue();
|
|
274
|
+
const parsedFontSize = parseChatFontSize(currentInputValue);
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
currentInputValue.trim().length > 0 &&
|
|
278
|
+
parsedFontSize === null
|
|
279
|
+
) {
|
|
280
|
+
text.setValue(getCurrentDisplayValue());
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (parsedFontSize !== null) {
|
|
285
|
+
text.setValue(String(parsedFontSize));
|
|
286
|
+
const hasChanged =
|
|
287
|
+
this.plugin.settings.displaySettings.fontSize !==
|
|
288
|
+
parsedFontSize;
|
|
289
|
+
if (hasChanged) {
|
|
290
|
+
void persistChatFontSize(parsedFontSize);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
text.setValue("");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
new Setting(containerEl)
|
|
300
|
+
.setName("Show emojis")
|
|
301
|
+
.setDesc(
|
|
302
|
+
"Display emoji icons in tool calls, thoughts, plans, and terminal blocks.",
|
|
303
|
+
)
|
|
304
|
+
.addToggle((toggle) =>
|
|
305
|
+
toggle
|
|
306
|
+
.setValue(this.plugin.settings.displaySettings.showEmojis)
|
|
307
|
+
.onChange(async (value) => {
|
|
308
|
+
this.plugin.settings.displaySettings.showEmojis = value;
|
|
309
|
+
await this.plugin.saveSettings();
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
new Setting(containerEl)
|
|
314
|
+
.setName("Auto-collapse long diffs")
|
|
315
|
+
.setDesc(
|
|
316
|
+
"Automatically collapse diffs that exceed the line threshold.",
|
|
317
|
+
)
|
|
318
|
+
.addToggle((toggle) =>
|
|
319
|
+
toggle
|
|
320
|
+
.setValue(
|
|
321
|
+
this.plugin.settings.displaySettings.autoCollapseDiffs,
|
|
322
|
+
)
|
|
323
|
+
.onChange(async (value) => {
|
|
324
|
+
this.plugin.settings.displaySettings.autoCollapseDiffs =
|
|
325
|
+
value;
|
|
326
|
+
await this.plugin.saveSettings();
|
|
327
|
+
this.display();
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (this.plugin.settings.displaySettings.autoCollapseDiffs) {
|
|
332
|
+
new Setting(containerEl)
|
|
333
|
+
.setName("Collapse threshold")
|
|
334
|
+
.setDesc(
|
|
335
|
+
"Diffs with more lines than this will be collapsed by default.",
|
|
336
|
+
)
|
|
337
|
+
.addText((text) =>
|
|
338
|
+
text
|
|
339
|
+
.setPlaceholder("10")
|
|
340
|
+
.setValue(
|
|
341
|
+
String(
|
|
342
|
+
this.plugin.settings.displaySettings
|
|
343
|
+
.diffCollapseThreshold,
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
.onChange(async (value) => {
|
|
347
|
+
const num = parseInt(value, 10);
|
|
348
|
+
if (!isNaN(num) && num > 0) {
|
|
349
|
+
this.plugin.settings.displaySettings.diffCollapseThreshold =
|
|
350
|
+
num;
|
|
351
|
+
await this.plugin.saveSettings();
|
|
352
|
+
}
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
358
|
+
// Floating chat
|
|
359
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
new Setting(containerEl).setName("Floating chat").setHeading();
|
|
362
|
+
|
|
363
|
+
new Setting(containerEl)
|
|
364
|
+
.setName("Enable floating chat")
|
|
365
|
+
.setDesc(
|
|
366
|
+
"Enable the floating chat button and draggable chat windows.",
|
|
367
|
+
)
|
|
368
|
+
.addToggle((toggle) =>
|
|
369
|
+
toggle
|
|
370
|
+
.setValue(this.plugin.settings.enableFloatingChat)
|
|
371
|
+
.onChange(async (value) => {
|
|
372
|
+
const wasEnabled =
|
|
373
|
+
this.plugin.settings.enableFloatingChat;
|
|
374
|
+
await this.plugin.settingsService.updateSettings({
|
|
375
|
+
enableFloatingChat: value,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Handle dynamic toggle of floating chat
|
|
379
|
+
if (value && !wasEnabled) {
|
|
380
|
+
// Turning ON: create floating chat instance
|
|
381
|
+
this.plugin.openNewFloatingChat();
|
|
382
|
+
} else if (!value && wasEnabled) {
|
|
383
|
+
// Turning OFF: close all floating chat instances
|
|
384
|
+
const instances =
|
|
385
|
+
this.plugin.getFloatingChatInstances();
|
|
386
|
+
for (const instanceId of instances) {
|
|
387
|
+
this.plugin.closeFloatingChat(instanceId);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
new Setting(containerEl)
|
|
394
|
+
.setName("Floating button image")
|
|
395
|
+
.setDesc(
|
|
396
|
+
"URL or path to an image for the floating button. Leave empty for default icon.",
|
|
397
|
+
)
|
|
398
|
+
.addText((text) =>
|
|
399
|
+
text
|
|
400
|
+
.setPlaceholder("https://example.com/avatar.png")
|
|
401
|
+
.setValue(this.plugin.settings.floatingButtonImage)
|
|
402
|
+
.onChange(async (value) => {
|
|
403
|
+
this.plugin.settings.floatingButtonImage = value.trim();
|
|
404
|
+
await this.plugin.saveSettings();
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
409
|
+
// Permissions
|
|
410
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
new Setting(containerEl).setName("Permissions").setHeading();
|
|
413
|
+
|
|
414
|
+
new Setting(containerEl)
|
|
415
|
+
.setName("Auto-allow permissions")
|
|
416
|
+
.setDesc(
|
|
417
|
+
"Automatically allow all permission requests from agents. ⚠️ Use with caution - this gives agents full access to your system.",
|
|
418
|
+
)
|
|
419
|
+
.addToggle((toggle) =>
|
|
420
|
+
toggle
|
|
421
|
+
.setValue(this.plugin.settings.autoAllowPermissions)
|
|
422
|
+
.onChange(async (value) => {
|
|
423
|
+
this.plugin.settings.autoAllowPermissions = value;
|
|
424
|
+
await this.plugin.saveSettings();
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
429
|
+
// Notifications
|
|
430
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
new Setting(containerEl).setName("Notifications").setHeading();
|
|
433
|
+
|
|
434
|
+
new Setting(containerEl)
|
|
435
|
+
.setName("System notifications")
|
|
436
|
+
.setDesc(
|
|
437
|
+
"Show OS notifications when the agent completes a response or requests permission. Notifications are suppressed while Obsidian is focused.",
|
|
438
|
+
)
|
|
439
|
+
.addToggle((toggle) =>
|
|
440
|
+
toggle
|
|
441
|
+
.setValue(this.plugin.settings.enableSystemNotifications)
|
|
442
|
+
.onChange(async (value) => {
|
|
443
|
+
this.plugin.settings.enableSystemNotifications = value;
|
|
444
|
+
await this.plugin.saveSettings();
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
449
|
+
// Windows WSL Settings (Windows only)
|
|
450
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
if (Platform.isWin) {
|
|
453
|
+
new Setting(containerEl)
|
|
454
|
+
.setName("Windows Subsystem for Linux")
|
|
455
|
+
.setHeading();
|
|
456
|
+
|
|
457
|
+
new Setting(containerEl)
|
|
458
|
+
.setName("Enable WSL mode")
|
|
459
|
+
.setDesc(
|
|
460
|
+
"Run agents inside Windows Subsystem for Linux. Recommended for agents like Codex that don't work well in native Windows environments.",
|
|
461
|
+
)
|
|
462
|
+
.addToggle((toggle) =>
|
|
463
|
+
toggle
|
|
464
|
+
.setValue(this.plugin.settings.windowsWslMode)
|
|
465
|
+
.onChange(async (value) => {
|
|
466
|
+
this.plugin.settings.windowsWslMode = value;
|
|
467
|
+
await this.plugin.saveSettings();
|
|
468
|
+
this.display(); // Refresh to show/hide distribution setting
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
if (this.plugin.settings.windowsWslMode) {
|
|
473
|
+
new Setting(containerEl)
|
|
474
|
+
.setName("WSL distribution")
|
|
475
|
+
.setDesc(
|
|
476
|
+
"Specify WSL distribution name (leave empty for default). Example: Ubuntu, Debian",
|
|
477
|
+
)
|
|
478
|
+
.addText((text) =>
|
|
479
|
+
text
|
|
480
|
+
.setPlaceholder("Leave empty for default")
|
|
481
|
+
.setValue(
|
|
482
|
+
this.plugin.settings.windowsWslDistribution ||
|
|
483
|
+
"",
|
|
484
|
+
)
|
|
485
|
+
.onChange(async (value) => {
|
|
486
|
+
this.plugin.settings.windowsWslDistribution =
|
|
487
|
+
value.trim() || undefined;
|
|
488
|
+
await this.plugin.saveSettings();
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
495
|
+
// Agents
|
|
496
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
new Setting(containerEl).setName("Built-in agents").setHeading();
|
|
499
|
+
|
|
500
|
+
this.renderClaudeSettings(containerEl);
|
|
501
|
+
this.renderCodexSettings(containerEl);
|
|
502
|
+
this.renderGeminiSettings(containerEl);
|
|
503
|
+
|
|
504
|
+
new Setting(containerEl).setName("Custom agents").setHeading();
|
|
505
|
+
|
|
506
|
+
this.renderCustomAgents(containerEl);
|
|
507
|
+
|
|
508
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
509
|
+
// Export
|
|
510
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
new Setting(containerEl).setName("Export").setHeading();
|
|
513
|
+
|
|
514
|
+
new Setting(containerEl)
|
|
515
|
+
.setName("Export folder")
|
|
516
|
+
.setDesc("Folder where chat exports will be saved")
|
|
517
|
+
.addText((text) =>
|
|
518
|
+
text
|
|
519
|
+
.setPlaceholder("Agent Client")
|
|
520
|
+
.setValue(this.plugin.settings.exportSettings.defaultFolder)
|
|
521
|
+
.onChange(async (value) => {
|
|
522
|
+
this.plugin.settings.exportSettings.defaultFolder =
|
|
523
|
+
value;
|
|
524
|
+
await this.plugin.saveSettings();
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
new Setting(containerEl)
|
|
529
|
+
.setName("Filename")
|
|
530
|
+
.setDesc(
|
|
531
|
+
"Template for exported filenames. Use {date} for date and {time} for time",
|
|
532
|
+
)
|
|
533
|
+
.addText((text) =>
|
|
534
|
+
text
|
|
535
|
+
.setPlaceholder("agent_client_{date}_{time}")
|
|
536
|
+
.setValue(
|
|
537
|
+
this.plugin.settings.exportSettings.filenameTemplate,
|
|
538
|
+
)
|
|
539
|
+
.onChange(async (value) => {
|
|
540
|
+
this.plugin.settings.exportSettings.filenameTemplate =
|
|
541
|
+
value;
|
|
542
|
+
await this.plugin.saveSettings();
|
|
543
|
+
}),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
new Setting(containerEl)
|
|
547
|
+
.setName("Frontmatter tag")
|
|
548
|
+
.setDesc(
|
|
549
|
+
"Tag to add to exported notes. Supports nested tags (e.g., projects/agent-client). Leave empty to disable.",
|
|
550
|
+
)
|
|
551
|
+
.addText((text) =>
|
|
552
|
+
text
|
|
553
|
+
.setPlaceholder("agent-client")
|
|
554
|
+
.setValue(
|
|
555
|
+
this.plugin.settings.exportSettings.frontmatterTag,
|
|
556
|
+
)
|
|
557
|
+
.onChange(async (value) => {
|
|
558
|
+
this.plugin.settings.exportSettings.frontmatterTag =
|
|
559
|
+
value;
|
|
560
|
+
await this.plugin.saveSettings();
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
new Setting(containerEl)
|
|
565
|
+
.setName("Include images")
|
|
566
|
+
.setDesc("Include images in exported markdown files")
|
|
567
|
+
.addToggle((toggle) =>
|
|
568
|
+
toggle
|
|
569
|
+
.setValue(this.plugin.settings.exportSettings.includeImages)
|
|
570
|
+
.onChange(async (value) => {
|
|
571
|
+
this.plugin.settings.exportSettings.includeImages =
|
|
572
|
+
value;
|
|
573
|
+
await this.plugin.saveSettings();
|
|
574
|
+
this.display();
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
if (this.plugin.settings.exportSettings.includeImages) {
|
|
579
|
+
new Setting(containerEl)
|
|
580
|
+
.setName("Image location")
|
|
581
|
+
.setDesc("Where to save exported images")
|
|
582
|
+
.addDropdown((dropdown) =>
|
|
583
|
+
dropdown
|
|
584
|
+
.addOption(
|
|
585
|
+
"obsidian",
|
|
586
|
+
"Use Obsidian's attachment setting",
|
|
587
|
+
)
|
|
588
|
+
.addOption("custom", "Save to custom folder")
|
|
589
|
+
.addOption(
|
|
590
|
+
"base64",
|
|
591
|
+
"Embed as Base64 (not recommended)",
|
|
592
|
+
)
|
|
593
|
+
.setValue(
|
|
594
|
+
this.plugin.settings.exportSettings.imageLocation,
|
|
595
|
+
)
|
|
596
|
+
.onChange(async (value) => {
|
|
597
|
+
this.plugin.settings.exportSettings.imageLocation =
|
|
598
|
+
value as "obsidian" | "custom" | "base64";
|
|
599
|
+
await this.plugin.saveSettings();
|
|
600
|
+
this.display();
|
|
601
|
+
}),
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
if (
|
|
605
|
+
this.plugin.settings.exportSettings.imageLocation === "custom"
|
|
606
|
+
) {
|
|
607
|
+
new Setting(containerEl)
|
|
608
|
+
.setName("Custom image folder")
|
|
609
|
+
.setDesc(
|
|
610
|
+
"Folder path for exported images (relative to vault root)",
|
|
611
|
+
)
|
|
612
|
+
.addText((text) =>
|
|
613
|
+
text
|
|
614
|
+
.setPlaceholder("Agent Client")
|
|
615
|
+
.setValue(
|
|
616
|
+
this.plugin.settings.exportSettings
|
|
617
|
+
.imageCustomFolder,
|
|
618
|
+
)
|
|
619
|
+
.onChange(async (value) => {
|
|
620
|
+
this.plugin.settings.exportSettings.imageCustomFolder =
|
|
621
|
+
value;
|
|
622
|
+
await this.plugin.saveSettings();
|
|
623
|
+
}),
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
new Setting(containerEl)
|
|
629
|
+
.setName("Auto-export on new chat")
|
|
630
|
+
.setDesc(
|
|
631
|
+
"Automatically export the current chat when starting a new chat",
|
|
632
|
+
)
|
|
633
|
+
.addToggle((toggle) =>
|
|
634
|
+
toggle
|
|
635
|
+
.setValue(
|
|
636
|
+
this.plugin.settings.exportSettings.autoExportOnNewChat,
|
|
637
|
+
)
|
|
638
|
+
.onChange(async (value) => {
|
|
639
|
+
this.plugin.settings.exportSettings.autoExportOnNewChat =
|
|
640
|
+
value;
|
|
641
|
+
await this.plugin.saveSettings();
|
|
642
|
+
}),
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
new Setting(containerEl)
|
|
646
|
+
.setName("Auto-export on close chat")
|
|
647
|
+
.setDesc(
|
|
648
|
+
"Automatically export the current chat when closing the chat view",
|
|
649
|
+
)
|
|
650
|
+
.addToggle((toggle) =>
|
|
651
|
+
toggle
|
|
652
|
+
.setValue(
|
|
653
|
+
this.plugin.settings.exportSettings
|
|
654
|
+
.autoExportOnCloseChat,
|
|
655
|
+
)
|
|
656
|
+
.onChange(async (value) => {
|
|
657
|
+
this.plugin.settings.exportSettings.autoExportOnCloseChat =
|
|
658
|
+
value;
|
|
659
|
+
await this.plugin.saveSettings();
|
|
660
|
+
}),
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
new Setting(containerEl)
|
|
664
|
+
.setName("Open note after export")
|
|
665
|
+
.setDesc("Automatically open the exported note after exporting")
|
|
666
|
+
.addToggle((toggle) =>
|
|
667
|
+
toggle
|
|
668
|
+
.setValue(
|
|
669
|
+
this.plugin.settings.exportSettings.openFileAfterExport,
|
|
670
|
+
)
|
|
671
|
+
.onChange(async (value) => {
|
|
672
|
+
this.plugin.settings.exportSettings.openFileAfterExport =
|
|
673
|
+
value;
|
|
674
|
+
await this.plugin.saveSettings();
|
|
675
|
+
}),
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
679
|
+
// Developer
|
|
680
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
681
|
+
|
|
682
|
+
new Setting(containerEl).setName("Developer").setHeading();
|
|
683
|
+
|
|
684
|
+
new Setting(containerEl)
|
|
685
|
+
.setName("Debug mode")
|
|
686
|
+
.setDesc(
|
|
687
|
+
"Enable debug logging to console. Useful for development and troubleshooting.",
|
|
688
|
+
)
|
|
689
|
+
.addToggle((toggle) =>
|
|
690
|
+
toggle
|
|
691
|
+
.setValue(this.plugin.settings.debugMode)
|
|
692
|
+
.onChange(async (value) => {
|
|
693
|
+
this.plugin.settings.debugMode = value;
|
|
694
|
+
await this.plugin.saveSettings();
|
|
695
|
+
}),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Update the agent dropdown when settings change.
|
|
701
|
+
* Only updates if the value is different to avoid infinite loops.
|
|
702
|
+
*/
|
|
703
|
+
private updateAgentDropdown(): void {
|
|
704
|
+
if (!this.agentSelector) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Get latest settings from store snapshot
|
|
709
|
+
const settings = this.plugin.settingsService.getSnapshot();
|
|
710
|
+
const currentValue = this.agentSelector.getValue();
|
|
711
|
+
|
|
712
|
+
// Only update if different to avoid triggering onChange
|
|
713
|
+
if (settings.defaultAgentId !== currentValue) {
|
|
714
|
+
this.agentSelector.setValue(settings.defaultAgentId);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Called when the settings tab is hidden.
|
|
720
|
+
* Clean up subscriptions to prevent memory leaks.
|
|
721
|
+
*/
|
|
722
|
+
hide(): void {
|
|
723
|
+
if (this.unsubscribe) {
|
|
724
|
+
this.unsubscribe();
|
|
725
|
+
this.unsubscribe = null;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private renderAgentSelector(containerEl: HTMLElement) {
|
|
730
|
+
this.plugin.ensureDefaultAgentId();
|
|
731
|
+
|
|
732
|
+
new Setting(containerEl)
|
|
733
|
+
.setName("Default agent")
|
|
734
|
+
.setDesc("Choose which agent is used when opening a new chat view.")
|
|
735
|
+
.addDropdown((dropdown) => {
|
|
736
|
+
this.agentSelector = dropdown;
|
|
737
|
+
this.populateAgentDropdown(dropdown);
|
|
738
|
+
dropdown.setValue(this.plugin.settings.defaultAgentId);
|
|
739
|
+
dropdown.onChange(async (value) => {
|
|
740
|
+
const nextSettings = {
|
|
741
|
+
...this.plugin.settings,
|
|
742
|
+
defaultAgentId: value,
|
|
743
|
+
};
|
|
744
|
+
this.plugin.ensureDefaultAgentId();
|
|
745
|
+
await this.plugin.saveSettingsAndNotify(nextSettings);
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private populateAgentDropdown(dropdown: DropdownComponent) {
|
|
751
|
+
dropdown.selectEl.empty();
|
|
752
|
+
for (const option of this.getAgentOptions()) {
|
|
753
|
+
dropdown.addOption(option.id, option.label);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private refreshAgentDropdown() {
|
|
758
|
+
if (!this.agentSelector) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
this.populateAgentDropdown(this.agentSelector);
|
|
762
|
+
this.agentSelector.setValue(this.plugin.settings.defaultAgentId);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private getAgentOptions(): { id: string; label: string }[] {
|
|
766
|
+
const toOption = (id: string, displayName: string) => ({
|
|
767
|
+
id,
|
|
768
|
+
label: `${displayName} (${id})`,
|
|
769
|
+
});
|
|
770
|
+
const options: { id: string; label: string }[] = [
|
|
771
|
+
toOption(
|
|
772
|
+
this.plugin.settings.claude.id,
|
|
773
|
+
this.plugin.settings.claude.displayName ||
|
|
774
|
+
this.plugin.settings.claude.id,
|
|
775
|
+
),
|
|
776
|
+
toOption(
|
|
777
|
+
this.plugin.settings.codex.id,
|
|
778
|
+
this.plugin.settings.codex.displayName ||
|
|
779
|
+
this.plugin.settings.codex.id,
|
|
780
|
+
),
|
|
781
|
+
toOption(
|
|
782
|
+
this.plugin.settings.gemini.id,
|
|
783
|
+
this.plugin.settings.gemini.displayName ||
|
|
784
|
+
this.plugin.settings.gemini.id,
|
|
785
|
+
),
|
|
786
|
+
];
|
|
787
|
+
for (const agent of this.plugin.settings.customAgents) {
|
|
788
|
+
if (agent.id && agent.id.length > 0) {
|
|
789
|
+
const labelSource =
|
|
790
|
+
agent.displayName && agent.displayName.length > 0
|
|
791
|
+
? agent.displayName
|
|
792
|
+
: agent.id;
|
|
793
|
+
options.push(toOption(agent.id, labelSource));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const seen = new Set<string>();
|
|
797
|
+
return options.filter(({ id }) => {
|
|
798
|
+
if (seen.has(id)) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
seen.add(id);
|
|
802
|
+
return true;
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private renderGeminiSettings(sectionEl: HTMLElement) {
|
|
807
|
+
const gemini = this.plugin.settings.gemini;
|
|
808
|
+
|
|
809
|
+
new Setting(sectionEl)
|
|
810
|
+
.setName(gemini.displayName || "Gemini CLI")
|
|
811
|
+
.setHeading();
|
|
812
|
+
|
|
813
|
+
new Setting(sectionEl)
|
|
814
|
+
.setName("API key")
|
|
815
|
+
.setDesc(
|
|
816
|
+
"Gemini API key. Required if not logging in with a Google account. (Stored as plain text)",
|
|
817
|
+
)
|
|
818
|
+
.addText((text) => {
|
|
819
|
+
text.setPlaceholder("Enter your Gemini API key")
|
|
820
|
+
.setValue(gemini.apiKey)
|
|
821
|
+
.onChange(async (value) => {
|
|
822
|
+
this.plugin.settings.gemini.apiKey = value.trim();
|
|
823
|
+
await this.plugin.saveSettings();
|
|
824
|
+
});
|
|
825
|
+
text.inputEl.type = "password";
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const geminiPathSetting = new Setting(sectionEl)
|
|
829
|
+
.setName("Path")
|
|
830
|
+
.setDesc(
|
|
831
|
+
'Command name or path to the Gemini CLI. Use just "gemini" to let the login shell resolve it, or enter an absolute path for a specific version.',
|
|
832
|
+
)
|
|
833
|
+
.addText((text) => {
|
|
834
|
+
text.setPlaceholder("gemini")
|
|
835
|
+
.setValue(gemini.command)
|
|
836
|
+
.onChange(async (value) => {
|
|
837
|
+
this.plugin.settings.gemini.command = value.trim();
|
|
838
|
+
await this.plugin.saveSettings();
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
this.addAutoDetectButton(geminiPathSetting, "gemini", async (path) => {
|
|
842
|
+
this.plugin.settings.gemini.command = path;
|
|
843
|
+
await this.plugin.saveSettings();
|
|
844
|
+
});
|
|
845
|
+
this.addInstallHint(sectionEl, "@google/gemini-cli");
|
|
846
|
+
|
|
847
|
+
new Setting(sectionEl)
|
|
848
|
+
.setName("Arguments")
|
|
849
|
+
.setDesc(
|
|
850
|
+
'Enter one argument per line. Leave empty to run without arguments.(Currently, the Gemini CLI requires the "--experimental-acp" option.)',
|
|
851
|
+
)
|
|
852
|
+
.addTextArea((text) => {
|
|
853
|
+
text.setPlaceholder("")
|
|
854
|
+
.setValue(this.formatArgs(gemini.args))
|
|
855
|
+
.onChange(async (value) => {
|
|
856
|
+
this.plugin.settings.gemini.args =
|
|
857
|
+
this.parseArgs(value);
|
|
858
|
+
await this.plugin.saveSettings();
|
|
859
|
+
});
|
|
860
|
+
text.inputEl.rows = 3;
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
new Setting(sectionEl)
|
|
864
|
+
.setName("Environment variables")
|
|
865
|
+
.setDesc(
|
|
866
|
+
"Enter KEY=VALUE pairs, one per line. Required to authenticate with Vertex AI. GEMINI_API_KEY is derived from the field above.(Stored as plain text)",
|
|
867
|
+
)
|
|
868
|
+
.addTextArea((text) => {
|
|
869
|
+
text.setPlaceholder("GOOGLE_CLOUD_PROJECT=...")
|
|
870
|
+
.setValue(this.formatEnv(gemini.env))
|
|
871
|
+
.onChange(async (value) => {
|
|
872
|
+
this.plugin.settings.gemini.env = this.parseEnv(value);
|
|
873
|
+
await this.plugin.saveSettings();
|
|
874
|
+
});
|
|
875
|
+
text.inputEl.rows = 3;
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
private renderClaudeSettings(sectionEl: HTMLElement) {
|
|
880
|
+
const claude = this.plugin.settings.claude;
|
|
881
|
+
|
|
882
|
+
new Setting(sectionEl)
|
|
883
|
+
.setName(claude.displayName || "Claude Code (ACP)")
|
|
884
|
+
.setHeading();
|
|
885
|
+
|
|
886
|
+
new Setting(sectionEl)
|
|
887
|
+
.setName("API key")
|
|
888
|
+
.setDesc(
|
|
889
|
+
"Anthropic API key. Required if not logging in with an Anthropic account. (Stored as plain text)",
|
|
890
|
+
)
|
|
891
|
+
.addText((text) => {
|
|
892
|
+
text.setPlaceholder("Enter your Anthropic API key")
|
|
893
|
+
.setValue(claude.apiKey)
|
|
894
|
+
.onChange(async (value) => {
|
|
895
|
+
this.plugin.settings.claude.apiKey = value.trim();
|
|
896
|
+
await this.plugin.saveSettings();
|
|
897
|
+
});
|
|
898
|
+
text.inputEl.type = "password";
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
const claudePathSetting = new Setting(sectionEl)
|
|
902
|
+
.setName("Path")
|
|
903
|
+
.setDesc(
|
|
904
|
+
'Command name or path to claude-agent-acp. Use just "claude-agent-acp" to let the login shell resolve it, or enter an absolute path.',
|
|
905
|
+
)
|
|
906
|
+
.addText((text) => {
|
|
907
|
+
text.setPlaceholder("claude-agent-acp")
|
|
908
|
+
.setValue(claude.command)
|
|
909
|
+
.onChange(async (value) => {
|
|
910
|
+
this.plugin.settings.claude.command = value.trim();
|
|
911
|
+
await this.plugin.saveSettings();
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
this.addAutoDetectButton(
|
|
915
|
+
claudePathSetting,
|
|
916
|
+
"claude-agent-acp",
|
|
917
|
+
async (path) => {
|
|
918
|
+
this.plugin.settings.claude.command = path;
|
|
919
|
+
await this.plugin.saveSettings();
|
|
920
|
+
},
|
|
921
|
+
);
|
|
922
|
+
this.addInstallHint(sectionEl, "@agentclientprotocol/claude-agent-acp");
|
|
923
|
+
|
|
924
|
+
new Setting(sectionEl)
|
|
925
|
+
.setName("Arguments")
|
|
926
|
+
.setDesc(
|
|
927
|
+
"Enter one argument per line. Leave empty to run without arguments.",
|
|
928
|
+
)
|
|
929
|
+
.addTextArea((text) => {
|
|
930
|
+
text.setPlaceholder("")
|
|
931
|
+
.setValue(this.formatArgs(claude.args))
|
|
932
|
+
.onChange(async (value) => {
|
|
933
|
+
this.plugin.settings.claude.args =
|
|
934
|
+
this.parseArgs(value);
|
|
935
|
+
await this.plugin.saveSettings();
|
|
936
|
+
});
|
|
937
|
+
text.inputEl.rows = 3;
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
new Setting(sectionEl)
|
|
941
|
+
.setName("Environment variables")
|
|
942
|
+
.setDesc(
|
|
943
|
+
"Enter KEY=VALUE pairs, one per line. ANTHROPIC_API_KEY is derived from the field above.",
|
|
944
|
+
)
|
|
945
|
+
.addTextArea((text) => {
|
|
946
|
+
text.setPlaceholder("")
|
|
947
|
+
.setValue(this.formatEnv(claude.env))
|
|
948
|
+
.onChange(async (value) => {
|
|
949
|
+
this.plugin.settings.claude.env = this.parseEnv(value);
|
|
950
|
+
await this.plugin.saveSettings();
|
|
951
|
+
});
|
|
952
|
+
text.inputEl.rows = 3;
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private renderCodexSettings(sectionEl: HTMLElement) {
|
|
957
|
+
const codex = this.plugin.settings.codex;
|
|
958
|
+
|
|
959
|
+
new Setting(sectionEl)
|
|
960
|
+
.setName(codex.displayName || "Codex")
|
|
961
|
+
.setHeading();
|
|
962
|
+
|
|
963
|
+
new Setting(sectionEl)
|
|
964
|
+
.setName("API key")
|
|
965
|
+
.setDesc(
|
|
966
|
+
"OpenAI API key. Required if not logging in with an OpenAI account. (Stored as plain text)",
|
|
967
|
+
)
|
|
968
|
+
.addText((text) => {
|
|
969
|
+
text.setPlaceholder("Enter your OpenAI API key")
|
|
970
|
+
.setValue(codex.apiKey)
|
|
971
|
+
.onChange(async (value) => {
|
|
972
|
+
this.plugin.settings.codex.apiKey = value.trim();
|
|
973
|
+
await this.plugin.saveSettings();
|
|
974
|
+
});
|
|
975
|
+
text.inputEl.type = "password";
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
const codexPathSetting = new Setting(sectionEl)
|
|
979
|
+
.setName("Path")
|
|
980
|
+
.setDesc(
|
|
981
|
+
'Command name or path to codex-acp. Use just "codex-acp" to let the login shell resolve it, or enter an absolute path.',
|
|
982
|
+
)
|
|
983
|
+
.addText((text) => {
|
|
984
|
+
text.setPlaceholder("codex-acp")
|
|
985
|
+
.setValue(codex.command)
|
|
986
|
+
.onChange(async (value) => {
|
|
987
|
+
this.plugin.settings.codex.command = value.trim();
|
|
988
|
+
await this.plugin.saveSettings();
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
this.addAutoDetectButton(
|
|
992
|
+
codexPathSetting,
|
|
993
|
+
"codex-acp",
|
|
994
|
+
async (path) => {
|
|
995
|
+
this.plugin.settings.codex.command = path;
|
|
996
|
+
await this.plugin.saveSettings();
|
|
997
|
+
},
|
|
998
|
+
);
|
|
999
|
+
this.addInstallHint(sectionEl, "@zed-industries/codex-acp");
|
|
1000
|
+
|
|
1001
|
+
new Setting(sectionEl)
|
|
1002
|
+
.setName("Arguments")
|
|
1003
|
+
.setDesc(
|
|
1004
|
+
"Enter one argument per line. Leave empty to run without arguments.",
|
|
1005
|
+
)
|
|
1006
|
+
.addTextArea((text) => {
|
|
1007
|
+
text.setPlaceholder("")
|
|
1008
|
+
.setValue(this.formatArgs(codex.args))
|
|
1009
|
+
.onChange(async (value) => {
|
|
1010
|
+
this.plugin.settings.codex.args = this.parseArgs(value);
|
|
1011
|
+
await this.plugin.saveSettings();
|
|
1012
|
+
});
|
|
1013
|
+
text.inputEl.rows = 3;
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
new Setting(sectionEl)
|
|
1017
|
+
.setName("Environment variables")
|
|
1018
|
+
.setDesc(
|
|
1019
|
+
"Enter KEY=VALUE pairs, one per line. OPENAI_API_KEY is derived from the field above.",
|
|
1020
|
+
)
|
|
1021
|
+
.addTextArea((text) => {
|
|
1022
|
+
text.setPlaceholder("")
|
|
1023
|
+
.setValue(this.formatEnv(codex.env))
|
|
1024
|
+
.onChange(async (value) => {
|
|
1025
|
+
this.plugin.settings.codex.env = this.parseEnv(value);
|
|
1026
|
+
await this.plugin.saveSettings();
|
|
1027
|
+
});
|
|
1028
|
+
text.inputEl.rows = 3;
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private renderCustomAgents(containerEl: HTMLElement) {
|
|
1033
|
+
if (this.plugin.settings.customAgents.length === 0) {
|
|
1034
|
+
containerEl.createEl("p", {
|
|
1035
|
+
text: "No custom agents configured yet.",
|
|
1036
|
+
});
|
|
1037
|
+
} else {
|
|
1038
|
+
this.plugin.settings.customAgents.forEach((agent, index) => {
|
|
1039
|
+
this.renderCustomAgent(containerEl, agent, index);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
new Setting(containerEl).addButton((button) => {
|
|
1044
|
+
button
|
|
1045
|
+
.setButtonText("Add custom agent")
|
|
1046
|
+
.setCta()
|
|
1047
|
+
.onClick(async () => {
|
|
1048
|
+
const newId = this.generateCustomAgentId();
|
|
1049
|
+
const newDisplayName =
|
|
1050
|
+
this.generateCustomAgentDisplayName();
|
|
1051
|
+
this.plugin.settings.customAgents.push({
|
|
1052
|
+
id: newId,
|
|
1053
|
+
displayName: newDisplayName,
|
|
1054
|
+
command: "",
|
|
1055
|
+
args: [],
|
|
1056
|
+
env: [],
|
|
1057
|
+
});
|
|
1058
|
+
this.plugin.ensureDefaultAgentId();
|
|
1059
|
+
await this.plugin.saveSettings();
|
|
1060
|
+
this.display();
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
private renderCustomAgent(
|
|
1066
|
+
containerEl: HTMLElement,
|
|
1067
|
+
agent: CustomAgentSettings,
|
|
1068
|
+
index: number,
|
|
1069
|
+
) {
|
|
1070
|
+
const blockEl = containerEl.createDiv({
|
|
1071
|
+
cls: "agent-client-custom-agent",
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
const idSetting = new Setting(blockEl)
|
|
1075
|
+
.setName("Agent ID")
|
|
1076
|
+
.setDesc("Unique identifier used to reference this agent.")
|
|
1077
|
+
.addText((text) => {
|
|
1078
|
+
text.setPlaceholder("custom-agent")
|
|
1079
|
+
.setValue(agent.id)
|
|
1080
|
+
.onChange(async (value) => {
|
|
1081
|
+
const previousId =
|
|
1082
|
+
this.plugin.settings.customAgents[index].id;
|
|
1083
|
+
const trimmed = value.trim();
|
|
1084
|
+
let nextId = trimmed;
|
|
1085
|
+
if (nextId.length === 0) {
|
|
1086
|
+
nextId = this.generateCustomAgentId();
|
|
1087
|
+
text.setValue(nextId);
|
|
1088
|
+
}
|
|
1089
|
+
this.plugin.settings.customAgents[index].id = nextId;
|
|
1090
|
+
if (
|
|
1091
|
+
this.plugin.settings.defaultAgentId === previousId
|
|
1092
|
+
) {
|
|
1093
|
+
this.plugin.settings.defaultAgentId = nextId;
|
|
1094
|
+
}
|
|
1095
|
+
this.plugin.ensureDefaultAgentId();
|
|
1096
|
+
await this.plugin.saveSettings();
|
|
1097
|
+
this.refreshAgentDropdown();
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
idSetting.addExtraButton((button) => {
|
|
1102
|
+
button
|
|
1103
|
+
.setIcon("trash")
|
|
1104
|
+
.setTooltip("Delete this agent")
|
|
1105
|
+
.onClick(async () => {
|
|
1106
|
+
this.plugin.settings.customAgents.splice(index, 1);
|
|
1107
|
+
this.plugin.ensureDefaultAgentId();
|
|
1108
|
+
await this.plugin.saveSettings();
|
|
1109
|
+
this.display();
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
new Setting(blockEl)
|
|
1114
|
+
.setName("Display name")
|
|
1115
|
+
.setDesc("Shown in menus and headers.")
|
|
1116
|
+
.addText((text) => {
|
|
1117
|
+
text.setPlaceholder("Custom agent")
|
|
1118
|
+
.setValue(agent.displayName || agent.id)
|
|
1119
|
+
.onChange(async (value) => {
|
|
1120
|
+
const trimmed = value.trim();
|
|
1121
|
+
this.plugin.settings.customAgents[index].displayName =
|
|
1122
|
+
trimmed.length > 0
|
|
1123
|
+
? trimmed
|
|
1124
|
+
: this.plugin.settings.customAgents[index].id;
|
|
1125
|
+
await this.plugin.saveSettings();
|
|
1126
|
+
this.refreshAgentDropdown();
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
new Setting(blockEl)
|
|
1131
|
+
.setName("Path")
|
|
1132
|
+
.setDesc(
|
|
1133
|
+
"Command name or path to the custom agent. Use just the command name to let the login shell resolve it, or enter an absolute path.",
|
|
1134
|
+
)
|
|
1135
|
+
.addText((text) => {
|
|
1136
|
+
text.setPlaceholder("Command name or path")
|
|
1137
|
+
.setValue(agent.command)
|
|
1138
|
+
.onChange(async (value) => {
|
|
1139
|
+
this.plugin.settings.customAgents[index].command =
|
|
1140
|
+
value.trim();
|
|
1141
|
+
await this.plugin.saveSettings();
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
new Setting(blockEl)
|
|
1146
|
+
.setName("Arguments")
|
|
1147
|
+
.setDesc(
|
|
1148
|
+
"Enter one argument per line. Leave empty to run without arguments.",
|
|
1149
|
+
)
|
|
1150
|
+
.addTextArea((text) => {
|
|
1151
|
+
text.setPlaceholder("--flag\n--another=value")
|
|
1152
|
+
.setValue(this.formatArgs(agent.args))
|
|
1153
|
+
.onChange(async (value) => {
|
|
1154
|
+
this.plugin.settings.customAgents[index].args =
|
|
1155
|
+
this.parseArgs(value);
|
|
1156
|
+
await this.plugin.saveSettings();
|
|
1157
|
+
});
|
|
1158
|
+
text.inputEl.rows = 3;
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
new Setting(blockEl)
|
|
1162
|
+
.setName("Environment variables")
|
|
1163
|
+
.setDesc(
|
|
1164
|
+
"Enter KEY=VALUE pairs, one per line. (Stored as plain text)",
|
|
1165
|
+
)
|
|
1166
|
+
.addTextArea((text) => {
|
|
1167
|
+
text.setPlaceholder("TOKEN=...")
|
|
1168
|
+
.setValue(this.formatEnv(agent.env))
|
|
1169
|
+
.onChange(async (value) => {
|
|
1170
|
+
this.plugin.settings.customAgents[index].env =
|
|
1171
|
+
this.parseEnv(value);
|
|
1172
|
+
await this.plugin.saveSettings();
|
|
1173
|
+
});
|
|
1174
|
+
text.inputEl.rows = 3;
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private generateCustomAgentDisplayName(): string {
|
|
1179
|
+
const base = "Custom agent";
|
|
1180
|
+
const existing = new Set<string>();
|
|
1181
|
+
existing.add(
|
|
1182
|
+
this.plugin.settings.claude.displayName ||
|
|
1183
|
+
this.plugin.settings.claude.id,
|
|
1184
|
+
);
|
|
1185
|
+
existing.add(
|
|
1186
|
+
this.plugin.settings.codex.displayName ||
|
|
1187
|
+
this.plugin.settings.codex.id,
|
|
1188
|
+
);
|
|
1189
|
+
existing.add(
|
|
1190
|
+
this.plugin.settings.gemini.displayName ||
|
|
1191
|
+
this.plugin.settings.gemini.id,
|
|
1192
|
+
);
|
|
1193
|
+
for (const item of this.plugin.settings.customAgents) {
|
|
1194
|
+
existing.add(item.displayName || item.id);
|
|
1195
|
+
}
|
|
1196
|
+
if (!existing.has(base)) {
|
|
1197
|
+
return base;
|
|
1198
|
+
}
|
|
1199
|
+
let counter = 2;
|
|
1200
|
+
let candidate = `${base} ${counter}`;
|
|
1201
|
+
while (existing.has(candidate)) {
|
|
1202
|
+
counter += 1;
|
|
1203
|
+
candidate = `${base} ${counter}`;
|
|
1204
|
+
}
|
|
1205
|
+
return candidate;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Create a readable ID for new custom agents and avoid collisions
|
|
1209
|
+
private generateCustomAgentId(): string {
|
|
1210
|
+
const base = "custom-agent";
|
|
1211
|
+
const existing = new Set(
|
|
1212
|
+
this.plugin.settings.customAgents.map((item) => item.id),
|
|
1213
|
+
);
|
|
1214
|
+
if (!existing.has(base)) {
|
|
1215
|
+
return base;
|
|
1216
|
+
}
|
|
1217
|
+
let counter = 2;
|
|
1218
|
+
let candidate = `${base}-${counter}`;
|
|
1219
|
+
while (existing.has(candidate)) {
|
|
1220
|
+
counter += 1;
|
|
1221
|
+
candidate = `${base}-${counter}`;
|
|
1222
|
+
}
|
|
1223
|
+
return candidate;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Renders a copyable npm install command hint below a Path setting.
|
|
1228
|
+
*/
|
|
1229
|
+
private addInstallHint(containerEl: HTMLElement, npmPackage: string): void {
|
|
1230
|
+
const command = `npm install -g ${npmPackage}@latest`;
|
|
1231
|
+
const frag = createFragment();
|
|
1232
|
+
frag.appendText("Not installed? Run in terminal: ");
|
|
1233
|
+
frag.createEl("code", { text: command });
|
|
1234
|
+
new Setting(containerEl).setDesc(frag).addButton((btn) => {
|
|
1235
|
+
btn.setButtonText("Copy").onClick(() => {
|
|
1236
|
+
void navigator.clipboard.writeText(command).then(
|
|
1237
|
+
() => {
|
|
1238
|
+
btn.setButtonText("Copied!");
|
|
1239
|
+
window.setTimeout(() => {
|
|
1240
|
+
btn.setButtonText("Copy");
|
|
1241
|
+
}, 1500);
|
|
1242
|
+
},
|
|
1243
|
+
() => undefined,
|
|
1244
|
+
);
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Shared helper: adds an "Auto-detect" button to a Path setting.
|
|
1251
|
+
* Calls `resolveCommandPath(commandName)` and, on success, writes the
|
|
1252
|
+
* resolved absolute path via `onResolved`, then re-renders the tab.
|
|
1253
|
+
*/
|
|
1254
|
+
private addAutoDetectButton(
|
|
1255
|
+
setting: import("obsidian").Setting,
|
|
1256
|
+
commandName: string,
|
|
1257
|
+
onResolved: (path: string) => Promise<void>,
|
|
1258
|
+
): void {
|
|
1259
|
+
setting.addButton((btn) => {
|
|
1260
|
+
const isWsl = Platform.isWin && this.plugin.settings.windowsWslMode;
|
|
1261
|
+
const lookupCmd = Platform.isWin && !isWsl ? "where" : "which";
|
|
1262
|
+
btn.setButtonText("Auto-detect")
|
|
1263
|
+
.setTooltip(
|
|
1264
|
+
`Run \`${lookupCmd} ${commandName}\` to find the path`,
|
|
1265
|
+
)
|
|
1266
|
+
.onClick(async () => {
|
|
1267
|
+
btn.setButtonText("Detecting…");
|
|
1268
|
+
btn.setDisabled(true);
|
|
1269
|
+
try {
|
|
1270
|
+
const found = isWsl
|
|
1271
|
+
? await resolveCommandPathInWsl(
|
|
1272
|
+
commandName,
|
|
1273
|
+
this.plugin.settings
|
|
1274
|
+
.windowsWslDistribution || undefined,
|
|
1275
|
+
)
|
|
1276
|
+
: await resolveCommandPath(commandName);
|
|
1277
|
+
if (found) {
|
|
1278
|
+
await onResolved(found);
|
|
1279
|
+
this.display();
|
|
1280
|
+
} else {
|
|
1281
|
+
btn.setButtonText("Not found");
|
|
1282
|
+
window.setTimeout(() => {
|
|
1283
|
+
btn.setButtonText("Auto-detect");
|
|
1284
|
+
btn.setDisabled(false);
|
|
1285
|
+
}, 2000);
|
|
1286
|
+
}
|
|
1287
|
+
} catch {
|
|
1288
|
+
btn.setButtonText("Error");
|
|
1289
|
+
window.setTimeout(() => {
|
|
1290
|
+
btn.setButtonText("Auto-detect");
|
|
1291
|
+
btn.setDisabled(false);
|
|
1292
|
+
}, 2000);
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
private formatArgs(args: string[]): string {
|
|
1299
|
+
return args.join("\n");
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
private parseArgs(value: string): string[] {
|
|
1303
|
+
return value
|
|
1304
|
+
.split(/\r?\n/)
|
|
1305
|
+
.map((line) => line.trim())
|
|
1306
|
+
.filter((line) => line.length > 0);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
private formatEnv(env: AgentEnvVar[]): string {
|
|
1310
|
+
return env
|
|
1311
|
+
.map((entry) => `${entry.key}=${entry.value ?? ""}`)
|
|
1312
|
+
.join("\n");
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
private parseEnv(value: string): AgentEnvVar[] {
|
|
1316
|
+
const envVars: AgentEnvVar[] = [];
|
|
1317
|
+
|
|
1318
|
+
for (const line of value.split(/\r?\n/)) {
|
|
1319
|
+
const trimmed = line.trim();
|
|
1320
|
+
if (!trimmed) {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
const delimiter = trimmed.indexOf("=");
|
|
1324
|
+
if (delimiter === -1) {
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
const key = trimmed.slice(0, delimiter).trim();
|
|
1328
|
+
const envValue = trimmed.slice(delimiter + 1).trim();
|
|
1329
|
+
if (!key) {
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
envVars.push({ key, value: envValue });
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return normalizeEnvVars(envVars);
|
|
1336
|
+
}
|
|
1337
|
+
}
|