farming-code 2.2.8 → 2.2.11

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 (102) hide show
  1. package/README.md +36 -23
  2. package/README.zh_cn.md +36 -23
  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 +1482 -730
  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 +45 -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 +30 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/farming-net-pass.js +285 -0
  22. package/backend/farming-net-registry.js +112 -0
  23. package/backend/farming-net-server.js +298 -0
  24. package/backend/farming-session-store.js +5 -13
  25. package/backend/git-worktree-info.js +181 -0
  26. package/backend/local-session-engine.js +411 -186
  27. package/backend/main-page-session.js +5 -2
  28. package/backend/native-pty-controller-generation.js +106 -0
  29. package/backend/native-pty-host-client.js +275 -7
  30. package/backend/native-pty-host-identity.js +86 -0
  31. package/backend/native-pty-host.js +813 -114
  32. package/backend/native-session-engine.js +100 -28
  33. package/backend/packaged-node-pty.js +22 -2
  34. package/backend/provider-adapters.js +253 -0
  35. package/backend/provider-session-service.js +241 -0
  36. package/backend/runtime-observation.js +81 -0
  37. package/backend/server.js +294 -86
  38. package/backend/session-engine-bridge.js +21 -2
  39. package/backend/session-engine-router.js +1 -1
  40. package/backend/session-engine.js +1 -1
  41. package/backend/session-stream-protocol.js +185 -0
  42. package/backend/storage-layout.js +55 -0
  43. package/backend/terminal-attach-checkpoint.js +74 -0
  44. package/backend/terminal-exit-quiescence.js +39 -0
  45. package/backend/terminal-reducer-flow-control.js +97 -0
  46. package/backend/terminal-screen-state.js +11 -2
  47. package/backend/terminal-screen-worker-pool.js +59 -6
  48. package/backend/terminal-screen-worker-thread.js +97 -57
  49. package/backend/terminal-screen-worker.js +133 -51
  50. package/backend/terminal-state-serialization.js +127 -0
  51. package/backend/terminal-status.js +23 -4
  52. package/backend/usage-monitor.js +81 -4
  53. package/backend/workspace-directory.js +152 -0
  54. package/backend/workspace-file-router.js +182 -76
  55. package/backend/workspace-file-service.js +319 -4
  56. package/backend/workspace-root-registry.js +164 -0
  57. package/dist/assets/App-CHYpgECZ.js +208 -0
  58. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
  59. package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
  60. package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
  61. package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
  62. package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
  63. package/dist/assets/code-dark-B1pAtO_P.css +1 -0
  64. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  65. package/dist/assets/{index-BrbljRqn.js → index-miO2r9IZ.js} +3 -3
  66. package/dist/assets/main-DNe0jPw-.css +1 -0
  67. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  68. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
  69. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
  70. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  71. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  72. package/dist/index.html +1 -1
  73. package/frontend/farming-net/app.css +625 -0
  74. package/frontend/farming-net/app.js +268 -0
  75. package/frontend/farming-net/index.html +86 -0
  76. package/frontend/reading-anchor.js +198 -0
  77. package/frontend/session-bridge.js +12 -3
  78. package/frontend/session-modal-bridge.js +5 -12
  79. package/frontend/skins/crt/app.js +1976 -793
  80. package/frontend/skins/crt/index.html +313 -23
  81. package/frontend/skins/crt/styles/billing.css +294 -223
  82. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  83. package/frontend/terminal-replay.js +372 -0
  84. package/package.json +10 -3
  85. package/shared/browser-protocol.d.ts +5 -0
  86. package/shared/browser-protocol.js +130 -0
  87. package/dist/assets/App-8dYAM6ql.js +0 -124
  88. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  89. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  90. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  91. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  92. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  93. package/dist/assets/main-D073SnW4.css +0 -1
  94. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  95. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  96. package/dist/assets/qoder-gHCinseV.svg +0 -1
  97. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  98. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  99. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  100. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  101. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  102. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -14,10 +14,15 @@ const DEFAULT_SEARCH_LIMIT = 100;
14
14
  const DEFAULT_GIT_CHANGES_LIMIT = 500;
