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
@@ -1,6 +1,7 @@
1
1
  import { basename } from "node:path";
2
2
  import { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
3
3
  import { AcpClient } from "./acp-client.js";
4
+ import { ClaudeSdkClient } from "./claude-sdk-client.js";
4
5
  import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
5
6
  import { resolveAgentCommand } from "./provider-resolver.js";
6
7
  const PERMISSION_TIMEOUT_MS = 5 * 60_000;
@@ -219,9 +220,20 @@ function commandExecutionFromTool(toolCall) {
219
220
  status: toolCall.status,
220
221
  };
221
222
  }
222
- function fileChangeFromTool(toolCall) {
223
- const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
224
- const entries = (toolCall.input ?? "")
223
+ function fileChangeFromStructuredInput(input) {
224
+ const raw = input?.trim();
225
+ if (!raw)
226
+ return [];
227
+ try {
228
+ const parsed = JSON.parse(raw);
229
+ if (parsed && typeof parsed === "object") {
230
+ return fileChangeEntriesFromItem(parsed);
231
+ }
232
+ }
233
+ catch {
234
+ // Fall through to line parser.
235
+ }
236
+ return raw
225
237
  .split("\n")
226
238
  .map((line) => line.trim())
227
239
  .filter(Boolean)
@@ -234,6 +246,10 @@ function fileChangeFromTool(toolCall) {
234
246
  return entry;
235
247
  })
236
248
  .filter((entry) => entry.path.length > 0);
249
+ }
250
+ function fileChangeFromTool(toolCall) {
251
+ const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
252
+ const entries = fileChangeFromStructuredInput(toolCall.input);
237
253
  if (entries.length === 0 && !diff && !toolCall.output)
238
254
  return undefined;
239
255
  return {
@@ -486,35 +502,95 @@ function providerLabel(provider) {
486
502
  return "Claude";
487
503
  return "Custom";
488
504
  }
489
- function providerModels(provider) {
490
- if (provider === "claude") {
491
- return [
492
- { id: "default", label: "默认模型" },
493
- { id: "opus", label: "Opus 4.7" },
494
- { id: "sonnet", label: "Sonnet 4.6" },
495
- { id: "haiku", label: "Haiku 4.5" },
496
- ];
505
+ const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
506
+ const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
507
+ function parseModelListCapabilities(value) {
508
+ const raw = asRecord(value);
509
+ const modelsValue = Array.isArray(value) ? value :
510
+ Array.isArray(raw?.models) ? raw.models :
511
+ Array.isArray(raw?.items) ? raw.items :
512
+ Array.isArray(raw?.modelOptions) ? raw.modelOptions :
513
+ [];
514
+ const models = modelsValue
515
+ .map((entry, index) => {
516
+ const model = asRecord(entry);
517
+ if (!model) {
518
+ return typeof entry === "string" && entry
519
+ ? { id: entry, label: entry }
520
+ : undefined;
521
+ }
522
+ const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
523
+ const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
524
+ return { id: modelId, label };
525
+ })
526
+ .filter((entry) => Boolean(entry));
527
+ const defaultModel = firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
528
+ firstString(asRecord(raw?.defaults), ["model"]);
529
+ const effortsValue = Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
530
+ Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
531
+ Array.isArray(raw?.efforts) ? raw.efforts :
532
+ undefined;
533
+ const reasoningEfforts = effortsValue
534
+ ?.filter((entry) => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry));
535
+ if (models.length === 0 && !defaultModel && !reasoningEfforts?.length)
536
+ return undefined;
537
+ return {
538
+ ...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
539
+ ...(defaultModel ? { defaultModel } : {}),
540
+ ...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
541
+ };
542
+ }
543
+ function parseTimestamp(value) {
544
+ if (typeof value === "number" && Number.isFinite(value)) {
545
+ return value > 10_000_000_000 ? value : value * 1000;
497
546
  }
498
- if (provider === "codex") {
499
- return [
500
- { id: "default", label: "默认模型" },
501
- { id: "gpt-5.5", label: "GPT-5.5" },
502
- { id: "gpt-5.4", label: "GPT-5.4" },
503
- { id: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
504
- { id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
505
- ];
547
+ if (typeof value === "string" && value.trim()) {
548
+ const parsed = Date.parse(value);
549
+ return Number.isNaN(parsed) ? undefined : parsed;
506
550
  }
507
- return [{ id: "default", label: "默认模型" }];
551
+ return undefined;
552
+ }
553
+ function parseRemoteSessions(value) {
554
+ const raw = asRecord(value);
555
+ const sessionsValue = Array.isArray(value) ? value :
556
+ Array.isArray(raw?.threads) ? raw.threads :
557
+ Array.isArray(raw?.sessions) ? raw.sessions :
558
+ Array.isArray(raw?.items) ? raw.items :
559
+ [];
560
+ const result = [];
561
+ for (const entry of sessionsValue) {
562
+ const session = asRecord(entry);
563
+ if (!session) {
564
+ if (typeof entry === "string" && entry)
565
+ result.push({ id: entry });
566
+ continue;
567
+ }
568
+ const nestedThread = asRecord(session.thread);
569
+ const source = nestedThread ?? session;
570
+ const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]);
571
+ if (!id)
572
+ continue;
573
+ result.push({
574
+ id,
575
+ cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
576
+ title: firstString(source, ["title", "name", "summary"]),
577
+ model: firstString(source, ["model", "modelId"]),
578
+ createdAt: parseTimestamp(source.createdAt ?? source.created_at),
579
+ lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
580
+ });
581
+ }
582
+ return result;
508
583
  }
509
584
  export class AgentWorkspaceProxy {
510
585
  input;
511
586
  clients = new Map();
512
587
  agentProtocols = new Map();
588
+ providerCapabilities = new Map();
513
589
  initialized = false;
514
590
  status = "unavailable";
515
591
  error;
516
592
  activeConversationId;
517
- currentTurnId;
593
+ currentTurnIds = new Map();
518
594
  conversations = new Map();
519
595
  conversationByAgentSessionId = new Map();
520
596
  timelines = new Map();
@@ -541,6 +617,7 @@ export class AgentWorkspaceProxy {
541
617
  }
542
618
  case "agent.v2.conversation.list": {
543
619
  const payload = parseTypedPayload("agent.v2.conversation.list", envelope.payload);
620
+ await this.syncProviderSessions();
544
621
  const conversations = [...this.conversations.values()].filter((conversation) => payload.includeArchived ? true : !conversation.archived);
545
622
  this.input.send(createEnvelope({
546
623
  type: "agent.v2.conversation.list.result",
@@ -566,9 +643,9 @@ export class AgentWorkspaceProxy {
566
643
  const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
567
644
  cancelClient?.cancel({
568
645
  sessionId: conversation?.agentSessionId,
569
- turnId: this.currentTurnId,
646
+ turnId: this.currentTurnIds.get(payload.conversationId),
570
647
  });
571
- this.currentTurnId = undefined;
648
+ this.currentTurnIds.delete(payload.conversationId);
572
649
  this.updateConversationStatus(payload.conversationId, "idle");
573
650
  this.emitStatus(payload.conversationId, "idle", "已停止");
574
651
  break;
@@ -622,61 +699,174 @@ export class AgentWorkspaceProxy {
622
699
  }
623
700
  return undefined;
624
701
  }
625
- try {
626
- this.agentProtocols.set(provider, resolved.protocol);
627
- const isClaudeStreamJson = resolved.protocol === "claude-stream-json";
628
- const client = isClaudeStreamJson
629
- ? new ClaudeStreamJsonClient({
630
- command: resolved.command,
631
- protocol: resolved.protocol,
632
- framing: resolved.framing,
702
+ const tryCreateClient = async (config) => {
703
+ this.agentProtocols.set(provider, config.protocol);
704
+ const isClaudeSdk = config.protocol === "claude-agent-sdk";
705
+ const isClaudeStreamJson = config.protocol === "claude-stream-json";
706
+ const client = isClaudeSdk
707
+ ? new ClaudeSdkClient({
708
+ command: config.command,
709
+ protocol: config.protocol,
710
+ framing: config.framing,
633
711
  cwd: this.input.cwd,
634
712
  onNotification: (method, params) => this.handleNotification(method, params),
635
713
  onRequest: (method, params) => this.handleRequest(method, params),
636
714
  onExit: (message) => this.handleProviderExit(provider, message),
637
715
  })
638
- : new AcpClient({
639
- command: resolved.command,
640
- protocol: resolved.protocol,
641
- framing: resolved.framing,
642
- cwd: this.input.cwd,
643
- onNotification: (method, params) => this.handleNotification(method, params),
644
- onRequest: (method, params) => this.handleRequest(method, params),
645
- onExit: (message) => this.handleProviderExit(provider, message),
646
- });
716
+ : isClaudeStreamJson
717
+ ? new ClaudeStreamJsonClient({
718
+ command: config.command,
719
+ protocol: config.protocol,
720
+ framing: config.framing,
721
+ cwd: this.input.cwd,
722
+ onNotification: (method, params) => this.handleNotification(method, params),
723
+ onRequest: (method, params) => this.handleRequest(method, params),
724
+ onExit: (message) => this.handleProviderExit(provider, message),
725
+ })
726
+ : new AcpClient({
727
+ command: config.command,
728
+ protocol: config.protocol,
729
+ framing: config.framing,
730
+ cwd: this.input.cwd,
731
+ onNotification: (method, params) => this.handleNotification(method, params),
732
+ onRequest: (method, params) => this.handleRequest(method, params),
733
+ onExit: (message) => this.handleProviderExit(provider, message),
734
+ });
647
735
  await client.initialize();
736
+ return client;
737
+ };
738
+ try {
739
+ const client = await tryCreateClient(resolved);
648
740
  this.clients.set(provider, client);
741
+ await this.refreshProviderCapabilities(provider, client, resolved.protocol);
649
742
  this.status = "idle";
650
743
  this.error = undefined;
651
744
  this.sendCapabilities();
652
745
  return client;
653
746
  }
654
747
  catch (error) {
748
+ if (provider === "claude" && resolved.protocol === "claude-agent-sdk") {
749
+ if (this.input.verbose) {
750
+ process.stderr.write(`[agent:v2] Claude SDK failed, falling back to stream-json: ${error instanceof Error ? error.message : String(error)}\n`);
751
+ }
752
+ try {
753
+ const fallback = {
754
+ provider,
755
+ command: "claude --print --output-format stream-json --input-format stream-json --verbose --permission-mode bypassPermissions",
756
+ protocol: "claude-stream-json",
757
+ framing: "newline",
758
+ };
759
+ const client = await tryCreateClient(fallback);
760
+ this.clients.set(provider, client);
761
+ await this.refreshProviderCapabilities(provider, client, fallback.protocol);
762
+ this.status = "idle";
763
+ this.error = undefined;
764
+ this.sendCapabilities();
765
+ return client;
766
+ }
767
+ catch (fallbackError) {
768
+ if (this.input.verbose) {
769
+ process.stderr.write(`[agent:v2] Claude stream-json fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}\n`);
770
+ }
771
+ }
772
+ }
655
773
  if (this.input.verbose) {
656
774
  process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
657
775
  }
658
776
  return undefined;
659
777
  }
660
778
  }
779
+ async refreshProviderCapabilities(provider, client, protocol) {
780
+ if (!(client instanceof AcpClient) || protocol !== "codex-app-server")
781
+ return;
782
+ try {
783
+ const result = await client.listModels();
784
+ const runtimeCapabilities = parseModelListCapabilities(result);
785
+ if (runtimeCapabilities)
786
+ this.providerCapabilities.set(provider, runtimeCapabilities);
787
+ }
788
+ catch (error) {
789
+ if (this.input.verbose) {
790
+ process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
791
+ }
792
+ }
793
+ }
794
+ async syncProviderSessions() {
795
+ await this.initialize();
796
+ for (const [provider, client] of this.clients) {
797
+ try {
798
+ const result = await client.listSessions();
799
+ for (const remote of parseRemoteSessions(result)) {
800
+ const agentSessionId = remote.id;
801
+ const existingId = this.conversationByAgentSessionId.get(agentSessionId);
802
+ const now = Date.now();
803
+ const conversationId = existingId ?? `agent:${agentSessionId}`;
804
+ const existing = this.conversations.get(conversationId);
805
+ const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
806
+ const conversation = {
807
+ id: conversationId,
808
+ agentSessionId,
809
+ provider,
810
+ cwd,
811
+ title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
812
+ model: remote.model ?? existing?.model,
813
+ reasoningEffort: existing?.reasoningEffort,
814
+ permissionMode: existing?.permissionMode,
815
+ status: existing?.status ?? "idle",
816
+ archived: existing?.archived ?? false,
817
+ lastMessagePreview: existing?.lastMessagePreview,
818
+ lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
819
+ createdAt: remote.createdAt ?? existing?.createdAt ?? now,
820
+ };
821
+ this.conversations.set(conversation.id, conversation);
822
+ this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
823
+ this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
824
+ }
825
+ }
826
+ catch (error) {
827
+ if (this.input.verbose) {
828
+ process.stderr.write(`[agent:v2] session list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
829
+ }
830
+ }
831
+ }
832
+ }
661
833
  sendCapabilities() {
662
834
  const providers = this.input.availableProviders.map((provider) => {
663
835
  const client = this.clients.get(provider);
664
836
  const protocol = this.agentProtocols.get(provider);
837
+ const runtimeCapabilities = this.providerCapabilities.get(provider);
665
838
  const enabled = Boolean(client);
666
839
  const supportsImages = enabled && protocol === "codex-app-server";
840
+ const isClaudeFallback = protocol === "claude-stream-json";
841
+ const supportsPermission = enabled && !isClaudeFallback;
842
+ const supportsReasoningEffort = enabled && !isClaudeFallback;
667
843
  return {
668
844
  id: provider,
669
845
  label: providerLabel(provider),
670
846
  enabled,
671
847
  reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
672
848
  supportsImages,
673
- supportsPermission: enabled,
849
+ supportsPermission,
674
850
  supportsPlan: enabled,
675
851
  supportsCancel: enabled,
676
- models: providerModels(provider),
852
+ models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
853
+ defaultModel: runtimeCapabilities?.defaultModel ?? "default",
854
+ reasoningEfforts: supportsReasoningEffort
855
+ ? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
856
+ : [],
857
+ permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
858
+ features: {
859
+ images: supportsImages,
860
+ permissions: supportsPermission,
861
+ plan: enabled,
862
+ cancel: enabled,
863
+ reasoningEffort: supportsReasoningEffort,
864
+ streamJsonFallback: isClaudeFallback,
865
+ },
677
866
  };
678
867
  });
679
868
  const anyEnabled = providers.some((p) => p.enabled);
869
+ const anyPermission = providers.some((p) => p.supportsPermission);
680
870
  this.input.send(createEnvelope({
681
871
  type: "agent.v2.capabilities",
682
872
  sessionId: this.input.sessionId,
@@ -691,7 +881,7 @@ export class AgentWorkspaceProxy {
691
881
  supportsSessionLoad: anyEnabled,
692
882
  supportsImages: providers.some((p) => p.supportsImages),
693
883
  supportsAudio: false,
694
- supportsPermission: anyEnabled,
884
+ supportsPermission: anyPermission,
695
885
  supportsPlan: anyEnabled,
696
886
  supportsCancel: anyEnabled,
697
887
  },
@@ -849,8 +1039,17 @@ export class AgentWorkspaceProxy {
849
1039
  permissionMode: payload.permissionMode,
850
1040
  cwd: conversation.cwd,
851
1041
  });
852
- this.currentTurnId = this.extractTurnId(result) ?? this.currentTurnId;
853
- if (conversation.status === "running") {
1042
+ const nextAgentSessionId = this.extractSessionId(result);
1043
+ if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
1044
+ if (conversation.agentSessionId)
1045
+ this.conversationByAgentSessionId.delete(conversation.agentSessionId);
1046
+ conversation.agentSessionId = nextAgentSessionId;
1047
+ this.conversationByAgentSessionId.set(nextAgentSessionId, conversation.id);
1048
+ }
1049
+ const turnId = this.extractTurnId(result);
1050
+ if (turnId)
1051
+ this.currentTurnIds.set(conversation.id, turnId);
1052
+ if (conversation.status === "running" && protocol !== "codex-app-server") {
854
1053
  this.updateConversationStatus(conversation.id, "idle");
855
1054
  }
856
1055
  }
@@ -870,6 +1069,9 @@ export class AgentWorkspaceProxy {
870
1069
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
871
1070
  return this.handleStructuredInput(params, true);
872
1071
  }
1072
+ if (method === "mcpServer/elicitation/request") {
1073
+ return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
1074
+ }
873
1075
  if (isPermissionRequestMethod(method)) {
874
1076
  return this.handlePermission(params, true, method);
875
1077
  }
@@ -896,6 +1098,10 @@ export class AgentWorkspaceProxy {
896
1098
  this.handleStructuredInput(params);
897
1099
  return;
898
1100
  }
1101
+ if (method === "mcpServer/elicitation/request") {
1102
+ this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method });
1103
+ return;
1104
+ }
899
1105
  if (isPermissionRequestMethod(method)) {
900
1106
  this.handlePermission(params, false, method);
901
1107
  return;
@@ -911,15 +1117,19 @@ export class AgentWorkspaceProxy {
911
1117
  return;
912
1118
  }
913
1119
  if (method === "turn/started") {
914
- this.currentTurnId = this.extractTurnId(params) ?? this.currentTurnId;
915
- if (conversationId)
1120
+ if (conversationId) {
1121
+ const turnId = this.extractTurnId(params);
1122
+ if (turnId)
1123
+ this.currentTurnIds.set(conversationId, turnId);
916
1124
  this.updateConversationStatus(conversationId, "running");
1125
+ }
917
1126
  return;
918
1127
  }
919
1128
  if (method === "turn/completed") {
920
- this.currentTurnId = undefined;
921
- if (conversationId)
1129
+ if (conversationId) {
1130
+ this.currentTurnIds.delete(conversationId);
922
1131
  this.updateConversationStatus(conversationId, "idle");
1132
+ }
923
1133
  return;
924
1134
  }
925
1135
  if (method === "session/request_permission") {
@@ -957,7 +1167,11 @@ export class AgentWorkspaceProxy {
957
1167
  this.handleCommandExecDelta(params);
958
1168
  return;
959
1169
  case "item/autoApprovalReview/started":
1170
+ this.handleAutoApprovalReview(params, true);
1171
+ return;
960
1172
  case "item/autoApprovalReview/completed":
1173
+ this.handleAutoApprovalReview(params, false);
1174
+ return;
961
1175
  case "item/commandExecution/terminalInteraction":
962
1176
  return;
963
1177
  }
@@ -1199,6 +1413,36 @@ export class AgentWorkspaceProxy {
1199
1413
  status: "running",
1200
1414
  });
1201
1415
  }
1416
+ handleAutoApprovalReview(params, streaming) {
1417
+ const raw = asRecord(params) ?? {};
1418
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1419
+ if (!conversationId)
1420
+ return;
1421
+ const itemId = firstString(raw, ["itemId", "id", "reviewId"]) ?? "auto-approval-review";
1422
+ const existing = this.findItem(conversationId, itemId);
1423
+ const decision = firstString(raw, ["decision", "result", "outcome", "status"]);
1424
+ const summary = firstString(raw, ["summary", "message", "text", "reason"]) ??
1425
+ stringifyDefined(raw.review ?? raw.details);
1426
+ this.upsertItem(conversationId, {
1427
+ id: itemId,
1428
+ conversationId,
1429
+ type: "status",
1430
+ kind: "review",
1431
+ role: "system",
1432
+ turnId: this.extractTurnId(raw) ?? this.currentTurnIds.get(conversationId),
1433
+ itemId,
1434
+ text: summary ?? (streaming ? "正在审查自动授权" : decision ? `自动授权审查:${decision}` : "已完成自动授权审查"),
1435
+ metadata: {
1436
+ ...(existing?.metadata ?? {}),
1437
+ autoApprovalReview: true,
1438
+ ...(decision ? { decision } : {}),
1439
+ },
1440
+ createdAt: existing?.createdAt ?? Date.now(),
1441
+ updatedAt: Date.now(),
1442
+ isStreaming: streaming,
1443
+ });
1444
+ this.updateConversationPreview(conversationId, streaming ? "正在审查自动授权" : "已完成自动授权审查", streaming ? "running" : undefined);
1445
+ }
1202
1446
  handleCompletedMessageItem(item, streaming) {
1203
1447
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1204
1448
  if (!conversationId)
@@ -1271,7 +1515,7 @@ export class AgentWorkspaceProxy {
1271
1515
  conversationId,
1272
1516
  type: "status",
1273
1517
  role: "system",
1274
- turnId: this.extractTurnId(item) ?? this.currentTurnId,
1518
+ turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
1275
1519
  itemId,
1276
1520
  createdAt: existing?.createdAt ?? Date.now(),
1277
1521
  updatedAt: Date.now(),
@@ -1280,10 +1524,12 @@ export class AgentWorkspaceProxy {
1280
1524
  if (normalized === "reasoning" || normalized === "thinking") {
1281
1525
  const text = firstString(item, ["text", "content", "summary", "message"]) ??
1282
1526
  stringifyDefined(item.contentItems ?? item.summary);
1527
+ if (!text?.trim())
1528
+ return true;
1283
1529
  this.upsertItem(conversationId, {
1284
1530
  ...base,
1285
1531
  kind: "thinking",
1286
- text: text ?? (streaming ? "正在思考" : "完成思考"),
1532
+ text,
1287
1533
  });
1288
1534
  return true;
1289
1535
  }
@@ -1322,7 +1568,7 @@ export class AgentWorkspaceProxy {
1322
1568
  type: "status",
1323
1569
  kind: "subagent_action",
1324
1570
  role: "system",
1325
- turnId: this.extractTurnId(item) ?? this.currentTurnId,
1571
+ turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
1326
1572
  itemId,
1327
1573
  text,
1328
1574
  subagent,
@@ -1335,11 +1581,15 @@ export class AgentWorkspaceProxy {
1335
1581
  handleStructuredInput(params, waitForResponse = false) {
1336
1582
  const raw = asRecord(params) ?? {};
1337
1583
  const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1584
+ const source = firstString(raw, ["method", "source", "requestMethod"]);
1585
+ const formatResponse = source === "mcpServer/elicitation/request"
1586
+ ? formatMcpElicitationResponse
1587
+ : formatStructuredInputResponse;
1338
1588
  if (!conversationId)
1339
- return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1589
+ return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
1340
1590
  const structuredInput = decodeStructuredInput(raw);
1341
1591
  if (!structuredInput)
1342
- return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1592
+ return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
1343
1593
  const text = structuredInput.questions.map((question) => question.question).join("\n");
1344
1594
  this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
1345
1595
  this.upsertItem(conversationId, {
@@ -1361,13 +1611,13 @@ export class AgentWorkspaceProxy {
1361
1611
  const timer = setTimeout(() => {
1362
1612
  this.pendingStructuredInputs.delete(structuredInput.requestId);
1363
1613
  this.structuredInputWaiters.delete(structuredInput.requestId);
1364
- resolve(formatStructuredInputResponse({}));
1614
+ resolve(formatResponse({}));
1365
1615
  this.markStructuredInput(conversationId, structuredInput.requestId, {
1366
1616
  inputPending: false,
1367
1617
  inputError: "等待用户输入超时",
1368
1618
  });
1369
1619
  }, PERMISSION_TIMEOUT_MS);
1370
- this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
1620
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
1371
1621
  });
1372
1622
  }
1373
1623
  toolCallFromItem(item, fallbackStatus) {
@@ -1460,6 +1710,12 @@ export class AgentWorkspaceProxy {
1460
1710
  optionId: selectedOptionId,
1461
1711
  });
1462
1712
  }
1713
+ this.markPermission(payload.conversationId, payload.requestId, {
1714
+ permissionOutcome: payload.outcome,
1715
+ optionId: selectedOptionId,
1716
+ permissionError: undefined,
1717
+ permissionPending: false,
1718
+ });
1463
1719
  this.updateConversationStatus(payload.conversationId, "running");
1464
1720
  }
1465
1721
  respondStructuredInput(payload) {
@@ -1469,15 +1725,30 @@ export class AgentWorkspaceProxy {
1469
1725
  if (waiter) {
1470
1726
  clearTimeout(waiter.timer);
1471
1727
  this.structuredInputWaiters.delete(payload.requestId);
1472
- waiter.resolve(formatStructuredInputResponse(payload.answers));
1728
+ const formatResponse = waiter.source === "mcpServer/elicitation/request"
1729
+ ? formatMcpElicitationResponse
1730
+ : formatStructuredInputResponse;
1731
+ waiter.resolve(formatResponse(payload.answers));
1473
1732
  }
1474
1733
  this.markStructuredInput(payload.conversationId, payload.requestId, {
1475
1734
  inputPending: false,
1476
1735
  inputSubmitted: true,
1736
+ inputSubmitting: false,
1737
+ inputError: undefined,
1477
1738
  answers: payload.answers,
1478
1739
  });
1479
1740
  this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
1480
1741
  }
1742
+ markPermission(conversationId, requestId, metadata) {
1743
+ const item = this.findItem(conversationId, `permission:${requestId}`);
1744
+ if (!item)
1745
+ return;
1746
+ this.upsertItem(conversationId, {
1747
+ ...item,
1748
+ metadata: { ...(item.metadata ?? {}), ...metadata },
1749
+ updatedAt: Date.now(),
1750
+ });
1751
+ }
1481
1752
  markStructuredInput(conversationId, requestId, metadata) {
1482
1753
  const item = this.findItem(conversationId, `input:${requestId}`);
1483
1754
  if (!item)
@@ -1795,7 +2066,8 @@ function selectPermissionOption(permission, outcome) {
1795
2066
  function isPermissionRequestMethod(method) {
1796
2067
  return (method === "session/request_permission" ||
1797
2068
  method.endsWith("/requestApproval") ||
1798
- method === "mcpServer/elicitation/request");
2069
+ method === "mcpServer/elicitation/request" ||
2070
+ method === "claude/requestApproval");
1799
2071
  }
1800
2072
  function formatStructuredInputResponse(answers) {
1801
2073
  return {
@@ -1805,6 +2077,17 @@ function formatStructuredInputResponse(answers) {
1805
2077
  ])),
1806
2078
  };
1807
2079
  }
2080
+ function formatMcpElicitationResponse(answers) {
2081
+ const content = Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
2082
+ questionId,
2083
+ values.length <= 1 ? values[0] ?? "" : values,
2084
+ ]));
2085
+ return {
2086
+ action: Object.keys(content).length > 0 ? "accept" : "cancel",
2087
+ content,
2088
+ _meta: { source: "linkshell" },
2089
+ };
2090
+ }
1808
2091
  function formatPermissionResponse(source, outcome, optionId) {
1809
2092
  if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
1810
2093
  return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };
@@ -1818,6 +2101,14 @@ function formatPermissionResponse(source, outcome, optionId) {
1818
2101
  }
1819
2102
  return { permissions: { type: "managed", network: { enabled: false }, fileSystem: { type: "readOnly" } } };
1820
2103
  }
2104
+ if (source === "claude/requestApproval") {
2105
+ return { behavior: outcome === "allow" ? "allow" : "deny" };
2106
+ }
2107
+ if (source === "mcpServer/elicitation/request") {
2108
+ return outcome === "allow"
2109
+ ? { action: "accept", content: { optionId }, _meta: { source: "linkshell" } }
2110
+ : { action: outcome === "cancelled" ? "cancel" : "decline", content: {}, _meta: { source: "linkshell" } };
2111
+ }
1821
2112
  return {
1822
2113
  outcome: outcome === "cancelled"
1823
2114
  ? { outcome: "cancelled" }