farming-code 2.2.17 → 2.2.19

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/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');
@@ -1433,6 +1434,40 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), asyn
1433
1434
  return;
1434
1435
  }
1435
1436
 
1437
+ try {
1438
+ const membership = configManager.mountProjectWorkspace(result.workspace);
1439
+ result.projectWorkspaces = membership.projectWorkspaces;
1440
+ result.pinnedProjectWorkspaces = membership.pinnedProjectWorkspaces;
1441
+ } catch (error) {
1442
+ let rollbackError = '';
1443
+ try {
1444
+ if (mode === 'new-worktree') {
1445
+ const rollback = await agentManager.deleteForkWorktreeProject(result.workspace, { force: true });
1446
+ if (!rollback?.deleted || rollback.cleanupError) {
1447
+ rollbackError = rollback?.error || rollback?.cleanupError || 'Failed to delete the fork worktree';
1448
+ }
1449
+ } else {
1450
+ const rollback = await agentManager.archiveAgent(result.agentId, {
1451
+ reason: 'project-mount-failed',
1452
+ recordHistory: false,
1453
+ requireEngineExit: true,
1454
+ scheduleProviderArchive: false,
1455
+ });
1456
+ if (rollback?.error) rollbackError = rollback.error;
1457
+ }
1458
+ } catch (cleanupError) {
1459
+ rollbackError = cleanupError?.message || String(cleanupError);
1460
+ }
1461
+ broadcastState();
1462
+ const mountError = error.message || 'Failed to create Project';
1463
+ res.status(500).json({
1464
+ error: rollbackError
1465
+ ? `${mountError}. Rollback failed: ${rollbackError}`
1466
+ : mountError,
1467
+ ...(rollbackError ? { rollbackError } : {}),
1468
+ });
1469
+ return;
1470
+ }
1436
1471
  broadcastState();
1437
1472
  res.status(201).json(result);
1438
1473
  });
@@ -1464,54 +1499,132 @@ app.post(routePath(BASE_PATH, '/api/projects/reveal'), express.json(), async (re
1464
1499
  }
1465
1500
  });
1466
1501
 
1502
+ app.post(routePath(BASE_PATH, '/api/projects/mount'), express.json(), async (req, res) => {
1503
+ try {
1504
+ const workspace = await canonicalProjectWorkspace(req.body?.workspace);
1505
+ const membership = configManager.mountProjectWorkspace(workspace);
1506
+ broadcastState();
1507
+ res.json(membership);
1508
+ } catch (error) {
1509
+ res.status(400).json({ error: error.message || 'Failed to create Project' });
1510
+ }
1511
+ });
1512
+
1513
+ app.post(routePath(BASE_PATH, '/api/projects/remove'), express.json(), (req, res) => {
1514
+ try {
1515
+ const membership = configManager.removeProjectWorkspace(req.body?.workspace);
1516
+ broadcastState();
1517
+ res.json(membership);
1518
+ } catch (error) {
1519
+ res.status(400).json({ error: error.message || 'Failed to remove Project' });
1520
+ }
1521
+ });
1522
+
1523
+ app.post(routePath(BASE_PATH, '/api/projects/pin'), express.json(), (req, res) => {
1524
+ try {
1525
+ const membership = configManager.setProjectWorkspacePinned(
1526
+ req.body?.workspace,
1527
+ req.body?.pinned === true
1528
+ );
1529
+ broadcastState();
1530
+ res.json(membership);
1531
+ } catch (error) {
1532
+ res.status(400).json({ error: error.message || 'Failed to update Project pin' });
1533
+ }
1534
+ });
1535
+
1467
1536
  app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
1468
1537
  let created = null;
