linkshell-cli 0.2.99 → 0.2.101

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 (34) hide show
  1. package/dist/cli/src/index.js +1 -0
  2. package/dist/cli/src/index.js.map +1 -1
  3. package/dist/cli/src/providers.js +49 -3
  4. package/dist/cli/src/providers.js.map +1 -1
  5. package/dist/cli/src/runtime/acp/acp-client.d.ts +1 -0
  6. package/dist/cli/src/runtime/acp/acp-client.js +17 -3
  7. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  8. package/dist/cli/src/runtime/acp/agent-session.d.ts +1 -0
  9. package/dist/cli/src/runtime/acp/agent-session.js +73 -19
  10. package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
  11. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -1
  12. package/dist/cli/src/runtime/acp/agent-workspace.js +349 -58
  13. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  14. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +51 -0
  15. package/dist/cli/src/runtime/acp/claude-sdk-client.js +318 -0
  16. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -0
  17. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +1 -1
  18. package/dist/cli/src/runtime/acp/provider-resolver.js +22 -2
  19. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
  20. package/dist/cli/src/runtime/bridge-session.js +1 -0
  21. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  22. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  23. package/dist/shared-protocol/src/index.d.ts +1885 -265
  24. package/dist/shared-protocol/src/index.js +36 -10
  25. package/dist/shared-protocol/src/index.js.map +1 -1
  26. package/package.json +8 -5
  27. package/src/index.ts +1 -0
  28. package/src/providers.ts +50 -3
  29. package/src/runtime/acp/acp-client.ts +18 -3
  30. package/src/runtime/acp/agent-session.ts +68 -14
  31. package/src/runtime/acp/agent-workspace.ts +378 -55
  32. package/src/runtime/acp/claude-sdk-client.ts +372 -0
  33. package/src/runtime/acp/provider-resolver.ts +24 -3
  34. 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(),
