farming-code 2.2.11 → 2.2.13

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 (100) hide show
  1. package/README.md +9 -8
  2. package/README.zh_cn.md +9 -8
  3. package/backend/agent-manager.js +108 -5
  4. package/backend/codex-session-archive.js +13 -4
  5. package/backend/config-manager.js +5 -0
  6. package/backend/executable-discovery.js +2 -0
  7. package/backend/server.js +72 -0
  8. package/backend/usage-monitor.js +97 -15
  9. package/backend/workspace-directory.js +80 -0
  10. package/dist/assets/App-xASd_Y0F.js +206 -0
  11. package/dist/assets/{FileEditorMarkdownPreview--Cri80nQ.js → FileEditorMarkdownPreview-UlVd0O4D.js} +1 -1
  12. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  13. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  14. package/dist/assets/{ProjectFilesSection-Cz1wG4NT.js → ProjectFilesSection-DE7Nb9Jk.js} +3 -3
  15. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  16. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  17. package/dist/assets/{index-miO2r9IZ.js → index-CYZKdoqi.js} +2 -2
  18. package/dist/assets/main-Bqtb5kuz.css +1 -0
  19. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  20. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  21. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  22. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  23. package/dist/crt-markdown-assets/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
  24. package/dist/crt-markdown-assets/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
  25. package/dist/crt-markdown-assets/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
  26. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
  27. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
  28. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
  29. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
  30. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
  31. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
  32. package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
  33. package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
  34. package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
  35. package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
  36. package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
  37. package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
  38. package/dist/crt-markdown-assets/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
  39. package/dist/crt-markdown-assets/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
  40. package/dist/crt-markdown-assets/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
  41. package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
  42. package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
  43. package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
  44. package/dist/crt-markdown-assets/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
  45. package/dist/crt-markdown-assets/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
  46. package/dist/crt-markdown-assets/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
  47. package/dist/crt-markdown-assets/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
  48. package/dist/crt-markdown-assets/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
  49. package/dist/crt-markdown-assets/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
  50. package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
  51. package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
  52. package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
  53. package/dist/crt-markdown-assets/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
  54. package/dist/crt-markdown-assets/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
  55. package/dist/crt-markdown-assets/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
  56. package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
  57. package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
  58. package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
  59. package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
  60. package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
  61. package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
  62. package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
  63. package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
  64. package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
  65. package/dist/crt-markdown-assets/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
  66. package/dist/crt-markdown-assets/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
  67. package/dist/crt-markdown-assets/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
  68. package/dist/crt-markdown-assets/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
  69. package/dist/crt-markdown-assets/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
  70. package/dist/crt-markdown-assets/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
  71. package/dist/crt-markdown-assets/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
  72. package/dist/crt-markdown-assets/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
  73. package/dist/crt-markdown-assets/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
  74. package/dist/crt-markdown-assets/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
  75. package/dist/crt-markdown-assets/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
  76. package/dist/crt-markdown-assets/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
  77. package/dist/crt-markdown-assets/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
  78. package/dist/crt-markdown-assets/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
  79. package/dist/crt-markdown-assets/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
  80. package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
  81. package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
  82. package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
  83. package/dist/crt-markdown-renderer.css +1 -0
  84. package/dist/crt-markdown-renderer.js +361 -0
  85. package/dist/crt-mermaid-renderer.js +3595 -0
  86. package/dist/farming-2/site.webmanifest +2 -0
  87. package/dist/index.html +1 -1
  88. package/frontend/skins/crt/app.js +55 -32
  89. package/frontend/skins/crt/index.html +47 -31
  90. package/frontend/skins/crt/styles/markdown.css +251 -0
  91. package/frontend/skins/crt/styles/monochrome-green.css +14 -3
  92. package/package.json +5 -2
  93. package/dist/assets/App-CHYpgECZ.js +0 -208
  94. package/dist/assets/FileEditorPane-DGJlEKdf.js +0 -2
  95. package/dist/assets/IconGlyphs-Cc14sepw.js +0 -1
  96. package/dist/assets/ReviewPage-LonRc6Fz.js +0 -3
  97. package/dist/assets/code-dark-B1pAtO_P.css +0 -1
  98. package/dist/assets/main-DNe0jPw-.css +0 -1
  99. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +0 -1
  100. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +0 -4
