farming-code 2.2.20 → 2.2.22

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 (49) hide show
  1. package/README.md +5 -5
  2. package/README.zh_cn.md +5 -5
  3. package/THIRD_PARTY_NOTICES.md +5 -8
  4. package/backend/acp/client-services.js +37 -5
  5. package/backend/acp/patch-decisions.js +19 -3
  6. package/backend/acp-runtime.js +1201 -198
  7. package/backend/acp-transcript.js +66 -0
  8. package/backend/agent-lifecycle-journal.js +133 -0
  9. package/backend/agent-manager.js +2715 -354
  10. package/backend/agent-order.js +8 -4
  11. package/backend/agent-runtime-binding.js +1 -0
  12. package/backend/bundle-update-helper.js +5 -1
  13. package/backend/config-manager.js +16 -0
  14. package/backend/control-api.js +146 -20
  15. package/backend/farming-app-cli.js +335 -10
  16. package/backend/farming-session-store.js +283 -36
  17. package/backend/json-cli-runtime.js +120 -6
  18. package/backend/npm-update-helper.js +97 -18
  19. package/backend/provider-adapters.js +18 -6
  20. package/backend/review-session-service.js +31 -17
  21. package/backend/server.js +332 -102
  22. package/backend/storage-layout.js +10 -0
  23. package/backend/theme-manager.js +23 -5
  24. package/backend/update-service.js +239 -27
  25. package/backend/workspace-file-router.js +9 -3
  26. package/backend/workspace-file-service.js +292 -62
  27. package/dist/acp/codex-acp-1.1.4.js +32 -1
  28. package/dist/assets/App-CmZdNavW.js +210 -0
  29. package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
  30. package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
  31. package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
  32. package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
  33. package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
  34. package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
  35. package/dist/assets/file-icons-CHWt5nSy.js +1 -0
  36. package/dist/assets/{index-DuMjesL2.js → index-RvUiMpGP.js} +2 -2
  37. package/dist/assets/main-XMXgyJJt.css +1 -0
  38. package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
  39. package/dist/index.html +1 -1
  40. package/frontend/skins/crt/app.js +199 -81
  41. package/package.json +9 -3
  42. package/dist/assets/App-BupehIbc.js +0 -210
  43. package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
  44. package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
  45. package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
  46. package/dist/assets/code-dark-BAt0Q_Op.css +0 -1
  47. package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
  48. package/dist/assets/main-y2aK_56c.css +0 -1
  49. package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
@@ -89,6 +89,7 @@ const crtStructuredPreviewTimers = new Map();
89
89
  let sessionRuntime = null;
90
90
  let structuredSessionPoller = null;
91
91
  let structuredSessionLoading = false;
92
+ let structuredSessionGeneration = 0;
92
93
  let structuredSessionRenderedAt = '';
93
94
  let structuredSessionSnapshot = null;
94
95
  let structuredSessionControlsLoading = false;
@@ -107,6 +108,8 @@ let runtimeSwitchPending = false;
107
108
  let pendingRuntimeSwitchAgentId = '';
108
109
  let runtimeSwitchRequestSequence = 0;
109
110
  let crtTerminalReplication = null;
111
+ const globalSettingsSaveRevisions = new Map();
112
+ let globalSettingsSaveTail = Promise.resolve();
110
113
  const terminalPreviewSnapshots = new Map();
111
114
  const crtBrandPulseTimers = new Map();
112
115
  const SESSION_LINK_LIMIT = 6;
@@ -128,6 +131,7 @@ const CRT_BILLING_TOTAL_ANIMATION_MS = 900;
128
131
  const CRT_BILLING_DAY_DETAIL_RETRY_MS = 750;
129
132
  const CRT_BILLING_DAY_DETAIL_MAX_RETRIES = 4;
130
133
  const CRT_TERMINAL_CHECKPOINT_REQUEST_TIMEOUT_MS = 5_000;
134
+ const CRT_SETTINGS_REQUEST_TIMEOUT_MS = 15_000;
131
135
  const CRT_TERMINAL_RESIZE_SETTLE_MS = 250;
132
136
  const CRT_TERMINAL_MIN_COLS = 40;
133
137
  const CRT_TERMINAL_MIN_ROWS = 10;
@@ -849,8 +853,14 @@ function fallbackCopyText(text) {
849
853
  return copied;
850
854
  }
851
855
 
