linkshell-cli 0.2.108 → 0.2.110
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 +5 -0
- package/dist/cli/src/runtime/acp/acp-client.js +16 -0
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +3 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +414 -3
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -0
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +2 -0
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.js +2 -1
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +1573 -448
- package/dist/shared-protocol/src/index.js +34 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/acp/acp-client.ts +19 -0
- package/src/runtime/acp/agent-workspace.ts +489 -2
- package/src/runtime/acp/claude-sdk-client.ts +1 -0
- package/src/runtime/acp/claude-stream-json-client.ts +2 -0
- package/src/runtime/bridge-session.ts +2 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, join, relative } from "node:path";
|
|
2
4
|
import {
|
|
3
5
|
createEnvelope,
|
|
4
6
|
parseTypedPayload,
|
|
@@ -12,6 +14,9 @@ import { resolveAgentCommand } from "./provider-resolver.js";
|
|
|
12
14
|
|
|
13
15
|
type AgentStatus = "unavailable" | "idle" | "running" | "waiting_permission" | "error";
|
|
14
16
|
type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
|
|
17
|
+
type AgentCollaborationMode = "default" | "plan";
|
|
18
|
+
type AgentCommandExecutionKind = "prompt" | "native" | "local_ui";
|
|
19
|
+
type AgentCommandSource = "built_in" | "custom" | "project" | "user" | "linkshell";
|
|
15
20
|
|
|
16
21
|
interface AgentContentBlock {
|
|
17
22
|
type: "text" | "image";
|
|
@@ -124,6 +129,27 @@ interface AgentSubagentAction {
|
|
|
124
129
|
agentStates: Record<string, AgentSubagentState>;
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
interface AgentCommandDescriptor {
|
|
133
|
+
id: string;
|
|
134
|
+
name: string;
|
|
135
|
+
title: string;
|
|
136
|
+
description?: string;
|
|
137
|
+
provider?: AgentProvider;
|
|
138
|
+
source: AgentCommandSource;
|
|
139
|
+
category?: string;
|
|
140
|
+
argsMode: "none" | "optional" | "required" | "raw";
|
|
141
|
+
requiresIdle?: boolean;
|
|
142
|
+
destructive?: boolean;
|
|
143
|
+
disabledReason?: string;
|
|
144
|
+
executionKind: AgentCommandExecutionKind;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface AgentModeDescriptor {
|
|
148
|
+
id: string;
|
|
149
|
+
title: string;
|
|
150
|
+
description?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
interface AgentConversation {
|
|
128
154
|
id: string;
|
|
129
155
|
agentSessionId?: string;
|
|
@@ -133,6 +159,7 @@ interface AgentConversation {
|
|
|
133
159
|
model?: string;
|
|
134
160
|
reasoningEffort?: string;
|
|
135
161
|
permissionMode?: AgentPermissionMode;
|
|
162
|
+
collaborationMode?: AgentCollaborationMode;
|
|
136
163
|
status: AgentStatus;
|
|
137
164
|
archived: boolean;
|
|
138
165
|
lastMessagePreview?: string;
|
|
@@ -688,10 +715,241 @@ interface ProviderRuntimeCapabilities {
|
|
|
688
715
|
models?: AgentModelOption[];
|
|
689
716
|
defaultModel?: string;
|
|
690
717
|
reasoningEfforts?: string[];
|
|
718
|
+
commands?: AgentCommandDescriptor[];
|
|
719
|
+
modes?: AgentModeDescriptor[];
|
|
720
|
+
currentMode?: string;
|
|
691
721
|
}
|
|
692
722
|
|
|
693
723
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
694
724
|
const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
|
|
725
|
+
const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"] as const;
|
|
726
|
+
const CLAUDE_BUILT_IN_COMMANDS: Array<{ name: string; description: string; argsMode?: AgentCommandDescriptor["argsMode"]; destructive?: boolean }> = [
|
|
727
|
+
{ name: "add-dir", description: "Add additional working directories" },
|
|
728
|
+
{ name: "agents", description: "Manage subagents" },
|
|
729
|
+
{ name: "bug", description: "Report a Claude Code bug" },
|
|
730
|
+
{ name: "clear", description: "Clear conversation context", argsMode: "none", destructive: true },
|
|
731
|
+
{ name: "compact", description: "Compact conversation history" },
|
|
732
|
+
{ name: "config", description: "Open configuration" },
|
|
733
|
+
{ name: "cost", description: "Show usage cost" },
|
|
734
|
+
{ name: "doctor", description: "Check Claude Code health" },
|
|
735
|
+
{ name: "exit", description: "Exit Claude Code", argsMode: "none", destructive: true },
|
|
736
|
+
{ name: "export", description: "Export conversation" },
|
|
737
|
+
{ name: "help", description: "Show help" },
|
|
738
|
+
{ name: "ide", description: "Manage IDE integration" },
|
|
739
|
+
{ name: "init", description: "Create or update CLAUDE.md" },
|
|
740
|
+
{ name: "login", description: "Sign in" },
|
|
741
|
+
{ name: "logout", description: "Sign out" },
|
|
742
|
+
{ name: "mcp", description: "Manage MCP servers" },
|
|
743
|
+
{ name: "memory", description: "Edit memory files" },
|
|
744
|
+
{ name: "model", description: "Switch model" },
|
|
745
|
+
{ name: "permissions", description: "Manage permissions" },
|
|
746
|
+
{ name: "pr-comments", description: "Fetch PR comments" },
|
|
747
|
+
{ name: "release-notes", description: "Show release notes" },
|
|
748
|
+
{ name: "resume", description: "Resume a conversation" },
|
|
749
|
+
{ name: "review", description: "Review local changes" },
|
|
750
|
+
{ name: "security-review", description: "Run a security review" },
|
|
751
|
+
{ name: "status", description: "Show status" },
|
|
752
|
+
{ name: "statusline", description: "Configure status line" },
|
|
753
|
+
{ name: "terminal-setup", description: "Configure terminal integration" },
|
|
754
|
+
{ name: "upgrade", description: "Upgrade Claude Code" },
|
|
755
|
+
{ name: "vim", description: "Toggle vim mode" },
|
|
756
|
+
];
|
|
757
|
+
|
|
758
|
+
function commandId(provider: AgentProvider, name: string, source: AgentCommandSource = "built_in"): string {
|
|
759
|
+
return `${provider}:${source}:${name.replace(/^\/+/, "")}`;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function commandTitle(name: string): string {
|
|
763
|
+
return `/${name.replace(/^\/+/, "")}`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function makeCommand(input: {
|
|
767
|
+
provider: AgentProvider;
|
|
768
|
+
name: string;
|
|
769
|
+
description?: string;
|
|
770
|
+
source?: AgentCommandSource;
|
|
771
|
+
category?: string;
|
|
772
|
+
argsMode?: AgentCommandDescriptor["argsMode"];
|
|
773
|
+
requiresIdle?: boolean;
|
|
774
|
+
destructive?: boolean;
|
|
775
|
+
disabledReason?: string;
|
|
776
|
+
executionKind?: AgentCommandExecutionKind;
|
|
777
|
+
}): AgentCommandDescriptor {
|
|
778
|
+
const cleanName = input.name.replace(/^\/+/, "");
|
|
779
|
+
const source = input.source ?? "built_in";
|
|
780
|
+
return {
|
|
781
|
+
id: commandId(input.provider, cleanName, source),
|
|
782
|
+
name: cleanName,
|
|
783
|
+
title: commandTitle(cleanName),
|
|
784
|
+
description: input.description,
|
|
785
|
+
provider: input.provider,
|
|
786
|
+
source,
|
|
787
|
+
category: input.category,
|
|
788
|
+
argsMode: input.argsMode ?? "optional",
|
|
789
|
+
requiresIdle: input.requiresIdle,
|
|
790
|
+
destructive: input.destructive,
|
|
791
|
+
disabledReason: input.disabledReason,
|
|
792
|
+
executionKind: input.executionKind ?? "prompt",
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function commandFromMarkdownFile(provider: AgentProvider, root: string, filePath: string, source: AgentCommandSource): AgentCommandDescriptor | undefined {
|
|
797
|
+
if (!filePath.endsWith(".md")) return undefined;
|
|
798
|
+
const rel = relative(root, filePath).replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
799
|
+
const name = rel.split("/").filter(Boolean).join(":");
|
|
800
|
+
if (!name) return undefined;
|
|
801
|
+
let description: string | undefined;
|
|
802
|
+
try {
|
|
803
|
+
const text = readFileSync(filePath, "utf8");
|
|
804
|
+
description = text.split(/\r?\n/).map((line) => line.trim()).find((line) => line && !line.startsWith("---"))?.slice(0, 160);
|
|
805
|
+
} catch {
|
|
806
|
+
description = undefined;
|
|
807
|
+
}
|
|
808
|
+
return makeCommand({
|
|
809
|
+
provider,
|
|
810
|
+
name,
|
|
811
|
+
description: description || "Custom Claude command",
|
|
812
|
+
source,
|
|
813
|
+
category: source === "project" ? "Project commands" : "User commands",
|
|
814
|
+
argsMode: "raw",
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function walkMarkdownCommands(provider: AgentProvider, root: string, source: AgentCommandSource): AgentCommandDescriptor[] {
|
|
819
|
+
if (!existsSync(root)) return [];
|
|
820
|
+
const result: AgentCommandDescriptor[] = [];
|
|
821
|
+
const walk = (dir: string) => {
|
|
822
|
+
let entries: string[] = [];
|
|
823
|
+
try {
|
|
824
|
+
entries = readdirSync(dir);
|
|
825
|
+
} catch {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
for (const entry of entries) {
|
|
829
|
+
const path = join(dir, entry);
|
|
830
|
+
let stat;
|
|
831
|
+
try {
|
|
832
|
+
stat = statSync(path);
|
|
833
|
+
} catch {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (stat.isDirectory()) walk(path);
|
|
837
|
+
else if (stat.isFile()) {
|
|
838
|
+
const command = commandFromMarkdownFile(provider, root, path, source);
|
|
839
|
+
if (command) result.push(command);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
walk(root);
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function customClaudeCommands(cwd: string): AgentCommandDescriptor[] {
|
|
848
|
+
const projectCommands = walkMarkdownCommands("claude", join(cwd, ".claude", "commands"), "project");
|
|
849
|
+
const userCommands = walkMarkdownCommands("claude", join(homedir(), ".claude", "commands"), "user");
|
|
850
|
+
return [...projectCommands, ...userCommands];
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function defaultProviderCommands(provider: AgentProvider, cwd: string, enabled: boolean): AgentCommandDescriptor[] {
|
|
854
|
+
const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
|
|
855
|
+
if (provider === "codex") {
|
|
856
|
+
return CODEX_COMMAND_NAMES.map((name) => makeCommand({
|
|
857
|
+
provider,
|
|
858
|
+
name,
|
|
859
|
+
source: "linkshell",
|
|
860
|
+
category: name === "plan" || name === "exit-plan" ? "Modes" : "Codex",
|
|
861
|
+
description: {
|
|
862
|
+
"plan": "Enter Codex plan mode",
|
|
863
|
+
"exit-plan": "Exit Codex plan mode",
|
|
864
|
+
compact: "Compact the current thread",
|
|
865
|
+
clear: "Start a fresh Codex thread",
|
|
866
|
+
status: "Show LinkShell agent status",
|
|
867
|
+
review: "Ask Codex to review local changes",
|
|
868
|
+
subagents: "Insert a delegation prompt",
|
|
869
|
+
}[name],
|
|
870
|
+
argsMode: name === "review" || name === "subagents" ? "optional" : "none",
|
|
871
|
+
destructive: name === "clear",
|
|
872
|
+
disabledReason,
|
|
873
|
+
executionKind: name === "review" || name === "subagents" ? "prompt" : "native",
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
if (provider === "claude") {
|
|
877
|
+
const builtIns = CLAUDE_BUILT_IN_COMMANDS.map((entry) => makeCommand({
|
|
878
|
+
provider,
|
|
879
|
+
name: entry.name,
|
|
880
|
+
description: entry.description,
|
|
881
|
+
argsMode: entry.argsMode,
|
|
882
|
+
destructive: entry.destructive,
|
|
883
|
+
disabledReason,
|
|
884
|
+
executionKind: "prompt",
|
|
885
|
+
}));
|
|
886
|
+
const custom = customClaudeCommands(cwd).map((command) => ({
|
|
887
|
+
...command,
|
|
888
|
+
disabledReason: command.disabledReason ?? disabledReason,
|
|
889
|
+
}));
|
|
890
|
+
return [...builtIns, ...custom];
|
|
891
|
+
}
|
|
892
|
+
return [
|
|
893
|
+
makeCommand({
|
|
894
|
+
provider,
|
|
895
|
+
name: "status",
|
|
896
|
+
source: "linkshell",
|
|
897
|
+
category: "LinkShell",
|
|
898
|
+
description: "Show LinkShell agent status",
|
|
899
|
+
argsMode: "none",
|
|
900
|
+
disabledReason,
|
|
901
|
+
executionKind: "native",
|
|
902
|
+
}),
|
|
903
|
+
];
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>): AgentCommandDescriptor[] {
|
|
907
|
+
const map = new Map<string, AgentCommandDescriptor>();
|
|
908
|
+
for (const group of groups) {
|
|
909
|
+
for (const command of group ?? []) {
|
|
910
|
+
const key = `${command.provider ?? ""}:${command.name}`;
|
|
911
|
+
map.set(key, { ...map.get(key), ...command });
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function runtimeCommands(provider: AgentProvider, value: unknown): AgentCommandDescriptor[] {
|
|
918
|
+
const raw = asRecord(value);
|
|
919
|
+
const commandsValue =
|
|
920
|
+
Array.isArray(value) ? value :
|
|
921
|
+
Array.isArray(raw?.commands) ? raw.commands :
|
|
922
|
+
Array.isArray(raw?.slashCommands) ? raw.slashCommands :
|
|
923
|
+
Array.isArray(raw?.slash_commands) ? raw.slash_commands :
|
|
924
|
+
Array.isArray(raw?.available_commands) ? raw.available_commands :
|
|
925
|
+
[];
|
|
926
|
+
return commandsValue
|
|
927
|
+
.map((entry) => {
|
|
928
|
+
if (typeof entry === "string") {
|
|
929
|
+
return makeCommand({
|
|
930
|
+
provider,
|
|
931
|
+
name: entry,
|
|
932
|
+
description: undefined,
|
|
933
|
+
source: "built_in",
|
|
934
|
+
argsMode: "raw",
|
|
935
|
+
executionKind: "prompt",
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
const record = asRecord(entry);
|
|
939
|
+
const name = firstString(record, ["name", "command", "id"]);
|
|
940
|
+
if (!name) return undefined;
|
|
941
|
+
return makeCommand({
|
|
942
|
+
provider,
|
|
943
|
+
name,
|
|
944
|
+
description: firstString(record, ["description", "summary"]),
|
|
945
|
+
source: "built_in",
|
|
946
|
+
category: firstString(record, ["category", "group"]),
|
|
947
|
+
argsMode: "raw",
|
|
948
|
+
executionKind: "prompt",
|
|
949
|
+
});
|
|
950
|
+
})
|
|
951
|
+
.filter((entry): entry is AgentCommandDescriptor => Boolean(entry));
|
|
952
|
+
}
|
|
695
953
|
|
|
696
954
|
function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities | undefined {
|
|
697
955
|
const raw = asRecord(value);
|
|
@@ -853,6 +1111,11 @@ export class AgentWorkspaceProxy {
|
|
|
853
1111
|
await this.sendPrompt(payload);
|
|
854
1112
|
break;
|
|
855
1113
|
}
|
|
1114
|
+
case "agent.v2.command.execute": {
|
|
1115
|
+
const payload = parseTypedPayload("agent.v2.command.execute", envelope.payload);
|
|
1116
|
+
await this.executeCommand(payload);
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
856
1119
|
case "agent.v2.cancel": {
|
|
857
1120
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
858
1121
|
const conversation = this.conversations.get(payload.conversationId);
|
|
@@ -1038,6 +1301,7 @@ export class AgentWorkspaceProxy {
|
|
|
1038
1301
|
model: remote.model ?? existing?.model,
|
|
1039
1302
|
reasoningEffort: existing?.reasoningEffort,
|
|
1040
1303
|
permissionMode: existing?.permissionMode,
|
|
1304
|
+
collaborationMode: existing?.collaborationMode,
|
|
1041
1305
|
status: existing?.status ?? "idle",
|
|
1042
1306
|
archived: existing?.archived ?? false,
|
|
1043
1307
|
lastMessagePreview: existing?.lastMessagePreview,
|
|
@@ -1066,6 +1330,11 @@ export class AgentWorkspaceProxy {
|
|
|
1066
1330
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
1067
1331
|
const supportsPermission = enabled && !isClaudeFallback;
|
|
1068
1332
|
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
1333
|
+
const commands = mergeCommands(
|
|
1334
|
+
defaultProviderCommands(provider, this.input.cwd, enabled),
|
|
1335
|
+
runtimeCapabilities?.commands,
|
|
1336
|
+
);
|
|
1337
|
+
const currentMode = [...this.conversations.values()].find((conversation) => conversation.provider === provider)?.collaborationMode;
|
|
1069
1338
|
return {
|
|
1070
1339
|
id: provider,
|
|
1071
1340
|
label: providerLabel(provider),
|
|
@@ -1081,6 +1350,12 @@ export class AgentWorkspaceProxy {
|
|
|
1081
1350
|
? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
|
|
1082
1351
|
: [],
|
|
1083
1352
|
permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
|
|
1353
|
+
commands,
|
|
1354
|
+
modes: runtimeCapabilities?.modes ?? (provider === "codex" ? [
|
|
1355
|
+
{ id: "default", title: "Default", description: "Run normal implementation turns" },
|
|
1356
|
+
{ id: "plan", title: "Plan", description: "Discuss and produce an implementation plan first" },
|
|
1357
|
+
] : []),
|
|
1358
|
+
currentMode,
|
|
1084
1359
|
features: {
|
|
1085
1360
|
images: supportsImages,
|
|
1086
1361
|
permissions: supportsPermission,
|
|
@@ -1122,6 +1397,7 @@ export class AgentWorkspaceProxy {
|
|
|
1122
1397
|
model?: string;
|
|
1123
1398
|
reasoningEffort?: string;
|
|
1124
1399
|
permissionMode?: AgentPermissionMode;
|
|
1400
|
+
collaborationMode?: AgentCollaborationMode;
|
|
1125
1401
|
title?: string;
|
|
1126
1402
|
}): Promise<AgentConversation | undefined> {
|
|
1127
1403
|
const provider = payload.provider ?? this.input.availableProviders[0];
|
|
@@ -1182,6 +1458,7 @@ export class AgentWorkspaceProxy {
|
|
|
1182
1458
|
model: payload.model ?? existingConversation?.model,
|
|
1183
1459
|
reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
|
|
1184
1460
|
permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
|
|
1461
|
+
collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
|
|
1185
1462
|
status: "idle",
|
|
1186
1463
|
archived: existingConversation?.archived ?? false,
|
|
1187
1464
|
lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
|
|
@@ -1212,6 +1489,7 @@ export class AgentWorkspaceProxy {
|
|
|
1212
1489
|
model?: string;
|
|
1213
1490
|
reasoningEffort?: string;
|
|
1214
1491
|
permissionMode?: AgentPermissionMode;
|
|
1492
|
+
collaborationMode?: AgentCollaborationMode;
|
|
1215
1493
|
title?: string;
|
|
1216
1494
|
},
|
|
1217
1495
|
message: string,
|
|
@@ -1227,6 +1505,7 @@ export class AgentWorkspaceProxy {
|
|
|
1227
1505
|
model: payload.model,
|
|
1228
1506
|
reasoningEffort: payload.reasoningEffort,
|
|
1229
1507
|
permissionMode: payload.permissionMode,
|
|
1508
|
+
collaborationMode: payload.collaborationMode,
|
|
1230
1509
|
status: "error",
|
|
1231
1510
|
archived: false,
|
|
1232
1511
|
lastMessagePreview: message,
|
|
@@ -1257,6 +1536,7 @@ export class AgentWorkspaceProxy {
|
|
|
1257
1536
|
model?: string;
|
|
1258
1537
|
reasoningEffort?: string;
|
|
1259
1538
|
permissionMode?: AgentPermissionMode;
|
|
1539
|
+
collaborationMode?: AgentCollaborationMode;
|
|
1260
1540
|
}): Promise<void> {
|
|
1261
1541
|
const conversation =
|
|
1262
1542
|
this.conversations.get(payload.conversationId) ??
|
|
@@ -1283,6 +1563,7 @@ export class AgentWorkspaceProxy {
|
|
|
1283
1563
|
conversation.model = payload.model ?? conversation.model;
|
|
1284
1564
|
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1285
1565
|
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1566
|
+
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
1286
1567
|
conversation.status = "running";
|
|
1287
1568
|
conversation.lastActivityAt = Date.now();
|
|
1288
1569
|
this.activeConversationId = conversation.id;
|
|
@@ -1307,6 +1588,7 @@ export class AgentWorkspaceProxy {
|
|
|
1307
1588
|
model: payload.model,
|
|
1308
1589
|
reasoningEffort: payload.reasoningEffort,
|
|
1309
1590
|
permissionMode: payload.permissionMode,
|
|
1591
|
+
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
1310
1592
|
cwd: conversation.cwd,
|
|
1311
1593
|
});
|
|
1312
1594
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
@@ -1333,6 +1615,196 @@ export class AgentWorkspaceProxy {
|
|
|
1333
1615
|
}
|
|
1334
1616
|
}
|
|
1335
1617
|
|
|
1618
|
+
private commandForConversation(conversation: AgentConversation, commandId: string): AgentCommandDescriptor | undefined {
|
|
1619
|
+
const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
|
|
1620
|
+
const commands = mergeCommands(
|
|
1621
|
+
defaultProviderCommands(conversation.provider, conversation.cwd, true),
|
|
1622
|
+
runtimeCapabilities?.commands,
|
|
1623
|
+
);
|
|
1624
|
+
return commands.find((command) =>
|
|
1625
|
+
command.id === commandId ||
|
|
1626
|
+
command.name === commandId ||
|
|
1627
|
+
`/${command.name}` === commandId
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
private async executeCommand(payload: {
|
|
1632
|
+
conversationId: string;
|
|
1633
|
+
commandId: string;
|
|
1634
|
+
rawText?: string;
|
|
1635
|
+
args?: string;
|
|
1636
|
+
clientMessageId: string;
|
|
1637
|
+
}): Promise<void> {
|
|
1638
|
+
const conversation =
|
|
1639
|
+
this.conversations.get(payload.conversationId) ??
|
|
1640
|
+
await this.openConversation({ conversationId: payload.conversationId });
|
|
1641
|
+
if (!conversation || !conversation.agentSessionId) return;
|
|
1642
|
+
|
|
1643
|
+
const command = this.commandForConversation(conversation, payload.commandId);
|
|
1644
|
+
if (!command) {
|
|
1645
|
+
this.addItem(conversation.id, {
|
|
1646
|
+
id: id("error"),
|
|
1647
|
+
conversationId: conversation.id,
|
|
1648
|
+
type: "error",
|
|
1649
|
+
error: `未知命令:${payload.commandId}`,
|
|
1650
|
+
createdAt: Date.now(),
|
|
1651
|
+
});
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (command.disabledReason) {
|
|
1656
|
+
this.addItem(conversation.id, {
|
|
1657
|
+
id: id("error"),
|
|
1658
|
+
conversationId: conversation.id,
|
|
1659
|
+
type: "error",
|
|
1660
|
+
error: command.disabledReason,
|
|
1661
|
+
createdAt: Date.now(),
|
|
1662
|
+
});
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const rawText = payload.rawText?.trim() || `/${command.name}${payload.args?.trim() ? ` ${payload.args.trim()}` : ""}`;
|
|
1667
|
+
if (command.executionKind === "prompt") {
|
|
1668
|
+
await this.sendPrompt({
|
|
1669
|
+
conversationId: conversation.id,
|
|
1670
|
+
clientMessageId: payload.clientMessageId,
|
|
1671
|
+
contentBlocks: [{ type: "text", text: rawText }],
|
|
1672
|
+
model: conversation.model,
|
|
1673
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1674
|
+
permissionMode: conversation.permissionMode,
|
|
1675
|
+
collaborationMode: conversation.collaborationMode,
|
|
1676
|
+
});
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
this.addItem(conversation.id, {
|
|
1681
|
+
id: payload.clientMessageId,
|
|
1682
|
+
conversationId: conversation.id,
|
|
1683
|
+
type: "message",
|
|
1684
|
+
kind: "chat",
|
|
1685
|
+
role: "user",
|
|
1686
|
+
content: [{ type: "text", text: rawText }],
|
|
1687
|
+
text: rawText,
|
|
1688
|
+
metadata: { commandId: command.id, commandExecutionKind: command.executionKind },
|
|
1689
|
+
createdAt: Date.now(),
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
if (command.executionKind === "local_ui") {
|
|
1693
|
+
this.emitStatus(conversation.id, "idle", `${command.title} 已由移动端处理。`);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
await this.executeNativeCommand(conversation, command, payload.args?.trim());
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
private async executeNativeCommand(
|
|
1701
|
+
conversation: AgentConversation,
|
|
1702
|
+
command: AgentCommandDescriptor,
|
|
1703
|
+
args?: string,
|
|
1704
|
+
): Promise<void> {
|
|
1705
|
+
const client = this.clientForProvider(conversation.provider);
|
|
1706
|
+
const now = Date.now();
|
|
1707
|
+
try {
|
|
1708
|
+
if (command.name === "status") {
|
|
1709
|
+
this.emitStatus(
|
|
1710
|
+
conversation.id,
|
|
1711
|
+
conversation.status,
|
|
1712
|
+
`${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`,
|
|
1713
|
+
);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if (conversation.provider !== "codex") {
|
|
1718
|
+
this.addItem(conversation.id, {
|
|
1719
|
+
id: id("error"),
|
|
1720
|
+
conversationId: conversation.id,
|
|
1721
|
+
type: "error",
|
|
1722
|
+
error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
|
|
1723
|
+
createdAt: now,
|
|
1724
|
+
});
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
if (command.name === "plan" || command.name === "exit-plan") {
|
|
1729
|
+
conversation.collaborationMode = command.name === "plan" ? "plan" : "default";
|
|
1730
|
+
conversation.status = "idle";
|
|
1731
|
+
conversation.lastMessagePreview = command.name === "plan" ? "已进入 Plan mode" : "已退出 Plan mode";
|
|
1732
|
+
conversation.lastActivityAt = now;
|
|
1733
|
+
this.emitConversation(conversation);
|
|
1734
|
+
this.sendCapabilities();
|
|
1735
|
+
this.emitStatus(conversation.id, "idle", command.name === "plan"
|
|
1736
|
+
? "已进入 Plan mode。下一条消息会先制定计划。"
|
|
1737
|
+
: "已退出 Plan mode。");
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
if (command.name === "compact") {
|
|
1742
|
+
if (!(client instanceof AcpClient)) throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
1743
|
+
conversation.status = "running";
|
|
1744
|
+
this.emitConversation(conversation);
|
|
1745
|
+
this.addItem(conversation.id, {
|
|
1746
|
+
id: id("compact"),
|
|
1747
|
+
conversationId: conversation.id,
|
|
1748
|
+
type: "status",
|
|
1749
|
+
kind: "context_compaction",
|
|
1750
|
+
text: "正在压缩上下文",
|
|
1751
|
+
status: "running",
|
|
1752
|
+
isStreaming: true,
|
|
1753
|
+
createdAt: now,
|
|
1754
|
+
});
|
|
1755
|
+
await client.compact({ sessionId: conversation.agentSessionId! });
|
|
1756
|
+
this.updateConversationStatus(conversation.id, "idle", "上下文压缩完成");
|
|
1757
|
+
this.emitStatus(conversation.id, "idle", "上下文压缩完成。");
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
if (command.name === "clear") {
|
|
1762
|
+
if (!client) throw new Error("Agent provider 不在线。");
|
|
1763
|
+
const result = await client.newSession({ cwd: conversation.cwd });
|
|
1764
|
+
const nextAgentSessionId = this.extractSessionId(result) ?? id("agent-session");
|
|
1765
|
+
if (conversation.agentSessionId) this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
1766
|
+
conversation.agentSessionId = nextAgentSessionId;
|
|
1767
|
+
conversation.collaborationMode = "default";
|
|
1768
|
+
conversation.status = "idle";
|
|
1769
|
+
conversation.lastMessagePreview = "上下文已重置";
|
|
1770
|
+
conversation.lastActivityAt = now;
|
|
1771
|
+
this.conversationByAgentSessionId.set(nextAgentSessionId, conversation.id);
|
|
1772
|
+
this.timelines.set(conversation.id, []);
|
|
1773
|
+
this.emitConversation(conversation);
|
|
1774
|
+
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
if (command.name === "review" || command.name === "subagents") {
|
|
1779
|
+
const prompt = command.name === "review"
|
|
1780
|
+
? args || "Review the current local changes."
|
|
1781
|
+
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1782
|
+
await this.sendPrompt({
|
|
1783
|
+
conversationId: conversation.id,
|
|
1784
|
+
clientMessageId: id(command.name),
|
|
1785
|
+
contentBlocks: [{ type: "text", text: prompt }],
|
|
1786
|
+
model: conversation.model,
|
|
1787
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1788
|
+
permissionMode: conversation.permissionMode,
|
|
1789
|
+
collaborationMode: conversation.collaborationMode,
|
|
1790
|
+
});
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
throw new Error(`命令暂未实现:/${command.name}`);
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1797
|
+
this.updateConversationStatus(conversation.id, "error", message);
|
|
1798
|
+
this.addItem(conversation.id, {
|
|
1799
|
+
id: id("error"),
|
|
1800
|
+
conversationId: conversation.id,
|
|
1801
|
+
type: "error",
|
|
1802
|
+
error: message,
|
|
1803
|
+
createdAt: Date.now(),
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1336
1808
|
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
1337
1809
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1338
1810
|
return this.handleStructuredInput(params, true);
|
|
@@ -1353,8 +1825,23 @@ export class AgentWorkspaceProxy {
|
|
|
1353
1825
|
if (this.input.verbose) {
|
|
1354
1826
|
process.stderr.write(`[agent:v2] ${method} ${stringify(params).slice(0, 500)}\n`);
|
|
1355
1827
|
}
|
|
1828
|
+
if (method === "initialized") {
|
|
1829
|
+
const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
|
|
1830
|
+
const provider = conversationId ? this.conversations.get(conversationId)?.provider : this.input.availableProviders[0];
|
|
1831
|
+
if (provider) {
|
|
1832
|
+
const commands = runtimeCommands(provider, params);
|
|
1833
|
+
if (commands.length > 0) {
|
|
1834
|
+
const existing = this.providerCapabilities.get(provider);
|
|
1835
|
+
this.providerCapabilities.set(provider, {
|
|
1836
|
+
...(existing ?? {}),
|
|
1837
|
+
commands: mergeCommands(existing?.commands, commands),
|
|
1838
|
+
});
|
|
1839
|
+
this.sendCapabilities();
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1356
1844
|
if (
|
|
1357
|
-
method === "initialized" ||
|
|
1358
1845
|
method.startsWith("account/") ||
|
|
1359
1846
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
1360
1847
|
method === "thread/status/changed" ||
|
|
@@ -208,6 +208,7 @@ export class ClaudeSdkClient {
|
|
|
208
208
|
model?: string;
|
|
209
209
|
reasoningEffort?: string;
|
|
210
210
|
permissionMode?: AgentPermissionMode;
|
|
211
|
+
collaborationMode?: "default" | "plan";
|
|
211
212
|
cwd: string;
|
|
212
213
|
}): Promise<unknown> {
|
|
213
214
|
if (!this.query) await this.initialize();
|
|
@@ -130,6 +130,7 @@ export class ClaudeStreamJsonClient {
|
|
|
130
130
|
model?: string;
|
|
131
131
|
reasoningEffort?: string;
|
|
132
132
|
permissionMode?: AgentPermissionMode;
|
|
133
|
+
collaborationMode?: "default" | "plan";
|
|
133
134
|
cwd: string;
|
|
134
135
|
}): Promise<unknown> {
|
|
135
136
|
if (this.child) {
|
|
@@ -258,6 +259,7 @@ export class ClaudeStreamJsonClient {
|
|
|
258
259
|
model: event.model,
|
|
259
260
|
};
|
|
260
261
|
if (event.tools) initParams.tools = event.tools;
|
|
262
|
+
if (event.slash_commands) initParams.slashCommands = event.slash_commands;
|
|
261
263
|
if (event.mcp_servers) initParams.mcpServers = event.mcp_servers;
|
|
262
264
|
this.input.onNotification("initialized", initParams);
|
|
263
265
|
}
|
|
@@ -788,6 +788,7 @@ export class BridgeSession {
|
|
|
788
788
|
case "agent.v2.conversation.open":
|
|
789
789
|
case "agent.v2.conversation.list":
|
|
790
790
|
case "agent.v2.prompt":
|
|
791
|
+
case "agent.v2.command.execute":
|
|
791
792
|
case "agent.v2.cancel":
|
|
792
793
|
case "agent.v2.permission.respond":
|
|
793
794
|
case "agent.v2.structured_input.respond":
|
|
@@ -817,7 +818,7 @@ export class BridgeSession {
|
|
|
817
818
|
);
|
|
818
819
|
break;
|
|
819
820
|
}
|
|
820
|
-
if (envelope.type === "agent.v2.prompt") this.refreshAgentPermissionHooks();
|
|
821
|
+
if (envelope.type === "agent.v2.prompt" || envelope.type === "agent.v2.command.execute") this.refreshAgentPermissionHooks();
|
|
821
822
|
await this.agentWorkspace.handleEnvelope(envelope);
|
|
822
823
|
break;
|
|
823
824
|
}
|