@mseep/obsidian-agent-client 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude/hooks/gh-setup.sh +49 -0
  2. package/.claude/settings.json +15 -0
  3. package/.claude/skills/release-notes/SKILL.md +331 -0
  4. package/.editorconfig +10 -0
  5. package/.github/FUNDING.yml +2 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
  9. package/.github/copilot-instructions.md +45 -0
  10. package/.github/pull_request_template.md +32 -0
  11. package/.github/workflows/ci.yaml +25 -0
  12. package/.github/workflows/docs.yml +58 -0
  13. package/.github/workflows/relay_to_openclaw.yml +59 -0
  14. package/.github/workflows/release.yaml +45 -0
  15. package/.prettierignore +10 -0
  16. package/.prettierrc +13 -0
  17. package/.vscode/extensions.json +7 -0
  18. package/.vscode/settings.json +37 -0
  19. package/.zed/settings.json +42 -0
  20. package/AGENTS.md +330 -0
  21. package/ARCHITECTURE.md +390 -0
  22. package/CONTRIBUTING.md +216 -0
  23. package/LICENSE +202 -0
  24. package/NOTICE +2 -0
  25. package/README.ja.md +121 -0
  26. package/README.md +125 -0
  27. package/docs/.vitepress/config.mts +124 -0
  28. package/docs/.vitepress/theme/custom.css +111 -0
  29. package/docs/.vitepress/theme/index.ts +4 -0
  30. package/docs/agent-setup/claude-code.md +84 -0
  31. package/docs/agent-setup/codex.md +76 -0
  32. package/docs/agent-setup/custom-agents.md +67 -0
  33. package/docs/agent-setup/gemini-cli.md +99 -0
  34. package/docs/agent-setup/index.md +34 -0
  35. package/docs/announcements/gemini-cli-deprecation.md +73 -0
  36. package/docs/getting-started/index.md +78 -0
  37. package/docs/getting-started/quick-start.md +38 -0
  38. package/docs/help/faq.md +181 -0
  39. package/docs/help/troubleshooting.md +221 -0
  40. package/docs/index.md +63 -0
  41. package/docs/public/apple-touch-icon.png +0 -0
  42. package/docs/public/demo.mp4 +0 -0
  43. package/docs/public/favicon-16x16.png +0 -0
  44. package/docs/public/favicon-32x32.png +0 -0
  45. package/docs/public/favicon.ico +0 -0
  46. package/docs/public/images/editing.webp +0 -0
  47. package/docs/public/images/export.webp +0 -0
  48. package/docs/public/images/floating-chat-button.webp +0 -0
  49. package/docs/public/images/floating-chat-instance-menu.webp +0 -0
  50. package/docs/public/images/floating-chat-view.webp +0 -0
  51. package/docs/public/images/mode-selection.webp +0 -0
  52. package/docs/public/images/model-selection.webp +0 -0
  53. package/docs/public/images/multi-session.webp +0 -0
  54. package/docs/public/images/remove-image.webp +0 -0
  55. package/docs/public/images/ribbon-icon.webp +0 -0
  56. package/docs/public/images/selection-context.gif +0 -0
  57. package/docs/public/images/sending-images.webp +0 -0
  58. package/docs/public/images/sending-messages.webp +0 -0
  59. package/docs/public/images/session-history-button.webp +0 -0
  60. package/docs/public/images/slash-commands-1.webp +0 -0
  61. package/docs/public/images/slash-commands-2.webp +0 -0
  62. package/docs/public/images/switch-agent.webp +0 -0
  63. package/docs/public/images/switch-default-agent.webp +0 -0
  64. package/docs/public/images/temporary-disable.gif +0 -0
  65. package/docs/reference/acp-support.md +110 -0
  66. package/docs/usage/chat-export.md +80 -0
  67. package/docs/usage/commands.md +51 -0
  68. package/docs/usage/context-files.md +57 -0
  69. package/docs/usage/editing.md +69 -0
  70. package/docs/usage/floating-chat.md +84 -0
  71. package/docs/usage/index.md +97 -0
  72. package/docs/usage/mcp-tools.md +33 -0
  73. package/docs/usage/mentions.md +70 -0
  74. package/docs/usage/mode-selection.md +28 -0
  75. package/docs/usage/model-selection.md +32 -0
  76. package/docs/usage/multi-session.md +68 -0
  77. package/docs/usage/sending-images.md +64 -0
  78. package/docs/usage/session-history.md +91 -0
  79. package/docs/usage/slash-commands.md +44 -0
  80. package/esbuild.config.mjs +49 -0
  81. package/eslint.config.mjs +25 -0
  82. package/main.js +228 -0
  83. package/manifest.json +11 -0
  84. package/package.json +52 -0
  85. package/src/acp/acp-client.ts +921 -0
  86. package/src/acp/acp-handler.ts +252 -0
  87. package/src/acp/permission-handler.ts +282 -0
  88. package/src/acp/terminal-handler.ts +264 -0
  89. package/src/acp/type-converter.ts +272 -0
  90. package/src/hooks/useAgent.ts +250 -0
  91. package/src/hooks/useAgentMessages.ts +470 -0
  92. package/src/hooks/useAgentSession.ts +544 -0
  93. package/src/hooks/useChatActions.ts +400 -0
  94. package/src/hooks/useHistoryModal.ts +219 -0
  95. package/src/hooks/useSessionHistory.ts +863 -0
  96. package/src/hooks/useSettings.ts +19 -0
  97. package/src/hooks/useSuggestions.ts +342 -0
  98. package/src/main.ts +9 -0
  99. package/src/plugin.ts +1126 -0
  100. package/src/services/chat-exporter.ts +552 -0
  101. package/src/services/message-sender.ts +755 -0
  102. package/src/services/message-state.ts +375 -0
  103. package/src/services/session-helpers.ts +211 -0
  104. package/src/services/session-state.ts +130 -0
  105. package/src/services/session-storage.ts +267 -0
  106. package/src/services/settings-normalizer.ts +255 -0
  107. package/src/services/settings-service.ts +285 -0
  108. package/src/services/update-checker.ts +128 -0
  109. package/src/services/vault-service.ts +558 -0
  110. package/src/services/view-registry.ts +345 -0
  111. package/src/types/agent.ts +92 -0
  112. package/src/types/chat.ts +351 -0
  113. package/src/types/errors.ts +136 -0
  114. package/src/types/obsidian-internals.d.ts +14 -0
  115. package/src/types/session.ts +731 -0
  116. package/src/ui/ChangeDirectoryModal.ts +137 -0
  117. package/src/ui/ChatContext.ts +25 -0
  118. package/src/ui/ChatHeader.tsx +295 -0
  119. package/src/ui/ChatPanel.tsx +1162 -0
  120. package/src/ui/ChatView.tsx +348 -0
  121. package/src/ui/ErrorBanner.tsx +104 -0
  122. package/src/ui/FloatingButton.tsx +351 -0
  123. package/src/ui/FloatingChatView.tsx +531 -0
  124. package/src/ui/InputArea.tsx +1107 -0
  125. package/src/ui/InputToolbar.tsx +371 -0
  126. package/src/ui/MessageBubble.tsx +442 -0
  127. package/src/ui/MessageList.tsx +265 -0
  128. package/src/ui/PermissionBanner.tsx +61 -0
  129. package/src/ui/SessionHistoryModal.tsx +821 -0
  130. package/src/ui/SettingsTab.ts +1337 -0
  131. package/src/ui/SuggestionPopup.tsx +138 -0
  132. package/src/ui/TerminalBlock.tsx +107 -0
  133. package/src/ui/ToolCallBlock.tsx +456 -0
  134. package/src/ui/shared/AttachmentStrip.tsx +57 -0
  135. package/src/ui/shared/IconButton.tsx +55 -0
  136. package/src/ui/shared/MarkdownRenderer.tsx +103 -0
  137. package/src/ui/view-host.ts +56 -0
  138. package/src/utils/error-utils.ts +274 -0
  139. package/src/utils/logger.ts +44 -0
  140. package/src/utils/mention-parser.ts +129 -0
  141. package/src/utils/paths.ts +246 -0
  142. package/src/utils/platform.ts +425 -0
  143. package/styles.css +2322 -0
  144. package/tsconfig.json +18 -0
  145. package/version-bump.mjs +18 -0
  146. package/versions.json +3 -0
@@ -0,0 +1,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
+ }