opencode-idea 0.0.4 → 0.0.5
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/CHANGELOG.md +8 -0
- package/package.json +1 -2
- package/src/idea-mcp.js +2 -2
- package/src/plugin.js +187 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
- **冷启动等待 MCP 端点**:`/open-in-idea` 在 `open -a` 之后按 `mcpStartTimeoutMs`(默认 60s)轮询,直到 IDEA 自带的 MCP server 开始监听。冷启动时一次命令即可连上,不再像之前那样探测一次失败就报「未检测到 MCP 服务」。
|
|
6
|
+
- **工具就绪判定**:连接后轮询 `ctx.tool.list()`,确认 `idea_read_file` / `idea_apply_patch` 已注册。未就绪时状态为 `tools-loading` 并提示稍后重试,不再在原生工具表尚未就绪时谎称「已连接」。
|
|
7
|
+
- **重复执行刷新工具列表**:再次执行 `/open-in-idea` 会重建 MCP server 并依次 `ctx.mcp.reload()` + `ctx.tool.reload()`,让已经捕获过工具快照的会话立即看到最新工具,而不是停留在旧快照。
|
|
8
|
+
- **IDE 掉线自动注销**:每次请求前复检端点,IDEA 关闭或某次 `idea_*` 工具调用失败时,立即移除 `idea_*` 工具并恢复原生工具,避免后续请求继续调用已失效的 IDE 工具;只有再次执行 `/open-in-idea` 才能重新注册。
|
|
9
|
+
- **清理**:移除调试日志;删除过时的 `scripts/publish.sh`(旧包名 + bypass token 流程,已由 `scripts/release.mjs` 取代);`package.json` 的 `files` 移除早已删除的 `src/idea-tool-descriptions.js`。
|
|
10
|
+
|
|
3
11
|
## 0.0.4
|
|
4
12
|
|
|
5
13
|
- **`/open-in-idea` 刷新工具列表**:在 `ctx.mcp.reload()` 之后显式调用 `ctx.tool.reload()`。之前只有 MCP 层重建目录,已经捕获过工具快照的会话仍然看不到新的 `idea_*` 原生工具;现在下一次模型请求即可用,无需重开会话。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-idea",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "OpenCode plugin that connects to JetBrains IDE MCP servers and prefers IDE tools when the current project is open in the IDE",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"src/env.js",
|
|
15
15
|
"src/ide-launcher.js",
|
|
16
16
|
"src/ide-guidance.js",
|
|
17
|
-
"src/idea-tool-descriptions.js",
|
|
18
17
|
"README.md",
|
|
19
18
|
"CHANGELOG.md"
|
|
20
19
|
],
|
package/src/idea-mcp.js
CHANGED
|
@@ -81,7 +81,7 @@ export async function probePort(port, options = {}) {
|
|
|
81
81
|
params: {
|
|
82
82
|
protocolVersion: PROTOCOL_VERSION,
|
|
83
83
|
capabilities: {},
|
|
84
|
-
clientInfo: { name: 'opencode-idea', version: '0.0.
|
|
84
|
+
clientInfo: { name: 'opencode-idea', version: '0.0.5' },
|
|
85
85
|
},
|
|
86
86
|
}),
|
|
87
87
|
signal: controller.signal,
|
|
@@ -151,7 +151,7 @@ export async function callTool(port, projectPath, name, args = {}, options = {})
|
|
|
151
151
|
params: {
|
|
152
152
|
protocolVersion: PROTOCOL_VERSION,
|
|
153
153
|
capabilities: {},
|
|
154
|
-
clientInfo: { name: 'opencode-idea', version: '0.0.
|
|
154
|
+
clientInfo: { name: 'opencode-idea', version: '0.0.5' },
|
|
155
155
|
},
|
|
156
156
|
});
|
|
157
157
|
const response = await post({
|
package/src/plugin.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
DEFAULT_PORTS,
|
|
24
24
|
IDEA_SERVER_NAME,
|
|
25
25
|
findIdePort,
|
|
26
|
+
probePort,
|
|
26
27
|
serverConfig,
|
|
27
28
|
} from './idea-mcp.js';
|
|
28
29
|
import { readIdeTerminalEnv } from './ide-env.js';
|
|
@@ -43,10 +44,38 @@ const DEFAULT_LAUNCH_MAX_ATTEMPTS = 5;
|
|
|
43
44
|
// MCP availability is a fast preflight. If it is not ready, fail fast and let
|
|
44
45
|
// the user enable MCP + Brave Mode in IDEA before retrying the command.
|
|
45
46
|
const DEFAULT_MCP_PROBE_TIMEOUT_MS = 1000;
|
|
47
|
+
// A cold IDE can take tens of seconds before its bundled MCP server listens.
|
|
48
|
+
// After `open -a`, poll for the endpoint instead of reporting failure once.
|
|
49
|
+
const DEFAULT_MCP_START_TIMEOUT_MS = 60000;
|
|
50
|
+
const MCP_START_POLL_MS = 1000;
|
|
51
|
+
const DEFAULT_TOOL_REFRESH_TIMEOUT_MS = 5000;
|
|
52
|
+
const TOOL_REFRESH_POLL_MS = 25;
|
|
53
|
+
const IDEA_READY_TOOL_IDS = new Set(['idea_read_file', 'idea_apply_patch']);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Remove IDEA MCP definitions from one request's tool record.
|
|
57
|
+
*
|
|
58
|
+
* @param {Record<string, unknown> | undefined} tools
|
|
59
|
+
*/
|
|
60
|
+
function removeIdeaToolDefinitions(tools) {
|
|
61
|
+
if (!tools || typeof tools !== 'object') return 0;
|
|
62
|
+
let removed = 0;
|
|
63
|
+
for (const name of Object.keys(tools)) {
|
|
64
|
+
if (!name.startsWith('idea_')) continue;
|
|
65
|
+
delete tools[name];
|
|
66
|
+
removed += 1;
|
|
67
|
+
}
|
|
68
|
+
return removed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {ReadonlyArray<{ id?: string }> | undefined} tools */
|
|
72
|
+
function toolIds(tools) {
|
|
73
|
+
return (tools ?? []).map((tool) => tool.id).filter((id) => typeof id === 'string').sort();
|
|
74
|
+
}
|
|
46
75
|
|
|
47
76
|
/**
|
|
48
77
|
* @typedef {{
|
|
49
|
-
* status: 'connected' | 'mcp-unavailable' | 'launch-failed' | 'disabled',
|
|
78
|
+
* status: 'connected' | 'tools-loading' | 'mcp-unavailable' | 'launch-failed' | 'disabled',
|
|
50
79
|
* port?: number,
|
|
51
80
|
* opened?: boolean,
|
|
52
81
|
* unavailableNoticeSent?: boolean
|
|
@@ -71,10 +100,12 @@ const asPositiveNumber = (value, fallback) =>
|
|
|
71
100
|
export function ideFeedback(result) {
|
|
72
101
|
switch (result?.status) {
|
|
73
102
|
case 'connected':
|
|
74
|
-
return 'IDE MCP 已连接;
|
|
103
|
+
return 'IDE MCP 已连接;IDEA 原生工具已注册并可用于后续请求。';
|
|
104
|
+
case 'tools-loading':
|
|
105
|
+
return 'IDE MCP 已连接,但原生 IDEA 工具列表尚未完成注册。请稍后再次执行 /open-in-idea。';
|
|
75
106
|
case 'mcp-unavailable':
|
|
76
107
|
return result?.opened
|
|
77
|
-
? '已请求打开/激活 IntelliJ IDEA
|
|
108
|
+
? '已请求打开/激活 IntelliJ IDEA 并等待 MCP 服务启动,但未检测到 MCP 服务。请在 IDEA 中开启 MCP 服务(Settings → MCP Server,需 2026.2+)并启用 Brave Mode,然后再次执行 /open-in-idea。'
|
|
78
109
|
: '未检测到 IntelliJ IDEA 的 MCP 服务。请确认 IDEA 已打开,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后再次执行 /open-in-idea。';
|
|
79
110
|
case 'launch-failed':
|
|
80
111
|
return '无法拉起 IntelliJ IDEA(未找到应用或启动失败)。请确认已安装 IDEA,或手动打开后重试 /open-in-idea。';
|
|
@@ -112,6 +143,8 @@ export default {
|
|
|
112
143
|
* launchCooldownMs?: number,
|
|
113
144
|
* launchMaxAttempts?: number,
|
|
114
145
|
* mcpProbeTimeoutMs?: number,
|
|
146
|
+
* mcpStartTimeoutMs?: number,
|
|
147
|
+
* toolRefreshTimeoutMs?: number,
|
|
115
148
|
* feedback?: false | 'message',
|
|
116
149
|
* },
|
|
117
150
|
* command?: {
|
|
@@ -123,10 +156,21 @@ export default {
|
|
|
123
156
|
* },
|
|
124
157
|
* session?: {
|
|
125
158
|
* prompt: (input: { sessionID: string, text: string, delivery: unknown }) => Promise<unknown>,
|
|
126
|
-
* hook: (name: string, cb: (event: {
|
|
159
|
+
* hook: (name: string, cb: (event: {
|
|
160
|
+
* sessionID?: string,
|
|
161
|
+
* agent?: string,
|
|
162
|
+
* system?: unknown[],
|
|
163
|
+
* tools?: Record<string, unknown>,
|
|
164
|
+
* }) => void | Promise<void>) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
127
165
|
* },
|
|
128
166
|
* tool?: {
|
|
167
|
+
* list?: () => Promise<ReadonlyArray<{ id?: string }>>,
|
|
129
168
|
* reload?: () => Promise<void>,
|
|
169
|
+
* hook?: (name: string, cb: (event: {
|
|
170
|
+
* tool?: string,
|
|
171
|
+
* status?: string,
|
|
172
|
+
* error?: { message?: string },
|
|
173
|
+
* }) => void | Promise<void>) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
130
174
|
* },
|
|
131
175
|
* mcp: {
|
|
132
176
|
* transform: (cb: (editor: { set: (name: string, config: unknown) => void }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
@@ -145,6 +189,8 @@ export default {
|
|
|
145
189
|
const injectGuidance = options.injectGuidance !== false;
|
|
146
190
|
const ideApp = resolveIdeApp(options.openInIde);
|
|
147
191
|
const mcpProbeTimeoutMs = asPositiveNumber(options.mcpProbeTimeoutMs, DEFAULT_MCP_PROBE_TIMEOUT_MS);
|
|
192
|
+
const mcpStartTimeoutMs = asPositiveNumber(options.mcpStartTimeoutMs, DEFAULT_MCP_START_TIMEOUT_MS);
|
|
193
|
+
const toolRefreshTimeoutMs = asPositiveNumber(options.toolRefreshTimeoutMs, DEFAULT_TOOL_REFRESH_TIMEOUT_MS);
|
|
148
194
|
// "message" (default) injects a clearly-labelled notification that the
|
|
149
195
|
// model only acknowledges ("收到"); the user sees the clean notice via
|
|
150
196
|
// metadata.displayText. false is silent.
|
|
@@ -153,9 +199,9 @@ export default {
|
|
|
153
199
|
cooldownMs: asPositiveNumber(options.launchCooldownMs, DEFAULT_LAUNCH_COOLDOWN_MS),
|
|
154
200
|
maxAttempts: asPositiveNumber(options.launchMaxAttempts, DEFAULT_LAUNCH_MAX_ATTEMPTS),
|
|
155
201
|
});
|
|
156
|
-
|
|
157
202
|
/** @type {number | undefined} */
|
|
158
203
|
let activePort;
|
|
204
|
+
let activeToolsReady = false;
|
|
159
205
|
let currentEnv = { env: {}, prependPath: [] };
|
|
160
206
|
|
|
161
207
|
/** @type {Array<{ dispose: () => Promise<void> | void }>} */
|
|
@@ -166,10 +212,13 @@ export default {
|
|
|
166
212
|
let sessionRegistration;
|
|
167
213
|
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
168
214
|
let commandRegistration;
|
|
215
|
+
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
216
|
+
let toolRegistration;
|
|
169
217
|
let disposed = false;
|
|
170
218
|
|
|
171
219
|
const deactivateIde = async () => {
|
|
172
220
|
activePort = undefined;
|
|
221
|
+
activeToolsReady = false;
|
|
173
222
|
const current = registrations.slice();
|
|
174
223
|
registrations.length = 0;
|
|
175
224
|
if (current.length === 0) return;
|
|
@@ -182,22 +231,73 @@ export default {
|
|
|
182
231
|
}
|
|
183
232
|
};
|
|
184
233
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
234
|
+
const waitForIdeaTools = async () => {
|
|
235
|
+
if (typeof ctx.tool?.list !== 'function') return false;
|
|
236
|
+
const deadline = Date.now() + toolRefreshTimeoutMs;
|
|
237
|
+
while (Date.now() < deadline) {
|
|
238
|
+
try {
|
|
239
|
+
const ids = toolIds(await ctx.tool.list());
|
|
240
|
+
if ([...IDEA_READY_TOOL_IDS].every((id) => ids.includes(id))) return true;
|
|
241
|
+
} catch {
|
|
242
|
+
// The MCP catalog may still be settling; retry within the bounded wait.
|
|
243
|
+
}
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, TOOL_REFRESH_POLL_MS));
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const refreshIdeTools = async () => {
|
|
250
|
+
activeToolsReady = false;
|
|
191
251
|
if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
|
|
192
252
|
// Reconcile the MCP servers above, then replay the tool registry so the
|
|
193
253
|
// refreshed catalog lands in the next model request's tool snapshot.
|
|
194
254
|
// Without this, a session that already captured a snapshot keeps the old
|
|
195
255
|
// tool list until the session is reopened.
|
|
196
256
|
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
257
|
+
activeToolsReady = await waitForIdeaTools();
|
|
258
|
+
return activeToolsReady;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const activateIde = async (port) => {
|
|
262
|
+
const mcpRegistration = await ctx.mcp.transform((editor) => {
|
|
263
|
+
editor.set(IDEA_SERVER_NAME, serverConfig(port, projectPath));
|
|
264
|
+
});
|
|
265
|
+
registrations = [mcpRegistration];
|
|
266
|
+
activePort = port;
|
|
267
|
+
return refreshIdeTools();
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/** @type {Promise<boolean> | undefined} */
|
|
271
|
+
let healthCheck;
|
|
272
|
+
/**
|
|
273
|
+
* A manually activated IDE can disappear while the OpenCode background
|
|
274
|
+
* service keeps running (the desktop client and the service have separate
|
|
275
|
+
* lifetimes). Re-check the endpoint before every request that could expose
|
|
276
|
+
* IDEA tools, and unregister them as soon as the endpoint is gone. Only
|
|
277
|
+
* `/open-in-idea` may activate them again.
|
|
278
|
+
*
|
|
279
|
+
* @returns {Promise<boolean>}
|
|
280
|
+
*/
|
|
281
|
+
const verifyActiveIde = async () => {
|
|
282
|
+
if (activePort === undefined) return true;
|
|
283
|
+
if (healthCheck) return healthCheck;
|
|
284
|
+
const port = activePort;
|
|
285
|
+
healthCheck = (async () => {
|
|
286
|
+
const available = await probePort(port, { timeoutMs: mcpProbeTimeoutMs });
|
|
287
|
+
if (available) return true;
|
|
288
|
+
if (activePort !== port) return true;
|
|
289
|
+
await deactivateIde();
|
|
290
|
+
if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
|
|
291
|
+
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
292
|
+
return false;
|
|
293
|
+
})().finally(() => {
|
|
294
|
+
healthCheck = undefined;
|
|
295
|
+
});
|
|
296
|
+
return healthCheck;
|
|
197
297
|
};
|
|
198
298
|
|
|
199
299
|
const reconcileOnce = async ({ probeIde = true } = {}) => {
|
|
200
|
-
if (disposed) return;
|
|
300
|
+
if (disposed) return false;
|
|
201
301
|
|
|
202
302
|
// Manual: only `/open-in-idea` probes the IDE. Setup calls this with
|
|
203
303
|
// `probeIde: false` so it never connects until the command asks.
|
|
@@ -206,7 +306,7 @@ export default {
|
|
|
206
306
|
if (!isJetBrains) {
|
|
207
307
|
currentEnv = { env: {}, prependPath: [] };
|
|
208
308
|
await deactivateIde();
|
|
209
|
-
return;
|
|
309
|
+
return false;
|
|
210
310
|
}
|
|
211
311
|
|
|
212
312
|
// Environment: read from the IDE integrated terminal, the single source
|
|
@@ -220,9 +320,13 @@ export default {
|
|
|
220
320
|
shellRegistration = await ctx.shell.hook('create.before', (input) => applyEnv(input, currentEnv));
|
|
221
321
|
}
|
|
222
322
|
|
|
223
|
-
|
|
323
|
+
// A repeated /open-in-idea must still refresh the current server. The
|
|
324
|
+
// previous implementation returned here, leaving the session on its old
|
|
325
|
+
// tool snapshot even though the command appeared successful.
|
|
326
|
+
if (port !== undefined && port === activePort) return refreshIdeTools();
|
|
224
327
|
await deactivateIde();
|
|
225
|
-
if (port
|
|
328
|
+
if (port === undefined) return false;
|
|
329
|
+
return activateIde(port);
|
|
226
330
|
};
|
|
227
331
|
|
|
228
332
|
// Serialize reconciles so overlapping callers can never register/activate
|
|
@@ -237,6 +341,25 @@ export default {
|
|
|
237
341
|
return next;
|
|
238
342
|
};
|
|
239
343
|
|
|
344
|
+
/**
|
|
345
|
+
* `open -a` only requests activation/opening. A cold IDE needs time before
|
|
346
|
+
* its bundled MCP server listens, so poll until the endpoint appears. This
|
|
347
|
+
* is the step that makes a single `/open-in-idea` enough on a cold start.
|
|
348
|
+
*
|
|
349
|
+
* @returns {Promise<number | undefined>}
|
|
350
|
+
*/
|
|
351
|
+
const waitForIdeServer = async () => {
|
|
352
|
+
const deadline = Date.now() + mcpStartTimeoutMs;
|
|
353
|
+
while (Date.now() < deadline) {
|
|
354
|
+
const port = await findIdePort(ports, { timeoutMs: mcpProbeTimeoutMs }).catch(() => undefined);
|
|
355
|
+
if (port !== undefined) return port;
|
|
356
|
+
const remaining = deadline - Date.now();
|
|
357
|
+
if (remaining <= 0) break;
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(MCP_START_POLL_MS, remaining)));
|
|
359
|
+
}
|
|
360
|
+
return undefined;
|
|
361
|
+
};
|
|
362
|
+
|
|
240
363
|
// Re-entrant: repeat invocations share one open request so the IDE is not
|
|
241
364
|
// spawned twice while the application is starting.
|
|
242
365
|
/** @type {Promise<IdeCommandResult> | undefined} */
|
|
@@ -277,9 +400,15 @@ export default {
|
|
|
277
400
|
guard.markSpawn();
|
|
278
401
|
const launched = await openInIde(projectPath, { app: ideApp });
|
|
279
402
|
if (!launched) return { status: 'launch-failed' };
|
|
280
|
-
// `open -a` only requests activation/opening.
|
|
281
|
-
//
|
|
282
|
-
//
|
|
403
|
+
// `open -a` only requests activation/opening. A cold IDE needs time to
|
|
404
|
+
// start its bundled MCP server, so wait for the endpoint instead of
|
|
405
|
+
// reporting failure immediately.
|
|
406
|
+
const started = await waitForIdeServer();
|
|
407
|
+
if (started !== undefined) {
|
|
408
|
+
guard.reset();
|
|
409
|
+
launchNoticeSent = false;
|
|
410
|
+
return { status: 'connected', port: started, unavailableNoticeSent: false };
|
|
411
|
+
}
|
|
283
412
|
await reportUnavailable(true);
|
|
284
413
|
return { status: 'mcp-unavailable', opened: true, unavailableNoticeSent: launchNoticeSent };
|
|
285
414
|
})();
|
|
@@ -328,36 +457,59 @@ export default {
|
|
|
328
457
|
return unavailableNoticeSent;
|
|
329
458
|
},
|
|
330
459
|
});
|
|
331
|
-
const { port
|
|
460
|
+
const { port } = result;
|
|
461
|
+
let toolsReady = false;
|
|
332
462
|
try {
|
|
333
463
|
// Do not repeat the expensive probe when the preflight already
|
|
334
464
|
// found no MCP endpoint. The command will retry after the user
|
|
335
465
|
// enables MCP + Brave Mode.
|
|
336
|
-
await reconcile({ probeIde: port !== undefined });
|
|
466
|
+
toolsReady = (await reconcile({ probeIde: port !== undefined })) === true;
|
|
337
467
|
} catch {
|
|
338
468
|
// capability load is best effort; feedback still matters
|
|
339
469
|
}
|
|
470
|
+
const finalResult = result.status === 'connected' && !toolsReady
|
|
471
|
+
? { ...result, status: 'tools-loading' }
|
|
472
|
+
: result;
|
|
340
473
|
// The unavailable notice is the final result for this command. If
|
|
341
474
|
// it was already delivered, do not show the same warning twice.
|
|
342
|
-
const unavailableAlreadySent = unavailableNoticeSent ||
|
|
343
|
-
if (!unavailableAlreadySent || status !== 'mcp-unavailable') {
|
|
344
|
-
await sendFeedback(input,
|
|
475
|
+
const unavailableAlreadySent = unavailableNoticeSent || result.unavailableNoticeSent;
|
|
476
|
+
if (!unavailableAlreadySent || finalResult.status !== 'mcp-unavailable') {
|
|
477
|
+
await sendFeedback(input, finalResult);
|
|
345
478
|
}
|
|
346
479
|
},
|
|
347
480
|
});
|
|
348
481
|
});
|
|
349
482
|
}
|
|
350
483
|
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
484
|
+
// Every model request re-checks a manually activated IDE. If the endpoint
|
|
485
|
+
// is gone, drop IDEA tools from this request and from the registry so only
|
|
486
|
+
// a new `/open-in-idea` can bring them back. Guidance is appended only for
|
|
487
|
+
// an IDE that is still alive.
|
|
488
|
+
if (typeof ctx.session?.hook === 'function') {
|
|
489
|
+
sessionRegistration = await ctx.session.hook('context', async (event) => {
|
|
490
|
+
const ideActive = activePort !== undefined && activeToolsReady;
|
|
491
|
+
const available = ideActive ? await verifyActiveIde() : false;
|
|
492
|
+
const tools = event?.tools;
|
|
493
|
+
if (!available) removeIdeaToolDefinitions(tools);
|
|
355
494
|
appendIdeRecoveryGuidance(event?.system);
|
|
356
|
-
if (
|
|
495
|
+
if (!injectGuidance) return;
|
|
496
|
+
if (!available) return;
|
|
357
497
|
appendIdeGuidance(event?.system, projectPath);
|
|
358
498
|
});
|
|
359
499
|
}
|
|
360
500
|
|
|
501
|
+
// A failed IDEA MCP call is also a signal that the IDE endpoint is gone.
|
|
502
|
+
// Unregister immediately so later requests cannot keep calling a dead tool.
|
|
503
|
+
if (typeof ctx.tool?.hook === 'function') {
|
|
504
|
+
toolRegistration = await ctx.tool.hook('execute.after', async (event) => {
|
|
505
|
+
if (typeof event?.tool !== 'string' || !event.tool.startsWith('idea_')) return;
|
|
506
|
+
if (event.status !== 'error') return;
|
|
507
|
+
await deactivateIde();
|
|
508
|
+
if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
|
|
509
|
+
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
361
513
|
return async () => {
|
|
362
514
|
disposed = true;
|
|
363
515
|
if (shellRegistration) {
|
|
@@ -381,6 +533,13 @@ export default {
|
|
|
381
533
|
// best effort
|
|
382
534
|
}
|
|
383
535
|
}
|
|
536
|
+
if (toolRegistration) {
|
|
537
|
+
try {
|
|
538
|
+
await toolRegistration.dispose();
|
|
539
|
+
} catch {
|
|
540
|
+
// best effort
|
|
541
|
+
}
|
|
542
|
+
}
|
|
384
543
|
await deactivateIde();
|
|
385
544
|
};
|
|
386
545
|
},
|