@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,252 @@
|
|
|
1
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
2
|
+
|
|
3
|
+
import type { SessionUpdate } from "../types/session";
|
|
4
|
+
import { AcpTypeConverter } from "./type-converter";
|
|
5
|
+
import type { PermissionManager } from "./permission-handler";
|
|
6
|
+
import type { TerminalManager } from "./terminal-handler";
|
|
7
|
+
import type { Logger } from "../utils/logger";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Handles incoming ACP protocol events from the agent.
|
|
11
|
+
*
|
|
12
|
+
* Implements the acp.Client interface to receive session updates,
|
|
13
|
+
* permission requests, and terminal operations from the SDK's
|
|
14
|
+
* ClientSideConnection dispatch.
|
|
15
|
+
*
|
|
16
|
+
* This class does not initiate communication — that is AcpClient's role.
|
|
17
|
+
* It only reacts to events from the agent side.
|
|
18
|
+
*/
|
|
19
|
+
export class AcpHandler {
|
|
20
|
+
private sessionUpdateListeners = new Set<(update: SessionUpdate) => void>();
|
|
21
|
+
|
|
22
|
+
/** Tracks session updates during a prompt. */
|
|
23
|
+
private promptSessionUpdateCount = 0;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private permissionManager: PermissionManager,
|
|
27
|
+
private terminalManager: TerminalManager,
|
|
28
|
+
private getWorkingDirectory: () => string,
|
|
29
|
+
private getCurrentSessionId: () => string | null,
|
|
30
|
+
private logger: Logger,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
// ====================================================================
|
|
34
|
+
// Callback Registration
|
|
35
|
+
// ====================================================================
|
|
36
|
+
|
|
37
|
+
/** Reset the update counter. Called by AcpClient before each sendPrompt. */
|
|
38
|
+
resetUpdateCount(): void {
|
|
39
|
+
this.promptSessionUpdateCount = 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Whether any session update was received since the last reset. */
|
|
43
|
+
hasReceivedUpdates(): boolean {
|
|
44
|
+
return this.promptSessionUpdateCount > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onSessionUpdate(callback: (update: SessionUpdate) => void): () => void {
|
|
48
|
+
this.sessionUpdateListeners.add(callback);
|
|
49
|
+
return () => this.sessionUpdateListeners.delete(callback);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Emit a session update to all listeners. Filters by current sessionId. */
|
|
53
|
+
emitSessionUpdate(update: SessionUpdate): void {
|
|
54
|
+
const currentId = this.getCurrentSessionId();
|
|
55
|
+
if (currentId && update.sessionId !== currentId) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const listener of this.sessionUpdateListeners) {
|
|
59
|
+
listener(update);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ====================================================================
|
|
64
|
+
// ACP Client Protocol Handlers (called by ClientSideConnection)
|
|
65
|
+
// ====================================================================
|
|
66
|
+
|
|
67
|
+
sessionUpdate(params: acp.SessionNotification): Promise<void> {
|
|
68
|
+
const update = params.update;
|
|
69
|
+
const sessionId = params.sessionId;
|
|
70
|
+
this.promptSessionUpdateCount++;
|
|
71
|
+
this.logger.log("[AcpHandler] sessionUpdate:", { sessionId, update });
|
|
72
|
+
|
|
73
|
+
switch (update.sessionUpdate) {
|
|
74
|
+
case "agent_message_chunk":
|
|
75
|
+
case "agent_thought_chunk":
|
|
76
|
+
case "user_message_chunk":
|
|
77
|
+
if (update.content.type === "text") {
|
|
78
|
+
this.emitSessionUpdate({
|
|
79
|
+
type: update.sessionUpdate,
|
|
80
|
+
sessionId,
|
|
81
|
+
text: update.content.text,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case "tool_call":
|
|
87
|
+
case "tool_call_update":
|
|
88
|
+
this.emitSessionUpdate({
|
|
89
|
+
type: update.sessionUpdate,
|
|
90
|
+
sessionId,
|
|
91
|
+
toolCallId: update.toolCallId,
|
|
92
|
+
title: update.title ?? undefined,
|
|
93
|
+
status: update.status || "pending",
|
|
94
|
+
kind: update.kind ?? undefined,
|
|
95
|
+
content: AcpTypeConverter.toToolCallContent(update.content),
|
|
96
|
+
locations: update.locations ?? undefined,
|
|
97
|
+
rawInput: update.rawInput as
|
|
98
|
+
| { [k: string]: unknown }
|
|
99
|
+
| undefined,
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case "plan":
|
|
104
|
+
this.emitSessionUpdate({
|
|
105
|
+
type: "plan",
|
|
106
|
+
sessionId,
|
|
107
|
+
entries: update.entries,
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case "available_commands_update":
|
|
112
|
+
this.emitSessionUpdate({
|
|
113
|
+
type: "available_commands_update",
|
|
114
|
+
sessionId,
|
|
115
|
+
commands: AcpTypeConverter.toSlashCommands(
|
|
116
|
+
update.availableCommands,
|
|
117
|
+
),
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case "current_mode_update":
|
|
122
|
+
this.emitSessionUpdate({
|
|
123
|
+
type: "current_mode_update",
|
|
124
|
+
sessionId,
|
|
125
|
+
currentModeId: update.currentModeId,
|
|
126
|
+
});
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case "session_info_update":
|
|
130
|
+
this.emitSessionUpdate({
|
|
131
|
+
type: "session_info_update",
|
|
132
|
+
sessionId,
|
|
133
|
+
title: update.title,
|
|
134
|
+
updatedAt: update.updatedAt,
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case "usage_update":
|
|
139
|
+
this.emitSessionUpdate({
|
|
140
|
+
type: "usage_update",
|
|
141
|
+
sessionId,
|
|
142
|
+
size: update.size,
|
|
143
|
+
used: update.used,
|
|
144
|
+
cost: update.cost ?? undefined,
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case "config_option_update":
|
|
149
|
+
this.emitSessionUpdate({
|
|
150
|
+
type: "config_option_update",
|
|
151
|
+
sessionId,
|
|
152
|
+
configOptions: AcpTypeConverter.toSessionConfigOptions(
|
|
153
|
+
update.configOptions,
|
|
154
|
+
),
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
return Promise.resolve();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
requestPermission(
|
|
162
|
+
params: acp.RequestPermissionRequest,
|
|
163
|
+
): Promise<acp.RequestPermissionResponse> {
|
|
164
|
+
return this.permissionManager.request(params);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ====================================================================
|
|
168
|
+
// ACP Extension Handlers
|
|
169
|
+
// ====================================================================
|
|
170
|
+
|
|
171
|
+
async extNotification(
|
|
172
|
+
method: string,
|
|
173
|
+
params: Record<string, unknown>,
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
this.logger.log(
|
|
176
|
+
`[AcpHandler] Extension notification received: ${method}`,
|
|
177
|
+
params,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ====================================================================
|
|
182
|
+
// File System Stubs
|
|
183
|
+
// ====================================================================
|
|
184
|
+
|
|
185
|
+
readTextFile(_params: acp.ReadTextFileRequest) {
|
|
186
|
+
return Promise.resolve({ content: "" });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
writeTextFile(_params: acp.WriteTextFileRequest) {
|
|
190
|
+
return Promise.resolve({});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ====================================================================
|
|
194
|
+
// Terminal Operations (called by ClientSideConnection)
|
|
195
|
+
// ====================================================================
|
|
196
|
+
|
|
197
|
+
createTerminal(
|
|
198
|
+
params: acp.CreateTerminalRequest,
|
|
199
|
+
): Promise<acp.CreateTerminalResponse> {
|
|
200
|
+
this.logger.log(
|
|
201
|
+
"[AcpHandler] createTerminal called with params:",
|
|
202
|
+
params,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const terminalId = this.terminalManager.createTerminal({
|
|
206
|
+
command: params.command,
|
|
207
|
+
args: params.args,
|
|
208
|
+
cwd: params.cwd || this.getWorkingDirectory(),
|
|
209
|
+
env: params.env ?? undefined,
|
|
210
|
+
outputByteLimit: params.outputByteLimit ?? undefined,
|
|
211
|
+
});
|
|
212
|
+
return Promise.resolve({ terminalId });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
terminalOutput(
|
|
216
|
+
params: acp.TerminalOutputRequest,
|
|
217
|
+
): Promise<acp.TerminalOutputResponse> {
|
|
218
|
+
const result = this.terminalManager.getOutput(params.terminalId);
|
|
219
|
+
if (!result) {
|
|
220
|
+
throw new Error(`Terminal ${params.terminalId} not found`);
|
|
221
|
+
}
|
|
222
|
+
return Promise.resolve(result);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async waitForTerminalExit(
|
|
226
|
+
params: acp.WaitForTerminalExitRequest,
|
|
227
|
+
): Promise<acp.WaitForTerminalExitResponse> {
|
|
228
|
+
return await this.terminalManager.waitForExit(params.terminalId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
killTerminal(
|
|
232
|
+
params: acp.KillTerminalCommandRequest,
|
|
233
|
+
): Promise<acp.KillTerminalCommandResponse> {
|
|
234
|
+
const success = this.terminalManager.killTerminal(params.terminalId);
|
|
235
|
+
if (!success) {
|
|
236
|
+
throw new Error(`Terminal ${params.terminalId} not found`);
|
|
237
|
+
}
|
|
238
|
+
return Promise.resolve({});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
releaseTerminal(
|
|
242
|
+
params: acp.ReleaseTerminalRequest,
|
|
243
|
+
): Promise<acp.ReleaseTerminalResponse> {
|
|
244
|
+
const success = this.terminalManager.releaseTerminal(params.terminalId);
|
|
245
|
+
if (!success) {
|
|
246
|
+
this.logger.log(
|
|
247
|
+
`[AcpHandler] releaseTerminal: Terminal ${params.terminalId} not found (may have been already cleaned up)`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return Promise.resolve({});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { PermissionOption } from "../types/chat";
|
|
3
|
+
import type { SessionUpdate } from "../types/session";
|
|
4
|
+
import { AcpTypeConverter } from "./type-converter";
|
|
5
|
+
import { getLogger, Logger } from "../utils/logger";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Callbacks that PermissionManager uses to communicate with the outside world.
|
|
9
|
+
*
|
|
10
|
+
* Injected by AcpClient. All UI updates (permission requests, responses,
|
|
11
|
+
* cancellations) flow through the single onSessionUpdate channel.
|
|
12
|
+
*/
|
|
13
|
+
interface PermissionManagerCallbacks {
|
|
14
|
+
/** Emit a session update event (used for all permission UI notifications) */
|
|
15
|
+
onSessionUpdate: (update: SessionUpdate) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Manages permission request lifecycle for ACP agent operations.
|
|
20
|
+
*
|
|
21
|
+
* Handles:
|
|
22
|
+
* - Receiving permission requests from the agent (via ACP protocol)
|
|
23
|
+
* - Auto-approval based on user settings
|
|
24
|
+
* - Queuing requests (only one active at a time in UI)
|
|
25
|
+
* - Resolving/cancelling pending permission Promises
|
|
26
|
+
* - Notifying UI of permission state changes
|
|
27
|
+
*
|
|
28
|
+
* This class was extracted from AcpClient to separate the permission
|
|
29
|
+
* state machine from the main protocol adapter.
|
|
30
|
+
*/
|
|
31
|
+
export class PermissionManager {
|
|
32
|
+
private logger: Logger;
|
|
33
|
+
private callbacks: PermissionManagerCallbacks;
|
|
34
|
+
private autoAllow: boolean;
|
|
35
|
+
|
|
36
|
+
/** Map of pending permission requests awaiting user response */
|
|
37
|
+
private pendingRequests = new Map<
|
|
38
|
+
string,
|
|
39
|
+
{
|
|
40
|
+
resolve: (response: acp.RequestPermissionResponse) => void;
|
|
41
|
+
toolCallId: string;
|
|
42
|
+
options: PermissionOption[];
|
|
43
|
+
sessionId: string;
|
|
44
|
+
}
|
|
45
|
+
>();
|
|
46
|
+
|
|
47
|
+
/** Queue of permission requests (first entry is the active one in UI) */
|
|
48
|
+
private requestQueue: Array<{
|
|
49
|
+
requestId: string;
|
|
50
|
+
toolCallId: string;
|
|
51
|
+
options: PermissionOption[];
|
|
52
|
+
sessionId: string;
|
|
53
|
+
}> = [];
|
|
54
|
+
|
|
55
|
+
constructor(callbacks: PermissionManagerCallbacks, autoAllow: boolean) {
|
|
56
|
+
this.logger = getLogger();
|
|
57
|
+
this.callbacks = callbacks;
|
|
58
|
+
this.autoAllow = autoAllow;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Update the auto-allow setting.
|
|
63
|
+
* Called by AcpClient during initialize() when settings are read.
|
|
64
|
+
*/
|
|
65
|
+
setAutoAllow(autoAllow: boolean): void {
|
|
66
|
+
this.autoAllow = autoAllow;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handle a permission request from the agent (ACP protocol).
|
|
71
|
+
*
|
|
72
|
+
* This is the core method called by AcpClient.requestPermission().
|
|
73
|
+
* It either auto-approves or creates a pending request with a Promise
|
|
74
|
+
* that resolves when the user responds via the UI.
|
|
75
|
+
*/
|
|
76
|
+
async request(
|
|
77
|
+
params: acp.RequestPermissionRequest,
|
|
78
|
+
): Promise<acp.RequestPermissionResponse> {
|
|
79
|
+
this.logger.log(
|
|
80
|
+
"[PermissionManager] Permission request received:",
|
|
81
|
+
params,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// If auto-allow is enabled, automatically approve the first allow option
|
|
85
|
+
if (this.autoAllow) {
|
|
86
|
+
const allowOption =
|
|
87
|
+
params.options.find(
|
|
88
|
+
(option) =>
|
|
89
|
+
option.kind === "allow_once" ||
|
|
90
|
+
option.kind === "allow_always" ||
|
|
91
|
+
(!option.kind &&
|
|
92
|
+
option.name.toLowerCase().includes("allow")),
|
|
93
|
+
) || params.options[0]; // fallback to first option
|
|
94
|
+
|
|
95
|
+
this.logger.log(
|
|
96
|
+
"[PermissionManager] Auto-allowing permission request:",
|
|
97
|
+
allowOption,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return Promise.resolve({
|
|
101
|
+
outcome: {
|
|
102
|
+
outcome: "selected",
|
|
103
|
+
optionId: allowOption.optionId,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Generate unique ID for this permission request
|
|
109
|
+
const requestId = crypto.randomUUID();
|
|
110
|
+
const toolCallId = params.toolCall?.toolCallId || crypto.randomUUID();
|
|
111
|
+
const sessionId = params.sessionId;
|
|
112
|
+
|
|
113
|
+
const normalizedOptions: PermissionOption[] = params.options.map(
|
|
114
|
+
(option) => {
|
|
115
|
+
const normalizedKind =
|
|
116
|
+
option.kind === "reject_always"
|
|
117
|
+
? "reject_once"
|
|
118
|
+
: option.kind;
|
|
119
|
+
const kind: PermissionOption["kind"] = normalizedKind
|
|
120
|
+
? normalizedKind
|
|
121
|
+
: option.name.toLowerCase().includes("allow")
|
|
122
|
+
? "allow_once"
|
|
123
|
+
: "reject_once";
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
optionId: option.optionId,
|
|
127
|
+
name: option.name,
|
|
128
|
+
kind,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const isFirstRequest = this.requestQueue.length === 0;
|
|
134
|
+
|
|
135
|
+
// Prepare permission request data
|
|
136
|
+
const permissionRequestData = {
|
|
137
|
+
requestId: requestId,
|
|
138
|
+
options: normalizedOptions,
|
|
139
|
+
isActive: isFirstRequest,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
this.requestQueue.push({
|
|
143
|
+
requestId,
|
|
144
|
+
toolCallId,
|
|
145
|
+
options: normalizedOptions,
|
|
146
|
+
sessionId,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Emit tool_call with permission request via session update callback
|
|
150
|
+
const toolCallInfo = params.toolCall;
|
|
151
|
+
this.callbacks.onSessionUpdate({
|
|
152
|
+
type: "tool_call",
|
|
153
|
+
sessionId,
|
|
154
|
+
toolCallId: toolCallId,
|
|
155
|
+
title: toolCallInfo?.title ?? undefined,
|
|
156
|
+
status: toolCallInfo?.status || "pending",
|
|
157
|
+
kind: (toolCallInfo?.kind as acp.ToolKind | undefined) ?? undefined,
|
|
158
|
+
content: AcpTypeConverter.toToolCallContent(
|
|
159
|
+
toolCallInfo?.content,
|
|
160
|
+
),
|
|
161
|
+
rawInput: toolCallInfo?.rawInput as
|
|
162
|
+
| { [k: string]: unknown }
|
|
163
|
+
| undefined,
|
|
164
|
+
permissionRequest: permissionRequestData,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Return a Promise that will be resolved when user clicks a button
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
this.pendingRequests.set(requestId, {
|
|
170
|
+
resolve,
|
|
171
|
+
toolCallId,
|
|
172
|
+
options: normalizedOptions,
|
|
173
|
+
sessionId,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Handle user's response to a permission request.
|
|
180
|
+
*
|
|
181
|
+
* Resolves the pending Promise, updates UI, and activates the next
|
|
182
|
+
* queued request if any.
|
|
183
|
+
*/
|
|
184
|
+
respond(requestId: string, optionId: string): void {
|
|
185
|
+
const request = this.pendingRequests.get(requestId);
|
|
186
|
+
if (!request) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { resolve, toolCallId, options, sessionId } = request;
|
|
191
|
+
|
|
192
|
+
// Reflect the selection in the UI via session update
|
|
193
|
+
this.callbacks.onSessionUpdate({
|
|
194
|
+
type: "tool_call_update",
|
|
195
|
+
sessionId,
|
|
196
|
+
toolCallId,
|
|
197
|
+
permissionRequest: {
|
|
198
|
+
requestId,
|
|
199
|
+
options,
|
|
200
|
+
selectedOptionId: optionId,
|
|
201
|
+
isActive: false,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
resolve({
|
|
206
|
+
outcome: {
|
|
207
|
+
outcome: "selected",
|
|
208
|
+
optionId,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
this.pendingRequests.delete(requestId);
|
|
212
|
+
this.requestQueue = this.requestQueue.filter(
|
|
213
|
+
(entry) => entry.requestId !== requestId,
|
|
214
|
+
);
|
|
215
|
+
this.activateNext();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Cancel all pending permission requests.
|
|
220
|
+
*
|
|
221
|
+
* Called during cancel() and disconnect() to clean up.
|
|
222
|
+
* Updates UI to show cancelled state and resolves all Promises
|
|
223
|
+
* with cancelled outcome.
|
|
224
|
+
*/
|
|
225
|
+
cancelAll(): void {
|
|
226
|
+
this.logger.log(
|
|
227
|
+
`[PermissionManager] Cancelling ${this.pendingRequests.size} pending permission requests`,
|
|
228
|
+
);
|
|
229
|
+
this.pendingRequests.forEach(
|
|
230
|
+
({ resolve, toolCallId, options, sessionId }, requestId) => {
|
|
231
|
+
// Update UI to show cancelled state via session update
|
|
232
|
+
this.callbacks.onSessionUpdate({
|
|
233
|
+
type: "tool_call_update",
|
|
234
|
+
sessionId,
|
|
235
|
+
toolCallId,
|
|
236
|
+
status: "completed",
|
|
237
|
+
permissionRequest: {
|
|
238
|
+
requestId,
|
|
239
|
+
options,
|
|
240
|
+
isCancelled: true,
|
|
241
|
+
isActive: false,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Resolve the promise with cancelled outcome
|
|
246
|
+
resolve({
|
|
247
|
+
outcome: {
|
|
248
|
+
outcome: "cancelled",
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
this.pendingRequests.clear();
|
|
254
|
+
this.requestQueue = [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Activate the next queued permission request in UI.
|
|
259
|
+
*/
|
|
260
|
+
private activateNext(): void {
|
|
261
|
+
if (this.requestQueue.length === 0) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const next = this.requestQueue[0];
|
|
266
|
+
const pending = this.pendingRequests.get(next.requestId);
|
|
267
|
+
if (!pending) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.callbacks.onSessionUpdate({
|
|
272
|
+
type: "tool_call_update",
|
|
273
|
+
sessionId: next.sessionId,
|
|
274
|
+
toolCallId: next.toolCallId,
|
|
275
|
+
permissionRequest: {
|
|
276
|
+
requestId: next.requestId,
|
|
277
|
+
options: pending.options,
|
|
278
|
+
isActive: true,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|