@getpaseo/server 0.1.95 → 0.1.97-beta.1
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/dist/server/{utils/executable.d.ts → executable-resolution/executable-resolution.d.ts} +2 -2
- package/dist/server/{utils/executable.js → executable-resolution/executable-resolution.js} +16 -14
- package/dist/server/executable-resolution/windows.d.ts +18 -0
- package/dist/server/executable-resolution/windows.js +62 -0
- package/dist/server/server/agent/agent-loading.js +4 -1
- package/dist/server/server/agent/agent-manager.d.ts +10 -2
- package/dist/server/server/agent/agent-manager.js +34 -46
- package/dist/server/server/agent/agent-projections.js +3 -0
- package/dist/server/server/agent/agent-prompt.js +19 -1
- package/dist/server/server/agent/agent-response-loop.js +2 -4
- package/dist/server/server/agent/agent-storage.d.ts +18 -19
- package/dist/server/server/agent/agent-storage.js +6 -23
- package/dist/server/server/agent/create-agent/create.d.ts +2 -12
- package/dist/server/server/agent/create-agent/create.js +28 -30
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +4 -2
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +31 -22
- package/dist/server/server/agent/import-sessions.d.ts +1 -10
- package/dist/server/server/agent/import-sessions.js +1 -53
- package/dist/server/server/agent/lifecycle-command.js +5 -4
- package/dist/server/server/agent/mcp-server.d.ts +8 -5
- package/dist/server/server/agent/mcp-server.js +41 -14
- package/dist/server/server/agent/mcp-shared.d.ts +6 -3
- package/dist/server/server/agent/mcp-shared.js +3 -0
- package/dist/server/server/agent/provider-launch-config.js +1 -1
- package/dist/server/server/agent/providers/acp-agent.d.ts +5 -0
- package/dist/server/server/agent/providers/acp-agent.js +31 -26
- package/dist/server/server/agent/providers/claude/agent.js +45 -6
- package/dist/server/server/agent/providers/codex-app-server-agent.js +1 -1
- package/dist/server/server/agent/providers/copilot-acp-agent.js +1 -0
- package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +0 -7
- package/dist/server/server/agent/providers/cursor-acp-agent.js +0 -78
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.js +73 -1
- package/dist/server/server/agent/providers/opencode/server-manager.js +1 -1
- package/dist/server/server/agent/structured-generation-providers.js +45 -1
- package/dist/server/server/agent-attention-policy.d.ts +12 -3
- package/dist/server/server/agent-attention-policy.js +15 -3
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +7 -6
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +21 -16
- package/dist/server/server/bootstrap.d.ts +3 -0
- package/dist/server/server/bootstrap.js +91 -12
- package/dist/server/server/config.js +1 -0
- package/dist/server/server/daemon-config-store.js +1 -0
- package/dist/server/server/exports.d.ts +1 -1
- package/dist/server/server/exports.js +1 -1
- package/dist/server/server/loop-service.d.ts +24 -24
- package/dist/server/server/migrations/backfill-workspace-id.migration.d.ts +9 -0
- package/dist/server/server/migrations/backfill-workspace-id.migration.js +60 -0
- package/dist/server/server/paseo-worktree-service.d.ts +9 -0
- package/dist/server/server/paseo-worktree-service.js +71 -12
- package/dist/server/server/path-utils.d.ts +1 -0
- package/dist/server/server/path-utils.js +6 -1
- package/dist/server/server/persisted-config.d.ts +7 -0
- package/dist/server/server/persisted-config.js +1 -0
- package/dist/server/server/persistence-hooks.d.ts +1 -0
- package/dist/server/server/persistence-hooks.js +13 -5
- package/dist/server/server/resolve-workspace-id-for-path.d.ts +3 -0
- package/dist/server/server/resolve-workspace-id-for-path.js +41 -0
- package/dist/server/server/script-proxy.d.ts +1 -1
- package/dist/server/server/script-proxy.js +1 -1
- package/dist/server/server/service-proxy.js +1 -1
- package/dist/server/server/session.d.ts +31 -6
- package/dist/server/server/session.js +640 -196
- package/dist/server/server/websocket-server.d.ts +5 -0
- package/dist/server/server/websocket-server.js +137 -3
- package/dist/server/server/workspace-archive-service.d.ts +60 -3
- package/dist/server/server/workspace-archive-service.js +217 -4
- package/dist/server/server/workspace-directory.d.ts +20 -2
- package/dist/server/server/workspace-directory.js +148 -70
- package/dist/server/server/workspace-git-service.js +21 -21
- package/dist/server/server/workspace-reconciliation-service.d.ts +1 -1
- package/dist/server/server/workspace-reconciliation-service.js +21 -22
- package/dist/server/server/workspace-registry-bootstrap.js +23 -10
- package/dist/server/server/workspace-registry-model.d.ts +3 -3
- package/dist/server/server/workspace-registry-model.js +9 -10
- package/dist/server/server/workspace-registry.d.ts +17 -4
- package/dist/server/server/workspace-registry.js +27 -0
- package/dist/server/server/worktree/commands.d.ts +7 -5
- package/dist/server/server/worktree/commands.js +38 -18
- package/dist/server/server/worktree-bootstrap.d.ts +1 -0
- package/dist/server/server/worktree-bootstrap.js +4 -1
- package/dist/server/server/worktree-branch-name-generator.d.ts +5 -1
- package/dist/server/server/worktree-branch-name-generator.js +8 -2
- package/dist/server/server/worktree-session.d.ts +4 -5
- package/dist/server/server/worktree-session.js +9 -3
- package/dist/server/services/github-service.js +1 -1
- package/dist/server/terminal/activity/terminal-activity-tracker.d.ts +20 -0
- package/dist/server/terminal/activity/terminal-activity-tracker.js +59 -0
- package/dist/server/terminal/agent-hooks/agent-hook-installer.d.ts +62 -0
- package/dist/server/terminal/agent-hooks/agent-hook-installer.js +117 -0
- package/dist/server/terminal/agent-hooks/claude/claude-settings.d.ts +7 -0
- package/dist/server/terminal/agent-hooks/claude/claude-settings.js +88 -0
- package/dist/server/terminal/agent-hooks/claude/claude.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/claude/claude.js +47 -0
- package/dist/server/terminal/agent-hooks/codex/codex-settings.d.ts +7 -0
- package/dist/server/terminal/agent-hooks/codex/codex-settings.js +99 -0
- package/dist/server/terminal/agent-hooks/codex/codex.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/codex/codex.js +30 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.js +46 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode.d.ts +3 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode.js +23 -0
- package/dist/server/terminal/agent-hooks/provider-registry.d.ts +24 -0
- package/dist/server/terminal/agent-hooks/provider-registry.js +36 -0
- package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.d.ts +10 -0
- package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.js +26 -0
- package/dist/server/terminal/terminal-manager-factory.d.ts +4 -1
- package/dist/server/terminal/terminal-manager-factory.js +2 -2
- package/dist/server/terminal/terminal-manager.d.ts +33 -2
- package/dist/server/terminal/terminal-manager.js +144 -18
- package/dist/server/terminal/terminal-output-coalescer.d.ts +4 -0
- package/dist/server/terminal/terminal-output-coalescer.js +18 -0
- package/dist/server/terminal/terminal-restore.d.ts +1 -0
- package/dist/server/terminal/terminal-restore.js +6 -0
- package/dist/server/terminal/terminal-session-controller.d.ts +4 -2
- package/dist/server/terminal/terminal-session-controller.js +65 -24
- package/dist/server/terminal/terminal-worker-process.js +146 -63
- package/dist/server/terminal/terminal-worker-protocol.d.ts +19 -14
- package/dist/server/terminal/terminal.d.ts +42 -0
- package/dist/server/terminal/terminal.js +235 -16
- package/dist/server/terminal/worker-terminal-manager.d.ts +1 -0
- package/dist/server/terminal/worker-terminal-manager.js +220 -36
- package/dist/server/utils/build-metadata-prompt.d.ts +1 -1
- package/dist/server/utils/github-remote.js +1 -1
- package/dist/server/utils/tree-kill.d.ts +2 -2
- package/dist/src/{utils/executable.js → executable-resolution/executable-resolution.js} +16 -14
- package/dist/src/executable-resolution/windows.js +62 -0
- package/dist/src/server/agent/provider-launch-config.js +1 -1
- package/dist/src/server/persisted-config.js +1 -0
- package/package.json +10 -5
- package/dist/server/server/agent/agent-metadata-generator.d.ts +0 -36
- package/dist/server/server/agent/agent-metadata-generator.js +0 -112
- package/dist/server/server/paseo-worktree-archive-service.d.ts +0 -41
- package/dist/server/server/paseo-worktree-archive-service.js +0 -144
|
@@ -2,7 +2,7 @@ import equal from "fast-deep-equal";
|
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
|
-
import {
|
|
5
|
+
import { resolve, sep } from "path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
|
|
@@ -42,8 +42,9 @@ import { StructuredAgentFallbackError, StructuredAgentResponseError, generateStr
|
|
|
42
42
|
import { resolveStructuredGenerationProviders, } from "./agent/structured-generation-providers.js";
|
|
43
43
|
import { getAgentStreamEventTurnId, } from "./agent/agent-sdk-types.js";
|
|
44
44
|
import { ImportSessionsRequestError, importProviderSession, listImportableProviderSessions, normalizeImportAgentRequest, } from "./agent/import-sessions.js";
|
|
45
|
-
import { checkoutLiteFromGitSnapshot,
|
|
46
|
-
import {
|
|
45
|
+
import { checkoutLiteFromGitSnapshot, classifyDirectoryForProjectMembership, deriveWorkspaceDisplayName, generateWorkspaceId, } from "./workspace-registry-model.js";
|
|
46
|
+
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
|
|
47
|
+
import { createPersistedProjectRecord, createPersistedWorkspaceRecord, resolveProjectDisplayName, resolveWorkspaceDisplayName, resolveWorkspaceName, } from "./workspace-registry.js";
|
|
47
48
|
import { buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, wrapSpokenInput, } from "./voice-config.js";
|
|
48
49
|
import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
|
|
49
50
|
import { listDirectoryEntries, readExplorerFile, readExplorerFileBytes, getDownloadableFileInfo, } from "./file-explorer/service.js";
|
|
@@ -51,7 +52,7 @@ import { readPaseoConfigForEdit, writePaseoConfigForEdit, } from "../utils/paseo
|
|
|
51
52
|
import { buildMetadataPrompt } from "../utils/build-metadata-prompt.js";
|
|
52
53
|
import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
|
|
53
54
|
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
|
54
|
-
import { checkoutResolvedBranch, commitChanges, mergeToBase, mergeFromBase, pullCurrentBranch, pushCurrentBranch, createPullRequest, renameCurrentBranch, } from "../utils/checkout-git.js";
|
|
55
|
+
import { checkoutResolvedBranch, commitChanges, mergeToBase, mergeFromBase, pullCurrentBranch, pushCurrentBranch, createPullRequest, renameCurrentBranch as renameCurrentBranchDefault, } from "../utils/checkout-git.js";
|
|
55
56
|
import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
|
56
57
|
import { getProjectIcon } from "../utils/project-icon.js";
|
|
57
58
|
import { expandTilde } from "../utils/path.js";
|
|
@@ -63,11 +64,12 @@ import { ChatServiceError, parseMentionAgentIds, } from "./chat/chat-service.js"
|
|
|
63
64
|
import { notifyChatMentions, prepareChatMentionFanout } from "./chat/chat-mentions.js";
|
|
64
65
|
import { execCommand } from "../utils/spawn.js";
|
|
65
66
|
import { assertPullRequestAutoMergeDisableReady, assertPullRequestAutoMergeEnableReady, createGitHubService, } from "../services/github-service.js";
|
|
66
|
-
import { summarizeFetchWorkspacesEntries, WorkspaceDirectory, } from "./workspace-directory.js";
|
|
67
|
+
import { summarizeFetchWorkspacesEntries, workspaceIdsOnCheckout, WorkspaceDirectory, } from "./workspace-directory.js";
|
|
67
68
|
import { shouldEmitPendingBootstrapUpdate } from "./workspace-bootstrap-dedupe.js";
|
|
68
|
-
import { attemptFirstAgentBranchAutoName, createPaseoWorktree, } from "./paseo-worktree-service.js";
|
|
69
|
-
import { generateBranchNameFromFirstAgentContext } from "./worktree-branch-name-generator.js";
|
|
69
|
+
import { attemptFirstAgentBranchAutoName, createLocalCheckoutWorkspace, createPaseoWorktree, } from "./paseo-worktree-service.js";
|
|
70
|
+
import { generateBranchNameFromFirstAgentContext, } from "./worktree-branch-name-generator.js";
|
|
70
71
|
import { assertSafeGitRef as assertWorktreeSafeGitRef, buildAgentSessionConfig as buildWorktreeAgentSessionConfig, createPaseoWorktreeWorkflow as createWorktreeWorkflow, handleCreatePaseoWorktreeRequest as handleCreateWorktreeRequest, handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest, handlePaseoWorktreeListRequest as handleWorktreeListRequest, handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage, } from "./worktree-session.js";
|
|
72
|
+
import { archiveByScope } from "./workspace-archive-service.js";
|
|
71
73
|
import { toWorktreeWireError } from "./worktree-errors.js";
|
|
72
74
|
import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
|
|
73
75
|
const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
|
|
@@ -136,7 +138,12 @@ function diffChangeTypeFor(file) {
|
|
|
136
138
|
return "D";
|
|
137
139
|
return "M";
|
|
138
140
|
}
|
|
139
|
-
function buildWorkspaceCheckout(workspace, project
|
|
141
|
+
function buildWorkspaceCheckout(workspace, project,
|
|
142
|
+
// The persisted `branch` field is the source of truth, but it is null for
|
|
143
|
+
// records created before branch was lifted to its own field (no migrations,
|
|
144
|
+
// per data-model.md) and for any path that didn't backfill it. Fall back to
|
|
145
|
+
// the live git branch so checkout.currentBranch never regresses to null.
|
|
146
|
+
fallbackBranch) {
|
|
140
147
|
if (project.kind !== "git") {
|
|
141
148
|
return {
|
|
142
149
|
cwd: workspace.cwd,
|
|
@@ -148,11 +155,12 @@ function buildWorkspaceCheckout(workspace, project) {
|
|
|
148
155
|
mainRepoRoot: null,
|
|
149
156
|
};
|
|
150
157
|
}
|
|
158
|
+
const currentBranch = workspace.branch ?? fallbackBranch ?? null;
|
|
151
159
|
if (workspace.kind === "worktree") {
|
|
152
160
|
return {
|
|
153
161
|
cwd: workspace.cwd,
|
|
154
162
|
isGit: true,
|
|
155
|
-
currentBranch
|
|
163
|
+
currentBranch,
|
|
156
164
|
remoteUrl: null,
|
|
157
165
|
worktreeRoot: workspace.cwd,
|
|
158
166
|
isPaseoOwnedWorktree: true,
|
|
@@ -162,7 +170,7 @@ function buildWorkspaceCheckout(workspace, project) {
|
|
|
162
170
|
return {
|
|
163
171
|
cwd: workspace.cwd,
|
|
164
172
|
isGit: true,
|
|
165
|
-
currentBranch
|
|
173
|
+
currentBranch,
|
|
166
174
|
remoteUrl: null,
|
|
167
175
|
worktreeRoot: workspace.cwd,
|
|
168
176
|
isPaseoOwnedWorktree: false,
|
|
@@ -296,6 +304,7 @@ export class Session {
|
|
|
296
304
|
this.agentMcpClient = null;
|
|
297
305
|
this.agentTools = null;
|
|
298
306
|
this.unsubscribeAgentEvents = null;
|
|
307
|
+
this.unsubscribeTerminalWorkspaceContributionEvents = null;
|
|
299
308
|
this.agentUpdatesSubscription = null;
|
|
300
309
|
this.workspaceUpdatesSubscription = null;
|
|
301
310
|
this.clientActivity = null;
|
|
@@ -331,13 +340,14 @@ export class Session {
|
|
|
331
340
|
}
|
|
332
341
|
},
|
|
333
342
|
});
|
|
334
|
-
const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, filesystem, chatService, scheduleService, loopService, checkoutDiffManager, github, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
|
|
343
|
+
const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, getTransportBufferedAmount, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, filesystem, chatService, scheduleService, loopService, checkoutDiffManager, github, renameCurrentBranch, generateWorkspaceName, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
|
|
335
344
|
this.clientId = clientId;
|
|
336
345
|
this.appVersion = appVersion ?? null;
|
|
337
346
|
this.clientCapabilities = parseClientCapabilities(clientCapabilities);
|
|
338
347
|
this.sessionId = uuidv4();
|
|
339
348
|
this.onMessage = onMessage;
|
|
340
349
|
this.onBinaryMessage = onBinaryMessage ?? null;
|
|
350
|
+
this.getTransportBufferedAmount = getTransportBufferedAmount ?? (() => 0);
|
|
341
351
|
this.onLifecycleIntent = onLifecycleIntent ?? null;
|
|
342
352
|
this.downloadTokenStore = downloadTokenStore;
|
|
343
353
|
this.pushTokenStore = pushTokenStore;
|
|
@@ -359,6 +369,8 @@ export class Session {
|
|
|
359
369
|
this.loopService = loopService;
|
|
360
370
|
this.checkoutDiffManager = checkoutDiffManager;
|
|
361
371
|
this.github = github ?? createGitHubService();
|
|
372
|
+
this.renameCurrentBranch = renameCurrentBranch ?? renameCurrentBranchDefault;
|
|
373
|
+
this.generateWorkspaceName = generateWorkspaceName ?? generateBranchNameFromFirstAgentContext;
|
|
362
374
|
this.workspaceGitService = workspaceGitService;
|
|
363
375
|
this.daemonConfigStore = daemonConfigStore;
|
|
364
376
|
this.mcpBaseUrl = mcpBaseUrl ?? null;
|
|
@@ -377,6 +389,7 @@ export class Session {
|
|
|
377
389
|
.map((workspace) => workspace.cwd);
|
|
378
390
|
},
|
|
379
391
|
clientSupportsWrapReflow: () => this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
|
|
392
|
+
getClientBufferedAmount: () => this.getTransportBufferedAmount(),
|
|
380
393
|
});
|
|
381
394
|
this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
|
|
382
395
|
paseoHome: this.paseoHome,
|
|
@@ -387,6 +400,8 @@ export class Session {
|
|
|
387
400
|
workspaceGitService: this.workspaceGitService,
|
|
388
401
|
createPaseoWorktreeWorkflow: (input, workflowOptions) => this.createPaseoWorktreeWorkflow(input, workflowOptions),
|
|
389
402
|
archiveAgentForClose: (agentId) => this.archiveAgentForClose(agentId),
|
|
403
|
+
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
|
|
404
|
+
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
|
|
390
405
|
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
|
391
406
|
emit: (message) => this.emit(message),
|
|
392
407
|
emitAgentRemove: (agentId) => {
|
|
@@ -400,8 +415,7 @@ export class Session {
|
|
|
400
415
|
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
|
|
401
416
|
markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
|
|
402
417
|
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
|
|
403
|
-
|
|
404
|
-
killTerminalsUnderPath: (rootPath) => this.terminalController.killTerminalsUnderPath(rootPath),
|
|
418
|
+
killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
|
|
405
419
|
logger: this.sessionLogger,
|
|
406
420
|
});
|
|
407
421
|
this.providerSnapshotManager = providerSnapshotManager;
|
|
@@ -425,6 +439,7 @@ export class Session {
|
|
|
425
439
|
projectRegistry: this.projectRegistry,
|
|
426
440
|
workspaceRegistry: this.workspaceRegistry,
|
|
427
441
|
listAgentPayloads: () => this.listAgentPayloads(),
|
|
442
|
+
listTerminalActivityContributions: () => this.listTerminalActivityContributions(),
|
|
428
443
|
isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
|
|
429
444
|
buildWorkspaceDescriptor: (input) => this.buildWorkspaceDescriptor(input),
|
|
430
445
|
});
|
|
@@ -477,9 +492,6 @@ export class Session {
|
|
|
477
492
|
async emitWorkspaceUpdatesForExternalWorkspaceIds(workspaceIds) {
|
|
478
493
|
await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds);
|
|
479
494
|
}
|
|
480
|
-
async emitWorkspaceUpdatesForExternalCwds(cwds) {
|
|
481
|
-
await Promise.all(Array.from(cwds, (cwd) => this.emitWorkspaceUpdateForCwd(cwd)));
|
|
482
|
-
}
|
|
483
495
|
async warmWorkspaceGitDataForWorkspace(workspace) {
|
|
484
496
|
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
|
485
497
|
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
|
@@ -626,6 +638,14 @@ export class Session {
|
|
|
626
638
|
*/
|
|
627
639
|
subscribeToOptionalManagers() {
|
|
628
640
|
this.terminalController.start();
|
|
641
|
+
if (this.terminalManager) {
|
|
642
|
+
this.unsubscribeTerminalWorkspaceContributionEvents =
|
|
643
|
+
this.terminalManager.subscribeTerminalWorkspaceContributionChanged((event) => {
|
|
644
|
+
void this.emitWorkspaceUpdateForTerminalContribution(event).catch((error) => {
|
|
645
|
+
this.sessionLogger.warn({ err: error, terminalId: event.terminalId }, "Failed to emit workspace update after terminal contribution changed");
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
629
649
|
const handleProviderSnapshotChange = (entries, cwd) => {
|
|
630
650
|
// COMPAT(providersSnapshot): keep provider visibility gating for older clients.
|
|
631
651
|
const visibleEntries = entries.filter((entry) => this.isProviderVisibleToClient(entry.provider));
|
|
@@ -763,7 +783,7 @@ export class Session {
|
|
|
763
783
|
payload.archivedAt = storedRecord?.archivedAt ?? null;
|
|
764
784
|
return payload;
|
|
765
785
|
}
|
|
766
|
-
buildStoredAgentPayload(record, registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds()) {
|
|
786
|
+
buildStoredAgentPayload(record, registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds())) {
|
|
767
787
|
return buildStoredAgentPayload(record, registeredProviderIds);
|
|
768
788
|
}
|
|
769
789
|
isProviderVisibleToClient(provider) {
|
|
@@ -867,61 +887,37 @@ export class Session {
|
|
|
867
887
|
});
|
|
868
888
|
}
|
|
869
889
|
}
|
|
870
|
-
async findWorkspaceByDirectory(cwd, options) {
|
|
871
|
-
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
|
|
872
|
-
const workspaces = await this.workspaceRegistry.list();
|
|
873
|
-
const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(normalizedCwd, workspaces);
|
|
874
|
-
return workspaces.find((workspace) => workspace.workspaceId === workspaceId) ?? null;
|
|
875
|
-
}
|
|
876
890
|
async findExactWorkspaceByDirectory(cwd, options) {
|
|
877
891
|
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
|
|
878
892
|
const workspaces = await this.workspaceRegistry.list();
|
|
879
893
|
return workspaces.find((workspace) => workspace.cwd === normalizedCwd) ?? null;
|
|
880
894
|
}
|
|
881
895
|
async resolveWorkspaceDirectory(cwd, options) {
|
|
882
|
-
const normalizedCwd =
|
|
896
|
+
const normalizedCwd = resolve(cwd);
|
|
883
897
|
if (options?.refreshGit === false) {
|
|
884
898
|
const snapshot = this.workspaceGitService.peekSnapshot(normalizedCwd);
|
|
885
|
-
return
|
|
899
|
+
return resolve(snapshot?.git.repoRoot ?? normalizedCwd);
|
|
886
900
|
}
|
|
887
901
|
const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
|
|
888
|
-
return
|
|
902
|
+
return resolve(checkout.worktreeRoot ?? normalizedCwd);
|
|
889
903
|
}
|
|
890
904
|
async buildProjectPlacementForWorkspace(workspace, projectRecord) {
|
|
891
905
|
const project = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
|
|
892
906
|
if (!project) {
|
|
893
907
|
throw new Error(`Project not found for workspace ${workspace.workspaceId}`);
|
|
894
908
|
}
|
|
895
|
-
const
|
|
909
|
+
const liveBranch = this.workspaceGitService.peekSnapshot(workspace.cwd)?.git.currentBranch ?? null;
|
|
910
|
+
const checkout = buildWorkspaceCheckout(workspace, project, liveBranch);
|
|
896
911
|
return {
|
|
897
912
|
projectKey: project.projectId,
|
|
898
913
|
projectName: resolveProjectDisplayName(project),
|
|
899
914
|
checkout,
|
|
900
915
|
};
|
|
901
916
|
}
|
|
902
|
-
async
|
|
903
|
-
const workspace = await this.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (!workspace) {
|
|
907
|
-
if (!options?.fallback) {
|
|
908
|
-
return null;
|
|
909
|
-
}
|
|
910
|
-
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
|
911
|
-
return {
|
|
912
|
-
projectKey: normalizedCwd,
|
|
913
|
-
projectName: deriveProjectGroupingName(normalizedCwd),
|
|
914
|
-
checkout: {
|
|
915
|
-
cwd: normalizedCwd,
|
|
916
|
-
isGit: false,
|
|
917
|
-
currentBranch: null,
|
|
918
|
-
remoteUrl: null,
|
|
919
|
-
worktreeRoot: null,
|
|
920
|
-
isPaseoOwnedWorktree: false,
|
|
921
|
-
mainRepoRoot: null,
|
|
922
|
-
},
|
|
923
|
-
};
|
|
924
|
-
}
|
|
917
|
+
async buildProjectPlacementForWorkspaceId(workspaceId) {
|
|
918
|
+
const workspace = await this.workspaceRegistry.get(workspaceId);
|
|
919
|
+
if (!workspace)
|
|
920
|
+
return null;
|
|
925
921
|
return this.buildProjectPlacementForWorkspace(workspace);
|
|
926
922
|
}
|
|
927
923
|
async forwardAgentUpdate(agent) {
|
|
@@ -929,33 +925,41 @@ export class Session {
|
|
|
929
925
|
const subscription = this.agentUpdatesSubscription;
|
|
930
926
|
const payload = await this.buildAgentPayload(agent);
|
|
931
927
|
if (subscription) {
|
|
932
|
-
const project =
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
});
|
|
928
|
+
const project = payload.workspaceId
|
|
929
|
+
? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
|
|
930
|
+
: null;
|
|
936
931
|
if (!project) {
|
|
937
|
-
throw new Error(`Workspace not found for agent ${payload.id}`);
|
|
938
|
-
}
|
|
939
|
-
const matches = this.matchesAgentFilter({
|
|
940
|
-
agent: payload,
|
|
941
|
-
project,
|
|
942
|
-
filter: subscription.filter,
|
|
943
|
-
});
|
|
944
|
-
if (matches) {
|
|
945
932
|
this.bufferOrEmitAgentUpdate(subscription, {
|
|
946
|
-
kind: "
|
|
947
|
-
|
|
948
|
-
project,
|
|
933
|
+
kind: "remove",
|
|
934
|
+
agentId: payload.id,
|
|
949
935
|
});
|
|
950
936
|
}
|
|
951
937
|
else {
|
|
952
|
-
this.
|
|
953
|
-
|
|
954
|
-
|
|
938
|
+
const matches = this.matchesAgentFilter({
|
|
939
|
+
agent: payload,
|
|
940
|
+
project,
|
|
941
|
+
filter: subscription.filter,
|
|
955
942
|
});
|
|
943
|
+
if (matches) {
|
|
944
|
+
this.bufferOrEmitAgentUpdate(subscription, {
|
|
945
|
+
kind: "upsert",
|
|
946
|
+
agent: payload,
|
|
947
|
+
project,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
this.bufferOrEmitAgentUpdate(subscription, {
|
|
952
|
+
kind: "remove",
|
|
953
|
+
agentId: payload.id,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
956
|
}
|
|
957
957
|
}
|
|
958
|
-
|
|
958
|
+
// A lifecycle change updates exactly the agent's owning workspace, never
|
|
959
|
+
// every workspace sharing its cwd. Ownership is the agent's workspaceId.
|
|
960
|
+
if (payload.workspaceId) {
|
|
961
|
+
await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
|
|
962
|
+
}
|
|
959
963
|
}
|
|
960
964
|
catch (error) {
|
|
961
965
|
this.sessionLogger.error({ err: error }, "Failed to emit agent update");
|
|
@@ -1341,8 +1345,12 @@ export class Session {
|
|
|
1341
1345
|
return this.handleOpenProjectRequest(msg);
|
|
1342
1346
|
case "archive_workspace_request":
|
|
1343
1347
|
return this.handleArchiveWorkspaceRequest(msg);
|
|
1348
|
+
case "workspace.create.request":
|
|
1349
|
+
return this.handleWorkspaceCreateRequest(msg);
|
|
1344
1350
|
case "workspace.clear_attention.request":
|
|
1345
1351
|
return this.handleWorkspaceClearAttentionRequest(msg);
|
|
1352
|
+
case "workspace.title.set.request":
|
|
1353
|
+
return this.handleWorkspaceTitleSetRequest(msg.workspaceId, msg.title, msg.requestId);
|
|
1346
1354
|
case "file_explorer_request":
|
|
1347
1355
|
return this.handleFileExplorerRequest(msg);
|
|
1348
1356
|
case "project_icon_request":
|
|
@@ -1506,8 +1514,8 @@ export class Session {
|
|
|
1506
1514
|
}
|
|
1507
1515
|
async handleDeleteAgentRequest(agentId, requestId) {
|
|
1508
1516
|
this.sessionLogger.info({ agentId }, `Deleting agent ${agentId} from registry`);
|
|
1509
|
-
const
|
|
1510
|
-
(await this.agentStorage.get(agentId))?.
|
|
1517
|
+
const knownWorkspaceId = this.agentManager.getAgent(agentId)?.workspaceId ??
|
|
1518
|
+
(await this.agentStorage.get(agentId))?.workspaceId ??
|
|
1511
1519
|
null;
|
|
1512
1520
|
// File-backed storage still needs an early delete fence before closeAgent().
|
|
1513
1521
|
beginAgentDeleteIfSupported(this.agentStorage, agentId);
|
|
@@ -1540,8 +1548,8 @@ export class Session {
|
|
|
1540
1548
|
agentId,
|
|
1541
1549
|
});
|
|
1542
1550
|
}
|
|
1543
|
-
if (
|
|
1544
|
-
await this.
|
|
1551
|
+
if (knownWorkspaceId) {
|
|
1552
|
+
await this.emitWorkspaceUpdateForWorkspaceId(knownWorkspaceId);
|
|
1545
1553
|
}
|
|
1546
1554
|
}
|
|
1547
1555
|
async handleArchiveAgentRequest(agentId, requestId) {
|
|
@@ -1564,7 +1572,9 @@ export class Session {
|
|
|
1564
1572
|
}, agentId);
|
|
1565
1573
|
if (this.agentUpdatesSubscription) {
|
|
1566
1574
|
const payload = this.buildStoredAgentPayload(archivedRecord);
|
|
1567
|
-
const project =
|
|
1575
|
+
const project = payload.workspaceId
|
|
1576
|
+
? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
|
|
1577
|
+
: null;
|
|
1568
1578
|
if (project) {
|
|
1569
1579
|
const matches = this.matchesAgentFilter({
|
|
1570
1580
|
agent: payload,
|
|
@@ -1588,7 +1598,9 @@ export class Session {
|
|
|
1588
1598
|
agentId,
|
|
1589
1599
|
});
|
|
1590
1600
|
}
|
|
1591
|
-
|
|
1601
|
+
if (payload.workspaceId) {
|
|
1602
|
+
await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
|
|
1603
|
+
}
|
|
1592
1604
|
}
|
|
1593
1605
|
return { agentId, archivedAt };
|
|
1594
1606
|
}
|
|
@@ -1752,6 +1764,67 @@ export class Session {
|
|
|
1752
1764
|
});
|
|
1753
1765
|
}
|
|
1754
1766
|
}
|
|
1767
|
+
async handleWorkspaceTitleSetRequest(workspaceId, title, requestId) {
|
|
1768
|
+
this.sessionLogger.info({ workspaceId, requestId, hasTitle: typeof title === "string" }, "session: workspace.title.set.request");
|
|
1769
|
+
try {
|
|
1770
|
+
const existing = await this.workspaceRegistry.get(workspaceId);
|
|
1771
|
+
if (!existing) {
|
|
1772
|
+
this.emit({
|
|
1773
|
+
type: "workspace.title.set.response",
|
|
1774
|
+
payload: {
|
|
1775
|
+
requestId,
|
|
1776
|
+
workspaceId,
|
|
1777
|
+
accepted: false,
|
|
1778
|
+
title: null,
|
|
1779
|
+
error: "Workspace not found",
|
|
1780
|
+
},
|
|
1781
|
+
});
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const trimmed = title?.trim() ?? "";
|
|
1785
|
+
const nextTitle = trimmed.length === 0 ? null : trimmed;
|
|
1786
|
+
await this.workspaceRegistry.upsert({
|
|
1787
|
+
...existing,
|
|
1788
|
+
title: nextTitle,
|
|
1789
|
+
updatedAt: new Date().toISOString(),
|
|
1790
|
+
});
|
|
1791
|
+
this.emit({
|
|
1792
|
+
type: "workspace.title.set.response",
|
|
1793
|
+
payload: {
|
|
1794
|
+
requestId,
|
|
1795
|
+
workspaceId,
|
|
1796
|
+
accepted: true,
|
|
1797
|
+
title: nextTitle,
|
|
1798
|
+
error: null,
|
|
1799
|
+
},
|
|
1800
|
+
});
|
|
1801
|
+
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], {
|
|
1802
|
+
skipReconcile: true,
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
catch (error) {
|
|
1806
|
+
this.sessionLogger.error({ err: error, workspaceId, requestId }, "session: workspace.title.set.request error");
|
|
1807
|
+
this.emit({
|
|
1808
|
+
type: "activity_log",
|
|
1809
|
+
payload: {
|
|
1810
|
+
id: uuidv4(),
|
|
1811
|
+
timestamp: new Date(),
|
|
1812
|
+
type: "error",
|
|
1813
|
+
content: `Failed to set workspace title: ${getErrorMessage(error)}`,
|
|
1814
|
+
},
|
|
1815
|
+
});
|
|
1816
|
+
this.emit({
|
|
1817
|
+
type: "workspace.title.set.response",
|
|
1818
|
+
payload: {
|
|
1819
|
+
requestId,
|
|
1820
|
+
workspaceId,
|
|
1821
|
+
accepted: false,
|
|
1822
|
+
title: null,
|
|
1823
|
+
error: getErrorMessageOr(error, "Failed to set workspace title"),
|
|
1824
|
+
},
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1755
1828
|
toVoiceFeatureUnavailableContext(state) {
|
|
1756
1829
|
return {
|
|
1757
1830
|
reasonCode: state.reasonCode,
|
|
@@ -2088,7 +2161,7 @@ export class Session {
|
|
|
2088
2161
|
let createdAgentId = null;
|
|
2089
2162
|
try {
|
|
2090
2163
|
const trimmedPrompt = initialPrompt?.trim();
|
|
2091
|
-
const {
|
|
2164
|
+
const { provisionalTitle } = resolveCreateAgentTitles({
|
|
2092
2165
|
configTitle: config.title,
|
|
2093
2166
|
initialPrompt: trimmedPrompt,
|
|
2094
2167
|
});
|
|
@@ -2106,19 +2179,23 @@ export class Session {
|
|
|
2106
2179
|
const createAgentConfig = createdWorktree
|
|
2107
2180
|
? { ...config, cwd: createdWorktree.worktree.worktreePath }
|
|
2108
2181
|
: config;
|
|
2182
|
+
// Ownership comes from an explicit id (worktree or request). An agent
|
|
2183
|
+
// created with no explicit workspace mints a fresh one — we never resolve
|
|
2184
|
+
// an existing workspace by cwd, because many workspaces may share a cwd.
|
|
2185
|
+
const workspaceId = createdWorktree?.workspace.workspaceId ??
|
|
2186
|
+
msg.workspaceId ??
|
|
2187
|
+
(await this.createWorkspaceForDirectory(createAgentConfig.cwd)).workspaceId;
|
|
2109
2188
|
const { snapshot, liveSnapshot } = await createAgentCommand({
|
|
2110
2189
|
agentManager: this.agentManager,
|
|
2111
2190
|
agentStorage: this.agentStorage,
|
|
2112
2191
|
logger: this.sessionLogger,
|
|
2113
2192
|
paseoHome: this.paseoHome,
|
|
2114
2193
|
worktreesRoot: this.worktreesRoot,
|
|
2115
|
-
workspaceGitService: this.workspaceGitService,
|
|
2116
2194
|
providerSnapshotManager: this.providerSnapshotManager,
|
|
2117
|
-
daemonConfig: this.readStructuredGenerationDaemonConfig(),
|
|
2118
2195
|
}, {
|
|
2119
2196
|
kind: "session",
|
|
2120
2197
|
config: createAgentConfig,
|
|
2121
|
-
workspaceId
|
|
2198
|
+
workspaceId,
|
|
2122
2199
|
worktreeName,
|
|
2123
2200
|
initialPrompt,
|
|
2124
2201
|
clientMessageId,
|
|
@@ -2129,13 +2206,18 @@ export class Session {
|
|
|
2129
2206
|
labels,
|
|
2130
2207
|
env,
|
|
2131
2208
|
provisionalTitle,
|
|
2132
|
-
explicitTitle,
|
|
2133
2209
|
firstAgentContext,
|
|
2134
2210
|
buildSessionConfig: (sessionConfig, gitOptions, legacyWorktreeName, ctx) => this.buildAgentSessionConfig(sessionConfig, gitOptions, legacyWorktreeName, ctx),
|
|
2135
|
-
resolveWorkspace: ({ cwd, workspaceId }) => this.resolveCreateAgentWorkspace(cwd, workspaceId),
|
|
2136
2211
|
});
|
|
2137
2212
|
createdAgentId = snapshot.id;
|
|
2138
2213
|
await this.forwardAgentUpdate(snapshot);
|
|
2214
|
+
if (!createdWorktree && trimmedPrompt) {
|
|
2215
|
+
await this.scheduleAutoNameLocalWorkspaceTitleForFirstAgent({
|
|
2216
|
+
workspaceId,
|
|
2217
|
+
cwd: createAgentConfig.cwd,
|
|
2218
|
+
firstAgentContext,
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2139
2221
|
this.createAgentLifecycleDispatch.registerAutoArchiveIfRequested({
|
|
2140
2222
|
autoArchive,
|
|
2141
2223
|
agentId: snapshot.id,
|
|
@@ -2184,16 +2266,6 @@ export class Session {
|
|
|
2184
2266
|
});
|
|
2185
2267
|
}
|
|
2186
2268
|
}
|
|
2187
|
-
async resolveCreateAgentWorkspace(cwd, workspaceId) {
|
|
2188
|
-
const resolvedWorkspace = workspaceId
|
|
2189
|
-
? await this.workspaceRegistry.get(workspaceId)
|
|
2190
|
-
: ((await this.findWorkspaceByDirectory(cwd)) ??
|
|
2191
|
-
(await this.findOrCreateWorkspaceForDirectory(cwd)));
|
|
2192
|
-
if (!resolvedWorkspace) {
|
|
2193
|
-
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
2194
|
-
}
|
|
2195
|
-
return { workspaceId: resolvedWorkspace.workspaceId };
|
|
2196
|
-
}
|
|
2197
2269
|
async handleResumeAgentRequest(msg) {
|
|
2198
2270
|
const { handle, overrides, requestId } = msg;
|
|
2199
2271
|
if (!handle) {
|
|
@@ -2283,17 +2355,20 @@ export class Session {
|
|
|
2283
2355
|
const { provider, providerHandleId, requestId } = normalized;
|
|
2284
2356
|
this.sessionLogger.info({ providerHandleId, provider }, `Importing agent ${providerHandleId} (${provider})`);
|
|
2285
2357
|
try {
|
|
2358
|
+
if (!normalized.cwd) {
|
|
2359
|
+
throw new Error("Import requires cwd from the selected provider session");
|
|
2360
|
+
}
|
|
2361
|
+
// An imported agent mints its own workspace; ownership is its workspaceId,
|
|
2362
|
+
// never an existing same-cwd workspace resolved by path.
|
|
2363
|
+
const workspace = await this.createWorkspaceForDirectory(normalized.cwd);
|
|
2286
2364
|
const { snapshot, timelineSize } = await importProviderSession({
|
|
2287
2365
|
request: normalized,
|
|
2366
|
+
workspaceId: workspace.workspaceId,
|
|
2288
2367
|
agentManager: this.agentManager,
|
|
2289
2368
|
agentStorage: this.agentStorage,
|
|
2290
|
-
workspaceGitService: this.workspaceGitService,
|
|
2291
|
-
providerSnapshotManager: this.providerSnapshotManager,
|
|
2292
|
-
daemonConfig: this.readStructuredGenerationDaemonConfig(),
|
|
2293
|
-
paseoHome: this.paseoHome,
|
|
2294
2369
|
logger: this.sessionLogger,
|
|
2295
2370
|
});
|
|
2296
|
-
await this.registerWorkspaceForImportedAgent(
|
|
2371
|
+
await this.registerWorkspaceForImportedAgent(workspace);
|
|
2297
2372
|
const agentPayload = await this.buildAgentPayload(snapshot);
|
|
2298
2373
|
this.emit({
|
|
2299
2374
|
type: "status",
|
|
@@ -2472,18 +2547,20 @@ export class Session {
|
|
|
2472
2547
|
}, config, gitOptions, legacyWorktreeName, firstAgentContext);
|
|
2473
2548
|
}
|
|
2474
2549
|
scheduleAutoNameWorkspaceBranchForFirstAgent(input) {
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
}, 0);
|
|
2550
|
+
this.scheduleWorkspaceNaming(() => this.maybeAutoNameWorkspaceBranchForFirstAgent(input), {
|
|
2551
|
+
cwd: input.workspace.cwd,
|
|
2552
|
+
message: "Failed to auto-name worktree branch",
|
|
2553
|
+
});
|
|
2480
2554
|
}
|
|
2481
2555
|
async maybeAutoNameWorkspaceBranchForFirstAgent(input) {
|
|
2556
|
+
// Capture the generated title from the generator callback so we can write
|
|
2557
|
+
// title := generated title after the branch rename completes.
|
|
2558
|
+
let generatedTitle = null;
|
|
2482
2559
|
const result = await attemptFirstAgentBranchAutoName({
|
|
2483
2560
|
cwd: input.workspace.cwd,
|
|
2484
2561
|
firstAgentContext: input.firstAgentContext,
|
|
2485
2562
|
generateBranchNameFromContext: ({ cwd, firstAgentContext }) => {
|
|
2486
|
-
return
|
|
2563
|
+
return this.generateWorkspaceName({
|
|
2487
2564
|
agentManager: this.agentManager,
|
|
2488
2565
|
cwd,
|
|
2489
2566
|
workspaceGitService: this.workspaceGitService,
|
|
@@ -2492,21 +2569,80 @@ export class Session {
|
|
|
2492
2569
|
currentSelection: this.getFocusedAgentSelectionForCwd(cwd),
|
|
2493
2570
|
firstAgentContext,
|
|
2494
2571
|
logger: this.sessionLogger,
|
|
2572
|
+
}).then((r) => {
|
|
2573
|
+
generatedTitle = r?.title ?? null;
|
|
2574
|
+
return r?.branch ?? null;
|
|
2495
2575
|
});
|
|
2496
2576
|
},
|
|
2497
2577
|
});
|
|
2498
|
-
if (!result.renamed || !
|
|
2499
|
-
return
|
|
2578
|
+
if (!result.renamed || !generatedTitle) {
|
|
2579
|
+
return;
|
|
2500
2580
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2581
|
+
// K4: re-read from the registry before writing so any concurrent upsert
|
|
2582
|
+
// that happened between workspace creation and this async path is not clobbered.
|
|
2583
|
+
// The first-agent rename renamed the git branch too, so persist the new branch
|
|
2584
|
+
// alongside the title — both are this path's own fields.
|
|
2585
|
+
await this.applyGeneratedWorkspaceTitle(input.workspace.workspaceId, {
|
|
2586
|
+
title: generatedTitle,
|
|
2587
|
+
branch: result.branchName,
|
|
2588
|
+
});
|
|
2507
2589
|
await this.notifyGitMutation(input.workspace.cwd, "rename-branch");
|
|
2508
2590
|
await this.emitWorkspaceUpdateForCwd(input.workspace.cwd);
|
|
2509
|
-
|
|
2591
|
+
}
|
|
2592
|
+
// applyGeneratedWorkspaceTitle fills the generated title only while the
|
|
2593
|
+
// workspace is still untitled. It re-reads the current record from the
|
|
2594
|
+
// registry so concurrent upserts that happened after workspace creation are
|
|
2595
|
+
// not clobbered, while still persisting branch metadata from the rename path.
|
|
2596
|
+
async applyGeneratedWorkspaceTitle(workspaceId, input) {
|
|
2597
|
+
const current = await this.workspaceRegistry.get(workspaceId);
|
|
2598
|
+
if (!current) {
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
await this.workspaceRegistry.upsert({
|
|
2602
|
+
...current,
|
|
2603
|
+
title: current.title || input.title,
|
|
2604
|
+
...(input.branch ? { branch: input.branch } : {}),
|
|
2605
|
+
updatedAt: new Date().toISOString(),
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
// Wraps the injected workspace-name generator for a directory workspace.
|
|
2609
|
+
async generateWorkspaceTitleFromContext(input) {
|
|
2610
|
+
return this.generateWorkspaceName({
|
|
2611
|
+
agentManager: this.agentManager,
|
|
2612
|
+
cwd: input.cwd,
|
|
2613
|
+
workspaceGitService: this.workspaceGitService,
|
|
2614
|
+
providerSnapshotManager: this.providerSnapshotManager,
|
|
2615
|
+
daemonConfig: this.readStructuredGenerationDaemonConfig(),
|
|
2616
|
+
currentSelection: this.getFocusedAgentSelectionForCwd(input.cwd),
|
|
2617
|
+
firstAgentContext: input.firstAgentContext,
|
|
2618
|
+
logger: this.sessionLogger,
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
// Generates a human title for a directory workspace from the firstAgentContext
|
|
2622
|
+
// prompt and writes it as displayName. No branch rename — directory workspaces
|
|
2623
|
+
// have no worktree git state.
|
|
2624
|
+
// TODO(K7): same-dir directory-workspace display disambiguation not yet implemented.
|
|
2625
|
+
async maybeAutoNameDirectoryWorkspaceTitle(input) {
|
|
2626
|
+
const generated = await this.generateWorkspaceTitleFromContext({
|
|
2627
|
+
cwd: input.cwd,
|
|
2628
|
+
firstAgentContext: input.firstAgentContext,
|
|
2629
|
+
});
|
|
2630
|
+
const title = generated?.title ?? null;
|
|
2631
|
+
if (!title) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
// K4: applyGeneratedWorkspaceTitle re-reads from the registry before writing.
|
|
2635
|
+
// Directory workspaces have no branch — write only the title.
|
|
2636
|
+
await this.applyGeneratedWorkspaceTitle(input.workspaceId, { title });
|
|
2637
|
+
await this.emitWorkspaceUpdateForWorkspaceId(input.workspaceId);
|
|
2638
|
+
}
|
|
2639
|
+
async scheduleAutoNameLocalWorkspaceTitleForFirstAgent(input) {
|
|
2640
|
+
const workspaceId = input.workspaceId;
|
|
2641
|
+
this.scheduleWorkspaceNaming(() => this.maybeAutoNameDirectoryWorkspaceTitle({
|
|
2642
|
+
workspaceId,
|
|
2643
|
+
cwd: input.cwd,
|
|
2644
|
+
firstAgentContext: input.firstAgentContext,
|
|
2645
|
+
}), { cwd: input.cwd, message: "Failed to auto-name local workspace title" });
|
|
2510
2646
|
}
|
|
2511
2647
|
emitProviderDisabledResponse(kind, provider, requestId, fetchedAt) {
|
|
2512
2648
|
const payload = {
|
|
@@ -2719,6 +2855,7 @@ export class Session {
|
|
|
2719
2855
|
relayPublicEndpoint: relay?.publicEndpoint,
|
|
2720
2856
|
relayUseTls: relay?.useTls,
|
|
2721
2857
|
relayPublicUseTls: relay?.publicUseTls,
|
|
2858
|
+
appBaseUrl: this.daemonRuntimeConfig?.appBaseUrl,
|
|
2722
2859
|
includeQr: true,
|
|
2723
2860
|
logger: this.sessionLogger,
|
|
2724
2861
|
});
|
|
@@ -3211,16 +3348,33 @@ export class Session {
|
|
|
3211
3348
|
* Handle client heartbeat for activity tracking
|
|
3212
3349
|
*/
|
|
3213
3350
|
handleClientHeartbeat(msg) {
|
|
3351
|
+
const focusedTerminalId = msg.focusedTerminalId?.trim() || null;
|
|
3214
3352
|
const appVisibilityChangedAt = msg.appVisibilityChangedAt
|
|
3215
3353
|
? new Date(msg.appVisibilityChangedAt)
|
|
3216
3354
|
: new Date(msg.lastActivityAt);
|
|
3217
3355
|
this.clientActivity = {
|
|
3218
3356
|
deviceType: msg.deviceType,
|
|
3219
3357
|
focusedAgentId: msg.focusedAgentId,
|
|
3358
|
+
focusedTerminalId,
|
|
3220
3359
|
lastActivityAt: new Date(msg.lastActivityAt),
|
|
3221
3360
|
appVisible: msg.appVisible,
|
|
3222
3361
|
appVisibilityChangedAt,
|
|
3223
3362
|
};
|
|
3363
|
+
if (msg.appVisible && focusedTerminalId) {
|
|
3364
|
+
void this.clearFocusedTerminalAttention(focusedTerminalId);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
async clearFocusedTerminalAttention(terminalId) {
|
|
3368
|
+
const terminalManager = this.terminalManager;
|
|
3369
|
+
if (!terminalManager) {
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
try {
|
|
3373
|
+
await terminalManager.clearTerminalAttention(terminalId);
|
|
3374
|
+
}
|
|
3375
|
+
catch (error) {
|
|
3376
|
+
this.sessionLogger.warn({ err: error, terminalId }, "Failed to clear terminal attention");
|
|
3377
|
+
}
|
|
3224
3378
|
}
|
|
3225
3379
|
/**
|
|
3226
3380
|
* Handle push token registration
|
|
@@ -3542,7 +3696,7 @@ export class Session {
|
|
|
3542
3696
|
target.watchers.length = 0;
|
|
3543
3697
|
}
|
|
3544
3698
|
async removeWorkspaceGitWatchTarget(cwd) {
|
|
3545
|
-
const normalizedCwd =
|
|
3699
|
+
const normalizedCwd = resolve(cwd);
|
|
3546
3700
|
const target = this.workspaceGitWatchTargets.get(normalizedCwd);
|
|
3547
3701
|
if (target) {
|
|
3548
3702
|
this.closeWorkspaceGitWatchTarget(target);
|
|
@@ -3550,7 +3704,7 @@ export class Session {
|
|
|
3550
3704
|
}
|
|
3551
3705
|
}
|
|
3552
3706
|
removeWorkspaceGitSubscription(cwd) {
|
|
3553
|
-
const normalizedCwd =
|
|
3707
|
+
const normalizedCwd = resolve(cwd);
|
|
3554
3708
|
const target = this.workspaceGitWatchTargets.get(normalizedCwd);
|
|
3555
3709
|
if (target) {
|
|
3556
3710
|
const unsubscribeFetch = this.workspaceGitFetchSubscriptions.get(normalizedCwd);
|
|
@@ -3571,8 +3725,16 @@ export class Session {
|
|
|
3571
3725
|
workspace.diffStat ? [workspace.diffStat.additions, workspace.diffStat.deletions] : null,
|
|
3572
3726
|
]);
|
|
3573
3727
|
}
|
|
3728
|
+
resolveWorkspaceGitWatchTarget(workspaceId) {
|
|
3729
|
+
for (const target of this.workspaceGitWatchTargets.values()) {
|
|
3730
|
+
if (target.workspaceId === workspaceId) {
|
|
3731
|
+
return target;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
return null;
|
|
3735
|
+
}
|
|
3574
3736
|
shouldSkipWorkspaceGitWatchUpdate(workspaceId, workspace) {
|
|
3575
|
-
const target = this.
|
|
3737
|
+
const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
|
3576
3738
|
if (!target) {
|
|
3577
3739
|
return false;
|
|
3578
3740
|
}
|
|
@@ -3584,7 +3746,7 @@ export class Session {
|
|
|
3584
3746
|
return false;
|
|
3585
3747
|
}
|
|
3586
3748
|
rememberWorkspaceGitDescriptorState(workspaceId, workspace) {
|
|
3587
|
-
const target = this.
|
|
3749
|
+
const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
|
3588
3750
|
if (!target) {
|
|
3589
3751
|
return;
|
|
3590
3752
|
}
|
|
@@ -3592,7 +3754,7 @@ export class Session {
|
|
|
3592
3754
|
target.lastBranchName = workspace?.name ?? null;
|
|
3593
3755
|
}
|
|
3594
3756
|
handleWorkspaceGitBranchSnapshot(cwd, branchName) {
|
|
3595
|
-
const target = this.workspaceGitWatchTargets.get(
|
|
3757
|
+
const target = this.workspaceGitWatchTargets.get(resolve(cwd));
|
|
3596
3758
|
if (!target) {
|
|
3597
3759
|
return;
|
|
3598
3760
|
}
|
|
@@ -3613,7 +3775,7 @@ export class Session {
|
|
|
3613
3775
|
}
|
|
3614
3776
|
}
|
|
3615
3777
|
syncWorkspaceGitObserver(cwd, options) {
|
|
3616
|
-
const normalizedCwd =
|
|
3778
|
+
const normalizedCwd = resolve(cwd);
|
|
3617
3779
|
if (!options.isGit) {
|
|
3618
3780
|
this.removeWorkspaceGitSubscription(normalizedCwd);
|
|
3619
3781
|
return;
|
|
@@ -3634,7 +3796,9 @@ export class Session {
|
|
|
3634
3796
|
this.workspaceGitWatchTargets.set(normalizedCwd, target);
|
|
3635
3797
|
const subscription = this.workspaceGitService.registerWorkspace({ cwd: normalizedCwd }, (snapshot) => {
|
|
3636
3798
|
this.handleWorkspaceGitBranchSnapshot(normalizedCwd, snapshot.git.currentBranch ?? null);
|
|
3637
|
-
void this.emitWorkspaceUpdateForCwd(normalizedCwd)
|
|
3799
|
+
void this.emitWorkspaceUpdateForCwd(normalizedCwd).catch((error) => {
|
|
3800
|
+
this.sessionLogger.warn({ err: error, cwd: normalizedCwd }, "Failed to emit workspace update after git branch snapshot");
|
|
3801
|
+
});
|
|
3638
3802
|
this.emitCheckoutStatusUpdate(normalizedCwd, snapshot);
|
|
3639
3803
|
});
|
|
3640
3804
|
this.workspaceGitSubscriptions.set(normalizedCwd, subscription.unsubscribe);
|
|
@@ -3739,10 +3903,14 @@ export class Session {
|
|
|
3739
3903
|
return;
|
|
3740
3904
|
}
|
|
3741
3905
|
try {
|
|
3742
|
-
const result = await renameCurrentBranch(cwd, branch);
|
|
3906
|
+
const result = await this.renameCurrentBranch(cwd, branch);
|
|
3743
3907
|
await this.notifyGitMutation(cwd, "rename-branch", { invalidateGithub: true });
|
|
3744
3908
|
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
|
3745
3909
|
this.handleWorkspaceGitBranchSnapshot(cwd, result.currentBranch);
|
|
3910
|
+
// Branch is a git fact derived per-descriptor from each workspace's own
|
|
3911
|
+
// live git snapshot (id → cwd); the reconciliation pass re-persists the
|
|
3912
|
+
// `branch` field per workspace from its own cwd. No cwd → ids fan-out here.
|
|
3913
|
+
// TODO(K10): PR-binding on branch rename is deferred — see plan K10.
|
|
3746
3914
|
// Push a workspace_update immediately so the sidebar/header reflect
|
|
3747
3915
|
// the new branch name without waiting for the background git watcher.
|
|
3748
3916
|
await this.emitWorkspaceUpdateForCwd(cwd);
|
|
@@ -4348,18 +4516,19 @@ export class Session {
|
|
|
4348
4516
|
async handlePaseoWorktreeArchiveRequest(msg) {
|
|
4349
4517
|
return handleWorktreeArchiveRequest({
|
|
4350
4518
|
paseoHome: this.paseoHome,
|
|
4351
|
-
|
|
4519
|
+
paseoWorktreesBaseRoot: this.worktreesRoot,
|
|
4352
4520
|
github: this.github,
|
|
4353
4521
|
workspaceGitService: this.workspaceGitService,
|
|
4354
4522
|
agentManager: this.agentManager,
|
|
4355
4523
|
agentStorage: this.agentStorage,
|
|
4524
|
+
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
|
|
4525
|
+
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
|
|
4356
4526
|
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
|
4357
4527
|
emit: (message) => this.emit(message),
|
|
4358
4528
|
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
|
|
4359
4529
|
markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
|
|
4360
4530
|
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
|
|
4361
|
-
|
|
4362
|
-
killTerminalsUnderPath: (rootPath) => this.terminalController.killTerminalsUnderPath(rootPath),
|
|
4531
|
+
killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
|
|
4363
4532
|
sessionLogger: this.sessionLogger,
|
|
4364
4533
|
}, msg);
|
|
4365
4534
|
}
|
|
@@ -4569,10 +4738,30 @@ export class Session {
|
|
|
4569
4738
|
});
|
|
4570
4739
|
}
|
|
4571
4740
|
}
|
|
4741
|
+
async listTerminalActivityContributions() {
|
|
4742
|
+
const terminalManager = this.terminalManager;
|
|
4743
|
+
if (!terminalManager) {
|
|
4744
|
+
return [];
|
|
4745
|
+
}
|
|
4746
|
+
const directories = terminalManager.listDirectories();
|
|
4747
|
+
const terminalsByDirectory = await Promise.all(directories.map((cwd) => terminalManager.getTerminals(cwd)));
|
|
4748
|
+
return terminalsByDirectory.flat().map((session) => {
|
|
4749
|
+
const contribution = {
|
|
4750
|
+
cwd: session.cwd,
|
|
4751
|
+
activity: session.getActivity(),
|
|
4752
|
+
};
|
|
4753
|
+
if (session.workspaceId) {
|
|
4754
|
+
contribution.workspaceId = session.workspaceId;
|
|
4755
|
+
}
|
|
4756
|
+
return contribution;
|
|
4757
|
+
});
|
|
4758
|
+
}
|
|
4572
4759
|
/**
|
|
4573
4760
|
* Build the current agent list payload (live + persisted), optionally filtered by labels.
|
|
4574
4761
|
*/
|
|
4575
4762
|
async listAgentPayloads(filter) {
|
|
4763
|
+
const includeArchived = filter?.includeArchived === true;
|
|
4764
|
+
const labelEntries = filter?.labels ? Object.entries(filter.labels) : [];
|
|
4576
4765
|
// Get live agents with session modes
|
|
4577
4766
|
const agentSnapshots = this.agentManager.listAgents();
|
|
4578
4767
|
const liveAgents = await Promise.all(agentSnapshots.map((agent) => this.buildAgentPayload(agent)));
|
|
@@ -4580,18 +4769,23 @@ export class Session {
|
|
|
4580
4769
|
// (excluding internal agents which are for ephemeral system tasks)
|
|
4581
4770
|
const registryRecords = await this.agentStorage.list();
|
|
4582
4771
|
const liveIds = new Set(agentSnapshots.map((a) => a.id));
|
|
4583
|
-
const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
|
|
4772
|
+
const registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds());
|
|
4584
4773
|
const persistedAgents = registryRecords
|
|
4585
4774
|
.filter((record) => !liveIds.has(record.id) && !record.internal)
|
|
4775
|
+
// Keep raw-record filters ahead of projection; seeded homes can carry thousands of archived agents.
|
|
4776
|
+
.filter((record) => includeArchived || !record.archivedAt)
|
|
4777
|
+
.filter((record) => labelEntries.every(([key, value]) => record.labels?.[key] === value))
|
|
4586
4778
|
.filter((record) => filter?.includeUnavailablePersisted === true ||
|
|
4587
4779
|
isStoredAgentProviderAvailable(record, registeredProviderIds))
|
|
4588
4780
|
.map((record) => this.buildStoredAgentPayload(record, registeredProviderIds));
|
|
4589
4781
|
let agents = [...liveAgents, ...persistedAgents];
|
|
4590
4782
|
agents = agents.filter((agent) => this.isProviderVisibleToClient(agent.provider));
|
|
4783
|
+
if (!includeArchived) {
|
|
4784
|
+
agents = agents.filter((agent) => !agent.archivedAt);
|
|
4785
|
+
}
|
|
4591
4786
|
// Filter by labels if filter provided
|
|
4592
|
-
if (
|
|
4593
|
-
|
|
4594
|
-
agents = agents.filter((agent) => Object.entries(filterLabels).every(([key, _value]) => agent.labels[key] === filterLabels[key]));
|
|
4787
|
+
if (labelEntries.length > 0) {
|
|
4788
|
+
agents = agents.filter((agent) => labelEntries.every(([key, value]) => agent.labels[key] === value));
|
|
4595
4789
|
}
|
|
4596
4790
|
return agents;
|
|
4597
4791
|
}
|
|
@@ -4653,7 +4847,7 @@ export class Session {
|
|
|
4653
4847
|
const payload = this.buildStoredAgentPayload(record);
|
|
4654
4848
|
return this.isProviderVisibleToClient(payload.provider) ? payload : null;
|
|
4655
4849
|
}
|
|
4656
|
-
async
|
|
4850
|
+
async buildActiveProjectPlacementsByWorkspaceId() {
|
|
4657
4851
|
const [persistedWorkspaces, persistedProjects] = await Promise.all([
|
|
4658
4852
|
this.workspaceRegistry.list(),
|
|
4659
4853
|
this.projectRegistry.list(),
|
|
@@ -4661,7 +4855,7 @@ export class Session {
|
|
|
4661
4855
|
const activeProjects = new Map(persistedProjects
|
|
4662
4856
|
.filter((project) => !project.archivedAt)
|
|
4663
4857
|
.map((project) => [project.projectId, project]));
|
|
4664
|
-
const
|
|
4858
|
+
const placementsByWorkspaceId = new Map();
|
|
4665
4859
|
const pairs = persistedWorkspaces.flatMap((workspace) => {
|
|
4666
4860
|
if (workspace.archivedAt)
|
|
4667
4861
|
return [];
|
|
@@ -4672,9 +4866,9 @@ export class Session {
|
|
|
4672
4866
|
});
|
|
4673
4867
|
const placements = await Promise.all(pairs.map(({ workspace, project }) => this.buildProjectPlacementForWorkspace(workspace, project)));
|
|
4674
4868
|
for (let i = 0; i < pairs.length; i += 1) {
|
|
4675
|
-
|
|
4869
|
+
placementsByWorkspaceId.set(pairs[i].workspace.workspaceId, placements[i]);
|
|
4676
4870
|
}
|
|
4677
|
-
return
|
|
4871
|
+
return placementsByWorkspaceId;
|
|
4678
4872
|
}
|
|
4679
4873
|
async collectFetchAgentsEntries(params) {
|
|
4680
4874
|
const { candidates, limit, getPlacement, filter } = params;
|
|
@@ -4683,7 +4877,7 @@ export class Session {
|
|
|
4683
4877
|
for (let start = 0; start < candidates.length && matchedEntries.length <= limit; start += batchSize) {
|
|
4684
4878
|
const batch = candidates.slice(start, start + batchSize);
|
|
4685
4879
|
const batchEntries = await Promise.all(batch.map(async (agent) => {
|
|
4686
|
-
const project = await getPlacement(agent.
|
|
4880
|
+
const project = await getPlacement(agent.workspaceId);
|
|
4687
4881
|
return project ? { agent, project } : null;
|
|
4688
4882
|
}));
|
|
4689
4883
|
for (const entry of batchEntries) {
|
|
@@ -4714,23 +4908,29 @@ export class Session {
|
|
|
4714
4908
|
const sort = this.agentsPager.normalizeSort(request.sort);
|
|
4715
4909
|
let agents = await this.listAgentPayloads({
|
|
4716
4910
|
labels: filter?.labels,
|
|
4911
|
+
includeArchived: filter?.includeArchived,
|
|
4717
4912
|
includeUnavailablePersisted: request.type === "fetch_agent_history_request",
|
|
4718
4913
|
});
|
|
4719
|
-
const
|
|
4720
|
-
if (
|
|
4721
|
-
agents = agents.filter((agent) => !agent.archivedAt &&
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4914
|
+
const activePlacementsByWorkspaceId = scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceId() : null;
|
|
4915
|
+
if (activePlacementsByWorkspaceId) {
|
|
4916
|
+
agents = agents.filter((agent) => !agent.archivedAt &&
|
|
4917
|
+
agent.workspaceId != null &&
|
|
4918
|
+
activePlacementsByWorkspaceId.has(agent.workspaceId));
|
|
4919
|
+
}
|
|
4920
|
+
const placementByWorkspaceId = new Map();
|
|
4921
|
+
const getPlacement = (workspaceId) => {
|
|
4922
|
+
if (!workspaceId) {
|
|
4923
|
+
return Promise.resolve(null);
|
|
4727
4924
|
}
|
|
4728
|
-
|
|
4925
|
+
if (activePlacementsByWorkspaceId) {
|
|
4926
|
+
return Promise.resolve(activePlacementsByWorkspaceId.get(workspaceId) ?? null);
|
|
4927
|
+
}
|
|
4928
|
+
const existing = placementByWorkspaceId.get(workspaceId);
|
|
4729
4929
|
if (existing) {
|
|
4730
4930
|
return existing;
|
|
4731
4931
|
}
|
|
4732
|
-
const placementPromise = this.
|
|
4733
|
-
|
|
4932
|
+
const placementPromise = this.buildProjectPlacementForWorkspaceId(workspaceId);
|
|
4933
|
+
placementByWorkspaceId.set(workspaceId, placementPromise);
|
|
4734
4934
|
return placementPromise;
|
|
4735
4935
|
};
|
|
4736
4936
|
let candidates = [...agents];
|
|
@@ -4790,7 +4990,8 @@ export class Session {
|
|
|
4790
4990
|
workspaceDirectory: workspace.cwd,
|
|
4791
4991
|
projectKind: (resolvedProjectRecord?.kind ?? "directory") === "git" ? "git" : "non_git",
|
|
4792
4992
|
workspaceKind: workspace.kind,
|
|
4793
|
-
name: workspace
|
|
4993
|
+
name: resolveWorkspaceDisplayName(workspace),
|
|
4994
|
+
title: workspace.title,
|
|
4794
4995
|
archivingAt: null,
|
|
4795
4996
|
status: "done",
|
|
4796
4997
|
statusEnteredAt: null,
|
|
@@ -4847,7 +5048,7 @@ export class Session {
|
|
|
4847
5048
|
const displayName = deriveWorkspaceDisplayName({ cwd: workspace.cwd, checkout });
|
|
4848
5049
|
return {
|
|
4849
5050
|
...base,
|
|
4850
|
-
name: displayName,
|
|
5051
|
+
name: resolveWorkspaceName({ title: workspace.title, derivedDisplayName: displayName }),
|
|
4851
5052
|
diffStat: snapshot.git.diffStat ?? null,
|
|
4852
5053
|
gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot) ?? undefined,
|
|
4853
5054
|
githubRuntime: this.buildWorkspaceGitHubRuntimePayload(snapshot),
|
|
@@ -4866,10 +5067,14 @@ export class Session {
|
|
|
4866
5067
|
workspaceDirectory: result.workspace.cwd,
|
|
4867
5068
|
projectKind: "git",
|
|
4868
5069
|
workspaceKind: result.workspace.kind,
|
|
4869
|
-
name:
|
|
5070
|
+
name: resolveWorkspaceName({
|
|
5071
|
+
title: result.workspace.title,
|
|
5072
|
+
derivedDisplayName: result.worktree.branchName || result.workspace.displayName,
|
|
5073
|
+
}),
|
|
5074
|
+
title: result.workspace.title,
|
|
4870
5075
|
archivingAt: null,
|
|
4871
5076
|
status: "done",
|
|
4872
|
-
statusEnteredAt:
|
|
5077
|
+
statusEnteredAt: result.workspace.createdAt,
|
|
4873
5078
|
activityAt: null,
|
|
4874
5079
|
diffStat: { additions: 0, deletions: 0 },
|
|
4875
5080
|
scripts: [],
|
|
@@ -4900,8 +5105,13 @@ export class Session {
|
|
|
4900
5105
|
async buildWorkspaceDescriptorMap(options) {
|
|
4901
5106
|
return this.workspaceDirectory.buildDescriptorMap(options);
|
|
4902
5107
|
}
|
|
4903
|
-
|
|
4904
|
-
|
|
5108
|
+
// external path→workspace adapter, not ownership. Used by archive-by-path flows
|
|
5109
|
+
// where the request carries a worktree path (unique to one workspace) rather
|
|
5110
|
+
// than a workspaceId. This is a directory lookup for an archive target, not a
|
|
5111
|
+
// status/ownership attribution.
|
|
5112
|
+
async findWorkspaceIdForCwd(cwd) {
|
|
5113
|
+
const workspaces = await this.workspaceRegistry.list();
|
|
5114
|
+
return resolveWorkspaceIdForPath(cwd, workspaces);
|
|
4905
5115
|
}
|
|
4906
5116
|
matchesWorkspaceFilter(input) {
|
|
4907
5117
|
return this.workspaceDirectory.matchesFilter(input);
|
|
@@ -4969,7 +5179,7 @@ export class Session {
|
|
|
4969
5179
|
}
|
|
4970
5180
|
}
|
|
4971
5181
|
async findOrCreateWorkspaceForDirectory(cwd) {
|
|
4972
|
-
const inputCwd =
|
|
5182
|
+
const inputCwd = resolve(cwd);
|
|
4973
5183
|
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd);
|
|
4974
5184
|
const existingWorkspace = await this.findExactWorkspaceByDirectory(normalizedCwd, {
|
|
4975
5185
|
refreshGit: false,
|
|
@@ -4977,22 +5187,26 @@ export class Session {
|
|
|
4977
5187
|
if (existingWorkspace) {
|
|
4978
5188
|
if (existingWorkspace.archivedAt && inputCwd !== normalizedCwd) {
|
|
4979
5189
|
const timestamp = new Date().toISOString();
|
|
4980
|
-
const
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
5190
|
+
const checkout = checkoutLiteFromGitSnapshot(inputCwd, {
|
|
5191
|
+
isGit: false,
|
|
5192
|
+
currentBranch: null,
|
|
5193
|
+
remoteUrl: null,
|
|
5194
|
+
repoRoot: null,
|
|
5195
|
+
isPaseoOwnedWorktree: false,
|
|
5196
|
+
mainRepoRoot: null,
|
|
5197
|
+
});
|
|
5198
|
+
const membership = classifyDirectoryForProjectMembership({ cwd: inputCwd, checkout });
|
|
5199
|
+
const projectRecord = await this.resolveProjectRecordForPlacement({
|
|
5200
|
+
membership,
|
|
5201
|
+
timestamp,
|
|
4988
5202
|
});
|
|
4989
5203
|
await this.projectRegistry.upsert(projectRecord);
|
|
4990
5204
|
const workspaceRecord = createPersistedWorkspaceRecord({
|
|
4991
|
-
workspaceId:
|
|
5205
|
+
workspaceId: generateWorkspaceId(),
|
|
4992
5206
|
projectId: projectRecord.projectId,
|
|
4993
5207
|
cwd: inputCwd,
|
|
4994
|
-
kind:
|
|
4995
|
-
displayName,
|
|
5208
|
+
kind: membership.workspaceKind,
|
|
5209
|
+
displayName: membership.workspaceDisplayName,
|
|
4996
5210
|
createdAt: timestamp,
|
|
4997
5211
|
updatedAt: timestamp,
|
|
4998
5212
|
});
|
|
@@ -5017,7 +5231,7 @@ export class Session {
|
|
|
5017
5231
|
});
|
|
5018
5232
|
await this.projectRegistry.upsert(projectRecord);
|
|
5019
5233
|
const workspaceRecord = createPersistedWorkspaceRecord({
|
|
5020
|
-
workspaceId:
|
|
5234
|
+
workspaceId: generateWorkspaceId(),
|
|
5021
5235
|
projectId: projectRecord.projectId,
|
|
5022
5236
|
cwd,
|
|
5023
5237
|
kind: membership.workspaceKind,
|
|
@@ -5039,8 +5253,7 @@ export class Session {
|
|
|
5039
5253
|
const projectId = projectRecord.projectId;
|
|
5040
5254
|
const kind = membership.workspaceKind;
|
|
5041
5255
|
const displayName = membership.workspaceDisplayName;
|
|
5042
|
-
if (input.workspace.
|
|
5043
|
-
input.workspace.projectId === projectId &&
|
|
5256
|
+
if (input.workspace.projectId === projectId &&
|
|
5044
5257
|
input.workspace.kind === kind &&
|
|
5045
5258
|
input.workspace.displayName === displayName) {
|
|
5046
5259
|
return this.ensureWorkspaceRecordUnarchived(input.workspace);
|
|
@@ -5048,7 +5261,7 @@ export class Session {
|
|
|
5048
5261
|
await this.projectRegistry.upsert(projectRecord);
|
|
5049
5262
|
const nextWorkspace = {
|
|
5050
5263
|
...input.workspace,
|
|
5051
|
-
workspaceId:
|
|
5264
|
+
workspaceId: input.workspace.workspaceId,
|
|
5052
5265
|
projectId,
|
|
5053
5266
|
cwd: input.cwd,
|
|
5054
5267
|
kind,
|
|
@@ -5122,16 +5335,28 @@ export class Session {
|
|
|
5122
5335
|
});
|
|
5123
5336
|
return result;
|
|
5124
5337
|
}
|
|
5338
|
+
async listActiveWorkspaceRefs() {
|
|
5339
|
+
const workspaces = await this.workspaceRegistry.list();
|
|
5340
|
+
return workspaces
|
|
5341
|
+
.filter((workspace) => !workspace.archivedAt)
|
|
5342
|
+
.map((workspace) => ({
|
|
5343
|
+
workspaceId: workspace.workspaceId,
|
|
5344
|
+
cwd: workspace.cwd,
|
|
5345
|
+
kind: workspace.kind,
|
|
5346
|
+
}));
|
|
5347
|
+
}
|
|
5125
5348
|
async archiveWorkspaceRecord(workspaceId, archivedAt) {
|
|
5126
5349
|
const archiveTimestamp = archivedAt ?? new Date().toISOString();
|
|
5127
5350
|
const existingWorkspace = await archivePersistedWorkspaceRecord({
|
|
5128
5351
|
workspaceId,
|
|
5129
5352
|
archivedAt: archiveTimestamp,
|
|
5130
5353
|
workspaceRegistry: this.workspaceRegistry,
|
|
5131
|
-
projectRegistry: this.projectRegistry,
|
|
5132
5354
|
});
|
|
5133
5355
|
if (!existingWorkspace) {
|
|
5134
|
-
this.
|
|
5356
|
+
const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
|
5357
|
+
if (watchTarget) {
|
|
5358
|
+
this.removeWorkspaceGitSubscription(watchTarget.cwd);
|
|
5359
|
+
}
|
|
5135
5360
|
return;
|
|
5136
5361
|
}
|
|
5137
5362
|
if (!existingWorkspace.archivedAt) {
|
|
@@ -5144,9 +5369,18 @@ export class Session {
|
|
|
5144
5369
|
archivedAt: archiveTimestamp,
|
|
5145
5370
|
}, "Workspace archived");
|
|
5146
5371
|
}
|
|
5147
|
-
await this.
|
|
5148
|
-
|
|
5149
|
-
|
|
5372
|
+
await this.teardownArchivedWorkspace({
|
|
5373
|
+
workspaceId: existingWorkspace.workspaceId,
|
|
5374
|
+
cwd: existingWorkspace.cwd,
|
|
5375
|
+
});
|
|
5376
|
+
}
|
|
5377
|
+
// Git watch and subscription state is keyed by directory; the script runtime
|
|
5378
|
+
// store is keyed by the opaque workspace id. Each cleanup uses its own key so an
|
|
5379
|
+
// opaque id is never resolved as a filesystem path.
|
|
5380
|
+
async teardownArchivedWorkspace(input) {
|
|
5381
|
+
await this.removeWorkspaceGitWatchTarget(input.cwd);
|
|
5382
|
+
this.scriptRuntimeStore?.removeForWorkspace(input.workspaceId);
|
|
5383
|
+
this.removeWorkspaceGitSubscription(input.cwd);
|
|
5150
5384
|
}
|
|
5151
5385
|
async reconcileAndEmitWorkspaceUpdates() {
|
|
5152
5386
|
if (!this.workspaceUpdatesSubscription) {
|
|
@@ -5178,9 +5412,10 @@ export class Session {
|
|
|
5178
5412
|
await Promise.all(result.changesApplied.map(async (change) => {
|
|
5179
5413
|
switch (change.kind) {
|
|
5180
5414
|
case "workspace_archived":
|
|
5181
|
-
await this.
|
|
5182
|
-
|
|
5183
|
-
|
|
5415
|
+
await this.teardownArchivedWorkspace({
|
|
5416
|
+
workspaceId: change.workspaceId,
|
|
5417
|
+
cwd: change.directory,
|
|
5418
|
+
});
|
|
5184
5419
|
changedWorkspaceIds.add(change.workspaceId);
|
|
5185
5420
|
break;
|
|
5186
5421
|
case "workspace_updated":
|
|
@@ -5223,7 +5458,7 @@ export class Session {
|
|
|
5223
5458
|
this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)) {
|
|
5224
5459
|
continue;
|
|
5225
5460
|
}
|
|
5226
|
-
const watchTarget = this.
|
|
5461
|
+
const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
|
5227
5462
|
if (watchTarget && this.onBranchChanged) {
|
|
5228
5463
|
const newBranchName = nextWorkspace?.name ?? null;
|
|
5229
5464
|
if (newBranchName !== watchTarget.lastBranchName) {
|
|
@@ -5236,6 +5471,7 @@ export class Session {
|
|
|
5236
5471
|
this.bufferOrEmitWorkspaceUpdate(subscription, {
|
|
5237
5472
|
kind: "remove",
|
|
5238
5473
|
id: workspaceId,
|
|
5474
|
+
...(await this.resolveEmptyProjectForArchivedWorkspace(workspaceId)),
|
|
5239
5475
|
});
|
|
5240
5476
|
continue;
|
|
5241
5477
|
}
|
|
@@ -5255,10 +5491,38 @@ export class Session {
|
|
|
5255
5491
|
void this.reconcileAndEmitWorkspaceUpdates();
|
|
5256
5492
|
}
|
|
5257
5493
|
}
|
|
5494
|
+
// When a workspace is archived its project may become empty. Resolve the
|
|
5495
|
+
// now-empty project parent so the `remove` update can carry it, keeping the
|
|
5496
|
+
// sidebar's empty project row in sync without a full re-hydration.
|
|
5497
|
+
async resolveEmptyProjectForArchivedWorkspace(workspaceId) {
|
|
5498
|
+
const archivedWorkspace = await this.workspaceRegistry.get(workspaceId);
|
|
5499
|
+
if (!archivedWorkspace) {
|
|
5500
|
+
return null;
|
|
5501
|
+
}
|
|
5502
|
+
const emptyProject = (await this.workspaceDirectory.listEmptyProjects()).find((project) => project.projectId === archivedWorkspace.projectId);
|
|
5503
|
+
return emptyProject ? { emptyProject } : null;
|
|
5504
|
+
}
|
|
5505
|
+
async emitWorkspaceUpdateForTerminalContribution(event) {
|
|
5506
|
+
// A terminal's activity contributes only to the workspace it carries. A
|
|
5507
|
+
// terminal with no workspaceId attributes to nothing — status is per-id.
|
|
5508
|
+
if (!event.workspaceId) {
|
|
5509
|
+
return;
|
|
5510
|
+
}
|
|
5511
|
+
await this.emitWorkspaceUpdatesForWorkspaceIds([event.workspaceId], {
|
|
5512
|
+
skipReconcile: true,
|
|
5513
|
+
});
|
|
5514
|
+
}
|
|
5515
|
+
// A git fact (branch, diff, dirty, PR) changed at `cwd`. Every workspace whose
|
|
5516
|
+
// OWN cwd is this folder re-derives its git facts from that folder (id → cwd)
|
|
5517
|
+
// and emits its own per-id descriptor. This is a deliberate same-folder fan,
|
|
5518
|
+
// not a cwd → id ownership lookup: git never resolves which workspace owns a
|
|
5519
|
+
// path. See `workspaceIdsOnCheckout`.
|
|
5258
5520
|
async emitWorkspaceUpdateForCwd(cwd, options) {
|
|
5259
|
-
const
|
|
5260
|
-
|
|
5261
|
-
|
|
5521
|
+
const workspaceIds = workspaceIdsOnCheckout(await this.workspaceRegistry.list(), cwd);
|
|
5522
|
+
if (workspaceIds.length === 0) {
|
|
5523
|
+
return;
|
|
5524
|
+
}
|
|
5525
|
+
await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds, options);
|
|
5262
5526
|
}
|
|
5263
5527
|
async handleFetchAgents(request) {
|
|
5264
5528
|
const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
|
|
@@ -5450,16 +5714,162 @@ export class Session {
|
|
|
5450
5714
|
}
|
|
5451
5715
|
return { snapshotByWorkspaceId };
|
|
5452
5716
|
}
|
|
5453
|
-
async registerWorkspaceForImportedAgent(
|
|
5717
|
+
async registerWorkspaceForImportedAgent(workspace) {
|
|
5454
5718
|
try {
|
|
5455
|
-
const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
|
|
5456
5719
|
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
|
5457
5720
|
await this.describeWorkspaceRecord(workspace);
|
|
5458
|
-
await this.
|
|
5721
|
+
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
|
5459
5722
|
}
|
|
5460
5723
|
catch (error) {
|
|
5461
|
-
this.sessionLogger.warn({ err: error, cwd }, "Failed to register workspace for imported agent");
|
|
5724
|
+
this.sessionLogger.warn({ err: error, workspaceId: workspace.workspaceId, cwd: workspace.cwd }, "Failed to register workspace for imported agent");
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
async handleWorkspaceCreateRequest(request) {
|
|
5728
|
+
try {
|
|
5729
|
+
if (request.source.kind === "directory") {
|
|
5730
|
+
await this.handleWorkspaceCreateLocal(request);
|
|
5731
|
+
return;
|
|
5732
|
+
}
|
|
5733
|
+
await this.handleWorkspaceCreateWorktree(request);
|
|
5734
|
+
}
|
|
5735
|
+
catch (error) {
|
|
5736
|
+
const message = error instanceof Error ? error.message : "Failed to create workspace";
|
|
5737
|
+
this.sessionLogger.error({ err: error, sourceKind: request.source.kind, requestId: request.requestId }, "Failed to create workspace");
|
|
5738
|
+
this.emit({
|
|
5739
|
+
type: "workspace.create.response",
|
|
5740
|
+
payload: {
|
|
5741
|
+
requestId: request.requestId,
|
|
5742
|
+
workspace: null,
|
|
5743
|
+
setupTerminalId: null,
|
|
5744
|
+
error: message,
|
|
5745
|
+
},
|
|
5746
|
+
});
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
async handleWorkspaceCreateLocal(request) {
|
|
5750
|
+
if (request.source.kind !== "directory") {
|
|
5751
|
+
return;
|
|
5752
|
+
}
|
|
5753
|
+
const cwd = expandTilde(request.source.path);
|
|
5754
|
+
const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
|
|
5755
|
+
if (!directoryExists) {
|
|
5756
|
+
this.emit({
|
|
5757
|
+
type: "workspace.create.response",
|
|
5758
|
+
payload: {
|
|
5759
|
+
requestId: request.requestId,
|
|
5760
|
+
workspace: null,
|
|
5761
|
+
setupTerminalId: null,
|
|
5762
|
+
error: `Directory not found: ${cwd}`,
|
|
5763
|
+
errorCode: "directory_not_found",
|
|
5764
|
+
},
|
|
5765
|
+
});
|
|
5766
|
+
return;
|
|
5767
|
+
}
|
|
5768
|
+
const workspace = await createLocalCheckoutWorkspace({ cwd, title: request.title ?? null }, {
|
|
5769
|
+
projectRegistry: this.projectRegistry,
|
|
5770
|
+
workspaceRegistry: this.workspaceRegistry,
|
|
5771
|
+
workspaceGitService: this.workspaceGitService,
|
|
5772
|
+
});
|
|
5773
|
+
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
|
5774
|
+
const descriptor = await this.describeWorkspaceRecord(workspace);
|
|
5775
|
+
this.emit({
|
|
5776
|
+
type: "workspace.create.response",
|
|
5777
|
+
payload: {
|
|
5778
|
+
requestId: request.requestId,
|
|
5779
|
+
workspace: descriptor,
|
|
5780
|
+
setupTerminalId: null,
|
|
5781
|
+
error: null,
|
|
5782
|
+
},
|
|
5783
|
+
});
|
|
5784
|
+
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
|
5785
|
+
void this.workspaceGitService
|
|
5786
|
+
.getSnapshot(workspace.cwd, { force: true, includeGitHub: true, reason: "open_project" })
|
|
5787
|
+
.catch((error) => {
|
|
5788
|
+
this.sessionLogger.warn({ err: error, cwd: workspace.cwd }, "Background snapshot refresh failed after workspace.create");
|
|
5789
|
+
});
|
|
5790
|
+
if (request.firstAgentContext) {
|
|
5791
|
+
const firstAgentContext = request.firstAgentContext;
|
|
5792
|
+
this.scheduleWorkspaceNaming(() => this.maybeAutoNameDirectoryWorkspaceTitle({
|
|
5793
|
+
workspaceId: workspace.workspaceId,
|
|
5794
|
+
cwd: workspace.cwd,
|
|
5795
|
+
firstAgentContext,
|
|
5796
|
+
}), { cwd: workspace.cwd, message: "Failed to auto-name directory workspace title" });
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
// Schedules a background workspace-naming write off the request path. The
|
|
5800
|
+
// setTimeout(0) keeps the LLM call off the hot path.
|
|
5801
|
+
scheduleWorkspaceNaming(run, context) {
|
|
5802
|
+
setTimeout(() => {
|
|
5803
|
+
void run().catch((error) => {
|
|
5804
|
+
this.sessionLogger.warn({ err: error, cwd: context.cwd }, context.message);
|
|
5805
|
+
});
|
|
5806
|
+
}, 0);
|
|
5807
|
+
}
|
|
5808
|
+
async handleWorkspaceCreateWorktree(request) {
|
|
5809
|
+
if (request.source.kind !== "worktree") {
|
|
5810
|
+
return;
|
|
5811
|
+
}
|
|
5812
|
+
const source = request.source;
|
|
5813
|
+
if (!source.cwd && !source.projectId) {
|
|
5814
|
+
this.emit({
|
|
5815
|
+
type: "workspace.create.response",
|
|
5816
|
+
payload: {
|
|
5817
|
+
requestId: request.requestId,
|
|
5818
|
+
workspace: null,
|
|
5819
|
+
setupTerminalId: null,
|
|
5820
|
+
error: "cwd or projectId is required for a worktree-backed workspace",
|
|
5821
|
+
errorCode: "source_required",
|
|
5822
|
+
},
|
|
5823
|
+
});
|
|
5824
|
+
return;
|
|
5825
|
+
}
|
|
5826
|
+
const sourceCwd = await this.resolveWorktreeSourceCwd({
|
|
5827
|
+
cwd: source.cwd,
|
|
5828
|
+
projectId: source.projectId,
|
|
5829
|
+
});
|
|
5830
|
+
const result = await this.createPaseoWorktreeWorkflow({
|
|
5831
|
+
cwd: sourceCwd,
|
|
5832
|
+
projectId: source.projectId,
|
|
5833
|
+
worktreeSlug: source.worktreeSlug,
|
|
5834
|
+
action: source.action,
|
|
5835
|
+
refName: source.refName,
|
|
5836
|
+
githubPrNumber: source.githubPrNumber,
|
|
5837
|
+
firstAgentContext: request.firstAgentContext,
|
|
5838
|
+
}, source.baseBranch
|
|
5839
|
+
? { resolveDefaultBranch: async () => source.baseBranch }
|
|
5840
|
+
: undefined);
|
|
5841
|
+
if (request.title?.trim()) {
|
|
5842
|
+
await this.workspaceRegistry.upsert({
|
|
5843
|
+
...result.workspace,
|
|
5844
|
+
title: request.title.trim(),
|
|
5845
|
+
updatedAt: new Date().toISOString(),
|
|
5846
|
+
});
|
|
5847
|
+
result.workspace.title = request.title.trim();
|
|
5462
5848
|
}
|
|
5849
|
+
const descriptor = await this.describeCreatedWorktreeWorkspace(result);
|
|
5850
|
+
this.emit({
|
|
5851
|
+
type: "workspace.create.response",
|
|
5852
|
+
payload: {
|
|
5853
|
+
requestId: request.requestId,
|
|
5854
|
+
workspace: descriptor,
|
|
5855
|
+
setupTerminalId: null,
|
|
5856
|
+
error: null,
|
|
5857
|
+
},
|
|
5858
|
+
});
|
|
5859
|
+
this.emit({
|
|
5860
|
+
type: "workspace_update",
|
|
5861
|
+
payload: { kind: "upsert", workspace: descriptor },
|
|
5862
|
+
});
|
|
5863
|
+
}
|
|
5864
|
+
async resolveWorktreeSourceCwd(input) {
|
|
5865
|
+
if (input.cwd) {
|
|
5866
|
+
return expandTilde(input.cwd);
|
|
5867
|
+
}
|
|
5868
|
+
const project = await this.projectRegistry.get(input.projectId);
|
|
5869
|
+
if (!project || project.archivedAt) {
|
|
5870
|
+
throw new Error(`Project not found: ${input.projectId}`);
|
|
5871
|
+
}
|
|
5872
|
+
return project.rootPath;
|
|
5463
5873
|
}
|
|
5464
5874
|
async handleOpenProjectRequest(request) {
|
|
5465
5875
|
const requestedCwd = request.cwd;
|
|
@@ -5491,7 +5901,7 @@ export class Session {
|
|
|
5491
5901
|
const project = await this.projectRegistry.get(workspace.projectId);
|
|
5492
5902
|
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
|
5493
5903
|
const descriptor = await this.describeWorkspaceRecord(workspace);
|
|
5494
|
-
await this.
|
|
5904
|
+
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
|
5495
5905
|
this.sessionLogger.info({
|
|
5496
5906
|
requestedCwd,
|
|
5497
5907
|
resolvedCwd: cwd,
|
|
@@ -5664,7 +6074,7 @@ export class Session {
|
|
|
5664
6074
|
createPaseoWorktree: (workflowInput, serviceOptions) => this.createPaseoWorktree(workflowInput, serviceOptions),
|
|
5665
6075
|
warmWorkspaceGitData: (workspace) => this.warmWorkspaceGitDataForWorkspace(workspace),
|
|
5666
6076
|
autoNameWorkspaceBranchForFirstAgent: (autoNameInput) => this.scheduleAutoNameWorkspaceBranchForFirstAgent(autoNameInput),
|
|
5667
|
-
|
|
6077
|
+
emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
|
|
5668
6078
|
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) => {
|
|
5669
6079
|
this.workspaceSetupSnapshots.set(workspaceId, snapshot);
|
|
5670
6080
|
},
|
|
@@ -5694,12 +6104,33 @@ export class Session {
|
|
|
5694
6104
|
if (!existing) {
|
|
5695
6105
|
throw new Error(`Workspace not found: ${request.workspaceId}`);
|
|
5696
6106
|
}
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
const
|
|
5701
|
-
await
|
|
5702
|
-
|
|
6107
|
+
const gitSnapshot = await this.workspaceGitService
|
|
6108
|
+
.getSnapshot(existing.cwd)
|
|
6109
|
+
.catch(() => null);
|
|
6110
|
+
const repoRoot = gitSnapshot?.git?.repoRoot ?? null;
|
|
6111
|
+
await archiveByScope({
|
|
6112
|
+
paseoHome: this.paseoHome,
|
|
6113
|
+
paseoWorktreesBaseRoot: this.worktreesRoot,
|
|
6114
|
+
github: this.github,
|
|
6115
|
+
workspaceGitService: this.workspaceGitService,
|
|
6116
|
+
agentManager: this.agentManager,
|
|
6117
|
+
agentStorage: this.agentStorage,
|
|
6118
|
+
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
|
|
6119
|
+
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
|
|
6120
|
+
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
|
6121
|
+
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
|
|
6122
|
+
markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
|
|
6123
|
+
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
|
|
6124
|
+
killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
|
|
6125
|
+
sessionLogger: this.sessionLogger,
|
|
6126
|
+
}, {
|
|
6127
|
+
scope: { kind: "workspace", workspaceId: existing.workspaceId },
|
|
6128
|
+
repoRoot,
|
|
6129
|
+
paseoWorktreesBaseRoot: this.worktreesRoot,
|
|
6130
|
+
requestId: request.requestId,
|
|
6131
|
+
});
|
|
6132
|
+
const archivedWorkspace = await this.workspaceRegistry.get(request.workspaceId);
|
|
6133
|
+
const archivedAt = archivedWorkspace?.archivedAt ?? new Date().toISOString();
|
|
5703
6134
|
this.emit({
|
|
5704
6135
|
type: "archive_workspace_response",
|
|
5705
6136
|
payload: {
|
|
@@ -5760,10 +6191,12 @@ export class Session {
|
|
|
5760
6191
|
if (!workspace || workspace.archivedAt) {
|
|
5761
6192
|
throw new Error(`Workspace not found: ${requestedWorkspaceId}`);
|
|
5762
6193
|
}
|
|
5763
|
-
|
|
6194
|
+
// Clearing attention is scoped to the workspace that OWNS the agent, by
|
|
6195
|
+
// workspaceId — never by comparing cwd strings. A sibling workspace
|
|
6196
|
+
// sharing the same directory keeps its own agents' attention.
|
|
5764
6197
|
const clearableAgentIds = agents
|
|
5765
6198
|
.filter((agent) => !agent.archivedAt)
|
|
5766
|
-
.filter((agent) =>
|
|
6199
|
+
.filter((agent) => agent.workspaceId === workspace.workspaceId)
|
|
5767
6200
|
.filter((agent) => agent.requiresAttention === true)
|
|
5768
6201
|
.filter((agent) => (agent.pendingPermissions?.length ?? 0) === 0)
|
|
5769
6202
|
.filter((agent) => agent.attentionReason !== "permission")
|
|
@@ -5791,7 +6224,7 @@ export class Session {
|
|
|
5791
6224
|
};
|
|
5792
6225
|
await this.agentStorage.upsert(nextRecord);
|
|
5793
6226
|
const agent = this.buildStoredAgentPayload(nextRecord);
|
|
5794
|
-
const project = await this.
|
|
6227
|
+
const project = await this.buildProjectPlacementForWorkspace(workspace);
|
|
5795
6228
|
this.emit({
|
|
5796
6229
|
type: "agent_update",
|
|
5797
6230
|
payload: {
|
|
@@ -5862,7 +6295,9 @@ export class Session {
|
|
|
5862
6295
|
});
|
|
5863
6296
|
return;
|
|
5864
6297
|
}
|
|
5865
|
-
const project =
|
|
6298
|
+
const project = agent.workspaceId
|
|
6299
|
+
? await this.buildProjectPlacementForWorkspaceId(agent.workspaceId)
|
|
6300
|
+
: null;
|
|
5866
6301
|
this.emit({
|
|
5867
6302
|
type: "fetch_agent_response",
|
|
5868
6303
|
payload: { requestId, agent, project, error: null },
|
|
@@ -6651,10 +7086,15 @@ export class Session {
|
|
|
6651
7086
|
* Emit a message to the client
|
|
6652
7087
|
*/
|
|
6653
7088
|
emit(msg) {
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
7089
|
+
// JSON.stringify(msg) is only computed when trace is enabled — it runs for
|
|
7090
|
+
// every outbound message otherwise, and trace is disabled by default.
|
|
7091
|
+
// Optional-chained because test logger stubs don't implement isLevelEnabled.
|
|
7092
|
+
if (this.sessionLogger.isLevelEnabled?.("trace")) {
|
|
7093
|
+
this.sessionLogger.trace({
|
|
7094
|
+
messageType: msg.type,
|
|
7095
|
+
payloadBytes: JSON.stringify(msg).length,
|
|
7096
|
+
}, "agent.session.outbound");
|
|
7097
|
+
}
|
|
6658
7098
|
if (msg.type === "audio_output" &&
|
|
6659
7099
|
(process.env.TTS_DEBUG_AUDIO_DIR || isPaseoDictationDebugEnabled()) &&
|
|
6660
7100
|
msg.payload.groupId &&
|
|
@@ -6714,6 +7154,10 @@ export class Session {
|
|
|
6714
7154
|
this.unsubscribeAgentEvents();
|
|
6715
7155
|
this.unsubscribeAgentEvents = null;
|
|
6716
7156
|
}
|
|
7157
|
+
if (this.unsubscribeTerminalWorkspaceContributionEvents) {
|
|
7158
|
+
this.unsubscribeTerminalWorkspaceContributionEvents();
|
|
7159
|
+
this.unsubscribeTerminalWorkspaceContributionEvents = null;
|
|
7160
|
+
}
|
|
6717
7161
|
if (this.unsubscribeProviderSnapshotEvents) {
|
|
6718
7162
|
this.unsubscribeProviderSnapshotEvents();
|
|
6719
7163
|
this.unsubscribeProviderSnapshotEvents = null;
|