farming-code 2.2.8 → 2.2.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.
Files changed (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1592 -741
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +54 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const fsp = require('fs/promises');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
+ const { fileURLToPath } = require('url');
5
6
  const { findCodexRolloutFile } = require('./codex-rollout-follower');
6
7
  const {
7
8
  heartbeatAssistantMessage,
@@ -18,8 +19,12 @@ const USER_MESSAGE_BEGIN = '## My request for Codex:';
18
19
  const MAX_USER_IMAGES_PER_TURN = 6;
19
20
  const MAX_USER_IMAGE_URL_LENGTH = 5 * 1024 * 1024;
20
21
  const MAX_LOCAL_IMAGE_BYTES = 5 * 1024 * 1024;
22
+ const MAX_USER_AUDIOS_PER_TURN = 6;
23
+ const MAX_USER_AUDIO_URL_LENGTH = 10 * 1024 * 1024;
24
+ const MAX_LOCAL_AUDIO_BYTES = 10 * 1024 * 1024;
21
25
  const MAX_USER_FILES_PER_TURN = 6;
22
26
  const MAX_USER_FILE_CONTENT_CHARS = 50_000;
27
+ const CODEX_HISTORY_IMAGE_LINK_PATTERN = /\[@(?:image|[^\]]+\.(?:gif|jpe?g|png|svg|webp))\]\(([^)\n]+)\)/gi;
23
28
 