15
15
  const DEFAULT_GIT_STATUS_CACHE_TTL_MS = 30000;
16
16
  const DEFAULT_GIT_STATUS_INLINE_TIMEOUT_MS = 80;
17
+ const DEFAULT_GIT_STATUS_TIMEOUT_MS = 15000;
17
18
  const DEFAULT_SEARCH_TIMEOUT_MS = 3000;
18
19
  const DEFAULT_BLAME_TIMEOUT_MS = 5000;
19
20
  const DEFAULT_DIFF_TIMEOUT_MS = 5000;
20
21
  const DEFAULT_DIFF_MAX_BUFFER = 1024 * 1024;
22
+ const DEFAULT_GIT_HISTORY_LIMIT = 50;
23
+ const MAX_GIT_HISTORY_LIMIT = 100;
24
+ const DEFAULT_GIT_HISTORY_TIMEOUT_MS = 5000;
25
+ const DEFAULT_GIT_HISTORY_MAX_BUFFER = 8 * 1024 * 1024;
21
26
  const DEFAULT_WATCH_DEPTH = 1;
22
27
  const SEARCH_FILE_LIST_MAX_BUFFER = 16 * 1024 * 1024;
23
28
  const BINARY_SNIFF_BYTES = 8192;
@@ -389,6 +394,7 @@ function sha1(buffer) {
389
394
 
390
395
  function previewForPath(filePath, options = {}) {
391
396
  const extension = path.extname(filePath).toLowerCase();
397
+ if (extension === '.pdf') return { kind: 'pdf', mediaType: 'application/pdf' };
392
398
  if (!options.includeTextImages && TEXT_IMAGE_PREVIEW_EXTENSIONS.has(extension)) return null;
393
399
  const mediaType = IMAGE_PREVIEW_MEDIA_TYPES.get(extension);
394
400
  return mediaType ? { kind: 'image', mediaType } : null;
@@ -509,6 +515,132 @@ function gitStatusReviewRank(kind) {
509
515
  return 6;
510
516
  }
511
517
 
518
+ function normalizeGitHistoryLimit(value) {
519
+ const parsed = Number(value);
520
+ if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_GIT_HISTORY_LIMIT;
521
+ return Math.min(parsed, MAX_GIT_HISTORY_LIMIT);
522
+ }
523
+
524
+ function normalizeGitHistorySkip(value) {
525
+ const parsed = Number(value);
526
+ if (!Number.isInteger(parsed) || parsed < 0) return 0;
527
+ return Math.min(parsed, 1_000_000);
528
+ }
529
+
530
+ function normalizeGitObjectId(value, fieldName = 'commit') {
531
+ const normalized = String(value || '').trim();
532
+ if (!/^[0-9a-f]{40,64}$/i.test(normalized)) {
533
+ throw new WorkspaceFileError(`${fieldName} must be a full git object id`, 400);
534
+ }
535
+ return normalized.toLowerCase();
536
+ }
537
+
538
+ function gitEmptyTreeObjectId(objectFormat) {
539
+ const algorithm = objectFormat === 'sha256' ? 'sha256' : 'sha1';
540
+ return crypto.createHash(algorithm).update(Buffer.from('tree 0\0')).digest('hex');
541
+ }
542
+
543
+ function gitHistoryReference(value) {
544
+ let id = String(value || '').trim();
545
+ if (!id) return null;
546
+ if (id.startsWith('tag: ')) id = id.slice(5);
547
+ if (id === 'HEAD') return { id, name: id, category: 'head' };
548
+ if (id.startsWith('refs/heads/')) {
549
+ return { id, name: id.slice('refs/heads/'.length), category: 'local-branch' };
550
+ }
551
+ if (id.startsWith('refs/remotes/')) {
552
+ if (/\/HEAD$/.test(id)) return null;
553
+ return { id, name: id.slice('refs/remotes/'.length), category: 'remote-branch' };
554
+ }
555
+ if (id.startsWith('refs/tags/')) {
556
+ return { id, name: id.slice('refs/tags/'.length), category: 'tag' };
557
+ }
558
+ return { id, name: id.replace(/^refs\//, ''), category: 'reference' };
559
+ }
560
+
561
+ function parseGitHistoryReferences(value) {
562
+ const references = [];
563
+ const seen = new Set();
564
+ const add = candidate => {
565
+ const reference = gitHistoryReference(candidate);
566
+ if (!reference || seen.has(reference.id)) return;
567
+ seen.add(reference.id);
568
+ references.push(reference);
569
+ };
570
+
571
+ String(value || '').split(', ').forEach(decoration => {
572
+ const arrowIndex = decoration.indexOf(' -> ');
573
+ if (arrowIndex === -1) {
574
+ add(decoration);
575
+ return;
576
+ }
577
+ add(decoration.slice(0, arrowIndex));
578
+ add(decoration.slice(arrowIndex + 4));
579
+ });
580
+ return references;
581
+ }
582
+
583
+ function parseGitHistoryLog(stdout) {
584
+ const fields = String(stdout || '').split('\0');
585
+ const items = [];
586
+ for (let index = 0; index + 7 < fields.length; index += 8) {
587
+ const id = fields[index];
588
+ if (!id) continue;
589
+ const timestamp = Date.parse(fields[index + 4]);
590
+ items.push({
591
+ id,
592
+ displayId: id.slice(0, 8),
593
+ parentIds: fields[index + 1].split(' ').filter(Boolean),
594
+ author: fields[index + 2],
595
+ authorEmail: fields[index + 3],
596
+ timestamp: Number.isFinite(timestamp) ? timestamp : undefined,
597
+ subject: fields[index + 5],
598
+ message: String(fields[index + 6] || '').trimEnd(),
599
+ references: parseGitHistoryReferences(fields[index + 7]),
600
+ });
601
+ }
602
+ return items;
603
+ }
604
+
605
+ function gitHistoryChangeKind(statusCode) {
606
+ if (statusCode.startsWith('A')) return 'added';
607
+ if (statusCode.startsWith('D')) return 'deleted';
608
+ if (statusCode.startsWith('R')) return 'renamed';
609
+ if (statusCode.startsWith('C')) return 'copied';
610
+ if (statusCode.startsWith('T')) return 'type-changed';
611
+ return 'modified';
612
+ }
613
+
614
+ function gitHistoryChangeLabel(kind) {
615
+ if (kind === 'added') return 'A';
616
+ if (kind === 'deleted') return 'D';
617
+ if (kind === 'renamed') return 'R';
618
+ if (kind === 'copied') return 'C';
619
+ if (kind === 'type-changed') return 'T';
620
+ return 'M';
621
+ }
622
+
623
+ function parseGitHistoryChanges(stdout) {
624
+ const fields = String(stdout || '').split('\0');
625
+ const items = [];
626
+ for (let index = 0; index < fields.length;) {
627
+ const statusCode = fields[index++];
628
+ if (!statusCode) continue;
629
+ const kind = gitHistoryChangeKind(statusCode);
630
+ if (kind === 'renamed' || kind === 'copied') {
631
+ const previousPath = fields[index++] || '';
632
+ const filePath = fields[index++] || '';
633
+ if (!filePath) continue;
634
+ items.push({ path: filePath, previousPath, status: kind, statusLabel: gitHistoryChangeLabel(kind) });
635
+ continue;
636
+ }
637
+ const filePath = fields[index++] || '';
638
+ if (!filePath) continue;
639
+ items.push({ path: filePath, status: kind, statusLabel: gitHistoryChangeLabel(kind) });
640
+ }
641
+ return items;
642
+ }
643
+
512
644
  async function workspaceEntryTypeForGitChange(root, filePath) {
513
645
  try {
514
646
  const stat = await fsp.lstat(path.join(root, filePath.replace(/\/+$/, '')));
@@ -978,6 +1110,8 @@ class WorkspaceFileService {
978
1110
  this.blameTimeoutMs = options.blameTimeoutMs ?? DEFAULT_BLAME_TIMEOUT_MS;
979
1111
  this.diffTimeoutMs = options.diffTimeoutMs ?? DEFAULT_DIFF_TIMEOUT_MS;
980
1112
  this.diffMaxBuffer = options.diffMaxBuffer ?? DEFAULT_DIFF_MAX_BUFFER;
1113
+ this.gitHistoryTimeoutMs = options.gitHistoryTimeoutMs ?? DEFAULT_GIT_HISTORY_TIMEOUT_MS;
1114
+ this.gitHistoryMaxBuffer = options.gitHistoryMaxBuffer ?? DEFAULT_GIT_HISTORY_MAX_BUFFER;
981
1115
  const bundledRipgrepPath = resolveBundledRipgrepPath();
982
1116
  this.rgPath = options.rgPath || process.env.FARMING_RG_BIN || bundledRipgrepPath || 'rg';
983
1117
  this.rgFallbackPath = options.rgFallbackPath ?? (this.rgPath === 'rg' ? bundledRipgrepPath : 'rg');
@@ -986,6 +1120,7 @@ class WorkspaceFileService {
986
1120
  this.gitPath = options.gitPath || 'git';
987
1121
  this.gitStatusCacheTtlMs = options.gitStatusCacheTtlMs ?? DEFAULT_GIT_STATUS_CACHE_TTL_MS;
988
1122
  this.gitStatusInlineTimeoutMs = options.gitStatusInlineTimeoutMs ?? DEFAULT_GIT_STATUS_INLINE_TIMEOUT_MS;
1123
+ this.gitStatusTimeoutMs = options.gitStatusTimeoutMs ?? DEFAULT_GIT_STATUS_TIMEOUT_MS;
989
1124
  this.gitStatusCache = new Map();
990
1125
  this.watchers = new Map();
991
1126
  this.watchOptions = options.watchOptions || {};
@@ -1588,11 +1723,31 @@ class WorkspaceFileService {
1588
1723
  `--untracked-files=${untrackedFiles}`,
1589
1724
  '--ignored=no',
1590
1725
  ...gitStatusExcludePathspecArgs(),
1591
- ], { cwd: root });
1726
+ ], { cwd: root, timeout: options.timeoutMs ?? this.gitStatusTimeoutMs });
1592
1727
  return parseGitStatus(stdout);
1593
1728
  } catch (error) {
1594
- if (error.code === 'ENOENT') return new Map();
1729
+ if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' && options.allowPartial === true) {
1730
+ const partialOutput = String(error.stdout || '');
1731
+ const lastCompleteRecord = partialOutput.lastIndexOf('\0');
1732
+ const statusByPath = parseGitStatus(lastCompleteRecord >= 0
1733
+ ? partialOutput.slice(0, lastCompleteRecord + 1)
1734
+ : '');
1735
+ statusByPath.truncated = true;
1736
+ return statusByPath;
1737
+ }
1738
+ if (error.code === 'ENOENT') {
1739
+ if (options.throwOnError === true) {
1740
+ throw new WorkspaceFileError('git is not installed', 501);
1741
+ }
1742
+ return new Map();
1743
+ }
1744
+ if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
1745
+ throw new WorkspaceFileError('git status timed out', 504);
1746
+ }
1595
1747
  if (error.stderr && /not a git repository/i.test(error.stderr)) return new Map();
1748
+ if (options.throwOnError === true) {
1749
+ throw new WorkspaceFileError(error.stderr || 'git status failed', 500);
1750
+ }
1596
1751
  return new Map();
1597
1752
  }
1598
1753
  }
@@ -1719,10 +1874,165 @@ class WorkspaceFileService {
1719
1874
  }
1720
1875
  }