1469
- let previousProjects = null;
1470
1538
  try {
1471
1539
  const root = resolveProjectActionRoot(req.body?.rootId);
1472
1540
  created = await agentManager.createPermanentWorktree(root.canonicalPath);
1473
- previousProjects = configManager.getSettings().projectWorkspaces || [];
1474
- configManager.updateSettings({
1475
- projectWorkspaces: [
1476
- created.workspace,
1477
- ...previousProjects.filter(workspace => workspace !== created.workspace),
1478
- ],
1479
- });
1541
+ const membership = configManager.mountProjectWorkspace(created.workspace);
1542
+ broadcastState();
1480
1543
  res.status(201).json({
1481
1544
  workspace: created.workspace,
1482
1545
  branch: created.branch,
1483
- projectWorkspaces: configManager.getSettings().projectWorkspaces,
1546
+ projectWorkspaces: membership.projectWorkspaces,
1547
+ pinnedProjectWorkspaces: membership.pinnedProjectWorkspaces,
1484
1548
  });
1485
1549
  } catch (error) {
1486
- if (created) await agentManager.rollbackPermanentWorktree(created);
1487
- if (previousProjects) {
1488
- try {
1489
- configManager.updateSettings({ projectWorkspaces: previousProjects });
1490
- } catch {
1491
- // Keep the original error visible; the failed settings write is already the root cause.
1492
- }
1550
+ let rollbackError = '';
1551
+ if (created) {
1552
+ const rollback = await agentManager.rollbackPermanentWorktree(created);
1553
+ if (rollback?.rolledBack === false) rollbackError = rollback.error || 'Unknown rollback failure';
1493
1554
  }
1494
1555
  const status = error instanceof WorkspaceFileError ? error.statusCode : 400;
1495
- res.status(status).json({ error: error.message || 'Failed to create permanent worktree' });
1556
+ const createError = error.message || 'Failed to create permanent worktree';
1557
+ res.status(status).json({
1558
+ error: rollbackError
1559
+ ? `${createError}. Rollback failed: ${rollbackError}`
1560
+ : createError,
1561
+ ...(rollbackError ? { rollbackError } : {}),
1562
+ });
1496
1563
  }
1497
1564
  });
1498
1565
 
1499
1566
  app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
1500
1567
  const body = req.body || {};
1501
1568
  const workspace = typeof body.workspace === 'string' ? body.workspace : '';
1502
- const result = await agentManager.deleteForkWorktreeProject(workspace, { force: body.force === true });
1503
- if (result.error) {
1504
- if (result.requiresForce) {
1505
- res.status(409).json(result);
1569
+ const inspected = await agentManager.inspectForkWorktreeProject(workspace);
1570
+ if (inspected.error || (inspected.requiresForce && body.force !== true)) {
1571
+ if (inspected.requiresForce) {
1572
+ res.status(409).json({
1573
+ ...inspected,
1574
+ error: 'Worktree has uncommitted or untracked files',
1575
+ });
1506
1576
  return;
1507
1577
  }
1508
- const status = result.error === 'Workspace not found' || result.error === 'Workspace is required' ? 404 : 400;
1509
- res.status(status).json(result);
1578
+ const status = inspected.error === 'Workspace not found' || inspected.error === 'Workspace is required' ? 404 : 400;
1579
+ res.status(status).json(inspected);
1510
1580
  return;
1511
1581
  }
1512
1582
 
1583
+ const wasPinned = (configManager.getSettings().pinnedProjectWorkspaces || []).includes(inspected.workspace);
1584
+ let membership;
1585
+ try {
1586
+ membership = configManager.removeProjectWorkspace(inspected.workspace);
1587
+ } catch (error) {
1588
+ res.status(500).json({
1589
+ ...inspected,
1590
+ error: error.message || 'Project removal failed; the worktree was not deleted',
1591
+ });
1592
+ return;
1593
+ }
1594
+ const result = await agentManager.deleteForkWorktreeProject(inspected.workspace, { force: body.force === true });
1595
+ if (result.error) {
1596
+ let rollbackError = '';
1597
+ try {
1598
+ configManager.mountProjectWorkspace(inspected.workspace);
1599
+ if (wasPinned) configManager.setProjectWorkspacePinned(inspected.workspace, true);
1600
+ } catch (error) {
1601
+ rollbackError = error.message || String(error);
1602
+ }
1603
+ broadcastState();
1604
+ const status = result.requiresForce ? 409 : 400;
1605
+ res.status(status).json({
1606
+ ...result,
1607
+ ...(rollbackError ? {
1608
+ error: `${result.error}. Project restore failed: ${rollbackError}`,
1609
+ rollbackError,
1610
+ } : {}),
1611
+ });
1612
+ return;
1613
+ }
1614
+ if (result.cleanupError) {
1615
+ broadcastState();
1616
+ res.status(500).json({
1617
+ ...result,
1618
+ error: `Worktree was deleted, but Agent cleanup failed: ${result.cleanupError}`,
1619
+ });
1620
+ return;
1621
+ }
1513
1622
  broadcastState();
1514
- res.json(result);
1623
+ res.json({
1624
+ ...result,
1625
+ projectWorkspaces: membership.projectWorkspaces,
1626
+ pinnedProjectWorkspaces: membership.pinnedProjectWorkspaces,
1627
+ });
1515
1628
  });
