@makerbi/remodex 1.5.4 → 1.5.8

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.
@@ -2,6 +2,7 @@
2
2
  // Purpose: Reconstructs a small thread/turns/list page from local Codex session JSONL files.
3
3
 
4
4
  const fs = require("fs");
5
+ const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
5
6
 
6
7
  function readThreadTurnsListPageFromSessionJsonl(filePath, {
7
8
  threadId = "",
@@ -31,16 +32,72 @@ function readThreadTurnsListPageFromSessionJsonl(filePath, {
31
32
  };
32
33
  }
33
34
 
35
+ // Extracts thread-level context that app-server history can omit for desktop-origin runs.
36
+ function parseSessionJsonlMetadata(content) {
37
+ let threadId = "";
38
+ let cwd = "";
39
+
40
+ const raw = String(content || "");
41
+ let lineStart = 0;
42
+ while (lineStart < raw.length) {
43
+ let lineEnd = raw.indexOf("\n", lineStart);
44
+ if (lineEnd === -1) {
45
+ lineEnd = raw.length;
46
+ }
47
+ const line = raw.substring(lineStart, lineEnd).trim();
48
+ lineStart = lineEnd + 1;
49
+ if (!line) {
50
+ continue;
51
+ }
52
+
53
+ let entry;
54
+ try {
55
+ entry = JSON.parse(line);
56
+ } catch {
57
+ continue;
58
+ }
59
+
60
+ if (entry?.type !== "session_meta") {
61
+ continue;
62
+ }
63
+
64
+ const payload = objectValue(entry.payload);
65
+ threadId ||= normalizeString(payload?.id)
66
+ || normalizeString(payload?.thread_id)
67
+ || normalizeString(payload?.threadId);
68
+ cwd ||= normalizeString(payload?.cwd)
69
+ || normalizeString(payload?.current_working_directory)
70
+ || normalizeString(payload?.working_directory);
71
+
72
+ if (threadId && cwd) {
73
+ break;
74
+ }
75
+ }
76
+
77
+ return { threadId, cwd };
78
+ }
79
+
34
80
  function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
35
81
  const turns = [];
36
82
  const turnsById = new Map();
37
83
  let activeTurnId = "";
38
84
  let sessionThreadId = normalizeString(threadId);
85
+ let sessionCwd = "";
39
86
  const skippedCallIds = new Set();
87
+ const toolCallsByCallId = new Map();
88
+ const pendingUserMessages = [];
40
89
 
41
- const lines = String(content || "").split(/\r?\n/);
42
- for (let index = 0; index < lines.length; index += 1) {
43
- const line = lines[index].trim();
90
+ const raw = String(content || "");
91
+ let index = -1;
92
+ let lineStart = 0;
93
+ while (lineStart < raw.length) {
94
+ index += 1;
95
+ let lineEnd = raw.indexOf("\n", lineStart);
96
+ if (lineEnd === -1) {
97
+ lineEnd = raw.length;
98
+ }
99
+ const line = raw.substring(lineStart, lineEnd).trim();
100
+ lineStart = lineEnd + 1;
44
101
  if (!line) {
45
102
  continue;
46
103
  }
@@ -57,6 +114,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
57
114
  sessionThreadId ||= normalizeString(payload?.id)
58
115
  || normalizeString(payload?.thread_id)
59
116
  || normalizeString(payload?.threadId);
117
+ sessionCwd ||= normalizeString(payload?.cwd);
60
118
  continue;
61
119
  }
62
120
 
@@ -68,7 +126,8 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
68
126
  || normalizeString(payload?.turnId)
69
127
  || activeTurnId
70
128
  || `turn-line-${index + 1}`;
71
- ensureTurn(turns, turnsById, activeTurnId, sessionThreadId, entry.timestamp);
129
+ const turn = ensureTurn(turns, turnsById, activeTurnId, sessionThreadId, entry.timestamp);
130
+ flushPendingUserMessagesToTurn(turn, pendingUserMessages);
72
131
  continue;
73
132
  }
74
133
 
@@ -81,10 +140,16 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
81
140
  entry.timestamp
82
141
  );
83
142
  turn.status = "completed";
143
+ activeTurnId = "";
84
144
  continue;
85
145
  }
86
146
 
