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.
@@ -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
- : [...this.timelines.values()].flat();
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: existing?.toolCall?.status ?? "running",
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" },