1516
1629
 
1517
1630
  app.post(routePath(BASE_PATH, '/api/codex/sessions/:sessionId/resume'), express.json(), async (req, res) => {
@@ -1523,6 +1636,7 @@ app.post(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId/resume')
1523
1636
  });
1524
1637
 
1525
1638
  const pendingResumeStarts = new Map();
1639
+ const pendingProjectWorkspaceResolutions = new Map();
1526
1640
 
1527
1641
  function resumedAgentStartKey(provider, sessionId, options = {}) {
1528
1642
  return [
@@ -1584,6 +1698,14 @@ function forgetMainPageAgentSession(provider, sessionId, providerHomeId = '') {
1584
1698
  });
1585
1699
  }
1586
1700
 
1701
+ function savedFarmingAgentSession(provider, sessionId, providerHomeId = '') {
1702
+ if (typeof configManager.getAgentSessionRecordForProviderSessionKey !== 'function') return null;
1703
+ const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
1704
+ return sessionKey
1705
+ ? configManager.getAgentSessionRecordForProviderSessionKey(sessionKey)
1706
+ : null;
1707
+ }
1708
+
1587
1709
  async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1588
1710
  const normalizedProvider = normalizeProvider(provider);
1589
1711
  const sessionId = String(rawSessionId || '').trim();
@@ -1604,7 +1726,12 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1604
1726
  const existingAgent = findResumedAgent(normalizedProvider, sessionId, providerHomeId);
1605
1727
  if (existingAgent) {
1606
1728
  if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1607
- return { agentId: existingAgent.id, reused: true };
1729
+ const projectWorkspace = requestedAsMain
1730
+ ? ''
1731
+ : await canonicalProjectWorkspace(
1732
+ existingAgent.gitWorktree?.workspace || existingAgent.projectWorkspace || existingAgent.cwd
1733
+ );
1734
+ return { agentId: existingAgent.id, projectWorkspace, reused: true };
1608
1735
  }
1609
1736
  }
1610
1737
  const pendingStart = pendingResumeStarts.get(pendingResumeId);
@@ -1616,6 +1743,7 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1616
1743
  if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1617
1744
  return {
1618
1745
  agentId: result.agentId,
1746
+ projectWorkspace: result.projectWorkspace || '',
1619
1747
  reused: true,
1620
1748
  pending: true,
1621
1749
  ...(result.claimed ? { claimed: true } : {}),
@@ -1663,7 +1791,10 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1663
1791
  });
1664
1792
  if (claimingAgent) {
1665
1793
  if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1666
- return { agentId: claimingAgent.id, reused: true, claimed: true };
1794
+ const projectWorkspace = await canonicalProjectWorkspace(
1795
+ claimingAgent.gitWorktree?.workspace || claimingAgent.projectWorkspace || claimingAgent.cwd
1796
+ );
1797
+ return { agentId: claimingAgent.id, projectWorkspace, reused: true, claimed: true };
1667
1798
  }
1668
1799
  }
1669
1800
 
@@ -1672,7 +1803,20 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1672
1803
  return { error: 'session is not a Main Agent session', status: 400 };
1673
1804
  }
1674
1805
 
