linkshell-cli 0.2.109 → 0.2.111
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 +17 -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 +445 -6
- 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 +20 -0
- package/src/runtime/acp/agent-workspace.ts +521 -5
- 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];
|
|
@@ -1149,7 +1425,7 @@ export class AgentWorkspaceProxy {
|
|
|
1149
1425
|
(payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
1150
1426
|
(agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
|
|
1151
1427
|
|
|
1152
|
-
if (existingConversation && existingConversation.status !== "error") {
|
|
1428
|
+
if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
|
|
1153
1429
|
if (payload.conversationId && existingConversation.id !== payload.conversationId) {
|
|
1154
1430
|
existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
|
|
1155
1431
|
}
|
|
@@ -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,13 +1536,33 @@ 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) ??
|
|
1263
1543
|
await this.openConversation({ conversationId: payload.conversationId });
|
|
1264
|
-
if (!conversation
|
|
1544
|
+
if (!conversation) return;
|
|
1545
|
+
if (!conversation.agentSessionId) {
|
|
1546
|
+
this.addItem(payload.conversationId, {
|
|
1547
|
+
id: id("error"),
|
|
1548
|
+
conversationId: payload.conversationId,
|
|
1549
|
+
type: "error",
|
|
1550
|
+
error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
|
|
1551
|
+
createdAt: Date.now(),
|
|
1552
|
+
});
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1265
1555
|
const client = this.clientForProvider(conversation.provider);
|
|
1266
|
-
if (!client)
|
|
1556
|
+
if (!client) {
|
|
1557
|
+
this.addItem(conversation.id, {
|
|
1558
|
+
id: id("error"),
|
|
1559
|
+
conversationId: conversation.id,
|
|
1560
|
+
type: "error",
|
|
1561
|
+
error: `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
|
|
1562
|
+
createdAt: Date.now(),
|
|
1563
|
+
});
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1267
1566
|
|
|
1268
1567
|
const protocol = this.protocolForProvider(conversation.provider);
|
|
1269
1568
|
if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
|
|
@@ -1283,6 +1582,7 @@ export class AgentWorkspaceProxy {
|
|
|
1283
1582
|
conversation.model = payload.model ?? conversation.model;
|
|
1284
1583
|
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1285
1584
|
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1585
|
+
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
1286
1586
|
conversation.status = "running";
|
|
1287
1587
|
conversation.lastActivityAt = Date.now();
|
|
1288
1588
|
this.activeConversationId = conversation.id;
|
|
@@ -1307,6 +1607,7 @@ export class AgentWorkspaceProxy {
|
|
|
1307
1607
|
model: payload.model,
|
|
1308
1608
|
reasoningEffort: payload.reasoningEffort,
|
|
1309
1609
|
permissionMode: payload.permissionMode,
|
|
1610
|
+
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
1310
1611
|
cwd: conversation.cwd,
|
|
1311
1612
|
});
|
|
1312
1613
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
@@ -1333,6 +1634,206 @@ export class AgentWorkspaceProxy {
|
|
|
1333
1634
|
}
|
|
1334
1635
|
}
|
|
1335
1636
|
|
|
1637
|
+
private commandForConversation(conversation: AgentConversation, commandId: string): AgentCommandDescriptor | undefined {
|
|
1638
|
+
const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
|
|
1639
|
+
const commands = mergeCommands(
|
|
1640
|
+
defaultProviderCommands(conversation.provider, conversation.cwd, true),
|
|
1641
|
+
runtimeCapabilities?.commands,
|
|
1642
|
+
);
|
|
1643
|
+
return commands.find((command) =>
|
|
1644
|
+
command.id === commandId ||
|
|
1645
|
+
command.name === commandId ||
|
|
1646
|
+
`/${command.name}` === commandId
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
private async executeCommand(payload: {
|
|
1651
|
+
conversationId: string;
|
|
1652
|
+
commandId: string;
|
|
1653
|
+
rawText?: string;
|
|
1654
|
+
args?: string;
|
|
1655
|
+
clientMessageId: string;
|
|
1656
|
+
}): Promise<void> {
|
|
1657
|
+
const conversation =
|
|
1658
|
+
this.conversations.get(payload.conversationId) ??
|
|
1659
|
+
await this.openConversation({ conversationId: payload.conversationId });
|
|
1660
|
+
if (!conversation) return;
|
|
1661
|
+
if (!conversation.agentSessionId) {
|
|
1662
|
+
this.addItem(payload.conversationId, {
|
|
1663
|
+
id: id("error"),
|
|
1664
|
+
conversationId: payload.conversationId,
|
|
1665
|
+
type: "error",
|
|
1666
|
+
error: "Agent session 尚未就绪,命令没有执行。请重新打开对话后再试。",
|
|
1667
|
+
createdAt: Date.now(),
|
|
1668
|
+
});
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const command = this.commandForConversation(conversation, payload.commandId);
|
|
1673
|
+
if (!command) {
|
|
1674
|
+
this.addItem(conversation.id, {
|
|
1675
|
+
id: id("error"),
|
|
1676
|
+
conversationId: conversation.id,
|
|
1677
|
+
type: "error",
|
|
1678
|
+
error: `未知命令:${payload.commandId}`,
|
|
1679
|
+
createdAt: Date.now(),
|
|
1680
|
+
});
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (command.disabledReason) {
|
|
1685
|
+
this.addItem(conversation.id, {
|
|
1686
|
+
id: id("error"),
|
|
1687
|
+
conversationId: conversation.id,
|
|
1688
|
+
type: "error",
|
|
1689
|
+
error: command.disabledReason,
|
|
1690
|
+
createdAt: Date.now(),
|
|
1691
|
+
});
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const rawText = payload.rawText?.trim() || `/${command.name}${payload.args?.trim() ? ` ${payload.args.trim()}` : ""}`;
|
|
1696
|
+
if (command.executionKind === "prompt") {
|
|
1697
|
+
await this.sendPrompt({
|
|
1698
|
+
conversationId: conversation.id,
|
|
1699
|
+
clientMessageId: payload.clientMessageId,
|
|
1700
|
+
contentBlocks: [{ type: "text", text: rawText }],
|
|
1701
|
+
model: conversation.model,
|
|
1702
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1703
|
+
permissionMode: conversation.permissionMode,
|
|
1704
|
+
collaborationMode: conversation.collaborationMode,
|
|
1705
|
+
});
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
this.addItem(conversation.id, {
|
|
1710
|
+
id: payload.clientMessageId,
|
|
1711
|
+
conversationId: conversation.id,
|
|
1712
|
+
type: "message",
|
|
1713
|
+
kind: "chat",
|
|
1714
|
+
role: "user",
|
|
1715
|
+
content: [{ type: "text", text: rawText }],
|
|
1716
|
+
text: rawText,
|
|
1717
|
+
metadata: { commandId: command.id, commandExecutionKind: command.executionKind },
|
|
1718
|
+
createdAt: Date.now(),
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
if (command.executionKind === "local_ui") {
|
|
1722
|
+
this.emitStatus(conversation.id, "idle", `${command.title} 已由移动端处理。`);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
await this.executeNativeCommand(conversation, command, payload.args?.trim());
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
private async executeNativeCommand(
|
|
1730
|
+
conversation: AgentConversation,
|
|
1731
|
+
command: AgentCommandDescriptor,
|
|
1732
|
+
args?: string,
|
|
1733
|
+
): Promise<void> {
|
|
1734
|
+
const client = this.clientForProvider(conversation.provider);
|
|
1735
|
+
const now = Date.now();
|
|
1736
|
+
try {
|
|
1737
|
+
if (command.name === "status") {
|
|
1738
|
+
this.emitStatus(
|
|
1739
|
+
conversation.id,
|
|
1740
|
+
conversation.status,
|
|
1741
|
+
`${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`,
|
|
1742
|
+
);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (conversation.provider !== "codex") {
|
|
1747
|
+
this.addItem(conversation.id, {
|
|
1748
|
+
id: id("error"),
|
|
1749
|
+
conversationId: conversation.id,
|
|
1750
|
+
type: "error",
|
|
1751
|
+
error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
|
|
1752
|
+
createdAt: now,
|
|
1753
|
+
});
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (command.name === "plan" || command.name === "exit-plan") {
|
|
1758
|
+
conversation.collaborationMode = command.name === "plan" ? "plan" : "default";
|
|
1759
|
+
conversation.status = "idle";
|
|
1760
|
+
conversation.lastMessagePreview = command.name === "plan" ? "已进入 Plan mode" : "已退出 Plan mode";
|
|
1761
|
+
conversation.lastActivityAt = now;
|
|
1762
|
+
this.emitConversation(conversation);
|
|
1763
|
+
this.sendCapabilities();
|
|
1764
|
+
this.emitStatus(conversation.id, "idle", command.name === "plan"
|
|
1765
|
+
? "已进入 Plan mode。下一条消息会先制定计划。"
|
|
1766
|
+
: "已退出 Plan mode。");
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
if (command.name === "compact") {
|
|
1771
|
+
if (!(client instanceof AcpClient)) throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
1772
|
+
conversation.status = "running";
|
|
1773
|
+
this.emitConversation(conversation);
|
|
1774
|
+
this.addItem(conversation.id, {
|
|
1775
|
+
id: id("compact"),
|
|
1776
|
+
conversationId: conversation.id,
|
|
1777
|
+
type: "status",
|
|
1778
|
+
kind: "context_compaction",
|
|
1779
|
+
text: "正在压缩上下文",
|
|
1780
|
+
status: "running",
|
|
1781
|
+
isStreaming: true,
|
|
1782
|
+
createdAt: now,
|
|
1783
|
+
});
|
|
1784
|
+
await client.compact({ sessionId: conversation.agentSessionId! });
|
|
1785
|
+
this.updateConversationStatus(conversation.id, "idle", "上下文压缩完成");
|
|
1786
|
+
this.emitStatus(conversation.id, "idle", "上下文压缩完成。");
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
if (command.name === "clear") {
|
|
1791
|
+
if (!client) throw new Error("Agent provider 不在线。");
|
|
1792
|
+
const result = await client.newSession({ cwd: conversation.cwd });
|
|
1793
|
+
const nextAgentSessionId = this.extractSessionId(result) ?? id("agent-session");
|
|
1794
|
+
if (conversation.agentSessionId) this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
1795
|
+
conversation.agentSessionId = nextAgentSessionId;
|
|
1796
|
+
conversation.collaborationMode = "default";
|
|
1797
|
+
conversation.status = "idle";
|
|
1798
|
+
conversation.lastMessagePreview = "上下文已重置";
|
|
1799
|
+
conversation.lastActivityAt = now;
|
|
1800
|
+
this.conversationByAgentSessionId.set(nextAgentSessionId, conversation.id);
|
|
1801
|
+
this.timelines.set(conversation.id, []);
|
|
1802
|
+
this.emitConversation(conversation);
|
|
1803
|
+
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
if (command.name === "review" || command.name === "subagents") {
|
|
1808
|
+
const prompt = command.name === "review"
|
|
1809
|
+
? args || "Review the current local changes."
|
|
1810
|
+
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1811
|
+
await this.sendPrompt({
|
|
1812
|
+
conversationId: conversation.id,
|
|
1813
|
+
clientMessageId: id(command.name),
|
|
1814
|
+
contentBlocks: [{ type: "text", text: prompt }],
|
|
1815
|
+
model: conversation.model,
|
|
1816
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1817
|
+
permissionMode: conversation.permissionMode,
|
|
1818
|
+
collaborationMode: conversation.collaborationMode,
|
|
1819
|
+
});
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
throw new Error(`命令暂未实现:/${command.name}`);
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1826
|
+
this.updateConversationStatus(conversation.id, "error", message);
|
|
1827
|
+
this.addItem(conversation.id, {
|
|
1828
|
+
id: id("error"),
|
|
1829
|
+
conversationId: conversation.id,
|
|
1830
|
+
type: "error",
|
|
1831
|
+
error: message,
|
|
1832
|
+
createdAt: Date.now(),
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1336
1837
|
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
1337
1838
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1338
1839
|
return this.handleStructuredInput(params, true);
|
|
@@ -1353,8 +1854,23 @@ export class AgentWorkspaceProxy {
|
|
|
1353
1854
|
if (this.input.verbose) {
|
|
1354
1855
|
process.stderr.write(`[agent:v2] ${method} ${stringify(params).slice(0, 500)}\n`);
|
|
1355
1856
|
}
|
|
1857
|
+
if (method === "initialized") {
|
|
1858
|
+
const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
|
|
1859
|
+
const provider = conversationId ? this.conversations.get(conversationId)?.provider : this.input.availableProviders[0];
|
|
1860
|
+
if (provider) {
|
|
1861
|
+
const commands = runtimeCommands(provider, params);
|
|
1862
|
+
if (commands.length > 0) {
|
|
1863
|
+
const existing = this.providerCapabilities.get(provider);
|
|
1864
|
+
this.providerCapabilities.set(provider, {
|
|
1865
|
+
...(existing ?? {}),
|
|
1866
|
+
commands: mergeCommands(existing?.commands, commands),
|
|
1867
|
+
});
|
|
1868
|
+
this.sendCapabilities();
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1356
1873
|
if (
|
|
1357
|
-
method === "initialized" ||
|
|
1358
1874
|
method.startsWith("account/") ||
|
|
1359
1875
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
1360
1876
|
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
|
}
|