jetbrains-pi-acp 0.1.7 → 0.1.9

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
@@ -11,7 +11,7 @@ Requires **Node.js 22.19.0+** and a JetBrains IDE with ACP support, such as Inte
11
11
  1. Sign in to your model provider from a terminal:
12
12
 
13
13
  ```bash
14
- npx --yes jetbrains-pi-acp@0.1.7 --setup
14
+ npx --yes jetbrains-pi-acp@0.1.9 --setup
15
15
  ```
16
16
 
17
17
  In pi, run `/login`, complete sign-in, then exit pi and return to the IDE. Credentials are managed by pi.
@@ -23,7 +23,7 @@ Requires **Node.js 22.19.0+** and a JetBrains IDE with ACP support, such as Inte
23
23
  "agent_servers": {
24
24
  "pi": {
25
25
  "command": "npx",
26
- "args": ["--yes", "jetbrains-pi-acp@0.1.7"],
26
+ "args": ["--yes", "jetbrains-pi-acp@0.1.9"],
27
27
  "use_idea_mcp": true,
28
28
  "use_custom_mcp": true
29
29
  }
@@ -37,6 +37,7 @@ The IDE must be able to find `npx`; use its absolute path if needed. Menu locati
37
37
 
38
38
  ## Features
39
39
 
40
+ - `/goal <objective>` and `/goal resume` continue automatically until the model verifies completion and calls `goal_complete` to clear the goal. After 20 unfinished rounds, execution pauses; use `/goal resume` to continue. Stop cancels execution; `/goal pause` or pausing in the Air goal panel prevents further rounds. Completion is judged by the executing model, without an independent auditor. Setting or resuming a goal in the Air panel only updates its state; send `/goal resume` to start execution.
40
41
  - ACP v1 and v2 draft support, with streaming responses, reasoning, tool status, terminal output, and usage reporting.
41
42
  - Native pi session history: list, load, resume, rename, fork, and delete sessions. Use `/tree` to view the session tree in a selection form and switch context while preserving previous branches. Selecting a user message returns to before it was sent and shows the original text for resending (reattach any attachments). Switching does not revert files, remove existing IDE messages, or generate a branch summary. Clients without form support only display the tree.
42
43
  - Model and reasoning-level selection, plus full-access, standard, read-only, and plan modes.
@@ -52,7 +53,7 @@ The IDE must be able to find `npx`; use its absolute path if needed. Menu locati
52
53
  Append pi options after `--` in the agent's `args`, for example:
53
54
 
54
55
  ```json
55
- ["--yes", "jetbrains-pi-acp@0.1.7", "--", "--model", "provider/model-id", "--thinking", "medium"]
56
+ ["--yes", "jetbrains-pi-acp@0.1.9", "--", "--model", "provider/model-id", "--thinking", "medium"]
56
57
  ```
57
58
 
58
59
  Use configuration options here, not initial prompts, interactive mode, or session-selection arguments. The adapter manages RPC mode and sessions.
@@ -72,6 +73,7 @@ Restart the agent connection after updating the adapter or changing startup opti
72
73
  - Only commands the model runs with `interactive: true` accept input. Other commands get an empty stdin (`/dev/null`) like native pi, so commands that read stdin by mistake exit instead of hanging.
73
74
  - Diffs have size limits and may be unavailable for binary files or older history. File reports scan at most 20,000 entries or two seconds per snapshot and cannot reliably attribute concurrent external changes.
74
75
  - MCP SSE, audio, and binary embedded resources are unsupported. Image support depends on the model.
76
+ - RPC does not support custom terminal panels from extensions. With `pi-mcp-adapter`, `/mcp` falls back to text status, and command notifications appear in chat; these notifications are not saved in pi session history.
75
77
  - ACP v2 follows a draft schema. Client UI behavior, real provider sign-in, and Windows hardware still need verification.
76
78
 
77
79
  ## Development
package/README.zh-CN.md CHANGED
@@ -11,7 +11,7 @@
11
11
  1. 在终端中登录模型提供商:
12
12
 
13
13
  ```bash
14
- npx --yes jetbrains-pi-acp@0.1.7 --setup
14
+ npx --yes jetbrains-pi-acp@0.1.9 --setup
15
15
  ```
16
16
 
17
17
  在 pi 中运行 `/login`,完成登录后退出 pi 并返回 IDE。凭据由 pi 管理。
@@ -23,7 +23,7 @@
23
23
  "agent_servers": {
24
24
  "pi": {
25
25
  "command": "npx",
26
- "args": ["--yes", "jetbrains-pi-acp@0.1.7"],
26
+ "args": ["--yes", "jetbrains-pi-acp@0.1.9"],
27
27
  "use_idea_mcp": true,
28
28
  "use_custom_mcp": true
29
29
  }
@@ -44,6 +44,7 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
44
44
  - 界面文案(配置名称、模式说明、命令说明、授权选项、报错等)按系统语言自动切换中英文,给模型看的工具描述和系统提示固定使用英文。语言判断依次读取非空的 `LC_ALL`、`LC_MESSAGES`、`LANG`,未设置时使用系统默认语言;中文使用中文,其余语言使用英文。更改系统语言后需重启适配器。
45
45
  - 展示内置 `edit/write` 工具的文件 diff,可协商 Air Git patch 和每轮文件报告。文件报告快照遵守 `.gitignore` 等 Git 忽略规则,保留已跟踪文件;非 Git 项目或 Git 不可用时使用文件系统扫描,跳过 `node_modules/`、`vendor/`、`target/`、`build/`、`dist/`、`coverage/`、`.cache/`、`.venv/` 等常见依赖、构建和缓存目录(包括嵌套目录)。快照始终跳过 `.DS_Store`。
46
46
  - 转发 stdio/HTTP MCP 配置及 IDE 工具;支持子 agent、后台 shell 任务和定向停止。
47
+ - `/goal <目标>` 和 `/goal resume` 会自动持续执行,直到模型验证完成并调用 `goal_complete` 清除目标;连续 20 轮仍未完成会暂停,可用 `/goal resume` 继续。停止按钮取消当前执行,`/goal pause` 或 Air 目标面板的暂停操作阻止后续轮次。完成由执行模型判断,不包含独立审计。Air 面板设置或恢复目标仅同步状态,启动执行请发送 `/goal resume`。
47
48
  - 按客户端能力提供 steering、终端 stdin、表单和 URL 交互。Air v1 可在命令执行期间发送 `/stdin 文本` 和 `/eof`。
48
49
 
49
50
  **默认完全访问,自动允许工具调用。** 标准访问对内置只读工具以外的工具逐次授权;只读和规划模式拒绝这些工具。权限模式不是操作系统沙箱,安装的 pi 扩展仍属于受信任代码。具体功能取决于客户端协商能力。
@@ -53,7 +54,7 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
53
54
  在 Agent 的 `args` 末尾添加 `--`,随后传递 pi 参数,例如:
54
55
 
55
56
  ```json
56
- ["--yes", "jetbrains-pi-acp@0.1.7", "--", "--model", "provider/model-id", "--thinking", "medium"]
57
+ ["--yes", "jetbrains-pi-acp@0.1.9", "--", "--model", "provider/model-id", "--thinking", "medium"]
57
58
  ```
58
59
 
59
60
  这里只传配置选项,不传初始提示、交互模式或会话选择参数。RPC 模式和会话由适配器管理。
@@ -73,6 +74,7 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
73
74
  - 只有模型以 `interactive: true` 调用的命令可以接收输入;其他命令与 pi 原生一致,stdin 为空(`/dev/null`),误读 stdin 的命令会直接结束,不会卡住。
74
75
  - diff 有大小限制,二进制文件或旧历史可能无法展示。每轮文件报告的单次快照最多扫描 20,000 项或两秒,无法准确区分外部并发修改。
75
76
  - 不支持 MCP SSE、音频和二进制内嵌资源,图片支持取决于模型。
77
+ - RPC 不支持扩展的终端自定义面板。`pi-mcp-adapter` 的 `/mcp` 会降级为文字状态,命令通知在聊天中显示;这些通知不会写入 pi 会话历史。
76
78
  - ACP v2 仍为草案,客户端界面展示、真实提供商账号登录及 Windows 实机仍需验证。
77
79
  - 每轮 token 在 v1 提示响应的 `usage`、v2 完成状态的 `usage` 中返回;未上报用量时省略,不单列思考 token 或每轮费用。费用取自 pi 的模型计价数据;上下文用量不可用时不发送 `usage_update`,IDE 展示取决于客户端支持。
78
80
 
package/extensions/acp.ts CHANGED
@@ -121,7 +121,9 @@ export default function (pi: any) {
121
121
  const mutations = new Map<string, { path: string; conflicted: boolean; oldText?: string | null }>();
122
122
  let planMode = false;
123
123
  let goal: { status: string; objective: string } | null = null;
124
+ let runningGoal: typeof goal = null;
124
125
  pi.on('before_agent_start', (event: any) => {
126
+ runningGoal = goal;
125
127
  const sections = { ...event.systemPromptOptions.sections };
126
128
  delete sections.acp_client_instructions;
127
129
  delete sections.acp_plan_mode;
@@ -130,7 +132,7 @@ export default function (pi: any) {
130
132
  ...sections,
131
133
  ...(config.customInstructions ? { acp_client_instructions: config.customInstructions } : {}),
132
134
  ...(planMode ? { acp_plan_mode: 'Only analyze the problem and produce an actionable plan. Do not modify files, run commands, or use other tools with side effects.' } : {}),
133
- ...(goal?.status === 'active' ? { acp_goal: `Long-term goal for this session: ${goal.objective}` } : {}),
135
+ ...(goal?.status === 'active' ? { acp_goal: `Long-term goal for this session: ${goal.objective}\nKeep working until the goal is fully achieved and verified, then call the goal_complete tool. Do not call it before that.` } : {}),
134
136
  };
135
137
  });
136
138
  pi.registerCommand('__acp_mode', {
@@ -160,6 +162,18 @@ export default function (pi: any) {
160
162
  goal = value;
161
163
  },
162
164
  });
165
+ pi.registerTool({
166
+ name: 'goal_complete', label: t('goalCompleteLabel'),
167
+ description: 'Mark the current long-term goal as achieved. Call only when the goal is fully done and verified.',
168
+ parameters: Type.Object({ summary: Type.String({ maxLength: 10000 }) }),
169
+ execute: async (_id: string, input: any, _signal: AbortSignal, _update: any, ctx: any) => {
170
+ if (goal?.status !== 'active' || goal !== runningGoal) throw new Error(t('noGoalSet'));
171
+ pi.appendEntry('acp_goal', null);
172
+ goal = null;
173
+ ctx.ui.notify(`PI_ACP_GOAL:null`, 'info');
174
+ return { content: [{ type: 'text', text: 'Goal completed' }], details: { summary: input.summary } };
175
+ },
176
+ });
163
177
  pi.registerTool({
164
178
  name: 'update_plan', label: t('updatePlanLabel'),
165
179
  description: 'Update the execution plan and progress of the current task with structured steps. Only records the plan; performs no file or command operations.',
@@ -185,7 +199,7 @@ export default function (pi: any) {
185
199
  return { message: { ...event.message, acpMessageId: id } };
186
200
  }
187
201
  });
188
- const archiveCache = new Map<string, { modified: number; archived: boolean }>();
202
+ const archiveCache = new Map<string, { modified: number; archived: boolean; children: string[]; records: string[] }>();
189
203
  for (const [name, server] of Object.entries(config.mcpServers)) {
190
204
  pi.registerMcpServer(name, server);
191
205
  }
@@ -219,19 +233,42 @@ export default function (pi: any) {
219
233
  const currentPaths = new Set(sessions.map(session => session.path));
220
234
  const idsByPath = new Map(sessions.map(session => [session.path, session.id]));
221
235
  for (const path of archiveCache.keys()) if (!currentPaths.has(path)) archiveCache.delete(path);
222
- ctx.ui.notify(`PI_ACP_SESSIONS:${JSON.stringify(sessions.map(session => {
236
+ const infos = sessions.map(session => {
223
237
  const modified = statSync(session.path).mtimeMs;
224
238
  let cached = archiveCache.get(session.path);
225
239
  if (!cached || cached.modified !== modified) {
226
- const archived = SessionManager.open(session.path).getBranch().findLast((entry: any) =>
240
+ const manager = SessionManager.open(session.path);
241
+ const archived = manager.getBranch().findLast((entry: any) =>
227
242
  entry.type === 'custom' && entry.customType === 'acp_archive')?.data === true;
228
- cached = { modified, archived };
243
+ const entries = manager.getEntries() as any[];
244
+ const children = entries.flatMap(entry =>
245
+ entry.type === 'custom' && entry.customType === 'acp_task' && entry.data?.kind === 'agent' &&
246
+ entry.data.owner === session.id && typeof entry.data.sessionId === 'string' ? [entry.data.sessionId] : []);
247
+ const records = entries.flatMap(entry =>
248
+ entry.type === 'custom' && entry.customType === 'subagents:record' && typeof entry.data?.id === 'string' ? [entry.data.id] : []);
249
+ cached = { modified, archived, children, records };
229
250
  archiveCache.set(session.path, cached);
230
251
  }
231
252
  return { sessionId: session.id, cwd: session.cwd, title: session.name || session.firstMessage,
232
253
  updatedAt: session.modified.toISOString(), path: session.path, archived: cached.archived,
233
254
  ...(idsByPath.has(session.parentSessionPath) ? { forkedFrom: idsByPath.get(session.parentSessionPath) } : {}) };
234
- }))}`, 'info');
255
+ });
256
+ // 子 agent 会话标记 subagent,由适配器从列表中过滤:
257
+ // 1) 适配器自己的子 agent:父会话的 acp_task 引用其 ID(自有格式,稳定);
258
+ // 2) 旧版 @tintinweb/pi-subagents 写在顶层目录的历史会话:名称为 `类型#8位hex`,
259
+ // 且父会话有 id 以该 hex 开头的 subagents:record。
260
+ // 当前 pi-subagents 的子会话在嵌套目录或临时目录,SessionManager.list 本就不返回。
261
+ // shortcut: 规则 2 只为旧包的历史数据,不再有此类会话后可整段删除。
262
+ const children = new Set(sessions.flatMap(session => archiveCache.get(session.path)?.children ?? []));
263
+ const recordsByPath = new Map(sessions.map(session => [session.path, archiveCache.get(session.path)?.records ?? []]));
264
+ const isSubagent = (session: any) => {
265
+ const hex = /#([0-9a-f]{8})$/.exec(session.name ?? '')?.[1];
266
+ return children.has(session.id) || (!!hex && !!session.parentSessionPath &&
267
+ !!recordsByPath.get(session.parentSessionPath)?.some(id => id.startsWith(hex)));
268
+ };
269
+ const subagents = new Set(sessions.filter(isSubagent).map(session => session.id));
270
+ ctx.ui.notify(`PI_ACP_SESSIONS:${JSON.stringify(infos.map(info =>
271
+ subagents.has(info.sessionId) ? { ...info, subagent: true } : info))}`, 'info');
235
272
  },
236
273
  });