1806
+ const savedSession = shouldFork
1807
+ ? null
1808
+ : savedFarmingAgentSession(normalizedProvider, sessionId, session
1809
+ ? (session.providerHomeId || providerHomeId)
1810
+ : providerHomeId);
1811
+ const hasRequestedCustomTitle = Object.prototype.hasOwnProperty.call(options, 'customTitle');
1812
+ const savedAttentionSeq = Number(savedSession?.attentionSeq) || 0;
1813
+ const savedReadAttentionSeq = Number(savedSession?.readAttentionSeq) || 0;
1675
1814
  const workingDirectory = session && (session.cwd || session.workspace) ? (session.cwd || session.workspace) : null;
1815
+ const projectWorkspace = resumeAsMain
1816
+ ? ''
1817
+ : await canonicalProjectWorkspace(
1818
+ savedSession?.projectWorkspace || (session ? (session.workspace || session.cwd || '') : workingDirectory)
1819
+ );
1676
1820
  const command = buildAgentSessionResumeCommand(normalizedProvider, sessionId, {
1677
1821
  fork: shouldFork,
1678
1822
  cwd: workingDirectory,
@@ -1699,19 +1843,38 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1699
1843
  return;
1700
1844
  }
1701
1845
 
1702
- resolve({ agentId });
1846
+ resolve({ agentId, projectWorkspace });
1703
1847
  }, {
1704
1848
  wantsMain: resumeAsMain,
1705
- task: session ? session.title : '',
1706
- customTitle: typeof options.customTitle === 'string' ? options.customTitle : '',
1849
+ task: savedSession?.task || (session ? session.title : ''),
1850
+ workflowTemplate: savedSession?.workflowTemplate || '',
1851
+ customTitle: hasRequestedCustomTitle
1852
+ ? (typeof options.customTitle === 'string' ? options.customTitle : '')
1853
+ : (savedSession?.customTitle || ''),
1854
+ customTitleExplicit: hasRequestedCustomTitle,
1707
1855
  requiredCliVersion: normalizedProvider === 'codex' && session ? session.cliVersion : '',
1708
- projectWorkspace: session ? (session.workspace || session.cwd || '') : '',
1856
+ projectWorkspace,
1709
1857
  source: shouldFork ? resumeSource.replace('-history:', '-history-fork:') : resumeSource,
1710
1858
  agentRuntimeMode: ['chat', 'acp'].includes(options.agentRuntimeMode) ? 'chat' : 'terminal',
1711
1859
  acpHistoryMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
1712
1860
  providerHomeId: resolvedProviderHomeId,
1713
1861
  providerHomePath: session ? (session.providerHomePath || '') : '',
1714
- autoReadInitialAttention: options.autoReadInitialAttention === true,
1862
+ providerSessionTitle: session?.title || savedSession?.providerSessionTitle || '',
1863
+ persistentSessionId: savedSession?.id || '',
1864
+ pinned: savedSession?.pinned === true,
1865
+ projectOrder: savedSession?.projectOrder,
1866
+ pinnedOrder: savedSession?.pinnedOrder,
1867
+ attentionSeq: savedAttentionSeq,
1868
+ readAttentionSeq: savedReadAttentionSeq,
1869
+ attentionUpdatedAt: savedSession?.attentionUpdatedAt,
1870
+ readAttentionAt: savedSession?.readAttentionAt,
1871
+ attentionReason: savedSession?.attentionReason,
1872
+ attentionOutputEpoch: savedSession?.attentionOutputEpoch,
1873
+ attentionOutputSeq: savedSession?.attentionOutputSeq,
1874
+ readOutputEpoch: savedSession?.readOutputEpoch,
1875
+ readOutputSeq: savedSession?.readOutputSeq,
1876
+ autoReadInitialAttention: options.autoReadInitialAttention === true
1877
+ && savedAttentionSeq <= savedReadAttentionSeq,
1715
1878
  preserveProviderSessionProfile: normalizedProvider === 'codex',
1716
1879
  });
