@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,863 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useMemo } from "react";
|
|
2
|
+
import type { AcpClient } from "../acp/acp-client";
|
|
3
|
+
import type { ISettingsAccess } from "../services/settings-service";
|
|
4
|
+
import type {
|
|
5
|
+
SessionInfo,
|
|
6
|
+
ListSessionsResult,
|
|
7
|
+
SavedSessionInfo,
|
|
8
|
+
ChatSession,
|
|
9
|
+
SessionModeState,
|
|
10
|
+
SessionModelState,
|
|
11
|
+
SessionConfigOption,
|
|
12
|
+
AgentCapabilities,
|
|
13
|
+
} from "../types/session";
|
|
14
|
+
import type { ChatMessage } from "../types/chat";
|
|
15
|
+
import { extractErrorMessage } from "../utils/error-utils";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Session Capability Helpers (from session-capability-utils.ts)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
interface SessionCapabilityFlags {
|
|
22
|
+
/** Whether session/load is supported (stable) */
|
|
23
|
+
canLoad: boolean;
|
|
24
|
+
/** Whether session/resume is supported (unstable) */
|
|
25
|
+
canResume: boolean;
|
|
26
|
+
/** Whether session/fork is supported (unstable) */
|
|
27
|
+
canFork: boolean;
|
|
28
|
+
/** Whether session/list is supported (unstable) */
|
|
29
|
+
canList: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getSessionCapabilityFlags(
|
|
33
|
+
agentCapabilities?: AgentCapabilities,
|
|
34
|
+
): SessionCapabilityFlags {
|
|
35
|
+
const sessionCaps = agentCapabilities?.sessionCapabilities;
|
|
36
|
+
return {
|
|
37
|
+
canLoad: agentCapabilities?.loadSession === true,
|
|
38
|
+
canResume: sessionCaps?.resume !== undefined,
|
|
39
|
+
canFork: sessionCaps?.fork !== undefined,
|
|
40
|
+
canList: sessionCaps?.list !== undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Types
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Callback invoked when a session is successfully loaded/resumed/forked.
|
|
50
|
+
* Provides the loaded session metadata to integrate with chat state.
|
|
51
|
+
*
|
|
52
|
+
* Note: Conversation history for load is received via session/update notifications,
|
|
53
|
+
* not via this callback.
|
|
54
|
+
*/
|
|
55
|
+
export interface SessionLoadCallback {
|
|
56
|
+
/**
|
|
57
|
+
* @param sessionId - ID of the session (new session ID for fork)
|
|
58
|
+
* @param modes - Available modes from the session
|
|
59
|
+
* @param models - Available models from the session
|
|
60
|
+
* @param configOptions - Config options from the session
|
|
61
|
+
*/
|
|
62
|
+
(
|
|
63
|
+
sessionId: string,
|
|
64
|
+
modes?: SessionModeState,
|
|
65
|
+
models?: SessionModelState,
|
|
66
|
+
configOptions?: SessionConfigOption[],
|
|
67
|
+
): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Callback invoked when messages should be restored from local storage.
|
|
72
|
+
* Used for resume/fork operations where the agent doesn't return history.
|
|
73
|
+
*/
|
|
74
|
+
export interface MessagesRestoreCallback {
|
|
75
|
+
/**
|
|
76
|
+
* @param messages - Messages to restore
|
|
77
|
+
*/
|
|
78
|
+
(messages: ChatMessage[]): void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Options for useSessionHistory hook.
|
|
83
|
+
*/
|
|
84
|
+
export interface UseSessionHistoryOptions {
|
|
85
|
+
/** Agent client for session operations */
|
|
86
|
+
agentClient: AcpClient;
|
|
87
|
+
/** Current session (used to access agentCapabilities and agentId) */
|
|
88
|
+
session: ChatSession;
|
|
89
|
+
/** Settings access for local session storage */
|
|
90
|
+
settingsAccess: ISettingsAccess;
|
|
91
|
+
/** Vault root path — used for session list filtering */
|
|
92
|
+
cwd: string;
|
|
93
|
+
/** Agent working directory — used for saving new session metadata */
|
|
94
|
+
agentCwd: string;
|
|
95
|
+
/** Callback invoked when a session is loaded/resumed/forked */
|
|
96
|
+
onSessionLoad: SessionLoadCallback;
|
|
97
|
+
/** Callback invoked when messages should be restored from local storage */
|
|
98
|
+
onMessagesRestore?: MessagesRestoreCallback;
|
|
99
|
+
/** Control whether useMessages ignores incoming updates (for history replay suppression) */
|
|
100
|
+
onIgnoreUpdates?: (ignore: boolean) => void;
|
|
101
|
+
/** Clear messages before restoring from local storage */
|
|
102
|
+
onClearMessages?: () => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Return type for useSessionHistory hook.
|
|
107
|
+
*/
|
|
108
|
+
export interface UseSessionHistoryReturn {
|
|
109
|
+
/** List of sessions */
|
|
110
|
+
sessions: SessionInfo[];
|
|
111
|
+
/** Whether sessions are being fetched */
|
|
112
|
+
loading: boolean;
|
|
113
|
+
/** Error message if fetch fails */
|
|
114
|
+
error: string | null;
|
|
115
|
+
/** Whether there are more sessions to load */
|
|
116
|
+
hasMore: boolean;
|
|
117
|
+
|
|
118
|
+
// Capability flags (from session.agentCapabilities)
|
|
119
|
+
/** Whether session history UI should be shown */
|
|
120
|
+
canShowSessionHistory: boolean;
|
|
121
|
+
/** Whether session can be restored (load or resume supported) */
|
|
122
|
+
canRestore: boolean;
|
|
123
|
+
/** Whether session/fork is supported (unstable) */
|
|
124
|
+
canFork: boolean;
|
|
125
|
+
/** Whether session/list is supported (unstable) */
|
|
126
|
+
canList: boolean;
|
|
127
|
+
/** Whether sessions are from local storage (agent doesn't support list) */
|
|
128
|
+
isUsingLocalSessions: boolean;
|
|
129
|
+
|
|
130
|
+
/** Set of session IDs that have local data (for UI filtering) */
|
|
131
|
+
localSessionIds: Set<string>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fetch sessions list from agent.
|
|
135
|
+
* Replaces existing sessions in state.
|
|
136
|
+
* @param cwd - Optional working directory filter
|
|
137
|
+
*/
|
|
138
|
+
fetchSessions: (cwd?: string) => Promise<void>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Load more sessions (pagination).
|
|
142
|
+
* Appends to existing sessions list.
|
|
143
|
+
*/
|
|
144
|
+
loadMoreSessions: () => Promise<void>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Restore a specific session by ID.
|
|
148
|
+
* Uses load if available (with history replay), otherwise resume (without history replay).
|
|
149
|
+
* Only available if canRestore is true.
|
|
150
|
+
* @param sessionId - Session to restore
|
|
151
|
+
* @param cwd - Working directory for the session
|
|
152
|
+
*/
|
|
153
|
+
restoreSession: (sessionId: string, cwd: string) => Promise<void>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fork a specific session to create a new branch.
|
|
157
|
+
* Only available if canFork is true.
|
|
158
|
+
* @param sessionId - Session to fork
|
|
159
|
+
* @param cwd - Working directory for the session
|
|
160
|
+
*/
|
|
161
|
+
forkSession: (sessionId: string, cwd: string) => Promise<void>;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Delete a session (local metadata + message file).
|
|
165
|
+
* @param sessionId - Session to delete
|
|
166
|
+
*/
|
|
167
|
+
deleteSession: (sessionId: string) => Promise<void>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Update the title of a saved session.
|
|
171
|
+
* @param sessionId - Session to update
|
|
172
|
+
* @param newTitle - New title string
|
|
173
|
+
* @param sessionCwd - Original cwd of the session (used when creating a new local entry)
|
|
174
|
+
*/
|
|
175
|
+
updateSessionTitle: (
|
|
176
|
+
sessionId: string,
|
|
177
|
+
newTitle: string,
|
|
178
|
+
sessionCwd: string,
|
|
179
|
+
) => Promise<void>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Save session metadata locally.
|
|
183
|
+
* Called when the first message is sent in a new session.
|
|
184
|
+
* @param sessionId - Session ID to save
|
|
185
|
+
* @param messageContent - First message content (used to generate title)
|
|
186
|
+
*/
|
|
187
|
+
saveSessionLocally: (
|
|
188
|
+
sessionId: string,
|
|
189
|
+
messageContent: string,
|
|
190
|
+
) => Promise<void>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Save session messages locally.
|
|
194
|
+
* Called when a turn ends (agent response complete).
|
|
195
|
+
* @param sessionId - Session ID
|
|
196
|
+
* @param messages - Messages to save
|
|
197
|
+
*/
|
|
198
|
+
saveSessionMessages: (
|
|
199
|
+
sessionId: string,
|
|
200
|
+
messages: import("../types/chat").ChatMessage[],
|
|
201
|
+
) => void;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Invalidate the session cache.
|
|
205
|
+
* Call this when creating a new session to refresh the list.
|
|
206
|
+
*/
|
|
207
|
+
invalidateCache: () => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Cache entry for session list.
|
|
212
|
+
*/
|
|
213
|
+
interface SessionCache {
|
|
214
|
+
sessions: SessionInfo[];
|
|
215
|
+
nextCursor?: string;
|
|
216
|
+
cwd?: string;
|
|
217
|
+
timestamp: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Constants
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
/** Cache expiry time in milliseconds (5 minutes) */
|
|
225
|
+
const CACHE_EXPIRY_MS = 5 * 60 * 1000;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Merge agent sessions with locally saved titles.
|
|
229
|
+
* Prefers local titles over agent-provided titles for better UX.
|
|
230
|
+
*
|
|
231
|
+
* Some agents return poor quality titles (e.g., "ACP Session {id}" or
|
|
232
|
+
* system prompt text), so we prefer locally saved titles when available.
|
|
233
|
+
*
|
|
234
|
+
* @param agentSessions - Sessions from agent's session/list
|
|
235
|
+
* @param localSessions - Locally saved session metadata
|
|
236
|
+
* @returns Sessions with local titles merged in
|
|
237
|
+
*/
|
|
238
|
+
function mergeWithLocalTitles(
|
|
239
|
+
agentSessions: SessionInfo[],
|
|
240
|
+
localSessions: SavedSessionInfo[],
|
|
241
|
+
): SessionInfo[] {
|
|
242
|
+
// Create a map for O(1) lookup
|
|
243
|
+
const localMap = new Map(localSessions.map((s) => [s.sessionId, s]));
|
|
244
|
+
|
|
245
|
+
return agentSessions.map((s) => {
|
|
246
|
+
const local = localMap.get(s.sessionId);
|
|
247
|
+
return {
|
|
248
|
+
...s,
|
|
249
|
+
title: local?.title ?? s.title,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Hook Implementation
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Hook for managing session history.
|
|
260
|
+
*
|
|
261
|
+
* Handles listing, loading, resuming, forking, and caching of previous chat sessions.
|
|
262
|
+
* Integrates with the agent client to fetch session metadata and
|
|
263
|
+
* load previous conversations.
|
|
264
|
+
*
|
|
265
|
+
* Capability detection is based on session.agentCapabilities, which is set
|
|
266
|
+
* during initialization and persists for the session lifetime.
|
|
267
|
+
*
|
|
268
|
+
* @param options - Hook options including agentClient, session, and onSessionLoad
|
|
269
|
+
*/
|
|
270
|
+
export function useSessionHistory(
|
|
271
|
+
options: UseSessionHistoryOptions,
|
|
272
|
+
): UseSessionHistoryReturn {
|
|
273
|
+
const {
|
|
274
|
+
agentClient,
|
|
275
|
+
session,
|
|
276
|
+
settingsAccess,
|
|
277
|
+
agentCwd,
|
|
278
|
+
onSessionLoad,
|
|
279
|
+
onMessagesRestore,
|
|
280
|
+
onIgnoreUpdates,
|
|
281
|
+
onClearMessages,
|
|
282
|
+
} = options;
|
|
283
|
+
|
|
284
|
+
// Derive capability flags from session.agentCapabilities
|
|
285
|
+
const capabilities: SessionCapabilityFlags = useMemo(
|
|
286
|
+
() => getSessionCapabilityFlags(session.agentCapabilities),
|
|
287
|
+
[session.agentCapabilities],
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// State
|
|
291
|
+
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
292
|
+
const [loading, setLoading] = useState(false);
|
|
293
|
+
const [error, setError] = useState<string | null>(null);
|
|
294
|
+
const [nextCursor, setNextCursor] = useState<string | undefined>(undefined);
|
|
295
|
+
const [localSessionIds, setLocalSessionIds] = useState<Set<string>>(
|
|
296
|
+
new Set(),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Cache reference (not state to avoid re-renders)
|
|
300
|
+
const cacheRef = useRef<SessionCache | null>(null);
|
|
301
|
+
const currentCwdRef = useRef<string | undefined>(undefined);
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Check if cache is valid.
|
|
305
|
+
*/
|
|
306
|
+
const isCacheValid = useCallback((cwd?: string): boolean => {
|
|
307
|
+
if (!cacheRef.current) return false;
|
|
308
|
+
|
|
309
|
+
// Check if cwd matches
|
|
310
|
+
if (cacheRef.current.cwd !== cwd) return false;
|
|
311
|
+
|
|
312
|
+
// Check if cache has expired
|
|
313
|
+
const age = Date.now() - cacheRef.current.timestamp;
|
|
314
|
+
return age < CACHE_EXPIRY_MS;
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Invalidate the cache.
|
|
319
|
+
*/
|
|
320
|
+
const invalidateCache = useCallback(() => {
|
|
321
|
+
cacheRef.current = null;
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
// Check if any restoration operation is available
|
|
325
|
+
const canPerformAnyOperation =
|
|
326
|
+
capabilities.canLoad || capabilities.canResume || capabilities.canFork;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Fetch sessions list from agent or local storage.
|
|
330
|
+
* Uses agent's session/list if supported, otherwise falls back to local storage.
|
|
331
|
+
* For agents that don't support restoration, local sessions are used for deletion.
|
|
332
|
+
* Replaces existing sessions in state.
|
|
333
|
+
*/
|
|
334
|
+
const fetchSessions = useCallback(
|
|
335
|
+
async (cwd?: string) => {
|
|
336
|
+
// Use local sessions if:
|
|
337
|
+
// - Agent doesn't support session/list, OR
|
|
338
|
+
// - Agent doesn't support any restoration operation (for delete only)
|
|
339
|
+
const shouldUseLocalSessions =
|
|
340
|
+
!capabilities.canList || !canPerformAnyOperation;
|
|
341
|
+
|
|
342
|
+
if (shouldUseLocalSessions) {
|
|
343
|
+
// Get locally saved sessions for this agent
|
|
344
|
+
const localSessions = settingsAccess.getSavedSessions(
|
|
345
|
+
session.agentId,
|
|
346
|
+
cwd,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Convert SavedSessionInfo to SessionInfo format
|
|
350
|
+
const sessionInfos: SessionInfo[] = localSessions.map((s) => ({
|
|
351
|
+
sessionId: s.sessionId,
|
|
352
|
+
cwd: s.cwd,
|
|
353
|
+
title: s.title,
|
|
354
|
+
updatedAt: s.updatedAt,
|
|
355
|
+
}));
|
|
356
|
+
|
|
357
|
+
setSessions(sessionInfos);
|
|
358
|
+
setLocalSessionIds(
|
|
359
|
+
new Set(localSessions.map((s) => s.sessionId)),
|
|
360
|
+
);
|
|
361
|
+
setNextCursor(undefined); // No pagination for local sessions
|
|
362
|
+
setError(null);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check cache first
|
|
367
|
+
if (isCacheValid(cwd)) {
|
|
368
|
+
// Update localSessionIds even on cache hit
|
|
369
|
+
const localSessions = settingsAccess.getSavedSessions(
|
|
370
|
+
session.agentId,
|
|
371
|
+
cwd,
|
|
372
|
+
);
|
|
373
|
+
setLocalSessionIds(
|
|
374
|
+
new Set(localSessions.map((s) => s.sessionId)),
|
|
375
|
+
);
|
|
376
|
+
// Re-merge with local titles to pick up newly saved session titles
|
|
377
|
+
const sessionsWithLocalTitles = mergeWithLocalTitles(
|
|
378
|
+
cacheRef.current!.sessions,
|
|
379
|
+
localSessions,
|
|
380
|
+
);
|
|
381
|
+
setSessions(sessionsWithLocalTitles);
|
|
382
|
+
setNextCursor(cacheRef.current!.nextCursor);
|
|
383
|
+
setError(null);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
setLoading(true);
|
|
388
|
+
setError(null);
|
|
389
|
+
currentCwdRef.current = cwd;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const result: ListSessionsResult =
|
|
393
|
+
await agentClient.listSessions(cwd);
|
|
394
|
+
|
|
395
|
+
// Merge with local titles for better UX
|
|
396
|
+
// (some agents return poor quality titles)
|
|
397
|
+
const localSessions = settingsAccess.getSavedSessions(
|
|
398
|
+
session.agentId,
|
|
399
|
+
cwd,
|
|
400
|
+
);
|
|
401
|
+
const sessionsWithLocalTitles = mergeWithLocalTitles(
|
|
402
|
+
result.sessions,
|
|
403
|
+
localSessions,
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Update state
|
|
407
|
+
setSessions(sessionsWithLocalTitles);
|
|
408
|
+
setLocalSessionIds(
|
|
409
|
+
new Set(localSessions.map((s) => s.sessionId)),
|
|
410
|
+
);
|
|
411
|
+
setNextCursor(result.nextCursor);
|
|
412
|
+
|
|
413
|
+
// Update cache (with merged titles)
|
|
414
|
+
cacheRef.current = {
|
|
415
|
+
sessions: sessionsWithLocalTitles,
|
|
416
|
+
nextCursor: result.nextCursor,
|
|
417
|
+
cwd,
|
|
418
|
+
timestamp: Date.now(),
|
|
419
|
+
};
|
|
420
|
+
} catch (err) {
|
|
421
|
+
const errorMessage = extractErrorMessage(err);
|
|
422
|
+
setError(`Failed to fetch sessions: ${errorMessage}`);
|
|
423
|
+
setSessions([]);
|
|
424
|
+
setNextCursor(undefined);
|
|
425
|
+
} finally {
|
|
426
|
+
setLoading(false);
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
[
|
|
430
|
+
agentClient,
|
|
431
|
+
capabilities.canList,
|
|
432
|
+
canPerformAnyOperation,
|
|
433
|
+
isCacheValid,
|
|
434
|
+
settingsAccess,
|
|
435
|
+
session.agentId,
|
|
436
|
+
],
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Load more sessions (pagination).
|
|
441
|
+
* Appends to existing sessions list.
|
|
442
|
+
*/
|
|
443
|
+
const loadMoreSessions = useCallback(async () => {
|
|
444
|
+
// Guard: Check if there's more to load
|
|
445
|
+
if (!nextCursor || !capabilities.canList) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
setLoading(true);
|
|
450
|
+
setError(null);
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const result: ListSessionsResult = await agentClient.listSessions(
|
|
454
|
+
currentCwdRef.current,
|
|
455
|
+
nextCursor,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Merge with local titles for better UX
|
|
459
|
+
// (some agents return poor quality titles)
|
|
460
|
+
const localSessions = settingsAccess.getSavedSessions(
|
|
461
|
+
session.agentId,
|
|
462
|
+
currentCwdRef.current,
|
|
463
|
+
);
|
|
464
|
+
const sessionsWithLocalTitles = mergeWithLocalTitles(
|
|
465
|
+
result.sessions,
|
|
466
|
+
localSessions,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Append new sessions to existing list (use functional setState)
|
|
470
|
+
setSessions((prev) => [...prev, ...sessionsWithLocalTitles]);
|
|
471
|
+
setLocalSessionIds(new Set(localSessions.map((s) => s.sessionId)));
|
|
472
|
+
setNextCursor(result.nextCursor);
|
|
473
|
+
|
|
474
|
+
// Update cache with appended sessions (with merged titles)
|
|
475
|
+
if (cacheRef.current) {
|
|
476
|
+
cacheRef.current = {
|
|
477
|
+
...cacheRef.current,
|
|
478
|
+
sessions: [
|
|
479
|
+
...cacheRef.current.sessions,
|
|
480
|
+
...sessionsWithLocalTitles,
|
|
481
|
+
],
|
|
482
|
+
nextCursor: result.nextCursor,
|
|
483
|
+
timestamp: Date.now(),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
} catch (err) {
|
|
487
|
+
const errorMessage = extractErrorMessage(err);
|
|
488
|
+
setError(`Failed to load more sessions: ${errorMessage}`);
|
|
489
|
+
} finally {
|
|
490
|
+
setLoading(false);
|
|
491
|
+
}
|
|
492
|
+
}, [
|
|
493
|
+
agentClient,
|
|
494
|
+
capabilities.canList,
|
|
495
|
+
nextCursor,
|
|
496
|
+
settingsAccess,
|
|
497
|
+
session.agentId,
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Restore a specific session by ID.
|
|
502
|
+
* Uses load if available (with history replay), otherwise resume (without history replay).
|
|
503
|
+
*/
|
|
504
|
+
const restoreSession = useCallback(
|
|
505
|
+
async (sessionId: string, cwd: string) => {
|
|
506
|
+
setLoading(true);
|
|
507
|
+
setError(null);
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
// IMPORTANT: Update session.sessionId BEFORE calling restore
|
|
511
|
+
// so that session/update notifications are not ignored
|
|
512
|
+
onSessionLoad(sessionId, undefined, undefined, undefined);
|
|
513
|
+
|
|
514
|
+
if (capabilities.canLoad) {
|
|
515
|
+
// Check local messages first to decide whether to use them or agent replay
|
|
516
|
+
const localMessages =
|
|
517
|
+
await settingsAccess.loadSessionMessages(sessionId);
|
|
518
|
+
|
|
519
|
+
if (localMessages && onMessagesRestore) {
|
|
520
|
+
// Local messages available: ignore agent replay, restore from local
|
|
521
|
+
onIgnoreUpdates?.(true);
|
|
522
|
+
onClearMessages?.();
|
|
523
|
+
try {
|
|
524
|
+
const result = await agentClient.loadSession(
|
|
525
|
+
sessionId,
|
|
526
|
+
cwd,
|
|
527
|
+
);
|
|
528
|
+
onSessionLoad(
|
|
529
|
+
result.sessionId,
|
|
530
|
+
result.modes,
|
|
531
|
+
result.models,
|
|
532
|
+
result.configOptions,
|
|
533
|
+
);
|
|
534
|
+
onMessagesRestore(localMessages);
|
|
535
|
+
} finally {
|
|
536
|
+
onIgnoreUpdates?.(false);
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
// No local messages: let agent replay flow through to UI
|
|
540
|
+
const result = await agentClient.loadSession(
|
|
541
|
+
sessionId,
|
|
542
|
+
cwd,
|
|
543
|
+
);
|
|
544
|
+
onSessionLoad(
|
|
545
|
+
result.sessionId,
|
|
546
|
+
result.modes,
|
|
547
|
+
result.models,
|
|
548
|
+
result.configOptions,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
} else if (capabilities.canResume) {
|
|
552
|
+
// Use resume (without history replay, restore from local storage)
|
|
553
|
+
const result = await agentClient.resumeSession(
|
|
554
|
+
sessionId,
|
|
555
|
+
cwd,
|
|
556
|
+
);
|
|
557
|
+
onSessionLoad(
|
|
558
|
+
result.sessionId,
|
|
559
|
+
result.modes,
|
|
560
|
+
result.models,
|
|
561
|
+
result.configOptions,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// Resume doesn't return history, so restore from local storage
|
|
565
|
+
const localMessages =
|
|
566
|
+
await settingsAccess.loadSessionMessages(sessionId);
|
|
567
|
+
if (localMessages && onMessagesRestore) {
|
|
568
|
+
onMessagesRestore(localMessages);
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
throw new Error("Session restoration is not supported");
|
|
572
|
+
}
|
|
573
|
+
} catch (err) {
|
|
574
|
+
const errorMessage = extractErrorMessage(err);
|
|
575
|
+
setError(`Failed to restore session: ${errorMessage}`);
|
|
576
|
+
throw err; // Re-throw to allow caller to handle
|
|
577
|
+
} finally {
|
|
578
|
+
setLoading(false);
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
[
|
|
582
|
+
agentClient,
|
|
583
|
+
capabilities.canLoad,
|
|
584
|
+
capabilities.canResume,
|
|
585
|
+
onSessionLoad,
|
|
586
|
+
settingsAccess,
|
|
587
|
+
onMessagesRestore,
|
|
588
|
+
onIgnoreUpdates,
|
|
589
|
+
onClearMessages,
|
|
590
|
+
],
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Fork a specific session to create a new branch.
|
|
595
|
+
* Note: For fork, we update sessionId AFTER the call since a new session ID is created.
|
|
596
|
+
* Restores messages from the original session's local storage since agent doesn't return history.
|
|
597
|
+
*/
|
|
598
|
+
const forkSession = useCallback(
|
|
599
|
+
async (sessionId: string, cwd: string) => {
|
|
600
|
+
setLoading(true);
|
|
601
|
+
setError(null);
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const result = await agentClient.forkSession(sessionId, cwd);
|
|
605
|
+
|
|
606
|
+
// Update with new session ID and modes/models from result
|
|
607
|
+
// For fork, the new session ID is returned in result
|
|
608
|
+
onSessionLoad(
|
|
609
|
+
result.sessionId,
|
|
610
|
+
result.modes,
|
|
611
|
+
result.models,
|
|
612
|
+
result.configOptions,
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
// Fork doesn't return history, so restore from original session's local storage
|
|
616
|
+
const localMessages =
|
|
617
|
+
await settingsAccess.loadSessionMessages(sessionId);
|
|
618
|
+
if (localMessages && onMessagesRestore) {
|
|
619
|
+
onMessagesRestore(localMessages);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Save forked session to history
|
|
623
|
+
if (session.agentId) {
|
|
624
|
+
const originalSession = sessions.find(
|
|
625
|
+
(s) => s.sessionId === sessionId,
|
|
626
|
+
);
|
|
627
|
+
const originalTitle = originalSession?.title ?? "Session";
|
|
628
|
+
|
|
629
|
+
// Truncate title to 50 characters
|
|
630
|
+
const maxTitleLength = 50;
|
|
631
|
+
const prefix = "Fork: ";
|
|
632
|
+
const maxBaseLength = maxTitleLength - prefix.length;
|
|
633
|
+
const truncatedTitle =
|
|
634
|
+
originalTitle.length > maxBaseLength
|
|
635
|
+
? originalTitle.substring(0, maxBaseLength) + "..."
|
|
636
|
+
: originalTitle;
|
|
637
|
+
const newTitle = `${prefix}${truncatedTitle}`;
|
|
638
|
+
|
|
639
|
+
const now = new Date().toISOString();
|
|
640
|
+
|
|
641
|
+
await settingsAccess.saveSession({
|
|
642
|
+
sessionId: result.sessionId,
|
|
643
|
+
agentId: session.agentId,
|
|
644
|
+
cwd,
|
|
645
|
+
title: newTitle,
|
|
646
|
+
createdAt: now,
|
|
647
|
+
updatedAt: now,
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Save messages under new session ID for restore after restart
|
|
651
|
+
if (localMessages) {
|
|
652
|
+
void settingsAccess.saveSessionMessages(
|
|
653
|
+
result.sessionId,
|
|
654
|
+
session.agentId,
|
|
655
|
+
localMessages,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Invalidate cache since a new session was created
|
|
661
|
+
invalidateCache();
|
|
662
|
+
} catch (err) {
|
|
663
|
+
const errorMessage = extractErrorMessage(err);
|
|
664
|
+
setError(`Failed to fork session: ${errorMessage}`);
|
|
665
|
+
throw err; // Re-throw to allow caller to handle
|
|
666
|
+
} finally {
|
|
667
|
+
setLoading(false);
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
[
|
|
671
|
+
agentClient,
|
|
672
|
+
onSessionLoad,
|
|
673
|
+
settingsAccess,
|
|
674
|
+
onMessagesRestore,
|
|
675
|
+
invalidateCache,
|
|
676
|
+
session.agentId,
|
|
677
|
+
sessions,
|
|
678
|
+
],
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Delete a session (local metadata + message file).
|
|
683
|
+
* Removes from both local state and persistent storage.
|
|
684
|
+
*/
|
|
685
|
+
const deleteSession = useCallback(
|
|
686
|
+
async (sessionId: string) => {
|
|
687
|
+
try {
|
|
688
|
+
// Delete from persistent storage (metadata + message file)
|
|
689
|
+
await settingsAccess.deleteSession(sessionId);
|
|
690
|
+
|
|
691
|
+
// Remove from local state
|
|
692
|
+
setSessions((prev) =>
|
|
693
|
+
prev.filter((s) => s.sessionId !== sessionId),
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
// Invalidate cache to ensure consistency
|
|
697
|
+
invalidateCache();
|
|
698
|
+
} catch (err) {
|
|
699
|
+
const errorMessage = extractErrorMessage(err);
|
|
700
|
+
setError(`Failed to delete session: ${errorMessage}`);
|
|
701
|
+
throw err; // Re-throw to allow caller to handle
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
[settingsAccess, invalidateCache],
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Update the title of a saved session.
|
|
709
|
+
* Updates both local state and persistent storage.
|
|
710
|
+
*/
|
|
711
|
+
const updateSessionTitle = useCallback(
|
|
712
|
+
async (sessionId: string, newTitle: string, sessionCwd: string) => {
|
|
713
|
+
// Read current title for potential rollback
|
|
714
|
+
const savedSessions = settingsAccess.getSavedSessions();
|
|
715
|
+
const existing = savedSessions.find(
|
|
716
|
+
(s) => s.sessionId === sessionId,
|
|
717
|
+
);
|
|
718
|
+
const previousTitle = existing?.title;
|
|
719
|
+
|
|
720
|
+
// Optimistic update
|
|
721
|
+
setSessions((prev) =>
|
|
722
|
+
prev.map((s) =>
|
|
723
|
+
s.sessionId === sessionId ? { ...s, title: newTitle } : s,
|
|
724
|
+
),
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
if (existing) {
|
|
729
|
+
await settingsAccess.saveSession({
|
|
730
|
+
...existing,
|
|
731
|
+
title: newTitle,
|
|
732
|
+
updatedAt: new Date().toISOString(),
|
|
733
|
+
});
|
|
734
|
+
} else {
|
|
735
|
+
// Session exists only on agent side — create local entry
|
|
736
|
+
// Use sessionCwd (from SessionInfo) instead of hook's cwd
|
|
737
|
+
await settingsAccess.saveSession({
|
|
738
|
+
sessionId,
|
|
739
|
+
agentId: session.agentId,
|
|
740
|
+
cwd: sessionCwd,
|
|
741
|
+
title: newTitle,
|
|
742
|
+
createdAt: new Date().toISOString(),
|
|
743
|
+
updatedAt: new Date().toISOString(),
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
invalidateCache();
|
|
748
|
+
} catch (err) {
|
|
749
|
+
// Rollback optimistic update
|
|
750
|
+
setSessions((prev) =>
|
|
751
|
+
prev.map((s) =>
|
|
752
|
+
s.sessionId === sessionId
|
|
753
|
+
? { ...s, title: previousTitle }
|
|
754
|
+
: s,
|
|
755
|
+
),
|
|
756
|
+
);
|
|
757
|
+
const errorMessage = extractErrorMessage(err);
|
|
758
|
+
setError(`Failed to update title: ${errorMessage}`);
|
|
759
|
+
throw err;
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
[settingsAccess, session.agentId, invalidateCache],
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Save session metadata locally.
|
|
767
|
+
* Called when the first message is sent in a new session.
|
|
768
|
+
*/
|
|
769
|
+
const saveSessionLocally = useCallback(
|
|
770
|
+
async (sessionId: string, messageContent: string) => {
|
|
771
|
+
if (!session.agentId) return;
|
|
772
|
+
|
|
773
|
+
const title =
|
|
774
|
+
messageContent.length > 50
|
|
775
|
+
? messageContent.substring(0, 50) + "..."
|
|
776
|
+
: messageContent;
|
|
777
|
+
|
|
778
|
+
await settingsAccess.saveSession({
|
|
779
|
+
sessionId,
|
|
780
|
+
agentId: session.agentId,
|
|
781
|
+
cwd: agentCwd,
|
|
782
|
+
title,
|
|
783
|
+
createdAt: new Date().toISOString(),
|
|
784
|
+
updatedAt: new Date().toISOString(),
|
|
785
|
+
});
|
|
786
|
+
},
|
|
787
|
+
[session.agentId, agentCwd, settingsAccess],
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Save session messages locally.
|
|
792
|
+
* Called when a turn ends (agent response complete).
|
|
793
|
+
* Fire-and-forget (does not block UI).
|
|
794
|
+
*/
|
|
795
|
+
const saveSessionMessages = useCallback(
|
|
796
|
+
(
|
|
797
|
+
sessionId: string,
|
|
798
|
+
messages: import("../types/chat").ChatMessage[],
|
|
799
|
+
) => {
|
|
800
|
+
if (!session.agentId || messages.length === 0) return;
|
|
801
|
+
|
|
802
|
+
// Fire-and-forget
|
|
803
|
+
void settingsAccess.saveSessionMessages(
|
|
804
|
+
sessionId,
|
|
805
|
+
session.agentId,
|
|
806
|
+
messages,
|
|
807
|
+
);
|
|
808
|
+
},
|
|
809
|
+
[session.agentId, settingsAccess],
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
return useMemo(
|
|
813
|
+
() => ({
|
|
814
|
+
sessions,
|
|
815
|
+
loading,
|
|
816
|
+
error,
|
|
817
|
+
hasMore: nextCursor !== undefined,
|
|
818
|
+
|
|
819
|
+
// Capability flags
|
|
820
|
+
canShowSessionHistory:
|
|
821
|
+
capabilities.canList ||
|
|
822
|
+
capabilities.canLoad ||
|
|
823
|
+
capabilities.canResume ||
|
|
824
|
+
capabilities.canFork,
|
|
825
|
+
canRestore: capabilities.canLoad || capabilities.canResume,
|
|
826
|
+
canFork: capabilities.canFork,
|
|
827
|
+
canList: capabilities.canList,
|
|
828
|
+
isUsingLocalSessions: !capabilities.canList,
|
|
829
|
+
localSessionIds,
|
|
830
|
+
|
|
831
|
+
// Methods
|
|
832
|
+
fetchSessions,
|
|
833
|
+
loadMoreSessions,
|
|
834
|
+
restoreSession,
|
|
835
|
+
forkSession,
|
|
836
|
+
deleteSession,
|
|
837
|
+
updateSessionTitle,
|
|
838
|
+
saveSessionLocally,
|
|
839
|
+
saveSessionMessages,
|
|
840
|
+
invalidateCache,
|
|
841
|
+
}),
|
|
842
|
+
[
|
|
843
|
+
sessions,
|
|
844
|
+
loading,
|
|
845
|
+
error,
|
|
846
|
+
nextCursor,
|
|
847
|
+
capabilities.canList,
|
|
848
|
+
capabilities.canLoad,
|
|
849
|
+
capabilities.canResume,
|
|
850
|
+
capabilities.canFork,
|
|
851
|
+
localSessionIds,
|
|
852
|
+
fetchSessions,
|
|
853
|
+
loadMoreSessions,
|
|
854
|
+
restoreSession,
|
|
855
|
+
forkSession,
|
|
856
|
+
deleteSession,
|
|
857
|
+
updateSessionTitle,
|
|
858
|
+
saveSessionLocally,
|
|
859
|
+
saveSessionMessages,
|
|
860
|
+
invalidateCache,
|
|
861
|
+
],
|
|
862
|
+
);
|
|
863
|
+
}
|