@getpaseo/server 0.1.18 → 0.1.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.
Files changed (74) hide show
  1. package/dist/server/client/daemon-client.d.ts +5 -1
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +22 -0
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -1
  6. package/dist/server/server/agent/agent-management-mcp.js +11 -28
  7. package/dist/server/server/agent/agent-management-mcp.js.map +1 -1
  8. package/dist/server/server/agent/agent-manager.d.ts +2 -0
  9. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  10. package/dist/server/server/agent/agent-manager.js +50 -6
  11. package/dist/server/server/agent/agent-manager.js.map +1 -1
  12. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  13. package/dist/server/server/agent/mcp-server.js +11 -34
  14. package/dist/server/server/agent/mcp-server.js.map +1 -1
  15. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  16. package/dist/server/server/agent/providers/claude-agent.js +169 -43
  17. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  18. package/dist/server/server/bootstrap.d.ts +13 -0
  19. package/dist/server/server/bootstrap.d.ts.map +1 -1
  20. package/dist/server/server/bootstrap.js +46 -32
  21. package/dist/server/server/bootstrap.js.map +1 -1
  22. package/dist/server/server/persisted-config.d.ts +22 -22
  23. package/dist/server/server/session.d.ts +10 -6
  24. package/dist/server/server/session.d.ts.map +1 -1
  25. package/dist/server/server/session.js +191 -223
  26. package/dist/server/server/session.js.map +1 -1
  27. package/dist/server/server/speech/speech-types.d.ts +2 -2
  28. package/dist/server/server/websocket-server.d.ts +4 -1
  29. package/dist/server/server/websocket-server.d.ts.map +1 -1
  30. package/dist/server/server/websocket-server.js +27 -1
  31. package/dist/server/server/websocket-server.js.map +1 -1
  32. package/dist/server/server/workspace-registry-bootstrap.d.ts +11 -0
  33. package/dist/server/server/workspace-registry-bootstrap.d.ts.map +1 -0
  34. package/dist/server/server/workspace-registry-bootstrap.js +98 -0
  35. package/dist/server/server/workspace-registry-bootstrap.js.map +1 -0
  36. package/dist/server/server/workspace-registry-model.d.ts +26 -0
  37. package/dist/server/server/workspace-registry-model.d.ts.map +1 -0
  38. package/dist/server/server/workspace-registry-model.js +150 -0
  39. package/dist/server/server/workspace-registry-model.js.map +1 -0
  40. package/dist/server/server/workspace-registry.d.ts +128 -0
  41. package/dist/server/server/workspace-registry.d.ts.map +1 -0
  42. package/dist/server/server/workspace-registry.js +141 -0
  43. package/dist/server/server/workspace-registry.js.map +1 -0
  44. package/dist/server/shared/messages.d.ts +1510 -0
  45. package/dist/server/shared/messages.d.ts.map +1 -1
  46. package/dist/server/shared/messages.js +39 -0
  47. package/dist/server/shared/messages.js.map +1 -1
  48. package/dist/server/utils/checkout-git.d.ts +5 -0
  49. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  50. package/dist/server/utils/checkout-git.js +64 -4
  51. package/dist/server/utils/checkout-git.js.map +1 -1
  52. package/dist/src/server/agent/agent-manager.js +50 -6
  53. package/dist/src/server/agent/agent-manager.js.map +1 -1
  54. package/dist/src/server/agent/mcp-server.js +11 -34
  55. package/dist/src/server/agent/mcp-server.js.map +1 -1
  56. package/dist/src/server/agent/providers/claude-agent.js +169 -43
  57. package/dist/src/server/agent/providers/claude-agent.js.map +1 -1
  58. package/dist/src/server/bootstrap.js +46 -32
  59. package/dist/src/server/bootstrap.js.map +1 -1
  60. package/dist/src/server/session.js +191 -223
  61. package/dist/src/server/session.js.map +1 -1
  62. package/dist/src/server/websocket-server.js +27 -1
  63. package/dist/src/server/websocket-server.js.map +1 -1
  64. package/dist/src/server/workspace-registry-bootstrap.js +98 -0
  65. package/dist/src/server/workspace-registry-bootstrap.js.map +1 -0
  66. package/dist/src/server/workspace-registry-model.js +150 -0
  67. package/dist/src/server/workspace-registry-model.js.map +1 -0
  68. package/dist/src/server/workspace-registry.js +141 -0
  69. package/dist/src/server/workspace-registry.js.map +1 -0
  70. package/dist/src/shared/messages.js +39 -0
  71. package/dist/src/shared/messages.js.map +1 -1
  72. package/dist/src/utils/checkout-git.js +64 -4
  73. package/dist/src/utils/checkout-git.js.map +1 -1
  74. package/package.json +4 -3