87
- if (eventType === "user_message") {
147
+ if (eventType === "item_completed") {
148
+ const completedItem = objectValue(payload?.item);
149
+ if (!completedItem) {
150
+ continue;
151
+ }
152
+
88
153
  const turn = ensureTurn(
89
154
  turns,
90
155
  turnsById,
@@ -92,12 +157,32 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
92
157
  sessionThreadId,
93
158
  entry.timestamp
94
159
  );
95
- turn.items.push({
96
- id: normalizeString(payload?.id) || `user-message-line-${index + 1}`,
97
- type: "user_message",
98
- role: "user",
99
- text: normalizeString(payload?.message) || normalizeString(payload?.text),
160
+ const item = normalizeResponseItemForHistory(completedItem, index + 1, {
161
+ cwd: sessionCwd,
162
+ toolCallsByCallId,
100
163
  });
164
+ if (item) {
165
+ turn.items.push(item);
166
+ }
167
+ continue;
168
+ }
169
+
170
+ if (eventType === "user_message") {
171
+ const explicitTurnId = normalizeString(payload?.turn_id) || normalizeString(payload?.turnId);
172
+ const item = createUserMessageHistoryItem(payload, index + 1, entry.timestamp);
173
+ if (!explicitTurnId && !activeTurnId) {
174
+ pushPendingUserMessage(pendingUserMessages, item);
175
+ continue;
176
+ }
177
+
178
+ const turn = ensureTurn(
179
+ turns,
180
+ turnsById,
181
+ explicitTurnId || activeTurnId || `turn-line-${index + 1}`,
182
+ sessionThreadId,
183
+ entry.timestamp
184
+ );
185
+ addHistoryItemToTurn(turn, item);
101
186
  continue;
102
187
  }
103
188
 
@@ -111,6 +196,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
111
196
  if (!payload) {
112
197
  continue;
113
198
  }
199
+ rememberToolCallForHistory(payload, toolCallsByCallId);
114
200
  if (shouldSkipResponseItemForHistory(payload, skippedCallIds)) {
115
201
  continue;
116
202
  }
@@ -121,9 +207,22 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
121
207
  sessionThreadId,
122
208
  entry.timestamp
123
209
  );
124
- const item = normalizeResponseItemForHistory(payload, index + 1);
210
+ const item = normalizeResponseItemForHistory(payload, index + 1, {
211
+ cwd: sessionCwd,
212
+ toolCallsByCallId,
213
+ });
125
214
  if (item) {
126
- turn.items.push(item);
215
+ if (shouldSkipDuplicateProposedPlanMessage(turn, item)) {
216
+ continue;
217
+ }
218
+ const itemTimestamp = historyItemTimestamp(item, entry.timestamp);
219
+ if (itemTimestamp && !item.createdAt) {
220
+ item.createdAt = itemTimestamp;
221
+ }
222
+ if (itemTimestamp && !item.timestamp) {
223
+ item.timestamp = itemTimestamp;
224
+ }
225
+ addHistoryItemToTurn(turn, item);
127
226
  }
128
227
  }
129
228
  }
@@ -131,6 +230,159 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
131
230
  return turns.filter((turn) => turn.items.length > 0);
132
231
  }
133
232
 
