farming-code 2.2.28 → 2.2.30

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 (83) hide show
  1. package/README.md +27 -90
  2. package/README.zh_cn.md +17 -90
  3. package/THIRD_PARTY_NOTICES.md +25 -6
  4. package/backend/acp/packaged-claude-acp.js +13 -0
  5. package/backend/acp/packaged-codex-acp.js +2 -2
  6. package/backend/acp-runtime.js +38 -17
  7. package/backend/acp-session-state.js +78 -88
  8. package/backend/acp-transcript.js +174 -9
  9. package/backend/agent-manager.js +27 -2
  10. package/backend/agent-session-history.js +45 -12
  11. package/backend/codex-session-history.js +1 -1
  12. package/backend/codex-transcript.js +0 -202
  13. package/backend/config-manager.js +44 -16
  14. package/backend/data/LICENSE.agent-browser +201 -0
  15. package/backend/data/runtime-dependency-manifest.json +199 -0
  16. package/backend/data/runtime-dependency-sources.json +5 -0
  17. package/backend/executable-discovery.js +7 -0
  18. package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
  19. package/backend/farming-app-cli.js +161 -65
  20. package/backend/farming-cli.js +7 -59
  21. package/backend/index-html.js +31 -0
  22. package/backend/main-agent-skills.js +7 -28
  23. package/backend/npm-update-helper.js +43 -6
  24. package/backend/provider-adapters.js +10 -0
  25. package/backend/runtime-dependency-manager.js +759 -0
  26. package/backend/runtime-executable-invocation.js +20 -0
  27. package/backend/server-process-identity.js +47 -0
  28. package/backend/server.js +145 -204
  29. package/backend/slash-command-discovery.js +138 -1
  30. package/backend/storage-layout.js +30 -0
  31. package/backend/update-service.js +60 -914
  32. package/config/farming.deploy.env.example +1 -1
  33. package/config/farming.install.env.example +5 -1
  34. package/dist/acp/LICENSE.claude-agent-acp +191 -0
  35. package/dist/acp/LICENSE.claude-agent-sdk +1 -0
  36. package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
  37. package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +387 -7
  38. package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
  39. package/dist/assets/App-Z1N7T0eM.js +695 -0
  40. package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
  41. package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
  42. package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
  43. package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +12 -0
  44. package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
  45. package/dist/assets/code-dark-kLsQLvpf.css +1 -0
  46. package/dist/assets/{index-BzQO3pUr.js → index-D-zetSol.js} +2 -2
  47. package/dist/assets/main-DUTz-Rd2.css +1 -0
  48. package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
  49. package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
  50. package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
  51. package/dist/index.html +33 -3
  52. package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
  53. package/extensions/browser/backend/browser-mcp-server.js +352 -0
  54. package/extensions/browser/backend/browser-resource-manager.js +921 -114
  55. package/extensions/browser/backend/browser-resource-store.js +47 -6
  56. package/extensions/browser/backend/browser-router.js +15 -2
  57. package/extensions/browser/backend/executable-discovery.js +164 -17
  58. package/extensions/browser/backend/managed-chromium-installer.js +593 -0
  59. package/extensions/browser/bin/farming-browser +384 -30
  60. package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
  61. package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
  62. package/extensions/browser/frontend/browser.css +68 -17
  63. package/extensions/browser/frontend/types.ts +13 -0
  64. package/extensions/browser/frontend/useBrowserResources.ts +18 -3
  65. package/index.html +32 -2
  66. package/package.json +10 -6
  67. package/shared/backend-connection-status.d.ts +12 -0
  68. package/shared/backend-connection-status.js +35 -4
  69. package/backend/agent-activity.js +0 -35
  70. package/backend/agent-memory-report.js +0 -583
  71. package/backend/bundle-update-helper.js +0 -113
  72. package/backend/codex-rollout-follower.js +0 -326
  73. package/dist/assets/App-CPG4WcjO.js +0 -658
  74. package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
  75. package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
  76. package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
  77. package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
  78. package/dist/assets/code-dark-BDjwj7Um.css +0 -1
  79. package/dist/assets/main-BlVUXfxa.css +0 -1
  80. package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
  81. package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
  82. package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
  83. package/extensions/browser/backend/cdp-client.js +0 -167
@@ -105,6 +105,18 @@ function compareAgentSessions(a, b) {
105
105
  return timeDelta || agentSessionIdentity(a).localeCompare(agentSessionIdentity(b));
106
106
  }
107
107
 