@@ -23,12 +23,14 @@ import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } fro
23
23
  import { projectTimelineRows, selectTimelineWindowByProjectedLimit, } from './agent/timeline-projection.js';
24
24
  import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from './agent/agent-response-loop.js';
25
25
  import { isValidAgentProvider, AGENT_PROVIDER_IDS } from './agent/provider-manifest.js';
26
+ import { buildProjectPlacementForCwd, deriveProjectKind, deriveProjectRootPath, deriveWorkspaceDisplayName, deriveWorkspaceKind, normalizeWorkspaceId as normalizePersistedWorkspaceId, } from './workspace-registry-model.js';
27
+ import { createPersistedProjectRecord, createPersistedWorkspaceRecord, } from './workspace-registry.js';
26
28
  import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from './voice-config.js';
27
29
  import { isVoicePermissionAllowed } from './voice-permission-policy.js';
28
30
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from './file-explorer/service.js';
29
31
  import { slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from '../utils/worktree.js';
30
32
  import { createAgentWorktree, runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
31
- import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from '../utils/checkout-git.js';
33
+ import { getCheckoutDiff, getCheckoutShortstat, getCheckoutStatus, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from '../utils/checkout-git.js';
32
34
  import { getProjectIcon } from '../utils/project-icon.js';
33
35
  import { expandTilde } from '../utils/path.js';
34
36
  import { searchHomeDirectories, searchWorkspaceEntries } from '../utils/directory-suggestions.js';
@@ -73,66 +75,6 @@ export function resolveCreateAgentTitles(options) {
73
75
  provisionalTitle,
74
76
  };
75
77
  }
76
- function deriveRemoteProjectKey(remoteUrl) {
77
- if (!remoteUrl) {
78
- return null;
79
- }
80
- const trimmed = remoteUrl.trim();
81
- if (!trimmed) {
82
- return null;
83
- }
84
- let host = null;
85
- let path = null;
86
- const scpLike = trimmed.match(/^[^@]+@([^:]+):(.+)$/);
87
- if (scpLike) {
88
- host = scpLike[1] ?? null;
89
- path = scpLike[2] ?? null;
90
- }
91
- else if (trimmed.includes('://')) {
92
- try {
93
- const parsed = new URL(trimmed);
94
- host = parsed.hostname || null;
95
- path = parsed.pathname ? parsed.pathname.replace(/^\//, '') : null;
96
- }
97
- catch {
98
- return null;
99
- }
100
- }
101
- if (!host || !path) {
102
- return null;
103
- }
104
- let cleanedPath = path.trim().replace(/^\/+/, '').replace(/\/+$/, '');
105
- if (cleanedPath.endsWith('.git')) {
106
- cleanedPath = cleanedPath.slice(0, -4);
107
- }
108
- if (!cleanedPath.includes('/')) {
109
- return null;
110
- }
111
- const cleanedHost = host.toLowerCase();
112
- if (cleanedHost === 'github.com') {
113
- return `remote:github.com/${cleanedPath}`;
114
- }
115
- return `remote:${cleanedHost}/${cleanedPath}`;
116
- }
117
- function deriveProjectGroupingKey(options) {
118
- const remoteKey = deriveRemoteProjectKey(options.remoteUrl);
119
- if (remoteKey) {
120
- return remoteKey;
121
- }
122
- const mainRepoRoot = options.mainRepoRoot?.trim();
123
- if (options.isPaseoOwnedWorktree && mainRepoRoot) {
124
- return mainRepoRoot;
125
- }
126
- return options.cwd;
127
- }
128
- function deriveProjectGroupingName(projectKey) {
129
- const githubRemotePrefix = 'remote:github.com/';
130
- if (projectKey.startsWith(githubRemotePrefix)) {
131
- return projectKey.slice(githubRemotePrefix.length) || projectKey;
132
- }
133
- const segments = projectKey.split(/[\\/]/).filter(Boolean);
134
- return segments[segments.length - 1] || projectKey;
135
- }
136
78
  class SessionRequestError extends Error {
137
79
  constructor(code, message) {
138
80
  super(message);
@@ -260,7 +202,7 @@ export class Session {
260
202
  attention: 3,
261
203
  done: 4,
262
204
  };
263
- const { clientId, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
205
+ const { clientId, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, projectRegistry, workspaceRegistry, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
264
206
  this.clientId = clientId;
265
207
  this.sessionId = uuidv4();
266
208
  this.onMessage = onMessage;
@@ -271,6 +213,8 @@ export class Session {
271
213
  this.paseoHome = paseoHome;
272
214
  this.agentManager = agentManager;
273
215
  this.agentStorage = agentStorage;
216
+ this.projectRegistry = projectRegistry;
217
+ this.workspaceRegistry = workspaceRegistry;
274
218
  this.createAgentMcpTransport = createAgentMcpTransport;
275
219
  this.terminalManager = terminalManager;
276
220
  if (this.terminalManager) {
@@ -421,8 +365,12 @@ export class Session {
421
365
  }, 'startAgentStream: requested');
422
366
  let iterator;
423
367
  try {
424
- iterator = this.agentManager.streamAgent(agentId, prompt, runOptions);
425
- this.sessionLogger.trace({ agentId }, 'startAgentStream: streamAgent returned iterator');
368
+ const snapshot = this.agentManager.getAgent(agentId);
369
+ const shouldReplace = Boolean(snapshot && (snapshot.lifecycle === 'running' || snapshot.pendingRun));
370
+ iterator = shouldReplace
371
+ ? this.agentManager.replaceAgentRun(agentId, prompt, runOptions)
372
+ : this.agentManager.streamAgent(agentId, prompt, runOptions);
373
+ this.sessionLogger.trace({ agentId, shouldReplace }, 'startAgentStream: agent iterator returned');
426
374
  }
427
375
  catch (error) {
428
376
  this.handleAgentRunError(agentId, error, 'Failed to start agent run');
@@ -745,57 +693,15 @@ export class Session {
745
693
  });
746
694
  }
747
695
  }
748
- buildFallbackProjectCheckout(cwd) {
749
- return {
750
- cwd,
751
- isGit: false,
752
- currentBranch: null,
753
- remoteUrl: null,
754
- isPaseoOwnedWorktree: false,
755
- mainRepoRoot: null,
756
- };
757
- }
758
- toProjectCheckoutLite(cwd, status) {
759
- if (!status.isGit) {
760
- return this.buildFallbackProjectCheckout(cwd);
761
- }
762
- if (status.isPaseoOwnedWorktree) {
763
- return {
764
- cwd,
765
- isGit: true,
766
- currentBranch: status.currentBranch,
767
- remoteUrl: status.remoteUrl,
768
- isPaseoOwnedWorktree: true,
769
- mainRepoRoot: status.mainRepoRoot,
770
- };
771
- }
772
- return {
773
- cwd,
774
- isGit: true,
775
- currentBranch: status.currentBranch,
776
- remoteUrl: status.remoteUrl,
777
- isPaseoOwnedWorktree: false,
778
- mainRepoRoot: null,
779
- };
780
- }
781
696
  async buildProjectPlacement(cwd) {
782
- const checkout = await getCheckoutStatusLite(cwd, { paseoHome: this.paseoHome })
783
- .then((status) => this.toProjectCheckoutLite(cwd, status))
784
- .catch(() => this.buildFallbackProjectCheckout(cwd));
785
- const projectKey = deriveProjectGroupingKey({
697
+ return buildProjectPlacementForCwd({
786
698
  cwd,
787
- remoteUrl: checkout.remoteUrl,
788
- isPaseoOwnedWorktree: checkout.isPaseoOwnedWorktree,
789
- mainRepoRoot: checkout.mainRepoRoot,
699
+ paseoHome: this.paseoHome,
790
700
  });
791
- return {
792
- projectKey,
793
- projectName: deriveProjectGroupingName(projectKey),
794
- checkout,
795
- };
796
701
  }
797
702
  async forwardAgentUpdate(agent) {
798
703
  try {
704
+ await this.ensureWorkspaceRegistered(agent.cwd);
799
705
  const subscription = this.agentUpdatesSubscription;
800
706
  const payload = await this.buildAgentPayload(agent);
801
707
  if (subscription) {
@@ -975,6 +881,12 @@ export class Session {
975
881
  case 'paseo_worktree_archive_request':
976
882
  await this.handlePaseoWorktreeArchiveRequest(msg);
977
883
  break;
884
+ case 'open_project_request':
885
+ await this.handleOpenProjectRequest(msg);
886
+ break;
887
+ case 'archive_workspace_request':
888
+ await this.handleArchiveWorkspaceRequest(msg);
889
+ break;
978
890
  case 'file_explorer_request':
979
891
  await this.handleFileExplorerRequest(msg);
980
892
  break;
@@ -1222,7 +1134,7 @@ export class Session {
1222
1134
  }
1223
1135
  async handleArchiveAgentRequest(agentId, requestId) {
1224
1136
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
1225
- const { archivedAt, archivedRecord } = await this.archiveAgentState(agentId);
1137
+ const { archivedAt } = await this.archiveAgentState(agentId);
1226
1138
  this.emit({
1227
1139
  type: 'agent_archived',
1228
1140
  payload: {
@@ -1231,11 +1143,6 @@ export class Session {
1231
1143
  requestId,
1232
1144
  },
1233
1145
  });
1234
- await this.maybeArchiveWorktreeAfterLastAgentArchived({
1235
- archivedAgentId: agentId,
1236
- archivedAgentCwd: archivedRecord.cwd,
1237
- requestId,
1238
- });
1239
1146
  }
1240
1147
  async archiveAgentState(agentId) {
1241
1148
  if (this.agentManager.getAgent(agentId)) {
@@ -1773,14 +1680,6 @@ export class Session {
1773
1680
  this.handleAgentRunError(agentId, error, 'Failed to initialize agent before sending prompt');
1774
1681
  return;
1775
1682
  }
1776
- try {
1777
- await this.interruptAgentIfRunning(agentId);
1778
- }
1779
- catch (error) {
1780
- this.handleAgentRunError(agentId, error, 'Failed to interrupt running agent before sending prompt');
1781
- return;
1782
- }
1783
- const prompt = this.buildAgentPrompt(text, images);
1784
1683
  try {
1785
1684
  this.agentManager.recordUserMessage(agentId, text, {
1786
1685
  messageId,
@@ -1790,6 +1689,7 @@ export class Session {
1790
1689
  catch (error) {
1791
1690
  this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
1792
1691
  }
1692
+ const prompt = this.buildAgentPrompt(text, images);
1793
1693
  this.startAgentStream(agentId, prompt, runOptions);
1794
1694
  }
1795
1695
  /**
@@ -1809,6 +1709,7 @@ export class Session {
1809
1709
  ...(provisionalTitle ? { title: provisionalTitle } : {}),
1810
1710
  };
1811
1711
  const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(resolvedConfig, git, worktreeName, labels);
1712
+ await this.ensureWorkspaceRegistered(sessionConfig.cwd);
1812
1713
  const snapshot = await this.agentManager.createAgent(sessionConfig, undefined, { labels });
1813
1714
  await this.forwardAgentUpdate(snapshot);
1814
1715
  if (requestId) {
@@ -3469,59 +3370,6 @@ export class Session {
3469
3370
  });
3470
3371
  }
3471
3372
  }
3472
- async maybeArchiveWorktreeAfterLastAgentArchived(options) {
3473
- try {
3474
- const ownership = await isPaseoOwnedWorktreeCwd(options.archivedAgentCwd, {
3475
- paseoHome: this.paseoHome,
3476
- });
3477
- if (!ownership.allowed) {
3478
- return;
3479
- }
3480
- const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(options.archivedAgentCwd, {
3481
- paseoHome: this.paseoHome,
3482
- });
3483
- if (!resolvedWorktree) {
3484
- return;
3485
- }
3486
- const records = await this.agentStorage.list();
3487
- const recordsById = new Map(records.map((record) => [record.id, record]));
3488
- const targetPath = resolvedWorktree.worktreePath;
3489
- const hasRemainingNonArchivedRecord = records.some((record) => {
3490
- if (record.id === options.archivedAgentId || record.archivedAt) {
3491
- return false;
3492
- }
3493
- return this.isPathWithinRoot(targetPath, record.cwd);
3494
- });
3495
- if (hasRemainingNonArchivedRecord) {
3496
- return;
3497
- }
3498
- const hasUnknownLiveAgent = this.agentManager.listAgents().some((agent) => {
3499
- if (agent.id === options.archivedAgentId) {
3500
- return false;
3501
- }
3502
- if (!this.isPathWithinRoot(targetPath, agent.cwd)) {
3503
- return false;
3504
- }
3505
- return !recordsById.has(agent.id);
3506
- });
3507
- if (hasUnknownLiveAgent) {
3508
- return;
3509
- }
3510
- const repoRoot = ownership.repoRoot;
3511
- if (!repoRoot) {
3512
- this.sessionLogger.warn({ agentId: options.archivedAgentId, worktreePath: targetPath }, 'Unable to resolve repo root for auto-archive after agent archive');
3513
- return;
3514
- }
3515
- await this.archivePaseoWorktree({
3516
- targetPath,
3517
- repoRoot,
3518
- requestId: options.requestId,
3519
- });
3520
- }
3521
- catch (error) {
3522
- this.sessionLogger.warn({ err: error, agentId: options.archivedAgentId, cwd: options.archivedAgentCwd }, 'Failed to auto-archive worktree after agent archive');
3523
- }
3524
- }
3525
3373
  async archivePaseoWorktree(options) {
3526
3374
  let targetPath = options.targetPath;
3527
3375
  const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
@@ -3532,11 +3380,13 @@ export class Session {
3532
3380
  }
3533
3381
  const removedAgents = new Set();
3534
3382
  const affectedWorkspaceCwds = new Set([targetPath]);
3383
+ const affectedWorkspaceIds = new Set([normalizePersistedWorkspaceId(targetPath)]);
3535
3384
  const agents = this.agentManager.listAgents();
3536
3385
  for (const agent of agents) {
3537
3386
  if (this.isPathWithinRoot(targetPath, agent.cwd)) {
3538
3387
  removedAgents.add(agent.id);
3539
3388
  affectedWorkspaceCwds.add(agent.cwd);
3389
+ affectedWorkspaceIds.add(normalizePersistedWorkspaceId(agent.cwd));
3540
3390
  try {
3541
3391
  await this.agentManager.closeAgent(agent.id);
3542
3392
  }
@@ -3556,6 +3406,7 @@ export class Session {
3556
3406
  if (this.isPathWithinRoot(targetPath, record.cwd)) {
3557
3407
  removedAgents.add(record.id);
3558
3408
  affectedWorkspaceCwds.add(record.cwd);
3409
+ affectedWorkspaceIds.add(normalizePersistedWorkspaceId(record.cwd));
3559
3410
  try {
3560
3411
  await this.agentStorage.remove(record.id);
3561
3412
  }
@@ -3570,6 +3421,9 @@ export class Session {
3570
3421
  worktreePath: targetPath,
3571
3422
  paseoHome: this.paseoHome,
3572
3423
  });
3424
+ for (const workspaceId of affectedWorkspaceIds) {
3425
+ await this.archiveWorkspaceRecord(workspaceId);
3426
+ }
3573
3427
  for (const agentId of removedAgents) {
3574
3428
  this.emit({
3575
3429
  type: 'agent_deleted',
@@ -4109,13 +3963,6 @@ export class Session {
4109
3963
  },
4110
3964
  };
4111
3965
  }
4112
- normalizeWorkspaceId(cwd) {
4113
- const trimmed = cwd.trim();
4114
- if (!trimmed) {
4115
- return cwd;
4116
- }
4117
- return resolve(trimmed);
4118
- }
4119
3966
  deriveWorkspaceStateBucket(agent) {
4120
3967
  const pendingPermissionCount = agent.pendingPermissions?.length ?? 0;
4121
3968
  if (pendingPermissionCount > 0 || agent.attentionReason === 'permission') {
@@ -4132,18 +3979,6 @@ export class Session {
4132
3979
  }
4133
3980
  return 'done';
4134
3981
  }
4135
- deriveWorkspaceDirectoryName(cwd) {
4136
- const normalized = cwd.replace(/\\/g, '/');
4137
- const segments = normalized.split('/').filter(Boolean);
4138
- return segments[segments.length - 1] ?? cwd;
4139
- }
4140
- deriveWorkspaceName(input) {
4141
- const branch = input.checkout.currentBranch?.trim() ?? null;
4142
- if (branch && branch.toUpperCase() !== 'HEAD') {
4143
- return branch;
4144
- }
4145
- return this.deriveWorkspaceDirectoryName(input.cwd);
4146
- }
4147
3982
  accumulateLatestActivityAt(current, agent) {
4148
3983
  const candidateRaw = agent.lastUserMessageAt ?? agent.updatedAt;
4149
3984
  const candidateMs = Date.parse(candidateRaw);
@@ -4159,39 +3994,60 @@ export class Session {
4159
3994
  }
4160
3995
  return current;
4161
3996
  }
3997
+ async describeWorkspaceRecord(workspace, projectRecord) {
3998
+ const resolvedProjectRecord = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
3999
+ let displayName = workspace.displayName;
4000
+ try {
4001
+ const placement = await this.buildProjectPlacement(workspace.cwd);
4002
+ displayName = deriveWorkspaceDisplayName({
4003
+ cwd: workspace.cwd,
4004
+ checkout: placement.checkout,
4005
+ });
4006
+ }
4007
+ catch {
4008
+ // Fall back to the persisted label if checkout metadata is unavailable.
4009
+ }
4010
+ let diffStat = null;
4011
+ try {
4012
+ diffStat = await getCheckoutShortstat(workspace.cwd);
4013
+ }
4014
+ catch {
4015
+ // Non-critical — leave null on failure.
4016
+ }
4017
+ return {
4018
+ id: workspace.workspaceId,
4019
+ projectId: workspace.projectId,
4020
+ projectDisplayName: resolvedProjectRecord?.displayName ?? workspace.projectId,
4021
+ projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd,
4022
+ projectKind: resolvedProjectRecord?.kind ?? 'non_git',
4023
+ workspaceKind: workspace.kind,
4024
+ name: displayName,
4025
+ status: 'done',
4026
+ activityAt: null,
4027
+ diffStat,
4028
+ };
4029
+ }
4162
4030
  async listWorkspaceDescriptors() {
4163
- const agents = await this.listAgentPayloads();
4031
+ const [agents, persistedWorkspaces, persistedProjects] = await Promise.all([
4032
+ this.listAgentPayloads(),
4033
+ this.workspaceRegistry.list(),
4034
+ this.projectRegistry.list(),
4035
+ ]);
4036
+ const activeRecords = persistedWorkspaces.filter((workspace) => !workspace.archivedAt);
4037
+ const activeProjects = new Map(persistedProjects
4038
+ .filter((project) => !project.archivedAt)
4039
+ .map((project) => [project.projectId, project]));
4164
4040
  const descriptorsByWorkspaceId = new Map();
4165
- const placementByWorkspaceId = new Map();
4166
- const getPlacement = (workspaceCwd) => {
4167
- const key = this.normalizeWorkspaceId(workspaceCwd);
4168
- const existing = placementByWorkspaceId.get(key);
4169
- if (existing) {
4170
- return existing;
4171
- }
4172
- const next = this.buildProjectPlacement(workspaceCwd);
4173
- placementByWorkspaceId.set(key, next);
4174
- return next;
4175
- };
4041
+ for (const workspace of activeRecords) {
4042
+ descriptorsByWorkspaceId.set(workspace.workspaceId, await this.describeWorkspaceRecord(workspace, activeProjects.get(workspace.projectId) ?? null));
4043
+ }
4176
4044
  for (const agent of agents) {
4177
4045
  if (agent.archivedAt) {
4178
4046
  continue;
4179
4047
  }
4180
- const workspaceId = this.normalizeWorkspaceId(agent.cwd);
4181
- const placement = await getPlacement(workspaceId);
4048
+ const workspaceId = normalizePersistedWorkspaceId(agent.cwd);
4182
4049
  const existing = descriptorsByWorkspaceId.get(workspaceId);
4183
4050
  if (!existing) {
4184
- const bucket = this.deriveWorkspaceStateBucket(agent);
4185
- descriptorsByWorkspaceId.set(workspaceId, {
4186
- id: workspaceId,
4187
- projectId: placement.projectKey,
4188
- name: this.deriveWorkspaceName({
4189
- cwd: workspaceId,
4190
- checkout: placement.checkout,
4191
- }),
4192
- status: bucket,
4193
- activityAt: this.accumulateLatestActivityAt(null, agent),
4194
- });
4195
4051
  continue;
4196
4052
  }
4197
4053
  const bucket = this.deriveWorkspaceStateBucket(agent);
@@ -4399,12 +4255,62 @@ export class Session {
4399
4255
  });
4400
4256
  }
4401
4257
  }
4258
+ async ensureWorkspaceRegistered(cwd) {
4259
+ const workspaceId = normalizePersistedWorkspaceId(cwd);
4260
+ const existing = await this.workspaceRegistry.get(workspaceId);
4261
+ if (existing && !existing.archivedAt) {
4262
+ return existing;
4263
+ }
4264
+ const placement = await this.buildProjectPlacement(workspaceId);
4265
+ const now = new Date().toISOString();
4266
+ const projectExisting = await this.projectRegistry.get(placement.projectKey);
4267
+ const projectRecord = createPersistedProjectRecord({
4268
+ projectId: placement.projectKey,
4269
+ rootPath: deriveProjectRootPath({
4270
+ cwd: workspaceId,
4271
+ checkout: placement.checkout,
4272
+ }),
4273
+ kind: deriveProjectKind(placement.checkout),
4274
+ displayName: placement.projectName,
4275
+ createdAt: projectExisting?.createdAt ?? now,
4276
+ updatedAt: now,
4277
+ archivedAt: null,
4278
+ });
4279
+ await this.projectRegistry.upsert(projectRecord);
4280
+ const workspaceRecord = createPersistedWorkspaceRecord({
4281
+ workspaceId,
4282
+ projectId: placement.projectKey,
4283
+ cwd: workspaceId,
4284
+ kind: deriveWorkspaceKind(placement.checkout),
4285
+ displayName: deriveWorkspaceDisplayName({
4286
+ cwd: workspaceId,
4287
+ checkout: placement.checkout,
4288
+ }),
4289
+ createdAt: existing?.createdAt ?? now,
4290
+ updatedAt: now,
4291
+ archivedAt: null,
4292
+ });
4293
+ await this.workspaceRegistry.upsert(workspaceRecord);
4294
+ return workspaceRecord;
4295
+ }
4296
+ async archiveWorkspaceRecord(workspaceId, archivedAt) {
4297
+ const existing = await this.workspaceRegistry.get(workspaceId);
4298
+ if (!existing || existing.archivedAt) {
4299
+ return;
4300
+ }
4301
+ const nextArchivedAt = archivedAt ?? new Date().toISOString();
4302
+ await this.workspaceRegistry.archive(workspaceId, nextArchivedAt);
4303
+ const siblingWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === existing.projectId && !workspace.archivedAt);
4304
+ if (siblingWorkspaces.length === 0) {
4305
+ await this.projectRegistry.archive(existing.projectId, nextArchivedAt);
4306
+ }
4307
+ }
4402
4308
  async emitWorkspaceUpdateForCwd(cwd) {
4403
4309
  const subscription = this.workspaceUpdatesSubscription;
4404
4310
  if (!subscription) {
4405
4311
  return;
4406
4312
  }
4407
- const workspaceId = this.normalizeWorkspaceId(cwd);
4313
+ const workspaceId = normalizePersistedWorkspaceId(cwd);
4408
4314
  const all = await this.listWorkspaceDescriptors();
4409
4315
  const workspace = all.find((entry) => entry.id === workspaceId);
4410
4316
  if (!workspace) {
@@ -4432,7 +4338,7 @@ export class Session {
4432
4338
  }
4433
4339
  const uniqueWorkspaceCwds = new Set();
4434
4340
  for (const cwd of cwds) {
4435
- const normalized = this.normalizeWorkspaceId(cwd);
4341
+ const normalized = normalizePersistedWorkspaceId(cwd);
4436
4342
  if (!normalized) {
4437
4343
  continue;
4438
4344
  }
@@ -4552,6 +4458,69 @@ export class Session {
4552
4458
  });
4553
4459
  }
4554
4460
  }
4461
+ async handleOpenProjectRequest(request) {
4462
+ try {
4463
+ const workspace = await this.ensureWorkspaceRegistered(request.cwd);
4464
+ await this.emitWorkspaceUpdateForCwd(workspace.cwd);
4465
+ const descriptor = await this.describeWorkspaceRecord(workspace);
4466
+ this.emit({
4467
+ type: 'open_project_response',
4468
+ payload: {
4469
+ requestId: request.requestId,
4470
+ workspace: descriptor,
4471
+ error: null,
4472
+ },
4473
+ });
4474
+ }
4475
+ catch (error) {
4476
+ const message = error instanceof Error ? error.message : 'Failed to open project';
4477
+ this.sessionLogger.error({ err: error, cwd: request.cwd }, 'Failed to open project');
4478
+ this.emit({
4479
+ type: 'open_project_response',
4480
+ payload: {
4481
+ requestId: request.requestId,
4482
+ workspace: null,
4483
+ error: message,
4484
+ },
4485
+ });
4486
+ }
4487
+ }
4488
+ async handleArchiveWorkspaceRequest(request) {
4489
+ try {
4490
+ const existing = await this.workspaceRegistry.get(request.workspaceId);
4491
+ if (!existing) {
4492
+ throw new Error(`Workspace not found: ${request.workspaceId}`);
4493
+ }
4494
+ if (existing.kind === 'worktree') {
4495
+ throw new Error('Use worktree archive for Paseo worktrees');
4496
+ }
4497
+ const archivedAt = new Date().toISOString();
4498
+ await this.archiveWorkspaceRecord(request.workspaceId, archivedAt);
4499
+ await this.emitWorkspaceUpdateForCwd(existing.cwd);
4500
+ this.emit({
4501
+ type: 'archive_workspace_response',
4502
+ payload: {
4503
+ requestId: request.requestId,
4504
+ workspaceId: request.workspaceId,
4505
+ archivedAt,
4506
+ error: null,
4507
+ },
4508
+ });
4509
+ }
4510
+ catch (error) {
4511
+ const message = error instanceof Error ? error.message : 'Failed to archive workspace';
4512
+ this.sessionLogger.error({ err: error, workspaceId: request.workspaceId }, 'Failed to archive workspace');
4513
+ this.emit({
4514
+ type: 'archive_workspace_response',
4515
+ payload: {
4516
+ requestId: request.requestId,
4517
+ workspaceId: request.workspaceId,
4518
+ archivedAt: null,
4519
+ error: message,
4520
+ },
4521
+ });
4522
+ }
4523
+ }
4555
4524
  async handleFetchAgent(agentIdOrIdentifier, requestId) {
4556
4525
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4557
4526
  if (!resolved.ok) {
@@ -4731,7 +4700,6 @@ export class Session {
4731
4700
  const agentId = resolved.agentId;
4732
4701
  await this.unarchiveAgentState(agentId);
4733
4702
  await this.ensureAgentLoaded(agentId);
4734
- await this.interruptAgentIfRunning(agentId);
4735
4703
  try {
4736
4704
  this.agentManager.recordUserMessage(agentId, msg.text, {
4737
4705
  messageId: msg.messageId,