@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.
Files changed (146) hide show
  1. package/.claude/hooks/gh-setup.sh +49 -0
  2. package/.claude/settings.json +15 -0
  3. package/.claude/skills/release-notes/SKILL.md +331 -0
  4. package/.editorconfig +10 -0
  5. package/.github/FUNDING.yml +2 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
  9. package/.github/copilot-instructions.md +45 -0
  10. package/.github/pull_request_template.md +32 -0
  11. package/.github/workflows/ci.yaml +25 -0
  12. package/.github/workflows/docs.yml +58 -0
  13. package/.github/workflows/relay_to_openclaw.yml +59 -0
  14. package/.github/workflows/release.yaml +45 -0
  15. package/.prettierignore +10 -0
  16. package/.prettierrc +13 -0
  17. package/.vscode/extensions.json +7 -0
  18. package/.vscode/settings.json +37 -0
  19. package/.zed/settings.json +42 -0
  20. package/AGENTS.md +330 -0
  21. package/ARCHITECTURE.md +390 -0
  22. package/CONTRIBUTING.md +216 -0
  23. package/LICENSE +202 -0
  24. package/NOTICE +2 -0
  25. package/README.ja.md +121 -0
  26. package/README.md +125 -0
  27. package/docs/.vitepress/config.mts +124 -0
  28. package/docs/.vitepress/theme/custom.css +111 -0
  29. package/docs/.vitepress/theme/index.ts +4 -0
  30. package/docs/agent-setup/claude-code.md +84 -0
  31. package/docs/agent-setup/codex.md +76 -0
  32. package/docs/agent-setup/custom-agents.md +67 -0
  33. package/docs/agent-setup/gemini-cli.md +99 -0
  34. package/docs/agent-setup/index.md +34 -0
  35. package/docs/announcements/gemini-cli-deprecation.md +73 -0
  36. package/docs/getting-started/index.md +78 -0
  37. package/docs/getting-started/quick-start.md +38 -0
  38. package/docs/help/faq.md +181 -0
  39. package/docs/help/troubleshooting.md +221 -0
  40. package/docs/index.md +63 -0
  41. package/docs/public/apple-touch-icon.png +0 -0
  42. package/docs/public/demo.mp4 +0 -0
  43. package/docs/public/favicon-16x16.png +0 -0
  44. package/docs/public/favicon-32x32.png +0 -0
  45. package/docs/public/favicon.ico +0 -0
  46. package/docs/public/images/editing.webp +0 -0
  47. package/docs/public/images/export.webp +0 -0
  48. package/docs/public/images/floating-chat-button.webp +0 -0
  49. package/docs/public/images/floating-chat-instance-menu.webp +0 -0
  50. package/docs/public/images/floating-chat-view.webp +0 -0
  51. package/docs/public/images/mode-selection.webp +0 -0
  52. package/docs/public/images/model-selection.webp +0 -0
  53. package/docs/public/images/multi-session.webp +0 -0
  54. package/docs/public/images/remove-image.webp +0 -0
  55. package/docs/public/images/ribbon-icon.webp +0 -0
  56. package/docs/public/images/selection-context.gif +0 -0
  57. package/docs/public/images/sending-images.webp +0 -0
  58. package/docs/public/images/sending-messages.webp +0 -0
  59. package/docs/public/images/session-history-button.webp +0 -0
  60. package/docs/public/images/slash-commands-1.webp +0 -0
  61. package/docs/public/images/slash-commands-2.webp +0 -0
  62. package/docs/public/images/switch-agent.webp +0 -0
  63. package/docs/public/images/switch-default-agent.webp +0 -0
  64. package/docs/public/images/temporary-disable.gif +0 -0
  65. package/docs/reference/acp-support.md +110 -0
  66. package/docs/usage/chat-export.md +80 -0
  67. package/docs/usage/commands.md +51 -0
  68. package/docs/usage/context-files.md +57 -0
  69. package/docs/usage/editing.md +69 -0
  70. package/docs/usage/floating-chat.md +84 -0
  71. package/docs/usage/index.md +97 -0
  72. package/docs/usage/mcp-tools.md +33 -0
  73. package/docs/usage/mentions.md +70 -0
  74. package/docs/usage/mode-selection.md +28 -0
  75. package/docs/usage/model-selection.md +32 -0
  76. package/docs/usage/multi-session.md +68 -0
  77. package/docs/usage/sending-images.md +64 -0
  78. package/docs/usage/session-history.md +91 -0
  79. package/docs/usage/slash-commands.md +44 -0
  80. package/esbuild.config.mjs +49 -0
  81. package/eslint.config.mjs +25 -0
  82. package/main.js +228 -0
  83. package/manifest.json +11 -0
  84. package/package.json +52 -0
  85. package/src/acp/acp-client.ts +921 -0
  86. package/src/acp/acp-handler.ts +252 -0
  87. package/src/acp/permission-handler.ts +282 -0
  88. package/src/acp/terminal-handler.ts +264 -0
  89. package/src/acp/type-converter.ts +272 -0
  90. package/src/hooks/useAgent.ts +250 -0
  91. package/src/hooks/useAgentMessages.ts +470 -0
  92. package/src/hooks/useAgentSession.ts +544 -0
  93. package/src/hooks/useChatActions.ts +400 -0
  94. package/src/hooks/useHistoryModal.ts +219 -0
  95. package/src/hooks/useSessionHistory.ts +863 -0
  96. package/src/hooks/useSettings.ts +19 -0
  97. package/src/hooks/useSuggestions.ts +342 -0
  98. package/src/main.ts +9 -0
  99. package/src/plugin.ts +1126 -0
  100. package/src/services/chat-exporter.ts +552 -0
  101. package/src/services/message-sender.ts +755 -0
  102. package/src/services/message-state.ts +375 -0
  103. package/src/services/session-helpers.ts +211 -0
  104. package/src/services/session-state.ts +130 -0
  105. package/src/services/session-storage.ts +267 -0
  106. package/src/services/settings-normalizer.ts +255 -0
  107. package/src/services/settings-service.ts +285 -0
  108. package/src/services/update-checker.ts +128 -0
  109. package/src/services/vault-service.ts +558 -0
  110. package/src/services/view-registry.ts +345 -0
  111. package/src/types/agent.ts +92 -0
  112. package/src/types/chat.ts +351 -0
  113. package/src/types/errors.ts +136 -0
  114. package/src/types/obsidian-internals.d.ts +14 -0
  115. package/src/types/session.ts +731 -0
  116. package/src/ui/ChangeDirectoryModal.ts +137 -0
  117. package/src/ui/ChatContext.ts +25 -0
  118. package/src/ui/ChatHeader.tsx +295 -0
  119. package/src/ui/ChatPanel.tsx +1162 -0
  120. package/src/ui/ChatView.tsx +348 -0
  121. package/src/ui/ErrorBanner.tsx +104 -0
  122. package/src/ui/FloatingButton.tsx +351 -0
  123. package/src/ui/FloatingChatView.tsx +531 -0
  124. package/src/ui/InputArea.tsx +1107 -0
  125. package/src/ui/InputToolbar.tsx +371 -0
  126. package/src/ui/MessageBubble.tsx +442 -0
  127. package/src/ui/MessageList.tsx +265 -0
  128. package/src/ui/PermissionBanner.tsx +61 -0
  129. package/src/ui/SessionHistoryModal.tsx +821 -0
  130. package/src/ui/SettingsTab.ts +1337 -0
  131. package/src/ui/SuggestionPopup.tsx +138 -0
  132. package/src/ui/TerminalBlock.tsx +107 -0
  133. package/src/ui/ToolCallBlock.tsx +456 -0
  134. package/src/ui/shared/AttachmentStrip.tsx +57 -0
  135. package/src/ui/shared/IconButton.tsx +55 -0
  136. package/src/ui/shared/MarkdownRenderer.tsx +103 -0
  137. package/src/ui/view-host.ts +56 -0
  138. package/src/utils/error-utils.ts +274 -0
  139. package/src/utils/logger.ts +44 -0
  140. package/src/utils/mention-parser.ts +129 -0
  141. package/src/utils/paths.ts +246 -0
  142. package/src/utils/platform.ts +425 -0
  143. package/styles.css +2322 -0
  144. package/tsconfig.json +18 -0
  145. package/version-bump.mjs +18 -0
  146. package/versions.json +3 -0
