linkshell-cli 0.2.99 → 0.2.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/cli/src/runtime/acp/acp-client.d.ts +1 -0
  2. package/dist/cli/src/runtime/acp/acp-client.js +17 -3
  3. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  4. package/dist/cli/src/runtime/acp/agent-session.d.ts +1 -0
  5. package/dist/cli/src/runtime/acp/agent-session.js +73 -19
  6. package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
  7. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -1
  8. package/dist/cli/src/runtime/acp/agent-workspace.js +346 -57
  9. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  10. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +51 -0
  11. package/dist/cli/src/runtime/acp/claude-sdk-client.js +318 -0
  12. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -0
  13. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +1 -1
  14. package/dist/cli/src/runtime/acp/provider-resolver.js +22 -2
  15. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
  16. package/dist/cli/src/runtime/bridge-session.js +1 -0
  17. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  18. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  19. package/dist/shared-protocol/src/index.d.ts +1885 -265
  20. package/dist/shared-protocol/src/index.js +36 -10
  21. package/dist/shared-protocol/src/index.js.map +1 -1
  22. package/package.json +8 -5
  23. package/src/runtime/acp/acp-client.ts +18 -3
  24. package/src/runtime/acp/agent-session.ts +68 -14
  25. package/src/runtime/acp/agent-workspace.ts +376 -54
  26. package/src/runtime/acp/claude-sdk-client.ts +372 -0
  27. package/src/runtime/acp/provider-resolver.ts +24 -3
  28. package/src/runtime/bridge-session.ts +1 -0
@@ -5,6 +5,7 @@ import {
5
5
  type Envelope,
6
6
  } from "@linkshell/protocol";
7
7
  import { AcpClient } from "./acp-client.js";
8
+ import { ClaudeSdkClient } from "./claude-sdk-client.js";
8
9
  import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
9
10
  import type { AgentProtocol, AgentProvider } from "./provider-resolver.js";
10
11
  import { resolveAgentCommand } from "./provider-resolver.js";
@@ -172,6 +173,7 @@ interface PendingPermissionWaiter {
172
173
  interface PendingStructuredInputWaiter {
173
174
  resolve: (value: unknown) => void;
174
175
  timer: ReturnType<typeof setTimeout>;
176
+ source?: string;
175
177
  }
176
178
 
177
179
  const PERMISSION_TIMEOUT_MS = 5 * 60_000;
@@ -392,9 +394,18 @@ function commandExecutionFromTool(toolCall: AgentToolCall): AgentCommandExecutio
392
394
  };
393
395
  }
394
396
 
395
- function fileChangeFromTool(toolCall: AgentToolCall): AgentFileChange | undefined {
396
- const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
397
- const entries: AgentFileChangeEntry[] = (toolCall.input ?? "")
397
+ function fileChangeFromStructuredInput(input: string | undefined): AgentFileChangeEntry[] {
398
+ const raw = input?.trim();
399
+ if (!raw) return [];
400
+ try {
401
+ const parsed = JSON.parse(raw);
402
+ if (parsed && typeof parsed === "object") {
403
+ return fileChangeEntriesFromItem(parsed as Record<string, unknown>);
404
+ }
405
+ } catch {
406
+ // Fall through to line parser.
407
+ }
408
+ return raw
398
409
  .split("\n")
399
410
  .map((line) => line.trim())
400
411
  .filter(Boolean)
@@ -406,6 +417,11 @@ function fileChangeFromTool(toolCall: AgentToolCall): AgentFileChange | undefine
406
417
  return entry;
407
418
  })
408
419
  .filter((entry) => entry.path.length > 0);
