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.
@@ -1,4 +1,6 @@
1
- import { basename } from "node:path";
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 || !conversation.agentSessionId) return;
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) return;
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
  }