opencode-idea 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -138,7 +138,7 @@ configure_ide_project({ path: "modelserver", ide: "pycharm" }) // 配置一个
138
138
 
139
139
  ## 按能力选工具 (直接调用引导)
140
140
 
141
- IDE MCP 可用时,插件通过 **系统提示注入**告诉模型 **哪些事该走 IDE**;除下面「工具参数」那一处刻意的例外
141
+ IDE MCP 可用时,插件通过 **系统提示注入**告诉模型 **哪些事该走 IDE**;除下面「工具参数与输出」那几处刻意的接管
142
142
  (只做减法:从 schema 里删掉一个参数),不修改任何工具 (原生或 IDEA) 的定义、描述或可用性:
143
143
 
144
144
  1. **系统提示注入**:通过 `ctx.session.hook("context", …)` 往每次请求的 system 里追加一段引导。
@@ -224,7 +224,7 @@ IDE MCP 可用时,插件通过 **系统提示注入**告诉模型 **哪些事该
224
224
  所以引导里写了:返回的 `usages` 数比预期少,就是有引用没被解析到 —— **先确认 IDE 索引到那个模块**,
225
225
  别拿文本替换去"补"漏掉的引用。
226
226
 
227
- ## 工具参数 (schema 收敛 + `execute.before`)
227
+ ## 工具参数与输出 (插件接管的几处)
228
228
 
229
229
  有些 IDEA 工具**不传某个参数就会回答一个你没问的问题**。这种参数由本插件完全接管,所以要**两半一起做** ——
230
230
  只做一半都是错的:
@@ -248,6 +248,18 @@ IDE MCP 可用时,插件通过 **系统提示注入**告诉模型 **哪些事该
248
248
  schema 变换**绝不能**用来加建议,那是引导文本的职责。
249
249
  - 只对**点名的工具**生效,其余工具的参数一字不改。两半都**不依赖项目配置**。
250
250
 
251
+ ### 另两处只"抬下限 / 补提示",不固定值
252
+
253
+ `list_directory_tree` 有两处接管,都不用固定值 —— 模型仍然需要传 `maxDepth`,所以 schema 不删它:
254
+
255
+ - **调用前**(`RAISED_ARGUMENTS`):`maxDepth` 抬到至少 **50**,更大的值照用。它是绝对深度上限,却只展开
256
+ `maxDepth-1` 层,`6` 正好断在 Java 包目录上;而输出到边界时没有任何标记,一棵被切的树和空目录长得一模一样。
257
+ - **返回后**(`appendDirectoryTreeTruncationNotice`):树顶到边界时在结果末尾**追加**一句截断提示,原内容不动。
258
+ 起因是一次真实会话:模型把截断后的空包目录读成"工具看不到",整条查询链退回 `shell` 里的 `find` / `grep`,从那
259
+ 以后丢掉 `.gitignore` 规则。
260
+
261
+ 前者不依赖模型知道这个语义,后者不依赖模型读提示词 —— 两边都是"即使模型不配合也不会被骗"。
262
+
251
263
  ## Run Configuration 全套 (配套插件 `idea-mcp-enhance`)
252
264
 
253
265
  IDE 内置的 MCP Server 只能 **启动已有**的 run configuration (`execute_run_configuration`), **不能新建、不能停止、不能重启、
@@ -585,7 +597,7 @@ export { default } from "/Users/yutao/IdeaProjects/opencode-idea/src/index.js";
585
597
  IDE MCP 没有「打开项目」工具,因此打开动作走 OS/CLI,而非 MCP。
586
598
  - 插件热重载会断开 IDE 连接,需重跑 `/open-in-ide`。
587
599
  - IDE 工具以 `<项目前缀>_*` 直接暴露给模型(`codemode: false`),不用 `execute` 包裹;
588
- 插件不修改任何**它不接管**的工具定义或描述,唯一的例外是 `rebuild` 参数(见「工具参数」)。
600
+ 插件不修改任何**它不接管**的工具定义或描述,唯一的例外是 `rebuild` 参数(见「工具参数与输出」)。
589
601
 
590
602
  ## License
591
603
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-idea",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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,6 +14,7 @@
14
14
  "src/mcp/idea.js",
15
15
  "src/mcp/enhance.js",
16
16
  "src/mcp/idea-tool-defaults.js",
17
+ "src/mcp/idea-tool-output.js",
17
18
  "src/project.js",
18
19
  "src/ide-launcher.js",
19
20
  "src/ide-guidance.js",
@@ -1,5 +1,16 @@
1
1
  # IDE MCP:OpenCode 自带工具的补充
2
2
 