package/README.md CHANGED
@@ -23,7 +23,7 @@ npm install --global farming-code@latest && farming daemon
23
23
 
24
24
  Open the authenticated URL printed by the command, choose **New Agent**, select an Agent and workspace, and start in Chat or Terminal.
25
25
 
26
- ![Farming Code workspace](./docs/products/code/assets/01-code-workspace.png)
26
+ ![Farming CRT billing control room](./docs/products/crt/assets/crt-billing-control-room.png)
27
27
 
28
28
  ## Two Interfaces, One Runtime
29
29
 
@@ -51,6 +51,13 @@ A keyboard-first control room for watching many agents, opening structured Chat
51
51
 
52
52
  Switching interfaces does not restart or duplicate an Agent. If Farming Code cannot start or render, its bounded diagnostic view leaves the live CRT surface available rather than hiding the running sessions.
53
53
 
54
+ To switch without editing the URL:
55
+
56
+ - **Farming Code → CRT:** click the gear at the bottom-left, open **Interface**, then choose **Farming CRT**.
57
+ - **Farming CRT → Code:** press `S` (or choose **[S] SETTINGS**), then choose **Farming Code** under **UI Theme**.
58
+
59
+ Farming carries the focused Agent into the other interface when possible. Both views keep using the same live Agent and provider session; this is an interface switch, not a restart.
60
+
54
61
  The complete current capability map and screenshot tour are in the [Farming 2 product overview](./docs/products/README.md). See the focused [Farming Code guide](./docs/products/code/README.md) and [Farming CRT guide](./docs/products/crt/README.md) for the full workflows.
55
62
 
56
63
  ## Farming Net: One Portal For Deployments
@@ -74,8 +81,6 @@ The portal keeps its token, signing identity, and private `instances.json` regis
74
81
 
75
82
  ![Farming Code project files and blame](./docs/products/code/assets/04-files-editor-blame.png)
76
83
 
77
- ![Separate Review entry points for tracked and untracked workspace changes](./docs/products/code/assets/10-review-workflow.png)
78
-
79
84
  ## Supported Agent Paths
80
85
 
81
86
  Farming discovers installed CLIs on the host. The richer structured runtime currently applies to providers with ACP support; other detected coding agents remain first-class terminal sessions.
