farming-code 2.2.18 → 2.2.20
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/backend/agent-manager.js +145 -42
- package/backend/bundle-update-helper.js +109 -0
- package/backend/config-manager.js +110 -4
- package/backend/farming-session-store.js +62 -2
- package/backend/server.js +369 -59
- package/backend/terminal-screen-worker-pool.js +15 -6
- package/backend/update-service.js +46 -23
- package/backend/usage-monitor.js +11 -0
- package/dist/assets/App-BupehIbc.js +210 -0
- package/dist/assets/{FileEditorPane-DNy3LAqS.js → FileEditorPane-CJs6kfZO.js} +1 -1
- package/dist/assets/{IconGlyphs-BsUfqpKN.js → IconGlyphs-CZzDr5o2.js} +1 -1
- package/dist/assets/{ProjectFilesSection-BhDEUVHA.js → ProjectFilesSection-DWCV07lA.js} +1 -1
- package/dist/assets/{ReviewPage-ChtByBe3.js → ReviewPage-DBKNOWsJ.js} +1 -1
- package/dist/assets/code-dark-BAt0Q_Op.css +1 -0
- package/dist/assets/{index-fp8VhOes.js → index-DuMjesL2.js} +2 -2
- package/dist/assets/main-y2aK_56c.css +1 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +257 -35
- package/frontend/skins/crt/index.html +21 -38
- package/frontend/skins/crt/styles/billing.css +6 -1
- package/package.json +1 -1
- package/dist/assets/App-DTAQilnX.js +0 -210
- package/dist/assets/code-dark-5esj0bRu.css +0 -1
- package/dist/assets/main-DSO3i_Gs.css +0 -1
package/backend/server.js
CHANGED
|
@@ -36,6 +36,7 @@ const {
|
|
|
36
36
|
resumedAgentSource,
|
|
37
37
|
} = require('./main-page-session');
|
|
38
38
|
const { discoverAgentWorkspaces } = require('./workspace-discovery');
|
|
39
|
+
const { inspectGitWorktree } = require('./git-worktree-info');
|
|
39
40
|
const { createWorkspaceDirectoryRouter } = require('./workspace-directory');
|
|
40
41
|
const { createControlRouter } = require('./control-api');
|
|
41
42
|
const { WorkspaceFileService, WorkspaceFileError } = require('./workspace-file-service');
|
|
@@ -743,8 +744,12 @@ app.get(routePath(BASE_PATH, '/api/claude/settings'), (req, res) => {
|
|
|
743
744
|
app.get(routePath(BASE_PATH, '/api/usage'), async (req, res) => {
|
|
744
745
|
try {
|
|
745
746
|
const fresh = req.query.fresh === '1';
|
|
747
|
+
const live = req.query.live === '1';
|
|
746
748
|
if (fresh) usageMonitor.invalidateDailyCache();
|
|
747
|
-
const usage = await usageSummaryCache.get(
|
|
749
|
+
const usage = await usageSummaryCache.get(
|
|
750
|
+
'summary',
|
|
751
|
+
fresh ? { force: true } : live ? { maxAgeMs: 15_000 } : {},
|
|
752
|
+
);
|
|
748
753
|
res.json({ usage });
|
|
749
754
|
} catch (error) {
|
|
750
755
|
res.status(500).json({ error: error.message || 'Failed to read usage information' });
|
|
@@ -1433,6 +1438,40 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), asyn
|
|
|
1433
1438
|
return;
|
|
1434
1439
|
}
|
|
1435
1440
|
|
|
1441
|
+
try {
|
|
1442
|
+
const membership = configManager.mountProjectWorkspace(result.workspace);
|
|
1443
|
+
result.projectWorkspaces = membership.projectWorkspaces;
|
|
1444
|
+
result.pinnedProjectWorkspaces = membership.pinnedProjectWorkspaces;
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
let rollbackError = '';
|
|
1447
|
+
try {
|
|
1448
|
+
if (mode === 'new-worktree') {
|
|
1449
|
+
const rollback = await agentManager.deleteForkWorktreeProject(result.workspace, { force: true });
|
|
1450
|
+
if (!rollback?.deleted || rollback.cleanupError) {
|
|
1451
|
+
rollbackError = rollback?.error || rollback?.cleanupError || 'Failed to delete the fork worktree';
|
|
1452
|
+
}
|
|
1453
|
+
} else {
|
|
1454
|
+
const rollback = await agentManager.archiveAgent(result.agentId, {
|
|
1455
|
+
reason: 'project-mount-failed',
|
|
1456
|
+
recordHistory: false,
|
|
1457
|
+
requireEngineExit: true,
|
|
1458
|
+
scheduleProviderArchive: false,
|
|
1459
|
+
});
|
|
1460
|
+
if (rollback?.error) rollbackError = rollback.error;
|
|
1461
|
+
}
|
|
1462
|
+
} catch (cleanupError) {
|
|
1463
|
+
rollbackError = cleanupError?.message || String(cleanupError);
|
|
1464
|
+
}
|
|
1465
|
+
broadcastState();
|
|
1466
|
+
const mountError = error.message || 'Failed to create Project';
|
|
1467
|
+
res.status(500).json({
|
|
1468
|
+
error: rollbackError
|
|
1469
|
+
? `${mountError}. Rollback failed: ${rollbackError}`
|
|
1470
|
+
: mountError,
|
|
1471
|
+
...(rollbackError ? { rollbackError } : {}),
|
|
1472
|
+
});
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1436
1475
|
broadcastState();
|
|
1437
1476
|
res.status(201).json(result);
|
|
1438
1477
|
});
|
|
@@ -1464,54 +1503,132 @@ app.post(routePath(BASE_PATH, '/api/projects/reveal'), express.json(), async (re
|
|
|
1464
1503
|
}
|
|
1465
1504
|
});
|
|
1466
1505
|
|
|
1506
|
+
app.post(routePath(BASE_PATH, '/api/projects/mount'), express.json(), async (req, res) => {
|
|
1507
|
+
try {
|
|
1508
|
+
const workspace = await canonicalProjectWorkspace(req.body?.workspace);
|
|
1509
|
+
const membership = configManager.mountProjectWorkspace(workspace);
|
|
1510
|
+
broadcastState();
|
|
1511
|
+
res.json(membership);
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
res.status(400).json({ error: error.message || 'Failed to create Project' });
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
app.post(routePath(BASE_PATH, '/api/projects/remove'), express.json(), (req, res) => {
|
|
1518
|
+
try {
|
|
1519
|
+
const membership = configManager.removeProjectWorkspace(req.body?.workspace);
|
|
1520
|
+
broadcastState();
|
|
1521
|
+
res.json(membership);
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
res.status(400).json({ error: error.message || 'Failed to remove Project' });
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
app.post(routePath(BASE_PATH, '/api/projects/pin'), express.json(), (req, res) => {
|
|
1528
|
+
try {
|
|
1529
|
+
const membership = configManager.setProjectWorkspacePinned(
|
|
1530
|
+
req.body?.workspace,
|
|
1531
|
+
req.body?.pinned === true
|
|
1532
|
+
);
|
|
1533
|
+
broadcastState();
|
|
1534
|
+
res.json(membership);
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
res.status(400).json({ error: error.message || 'Failed to update Project pin' });
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1467
1540
|
app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
|
|
1468
1541
|
let created = null;
|
|
1469
|
-
let previousProjects = null;
|
|
1470
1542
|
try {
|
|
1471
1543
|
const root = resolveProjectActionRoot(req.body?.rootId);
|
|
1472
1544
|
created = await agentManager.createPermanentWorktree(root.canonicalPath);
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
projectWorkspaces: [
|
|
1476
|
-
created.workspace,
|
|
1477
|
-
...previousProjects.filter(workspace => workspace !== created.workspace),
|
|
1478
|
-
],
|
|
1479
|
-
});
|
|
1545
|
+
const membership = configManager.mountProjectWorkspace(created.workspace);
|
|
1546
|
+
broadcastState();
|
|
1480
1547
|
res.status(201).json({
|
|
1481
1548
|
workspace: created.workspace,
|
|
1482
1549
|
branch: created.branch,
|
|
1483
|
-
projectWorkspaces:
|
|
1550
|
+
projectWorkspaces: membership.projectWorkspaces,
|
|
1551
|
+
pinnedProjectWorkspaces: membership.pinnedProjectWorkspaces,
|
|
1484
1552
|
});
|
|
1485
1553
|
} catch (error) {
|
|
1486
|
-
|
|
1487
|
-
if (
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
} catch {
|
|
1491
|
-
// Keep the original error visible; the failed settings write is already the root cause.
|
|
1492
|
-
}
|
|
1554
|
+
let rollbackError = '';
|
|
1555
|
+
if (created) {
|
|
1556
|
+
const rollback = await agentManager.rollbackPermanentWorktree(created);
|
|
1557
|
+
if (rollback?.rolledBack === false) rollbackError = rollback.error || 'Unknown rollback failure';
|
|
1493
1558
|
}
|
|
1494
1559
|
const status = error instanceof WorkspaceFileError ? error.statusCode : 400;
|
|
1495
|
-
|
|
1560
|
+
const createError = error.message || 'Failed to create permanent worktree';
|
|
1561
|
+
res.status(status).json({
|
|
1562
|
+
error: rollbackError
|
|
1563
|
+
? `${createError}. Rollback failed: ${rollbackError}`
|
|
1564
|
+
: createError,
|
|
1565
|
+
...(rollbackError ? { rollbackError } : {}),
|
|
1566
|
+
});
|
|
1496
1567
|
}
|
|
1497
1568
|
});
|
|
1498
1569
|
|
|
1499
1570
|
app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
|
|
1500
1571
|
const body = req.body || {};
|
|
1501
1572
|
const workspace = typeof body.workspace === 'string' ? body.workspace : '';
|
|
1502
|
-
const
|
|
1503
|
-
if (
|
|
1504
|
-
if (
|
|
1505
|
-
res.status(409).json(
|
|
1573
|
+
const inspected = await agentManager.inspectForkWorktreeProject(workspace);
|
|
1574
|
+
if (inspected.error || (inspected.requiresForce && body.force !== true)) {
|
|
1575
|
+
if (inspected.requiresForce) {
|
|
1576
|
+
res.status(409).json({
|
|
1577
|
+
...inspected,
|
|
1578
|
+
error: 'Worktree has uncommitted or untracked files',
|
|
1579
|
+
});
|
|
1506
1580
|
return;
|
|
1507
1581
|
}
|
|
1508
|
-
const status =
|
|
1509
|
-
res.status(status).json(
|
|
1582
|
+
const status = inspected.error === 'Workspace not found' || inspected.error === 'Workspace is required' ? 404 : 400;
|
|
1583
|
+
res.status(status).json(inspected);
|
|
1510
1584
|
return;
|
|
1511
1585
|
}
|
|
1512
1586
|
|
|
1587
|
+
const wasPinned = (configManager.getSettings().pinnedProjectWorkspaces || []).includes(inspected.workspace);
|
|
1588
|
+
let membership;
|
|
1589
|
+
try {
|
|
1590
|
+
membership = configManager.removeProjectWorkspace(inspected.workspace);
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
res.status(500).json({
|
|
1593
|
+
...inspected,
|
|
1594
|
+
error: error.message || 'Project removal failed; the worktree was not deleted',
|
|
1595
|
+
});
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const result = await agentManager.deleteForkWorktreeProject(inspected.workspace, { force: body.force === true });
|
|
1599
|
+
if (result.error) {
|
|
1600
|
+
let rollbackError = '';
|
|
1601
|
+
try {
|
|
1602
|
+
configManager.mountProjectWorkspace(inspected.workspace);
|
|
1603
|
+
if (wasPinned) configManager.setProjectWorkspacePinned(inspected.workspace, true);
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
rollbackError = error.message || String(error);
|
|
1606
|
+
}
|
|
1607
|
+
broadcastState();
|
|
1608
|
+
const status = result.requiresForce ? 409 : 400;
|
|
1609
|
+
res.status(status).json({
|
|
1610
|
+
...result,
|
|
1611
|
+
...(rollbackError ? {
|
|
1612
|
+
error: `${result.error}. Project restore failed: ${rollbackError}`,
|
|
1613
|
+
rollbackError,
|
|
1614
|
+
} : {}),
|
|
1615
|
+
});
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
if (result.cleanupError) {
|
|
1619
|
+
broadcastState();
|
|
1620
|
+
res.status(500).json({
|
|
1621
|
+
...result,
|
|
1622
|
+
error: `Worktree was deleted, but Agent cleanup failed: ${result.cleanupError}`,
|
|
1623
|
+
});
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1513
1626
|
broadcastState();
|
|
1514
|
-
res.json(
|
|
1627
|
+
res.json({
|
|
1628
|
+
...result,
|
|
1629
|
+
projectWorkspaces: membership.projectWorkspaces,
|
|
1630
|
+
pinnedProjectWorkspaces: membership.pinnedProjectWorkspaces,
|
|
1631
|
+
});
|
|
1515
1632
|
});
|
|
1516
1633
|
|
|
1517
1634
|
app.post(routePath(BASE_PATH, '/api/codex/sessions/:sessionId/resume'), express.json(), async (req, res) => {
|
|
@@ -1523,6 +1640,7 @@ app.post(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId/resume')
|
|
|
1523
1640
|
});
|
|
1524
1641
|
|
|
1525
1642
|
const pendingResumeStarts = new Map();
|
|
1643
|
+
const pendingProjectWorkspaceResolutions = new Map();
|
|
1526
1644
|
|
|
1527
1645
|
function resumedAgentStartKey(provider, sessionId, options = {}) {
|
|
1528
1646
|
return [
|
|
@@ -1584,6 +1702,14 @@ function forgetMainPageAgentSession(provider, sessionId, providerHomeId = '') {
|
|
|
1584
1702
|
});
|
|
1585
1703
|
}
|
|
1586
1704
|
|
|
1705
|
+
function savedFarmingAgentSession(provider, sessionId, providerHomeId = '') {
|
|
1706
|
+
if (typeof configManager.getAgentSessionRecordForProviderSessionKey !== 'function') return null;
|
|
1707
|
+
const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
|
|
1708
|
+
return sessionKey
|
|
1709
|
+
? configManager.getAgentSessionRecordForProviderSessionKey(sessionKey)
|
|
1710
|
+
: null;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1587
1713
|
async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
1588
1714
|
const normalizedProvider = normalizeProvider(provider);
|
|
1589
1715
|
const sessionId = String(rawSessionId || '').trim();
|
|
@@ -1604,7 +1730,12 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1604
1730
|
const existingAgent = findResumedAgent(normalizedProvider, sessionId, providerHomeId);
|
|
1605
1731
|
if (existingAgent) {
|
|
1606
1732
|
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1607
|
-
|
|
1733
|
+
const projectWorkspace = requestedAsMain
|
|
1734
|
+
? ''
|
|
1735
|
+
: await canonicalProjectWorkspace(
|
|
1736
|
+
existingAgent.gitWorktree?.workspace || existingAgent.projectWorkspace || existingAgent.cwd
|
|
1737
|
+
);
|
|
1738
|
+
return { agentId: existingAgent.id, projectWorkspace, reused: true };
|
|
1608
1739
|
}
|
|
1609
1740
|
}
|
|
1610
1741
|
const pendingStart = pendingResumeStarts.get(pendingResumeId);
|
|
@@ -1616,6 +1747,7 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1616
1747
|
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1617
1748
|
return {
|
|
1618
1749
|
agentId: result.agentId,
|
|
1750
|
+
projectWorkspace: result.projectWorkspace || '',
|
|
1619
1751
|
reused: true,
|
|
1620
1752
|
pending: true,
|
|
1621
1753
|
...(result.claimed ? { claimed: true } : {}),
|
|
@@ -1663,7 +1795,10 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1663
1795
|
});
|
|
1664
1796
|
if (claimingAgent) {
|
|
1665
1797
|
if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
|
|
1666
|
-
|
|
1798
|
+
const projectWorkspace = await canonicalProjectWorkspace(
|
|
1799
|
+
claimingAgent.gitWorktree?.workspace || claimingAgent.projectWorkspace || claimingAgent.cwd
|
|
1800
|
+
);
|
|
1801
|
+
return { agentId: claimingAgent.id, projectWorkspace, reused: true, claimed: true };
|
|
1667
1802
|
}
|
|
1668
1803
|
}
|
|
1669
1804
|
|
|
@@ -1672,7 +1807,20 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1672
1807
|
return { error: 'session is not a Main Agent session', status: 400 };
|
|
1673
1808
|
}
|
|
1674
1809
|
|
|
1810
|
+
const savedSession = shouldFork
|
|
1811
|
+
? null
|
|
1812
|
+
: savedFarmingAgentSession(normalizedProvider, sessionId, session
|
|
1813
|
+
? (session.providerHomeId || providerHomeId)
|
|
1814
|
+
: providerHomeId);
|
|
1815
|
+
const hasRequestedCustomTitle = Object.prototype.hasOwnProperty.call(options, 'customTitle');
|
|
1816
|
+
const savedAttentionSeq = Number(savedSession?.attentionSeq) || 0;
|
|
1817
|
+
const savedReadAttentionSeq = Number(savedSession?.readAttentionSeq) || 0;
|
|
1675
1818
|
const workingDirectory = session && (session.cwd || session.workspace) ? (session.cwd || session.workspace) : null;
|
|
1819
|
+
const projectWorkspace = resumeAsMain
|
|
1820
|
+
? ''
|
|
1821
|
+
: await canonicalProjectWorkspace(
|
|
1822
|
+
savedSession?.projectWorkspace || (session ? (session.workspace || session.cwd || '') : workingDirectory)
|
|
1823
|
+
);
|
|
1676
1824
|
const command = buildAgentSessionResumeCommand(normalizedProvider, sessionId, {
|
|
1677
1825
|
fork: shouldFork,
|
|
1678
1826
|
cwd: workingDirectory,
|
|
@@ -1699,19 +1847,38 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1699
1847
|
return;
|
|
1700
1848
|
}
|
|
1701
1849
|
|
|
1702
|
-
resolve({ agentId });
|
|
1850
|
+
resolve({ agentId, projectWorkspace });
|
|
1703
1851
|
}, {
|
|
1704
1852
|
wantsMain: resumeAsMain,
|
|
1705
|
-
task: session ? session.title : '',
|
|
1706
|
-
|
|
1853
|
+
task: savedSession?.task || (session ? session.title : ''),
|
|
1854
|
+
workflowTemplate: savedSession?.workflowTemplate || '',
|
|
1855
|
+
customTitle: hasRequestedCustomTitle
|
|
1856
|
+
? (typeof options.customTitle === 'string' ? options.customTitle : '')
|
|
1857
|
+
: (savedSession?.customTitle || ''),
|
|
1858
|
+
customTitleExplicit: hasRequestedCustomTitle,
|
|
1707
1859
|
requiredCliVersion: normalizedProvider === 'codex' && session ? session.cliVersion : '',
|
|
1708
|
-
projectWorkspace
|
|
1860
|
+
projectWorkspace,
|
|
1709
1861
|
source: shouldFork ? resumeSource.replace('-history:', '-history-fork:') : resumeSource,
|
|
1710
1862
|
agentRuntimeMode: ['chat', 'acp'].includes(options.agentRuntimeMode) ? 'chat' : 'terminal',
|
|
1711
1863
|
acpHistoryMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
1712
1864
|
providerHomeId: resolvedProviderHomeId,
|
|
1713
1865
|
providerHomePath: session ? (session.providerHomePath || '') : '',
|
|
1714
|
-
|
|
1866
|
+
providerSessionTitle: session?.title || savedSession?.providerSessionTitle || '',
|
|
1867
|
+
persistentSessionId: savedSession?.id || '',
|
|
1868
|
+
pinned: savedSession?.pinned === true,
|
|
1869
|
+
projectOrder: savedSession?.projectOrder,
|
|
1870
|
+
pinnedOrder: savedSession?.pinnedOrder,
|
|
1871
|
+
attentionSeq: savedAttentionSeq,
|
|
1872
|
+
readAttentionSeq: savedReadAttentionSeq,
|
|
1873
|
+
attentionUpdatedAt: savedSession?.attentionUpdatedAt,
|
|
1874
|
+
readAttentionAt: savedSession?.readAttentionAt,
|
|
1875
|
+
attentionReason: savedSession?.attentionReason,
|
|
1876
|
+
attentionOutputEpoch: savedSession?.attentionOutputEpoch,
|
|
1877
|
+
attentionOutputSeq: savedSession?.attentionOutputSeq,
|
|
1878
|
+
readOutputEpoch: savedSession?.readOutputEpoch,
|
|
1879
|
+
readOutputSeq: savedSession?.readOutputSeq,
|
|
1880
|
+
autoReadInitialAttention: options.autoReadInitialAttention === true
|
|
1881
|
+
&& savedAttentionSeq <= savedReadAttentionSeq,
|
|
1715
1882
|
preserveProviderSessionProfile: normalizedProvider === 'codex',
|
|
1716
1883
|
});
|
|
1717
1884
|
Promise.resolve(startResult).catch((error) => {
|
|
@@ -1734,23 +1901,73 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
|
|
|
1734
1901
|
if (result.reused) {
|
|
1735
1902
|
return {
|
|
1736
1903
|
agentId: result.agentId,
|
|
1904
|
+
projectWorkspace: result.projectWorkspace || '',
|
|
1737
1905
|
reused: true,
|
|
1738
1906
|
...(result.claimed ? { claimed: true } : {}),
|
|
1739
1907
|
};
|
|
1740
1908
|
}
|
|
1741
|
-
return {
|
|
1909
|
+
return {
|
|
1910
|
+
agentId: result.agentId,
|
|
1911
|
+
projectWorkspace: result.projectWorkspace || '',
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
async function canonicalProjectWorkspace(workspace) {
|
|
1916
|
+
const candidate = configManager.expandWorkspacePath(String(workspace || '').trim());
|
|
1917
|
+
if (!candidate) return '';
|
|
1918
|
+
const pending = pendingProjectWorkspaceResolutions.get(candidate);
|
|
1919
|
+
if (pending) return pending;
|
|
1920
|
+
|
|
1921
|
+
const resolution = (async () => {
|
|
1922
|
+
try {
|
|
1923
|
+
const worktree = await inspectGitWorktree(candidate);
|
|
1924
|
+
if (worktree?.workspace) return worktree.workspace;
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
console.warn('Failed to resolve project worktree:', candidate, error?.message || error);
|
|
1927
|
+
}
|
|
1928
|
+
try {
|
|
1929
|
+
return fs.realpathSync(path.resolve(candidate));
|
|
1930
|
+
} catch {
|
|
1931
|
+
return candidate;
|
|
1932
|
+
}
|
|
1933
|
+
})();
|
|
1934
|
+
pendingProjectWorkspaceResolutions.set(candidate, resolution);
|
|
1935
|
+
try {
|
|
1936
|
+
return await resolution;
|
|
1937
|
+
} finally {
|
|
1938
|
+
if (pendingProjectWorkspaceResolutions.get(candidate) === resolution) {
|
|
1939
|
+
pendingProjectWorkspaceResolutions.delete(candidate);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1742
1942
|
}
|
|
1743
1943
|
|
|
1744
1944
|
async function startResumedAgentSession(req, res, provider, rawSessionId) {
|
|
1745
1945
|
const shouldFork = req.body && req.body.fork === true;
|
|
1746
1946
|
const requestedAsMain = req.body && req.body.asMain === true && !shouldFork;
|
|
1747
1947
|
const allowUnarchiveArchived = req.body && req.body.unarchiveArchived === true && !shouldFork && !requestedAsMain;
|
|
1948
|
+
const providerHomeId = req.body && typeof req.body.providerHomeId === 'string'
|
|
1949
|
+
? req.body.providerHomeId
|
|
1950
|
+
: '';
|
|
1951
|
+
const sessionKey = mainPageAgentSessionKey(provider, rawSessionId, providerHomeId);
|
|
1952
|
+
const mainPageSessionWasRemembered = !shouldFork
|
|
1953
|
+
&& !requestedAsMain
|
|
1954
|
+
&& configManager.getMainPageSessionKeys().includes(sessionKey);
|
|
1955
|
+
if (
|
|
1956
|
+
req.body
|
|
1957
|
+
&& Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
|
|
1958
|
+
&& typeof req.body.customTitle !== 'string'
|
|
1959
|
+
) {
|
|
1960
|
+
res.status(400).json({ error: 'customTitle must be a string' });
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1748
1963
|
const result = await resumeAgentSessionById(provider, rawSessionId, {
|
|
1749
1964
|
fork: shouldFork,
|
|
1750
1965
|
asMain: requestedAsMain,
|
|
1751
1966
|
allowUnarchiveArchived,
|
|
1752
|
-
providerHomeId
|
|
1753
|
-
|
|
1967
|
+
providerHomeId,
|
|
1968
|
+
...(req.body && Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
|
|
1969
|
+
? { customTitle: req.body.customTitle }
|
|
1970
|
+
: {}),
|
|
1754
1971
|
agentRuntimeMode: req.body && ['chat', 'acp'].includes(req.body.agentRuntimeMode) ? 'chat' : 'terminal',
|
|
1755
1972
|
acpHistoryMode: req.body && req.body.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
1756
1973
|
});
|
|
@@ -1760,10 +1977,49 @@ async function startResumedAgentSession(req, res, provider, rawSessionId) {
|
|
|
1760
1977
|
return;
|
|
1761
1978
|
}
|
|
1762
1979
|
|
|
1980
|
+
let projectMembership = {
|
|
1981
|
+
projectWorkspaces: configManager.getSettings().projectWorkspaces || [],
|
|
1982
|
+
pinnedProjectWorkspaces: configManager.getSettings().pinnedProjectWorkspaces || [],
|
|
1983
|
+
};
|
|
1984
|
+
try {
|
|
1985
|
+
if (!requestedAsMain && result.projectWorkspace) {
|
|
1986
|
+
projectMembership = configManager.mountProjectWorkspace(result.projectWorkspace);
|
|
1987
|
+
}
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
let rollbackError = '';
|
|
1990
|
+
if (!result.reused) {
|
|
1991
|
+
try {
|
|
1992
|
+
const rollback = await agentManager.archiveAgent(result.agentId, {
|
|
1993
|
+
reason: 'project-mount-failed',
|
|
1994
|
+
recordHistory: false,
|
|
1995
|
+
requireEngineExit: true,
|
|
1996
|
+
scheduleProviderArchive: false,
|
|
1997
|
+
});
|
|
1998
|
+
if (rollback?.error) rollbackError = rollback.error;
|
|
1999
|
+
} catch (cleanupError) {
|
|
2000
|
+
rollbackError = cleanupError?.message || String(cleanupError);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (!mainPageSessionWasRemembered && !rollbackError) {
|
|
2004
|
+
forgetMainPageAgentSession(provider, rawSessionId, providerHomeId || 'default');
|
|
2005
|
+
}
|
|
2006
|
+
broadcastState();
|
|
2007
|
+
const mountError = error.message || 'Failed to create Project';
|
|
2008
|
+
res.status(500).json({
|
|
2009
|
+
error: rollbackError
|
|
2010
|
+
? `${mountError}. Rollback failed: ${rollbackError}`
|
|
2011
|
+
: mountError,
|
|
2012
|
+
...(rollbackError ? { rollbackError } : {}),
|
|
2013
|
+
});
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
1763
2016
|
broadcastState();
|
|
1764
2017
|
if (result.reused) {
|
|
1765
2018
|
res.status(200).json({
|
|
1766
2019
|
agentId: result.agentId,
|
|
2020
|
+
...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
|
|
2021
|
+
projectWorkspaces: projectMembership.projectWorkspaces,
|
|
2022
|
+
pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
|
|
1767
2023
|
reused: true,
|
|
1768
2024
|
...(result.claimed ? { claimed: true } : {}),
|
|
1769
2025
|
...(result.pending ? { pending: true } : {}),
|
|
@@ -1771,7 +2027,12 @@ async function startResumedAgentSession(req, res, provider, rawSessionId) {
|
|
|
1771
2027
|
return;
|
|
1772
2028
|
}
|
|
1773
2029
|
|
|
1774
|
-
res.status(201).json({
|
|
2030
|
+
res.status(201).json({
|
|
2031
|
+
agentId: result.agentId,
|
|
2032
|
+
...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
|
|
2033
|
+
projectWorkspaces: projectMembership.projectWorkspaces,
|
|
2034
|
+
pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
|
|
2035
|
+
});
|
|
1775
2036
|
}
|
|
1776
2037
|
|
|
1777
2038
|
async function autoResumeMainPageAgentSessions() {
|
|
@@ -1847,7 +2108,10 @@ async function autoResumeMainPageAgentSessions() {
|
|
|
1847
2108
|
}
|
|
1848
2109
|
|
|
1849
2110
|
app.post(routePath(BASE_PATH, '/api/settings'), express.json(), (req, res) => {
|
|
1850
|
-
|
|
2111
|
+
const settingsPatch = { ...(req.body || {}) };
|
|
2112
|
+
delete settingsPatch.projectWorkspaces;
|
|
2113
|
+
delete settingsPatch.pinnedProjectWorkspaces;
|
|
2114
|
+
configManager.updateSettings(settingsPatch);
|
|
1851
2115
|
agentSessionsCache.invalidate();
|
|
1852
2116
|
res.json({
|
|
1853
2117
|
success: true,
|
|
@@ -2089,27 +2353,70 @@ function handleMessage(ws, data) {
|
|
|
2089
2353
|
break;
|
|
2090
2354
|
case 'start-agent': {
|
|
2091
2355
|
const workspace = data.workspace || null;
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2356
|
+
void (async () => {
|
|
2357
|
+
const projectWorkspace = data.asMain === true
|
|
2358
|
+
? ''
|
|
2359
|
+
: await canonicalProjectWorkspace(
|
|
2360
|
+
typeof data.projectWorkspace === 'string' && data.projectWorkspace.trim()
|
|
2361
|
+
? data.projectWorkspace
|
|
2362
|
+
: workspace
|
|
2363
|
+
);
|
|
2364
|
+
agentManager.startAgent(data.command, workspace, (agentId, error) => {
|
|
2365
|
+
if (error) {
|
|
2366
|
+
ws.send(JSON.stringify({ type: 'error', message: error }));
|
|
2367
|
+
} else if (agentId) {
|
|
2368
|
+
void (async () => {
|
|
2369
|
+
try {
|
|
2370
|
+
if (projectWorkspace) configManager.mountProjectWorkspace(projectWorkspace);
|
|
2371
|
+
} catch (mountError) {
|
|
2372
|
+
let rollbackError = '';
|
|
2373
|
+
try {
|
|
2374
|
+
const rollback = await agentManager.archiveAgent(agentId, {
|
|
2375
|
+
reason: 'project-mount-failed',
|
|
2376
|
+
recordHistory: false,
|
|
2377
|
+
requireEngineExit: true,
|
|
2378
|
+
scheduleProviderArchive: false,
|
|
2379
|
+
});
|
|
2380
|
+
if (rollback?.error) rollbackError = rollback.error;
|
|
2381
|
+
} catch (cleanupError) {
|
|
2382
|
+
rollbackError = cleanupError?.message || String(cleanupError);
|
|
2383
|
+
}
|
|
2384
|
+
broadcastState();
|
|
2385
|
+
const errorMessage = mountError.message || 'Failed to create Project';
|
|
2386
|
+
ws.send(JSON.stringify({
|
|
2387
|
+
type: 'error',
|
|
2388
|
+
message: rollbackError
|
|
2389
|
+
? `${errorMessage}. Rollback failed: ${rollbackError}`
|
|
2390
|
+
: errorMessage,
|
|
2391
|
+
}));
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
ws.agentId = agentId;
|
|
2395
|
+
broadcastState();
|
|
2396
|
+
ws.send(JSON.stringify({ type: 'agent-started', agentId }));
|
|
2397
|
+
})().catch(callbackError => {
|
|
2398
|
+
console.warn('Failed to finish started Agent transition:', agentId, callbackError?.message || callbackError);
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
}, {
|
|
2402
|
+
wantsMain: data.asMain === true,
|
|
2403
|
+
projectWorkspace,
|
|
2404
|
+
task: typeof data.task === 'string' ? data.task : '',
|
|
2405
|
+
workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
|
|
2406
|
+
customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
|
|
2407
|
+
codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
|
|
2408
|
+
agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
|
|
2409
|
+
acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
|
|
2410
|
+
providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
|
|
2411
|
+
...(Array.isArray(data.additionalDirectories) ? { additionalDirectories: data.additionalDirectories } : {}),
|
|
2412
|
+
...(Array.isArray(data.mcpServers) ? { mcpServers: data.mcpServers } : {}),
|
|
2413
|
+
...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
|
|
2414
|
+
});
|
|
2415
|
+
})().catch(error => {
|
|
2416
|
+
ws.send(JSON.stringify({
|
|
2417
|
+
type: 'error',
|
|
2418
|
+
message: error.message || 'Failed to resolve Project workspace',
|
|
2419
|
+
}));
|
|
2113
2420
|
});
|
|
2114
2421
|
break;
|
|
2115
2422
|
}
|
|
@@ -2262,6 +2569,7 @@ async function watchWorkspaceFiles(ws, data) {
|
|
|
2262
2569
|
|
|
2263
2570
|
function buildStatePayload() {
|
|
2264
2571
|
const state = agentManager.getState();
|
|
2572
|
+
const settings = configManager.getSettings();
|
|
2265
2573
|
return {
|
|
2266
2574
|
...state,
|
|
2267
2575
|
agents: state.agents.map(agent => ({
|
|
@@ -2271,7 +2579,9 @@ function buildStatePayload() {
|
|
|
2271
2579
|
workspaceRoots: workspaceRootRegistry.list(),
|
|
2272
2580
|
mainPageSessionKeys: typeof configManager.getMainPageSessionKeys === 'function'
|
|
2273
2581
|
? configManager.getMainPageSessionKeys()
|
|
2274
|
-
: (Array.isArray(
|
|
2582
|
+
: (Array.isArray(settings.mainPageSessionKeys) ? settings.mainPageSessionKeys : []),
|
|
2583
|
+
projectWorkspaces: settings.projectWorkspaces || [],
|
|
2584
|
+
pinnedProjectWorkspaces: settings.pinnedProjectWorkspaces || [],
|
|
2275
2585
|
};
|
|
2276
2586
|
}
|
|
2277
2587
|
|
|
@@ -23,6 +23,7 @@ class TerminalScreenWorkerPool {
|
|
|
23
23
|
this.waiters = [];
|
|
24
24
|
this.readyWaiters = [];
|
|
25
25
|
this.pendingStarts = 0;
|
|
26
|
+
this.startTasks = new Set();
|
|
26
27
|
this.consecutiveStartFailures = 0;
|
|
27
28
|
this.retryDelayMs = Number.isFinite(options.retryDelayMs)
|
|
28
29
|
? Math.max(0, Number(options.retryDelayMs))
|
|
@@ -91,13 +92,12 @@ class TerminalScreenWorkerPool {
|
|
|
91
92
|
startWorker() {
|
|
92
93
|
this.pendingStarts += 1;
|
|
93
94
|
let failed = false;
|
|
94
|
-
this.createReadyWorker()
|
|
95
|
+
const startTask = this.createReadyWorker()
|
|
95
96
|
.then((worker) => {
|
|
96
97
|
this.pendingStarts -= 1;
|
|
97
98
|
this.consecutiveStartFailures = 0;
|
|
98
99
|
if (this.disposed) {
|
|
99
|
-
worker.dispose().catch(() => {});
|
|
100
|
-
return;
|
|
100
|
+
return worker.dispose().catch(() => {});
|
|
101
101
|
}
|
|
102
102
|
this.deliverWorker(worker);
|
|
103
103
|
})
|
|
@@ -122,6 +122,11 @@ class TerminalScreenWorkerPool {
|
|
|
122
122
|
this.ensureCapacity();
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
|
+
this.startTasks.add(startTask);
|
|
126
|
+
startTask.then(
|
|
127
|
+
() => this.startTasks.delete(startTask),
|
|
128
|
+
() => this.startTasks.delete(startTask),
|
|
129
|
+
);
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
scheduleCapacityRetry() {
|
|
@@ -197,8 +202,8 @@ class TerminalScreenWorkerPool {
|
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
async dispose() {
|
|
200
|
-
if (this.
|
|
201
|
-
return;
|
|
205
|
+
if (this.disposePromise) {
|
|
206
|
+
return this.disposePromise;
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
this.disposed = true;
|
|
@@ -210,7 +215,11 @@ class TerminalScreenWorkerPool {
|
|
|
210
215
|
this.waiters.splice(0).forEach(({ reject }) => reject(error));
|
|
211
216
|
this.readyWaiters.splice(0).forEach(({ reject }) => reject(error));
|
|
212
217
|
const workers = this.idle.splice(0);
|
|
213
|
-
|
|
218
|
+
this.disposePromise = Promise.allSettled([
|
|
219
|
+
...workers.map(worker => worker.dispose()),
|
|
220
|
+
...this.startTasks,
|
|
221
|
+
]).then(() => undefined);
|
|
222
|
+
return this.disposePromise;
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
|