opencode-idea 0.0.3 → 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 +12 -0
- package/package.json +1 -2
- package/src/idea-mcp.js +2 -2
- package/src/plugin.js +189 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.0.4
|
|
12
|
+
|
|
13
|
+
- **`/open-in-idea` 刷新工具列表**:在 `ctx.mcp.reload()` 之后显式调用 `ctx.tool.reload()`。之前只有 MCP 层重建目录,已经捕获过工具快照的会话仍然看不到新的 `idea_*` 原生工具;现在下一次模型请求即可用,无需重开会话。
|
|
14
|
+
|
|
3
15
|
## 0.0.3
|
|
4
16
|
|
|
5
17
|
- **直接暴露 IDEA MCP 工具**:注册 MCP 服务器时设置 `codemode: false`,让 `idea_*` 工具直接出现在原生工具表(而非 Code Mode 的 `tools.idea.*`)。模型可以直接调用并看到完整参数 schema,不再需要先探索、也不会猜错参数名。
|
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,13 +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
|
-
* tool
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
166
|
+
* tool?: {
|
|
167
|
+
* list?: () => Promise<ReadonlyArray<{ id?: string }>>,
|
|
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 }>,
|
|
133
174
|
* },
|
|
134
175
|
* mcp: {
|
|
135
176
|
* transform: (cb: (editor: { set: (name: string, config: unknown) => void }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
|
|
@@ -148,6 +189,8 @@ export default {
|
|
|
148
189
|
const injectGuidance = options.injectGuidance !== false;
|
|
149
190
|
const ideApp = resolveIdeApp(options.openInIde);
|
|
150
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);
|
|
151
194
|
// "message" (default) injects a clearly-labelled notification that the
|
|
152
195
|
// model only acknowledges ("收到"); the user sees the clean notice via
|
|
153
196
|
// metadata.displayText. false is silent.
|
|
@@ -156,9 +199,9 @@ export default {
|
|
|
156
199
|
cooldownMs: asPositiveNumber(options.launchCooldownMs, DEFAULT_LAUNCH_COOLDOWN_MS),
|
|
157
200
|
maxAttempts: asPositiveNumber(options.launchMaxAttempts, DEFAULT_LAUNCH_MAX_ATTEMPTS),
|
|
158
201
|
});
|
|
159
|
-
|
|
160
202
|
/** @type {number | undefined} */
|
|
161
203
|
let activePort;
|
|
204
|
+
let activeToolsReady = false;
|
|
162
205
|
let currentEnv = { env: {}, prependPath: [] };
|
|
163
206
|
|
|
164
207
|
/** @type {Array<{ dispose: () => Promise<void> | void }>} */
|
|
@@ -169,10 +212,13 @@ export default {
|
|
|
169
212
|
let sessionRegistration;
|
|
170
213
|
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
171
214
|
let commandRegistration;
|
|
215
|
+
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
216
|
+
let toolRegistration;
|
|
172
217
|
let disposed = false;
|
|
173
218
|
|
|
174
219
|
const deactivateIde = async () => {
|
|
175
220
|
activePort = undefined;
|
|
221
|
+
activeToolsReady = false;
|
|
176
222
|
const current = registrations.slice();
|
|
177
223
|
registrations.length = 0;
|
|
178
224
|
if (current.length === 0) return;
|
|
@@ -185,17 +231,73 @@ export default {
|
|
|
185
231
|
}
|
|
186
232
|
};
|
|
187
233
|
|
|
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;
|
|
251
|
+
if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
|
|
252
|
+
// Reconcile the MCP servers above, then replay the tool registry so the
|
|
253
|
+
// refreshed catalog lands in the next model request's tool snapshot.
|
|
254
|
+
// Without this, a session that already captured a snapshot keeps the old
|
|
255
|
+
// tool list until the session is reopened.
|
|
256
|
+
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
257
|
+
activeToolsReady = await waitForIdeaTools();
|
|
258
|
+
return activeToolsReady;
|
|
259
|
+
};
|
|
260
|
+
|
|
188
261
|
const activateIde = async (port) => {
|
|
189
262
|
const mcpRegistration = await ctx.mcp.transform((editor) => {
|
|
190
263
|
editor.set(IDEA_SERVER_NAME, serverConfig(port, projectPath));
|
|
191
264
|
});
|
|
192
265
|
registrations = [mcpRegistration];
|
|
193
266
|
activePort = port;
|
|
194
|
-
|
|
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;
|
|
195
297
|
};
|
|
196
298
|
|
|
197
299
|
const reconcileOnce = async ({ probeIde = true } = {}) => {
|
|
198
|
-
if (disposed) return;
|
|
300
|
+
if (disposed) return false;
|
|
199
301
|
|
|
200
302
|
// Manual: only `/open-in-idea` probes the IDE. Setup calls this with
|
|
201
303
|
// `probeIde: false` so it never connects until the command asks.
|
|
@@ -204,7 +306,7 @@ export default {
|
|
|
204
306
|
if (!isJetBrains) {
|
|
205
307
|
currentEnv = { env: {}, prependPath: [] };
|
|
206
308
|
await deactivateIde();
|
|
207
|
-
return;
|
|
309
|
+
return false;
|
|
208
310
|
}
|
|
209
311
|
|
|
210
312
|
// Environment: read from the IDE integrated terminal, the single source
|
|
@@ -218,9 +320,13 @@ export default {
|
|
|
218
320
|
shellRegistration = await ctx.shell.hook('create.before', (input) => applyEnv(input, currentEnv));
|
|
219
321
|
}
|
|
220
322
|
|
|
221
|
-
|
|
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();
|
|
222
327
|
await deactivateIde();
|
|
223
|
-
if (port
|
|
328
|
+
if (port === undefined) return false;
|
|
329
|
+
return activateIde(port);
|
|
224
330
|
};
|
|
225
331
|
|
|
226
332
|
// Serialize reconciles so overlapping callers can never register/activate
|
|
@@ -235,6 +341,25 @@ export default {
|
|
|
235
341
|
return next;
|
|
236
342
|
};
|
|
237
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
|
+
|
|
238
363
|
// Re-entrant: repeat invocations share one open request so the IDE is not
|
|
239
364
|
// spawned twice while the application is starting.
|
|
240
365
|
/** @type {Promise<IdeCommandResult> | undefined} */
|
|
@@ -275,9 +400,15 @@ export default {
|
|
|
275
400
|
guard.markSpawn();
|
|
276
401
|
const launched = await openInIde(projectPath, { app: ideApp });
|
|
277
402
|
if (!launched) return { status: 'launch-failed' };
|
|
278
|
-
// `open -a` only requests activation/opening.
|
|
279
|
-
//
|
|
280
|
-
//
|
|
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
|
+
}
|
|
281
412
|
await reportUnavailable(true);
|
|
282
413
|
return { status: 'mcp-unavailable', opened: true, unavailableNoticeSent: launchNoticeSent };
|
|
283
414
|
})();
|
|
@@ -326,36 +457,59 @@ export default {
|
|
|
326
457
|
return unavailableNoticeSent;
|
|
327
458
|
},
|
|
328
459
|
});
|
|
329
|
-
const { port
|
|
460
|
+
const { port } = result;
|
|
461
|
+
let toolsReady = false;
|
|
330
462
|
try {
|
|
331
463
|
// Do not repeat the expensive probe when the preflight already
|
|
332
464
|
// found no MCP endpoint. The command will retry after the user
|
|
333
465
|
// enables MCP + Brave Mode.
|
|
334
|
-
await reconcile({ probeIde: port !== undefined });
|
|
466
|
+
toolsReady = (await reconcile({ probeIde: port !== undefined })) === true;
|
|
335
467
|
} catch {
|
|
336
468
|
// capability load is best effort; feedback still matters
|
|
337
469
|
}
|
|
470
|
+
const finalResult = result.status === 'connected' && !toolsReady
|
|
471
|
+
? { ...result, status: 'tools-loading' }
|
|
472
|
+
: result;
|
|
338
473
|
// The unavailable notice is the final result for this command. If
|
|
339
474
|
// it was already delivered, do not show the same warning twice.
|
|
340
|
-
const unavailableAlreadySent = unavailableNoticeSent ||
|
|
341
|
-
if (!unavailableAlreadySent || status !== 'mcp-unavailable') {
|
|
342
|
-
await sendFeedback(input,
|
|
475
|
+
const unavailableAlreadySent = unavailableNoticeSent || result.unavailableNoticeSent;
|
|
476
|
+
if (!unavailableAlreadySent || finalResult.status !== 'mcp-unavailable') {
|
|
477
|
+
await sendFeedback(input, finalResult);
|
|
343
478
|
}
|
|
344
479
|
},
|
|
345
480
|
});
|
|
346
481
|
});
|
|
347
482
|
}
|
|
348
483
|
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
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);
|
|
353
494
|
appendIdeRecoveryGuidance(event?.system);
|
|
354
|
-
if (
|
|
495
|
+
if (!injectGuidance) return;
|
|
496
|
+
if (!available) return;
|
|
355
497
|
appendIdeGuidance(event?.system, projectPath);
|
|
356
498
|
});
|
|
357
499
|
}
|
|
358
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
|
+
|
|
359
513
|
return async () => {
|
|
360
514
|
disposed = true;
|
|
361
515
|
if (shellRegistration) {
|
|
@@ -379,6 +533,13 @@ export default {
|
|
|
379
533
|
// best effort
|
|
380
534
|
}
|
|
381
535
|
}
|
|
536
|
+
if (toolRegistration) {
|
|
537
|
+
try {
|
|
538
|
+
await toolRegistration.dispose();
|
|
539
|
+
} catch {
|
|
540
|
+
// best effort
|
|
541
|
+
}
|
|
542
|
+
}
|
|
382
543
|
await deactivateIde();
|
|
383
544
|
};
|
|
384
545
|
},
|