linkshell-cli 0.3.11 → 0.3.12
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.js +64 -6
- 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 +73 -6
- package/src/runtime/acp/codex-sessions.ts +88 -5
|
@@ -207,6 +207,8 @@ interface PendingStructuredInputWaiter {
|
|
|
207
207
|
|
|
208
208
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
209
209
|
const MAX_TIMELINE_ITEMS = 200;
|
|
210
|
+
const MAX_SNAPSHOT_ITEMS = 80;
|
|
211
|
+
const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
|
|
210
212
|
|
|
211
213
|
function id(prefix: string): string {
|
|
212
214
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -221,6 +223,71 @@ function stringify(value: unknown): string {
|
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
function truncateUtf8(value: string | undefined, maxBytes = MAX_SNAPSHOT_TEXT_BYTES): string | undefined {
|
|
227
|
+
if (!value) return value;
|
|
228
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
229
|
+
let end = Math.min(value.length, maxBytes);
|
|
230
|
+
while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) {
|
|
231
|
+
end = Math.floor(end * 0.9);
|
|
232
|
+
}
|
|
233
|
+
return `${value.slice(0, end)}\n\n[truncated by LinkShell: original ${Buffer.byteLength(value, "utf8")} bytes]`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function snapshotContentBlocks(
|
|
237
|
+
blocks: AgentContentBlock[] | undefined,
|
|
238
|
+
options: { stripImages?: boolean } = {},
|
|
239
|
+
): AgentContentBlock[] | undefined {
|
|
240
|
+
if (!blocks) return undefined;
|
|
241
|
+
return blocks.map((block) =>
|
|
242
|
+
block.type === "image" && options.stripImages !== false
|
|
243
|
+
? { ...block, data: undefined, text: block.text || "图片附件" }
|
|
244
|
+
: { ...block, text: truncateUtf8(block.text) },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function snapshotTimelineItem(
|
|
249
|
+
item: AgentTimelineItem,
|
|
250
|
+
options: { stripImages?: boolean } = {},
|
|
251
|
+
): AgentTimelineItem {
|
|
252
|
+
return {
|
|
253
|
+
...item,
|
|
254
|
+
content: snapshotContentBlocks(item.content, options),
|
|
255
|
+
text: truncateUtf8(item.text),
|
|
256
|
+
toolCall: item.toolCall
|
|
257
|
+
? {
|
|
258
|
+
...item.toolCall,
|
|
259
|
+
input: truncateUtf8(item.toolCall.input),
|
|
260
|
+
output: truncateUtf8(item.toolCall.output),
|
|
261
|
+
}
|
|
262
|
+
: undefined,
|
|
263
|
+
commandExecution: item.commandExecution
|
|
264
|
+
? {
|
|
265
|
+
...item.commandExecution,
|
|
266
|
+
command: truncateUtf8(item.commandExecution.command, 16 * 1024),
|
|
267
|
+
output: truncateUtf8(item.commandExecution.output),
|
|
268
|
+
}
|
|
269
|
+
: undefined,
|
|
270
|
+
fileChange: item.fileChange
|
|
271
|
+
? {
|
|
272
|
+
...item.fileChange,
|
|
273
|
+
diff: truncateUtf8(item.fileChange.diff),
|
|
274
|
+
summary: truncateUtf8(item.fileChange.summary),
|
|
275
|
+
}
|
|
276
|
+
: undefined,
|
|
277
|
+
permission: item.permission
|
|
278
|
+
? {
|
|
279
|
+
...item.permission,
|
|
280
|
+
toolInput: truncateUtf8(item.permission.toolInput),
|
|
281
|
+
context: truncateUtf8(item.permission.context),
|
|
282
|
+
}
|
|
283
|
+
: undefined,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function snapshotTimelineItems(items: AgentTimelineItem[]): AgentTimelineItem[] {
|
|
288
|
+
return items.slice(-MAX_SNAPSHOT_ITEMS).map((item) => snapshotTimelineItem(item));
|
|
289
|
+
}
|
|
290
|
+
|
|
224
291
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
225
292
|
return typeof value === "object" && value ? value as Record<string, unknown> : undefined;
|
|
226
293
|
}
|
|
@@ -1583,7 +1650,7 @@ export class AgentWorkspaceProxy {
|
|
|
1583
1650
|
hostDeviceId: this.input.hostDeviceId,
|
|
1584
1651
|
payload: {
|
|
1585
1652
|
conversation: existingConversation,
|
|
1586
|
-
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1653
|
+
snapshot: snapshotTimelineItems(this.timelines.get(existingConversation.id) ?? []),
|
|
1587
1654
|
},
|
|
1588
1655
|
}));
|
|
1589
1656
|
return existingConversation;
|
|
@@ -1639,7 +1706,7 @@ export class AgentWorkspaceProxy {
|
|
|
1639
1706
|
this.input.send(createEnvelope({
|
|
1640
1707
|
type: "agent.v2.conversation.opened",
|
|
1641
1708
|
hostDeviceId: this.input.hostDeviceId,
|
|
1642
|
-
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1709
|
+
payload: { conversation, snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []) },
|
|
1643
1710
|
}));
|
|
1644
1711
|
return conversation;
|
|
1645
1712
|
} catch (error) {
|
|
@@ -1691,7 +1758,7 @@ export class AgentWorkspaceProxy {
|
|
|
1691
1758
|
this.input.send(createEnvelope({
|
|
1692
1759
|
type: "agent.v2.conversation.opened",
|
|
1693
1760
|
hostDeviceId: this.input.hostDeviceId,
|
|
1694
|
-
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1761
|
+
payload: { conversation, snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []) },
|
|
1695
1762
|
}));
|
|
1696
1763
|
return conversation;
|
|
1697
1764
|
}
|
|
@@ -2935,7 +3002,7 @@ export class AgentWorkspaceProxy {
|
|
|
2935
3002
|
this.input.send(createEnvelope({
|
|
2936
3003
|
type: "agent.v2.event",
|
|
2937
3004
|
hostDeviceId: this.input.hostDeviceId,
|
|
2938
|
-
payload: { conversationId, conversation, item },
|
|
3005
|
+
payload: { conversationId, conversation, item: snapshotTimelineItem(item, { stripImages: false }) },
|
|
2939
3006
|
}));
|
|
2940
3007
|
}
|
|
2941
3008
|
|
|
@@ -2994,8 +3061,8 @@ export class AgentWorkspaceProxy {
|
|
|
2994
3061
|
}
|
|
2995
3062
|
const conversations = [...this.conversations.values()];
|
|
2996
3063
|
const items = conversationId
|
|
2997
|
-
? this.timelines.get(conversationId) ?? []
|
|
2998
|
-
: [
|
|
3064
|
+
? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
|
|
3065
|
+
: [];
|
|
2999
3066
|
this.input.send(createEnvelope({
|
|
3000
3067
|
type: "agent.v2.snapshot",
|
|
3001
3068
|
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" },
|