@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,531 @@
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 type AgentClientPlugin from "../plugin";
6
+ import type {
7
+ IChatViewContainer,
8
+ ChatViewType,
9
+ } from "../services/view-registry";
10
+ import type { ChatInputState } from "../types/chat";
11
+
12
+ // Context imports
13
+ import { ChatContextProvider } from "./ChatContext";
14
+
15
+ // Component imports
16
+ import { ChatPanel, type ChatPanelCallbacks } from "./ChatPanel";
17
+
18
+ // Service imports
19
+ import { VaultService } from "../services/vault-service";
20
+
21
+ // Hooks imports
22
+ import { useSettings } from "../hooks/useSettings";
23
+
24
+ // ============================================================
25
+ // Helpers
26
+ // ============================================================
27
+
28
+ function clampSize(
29
+ width: number,
30
+ height: number,
31
+ ): { width: number; height: number } {
32
+ return {
33
+ width: Math.min(width, window.innerWidth),
34
+ height: Math.min(height, window.innerHeight),
35
+ };
36
+ }
37
+
38
+ function clampPosition(
39
+ x: number,
40
+ y: number,
41
+ width: number,
42
+ height: number,
43
+ ): { x: number; y: number } {
44
+ return {
45
+ x: Math.max(0, Math.min(x, window.innerWidth - width)),
46
+ y: Math.max(0, Math.min(y, window.innerHeight - height)),
47
+ };
48
+ }
49
+
50
+ function fitToViewport(
51
+ x: number,
52
+ y: number,
53
+ width: number,
54
+ height: number,
55
+ ): { position: { x: number; y: number }; size: { width: number; height: number } } {
56
+ const size = clampSize(width, height);
57
+ const position = clampPosition(x, y, size.width, size.height);
58
+ return { position, size };
59
+ }
60
+
61
+ // ============================================================
62
+ // FloatingViewContainer Class
63
+ // ============================================================
64
+
65
+ /**
66
+ * Wrapper class that implements IChatViewContainer for floating chat views.
67
+ * Manages the React component lifecycle and provides the interface for
68
+ * unified view management via ChatViewRegistry.
69
+ */
70
+ export class FloatingViewContainer implements IChatViewContainer {
71
+ readonly viewType: ChatViewType = "floating";
72
+ readonly viewId: string;
73
+
74
+ private plugin: AgentClientPlugin;
75
+ private root: Root | null = null;
76
+ private containerEl: HTMLElement;
77
+ private callbacks: ChatPanelCallbacks | null = null;
78
+ private setExpanded: ((expanded: boolean) => void) | null = null;
79
+ private isExpandedState = false;
80
+ private containerRefEl: HTMLElement | null = null;
81
+
82
+ constructor(plugin: AgentClientPlugin, instanceId: string) {
83
+ this.plugin = plugin;
84
+ // viewId format: "floating-chat-{instanceId}" to match adapter key
85
+ this.viewId = `floating-chat-${instanceId}`;
86
+ this.containerEl = activeDocument.body.createDiv({
87
+ cls: "agent-client-floating-view-root",
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Mount the React component and register with the plugin.
93
+ */
94
+ mount(
95
+ initialExpanded: boolean,
96
+ initialPosition?: { x: number; y: number },
97
+ ): void {
98
+ this.root = createRoot(this.containerEl);
99
+ this.root.render(
100
+ <FloatingChatComponent
101
+ plugin={this.plugin}
102
+ viewId={this.viewId}
103
+ initialExpanded={initialExpanded}
104
+ initialPosition={initialPosition}
105
+ onRegisterCallbacks={(cbs) => {
106
+ this.callbacks = cbs;
107
+ }}
108
+ onRegisterExpanded={(fn) => {
109
+ this.setExpanded = fn;
110
+ }}
111
+ onExpandedChange={(expanded) => {
112
+ this.isExpandedState = expanded;
113
+ }}
114
+ onContainerRef={(el) => {
115
+ this.containerRefEl = el;
116
+ }}
117
+ />,
118
+ );
119
+
120
+ // Register with plugin's view registry
121
+ this.plugin.viewRegistry.register(this);
122
+ }
123
+
124
+ /**
125
+ * Unmount the React component and unregister from the plugin.
126
+ */
127
+ unmount(): void {
128
+ this.plugin.viewRegistry.unregister(this.viewId);
129
+
130
+ if (this.root) {
131
+ this.root.unmount();
132
+ this.root = null;
133
+ }
134
+ this.containerEl.remove();
135
+ }
136
+
137
+ // ============================================================
138
+ // IChatViewContainer Implementation
139
+ // ============================================================
140
+
141
+ getDisplayName(): string {
142
+ return this.callbacks?.getDisplayName() ?? "Chat";
143
+ }
144
+
145
+ onActivate(): void {
146
+ this.containerEl.classList.add("is-focused");
147
+ }
148
+
149
+ onDeactivate(): void {
150
+ this.containerEl.classList.remove("is-focused");
151
+ }
152
+
153
+ focus(): void {
154
+ // Expand if collapsed, then focus
155
+ if (!this.isExpandedState) {
156
+ this.isExpandedState = true;
157
+ this.setExpanded?.(true);
158
+ }
159
+ // Focus after next render (expansion may need a frame)
160
+ window.requestAnimationFrame(() => {
161
+ const textarea = this.containerRefEl?.querySelector(
162
+ "textarea.agent-client-chat-input-textarea",
163
+ );
164
+ if (textarea instanceof HTMLTextAreaElement) {
165
+ textarea.focus();
166
+ }
167
+ });
168
+ }
169
+
170
+ hasFocus(): boolean {
171
+ return (
172
+ this.isExpandedState &&
173
+ (this.containerRefEl?.contains(activeDocument.activeElement) ?? false)
174
+ );
175
+ }
176
+
177
+ expand(): void {
178
+ if (!this.isExpandedState) {
179
+ this.isExpandedState = true;
180
+ this.setExpanded?.(true);
181
+ }
182
+ }
183
+
184
+ collapse(): void {
185
+ if (this.isExpandedState) {
186
+ this.isExpandedState = false;
187
+ this.setExpanded?.(false);
188
+ }
189
+ }
190
+
191
+ getInputState(): ChatInputState | null {
192
+ return this.callbacks?.getInputState() ?? null;
193
+ }
194
+
195
+ setInputState(state: ChatInputState): void {
196
+ this.callbacks?.setInputState(state);
197
+ }
198
+
199
+ canSend(): boolean {
200
+ return this.callbacks?.canSend() ?? false;
201
+ }
202
+
203
+ async sendMessage(): Promise<boolean> {
204
+ return (await this.callbacks?.sendMessage()) ?? false;
205
+ }
206
+
207
+ async cancelOperation(): Promise<void> {
208
+ await this.callbacks?.cancelOperation();
209
+ }
210
+
211
+ getContainerEl(): HTMLElement {
212
+ return this.containerEl;
213
+ }
214
+ }
215
+
216
+ // ============================================================
217
+ // FloatingChatComponent
218
+ // ============================================================
219
+
220
+ interface FloatingChatComponentProps {
221
+ plugin: AgentClientPlugin;
222
+ viewId: string;
223
+ initialExpanded?: boolean;
224
+ initialPosition?: { x: number; y: number };
225
+ onRegisterCallbacks?: (callbacks: ChatPanelCallbacks) => void;
226
+ onRegisterExpanded?: (setExpanded: (expanded: boolean) => void) => void;
227
+ onExpandedChange?: (expanded: boolean) => void;
228
+ onContainerRef?: (el: HTMLDivElement | null) => void;
229
+ }
230
+
231
+ function FloatingChatComponent({
232
+ plugin,
233
+ viewId,
234
+ initialExpanded = false,
235
+ initialPosition,
236
+ onRegisterCallbacks,
237
+ onRegisterExpanded,
238
+ onExpandedChange,
239
+ onContainerRef,
240
+ }: FloatingChatComponentProps) {
241
+ // ============================================================
242
+ // Services (owned by FloatingViewContainer, created here for context)
243
+ // ============================================================
244
+ const acpClient = useMemo(
245
+ () => plugin.getOrCreateAcpClient(viewId),
246
+ [plugin, viewId],
247
+ );
248
+
249
+ const vaultService = useMemo(() => new VaultService(plugin), [plugin]);
250
+
251
+ // Cleanup VaultService when component unmounts
252
+ useEffect(() => {
253
+ return () => {
254
+ vaultService.destroy();
255
+ };
256
+ }, [vaultService]);
257
+
258
+ // ============================================================
259
+ // Context Value
260
+ // ============================================================
261
+ const contextValue = useMemo(
262
+ () => ({
263
+ plugin,
264
+ acpClient,
265
+ vaultService,
266
+ settingsService: plugin.settingsService,
267
+ }),
268
+ [plugin, acpClient, vaultService],
269
+ );
270
+
271
+ // ============================================================
272
+ // UI State (View-Specific)
273
+ // ============================================================
274
+ const settings = useSettings(plugin);
275
+ const [isExpanded, setIsExpanded] = useState(initialExpanded);
276
+
277
+ // Register setIsExpanded with the class so it can call expand/collapse directly
278
+ useEffect(() => {
279
+ onRegisterExpanded?.(setIsExpanded);
280
+ }, [onRegisterExpanded]);
281
+
282
+ const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
283
+ const [size, setSize] = useState(settings.floatingWindowSize);
284
+ const [position, setPosition] = useState(() => {
285
+ if (initialPosition) {
286
+ return clampPosition(
287
+ initialPosition.x,
288
+ initialPosition.y,
289
+ settings.floatingWindowSize.width,
290
+ settings.floatingWindowSize.height,
291
+ );
292
+ }
293
+ if (settings.floatingWindowPosition) {
294
+ return clampPosition(
295
+ settings.floatingWindowPosition.x,
296
+ settings.floatingWindowPosition.y,
297
+ settings.floatingWindowSize.width,
298
+ settings.floatingWindowSize.height,
299
+ );
300
+ }
301
+ return clampPosition(
302
+ window.innerWidth - settings.floatingWindowSize.width - 50,
303
+ window.innerHeight - settings.floatingWindowSize.height - 50,
304
+ settings.floatingWindowSize.width,
305
+ settings.floatingWindowSize.height,
306
+ );
307
+ });
308
+ const [isDragging, setIsDragging] = useState(false);
309
+ const dragOffset = useRef({ x: 0, y: 0 });
310
+ const containerRef = useRef<HTMLDivElement>(null);
311
+
312
+ // Expose container element for ChatPanel focus tracking
313
+ useEffect(() => {
314
+ setContainerEl(containerRef.current);
315
+ }, []);
316
+
317
+ // Notify parent of expanded state changes
318
+ useEffect(() => {
319
+ onExpandedChange?.(isExpanded);
320
+ }, [isExpanded, onExpandedChange]);
321
+
322
+ // Keep refs up-to-date for viewport resize handler
323
+ const positionRef = useRef(position);
324
+ const sizeRef = useRef(size);
325
+ useEffect(() => { positionRef.current = position; }, [position]);
326
+ useEffect(() => { sizeRef.current = size; }, [size]);
327
+
328
+ // Fit to viewport on expand, and re-fit whenever the viewport resizes
329
+ useEffect(() => {
330
+ if (!isExpanded) return;
331
+
332
+ const adjust = () => {
333
+ const { position: newPos, size: newSize } = fitToViewport(
334
+ positionRef.current.x,
335
+ positionRef.current.y,
336
+ sizeRef.current.width,
337
+ sizeRef.current.height,
338
+ );
339
+ if (newSize.width !== sizeRef.current.width || newSize.height !== sizeRef.current.height) {
340
+ setSize(newSize);
341
+ }
342
+ if (newPos.x !== positionRef.current.x || newPos.y !== positionRef.current.y) {
343
+ setPosition(newPos);
344
+ }
345
+ };
346
+
347
+ adjust();
348
+ window.addEventListener("resize", adjust);
349
+ return () => window.removeEventListener("resize", adjust);
350
+ }, [isExpanded]);
351
+
352
+ // Notify parent of container ref
353
+ useEffect(() => {
354
+ onContainerRef?.(containerRef.current);
355
+ }, [onContainerRef, isExpanded]); // re-notify when expanded changes (containerRef may change)
356
+
357
+ // Handlers for window management
358
+ const handleOpenNewFloatingChat = useCallback(() => {
359
+ // Open new window with 30px offset from current position, clamped to viewport
360
+ plugin.openNewFloatingChat(
361
+ true,
362
+ clampPosition(
363
+ position.x - 30,
364
+ position.y - 30,
365
+ size.width,
366
+ size.height,
367
+ ),
368
+ );
369
+ }, [plugin, position, size.width, size.height]);
370
+
371
+ const handleMinimizeWindow = useCallback(() => {
372
+ setIsExpanded(false);
373
+ }, []);
374
+
375
+ const handleCloseWindow = useCallback(() => {
376
+ plugin.closeFloatingChat(viewId);
377
+ }, [plugin, viewId]);
378
+
379
+ // Sync manual resizing with state
380
+ useEffect(() => {
381
+ if (!isExpanded || !containerRef.current) return;
382
+
383
+ const observer = new ResizeObserver((entries) => {
384
+ for (const entry of entries) {
385
+ const { width, height } = entry.contentRect;
386
+ // Only update if significantly different to avoid loops
387
+ if (
388
+ Math.abs(width - size.width) > 5 ||
389
+ Math.abs(height - size.height) > 5
390
+ ) {
391
+ setSize({ width, height });
392
+ }
393
+ }
394
+ });
395
+
396
+ observer.observe(containerRef.current);
397
+ return () => observer.disconnect();
398
+ }, [isExpanded, size.width, size.height]);
399
+
400
+ // Save size to settings
401
+ useEffect(() => {
402
+ const saveSize = async () => {
403
+ if (
404
+ size.width !== settings.floatingWindowSize.width ||
405
+ size.height !== settings.floatingWindowSize.height
406
+ ) {
407
+ await plugin.saveSettingsAndNotify({
408
+ ...plugin.settings,
409
+ floatingWindowSize: size,
410
+ });
411
+ }
412
+ };
413
+
414
+ const timer = window.setTimeout(() => {
415
+ void saveSize();
416
+ }, 500);
417
+ return () => window.clearTimeout(timer);
418
+ }, [size, plugin, settings.floatingWindowSize]);
419
+
420
+ // Save position to settings
421
+ useEffect(() => {
422
+ const savePosition = async () => {
423
+ if (
424
+ !settings.floatingWindowPosition ||
425
+ position.x !== settings.floatingWindowPosition.x ||
426
+ position.y !== settings.floatingWindowPosition.y
427
+ ) {
428
+ await plugin.saveSettingsAndNotify({
429
+ ...plugin.settings,
430
+ floatingWindowPosition: position,
431
+ });
432
+ }
433
+ };
434
+
435
+ const timer = window.setTimeout(() => {
436
+ void savePosition();
437
+ }, 500);
438
+ return () => window.clearTimeout(timer);
439
+ }, [position, plugin, settings.floatingWindowPosition]);
440
+
441
+ // ============================================================
442
+ // Dragging Logic (View-Specific)
443
+ // ============================================================
444
+ const onMouseDown = useCallback(
445
+ (e: React.MouseEvent) => {
446
+ if (!containerRef.current) return;
447
+ setIsDragging(true);
448
+ dragOffset.current = {
449
+ x: e.clientX - position.x,
450
+ y: e.clientY - position.y,
451
+ };
452
+ },
453
+ [position],
454
+ );
455
+
456
+ useEffect(() => {
457
+ const onMouseMove = (e: MouseEvent) => {
458
+ if (!isDragging) return;
459
+ setPosition(
460
+ clampPosition(
461
+ e.clientX - dragOffset.current.x,
462
+ e.clientY - dragOffset.current.y,
463
+ size.width,
464
+ size.height,
465
+ ),
466
+ );
467
+ };
468
+
469
+ const onMouseUp = () => {
470
+ setIsDragging(false);
471
+ };
472
+
473
+ if (isDragging) {
474
+ window.addEventListener("mousemove", onMouseMove);
475
+ window.addEventListener("mouseup", onMouseUp);
476
+ }
477
+
478
+ return () => {
479
+ window.removeEventListener("mousemove", onMouseMove);
480
+ window.removeEventListener("mouseup", onMouseUp);
481
+ };
482
+ }, [isDragging, size.width, size.height]);
483
+
484
+ // ============================================================
485
+ // Render
486
+ // ============================================================
487
+ return (
488
+ <div
489
+ ref={containerRef}
490
+ className="agent-client-floating-window"
491
+ style={{
492
+ left: position.x,
493
+ top: position.y,
494
+ width: size.width,
495
+ height: size.height,
496
+ display: isExpanded ? undefined : "none",
497
+ }}
498
+ >
499
+ <ChatContextProvider value={contextValue}>
500
+ <ChatPanel
501
+ variant="floating"
502
+ viewId={viewId}
503
+ onRegisterCallbacks={onRegisterCallbacks}
504
+ onMinimize={handleMinimizeWindow}
505
+ onClose={handleCloseWindow}
506
+ onOpenNewWindow={handleOpenNewFloatingChat}
507
+ onFloatingHeaderMouseDown={onMouseDown}
508
+ containerEl={containerEl}
509
+ />
510
+ </ChatContextProvider>
511
+ </div>
512
+ );
513
+ }
514
+
515
+ /**
516
+ * Create a new floating chat view.
517
+ * @param plugin - The plugin instance
518
+ * @param instanceId - The instance ID (e.g., "0", "1", "2")
519
+ * @param initialExpanded - Whether to start expanded
520
+ * @returns The FloatingViewContainer instance
521
+ */
522
+ export function createFloatingChat(
523
+ plugin: AgentClientPlugin,
524
+ instanceId: string,
525
+ initialExpanded = false,
526
+ initialPosition?: { x: number; y: number },
527
+ ): FloatingViewContainer {
528
+ const container = new FloatingViewContainer(plugin, instanceId);
529
+ container.mount(initialExpanded, initialPosition);
530
+ return container;
531
+ }