@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,351 @@
1
+ import * as React from "react";
2
+ const { useState, useRef, useEffect, useCallback, useMemo } = React;
3
+ import { createRoot, type Root } from "react-dom/client";
4
+
5
+ import { setIcon } from "obsidian";
6
+ import type AgentClientPlugin from "../plugin";
7
+ import { useSettings } from "../hooks/useSettings";
8
+ function clampPosition(
9
+ x: number,
10
+ y: number,
11
+ width: number,
12
+ height: number,
13
+ ): { x: number; y: number } {
14
+ return {
15
+ x: Math.max(0, Math.min(x, window.innerWidth - width)),
16
+ y: Math.max(0, Math.min(y, window.innerHeight - height)),
17
+ };
18
+ }
19
+
20
+ interface VaultAdapterWithResourcePath {
21
+ getResourcePath?: (path: string) => string;
22
+ }
23
+
24
+ // ============================================================
25
+ // FloatingButtonContainer Class
26
+ // ============================================================
27
+
28
+ /**
29
+ * Container that manages the floating button React component lifecycle.
30
+ * Independent from any floating chat view instance.
31
+ */
32
+ export class FloatingButtonContainer {
33
+ private root: Root | null = null;
34
+ private containerEl: HTMLElement;
35
+
36
+ constructor(private plugin: AgentClientPlugin) {
37
+ this.containerEl = activeDocument.body.createDiv({
38
+ cls: "agent-client-floating-button-root",
39
+ });
40
+ }
41
+
42
+ mount(): void {
43
+ this.root = createRoot(this.containerEl);
44
+ this.root.render(<FloatingButtonComponent plugin={this.plugin} />);
45
+ }
46
+
47
+ unmount(): void {
48
+ if (this.root) {
49
+ this.root.unmount();
50
+ this.root = null;
51
+ }
52
+ this.containerEl.remove();
53
+ }
54
+ }
55
+
56
+ // ============================================================
57
+ // FloatingButtonComponent
58
+ // ============================================================
59
+
60
+ interface FloatingButtonProps {
61
+ plugin: AgentClientPlugin;
62
+ }
63
+
64
+ function FloatingButtonComponent({ plugin }: FloatingButtonProps) {
65
+ const settings = useSettings(plugin);
66
+
67
+ const [showInstanceMenu, setShowInstanceMenu] = useState(false);
68
+ const instanceMenuRef = useRef<HTMLDivElement>(null);
69
+
70
+ // Button / menu size constants
71
+ const BUTTON_SIZE = 48;
72
+ const MENU_MIN_WIDTH = 220;
73
+
74
+ // Dragging state
75
+ const [position, setPosition] = useState<{ x: number; y: number } | null>(
76
+ () => {
77
+ if (!settings.floatingButtonPosition) return null;
78
+ return clampPosition(
79
+ settings.floatingButtonPosition.x,
80
+ settings.floatingButtonPosition.y,
81
+ BUTTON_SIZE,
82
+ BUTTON_SIZE,
83
+ );
84
+ },
85
+ );
86
+ const [isDragging, setIsDragging] = useState(false);
87
+ const dragOffset = useRef({ x: 0, y: 0 });
88
+ const dragStartPos = useRef({ x: 0, y: 0 });
89
+ const wasDragged = useRef(false);
90
+
91
+ // Floating button image source
92
+ const floatingButtonImageSrc = useMemo(() => {
93
+ const img = settings.floatingButtonImage;
94
+ if (!img) return null;
95
+ if (
96
+ img.startsWith("http://") ||
97
+ img.startsWith("https://") ||
98
+ img.startsWith("data:")
99
+ ) {
100
+ return img;
101
+ }
102
+ return (
103
+ plugin.app.vault.adapter as VaultAdapterWithResourcePath
104
+ ).getResourcePath?.(img);
105
+ }, [settings.floatingButtonImage, plugin.app.vault.adapter]);
106
+
107
+ // Build display labels with duplicate numbering
108
+ const allInstances = plugin.getFloatingChatInstances();
109
+
110
+ const instanceLabels = useMemo(() => {
111
+ const views = plugin.viewRegistry.getByType("floating");
112
+ const entries = views.map((v) => ({
113
+ viewId: v.viewId,
114
+ label: v.getDisplayName(),
115
+ }));
116
+ const countMap = new Map<string, number>();
117
+ for (const e of entries) {
118
+ countMap.set(e.label, (countMap.get(e.label) ?? 0) + 1);
119
+ }
120
+ const indexMap = new Map<string, number>();
121
+ return entries.map((e) => {
122
+ if ((countMap.get(e.label) ?? 0) > 1) {
123
+ const idx = (indexMap.get(e.label) ?? 0) + 1;
124
+ indexMap.set(e.label, idx);
125
+ return {
126
+ viewId: e.viewId,
127
+ label: idx === 1 ? e.label : `${e.label} ${idx}`,
128
+ };
129
+ }
130
+ return e;
131
+ });
132
+ }, [plugin.viewRegistry, allInstances]);
133
+
134
+ // ============================================================
135
+ // Dragging Logic
136
+ // ============================================================
137
+ const DRAG_THRESHOLD = 5;
138
+
139
+ const handleMouseDown = useCallback(
140
+ (e: React.MouseEvent) => {
141
+ // Compute current position (from state or CSS default)
142
+ const currentX =
143
+ position?.x ?? window.innerWidth - 40 - BUTTON_SIZE;
144
+ const currentY =
145
+ position?.y ?? window.innerHeight - 30 - BUTTON_SIZE;
146
+
147
+ setIsDragging(true);
148
+ wasDragged.current = false;
149
+ dragStartPos.current = { x: e.clientX, y: e.clientY };
150
+ dragOffset.current = {
151
+ x: e.clientX - currentX,
152
+ y: e.clientY - currentY,
153
+ };
154
+ e.preventDefault();
155
+ },
156
+ [position],
157
+ );
158
+
159
+ useEffect(() => {
160
+ if (!isDragging) return;
161
+
162
+ const onMouseMove = (e: MouseEvent) => {
163
+ const dx = e.clientX - dragStartPos.current.x;
164
+ const dy = e.clientY - dragStartPos.current.y;
165
+ if (
166
+ !wasDragged.current &&
167
+ Math.abs(dx) < DRAG_THRESHOLD &&
168
+ Math.abs(dy) < DRAG_THRESHOLD
169
+ ) {
170
+ return;
171
+ }
172
+ wasDragged.current = true;
173
+ setPosition(
174
+ clampPosition(
175
+ e.clientX - dragOffset.current.x,
176
+ e.clientY - dragOffset.current.y,
177
+ BUTTON_SIZE,
178
+ BUTTON_SIZE,
179
+ ),
180
+ );
181
+ };
182
+
183
+ const onMouseUp = () => {
184
+ setIsDragging(false);
185
+ };
186
+
187
+ window.addEventListener("mousemove", onMouseMove);
188
+ window.addEventListener("mouseup", onMouseUp);
189
+ return () => {
190
+ window.removeEventListener("mousemove", onMouseMove);
191
+ window.removeEventListener("mouseup", onMouseUp);
192
+ };
193
+ }, [isDragging]);
194
+
195
+ // Save button position to settings (debounced)
196
+ useEffect(() => {
197
+ if (!position) return;
198
+ const timer = window.setTimeout(() => {
199
+ if (
200
+ !settings.floatingButtonPosition ||
201
+ position.x !== settings.floatingButtonPosition.x ||
202
+ position.y !== settings.floatingButtonPosition.y
203
+ ) {
204
+ void plugin.saveSettingsAndNotify({
205
+ ...plugin.settings,
206
+ floatingButtonPosition: position,
207
+ });
208
+ }
209
+ }, 500);
210
+ return () => window.clearTimeout(timer);
211
+ }, [position, plugin, settings.floatingButtonPosition]);
212
+
213
+ // Button click handler
214
+ const handleButtonClick = useCallback(() => {
215
+ if (wasDragged.current) return;
216
+ const instances = plugin.getFloatingChatInstances();
217
+ if (instances.length === 0) {
218
+ // No instances, create one and expand
219
+ plugin.openNewFloatingChat(true);
220
+ } else if (instances.length === 1) {
221
+ // Single instance, just expand
222
+ plugin.expandFloatingChat(instances[0]);
223
+ } else {
224
+ // Multiple instances, show menu
225
+ setShowInstanceMenu(true);
226
+ }
227
+ }, [plugin]);
228
+
229
+ // Close instance menu on outside click
230
+ useEffect(() => {
231
+ if (!showInstanceMenu) return;
232
+
233
+ const handleClickOutside = (event: MouseEvent) => {
234
+ if (
235
+ instanceMenuRef.current &&
236
+ !instanceMenuRef.current.contains(event.target as Node)
237
+ ) {
238
+ setShowInstanceMenu(false);
239
+ }
240
+ };
241
+
242
+ const doc = activeDocument;
243
+ doc.addEventListener("mousedown", handleClickOutside);
244
+ return () => {
245
+ doc.removeEventListener("mousedown", handleClickOutside);
246
+ };
247
+ }, [showInstanceMenu]);
248
+
249
+ if (!settings.enableFloatingChat) return null;
250
+
251
+ const buttonClassName = [
252
+ "agent-client-floating-button",
253
+ floatingButtonImageSrc ? "has-custom-image" : "",
254
+ isDragging ? "is-dragging" : "",
255
+ ]
256
+ .filter(Boolean)
257
+ .join(" ");
258
+
259
+ return (
260
+ <>
261
+ <div
262
+ className={buttonClassName}
263
+ onMouseDown={handleMouseDown}
264
+ onMouseUp={handleButtonClick}
265
+ style={
266
+ position
267
+ ? {
268
+ left: position.x,
269
+ top: position.y,
270
+ right: "auto",
271
+ bottom: "auto",
272
+ }
273
+ : undefined
274
+ }
275
+ >
276
+ {floatingButtonImageSrc ? (
277
+ <img src={floatingButtonImageSrc} alt="Open chat" />
278
+ ) : (
279
+ <div
280
+ className="agent-client-floating-button-fallback"
281
+ ref={(el) => {
282
+ if (el) setIcon(el, "bot-message-square");
283
+ }}
284
+ />
285
+ )}
286
+ </div>
287
+ {showInstanceMenu && (
288
+ <div
289
+ ref={instanceMenuRef}
290
+ className="agent-client-floating-instance-menu"
291
+ style={
292
+ position
293
+ ? {
294
+ bottom:
295
+ window.innerHeight - position.y + 10,
296
+ ...(position.x + MENU_MIN_WIDTH >
297
+ window.innerWidth
298
+ ? {
299
+ right:
300
+ window.innerWidth -
301
+ (position.x + BUTTON_SIZE),
302
+ left: "auto",
303
+ top: "auto",
304
+ }
305
+ : {
306
+ left: position.x,
307
+ right: "auto",
308
+ top: "auto",
309
+ }),
310
+ }
311
+ : undefined
312
+ }
313
+ >
314
+ <div className="agent-client-floating-instance-menu-header">
315
+ Select session to open
316
+ </div>
317
+ {instanceLabels.map(({ viewId: id, label }) => (
318
+ <div
319
+ key={id}
320
+ className="agent-client-floating-instance-menu-item"
321
+ onClick={() => {
322
+ plugin.expandFloatingChat(id);
323
+ plugin.viewRegistry.setFocused(id);
324
+ setShowInstanceMenu(false);
325
+ }}
326
+ >
327
+ <span className="agent-client-floating-instance-menu-label">
328
+ {label}
329
+ </span>
330
+ {instanceLabels.length > 1 && (
331
+ <button
332
+ className="agent-client-floating-instance-menu-close"
333
+ onClick={(e) => {
334
+ e.stopPropagation();
335
+ plugin.closeFloatingChat(id);
336
+ if (instanceLabels.length <= 2) {
337
+ setShowInstanceMenu(false);
338
+ }
339
+ }}
340
+ title="Close session"
341
+ >
342
+ ×
343
+ </button>
344
+ )}
345
+ </div>
346
+ ))}
347
+ </div>
348
+ )}
349
+ </>
350
+ );
351
+ }