linkshell-cli 0.5.36 → 0.5.38

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 (30) hide show
  1. package/dist/cli/src/runtime/acp/acp-client.d.ts +21 -0
  2. package/dist/cli/src/runtime/acp/acp-client.js +101 -5
  3. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  4. package/dist/cli/src/runtime/acp/agent-process-discovery.js +5 -1
  5. package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +1 -1
  6. package/dist/cli/src/runtime/acp/agent-workspace.js +146 -33
  7. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  8. package/dist/cli/src/runtime/acp/provider-resolver.js +19 -2
  9. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
  10. package/dist/cli/src/runtime/bridge-session.d.ts +0 -46
  11. package/dist/cli/src/runtime/bridge-session.js +10 -1034
  12. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  13. package/dist/cli/src/runtime/hook-command.d.ts +7 -17
  14. package/dist/cli/src/runtime/hook-command.js +75 -26
  15. package/dist/cli/src/runtime/hook-command.js.map +1 -1
  16. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  17. package/package.json +2 -2
  18. package/src/runtime/acp/acp-client.ts +116 -5
  19. package/src/runtime/acp/agent-process-discovery.ts +3 -1
  20. package/src/runtime/acp/agent-workspace.ts +152 -32
  21. package/src/runtime/acp/provider-resolver.ts +21 -2
  22. package/src/runtime/bridge-session.ts +9 -1110
  23. package/src/runtime/hook-command.ts +68 -30
  24. package/web/assets/{FileBrowser-Coy7Szjx.js → FileBrowser-Db2LlqTw.js} +1 -1
  25. package/web/assets/{PortPreview-D-XovbV1.js → PortPreview-Cobi7sn0.js} +1 -1
  26. package/web/assets/{TerminalPanel-Cah56AtC.js → TerminalPanel-_Wve-C8s.js} +1 -1
  27. package/web/assets/{UsageDashboard-DfkjJagC.js → UsageDashboard-DmXct3sk.js} +1 -1
  28. package/web/assets/index-pnfD2xom.js +35 -0
  29. package/web/index.html +1 -1
  30. package/web/assets/index-ClZzQ4ba.js +0 -35
@@ -4,6 +4,75 @@ import type { AgentFraming, AgentProtocol } from "./provider-resolver.js";
4
4
  type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
5
5
  type AgentCollaborationMode = "default" | "plan";
6
6
 