3
+ ## 工具使用铁律(先读)
4
+
5
+ 1. **读、搜、列、改都用 OpenCode 自带工具**:读文件用 `read`(读片段传 `offset` / `limit`),搜内容用 `grep`,
6
+ 列文件用 `glob`,改文件用 `edit` / `write`。
7
+ 2. **IDE 工具补自带工具做不到的**:符号与定义用 `<前缀>_search_symbol` + `<前缀>_read_file`;改名用
8
+ `<前缀>_rename_refactoring`;单文件检查用 `<前缀>_lint_files` / `<前缀>_get_file_problems`;跨模块构建用
9
+ `<前缀>_build_project`。
10
+ 3. **`shell` 用于必须起进程的命令**:`pnpm` / `git` / `docker` / `curl` / `psql`、迁移与生成。
11
+ 4. **校验节奏**:改动过程中对改动的文件用 `lint_files` / `get_file_problems`;全仓 `pnpm typecheck` 在收尾跑一次。
12
+ 5. **批量修改**:逐文件用 `edit`,或一次用 `<前缀>_apply_patch`。
13
+
3
14
  本会话接入了 {{projectCount}} 个 JetBrains 项目。**每个功能在所有项目下都各有一份工具**,命令名是
4
15
  `<项目前缀>_<工具名>`;下文写 `<前缀>` 就是指这一截。
5
16
 
@@ -10,44 +21,47 @@
10
21
  **子项目就是根目录下的目录**,只是被另一个 IDE 单独打开、配了另一套工具。所以这里说的"根项目",指的是
11
22
  **根目录中扣掉这些子项目之后的部分**。
12
23
 
13
- **规则:操作哪个目录下的文件/代码,就用那个目录对应的命令。** IDE 是按项目解析的 —— 跨项目调用要么直接报错,
14
- 要么被解析到另一个项目上、作用在错误的代码上。
24
+ **规则:操作哪个目录下的文件/代码,就用那个目录对应的命令。** IDE 按项目解析 —— 用错前缀时,调用要么报错,
25
+ 要么作用在另一个项目的代码上。
15
26
 
16
27
  按需读文件:要了解某个子项目自己的约定,读它的 `<子项目目录>/AGENTS.md`(它不会自动进入本会话的上下文,
17
28
  本会话是 root 打开的那一个)。
18
29
 
19
30
  ## 路由:先看路径在不在项目里
20
31
 
21
- **IDE 工具只在项目内有意义。** 请求的路径必须落在**某个已打开项目**的目录树内,IDE 才会去解析它;
22
- 项目外的路径(`/tmp`、别的仓库、用户主目录)两个 IDE 都会直接报错。
32
+ **IDE 工具只在项目内使用。** 请求的路径落在**某个已打开项目**的目录树内,IDE 才会去解析它;
33
+ 项目外的路径(`/tmp`、别的仓库、用户主目录)不在 IDE 的解析范围内。
23
34
 
24
35
  - **项目内** —— **OpenCode 自带的工具为主**,它们更快。**它们做不了、或者做得很差的,才用 IDE 工具**:
25
36
  符号检索、依赖实现、重构、格式化、校验、运行配置、跨文件补丁。
26
37
  **两边都不顺手时,你自己判断用 `shell` 里的命令** —— 那是第三选择,不是第一选择。
27
- - **项目外** —— 只有 OpenCode 自带的工具。IDE 工具在这里不参与(会报错),别去试。
38
+ - **项目外** —— 用 OpenCode 自带的工具;IDE 工具只对项目内路径生效。
28
39
 
29
40
  ## 总则 (所有项目通用)
30
41
 
31
42
  - **OpenCode 自带的工具直接调用** —— 工具列表里直接列出的那些(`read` / `grep` / `glob` / `edit` / `write` / `shell`),
32
- 参数按各自 schema 传。不要包进 `execute`,也不要用 Code Mode 的 `search` 去找(它们不在那份目录里,搜不到)。
33
- - **读文件用 `read` 工具** —— 要读某一段就传 `offset`(从第几行起)+ `limit`(读几行),别用 `shell` 里的 `sed -n`。
43
+ 参数按各自 schema 传。它们就在当前工具表里,直接调用即可。
44
+ - **读文件用 `read` 工具** —— 要读某一段就传 `offset`(从第几行起)+ `limit`(读几行),输出自带行号。
34
45
  - **搜文本用 `grep` 工具** —— 它按 `.gitignore` 排除,`node_modules` / `dist` 不进结果,范围用 `path` 收窄。
35
- 别用 `shell` 里的 `grep` / `rg` / `find`:那些不看 `.gitignore`,还跟着符号链接扫进依赖包,慢且全是噪音。
46
+ 要看命中行前后的大段上下文:先用它拿到**行号**,再用 `read` + `offset` / `limit` 读那一段。
47
+ - **工具输出不够用时,按这个顺序调整**:① 调参数重试 → ② 缩小路径范围重试 → ③ 换工具。
48
+ 换用 `shell` 的判断依据是"这个工具确实没有这个能力"。
49
+ 典型:`<前缀>_list_directory_tree` 的输出到 `maxDepth` 边界就停 —— 调大 `maxDepth` 再试一次。
36
50
  - **改文件**:改一个符号用 `<前缀>_rename_refactoring`;改一段文字用 `edit` / `write` 工具;
37
51
  同一段文字铺到多个文件用 `<前缀>_apply_patch`;跑命令用 `shell` 工具。