852
- async function pasteTerminalText(text) {
853
- if (!text || !focusedAgentId) {
856
+ function isCurrentCrtSession(agentId, sessionToken = null) {
857
+ if (!agentId || focusedAgentId !== agentId) return false;
858
+ const runtime = getSessionRuntime();
859
+ return sessionToken === null || !runtime || runtime.isCurrentSession(agentId, sessionToken);
860
+ }
861
+
862
+ async function pasteTerminalText(text, agentId = focusedAgentId, sessionToken = null) {
863
+ if (!text || !isCurrentCrtSession(agentId, sessionToken)) {
854
864
  return false;
855
865
  }
856
866
 
@@ -877,9 +887,13 @@ async function pasteFromClipboard() {
877
887
  return false;
878
888
  }
879
889
 
890
+ const agentId = focusedAgentId;
891
+ const runtime = getSessionRuntime();
892
+ const sessionToken = runtime ? runtime.getSessionToken() : null;
893
+ if (!agentId) return false;
880
894
  try {
881
895
  const text = await navigator.clipboard.readText();
882
- return pasteTerminalText(text);
896
+ return pasteTerminalText(text, agentId, sessionToken);
883
897
  } catch (error) {
884
898
  console.warn('Clipboard API paste failed:', error);
885
899
  return false;
@@ -2792,9 +2806,19 @@ async function loadThemes() {
2792
2806
  }
2793
2807
  }
2794
2808
 
2809
+ async function fetchCrtSettingsRequest(url, init) {
2810
+ const controller = new window.AbortController();
2811
+ const timeout = setTimeout(() => controller.abort(), CRT_SETTINGS_REQUEST_TIMEOUT_MS);
2812
+ try {
2813
+ return await fetch(url, { ...init, signal: controller.signal });
2814
+ } finally {
2815
+ clearTimeout(timeout);
2816
+ }
2817
+ }
2818
+
2795
2819
  async function loadGlobalSettings() {
2796
2820
  try {
2797
- const response = await fetch(farmingApiPath('/settings'));
2821
+ const response = await fetchCrtSettingsRequest(farmingApiPath('/settings'));
2798
2822
  const data = await response.json();
2799
2823
  globalSettings = {
2800
2824
  ...globalSettings,
@@ -2813,31 +2837,48 @@ async function loadGlobalSettings() {
2813
2837
  }
2814
2838
  }
2815
2839
 
2816
- async function saveGlobalSettings() {
2817
- try {
2818
- syncWorkspaceSettings();
2819
- const response = await fetch(farmingApiPath('/settings'), {
2820
- method: 'POST',
2821
- headers: {
2822
- 'Content-Type': 'application/json'
2823
- },
2824
- body: JSON.stringify(globalSettings)
2825
- });
2840
+ async function saveGlobalSettings(settingsPatch) {
2841
+ const patch = settingsPatch && typeof settingsPatch === 'object'
2842
+ ? { ...settingsPatch }
2843
+ : {};
2844
+ const keys = Object.keys(patch);
2845
+ if (keys.length === 0) return;
2846
+ const revisions = new Map(keys.map((key) => {
2847
+ const revision = (globalSettingsSaveRevisions.get(key) || 0) + 1;
2848
+ globalSettingsSaveRevisions.set(key, revision);
2849
+ return [key, revision];
2850
+ }));
2851
+ const save = globalSettingsSaveTail.catch(() => {}).then(async () => {
2852
+ try {
2853
+ const response = await fetchCrtSettingsRequest(farmingApiPath('/settings'), {
2854
+ method: 'POST',
2855
+ headers: {
2856
+ 'Content-Type': 'application/json'
2857
+ },
2858
+ body: JSON.stringify(patch)
2859
+ });
2826
2860
 
2827
- const data = await response.json();
2828
- if (data.success) {
2829
- globalSettings = {
2830
- ...globalSettings,
2831
- ...(data.settings || {})
2832
- };
2833
- if (globalSettings.instanceName) {
2834
- document.title = `${globalSettings.instanceName} · Farming CRT`;
2861
+ const data = await response.json();
2862
+ if (data.success) {
2863
+ const savedSettings = data.settings || {};
2864
+ keys.forEach((key) => {
2865
+ if (globalSettingsSaveRevisions.get(key) !== revisions.get(key)) return;
2866
+ globalSettings[key] = Object.prototype.hasOwnProperty.call(savedSettings, key)
2867
+ ? savedSettings[key]
2868
+ : patch[key];
2869
+ });
2870
+ if (globalSettings.instanceName) {
2871
+ document.title = `${globalSettings.instanceName} · Farming CRT`;
2872
+ }
2873
+ syncWorkspaceSettings();
2835
2874
  }
2836
- syncWorkspaceSettings();
2875
+ } catch (error) {
2876
+ console.error('Failed to save global settings:', error);
2877
+ await loadGlobalSettings();
2837
2878
  }
2838
- } catch (error) {
2839
- console.error('Failed to save global settings:', error);
2840
- }
2879
+ });
2880
+ globalSettingsSaveTail = save;
2881
+ return save;
2841
2882
  }
2842
2883
 
2843
2884
  async function setTheme(themeId) {
@@ -2941,7 +2982,7 @@ function scheduleCrtTerminalFontSizeSave() {
2941
2982
  if (crtTerminalFontSizeSaveTimer) clearTimeout(crtTerminalFontSizeSaveTimer);
2942
2983
  crtTerminalFontSizeSaveTimer = setTimeout(() => {
2943
2984
  crtTerminalFontSizeSaveTimer = null;
2944
- void saveGlobalSettings();
2985
+ void saveGlobalSettings({ crtTerminalFontSize: globalSettings.crtTerminalFontSize });
2945
2986
  }, 150);
2946
2987
  }
2947
2988
 
@@ -2959,7 +3000,7 @@ function initDisplaySettings() {
2959
3000
  crtToggle.onchange = async () => {
2960
3001
  globalSettings.crtSkinEffectsEnabled = crtToggle.checked;
2961
3002
  applyCRTEffects(crtToggle.checked);
2962
- await saveGlobalSettings();
3003
+ await saveGlobalSettings({ crtSkinEffectsEnabled: crtToggle.checked });
2963
3004
  };
2964
3005
  }
2965
3006
  } else {
@@ -2974,7 +3015,7 @@ function initDisplaySettings() {
2974
3015
  dynamicHeatToggle.onchange = async () => {
2975
3016
  globalSettings.crtDynamicHeatEnabled = dynamicHeatToggle.checked;
2976
3017
  renderState();
2977
- await saveGlobalSettings();
3018
+ await saveGlobalSettings({ crtDynamicHeatEnabled: dynamicHeatToggle.checked });
2978
3019
  };
2979
3020
  }
2980
3021
 
@@ -2994,7 +3035,9 @@ function initSessionEngineSettings() {
2994
3035
  skipPermissionCheckToggle.checked = globalSettings.dangerouslySkipAgentPermissionsByDefault === true;
2995
3036
  skipPermissionCheckToggle.onchange = async () => {
2996
3037
  globalSettings.dangerouslySkipAgentPermissionsByDefault = skipPermissionCheckToggle.checked;
2997
- await saveGlobalSettings();
3038
+ await saveGlobalSettings({
3039
+ dangerouslySkipAgentPermissionsByDefault: skipPermissionCheckToggle.checked,
3040
+ });
2998
3041
  };
2999
3042
  }
3000
3043
 
@@ -5358,11 +5401,16 @@ async function startCrtAgent(agent, workspaceToUse, asMainAgent) {
5358
5401
  }
5359
5402
  if (JSON.stringify(getWorkspaceHistory()) !== previousHistory) {
5360
5403
  refreshWorkspaceMemoryUI();
5361
- await saveGlobalSettings();
5404
+ syncWorkspaceSettings();
5405
+ await saveGlobalSettings({
5406
+ workspace: globalSettings.workspace,
5407
+ workspaceHistory: globalSettings.workspaceHistory,
5408
+ });
5362
5409
  }
5363
5410
 
5364
5411
  ws.send(JSON.stringify({
5365
5412
  type: 'start-agent',
5413
+ requestId: globalThis.crypto?.randomUUID?.(),
5366
5414
  command: agent.name,
5367
5415
  workspace: workspaceToUse,
5368
5416
  asMain: asMainAgent,
@@ -6303,7 +6351,8 @@ function flushStructuredComposerFollowUp(agent) {
6303
6351
  structuredComposerPendingFollowUps.set(agent.id, queue);
6304
6352
  return;
6305
6353
  }
6306
- setTimeout(() => void refreshStructuredSession(agent.id, true), 160);
6354
+ const generation = structuredSessionGeneration;
6355
+ setTimeout(() => void refreshStructuredSession(agent.id, true, generation), 160);
6307
6356
  }
6308
6357
 
6309
6358
  function structuredContextUsage(session) {
@@ -6378,6 +6427,14 @@ function structuredConfigValueLabel(option) {
6378
6427
  return selected && selected.name ? selected.name : String(option.currentValue || '');
6379
6428
  }
6380
6429
 
6430
+ function isCurrentStructuredSession(agentId, generation) {
6431
+ return Boolean(
6432
+ agentId
6433
+ && agentId === focusedAgentId
6434
+ && generation === structuredSessionGeneration
6435
+ );
6436
+ }
6437
+
6381
6438
  function resetStructuredSessionControls() {
6382
6439
  structuredSessionSnapshot = null;
6383
6440
  structuredSessionControlsLoading = false;
@@ -6502,19 +6559,28 @@ function backStructuredComposerMenu() {
6502
6559
  closeStructuredComposerMenu();
6503
6560
  }
6504
6561
 
6505
- async function patchStructuredAcpSession(patch) {
6506
- if (!focusedAgentId) return;
6562
+ async function patchStructuredAcpSession(agentId, generation, patch) {
6563
+ if (!isCurrentStructuredSession(agentId, generation)) return;
6507
6564
  const openerId = structuredComposerMenuOpenerId;
6508
- const response = await fetch(farmingApiPath(`/agents/${encodeURIComponent(focusedAgentId)}/acp-session`), {
6509
- method: 'PATCH',
6510
- headers: { 'Content-Type': 'application/json' },
6511
- body: JSON.stringify(patch)
6512
- });
6513
- const body = await response.json().catch(() => null);
6565
+ let response;
6566
+ let body;
6567
+ try {
6568
+ response = await fetch(farmingApiPath(`/agents/${encodeURIComponent(agentId)}/acp-session`), {
6569
+ method: 'PATCH',
6570
+ headers: { 'Content-Type': 'application/json' },
6571
+ body: JSON.stringify(patch)
6572
+ });
6573
+ body = await response.json().catch(() => null);
6574
+ } catch (error) {
6575
+ if (!isCurrentStructuredSession(agentId, generation)) return;
6576
+ throw error;
6577
+ }
6578
+ if (!isCurrentStructuredSession(agentId, generation)) return;
6514
6579
  if (!response.ok) throw new Error(body && body.error ? body.error : `Failed to update ACP session (${response.status})`);
6515
6580
  structuredComposerMenu = '';
6516
6581
  structuredComposerConfigId = '';
6517
- await refreshStructuredSessionControls(focusedAgentId, true);
6582
+ await refreshStructuredSessionControls(agentId, true, generation);
6583
+ if (!isCurrentStructuredSession(agentId, generation)) return;
6518
6584
  const opener = openerId ? document.getElementById(openerId) : null;
6519
6585
  if (opener && !opener.hidden && !opener.disabled) opener.focus();
6520
6586
  }
@@ -6542,6 +6608,8 @@ function renderStructuredComposerMenu() {
6542
6608
  const retainedFocus = captureStructuredComposerMenuFocus(menu);
6543
6609
  menu.replaceChildren();
6544
6610
  const session = structuredSessionSnapshot;
6611
+ const agentId = focusedAgentId;
6612
+ const generation = structuredSessionGeneration;
6545
6613
  if (!structuredComposerMenu || !session) {
6546
6614
  menu.hidden = true;
6547
6615
  return;
@@ -6583,7 +6651,7 @@ function renderStructuredComposerMenu() {
6583
6651
  const current = session.currentModeId || (session.modes && session.modes.currentModeId) || '';
6584
6652
  modes.forEach((mode) => {
6585
6653
  items.appendChild(structuredMenuButton(mode.name || mode.id, mode.description || '', mode.id === current, () => {
6586
- void patchStructuredAcpSession({ modeId: mode.id }).catch(showStructuredComposerError);
6654
+ void patchStructuredAcpSession(agentId, generation, { modeId: mode.id }).catch(showStructuredComposerError);
6587
6655
  }, `mode:${mode.id}`));
6588
6656
  });
6589
6657
  } else if (!selectedConfig) {
@@ -6603,13 +6671,13 @@ function renderStructuredComposerMenu() {
6603
6671
  } else if (selectedConfig.type === 'boolean') {
6604
6672
  [false, true].forEach((value) => {
6605
6673
  items.appendChild(structuredMenuButton(value ? 'ON' : 'OFF', selectedConfig.description || '', selectedConfig.currentValue === value, () => {
6606
- void patchStructuredAcpSession({ configId: selectedConfig.id, value }).catch(showStructuredComposerError);
6674
+ void patchStructuredAcpSession(agentId, generation, { configId: selectedConfig.id, value }).catch(showStructuredComposerError);
6607
6675
  }, `boolean:${value}`));
6608
6676
  });
6609
6677
  } else {
6610
6678
  structuredSelectOptions(selectedConfig).forEach((candidate) => {
6611
6679
  items.appendChild(structuredMenuButton(candidate.name || candidate.value, candidate.description || '', candidate.value === selectedConfig.currentValue, () => {
6612
- void patchStructuredAcpSession({ configId: selectedConfig.id, value: candidate.value }).catch(showStructuredComposerError);
6680
+ void patchStructuredAcpSession(agentId, generation, { configId: selectedConfig.id, value: candidate.value }).catch(showStructuredComposerError);
6613
6681
  }, `option:${candidate.value}`));
6614
6682
  });
6615
6683
  }
@@ -6644,8 +6712,12 @@ function renderStructuredSessionControls() {
6644
6712
  renderStructuredComposerMenu();
6645
6713
  }
6646
6714
 
6647
- async function refreshStructuredSessionControls(agentId = focusedAgentId, force = false) {
6648
- if (!agentId || structuredSessionControlsLoading) return;
6715
+ async function refreshStructuredSessionControls(
6716
+ agentId = focusedAgentId,
6717
+ force = false,
6718
+ generation = structuredSessionGeneration
6719
+ ) {
6720
+ if (!isCurrentStructuredSession(agentId, generation) || structuredSessionControlsLoading) return;
6649
6721
  const agent = state && state.agents.find((candidate) => candidate.id === agentId);
6650
6722
  if (!agent || agent.runtimeBinding?.kind !== 'acp') {
6651
6723
  resetStructuredSessionControls();
@@ -6660,6 +6732,7 @@ async function refreshStructuredSessionControls(agentId = focusedAgentId, force
6660
6732
  try {
6661
6733
  const response = await fetch(farmingApiPath(`/agents/${encodeURIComponent(agentId)}/acp-session`));
6662
6734
  const body = await response.json().catch(() => null);
6735
+ if (!isCurrentStructuredSession(agentId, generation)) return;
6663
6736
  if (!response.ok || !body || !body.session) {
6664
6737
  throw new Error(body && body.error ? body.error : `Failed to read ACP session (${response.status})`);
6665
6738
  }
@@ -6667,9 +6740,12 @@ async function refreshStructuredSessionControls(agentId = focusedAgentId, force
6667
6740
  structuredSessionControlsRevision = revision || String(body.session.updatedAt || Date.now());
6668
6741
  renderStructuredSessionControls();
6669
6742
  } catch (error) {
6743
+ if (!isCurrentStructuredSession(agentId, generation)) return;
6670
6744
  showStructuredComposerError(error);
6671
6745
  } finally {
6672
- structuredSessionControlsLoading = false;
6746
+ if (isCurrentStructuredSession(agentId, generation)) {
6747
+ structuredSessionControlsLoading = false;
6748
+ }
6673
6749
  }
6674
6750
  }
6675
6751
 
@@ -6741,7 +6817,7 @@ function setStructuredComposerActive(active) {
6741
6817
  }
6742
6818
  }
6743
6819
 
6744
- function updateStructuredComposerState(agent) {
6820
+ function updateStructuredComposerState(agent, { refreshControls = true } = {}) {
6745
6821
  const input = document.getElementById('crt-structured-input');
6746
6822
  const sendButton = document.getElementById('crt-structured-send');
6747
6823
  const statusNode = document.getElementById('crt-structured-composer-status');
@@ -6767,7 +6843,9 @@ function updateStructuredComposerState(agent) {
6767
6843
  statusNode.textContent = error || `${kind} ${String(runtimeStatus || 'idle').toUpperCase()}`;
6768
6844
  statusNode.classList.toggle('error', Boolean(error));
6769
6845
  renderStructuredPermissions(agent);
6770
- if (agent && agent.runtimeBinding?.kind === 'acp') void refreshStructuredSessionControls(agent.id);
6846
+ if (refreshControls && agent && agent.runtimeBinding?.kind === 'acp') {
6847
+ void refreshStructuredSessionControls(agent.id);
6848
+ }
6771
6849
  }
6772
6850
 
6773
6851
  function structuredAttachmentId(file) {
@@ -6926,6 +7004,8 @@ function renderStructuredPermissions(agent) {
6926
7004
  if (!container) return;
6927
7005
  container.replaceChildren();
6928
7006
  if (!agent || agent.runtimeBinding?.kind !== 'acp') return;
7007
+ const agentId = agent.id;
7008
+ const generation = structuredSessionGeneration;
6929
7009
  const runtime = agent.runtimeBinding;
6930
7010
  const requests = Array.isArray(runtime.pendingPermissions) && runtime.pendingPermissions.length
6931
7011
  ? runtime.pendingPermissions
@@ -6944,27 +7024,47 @@ function renderStructuredPermissions(agent) {
6944
7024
  const button = document.createElement('button');
6945
7025
  button.type = 'button';
6946
7026
  button.textContent = `[${option.name}]`;
6947
- button.onclick = () => void respondToStructuredPermission(request.requestId, option.optionId, false);
7027
+ button.onclick = () => void respondToStructuredPermission(
7028
+ agentId,
7029
+ generation,
7030
+ request.requestId,
7031
+ option.optionId,
7032
+ false,
7033
+ );
6948
7034
  actions.appendChild(button);
6949
7035
  });
6950
7036
  const decline = document.createElement('button');
6951
7037
  decline.type = 'button';
6952
7038
  decline.textContent = '[DECLINE]';
6953
- decline.onclick = () => void respondToStructuredPermission(request.requestId, undefined, true);
7039
+ decline.onclick = () => void respondToStructuredPermission(
7040
+ agentId,
7041
+ generation,
7042
+ request.requestId,
7043
+ undefined,
7044
+ true,
7045
+ );
6954
7046
  actions.appendChild(decline);
6955
7047
  panel.append(title, description, actions);
6956
7048
  container.appendChild(panel);
6957
7049
  });
6958
7050
  }
6959
7051
 
6960
- async function respondToStructuredPermission(requestId, optionId, cancelled) {
6961
- if (!focusedAgentId) return;
6962
- const response = await fetch(farmingApiPath(`/agents/${encodeURIComponent(focusedAgentId)}/acp-permission`), {
6963
- method: 'POST',
6964
- headers: { 'Content-Type': 'application/json' },
6965
- body: JSON.stringify({ requestId, optionId, cancelled: cancelled === true })
6966
- });
6967
- const body = await response.json().catch(() => null);
7052
+ async function respondToStructuredPermission(agentId, generation, requestId, optionId, cancelled) {
7053
+ if (!isCurrentStructuredSession(agentId, generation)) return;
7054
+ let response;
7055
+ let body;
7056
+ try {
7057
+ response = await fetch(farmingApiPath(`/agents/${encodeURIComponent(agentId)}/acp-permission`), {
7058
+ method: 'POST',
7059
+ headers: { 'Content-Type': 'application/json' },
7060
+ body: JSON.stringify({ requestId, optionId, cancelled: cancelled === true })
7061
+ });
7062
+ body = await response.json().catch(() => null);
7063
+ } catch (error) {
7064
+ if (isCurrentStructuredSession(agentId, generation)) showStructuredComposerError(error);
7065
+ return;
7066
+ }
7067
+ if (!isCurrentStructuredSession(agentId, generation)) return;
6968
7068
  if (!response.ok) {
6969
7069
  showStructuredComposerError(new Error(body && body.error ? body.error : `Permission response failed (${response.status})`));
6970
7070
  }
@@ -7204,13 +7304,13 @@ async function refreshCrtStructuredPreview(agent) {
7204
7304
  }
7205
7305
  }
7206
7306
 
7207
- function renderStructuredTranscript(transcript, force = false) {
7307
+ function renderStructuredTranscript(transcript, force = false, agentId = focusedAgentId) {
7208
7308
  const container = document.getElementById('terminal-output');
7209
- if (!container) return;
7309
+ if (!container || agentId !== focusedAgentId) return;
7210
7310
  const updatedAt = String(transcript && transcript.updatedAt || '');
7211
7311
  if (!force && updatedAt && updatedAt === structuredSessionRenderedAt) return;
7212
7312
  const nearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 80;
7213
- if (!nearBottom && focusedAgentId) saveCrtStructuredReadingAnchor(focusedAgentId);
7313
+ if (!nearBottom) saveCrtStructuredReadingAnchor(agentId);
7214
7314
  const turns = structuredTranscriptTurns(transcript);
7215
7315
 
7216
7316
  const markdownRenderer = typeof window !== 'undefined' ? window.FarmingCrtMarkdownRenderer : null;
@@ -7234,15 +7334,21 @@ function renderStructuredTranscript(transcript, force = false) {
7234
7334
  return;
7235
7335
  }
7236
7336
  structuredSessionRenderedAt = updatedAt;
7237
- if (!focusedAgentId || !restoreCrtStructuredReadingAnchor(focusedAgentId)) {
7337
+ if (!restoreCrtStructuredReadingAnchor(agentId)) {
7238
7338
  if (force || nearBottom) container.scrollTop = container.scrollHeight;
7239
7339
  }
7240
- container.onscroll = () => saveCrtStructuredReadingAnchor(focusedAgentId);
7340
+ container.onscroll = () => {
7341
+ if (focusedAgentId === agentId) saveCrtStructuredReadingAnchor(agentId);
7342
+ };
7241
7343
  requestAnimationFrame(updateStructuredTranscriptScrollState);
7242
7344
  }
7243
7345
 
7244
- async function refreshStructuredSession(agentId = focusedAgentId, force = false) {
7245
- if (!agentId || structuredSessionLoading) return;
7346
+ async function refreshStructuredSession(
7347
+ agentId = focusedAgentId,
7348
+ force = false,
7349
+ generation = structuredSessionGeneration
7350
+ ) {
7351
+ if (!isCurrentStructuredSession(agentId, generation) || structuredSessionLoading) return;
7246
7352
  const agent = state && state.agents.find((candidate) => candidate.id === agentId);
7247
7353
  if (!agent || !isStructuredRuntimeAgent(agent)) return;
7248
7354
  structuredSessionLoading = true;
@@ -7250,12 +7356,14 @@ async function refreshStructuredSession(agentId = focusedAgentId, force = false)
7250
7356
  const endpoint = structuredTranscriptEndpoint(agent);
7251
7357
  const response = await fetch(farmingApiPath(`/agents/${encodeURIComponent(agentId)}/${endpoint}?maxTurns=80`));
7252
7358
  const body = await response.json().catch(() => null);
7359
+ if (!isCurrentStructuredSession(agentId, generation)) return;
7253
7360
  if (!response.ok || !body || !body.transcript) {
7254
7361
  throw new Error(body && body.error ? body.error : `Failed to read conversation (${response.status})`);
7255
7362
  }
7256
- renderStructuredTranscript(body.transcript, force);
7363
+ renderStructuredTranscript(body.transcript, force, agentId);
7257
7364
  updateStructuredComposerState(agent);
7258
7365
  } catch (error) {
7366
+ if (!isCurrentStructuredSession(agentId, generation)) return;
7259
7367
  const container = document.getElementById('terminal-output');
7260
7368
  if (container && !container.querySelector('.crt-structured-transcript')) {
7261
7369
  const message = document.createElement('div');
@@ -7265,23 +7373,28 @@ async function refreshStructuredSession(agentId = focusedAgentId, force = false)
7265
7373
  }
7266
7374
  updateStructuredComposerState(agent);
7267
7375
  } finally {
7268
- structuredSessionLoading = false;
7376
+ if (isCurrentStructuredSession(agentId, generation)) {
7377
+ structuredSessionLoading = false;
7378
+ }
7269
7379
  }
7270
7380
  }
7271
7381
 
7272
- function stopStructuredSessionPolling() {
7382
+ function stopStructuredSessionPolling(revokeOwnership = true) {
7273
7383
  if (structuredSessionPoller) {
7274
7384
  clearInterval(structuredSessionPoller);
7275
7385
  structuredSessionPoller = null;
7276
7386
  }
7277
7387
  structuredSessionLoading = false;
7278
7388
  structuredSessionRenderedAt = '';
7389
+ if (revokeOwnership) structuredSessionGeneration += 1;
7279
7390
  }
7280
7391
 
7281
- function startStructuredSessionPolling(agentId) {
7282
- stopStructuredSessionPolling();
7392
+ function startStructuredSessionPolling(agentId, generation) {
7393
+ stopStructuredSessionPolling(false);
7283
7394
  structuredSessionPoller = setInterval(() => {
7284
- if (focusedAgentId === agentId) void refreshStructuredSession(agentId);
7395
+ if (isCurrentStructuredSession(agentId, generation)) {
7396
+ void refreshStructuredSession(agentId, false, generation);
7397
+ }
7285
7398
  }, 1000);
7286
7399
  }
7287
7400
 
@@ -7452,9 +7565,11 @@ function setupStructuredSessionComposer() {
7452
7565
  const agent = state && state.agents.find((candidate) => candidate.id === focusedAgentId);
7453
7566
  const action = structuredComposerAction(agent, input.value);
7454
7567
  if (!agent || action === 'disabled') return;
7568
+ const agentId = agent.id;
7569
+ const generation = structuredSessionGeneration;
7455
7570
  if (action === 'interrupt') {
7456
7571
  structuredComposerRestoreFocusAfterInterrupt = true;
7457
- const interrupted = getSessionClient()?.interruptAgent(focusedAgentId);
7572
+ const interrupted = getSessionClient()?.interruptAgent(agentId);
7458
7573
  if (!interrupted) {
7459
7574
  structuredComposerRestoreFocusAfterInterrupt = false;
7460
7575
  showStructuredComposerError(new Error('Connection unavailable'));
@@ -7468,7 +7583,7 @@ function setupStructuredSessionComposer() {
7468
7583
  if (!message && promptAttachments.length === 0) return;
7469
7584
  const statusNode = document.getElementById('crt-structured-composer-status');
7470
7585
  const completeSubmission = () => {
7471
- addStructuredComposerHistory(focusedAgentId, draft);
7586
+ addStructuredComposerHistory(agentId, draft);
7472
7587
  input.value = '';
7473
7588
  structuredComposerAttachments = [];
7474
7589
  renderStructuredComposerAttachments();
@@ -7478,11 +7593,11 @@ function setupStructuredSessionComposer() {
7478
7593
  statusNode.classList.remove('error');
7479
7594
  }
7480
7595
  updateStructuredComposerState(agent);
7481
- setTimeout(() => void refreshStructuredSession(focusedAgentId, true), 160);
7596
+ setTimeout(() => void refreshStructuredSession(agentId, true, generation), 160);
7482
7597
  };
7483
7598
  const sent = action === 'send' && structuredRuntimeStatus(agent) !== 'idle'
7484
- ? (queueStructuredComposerFollowUp(focusedAgentId, message, promptAttachments), true)
7485
- : getSessionClient()?.sendComposerMessage(focusedAgentId, message, promptAttachments);
7599
+ ? (queueStructuredComposerFollowUp(agentId, message, promptAttachments), true)
7600
+ : getSessionClient()?.sendComposerMessage(agentId, message, promptAttachments);
7486
7601
  if (!sent) {
7487
7602
  if (statusNode) {
7488
7603
  statusNode.textContent = 'Connection unavailable';
@@ -7498,14 +7613,17 @@ async function openStructuredSession(agent, sessionToken) {
7498
7613
  const runtime = getSessionRuntime();
7499
7614
  const container = document.getElementById('terminal-output');
7500
7615
  if (!container) return;
7616
+ const generation = ++structuredSessionGeneration;
7501
7617
  container.classList.add('crt-structured-session');
7502
7618
  setStructuredComposerActive(true);
7503
7619
  setupStructuredSessionComposer();
7504
- updateStructuredComposerState(agent);
7505
- await refreshStructuredSessionControls(agent.id, true);
7506
- await refreshStructuredSession(agent.id, true);
7620
+ updateStructuredComposerState(agent, { refreshControls: false });
7621
+ await refreshStructuredSessionControls(agent.id, true, generation);
7622
+ if (!isCurrentStructuredSession(agent.id, generation)) return;
7623
+ await refreshStructuredSession(agent.id, true, generation);
7624
+ if (!isCurrentStructuredSession(agent.id, generation)) return;
7507
7625
  if (runtime && !runtime.isCurrentSession(agent.id, sessionToken)) return;
7508
- startStructuredSessionPolling(agent.id);
7626
+ startStructuredSessionPolling(agent.id, generation);
7509
7627
  const input = document.getElementById('crt-structured-input');
7510
7628
  if (input) {
7511
7629
  resizeStructuredComposerInput(input);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "farming-code",
3
- "version": "2.2.20",
4
- "description": "Remote browser workspace for supervising AI coding agents",
3
+ "version": "2.2.22",
4
+ "description": "Farming Code is a self-hosted browser workspace for running and supervising AI coding agents",
5
5
  "main": "backend/server.js",
6
6
  "bin": {
7
7
  "farming": "bin/farming"
@@ -80,10 +80,16 @@
80
80
  },
81
81
  "keywords": [
82
82
  "ai-agent",
83
+ "ai-agent-workspace",
84
+ "browser-terminal",
85
+ "claude-code",
83
86
  "coding-agent",
84
87
  "codex",
85
- "claude",
88
+ "codex-cli",
89
+ "developer-tools",
90
+ "farming-code",
86
91
  "remote-workspace",
92
+ "self-hosted",
87
93
  "terminal"
88
94
  ],
89
95
  "author": "",