7
+ export interface AdvertisedAcpCapabilities {
8
+ protocolVersion?: number;
9
+ loadSession: boolean;
10
+ listSession: boolean;
11
+ forkSession: boolean;
12
+ setModel: boolean;
13
+ cancel: boolean;
14
+ images: boolean;
15
+ audio: boolean;
16
+ embeddedContext: boolean;
17
+ mcp: boolean;
18
+ methods: string[];
19
+ }
20
+
21
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
22
+ return value && typeof value === "object" && !Array.isArray(value)
23
+ ? value as Record<string, unknown>
24
+ : undefined;
25
+ }
26
+
27
+ function truthy(value: unknown): boolean {
28
+ return value === true || value === "true" || value === 1;
29
+ }
30
+
31
+ function collectAdvertisedMethods(...sources: Array<Record<string, unknown> | undefined>): string[] {
32
+ const methods = new Set<string>();
33
+ for (const source of sources) {
34
+ if (!source) continue;
35
+ const lists = [source.methods, source.supportedMethods, source.sessionMethods];
36
+ for (const list of lists) {
37
+ if (!Array.isArray(list)) continue;
38
+ for (const item of list) {
39
+ if (typeof item === "string" && item.trim()) methods.add(item.trim());
40
+ }
41
+ }
42
+ for (const [key, value] of Object.entries(source)) {
43
+ if (key.includes("/") && truthy(value)) methods.add(key);
44
+ }
45
+ }
46
+ return [...methods];
47
+ }
48
+
49
+ /** Parse ACP `initialize` result. Unadvertised optional methods stay false. */
50
+ export function parseAcpInitializeCapabilities(result: unknown): AdvertisedAcpCapabilities {
51
+ const raw = asRecord(result) ?? {};
52
+ const agentCaps = asRecord(raw.agentCapabilities) ?? asRecord(raw.capabilities) ?? {};
53
+ const sessionCaps = asRecord(raw.sessionCapabilities) ?? asRecord(agentCaps.sessionCapabilities) ?? {};
54
+ const promptCaps = asRecord(agentCaps.promptCapabilities) ?? {};
55
+ const mcpCaps = asRecord(agentCaps.mcpCapabilities);
56
+ const methods = collectAdvertisedMethods(raw, agentCaps, sessionCaps);
57
+ const has = (...keys: string[]) =>
58
+ keys.some((key) => truthy(agentCaps[key]) || truthy(sessionCaps[key]) || methods.includes(key));
59
+ return {
60
+ protocolVersion: typeof raw.protocolVersion === "number" ? raw.protocolVersion : undefined,
61
+ loadSession: has("loadSession", "session/load"),
62
+ listSession: has("listSession", "listSessions", "session/list"),
63
+ forkSession: has("forkSession", "session/fork"),
64
+ setModel: has("setModel", "session/set_model"),
65
+ // ACP v1 MUST session/cancel. AgentCapabilities has no cancel flag, so a
66
+ // real-shaped initialize that omits it still supports cancel.
67
+ cancel: true,
68
+ images: truthy(promptCaps.image) || has("image", "images"),
69
+ audio: truthy(promptCaps.audio) || has("audio"),
70
+ embeddedContext: truthy(promptCaps.embeddedContext),
71
+ mcp: Boolean(mcpCaps && Object.keys(mcpCaps).length > 0) || has("mcp"),
72
+ methods,
73
+ };
74
+ }
75
+
7
76
  function normalizeMcpServers(value: unknown): unknown[] {
8
77
  if (Array.isArray(value)) return value;
9
78
  if (!value || typeof value !== "object") return [];
@@ -55,6 +124,8 @@ function approvalPolicyForMode(
55
124
  export class AcpClient {
56
125
  private readonly transport: JsonRpcStdioTransport;
57
126
  private readonly protocol: AgentProtocol;
127
+ lastInitializeResult: unknown;
128
+ advertised: AdvertisedAcpCapabilities = parseAcpInitializeCapabilities(undefined);
58
129
 
59
130
  constructor(input: {
60
131
  command: string;
@@ -83,9 +154,20 @@ export class AcpClient {
83
154
  capabilities: { experimentalApi: true },
84
155
  });
85
156
  this.transport.notify("initialized", {});
157
+ this.lastInitializeResult = result;
158
+ this.advertised = {
159
+ ...parseAcpInitializeCapabilities(result),
160
+ loadSession: true,
161
+ listSession: true,
162
+ forkSession: true,
163
+ setModel: true,
164
+ cancel: true,
165
+ images: true,
166
+ mcp: true,
167
+ };
86
168
  return result;
87
169
  }
88
- return this.transport.request("initialize", {
170
+ const result = await this.transport.request("initialize", {
89
171
  protocolVersion: 1,
90
172
  clientInfo: { name: "LinkShell", version: "0.1" },
91
173
  clientCapabilities: {
@@ -93,6 +175,9 @@ export class AcpClient {
93
175
  terminal: false,
94
176
  },
95
177
  });
178
+ this.lastInitializeResult = result;
179
+ this.advertised = { ...parseAcpInitializeCapabilities(result), cancel: true };
180
+ return result;
96
181
  }
97
182
 
98
183
  newSession(input: { cwd: string; mcpServers?: unknown }): Promise<unknown> {
@@ -116,6 +201,9 @@ export class AcpClient {
116
201
  excludeTurns: false,
117
202
  });
118
203
  }
204
+ if (!this.advertised.loadSession) {
205
+ return Promise.reject(new Error("Provider did not advertise session/load."));
206
+ }
119
207
  return this.transport.request("session/load", {
120
208
  sessionId: input.sessionId,
121
209
  cwd: input.cwd,
@@ -154,6 +242,9 @@ export class AcpClient {
154
242
 
155
243
  async listSessions(): Promise<unknown> {
156
244
  if (this.protocol !== "codex-app-server") {
245
+ if (!this.advertised.listSession) {
246
+ return Promise.reject(new Error("Provider did not advertise session/list."));
247
+ }
157
248
  return this.transport.request("session/list", {});
158
249
  }
159
250
  const allThreads: unknown[] = [];
@@ -247,15 +338,34 @@ export class AcpClient {
247
338
  }
248
339
 
249
340
  forkThread(input: { sessionId: string; lastTurnId?: string }): Promise<unknown> {
250
- if (this.protocol !== "codex-app-server") {
251
- return Promise.reject(new Error("Provider does not support forkThread."));
341
+ if (this.protocol === "codex-app-server") {
342
+ return this.transport.request("thread/fork", {
343
+ threadId: input.sessionId,
344
+ ...(input.lastTurnId ? { lastTurnId: input.lastTurnId } : {}),
345
+ });
252
346
  }
253
- return this.transport.request("thread/fork", {
254
- threadId: input.sessionId,
347
+ if (!this.advertised.forkSession) {
348
+ return Promise.reject(new Error("Provider did not advertise session/fork."));
349
+ }
350
+ return this.transport.request("session/fork", {
351
+ sessionId: input.sessionId,
255
352
  ...(input.lastTurnId ? { lastTurnId: input.lastTurnId } : {}),
256
353
  });
257
354
  }
258
355
 
356
+ setSessionModel(input: { sessionId: string; modelId: string }): Promise<unknown> {
357
+ if (this.protocol === "codex-app-server") {
358
+ return this.updateThreadSettings({ sessionId: input.sessionId, model: input.modelId });
359
+ }
360
+ if (!this.advertised.setModel) {
361
+ return Promise.reject(new Error("Provider did not advertise session/set_model."));
362
+ }
363
+ return this.transport.request("session/set_model", {
364
+ sessionId: input.sessionId,
365
+ modelId: input.modelId,
366
+ });
367
+ }
368
+
259
369
  startReview(input: { sessionId: string; prompt?: string }): Promise<unknown> {
260
370
  if (this.protocol !== "codex-app-server") {
261
371
  return Promise.reject(new Error("Provider does not support startReview."));
@@ -385,6 +495,7 @@ export class AcpClient {
385
495
  }).catch(() => {});
386
496
  return;
387
497
  }
498
+ if (!this.advertised.cancel) return;
388
499
  this.transport.notify("session/cancel", { sessionId: input.sessionId });
389
500
  }
390
501
 
@@ -28,7 +28,7 @@ export const KNOWN_AGENT_PROVIDERS = [
28
28
 
29
29
  export type KnownAgentProvider = (typeof KNOWN_AGENT_PROVIDERS)[number];
30
30
 
31
- export const PROTOCOL_AGENT_PROVIDERS = new Set<string>(["codex", "claude"]);
31
+ export const PROTOCOL_AGENT_PROVIDERS = new Set<string>(["codex", "claude", "gemini", "cursor"]);
32
32
 
33
33
  export interface AgentProcessSnapshot {
34
34
  provider: string;
@@ -96,6 +96,8 @@ export function isControlPlaneCommand(command: string): boolean {
96
96
  if (lowered.includes("stream-json") && (lowered.includes("--print") || lowered.includes("--output-format"))) {
97
97
  return true;
98
98
  }
99
+ if (/\b--acp\b/.test(lowered)) return true;
100
+ if (commandBinaryName(command) === "cursor-agent" && /\sacp(\s|$)/.test(lowered)) return true;
99
101
  return false;
100
102
  }
101
103
 
@@ -6,7 +6,7 @@ import {
6
6
  parseTypedPayload,
7
7
  type Envelope,
8
8
  } from "@linkshell/protocol";
9
- import { AcpClient } from "./acp-client.js";
9
+ import { AcpClient, type AdvertisedAcpCapabilities } from "./acp-client.js";
10
10
  import { ClaudeSdkClient } from "./claude-sdk-client.js";
11
11
  import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
12
12
  import { aggregateUsage } from "../usage-report.js";
@@ -475,7 +475,9 @@ function normalizeToolStatus(value: unknown, completedFallback = false): AgentTo
475
475
 
476
476
  function normalizePlanStatus(value: unknown): AgentPlanStep["status"] {
477
477
  if (value === "completed" || value === "done") return "completed";
478
- if (value === "inProgress" || value === "running" || value === "active") return "in_progress";
478
+ if (value === "inProgress" || value === "in_progress" || value === "running" || value === "active") {
479
+ return "in_progress";
480
+ }
479
481
  return "pending";
480
482
  }
481
483
 
@@ -808,7 +810,11 @@ function contentBlocksFromItem(item: Record<string, unknown>): AgentContentBlock
808
810
  return text ? [{ type: "text", text }] : [];
809
811
  }
810
812
 
811
- function protocolSupportsImages(protocol: AgentProtocol | undefined): boolean {
813
+ function protocolSupportsImages(
814
+ protocol: AgentProtocol | undefined,
815
+ advertised?: AdvertisedAcpCapabilities,
816
+ ): boolean {
817
+ if (protocol === "acp") return Boolean(advertised?.images);
812
818
  return protocol === "codex-app-server" ||
813
819
  protocol === "claude-agent-sdk" ||
814
820
  protocol === "claude-stream-json";
@@ -997,6 +1003,7 @@ interface ProviderRuntimeCapabilities {
997
1003
  commands?: AgentCommandDescriptor[];
998
1004
  modes?: AgentModeDescriptor[];
999
1005
  currentMode?: string;
1006
+ advertised?: AdvertisedAcpCapabilities;
1000
1007
  // MCP server connection status, keyed by server name (last-writer-wins across
1001
1008
  // the init seed and live startupStatus events).
1002
1009
  mcpServers?: Map<string, AgentMcpServerDescriptor>;
@@ -2089,6 +2096,21 @@ export class AgentWorkspaceProxy {
2089
2096
  // Echo the updated record so every client refreshes (the web store
2090
2097
  // force-replaces by id on any event carrying `conversation`).
2091
2098
  this.emitConversation(conversation);
2099
+ if (conversation.agentSessionId) {
2100
+ const client = this.clientForProvider(conversation.provider);
2101
+ if (client instanceof AcpClient && model?.trim() && client.advertised.setModel && conversation.provider !== "codex") {
2102
+ try {
2103
+ await client.setSessionModel({
2104
+ sessionId: conversation.agentSessionId,
2105
+ modelId: model.trim(),
2106
+ });
2107
+ } catch (error) {
2108
+ if (this.input.verbose) {
2109
+ process.stderr.write(`[agent:v2] session/set_model failed: ${error instanceof Error ? error.message : String(error)}\n`);
2110
+ }
2111
+ }
2112
+ }
2113
+ }
2092
2114
  if (conversation.provider === "codex" && conversation.agentSessionId) {
2093
2115
  const client = this.clientForProvider(conversation.provider);
2094
2116
  const settingsChanged =
@@ -2693,7 +2715,14 @@ export class AgentWorkspaceProxy {
2693
2715
  client: AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient,
2694
2716
  protocol: AgentProtocol,
2695
2717
  ): Promise<void> {
2696
- if (client instanceof AcpClient && protocol !== "codex-app-server") return;
2718
+ if (client instanceof AcpClient) {
2719
+ const prior = this.providerCapabilities.get(provider);
2720
+ this.providerCapabilities.set(provider, {
2721
+ ...prior,
2722
+ advertised: client.advertised,
2723
+ });
2724
+ if (protocol !== "codex-app-server") return;
2725
+ }
2697
2726
  const listModels = (client as { listModels?: () => Promise<unknown> }).listModels;
2698
2727
  let runtimeCapabilities: ProviderRuntimeCapabilities | undefined;
2699
2728
  if (typeof listModels === "function") {
@@ -2722,6 +2751,8 @@ export class AgentWorkspaceProxy {
2722
2751
  commands: mergeCommands(runtimeCapabilities?.commands, skillCommands),
2723
2752
  modes: runtimeCapabilities?.modes,
2724
2753
  currentMode: runtimeCapabilities?.currentMode,
2754
+ advertised: this.providerCapabilities.get(provider)?.advertised
2755
+ ?? (client instanceof AcpClient ? client.advertised : undefined),
2725
2756
  // Preserve any MCP status already accumulated from startupStatus events.
2726
2757
  mcpServers: runtimeCapabilities?.mcpServers ?? this.providerCapabilities.get(provider)?.mcpServers,
2727
2758
  };
@@ -2745,7 +2776,12 @@ export class AgentWorkspaceProxy {
2745
2776
  commands: mergeCommands(runtimeCapabilities.commands, skillCommands),
2746
2777
  };
2747
2778
  }
2748
- this.providerCapabilities.set(provider, runtimeCapabilities);
2779
+ this.providerCapabilities.set(provider, {
2780
+ ...runtimeCapabilities,
2781
+ advertised: runtimeCapabilities.advertised
2782
+ ?? this.providerCapabilities.get(provider)?.advertised
2783
+ ?? (client instanceof AcpClient ? client.advertised : undefined),
2784
+ });
2749
2785
  }
2750
2786
  if (listedMcp.length > 0) this.mergeMcpServers(provider, listedMcp);
2751
2787
  }
@@ -3026,11 +3062,23 @@ export class AgentWorkspaceProxy {
3026
3062
  const client = this.clients.get(provider);
3027
3063
  const protocol = this.agentProtocols.get(provider);
3028
3064
  const runtimeCapabilities = this.providerCapabilities.get(provider);
3065
+ const advertised = runtimeCapabilities?.advertised
3066
+ ?? (client instanceof AcpClient ? client.advertised : undefined);
3029
3067
  const enabled = Boolean(client);
3030
- const supportsImages = enabled && protocolSupportsImages(protocol);
3068
+ const isAcp = protocol === "acp";
3069
+ const supportsImages = enabled && protocolSupportsImages(protocol, advertised);
3031
3070
  const isClaudeFallback = protocol === "claude-stream-json";
3032
3071
  const supportsPermission = enabled && !isClaudeFallback;
3033
3072
  const supportsReasoningEffort = enabled && !isClaudeFallback;
3073
+ const supportsSessionList = enabled && (isAcp ? Boolean(advertised?.listSession) : !isClaudeFallback);
3074
+ const supportsSessionLoad = enabled && (isAcp ? Boolean(advertised?.loadSession) : !isClaudeFallback);
3075
+ const supportsCancel = enabled && (isAcp ? Boolean(advertised?.cancel) : true);
3076
+ const supportsFork = enabled && (isAcp
3077
+ ? Boolean(advertised?.forkSession)
3078
+ : protocol === "codex-app-server" || protocol === "claude-agent-sdk");
3079
+ const supportsSetModel = enabled && (isAcp
3080
+ ? Boolean(advertised?.setModel)
3081
+ : protocol === "codex-app-server");
3034
3082
  const protocolBacked = isProtocolAgentProvider(provider);
3035
3083
  const commands = mergeCommands(
3036
3084
  defaultProviderCommands(provider, this.input.cwd, enabled),
@@ -3048,8 +3096,8 @@ export class AgentWorkspaceProxy {
3048
3096
  : `${providerLabel(provider)} 已在本机检测;远程对话请在终端继续`,
3049
3097
  supportsImages,
3050
3098
  supportsPermission,
3051
- supportsPlan: enabled,
3052
- supportsCancel: enabled,
3099
+ supportsPlan: enabled && (isAcp ? Boolean(advertised?.embeddedContext) : true),
3100
+ supportsCancel,
3053
3101
  models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
3054
3102
  defaultModel: runtimeCapabilities?.defaultModel ?? "default",
3055
3103
  reasoningEfforts: supportsReasoningEffort
@@ -3064,11 +3112,17 @@ export class AgentWorkspaceProxy {
3064
3112
  currentMode,
3065
3113
  features: {
3066
3114
  images: supportsImages,
3115
+ audio: Boolean(advertised?.audio),
3067
3116
  permissions: supportsPermission,
3068
- plan: enabled,
3069
- cancel: enabled,
3117
+ plan: enabled && (protocol === "acp" ? Boolean(advertised?.embeddedContext) : true),
3118
+ cancel: supportsCancel,
3070
3119
  reasoningEffort: supportsReasoningEffort,
3071
3120
  streamJsonFallback: isClaudeFallback,
3121
+ loadSession: supportsSessionLoad,
3122
+ sessionList: supportsSessionList,
3123
+ sessionFork: supportsFork,
3124
+ setModel: supportsSetModel,
3125
+ mcp: Boolean(advertised?.mcp) || Boolean(runtimeCapabilities?.mcpServers && runtimeCapabilities.mcpServers.size > 0),
3072
3126
  },
3073
3127
  mcpServers: runtimeCapabilities?.mcpServers && runtimeCapabilities.mcpServers.size > 0
3074
3128
  ? [...runtimeCapabilities.mcpServers.values()]
@@ -3087,13 +3141,13 @@ export class AgentWorkspaceProxy {
3087
3141
  protocolVersion: 1,
3088
3142
  workspaceProtocolVersion: 2,
3089
3143
  error: anyEnabled ? undefined : "没有可远程驱动的 Agent。请安装 Claude Code 或 Codex CLI;其它 Agent 只要在本机终端运行就会出现在列表里。",
3090
- supportsSessionList: anyEnabled,
3091
- supportsSessionLoad: anyEnabled,
3144
+ supportsSessionList: providers.some((p) => p.features.sessionList),
3145
+ supportsSessionLoad: providers.some((p) => p.features.loadSession),
3092
3146
  supportsImages: providers.some((p) => p.supportsImages),
3093
- supportsAudio: false,
3147
+ supportsAudio: providers.some((p) => p.features.audio),
3094
3148
  supportsPermission: anyPermission,
3095
- supportsPlan: anyEnabled,
3096
- supportsCancel: anyEnabled,
3149
+ supportsPlan: providers.some((p) => p.supportsPlan),
3150
+ supportsCancel: providers.some((p) => p.supportsCancel),
3097
3151
  },
3098
3152
  }));
3099
3153
  }
@@ -3207,7 +3261,7 @@ export class AgentWorkspaceProxy {
3207
3261
  } else {
3208
3262
  this.emitNotice({ kind: "warning", title: "分叉失败", detail: "无法复制该会话的历史记录,已新建空会话。" });
3209
3263
  }
3210
- } else if (provider === "codex" && typeof forkThread === "function" && sourceSessionId) {
3264
+ } else if (typeof forkThread === "function" && sourceSessionId) {
3211
3265
  if (source?.cwd && !payload.cwd) cwd = source.cwd;
3212
3266
  try {
3213
3267
  const forked = await forkThread.call(client, {
@@ -3497,7 +3551,9 @@ export class AgentWorkspaceProxy {
3497
3551
  }
3498
3552
 
3499
3553
  const protocol = this.protocolForProvider(conversation.provider);
3500
- if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
3554
+ const advertised = this.providerCapabilities.get(conversation.provider)?.advertised
3555
+ ?? (client instanceof AcpClient ? client.advertised : undefined);
3556
+ if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol, advertised)) {
3501
3557
  this.rejectAgentAction(
3502
3558
  conversation,
3503
3559
  "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
@@ -4589,32 +4645,96 @@ export class AgentWorkspaceProxy {
4589
4645
 
4590
4646
  private handleSessionUpdate(params: unknown): void {
4591
4647
  const raw = asRecord(params) ?? {};
4592
- const nested = asRecord(raw.params) ?? {};
4648
+ const update = asRecord(raw.update) ?? raw;
4649
+ const conversationId = this.conversationIdFromParams(raw)
4650
+ ?? this.conversationIdFromParams(update)
4651
+ ?? this.fallbackConversationId();
4652
+ if (!conversationId) return;
4653
+ const kind = normalizedIdentifier(firstString(update, ["sessionUpdate", "session_update", "type"]));
4654
+
4655
+ if (kind === "agentthoughtchunk" || kind === "agentthought") {
4656
+ const contentBlock = asRecord(update.content);
4657
+ const thought = (typeof contentBlock?.text === "string" ? contentBlock.text : undefined)
4658
+ ?? firstString(update, ["delta", "text", "content", "message"]);
4659
+ if (!thought) return;
4660
+ this.handleReasoningDelta({
4661
+ ...raw,
4662
+ itemId: firstString(update, ["itemId", "id"]) ?? "thinking",
4663
+ delta: thought,
4664
+ });
4665
+ return;
4666
+ }
4667
+
4668
+ if (kind === "toolcall" || kind === "toolcallupdate") {
4669
+ const toolCallId = firstString(update, ["toolCallId", "id"]) ?? id("tool");
4670
+ const existing = this.findTool(conversationId, toolCallId);
4671
+ const statusRaw = firstString(update, ["status"]);
4672
+ const status: AgentToolCall["status"] =
4673
+ statusRaw === "in_progress" ? "running"
4674
+ : statusRaw === "completed" || statusRaw === "failed" || statusRaw === "pending" || statusRaw === "running"
4675
+ ? statusRaw
4676
+ : existing?.status ?? "running";
4677
+ const input = stringifyDefined(update.rawInput ?? update.input);
4678
+ const output = stringifyDefined(update.rawOutput ?? update.output);
4679
+ this.upsertTool(conversationId, {
4680
+ id: toolCallId,
4681
+ name: firstString(update, ["title", "kind", "name"]) ?? existing?.name ?? "tool",
4682
+ ...(input ? { input } : {}),
4683
+ ...(output ? { output } : {}),
4684
+ createdAt: existing?.createdAt ?? Date.now(),
4685
+ status,
4686
+ });
4687
+ return;
4688
+ }
4689
+
4690
+ if (kind === "plan") {
4691
+ const entries = Array.isArray(update.entries) ? update.entries : [];
4692
+ this.handlePlanUpdated({
4693
+ ...raw,
4694
+ plan: entries.map((entry) => {
4695
+ const step = asRecord(entry) ?? {};
4696
+ return {
4697
+ text: firstString(step, ["content", "text", "title", "description"]),
4698
+ status: step.status,
4699
+ };
4700
+ }),
4701
+ });
4702
+ return;
4703
+ }
4704
+
4705
+ const nested = asRecord(raw.params) ?? asRecord(update.params) ?? {};
4706
+ const contentBlock = asRecord(update.content);
4593
4707
  const text =
4708
+ firstString(update, ["delta", "text", "content", "message"]) ??
4594
4709
  firstString(raw, ["delta", "text", "content", "message"]) ??
4595
- firstString(nested, ["delta", "text", "content", "message"]);
4596
- const content = contentBlocksFromItem(raw);
4597
- if (!text && content.length === 0) return;
4598
- const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
4599
- if (!conversationId) return;
4600
- if (firstString(raw, ["toolName", "tool", "name"])) {
4710
+ firstString(nested, ["delta", "text", "content", "message"]) ??
4711
+ (typeof contentBlock?.text === "string" ? contentBlock.text : undefined);
4712
+ const content = contentBlocksFromItem(update).length > 0
4713
+ ? contentBlocksFromItem(update)
4714
+ : contentBlocksFromItem(raw);
4715
+ if (firstString(raw, ["toolName", "tool", "name"]) || firstString(update, ["toolName", "tool", "name"])) {
4601
4716
  this.upsertTool(conversationId, {
4602
- id: firstString(raw, ["toolCallId", "callId", "id"]) ?? id("tool"),
4603
- name: firstString(raw, ["toolName", "tool", "name"]) ?? "tool",
4604
- input: stringify(raw.input ?? raw.toolInput ?? ""),
4605
- output: stringify(raw.output ?? raw.result ?? ""),
4717
+ id: firstString(update, ["toolCallId", "callId", "id"])
4718
+ ?? firstString(raw, ["toolCallId", "callId", "id"])
4719
+ ?? id("tool"),
4720
+ name: firstString(update, ["toolName", "tool", "name"])
4721
+ ?? firstString(raw, ["toolName", "tool", "name"])
4722
+ ?? "tool",
4723
+ input: stringify(update.input ?? update.toolInput ?? raw.input ?? raw.toolInput ?? ""),
4724
+ output: stringify(update.output ?? update.result ?? raw.output ?? raw.result ?? ""),
4606
4725
  createdAt: Date.now(),
4607
- status: raw.status === "completed" || raw.status === "failed" || raw.status === "running"
4608
- ? raw.status
4726
+ status: update.status === "completed" || raw.status === "completed" || update.status === "failed" || raw.status === "failed" || update.status === "running" || raw.status === "running"
4727
+ ? ((update.status ?? raw.status) as AgentToolCall["status"])
4609
4728
  : "running",
4610
4729
  });
4611
4730
  return;
4612
4731
  }
4732
+ if (!text && content.length === 0) return;
4613
4733
  const role = raw.role === "user" || raw.role === "system" ? raw.role : "assistant";
4614
4734
  const blocks = content.length > 0 ? content : [{ type: "text" as const, text }];
4615
4735
  const preview = textFromBlocks(blocks);
4616
4736
  this.upsertItem(conversationId, {
4617
- id: firstString(raw, ["messageId", "id"]) ?? id("msg"),
4737
+ id: firstString(update, ["messageId", "id"]) ?? firstString(raw, ["messageId", "id"]) ?? id("msg"),
4618
4738
  conversationId,
4619
4739
  type: "message",
4620
4740
  role,
@@ -4622,7 +4742,7 @@ export class AgentWorkspaceProxy {
4622
4742
  text: preview,
4623
4743
  createdAt: Date.now(),
4624
4744
  updatedAt: Date.now(),
4625
- isStreaming: raw.done === false || raw.isStreaming === true,
4745
+ isStreaming: raw.done === false || raw.isStreaming === true || kind === "agentmessagechunk",
4626
4746
  });
4627
4747
  this.updateConversationPreview(conversationId, preview || "图片附件", raw.done === true ? "idle" : "running");
4628
4748
  }
@@ -31,7 +31,8 @@ export function resolveAgentCommand(input: {
31
31
  provider: input.provider,
32
32
  command: explicit,
33
33
  protocol: isCodexAppServer ? "codex-app-server" : isClaudeCli ? "claude-stream-json" : "acp",
34
- framing: isCodexAppServer || isClaudeCli ? "newline" : "content-length",
34
+ // Gemini --acp, cursor-agent acp, and other ACP CLIs speak NDJSON, not LSP Content-Length.
35
+ framing: "newline",
35
36
  };
36
37
  }
37
38
 
@@ -61,7 +62,25 @@ export function resolveAgentCommand(input: {
61
62
  };
62
63
  }
63
64
 
64
- // custom: caller must provide --agent-command
65
+ if (input.provider === "gemini") {
66
+ return {
67
+ provider: "gemini",
68
+ command: "gemini --acp",
69
+ protocol: "acp",
70
+ framing: "newline",
71
+ };
72
+ }
73
+
74
+ if (input.provider === "cursor") {
75
+ return {
76
+ provider: "cursor",
77
+ command: "cursor-agent acp",
78
+ protocol: "acp",
79
+ framing: "newline",
80
+ };
81
+ }
82
+
83
+ // copilot / opencode / kimi / custom: PTY-only unless caller passes --agent-command
65
84
  return null;
66
85
  }
67
86