1717
1880
  Promise.resolve(startResult).catch((error) => {
@@ -1734,23 +1897,73 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1734
1897
  if (result.reused) {
1735
1898
  return {
1736
1899
  agentId: result.agentId,
1900
+ projectWorkspace: result.projectWorkspace || '',
1737
1901
  reused: true,
1738
1902
  ...(result.claimed ? { claimed: true } : {}),
1739
1903
  };
1740
1904
  }
1741
- return { agentId: result.agentId };
1905
+ return {
1906
+ agentId: result.agentId,
1907
+ projectWorkspace: result.projectWorkspace || '',
1908
+ };
1909
+ }
1910
+
1911
+ async function canonicalProjectWorkspace(workspace) {
1912
+ const candidate = configManager.expandWorkspacePath(String(workspace || '').trim());
1913
+ if (!candidate) return '';
1914
+ const pending = pendingProjectWorkspaceResolutions.get(candidate);
1915
+ if (pending) return pending;
1916
+
1917
+ const resolution = (async () => {
1918
+ try {
1919
+ const worktree = await inspectGitWorktree(candidate);
1920
+ if (worktree?.workspace) return worktree.workspace;
1921
+ } catch (error) {
1922
+ console.warn('Failed to resolve project worktree:', candidate, error?.message || error);
1923
+ }
1924
+ try {
1925
+ return fs.realpathSync(path.resolve(candidate));
1926
+ } catch {
1927
+ return candidate;
1928
+ }
1929
+ })();
1930
+ pendingProjectWorkspaceResolutions.set(candidate, resolution);
1931
+ try {
1932
+ return await resolution;
1933
+ } finally {
1934
+ if (pendingProjectWorkspaceResolutions.get(candidate) === resolution) {
1935
+ pendingProjectWorkspaceResolutions.delete(candidate);
1936
+ }
1937
+ }
1742
1938
  }
1743
1939
 
1744
1940
  async function startResumedAgentSession(req, res, provider, rawSessionId) {
1745
1941
  const shouldFork = req.body && req.body.fork === true;
1746
1942
  const requestedAsMain = req.body && req.body.asMain === true && !shouldFork;
1747
1943
  const allowUnarchiveArchived = req.body && req.body.unarchiveArchived === true && !shouldFork && !requestedAsMain;
1944
+ const providerHomeId = req.body && typeof req.body.providerHomeId === 'string'
1945
+ ? req.body.providerHomeId
1946
+ : '';
1947
+ const sessionKey = mainPageAgentSessionKey(provider, rawSessionId, providerHomeId);
1948
+ const mainPageSessionWasRemembered = !shouldFork
1949
+ && !requestedAsMain
1950
+ && configManager.getMainPageSessionKeys().includes(sessionKey);
1951
+ if (
1952
+ req.body
1953
+ && Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
1954
+ && typeof req.body.customTitle !== 'string'
1955
+ ) {
1956
+ res.status(400).json({ error: 'customTitle must be a string' });
1957
+ return;
1958
+ }
1748
1959
  const result = await resumeAgentSessionById(provider, rawSessionId, {
1749
1960
  fork: shouldFork,
1750
1961
  asMain: requestedAsMain,
1751
1962
  allowUnarchiveArchived,
1752
- providerHomeId: req.body && typeof req.body.providerHomeId === 'string' ? req.body.providerHomeId : '',
1753
- customTitle: req.body && typeof req.body.customTitle === 'string' ? req.body.customTitle : '',
1963
+ providerHomeId,
1964
+ ...(req.body && Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
1965
+ ? { customTitle: req.body.customTitle }
1966
+ : {}),
1754
1967
  agentRuntimeMode: req.body && ['chat', 'acp'].includes(req.body.agentRuntimeMode) ? 'chat' : 'terminal',
1755
1968
  acpHistoryMode: req.body && req.body.acpHistoryMode === 'resume' ? 'resume' : 'load',
1756
1969
  });
@@ -1760,10 +1973,49 @@ async function startResumedAgentSession(req, res, provider, rawSessionId) {
1760
1973
  return;
1761
1974
  }
1762
1975
 
1976
+ let projectMembership = {
1977
+ projectWorkspaces: configManager.getSettings().projectWorkspaces || [],
1978
+ pinnedProjectWorkspaces: configManager.getSettings().pinnedProjectWorkspaces || [],
1979
+ };
1980
+ try {
1981
+ if (!requestedAsMain && result.projectWorkspace) {
1982
+ projectMembership = configManager.mountProjectWorkspace(result.projectWorkspace);
1983
+ }
1984
+ } catch (error) {
1985
+ let rollbackError = '';
1986
+ if (!result.reused) {
1987
+ try {
1988
+ const rollback = await agentManager.archiveAgent(result.agentId, {
1989
+ reason: 'project-mount-failed',
1990
+ recordHistory: false,
1991
+ requireEngineExit: true,
1992
+ scheduleProviderArchive: false,
1993
+ });
1994
+ if (rollback?.error) rollbackError = rollback.error;
1995
+ } catch (cleanupError) {
1996
+ rollbackError = cleanupError?.message || String(cleanupError);
1997
+ }
1998
+ }
1999
+ if (!mainPageSessionWasRemembered && !rollbackError) {
2000
+ forgetMainPageAgentSession(provider, rawSessionId, providerHomeId || 'default');
2001
+ }
2002
+ broadcastState();
2003
+ const mountError = error.message || 'Failed to create Project';
2004
+ res.status(500).json({
2005
+ error: rollbackError
2006
+ ? `${mountError}. Rollback failed: ${rollbackError}`
2007
+ : mountError,
2008
+ ...(rollbackError ? { rollbackError } : {}),
2009
+ });
2010
+ return;
2011
+ }
1763
2012
  broadcastState();
1764
2013
  if (result.reused) {
1765
2014
  res.status(200).json({
1766
2015
  agentId: result.agentId,
2016
+ ...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
2017
+ projectWorkspaces: projectMembership.projectWorkspaces,
2018
+ pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
1767
2019
  reused: true,
1768
2020
  ...(result.claimed ? { claimed: true } : {}),
1769
2021
  ...(result.pending ? { pending: true } : {}),
@@ -1771,7 +2023,12 @@ async function startResumedAgentSession(req, res, provider, rawSessionId) {
1771
2023
  return;
1772
2024
  }
1773
2025
 
1774
- res.status(201).json({ agentId: result.agentId });
2026
+ res.status(201).json({
2027
+ agentId: result.agentId,
2028
+ ...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
2029
+ projectWorkspaces: projectMembership.projectWorkspaces,
2030
+ pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
2031
+ });
1775
2032
  }
1776
2033
 
1777
2034
  async function autoResumeMainPageAgentSessions() {
@@ -1847,7 +2104,10 @@ async function autoResumeMainPageAgentSessions() {
1847
2104
  }
1848
2105
 
1849
2106
  app.post(routePath(BASE_PATH, '/api/settings'), express.json(), (req, res) => {
1850
- configManager.updateSettings(req.body || {});
2107
+ const settingsPatch = { ...(req.body || {}) };
2108
+ delete settingsPatch.projectWorkspaces;
2109
+ delete settingsPatch.pinnedProjectWorkspaces;
2110
+ configManager.updateSettings(settingsPatch);
1851
2111
  agentSessionsCache.invalidate();
1852
2112
  res.json({
1853
2113
  success: true,
@@ -2089,27 +2349,70 @@ function handleMessage(ws, data) {
2089
2349
  break;
2090
2350
  case 'start-agent': {
2091
2351
  const workspace = data.workspace || null;
2092
- agentManager.startAgent(data.command, workspace, (agentId, error) => {
2093
- if (error) {
2094
- ws.send(JSON.stringify({ type: 'error', message: error }));
2095
- } else if (agentId) {
2096
- ws.agentId = agentId;
2097
- broadcastState();
2098
- ws.send(JSON.stringify({ type: 'agent-started', agentId }));
2099
- }
2100
- }, {
2101
- wantsMain: data.asMain === true,
2102
- projectWorkspace: typeof data.projectWorkspace === 'string' ? data.projectWorkspace : '',
2103
- task: typeof data.task === 'string' ? data.task : '',
2104
- workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
2105
- customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
2106
- codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
2107
- agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2108
- acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
2109
- providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
2110
- ...(Array.isArray(data.additionalDirectories) ? { additionalDirectories: data.additionalDirectories } : {}),
2111
- ...(Array.isArray(data.mcpServers) ? { mcpServers: data.mcpServers } : {}),
2112
- ...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
2352
+ void (async () => {
2353
+ const projectWorkspace = data.asMain === true
2354
+ ? ''
2355
+ : await canonicalProjectWorkspace(
2356
+ typeof data.projectWorkspace === 'string' && data.projectWorkspace.trim()
2357
+ ? data.projectWorkspace
2358
+ : workspace
2359
+ );
2360
+ agentManager.startAgent(data.command, workspace, (agentId, error) => {
2361
+ if (error) {
2362
+ ws.send(JSON.stringify({ type: 'error', message: error }));
2363
+ } else if (agentId) {
2364
+ void (async () => {
2365
+ try {
2366
+ if (projectWorkspace) configManager.mountProjectWorkspace(projectWorkspace);
2367
+ } catch (mountError) {
2368
+ let rollbackError = '';
2369
+ try {
2370
+ const rollback = await agentManager.archiveAgent(agentId, {
2371
+ reason: 'project-mount-failed',
2372
+ recordHistory: false,
2373
+ requireEngineExit: true,
2374
+ scheduleProviderArchive: false,
2375
+ });
2376
+ if (rollback?.error) rollbackError = rollback.error;
2377
+ } catch (cleanupError) {
2378
+ rollbackError = cleanupError?.message || String(cleanupError);
2379
+ }
2380
+ broadcastState();
2381
+ const errorMessage = mountError.message || 'Failed to create Project';
2382
+ ws.send(JSON.stringify({
2383
+ type: 'error',
2384
+ message: rollbackError
2385
+ ? `${errorMessage}. Rollback failed: ${rollbackError}`
2386
+ : errorMessage,
2387
+ }));
2388
+ return;
2389
+ }
2390
+ ws.agentId = agentId;
2391
+ broadcastState();
2392
+ ws.send(JSON.stringify({ type: 'agent-started', agentId }));
2393
+ })().catch(callbackError => {
2394
+ console.warn('Failed to finish started Agent transition:', agentId, callbackError?.message || callbackError);
2395
+ });
2396
+ }
2397
+ }, {
2398
+ wantsMain: data.asMain === true,
2399
+ projectWorkspace,
2400
+ task: typeof data.task === 'string' ? data.task : '',
2401
+ workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
2402
+ customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
2403
+ codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
2404
+ agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2405
+ acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
2406
+ providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
2407
+ ...(Array.isArray(data.additionalDirectories) ? { additionalDirectories: data.additionalDirectories } : {}),
2408
+ ...(Array.isArray(data.mcpServers) ? { mcpServers: data.mcpServers } : {}),
2409
+ ...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
2410
+ });
2411
+ })().catch(error => {
2412
+ ws.send(JSON.stringify({
2413
+ type: 'error',
2414
+ message: error.message || 'Failed to resolve Project workspace',
2415
+ }));
2113
2416
  });
2114
2417
  break;
2115
2418
  }
@@ -2262,6 +2565,7 @@ async function watchWorkspaceFiles(ws, data) {
2262
2565
 
2263
2566
  function buildStatePayload() {
2264
2567
  const state = agentManager.getState();
2568
+ const settings = configManager.getSettings();
2265
2569
  return {
2266
2570
  ...state,
2267
2571
  agents: state.agents.map(agent => ({
@@ -2271,7 +2575,9 @@ function buildStatePayload() {
2271
2575
  workspaceRoots: workspaceRootRegistry.list(),
2272
2576
  mainPageSessionKeys: typeof configManager.getMainPageSessionKeys === 'function'
2273
2577
  ? configManager.getMainPageSessionKeys()
2274
- : (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []),
2578
+ : (Array.isArray(settings.mainPageSessionKeys) ? settings.mainPageSessionKeys : []),
2579
+ projectWorkspaces: settings.projectWorkspaces || [],
2580
+ pinnedProjectWorkspaces: settings.pinnedProjectWorkspaces || [],
2275
2581
  };
2276
2582
  }
2277
2583