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.
- package/dist/cli/src/runtime/acp/acp-client.d.ts +21 -0
- package/dist/cli/src/runtime/acp/acp-client.js +101 -5
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.js +5 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.js +146 -33
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.js +19 -2
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +0 -46
- package/dist/cli/src/runtime/bridge-session.js +10 -1034
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/hook-command.d.ts +7 -17
- package/dist/cli/src/runtime/hook-command.js +75 -26
- package/dist/cli/src/runtime/hook-command.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/runtime/acp/acp-client.ts +116 -5
- package/src/runtime/acp/agent-process-discovery.ts +3 -1
- package/src/runtime/acp/agent-workspace.ts +152 -32
- package/src/runtime/acp/provider-resolver.ts +21 -2
- package/src/runtime/bridge-session.ts +9 -1110
- package/src/runtime/hook-command.ts +68 -30
- package/web/assets/{FileBrowser-Coy7Szjx.js → FileBrowser-Db2LlqTw.js} +1 -1
- package/web/assets/{PortPreview-D-XovbV1.js → PortPreview-Cobi7sn0.js} +1 -1
- package/web/assets/{TerminalPanel-Cah56AtC.js → TerminalPanel-_Wve-C8s.js} +1 -1
- package/web/assets/{UsageDashboard-DfkjJagC.js → UsageDashboard-DmXct3sk.js} +1 -1
- package/web/assets/index-pnfD2xom.js +35 -0
- package/web/index.html +1 -1
- 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
|
-
|
|
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
|
|
251
|
-
return
|
|
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
|
-
|
|
254
|
-
|
|
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")
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
3091
|
-
supportsSessionLoad:
|
|
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:
|
|
3147
|
+
supportsAudio: providers.some((p) => p.features.audio),
|
|
3094
3148
|
supportsPermission: anyPermission,
|
|
3095
|
-
supportsPlan:
|
|
3096
|
-
supportsCancel:
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
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(
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|