@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,1162 @@
1
+ import * as React from "react";
2
+ const { useState, useRef, useEffect, useMemo, useCallback } = React;
3
+ import {
4
+ Notice,
5
+ FileSystemAdapter,
6
+ Platform,
7
+ Menu,
8
+ setIcon,
9
+ type MenuItem,
10
+ } from "obsidian";
11
+
12
+ import type { AttachedFile, ChatInputState } from "../types/chat";
13
+ import { isSameDirectory } from "../utils/platform";
14
+ import { useHistoryModal } from "../hooks/useHistoryModal";
15
+ import { useChatActions } from "../hooks/useChatActions";
16
+ import { ChangeDirectoryModal } from "./ChangeDirectoryModal";
17
+
18
+ // Service imports
19
+ import { getLogger } from "../utils/logger";
20
+
21
+ // Adapter imports
22
+ import type { AcpClient } from "../acp/acp-client";
23
+
24
+ // Context imports
25
+ import { useChatContext } from "./ChatContext";
26
+
27
+ // Hooks imports
28
+ import { useSettings } from "../hooks/useSettings";
29
+ import { useSuggestions } from "../hooks/useSuggestions";
30
+ import { useAgent } from "../hooks/useAgent";
31
+ import { useSessionHistory } from "../hooks/useSessionHistory";
32
+
33
+ // Domain model imports
34
+ import {
35
+ flattenConfigSelectOptions,
36
+ type SlashCommand,
37
+ type SessionModeState,
38
+ type SessionModelState,
39
+ type SessionConfigOption,
40
+ } from "../types/session";
41
+ import { checkAgentUpdate } from "../services/update-checker";
42
+ import { buildGeminiDeprecationNotice } from "../services/session-helpers";
43
+
44
+ /** Stable empty array for useSuggestions when no commands available */
45
+ const EMPTY_COMMANDS: SlashCommand[] = [];
46
+
47
+ // Component imports
48
+ import { ChatHeader } from "./ChatHeader";
49
+ import { MessageList } from "./MessageList";
50
+ import { InputArea } from "./InputArea";
51
+ import type { IChatViewHost } from "./view-host";
52
+
53
+ // ============================================================================
54
+ // ChatPanelCallbacks - interface for class-level delegation
55
+ // ============================================================================
56
+
57
+ /**
58
+ * Callbacks that ChatPanel registers with its parent container class.
59
+ * Used by ChatView / FloatingViewContainer to implement IChatViewContainer
60
+ * by delegating to the React component's state and handlers.
61
+ */
62
+ export interface ChatPanelCallbacks {
63
+ getDisplayName: () => string;
64
+ getInputState: () => ChatInputState | null;
65
+ setInputState: (state: ChatInputState) => void;
66
+ canSend: () => boolean;
67
+ sendMessage: () => Promise<boolean>;
68
+ cancelOperation: () => Promise<void>;
69
+ }
70
+
71
+ // ============================================================================
72
+ // ChatPanelProps
73
+ // ============================================================================
74
+
75
+ export interface ChatPanelProps {
76
+ variant: "sidebar" | "floating";
77
+ viewId: string;
78
+ workingDirectory?: string;
79
+ initialAgentId?: string;
80
+ config?: { agent?: string; model?: string };
81
+ onRegisterCallbacks?: (callbacks: ChatPanelCallbacks) => void;
82
+ /** Called when agent ID changes (sidebar only — persists in Obsidian state) */
83
+ onAgentIdChanged?: (agentId: string) => void;
84
+ // Floating-specific
85
+ onMinimize?: () => void;
86
+ onClose?: () => void;
87
+ onOpenNewWindow?: () => void;
88
+ /** Mouse down handler for floating header drag area */
89
+ onFloatingHeaderMouseDown?: (e: React.MouseEvent) => void;
90
+ // Sidebar-specific: Obsidian view host for DOM event registration
91
+ viewHost?: IChatViewHost;
92
+ /** External container element for focus tracking (floating uses parent's container) */
93
+ containerEl?: HTMLElement | null;
94
+ }
95
+
96
+ // ============================================================================
97
+ // State Definitions
98
+ // ============================================================================
99
+
100
+ // Type definitions for Obsidian internal APIs (sidebar menu)
101
+ interface AppWithSettings {
102
+ setting: {
103
+ open: () => void;
104
+ openTabById: (id: string) => void;
105
+ };
106
+ }
107
+
108
+ // ============================================================================
109
+ // ChatPanel Component
110
+ // ============================================================================
111
+
112
+ /**
113
+ * Core chat panel component that encapsulates all chat logic.
114
+ *
115
+ * This is the single source of truth for chat state and behavior,
116
+ * shared between sidebar (ChatView) and floating (FloatingChatView) variants.
117
+ * It is a 1:1 migration of useChatController into a React component,
118
+ * with workspace event handlers moved from ChatComponent/FloatingChatComponent.
119
+ */
120
+ export function ChatPanel({
121
+ variant,
122
+ viewId,
123
+ workingDirectory,
124
+ initialAgentId,
125
+ config,
126
+ onRegisterCallbacks,
127
+ onAgentIdChanged,
128
+ onMinimize,
129
+ onClose,
130
+ onOpenNewWindow,
131
+ onFloatingHeaderMouseDown,
132
+ viewHost: viewHostProp,
133
+ containerEl: containerElProp,
134
+ }: ChatPanelProps) {
135
+ // ============================================================
136
+ // Platform Check
137
+ // ============================================================
138
+ if (!Platform.isDesktopApp) {
139
+ throw new Error("Agent Client is only available on desktop");
140
+ }
141
+
142
+ // ============================================================
143
+ // Context
144
+ // ============================================================
145
+ const { plugin, acpClient, vaultService } = useChatContext();
146
+
147
+ // ============================================================
148
+ // Memoized Services & Adapters
149
+ // ============================================================
150
+ const logger = getLogger();
151
+
152
+ const vaultPath = useMemo(() => {
153
+ if (workingDirectory) {
154
+ return workingDirectory;
155
+ }
156
+ const adapter = plugin.app.vault.adapter;
157
+ if (adapter instanceof FileSystemAdapter) {
158
+ return adapter.getBasePath();
159
+ }
160
+ // Fallback for non-FileSystemAdapter (e.g., mobile)
161
+ return process.cwd();
162
+ }, [plugin, workingDirectory]);
163
+
164
+ // Agent working directory — defaults to vault path.
165
+ // Can be changed independently via "New chat in directory..." action.
166
+ const [agentCwd, setAgentCwd] = useState(vaultPath);
167
+
168
+ // ============================================================
169
+ // Custom Hooks
170
+ // ============================================================
171
+ const settings = useSettings(plugin);
172
+
173
+ const agent = useAgent(
174
+ acpClient,
175
+ plugin.settingsService,
176
+ vaultService,
177
+ agentCwd,
178
+ initialAgentId,
179
+ );
180
+
181
+ const {
182
+ session,
183
+ isReady: isSessionReady,
184
+ messages,
185
+ isSending,
186
+ errorInfo,
187
+ } = agent;
188
+
189
+ const suggestions = useSuggestions(
190
+ vaultService,
191
+ plugin,
192
+ session.availableCommands || EMPTY_COMMANDS,
193
+ );
194
+
195
+ // Session history hook with callback for session load
196
+ const handleSessionLoad = useCallback(
197
+ (
198
+ sessionId: string,
199
+ modes?: SessionModeState,
200
+ models?: SessionModelState,
201
+ configOptions?: SessionConfigOption[],
202
+ ) => {
203
+ logger.log(
204
+ `[ChatPanel] Session loaded/resumed/forked: ${sessionId}`,
205
+ {
206
+ modes,
207
+ models,
208
+ configOptions,
209
+ },
210
+ );
211
+ void agent.updateSessionFromLoad(
212
+ sessionId,
213
+ modes,
214
+ models,
215
+ configOptions,
216
+ );
217
+ },
218
+ [logger, agent.updateSessionFromLoad],
219
+ );
220
+
221
+ const sessionHistory = useSessionHistory({
222
+ agentClient: acpClient,
223
+ session,
224
+ settingsAccess: plugin.settingsService,
225
+ cwd: vaultPath,
226
+ agentCwd,
227
+ onSessionLoad: handleSessionLoad,
228
+ onMessagesRestore: agent.setMessagesFromLocal,
229
+ onIgnoreUpdates: agent.setIgnoreUpdates,
230
+ onClearMessages: agent.clearMessages,
231
+ });
232
+
233
+ // ============================================================
234
+ // Local State
235
+ // ============================================================
236
+ const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
237
+
238
+ // Input state (for broadcast commands)
239
+ const [inputValue, setInputValue] = useState("");
240
+ const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
241
+
242
+ // ============================================================
243
+ // Refs
244
+ // ============================================================
245
+ const terminalClientRef = useRef<AcpClient>(acpClient);
246
+
247
+ // ============================================================
248
+ // Computed Values
249
+ // ============================================================
250
+ const activeAgentLabel = useMemo(() => {
251
+ const activeId = session.agentId;
252
+ if (activeId === plugin.settings.claude.id) {
253
+ return (
254
+ plugin.settings.claude.displayName || plugin.settings.claude.id
255
+ );
256
+ }
257
+ if (activeId === plugin.settings.codex.id) {
258
+ return (
259
+ plugin.settings.codex.displayName || plugin.settings.codex.id
260
+ );
261
+ }
262
+ if (activeId === plugin.settings.gemini.id) {
263
+ return (
264
+ plugin.settings.gemini.displayName || plugin.settings.gemini.id
265
+ );
266
+ }
267
+ const custom = plugin.settings.customAgents.find(
268
+ (agent) => agent.id === activeId,
269
+ );
270
+ return custom?.displayName || custom?.id || activeId;
271
+ }, [session.agentId, plugin.settings]);
272
+
273
+ const availableAgents = useMemo(() => {
274
+ return plugin.getAvailableAgents();
275
+ }, [plugin]);
276
+
277
+ // ============================================================
278
+ // Chat Actions
279
+ // ============================================================
280
+ const actions = useChatActions(
281
+ plugin,
282
+ agent,
283
+ sessionHistory,
284
+ suggestions,
285
+ session,
286
+ messages,
287
+ settings,
288
+ vaultPath,
289
+ );
290
+
291
+ const {
292
+ handleSendMessage,
293
+ handleStopGeneration,
294
+ handleNewChat,
295
+ handleExportChat,
296
+ handleSwitchAgent,
297
+ handleRestartAgent,
298
+ handleSetMode,
299
+ handleSetModel,
300
+ handleSetConfigOption,
301
+ handleClearError,
302
+ handleClearAgentUpdate,
303
+ handleRestoredMessageConsumed,
304
+ restoredMessage,
305
+ agentUpdateNotification,
306
+ setAgentUpdateNotification,
307
+ autoExportIfEnabled,
308
+ } = actions;
309
+
310
+ // ============================================================
311
+ // Gemini CLI deprecation notice (static, agent-id driven)
312
+ // ============================================================
313
+ // Independent channel from the npm-backed agentUpdateNotification:
314
+ // derived synchronously from the active agent id (no network).
315
+ const geminiNotice = useMemo(
316
+ () =>
317
+ session.agentId === plugin.settings.gemini.id
318
+ ? buildGeminiDeprecationNotice()
319
+ : null,
320
+ [session.agentId, plugin.settings.gemini.id],
321
+ );
322
+
323
+ // Dismiss state lives locally in ChatPanel so it never races with the
324
+ // async setAgentUpdateNotification owned by useChatActions.
325
+ const [geminiNoticeDismissed, setGeminiNoticeDismissed] = useState(false);
326
+
327
+ // Re-show the notice when switching agents (e.g. away and back to Gemini).
328
+ useEffect(() => {
329
+ setGeminiNoticeDismissed(false);
330
+ }, [session.agentId]);
331
+
332
+ const effectiveGeminiNotice =
333
+ geminiNotice && !geminiNoticeDismissed ? geminiNotice : null;
334
+
335
+ const handleClearGeminiNotice = useCallback(
336
+ () => setGeminiNoticeDismissed(true),
337
+ [],
338
+ );
339
+
340
+ // Wrap send so the Gemini notice also dismisses on send, mirroring how
341
+ // useChatActions clears agentUpdateNotification inside handleSendMessage.
342
+ const handleSendMessageWithGeminiDismiss = useCallback(
343
+ (content: string, attachments?: AttachedFile[]) => {
344
+ setGeminiNoticeDismissed(true);
345
+ return handleSendMessage(content, attachments);
346
+ },
347
+ [handleSendMessage],
348
+ );
349
+
350
+ const { handleOpenHistory } = useHistoryModal(
351
+ plugin,
352
+ agent,
353
+ sessionHistory,
354
+ vaultPath,
355
+ isSessionReady,
356
+ settings.debugMode,
357
+ setAgentCwd,
358
+ );
359
+
360
+ // ============================================================
361
+ // Sidebar-specific: handleNewChat wrapper that persists agent ID
362
+ // ============================================================
363
+ const handleNewChatWithPersist = useCallback(
364
+ async (requestedAgentId?: string) => {
365
+ await handleNewChat(requestedAgentId);
366
+ // Persist agent ID for this view (survives Obsidian restart)
367
+ if (requestedAgentId) {
368
+ onAgentIdChanged?.(requestedAgentId);
369
+ }
370
+ },
371
+ [handleNewChat, onAgentIdChanged],
372
+ );
373
+
374
+ // ============================================================
375
+ // Sidebar-specific: Header Menu (Obsidian native Menu API)
376
+ // ============================================================
377
+ const handleOpenSettings = useCallback(() => {
378
+ const appWithSettings = plugin.app as unknown as AppWithSettings;
379
+ appWithSettings.setting.open();
380
+ appWithSettings.setting.openTabById(plugin.manifest.id);
381
+ }, [plugin]);
382
+
383
+ const handleNewChatInDirectory = useCallback(
384
+ async (directory: string) => {
385
+ // Auto-export current chat before switching
386
+ if (messages.length > 0) {
387
+ await autoExportIfEnabled("newChat", messages, session);
388
+ }
389
+ agent.clearMessages();
390
+ setAgentCwd(directory);
391
+ await agent.restartSession(undefined, directory);
392
+ sessionHistory.invalidateCache();
393
+ },
394
+ [
395
+ messages,
396
+ session,
397
+ autoExportIfEnabled,
398
+ agent.clearMessages,
399
+ agent.restartSession,
400
+ sessionHistory.invalidateCache,
401
+ ],
402
+ );
403
+
404
+ const handleShowSidebarMenu = useCallback(
405
+ (e: React.MouseEvent<HTMLDivElement>) => {
406
+ const menu = new Menu();
407
+
408
+ // -- Switch agent section --
409
+ menu.addItem((item: MenuItem) => {
410
+ item.setTitle("Switch agent").setIsLabel(true);
411
+ });
412
+
413
+ for (const agent of availableAgents) {
414
+ menu.addItem((item: MenuItem) => {
415
+ item.setTitle(agent.displayName)
416
+ .setChecked(agent.id === (session.agentId || ""))
417
+ .onClick(() => {
418
+ void handleNewChatWithPersist(agent.id);
419
+ });
420
+ });
421
+ }
422
+
423
+ menu.addSeparator();
424
+
425
+ // -- Actions section --
426
+ menu.addItem((item: MenuItem) => {
427
+ item.setTitle("Open new view")
428
+ .setIcon("copy-plus")
429
+ .onClick(() => {
430
+ void plugin.openNewChatViewWithAgent(
431
+ plugin.settings.defaultAgentId,
432
+ );
433
+ });
434
+ });
435
+
436
+ menu.addItem((item: MenuItem) => {
437
+ item.setTitle("Restart agent")
438
+ .setIcon("refresh-cw")
439
+ .onClick(() => {
440
+ void handleRestartAgent();
441
+ });
442
+ });
443
+
444
+ menu.addItem((item: MenuItem) => {
445
+ item.setTitle("New chat in directory...")
446
+ .setIcon("folder-open")
447
+ .onClick(() => {
448
+ const modal = new ChangeDirectoryModal(
449
+ plugin.app,
450
+ agentCwd,
451
+ (directory) => {
452
+ void handleNewChatInDirectory(directory);
453
+ },
454
+ );
455
+ modal.open();
456
+ });
457
+ });
458
+
459
+ menu.addSeparator();
460
+
461
+ menu.addItem((item: MenuItem) => {
462
+ item.setTitle("Plugin settings")
463
+ .setIcon("settings")
464
+ .onClick(() => {
465
+ handleOpenSettings();
466
+ });
467
+ });
468
+
469
+ menu.showAtMouseEvent(e.nativeEvent);
470
+ },
471
+ [
472
+ availableAgents,
473
+ session.agentId,
474
+ handleNewChatWithPersist,
475
+ plugin,
476
+ handleRestartAgent,
477
+ agentCwd,
478
+ handleNewChatInDirectory,
479
+ handleOpenSettings,
480
+ ],
481
+ );
482
+
483
+ const handleShowFloatingMenu = useCallback(
484
+ (e: React.MouseEvent<HTMLElement>) => {
485
+ const menu = new Menu();
486
+
487
+ menu.addItem((item: MenuItem) => {
488
+ item.setTitle("New chat")
489
+ .setIcon("plus")
490
+ .onClick(() => {
491
+ void handleNewChat();
492
+ });
493
+ });
494
+
495
+ menu.addItem((item: MenuItem) => {
496
+ item.setTitle("Session history")
497
+ .setIcon("history")
498
+ .onClick(() => {
499
+ void handleOpenHistory();
500
+ });
501
+ });
502
+
503
+ menu.addItem((item: MenuItem) => {
504
+ item.setTitle("Export chat to Markdown")
505
+ .setIcon("save")
506
+ .onClick(() => {
507
+ void handleExportChat();
508
+ });
509
+ });
510
+
511
+ menu.addSeparator();
512
+
513
+ if (onOpenNewWindow) {
514
+ menu.addItem((item: MenuItem) => {
515
+ item.setTitle("Open new floating chat")
516
+ .setIcon("copy-plus")
517
+ .onClick(() => {
518
+ onOpenNewWindow();
519
+ });
520
+ });
521
+ }
522
+
523
+ menu.addItem((item: MenuItem) => {
524
+ item.setTitle("Restart agent")
525
+ .setIcon("refresh-cw")
526
+ .onClick(() => {
527
+ void handleRestartAgent();
528
+ });
529
+ });
530
+
531
+ menu.addItem((item: MenuItem) => {
532
+ item.setTitle("New chat in directory...")
533
+ .setIcon("folder-open")
534
+ .onClick(() => {
535
+ const modal = new ChangeDirectoryModal(
536
+ plugin.app,
537
+ agentCwd,
538
+ (directory) => {
539
+ void handleNewChatInDirectory(directory);
540
+ },
541
+ );
542
+ modal.open();
543
+ });
544
+ });
545
+
546
+ menu.addSeparator();
547
+
548
+ menu.addItem((item: MenuItem) => {
549
+ item.setTitle("Plugin settings")
550
+ .setIcon("settings")
551
+ .onClick(() => {
552
+ handleOpenSettings();
553
+ });
554
+ });
555
+
556
+ menu.showAtMouseEvent(e.nativeEvent);
557
+ },
558
+ [
559
+ handleNewChat,
560
+ handleOpenHistory,
561
+ handleExportChat,
562
+ onOpenNewWindow,
563
+ handleRestartAgent,
564
+ agentCwd,
565
+ handleNewChatInDirectory,
566
+ handleOpenSettings,
567
+ ],
568
+ );
569
+
570
+ // ============================================================
571
+ // viewHost creation for child components
572
+ // ============================================================
573
+ // Track registered listeners for cleanup (floating variant)
574
+ const registeredListenersRef = useRef<
575
+ {
576
+ target: Window | Document | HTMLElement;
577
+ type: string;
578
+ callback: EventListenerOrEventListenerObject;
579
+ }[]
580
+ >([]);
581
+
582
+ const viewHost: IChatViewHost = useMemo(() => {
583
+ // Sidebar: use the provided viewHost from the ChatView class
584
+ if (viewHostProp) {
585
+ return viewHostProp;
586
+ }
587
+ // Floating: create a shim with listener tracking
588
+ return {
589
+ app: plugin.app,
590
+ registerDomEvent: ((
591
+ target: Window | Document | HTMLElement,
592
+ type: string,
593
+ callback: EventListenerOrEventListenerObject,
594
+ ) => {
595
+ target.addEventListener(type, callback);
596
+ registeredListenersRef.current.push({ target, type, callback });
597
+ }),
598
+ };
599
+ }, [viewHostProp, plugin.app]);
600
+
601
+ // Cleanup registered listeners on unmount (floating variant)
602
+ useEffect(() => {
603
+ return () => {
604
+ for (const {
605
+ target,
606
+ type,
607
+ callback,
608
+ } of registeredListenersRef.current) {
609
+ target.removeEventListener(type, callback);
610
+ }
611
+ registeredListenersRef.current = [];
612
+ };
613
+ }, []);
614
+
615
+ // ============================================================
616
+ // Effects - Session Lifecycle
617
+ // ============================================================
618
+ // Initialize session on mount
619
+ useEffect(() => {
620
+ logger.log("[Debug] Starting connection setup via useSession...");
621
+ void agent.createSession(config?.agent || initialAgentId);
622
+ }, [agent.createSession, config?.agent, initialAgentId]);
623
+
624
+ // Apply configured model when session is ready
625
+ useEffect(() => {
626
+ if (!config?.model || !isSessionReady) return;
627
+
628
+ // Prefer configOptions if available
629
+ if (session.configOptions) {
630
+ const modelOption = session.configOptions.find(
631
+ (o) => o.category === "model",
632
+ );
633
+ if (modelOption && modelOption.currentValue !== config.model) {
634
+ const valueExists = flattenConfigSelectOptions(
635
+ modelOption.options,
636
+ ).some((o) => o.value === config.model);
637
+ if (valueExists) {
638
+ logger.log(
639
+ "[ChatPanel] Applying configured model via configOptions:",
640
+ config.model,
641
+ );
642
+ void agent.setConfigOption(modelOption.id, config.model);
643
+ }
644
+ }
645
+ return;
646
+ }
647
+
648
+ // Fallback to legacy models
649
+ if (session.models) {
650
+ const modelExists = session.models.availableModels.some(
651
+ (m) => m.modelId === config.model,
652
+ );
653
+ if (modelExists && session.models.currentModelId !== config.model) {
654
+ logger.log(
655
+ "[ChatPanel] Applying configured model:",
656
+ config.model,
657
+ );
658
+ void agent.setModel(config.model);
659
+ }
660
+ }
661
+ }, [
662
+ config?.model,
663
+ isSessionReady,
664
+ session.configOptions,
665
+ session.models,
666
+ agent.setConfigOption,
667
+ agent.setModel,
668
+ logger,
669
+ ]);
670
+
671
+ // Refs for cleanup (to access latest values in cleanup function)
672
+ const messagesRef = useRef(messages);
673
+ const sessionRef = useRef(session);
674
+ const autoExportRef = useRef(autoExportIfEnabled);
675
+ const closeSessionRef = useRef(agent.closeSession);
676
+ messagesRef.current = messages;
677
+ sessionRef.current = session;
678
+ autoExportRef.current = autoExportIfEnabled;
679
+ closeSessionRef.current = agent.closeSession;
680
+
681
+ // Cleanup on unmount only - auto-export and close session
682
+ useEffect(() => {
683
+ return () => {
684
+ logger.log("[ChatPanel] Cleanup: auto-export and close session");
685
+ void (async () => {
686
+ await autoExportRef.current(
687
+ "closeChat",
688
+ messagesRef.current,
689
+ sessionRef.current,
690
+ );
691
+ await closeSessionRef.current();
692
+ })();
693
+ };
694
+ }, [logger]);
695
+
696
+ // ============================================================
697
+ // Effects - Update Check
698
+ // ============================================================
699
+ useEffect(() => {
700
+ plugin
701
+ .checkForUpdates()
702
+ .then(setIsUpdateAvailable)
703
+ .catch((error) => {
704
+ logger.error("Failed to check for updates:", error);
705
+ });
706
+ }, [plugin, logger]);
707
+
708
+ // ============================================================
709
+ // Effects - Agent Update Check
710
+ // ============================================================
711
+ useEffect(() => {
712
+ if (!isSessionReady || !session.agentInfo?.name) {
713
+ return;
714
+ }
715
+
716
+ checkAgentUpdate(
717
+ session.agentInfo as { name: string; version?: string },
718
+ )
719
+ .then(setAgentUpdateNotification)
720
+ .catch((error) => {
721
+ logger.error("Failed to check agent update:", error);
722
+ });
723
+ }, [isSessionReady, session.agentInfo, logger]);
724
+
725
+ // ============================================================
726
+ // Effects - Save Session Messages on Turn End
727
+ // ============================================================
728
+ const prevIsSendingRef = useRef<boolean>(false);
729
+
730
+ useEffect(() => {
731
+ const wasSending = prevIsSendingRef.current;
732
+ prevIsSendingRef.current = isSending;
733
+
734
+ // Save when turn ends (isSending: true -> false) and has messages
735
+ if (
736
+ wasSending &&
737
+ !isSending &&
738
+ session.sessionId &&
739
+ messages.length > 0
740
+ ) {
741
+ sessionHistory.saveSessionMessages(session.sessionId, messages);
742
+ logger.log(
743
+ `[ChatPanel] Session messages saved: ${session.sessionId}`,
744
+ );
745
+
746
+ // System notification on response completion
747
+ if (settings.enableSystemNotifications && !activeDocument.hasFocus()) {
748
+ new Notification("Agent Client", {
749
+ body: `${activeAgentLabel} has completed the response.`,
750
+ });
751
+ }
752
+ }
753
+ }, [
754
+ isSending,
755
+ session.sessionId,
756
+ messages,
757
+ sessionHistory.saveSessionMessages,
758
+ settings.enableSystemNotifications,
759
+ activeAgentLabel,
760
+ logger,
761
+ ]);
762
+
763
+ // ============================================================
764
+ // Effects - System Notification on Permission Request
765
+ // ============================================================
766
+ const prevHasActivePermissionRef = useRef<boolean>(false);
767
+
768
+ useEffect(() => {
769
+ const wasActive = prevHasActivePermissionRef.current;
770
+ prevHasActivePermissionRef.current = agent.hasActivePermission;
771
+
772
+ // Notify when permission transitions from inactive to active
773
+ if (
774
+ !wasActive &&
775
+ agent.hasActivePermission &&
776
+ settings.enableSystemNotifications &&
777
+ !activeDocument.hasFocus()
778
+ ) {
779
+ new Notification("Agent Client", {
780
+ body: `${activeAgentLabel} is requesting permission.`,
781
+ });
782
+ }
783
+ }, [
784
+ agent.hasActivePermission,
785
+ settings.enableSystemNotifications,
786
+ activeAgentLabel,
787
+ ]);
788
+
789
+ // ============================================================
790
+ // Effects - Auto-mention Active Note Tracking
791
+ // ============================================================
792
+ useEffect(() => {
793
+ let isMounted = true;
794
+
795
+ const refreshActiveNote = async () => {
796
+ if (!isMounted) return;
797
+ await suggestions.mentions.updateActiveNote();
798
+ };
799
+
800
+ const unsubscribe = vaultService.subscribeSelectionChanges(() => {
801
+ void refreshActiveNote();
802
+ });
803
+
804
+ void refreshActiveNote();
805
+
806
+ return () => {
807
+ isMounted = false;
808
+ unsubscribe();
809
+ };
810
+ }, [suggestions.mentions.updateActiveNote, vaultService]);
811
+
812
+ // ============================================================
813
+ // Effects - Workspace Events (Hotkeys)
814
+ // ============================================================
815
+
816
+ // Refs for workspace event handlers (avoids re-registering on every render)
817
+ const handleNewChatWithPersistRef = useRef(handleNewChatWithPersist);
818
+ const handleNewChatRef = useRef(handleNewChat);
819
+ const approveActivePermissionRef = useRef(agent.approveActivePermission);
820
+ const rejectActivePermissionRef = useRef(agent.rejectActivePermission);
821
+ const handleStopGenerationRef = useRef(handleStopGeneration);
822
+ const handleExportChatRef = useRef(handleExportChat);
823
+ handleNewChatWithPersistRef.current = handleNewChatWithPersist;
824
+ handleNewChatRef.current = handleNewChat;
825
+ approveActivePermissionRef.current = agent.approveActivePermission;
826
+ rejectActivePermissionRef.current = agent.rejectActivePermission;
827
+ handleStopGenerationRef.current = handleStopGeneration;
828
+ handleExportChatRef.current = handleExportChat;
829
+
830
+ useEffect(() => {
831
+ const workspace = plugin.app.workspace;
832
+ const ws = workspace as unknown as {
833
+ on: (
834
+ name: string,
835
+ callback: (...args: never[]) => void,
836
+ ) => ReturnType<typeof workspace.on>;
837
+ };
838
+
839
+ const refs = [
840
+ // Toggle auto-mention
841
+ ws.on(
842
+ "agent-client:toggle-auto-mention",
843
+ (targetViewId?: string) => {
844
+ if (targetViewId && targetViewId !== viewId) return;
845
+ suggestions.mentions.toggleAutoMention();
846
+ },
847
+ ),
848
+
849
+ // New chat requested (from "New chat" or "Switch agent to" commands)
850
+ ws.on(
851
+ "agent-client:new-chat-requested",
852
+ (targetViewId?: string, agentId?: string) => {
853
+ if (targetViewId && targetViewId !== viewId) return;
854
+ if (variant === "sidebar") {
855
+ void handleNewChatWithPersistRef.current(agentId);
856
+ } else {
857
+ void handleNewChatRef.current(agentId);
858
+ }
859
+ },
860
+ ),
861
+
862
+ // Approve active permission
863
+ ws.on(
864
+ "agent-client:approve-active-permission",
865
+ (targetViewId?: string) => {
866
+ if (targetViewId && targetViewId !== viewId) return;
867
+ void (async () => {
868
+ const success =
869
+ await approveActivePermissionRef.current();
870
+ if (!success) {
871
+ new Notice(
872
+ "[Agent Client] No active permission request",
873
+ );
874
+ }
875
+ })();
876
+ },
877
+ ),
878
+
879
+ // Reject active permission
880
+ ws.on(
881
+ "agent-client:reject-active-permission",
882
+ (targetViewId?: string) => {
883
+ if (targetViewId && targetViewId !== viewId) return;
884
+ void (async () => {
885
+ const success =
886
+ await rejectActivePermissionRef.current();
887
+ if (!success) {
888
+ new Notice(
889
+ "[Agent Client] No active permission request",
890
+ );
891
+ }
892
+ })();
893
+ },
894
+ ),
895
+
896
+ // Cancel current message
897
+ ws.on("agent-client:cancel-message", (targetViewId?: string) => {
898
+ if (targetViewId && targetViewId !== viewId) return;
899
+ void handleStopGenerationRef.current();
900
+ }),
901
+
902
+ // Export chat
903
+ ws.on("agent-client:export-chat", (targetViewId?: string) => {
904
+ if (targetViewId && targetViewId !== viewId) return;
905
+ void handleExportChatRef.current();
906
+ }),
907
+ ];
908
+
909
+ return () => {
910
+ for (const ref of refs) {
911
+ workspace.offref(ref);
912
+ }
913
+ };
914
+ }, [
915
+ plugin.app.workspace,
916
+ plugin.lastActiveChatViewId,
917
+ viewId,
918
+ variant,
919
+ suggestions.mentions.toggleAutoMention,
920
+ ]);
921
+
922
+ // ============================================================
923
+ // Effects - Focus Tracking
924
+ // ============================================================
925
+ const containerRef = useRef<HTMLDivElement>(null);
926
+ useEffect(() => {
927
+ const handleFocus = () => {
928
+ plugin.setLastActiveChatViewId(viewId);
929
+ };
930
+
931
+ const container = containerElProp ?? containerRef.current;
932
+ if (!container) return;
933
+
934
+ container.addEventListener("focus", handleFocus, true);
935
+ container.addEventListener("click", handleFocus);
936
+
937
+ // Set as active on mount (first opened view becomes active)
938
+ plugin.setLastActiveChatViewId(viewId);
939
+
940
+ return () => {
941
+ container.removeEventListener("focus", handleFocus, true);
942
+ container.removeEventListener("click", handleFocus);
943
+ };
944
+ }, [plugin, viewId, containerElProp]);
945
+
946
+ // ============================================================
947
+ // Callback Registration for IChatViewContainer
948
+ // ============================================================
949
+ // Use refs so callbacks always access latest values
950
+ const inputValueRef = useRef(inputValue);
951
+ const attachedFilesRef = useRef(attachedFiles);
952
+ const isSessionReadyRef = useRef(isSessionReady);
953
+ const isSendingRef = useRef(isSending);
954
+ const sessionHistoryLoadingRef = useRef(sessionHistory.loading);
955
+ const handleSendMessageRef = useRef(handleSendMessage);
956
+ inputValueRef.current = inputValue;
957
+ attachedFilesRef.current = attachedFiles;
958
+ isSessionReadyRef.current = isSessionReady;
959
+ isSendingRef.current = isSending;
960
+ sessionHistoryLoadingRef.current = sessionHistory.loading;
961
+ handleSendMessageRef.current = handleSendMessage;
962
+
963
+ useEffect(() => {
964
+ onRegisterCallbacks?.({
965
+ getDisplayName: () => activeAgentLabel,
966
+ getInputState: () => ({
967
+ text: inputValueRef.current,
968
+ files: attachedFilesRef.current,
969
+ }),
970
+ setInputState: (state) => {
971
+ setInputValue(state.text);
972
+ setAttachedFiles(state.files);
973
+ },
974
+ canSend: () => {
975
+ const hasContent =
976
+ inputValueRef.current.trim() !== "" ||
977
+ attachedFilesRef.current.length > 0;
978
+ return (
979
+ hasContent &&
980
+ isSessionReadyRef.current &&
981
+ !sessionHistoryLoadingRef.current &&
982
+ !isSendingRef.current
983
+ );
984
+ },
985
+ sendMessage: async () => {
986
+ const currentInput = inputValueRef.current;
987
+ const currentFiles = attachedFilesRef.current;
988
+ // Allow sending if there's text OR attachments
989
+ if (!currentInput.trim() && currentFiles.length === 0) {
990
+ return false;
991
+ }
992
+ if (
993
+ !isSessionReadyRef.current ||
994
+ sessionHistoryLoadingRef.current
995
+ ) {
996
+ return false;
997
+ }
998
+ if (isSendingRef.current) {
999
+ return false;
1000
+ }
1001
+
1002
+ // Clear input before sending
1003
+ const messageToSend = currentInput.trim();
1004
+ const filesToSend =
1005
+ currentFiles.length > 0 ? [...currentFiles] : undefined;
1006
+ setInputValue("");
1007
+ setAttachedFiles([]);
1008
+
1009
+ await handleSendMessageRef.current(messageToSend, filesToSend);
1010
+ return true;
1011
+ },
1012
+ cancelOperation: async () => {
1013
+ if (isSendingRef.current) {
1014
+ await handleStopGenerationRef.current();
1015
+ }
1016
+ },
1017
+ });
1018
+ }, [onRegisterCallbacks, activeAgentLabel]);
1019
+
1020
+ // ============================================================
1021
+ // Render
1022
+ // ============================================================
1023
+ const chatFontSizeStyle =
1024
+ settings.displaySettings.fontSize !== null
1025
+ ? ({
1026
+ "--ac-chat-font-size": `${settings.displaySettings.fontSize}px`,
1027
+ } as React.CSSProperties)
1028
+ : undefined;
1029
+
1030
+ const headerElement =
1031
+ variant === "sidebar" ? (
1032
+ <ChatHeader
1033
+ variant="sidebar"
1034
+ agentLabel={activeAgentLabel}
1035
+ isUpdateAvailable={isUpdateAvailable}
1036
+ onNewChat={() => void handleNewChatWithPersist()}
1037
+ onExportChat={() => void handleExportChat()}
1038
+ onShowMenu={handleShowSidebarMenu}
1039
+ onOpenHistory={handleOpenHistory}
1040
+ />
1041
+ ) : (
1042
+ <ChatHeader
1043
+ variant="floating"
1044
+ agentLabel={activeAgentLabel}
1045
+ availableAgents={availableAgents}
1046
+ currentAgentId={session.agentId}
1047
+ isUpdateAvailable={isUpdateAvailable}
1048
+ onAgentChange={(agentId) => void handleSwitchAgent(agentId)}
1049
+ onShowMenu={handleShowFloatingMenu}
1050
+ onMinimize={onMinimize}
1051
+ onClose={onClose}
1052
+ />
1053
+ );
1054
+
1055
+ const cwdBanner =
1056
+ agentCwd !== vaultPath && !isSameDirectory(agentCwd, vaultPath) ? (
1057
+ <div className="agent-client-cwd-banner" title={agentCwd}>
1058
+ <span
1059
+ className="agent-client-cwd-banner-icon"
1060
+ ref={(el) => {
1061
+ if (el) setIcon(el, "folder-open");
1062
+ }}
1063
+ />
1064
+ <span className="agent-client-cwd-banner-path">{agentCwd}</span>
1065
+ </div>
1066
+ ) : null;
1067
+
1068
+ const messageListElement = (
1069
+ <MessageList
1070
+ messages={messages}
1071
+ isSending={isSending}
1072
+ isSessionReady={isSessionReady}
1073
+ isRestoringSession={sessionHistory.loading}
1074
+ agentLabel={activeAgentLabel}
1075
+ plugin={plugin}
1076
+ view={viewHost}
1077
+ terminalClient={terminalClientRef.current}
1078
+ onApprovePermission={agent.approvePermission}
1079
+ hasActivePermission={agent.hasActivePermission}
1080
+ />
1081
+ );
1082
+
1083
+ const inputAreaElement = (
1084
+ <InputArea
1085
+ isSending={isSending}
1086
+ isSessionReady={isSessionReady}
1087
+ isRestoringSession={sessionHistory.loading}
1088
+ agentLabel={activeAgentLabel}
1089
+ availableCommands={session.availableCommands || []}
1090
+ autoMentionEnabled={settings.autoMentionActiveNote}
1091
+ restoredMessage={restoredMessage}
1092
+ suggestions={suggestions}
1093
+ plugin={plugin}
1094
+ view={viewHost}
1095
+ onSendMessage={handleSendMessageWithGeminiDismiss}
1096
+ onStopGeneration={handleStopGeneration}
1097
+ onRestoredMessageConsumed={handleRestoredMessageConsumed}
1098
+ modes={session.modes}
1099
+ onModeChange={(modeId) => void handleSetMode(modeId)}
1100
+ models={session.models}
1101
+ onModelChange={(modelId) => void handleSetModel(modelId)}
1102
+ configOptions={session.configOptions}
1103
+ onConfigOptionChange={(configId, value) =>
1104
+ void handleSetConfigOption(configId, value)
1105
+ }
1106
+ usage={session.usage}
1107
+ supportsImages={session.promptCapabilities?.image ?? false}
1108
+ agentId={session.agentId}
1109
+ // Controlled component props (for broadcast commands)
1110
+ inputValue={inputValue}
1111
+ onInputChange={setInputValue}
1112
+ attachedFiles={attachedFiles}
1113
+ onAttachedFilesChange={setAttachedFiles}
1114
+ // Error overlay props
1115
+ errorInfo={errorInfo}
1116
+ onClearError={handleClearError}
1117
+ // Agent update notification props
1118
+ agentUpdateNotification={agentUpdateNotification}
1119
+ onClearAgentUpdate={handleClearAgentUpdate}
1120
+ // Gemini CLI deprecation notice props
1121
+ geminiNotice={effectiveGeminiNotice}
1122
+ onClearGeminiNotice={handleClearGeminiNotice}
1123
+ messages={messages}
1124
+ />
1125
+ );
1126
+
1127
+ if (variant === "floating") {
1128
+ // Floating layout: no wrapper div. Parent agent-client-floating-window is the flex container.
1129
+ // Focus tracking uses containerElProp (from FloatingChatView's containerRef).
1130
+ return (
1131
+ <>
1132
+ <div
1133
+ className="agent-client-floating-header"
1134
+ onMouseDown={onFloatingHeaderMouseDown}
1135
+ >
1136
+ {headerElement}
1137
+ </div>
1138
+ {cwdBanner}
1139
+ <div className="agent-client-floating-content">
1140
+ <div className="agent-client-floating-messages-container">
1141
+ {messageListElement}
1142
+ </div>
1143
+ {inputAreaElement}
1144
+ </div>
1145
+ </>
1146
+ );
1147
+ }
1148
+
1149
+ // Sidebar layout
1150
+ return (
1151
+ <div
1152
+ ref={containerRef}
1153
+ className="agent-client-chat-view-container"
1154
+ style={chatFontSizeStyle}
1155
+ >
1156
+ {headerElement}
1157
+ {cwdBanner}
1158
+ {messageListElement}
1159
+ {inputAreaElement}
1160
+ </div>
1161
+ );
1162
+ }