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.
- package/README.md +9 -8
- package/README.zh_cn.md +9 -8
- package/backend/agent-manager.js +108 -5
- package/backend/codex-session-archive.js +13 -4
- package/backend/config-manager.js +5 -0
- package/backend/executable-discovery.js +2 -0
- package/backend/server.js +72 -0
- package/backend/usage-monitor.js +97 -15
- package/backend/workspace-directory.js +80 -0
- package/dist/assets/App-xASd_Y0F.js +206 -0
- package/dist/assets/{FileEditorMarkdownPreview--Cri80nQ.js → FileEditorMarkdownPreview-UlVd0O4D.js} +1 -1
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
- package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
- package/dist/assets/{ProjectFilesSection-Cz1wG4NT.js → ProjectFilesSection-DE7Nb9Jk.js} +3 -3
- package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
- package/dist/assets/code-dark-DPiuyeSp.css +1 -0
- package/dist/assets/{index-miO2r9IZ.js → index-CYZKdoqi.js} +2 -2
- package/dist/assets/main-Bqtb5kuz.css +1 -0
- package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
- package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
- package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
- package/dist/crt-markdown-assets/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
- package/dist/crt-markdown-renderer.css +1 -0
- package/dist/crt-markdown-renderer.js +361 -0
- package/dist/crt-mermaid-renderer.js +3595 -0
- package/dist/farming-2/site.webmanifest +2 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +55 -32
- package/frontend/skins/crt/index.html +47 -31
- package/frontend/skins/crt/styles/markdown.css +251 -0
- package/frontend/skins/crt/styles/monochrome-green.css +14 -3
- package/package.json +5 -2
- package/dist/assets/App-CHYpgECZ.js +0 -208
- package/dist/assets/FileEditorPane-DGJlEKdf.js +0 -2
- package/dist/assets/IconGlyphs-Cc14sepw.js +0 -1
- package/dist/assets/ReviewPage-LonRc6Fz.js +0 -3
- package/dist/assets/code-dark-B1pAtO_P.css +0 -1
- package/dist/assets/main-DNe0jPw-.css +0 -1
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +0 -1
- 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
|
-

|
|
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
|

|
|
76
83
|
|
|
77
|
-

|
|
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
|
-

|
|
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
|

|
|
76
83
|
|
|
77
|
-

|
|
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
|
|
package/backend/agent-manager.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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', [
|
|
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') ||
|
|
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 : '';
|