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.
- package/dist/cli/src/index.js +1 -0
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/providers.js +49 -3
- package/dist/cli/src/providers.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +1 -0
- package/dist/cli/src/runtime/acp/acp-client.js +17 -3
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +1 -0
- package/dist/cli/src/runtime/acp/agent-session.js +73 -19
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -1
- package/dist/cli/src/runtime/acp/agent-workspace.js +349 -58
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +51 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +318 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -0
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.js +22 -2
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.js +1 -0
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +1885 -265
- package/dist/shared-protocol/src/index.js +36 -10
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +8 -5
- package/src/index.ts +1 -0
- package/src/providers.ts +50 -3
- package/src/runtime/acp/acp-client.ts +18 -3
- package/src/runtime/acp/agent-session.ts +68 -14
- package/src/runtime/acp/agent-workspace.ts +378 -55
- package/src/runtime/acp/claude-sdk-client.ts +372 -0
- package/src/runtime/acp/provider-resolver.ts +24 -3
- 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
|
|
223
|
-
const
|
|
224
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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 (
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
646
|
+
turnId: this.currentTurnIds.get(payload.conversationId),
|
|
570
647
|
});
|
|
571
|
-
this.
|
|
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
|
-
|
|
626
|
-
this.agentProtocols.set(provider,
|
|
627
|
-
const
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
:
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
|
849
|
+
supportsPermission,
|
|
674
850
|
supportsPlan: enabled,
|
|
675
851
|
supportsCancel: enabled,
|
|
676
|
-
models:
|
|
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:
|
|
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
|
-
|
|
853
|
-
if (conversation.
|
|
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
|
-
|
|
915
|
-
|
|
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
|
-
|
|
921
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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(
|
|
1589
|
+
return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
1340
1590
|
const structuredInput = decodeStructuredInput(raw);
|
|
1341
1591
|
if (!structuredInput)
|
|
1342
|
-
return waitForResponse ? Promise.resolve(
|
|
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(
|
|
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.
|
|
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" }
|