24
29
  const LOCAL_IMAGE_MIME_BY_EXT = {
25
30
  '.gif': 'image/gif',
@@ -30,6 +35,18 @@ const LOCAL_IMAGE_MIME_BY_EXT = {
30
35
  '.webp': 'image/webp',
31
36
  };
32
37
 
38
+ const LOCAL_AUDIO_MIME_BY_EXT = {
39
+ '.aac': 'audio/aac',
40
+ '.flac': 'audio/flac',
41
+ '.m4a': 'audio/mp4',
42
+ '.mp3': 'audio/mpeg',
43
+ '.oga': 'audio/ogg',
44
+ '.ogg': 'audio/ogg',
45
+ '.opus': 'audio/ogg',
46
+ '.wav': 'audio/wav',
47
+ '.webm': 'audio/webm',
48
+ };
49
+
33
50
  function normalizeText(value) {
34
51
  return String(value || '')
35
52
  .replace(/\r\n/g, '\n')
@@ -290,6 +307,81 @@ function stripRenderedComposerAttachmentBlocks(value, renderedKinds) {
290
307
  .trim();
291
308
  }
292
309
 
310
+ function codexHistoryImageTargets(value) {
311
+ const text = String(value || '');
312
+ const targets = [];
313
+ let match;
314
+ CODEX_HISTORY_IMAGE_LINK_PATTERN.lastIndex = 0;
315
+ while ((match = CODEX_HISTORY_IMAGE_LINK_PATTERN.exec(text))) {
316
+ const target = String(match[1] || '').trim();
317
+ if (!target || targets.includes(target)) continue;
318
+ targets.push(target);
319
+ if (targets.length >= MAX_USER_IMAGES_PER_TURN) break;
320
+ }
321
+ return targets;
322
+ }
323
+
324
+ function localImagePathFromTarget(value) {
325
+ const target = String(value || '').trim();
326
+ if (!target || target.includes('\0') || /^data:/i.test(target)) return '';
327
+ if (/^file:\/\//i.test(target)) {
328
+ try {
329
+ return fileURLToPath(target);
330
+ } catch {
331
+ return '';
332
+ }
333
+ }
334
+ return path.isAbsolute(target) ? target : '';
335
+ }
336
+
337
+ function localImagePathsFromUserContent(content) {
338
+ const paths = [];
339
+ const append = (value) => {
340
+ const filePath = localImagePathFromTarget(value);
341
+ if (!filePath || !LOCAL_IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()] || paths.includes(filePath)) return;
342
+ paths.push(filePath);
343
+ };
344
+ for (const part of Array.isArray(content) ? content : []) {
345
+ if (!part || typeof part !== 'object') continue;
346
+ if (['local_image', 'localImage'].includes(part.type)) append(part.path || part.file || part.url);
347
+ const text = typeof part.text === 'string' ? part.text : '';
348
+ for (const match of text.matchAll(/<image\b[^>]*\bpath=(['"])(.*?)\1/gi)) append(match[2]);
349
+ for (const match of text.matchAll(/^##\s+[^\n:]+:\s*(.+)$/gim)) append(match[1]);
350
+ }
351
+ return paths.slice(0, MAX_USER_IMAGES_PER_TURN);
352
+ }
353
+
354
+ function dataImageUrlsFromUserContent(content) {
355
+ const urls = [];
356
+ for (const part of Array.isArray(content) ? content : []) {
357
+ if (!part || typeof part !== 'object' || !['input_image', 'inputImage', 'image'].includes(part.type)) continue;
358
+ const value = String(part.image_url || part.imageUrl || part.url || part.data || '');
359
+ if (!/^data:image\/(?:gif|jpe?g|png|svg\+xml|webp);base64,/i.test(value)) continue;
360
+ if (value.length > MAX_USER_IMAGE_URL_LENGTH || urls.includes(value)) continue;
361
+ urls.push(value);
362
+ if (urls.length >= MAX_USER_IMAGES_PER_TURN) break;
363
+ }
364
+ return urls;
365
+ }
366
+
367
+ function appendHistoryImageDataFromContent(imageDataByPath, content) {
368
+ const paths = localImagePathsFromUserContent(content);
369
+ const urls = dataImageUrlsFromUserContent(content);
370
+ for (let index = 0; index < Math.min(paths.length, urls.length); index += 1) {
371
+ if (!imageDataByPath.has(paths[index])) imageDataByPath.set(paths[index], urls[index]);
372
+ }
373
+ }
374
+
375
+ function stripRenderedCodexHistoryAttachmentLinks(value, renderedKinds) {
376
+ const text = normalizeText(value);
377
+ if (!text || !renderedKinds.has('image')) return text;
378
+ CODEX_HISTORY_IMAGE_LINK_PATTERN.lastIndex = 0;
379
+ return text
380
+ .replace(CODEX_HISTORY_IMAGE_LINK_PATTERN, '')
381
+ .replace(/\n{3,}/g, '\n\n')
382
+ .trim();
383
+ }
384
+
293
385
  function visibleUserMessageText(value, options = {}) {
294
386
  const rawText = stripUserMessagePrefix(value);
295
387
  const heartbeatMessage = heartbeatUserMessage(rawText);
@@ -297,19 +389,24 @@ function visibleUserMessageText(value, options = {}) {
297
389
  const text = stripCodexInternalContextBlocks(rawText);
298
390
  if (isInjectedContextMessage(text)) return '';
299
391
  const renderedKinds = renderedAttachmentKindSet(options);
300
- return stripRenderedComposerAttachmentBlocks(stripRenderedAttachmentTagBlocks(text, renderedKinds), renderedKinds);
392
+ return stripRenderedCodexHistoryAttachmentLinks(
393
+ stripRenderedComposerAttachmentBlocks(stripRenderedAttachmentTagBlocks(text, renderedKinds), renderedKinds),
394
+ renderedKinds,
395
+ );
301
396
  }
302
397
 
303
398
  function renderedAttachmentKindsForTurn(turn) {
304
399
  const kinds = [];
305
400
  if (turn && Array.isArray(turn.userImages) && turn.userImages.length > 0) kinds.push('image');
401
+ if (turn && Array.isArray(turn.userAudios) && turn.userAudios.length > 0) kinds.push('audio');
306
402
  if (turn && Array.isArray(turn.userFiles) && turn.userFiles.length > 0) kinds.push('file');
307
403
  return kinds;
308
404
  }
309
405
 
310
- function renderedAttachmentKindsForAttachments({ images = [], files = [] } = {}) {
406
+ function renderedAttachmentKindsForAttachments({ images = [], audios = [], files = [] } = {}) {
311
407
  const kinds = [];
312
408
  if (Array.isArray(images) && images.length > 0) kinds.push('image');
409
+ if (Array.isArray(audios) && audios.length > 0) kinds.push('audio');
313
410
  if (Array.isArray(files) && files.length > 0) kinds.push('file');
314
411
  return kinds;
315
412
  }
@@ -409,6 +506,79 @@ function imagesFromContent(content) {
409
506
  return images;
410
507
  }
411
508
 
509
+ function audioMimeFromUrl(url) {
510
+ const dataMime = String(url || '').match(/^data:(audio\/[^;,]+)[;,]/i)?.[1];
511
+ if (dataMime) return dataMime.toLowerCase();
512
+ try {
513
+ return LOCAL_AUDIO_MIME_BY_EXT[path.extname(new URL(url).pathname).toLowerCase()] || '';
514
+ } catch {
515
+ return '';
516
+ }
517
+ }
518
+
519
+ function audioNameFromPart(part, index) {
520
+ const explicit = compactInline(part?.name || part?.filename || '');
521
+ if (explicit) return explicit;
522
+ const target = String(part?.path || part?.url || '');
523
+ if (target && !/^data:/i.test(target)) {
524
+ try {
525
+ const basename = path.basename(new URL(target, 'file:///').pathname);
526
+ if (basename) return basename;
527
+ } catch {
528
+ const basename = path.basename(target);
529
+ if (basename) return basename;
530
+ }
531
+ }
532
+ return `Audio ${index + 1}`;
533
+ }
534
+
535
+ function audioFromLocalPath(filePath, index) {
536
+ const normalized = String(filePath || '').trim();
537
+ if (!normalized || normalized.includes('\0')) return null;
538
+ const mimeType = LOCAL_AUDIO_MIME_BY_EXT[path.extname(normalized).toLowerCase()];
539
+ if (!mimeType) return null;
540
+ try {
541
+ const stat = fs.statSync(normalized);
542
+ if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_LOCAL_AUDIO_BYTES) return null;
543
+ return {
544
+ id: `local-audio-${index + 1}`,
545
+ url: `data:${mimeType};base64,${fs.readFileSync(normalized).toString('base64')}`,
546
+ mimeType,
547
+ name: path.basename(normalized) || `Audio ${index + 1}`,
548
+ };
549
+ } catch {
550
+ return null;
551
+ }
552
+ }
553
+
554
+ function audiosFromContent(content) {
555
+ if (!Array.isArray(content)) return [];
556
+ const audios = [];
557
+ for (const part of content) {
558
+ if (!part || typeof part !== 'object') continue;
559
+ let audio = null;
560
+ if (part.type === 'localAudio') {
561
+ audio = audioFromLocalPath(part.path, audios.length);
562
+ } else if (part.type === 'audio') {
563
+ const url = String(part.url || '');
564
+ if (
565
+ url.length <= MAX_USER_AUDIO_URL_LENGTH
566
+ && (/^data:audio\//i.test(url) || /^https?:\/\//i.test(url))
567
+ ) {
568
+ audio = {
569
+ id: `audio-${audios.length + 1}`,
570
+ url,
571
+ mimeType: audioMimeFromUrl(url),
572
+ name: audioNameFromPart(part, audios.length),
573
+ };
574
+ }
575
+ }
576
+ if (audio && !audios.some(existing => existing.url === audio.url)) audios.push(audio);
577
+ if (audios.length >= MAX_USER_AUDIOS_PER_TURN) break;
578
+ }
579
+ return audios;
580
+ }
581
+
412
582
  function inlineImageFromEntry(entry, index) {
413
583
  if (typeof entry === 'string') {
414
584
  return isDisplayableImageUrl(entry)
@@ -967,7 +1137,7 @@ function detailForPlanSteps(steps) {
967
1137
  .join('\n');
968
1138
  }
969
1139
 
970
- function applyUserMessageToTurn(turn, { id = '', message = '', images = [] } = {}) {
1140
+ function applyUserMessageToTurn(turn, { id = '', message = '', images = [], audios = [] } = {}) {
971
1141
  if (!turn) return;
972
1142
  const subagentNotification = parseSubagentNotification(message);
973
1143
  if (subagentNotification) {
@@ -981,6 +1151,7 @@ function applyUserMessageToTurn(turn, { id = '', message = '', images = [] } = {
981
1151
  return;
982
1152
  }
983
1153
  const userImages = Array.isArray(images) ? images.filter(Boolean) : [];
1154
+ const userAudios = Array.isArray(audios) ? audios.filter(Boolean) : [];
984
1155
  const pastedTranscript = extractReferencedPastedTranscriptContext(message);
985
1156
  const approvalTranscript = extractCodexApprovalTranscriptContext(pastedTranscript.text);
986
1157
  const userFiles = [
@@ -989,13 +1160,18 @@ function applyUserMessageToTurn(turn, { id = '', message = '', images = [] } = {
989
1160
  ...extractComposerFileAttachments(approvalTranscript.text).files,
990
1161
  ].slice(0, MAX_USER_FILES_PER_TURN);
991
1162
  const text = visibleUserMessageText(approvalTranscript.text, {
992
- renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images: userImages, files: userFiles }),
1163
+ renderedAttachmentKinds: renderedAttachmentKindsForAttachments({
1164
+ images: userImages,
1165
+ audios: userAudios,
1166
+ files: userFiles,
1167
+ }),
993
1168
  });
994
- if (!text && userImages.length <= 0 && userFiles.length <= 0) return;
1169
+ if (!text && userImages.length <= 0 && userAudios.length <= 0 && userFiles.length <= 0) return;
995
1170
 
996
1171
  if (!turn.userMessage) {
997
1172
  if (text) turn.userMessage = text;
998
1173
  if (userImages.length) turn.userImages = userImages;
1174
+ if (userAudios.length) turn.userAudios = userAudios;
999
1175
  if (userFiles.length) turn.userFiles = userFiles;
1000
1176
  return;
1001
1177
  }
@@ -1004,6 +1180,7 @@ function applyUserMessageToTurn(turn, { id = '', message = '', images = [] } = {
1004
1180
  text &&
1005
1181
  turn.userMessage === text &&
1006
1182
  userImages.length === 0 &&
1183
+ userAudios.length === 0 &&
1007
1184
  userFiles.length === 0
1008
1185
  ) {
1009
1186
  return;
@@ -1015,6 +1192,7 @@ function applyUserMessageToTurn(turn, { id = '', message = '', images = [] } = {
1015
1192
  title: text || 'User added context',
1016
1193
  detail: text,
1017
1194
  images: userImages,
1195
+ audios: userAudios,
1018
1196
  files: userFiles,
1019
1197
  status: 'completed',
1020
1198
  });
@@ -1025,10 +1203,12 @@ function appendTurnItem(turn, item, status = '') {
1025
1203
  if (!type) return false;
1026
1204
  if (type === 'usermessage') {
1027
1205
  const images = imagesFromContent(item.content);
1206
+ const audios = audiosFromContent(item.content);
1028
1207
  applyUserMessageToTurn(turn, {
1029
1208
  id: item.id,
1030
1209
  message: textFromUserInput(item.content),
1031
1210
  images,
1211
+ audios,
1032
1212
  });
1033
1213
  return true;
1034
1214
  }
@@ -1383,6 +1563,7 @@ function newTurn(id = '') {
1383
1563
  id: id || `turn-${Date.now()}-${Math.random().toString(36).slice(2)}`,
1384
1564
  userMessage: '',
1385
1565
  userImages: [],
1566
+ userAudios: [],
1386
1567
  userFiles: [],
1387
1568
  finalMessage: '',
1388
1569
  startedAt: null,
@@ -1399,6 +1580,7 @@ function newTurn(id = '') {
1399
1580
  function isEmptyTurn(turn) {
1400
1581
  return !turn.userMessage &&
1401
1582
  (!Array.isArray(turn.userImages) || turn.userImages.length === 0) &&
1583
+ (!Array.isArray(turn.userAudios) || turn.userAudios.length === 0) &&
1402
1584
  (!Array.isArray(turn.userFiles) || turn.userFiles.length === 0) &&
1403
1585
  !turn.finalMessage &&
1404
1586
  turn.processItems.length === 0;
@@ -1444,7 +1626,13 @@ function sanitizeProcessItemForOutput(item) {
1444
1626
  if (!item || typeof item !== 'object') return null;
1445
1627
  const title = stripCodexInternalContextBlocks(item.title);
1446
1628
  const detail = stripCodexInternalContextBlocks(item.detail);
1447
- if (!title && !detail && (!Array.isArray(item.images) || item.images.length <= 0) && (!Array.isArray(item.files) || item.files.length <= 0)) {
1629
+ if (
1630
+ !title
1631
+ && !detail
1632
+ && (!Array.isArray(item.images) || item.images.length <= 0)
1633
+ && (!Array.isArray(item.audios) || item.audios.length <= 0)
1634
+ && (!Array.isArray(item.files) || item.files.length <= 0)
1635
+ ) {
1448
1636
  return null;
1449
1637
  }
1450
1638
  return {
@@ -2276,10 +2464,11 @@ function isUserMessageLine(line) {
2276
2464
  if (turnItemType(payload.item) !== 'usermessage') return false;
2277
2465
  const text = textFromUserInput(payload.item?.content);
2278
2466
  const images = imagesFromContent(payload.item?.content);
2467
+ const audios = audiosFromContent(payload.item?.content);
2279
2468
  const files = extractComposerFileAttachments(text).files;
2280
2469
  return Boolean(visibleUserMessageText(text, {
2281
- renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images, files }),
2282
- }) || images.length > 0 || files.length > 0);
2470
+ renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images, audios, files }),
2471
+ }) || images.length > 0 || audios.length > 0 || files.length > 0);
2283
2472
  }
2284
2473
  return false;
2285
2474
  } catch {
@@ -2323,6 +2512,36 @@ async function readCodexTranscript(sessionId, options = {}) {
2323
2512
  };
2324
2513
  }
2325
2514
 
2515
+ async function readCodexHistoryImageData(sessionId, options = {}) {
2516
+ const normalizedSessionId = String(sessionId || '').trim();
2517
+ if (!normalizedSessionId) return new Map();
2518
+ const filePath = findCodexRolloutFile(normalizedSessionId, {
2519
+ codexHome: options.codexHome || path.join(os.homedir(), '.codex'),
2520
+ });
2521
+ if (!filePath) return new Map();
2522
+ const tail = await readTailLines(filePath, options.maxReadBytes);
2523
+ const imageDataByPath = new Map();
2524
+ for (const line of tail.lines) {
2525
+ if (!line.trim()) continue;
2526
+ try {
2527
+ const event = JSON.parse(line);
2528
+ const { type: eventType, payload } = normalizeEventEnvelope(event);
2529
+ if (!payload) continue;
2530
+ if (eventType === 'response_item' && payload.type === 'message' && payload.role === 'user') {
2531
+ appendHistoryImageDataFromContent(imageDataByPath, payload.content);
2532
+ continue;
2533
+ }
2534
+ if (['item/started', 'item/completed', 'item.started', 'item.completed'].includes(eventType)) {
2535
+ const item = payload.item;
2536
+ if (turnItemType(item) === 'usermessage') appendHistoryImageDataFromContent(imageDataByPath, item?.content);
2537
+ }
2538
+ } catch {
2539
+ // Ignore malformed or partial tail records; history replay remains usable without image recovery.
2540
+ }
2541
+ }
2542
+ return imageDataByPath;
2543
+ }
2544
+
2326
2545
  function buildTranscriptFromEvents(events, options = {}) {
2327
2546
  const lines = Array.isArray(events)
2328
2547
  ? events.filter(event => event && typeof event === 'object').map(event => JSON.stringify(event))
@@ -2334,8 +2553,11 @@ module.exports = {
2334
2553
  DEFAULT_MAX_TURNS,
2335
2554
  buildTranscriptFromEvents,
2336
2555
  buildTranscriptFromLines,
2556
+ codexHistoryImageTargets,
2337
2557
  dropLeadingPartialTurn,
2558
+ readCodexHistoryImageData,
2338
2559
  readCodexTranscript,
2339
2560
  stripUserMessagePrefix,
2340
2561
  textFromContent,
2562
+ visibleUserMessageText,
2341
2563
  };
@@ -72,6 +72,8 @@ const PERSISTED_SETTING_KEYS = new Set([
72
72
  'workspace',
73
73
  'lastMainWorkspace',
74
74
  'workspaceHistory',
75
+ 'projectWorkspaces',
76
+ 'pinnedProjectWorkspaces',
75
77
  'projectNames',
76
78
  'theme',
77
79
  'appearance',
@@ -196,6 +198,31 @@ class ConfigManager {
196
198
  return result.slice(0, 5);
197
199
  }
198
200
 
201
+ normalizeProjectWorkspaces(projects) {
202
+ const entries = Array.isArray(projects) ? projects : [];
203
+ const result = [];
204
+ const seen = new Set();
205
+
206
+ for (const entry of entries) {
207
+ const expanded = this.expandWorkspacePath(entry);
208
+ if (!expanded) continue;
209
+ const resolved = path.resolve(expanded);
210
+ if (resolved === path.parse(resolved).root || this.isInternalWorkspace(resolved)) continue;
211
+ let canonical;
212
+ try {
213
+ canonical = fs.realpathSync(resolved);
214
+ if (!fs.statSync(canonical).isDirectory()) continue;
215
+ } catch {
216
+ continue;
217
+ }
218
+ if (seen.has(canonical)) continue;
219
+ seen.add(canonical);
220
+ result.push(canonical);
221
+ }
222
+
223
+ return result.slice(0, 200);
224
+ }
225
+
199
226
  normalizeMainPageSessionKeys(keys) {
200
227
  const entries = Array.isArray(keys) ? keys : [];
201
228
  const result = [];
@@ -260,6 +287,8 @@ class ConfigManager {
260
287
  workspace: this.farmingDir,
261
288
  lastMainWorkspace: this.farmingDir,
262
289
  workspaceHistory: [],
290
+ projectWorkspaces: [],
291
+ pinnedProjectWorkspaces: [],
263
292
  projectNames: {},
264
293
  theme: 'terminal',
265
294
  appearance: 'light',
@@ -294,6 +323,8 @@ class ConfigManager {
294
323
  workspace: this.farmingDir,
295
324
  lastMainWorkspace: this.farmingDir,
296
325
  workspaceHistory: [],
326
+ projectWorkspaces: [],
327
+ pinnedProjectWorkspaces: [],
297
328
  projectNames: {},
298
329
  theme: 'terminal',
299
330
  appearance: 'light',
@@ -342,6 +373,8 @@ class ConfigManager {
342
373
  this.settings.workspace = this.farmingDir;
343
374
  this.settings.lastMainWorkspace = this.normalizeMainWorkspace(this.settings.lastMainWorkspace, this.farmingDir);
344
375
  this.settings.workspaceHistory = this.normalizeWorkspaceHistory(this.settings.workspaceHistory);
376
+ this.settings.projectWorkspaces = this.normalizeProjectWorkspaces(this.settings.projectWorkspaces);
377
+ this.settings.pinnedProjectWorkspaces = this.normalizeProjectWorkspaces(this.settings.pinnedProjectWorkspaces);
345
378
  this.settings.projectNames = this.normalizeProjectNames(this.settings.projectNames);
346
379
  this.settings.agentHomes = this.normalizeAgentHomes(this.settings.agentHomes);
347
380
  if (this.settings.updateUrl === LEGACY_DEFAULT_UPDATE_URL || this.settings.updateUrl === API_DEFAULT_UPDATE_URL) {
@@ -783,6 +816,8 @@ class ConfigManager {
783
816
  };
784
817
  this.settings.lastMainWorkspace = this.normalizeMainWorkspace(this.settings.lastMainWorkspace, previousMainWorkspace);
785
818
  this.settings.workspaceHistory = this.normalizeWorkspaceHistory(this.settings.workspaceHistory);
819
+ this.settings.projectWorkspaces = this.normalizeProjectWorkspaces(this.settings.projectWorkspaces);
820
+ this.settings.pinnedProjectWorkspaces = this.normalizeProjectWorkspaces(this.settings.pinnedProjectWorkspaces);
786
821
  this.settings.projectNames = this.normalizeProjectNames(this.settings.projectNames);
787
822
  this.settings.agentHomes = this.normalizeAgentHomes(this.settings.agentHomes);
788
823
  this.settings.updateUrl = this.normalizeUpdateUrl(this.settings.updateUrl);