@@ -0,0 +1,371 @@
1
+ import * as React from "react";
2
+ const { useRef, useEffect, useCallback } = React;
3
+ import { setIcon, DropdownComponent } from "obsidian";
4
+
5
+ import {
6
+ flattenConfigSelectOptions,
7
+ type SessionModeState,
8
+ type SessionModelState,
9
+ type SessionUsage,
10
+ type SessionConfigOption,
11
+ type SessionConfigSelectGroup,
12
+ } from "../types/session";
13
+
14
+ // ============================================================================
15
+ // Obsidian Dropdown Hook
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Hook for managing an Obsidian DropdownComponent lifecycle.
20
+ * Handles creation, option population, value sync, and cleanup.
21
+ */
22
+ function useObsidianDropdown(
23
+ containerRef: React.RefObject<HTMLDivElement | null>,
24
+ options: Array<{ value: string; label: string }> | undefined,
25
+ currentValue: string | undefined,
26
+ onChangeRef: React.RefObject<((value: string) => void) | undefined>,
27
+ ): void {
28
+ const instanceRef = useRef<DropdownComponent | null>(null);
29
+
30
+ // Create/destroy dropdown when options change
31
+ useEffect(() => {
32
+ const containerEl = containerRef.current;
33
+ if (!containerEl) return;
34
+
35
+ if (!options || options.length <= 1) {
36
+ if (instanceRef.current) {
37
+ containerEl.empty();
38
+ instanceRef.current = null;
39
+ }
40
+ return;
41
+ }
42
+
43
+ if (!instanceRef.current) {
44
+ const dropdown = new DropdownComponent(containerEl);
45
+ instanceRef.current = dropdown;
46
+
47
+ for (const opt of options) {
48
+ dropdown.addOption(opt.value, opt.label);
49
+ }
50
+
51
+ if (currentValue) {
52
+ dropdown.setValue(currentValue);
53
+ }
54
+
55
+ dropdown.onChange((value) => {
56
+ onChangeRef.current?.(value);
57
+ });
58
+ }
59
+
60
+ return () => {
61
+ if (instanceRef.current) {
62
+ containerEl.empty();
63
+ instanceRef.current = null;
64
+ }
65
+ };
66
+ }, [options, containerRef, onChangeRef, currentValue]);
67
+
68
+ // Sync value when it changes externally
69
+ useEffect(() => {
70
+ if (instanceRef.current && currentValue) {
71
+ instanceRef.current.setValue(currentValue);
72
+ }
73
+ }, [currentValue]);
74
+ }
75
+
76
+ // ============================================================================
77
+ // Utility Functions
78
+ // ============================================================================
79
+
80
+ /** Format token count for display (e.g., 21367 → "21.4K", 200000 → "200K") */
81
+ function formatTokenCount(tokens: number): string {
82
+ if (tokens < 1000) return String(tokens);
83
+ const k = tokens / 1000;
84
+ return k >= 100 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`;
85
+ }
86
+
87
+ /** Get CSS class for usage percentage color thresholds */
88
+ function getUsageColorClass(percentage: number): string {
89
+ if (percentage >= 90) return "agent-client-usage-danger";
90
+ if (percentage >= 80) return "agent-client-usage-warning";
91
+ if (percentage >= 70) return "agent-client-usage-caution";
92
+ return "agent-client-usage-normal";
93
+ }
94
+
95
+ // ============================================================================
96
+ // InputToolbar
97
+ // ============================================================================
98
+
99
+ export interface InputToolbarProps {
100
+ isSending: boolean;
101
+ isButtonDisabled: boolean;
102
+ hasContent: boolean;
103
+ onSendOrStop: () => void;
104
+ modes?: SessionModeState;
105
+ onModeChange?: (modeId: string) => void;
106
+ models?: SessionModelState;
107
+ onModelChange?: (modelId: string) => void;
108
+ configOptions?: SessionConfigOption[];
109
+ onConfigOptionChange?: (configId: string, value: string) => void;
110
+ usage?: SessionUsage;
111
+ isSessionReady: boolean;
112
+ }
113
+
114
+ export function InputToolbar({
115
+ isSending,
116
+ isButtonDisabled,
117
+ hasContent,
118
+ onSendOrStop,
119
+ modes,
120
+ onModeChange,
121
+ models,
122
+ onModelChange,
123
+ configOptions,
124
+ onConfigOptionChange,
125
+ usage,
126
+ isSessionReady,
127
+ }: InputToolbarProps) {
128
+ // Refs
129
+ const sendButtonRef = useRef<HTMLButtonElement>(null);
130
+ const modeDropdownRef = useRef<HTMLDivElement>(null);
131
+ const modelDropdownRef = useRef<HTMLDivElement>(null);
132
+ const configOptionsRef = useRef<HTMLDivElement>(null);
133
+ const configDropdownInstances = useRef<Map<string, DropdownComponent>>(
134
+ new Map(),
135
+ );
136
+
137
+ // Stable callback refs
138
+ const onModeChangeRef = useRef(onModeChange);
139
+ onModeChangeRef.current = onModeChange;
140
+
141
+ const onModelChangeRef = useRef(onModelChange);
142
+ onModelChangeRef.current = onModelChange;
143
+
144
+ const onConfigOptionChangeRef = useRef(onConfigOptionChange);
145
+ onConfigOptionChangeRef.current = onConfigOptionChange;
146
+
147
+ /**
148
+ * Update send button icon color based on state.
149
+ */
150
+ const updateIconColor = useCallback(
151
+ (svg: SVGElement) => {
152
+ svg.classList.remove(
153
+ "agent-client-icon-sending",
154
+ "agent-client-icon-active",
155
+ "agent-client-icon-inactive",
156
+ );
157
+
158
+ if (isSending) {
159
+ svg.classList.add("agent-client-icon-sending");
160
+ } else {
161
+ svg.classList.add(
162
+ hasContent
163
+ ? "agent-client-icon-active"
164
+ : "agent-client-icon-inactive",
165
+ );
166
+ }
167
+ },
168
+ [isSending, hasContent],
169
+ );
170
+
171
+ // Update send button icon based on sending state
172
+ useEffect(() => {
173
+ if (sendButtonRef.current) {
174
+ const iconName = isSending ? "square" : "send-horizontal";
175
+ setIcon(sendButtonRef.current, iconName);
176
+ const svg = sendButtonRef.current.querySelector("svg");
177
+ if (svg) {
178
+ updateIconColor(svg);
179
+ }
180
+ }
181
+ }, [isSending, updateIconColor]);
182
+
183
+ // Update icon color when hasContent changes
184
+ useEffect(() => {
185
+ if (sendButtonRef.current) {
186
+ const svg = sendButtonRef.current.querySelector("svg");
187
+ if (svg) {
188
+ updateIconColor(svg);
189
+ }
190
+ }
191
+ }, [updateIconColor]);
192
+
193
+ // Mode dropdown
194
+ const modeOptions = modes?.availableModes?.map((m) => ({
195
+ value: m.id,
196
+ label: m.name,
197
+ }));
198
+ useObsidianDropdown(
199
+ modeDropdownRef,
200
+ modeOptions,
201
+ modes?.currentModeId,
202
+ onModeChangeRef,
203
+ );
204
+
205
+ // Model dropdown
206
+ const modelOptions = models?.availableModels?.map((m) => ({
207
+ value: m.modelId,
208
+ label: m.name,
209
+ }));
210
+ useObsidianDropdown(
211
+ modelDropdownRef,
212
+ modelOptions,
213
+ models?.currentModelId,
214
+ onModelChangeRef,
215
+ );
216
+
217
+ // Initialize configOptions dropdowns (dynamic, replaces mode/model when present)
218
+ useEffect(() => {
219
+ const containerEl = configOptionsRef.current;
220
+ if (!containerEl) return;
221
+
222
+ // Clean up existing dropdowns
223
+ containerEl.empty();
224
+ configDropdownInstances.current.clear();
225
+
226
+ if (!configOptions || configOptions.length === 0) return;
227
+
228
+ for (const option of configOptions) {
229
+ // Flatten options (handle both flat and grouped)
230
+ const flatOptions = flattenConfigSelectOptions(option.options);
231
+
232
+ // Only show if there are multiple values
233
+ if (flatOptions.length <= 1) continue;
234
+
235
+ // Create wrapper div with appropriate class based on category
236
+ const categoryClass = option.category
237
+ ? `agent-client-config-selector-${option.category}`
238
+ : "agent-client-config-selector";
239
+ const wrapperEl = containerEl.createDiv({
240
+ cls: `agent-client-config-selector ${categoryClass}`,
241
+ attr: { title: option.description ?? option.name },
242
+ });
243
+
244
+ const dropdownContainer = wrapperEl.createDiv();
245
+ const dropdown = new DropdownComponent(dropdownContainer);
246
+
247
+ // Add options (with group prefix for grouped options)
248
+ if (option.options.length > 0 && "group" in option.options[0]) {
249
+ for (const group of option.options as SessionConfigSelectGroup[]) {
250
+ for (const opt of group.options) {
251
+ dropdown.addOption(
252
+ opt.value,
253
+ `${group.name} / ${opt.name}`,
254
+ );
255
+ }
256
+ }
257
+ } else {
258
+ for (const opt of flatOptions) {
259
+ dropdown.addOption(opt.value, opt.name);
260
+ }
261
+ }
262
+
263
+ // Set current value
264
+ dropdown.setValue(option.currentValue);
265
+
266
+ // Handle change
267
+ const configId = option.id;
268
+ dropdown.onChange((value) => {
269
+ if (onConfigOptionChangeRef.current) {
270
+ onConfigOptionChangeRef.current(configId, value);
271
+ }
272
+ });
273
+
274
+ // Add chevron icon
275
+ const iconEl = wrapperEl.createSpan({
276
+ cls: "agent-client-config-selector-icon",
277
+ });
278
+ setIcon(iconEl, "chevron-down");
279
+
280
+ configDropdownInstances.current.set(option.id, dropdown);
281
+ }
282
+
283
+ return () => {
284
+ containerEl.empty();
285
+ configDropdownInstances.current.clear();
286
+ };
287
+ }, [configOptions]);
288
+
289
+ return (
290
+ <div className="agent-client-chat-input-actions">
291
+ {/* Context Usage Indicator (left-aligned via margin-right: auto) */}
292
+ {usage && (
293
+ <span
294
+ className={`agent-client-usage-indicator ${getUsageColorClass(Math.round((usage.used / usage.size) * 100))}`}
295
+ aria-label={
296
+ usage.cost
297
+ ? `${formatTokenCount(usage.used)} / ${formatTokenCount(usage.size)} tokens\n$${usage.cost.amount.toFixed(2)}`
298
+ : `${formatTokenCount(usage.used)} / ${formatTokenCount(usage.size)} tokens`
299
+ }
300
+ >
301
+ {Math.round((usage.used / usage.size) * 100)}%
302
+ </span>
303
+ )}
304
+
305
+ {/* Config Options (supersedes legacy mode/model selectors) */}
306
+ {configOptions && configOptions.length > 0 ? (
307
+ <div
308
+ ref={configOptionsRef}
309
+ className="agent-client-config-options-container"
310
+ />
311
+ ) : (
312
+ <>
313
+ {/* Legacy Mode Selector */}
314
+ {modes && modes.availableModes.length > 1 && (
315
+ <div
316
+ className="agent-client-mode-selector"
317
+ title={
318
+ modes.availableModes.find(
319
+ (m) => m.id === modes.currentModeId,
320
+ )?.description ?? "Select mode"
321
+ }
322
+ >
323
+ <div ref={modeDropdownRef} />
324
+ <span
325
+ className="agent-client-mode-selector-icon"
326
+ ref={(el) => {
327
+ if (el) setIcon(el, "chevron-down");
328
+ }}
329
+ />
330
+ </div>
331
+ )}
332
+
333
+ {/* Legacy Model Selector */}
334
+ {models && models.availableModels.length > 1 && (
335
+ <div
336
+ className="agent-client-model-selector"
337
+ title={
338
+ models.availableModels.find(
339
+ (m) => m.modelId === models.currentModelId,
340
+ )?.description ?? "Select model"
341
+ }
342
+ >
343
+ <div ref={modelDropdownRef} />
344
+ <span
345
+ className="agent-client-model-selector-icon"
346
+ ref={(el) => {
347
+ if (el) setIcon(el, "chevron-down");
348
+ }}
349
+ />
350
+ </div>
351
+ )}
352
+ </>
353
+ )}
354
+
355
+ {/* Send/Stop Button */}
356
+ <button
357
+ ref={sendButtonRef}
358
+ onClick={onSendOrStop}
359
+ disabled={isButtonDisabled}
360
+ className={`agent-client-chat-send-button ${isSending ? "sending" : ""} ${isButtonDisabled ? "agent-client-disabled" : ""}`}
361
+ title={
362
+ !isSessionReady
363
+ ? "Connecting..."
364
+ : isSending
365
+ ? "Stop generation"
366
+ : "Send message"
367
+ }
368
+ ></button>
369
+ </div>
370
+ );
371
+ }