38
- **别在 `shell` 里写脚本改代码** —— 脚本会把没让改的地方也重排掉,还可能覆盖掉别人的改动。
52
+ 改代码始终落在具体的那几行上,其余内容保持原样。
39
53
  - **改完代码自己先查一遍**:`lint_files` / `build_project`,怎么挑见「校验」。
40
- - **文档类文件 (Markdown / 文档) 改完之后,用 `<前缀>_reformat_file` 过一遍** —— 表格对齐、换行宽度、缩进都交给 IDE
41
- 统一,别手搓空格。它从磁盘重读,外部写完直接格式化是安全的。
42
- - 某条功能下面没有你正要操作的那个项目的命令,就是那个项目没接上:提示用户执行 `/open-in-ide`,不要重试,也不要去别处找。
54
+ - **文档类文件 (Markdown / 文档) 改完之后,用 `<前缀>_reformat_file` 过一遍** —— 表格对齐、换行宽度、缩进交给 IDE
55
+ 统一。它从磁盘重读,外部写完直接格式化是安全的。
56
+ - 某条功能下面没有你正要操作的那个项目的命令,说明那个项目没接上:提示用户执行 `/open-in-ide`。
43
57
 
44
58
  ## 命令执行 (所有项目通用)
45
59
 
46
60
  - **跑命令用 `shell` 工具**;要项目环境的(IDE 里配好的 JDK / Node / Python 等)先看有没有现成的 run configuration ——
47
- 有就跑它 (常驻用 `rerun`,跑完即退用 `execute`);没有、又只是一次性的,用 `shell` 工具并显式指定解释器,
48
- **不要为它新建配置**。
61
+ 有就跑它 (常驻用 `rerun`,跑完即退用 `execute`);没有、又只是一次性的,用 `shell` 工具并显式指定解释器。
62
+ 一次性的命令留在 `shell` 里,配置留给会反复用到的流程。
49
63
  - **建 / 改 / 删运行配置,走 `idea-run-config-manager` skill**(服务化应用的配置归 `idea-standalone`):
