linkshell-cli 0.3.11 → 0.3.13
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-workspace.d.ts +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +220 -32
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/codex-sessions.js +87 -4
- package/dist/cli/src/runtime/acp/codex-sessions.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/runtime/acp/agent-workspace.ts +247 -33
- package/src/runtime/acp/codex-sessions.ts +88 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { basename, join, relative } from "node:path";
|
|
4
5
|
import {
|
|
@@ -207,6 +208,8 @@ interface PendingStructuredInputWaiter {
|
|
|
207
208
|
|
|
208
209
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
209
210
|
const MAX_TIMELINE_ITEMS = 200;
|
|
211
|
+
const MAX_SNAPSHOT_ITEMS = 80;
|
|
212
|
+
const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
|
|
210
213
|
|
|
211
214
|
function id(prefix: string): string {
|
|
212
215
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -221,6 +224,71 @@ function stringify(value: unknown): string {
|
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
function truncateUtf8(value: string | undefined, maxBytes = MAX_SNAPSHOT_TEXT_BYTES): string | undefined {
|
|
228
|
+
if (!value) return value;
|
|
229
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
230
|
+
let end = Math.min(value.length, maxBytes);
|
|
231
|
+
while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) {
|
|
232
|
+
end = Math.floor(end * 0.9);
|
|
233
|
+
}
|
|
234
|
+
return `${value.slice(0, end)}\n\n[truncated by LinkShell: original ${Buffer.byteLength(value, "utf8")} bytes]`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function snapshotContentBlocks(
|
|
238
|
+
blocks: AgentContentBlock[] | undefined,
|
|
239
|
+
options: { stripImages?: boolean } = {},
|
|
240
|
+
): AgentContentBlock[] | undefined {
|
|
241
|
+
if (!blocks) return undefined;
|
|
242
|
+
return blocks.map((block) =>
|
|
243
|
+
block.type === "image" && options.stripImages !== false
|
|
244
|
+
? { ...block, data: undefined, text: block.text || "图片附件" }
|
|
245
|
+
: { ...block, text: truncateUtf8(block.text) },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function snapshotTimelineItem(
|
|
250
|
+
item: AgentTimelineItem,
|
|
251
|
+
options: { stripImages?: boolean } = {},
|
|
252
|
+
): AgentTimelineItem {
|
|
253
|
+
return {
|
|
254
|
+
...item,
|
|
255
|
+
content: snapshotContentBlocks(item.content, options),
|
|
256
|
+
text: truncateUtf8(item.text),
|
|
257
|
+
toolCall: item.toolCall
|
|
258
|
+
? {
|
|
259
|
+
...item.toolCall,
|
|
260
|
+
input: truncateUtf8(item.toolCall.input),
|
|
261
|
+
output: truncateUtf8(item.toolCall.output),
|
|
262
|
+
}
|
|
263
|
+
: undefined,
|
|
264
|
+
commandExecution: item.commandExecution
|
|
265
|
+
? {
|
|
266
|
+
...item.commandExecution,
|
|
267
|
+
command: truncateUtf8(item.commandExecution.command, 16 * 1024),
|
|
268
|
+
output: truncateUtf8(item.commandExecution.output),
|
|
269
|
+
}
|
|
270
|
+
: undefined,
|
|
271
|
+
fileChange: item.fileChange
|
|
272
|
+
? {
|
|
273
|
+
...item.fileChange,
|
|
274
|
+
diff: truncateUtf8(item.fileChange.diff),
|
|
275
|
+
summary: truncateUtf8(item.fileChange.summary),
|
|
276
|
+
}
|
|
277
|
+
: undefined,
|
|
278
|
+
permission: item.permission
|
|
279
|
+
? {
|
|
280
|
+
...item.permission,
|
|
281
|
+
toolInput: truncateUtf8(item.permission.toolInput),
|
|
282
|
+
context: truncateUtf8(item.permission.context),
|
|
283
|
+
}
|
|
284
|
+
: undefined,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function snapshotTimelineItems(items: AgentTimelineItem[]): AgentTimelineItem[] {
|
|
289
|
+
return items.slice(-MAX_SNAPSHOT_ITEMS).map((item) => snapshotTimelineItem(item));
|
|
290
|
+
}
|
|
291
|
+
|
|
224
292
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
225
293
|
return typeof value === "object" && value ? value as Record<string, unknown> : undefined;
|
|
226
294
|
}
|
|
@@ -768,6 +836,30 @@ interface ProviderRuntimeCapabilities {
|
|
|
768
836
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
769
837
|
const CLAUDE_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"] as const;
|
|
770
838
|
const AGENT_PERMISSION_MODES: AgentPermissionMode[] = ["read_only", "workspace_write", "full_access"];
|
|
839
|
+
const COMMAND_OUTPUT_MAX_BYTES = 96 * 1024;
|
|
840
|
+
const LINKSHELL_NATIVE_COMMANDS: Array<{
|
|
841
|
+
name: string;
|
|
842
|
+
description: string;
|
|
843
|
+
category: string;
|
|
844
|
+
argsMode?: AgentCommandDescriptor["argsMode"];
|
|
845
|
+
destructive?: boolean;
|
|
846
|
+
providers?: AgentProvider[];
|
|
847
|
+
}> = [
|
|
848
|
+
{ name: "status", description: "Show current Agent and workspace status", category: "LinkShell", argsMode: "none" },
|
|
849
|
+
{ name: "plan", description: "Enter Plan mode for the next turn", category: "Agent", argsMode: "none" },
|
|
850
|
+
{ name: "exit-plan", description: "Exit Plan mode", category: "Agent", argsMode: "none" },
|
|
851
|
+
{ name: "review", description: "Ask the Agent to review current local changes", category: "Agent", argsMode: "optional" },
|
|
852
|
+
{ name: "subagents", description: "Ask the Agent to split work across subagents when useful", category: "Agent", argsMode: "optional" },
|
|
853
|
+
{ name: "compact", description: "Compact the active Codex context", category: "Codex", argsMode: "none", providers: ["codex"] },
|
|
854
|
+
{ name: "clear", description: "Start a fresh Agent context for this conversation", category: "Agent", argsMode: "none", destructive: true },
|
|
855
|
+
{ name: "git-status", description: "Show branch and working tree status", category: "Git", argsMode: "none" },
|
|
856
|
+
{ name: "git-diff", description: "Show a compact diffstat for current changes", category: "Git", argsMode: "none" },
|
|
857
|
+
{ name: "git-commit", description: "Commit staged changes with the given message", category: "Git", argsMode: "required" },
|
|
858
|
+
{ name: "git-pull", description: "Pull with fast-forward only", category: "Git", argsMode: "none" },
|
|
859
|
+
{ name: "git-push", description: "Push the current branch", category: "Git", argsMode: "none" },
|
|
860
|
+
{ name: "git-stash", description: "Stash current working tree changes", category: "Git", argsMode: "optional" },
|
|
861
|
+
{ name: "git-stash-pop", description: "Pop the latest stash", category: "Git", argsMode: "none" },
|
|
862
|
+
];
|
|
771
863
|
const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
772
864
|
"add-dir",
|
|
773
865
|
"agents",
|
|
@@ -1040,17 +1132,30 @@ function customClaudeCommands(cwd: string): AgentCommandDescriptor[] {
|
|
|
1040
1132
|
|
|
1041
1133
|
function defaultProviderCommands(provider: AgentProvider, cwd: string, enabled: boolean): AgentCommandDescriptor[] {
|
|
1042
1134
|
const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
|
|
1135
|
+
const linkshellCommands = LINKSHELL_NATIVE_COMMANDS
|
|
1136
|
+
.filter((command) => !command.providers || command.providers.includes(provider))
|
|
1137
|
+
.map((command) => makeCommand({
|
|
1138
|
+
provider,
|
|
1139
|
+
name: command.name,
|
|
1140
|
+
description: command.description,
|
|
1141
|
+
source: "linkshell",
|
|
1142
|
+
category: command.category,
|
|
1143
|
+
argsMode: command.argsMode ?? "optional",
|
|
1144
|
+
destructive: command.destructive,
|
|
1145
|
+
disabledReason,
|
|
1146
|
+
executionKind: "native",
|
|
1147
|
+
}));
|
|
1043
1148
|
if (provider === "codex") {
|
|
1044
|
-
return
|
|
1149
|
+
return linkshellCommands;
|
|
1045
1150
|
}
|
|
1046
1151
|
if (provider === "claude") {
|
|
1047
1152
|
const custom = customClaudeCommands(cwd).map((command) => ({
|
|
1048
1153
|
...command,
|
|
1049
1154
|
disabledReason: command.disabledReason ?? disabledReason,
|
|
1050
1155
|
}));
|
|
1051
|
-
return custom;
|
|
1156
|
+
return [...linkshellCommands, ...custom];
|
|
1052
1157
|
}
|
|
1053
|
-
return
|
|
1158
|
+
return linkshellCommands;
|
|
1054
1159
|
}
|
|
1055
1160
|
|
|
1056
1161
|
function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>): AgentCommandDescriptor[] {
|
|
@@ -1069,6 +1174,81 @@ function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>):
|
|
|
1069
1174
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1070
1175
|
}
|
|
1071
1176
|
|
|
1177
|
+
function isGitNativeCommand(name: string): boolean {
|
|
1178
|
+
return name === "git-status" ||
|
|
1179
|
+
name === "git-diff" ||
|
|
1180
|
+
name === "git-commit" ||
|
|
1181
|
+
name === "git-pull" ||
|
|
1182
|
+
name === "git-push" ||
|
|
1183
|
+
name === "git-stash" ||
|
|
1184
|
+
name === "git-stash-pop";
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function gitCommandArgs(name: string, args?: string): { display: string; argv: string[] } {
|
|
1188
|
+
const message = args?.trim();
|
|
1189
|
+
switch (name) {
|
|
1190
|
+
case "git-status":
|
|
1191
|
+
return { display: "git status --short --branch", argv: ["status", "--short", "--branch"] };
|
|
1192
|
+
case "git-diff":
|
|
1193
|
+
return { display: "git diff --stat", argv: ["diff", "--stat"] };
|
|
1194
|
+
case "git-commit":
|
|
1195
|
+
if (!message) throw new Error("请先输入提交信息,例如 /git-commit fix mobile agent timeline");
|
|
1196
|
+
return { display: `git commit -m ${JSON.stringify(message)}`, argv: ["commit", "-m", message] };
|
|
1197
|
+
case "git-pull":
|
|
1198
|
+
return { display: "git pull --ff-only", argv: ["pull", "--ff-only"] };
|
|
1199
|
+
case "git-push":
|
|
1200
|
+
return { display: "git push", argv: ["push"] };
|
|
1201
|
+
case "git-stash":
|
|
1202
|
+
return {
|
|
1203
|
+
display: "git stash push -u",
|
|
1204
|
+
argv: ["stash", "push", "-u", "-m", message || "LinkShell mobile stash"],
|
|
1205
|
+
};
|
|
1206
|
+
case "git-stash-pop":
|
|
1207
|
+
return { display: "git stash pop", argv: ["stash", "pop"] };
|
|
1208
|
+
default:
|
|
1209
|
+
throw new Error(`未知 Git 命令:/${name}`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function runProcess(
|
|
1214
|
+
command: string,
|
|
1215
|
+
args: string[],
|
|
1216
|
+
options: { cwd: string; maxBytes?: number },
|
|
1217
|
+
): Promise<{ output: string; exitCode: number | null; signal: NodeJS.Signals | null }> {
|
|
1218
|
+
return new Promise((resolve, reject) => {
|
|
1219
|
+
const child = spawn(command, args, {
|
|
1220
|
+
cwd: options.cwd,
|
|
1221
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1222
|
+
windowsHide: true,
|
|
1223
|
+
});
|
|
1224
|
+
const maxBytes = options.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES;
|
|
1225
|
+
let output = "";
|
|
1226
|
+
let bytes = 0;
|
|
1227
|
+
let truncated = false;
|
|
1228
|
+
const append = (chunk: Buffer) => {
|
|
1229
|
+
if (bytes >= maxBytes) {
|
|
1230
|
+
truncated = true;
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const remaining = maxBytes - bytes;
|
|
1234
|
+
const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
1235
|
+
output += slice.toString("utf8");
|
|
1236
|
+
bytes += slice.byteLength;
|
|
1237
|
+
if (slice.byteLength < chunk.byteLength) truncated = true;
|
|
1238
|
+
};
|
|
1239
|
+
child.stdout.on("data", append);
|
|
1240
|
+
child.stderr.on("data", append);
|
|
1241
|
+
child.once("error", reject);
|
|
1242
|
+
child.once("close", (exitCode, signal) => {
|
|
1243
|
+
resolve({
|
|
1244
|
+
output: `${output.trimEnd()}${truncated ? "\n\n[truncated by LinkShell]" : ""}`,
|
|
1245
|
+
exitCode,
|
|
1246
|
+
signal,
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1072
1252
|
function runtimeCommands(provider: AgentProvider, value: unknown): AgentCommandDescriptor[] {
|
|
1073
1253
|
const raw = asRecord(value);
|
|
1074
1254
|
const commandsValue =
|
|
@@ -1583,7 +1763,7 @@ export class AgentWorkspaceProxy {
|
|
|
1583
1763
|
hostDeviceId: this.input.hostDeviceId,
|
|
1584
1764
|
payload: {
|
|
1585
1765
|
conversation: existingConversation,
|
|
1586
|
-
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1766
|
+
snapshot: snapshotTimelineItems(this.timelines.get(existingConversation.id) ?? []),
|
|
1587
1767
|
},
|
|
1588
1768
|
}));
|
|
1589
1769
|
return existingConversation;
|
|
@@ -1639,7 +1819,7 @@ export class AgentWorkspaceProxy {
|
|
|
1639
1819
|
this.input.send(createEnvelope({
|
|
1640
1820
|
type: "agent.v2.conversation.opened",
|
|
1641
1821
|
hostDeviceId: this.input.hostDeviceId,
|
|
1642
|
-
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1822
|
+
payload: { conversation, snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []) },
|
|
1643
1823
|
}));
|
|
1644
1824
|
return conversation;
|
|
1645
1825
|
} catch (error) {
|
|
@@ -1691,7 +1871,7 @@ export class AgentWorkspaceProxy {
|
|
|
1691
1871
|
this.input.send(createEnvelope({
|
|
1692
1872
|
type: "agent.v2.conversation.opened",
|
|
1693
1873
|
hostDeviceId: this.input.hostDeviceId,
|
|
1694
|
-
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1874
|
+
payload: { conversation, snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []) },
|
|
1695
1875
|
}));
|
|
1696
1876
|
return conversation;
|
|
1697
1877
|
}
|
|
@@ -1918,14 +2098,8 @@ export class AgentWorkspaceProxy {
|
|
|
1918
2098
|
return;
|
|
1919
2099
|
}
|
|
1920
2100
|
|
|
1921
|
-
if (
|
|
1922
|
-
this.
|
|
1923
|
-
id: id("error"),
|
|
1924
|
-
conversationId: conversation.id,
|
|
1925
|
-
type: "error",
|
|
1926
|
-
error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
|
|
1927
|
-
createdAt: now,
|
|
1928
|
-
});
|
|
2101
|
+
if (isGitNativeCommand(command.name)) {
|
|
2102
|
+
await this.executeGitNativeCommand(conversation, command.name, args);
|
|
1929
2103
|
return;
|
|
1930
2104
|
}
|
|
1931
2105
|
|
|
@@ -1942,6 +2116,22 @@ export class AgentWorkspaceProxy {
|
|
|
1942
2116
|
return;
|
|
1943
2117
|
}
|
|
1944
2118
|
|
|
2119
|
+
if (command.name === "review" || command.name === "subagents") {
|
|
2120
|
+
const prompt = command.name === "review"
|
|
2121
|
+
? args || "Review the current local changes."
|
|
2122
|
+
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
2123
|
+
await this.sendPrompt({
|
|
2124
|
+
conversationId: conversation.id,
|
|
2125
|
+
clientMessageId: id(command.name),
|
|
2126
|
+
contentBlocks: [{ type: "text", text: prompt }],
|
|
2127
|
+
model: conversation.model,
|
|
2128
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
2129
|
+
permissionMode: conversation.permissionMode,
|
|
2130
|
+
collaborationMode: conversation.collaborationMode,
|
|
2131
|
+
});
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1945
2135
|
if (command.name === "compact") {
|
|
1946
2136
|
if (!(client instanceof AcpClient)) throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
1947
2137
|
conversation.status = "running";
|
|
@@ -1979,22 +2169,6 @@ export class AgentWorkspaceProxy {
|
|
|
1979
2169
|
return;
|
|
1980
2170
|
}
|
|
1981
2171
|
|
|
1982
|
-
if (command.name === "review" || command.name === "subagents") {
|
|
1983
|
-
const prompt = command.name === "review"
|
|
1984
|
-
? args || "Review the current local changes."
|
|
1985
|
-
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1986
|
-
await this.sendPrompt({
|
|
1987
|
-
conversationId: conversation.id,
|
|
1988
|
-
clientMessageId: id(command.name),
|
|
1989
|
-
contentBlocks: [{ type: "text", text: prompt }],
|
|
1990
|
-
model: conversation.model,
|
|
1991
|
-
reasoningEffort: conversation.reasoningEffort,
|
|
1992
|
-
permissionMode: conversation.permissionMode,
|
|
1993
|
-
collaborationMode: conversation.collaborationMode,
|
|
1994
|
-
});
|
|
1995
|
-
return;
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
2172
|
throw new Error(`命令暂未实现:/${command.name}`);
|
|
1999
2173
|
} catch (error) {
|
|
2000
2174
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2009,6 +2183,46 @@ export class AgentWorkspaceProxy {
|
|
|
2009
2183
|
}
|
|
2010
2184
|
}
|
|
2011
2185
|
|
|
2186
|
+
private async executeGitNativeCommand(
|
|
2187
|
+
conversation: AgentConversation,
|
|
2188
|
+
commandName: string,
|
|
2189
|
+
args?: string,
|
|
2190
|
+
): Promise<void> {
|
|
2191
|
+
const git = gitCommandArgs(commandName, args);
|
|
2192
|
+
const toolId = id(commandName);
|
|
2193
|
+
const now = Date.now();
|
|
2194
|
+
conversation.status = "running";
|
|
2195
|
+
conversation.lastMessagePreview = git.display;
|
|
2196
|
+
conversation.lastActivityAt = now;
|
|
2197
|
+
this.emitConversation(conversation);
|
|
2198
|
+
this.upsertTool(conversation.id, {
|
|
2199
|
+
id: toolId,
|
|
2200
|
+
name: "命令",
|
|
2201
|
+
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
2202
|
+
createdAt: now,
|
|
2203
|
+
status: "running",
|
|
2204
|
+
});
|
|
2205
|
+
const result = await runProcess("git", git.argv, {
|
|
2206
|
+
cwd: conversation.cwd,
|
|
2207
|
+
maxBytes: COMMAND_OUTPUT_MAX_BYTES,
|
|
2208
|
+
});
|
|
2209
|
+
const ok = result.exitCode === 0;
|
|
2210
|
+
this.upsertTool(conversation.id, {
|
|
2211
|
+
id: toolId,
|
|
2212
|
+
name: "命令",
|
|
2213
|
+
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
2214
|
+
output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
|
|
2215
|
+
createdAt: now,
|
|
2216
|
+
status: ok ? "completed" : "failed",
|
|
2217
|
+
});
|
|
2218
|
+
conversation.status = ok ? "idle" : "error";
|
|
2219
|
+
conversation.lastMessagePreview = ok
|
|
2220
|
+
? `${git.display} 完成`
|
|
2221
|
+
: `${git.display} 失败`;
|
|
2222
|
+
conversation.lastActivityAt = Date.now();
|
|
2223
|
+
this.emitConversation(conversation);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2012
2226
|
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
2013
2227
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2014
2228
|
return this.handleStructuredInput(params, true);
|
|
@@ -2935,7 +3149,7 @@ export class AgentWorkspaceProxy {
|
|
|
2935
3149
|
this.input.send(createEnvelope({
|
|
2936
3150
|
type: "agent.v2.event",
|
|
2937
3151
|
hostDeviceId: this.input.hostDeviceId,
|
|
2938
|
-
payload: { conversationId, conversation, item },
|
|
3152
|
+
payload: { conversationId, conversation, item: snapshotTimelineItem(item, { stripImages: false }) },
|
|
2939
3153
|
}));
|
|
2940
3154
|
}
|
|
2941
3155
|
|
|
@@ -2994,8 +3208,8 @@ export class AgentWorkspaceProxy {
|
|
|
2994
3208
|
}
|
|
2995
3209
|
const conversations = [...this.conversations.values()];
|
|
2996
3210
|
const items = conversationId
|
|
2997
|
-
? this.timelines.get(conversationId) ?? []
|
|
2998
|
-
: [
|
|
3211
|
+
? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
|
|
3212
|
+
: [];
|
|
2999
3213
|
this.input.send(createEnvelope({
|
|
3000
3214
|
type: "agent.v2.snapshot",
|
|
3001
3215
|
hostDeviceId: this.input.hostDeviceId,
|
|
@@ -62,6 +62,8 @@ interface CodexIndexEntry {
|
|
|
62
62
|
updatedAt?: number;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
type StoredToolStatus = NonNullable<StoredAgentTimelineItem["toolCall"]>["status"];
|
|
66
|
+
|
|
65
67
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
66
68
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
67
69
|
? value as Record<string, unknown>
|
|
@@ -366,7 +368,8 @@ function historyToolName(name: string | undefined): string {
|
|
|
366
368
|
return name;
|
|
367
369
|
}
|
|
368
370
|
|
|
369
|
-
function historyToolKind(name: string | undefined): "tool_activity" | "command_execution" {
|
|
371
|
+
function historyToolKind(name: string | undefined): "tool_activity" | "command_execution" | "file_change" {
|
|
372
|
+
if (name === "apply_patch") return "file_change";
|
|
370
373
|
return name?.endsWith("exec_command") || name?.endsWith("write_stdin") ? "command_execution" : "tool_activity";
|
|
371
374
|
}
|
|
372
375
|
|
|
@@ -390,6 +393,75 @@ function commandFromCodexTool(name: string | undefined, rawInput: unknown, outpu
|
|
|
390
393
|
return { command, cwd, output, status: output === undefined ? "running" : "completed" };
|
|
391
394
|
}
|
|
392
395
|
|
|
396
|
+
function patchTextFromCodexTool(name: string | undefined, rawInput: unknown, input: string | undefined): string | undefined {
|
|
397
|
+
if (name !== "apply_patch") return undefined;
|
|
398
|
+
if (typeof rawInput === "string") return rawInput;
|
|
399
|
+
const record = asRecord(rawInput);
|
|
400
|
+
for (const key of ["patch", "input", "text", "content"]) {
|
|
401
|
+
const value = record?.[key];
|
|
402
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
403
|
+
}
|
|
404
|
+
return input;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function fileChangeFromApplyPatch(
|
|
408
|
+
patchText: string | undefined,
|
|
409
|
+
status: StoredToolStatus,
|
|
410
|
+
): StoredAgentTimelineItem["fileChange"] | undefined {
|
|
411
|
+
if (!patchText?.trim()) return undefined;
|
|
412
|
+
const entries: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"] = [];
|
|
413
|
+
let current: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] | undefined;
|
|
414
|
+
|
|
415
|
+
const flush = () => {
|
|
416
|
+
if (!current?.path) return;
|
|
417
|
+
const existing = entries.find((entry) => entry.path === current!.path);
|
|
418
|
+
if (existing) {
|
|
419
|
+
existing.added = (existing.added ?? 0) + (current.added ?? 0);
|
|
420
|
+
existing.removed = (existing.removed ?? 0) + (current.removed ?? 0);
|
|
421
|
+
existing.kind ??= current.kind;
|
|
422
|
+
} else {
|
|
423
|
+
entries.push(current);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
for (const rawLine of patchText.split(/\r?\n/)) {
|
|
428
|
+
const add = rawLine.match(/^\*\*\* Add File:\s+(.+)$/);
|
|
429
|
+
const update = rawLine.match(/^\*\*\* Update File:\s+(.+)$/);
|
|
430
|
+
const del = rawLine.match(/^\*\*\* Delete File:\s+(.+)$/);
|
|
431
|
+
const move = rawLine.match(/^\*\*\* Move to:\s+(.+)$/);
|
|
432
|
+
if (add || update || del) {
|
|
433
|
+
flush();
|
|
434
|
+
current = {
|
|
435
|
+
path: (add?.[1] ?? update?.[1] ?? del?.[1] ?? "").trim(),
|
|
436
|
+
kind: add ? "create" : del ? "delete" : "update",
|
|
437
|
+
added: 0,
|
|
438
|
+
removed: 0,
|
|
439
|
+
};
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (move?.[1] && current) {
|
|
443
|
+
current.path = move[1].trim();
|
|
444
|
+
current.kind = "move";
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (!current) continue;
|
|
448
|
+
if (rawLine.startsWith("+") && !rawLine.startsWith("+++")) {
|
|
449
|
+
current.added = (current.added ?? 0) + 1;
|
|
450
|
+
} else if (rawLine.startsWith("-") && !rawLine.startsWith("---")) {
|
|
451
|
+
current.removed = (current.removed ?? 0) + 1;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
flush();
|
|
455
|
+
|
|
456
|
+
if (entries.length === 0) return undefined;
|
|
457
|
+
return {
|
|
458
|
+
entries,
|
|
459
|
+
diff: patchText,
|
|
460
|
+
summary: entries.map((entry) => [entry.kind, entry.path].filter(Boolean).join(" ")).join("\n"),
|
|
461
|
+
status,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
393
465
|
function upsertHistoryTool(
|
|
394
466
|
itemsById: Map<string, StoredAgentTimelineItem>,
|
|
395
467
|
conversationId: string,
|
|
@@ -402,21 +474,27 @@ function upsertHistoryTool(
|
|
|
402
474
|
const id = `history-tool:${callId}`;
|
|
403
475
|
const existing = itemsById.get(id);
|
|
404
476
|
const commandExecution = commandFromCodexTool(name, rawInput, existing?.commandExecution?.output);
|
|
477
|
+
const status: StoredToolStatus = existing?.toolCall?.status ?? "running";
|
|
478
|
+
const fileChange = fileChangeFromApplyPatch(patchTextFromCodexTool(name, rawInput, input), status);
|
|
405
479
|
itemsById.set(id, {
|
|
406
480
|
id,
|
|
407
481
|
conversationId,
|
|
408
482
|
type: "tool_call",
|
|
409
|
-
kind: historyToolKind(name),
|
|
483
|
+
kind: fileChange ? "file_change" : historyToolKind(name),
|
|
410
484
|
itemId: callId,
|
|
411
485
|
toolCall: {
|
|
412
486
|
id: callId,
|
|
413
|
-
name: historyToolName(name),
|
|
414
|
-
input: input ?? existing?.toolCall?.input,
|
|
487
|
+
name: fileChange ? "文件修改" : historyToolName(name),
|
|
488
|
+
input: fileChange?.summary ?? input ?? existing?.toolCall?.input,
|
|
415
489
|
output: existing?.toolCall?.output,
|
|
416
490
|
createdAt: existing?.toolCall?.createdAt ?? createdAt,
|
|
417
|
-
status
|
|
491
|
+
status,
|
|
418
492
|
},
|
|
419
493
|
commandExecution: commandExecution ?? existing?.commandExecution,
|
|
494
|
+
fileChange: fileChange ?? existing?.fileChange,
|
|
495
|
+
text: fileChange
|
|
496
|
+
? `已编辑 ${fileChange.entries.length} 个文件`
|
|
497
|
+
: existing?.text,
|
|
420
498
|
createdAt: existing?.createdAt ?? createdAt,
|
|
421
499
|
updatedAt: createdAt,
|
|
422
500
|
metadata: { source: "device-history", provider: "codex" },
|
|
@@ -435,6 +513,9 @@ function completeHistoryTool(
|
|
|
435
513
|
const commandExecution = existing?.commandExecution
|
|
436
514
|
? { ...existing.commandExecution, output, status: "completed" as const }
|
|
437
515
|
: undefined;
|
|
516
|
+
const fileChange = existing?.fileChange
|
|
517
|
+
? { ...existing.fileChange, summary: existing.fileChange.summary ?? output, status: "completed" as const }
|
|
518
|
+
: undefined;
|
|
438
519
|
itemsById.set(id, {
|
|
439
520
|
id,
|
|
440
521
|
conversationId,
|
|
@@ -450,6 +531,8 @@ function completeHistoryTool(
|
|
|
450
531
|
status: "completed",
|
|
451
532
|
},
|
|
452
533
|
commandExecution,
|
|
534
|
+
fileChange,
|
|
535
|
+
text: fileChange ? existing?.text ?? `已编辑 ${fileChange.entries.length} 个文件` : existing?.text,
|
|
453
536
|
createdAt: existing?.createdAt ?? createdAt,
|
|
454
537
|
updatedAt: createdAt,
|
|
455
538
|
metadata: { source: "device-history", provider: "codex" },
|