@makerbi/remodex 1.5.4 → 2.0.0

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