237
274
  pi.on('tool_call', async (event: any, ctx: any) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jetbrains-pi-acp",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "连接 JetBrains IDE 与 pi coding agent 的 ACP 适配器",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -385,10 +385,12 @@ export class Agent {
385
385
  return turn;
386
386
  }
387
387
 
388
- async prompt(session, prompt, steering, fileReportRequest) {
388
+ async prompt(session, prompt, steering, fileReportRequest, pursueGoal = false) {
389
389
  const created = !session.turn;
390
390
  if (created) this.beginTurn(session, steering, fileReportRequest);
391
391
  const turn = session.turn;
392
+ if (created) turn.extensionCommand = /^\/\S/.test(prompt.message);
393
+ if (pursueGoal) Object.assign(turn, { pursueGoal: true, goalCreatedAt: session.goal.createdAt, goalRounds: 0 });
392
394
  turn.pending++;
393
395
  // 串行提交,避免前一条提示尚在预处理时再次启动运行。
394
396
  const submission = (turn.submission ?? Promise.resolve()).catch(() => {}).then(async () => {
@@ -633,10 +635,10 @@ export class Agent {
633
635
  await this.sendUsage(session, await session.pi.request('get_session_stats'));
634
636
  }
635
637
 
636
- // 会话就绪后发布可用命令与用量;失败只记录日志。
638
+ // v2 初始命令随响应返回,v1 通过通知发布;失败只记录日志。
637
639
  async publishSession(session) {
638
640
  await Promise.all([
639
- this.commands.publish(session).catch(logError('publishCommandsFailed')),
641
+ this.commands.publish(session, this.protocolVersion !== 2).catch(logError('publishCommandsFailed')),
640
642
  this.refreshUsage(session).catch(logError('refreshUsageFailed')),
641
643
  ]);
642
644
  }
@@ -915,7 +917,7 @@ export class Agent {
915
917
  case 'agent_settled':
916
918
  if (event.aborted) session.turn.cancelled = true;
917
919
  session.turn.usage = this.refreshUsage(session).catch(logError('refreshUsageFailed'));
918
- session.turn.settle();
920
+ void this.commands.settleGoal(session);
919
921
  break;
920
922
  }
921
923
  }
package/src/commands.js CHANGED
@@ -22,6 +22,8 @@ export function treeOptions(tree, leafId) {
22
22
  }
23
23
 
24
24
  // 适配器自己实现的斜杠命令,以及发布给客户端的命令列表。
25
+ const MAX_GOAL_TURNS = 20;
26
+
25
27
  export class Commands {
26
28
  constructor(agent) {
27
29
  this.agent = agent;
@@ -43,6 +45,31 @@ export class Commands {
43
45
  return true;
44
46
  }
45
47
 
48
+ async settleGoal(session) {
49
+ const turn = session.turn;
50
+ const goal = session.goal;
51
+ if (!turn.pursueGoal || turn.cancelled || turn.error || turn.stopReason !== 'end_turn' ||
52
+ goal?.status !== 'active' || goal.createdAt !== turn.goalCreatedAt) return turn.settle();
53
+ turn.pending++;
54
+ try {
55
+ if (++turn.goalRounds >= MAX_GOAL_TURNS) {
56
+ await this.agent.goalSession({ sessionId: session.id, action: 'pause' });
57
+ this.agent.update(session, { sessionUpdate: 'agent_message_chunk',
58
+ content: { type: 'text', text: t('goalTurnLimit', { turns: turn.goalRounds }) } });
59
+ turn.settle();
60
+ } else {
61
+ const accepted = await session.pi.request('prompt', { message: t('goalContinue', goal) });
62
+ if (accepted?.disposition === 'handled') turn.settle();
63
+ }
64
+ } catch (error) {
65
+ turn.error = error;
66
+ turn.settle();
67
+ } finally {
68
+ turn.pending--;
69
+ turn.finish();
70
+ }
71
+ }
72
+
46
73
  async run(session, text, fileReportRequest) {
47
74
  const finish = result => {
48
75
  if (fileReportRequest) this.agent.sendFileReport(session, { fileReportRequest, changedPaths: new Set(), starts: 0,
@@ -59,6 +86,8 @@ export class Commands {
59
86
  ...(['pause', 'resume', 'clear'].includes(argument)
60
87
  ? { action: argument } : { action: 'set', objective: argument }),
61
88
  }));
89
+ // 设置或恢复后立即开工,否则 Air 面板显示 Running 而没有任何模型轮次在跑。
90
+ if (session.goal?.status === 'active') return this.agent.prompt(session, { message: t('goalKickoff', session.goal) }, false, fileReportRequest, true);
62
91
  }
63
92
  return finish({ stopReason: 'end_turn' });
64
93
  }
@@ -160,7 +189,7 @@ export class Commands {
160
189
  }
161
190
  }
162
191
 
163
- async publish(session) {
192
+ async publish(session, notify = true) {
164
193
  const result = await session.pi.request('get_commands');
165
194
  const availableCommands = new Map([
166
195
  ['status', { name: 'status', description: t('cmdStatus'), input: null }],
@@ -181,7 +210,8 @@ export class Commands {
181
210
  availableCommands.set(command.name, { name: command.name,
182
211
  description: command.description || command.name, input: null });
183
212
  }
184
- this.agent.update(session, { sessionUpdate: 'available_commands_update',
185
- availableCommands: [...availableCommands.values()] });
213
+ session.availableCommands = [...availableCommands.values()];
214
+ if (notify) this.agent.update(session, { sessionUpdate: 'available_commands_update',
215
+ availableCommands: session.availableCommands });
186
216
  }
187
217
  }
package/src/i18n.js CHANGED
@@ -164,6 +164,10 @@ const zh = {
164
164
  needOneTerminal: '需要恰好一个可输入的运行中终端',
165
165
  goalStatus: '长期目标({status}):{objective}',
166
166
  noGoal: '尚未设置长期目标',
167
+ goalKickoff: '开始推进长期目标:{objective}',
168
+ goalContinue: '继续推进长期目标:{objective}。若已完全达成并验证,请调用 goal_complete;否则继续工作。',
169
+ goalTurnLimit: '已连续运行 {turns} 轮仍未完成目标,已暂停自动续跑。可发送 /goal resume 继续。',
170
+ goalCompleteLabel: '完成目标',
167
171
  statusModel: '模型:{model}',
168
172
  statusThinking: '推理级别:{level}',
169
173
  statusMessages: '消息:{messages},工具调用:{toolCalls}',
@@ -376,6 +380,10 @@ const en = {
376
380
  needOneTerminal: 'Exactly one running terminal that accepts input is required',
377
381
  goalStatus: 'Goal ({status}): {objective}',
378
382
  noGoal: 'No goal set',
383
+ goalKickoff: 'Start working on the goal: {objective}',
384
+ goalContinue: 'Continue working on the goal: {objective}. If it is fully achieved and verified, call goal_complete; otherwise keep working.',
385
+ goalTurnLimit: 'The goal is still unfinished after {turns} consecutive turns; auto-continue is paused. Send /goal resume to continue.',
386
+ goalCompleteLabel: 'Complete goal',
379
387
  statusModel: 'Model: {model}',
380
388
  statusThinking: 'Thinking level: {level}',
381
389
  statusMessages: 'Messages: {messages}, tool calls: {toolCalls}',
@@ -100,6 +100,11 @@ export class Interactions {
100
100
  notifyHandlers[prefix](this.agent, session, message.slice(prefix.length));
101
101
  return;
102
102
  }
103
+ // RPC 扩展命令(如 /mcp)通过 notify 返回文字,需作为本轮回复展示。
104
+ if (message && session.turn?.extensionCommand && !session.turn.cancelled && !session.turn.settled && !session.turn.starts) {
105
+ this.agent.update(session, { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: `${message}\n` } });
106
+ return;
107
+ }
103
108
  const notices = this.agent.clientCapabilities?.session?.notices;
104
109
  if (notices && typeof notices === 'object' && !Array.isArray(notices)) {
105
110
  this.agent.update(session, { sessionUpdate: 'notice',
package/src/protocol.js CHANGED
@@ -6,6 +6,8 @@ import { invalid } from './shared.js';
6
6
  import { t } from './i18n.js';
7
7
 
8
8
  const options = values => values.map(({ id, ...option }) => ({ configId: id, ...option }));
9
+ const commands = values => values.map(command => ({ ...command,
10
+ ...(command.input ? { input: { ...command.input, type: 'text' } } : {}) }));
9
11
  const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
10
12
  const failure = error => ({ code: Number.isInteger(error.code) ? error.code : -32603, message: error.message });
11
13
 
@@ -255,10 +257,12 @@ export class ProtocolAgent {
255
257
  } });
256
258
  } else if (record.result?.configOptions) {
257
259
  const { models, modes, ...result } = record.result;
258
- this.send({ ...record, result: { ...result, configOptions: options(result.configOptions) } });
259
- if (['session/new', 'session/resume', 'session/fork'].includes(request?.method)) {
260
- this.state(result.sessionId ?? request.sessionId, 'idle');
261
- }
260
+ const setup = ['session/new', 'session/resume', 'session/fork'].includes(request?.method);
261
+ const sessionId = result.sessionId ?? request?.sessionId;
262
+ const availableCommands = setup ? this.agent.sessions.get(sessionId)?.availableCommands : undefined;
263
+ this.send({ ...record, result: { ...result, configOptions: options(result.configOptions),
264
+ ...(availableCommands ? { availableCommands: commands(availableCommands) } : {}) } });
265
+ if (setup) this.state(sessionId, 'idle');
262
266
  } else this.send(record);
263
267
  if (record.result && ['session/close', 'session/delete'].includes(request?.method)) this.states.delete(request.sessionId);
264
268
  return;
@@ -288,8 +292,7 @@ export class ProtocolAgent {
288
292
  if (update.sessionUpdate === 'current_mode_update') return;
289
293
  if (update.sessionUpdate === 'config_option_update') update = { ...update, configOptions: options(update.configOptions) };
290
294
  if (update.sessionUpdate === 'available_commands_update') update = { ...update,
291
- availableCommands: update.availableCommands.map(command => ({ ...command,
292
- ...(command.input ? { input: { ...command.input, type: 'text' } } : {}) })) };
295
+ availableCommands: commands(update.availableCommands) };
293
296
  if (update.sessionUpdate === 'plan') update = { sessionUpdate: 'plan_update',
294
297
  plan: { type: 'items', planId: 'pi-plan', entries: update.entries } };
295
298
  if (['user_message_chunk', 'agent_message_chunk', 'agent_thought_chunk'].includes(update.sessionUpdate)) {
@@ -7,7 +7,7 @@ import { t } from './i18n.js';
7
7
 
8
8
  const executeFile = promisify(execFile);
9
9
 
10
- // 历史会话索引:列表、查找、归档及列表变化订阅。
10
+ // 历史会话索引:列表、查找、归档及列表变化订阅;列表默认不含子 agent 会话,仅内部查找时包含。
11
11
  // 历史由临时启动的 discovery pi 进程通过 /__acp_list 读取。
12
12
  export class SessionIndex {
13
13
  constructor(agent) {
@@ -16,7 +16,7 @@ export class SessionIndex {
16
16
  }
17
17
 
18
18
  async find(sessionId, cwd) {
19
- const { sessions } = await this.list(cwd === undefined ? {} : { cwd }, { includeArchived: true });
19
+ const { sessions } = await this.list(cwd === undefined ? {} : { cwd }, { includeArchived: true, includeSubagents: true });
20
20
  return sessions.find(info => info.sessionId === sessionId && (cwd === undefined || resolve(info.cwd) === resolve(cwd)));
21
21
  }
22
22
 
@@ -31,7 +31,7 @@ export class SessionIndex {
31
31
  }) };
32
32
  }
33
33
 
34
- async read(context, params) {
34
+ async read(context, params, { includeSubagents = false } = {}) {
35
35
  let cwds = [context.cwd];
36
36
  if (params._meta?.jetbrains?.air?.list?.includeWorktrees === true) {
37
37
  try {
@@ -44,10 +44,10 @@ export class SessionIndex {
44
44
  context.history = undefined;
45
45
  await context.pi.request('prompt', { message: `/__acp_list ${JSON.stringify({ all: params.cwd === undefined, cwds })}` });
46
46
  if (!context.history) throw new Error(t('piNoHistory'));
47
- return context.history;
47
+ return includeSubagents ? context.history : context.history.filter(info => !info.subagent);
48
48
  }
49
49
 
50
- async list(params, { includeArchived = false } = {}) {
50
+ async list(params, { includeArchived = false, includeSubagents = false } = {}) {
51
51
  if (params.cursor !== undefined && (typeof params.cursor !== 'string' || !/^\d+$/.test(params.cursor))) {
52
52
  throw invalid(t('listCursorInvalid'));
53
53
  }
@@ -65,7 +65,7 @@ export class SessionIndex {
65
65
  }
66
66
  const created = await this.agent.newSession({ cwd: params.cwd ?? process.cwd(), mcpServers: [] }, { discovery: true });
67
67
  try {
68
- const history = (await this.read(created, params)).filter(info =>
68
+ const history = (await this.read(created, params, { includeSubagents })).filter(info =>
69
69
  archived === 'all' || (info.archived === true) === (archived === 'archived'));
70
70
  const sessions = history.slice(offset, offset + limit);
71
71
  return { sessions, ...(offset + limit < history.length ? { nextCursor: String(offset + limit) } : {}) };