420
+ }
421
+
422
+ function fileChangeFromTool(toolCall: AgentToolCall): AgentFileChange | undefined {
423
+ const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
424
+ const entries = fileChangeFromStructuredInput(toolCall.input);
409
425
  if (entries.length === 0 && !diff && !toolCall.output) return undefined;
410
426
  return {
411
427
  entries,
@@ -662,35 +678,119 @@ interface AgentModelOption {
662
678
  label: string;
663
679
  }
664
680
 
665
- function providerModels(provider: AgentProvider): AgentModelOption[] {
666
- if (provider === "claude") {
667
- return [
668
- { id: "default", label: "默认模型" },
669
- { id: "opus", label: "Opus 4.7" },
670
- { id: "sonnet", label: "Sonnet 4.6" },
671
- { id: "haiku", label: "Haiku 4.5" },
672
- ];
681
+ interface ProviderRuntimeCapabilities {
682
+ models?: AgentModelOption[];
683
+ defaultModel?: string;
684
+ reasoningEfforts?: string[];
685
+ }
686
+
687
+ const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
688
+ const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
689
+
690
+ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities | undefined {
691
+ const raw = asRecord(value);
692
+ const modelsValue =
693
+ Array.isArray(value) ? value :
694
+ Array.isArray(raw?.models) ? raw.models :
695
+ Array.isArray(raw?.items) ? raw.items :
696
+ Array.isArray(raw?.modelOptions) ? raw.modelOptions :
697
+ [];
698
+ const models = modelsValue
699
+ .map((entry, index) => {
700
+ const model = asRecord(entry);
701
+ if (!model) {
702
+ return typeof entry === "string" && entry
703
+ ? { id: entry, label: entry }
704
+ : undefined;
705
+ }
706
+ const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
707
+ const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
708
+ return { id: modelId, label };
709
+ })
710
+ .filter((entry): entry is AgentModelOption => Boolean(entry));
711
+ const defaultModel =
712
+ firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
713
+ firstString(asRecord(raw?.defaults), ["model"]);
714
+ const effortsValue =
715
+ Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
716
+ Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
717
+ Array.isArray(raw?.efforts) ? raw.efforts :
718
+ undefined;
719
+ const reasoningEfforts = effortsValue
720
+ ?.filter((entry): entry is string => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry as typeof ALL_REASONING_EFFORTS[number]));
721
+ if (models.length === 0 && !defaultModel && !reasoningEfforts?.length) return undefined;
722
+ return {
723
+ ...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
724
+ ...(defaultModel ? { defaultModel } : {}),
725
+ ...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
726
+ };
727
+ }
728
+
729
+ function parseTimestamp(value: unknown): number | undefined {
730
+ if (typeof value === "number" && Number.isFinite(value)) {
731
+ return value > 10_000_000_000 ? value : value * 1000;
673
732
  }
674
- if (provider === "codex") {
675
- return [
676
- { id: "default", label: "默认模型" },
677
- { id: "gpt-5.5", label: "GPT-5.5" },
678
- { id: "gpt-5.4", label: "GPT-5.4" },
679
- { id: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
680
- { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
681
- ];
733
+ if (typeof value === "string" && value.trim()) {
734
+ const parsed = Date.parse(value);
735
+ return Number.isNaN(parsed) ? undefined : parsed;
736
+ }
737
+ return undefined;
738
+ }
739
+
740
+ function parseRemoteSessions(value: unknown): Array<{
741
+ id: string;
742
+ cwd?: string;
743
+ title?: string;
744
+ model?: string;
745
+ createdAt?: number;
746
+ lastActivityAt?: number;
747
+ }> {
748
+ const raw = asRecord(value);
749
+ const sessionsValue =
750
+ Array.isArray(value) ? value :
751
+ Array.isArray(raw?.threads) ? raw.threads :
752
+ Array.isArray(raw?.sessions) ? raw.sessions :
753
+ Array.isArray(raw?.items) ? raw.items :
754
+ [];
755
+ const result: Array<{
756
+ id: string;
757
+ cwd?: string;
758
+ title?: string;
759
+ model?: string;
760
+ createdAt?: number;
761
+ lastActivityAt?: number;
762
+ }> = [];
763
+ for (const entry of sessionsValue) {
764
+ const session = asRecord(entry);
765
+ if (!session) {
766
+ if (typeof entry === "string" && entry) result.push({ id: entry });
767
+ continue;
768
+ }
769
+ const nestedThread = asRecord(session.thread);
770
+ const source = nestedThread ?? session;
771
+ const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]);
772
+ if (!id) continue;
773
+ result.push({
774
+ id,
775
+ cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
776
+ title: firstString(source, ["title", "name", "summary"]),
777
+ model: firstString(source, ["model", "modelId"]),
778
+ createdAt: parseTimestamp(source.createdAt ?? source.created_at),
779
+ lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
780
+ });
682
781
  }
683
- return [{ id: "default", label: "默认模型" }];
782
+ return result;
684
783
  }
685
784
 
686
785
  export class AgentWorkspaceProxy {
687
- private clients = new Map<AgentProvider, AcpClient | ClaudeStreamJsonClient>();
786
+ private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
688
787
  private agentProtocols = new Map<AgentProvider, AgentProtocol>();
788
+ private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
689
789
  private initialized = false;
690
790
  private status: AgentStatus = "unavailable";
691
791
  private error: string | undefined;
692
792
  private activeConversationId: string | undefined;
693
- private currentTurnId: string | undefined;
793
+ private currentTurnIds = new Map<string, string>();
694
794
  private conversations = new Map<string, AgentConversation>();
695
795
  private conversationByAgentSessionId = new Map<string, string>();
696
796
  private timelines = new Map<string, AgentTimelineItem[]>();
@@ -726,6 +826,7 @@ export class AgentWorkspaceProxy {
726
826
  }
727
827
  case "agent.v2.conversation.list": {
728
828
  const payload = parseTypedPayload("agent.v2.conversation.list", envelope.payload);
829
+ await this.syncProviderSessions();
729
830
  const conversations = [...this.conversations.values()].filter((conversation) =>
730
831
  payload.includeArchived ? true : !conversation.archived,
731
832
  );
@@ -753,9 +854,9 @@ export class AgentWorkspaceProxy {
753
854
  const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
754
855
  cancelClient?.cancel({
755
856
  sessionId: conversation?.agentSessionId,
756
- turnId: this.currentTurnId,
857
+ turnId: this.currentTurnIds.get(payload.conversationId),
757
858
  });
758
- this.currentTurnId = undefined;
859
+ this.currentTurnIds.delete(payload.conversationId);
759
860
  this.updateConversationStatus(payload.conversationId, "idle");
760
861
  this.emitStatus(payload.conversationId, "idle", "已停止");
761
862
  break;
@@ -780,7 +881,7 @@ export class AgentWorkspaceProxy {
780
881
  this.clients.clear();
781
882
  }
782
883
 
783
- private clientForProvider(provider: AgentProvider): AcpClient | ClaudeStreamJsonClient | undefined {
884
+ private clientForProvider(provider: AgentProvider): AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined {
784
885
  return this.clients.get(provider);
785
886
  }
786
887
 
@@ -799,7 +900,7 @@ export class AgentWorkspaceProxy {
799
900
  this.sendCapabilities();
800
901
  }
801
902
 
802
- private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | ClaudeStreamJsonClient | undefined> {
903
+ private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined> {
803
904
  const existing = this.clients.get(provider);
804
905
  if (existing) return existing;
805
906
 
@@ -814,35 +915,76 @@ export class AgentWorkspaceProxy {
814
915
  return undefined;
815
916
  }
816
917
 
817
- try {
818
- this.agentProtocols.set(provider, resolved.protocol);
819
- const isClaudeStreamJson = resolved.protocol === "claude-stream-json";
820
- const client = isClaudeStreamJson
918
+ const tryCreateClient = async (config: typeof resolved): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient> => {
919
+ this.agentProtocols.set(provider, config.protocol);
920
+ const isClaudeSdk = config.protocol === "claude-agent-sdk";
921
+ const isClaudeStreamJson = config.protocol === "claude-stream-json";
922
+ const client = isClaudeSdk
923
+ ? new ClaudeSdkClient({
924
+ command: config.command,
925
+ protocol: config.protocol,
926
+ framing: config.framing,
927
+ cwd: this.input.cwd,
928
+ onNotification: (method, params) => this.handleNotification(method, params),
929
+ onRequest: (method, params) => this.handleRequest(method, params),
930
+ onExit: (message) => this.handleProviderExit(provider, message),
931
+ })
932
+ : isClaudeStreamJson
821
933
  ? new ClaudeStreamJsonClient({
822
- command: resolved.command,
823
- protocol: resolved.protocol,
824
- framing: resolved.framing,
934
+ command: config.command,
935
+ protocol: config.protocol,
936
+ framing: config.framing,
825
937
  cwd: this.input.cwd,
826
938
  onNotification: (method, params) => this.handleNotification(method, params),
827
939
  onRequest: (method, params) => this.handleRequest(method, params),
828
940
  onExit: (message) => this.handleProviderExit(provider, message),
829
941
  })
830
942
  : new AcpClient({
831
- command: resolved.command,
832
- protocol: resolved.protocol,
833
- framing: resolved.framing,
943
+ command: config.command,
944
+ protocol: config.protocol,
945
+ framing: config.framing,
834
946
  cwd: this.input.cwd,
835
947
  onNotification: (method, params) => this.handleNotification(method, params),
836
948
  onRequest: (method, params) => this.handleRequest(method, params),
837
949
  onExit: (message) => this.handleProviderExit(provider, message),
838
- });
950
+ });
839
951
  await client.initialize();
952
+ return client;
953
+ };
954
+
955
+ try {
956
+ const client = await tryCreateClient(resolved);
840
957
  this.clients.set(provider, client);
958
+ await this.refreshProviderCapabilities(provider, client, resolved.protocol);
841
959
  this.status = "idle";
842
960
  this.error = undefined;
843
961
  this.sendCapabilities();
844
962
  return client;
845
963
  } catch (error) {
964
+ if (provider === "claude" && resolved.protocol === "claude-agent-sdk") {
965
+ if (this.input.verbose) {
966
+ process.stderr.write(`[agent:v2] Claude SDK failed, falling back to stream-json: ${error instanceof Error ? error.message : String(error)}\n`);
967
+ }
968
+ try {
969
+ const fallback = {
970
+ provider,
971
+ command: "claude --print --output-format stream-json --input-format stream-json --verbose --permission-mode bypassPermissions",
972
+ protocol: "claude-stream-json" as const,
973
+ framing: "newline" as const,
974
+ };
975
+ const client = await tryCreateClient(fallback);
976
+ this.clients.set(provider, client);
977
+ await this.refreshProviderCapabilities(provider, client, fallback.protocol);
978
+ this.status = "idle";
979
+ this.error = undefined;
980
+ this.sendCapabilities();
981
+ return client;
982
+ } catch (fallbackError) {
983
+ if (this.input.verbose) {
984
+ process.stderr.write(`[agent:v2] Claude stream-json fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}\n`);
985
+ }
986
+ }
987
+ }
846
988
  if (this.input.verbose) {
847
989
  process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
848
990
  }
@@ -850,25 +992,99 @@ export class AgentWorkspaceProxy {
850
992
  }
851
993
  }
852
994
 
995
+ private async refreshProviderCapabilities(
996
+ provider: AgentProvider,
997
+ client: AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient,
998
+ protocol: AgentProtocol,
999
+ ): Promise<void> {
1000
+ if (!(client instanceof AcpClient) || protocol !== "codex-app-server") return;
1001
+ try {
1002
+ const result = await client.listModels();
1003
+ const runtimeCapabilities = parseModelListCapabilities(result);
1004
+ if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
1005
+ } catch (error) {
1006
+ if (this.input.verbose) {
1007
+ process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ private async syncProviderSessions(): Promise<void> {
1013
+ await this.initialize();
1014
+ for (const [provider, client] of this.clients) {
1015
+ try {
1016
+ const result = await client.listSessions();
1017
+ for (const remote of parseRemoteSessions(result)) {
1018
+ const agentSessionId = remote.id;
1019
+ const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1020
+ const now = Date.now();
1021
+ const conversationId = existingId ?? `agent:${agentSessionId}`;
1022
+ const existing = this.conversations.get(conversationId);
1023
+ const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1024
+ const conversation: AgentConversation = {
1025
+ id: conversationId,
1026
+ agentSessionId,
1027
+ provider,
1028
+ cwd,
1029
+ title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
1030
+ model: remote.model ?? existing?.model,
1031
+ reasoningEffort: existing?.reasoningEffort,
1032
+ permissionMode: existing?.permissionMode,
1033
+ status: existing?.status ?? "idle",
1034
+ archived: existing?.archived ?? false,
1035
+ lastMessagePreview: existing?.lastMessagePreview,
1036
+ lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
1037
+ createdAt: remote.createdAt ?? existing?.createdAt ?? now,
1038
+ };
1039
+ this.conversations.set(conversation.id, conversation);
1040
+ this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1041
+ this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1042
+ }
1043
+ } catch (error) {
1044
+ if (this.input.verbose) {
1045
+ process.stderr.write(`[agent:v2] session list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1046
+ }
1047
+ }
1048
+ }
1049
+ }
1050
+
853
1051
  private sendCapabilities(): void {
854
1052
  const providers = this.input.availableProviders.map((provider) => {
855
1053
  const client = this.clients.get(provider);
856
1054
  const protocol = this.agentProtocols.get(provider);
1055
+ const runtimeCapabilities = this.providerCapabilities.get(provider);
857
1056
  const enabled = Boolean(client);
858
1057
  const supportsImages = enabled && protocol === "codex-app-server";
1058
+ const isClaudeFallback = protocol === "claude-stream-json";
1059
+ const supportsPermission = enabled && !isClaudeFallback;
1060
+ const supportsReasoningEffort = enabled && !isClaudeFallback;
859
1061
  return {
860
1062
  id: provider,
861
1063
  label: providerLabel(provider),
862
1064
  enabled,
863
1065
  reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
864
1066
  supportsImages,
865
- supportsPermission: enabled,
1067
+ supportsPermission,
866
1068
  supportsPlan: enabled,
867
1069
  supportsCancel: enabled,
868
- models: providerModels(provider),
1070
+ models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
1071
+ defaultModel: runtimeCapabilities?.defaultModel ?? "default",
1072
+ reasoningEfforts: supportsReasoningEffort
1073
+ ? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
1074
+ : [],
1075
+ permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
1076
+ features: {
1077
+ images: supportsImages,
1078
+ permissions: supportsPermission,
1079
+ plan: enabled,
1080
+ cancel: enabled,
1081
+ reasoningEffort: supportsReasoningEffort,
1082
+ streamJsonFallback: isClaudeFallback,
1083
+ },
869
1084
  };
870
1085
  });
871
1086
  const anyEnabled = providers.some((p) => p.enabled);
1087
+ const anyPermission = providers.some((p) => p.supportsPermission);
872
1088
  this.input.send(createEnvelope({
873
1089
  type: "agent.v2.capabilities",
874
1090
  sessionId: this.input.sessionId,
@@ -883,7 +1099,7 @@ export class AgentWorkspaceProxy {
883
1099
  supportsSessionLoad: anyEnabled,
884
1100
  supportsImages: providers.some((p) => p.supportsImages),
885
1101
  supportsAudio: false,
886
- supportsPermission: anyEnabled,
1102
+ supportsPermission: anyPermission,
887
1103
  supportsPlan: anyEnabled,
888
1104
  supportsCancel: anyEnabled,
889
1105
  },
@@ -1085,8 +1301,15 @@ export class AgentWorkspaceProxy {
1085
1301
  permissionMode: payload.permissionMode,
1086
1302
  cwd: conversation.cwd,
1087
1303
  });
1088
- this.currentTurnId = this.extractTurnId(result) ?? this.currentTurnId;
1089
- if (conversation.status === "running") {
1304
+ const nextAgentSessionId = this.extractSessionId(result);
1305
+ if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
1306
+ if (conversation.agentSessionId) this.conversationByAgentSessionId.delete(conversation.agentSessionId);
1307
+ conversation.agentSessionId = nextAgentSessionId;
1308
+ this.conversationByAgentSessionId.set(nextAgentSessionId, conversation.id);
1309
+ }
1310
+ const turnId = this.extractTurnId(result);
1311
+ if (turnId) this.currentTurnIds.set(conversation.id, turnId);
1312
+ if (conversation.status === "running" && protocol !== "codex-app-server") {
1090
1313
  this.updateConversationStatus(conversation.id, "idle");
1091
1314
  }
1092
1315
  } catch (error) {
@@ -1106,6 +1329,9 @@ export class AgentWorkspaceProxy {
1106
1329
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1107
1330
  return this.handleStructuredInput(params, true);
1108
1331
  }
1332
+ if (method === "mcpServer/elicitation/request") {
1333
+ return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
1334
+ }
1109
1335
  if (isPermissionRequestMethod(method)) {
1110
1336
  return this.handlePermission(params, true, method);
1111
1337
  }
@@ -1136,6 +1362,10 @@ export class AgentWorkspaceProxy {
1136
1362
  this.handleStructuredInput(params);
1137
1363
  return;
1138
1364
  }
1365
+ if (method === "mcpServer/elicitation/request") {
1366
+ this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method });
1367
+ return;
1368
+ }
1139
1369
  if (isPermissionRequestMethod(method)) {
1140
1370
  this.handlePermission(params, false, method);
1141
1371
  return;
@@ -1150,13 +1380,18 @@ export class AgentWorkspaceProxy {
1150
1380
  return;
1151
1381
  }
1152
1382
  if (method === "turn/started") {
1153
- this.currentTurnId = this.extractTurnId(params) ?? this.currentTurnId;
1154
- if (conversationId) this.updateConversationStatus(conversationId, "running");
1383
+ if (conversationId) {
1384
+ const turnId = this.extractTurnId(params);
1385
+ if (turnId) this.currentTurnIds.set(conversationId, turnId);
1386
+ this.updateConversationStatus(conversationId, "running");
1387
+ }
1155
1388
  return;
1156
1389
  }
1157
1390
  if (method === "turn/completed") {
1158
- this.currentTurnId = undefined;
1159
- if (conversationId) this.updateConversationStatus(conversationId, "idle");
1391
+ if (conversationId) {
1392
+ this.currentTurnIds.delete(conversationId);
1393
+ this.updateConversationStatus(conversationId, "idle");
1394
+ }
1160
1395
  return;
1161
1396
  }
1162
1397
  if (method === "session/request_permission") {
@@ -1195,7 +1430,11 @@ export class AgentWorkspaceProxy {
1195
1430
  this.handleCommandExecDelta(params);
1196
1431
  return;
1197
1432
  case "item/autoApprovalReview/started":
1433
+ this.handleAutoApprovalReview(params, true);
1434
+ return;
1198
1435
  case "item/autoApprovalReview/completed":
1436
+ this.handleAutoApprovalReview(params, false);
1437
+ return;
1199
1438
  case "item/commandExecution/terminalInteraction":
1200
1439
  return;
1201
1440
  }
@@ -1427,6 +1666,37 @@ export class AgentWorkspaceProxy {
1427
1666
  });
1428
1667
  }
1429
1668
 
1669
+ private handleAutoApprovalReview(params: unknown, streaming: boolean): void {
1670
+ const raw = asRecord(params) ?? {};
1671
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1672
+ if (!conversationId) return;
1673
+ const itemId = firstString(raw, ["itemId", "id", "reviewId"]) ?? "auto-approval-review";
1674
+ const existing = this.findItem(conversationId, itemId);
1675
+ const decision = firstString(raw, ["decision", "result", "outcome", "status"]);
1676
+ const summary =
1677
+ firstString(raw, ["summary", "message", "text", "reason"]) ??
1678
+ stringifyDefined(raw.review ?? raw.details);
1679
+ this.upsertItem(conversationId, {
1680
+ id: itemId,
1681
+ conversationId,
1682
+ type: "status",
1683
+ kind: "review",
1684
+ role: "system",
1685
+ turnId: this.extractTurnId(raw) ?? this.currentTurnIds.get(conversationId),
1686
+ itemId,
1687
+ text: summary ?? (streaming ? "正在审查自动授权" : decision ? `自动授权审查:${decision}` : "已完成自动授权审查"),
1688
+ metadata: {
1689
+ ...(existing?.metadata ?? {}),
1690
+ autoApprovalReview: true,
1691
+ ...(decision ? { decision } : {}),
1692
+ },
1693
+ createdAt: existing?.createdAt ?? Date.now(),
1694
+ updatedAt: Date.now(),
1695
+ isStreaming: streaming,
1696
+ });
1697
+ this.updateConversationPreview(conversationId, streaming ? "正在审查自动授权" : "已完成自动授权审查", streaming ? "running" : undefined);
1698
+ }
1699
+
1430
1700
  private handleCompletedMessageItem(item: Record<string, unknown>, streaming: boolean): void {
1431
1701
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1432
1702
  if (!conversationId) return;
@@ -1501,7 +1771,7 @@ export class AgentWorkspaceProxy {
1501
1771
  conversationId,
1502
1772
  type: "status" as const,
1503
1773
  role: "system" as const,
1504
- turnId: this.extractTurnId(item) ?? this.currentTurnId,
1774
+ turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
1505
1775
  itemId,
1506
1776
  createdAt: existing?.createdAt ?? Date.now(),
1507
1777
  updatedAt: Date.now(),
@@ -1559,7 +1829,7 @@ export class AgentWorkspaceProxy {
1559
1829
  type: "status",
1560
1830
  kind: "subagent_action",
1561
1831
  role: "system",
1562
- turnId: this.extractTurnId(item) ?? this.currentTurnId,
1832
+ turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
1563
1833
  itemId,
1564
1834
  text,
1565
1835
  subagent,
@@ -1573,9 +1843,13 @@ export class AgentWorkspaceProxy {
1573
1843
  private handleStructuredInput(params: unknown, waitForResponse = false): Promise<unknown> | void {
1574
1844
  const raw = asRecord(params) ?? {};
1575
1845
  const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1576
- if (!conversationId) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1846
+ const source = firstString(raw, ["method", "source", "requestMethod"]);
1847
+ const formatResponse = source === "mcpServer/elicitation/request"
1848
+ ? formatMcpElicitationResponse
1849
+ : formatStructuredInputResponse;
1850
+ if (!conversationId) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
1577
1851
  const structuredInput = decodeStructuredInput(raw);
1578
- if (!structuredInput) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1852
+ if (!structuredInput) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
1579
1853
  const text = structuredInput.questions.map((question) => question.question).join("\n");
1580
1854
  this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
1581
1855
  this.upsertItem(conversationId, {
@@ -1596,13 +1870,13 @@ export class AgentWorkspaceProxy {
1596
1870
  const timer = setTimeout(() => {
1597
1871
  this.pendingStructuredInputs.delete(structuredInput.requestId);
1598
1872
  this.structuredInputWaiters.delete(structuredInput.requestId);
1599
- resolve(formatStructuredInputResponse({}));
1873
+ resolve(formatResponse({}));
1600
1874
  this.markStructuredInput(conversationId, structuredInput.requestId, {
1601
1875
  inputPending: false,
1602
1876
  inputError: "等待用户输入超时",
1603
1877
  });
1604
1878
  }, PERMISSION_TIMEOUT_MS);
1605
- this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
1879
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
1606
1880
  });
1607
1881
  }
1608
1882
 
@@ -1711,6 +1985,12 @@ export class AgentWorkspaceProxy {
1711
1985
  optionId: selectedOptionId,
1712
1986
  });
1713
1987
  }
1988
+ this.markPermission(payload.conversationId, payload.requestId, {
1989
+ permissionOutcome: payload.outcome,
1990
+ optionId: selectedOptionId,
1991
+ permissionError: undefined,
1992
+ permissionPending: false,
1993
+ });
1714
1994
  this.updateConversationStatus(payload.conversationId, "running");
1715
1995
  }
1716
1996
 
@@ -1725,16 +2005,35 @@ export class AgentWorkspaceProxy {
1725
2005
  if (waiter) {
1726
2006
  clearTimeout(waiter.timer);
1727
2007
  this.structuredInputWaiters.delete(payload.requestId);
1728
- waiter.resolve(formatStructuredInputResponse(payload.answers));
2008
+ const formatResponse = waiter.source === "mcpServer/elicitation/request"
2009
+ ? formatMcpElicitationResponse
2010
+ : formatStructuredInputResponse;
2011
+ waiter.resolve(formatResponse(payload.answers));
1729
2012
  }
1730
2013
  this.markStructuredInput(payload.conversationId, payload.requestId, {
1731
2014
  inputPending: false,
1732
2015
  inputSubmitted: true,
2016
+ inputSubmitting: false,
2017
+ inputError: undefined,
1733
2018
  answers: payload.answers,
1734
2019
  });
1735
2020
  this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
1736
2021
  }
1737
2022
 
2023
+ private markPermission(
2024
+ conversationId: string,
2025
+ requestId: string,
2026
+ metadata: Record<string, unknown>,
2027
+ ): void {
2028
+ const item = this.findItem(conversationId, `permission:${requestId}`);
2029
+ if (!item) return;
2030
+ this.upsertItem(conversationId, {
2031
+ ...item,
2032
+ metadata: { ...(item.metadata ?? {}), ...metadata },
2033
+ updatedAt: Date.now(),
2034
+ });
2035
+ }
2036
+
1738
2037
  private markStructuredInput(
1739
2038
  conversationId: string,
1740
2039
  requestId: string,
@@ -2090,7 +2389,8 @@ function isPermissionRequestMethod(method: string): boolean {
2090
2389
  return (
2091
2390
  method === "session/request_permission" ||
2092
2391
  method.endsWith("/requestApproval") ||
2093
- method === "mcpServer/elicitation/request"
2392
+ method === "mcpServer/elicitation/request" ||
2393
+ method === "claude/requestApproval"
2094
2394
  );
2095
2395
  }
2096
2396
 
@@ -2105,6 +2405,20 @@ function formatStructuredInputResponse(answers: Record<string, string[]>): unkno
2105
2405
  };
2106
2406
  }
2107
2407
 
2408
+ function formatMcpElicitationResponse(answers: Record<string, string[]>): unknown {
2409
+ const content = Object.fromEntries(
2410
+ Object.entries(answers).map(([questionId, values]) => [
2411
+ questionId,
2412
+ values.length <= 1 ? values[0] ?? "" : values,
2413
+ ]),
2414
+ );
2415
+ return {
2416
+ action: Object.keys(content).length > 0 ? "accept" : "cancel",
2417
+ content,
2418
+ _meta: { source: "linkshell" },
2419
+ };
2420
+ }
2421
+
2108
2422
  function formatPermissionResponse(
2109
2423
  source: string | undefined,
2110
2424
  outcome: "allow" | "deny" | "cancelled",
@@ -2122,6 +2436,14 @@ function formatPermissionResponse(
2122
2436
  }
2123
2437
  return { permissions: { type: "managed", network: { enabled: false }, fileSystem: { type: "readOnly" } } };
2124
2438
  }
2439
+ if (source === "claude/requestApproval") {
2440
+ return { behavior: outcome === "allow" ? "allow" : "deny" };
2441
+ }
2442
+ if (source === "mcpServer/elicitation/request") {
2443
+ return outcome === "allow"
2444
+ ? { action: "accept", content: { optionId }, _meta: { source: "linkshell" } }
2445
+ : { action: outcome === "cancelled" ? "cancel" : "decline", content: {}, _meta: { source: "linkshell" } };
2446
+ }
2125
2447
  return {
2126
2448
  outcome:
2127
2449
  outcome === "cancelled"