@@ -119,10 +124,6 @@ The first authenticated start stores a readable random token in `~/.farming/.ses
119
124
 
120
125
  Desktop keeps the project, conversation, files, and review close together. Mobile focuses one conversation, terminal, or file at a time and moves navigation into a drawer, making it useful for checking progress and sending a short intervention.
121
126
 
122
- <p align="center">
123
- <img src="./docs/products/code/assets/05-mobile-agent-chat.jpg" alt="Farming Code on mobile" width="320">
124
- </p>
125
-
126
127
  Farming CRT is currently a desktop interface. Use Farming Code from a phone; CRT mobile concepts are not part of the supported product yet.
127
128
 
128
129
  ## Installation And Updates
@@ -157,7 +158,7 @@ Development host
157
158
 
158
159
  The backend owns lifecycle, authentication, session routing, workspace boundaries, history, and configuration. Interactive terminal sessions use a separate native PTY host by default, allowing browser and server reconnection without replacing the live process. The browser terminal renderer defaults to xterm.js WebGL as its single supported product path; the Ghostty web adapter remains an explicit debug path, not a runtime fallback.
159
160
 
160
- Runtime settings live in `~/.farming/settings.json`. Farming session metadata, the project membership index, archived runs, theme settings, update state, logs, and the startup token use separate files under `~/.farming/`. External provider histories remain read-only integrations.
161
+ Runtime settings live in `~/.farming/settings.json`. Farming session metadata, the project membership index, archived runs, theme settings, update state, logs, and the startup token use separate files under `~/.farming/`. External provider histories remain read-only integrations except for explicit Codex archive and unarchive lifecycle actions.
161
162
 
162
163
  ## Security
163
164
 
package/README.zh_cn.md CHANGED
@@ -23,7 +23,7 @@ npm install --global farming-code@latest && farming daemon
23
23
 
24
24
  打开命令输出的带鉴权 URL,选择 **New Agent**、Agent 类型和 Workspace,然后从 Chat 或 Terminal 开始工作。
25
25
 
26
- ![Farming Code 工作台](./docs/products/code/assets/01-code-workspace.png)
26
+ ![Farming CRT 用量控制室](./docs/products/crt/assets/crt-billing-control-room.png)
27
27
 
28
28
  ## 两套界面,同一套运行时
29
29
 
@@ -51,6 +51,13 @@ Farming 2 在同一批 Agent 和 Session 上提供两套完整的浏览器界面
51
51
 
52
52
  切换界面不会重启或复制 Agent。如果 Farming Code 启动或渲染失败,有限范围的诊断层仍会保留后面的实时 CRT 界面,而不是把正在运行的 Session 一起遮掉。
53
53
 
54
+ 不修改地址也可以直接切换:
55
+
56
+ - **Farming Code → CRT:**点击左下角齿轮,打开**界面**,再选择 **Farming CRT**。
57
+ - **Farming CRT → Code:**按 `S`(或点击 **[S] SETTINGS**),在 **UI Theme** 中选择 **Farming Code**。
58
+
59
+ 条件允许时,Farming 会把当前聚焦的 Agent 一起带到另一套界面。两边继续使用同一个实时 Agent 和 Provider Session;这里只切换界面,不会重启会话。
60
+
54
61
  完整能力矩阵和截图导览见 [Farming 2 产品总览](./docs/products/README.zh_cn.md)。两套界面的完整流程分别见 [Farming Code 指南](./docs/products/code/README.zh_cn.md) 和 [Farming CRT 指南](./docs/products/crt/README.zh_cn.md)。
55
62
 
56
63
  ## Farming Net:部署门户
@@ -74,8 +81,6 @@ FARMING_NET_PORT=6693 FARMING_NET_BASE_PATH=/farming-net npm run start:net
74
81
 
75
82
  ![Farming Code 项目文件与 Blame](./docs/products/code/assets/04-files-editor-blame.png)
76
83
 
77
- ![Files 中分开的已跟踪与未跟踪 Review 入口](./docs/products/code/assets/10-review-workflow.png)
78
-
79
84
  ## 支持的 Agent 路径
80
85
 
81
86
  Farming 会发现开发机上已经安装的 CLI。有 ACP 支持的 Provider 使用更完整的结构化运行时,其他检测到的 Coding Agent 仍然可以作为一等 Terminal Session 使用。
@@ -119,10 +124,6 @@ farming stop
119
124
 
120
125
  桌面端把项目、对话、文件和 Review 放在彼此靠近的位置。移动端一次聚焦一段对话、一个终端或一个文件,并把导航移入抽屉,更适合查看进度和发送短介入。
121
126
 
122
- <p align="center">
123
- <img src="./docs/products/code/assets/05-mobile-agent-chat.jpg" alt="Farming Code 手机界面" width="320">
124
- </p>
125
-
126
127
  Farming CRT 当前只作为桌面界面使用。手机请使用 Farming Code;CRT 手机方案目前仍是概念设计,不属于已支持的产品能力。
127
128
 
128
129
  ## 安装与更新
@@ -157,7 +158,7 @@ Development host
157
158
 
158
159
  后端负责生命周期、鉴权、Session 路由、Workspace 边界、History 和配置。交互式 Terminal 默认由独立的原生 PTY Host 持有,因此浏览器和 Server 可以重新连接,而不需要替换实际进程。xterm.js WebGL 是唯一受支持的产品 Terminal Renderer;Ghostty Web Adapter 只保留为显式调试路径,不作为运行时 Fallback。
159
160
 
160
- 运行时设置存放在 `~/.farming/settings.json`。Farming Session 元数据、项目成员索引、归档运行、主题设置、更新状态、日志和启动 Token 使用 `~/.farming/` 下彼此独立的文件。外部 Provider History 仍然只读。
161
+ 运行时设置存放在 `~/.farming/settings.json`。Farming Session 元数据、项目成员索引、归档运行、主题设置、更新状态、日志和启动 Token 使用 `~/.farming/` 下彼此独立的文件。外部 Provider History 保持只读,但明确的 Codex archive / unarchive 生命周期操作除外。
161
162
 
162
163
  ## 安全
163
164
 
@@ -11,7 +11,7 @@ const {
11
11
  buildAgentSessionResumeCommand,
12
12
  findAgentSession,
13
13
  } = require('./agent-session-history');
14
- const { unarchiveCodexSession } = require('./codex-session-archive');
14
+ const { archiveCodexSession, unarchiveCodexSession } = require('./codex-session-archive');
15
15
  const { buildAgentProviderSessionPlan, sessionFromExactResumeSource } = require('./agent-provider-session');
16
16
  const { resolveAgentExecutable, resolveCompatibleCodexExecutable } = require('./executable-discovery');
17
17
  const { ensureMainAgentSkillFiles, renderMainAgentBootstrap } = require('./main-agent-skills');
@@ -62,6 +62,7 @@ const {
62
62
  const { inspectGitWorktree } = require('./git-worktree-info');
63
63
  const { deserializeTerminalState } = require('./terminal-state-serialization');
64
64
  const { compareNativePtyRuntimeEpochs } = require('./native-pty-controller-generation');
65
+ const { canonicalWorkspacePath } = require('./workspace-root-registry');
65
66
 
66
67
  const SESSION_OUTPUT_LIMIT = 10000;
67
68
  const AGENT_USAGE_RATE_WINDOW_MS = 5 * 60 * 1000;
@@ -581,6 +582,7 @@ class AgentManager extends EventEmitter {
581
582
  }
582
583
  : {}),
583
584
  });
585
+ this.archiveCodexSession = options.archiveCodexSession || archiveCodexSession;
584
586
  this.unarchiveCodexSession = options.unarchiveCodexSession || unarchiveCodexSession;
585
587
  this.heartbeatInterval = null;
586
588
  this.disposed = false;
@@ -2402,6 +2404,10 @@ class AgentManager extends EventEmitter {
2402
2404
  if (wantsMain) {
2403
2405
  const existingMainStart = this.findActiveMainAgentStart();
2404
2406
  if (existingMainStart) {
2407
+ if (this.mainAgentId !== existingMainStart.id) {
2408
+ this.mainAgentId = existingMainStart.id;
2409
+ this.emit('update');
2410
+ }
2405
2411
  console.log('Main Agent already starting or running:', existingMainStart.id);
2406
2412
  if (callback) callback(existingMainStart.id);
2407
2413
  return existingMainStart.id;
@@ -4201,23 +4207,26 @@ class AgentManager extends EventEmitter {
4201
4207
  : this.markAgentReadCursor(agentId);
4202
4208
  }
4203
4209
 
4204
- async createForkWorktree(workspace) {
4210
+ async resolveGitWorktreeSourceRoot(workspace) {
4205
4211
  const sourceWorkspace = this.expandWorkspacePath(workspace);
4206
4212
  if (!sourceWorkspace) {
4207
4213
  throw new Error('Source workspace is empty');
4208
4214
  }
4209
4215
 
4210
- let root;
4211
4216
  try {
4212
4217
  const { stdout } = await execFileAsync('git', ['-C', sourceWorkspace, 'rev-parse', '--show-toplevel'], {
4213
4218
  timeout: 15000,
4214
4219
  maxBuffer: 1024 * 1024,
4215
4220
  });
4216
- root = stdout.trim();
4221
+ return stdout.trim();
4217
4222
  } catch (error) {
4218
4223
  const message = error && error.stderr ? String(error.stderr).trim() : '';
4219
4224
  throw new Error(message || 'Source workspace is not inside a git repository', { cause: error });
4220
4225
  }
4226
+ }
4227
+
4228
+ async createForkWorktree(workspace) {
4229
+ const root = await this.resolveGitWorktreeSourceRoot(workspace);
4221
4230
 
4222
4231
  const parentDir = path.dirname(root);
4223
4232
  const baseName = path.basename(root);
@@ -4242,6 +4251,70 @@ class AgentManager extends EventEmitter {
4242
4251
  return target;
4243
4252
  }
4244
4253
 
4254
+ async createPermanentWorktree(workspace) {
4255
+ const root = await this.resolveGitWorktreeSourceRoot(workspace);
4256
+ const parentDir = path.dirname(root);
4257
+ const baseName = path.basename(root);
4258
+ const slug = timestampSlug();
4259
+ let suffix = 1;
4260
+
4261
+ while (suffix < 1000) {
4262
+ const suffixText = suffix === 1 ? '' : `-${suffix}`;
4263
+ const target = path.join(parentDir, `${baseName}-farming-worktree-${slug}${suffixText}`);
4264
+ const branch = `farming/worktree-${slug}${suffixText}`;
4265
+ let branchExists = false;
4266
+ try {
4267
+ await execFileAsync('git', ['-C', root, 'show-ref', '--verify', '--quiet', `refs/heads/${branch}`], {
4268
+ timeout: 15000,
4269
+ maxBuffer: 1024 * 1024,
4270
+ });
4271
+ branchExists = true;
4272
+ } catch (error) {
4273
+ if (error?.code !== 1) {
4274
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
4275
+ throw new Error(message || 'Failed to inspect git branches', { cause: error });
4276
+ }
4277
+ }
4278
+
4279
+ if (!fs.existsSync(target) && !branchExists) {
4280
+ try {
4281
+ await execFileAsync('git', ['-C', root, 'worktree', 'add', '-b', branch, target, 'HEAD'], {
4282
+ timeout: 60000,
4283
+ maxBuffer: 1024 * 1024 * 4,
4284
+ });
4285
+ } catch (error) {
4286
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
4287
+ throw new Error(message || 'Failed to create permanent git worktree', { cause: error });
4288
+ }
4289
+ return { workspace: target, branch, sourceWorkspace: root };
4290
+ }
4291
+ suffix += 1;
4292
+ }
4293
+
4294
+ throw new Error('Unable to allocate a permanent worktree name');
4295
+ }
4296
+
4297
+ async rollbackPermanentWorktree(created) {
4298
+ if (!created?.workspace || !created?.sourceWorkspace) return;
4299
+ try {
4300
+ await execFileAsync('git', ['-C', created.sourceWorkspace, 'worktree', 'remove', '--force', created.workspace], {
4301
+ timeout: 60000,
4302
+ maxBuffer: 1024 * 1024 * 4,
4303
+ });
4304
+ } catch {
4305
+ // Rollback is best-effort so the original create/persist failure remains visible.
4306
+ }
4307
+ if (!created.branch) return;
4308
+ try {
4309
+ await execFileAsync('git', ['-C', created.sourceWorkspace, 'branch', '-D', created.branch], {
4310
+ timeout: 30000,
4311
+ maxBuffer: 1024 * 1024 * 4,
4312
+ });
4313
+ } catch {
4314
+ // The worktree removal may already have pruned the newly created branch.
4315
+ }
4316
+ }
4317
+
4245
4318
  async inspectForkWorktreeProject(workspace) {
4246
4319
  const expanded = this.expandWorkspacePath(workspace);
4247
4320
  const resolvedWorkspace = expanded ? path.resolve(expanded) : '';
@@ -4537,8 +4610,38 @@ class AgentManager extends EventEmitter {
4537
4610
  reason: 'manual-archive',
4538
4611
  recordHistory: !isEphemeralShellAgent(agent),
4539
4612
  });
4613
+ this.scheduleCodexSessionArchive(agent);
4540
4614
  return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
4541
4615
  }
4616
+
4617
+ scheduleCodexSessionArchive(agent) {
4618
+ if (
4619
+ !agent
4620
+ || agent.providerSessionProvider !== 'codex'
4621
+ || !agent.providerSessionId
4622
+ || agent.providerSessionTemporary === true
4623
+ ) {
4624
+ return;
4625
+ }
4626
+
4627
+ const sessionId = agent.providerSessionId;
4628
+ const session = {
4629
+ cliVersion: agent.cliVersion || '',
4630
+ cwd: agent.cwd || '',
4631
+ workspace: agent.projectWorkspace || '',
4632
+ providerHomePath: agent.providerHomePath || '',
4633
+ };
4634
+ void Promise.resolve()
4635
+ .then(() => this.archiveCodexSession(sessionId, session))
4636
+ .then(result => {
4637
+ if (result?.error) {
4638
+ console.error(`Failed to archive Codex session ${sessionId}: ${result.error}`);
4639
+ }
4640
+ })
4641
+ .catch(error => {
4642
+ console.error(`Failed to archive Codex session ${sessionId}:`, error);
4643
+ });
4644
+ }
4542
4645
 
4543
4646
  async killAgent(agentId, options = {}) {
4544
4647
  const agent = this.agents.get(agentId);
@@ -4884,7 +4987,7 @@ class AgentManager extends EventEmitter {
4884
4987
  command: agent.command,
4885
4988
  engineName: agent.engineName || '',
4886
4989
  cwd: agent.cwd,
4887
- projectWorkspace: agent.projectWorkspace || '',
4990
+ projectWorkspace: canonicalWorkspacePath(agent.projectWorkspace || ''),
4888
4991
  gitWorktree: publicAgentGitWorktree(agent),
4889
4992
  output: agent.output.slice(-2000),
4890
4993
  previewText: agent.previewText || '',
@@ -5,7 +5,7 @@ const { resolveCompatibleCodexExecutable } = require('./executable-discovery');
5
5
 
6
6
  const execFileAsync = promisify(execFile);
7
7
 
8
- async function unarchiveCodexSession(sessionId, session = {}, options = {}) {
8
+ async function runCodexSessionArchiveCommand(action, sessionId, session = {}, options = {}) {
9
9
  const resolveExecutable = options.resolveCompatibleCodexExecutable || resolveCompatibleCodexExecutable;
10
10
  const runExecFile = options.execFileAsync || execFileAsync;
11
11
  const processEnv = options.processEnv || process.env;
@@ -18,7 +18,7 @@ async function unarchiveCodexSession(sessionId, session = {}, options = {}) {
18
18
  }
19
19
 
20
20
  try {
21
- await runExecFile(codexResolution.path || 'codex', ['unarchive', sessionId], {
21
+ await runExecFile(codexResolution.path || 'codex', [action, sessionId], {
22
22
  cwd: session.cwd || session.workspace || os.homedir(),
23
23
  env: session.providerHomePath
24
24
  ? { ...processEnv, CODEX_HOME: session.providerHomePath }
@@ -26,13 +26,13 @@ async function unarchiveCodexSession(sessionId, session = {}, options = {}) {
26
26
  timeout: 30_000,
27
27
  maxBuffer: 1024 * 1024,
28
28
  });
29
- return { unarchived: true };
29
+ return { [action === 'archive' ? 'archived' : 'unarchived']: true };
30
30
  } catch (error) {
31
31
  const message = [
32
32
  error && error.stdout ? String(error.stdout).trim() : '',
33
33
  error && error.stderr ? String(error.stderr).trim() : '',
34
34
  error && error.message ? String(error.message).trim() : '',
35
- ].filter(Boolean).join('\n') || 'failed to unarchive Codex session';
35
+ ].filter(Boolean).join('\n') || `failed to ${action} Codex session`;
36
36
  return {
37
37
  error: message,
38
38
  status: 409,
@@ -40,6 +40,15 @@ async function unarchiveCodexSession(sessionId, session = {}, options = {}) {
40
40
  }
41
41
  }
42
42
 
43
+ async function archiveCodexSession(sessionId, session = {}, options = {}) {
44
+ return runCodexSessionArchiveCommand('archive', sessionId, session, options);
45
+ }
46
+
47
+ async function unarchiveCodexSession(sessionId, session = {}, options = {}) {
48
+ return runCodexSessionArchiveCommand('unarchive', sessionId, session, options);
49
+ }
50
+
43
51
  module.exports = {
52
+ archiveCodexSession,
44
53
  unarchiveCodexSession,
45
54
  };
@@ -73,6 +73,7 @@ const PERSISTED_SETTING_KEYS = new Set([
73
73
  'lastMainWorkspace',
74
74
  'workspaceHistory',
75
75
  'projectWorkspaces',
76
+ 'pinnedProjectWorkspaces',
76
77
  'projectNames',
77
78
  'theme',
78
79
  'appearance',
@@ -287,6 +288,7 @@ class ConfigManager {
287
288
  lastMainWorkspace: this.farmingDir,
288
289
  workspaceHistory: [],
289
290
  projectWorkspaces: [],
291
+ pinnedProjectWorkspaces: [],
290
292
  projectNames: {},
291
293
  theme: 'terminal',
292
294
  appearance: 'light',
@@ -322,6 +324,7 @@ class ConfigManager {
322
324
  lastMainWorkspace: this.farmingDir,
323
325
  workspaceHistory: [],
324
326
  projectWorkspaces: [],
327
+ pinnedProjectWorkspaces: [],
325
328
  projectNames: {},
326
329
  theme: 'terminal',
327
330
  appearance: 'light',
@@ -371,6 +374,7 @@ class ConfigManager {
371
374
  this.settings.lastMainWorkspace = this.normalizeMainWorkspace(this.settings.lastMainWorkspace, this.farmingDir);
372
375
  this.settings.workspaceHistory = this.normalizeWorkspaceHistory(this.settings.workspaceHistory);
373
376
  this.settings.projectWorkspaces = this.normalizeProjectWorkspaces(this.settings.projectWorkspaces);
377
+ this.settings.pinnedProjectWorkspaces = this.normalizeProjectWorkspaces(this.settings.pinnedProjectWorkspaces);
374
378
  this.settings.projectNames = this.normalizeProjectNames(this.settings.projectNames);
375
379
  this.settings.agentHomes = this.normalizeAgentHomes(this.settings.agentHomes);
376
380
  if (this.settings.updateUrl === LEGACY_DEFAULT_UPDATE_URL || this.settings.updateUrl === API_DEFAULT_UPDATE_URL) {
@@ -813,6 +817,7 @@ class ConfigManager {
813
817
  this.settings.lastMainWorkspace = this.normalizeMainWorkspace(this.settings.lastMainWorkspace, previousMainWorkspace);
814
818
  this.settings.workspaceHistory = this.normalizeWorkspaceHistory(this.settings.workspaceHistory);
815
819
  this.settings.projectWorkspaces = this.normalizeProjectWorkspaces(this.settings.projectWorkspaces);
820
+ this.settings.pinnedProjectWorkspaces = this.normalizeProjectWorkspaces(this.settings.pinnedProjectWorkspaces);
816
821
  this.settings.projectNames = this.normalizeProjectNames(this.settings.projectNames);
817
822
  this.settings.agentHomes = this.normalizeAgentHomes(this.settings.agentHomes);
818
823
  this.settings.updateUrl = this.normalizeUpdateUrl(this.settings.updateUrl);
@@ -4,6 +4,7 @@ const { execFileSync } = require('child_process');
4
4
  const { getUserLaunchAgents } = require('./cli-agents');
5
5
 
6
6
  const DEFAULT_CODEX_APP_BIN = '/Applications/Codex.app/Contents/Resources/codex';
7
+ const DEFAULT_CHATGPT_APP_CODEX_BIN = '/Applications/ChatGPT.app/Contents/Resources/codex';
7
8
  const executableVersionCache = new Map();
8
9
 
9
10
  function getPathDirectories(pathEnv = process.env.PATH || '') {
@@ -90,6 +91,7 @@ function getPreferredExecutableCandidates(agentName, pathEnv = process.env.PATH
90
91
  return [
91
92
  process.env.FARMING_CODEX_BIN || '',
92
93
  DEFAULT_CODEX_APP_BIN,
94
+ DEFAULT_CHATGPT_APP_CODEX_BIN,
93
95
  ...pathCandidates,
94
96
  ].filter(Boolean);
95
97
  }
package/backend/server.js CHANGED
@@ -5,6 +5,8 @@ const path = require('path');
5
5
  const fs = require('fs');
6
6
  const os = require('os');
7
7
  const crypto = require('crypto');
8
+ const { execFile } = require('child_process');
9
+ const { promisify } = require('util');
8
10
  const { URLSearchParams, pathToFileURL } = require('url');
9
11
  const AgentManager = require('./agent-manager');
10
12
  const { runtimeKind } = require('./agent-runtime-binding');
@@ -85,6 +87,7 @@ const WS_PATH = routePath(BASE_PATH, '/ws');
85
87
  const encodeCookieToken = TokenAuth.encodeCookieToken;
86
88
  const MAX_CODEX_TRANSCRIPT_TURNS = 1000;
87
89
  const INTERACTIVE_REFRESH_CACHE_MAX_AGE_MS = 3_000;
90
+ const execFileAsync = promisify(execFile);
88
91
 
89
92
  const app = express();
90
93
  const server = http.createServer(app);
@@ -491,6 +494,16 @@ app.get(crtEntryPath, (req, res) => {
491
494
  res.redirect(308, `${crtEntryPath}/${requestUrl.search}`);
492
495
  });
493
496
  app.use(`${crtEntryPath}/shared`, express.static(frontendDir, { index: false }));
497
+ app.get(`${crtEntryPath}/crt-markdown-renderer.js`, (_req, res) => {
498
+ res.sendFile(path.join(distDir, 'crt-markdown-renderer.js'));
499
+ });
500
+ app.get(`${crtEntryPath}/crt-markdown-renderer.css`, (_req, res) => {
501
+ res.sendFile(path.join(distDir, 'crt-markdown-renderer.css'));
502
+ });
503
+ app.use(`${crtEntryPath}/crt-markdown-assets`, express.static(path.join(distDir, 'crt-markdown-assets'), { index: false }));
504
+ app.get(`${crtEntryPath}/crt-mermaid-renderer.js`, (_req, res) => {
505
+ res.sendFile(path.join(distDir, 'crt-mermaid-renderer.js'));
506
+ });
494
507
  app.use(`${crtEntryPath}/`, express.static(crtFrontendDir, { index: false }));
495
508
  app.use(BASE_PATH || '/', express.static(staticAppDir, { index: false }));
496
509
 
@@ -1427,6 +1440,65 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), asyn
1427
1440
  res.status(201).json(result);
1428
1441
  });
1429
1442
 
1443
+ function resolveProjectActionRoot(rootId) {
1444
+ const root = workspaceRootRegistry.resolve(rootId);
1445
+ if (!root || root.kind === 'global') {
1446
+ throw new WorkspaceFileError('project workspace is required', 400);
1447
+ }
1448
+ return root;
1449
+ }
1450
+
1451
+ app.post(routePath(BASE_PATH, '/api/projects/reveal'), express.json(), async (req, res) => {
1452
+ try {
1453
+ const root = resolveProjectActionRoot(req.body?.rootId);
1454
+ const command = process.platform === 'darwin'
1455
+ ? 'open'
1456
+ : process.platform === 'win32'
1457
+ ? 'explorer'
1458
+ : 'xdg-open';
1459
+ await execFileAsync(command, [root.canonicalPath], {
1460
+ timeout: 15000,
1461
+ maxBuffer: 1024 * 1024,
1462
+ });
1463
+ res.json({ revealed: true, workspace: root.canonicalPath });
1464
+ } catch (error) {
1465
+ const status = error instanceof WorkspaceFileError ? error.statusCode : 500;
1466
+ res.status(status).json({ error: error.message || 'Failed to reveal project workspace' });
1467
+ }
1468
+ });
1469
+
1470
+ app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
1471
+ let created = null;
1472
+ let previousProjects = null;
1473
+ try {
1474
+ const root = resolveProjectActionRoot(req.body?.rootId);
1475
+ created = await agentManager.createPermanentWorktree(root.canonicalPath);
1476
+ previousProjects = configManager.getSettings().projectWorkspaces || [];
1477
+ configManager.updateSettings({
1478
+ projectWorkspaces: [
1479
+ created.workspace,
1480
+ ...previousProjects.filter(workspace => workspace !== created.workspace),
1481
+ ],
1482
+ });
1483
+ res.status(201).json({
1484
+ workspace: created.workspace,
1485
+ branch: created.branch,
1486
+ projectWorkspaces: configManager.getSettings().projectWorkspaces,
1487
+ });
1488
+ } catch (error) {
1489
+ if (created) await agentManager.rollbackPermanentWorktree(created);
1490
+ if (previousProjects) {
1491
+ try {
1492
+ configManager.updateSettings({ projectWorkspaces: previousProjects });
1493
+ } catch {
1494
+ // Keep the original error visible; the failed settings write is already the root cause.
1495
+ }
1496
+ }
1497
+ const status = error instanceof WorkspaceFileError ? error.statusCode : 400;
1498
+ res.status(status).json({ error: error.message || 'Failed to create permanent worktree' });
1499
+ }
1500
+ });
1501
+
1430
1502
  app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
1431
1503
  const body = req.body || {};
1432
1504
  const workspace = typeof body.workspace === 'string' ? body.workspace : '';