@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,821 @@
1
+ /**
2
+ * Session History Modal
3
+ *
4
+ * Contains the Obsidian Modal wrapper, the React content component,
5
+ * and the confirmation modal for session deletion.
6
+ */
7
+
8
+ import { Modal, App, setIcon } from "obsidian";
9
+ import * as React from "react";
10
+ const { useState, useCallback } = React;
11
+ import { createRoot, Root } from "react-dom/client";
12
+ import type { SessionInfo } from "../types/session";
13
+
14
+ // ============================================================
15
+ // ConfirmDeleteModal (internal)
16
+ // ============================================================
17
+
18
+ /**
19
+ * Confirmation modal for session deletion.
20
+ *
21
+ * Displays session title and asks user to confirm deletion.
22
+ * Calls onConfirm callback only when user clicks Delete button.
23
+ */
24
+ class ConfirmDeleteModal extends Modal {
25
+ private sessionTitle: string;
26
+ private onConfirm: () => void | Promise<void>;
27
+
28
+ constructor(
29
+ app: App,
30
+ sessionTitle: string,
31
+ onConfirm: () => void | Promise<void>,
32
+ ) {
33
+ super(app);
34
+ this.sessionTitle = sessionTitle;
35
+ this.onConfirm = onConfirm;
36
+ }
37
+
38
+ onOpen() {
39
+ const { contentEl } = this;
40
+ contentEl.empty();
41
+
42
+ // Title
43
+ contentEl.createEl("h2", { text: "Delete session?" });
44
+
45
+ // Message
46
+ contentEl.createEl("p", {
47
+ text: `Are you sure you want to delete "${this.sessionTitle}"?`,
48
+ cls: "agent-client-confirm-delete-message",
49
+ });
50
+
51
+ contentEl.createEl("p", {
52
+ text: "This only removes the session from this plugin. The session data will remain on the agent side.",
53
+ cls: "agent-client-confirm-delete-warning",
54
+ });
55
+
56
+ // Buttons container
57
+ const buttonContainer = contentEl.createDiv({
58
+ cls: "agent-client-confirm-delete-buttons",
59
+ });
60
+
61
+ // Cancel button
62
+ const cancelButton = buttonContainer.createEl("button", {
63
+ text: "Cancel",
64
+ cls: "agent-client-confirm-delete-cancel",
65
+ });
66
+ cancelButton.addEventListener("click", () => {
67
+ this.close();
68
+ });
69
+
70
+ // Delete button
71
+ const deleteButton = buttonContainer.createEl("button", {
72
+ text: "Delete",
73
+ cls: "agent-client-confirm-delete-confirm mod-warning",
74
+ });
75
+ deleteButton.addEventListener("click", () => {
76
+ this.close();
77
+ void this.onConfirm();
78
+ });
79
+ }
80
+
81
+ onClose() {
82
+ const { contentEl } = this;
83
+ contentEl.empty();
84
+ }
85
+ }
86
+
87
+ // ============================================================
88
+ // EditTitleModal (internal)
89
+ // ============================================================
90
+
91
+ /**
92
+ * Modal for editing a session title.
93
+ *
94
+ * Displays a text input pre-filled with the current title.
95
+ * Calls onSave callback with the new title when user clicks Save.
96
+ */
97
+ class EditTitleModal extends Modal {
98
+ private currentTitle: string;
99
+ private onSave: (newTitle: string) => void | Promise<void>;
100
+
101
+ constructor(
102
+ app: App,
103
+ currentTitle: string,
104
+ onSave: (newTitle: string) => void | Promise<void>,
105
+ ) {
106
+ super(app);
107
+ this.currentTitle = currentTitle;
108
+ this.onSave = onSave;
109
+ }
110
+
111
+ onOpen() {
112
+ const { contentEl } = this;
113
+ contentEl.empty();
114
+
115
+ contentEl.createEl("h2", { text: "Edit session title" });
116
+
117
+ const inputEl = contentEl.createEl("input", {
118
+ type: "text",
119
+ cls: "agent-client-edit-title-input",
120
+ attr: { maxlength: "100" },
121
+ });
122
+ // createEl sets HTML attribute; explicit assignment sets DOM property (displayed value)
123
+ inputEl.value = this.currentTitle;
124
+
125
+ // Focus and select all text for easy replacement
126
+ window.setTimeout(() => {
127
+ inputEl.focus();
128
+ inputEl.select();
129
+ }, 10);
130
+
131
+ // Enter key to save
132
+ inputEl.addEventListener("keydown", (e) => {
133
+ if (e.key === "Enter") {
134
+ e.preventDefault();
135
+ this.saveAndClose(inputEl.value);
136
+ }
137
+ });
138
+
139
+ const buttonContainer = contentEl.createDiv({
140
+ cls: "agent-client-edit-title-buttons",
141
+ });
142
+
143
+ buttonContainer
144
+ .createEl("button", { text: "Cancel" })
145
+ .addEventListener("click", () => {
146
+ this.close();
147
+ });
148
+
149
+ buttonContainer
150
+ .createEl("button", {
151
+ text: "Save",
152
+ cls: "mod-cta",
153
+ })
154
+ .addEventListener("click", () => {
155
+ this.saveAndClose(inputEl.value);
156
+ });
157
+ }
158
+
159
+ private saveAndClose(rawValue: string) {
160
+ const value = rawValue.trim();
161
+ if (!value) return;
162
+ this.close();
163
+ void this.onSave(value);
164
+ }
165
+
166
+ onClose() {
167
+ const { contentEl } = this;
168
+ contentEl.empty();
169
+ }
170
+ }
171
+
172
+ // ============================================================
173
+ // SessionHistoryContent (internal)
174
+ // ============================================================
175
+
176
+ /**
177
+ * Props for SessionHistoryContent component.
178
+ */
179
+ interface SessionHistoryContentProps {
180
+ /** Obsidian App instance (for creating modals) */
181
+ app: App;
182
+ /** List of sessions to display */
183
+ sessions: SessionInfo[];
184
+ /** Whether sessions are being fetched */
185
+ loading: boolean;
186
+ /** Error message if fetch fails */
187
+ error: string | null;
188
+ /** Whether there are more sessions to load */
189
+ hasMore: boolean;
190
+ /** Current working directory for filtering */
191
+ currentCwd: string;
192
+
193
+ // Capability flags (from useSessionHistory)
194
+ /** Whether session/list is supported (unstable) */
195
+ canList: boolean;
196
+ /** Whether session can be restored (load or resume supported) */
197
+ canRestore: boolean;
198
+ /** Whether session/fork is supported (unstable) */
199
+ canFork: boolean;
200
+
201
+ /** Whether using locally saved sessions (instead of agent session/list) */
202
+ isUsingLocalSessions: boolean;
203
+
204
+ /** Set of session IDs that have local data (for filtering) */
205
+ localSessionIds: Set<string>;
206
+
207
+ /** Whether the agent is ready (initialized) */
208
+ isAgentReady: boolean;
209
+
210
+ /** Whether debug mode is enabled (shows manual input form) */
211
+ debugMode: boolean;
212
+
213
+ /** Callback when a session is restored */
214
+ onRestoreSession: (sessionId: string, cwd: string) => Promise<void>;
215
+ /** Callback when a session is forked (create new branch) */
216
+ onForkSession: (sessionId: string, cwd: string) => Promise<void>;
217
+ /** Callback when a session is deleted */
218
+ onDeleteSession: (sessionId: string) => void | Promise<void>;
219
+ /** Callback when a session title is edited */
220
+ onEditTitle: (
221
+ sessionId: string,
222
+ newTitle: string,
223
+ sessionCwd: string,
224
+ ) => void | Promise<void>;
225
+ /** Callback to load more sessions (pagination) */
226
+ onLoadMore: () => void;
227
+ /** Callback to fetch sessions with filter */
228
+ onFetchSessions: (cwd?: string) => void;
229
+ /** Callback to close the modal */
230
+ onClose: () => void;
231
+ }
232
+
233
+ /**
234
+ * Icon button component using Obsidian's setIcon.
235
+ */
236
+ function IconButton({
237
+ iconName,
238
+ label,
239
+ className,
240
+ onClick,
241
+ }: {
242
+ iconName: string;
243
+ label: string;
244
+ className: string;
245
+ onClick: () => void;
246
+ }) {
247
+ const iconRef = React.useRef<HTMLDivElement>(null);
248
+
249
+ React.useEffect(() => {
250
+ if (iconRef.current) {
251
+ setIcon(iconRef.current, iconName);
252
+ }
253
+ }, [iconName]);
254
+
255
+ return (
256
+ <div
257
+ ref={iconRef}
258
+ className={className}
259
+ aria-label={label}
260
+ onClick={onClick}
261
+ />
262
+ );
263
+ }
264
+
265
+ /**
266
+ * Format timestamp as relative time.
267
+ * Examples: "2 hours ago", "yesterday", "3 days ago"
268
+ */
269
+ function formatRelativeTime(date: Date): string {
270
+ const now = Date.now();
271
+ const timestamp = date.getTime();
272
+ const diffMs = now - timestamp;
273
+ const diffSeconds = Math.floor(diffMs / 1000);
274
+ const diffMinutes = Math.floor(diffSeconds / 60);
275
+ const diffHours = Math.floor(diffMinutes / 60);
276
+ const diffDays = Math.floor(diffHours / 24);
277
+
278
+ if (diffMinutes < 1) {
279
+ return "just now";
280
+ } else if (diffMinutes < 60) {
281
+ return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`;
282
+ } else if (diffHours < 24) {
283
+ return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
284
+ } else if (diffDays === 1) {
285
+ return "yesterday";
286
+ } else if (diffDays < 7) {
287
+ return `${diffDays} days ago`;
288
+ } else {
289
+ const month = date.toLocaleString("default", { month: "short" });
290
+ const day = date.getDate();
291
+ const year = date.getFullYear();
292
+ return `${month} ${day}, ${year}`;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Truncate session title to 50 characters with ellipsis.
298
+ */
299
+ function truncateTitle(title: string): string {
300
+ if (title.length <= 50) {
301
+ return title;
302
+ }
303
+ return title.slice(0, 50) + "...";
304
+ }
305
+
306
+ /**
307
+ * Debug form for manual session input.
308
+ */
309
+ function DebugForm({
310
+ currentCwd,
311
+ onRestoreSession,
312
+ onForkSession,
313
+ onClose,
314
+ }: {
315
+ currentCwd: string;
316
+ onRestoreSession: (sessionId: string, cwd: string) => Promise<void>;
317
+ onForkSession: (sessionId: string, cwd: string) => Promise<void>;
318
+ onClose: () => void;
319
+ }) {
320
+ const [sessionId, setSessionId] = useState("");
321
+ const [cwd, setCwd] = useState(currentCwd);
322
+
323
+ const handleRestore = useCallback(() => {
324
+ if (sessionId.trim()) {
325
+ onClose();
326
+ void onRestoreSession(sessionId.trim(), cwd.trim() || currentCwd);
327
+ }
328
+ }, [sessionId, cwd, currentCwd, onRestoreSession, onClose]);
329
+
330
+ const handleFork = useCallback(() => {
331
+ if (sessionId.trim()) {
332
+ onClose();
333
+ void onForkSession(sessionId.trim(), cwd.trim() || currentCwd);
334
+ }
335
+ }, [sessionId, cwd, currentCwd, onForkSession, onClose]);
336
+
337
+ return (
338
+ <div className="agent-client-session-history-debug">
339
+ <h3>Debug: Manual Session Input</h3>
340
+
341
+ <div className="agent-client-session-history-debug-group">
342
+ <label htmlFor="debug-session-id">Session ID:</label>
343
+ <input
344
+ id="debug-session-id"
345
+ type="text"
346
+ placeholder="Enter session ID..."
347
+ className="agent-client-session-history-debug-input"
348
+ value={sessionId}
349
+ onChange={(e) => setSessionId(e.target.value)}
350
+ />
351
+ </div>
352
+
353
+ <div className="agent-client-session-history-debug-group">
354
+ <label htmlFor="debug-cwd">Working Directory (cwd):</label>
355
+ <input
356
+ id="debug-cwd"
357
+ type="text"
358
+ placeholder="Enter working directory..."
359
+ className="agent-client-session-history-debug-input"
360
+ value={cwd}
361
+ onChange={(e) => setCwd(e.target.value)}
362
+ />
363
+ </div>
364
+
365
+ <div className="agent-client-session-history-debug-actions">
366
+ <button
367
+ className="agent-client-session-history-debug-button"
368
+ onClick={handleRestore}
369
+ >
370
+ Restore
371
+ </button>
372
+ <button
373
+ className="agent-client-session-history-debug-button"
374
+ onClick={handleFork}
375
+ >
376
+ Fork
377
+ </button>
378
+ </div>
379
+
380
+ <hr className="agent-client-session-history-debug-separator" />
381
+ </div>
382
+ );
383
+ }
384
+
385
+ /**
386
+ * Session list item component.
387
+ */
388
+ function SessionItem({
389
+ session,
390
+ canRestore,
391
+ canFork,
392
+ currentCwd,
393
+ onRestoreSession,
394
+ onForkSession,
395
+ onDeleteSession,
396
+ onEditTitle,
397
+ onClose,
398
+ }: {
399
+ session: SessionInfo;
400
+ canRestore: boolean;
401
+ canFork: boolean;
402
+ currentCwd: string;
403
+ onRestoreSession: (sessionId: string, cwd: string) => Promise<void>;
404
+ onForkSession: (sessionId: string, cwd: string) => Promise<void>;
405
+ onDeleteSession: (sessionId: string) => void | Promise<void>;
406
+ onEditTitle: (sessionId: string) => void;
407
+ onClose: () => void;
408
+ }) {
409
+ const handleRestore = useCallback(() => {
410
+ onClose();
411
+ void onRestoreSession(session.sessionId, session.cwd);
412
+ }, [session, onRestoreSession, onClose]);
413
+
414
+ const handleFork = useCallback(() => {
415
+ onClose();
416
+ void onForkSession(session.sessionId, session.cwd);
417
+ }, [session, onForkSession, onClose]);
418
+
419
+ const handleDelete = useCallback(() => {
420
+ void onDeleteSession(session.sessionId);
421
+ }, [session.sessionId, onDeleteSession]);
422
+
423
+ const handleEditTitle = useCallback(() => {
424
+ onEditTitle(session.sessionId);
425
+ }, [session.sessionId, onEditTitle]);
426
+
427
+ return (
428
+ <div className="agent-client-session-history-item">
429
+ <div className="agent-client-session-history-item-content">
430
+ <div className="agent-client-session-history-item-title">
431
+ <span>
432
+ {truncateTitle(session.title ?? "Untitled Session")}
433
+ </span>
434
+ </div>
435
+ <div className="agent-client-session-history-item-metadata">
436
+ {session.updatedAt && (
437
+ <span className="agent-client-session-history-item-timestamp">
438
+ {formatRelativeTime(new Date(session.updatedAt))}
439
+ </span>
440
+ )}
441
+ {session.cwd !== currentCwd && (
442
+ <span
443
+ className="agent-client-session-history-item-cwd"
444
+ title={session.cwd}
445
+ >
446
+ {session.cwd}
447
+ </span>
448
+ )}
449
+ </div>
450
+ </div>
451
+
452
+ <div className="agent-client-session-history-item-actions">
453
+ <IconButton
454
+ iconName="pencil"
455
+ label="Edit session title"
456
+ className="agent-client-session-history-action-icon agent-client-session-history-edit-icon"
457
+ onClick={handleEditTitle}
458
+ />
459
+ {canRestore && (
460
+ <IconButton
461
+ iconName="play"
462
+ label="Restore session"
463
+ className="agent-client-session-history-action-icon agent-client-session-history-restore-icon"
464
+ onClick={handleRestore}
465
+ />
466
+ )}
467
+ {canFork && (
468
+ <IconButton
469
+ iconName="git-branch"
470
+ label="Fork session (create new branch)"
471
+ className="agent-client-session-history-action-icon agent-client-session-history-fork-icon"
472
+ onClick={handleFork}
473
+ />
474
+ )}
475
+ <IconButton
476
+ iconName="trash-2"
477
+ label="Delete session"
478
+ className="agent-client-session-history-action-icon agent-client-session-history-delete-icon"
479
+ onClick={handleDelete}
480
+ />
481
+ </div>
482
+ </div>
483
+ );
484
+ }
485
+
486
+ /**
487
+ * Session history content component.
488
+ *
489
+ * Renders the content of the session history modal including:
490
+ * - Debug form (when debug mode enabled)
491
+ * - Local sessions banner
492
+ * - Filter toggle (for agent session/list)
493
+ * - Session list with load/resume/fork actions
494
+ * - Pagination
495
+ */
496
+ function SessionHistoryContent({
497
+ app,
498
+ sessions,
499
+ loading,
500
+ error,
501
+ hasMore,
502
+ currentCwd,
503
+ canList,
504
+ canRestore,
505
+ canFork,
506
+ isUsingLocalSessions,
507
+ localSessionIds,
508
+ isAgentReady,
509
+ debugMode,
510
+ onRestoreSession,
511
+ onForkSession,
512
+ onDeleteSession,
513
+ onEditTitle,
514
+ onLoadMore,
515
+ onFetchSessions,
516
+ onClose,
517
+ }: SessionHistoryContentProps) {
518
+ const [filterByCurrentVault, setFilterByCurrentVault] = useState(true);
519
+ const [hideNonLocalSessions, setHideNonLocalSessions] = useState(false);
520
+
521
+ const handleFilterChange = useCallback(
522
+ (e: React.ChangeEvent<HTMLInputElement>) => {
523
+ const checked = e.target.checked;
524
+ setFilterByCurrentVault(checked);
525
+ const cwd = checked ? currentCwd : undefined;
526
+ onFetchSessions(cwd);
527
+ },
528
+ [currentCwd, onFetchSessions],
529
+ );
530
+
531
+ const handleRetry = useCallback(() => {
532
+ const cwd = filterByCurrentVault ? currentCwd : undefined;
533
+ onFetchSessions(cwd);
534
+ }, [filterByCurrentVault, currentCwd, onFetchSessions]);
535
+
536
+ // Wrap onDeleteSession to show confirmation modal
537
+ const handleDeleteWithConfirmation = useCallback(
538
+ (sessionId: string) => {
539
+ const targetSession = sessions.find(
540
+ (s) => s.sessionId === sessionId,
541
+ );
542
+ const sessionTitle = targetSession?.title ?? "Untitled Session";
543
+
544
+ const confirmModal = new ConfirmDeleteModal(
545
+ app,
546
+ sessionTitle,
547
+ () => {
548
+ void onDeleteSession(sessionId);
549
+ },
550
+ );
551
+ confirmModal.open();
552
+ },
553
+ [app, sessions, onDeleteSession],
554
+ );
555
+
556
+ // Open edit title modal for a session
557
+ const handleEditWithModal = useCallback(
558
+ (sessionId: string) => {
559
+ const targetSession = sessions.find(
560
+ (s) => s.sessionId === sessionId,
561
+ );
562
+ const currentTitle = targetSession?.title ?? "Untitled Session";
563
+ const sessionCwd = targetSession?.cwd ?? currentCwd;
564
+
565
+ const modal = new EditTitleModal(app, currentTitle, (newTitle) => {
566
+ void onEditTitle(sessionId, newTitle, sessionCwd);
567
+ });
568
+ modal.open();
569
+ },
570
+ [app, sessions, currentCwd, onEditTitle],
571
+ );
572
+
573
+ // Filter sessions based on hideNonLocalSessions setting
574
+ // Only applies to agent session/list (not local sessions which are already filtered)
575
+ const filteredSessions = React.useMemo(() => {
576
+ if (isUsingLocalSessions || !hideNonLocalSessions) {
577
+ return sessions;
578
+ }
579
+ return sessions.filter((s) => localSessionIds.has(s.sessionId));
580
+ }, [sessions, isUsingLocalSessions, hideNonLocalSessions, localSessionIds]);
581
+
582
+ // Show preparing message if agent is not ready
583
+ if (!isAgentReady) {
584
+ return (
585
+ <div className="agent-client-session-history-loading">
586
+ <p>Preparing agent...</p>
587
+ </div>
588
+ );
589
+ }
590
+
591
+ // Check if any session operation is available
592
+ const canPerformAnyOperation = canRestore || canFork;
593
+
594
+ // Show local sessions list (always show for delete functionality)
595
+ // - If agent supports list: use agent's session/list
596
+ // - If agent doesn't support list OR doesn't support restoration: use locally saved sessions
597
+ const canShowList =
598
+ canList || isUsingLocalSessions || !canPerformAnyOperation;
599
+
600
+ return (
601
+ <>
602
+ {/* Debug form */}
603
+ {debugMode && (
604
+ <DebugForm
605
+ currentCwd={currentCwd}
606
+ onRestoreSession={onRestoreSession}
607
+ onForkSession={onForkSession}
608
+ onClose={onClose}
609
+ />
610
+ )}
611
+
612
+ {/* Warning banner for agents that don't support restoration */}
613
+ {!canPerformAnyOperation && (
614
+ <div className="agent-client-session-history-warning-banner">
615
+ <p>This agent does not support session restoration.</p>
616
+ </div>
617
+ )}
618
+
619
+ {/* Local sessions banner */}
620
+ {(isUsingLocalSessions || !canPerformAnyOperation) && (
621
+ <div className="agent-client-session-history-local-banner">
622
+ <span>These sessions are saved in the plugin.</span>
623
+ </div>
624
+ )}
625
+
626
+ {/* No list capability message */}
627
+ {!canShowList && !debugMode && (
628
+ <div className="agent-client-session-history-empty">
629
+ <p className="agent-client-session-history-empty-text">
630
+ Session list is not available for this agent.
631
+ </p>
632
+ <p className="agent-client-session-history-empty-text">
633
+ Enable Debug Mode in settings to manually enter session
634
+ IDs.
635
+ </p>
636
+ </div>
637
+ )}
638
+
639
+ {canShowList && (
640
+ <>
641
+ {/* Filter toggles - only for agent session/list */}
642
+ {canList && !isUsingLocalSessions && (
643
+ <div className="agent-client-session-history-filter">
644
+ <label className="agent-client-session-history-filter-label">
645
+ <input
646
+ type="checkbox"
647
+ checked={filterByCurrentVault}
648
+ onChange={handleFilterChange}
649
+ />
650
+ <span>Show current vault only</span>
651
+ </label>
652
+ <label className="agent-client-session-history-filter-label">
653
+ <input
654
+ type="checkbox"
655
+ checked={hideNonLocalSessions}
656
+ onChange={(e) =>
657
+ setHideNonLocalSessions(
658
+ e.target.checked,
659
+ )
660
+ }
661
+ />
662
+ <span>Hide sessions without local data</span>
663
+ </label>
664
+ </div>
665
+ )}
666
+
667
+ {/* Error state */}
668
+ {error && (
669
+ <div className="agent-client-session-history-error">
670
+ <p className="agent-client-session-history-error-text">
671
+ {error}
672
+ </p>
673
+ <button
674
+ className="agent-client-session-history-retry-button"
675
+ onClick={handleRetry}
676
+ >
677
+ Retry
678
+ </button>
679
+ </div>
680
+ )}
681
+
682
+ {/* Loading state */}
683
+ {!error && loading && filteredSessions.length === 0 && (
684
+ <div className="agent-client-session-history-loading">
685
+ <p>Loading sessions...</p>
686
+ </div>
687
+ )}
688
+
689
+ {/* Empty state */}
690
+ {!error && !loading && filteredSessions.length === 0 && (
691
+ <div className="agent-client-session-history-empty">
692
+ <p className="agent-client-session-history-empty-text">
693
+ No previous sessions
694
+ </p>
695
+ </div>
696
+ )}
697
+
698
+ {/* Session list */}
699
+ {!error && filteredSessions.length > 0 && (
700
+ <div className="agent-client-session-history-list">
701
+ {filteredSessions.map((session) => (
702
+ <SessionItem
703
+ key={session.sessionId}
704
+ session={session}
705
+ canRestore={canRestore}
706
+ canFork={canFork}
707
+ currentCwd={currentCwd}
708
+ onRestoreSession={onRestoreSession}
709
+ onForkSession={onForkSession}
710
+ onDeleteSession={
711
+ handleDeleteWithConfirmation
712
+ }
713
+ onEditTitle={handleEditWithModal}
714
+ onClose={onClose}
715
+ />
716
+ ))}
717
+ </div>
718
+ )}
719
+
720
+ {/* Load more button */}
721
+ {!error && hasMore && (
722
+ <div className="agent-client-session-history-load-more">
723
+ <button
724
+ className="agent-client-session-history-load-more-button"
725
+ disabled={loading}
726
+ onClick={onLoadMore}
727
+ >
728
+ {loading ? "Loading..." : "Load more"}
729
+ </button>
730
+ </div>
731
+ )}
732
+ </>
733
+ )}
734
+ </>
735
+ );
736
+ }
737
+
738
+ // ============================================================
739
+ // SessionHistoryModal (exported)
740
+ // ============================================================
741
+
742
+ /**
743
+ * Props for SessionHistoryModal (same as SessionHistoryContentProps minus onClose and app).
744
+ */
745
+ export type SessionHistoryModalProps = Omit<
746
+ SessionHistoryContentProps,
747
+ "onClose" | "app"
748
+ >;
749
+
750
+ /**
751
+ * Modal for displaying and selecting from session history.
752
+ *
753
+ * This is a thin wrapper around the SessionHistoryContent React component.
754
+ * It extends Obsidian's Modal class for proper modal behavior (backdrop,
755
+ * escape key handling, etc.) while delegating all UI rendering to React.
756
+ */
757
+ export class SessionHistoryModal extends Modal {
758
+ private root: Root | null = null;
759
+ private props: SessionHistoryModalProps;
760
+
761
+ constructor(app: App, props: SessionHistoryModalProps) {
762
+ super(app);
763
+ this.props = props;
764
+ }
765
+
766
+ /**
767
+ * Update modal props and re-render the React component.
768
+ * Call this when session data changes.
769
+ */
770
+ updateProps(props: SessionHistoryModalProps) {
771
+ this.props = props;
772
+ this.renderContent();
773
+ }
774
+
775
+ /**
776
+ * Called when modal is opened.
777
+ * Creates React root and renders the content.
778
+ */
779
+ onOpen() {
780
+ const { contentEl } = this;
781
+ contentEl.empty();
782
+
783
+ // Add modal title
784
+ contentEl.createEl("h2", { text: "Session history" });
785
+
786
+ // Create container for React content
787
+ const reactContainer = contentEl.createDiv();
788
+
789
+ // Create React root and render
790
+ this.root = createRoot(reactContainer);
791
+ this.renderContent();
792
+ }
793
+
794
+ /**
795
+ * Render or re-render the React content.
796
+ */
797
+ private renderContent() {
798
+ if (this.root) {
799
+ this.root.render(
800
+ React.createElement(SessionHistoryContent, {
801
+ ...this.props,
802
+ app: this.app,
803
+ onClose: () => this.close(),
804
+ }),
805
+ );
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Called when modal is closed.
811
+ * Unmounts React component and cleans up.
812
+ */
813
+ onClose() {
814
+ if (this.root) {
815
+ this.root.unmount();
816
+ this.root = null;
817
+ }
818
+ const { contentEl } = this;
819
+ contentEl.empty();
820
+ }
821
+ }