@@ -1511,10 +1781,11 @@ export class AgentWorkspaceProxy {
1511
1781
  if (normalized === "reasoning" || normalized === "thinking") {
1512
1782
  const text = firstString(item, ["text", "content", "summary", "message"]) ??
1513
1783
  stringifyDefined(item.contentItems ?? item.summary);
1784
+ if (!text?.trim()) return true;
1514
1785
  this.upsertItem(conversationId, {
1515
1786
  ...base,
1516
1787
  kind: "thinking",
1517
- text: text ?? (streaming ? "正在思考" : "完成思考"),
1788
+ text,
1518
1789
  });
1519
1790
  return true;
1520
1791
  }
@@ -1559,7 +1830,7 @@ export class AgentWorkspaceProxy {
1559
1830
  type: "status",
1560
1831
  kind: "subagent_action",
1561
1832
  role: "system",
1562
- turnId: this.extractTurnId(item) ?? this.currentTurnId,
1833
+ turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
1563
1834
  itemId,
1564
1835
  text,
1565
1836
  subagent,
@@ -1573,9 +1844,13 @@ export class AgentWorkspaceProxy {
1573
1844
  private handleStructuredInput(params: unknown, waitForResponse = false): Promise<unknown> | void {
1574
1845
  const raw = asRecord(params) ?? {};
1575
1846
  const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1576
- if (!conversationId) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1847
+ const source = firstString(raw, ["method", "source", "requestMethod"]);
1848
+ const formatResponse = source === "mcpServer/elicitation/request"
1849
+ ? formatMcpElicitationResponse
1850
+ : formatStructuredInputResponse;
1851
+ if (!conversationId) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
1577
1852
  const structuredInput = decodeStructuredInput(raw);
1578
- if (!structuredInput) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1853
+ if (!structuredInput) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
1579
1854
  const text = structuredInput.questions.map((question) => question.question).join("\n");
1580
1855
  this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
1581
1856
  this.upsertItem(conversationId, {
@@ -1596,13 +1871,13 @@ export class AgentWorkspaceProxy {
1596
1871
  const timer = setTimeout(() => {
1597
1872
  this.pendingStructuredInputs.delete(structuredInput.requestId);
1598
1873
  this.structuredInputWaiters.delete(structuredInput.requestId);
1599
- resolve(formatStructuredInputResponse({}));
1874
+ resolve(formatResponse({}));
1600
1875
  this.markStructuredInput(conversationId, structuredInput.requestId, {
1601
1876
  inputPending: false,
1602
1877
  inputError: "等待用户输入超时",
1603
1878
  });
1604
1879
  }, PERMISSION_TIMEOUT_MS);
1605
- this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
1880
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
1606
1881
  });
1607
1882
  }
1608
1883
 
@@ -1711,6 +1986,12 @@ export class AgentWorkspaceProxy {
1711
1986
  optionId: selectedOptionId,
1712
1987
  });
1713
1988
  }
1989
+ this.markPermission(payload.conversationId, payload.requestId, {
1990
+ permissionOutcome: payload.outcome,
1991
+ optionId: selectedOptionId,
1992
+ permissionError: undefined,
1993
+ permissionPending: false,
1994
+ });
1714
1995
  this.updateConversationStatus(payload.conversationId, "running");
1715
1996
  }
1716
1997
 
@@ -1725,16 +2006,35 @@ export class AgentWorkspaceProxy {
1725
2006
  if (waiter) {
1726
2007
  clearTimeout(waiter.timer);
1727
2008
  this.structuredInputWaiters.delete(payload.requestId);
1728
- waiter.resolve(formatStructuredInputResponse(payload.answers));
2009
+ const formatResponse = waiter.source === "mcpServer/elicitation/request"
2010
+ ? formatMcpElicitationResponse
2011
+ : formatStructuredInputResponse;
2012
+ waiter.resolve(formatResponse(payload.answers));
1729
2013
  }
1730
2014
  this.markStructuredInput(payload.conversationId, payload.requestId, {
1731
2015
  inputPending: false,
1732
2016
  inputSubmitted: true,
2017
+ inputSubmitting: false,
2018
+ inputError: undefined,
1733
2019
  answers: payload.answers,
1734
2020
  });
1735
2021
  this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
1736
2022
  }
1737
2023
 
2024
+ private markPermission(
2025
+ conversationId: string,
2026
+ requestId: string,
2027
+ metadata: Record<string, unknown>,
2028
+ ): void {
2029
+ const item = this.findItem(conversationId, `permission:${requestId}`);
2030
+ if (!item) return;
2031
+ this.upsertItem(conversationId, {
2032
+ ...item,
2033
+ metadata: { ...(item.metadata ?? {}), ...metadata },
2034
+ updatedAt: Date.now(),
2035
+ });
2036
+ }
2037
+
1738
2038
  private markStructuredInput(
1739
2039
  conversationId: string,
1740
2040
  requestId: string,
@@ -2090,7 +2390,8 @@ function isPermissionRequestMethod(method: string): boolean {
2090
2390
  return (
2091
2391
  method === "session/request_permission" ||
2092
2392
  method.endsWith("/requestApproval") ||
2093
- method === "mcpServer/elicitation/request"
2393
+ method === "mcpServer/elicitation/request" ||
2394
+ method === "claude/requestApproval"
2094
2395
  );
2095
2396
  }
2096
2397
 
@@ -2105,6 +2406,20 @@ function formatStructuredInputResponse(answers: Record<string, string[]>): unkno
2105
2406
  };
2106
2407
  }
2107
2408
 
2409
+ function formatMcpElicitationResponse(answers: Record<string, string[]>): unknown {
2410
+ const content = Object.fromEntries(
2411
+ Object.entries(answers).map(([questionId, values]) => [
2412
+ questionId,
2413
+ values.length <= 1 ? values[0] ?? "" : values,
2414
+ ]),
2415
+ );
2416
+ return {
2417
+ action: Object.keys(content).length > 0 ? "accept" : "cancel",
2418
+ content,
2419
+ _meta: { source: "linkshell" },
2420
+ };
2421
+ }
2422
+
2108
2423
  function formatPermissionResponse(
2109
2424
  source: string | undefined,
2110
2425
  outcome: "allow" | "deny" | "cancelled",
@@ -2122,6 +2437,14 @@ function formatPermissionResponse(
2122
2437
  }
2123
2438
  return { permissions: { type: "managed", network: { enabled: false }, fileSystem: { type: "readOnly" } } };
2124
2439
  }
2440
+ if (source === "claude/requestApproval") {
2441
+ return { behavior: outcome === "allow" ? "allow" : "deny" };
2442
+ }
2443
+ if (source === "mcpServer/elicitation/request") {
2444
+ return outcome === "allow"
2445
+ ? { action: "accept", content: { optionId }, _meta: { source: "linkshell" } }
2446
+ : { action: outcome === "cancelled" ? "cancel" : "decline", content: {}, _meta: { source: "linkshell" } };
2447
+ }
2125
2448
  return {
2126
2449
  outcome:
2127
2450
  outcome === "cancelled"