linkshell-cli 0.2.86 → 0.2.88
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/runtime/acp/agent-session.d.ts +2 -0
- package/dist/cli/src/runtime/acp/agent-session.js +100 -11
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +3 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +106 -7
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/runtime/acp/agent-session.ts +97 -11
- package/src/runtime/acp/agent-workspace.ts +110 -7
|
@@ -169,11 +169,55 @@ function toolInputFromItem(item: Record<string, unknown>): string | undefined {
|
|
|
169
169
|
}
|
|
170
170
|
if (itemType === "fileChange") {
|
|
171
171
|
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
172
|
-
return summarizeFileChanges(changes);
|
|
172
|
+
return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
|
|
173
173
|
}
|
|
174
174
|
return stringifyDefined(item.arguments ?? item.input ?? item.toolInput);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
function looksLikeDiff(text: string): boolean {
|
|
178
|
+
const value = text.trim();
|
|
179
|
+
return (
|
|
180
|
+
value.startsWith("diff --git ") ||
|
|
181
|
+
value.startsWith("@@ ") ||
|
|
182
|
+
value.includes("\n@@ ") ||
|
|
183
|
+
(value.includes("\n--- ") && value.includes("\n+++ "))
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function collectDiffStrings(value: unknown, depth = 0): string[] {
|
|
188
|
+
if (depth > 6 || value === undefined || value === null) return [];
|
|
189
|
+
if (typeof value === "string") return looksLikeDiff(value) ? [value] : [];
|
|
190
|
+
if (Array.isArray(value)) return value.flatMap((entry) => collectDiffStrings(entry, depth + 1));
|
|
191
|
+
const raw = asRecord(value);
|
|
192
|
+
if (!raw) return [];
|
|
193
|
+
const direct: string[] = [];
|
|
194
|
+
const nested: string[] = [];
|
|
195
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
196
|
+
const lowerKey = key.toLowerCase();
|
|
197
|
+
const isDiffField =
|
|
198
|
+
lowerKey.includes("diff") ||
|
|
199
|
+
lowerKey.includes("patch") ||
|
|
200
|
+
lowerKey.includes("unified");
|
|
201
|
+
if (typeof entry === "string" && isDiffField && entry.trim()) {
|
|
202
|
+
direct.push(entry);
|
|
203
|
+
} else if (typeof entry === "object" && entry) {
|
|
204
|
+
nested.push(...collectDiffStrings(entry, depth + 1));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return [...direct, ...nested].filter((entry) => looksLikeDiff(entry));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function extractDiffText(value: unknown): string | undefined {
|
|
211
|
+
const diffs = collectDiffStrings(value)
|
|
212
|
+
.map((entry) => entry.trim())
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
if (diffs.length === 0) return undefined;
|
|
215
|
+
return diffs
|
|
216
|
+
.filter((entry, index, array) => array.indexOf(entry) === index)
|
|
217
|
+
.join("\n\n")
|
|
218
|
+
.slice(0, 24_000);
|
|
219
|
+
}
|
|
220
|
+
|
|
177
221
|
function summarizeFileChanges(changes: unknown[]): string | undefined {
|
|
178
222
|
const lines = changes
|
|
179
223
|
.map((change) => {
|
|
@@ -183,7 +227,7 @@ function summarizeFileChanges(changes: unknown[]): string | undefined {
|
|
|
183
227
|
firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
|
|
184
228
|
firstString(asRecord(raw.update) ?? {}, ["path", "file", "filePath"]);
|
|
185
229
|
const kind = firstString(raw, ["kind", "type", "operation", "action"]);
|
|
186
|
-
return [kind, path].filter(Boolean).join(" ");
|
|
230
|
+
return [kind, path].filter(Boolean).join(" ") || path;
|
|
187
231
|
})
|
|
188
232
|
.filter((line): line is string => Boolean(line));
|
|
189
233
|
return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
|
|
@@ -449,7 +493,6 @@ export class AgentSessionProxy {
|
|
|
449
493
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
450
494
|
method === "thread/status/changed" ||
|
|
451
495
|
method === "thread/tokenUsage/updated" ||
|
|
452
|
-
method === "turn/diff/updated" ||
|
|
453
496
|
method === "serverRequest/resolved" ||
|
|
454
497
|
method === "mcpServer/oauthLogin/completed"
|
|
455
498
|
) {
|
|
@@ -502,6 +545,9 @@ export class AgentSessionProxy {
|
|
|
502
545
|
case "item/fileChange/patchUpdated":
|
|
503
546
|
this.handleFilePatchUpdated(params);
|
|
504
547
|
return;
|
|
548
|
+
case "turn/diff/updated":
|
|
549
|
+
this.handleTurnDiffUpdated(params);
|
|
550
|
+
return;
|
|
505
551
|
case "command/exec/outputDelta":
|
|
506
552
|
this.handleCommandExecDelta(params);
|
|
507
553
|
return;
|
|
@@ -700,17 +746,41 @@ export class AgentSessionProxy {
|
|
|
700
746
|
if (!raw) return;
|
|
701
747
|
const itemId = firstString(raw, ["itemId", "id"]) ?? id("file");
|
|
702
748
|
const changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
703
|
-
const output = summarizeFileChanges(changes);
|
|
749
|
+
const output = extractDiffText(raw) ?? summarizeFileChanges(changes);
|
|
704
750
|
const existing = this.toolCalls.get(itemId);
|
|
705
|
-
const toolCall
|
|
751
|
+
const toolCall = this.withToolCreatedAt({
|
|
706
752
|
id: itemId,
|
|
707
753
|
name: existing?.name ?? "文件修改",
|
|
708
754
|
input: existing?.input,
|
|
709
755
|
output: output || existing?.output,
|
|
710
756
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
711
757
|
status: existing?.status ?? "running",
|
|
712
|
-
};
|
|
713
|
-
|
|
758
|
+
});
|
|
759
|
+
if (!toolCall) return;
|
|
760
|
+
if (toolCall.id !== itemId) this.toolCalls.delete(itemId);
|
|
761
|
+
this.toolCalls.set(toolCall.id, toolCall);
|
|
762
|
+
this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private handleTurnDiffUpdated(params: unknown): void {
|
|
766
|
+
const raw = asRecord(params);
|
|
767
|
+
if (!raw) return;
|
|
768
|
+
const diff = extractDiffText(raw);
|
|
769
|
+
if (!diff) return;
|
|
770
|
+
const itemId = firstString(raw, ["itemId", "id", "turnId"]) ?? "workspace-diff";
|
|
771
|
+
const changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
772
|
+
const existing = this.toolCalls.get(itemId);
|
|
773
|
+
const toolCall = this.withToolCreatedAt({
|
|
774
|
+
id: itemId,
|
|
775
|
+
name: existing?.name ?? "文件修改",
|
|
776
|
+
input: existing?.input ?? summarizeFileChanges(changes),
|
|
777
|
+
output: diff,
|
|
778
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
779
|
+
status: existing?.status ?? "running",
|
|
780
|
+
});
|
|
781
|
+
if (!toolCall) return;
|
|
782
|
+
if (toolCall.id !== itemId) this.toolCalls.delete(itemId);
|
|
783
|
+
this.toolCalls.set(toolCall.id, toolCall);
|
|
714
784
|
this.sendUpdate({ kind: "tool_call", toolCall, status: "running" });
|
|
715
785
|
}
|
|
716
786
|
|
|
@@ -766,15 +836,18 @@ export class AgentSessionProxy {
|
|
|
766
836
|
const itemType = firstString(item, ["type"]);
|
|
767
837
|
const name = toolNameFromItem(item);
|
|
768
838
|
if (!name && !isToolItemType(itemType)) return undefined;
|
|
769
|
-
const
|
|
839
|
+
const bufferedOutput = this.toolOutputBuffers.get(itemId);
|
|
840
|
+
const rawOutput =
|
|
770
841
|
firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
771
842
|
stringifyDefined(item.result ?? item.error ?? item.contentItems);
|
|
772
|
-
const
|
|
843
|
+
const output = itemType === "fileChange"
|
|
844
|
+
? extractDiffText(item) ?? bufferedOutput ?? rawOutput
|
|
845
|
+
: rawOutput ?? bufferedOutput;
|
|
773
846
|
return {
|
|
774
847
|
id: itemId,
|
|
775
848
|
name: name ?? "工具",
|
|
776
849
|
input: toolInputFromItem(item),
|
|
777
|
-
output
|
|
850
|
+
output,
|
|
778
851
|
createdAt: Date.now(),
|
|
779
852
|
status: normalizeToolStatus(item.status, fallbackStatus === "completed"),
|
|
780
853
|
};
|
|
@@ -855,13 +928,26 @@ export class AgentSessionProxy {
|
|
|
855
928
|
|
|
856
929
|
private withToolCreatedAt(toolCall: AgentToolCall | undefined): AgentToolCall | undefined {
|
|
857
930
|
if (!toolCall) return undefined;
|
|
858
|
-
const
|
|
931
|
+
const duplicate = this.findDuplicateFileTool(toolCall);
|
|
932
|
+
const existing = duplicate ?? this.toolCalls.get(toolCall.id);
|
|
859
933
|
return {
|
|
934
|
+
...existing,
|
|
860
935
|
...toolCall,
|
|
936
|
+
id: existing?.id ?? toolCall.id,
|
|
861
937
|
createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
|
|
862
938
|
};
|
|
863
939
|
}
|
|
864
940
|
|
|
941
|
+
private findDuplicateFileTool(toolCall: AgentToolCall): AgentToolCall | undefined {
|
|
942
|
+
const output = toolCall.output?.trim();
|
|
943
|
+
if (!toolCall.name.includes("文件") || !output) return undefined;
|
|
944
|
+
return [...this.toolCalls.values()].find((existing) =>
|
|
945
|
+
existing.id !== toolCall.id &&
|
|
946
|
+
existing.name.includes("文件") &&
|
|
947
|
+
existing.output?.trim() === output
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
865
951
|
private sendCapabilities(): void {
|
|
866
952
|
const enabled = Boolean(this.client && this.initialized && !this.error);
|
|
867
953
|
this.input.send(createEnvelope({
|
|
@@ -195,12 +195,56 @@ function summarizeFileChanges(changes: unknown[]): string | undefined {
|
|
|
195
195
|
firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
|
|
196
196
|
firstString(asRecord(raw.update), ["path", "file", "filePath"]);
|
|
197
197
|
const kind = firstString(raw, ["kind", "type", "operation", "action"]);
|
|
198
|
-
return [kind, path].filter(Boolean).join(" ");
|
|
198
|
+
return [kind, path].filter(Boolean).join(" ") || path;
|
|
199
199
|
})
|
|
200
200
|
.filter((line): line is string => Boolean(line));
|
|
201
201
|
return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
function looksLikeDiff(text: string): boolean {
|
|
205
|
+
const value = text.trim();
|
|
206
|
+
return (
|
|
207
|
+
value.startsWith("diff --git ") ||
|
|
208
|
+
value.startsWith("@@ ") ||
|
|
209
|
+
value.includes("\n@@ ") ||
|
|
210
|
+
(value.includes("\n--- ") && value.includes("\n+++ "))
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function collectDiffStrings(value: unknown, depth = 0): string[] {
|
|
215
|
+
if (depth > 6 || value === undefined || value === null) return [];
|
|
216
|
+
if (typeof value === "string") return looksLikeDiff(value) ? [value] : [];
|
|
217
|
+
if (Array.isArray(value)) return value.flatMap((entry) => collectDiffStrings(entry, depth + 1));
|
|
218
|
+
const raw = asRecord(value);
|
|
219
|
+
if (!raw) return [];
|
|
220
|
+
const direct: string[] = [];
|
|
221
|
+
const nested: string[] = [];
|
|
222
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
223
|
+
const lowerKey = key.toLowerCase();
|
|
224
|
+
const isDiffField =
|
|
225
|
+
lowerKey.includes("diff") ||
|
|
226
|
+
lowerKey.includes("patch") ||
|
|
227
|
+
lowerKey.includes("unified");
|
|
228
|
+
if (typeof entry === "string" && isDiffField && entry.trim()) {
|
|
229
|
+
direct.push(entry);
|
|
230
|
+
} else if (typeof entry === "object" && entry) {
|
|
231
|
+
nested.push(...collectDiffStrings(entry, depth + 1));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return [...direct, ...nested].filter((entry) => looksLikeDiff(entry));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function extractDiffText(value: unknown): string | undefined {
|
|
238
|
+
const diffs = collectDiffStrings(value)
|
|
239
|
+
.map((entry) => entry.trim())
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
if (diffs.length === 0) return undefined;
|
|
242
|
+
return diffs
|
|
243
|
+
.filter((entry, index, array) => array.indexOf(entry) === index)
|
|
244
|
+
.join("\n\n")
|
|
245
|
+
.slice(0, 24_000);
|
|
246
|
+
}
|
|
247
|
+
|
|
204
248
|
function toolInputFromItem(item: Record<string, unknown>): string | undefined {
|
|
205
249
|
const itemType = firstString(item, ["type"]);
|
|
206
250
|
if (itemType === "commandExecution") {
|
|
@@ -211,7 +255,7 @@ function toolInputFromItem(item: Record<string, unknown>): string | undefined {
|
|
|
211
255
|
}
|
|
212
256
|
if (itemType === "fileChange") {
|
|
213
257
|
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
214
|
-
return summarizeFileChanges(changes);
|
|
258
|
+
return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
|
|
215
259
|
}
|
|
216
260
|
return stringifyDefined(item.arguments ?? item.input ?? item.toolInput);
|
|
217
261
|
}
|
|
@@ -645,7 +689,6 @@ export class AgentWorkspaceProxy {
|
|
|
645
689
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
646
690
|
method === "thread/status/changed" ||
|
|
647
691
|
method === "thread/tokenUsage/updated" ||
|
|
648
|
-
method === "turn/diff/updated" ||
|
|
649
692
|
method === "serverRequest/resolved" ||
|
|
650
693
|
method === "mcpServer/oauthLogin/completed"
|
|
651
694
|
) {
|
|
@@ -705,6 +748,9 @@ export class AgentWorkspaceProxy {
|
|
|
705
748
|
case "item/fileChange/patchUpdated":
|
|
706
749
|
this.handleFilePatchUpdated(params);
|
|
707
750
|
return;
|
|
751
|
+
case "turn/diff/updated":
|
|
752
|
+
this.handleTurnDiffUpdated(params);
|
|
753
|
+
return;
|
|
708
754
|
case "command/exec/outputDelta":
|
|
709
755
|
this.handleCommandExecDelta(params);
|
|
710
756
|
return;
|
|
@@ -863,7 +909,9 @@ export class AgentWorkspaceProxy {
|
|
|
863
909
|
this.toolConversationIds.get(itemId) ??
|
|
864
910
|
this.activeConversationId;
|
|
865
911
|
if (!conversationId) return;
|
|
866
|
-
const output =
|
|
912
|
+
const output =
|
|
913
|
+
extractDiffText(raw) ??
|
|
914
|
+
summarizeFileChanges(Array.isArray(raw.changes) ? raw.changes : []);
|
|
867
915
|
const existing = this.findTool(conversationId, itemId);
|
|
868
916
|
this.upsertTool(conversationId, {
|
|
869
917
|
id: itemId,
|
|
@@ -875,6 +923,28 @@ export class AgentWorkspaceProxy {
|
|
|
875
923
|
});
|
|
876
924
|
}
|
|
877
925
|
|
|
926
|
+
private handleTurnDiffUpdated(params: unknown): void {
|
|
927
|
+
const raw = asRecord(params);
|
|
928
|
+
if (!raw) return;
|
|
929
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
930
|
+
if (!conversationId) return;
|
|
931
|
+
const diff = extractDiffText(raw);
|
|
932
|
+
if (!diff) return;
|
|
933
|
+
const itemId =
|
|
934
|
+
firstString(raw, ["itemId", "id", "turnId"]) ??
|
|
935
|
+
`workspace-diff:${conversationId}`;
|
|
936
|
+
const existing = this.findTool(conversationId, itemId);
|
|
937
|
+
const changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
938
|
+
this.upsertTool(conversationId, {
|
|
939
|
+
id: itemId,
|
|
940
|
+
name: existing?.name ?? "文件修改",
|
|
941
|
+
input: existing?.input ?? summarizeFileChanges(changes),
|
|
942
|
+
output: diff,
|
|
943
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
944
|
+
status: existing?.status ?? "running",
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
878
948
|
private handleCommandExecDelta(params: unknown): void {
|
|
879
949
|
const raw = asRecord(params);
|
|
880
950
|
if (!raw) return;
|
|
@@ -968,14 +1038,18 @@ export class AgentWorkspaceProxy {
|
|
|
968
1038
|
const itemType = firstString(item, ["type"]);
|
|
969
1039
|
const name = toolNameFromItem(item);
|
|
970
1040
|
if (!name && !isToolItemType(itemType)) return undefined;
|
|
971
|
-
const
|
|
1041
|
+
const bufferedOutput = this.toolOutputBuffers.get(itemId);
|
|
1042
|
+
const rawOutput =
|
|
972
1043
|
firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
|
|
973
1044
|
stringifyDefined(item.result ?? item.error ?? item.contentItems);
|
|
1045
|
+
const output = itemType === "fileChange"
|
|
1046
|
+
? extractDiffText(item) ?? bufferedOutput ?? rawOutput
|
|
1047
|
+
: rawOutput ?? bufferedOutput;
|
|
974
1048
|
return {
|
|
975
1049
|
id: itemId,
|
|
976
1050
|
name: name ?? "工具",
|
|
977
1051
|
input: toolInputFromItem(item),
|
|
978
|
-
output
|
|
1052
|
+
output,
|
|
979
1053
|
createdAt: Date.now(),
|
|
980
1054
|
status: normalizeToolStatus(item.status, fallbackStatus === "completed"),
|
|
981
1055
|
};
|
|
@@ -1085,11 +1159,19 @@ export class AgentWorkspaceProxy {
|
|
|
1085
1159
|
}
|
|
1086
1160
|
|
|
1087
1161
|
private upsertTool(conversationId: string, toolCall: AgentToolCall): void {
|
|
1088
|
-
const
|
|
1162
|
+
const duplicate = this.findDuplicateFileTool(conversationId, toolCall);
|
|
1163
|
+
if (duplicate && duplicate.id !== toolCall.id) {
|
|
1164
|
+
this.removeToolItem(conversationId, toolCall.id);
|
|
1165
|
+
}
|
|
1166
|
+
const targetToolId = duplicate?.id ?? toolCall.id;
|
|
1167
|
+
const existing = this.findTool(conversationId, targetToolId);
|
|
1089
1168
|
const nextToolCall = {
|
|
1169
|
+
...existing,
|
|
1090
1170
|
...toolCall,
|
|
1171
|
+
id: targetToolId,
|
|
1091
1172
|
createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
|
|
1092
1173
|
};
|
|
1174
|
+
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
1093
1175
|
this.toolConversationIds.set(nextToolCall.id, conversationId);
|
|
1094
1176
|
this.upsertItem(conversationId, {
|
|
1095
1177
|
id: `tool:${nextToolCall.id}`,
|
|
@@ -1101,6 +1183,27 @@ export class AgentWorkspaceProxy {
|
|
|
1101
1183
|
});
|
|
1102
1184
|
}
|
|
1103
1185
|
|
|
1186
|
+
private findDuplicateFileTool(
|
|
1187
|
+
conversationId: string,
|
|
1188
|
+
toolCall: AgentToolCall,
|
|
1189
|
+
): AgentToolCall | undefined {
|
|
1190
|
+
const output = toolCall.output?.trim();
|
|
1191
|
+
if (!toolCall.name.includes("文件") || !output) return undefined;
|
|
1192
|
+
return this.timelines.get(conversationId)?.find((entry) =>
|
|
1193
|
+
entry.type === "tool_call" &&
|
|
1194
|
+
entry.toolCall?.id !== toolCall.id &&
|
|
1195
|
+
entry.toolCall?.name.includes("文件") &&
|
|
1196
|
+
entry.toolCall.output?.trim() === output
|
|
1197
|
+
)?.toolCall;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
private removeToolItem(conversationId: string, toolId: string): void {
|
|
1201
|
+
const timeline = this.timelines.get(conversationId);
|
|
1202
|
+
if (!timeline) return;
|
|
1203
|
+
const index = timeline.findIndex((entry) => entry.id === `tool:${toolId}`);
|
|
1204
|
+
if (index >= 0) timeline.splice(index, 1);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1104
1207
|
private findItem(conversationId: string, itemId: string): AgentTimelineItem | undefined {
|
|
1105
1208
|
return this.timelines.get(conversationId)?.find((item) => item.id === itemId);
|
|
1106
1209
|
}
|