@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,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functions for message state updates.
|
|
3
|
+
*
|
|
4
|
+
* These functions are extracted from useMessages to keep the hook thin
|
|
5
|
+
* and to allow independent testing. They handle message array transformations
|
|
6
|
+
* for streaming updates, tool call management, and permission state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ChatMessage,
|
|
11
|
+
MessageContent,
|
|
12
|
+
ActivePermission,
|
|
13
|
+
PermissionOption,
|
|
14
|
+
} from "../types/chat";
|
|
15
|
+
import type { SessionUpdate } from "../types/session";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/** Tool call content type extracted for type safety */
|
|
22
|
+
export type ToolCallMessageContent = Extract<
|
|
23
|
+
MessageContent,
|
|
24
|
+
{ type: "tool_call" }
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Tool Call Merge
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Merge new tool call content into existing tool call.
|
|
33
|
+
* Preserves existing values when new values are undefined.
|
|
34
|
+
*/
|
|
35
|
+
export function mergeToolCallContent(
|
|
36
|
+
existing: ToolCallMessageContent,
|
|
37
|
+
update: ToolCallMessageContent,
|
|
38
|
+
): ToolCallMessageContent {
|
|
39
|
+
// Merge content arrays
|
|
40
|
+
let mergedContent = existing.content || [];
|
|
41
|
+
if (update.content !== undefined) {
|
|
42
|
+
const newContent = update.content || [];
|
|
43
|
+
|
|
44
|
+
// If new content contains diff, replace all old diffs
|
|
45
|
+
const hasDiff = newContent.some((item) => item.type === "diff");
|
|
46
|
+
if (hasDiff) {
|
|
47
|
+
mergedContent = mergedContent.filter(
|
|
48
|
+
(item) => item.type !== "diff",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
mergedContent = [...mergedContent, ...newContent];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...existing,
|
|
57
|
+
toolCallId: update.toolCallId,
|
|
58
|
+
title: update.title !== undefined ? update.title : existing.title,
|
|
59
|
+
kind: update.kind !== undefined ? update.kind : existing.kind,
|
|
60
|
+
status: update.status !== undefined ? update.status : existing.status,
|
|
61
|
+
content: mergedContent,
|
|
62
|
+
locations:
|
|
63
|
+
update.locations !== undefined
|
|
64
|
+
? update.locations
|
|
65
|
+
: existing.locations,
|
|
66
|
+
rawInput:
|
|
67
|
+
update.rawInput !== undefined &&
|
|
68
|
+
Object.keys(update.rawInput).length > 0
|
|
69
|
+
? update.rawInput
|
|
70
|
+
: existing.rawInput,
|
|
71
|
+
permissionRequest:
|
|
72
|
+
update.permissionRequest !== undefined
|
|
73
|
+
? update.permissionRequest
|
|
74
|
+
: existing.permissionRequest,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Message Array Update Functions (for batching)
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Apply a "last assistant message" update to the messages array.
|
|
84
|
+
* Creates a new assistant message if needed.
|
|
85
|
+
*/
|
|
86
|
+
export function applyUpdateLastMessage(
|
|
87
|
+
prev: ChatMessage[],
|
|
88
|
+
content: MessageContent,
|
|
89
|
+
): ChatMessage[] {
|
|
90
|
+
if (prev.length === 0 || prev[prev.length - 1].role !== "assistant") {
|
|
91
|
+
const newMessage: ChatMessage = {
|
|
92
|
+
id: crypto.randomUUID(),
|
|
93
|
+
role: "assistant",
|
|
94
|
+
content: [content],
|
|
95
|
+
timestamp: new Date(),
|
|
96
|
+
};
|
|
97
|
+
return [...prev, newMessage];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const lastMessage = prev[prev.length - 1];
|
|
101
|
+
const updatedMessage = { ...lastMessage };
|
|
102
|
+
|
|
103
|
+
if (content.type === "text" || content.type === "agent_thought") {
|
|
104
|
+
const existingContentIndex = updatedMessage.content.findIndex(
|
|
105
|
+
(c) => c.type === content.type,
|
|
106
|
+
);
|
|
107
|
+
if (existingContentIndex >= 0) {
|
|
108
|
+
const existingContent =
|
|
109
|
+
updatedMessage.content[existingContentIndex];
|
|
110
|
+
if (
|
|
111
|
+
existingContent.type === "text" ||
|
|
112
|
+
existingContent.type === "agent_thought"
|
|
113
|
+
) {
|
|
114
|
+
updatedMessage.content[existingContentIndex] = {
|
|
115
|
+
type: content.type,
|
|
116
|
+
text: existingContent.text + content.text,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
updatedMessage.content.push(content);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
const existingIndex = updatedMessage.content.findIndex(
|
|
124
|
+
(c) => c.type === content.type,
|
|
125
|
+
);
|
|
126
|
+
if (existingIndex >= 0) {
|
|
127
|
+
updatedMessage.content[existingIndex] = content;
|
|
128
|
+
} else {
|
|
129
|
+
updatedMessage.content.push(content);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return [...prev.slice(0, -1), updatedMessage];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Apply a "last user message" update to the messages array.
|
|
138
|
+
* Creates a new user message if needed. Used for session/load history replay.
|
|
139
|
+
*/
|
|
140
|
+
export function applyUpdateUserMessage(
|
|
141
|
+
prev: ChatMessage[],
|
|
142
|
+
content: MessageContent,
|
|
143
|
+
): ChatMessage[] {
|
|
144
|
+
if (prev.length === 0 || prev[prev.length - 1].role !== "user") {
|
|
145
|
+
const newMessage: ChatMessage = {
|
|
146
|
+
id: crypto.randomUUID(),
|
|
147
|
+
role: "user",
|
|
148
|
+
content: [content],
|
|
149
|
+
timestamp: new Date(),
|
|
150
|
+
};
|
|
151
|
+
return [...prev, newMessage];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const lastMessage = prev[prev.length - 1];
|
|
155
|
+
const updatedMessage = { ...lastMessage };
|
|
156
|
+
|
|
157
|
+
if (content.type === "text") {
|
|
158
|
+
const existingContentIndex = updatedMessage.content.findIndex(
|
|
159
|
+
(c) => c.type === "text",
|
|
160
|
+
);
|
|
161
|
+
if (existingContentIndex >= 0) {
|
|
162
|
+
const existingContent =
|
|
163
|
+
updatedMessage.content[existingContentIndex];
|
|
164
|
+
if (existingContent.type === "text") {
|
|
165
|
+
updatedMessage.content[existingContentIndex] = {
|
|
166
|
+
type: "text",
|
|
167
|
+
text: existingContent.text + content.text,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
updatedMessage.content.push(content);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
const existingIndex = updatedMessage.content.findIndex(
|
|
175
|
+
(c) => c.type === content.type,
|
|
176
|
+
);
|
|
177
|
+
if (existingIndex >= 0) {
|
|
178
|
+
updatedMessage.content[existingIndex] = content;
|
|
179
|
+
} else {
|
|
180
|
+
updatedMessage.content.push(content);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return [...prev.slice(0, -1), updatedMessage];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Apply a tool call upsert to the messages array.
|
|
189
|
+
* If a tool call with the given ID exists, merges. Otherwise creates new message.
|
|
190
|
+
*/
|
|
191
|
+
export function applyUpsertToolCall(
|
|
192
|
+
prev: ChatMessage[],
|
|
193
|
+
content: ToolCallMessageContent,
|
|
194
|
+
toolCallIndex: Map<string, number>,
|
|
195
|
+
): ChatMessage[] {
|
|
196
|
+
// O(1) lookup via index
|
|
197
|
+
const messageIdx = toolCallIndex.get(content.toolCallId);
|
|
198
|
+
if (messageIdx !== undefined && messageIdx < prev.length) {
|
|
199
|
+
const message = prev[messageIdx];
|
|
200
|
+
const hasTarget = message.content.some(
|
|
201
|
+
(c) =>
|
|
202
|
+
c.type === "tool_call" && c.toolCallId === content.toolCallId,
|
|
203
|
+
);
|
|
204
|
+
if (hasTarget) {
|
|
205
|
+
const updatedMessage = {
|
|
206
|
+
...message,
|
|
207
|
+
content: message.content.map((c) => {
|
|
208
|
+
if (
|
|
209
|
+
c.type === "tool_call" &&
|
|
210
|
+
c.toolCallId === content.toolCallId
|
|
211
|
+
) {
|
|
212
|
+
return mergeToolCallContent(c, content);
|
|
213
|
+
}
|
|
214
|
+
return c;
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
const result = [...prev];
|
|
218
|
+
result[messageIdx] = updatedMessage;
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Fallback: linear scan (index miss or stale index)
|
|
224
|
+
let found = false;
|
|
225
|
+
const updated = prev.map((message, idx) => {
|
|
226
|
+
const hasTarget = message.content.some(
|
|
227
|
+
(c) =>
|
|
228
|
+
c.type === "tool_call" && c.toolCallId === content.toolCallId,
|
|
229
|
+
);
|
|
230
|
+
if (!hasTarget) return message;
|
|
231
|
+
found = true;
|
|
232
|
+
toolCallIndex.set(content.toolCallId, idx); // Fix stale index
|
|
233
|
+
return {
|
|
234
|
+
...message,
|
|
235
|
+
content: message.content.map((c) => {
|
|
236
|
+
if (
|
|
237
|
+
c.type === "tool_call" &&
|
|
238
|
+
c.toolCallId === content.toolCallId
|
|
239
|
+
) {
|
|
240
|
+
return mergeToolCallContent(c, content);
|
|
241
|
+
}
|
|
242
|
+
return c;
|
|
243
|
+
}),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (found) return updated;
|
|
248
|
+
|
|
249
|
+
// Not found: create new message and register in index
|
|
250
|
+
toolCallIndex.set(content.toolCallId, prev.length);
|
|
251
|
+
return [
|
|
252
|
+
...prev,
|
|
253
|
+
{
|
|
254
|
+
id: crypto.randomUUID(),
|
|
255
|
+
role: "assistant" as const,
|
|
256
|
+
content: [content],
|
|
257
|
+
timestamp: new Date(),
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Rebuild the tool call index from a messages array.
|
|
264
|
+
*/
|
|
265
|
+
export function rebuildToolCallIndex(
|
|
266
|
+
messages: ChatMessage[],
|
|
267
|
+
toolCallIndex: Map<string, number>,
|
|
268
|
+
): void {
|
|
269
|
+
toolCallIndex.clear();
|
|
270
|
+
messages.forEach((msg, msgIdx) => {
|
|
271
|
+
for (const c of msg.content) {
|
|
272
|
+
if (c.type === "tool_call") {
|
|
273
|
+
toolCallIndex.set(c.toolCallId, msgIdx);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Apply a single session update to the messages array.
|
|
281
|
+
* Returns the same array reference if no change (session-level updates).
|
|
282
|
+
*/
|
|
283
|
+
export function applySingleUpdate(
|
|
284
|
+
prev: ChatMessage[],
|
|
285
|
+
update: SessionUpdate,
|
|
286
|
+
toolCallIndex: Map<string, number>,
|
|
287
|
+
): ChatMessage[] {
|
|
288
|
+
switch (update.type) {
|
|
289
|
+
case "agent_message_chunk":
|
|
290
|
+
return applyUpdateLastMessage(prev, {
|
|
291
|
+
type: "text",
|
|
292
|
+
text: update.text,
|
|
293
|
+
});
|
|
294
|
+
case "agent_thought_chunk":
|
|
295
|
+
return applyUpdateLastMessage(prev, {
|
|
296
|
+
type: "agent_thought",
|
|
297
|
+
text: update.text,
|
|
298
|
+
});
|
|
299
|
+
case "user_message_chunk":
|
|
300
|
+
return applyUpdateUserMessage(prev, {
|
|
301
|
+
type: "text",
|
|
302
|
+
text: update.text,
|
|
303
|
+
});
|
|
304
|
+
case "tool_call":
|
|
305
|
+
case "tool_call_update":
|
|
306
|
+
return applyUpsertToolCall(
|
|
307
|
+
prev,
|
|
308
|
+
{
|
|
309
|
+
type: "tool_call",
|
|
310
|
+
toolCallId: update.toolCallId,
|
|
311
|
+
title: update.title,
|
|
312
|
+
status: update.status || "pending",
|
|
313
|
+
kind: update.kind,
|
|
314
|
+
content: update.content,
|
|
315
|
+
locations: update.locations,
|
|
316
|
+
rawInput: update.rawInput,
|
|
317
|
+
permissionRequest: update.permissionRequest,
|
|
318
|
+
},
|
|
319
|
+
toolCallIndex,
|
|
320
|
+
);
|
|
321
|
+
case "plan":
|
|
322
|
+
return applyUpdateLastMessage(prev, {
|
|
323
|
+
type: "plan",
|
|
324
|
+
entries: update.entries,
|
|
325
|
+
});
|
|
326
|
+
default:
|
|
327
|
+
return prev;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// Permission Helper Functions
|
|
333
|
+
// ============================================================================
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Find the active permission request from messages.
|
|
337
|
+
*/
|
|
338
|
+
export function findActivePermission(
|
|
339
|
+
messages: ChatMessage[],
|
|
340
|
+
): ActivePermission | null {
|
|
341
|
+
for (const message of messages) {
|
|
342
|
+
for (const content of message.content) {
|
|
343
|
+
if (content.type === "tool_call") {
|
|
344
|
+
const permission = content.permissionRequest;
|
|
345
|
+
if (permission?.isActive) {
|
|
346
|
+
return {
|
|
347
|
+
requestId: permission.requestId,
|
|
348
|
+
toolCallId: content.toolCallId,
|
|
349
|
+
options: permission.options,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Select an option from the available options based on preferred kinds.
|
|
360
|
+
*/
|
|
361
|
+
export function selectOption(
|
|
362
|
+
options: PermissionOption[],
|
|
363
|
+
preferredKinds: PermissionOption["kind"][],
|
|
364
|
+
fallback?: (option: PermissionOption) => boolean,
|
|
365
|
+
): PermissionOption | undefined {
|
|
366
|
+
for (const kind of preferredKinds) {
|
|
367
|
+
const match = options.find((opt) => opt.kind === kind);
|
|
368
|
+
if (match) return match;
|
|
369
|
+
}
|
|
370
|
+
if (fallback) {
|
|
371
|
+
const fallbackOption = options.find(fallback);
|
|
372
|
+
if (fallbackOption) return fallbackOption;
|
|
373
|
+
}
|
|
374
|
+
return options[0];
|
|
375
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper functions for agent session management.
|
|
3
|
+
* Extracted from useSession hook for reusability and testability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AgentClientPluginSettings } from "../plugin";
|
|
7
|
+
import type {
|
|
8
|
+
BaseAgentSettings,
|
|
9
|
+
ClaudeAgentSettings,
|
|
10
|
+
GeminiAgentSettings,
|
|
11
|
+
CodexAgentSettings,
|
|
12
|
+
} from "../types/agent";
|
|
13
|
+
import type { ChatSession } from "../types/session";
|
|
14
|
+
import { toAgentConfig } from "./settings-normalizer";
|
|
15
|
+
import type { AgentUpdateNotification } from "./update-checker";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Agent information for display.
|
|
23
|
+
* (Inlined from SwitchAgentUseCase)
|
|
24
|
+
*/
|
|
25
|
+
export interface AgentDisplayInfo {
|
|
26
|
+
/** Unique agent ID */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Display name for UI */
|
|
29
|
+
displayName: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Helper Functions (Inlined from SwitchAgentUseCase)
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the default agent ID from settings (for new views).
|
|
38
|
+
*/
|
|
39
|
+
export function getDefaultAgentId(settings: AgentClientPluginSettings): string {
|
|
40
|
+
return settings.defaultAgentId || settings.claude.id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get list of all available agents from settings.
|
|
45
|
+
*/
|
|
46
|
+
export function getAvailableAgentsFromSettings(
|
|
47
|
+
settings: AgentClientPluginSettings,
|
|
48
|
+
): AgentDisplayInfo[] {
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
id: settings.claude.id,
|
|
52
|
+
displayName: settings.claude.displayName || settings.claude.id,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: settings.codex.id,
|
|
56
|
+
displayName: settings.codex.displayName || settings.codex.id,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: settings.gemini.id,
|
|
60
|
+
displayName: settings.gemini.displayName || settings.gemini.id,
|
|
61
|
+
},
|
|
62
|
+
...settings.customAgents.map((agent) => ({
|
|
63
|
+
id: agent.id,
|
|
64
|
+
displayName: agent.displayName || agent.id,
|
|
65
|
+
})),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the currently active agent information from settings.
|
|
71
|
+
*/
|
|
72
|
+
export function getCurrentAgent(
|
|
73
|
+
settings: AgentClientPluginSettings,
|
|
74
|
+
agentId?: string,
|
|
75
|
+
): AgentDisplayInfo {
|
|
76
|
+
const activeId = agentId || getDefaultAgentId(settings);
|
|
77
|
+
const agents = getAvailableAgentsFromSettings(settings);
|
|
78
|
+
return (
|
|
79
|
+
agents.find((agent) => agent.id === activeId) || {
|
|
80
|
+
id: activeId,
|
|
81
|
+
displayName: activeId,
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Helper Functions (Inlined from ManageSessionUseCase)
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Find agent settings by ID from plugin settings.
|
|
92
|
+
*/
|
|
93
|
+
export function findAgentSettings(
|
|
94
|
+
settings: AgentClientPluginSettings,
|
|
95
|
+
agentId: string,
|
|
96
|
+
): BaseAgentSettings | null {
|
|
97
|
+
if (agentId === settings.claude.id) {
|
|
98
|
+
return settings.claude;
|
|
99
|
+
}
|
|
100
|
+
if (agentId === settings.codex.id) {
|
|
101
|
+
return settings.codex;
|
|
102
|
+
}
|
|
103
|
+
if (agentId === settings.gemini.id) {
|
|
104
|
+
return settings.gemini;
|
|
105
|
+
}
|
|
106
|
+
// Search in custom agents
|
|
107
|
+
const customAgent = settings.customAgents.find(
|
|
108
|
+
(agent) => agent.id === agentId,
|
|
109
|
+
);
|
|
110
|
+
return customAgent || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build AgentConfig with API key injection for known agents.
|
|
115
|
+
*/
|
|
116
|
+
export function buildAgentConfigWithApiKey(
|
|
117
|
+
settings: AgentClientPluginSettings,
|
|
118
|
+
agentSettings: BaseAgentSettings,
|
|
119
|
+
agentId: string,
|
|
120
|
+
workingDirectory: string,
|
|
121
|
+
) {
|
|
122
|
+
const baseConfig = toAgentConfig(agentSettings, workingDirectory);
|
|
123
|
+
|
|
124
|
+
// Add API keys to environment for Claude, Codex, and Gemini
|
|
125
|
+
if (agentId === settings.claude.id) {
|
|
126
|
+
const claudeSettings = agentSettings as ClaudeAgentSettings;
|
|
127
|
+
return {
|
|
128
|
+
...baseConfig,
|
|
129
|
+
env: {
|
|
130
|
+
...baseConfig.env,
|
|
131
|
+
ANTHROPIC_API_KEY: claudeSettings.apiKey,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (agentId === settings.codex.id) {
|
|
136
|
+
const codexSettings = agentSettings as CodexAgentSettings;
|
|
137
|
+
return {
|
|
138
|
+
...baseConfig,
|
|
139
|
+
env: {
|
|
140
|
+
...baseConfig.env,
|
|
141
|
+
OPENAI_API_KEY: codexSettings.apiKey,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (agentId === settings.gemini.id) {
|
|
146
|
+
const geminiSettings = agentSettings as GeminiAgentSettings;
|
|
147
|
+
return {
|
|
148
|
+
...baseConfig,
|
|
149
|
+
env: {
|
|
150
|
+
...baseConfig.env,
|
|
151
|
+
GEMINI_API_KEY: geminiSettings.apiKey,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Custom agents - no API key injection
|
|
157
|
+
return baseConfig;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Initial State
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create initial session state.
|
|
166
|
+
*/
|
|
167
|
+
export function createInitialSession(
|
|
168
|
+
agentId: string,
|
|
169
|
+
agentDisplayName: string,
|
|
170
|
+
workingDirectory: string,
|
|
171
|
+
): ChatSession {
|
|
172
|
+
return {
|
|
173
|
+
sessionId: null,
|
|
174
|
+
state: "disconnected",
|
|
175
|
+
agentId,
|
|
176
|
+
agentDisplayName,
|
|
177
|
+
authMethods: [],
|
|
178
|
+
availableCommands: undefined,
|
|
179
|
+
modes: undefined,
|
|
180
|
+
models: undefined,
|
|
181
|
+
createdAt: new Date(),
|
|
182
|
+
lastActivityAt: new Date(),
|
|
183
|
+
workingDirectory,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Gemini CLI Deprecation Notice
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
/** Docs URL for the Gemini CLI deprecation announcement. */
|
|
192
|
+
export const GEMINI_DEPRECATION_DOCS_URL =
|
|
193
|
+
"https://rait-09.github.io/obsidian-agent-client/announcements/gemini-cli-deprecation.html";
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build the in-app notice shown while the Gemini CLI agent is selected.
|
|
197
|
+
*
|
|
198
|
+
* Google is retiring Gemini CLI for account-login (Pro/Ultra/free) tiers on
|
|
199
|
+
* June 18, 2026. This notice is static (no network) and is driven purely by the
|
|
200
|
+
* active agent id, unlike the npm-registry-backed agent update check.
|
|
201
|
+
*/
|
|
202
|
+
export function buildGeminiDeprecationNotice(): AgentUpdateNotification {
|
|
203
|
+
return {
|
|
204
|
+
variant: "info",
|
|
205
|
+
title: "Gemini CLI is being discontinued",
|
|
206
|
+
message:
|
|
207
|
+
"Google is retiring account login for Gemini CLI (Pro/Ultra/free tiers) on June 18, 2026. " +
|
|
208
|
+
"Google states Gemini CLI stays accessible via a paid Gemini API key — see the guide for setup and privacy notes.",
|
|
209
|
+
link: { text: "Learn more", url: GEMINI_DEPRECATION_DOCS_URL },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functions for session state updates.
|
|
3
|
+
*
|
|
4
|
+
* These functions are extracted from useSession to keep the hook thin
|
|
5
|
+
* and to allow independent testing. They handle session config restoration
|
|
6
|
+
* and legacy mode/model management.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ChatSession,
|
|
11
|
+
SessionConfigOption,
|
|
12
|
+
SessionResult,
|
|
13
|
+
} from "../types/session";
|
|
14
|
+
import { flattenConfigSelectOptions } from "../types/session";
|
|
15
|
+
import type { AcpClient } from "../acp/acp-client";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Legacy Config Helpers
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Apply a legacy mode/model value to the session state.
|
|
23
|
+
* Used for both optimistic updates and rollbacks.
|
|
24
|
+
*/
|
|
25
|
+
export function applyLegacyValue(
|
|
26
|
+
prev: ChatSession,
|
|
27
|
+
kind: "mode" | "model",
|
|
28
|
+
value: string,
|
|
29
|
+
): ChatSession {
|
|
30
|
+
if (kind === "mode") {
|
|
31
|
+
if (!prev.modes) return prev;
|
|
32
|
+
return { ...prev, modes: { ...prev.modes, currentModeId: value } };
|
|
33
|
+
}
|
|
34
|
+
if (!prev.models) return prev;
|
|
35
|
+
return { ...prev, models: { ...prev.models, currentModelId: value } };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Config Restore Helpers
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Try to restore a saved config option value by category.
|
|
44
|
+
* Returns updated configOptions if restored, or the original if unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export async function tryRestoreConfigOption(
|
|
47
|
+
agentClient: AcpClient,
|
|
48
|
+
sessionId: string,
|
|
49
|
+
configOptions: SessionConfigOption[],
|
|
50
|
+
category: string,
|
|
51
|
+
savedValue: string | undefined,
|
|
52
|
+
): Promise<SessionConfigOption[]> {
|
|
53
|
+
if (!savedValue) return configOptions;
|
|
54
|
+
|
|
55
|
+
const option = configOptions.find((o) => o.category === category);
|
|
56
|
+
if (!option) return configOptions;
|
|
57
|
+
if (savedValue === option.currentValue) return configOptions;
|
|
58
|
+
if (
|
|
59
|
+
!flattenConfigSelectOptions(option.options).some(
|
|
60
|
+
(o) => o.value === savedValue,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
return configOptions;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return await agentClient.setSessionConfigOption(
|
|
67
|
+
sessionId,
|
|
68
|
+
option.id,
|
|
69
|
+
savedValue,
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
return configOptions;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Restore last used mode/model via legacy APIs.
|
|
78
|
+
* Only called when configOptions is not available.
|
|
79
|
+
*/
|
|
80
|
+
export async function restoreLegacyConfig(
|
|
81
|
+
agentClient: AcpClient,
|
|
82
|
+
sessionResult: SessionResult,
|
|
83
|
+
savedModelId: string | undefined,
|
|
84
|
+
savedModeId: string | undefined,
|
|
85
|
+
setSession: (updater: (prev: ChatSession) => ChatSession) => void,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
if (!sessionResult.sessionId) return;
|
|
88
|
+
|
|
89
|
+
// Legacy model restore
|
|
90
|
+
if (sessionResult.models && savedModelId) {
|
|
91
|
+
if (
|
|
92
|
+
savedModelId !== sessionResult.models.currentModelId &&
|
|
93
|
+
sessionResult.models.availableModels.some(
|
|
94
|
+
(m) => m.modelId === savedModelId,
|
|
95
|
+
)
|
|
96
|
+
) {
|
|
97
|
+
try {
|
|
98
|
+
await agentClient.setSessionModel(
|
|
99
|
+
sessionResult.sessionId,
|
|
100
|
+
savedModelId,
|
|
101
|
+
);
|
|
102
|
+
setSession((prev) =>
|
|
103
|
+
applyLegacyValue(prev, "model", savedModelId),
|
|
104
|
+
);
|
|
105
|
+
} catch {
|
|
106
|
+
// Agent default is fine as fallback
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Legacy mode restore
|
|
112
|
+
if (sessionResult.modes && savedModeId) {
|
|
113
|
+
if (
|
|
114
|
+
savedModeId !== sessionResult.modes.currentModeId &&
|
|
115
|
+
sessionResult.modes.availableModes.some((m) => m.id === savedModeId)
|
|
116
|
+
) {
|
|
117
|
+
try {
|
|
118
|
+
await agentClient.setSessionMode(
|
|
119
|
+
sessionResult.sessionId,
|
|
120
|
+
savedModeId,
|
|
121
|
+
);
|
|
122
|
+
setSession((prev) =>
|
|
123
|
+
applyLegacyValue(prev, "mode", savedModeId),
|
|
124
|
+
);
|
|
125
|
+
} catch {
|
|
126
|
+
// Agent default is fine as fallback
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|