1721
1876
 
1877
+ async gitHistory(workspaceRoot, options = {}) {
1878
+ const root = await this.resolveRoot(workspaceRoot);
1879
+ const limit = normalizeGitHistoryLimit(options.limit);
1880
+ const skip = normalizeGitHistorySkip(options.skip);
1881
+ const scope = options.scope === 'all' ? 'all' : 'current';
1882
+ let branch = '';
1883
+ try {
1884
+ branch = await this.gitBranch(root);
1885
+ await this.execFile(this.gitPath, ['-C', root, 'rev-parse', '--is-inside-work-tree'], {
1886
+ timeout: 1500,
1887
+ maxBuffer: 64 * 1024,
1888
+ });
1889
+ } catch (error) {
1890
+ if (error.code === 'ENOENT') throw new WorkspaceFileError('git is not installed', 501);
1891
+ if (error.stderr && /not a git repository/i.test(String(error.stderr))) {
1892
+ return { isGitRepo: false, branch: '', head: '', scope, items: [], hasMore: false, nextSkip: null };
1893
+ }
1894
+ throw new WorkspaceFileError(error.stderr || 'git history failed', 500);
1895
+ }
1896
+
1897
+ let head = '';
1898
+ try {
1899
+ const result = await this.execFile(this.gitPath, ['-C', root, 'rev-parse', '--verify', 'HEAD'], {
1900
+ timeout: 1500,
1901
+ maxBuffer: 64 * 1024,
1902
+ });
1903
+ head = String(result.stdout || '').trim();
1904
+ } catch (error) {
1905
+ if (error.stderr && /(needed a single revision|unknown revision|bad revision|ambiguous argument)/i.test(String(error.stderr))) {
1906
+ return { isGitRepo: true, branch, head: '', scope, items: [], hasMore: false, nextSkip: null };
1907
+ }
1908
+ throw new WorkspaceFileError(error.stderr || 'git history failed', 500);
1909
+ }
1910
+
1911
+ try {
1912
+ const revisionArgs = scope === 'all'
1913
+ ? ['HEAD', '--branches', '--tags', '--remotes']
1914
+ : ['--first-parent', 'HEAD'];
1915
+ const { stdout } = await this.execFile(this.gitPath, [
1916
+ '-C', root,
1917
+ 'log',
1918
+ '-z',
1919
+ '--date-order',
1920
+ '--decorate=full',
1921
+ '--format=%H%x00%P%x00%an%x00%ae%x00%aI%x00%s%x00%B%x00%D',
1922
+ `--max-count=${limit + 1}`,
1923
+ `--skip=${skip}`,
1924
+ ...revisionArgs,
1925
+ ], {
1926
+ timeout: this.gitHistoryTimeoutMs,
1927
+ maxBuffer: this.gitHistoryMaxBuffer,
1928
+ });
1929
+ const parsed = parseGitHistoryLog(stdout);
1930
+ const hasMore = parsed.length > limit;
1931
+ const items = parsed.slice(0, limit);
1932
+ return {
1933
+ isGitRepo: true,
1934
+ branch,
1935
+ head,
1936
+ scope,
1937
+ items,
1938
+ hasMore,
1939
+ nextSkip: hasMore ? skip + items.length : null,
1940
+ };
1941
+ } catch (error) {
1942
+ if (error.code === 'ENOENT') throw new WorkspaceFileError('git is not installed', 501);
1943
+ if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
1944
+ throw new WorkspaceFileError('git history timed out', 504);
1945
+ }
1946
+ if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
1947
+ throw new WorkspaceFileError('git history output is too large', 413);
1948
+ }
1949
+ if (error.stderr && /not a git repository/i.test(String(error.stderr))) {
1950
+ return { isGitRepo: false, branch: '', head: '', scope, items: [], hasMore: false, nextSkip: null };
1951
+ }
1952
+ throw new WorkspaceFileError(error.stderr || 'git history failed', 500);
1953
+ }
1954
+ }
1955
+
1956
+ async gitHistoryChanges(workspaceRoot, commitValue, parentValue, options = {}) {
1957
+ const root = await this.resolveRoot(workspaceRoot);
1958
+ const commit = normalizeGitObjectId(commitValue);
1959
+ const requestedParent = parentValue ? normalizeGitObjectId(parentValue, 'parent') : '';
1960
+ const limit = Math.max(1, Math.min(2000, Number(options.limit) || DEFAULT_GIT_CHANGES_LIMIT));
1961
+
1962
+ try {
1963
+ const { stdout: parentOutput } = await this.execFile(this.gitPath, [
1964
+ '-C', root, 'rev-list', '--parents', '-n', '1', commit,
1965
+ ], {
1966
+ timeout: this.gitHistoryTimeoutMs,
1967
+ maxBuffer: 64 * 1024,
1968
+ });
1969
+ const revision = String(parentOutput || '').trim().split(/\s+/).filter(Boolean);
1970
+ if (!revision.length || revision[0].toLowerCase() !== commit) {
1971
+ throw new WorkspaceFileError('commit was not found', 404);
1972
+ }
1973
+ const parentIds = revision.slice(1).map(value => value.toLowerCase());
1974
+ if (requestedParent && !parentIds.includes(requestedParent)) {
1975
+ throw new WorkspaceFileError('parent is not a parent of commit', 400);
1976
+ }
1977
+ const parent = requestedParent || parentIds[0] || '';
1978
+ let comparisonBase = parent;
1979
+ if (!comparisonBase) {
1980
+ let objectFormat = 'sha1';
1981
+ try {
1982
+ const result = await this.execFile(this.gitPath, ['-C', root, 'rev-parse', '--show-object-format'], {
1983
+ timeout: 1500,
1984
+ maxBuffer: 64 * 1024,
1985
+ });
1986
+ objectFormat = String(result.stdout || '').trim();
1987
+ } catch {
1988
+ // Git versions without --show-object-format use SHA-1 repositories.
1989
+ }
1990
+ comparisonBase = gitEmptyTreeObjectId(objectFormat);
1991
+ }
1992
+ const args = parent
1993
+ ? ['-C', root, 'diff', '--name-status', '-z', '-M', parent, commit, '--']
1994
+ : ['-C', root, 'diff-tree', '--root', '--no-commit-id', '--name-status', '-r', '-z', '-M', commit];
1995
+ const { stdout } = await this.execFile(this.gitPath, args, {
1996
+ timeout: this.gitHistoryTimeoutMs,
1997
+ maxBuffer: this.gitHistoryMaxBuffer,
1998
+ });
1999
+ const allItems = parseGitHistoryChanges(stdout);
2000
+ return {
2001
+ commit,
2002
+ comparisonBase,
2003
+ parent: parent || null,
2004
+ parentIds,
2005
+ items: allItems.slice(0, limit),
2006
+ truncated: allItems.length > limit,
2007
+ };
2008
+ } catch (error) {
2009
+ if (error instanceof WorkspaceFileError) throw error;
2010
+ if (error.code === 'ENOENT') throw new WorkspaceFileError('git is not installed', 501);
2011
+ if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
2012
+ throw new WorkspaceFileError('git commit changes timed out', 504);
2013
+ }
2014
+ if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
2015
+ throw new WorkspaceFileError('git commit changes output is too large', 413);
2016
+ }
2017
+ if (error.stderr && /(bad object|unknown revision|ambiguous argument|not a valid object name)/i.test(String(error.stderr))) {
2018
+ throw new WorkspaceFileError('commit was not found', 404);
2019
+ }
2020
+ if (error.stderr && /not a git repository/i.test(String(error.stderr))) {
2021
+ throw new WorkspaceFileError('workspace is not a git repository', 409);
2022
+ }
2023
+ throw new WorkspaceFileError(error.stderr || 'git commit changes failed', 500);
2024
+ }
2025
+ }
2026
+
1722
2027
  async changes(workspaceRoot, options = {}) {
1723
2028
  const root = await this.resolveRoot(workspaceRoot);
1724
2029
  const limit = Math.max(1, Math.min(2000, Number(options.limit) || DEFAULT_GIT_CHANGES_LIMIT));
1725
- const gitStatusByPath = await this.loadGitStatusByPath(root, { untrackedFiles: 'all' });
2030
+ const gitStatusByPath = await this.loadGitStatusByPath(root, {
2031
+ allowPartial: true,
2032
+ throwOnError: true,
2033
+ untrackedFiles: 'all',
2034
+ });
2035
+ this.invalidateGitStatus(root);
1726
2036
  const allItems = await Promise.all(Array.from(gitStatusByPath.entries())
1727
2037
  .map(async ([filePath, status]) => ({
1728
2038
  path: filePath,
@@ -1740,7 +2050,7 @@ class WorkspaceFileService {
1740
2050
 
1741
2051
  return {
1742
2052
  items: allItems.slice(0, limit),
1743
- truncated: allItems.length > limit,
2053
+ truncated: gitStatusByPath.truncated === true || allItems.length > limit,
1744
2054
  };
1745
2055
  }
1746
2056
 
@@ -2762,4 +3072,9 @@ module.exports = {
2762
3072
  parseUnifiedDiffHunks,
2763
3073
  selectUnifiedDiffHunk,
2764
3074
  resolveCommandRunnerNodePath,
3075
+ normalizeGitHistoryLimit,
3076
+ normalizeGitHistorySkip,
3077
+ parseGitHistoryChanges,
3078
+ parseGitHistoryLog,
3079
+ parseGitHistoryReferences,
2765
3080
  };
@@ -0,0 +1,164 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { WorkspaceFileError } = require('./workspace-file-service');
5
+
6
+ const GLOBAL_WORKSPACE_FILES_AGENT_ID = '__farming_global_files__';
7
+ const GLOBAL_WORKSPACE_ROOT_ID = 'wroot_global';
8
+ const GLOBAL_WORKSPACE_FILES_ROOT = '/';
9
+ const PROJECT_FILES_WORKSPACE_PREFIX = '__farming_project__:';
10
+
11
+ function normalizeWorkspacePath(value) {
12
+ const raw = String(value || '').trim();
13
+ if (!raw) return '';
14
+ return path.resolve(raw.replace(/^~(?=$|[\\/])/, os.homedir()));
15
+ }
16
+
17
+ function canonicalWorkspacePath(value) {
18
+ const normalized = normalizeWorkspacePath(value);
19
+ if (!normalized) return '';
20
+ try {
21
+ return fs.realpathSync(normalized);
22
+ } catch {
23
+ return normalized;
24
+ }
25
+ }
26
+
27
+ function rootIdForPath(value) {
28
+ const canonicalPath = canonicalWorkspacePath(value);
29
+ if (!canonicalPath) return '';
30
+ let hash = 0xcbf29ce484222325n;
31
+ for (const byte of Buffer.from(canonicalPath, 'utf8')) {
32
+ hash ^= BigInt(byte);
33
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
34
+ }
35
+ return `wroot_${hash.toString(16).padStart(16, '0')}`;
36
+ }
37
+
38
+ function projectWorkspaceFromLegacyRef(ref) {
39
+ const value = String(ref || '');
40
+ if (!value.startsWith(PROJECT_FILES_WORKSPACE_PREFIX)) return '';
41
+ try {
42
+ return normalizeWorkspacePath(decodeURIComponent(value.slice(PROJECT_FILES_WORKSPACE_PREFIX.length)));
43
+ } catch {
44
+ return '';
45
+ }
46
+ }
47
+
48
+ function workspaceRootSnapshot(root) {
49
+ return {
50
+ rootId: root.rootId,
51
+ kind: root.kind,
52
+ canonicalPath: root.canonicalPath,
53
+ repositoryId: root.repositoryId || '',
54
+ accessPolicy: { ...root.accessPolicy },
55
+ };
56
+ }
57
+
58
+ class WorkspaceRootRegistry {
59
+ constructor(agentManager) {
60
+ this.agentManager = agentManager;
61
+ this.roots = new Map();
62
+ this.register({
63
+ rootId: GLOBAL_WORKSPACE_ROOT_ID,
64
+ kind: 'global',
65
+ canonicalPath: GLOBAL_WORKSPACE_FILES_ROOT,
66
+ accessPolicy: { readOnly: true, watch: false, externalReads: false },
67
+ });
68
+ }
69
+
70
+ register(options = {}) {
71
+ const canonicalPath = options.rootId === GLOBAL_WORKSPACE_ROOT_ID
72
+ ? GLOBAL_WORKSPACE_FILES_ROOT
73
+ : canonicalWorkspacePath(options.canonicalPath);
74
+ if (!canonicalPath) throw new WorkspaceFileError('workspace root path is required', 400);
75
+ const rootId = options.rootId || rootIdForPath(canonicalPath);
76
+ const current = this.roots.get(rootId);
77
+ if (current && current.canonicalPath !== canonicalPath) {
78
+ throw new WorkspaceFileError('workspace root identity collision', 409);
79
+ }
80
+ const root = Object.freeze({
81
+ rootId,
82
+ kind: options.kind || current?.kind || 'directory',
83
+ canonicalPath,
84
+ repositoryId: options.repositoryId || current?.repositoryId || '',
85
+ accessPolicy: Object.freeze({
86
+ readOnly: options.accessPolicy?.readOnly === true,
87
+ watch: options.accessPolicy?.watch !== false,
88
+ externalReads: options.accessPolicy?.externalReads !== false,
89
+ }),
90
+ });
91
+ this.roots.set(rootId, root);
92
+ return root;
93
+ }
94
+
95
+ configuredProjectPaths() {
96
+ const settings = this.agentManager?.configManager?.getSettings?.() || {};
97
+ return (Array.isArray(settings.projectWorkspaces) ? settings.projectWorkspaces : [])
98
+ .map(normalizeWorkspacePath)
99
+ .filter(Boolean);
100
+ }
101
+
102
+ liveAgentPaths() {
103
+ const state = this.agentManager?.getState?.() || { agents: [] };
104
+ return (state.agents || []).filter(agent => agent && !agent.isMain).map(agent => ({
105
+ agentId: agent.id,
106
+ path: normalizeWorkspacePath(agent.projectWorkspace || agent.gitWorktree?.workspace || agent.cwd),
107
+ })).filter(entry => entry.path);
108
+ }
109
+
110
+ refresh() {
111
+ const activeRootIds = new Set([GLOBAL_WORKSPACE_ROOT_ID]);
112
+ for (const projectPath of this.configuredProjectPaths()) {
113
+ activeRootIds.add(this.register({ kind: 'directory', canonicalPath: projectPath }).rootId);
114
+ }
115
+ for (const entry of this.liveAgentPaths()) {
116
+ activeRootIds.add(this.register({ kind: 'directory', canonicalPath: entry.path }).rootId);
117
+ }
118
+ for (const rootId of this.roots.keys()) {
119
+ if (!activeRootIds.has(rootId)) this.roots.delete(rootId);
120
+ }
121
+ }
122
+
123
+ resolve(ref) {
124
+ const value = String(ref || '').trim();
125
+ if (!value) throw new WorkspaceFileError('rootId is required', 400);
126
+ if (value === GLOBAL_WORKSPACE_ROOT_ID || value === GLOBAL_WORKSPACE_FILES_AGENT_ID) {
127
+ return this.roots.get(GLOBAL_WORKSPACE_ROOT_ID);
128
+ }
129
+
130
+ this.refresh();
131
+ const registered = this.roots.get(value);
132
+ if (registered) return registered;
133
+
134
+ const projectPath = projectWorkspaceFromLegacyRef(value);
135
+ if (projectPath) {
136
+ const authorized = this.configuredProjectPaths().includes(projectPath)
137
+ || this.liveAgentPaths().some(entry => entry.path === projectPath);
138
+ if (!authorized) throw new WorkspaceFileError('project not found', 404);
139
+ return this.register({ kind: 'directory', canonicalPath: projectPath });
140
+ }
141
+
142
+ const agentPath = this.liveAgentPaths().find(entry => entry.agentId === value)?.path
143
+ || normalizeWorkspacePath(this.agentManager?.getAgentWorkspaceRoot?.(value));
144
+ if (agentPath) return this.register({ kind: 'directory', canonicalPath: agentPath });
145
+ throw new WorkspaceFileError(value.startsWith('wroot_') ? 'workspace root not found' : 'agent not found', 404);
146
+ }
147
+
148
+ list() {
149
+ this.refresh();
150
+ return [...this.roots.values()].map(workspaceRootSnapshot);
151
+ }
152
+ }
153
+
154
+ module.exports = {
155
+ GLOBAL_WORKSPACE_FILES_AGENT_ID,
156
+ GLOBAL_WORKSPACE_FILES_ROOT,
157
+ GLOBAL_WORKSPACE_ROOT_ID,
158
+ PROJECT_FILES_WORKSPACE_PREFIX,
159
+ WorkspaceRootRegistry,
160
+ canonicalWorkspacePath,
161
+ projectWorkspaceFromLegacyRef,
162
+ rootIdForPath,
163
+ workspaceRootSnapshot,
164
+ };