50
- 它落手用的就是「运行配置」里的那组工具。 **不要自己写文件、也不要自己删文件**。
64
+ 它落手用的就是「运行配置」里的那组工具 —— 配置文件由工具写入和删除。
51
65
  {{#if canCreateRunConfiguration}}
52
66
  {{else}}
53
67
  本会话没有新建工具,"建"这一步它只能讲清配置名、实际执行的命令、工作目录、为什么值得留, **请用户在 IDE 里创建**
@@ -59,7 +73,7 @@
59
73
 
60
74
  **要读任何类 / 方法 / 字段的源码,就两步**:`<前缀>_search_symbol`(`q` 传名字, **带 `include_external: true`**)找到位置 →
61
75
  `<前缀>_read_file` 原样读它给的路径。 **依赖库 / 三方包 / 平台 jar 里的实现**正是它最该用的场合 —— IDE 索引里那份就是项目
62
- 编译时真正用的依赖 (`javap` / 解包 jar / 用 `shell` 里的 `grep` 按 jar 路径拼出来的往往是另一份,版本对不上),读出来还是反编译源码。
76
+ 编译时真正用的依赖,读出来还是反编译源码。
63
77
 
64
78
  - {{cmd_search_symbol}} **`q` 传查询文本,固定带上 `include_external: true`。** **本插件最不可替代的能力**在于它搜的是**符号**
65
79
  (类 / 接口 / 方法 / 函数 / 字段 / 类型 / 常量这类 **有名字的定义**)的**声明位置**,不是"哪段文本提到过它"(那是
@@ -70,10 +84,10 @@
70
84
  (`fastapi` → 入口文件就是导出清单),再读它的入口。
71
85
  - **Node/TS 例外**:带 `-` 或 `@scope/` 的包名这么拼是 0 条(`element-plus`);改用 import 的符号名,并用 `paths` 圈到
72
86
  那个 `node_modules`(`paths: ['frontend/user/**']` 就收敛到一处)。
73
- - **搜不到 = 依赖没进来**:先把它加进依赖,或改用 web 搜索 / GitHub 看源码;别反复换写法试。
74
- - **同名符号往往有多处定义**(标准库 / 依赖 / 测试各一份):按调用处的 import 与参数类型选,别拿第一行当答案。
75
- - **它返回的路径原样交给同一个项目的 `<前缀>_read_file`** —— 那个路径的基准是**它所在的项目**,不是会话工作目录,
76
- **别交给 `read` 工具**:子项目的相对路径会落到错的位置。
87
+ - **搜不到 = 依赖没进来**:先把它加进依赖,或改用 web 搜索 / GitHub 看源码。
88
+ - **同名符号往往有多处定义**(标准库 / 依赖 / 测试各一份):按调用处的 import 与参数类型来选。
89
+ - **它返回的路径原样交给同一个项目的 `<前缀>_read_file`** —— 那个路径的基准是**它所在的项目**,不是会话工作目录;
90
+ 交回 `read` 工具时,子项目的相对路径会落到另一个位置。
77
91
  - **依赖索引是按项目的**:用 **它所在项目** 前缀的工具搜,拿别的项目的工具搜不到库。
78
92
  - {{cmd_get_symbol_info}} **一个符号的签名 / 文档 / 声明位置**:`filePath` + `line` + `column`。
79
93
  - {{cmd_analyze_calls}} **调用关系**:`symbolFqn` 传符号名,`analysisKind` 取 `INCOMING_CALLS` / `OUTGOING_CALLS`;`depth`
@@ -82,6 +96,10 @@
82
96
  ### 项目与编辑器
83
97
 
84
98
  - {{cmd_list_directory_tree}} **项目视角的目录树**(尊重内容根与排除项):`maxDepth` 控深度。
99
+ **它是绝对深度上限,实际只展开到 `maxDepth-1` 层;省略时默认 5(只展开 4 层)。** 要看全就传一个明显够大的值
100
+ (如 50),不会报错、也没有额外代价。**输出到边界就停,不带任何截断标记** —— 所以"某个目录后面没有子项"有两种
101
+ 可能:真空目录,或者被你截断了。判据:树的深度正好顶到你传的值,或者**包目录 / 源码目录显示为空** —— 那就是被截断了。
102
+ **把 `maxDepth` 调大重试,或把 `directoryPath` 直接指到那个子目录再看一层。**
85
103
  - {{cmd_get_project_modules}} **项目模块及类型**。
86
104
  - {{cmd_get_project_dependencies}} **项目直接声明的依赖**。
87
105
  - {{cmd_get_all_open_file_paths}} **用户在 IDE 里打开了哪些文件**(含当前激活的那个)。
@@ -90,7 +108,7 @@
90
108
  ### 读文件
91
109
 
92
110
  - {{cmd_read_file}} **按项目解析路径读文件** —— 普通文件用 `read` 工具就够,它只在三种情况下用得着:
93
- `search_symbol` 返回的路径(它的基准是那个项目,不是会话工作目录);**压缩包内的条目**(`read` 工具打不开压缩包,
111
+ `search_symbol` 返回的路径(它的基准是那个项目,不是会话工作目录);**压缩包内的条目**(`read` 工具读不到压缩包内部,
94
112
  而它还能把 `.class` **反编译成源码**);路径按那个项目解析更省事的时候。
95
113
  压缩包路径两种写法:`<路径>.jar!/<条目>`(相对或绝对都行)或 `jar://<**绝对**路径>.jar!/<条目>`(`jar://` 只认绝对路径)。
96
114
 
@@ -98,42 +116,42 @@
98
116
 
99
117
  - {{cmd_rename_refactoring}} **改一个符号(变量 / 函数 / 类 / 字段 / 方法),哪怕散落在几十个文件里,优先用它** ——
100
118
  它按**语义**更新所有**真引用**:跨文件一起改,类名连**文件名**一起改。
101
- - **它只改代码**:注释、字符串字面量、别的语言的同名符号原样不动 —— 拿 `edit` / `apply_patch` 工具去改名,
102
- 会把这些一起改掉,那是改了程序的输出。
119
+ - **它只改代码**:注释、字符串字面量、别的语言的同名符号原样不动 —— 名字只落在真正的引用上。
103
120
  - 返回里 `with N usages` 的 N 是**真引用数**;比预期少,说明有引用没被解析到(文件没进 IDE 索引),
104
- 这时**先确认索引到那个模块**,别拿 `edit` 工具去"补"。
105
- - 反过来也有该用 `edit` / `write` / `apply_patch` 工具的场合:改的本来就是文字(配置 key、模板占位符、文档、SQL)、
106
- **你就是想改那句字符串**、或者那个文件 IDE 根本没索引(生成物、vendor 目录)。这时先用 `grep` 工具确认命中范围。
121
+ 这时**先确认索引到那个模块**,再处理剩下的引用。
122
+ - 改的本来就是文字(配置 key、模板占位符、文档、SQL)、**就是要改那句字符串**、或者文件 IDE 没索引(生成物、
123
+ vendor 目录)时,用 `edit` / `write` / `apply_patch`。动手前先用 `grep` 工具确认命中范围。
107
124
  - {{cmd_reformat_file}} **按 IDE 的代码风格重排**。
108
125
  - {{cmd_apply_patch}} **一次改多个文件**:`input` 传补丁正文,Codex 格式(`*** Begin Patch` / `*** Update File: <路径>`
109
126
  / `@@` / `-` 旧行 / `+` 新行)与 unified git diff 都收。`edit` / `write` 工具都只管一个文件。
110
127
  - 它**按文本**改,适合同一段文字铺到多个文件;**改符号走上面那条**。
111
- - 它**只碰补丁里写明的那几行**,不会重排你没让改的地方。
128
+ - 它**只碰补丁里写明的那几行**,其余内容保持原样。
112
129
  - 上下文对不上时那一个 hunk 失败、**该文件原样不动**,返回里点名是哪个文件(`<路径>: Hunk context not found`)。
113
130
  - 多个文件是**逐个应用**的:返回里 `N out of M operations applied`,失败的那些读一下再改;
114
- 改完用 `grep` 工具确认落盘,别只信成功数。
131
+ 改完用 `grep` 工具确认落盘。
115
132
  - {{cmd_lint_files}} **批量检查**:`files` 传数组;只列有问题的文件。
116
133
  - {{cmd_get_file_problems}} **单文件检查**:`filePath`;结果在 `errors`。
117
134
  - {{cmd_build_project}} **构建校验**:`filesToRebuild` 可只构建指定文件;有问题返回在 `problems`,没返回即没问题。
118
135
 
119
136
  **改完代码,自己先查一遍再收尾。** 查一次很便宜,而问题一旦漏到用户看见、或留到构建 / 测试炸了才暴露,回头纠正的
120
- 代价就高一个量级。 **别把校验整段省掉。** 但也不是每步都查 —— 查太勤和压根不查一样是错的,而"这一轮属于哪一种"只有你自己知道。
137
+ 代价就高一个量级。 校验跟着改动范围走 —— 这一轮属于"改一大片"还是"改一处",见下面两条。
121
138
 
122
- - **这次要改一大片**(多文件、跨模块、"顺手把这几处一起对齐")→ 全部改完再 `build_project` 一次。中途不查:
123
- 半成品状态下的检查结果只会误导你。
139
+ - **这次要改一大片**(多文件、跨模块、"顺手把这几处一起对齐")→ 全部改完再 `build_project` 一次,
140
+ 在完整状态上检查。
124
141
  - **这次只改一处,或在同一个文件上反复微调** → 告一段落时,把这轮改过的文件一次 `lint_files` 掉;要看单个
125
142
  文件的细节再上 `get_file_problems`。
126
143
  - **改了签名 / 接口 / import / 依赖**这类"别处会跟着坏"的地方 → 走 `build_project`;只动了文件内部的实现 →
127
144
  lint 就够。
128
- - 查出来的问题 **改掉,或说清为什么不改**,别把没处理的问题原样交给用户。
129
- - 校验走上面这些工具, **不要用 `shell` 里临时编一遍代替**;也别先猜"索引还没同步 / 那个环境是另一个进程"就绕开 ——
130
- 先调一次,它真看不到会明说。
145
+ - 查出来的问题 **改掉,或说清为什么不改**。
146
+ - 校验走上面这些工具;先调用一次,索引没同步或环境不对时它会明说。
147
+ - **全仓类型检查是收尾动作,不是编辑循环**:同一任务只在最后跑一次 `pnpm typecheck`;过程中只对改动文件跑
148
+ `lint_files` / `get_file_problems`。局部检查用这两个工具,全仓 typecheck 留给收尾。
149
+ - 只有改依赖清单、生成 Prisma Client、跑迁移这类必须执行命令的动作,才额外调用 `pnpm`;一个任务通常不超过两次。
131
150
 
132
151
  ### 运行配置
133
152
 
134
153
  - {{cmd_get_run_configurations}} **项目里有哪些运行配置**:传 `filePath` 可列该文件的运行入口。
135
- **别去项目目录里翻配置** ——
136
- 存在 IDE 自己存储里 (不在项目目录)的那些,翻文件是看不到的。
154
+ 配置由 IDE 自己存储(通常在项目目录之外),用这个工具读。
137
155
  - {{cmd_execute_run_configuration}} **跑已有配置**:配置里的 `EXECUTE_IN_TERMINAL` 要是 `false`(共享出去的配置都这样)
138
156
  ;覆盖参数 (`programArguments` / `workingDirectory` / `envs`)需配置支持 `supportsDynamicLaunchOverrides`,否则会直接报错。
139
157
  {{#if canCreateRunConfiguration}}
@@ -143,8 +161,8 @@
143
161
  **要不要建、建什么、怎么建、档案怎么记,全按上面那个 skill 走。** 判据只有一条: **这件事以后会不会以同样的形态再发生?**
144
162
  会 (改表后同步 DDL、跑迁移、生成代码、打包、起 dev server、往某个环境部署这类)就 **主动提**一次
145
163
  "要不要固化成 run configuration",并把配置名 / 类型 / 命令 / 工作目录 / 为什么值得留摊开;不会 (查条数据、看个日志、
146
- 试一条命令)就用 `shell` 工具跑完算,别给项目留垃圾。
147
- **改完别忘了同步档案**:新增 / 删除 / 改名之后按 skill 更新 `AGENTS.md` 里的档案块 —— 档案和项目里的配置对不上,比没有档案更糟。
164
+ 试一条命令)就用 `shell` 工具跑完算。
165
+ **改完同步档案**:新增 / 删除 / 改名之后按 skill 更新 `AGENTS.md` 里的档案块 —— 档案和项目里的配置对不上,比没有档案更糟。
148
166
  {{/if}}
149
167
  {{#if canInspectRunConfiguration}}
150
168
  - {{cmd_inspect_run_configuration}} **看一个配置的全貌**:定义 (options / envs / before-launch)、命名解析 (`win` /
@@ -168,23 +186,22 @@
168
186
  {{#if canRerunRunConfiguration}}
169
187
  - {{cmd_rerun_run_configuration}} **启动 / 重启常驻进程**:在跑就重启、没在跑就启动 —— 改完代码要重启就直接用它,
170
188
  有没有热重载都一样;读 `started` 判断新进程是否真的起来了。这次运行的输出在 `output`,完整日志文件路径在
171
- `fullOutputPath`。不要用 `pkill` / `kill` / `shell` 代替。
189
+ `fullOutputPath`。启停都走这个工具,IDE 的 Run 窗口状态才会跟着更新。
172
190
  {{/if}}
173
191
 
174
192
  **配置名怎么读**:`[os@]base[@env]`,`@` 分割、最多三段 —— `os` = `win`(缺省 macOS / Linux)、`base` = 配置本体(必填)、
175
193
  `env` = 环境名(缺省 = 默认环境)。`win@base`(换平台)与 `base@env`(换环境)都是 `base` 的 **变体**,跑哪一个:
176
194
 
177
- - **平台**:macOS / Linux 用 `base`,Windows 用 `win@base` —— `win@base` 不存在就 **交回用户**,别拿 base 顶替。
195
+ - **平台**:macOS / Linux 用 `base`,Windows 用 `win@base` —— `win@base` 不存在时 **交回用户**。
178
196
  - **环境**:不带 `@` 的就是 **默认环境**。一批一起起时 **逐条解析**:有 `base@<要的环境>` 就用它,没有就用默认;只有 `@env`
179
197
  变体、没有默认版的,就用它自己那个 —— "10 个服务"可以是"3 个 `@dev` + 7 个默认",不必逐条问。
180
- - 一条配置同时有 `base` 与 `base@<env>`、又没说走哪个环境 → **问一次走哪个环境**,选定后本会话默认走这套,别反复问。
198
+ - 一条配置同时有 `base` 与 `base@<env>`、又没说走哪个环境 → **问一次走哪个环境**,选定后本会话默认走这套。
181
199
  - 一个环境怎么接上(启动参数 / 环境变量 / 配置文件)是项目自己的事:照现成的同类变体来,或问用户。
182
200
  {{#if canListDeploymentServers}}
183
201
 
184
202
  ### 服务器文件同步
185
203
 
186
- 往服务器传文件 / 从服务器取文件,走这一组 —— **别用 `ssh` / `rsync` / `scp` 自己拼一份**:IDE 的连接、路径映射、排除规则都在它们后面,自己拼的不是
187
- IDE 在管的那一份。
204
+ 往服务器传文件 / 从服务器取文件,走这一组 —— IDE 的连接、路径映射、排除规则都在它们后面,传的就是 IDE 在管的那一份。
188
205
 
189
206
  - {{cmd_list_deployment_servers}} **本项目能用的部署服务器**:名称 (后面几条用它当 `serverName`)
190
207
  、项目级还是应用级、协议、host / port、在本项目里的路径映射,以及 **项目级**的部署选项与危险组合告警。 **只读 IDE
@@ -202,5 +219,5 @@ IDE 在管的那一份。
202
219
  {{/if}}
203
220
 
204
221
  上传 / 下载都在 IDE 后台跑,单次调用只等到 `waitMs`(默认 10 秒)为止;返回 `in_progress` 是 **正常**的,不是失败 —— 拿
205
- `handle` 去查进度, **别用重复调上传 / 下载代替**(那样拿不到进度)。
222
+ `handle` 去查进度;重复调用上传 / 下载会再起一次传输。
206
223
  {{/if}}
@@ -100,6 +100,56 @@ export function applyIdeToolDefaults(tool, input, prefixes) {
100
100
  return changed;
101
101
  }
102
102
 
103
+ /**
104
+ * Arguments this plugin only ever RAISES to a floor, never pins.
105
+ *
106
+ * 和 [`FORCED_ARGUMENTS`] 的区别是**单向**:调用方给了更大的值就照用,只把"太小或没给"抬到下限。
107
+ * 所以参数必须继续留在 schema 里 —— 模型仍然需要它,`hideForcedArguments` 也不碰它。
108
+ *
109
+ * `list_directory_tree` 的 `maxDepth` 是**绝对深度上限**,但实际只展开到 `maxDepth-1` 层,省略时默认 5
110
+ * (只展开 4 层)。实测(2026-10,真实 MCP 调用):`maxDepth=6` 时 Java 包树正好断在包目录那一层,而输出里
111
+ * 没有任何截断标记 —— 一棵被切的树和一个空目录长得一模一样,模型据此判定"工具看不到这个目录",
112
+ * 整条查询链退回 `shell` 里的 `find` / `grep` / `sed`,从此丢掉 `.gitignore` 规则。抬到 50 是实测
113
+ * 无副作用的量级(同一目录 50 与 8 输出完全一致)。
114
+ *
115
+ * @type {ReadonlyArray<{ suffix: string, argument: string, minimum: number }>}
116
+ */
117
+ export const RAISED_ARGUMENTS = [
118
+ {
119
+ suffix: 'list_directory_tree',
120
+ argument: 'maxDepth',
121
+ minimum: 50,
122
+ },
123
+ ];
124
+
125
+ /**
126
+ * Raise a floor argument on one call. Never lowers, never pins.
127
+ *
128
+ * Mutates `input` in place and reports whether anything changed. A non-numeric
129
+ * value counts as "not given": this function cannot vouch for what a string
130
+ * `"6"` means, so it replaces it with the floor rather than trusting it.
131
+ *
132
+ * @param {unknown} tool Effective tool name for this call (`event.tool`).
133
+ * @param {unknown} input Mutable argument draft for this call (`event.input`).
134
+ * @param {readonly string[]} prefixes 本会话所有项目的工具前缀(`ide_` 等)。
135
+ * @returns {boolean} Whether `input` was modified.
136
+ */
137
+ export function raiseMinimumArguments(tool, input, prefixes) {
138
+ if (typeof tool !== 'string') return false;
139
+ if (input === null || typeof input !== 'object') return false;
140
+
141
+ let changed = false;
142
+ for (const raised of RAISED_ARGUMENTS) {
143
+ const matches = prefixes.some((prefix) => tool === `${prefix}${raised.suffix}`);
144
+ if (!matches) continue;
145
+ const current = input[raised.argument];
146
+ if (typeof current === 'number' && Number.isFinite(current) && current >= raised.minimum) continue;
147
+ input[raised.argument] = raised.minimum;
148
+ changed = true;
149
+ }
150
+ return changed;
151
+ }
152
+
103
153
  /**
104
154
  * Drop one property from a JSON schema, returning a new object.
105
155
  *
@@ -0,0 +1,118 @@
1
+ // IDE 工具的**输出**增强。
2
+ //
3
+ // 和 `idea-tool-defaults.js` 分开:那个管"调用前要传什么",这个管"回来以后怎么读"。
4
+ //
5
+ // `list_directory_tree` 的输出在到达 maxDepth 边界时会**静默停止** —— 没有 `…`、没有"已达深度上限",
6
+ // 一棵被截断的树和一个空目录在文本里长得一模一样。实测(2026-10):模型把 `maxDepth=6` 截断后的空包目录
7
+ // 读成"工具看不到",随后整条查询链退回 `shell` 里的 `find` / `grep` / `sed`。
8
+ //
9
+ // 参数侧(`RAISED_ARGUMENTS`)已经把下限抬到 50,这里再加一道**看得见**的提示:只要树的深度顶到了上限,
10
+ // 就在结果后面追加一句,让"被截断"和"真的空"不再同形。提示是追加的,不改原有内容。
11
+
12
+ /** ` ├── name` / ` │ └── name`:每一层固定占 4 个字符(空格或竖线)。 */
13
+ const TREE_ENTRY = /^([\s│]*)(?:├──|└──)\s/;
14
+
15
+ const LIST_DIRECTORY_TREE_SUFFIX = 'list_directory_tree';
16
+
17
+ /** 与 `RAISED_ARGUMENTS` 的下限一致:拿不到输入时按这个判断。 */
18
+ const DEFAULT_MAX_DEPTH = 50;
19
+
20
+ /**
21
+ * @param {unknown} input
22
+ * @returns {number}
23
+ */
24
+ function readMaxDepth(input) {
25
+ const raw = input !== null && typeof input === 'object' ? input.maxDepth : undefined;
26
+ return typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_DEPTH;
27
+ }
28
+
29
+ /**
30
+ * 从 MCP 的 text 里取出树本身;不是那个 JSON 就整段当树。
31
+ *
32
+ * @param {string} text
33
+ * @returns {string}
34
+ */
35
+ function treeText(text) {
36
+ try {
37
+ const parsed = JSON.parse(text);
38
+ if (parsed !== null && typeof parsed === 'object' && typeof parsed.tree === 'string') {
39
+ return parsed.tree;
40
+ }
41
+ } catch {
42
+ // 不是 JSON:按纯文本树处理
43
+ }
44
+ return text;
45
+ }
46
+
47
+ /**
48
+ * 最深的条目在第几层(根目录下的直接子项是第 1 层)。
49
+ *
50
+ * @param {string} text
51
+ * @returns {number}
52
+ */
53
+ function deepestLevel(text) {
54
+ let deepest = 0;
55
+ for (const line of text.split('\n')) {
56
+ const match = TREE_ENTRY.exec(line);
57
+ if (!match) continue;
58
+ const level = Math.floor(match[1].length / 4);
59
+ if (level > deepest) deepest = level;
60
+ }
61
+ return deepest;
62
+ }
63
+
64
+ /**
65
+ * 把一段提示接到结果的文本后面,不改原有内容。
66
+ *
67
+ * `Tool.Result.content` 允许 `string` 或 `Content[]`,两种都要覆盖。
68
+ *
69
+ * @param {{ content?: unknown }} result
70
+ * @param {string} text
71
+ * @returns {unknown} 新结果;格式不认识时返回 `undefined`。
72
+ */
73
+ function appendText(result, text) {
74
+ const content = result.content;
75
+ if (typeof content === 'string') {
76
+ return { ...result, content: `${content}\n\n${text}` };
77
+ }
78
+ if (Array.isArray(content)) {
79
+ return { ...result, content: [...content, { type: 'text', text }] };
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ /**
85
+ * 如果这棵目录树顶到了 `maxDepth` 的边界,追加一句截断提示。
86
+ *
87
+ * @param {unknown} result `Tool.Result`(只读,不改原对象)
88
+ * @param {{ tool: string, input: unknown, prefixes: readonly string[] }} call
89
+ * @returns {unknown} 新的 result;不该动时返回**原引用**,调用方据此跳过改写。
90
+ */
91
+ export function appendDirectoryTreeTruncationNotice(result, { tool, input, prefixes }) {
92
+ if (typeof tool !== 'string') return result;
93
+ if (!prefixes.some((prefix) => tool === `${prefix}${LIST_DIRECTORY_TREE_SUFFIX}`)) return result;
94
+ if (result === null || typeof result !== 'object') return result;
95
+
96
+ const maxDepth = readMaxDepth(input);
97
+ const content = result.content;
98
+ const texts =
99
+ typeof content === 'string'
100
+ ? [content]
101
+ : Array.isArray(content)
102
+ ? content
103
+ .filter((item) => item?.type === 'text' && typeof item.text === 'string')
104
+ .map((item) => item.text)
105
+ : [];
106
+ if (texts.length === 0) return result;
107
+
108
+ const deepest = Math.max(...texts.map((text) => deepestLevel(treeText(text))));
109
+ if (deepest < maxDepth - 1) return result;
110
+
111
+ const next = appendText(
112
+ result,
113
+ `注意:这棵树已经到达 maxDepth=${maxDepth} 的边界(最深渲染到第 ${deepest} 层),` +
114
+ `更深的条目可能没有显示。要看完整内容就把 maxDepth 调大(例如 ${maxDepth * 2})重试,` +
115
+ '或者把 directoryPath 直接指到目标子目录再看一层。',
116
+ );
117
+ return next ?? result;
118
+ }
package/src/mcp/idea.js CHANGED
@@ -127,7 +127,7 @@ export async function callTool(port, projectPath, name, args = {}, options = {})
127
127
  params: {
128
128
  protocolVersion: PROTOCOL_VERSION,
129
129
  capabilities: {},
130
- clientInfo: { name: 'opencode-idea', version: '0.1.0' },
130
+ clientInfo: { name: 'opencode-idea', version: '0.1.2' },
131
131
  },
132
132
  });
133
133
  // The IDE's Streamable-HTTP endpoint binds follow-up requests to the session
package/src/plugin.js CHANGED
@@ -55,7 +55,8 @@ import {existsSync} from 'node:fs';
55
55
  import {resolve} from 'node:path';
56
56
 
57
57
  import {callTool, serverConfig} from './mcp/idea.js';
58
- import {applyIdeToolDefaults, hideForcedArguments} from './mcp/idea-tool-defaults.js';
58
+ import {applyIdeToolDefaults, hideForcedArguments, raiseMinimumArguments} from './mcp/idea-tool-defaults.js';
59
+ import {appendDirectoryTreeTruncationNotice} from './mcp/idea-tool-output.js';
59
60
  import {currentProjectPath} from './project.js';
60
61
  import {
61
62
  appNameFor,
@@ -1431,6 +1432,8 @@ export default {
1431
1432
  toolBeforeRegistration = await ctx.tool.hook('execute.before', (event) => {
1432
1433
  if (typeof event?.tool !== 'string') return;
1433
1434
  applyIdeToolDefaults(event.tool, event.input, prefixes());
1435
+ // 下限抬升与"固定值"是两件事:前者保留调用方更大的值,所以参数继续留在 schema 里。
1436
+ raiseMinimumArguments(event.tool, event.input, prefixes());
1434
1437
  });
1435
1438
  }
1436
1439
 
@@ -1441,6 +1444,18 @@ export default {
1441
1444
  // "the project is not open in the IDE" do.
1442
1445
  if (typeof ctx.tool?.hook === 'function') {
1443
1446
  toolRegistration = await ctx.tool.hook('execute.after', async (event) => {
1447
+ if (event?.status === 'completed') {
1448
+ // 输出侧:目录树顶到 maxDepth 边界时补一句看得见的截断提示
1449
+ // (静默截断正是模型把"被切"读成"空目录"的原因,见 `mcp/idea-tool-output.js`)。
1450
+ if (typeof event?.tool !== 'string' || !isIdeToolName(event.tool)) return;
1451
+ const next = appendDirectoryTreeTruncationNotice(event.result, {
1452
+ tool: event.tool,
1453
+ input: event.input,
1454
+ prefixes: prefixes(),
1455
+ });
1456
+ if (next !== event.result) event.result = next;
1457
+ return;
1458
+ }
1444
1459
  if (event?.status !== 'error') return;
1445
1460
  if (typeof event?.tool !== 'string' || !isIdeToolName(event.tool)) return;
1446
1461
  if (!config) return;