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