108
+ function dedupeAgentSessions(sessions) {
109
+ const sessionsByIdentity = new Map();
110
+ for (const session of sessions) {
111
+ const identity = agentSessionIdentity(session);
112
+ const current = sessionsByIdentity.get(identity);
113
+ if (!current || timestampMs(session?.updatedAt || session?.createdAt) > timestampMs(current?.updatedAt || current?.createdAt)) {
114
+ sessionsByIdentity.set(identity, session);
115
+ }
116
+ }
117
+ return Array.from(sessionsByIdentity.values());
118
+ }
119
+
108
120
  function encodeAgentSessionCursor(session) {
109
121
  if (!session?.provider || !session?.id) return '';
110
122
  return Buffer.from(JSON.stringify({
@@ -140,7 +152,10 @@ function paginateAgentSessions(sessions, options = {}) {
140
152
  if (cursorValue) {
141
153
  const cursor = decodeAgentSessionCursor(cursorValue);
142
154
  if (!cursor) return { sessions: [], nextCursor: '', hasMore: false, invalidCursor: true };
143
- const exactIndex = sessions.findIndex(session => agentSessionIdentity(session) === agentSessionIdentity(cursor));
155
+ const exactIndex = sessions.findIndex(session => (
156
+ agentSessionIdentity(session) === agentSessionIdentity(cursor)
157
+ && String(session.updatedAt || session.createdAt || '') === cursor.updatedAt
158
+ ));
144
159
  if (exactIndex >= 0) {
145
160
  start = exactIndex + 1;
146
161
  } else {
@@ -164,7 +179,7 @@ function searchAgentSessions(sessions, query, options = {}) {
164
179
  ? Math.max(1, Math.min(MAX_AGENT_SESSION_HISTORY_LIMIT, Math.floor(options.limit)))
165
180
  : DEFAULT_LIMIT;
166
181
  if (!normalizedQuery) {
167
- return { sessions: [], total: 0, query: '', scope: 'title-project' };
182
+ return { sessions: [], total: 0, query: '', scope: 'id-title-project' };
168
183
  }
169
184
 
170
185
  const projectNames = options.projectNames && typeof options.projectNames === 'object'
@@ -173,6 +188,7 @@ function searchAgentSessions(sessions, query, options = {}) {
173
188
  const matches = sessions.filter(session => {
174
189
  const projectPaths = [session?.workspace, session?.cwd];
175
190
  return [
191
+ session?.id,
176
192
  session?.title,
177
193
  ...projectPaths,
178
194
  ...projectPaths.map(workspace => projectNames[String(workspace || '')]),
@@ -183,7 +199,7 @@ function searchAgentSessions(sessions, query, options = {}) {
183
199
  sessions: matches.slice(0, limit),
184
200
  total: matches.length,
185
201
  query: normalizedQuery,
186
- scope: 'title-project',
202
+ scope: 'id-title-project',
187
203
  };
188
204
  }
189
205
 
@@ -340,7 +356,13 @@ function scheduleFromClaudeEvent(event, sessionId) {
340
356
  return null;
341
357
  }
342
358
 
343
- async function collectRecentFiles(root, extension, limit, acceptFile = () => true) {
359
+ async function collectRecentFiles(
360
+ root,
361
+ extension,
362
+ limit,
363
+ acceptFile = () => true,
364
+ descendDirectory = () => true
365
+ ) {
344
366
  const directories = [root];
345
367
  let files = [];
346
368
  let visitedDirectories = 0;
@@ -362,7 +384,7 @@ async function collectRecentFiles(root, extension, limit, acceptFile = () => tru
362
384
  for (const entry of entries) {
363
385
  const fullPath = path.join(directory, entry.name);
364
386
  if (entry.isDirectory()) {
365
- directories.push(fullPath);
387
+ if (descendDirectory(fullPath)) directories.push(fullPath);
366
388
  } else if (entry.isFile() && entry.name.endsWith(extension) && acceptFile(fullPath)) {
367
389
  let mtimeMs = 0;
368
390
  try {
@@ -508,7 +530,17 @@ async function listClaudeSessions(options = {}) {
508
530
  ? Math.max(limit, Math.min(MAX_AGENT_SESSION_SCAN_LIMIT, Math.floor(options.scanLimit)))
509
531
  : DEFAULT_SCAN_LIMIT;
510
532
  const promptHistory = await readClaudePromptHistory(claudeHome);
511
- const sessionFiles = await collectRecentFiles(path.join(claudeHome, 'projects'), '.jsonl', scanLimit);
533
+ const projectsRoot = path.join(claudeHome, 'projects');
534
+ // Claude child-agent transcripts live below <project>/<session-id>/subagents
535
+ // and inherit the parent session id. Only direct project session files are
536
+ // resumable roots and should count as History entries.
537
+ const sessionFiles = await collectRecentFiles(
538
+ projectsRoot,
539
+ '.jsonl',
540
+ scanLimit,
541
+ filePath => path.relative(projectsRoot, filePath).split(path.sep).length === 2,
542
+ directoryPath => path.relative(projectsRoot, directoryPath).split(path.sep).length === 1
543
+ );
512
544
 
513
545
  const sessions = [];
514
546
  for (const { filePath, mtimeMs } of sessionFiles) {
@@ -542,7 +574,7 @@ async function listClaudeSessions(options = {}) {
542
574
  }
543
575
  }
544
576
 
545
- return sessions
577
+ return dedupeAgentSessions(sessions)
546
578
  .sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt))
547
579
  .slice(0, limit);
548
580
  }
@@ -674,7 +706,8 @@ async function listQoderSessions(options = {}) {
674
706
  projectsRoot,
675
707
  '.jsonl',
676
708
  scanLimit,
677
- filePath => path.relative(projectsRoot, filePath).split(path.sep).length === 2
709
+ filePath => path.relative(projectsRoot, filePath).split(path.sep).length === 2,
710
+ directoryPath => path.relative(projectsRoot, directoryPath).split(path.sep).length === 1
678
711
  );
679
712
 
680
713
  const sessions = [];
@@ -709,7 +742,7 @@ async function listQoderSessions(options = {}) {
709
742
  }
710
743
  }
711
744
 
712
- return sessions
745
+ return dedupeAgentSessions(sessions)
713
746
  .sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt))
714
747
  .slice(0, limit);
715
748
  }
@@ -755,7 +788,7 @@ async function listOpenCodeSessions(options = {}) {
755
788
  }
756
789
  if (!Array.isArray(rawSessions)) return [];
757
790
 
758
- return rawSessions
791
+ return dedupeAgentSessions(rawSessions
759
792
  .map(raw => {
760
793
  const id = firstTrimmedString(raw?.id);
761
794
  const cwd = normalizePathValue(raw?.directory || '');
@@ -777,7 +810,7 @@ async function listOpenCodeSessions(options = {}) {
777
810
  capabilities: ['resume', 'fork'],
778
811
  };
779
812
  })
780
- .filter(session => session && isVisibleAgentSession(session))
813
+ .filter(session => session && isVisibleAgentSession(session)))
781
814
  .sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt))
782
815
  .slice(0, limit);
783
816
  }
@@ -912,7 +945,7 @@ async function listAgentSessions(options = {}) {
912
945
  })));
913
946
  }
914
947
 
915
- return sessions
948
+ return dedupeAgentSessions(sessions)
916
949
  .sort(compareAgentSessions)
917
950
  .slice(0, limit);
918
951
  }
@@ -6,7 +6,7 @@ const { stripCodexInternalContextBlocks } = require('./codex-transcript-sanitize
6
6
 
7
7
  const DEFAULT_LIMIT = 40;
8
8
  const DEFAULT_SCAN_LIMIT = 400;
9
- const MAX_SESSION_HISTORY_LIMIT = 1000;
9
+ const MAX_SESSION_HISTORY_LIMIT = 5000;
10
10
  const MAX_SESSION_HISTORY_SCAN_LIMIT = 5000;
11
11
  const MAX_SCAN_DIRECTORIES = 2000;
12
12
  const SESSION_INDEX_TAIL_BYTES = 4 * 1024 * 1024;
@@ -1,9 +1,5 @@
1
1
  const fs = require('fs');
2
- const fsp = require('fs/promises');
3
- const os = require('os');
4
2
  const path = require('path');
5
- const { fileURLToPath } = require('url');
6
- const { findCodexRolloutFile } = require('./codex-rollout-follower');
7
3
  const {
8
4
  heartbeatAssistantMessage,
9
5
  heartbeatUserMessage,
@@ -11,10 +7,8 @@ const {
11
7
  stripCodexInternalContextBlocks,
12
8
  } = require('./codex-transcript-sanitizer');
13
9
 
14
- // @deprecated `readCodexTranscript` below is the legacy JSONL file reader.
15
10
  // The generic event-to-turn projection remains for JSON compatibility feeds;
16
11
  // live structured Chat is reduced from ACP session updates instead.
17
- const DEFAULT_MAX_READ_BYTES = 32 * 1024 * 1024;
18
12
  const DEFAULT_MAX_TURNS = 240;
19
13
  const USER_MESSAGE_BEGIN = '## My request for Codex:';
20
14
  const MAX_USER_IMAGES_PER_TURN = 6;
@@ -308,71 +302,6 @@ function stripRenderedComposerAttachmentBlocks(value, renderedKinds) {
308
302
  .trim();
309
303
  }
310
304
 
311
- function codexHistoryImageTargets(value) {
312
- const text = String(value || '');
313
- const targets = [];
314
- let match;
315
- CODEX_HISTORY_IMAGE_LINK_PATTERN.lastIndex = 0;
316
- while ((match = CODEX_HISTORY_IMAGE_LINK_PATTERN.exec(text))) {
317
- const target = String(match[1] || '').trim();
318
- if (!target || targets.includes(target)) continue;
319
- targets.push(target);
320
- if (targets.length >= MAX_USER_IMAGES_PER_TURN) break;
321
- }
322
- return targets;
323
- }
324
-
325
- function localImagePathFromTarget(value) {
326
- const target = String(value || '').trim();
327
- if (!target || target.includes('\0') || /^data:/i.test(target)) return '';
328
- if (/^file:\/\//i.test(target)) {
329
- try {
330
- return fileURLToPath(target);
331
- } catch {
332
- return '';
333
- }
334
- }
335
- return path.isAbsolute(target) ? target : '';
336
- }
337
-
338
- function localImagePathsFromUserContent(content) {
339
- const paths = [];
340
- const append = (value) => {
341
- const filePath = localImagePathFromTarget(value);
342
- if (!filePath || !LOCAL_IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()] || paths.includes(filePath)) return;
343
- paths.push(filePath);
344
- };
345
- for (const part of Array.isArray(content) ? content : []) {
346
- if (!part || typeof part !== 'object') continue;
347
- if (['local_image', 'localImage'].includes(part.type)) append(part.path || part.file || part.url);
348
- const text = typeof part.text === 'string' ? part.text : '';
349
- for (const match of text.matchAll(/<image\b[^>]*\bpath=(['"])(.*?)\1/gi)) append(match[2]);
350
- for (const match of text.matchAll(/^##\s+[^\n:]+:\s*(.+)$/gim)) append(match[1]);
351
- }
352
- return paths.slice(0, MAX_USER_IMAGES_PER_TURN);
353
- }
354
-
355
- function dataImageUrlsFromUserContent(content) {
356
- const urls = [];
357
- for (const part of Array.isArray(content) ? content : []) {
358
- if (!part || typeof part !== 'object' || !['input_image', 'inputImage', 'image'].includes(part.type)) continue;
359
- const value = String(part.image_url || part.imageUrl || part.url || part.data || '');
360
- if (!/^data:image\/(?:gif|jpe?g|png|svg\+xml|webp);base64,/i.test(value)) continue;
361
- if (value.length > MAX_USER_IMAGE_URL_LENGTH || urls.includes(value)) continue;
362
- urls.push(value);
363
- if (urls.length >= MAX_USER_IMAGES_PER_TURN) break;
364
- }
365
- return urls;
366
- }
367
-
368
- function appendHistoryImageDataFromContent(imageDataByPath, content) {
369
- const paths = localImagePathsFromUserContent(content);
370
- const urls = dataImageUrlsFromUserContent(content);
371
- for (let index = 0; index < Math.min(paths.length, urls.length); index += 1) {
372
- if (!imageDataByPath.has(paths[index])) imageDataByPath.set(paths[index], urls[index]);
373
- }
374
- }
375
-
376
305
  function stripRenderedCodexHistoryAttachmentLinks(value, renderedKinds) {
377
306
  const text = normalizeText(value);
378
307
  if (!text || !renderedKinds.has('image')) return text;
@@ -2416,133 +2345,6 @@ function buildTranscriptFromLines(lines, options = {}) {
2416
2345
  .slice(-maxTurns);
2417
2346
  }
2418
2347
 
2419
- async function readTailLines(filePath, maxReadBytes = DEFAULT_MAX_READ_BYTES) {
2420
- const stat = await fsp.stat(filePath);
2421
- const start = Math.max(0, stat.size - maxReadBytes);
2422
- const handle = await fsp.open(filePath, 'r');
2423
- try {
2424
- const buffer = Buffer.alloc(stat.size - start);
2425
- const result = await handle.read(buffer, 0, buffer.length, start);
2426
- let text = buffer.subarray(0, result.bytesRead).toString('utf8');
2427
- if (start > 0) {
2428
- const firstNewline = text.indexOf('\n');
2429
- text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
2430
- }
2431
- return {
2432
- truncated: start > 0,
2433
- lines: text.split('\n'),
2434
- };
2435
- } finally {
2436
- await handle.close().catch(() => {});
2437
- }
2438
- }
2439
-
2440
- function isUserMessageLine(line) {
2441
- if (!line.trim()) return false;
2442
- try {
2443
- const event = JSON.parse(line);
2444
- const { type: eventType, payload } = normalizeEventEnvelope(event);
2445
- if (!payload) return false;
2446
- if (eventType === 'event_msg') {
2447
- if (payload.type !== 'user_message') return false;
2448
- const text = textFromUserMessagePayload(payload);
2449
- const images = imagesFromUserMessagePayload(payload);
2450
- const files = extractComposerFileAttachments(text).files;
2451
- return Boolean(visibleUserMessageText(text, {
2452
- renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images, files }),
2453
- }) || images.length > 0 || files.length > 0);
2454
- }
2455
- if (eventType === 'response_item') {
2456
- if (payload.type !== 'message' || payload.role !== 'user') return false;
2457
- const text = textFromContent(payload.content);
2458
- const images = imagesFromContent(payload.content);
2459
- const files = extractComposerFileAttachments(text).files;
2460
- return Boolean(visibleUserMessageText(text, {
2461
- renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images, files }),
2462
- }) || images.length > 0 || files.length > 0);
2463
- }
2464
- if (eventType === 'item/started' || eventType === 'item/completed' || eventType === 'item.started' || eventType === 'item.completed') {
2465
- if (turnItemType(payload.item) !== 'usermessage') return false;
2466
- const text = textFromUserInput(payload.item?.content);
2467
- const images = imagesFromContent(payload.item?.content);
2468
- const audios = audiosFromContent(payload.item?.content);
2469
- const files = extractComposerFileAttachments(text).files;
2470
- return Boolean(visibleUserMessageText(text, {
2471
- renderedAttachmentKinds: renderedAttachmentKindsForAttachments({ images, audios, files }),
2472
- }) || images.length > 0 || audios.length > 0 || files.length > 0);
2473
- }
2474
- return false;
2475
- } catch {
2476
- return false;
2477
- }
2478
- }
2479
-
2480
- function dropLeadingPartialTurn(lines) {
2481
- const firstUserIndex = lines.findIndex(isUserMessageLine);
2482
- return firstUserIndex > 0 ? lines.slice(firstUserIndex) : lines;
2483
- }
2484
-
2485
- async function readCodexTranscript(sessionId, options = {}) {
2486
- const normalizedSessionId = String(sessionId || '').trim();
2487
- if (!normalizedSessionId) {
2488
- return { available: false, reason: 'missing-session-id', sessionId: '', turns: [] };
2489
- }
2490
-
2491
- const filePath = findCodexRolloutFile(normalizedSessionId, {
2492
- codexHome: options.codexHome || path.join(os.homedir(), '.codex'),
2493
- });
2494
- if (!filePath) {
2495
- return { available: false, reason: 'history-not-found', sessionId: normalizedSessionId, turns: [] };
2496
- }
2497
-
2498
- const tail = await readTailLines(filePath, options.maxReadBytes);
2499
- const lines = tail.truncated ? dropLeadingPartialTurn(tail.lines) : tail.lines;
2500
- const stat = fs.statSync(filePath);
2501
- const maxTurns = Number.isFinite(options.maxTurns) ? Math.max(1, Math.floor(options.maxTurns)) : DEFAULT_MAX_TURNS;
2502
- const allTurns = buildTranscriptFromLines(lines, { maxTurns: Number.MAX_SAFE_INTEGER });
2503
- const turns = allTurns.slice(-maxTurns);
2504
- return {
2505
- available: true,
2506
- sessionId: normalizedSessionId,
2507
- filePath,
2508
- updatedAt: new Date(stat.mtimeMs).toISOString(),
2509
- source: 'codex-rollout-jsonl',
2510
- hasMoreBefore: allTurns.length > maxTurns || tail.truncated,
2511
- turnLimit: maxTurns,
2512
- turns,
2513
- };
2514
- }
2515
-
2516
- async function readCodexHistoryImageData(sessionId, options = {}) {
2517
- const normalizedSessionId = String(sessionId || '').trim();
2518
- if (!normalizedSessionId) return new Map();
2519
- const filePath = findCodexRolloutFile(normalizedSessionId, {
2520
- codexHome: options.codexHome || path.join(os.homedir(), '.codex'),
2521
- });
2522
- if (!filePath) return new Map();
2523
- const tail = await readTailLines(filePath, options.maxReadBytes);
2524
- const imageDataByPath = new Map();
2525
- for (const line of tail.lines) {
2526
- if (!line.trim()) continue;
2527
- try {
2528
- const event = JSON.parse(line);
2529
- const { type: eventType, payload } = normalizeEventEnvelope(event);
2530
- if (!payload) continue;
2531
- if (eventType === 'response_item' && payload.type === 'message' && payload.role === 'user') {
2532
- appendHistoryImageDataFromContent(imageDataByPath, payload.content);
2533
- continue;
2534
- }
2535
- if (['item/started', 'item/completed', 'item.started', 'item.completed'].includes(eventType)) {
2536
- const item = payload.item;
2537
- if (turnItemType(item) === 'usermessage') appendHistoryImageDataFromContent(imageDataByPath, item?.content);
2538
- }
2539
- } catch {
2540
- // Ignore malformed or partial tail records; history replay remains usable without image recovery.
2541
- }
2542
- }
2543
- return imageDataByPath;
2544
- }
2545
-
2546
2348
  function buildTranscriptFromEvents(events, options = {}) {
2547
2349
  const lines = Array.isArray(events)
2548
2350
  ? events.filter(event => event && typeof event === 'object').map(event => JSON.stringify(event))
@@ -2554,10 +2356,6 @@ module.exports = {
2554
2356
  DEFAULT_MAX_TURNS,
2555
2357
  buildTranscriptFromEvents,
2556
2358
  buildTranscriptFromLines,
2557
- codexHistoryImageTargets,
2558
- dropLeadingPartialTurn,
2559
- readCodexHistoryImageData,
2560
- readCodexTranscript,
2561
2359
  stripUserMessagePrefix,
2562
2360
  textFromContent,
2563
2361
  visibleUserMessageText,
@@ -59,9 +59,6 @@ const DEFAULT_AGENT_HOMES = {
59
59
  qoder: [{ id: 'default', path: '~/.qoder' }],
60
60
  };
61
61
 
62
- const DEFAULT_UPDATE_URL = 'https://github.com/zhuwenzhuang/farming/releases/latest';
63
- const LEGACY_DEFAULT_UPDATE_URL = 'https://github.com/zhuwenzhuang/farming/releases/latest/download/manifest.json';
64
- const API_DEFAULT_UPDATE_URL = 'https://api.github.com/repos/zhuwenzhuang/farming/releases/latest';
65
62
  const LEGACY_DEFAULT_WORKSPACE_FILE_SEARCH_TIMEOUT_MS = 3000;
66
63
  const DEFAULT_SEARCH_TIMEOUT_MS = 15000;
67
64
  const MIN_SEARCH_TIMEOUT_MS = 3000;
@@ -85,16 +82,19 @@ const PERSISTED_SETTING_KEYS = new Set([
85
82
  'theme',
86
83
  'appearance',
87
84
  'language',
85
+ 'restReminderIntervalSeconds',
88
86
  'heartbeatInterval',
89
87
  'dangerouslySkipAgentPermissionsByDefault',
90
88
  'browserExtensionEnabled',
89
+ 'browserSource',
90
+ 'browserExecutablePath',
91
+ 'browserExternalCdpUrl',
91
92
  'crtSkinEffectsEnabled',
92
93
  'crtDynamicHeatEnabled',
93
94
  'crtTerminalFontSize',
94
95
  'defaultLaunchAgent',
95
96
  'agentLaunchProfiles',
96
97
  'agentHomes',
97
- 'updateUrl',
98
98
  'searchTimeoutMs',
99
99
  'codexApprovalMode',
100
100
  'codexModel',
@@ -317,9 +317,13 @@ class ConfigManager {
317
317
  theme: 'terminal',
318
318
  appearance: 'system',
319
319
  language: 'en',
320
+ restReminderIntervalSeconds: null,
320
321
  heartbeatInterval: 1000,
321
322
  dangerouslySkipAgentPermissionsByDefault: false,
322
323
  browserExtensionEnabled: false,
324
+ browserSource: process.env.FARMING_BROWSER_CDP_URL ? 'external-cdp' : 'system',
325
+ browserExecutablePath: process.env.FARMING_BROWSER_EXECUTABLE || '',
326
+ browserExternalCdpUrl: process.env.FARMING_BROWSER_CDP_URL || 'http://127.0.0.1:9222',
323
327
  crtSkinEffectsEnabled: true,
324
328
  crtDynamicHeatEnabled: false,
325
329
  crtTerminalFontSize: DEFAULT_CRT_TERMINAL_FONT_SIZE,
@@ -329,7 +333,6 @@ class ConfigManager {
329
333
  claude: cloneLaunchProfile(DEFAULT_CLAUDE_LAUNCH_PROFILE),
330
334
  },
331
335
  agentHomes: cloneAgentHomes(DEFAULT_AGENT_HOMES),
332
- updateUrl: DEFAULT_UPDATE_URL,
333
336
  searchTimeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
334
337
  codexApprovalMode: 'approve',
335
338
  codexModel: 'gpt-5.5',
@@ -355,9 +358,13 @@ class ConfigManager {
355
358
  theme: 'terminal',
356
359
  appearance: 'system',
357
360
  language: 'en',
361
+ restReminderIntervalSeconds: null,
358
362
  heartbeatInterval: 1000,
359
363
  dangerouslySkipAgentPermissionsByDefault: false,
360
364
  browserExtensionEnabled: false,
365
+ browserSource: process.env.FARMING_BROWSER_CDP_URL ? 'external-cdp' : 'system',
366
+ browserExecutablePath: process.env.FARMING_BROWSER_EXECUTABLE || '',
367
+ browserExternalCdpUrl: process.env.FARMING_BROWSER_CDP_URL || 'http://127.0.0.1:9222',
361
368
  crtSkinEffectsEnabled: true,
362
369
  crtDynamicHeatEnabled: false,
363
370
  crtTerminalFontSize: DEFAULT_CRT_TERMINAL_FONT_SIZE,
@@ -367,7 +374,6 @@ class ConfigManager {
367
374
  claude: cloneLaunchProfile(DEFAULT_CLAUDE_LAUNCH_PROFILE),
368
375
  },
369
376
  agentHomes: cloneAgentHomes(DEFAULT_AGENT_HOMES),
370
- updateUrl: DEFAULT_UPDATE_URL,
371
377
  searchTimeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
372
378
  codexApprovalMode: 'approve',
373
379
  codexModel: 'gpt-5.5',
@@ -405,10 +411,7 @@ class ConfigManager {
405
411
  this.settings.projectOperations = this.normalizeProjectOperations(this.settings.projectOperations);
406
412
  this.settings.instanceName = this.normalizeInstanceName(this.settings.instanceName);
407
413
  this.settings.agentHomes = this.normalizeAgentHomes(this.settings.agentHomes);
408
- if (this.settings.updateUrl === LEGACY_DEFAULT_UPDATE_URL || this.settings.updateUrl === API_DEFAULT_UPDATE_URL) {
409
- this.settings.updateUrl = DEFAULT_UPDATE_URL;
410
- }
411
- this.settings.updateUrl = this.normalizeUpdateUrl(this.settings.updateUrl);
414
+ delete this.settings.updateUrl;
412
415
  this.settings.searchTimeoutMs = this.normalizeSearchTimeoutMs(this.settings.searchTimeoutMs);
413
416
  delete this.settings.codexRuntimeMode;
414
417
  const legacyMainPageSessionKeys = this.normalizeMainPageSessionKeys(this.settings.mainPageSessionKeys);
@@ -419,7 +422,14 @@ class ConfigManager {
419
422
  this.runHistoryStore.init({ legacyTaskHistory });
420
423
  this.settings.appearance = this.normalizeAppearance(this.settings.appearance);
421
424
  this.settings.language = this.normalizeLanguage(this.settings.language);
425
+ this.settings.restReminderIntervalSeconds = this.normalizeRestReminderIntervalSeconds(
426
+ this.settings.restReminderIntervalSeconds,
427
+ );
422
428
  this.settings.browserExtensionEnabled = this.settings.browserExtensionEnabled === true;
429
+ this.settings.browserSource = this.normalizeBrowserSource(this.settings.browserSource);
430
+ this.settings.browserExecutablePath = this.normalizeBrowserSetting(this.settings.browserExecutablePath);
431
+ this.settings.browserExternalCdpUrl = this.normalizeBrowserSetting(this.settings.browserExternalCdpUrl)
432
+ || 'http://127.0.0.1:9222';
423
433
  this.settings.crtSkinEffectsEnabled = this.settings.crtSkinEffectsEnabled !== false;
424
434
  this.settings.crtDynamicHeatEnabled = this.settings.crtDynamicHeatEnabled === true;
425
435
  this.settings.crtTerminalFontSize = this.normalizeCrtTerminalFontSize(this.settings.crtTerminalFontSize);
@@ -452,6 +462,16 @@ class ConfigManager {
452
462
  );
453
463
  }
454
464
 
465
+ normalizeRestReminderIntervalSeconds(value) {
466
+ if (value === null || value === undefined || value === '') return null;
467
+ const seconds = Number(value);
468
+ if (!Number.isInteger(seconds)) return null;
469
+ if (seconds === 0 || seconds === 5) return seconds;
470
+ return seconds >= 60 && seconds <= 240 * 60 && seconds % 60 === 0
471
+ ? seconds
472
+ : null;
473
+ }
474
+
455
475
  normalizeCrtTerminalFontSize(value) {
456
476
  const fontSize = Number(value);
457
477
  if (!Number.isFinite(fontSize)) return DEFAULT_CRT_TERMINAL_FONT_SIZE;
@@ -579,10 +599,12 @@ class ConfigManager {
579
599
  return ['en', 'zh'].includes(language) ? language : 'en';
580
600
  }
581
601
 
582
- normalizeUpdateUrl(value) {
583
- const url = String(value || '').trim();
584
- if (!url) return DEFAULT_UPDATE_URL;
585
- return /^https?:\/\//i.test(url) ? url.slice(0, 2000) : DEFAULT_UPDATE_URL;
602
+ normalizeBrowserSource(source) {
603
+ return ['external-cdp', 'managed'].includes(source) ? source : 'system';
604
+ }
605
+
606
+ normalizeBrowserSetting(value) {
607
+ return String(value || '').trim().slice(0, 2000);
586
608
  }
587
609
 
588
610
  normalizeClaudePermissionMode(mode) {
@@ -1057,7 +1079,7 @@ class ConfigManager {
1057
1079
  nextSettings.projectOperations = this.normalizeProjectOperations(nextSettings.projectOperations);
1058
1080
  nextSettings.instanceName = this.normalizeInstanceName(nextSettings.instanceName);
1059
1081
  nextSettings.agentHomes = this.normalizeAgentHomes(nextSettings.agentHomes);
1060
- nextSettings.updateUrl = this.normalizeUpdateUrl(nextSettings.updateUrl);
1082
+ delete nextSettings.updateUrl;
1061
1083
  nextSettings.searchTimeoutMs = this.normalizeSearchTimeoutMs(nextSettings.searchTimeoutMs);
1062
1084
  delete nextSettings.codexRuntimeMode;
1063
1085
  delete nextSettings.mainPageSessionKeys;
@@ -1070,7 +1092,14 @@ class ConfigManager {
1070
1092
  }
1071
1093
  nextSettings.appearance = this.normalizeAppearance(nextSettings.appearance);
1072
1094
  nextSettings.language = this.normalizeLanguage(nextSettings.language);
1095
+ nextSettings.restReminderIntervalSeconds = this.normalizeRestReminderIntervalSeconds(
1096
+ nextSettings.restReminderIntervalSeconds,
1097
+ );
1073
1098
  nextSettings.browserExtensionEnabled = nextSettings.browserExtensionEnabled === true;
1099
+ nextSettings.browserSource = this.normalizeBrowserSource(nextSettings.browserSource);
1100
+ nextSettings.browserExecutablePath = this.normalizeBrowserSetting(nextSettings.browserExecutablePath);
1101
+ nextSettings.browserExternalCdpUrl = this.normalizeBrowserSetting(nextSettings.browserExternalCdpUrl)
1102
+ || 'http://127.0.0.1:9222';
1074
1103
  nextSettings.crtSkinEffectsEnabled = nextSettings.crtSkinEffectsEnabled !== false;
1075
1104
  nextSettings.crtDynamicHeatEnabled = nextSettings.crtDynamicHeatEnabled === true;
1076
1105
  nextSettings.crtTerminalFontSize = this.normalizeCrtTerminalFontSize(nextSettings.crtTerminalFontSize);
@@ -1082,7 +1111,6 @@ class ConfigManager {
1082
1111
  }
1083
1112
 
1084
1113
  module.exports = ConfigManager;
1085
- module.exports.DEFAULT_UPDATE_URL = DEFAULT_UPDATE_URL;
1086
1114
  module.exports.DEFAULT_SEARCH_TIMEOUT_MS = DEFAULT_SEARCH_TIMEOUT_MS;
1087
1115
  module.exports.DEFAULT_CRT_TERMINAL_FONT_SIZE = DEFAULT_CRT_TERMINAL_FONT_SIZE;
1088
1116
  module.exports.MIN_CRT_TERMINAL_FONT_SIZE = MIN_CRT_TERMINAL_FONT_SIZE;