233
+ function createUserMessageHistoryItem(payload, lineNumber, timestamp) {
234
+ const createdAt = historyItemTimestamp(payload, timestamp);
235
+ return {
236
+ id: normalizeString(payload?.id) || `user-message-line-${lineNumber}`,
237
+ type: "user_message",
238
+ role: "user",
239
+ text: normalizeString(payload?.message) || normalizeString(payload?.text),
240
+ createdAt: createdAt || undefined,
241
+ timestamp: createdAt || undefined,
242
+ };
243
+ }
244
+
245
+ function pushPendingUserMessage(pendingUserMessages, item) {
246
+ if (!item || !historyUserItemText(item)) {
247
+ return;
248
+ }
249
+ if (pendingUserMessages.some((candidate) => areDuplicateUserHistoryItems(candidate, item))) {
250
+ return;
251
+ }
252
+ pendingUserMessages.push(item);
253
+ }
254
+
255
+ function addHistoryItemToTurn(turn, item) {
256
+ if (!turn || !item) {
257
+ return;
258
+ }
259
+
260
+ if (isUserHistoryItem(item)) {
261
+ const duplicateIndex = turn.items.findIndex((candidate) => areDuplicateUserHistoryItems(candidate, item));
262
+ if (duplicateIndex !== -1) {
263
+ turn.items[duplicateIndex] = mergeDuplicateUserHistoryItems(turn.items[duplicateIndex], item);
264
+ return;
265
+ }
266
+ }
267
+
268
+ turn.items.push(item);
269
+ }
270
+
271
+ function mergeDuplicateUserHistoryItems(existing, incoming) {
272
+ const existingHasStructuredContent = hasStructuredUserHistoryContent(existing);
273
+ const incomingHasStructuredContent = hasStructuredUserHistoryContent(incoming);
274
+ const preferStructured = existingHasStructuredContent !== incomingHasStructuredContent;
275
+ const preferIncoming = preferStructured
276
+ ? incomingHasStructuredContent
277
+ : normalizeHistoryToken(incoming?.type) === "usermessage"
278
+ && normalizeHistoryToken(existing?.type) !== "usermessage";
279
+ const base = preferIncoming ? incoming : existing;
280
+ const fallback = preferIncoming ? existing : incoming;
281
+ return {
282
+ ...base,
283
+ content: Array.isArray(base?.content) ? base.content : fallback?.content,
284
+ attachments: Array.isArray(base?.attachments) ? base.attachments : fallback?.attachments,
285
+ createdAt: historyItemTimestamp(base, historyItemTimestamp(fallback)) || undefined,
286
+ timestamp: historyItemTimestamp(base, historyItemTimestamp(fallback)) || undefined,
287
+ };
288
+ }
289
+
290
+ function hasStructuredUserHistoryContent(item) {
291
+ const content = Array.isArray(item?.content) ? item.content : [];
292
+ return content
293
+ .map((entry) => objectValue(entry))
294
+ .filter(Boolean)
295
+ .some((entry) => {
296
+ const type = normalizeHistoryToken(entry.type);
297
+ return type === "skill"
298
+ || type === "mention"
299
+ || type === "image"
300
+ || type === "inputimage";
301
+ });
302
+ }
303
+
304
+ function areDuplicateUserHistoryItems(first, second) {
305
+ if (!isUserHistoryItem(first) || !isUserHistoryItem(second)) {
306
+ return false;
307
+ }
308
+ const firstText = historyUserItemText(first);
309
+ const secondText = historyUserItemText(second);
310
+ if (!firstText || !secondText) {
311
+ return false;
312
+ }
313
+ if (firstText === secondText) {
314
+ return true;
315
+ }
316
+ const firstKey = canonicalUserHistoryTextKey(firstText);
317
+ const secondKey = canonicalUserHistoryTextKey(secondText);
318
+ if (firstKey.hasMentions && firstKey.key === secondKey.key) {
319
+ return true;
320
+ }
321
+ return Boolean(
322
+ firstKey.text
323
+ && firstKey.text === secondKey.text
324
+ && (firstKey.hasMentions || secondKey.hasMentions)
325
+ && sameUserHistoryTimestamp(first, second)
326
+ );
327
+ }
328
+
329
+ function sameUserHistoryTimestamp(first, second) {
330
+ const firstTimestamp = historyItemTimestamp(first);
331
+ const secondTimestamp = historyItemTimestamp(second);
332
+ return Boolean(firstTimestamp && secondTimestamp && firstTimestamp === secondTimestamp);
333
+ }
334
+
335
+ function historyItemTimestamp(item, fallbackTimestamp = "") {
336
+ return firstNonEmptyString([
337
+ normalizeString(item?.createdAt),
338
+ normalizeString(item?.created_at),
339
+ normalizeString(item?.timestamp),
340
+ normalizeString(item?.time),
341
+ normalizeString(fallbackTimestamp),
342
+ ]);
343
+ }
344
+
345
+ function isUserHistoryItem(item) {
346
+ return normalizeHistoryToken(item?.type) === "usermessage"
347
+ || normalizeString(item?.role).toLowerCase() === "user";
348
+ }
349
+
350
+ function historyUserItemText(item) {
351
+ return normalizeString(item?.text)
352
+ || normalizeString(item?.message)
353
+ || responseItemMessageText(item);
354
+ }
355
+
356
+ function shouldSkipDuplicateProposedPlanMessage(turn, item) {
357
+ if (!turn || !item || normalizeHistoryToken(item.type) !== "message") {
358
+ return false;
359
+ }
360
+
361
+ const role = normalizeString(item.role).toLowerCase();
362
+ if (role && role !== "assistant") {
363
+ return false;
364
+ }
365
+
366
+ if (!responseItemMessageText(item).includes("<proposed_plan>")) {
367
+ return false;
368
+ }
369
+
370
+ return turn.items.some((candidate) => (
371
+ normalizeHistoryToken(candidate?.type) === "plan"
372
+ && candidate?.remodexJsonlProgressPlan !== true
373
+ ));
374
+ }
375
+
376
+ function flushPendingUserMessagesToTurn(turn, pendingUserMessages) {
377
+ if (!turn || pendingUserMessages.length === 0) {
378
+ return;
379
+ }
380
+
381
+ for (const item of pendingUserMessages.splice(0)) {
382
+ addHistoryItemToTurn(turn, item);
383
+ }
384
+ }
385
+
134
386
  function ensureTurn(turns, turnsById, turnId, threadId, timestamp) {
135
387
  const normalizedTurnId = normalizeString(turnId) || `turn-${turns.length + 1}`;
136
388
  let turn = turnsById.get(normalizedTurnId);
@@ -151,12 +403,34 @@ function ensureTurn(turns, turnsById, turnId, threadId, timestamp) {
151
403
  return turn;
152
404
  }
153
405
 
154
- function normalizeResponseItemForHistory(payload, lineNumber) {
406
+ function normalizeResponseItemForHistory(payload, lineNumber, { cwd = "", toolCallsByCallId = new Map() } = {}) {
155
407
  const type = normalizeHistoryItemType(payload.type);
156
408
  if (!type) {
157
409
  return null;
158
410
  }
159
411
 
412
+ const progressPlanItem = normalizeProgressPlanItemForHistory(payload);
413
+ if (progressPlanItem) {
414
+ return progressPlanItem;
415
+ }
416
+
417
+ const applyPatchItem = normalizeApplyPatchItemForHistory(payload, lineNumber, { cwd });
418
+ if (applyPatchItem) {
419
+ return applyPatchItem;
420
+ }
421
+
422
+ const toolOutputImageViewItem = normalizeToolOutputImageViewItemForHistory(payload, lineNumber, {
423
+ toolCallsByCallId,
424
+ });
425
+ if (toolOutputImageViewItem) {
426
+ return toolOutputImageViewItem;
427
+ }
428
+
429
+ const readableToolItem = normalizeReadableToolItemForHistory(payload, lineNumber, { cwd });
430
+ if (readableToolItem) {
431
+ return readableToolItem;
432
+ }
433
+
160
434
  const item = {
161
435
  ...payload,
162
436
  id: normalizeString(payload.id)
@@ -173,6 +447,423 @@ function normalizeResponseItemForHistory(payload, lineNumber) {
173
447
  return item;
174
448
  }
175
449
 
450
+ // Converts `view_image` tool output blobs into a lightweight local image reference.
451
+ function normalizeToolOutputImageViewItemForHistory(payload, lineNumber, { toolCallsByCallId = new Map() } = {}) {
452
+ const type = normalizeHistoryItemType(payload.type);
453
+ if (normalizeHistoryToken(type) !== "toolcalloutput") {
454
+ return null;
455
+ }
456
+
457
+ const callId = normalizeString(payload.call_id)
458
+ || normalizeString(payload.callId)
459
+ || normalizeString(payload.id);
460
+ const toolCall = callId ? toolCallsByCallId.get(callId) : null;
461
+ if (!toolCall || normalizeString(toolCall.toolName).toLowerCase() !== "view_image") {
462
+ return null;
463
+ }
464
+
465
+ const imagePath = normalizeString(toolCall.imagePath);
466
+ if (!imagePath || !toolCallOutputContainsInlineImage(payload.output)) {
467
+ return null;
468
+ }
469
+
470
+ return {
471
+ id: `${callId || `tool-output-line-${lineNumber}`}-image-view`,
472
+ type: "imageView",
473
+ status: normalizeString(payload.status) || "completed",
474
+ path: imagePath,
475
+ call_id: callId || undefined,
476
+ tool_name: toolCall.toolName,
477
+ remodexJsonlToolOutputImage: true,
478
+ };
479
+ }
480
+
481
+ // Enriches raw tool-call JSONL records so mobile history can render useful rows.
482
+ function normalizeReadableToolItemForHistory(payload, lineNumber, { cwd = "" } = {}) {
483
+ const type = normalizeHistoryItemType(payload.type);
484
+ const typeToken = normalizeHistoryToken(type);
485
+ if (typeToken !== "toolcall" && typeToken !== "customtoolcall") {
486
+ return null;
487
+ }
488
+
489
+ const toolName = normalizeString(payload.name)
490
+ || normalizeString(payload.tool_name)
491
+ || normalizeString(payload.toolName);
492
+ if (!toolName) {
493
+ return null;
494
+ }
495
+
496
+ const callId = normalizeString(payload.call_id)
497
+ || normalizeString(payload.callId)
498
+ || normalizeString(payload.id);
499
+ const argumentsObject = parseToolArguments(
500
+ payload.arguments !== undefined ? payload.arguments : payload.input
501
+ );
502
+ const id = callId || normalizeString(payload.id) || `tool-call-line-${lineNumber}`;
503
+ const status = normalizeString(payload.status) || "completed";
504
+
505
+ if (isCommandToolName(toolName)) {
506
+ return {
507
+ ...payload,
508
+ id,
509
+ type: "commandExecution",
510
+ status,
511
+ command: resolveToolCommand(toolName, argumentsObject),
512
+ cwd: resolveToolWorkingDirectory(argumentsObject, { cwd }),
513
+ call_id: callId || undefined,
514
+ tool_name: toolName,
515
+ arguments: payload.arguments,
516
+ };
517
+ }
518
+
519
+ const message = readableToolActivityMessage(toolName, argumentsObject, payload);
520
+ if (!message) {
521
+ return null;
522
+ }
523
+
524
+ return {
525
+ ...payload,
526
+ id,
527
+ type: "tool_call",
528
+ status,
529
+ message,
530
+ call_id: callId || undefined,
531
+ tool_name: toolName,
532
+ };
533
+ }
534
+
535
+ function rememberToolCallForHistory(payload, toolCallsByCallId) {
536
+ const typeToken = normalizeHistoryToken(normalizeHistoryItemType(payload?.type));
537
+ if (typeToken !== "toolcall" && typeToken !== "customtoolcall") {
538
+ return;
539
+ }
540
+
541
+ const callId = normalizeString(payload.call_id)
542
+ || normalizeString(payload.callId)
543
+ || normalizeString(payload.id);
544
+ const toolName = normalizeString(payload.name)
545
+ || normalizeString(payload.tool_name)
546
+ || normalizeString(payload.toolName);
547
+ if (!callId || !toolName) {
548
+ return;
549
+ }
550
+
551
+ const argumentsObject = parseToolArguments(
552
+ payload.arguments !== undefined ? payload.arguments : payload.input
553
+ );
554
+ toolCallsByCallId.set(callId, {
555
+ toolName,
556
+ imagePath: resolveToolImagePath(toolName, argumentsObject, payload),
557
+ });
558
+ }
559
+
560
+ function resolveToolImagePath(toolName, argumentsObject, payload) {
561
+ if (normalizeString(toolName).toLowerCase() !== "view_image") {
562
+ return "";
563
+ }
564
+
565
+ return firstNonEmptyString([
566
+ normalizeString(argumentsObject.path),
567
+ normalizeString(argumentsObject.filePath),
568
+ normalizeString(argumentsObject.file_path),
569
+ normalizeString(argumentsObject.localPath),
570
+ normalizeString(argumentsObject.local_path),
571
+ normalizeString(payload.path),
572
+ normalizeString(payload.filePath),
573
+ normalizeString(payload.file_path),
574
+ normalizeString(payload.localPath),
575
+ normalizeString(payload.local_path),
576
+ ]);
577
+ }
578
+
579
+ function toolCallOutputContainsInlineImage(rawOutput) {
580
+ const parsedOutput = typeof rawOutput === "string"
581
+ ? safeParseJSON(rawOutput) || rawOutput
582
+ : rawOutput;
583
+ return containsInlineImageDataURL(parsedOutput);
584
+ }
585
+
586
+ function containsInlineImageDataURL(value) {
587
+ if (typeof value === "string") {
588
+ return value.toLowerCase().startsWith("data:image");
589
+ }
590
+
591
+ if (Array.isArray(value)) {
592
+ return value.some(containsInlineImageDataURL);
593
+ }
594
+
595
+ if (value && typeof value === "object") {
596
+ return Object.values(value).some(containsInlineImageDataURL);
597
+ }
598
+
599
+ return false;
600
+ }
601
+
602
+ function normalizeApplyPatchItemForHistory(payload, lineNumber, { cwd = "" } = {}) {
603
+ const type = normalizeHistoryItemType(payload.type);
604
+ if (normalizeString(payload.name) !== "apply_patch" || normalizeHistoryToken(type) !== "customtoolcall") {
605
+ return null;
606
+ }
607
+
608
+ const callId = normalizeString(payload.call_id)
609
+ || normalizeString(payload.callId)
610
+ || normalizeString(payload.id);
611
+ const item = buildApplyPatchFileChangeItem({
612
+ callId,
613
+ patch: normalizeString(payload.input),
614
+ status: normalizeString(payload.status) || "completed",
615
+ idFallback: callId || `apply-patch-line-${lineNumber}`,
616
+ cwd,
617
+ });
618
+ return item ? { ...payload, ...item } : null;
619
+ }
620
+
621
+ function normalizeProgressPlanItemForHistory(payload) {
622
+ const type = normalizeHistoryItemType(payload.type);
623
+ if (!isInternalProgressPlanCall(payload) || normalizeHistoryToken(type) !== "toolcall") {
624
+ return null;
625
+ }
626
+
627
+ const argumentsObject = parseToolArguments(payload.arguments);
628
+ const explanation = normalizeString(argumentsObject.explanation);
629
+ const plan = normalizeHistoryPlanSteps(argumentsObject.plan);
630
+ if (!explanation && plan.length === 0) {
631
+ return null;
632
+ }
633
+
634
+ return {
635
+ id: normalizeString(payload.call_id)
636
+ || normalizeString(payload.callId)
637
+ || normalizeString(payload.id)
638
+ || undefined,
639
+ type: "plan",
640
+ text: explanation || "Planning...",
641
+ explanation: explanation || undefined,
642
+ plan,
643
+ remodexJsonlProgressPlan: true,
644
+ };
645
+ }
646
+
647
+ function normalizeHistoryPlanSteps(rawPlan) {
648
+ if (!Array.isArray(rawPlan)) {
649
+ return [];
650
+ }
651
+
652
+ return rawPlan.flatMap((rawStep) => {
653
+ const stepObject = objectValue(rawStep);
654
+ const step = normalizeString(stepObject?.step);
655
+ const status = normalizeHistoryPlanStatus(stepObject?.status);
656
+ return step && status ? [{ step, status }] : [];
657
+ });
658
+ }
659
+
660
+ function normalizeHistoryPlanStatus(rawStatus) {
661
+ const normalized = normalizeString(rawStatus);
662
+ switch (normalized) {
663
+ case "pending":
664
+ case "in_progress":
665
+ case "inProgress":
666
+ case "completed":
667
+ return normalized;
668
+ default:
669
+ return "";
670
+ }
671
+ }
672
+
673
+ function parseToolArguments(rawArguments) {
674
+ const parsed = typeof rawArguments === "string"
675
+ ? safeParseJSON(normalizeString(rawArguments))
676
+ : rawArguments;
677
+ return objectValue(parsed) || {};
678
+ }
679
+
680
+ function resolveToolCommand(toolName, argumentsObject) {
681
+ if (!isCommandToolName(toolName)) {
682
+ return toolName;
683
+ }
684
+
685
+ return firstNonEmptyString([
686
+ normalizeString(argumentsObject.cmd),
687
+ normalizeString(argumentsObject.command),
688
+ normalizeString(argumentsObject.raw_command),
689
+ normalizeString(argumentsObject.rawCommand),
690
+ normalizeString(argumentsObject.input),
691
+ ]) || toolName;
692
+ }
693
+
694
+ function resolveToolWorkingDirectory(argumentsObject, { cwd = "" } = {}) {
695
+ return firstNonEmptyString([
696
+ normalizeString(argumentsObject.workdir),
697
+ normalizeString(argumentsObject.cwd),
698
+ normalizeString(argumentsObject.working_directory),
699
+ normalizeString(argumentsObject.workingDirectory),
700
+ normalizeString(cwd),
701
+ ]) || "";
702
+ }
703
+
704
+ function isCommandToolName(toolName) {
705
+ const normalized = normalizeString(toolName).toLowerCase();
706
+ return normalized === "exec_command" || normalized === "shell_command";
707
+ }
708
+
709
+ function readableToolActivityMessage(toolName, argumentsObject, payload) {
710
+ const normalized = normalizeString(toolName).toLowerCase();
711
+ switch (normalized) {
712
+ case "write_stdin":
713
+ return "Write to terminal";
714
+ case "read_thread_terminal":
715
+ return "Read terminal output";
716
+ case "view_image": {
717
+ const imagePath = firstNonEmptyString([
718
+ normalizeString(argumentsObject.path),
719
+ normalizeString(payload.path),
720
+ ]);
721
+ return imagePath ? `Open image ${compactHistoryPath(imagePath)}` : "Open image";
722
+ }
723
+ case "open":
724
+ case "browser.open":
725
+ return readableTargetMessage("Open", argumentsObject, payload);
726
+ case "click":
727
+ case "browser.click":
728
+ return readableTargetMessage("Click", argumentsObject, payload);
729
+ case "find":
730
+ case "browser.find":
731
+ return readableTargetMessage("Find", argumentsObject, payload);
732
+ case "screenshot":
733
+ case "browser.screenshot":
734
+ return "Capture screenshot";
735
+ case "web.run":
736
+ case "search_query":
737
+ case "image_query":
738
+ return readableSearchMessage(argumentsObject, payload);
739
+ case "weather":
740
+ return readableLocationMessage("Check weather", argumentsObject, payload);
741
+ case "finance":
742
+ return readableSymbolMessage("Check market data", argumentsObject, payload);
743
+ case "sports":
744
+ return readableLocationMessage("Check sports", argumentsObject, payload);
745
+ case "automation_update":
746
+ return "Update automation";
747
+ case "update_goal":
748
+ return "Update goal";
749
+ case "create_goal":
750
+ return "Create goal";
751
+ case "get_goal":
752
+ return "Read goal";
753
+ case "request_user_input":
754
+ return "Request input";
755
+ default:
756
+ return `Run ${humanizeToolName(toolName)}`;
757
+ }
758
+ }
759
+
760
+ function readableTargetMessage(verb, argumentsObject, payload) {
761
+ const target = firstNonEmptyString([
762
+ normalizeString(argumentsObject.ref_id),
763
+ normalizeString(argumentsObject.refId),
764
+ normalizeString(argumentsObject.url),
765
+ normalizeString(argumentsObject.pattern),
766
+ normalizeString(argumentsObject.query),
767
+ normalizeString(payload.ref_id),
768
+ normalizeString(payload.url),
769
+ ]);
770
+ return target ? `${verb} ${compactHistoryPath(target)}` : verb;
771
+ }
772
+
773
+ function readableSearchMessage(argumentsObject, payload) {
774
+ const query = firstSearchQuery(argumentsObject) || firstSearchQuery(payload);
775
+ return query ? `Search ${query}` : "Search web";
776
+ }
777
+
778
+ function firstSearchQuery(object) {
779
+ const direct = normalizeString(object.q)
780
+ || normalizeString(object.query)
781
+ || normalizeString(object.search_query);
782
+ if (direct) {
783
+ return direct;
784
+ }
785
+
786
+ let searchArray = null;
787
+ if (Array.isArray(object.search_query)) {
788
+ searchArray = object.search_query;
789
+ } else if (Array.isArray(object.image_query)) {
790
+ searchArray = object.image_query;
791
+ }
792
+ if (!searchArray) {
793
+ return "";
794
+ }
795
+ for (const item of searchArray) {
796
+ const query = normalizeString(objectValue(item)?.q)
797
+ || normalizeString(objectValue(item)?.query);
798
+ if (query) {
799
+ return query;
800
+ }
801
+ }
802
+ return "";
803
+ }
804
+
805
+ function readableLocationMessage(verb, argumentsObject, payload) {
806
+ const target = firstNonEmptyString([
807
+ normalizeString(argumentsObject.location),
808
+ normalizeString(argumentsObject.team),
809
+ normalizeString(argumentsObject.league),
810
+ normalizeString(payload.location),
811
+ ]);
812
+ return target ? `${verb} ${target}` : verb;
813
+ }
814
+
815
+ function readableSymbolMessage(verb, argumentsObject, payload) {
816
+ const target = firstNonEmptyString([
817
+ normalizeString(argumentsObject.ticker),
818
+ normalizeString(argumentsObject.symbol),
819
+ normalizeString(payload.ticker),
820
+ ]);
821
+ return target ? `${verb} ${target}` : verb;
822
+ }
823
+
824
+ function humanizeToolName(toolName) {
825
+ return normalizeString(toolName)
826
+ .replace(/^[^.]+\./, "")
827
+ .replace(/[_-]+/g, " ")
828
+ .replace(/\s+/g, " ")
829
+ .trim() || "tool";
830
+ }
831
+
832
+ function compactHistoryPath(path) {
833
+ const text = normalizeString(path);
834
+ if (!text) {
835
+ return "";
836
+ }
837
+ const normalized = text.replace(/\\/g, "/");
838
+ const parts = normalized.split("/").filter(Boolean);
839
+ if (parts.length <= 2) {
840
+ return text;
841
+ }
842
+ const prefix = normalized.startsWith("/") ? "…/" : "";
843
+ return `${prefix}${parts.slice(-2).join("/")}`;
844
+ }
845
+
846
+ function firstNonEmptyString(values) {
847
+ for (const value of values) {
848
+ const normalized = normalizeString(value);
849
+ if (normalized) {
850
+ return normalized;
851
+ }
852
+ }
853
+ return "";
854
+ }
855
+
856
+ function safeParseJSON(rawValue) {
857
+ if (!rawValue) {
858
+ return null;
859
+ }
860
+ try {
861
+ return JSON.parse(rawValue);
862
+ } catch {
863
+ return null;
864
+ }
865
+ }
866
+
176
867
  // Filters desktop transcript internals that are stored as response items but are not chat history.
177
868
  function shouldSkipResponseItemForHistory(payload, skippedCallIds) {
178
869
  const type = normalizeHistoryItemType(payload.type);
@@ -189,6 +880,13 @@ function shouldSkipResponseItemForHistory(payload, skippedCallIds) {
189
880
  return true;
190
881
  }
191
882
 
883
+ if (type === "tool_call" && isInternalProgressPlanCall(payload)) {
884
+ if (callId) {
885
+ skippedCallIds.add(callId);
886
+ }
887
+ return false;
888
+ }
889
+
192
890
  if (type !== "message") {
193
891
  return false;
194
892
  }
@@ -214,6 +912,10 @@ function isSubagentOrchestrationCall(payload) {
214
912
  || name === "close_agent";
215
913
  }
216
914
 
915
+ function isInternalProgressPlanCall(payload) {
916
+ return normalizeString(payload.name).toLowerCase() === "update_plan";
917
+ }
918
+
217
919
  function isSubagentNotificationMessage(payload) {
218
920
  const text = responseItemMessageText(payload).trimStart();
219
921
  return text.startsWith("<subagent_notification>");
@@ -229,13 +931,84 @@ function responseItemMessageText(payload) {
229
931
  return content
230
932
  .map((item) => objectValue(item))
231
933
  .filter(Boolean)
232
- .map((item) => normalizeString(item.text) || normalizeString(objectValue(item.data)?.text))
934
+ .map((item) => responseItemContentText(item))
233
935
  .filter(Boolean)
234
936
  .join("\n");
235
937
  }
236
938
 
939
+ function responseItemContentText(item) {
940
+ const type = normalizeHistoryToken(item?.type);
941
+ if (type === "skill") {
942
+ const skillName = normalizeString(item.id) || normalizeString(item.name);
943
+ return skillName ? `$${skillName}` : "";
944
+ }
945
+ if (type === "mention") {
946
+ const mentionName = normalizeString(item.name) || normalizeString(item.id);
947
+ return mentionName ? `@${mentionName}` : "";
948
+ }
949
+ return normalizeString(item.text) || normalizeString(objectValue(item.data)?.text);
950
+ }
951
+
952
+ function canonicalUserHistoryTextKey(text) {
953
+ const mentions = { skills: new Set(), plugins: new Set() };
954
+ let body = normalizeString(text).replace(
955
+ /(^|\s)([$/@])([A-Za-z0-9][A-Za-z0-9._-]*)(?=[\s,.;:!?)\]}>]|$)/g,
956
+ (match, prefix, trigger, rawName) => {
957
+ const name = normalizeString(rawName).toLowerCase();
958
+ if (!name) {
959
+ return match;
960
+ }
961
+ if (trigger === "$" || trigger === "/") {
962
+ mentions.skills.add(name);
963
+ } else if (trigger === "@") {
964
+ mentions.plugins.add(name);
965
+ }
966
+ return prefix || "";
967
+ }
968
+ );
969
+
970
+ for (const skill of mentions.skills) {
971
+ body = removeBoundedUserMentionPhrase(body, `$${skill}`);
972
+ body = removeBoundedUserMentionPhrase(body, `/${skill}`);
973
+ body = removeBoundedUserMentionPhrase(body, displayNameForUserMention(skill));
974
+ }
975
+ for (const plugin of mentions.plugins) {
976
+ body = removeBoundedUserMentionPhrase(body, `@${plugin}`);
977
+ }
978
+
979
+ const normalizedBody = body.trim().replace(/\s+/g, " ").toLowerCase();
980
+ const skills = [...mentions.skills].sort();
981
+ const plugins = [...mentions.plugins].sort();
982
+ return {
983
+ hasMentions: skills.length > 0 || plugins.length > 0,
984
+ text: normalizedBody,
985
+ key: `${normalizedBody}|skills:${skills.join(",")}|plugins:${plugins.join(",")}`,
986
+ };
987
+ }
988
+
989
+ function removeBoundedUserMentionPhrase(text, phrase) {
990
+ const normalizedPhrase = normalizeString(phrase);
991
+ if (!normalizedPhrase) {
992
+ return text;
993
+ }
994
+ const pattern = new RegExp(`(^|\\s)${escapeRegExp(normalizedPhrase)}(?=[\\s,.;:!?)\\]}>]|$)`, "gi");
995
+ return text.replace(pattern, (match, prefix) => prefix || "");
996
+ }
997
+
998
+ function displayNameForUserMention(name) {
999
+ return normalizeString(name)
1000
+ .split(/[-_]+/)
1001
+ .filter(Boolean)
1002
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
1003
+ .join(" ");
1004
+ }
1005
+
1006
+ function escapeRegExp(value) {
1007
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1008
+ }
1009
+
237
1010
  function normalizeHistoryItemType(rawType) {
238
- const normalized = normalizeString(rawType).toLowerCase().replace(/[\s_-]+/g, "");
1011
+ const normalized = normalizeHistoryToken(rawType);
239
1012
  if (!normalized) {
240
1013
  return "";
241
1014
  }
@@ -245,9 +1018,16 @@ function normalizeHistoryItemType(rawType) {
245
1018
  if (normalized === "functioncalloutput") {
246
1019
  return "tool_call_output";
247
1020
  }
1021
+ if (normalized === "plan") {
1022
+ return "plan";
1023
+ }
248
1024
  return rawType;
249
1025
  }
250
1026
 
1027
+ function normalizeHistoryToken(rawType) {
1028
+ return normalizeString(rawType).toLowerCase().replace(/[\s_-]+/g, "");
1029
+ }
1030
+
251
1031
  function objectValue(value) {
252
1032
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
253
1033
  }
@@ -257,6 +1037,7 @@ function normalizeString(value) {
257
1037
  }
258
1038
 
259
1039
  module.exports = {
1040
+ parseSessionJsonlMetadata,
260
1041
  parseSessionJsonlTurns,
261
1042
  readThreadTurnsListPageFromSessionJsonl,
262
1043
  };