@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,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
|
+
}
|