@getpaseo/server 0.1.96 → 0.1.97-beta.2
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/create-agent-title.d.ts +2 -0
- package/dist/server/server/agent/create-agent-title.js +5 -0
- 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 +125 -64
- 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 +74 -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 +33 -6
- package/dist/server/server/session.js +691 -202
- 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 +29 -7
- 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 +8 -3
- package/dist/server/utils/build-metadata-prompt.js +10 -9
- 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
- package/dist/server/utils/wrap-user-instructions.d.ts +0 -2
- package/dist/server/utils/wrap-user-instructions.js +0 -13
|
@@ -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";
|
|
@@ -23,7 +23,7 @@ import { createVoiceTurnController, } from "./voice/voice-turn-controller.js";
|
|
|
23
23
|
import { buildConfigOverrides, extractTimestamps, isStoredAgentProviderAvailable, toAgentPersistenceHandle, } from "./persistence-hooks.js";
|
|
24
24
|
import { ensureAgentLoaded } from "./agent/agent-loading.js";
|
|
25
25
|
import { formatSystemNotificationPrompt, sendPromptToAgent, waitForAgentRunStartWithTimeout, unarchiveAgentState, } from "./agent/agent-prompt.js";
|
|
26
|
-
import { resolveCreateAgentTitles } from "./agent/create-agent-title.js";
|
|
26
|
+
import { resolveCreateAgentTitles, resolveFirstAgentPromptTitle, } from "./agent/create-agent-title.js";
|
|
27
27
|
import { respondToAgentPermission } from "./agent/permission-response.js";
|
|
28
28
|
import { experimental_createMCPClient } from "ai";
|
|
29
29
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
@@ -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
|
});
|
|
@@ -2096,6 +2169,7 @@ export class Session {
|
|
|
2096
2169
|
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
|
|
2097
2170
|
...(attachments && attachments.length > 0 ? { attachments } : {}),
|
|
2098
2171
|
};
|
|
2172
|
+
const workspacePromptTitle = resolveFirstAgentPromptTitle(firstAgentContext);
|
|
2099
2173
|
const createdWorktree = await this.createAgentLifecycleDispatch.createWorktreeForRequest({
|
|
2100
2174
|
cwd: config.cwd,
|
|
2101
2175
|
target: worktree,
|
|
@@ -2106,19 +2180,23 @@ export class Session {
|
|
|
2106
2180
|
const createAgentConfig = createdWorktree
|
|
2107
2181
|
? { ...config, cwd: createdWorktree.worktree.worktreePath }
|
|
2108
2182
|
: config;
|
|
2183
|
+
const workspaceId = await this.resolveOrCreateWorkspaceIdForCreateAgent({
|
|
2184
|
+
createdWorktree,
|
|
2185
|
+
requestedWorkspaceId: msg.workspaceId,
|
|
2186
|
+
cwd: createAgentConfig.cwd,
|
|
2187
|
+
initialTitle: workspacePromptTitle,
|
|
2188
|
+
});
|
|
2109
2189
|
const { snapshot, liveSnapshot } = await createAgentCommand({
|
|
2110
2190
|
agentManager: this.agentManager,
|
|
2111
2191
|
agentStorage: this.agentStorage,
|
|
2112
2192
|
logger: this.sessionLogger,
|
|
2113
2193
|
paseoHome: this.paseoHome,
|
|
2114
2194
|
worktreesRoot: this.worktreesRoot,
|
|
2115
|
-
workspaceGitService: this.workspaceGitService,
|
|
2116
2195
|
providerSnapshotManager: this.providerSnapshotManager,
|
|
2117
|
-
daemonConfig: this.readStructuredGenerationDaemonConfig(),
|
|
2118
2196
|
}, {
|
|
2119
2197
|
kind: "session",
|
|
2120
2198
|
config: createAgentConfig,
|
|
2121
|
-
workspaceId
|
|
2199
|
+
workspaceId,
|
|
2122
2200
|
worktreeName,
|
|
2123
2201
|
initialPrompt,
|
|
2124
2202
|
clientMessageId,
|
|
@@ -2129,13 +2207,21 @@ export class Session {
|
|
|
2129
2207
|
labels,
|
|
2130
2208
|
env,
|
|
2131
2209
|
provisionalTitle,
|
|
2132
|
-
explicitTitle,
|
|
2133
2210
|
firstAgentContext,
|
|
2134
2211
|
buildSessionConfig: (sessionConfig, gitOptions, legacyWorktreeName, ctx) => this.buildAgentSessionConfig(sessionConfig, gitOptions, legacyWorktreeName, ctx),
|
|
2135
|
-
resolveWorkspace: ({ cwd, workspaceId }) => this.resolveCreateAgentWorkspace(cwd, workspaceId),
|
|
2136
2212
|
});
|
|
2137
2213
|
createdAgentId = snapshot.id;
|
|
2214
|
+
if (!createdWorktree && msg.workspaceId) {
|
|
2215
|
+
await this.writeInitialWorkspaceTitleIfUntitled(workspaceId, workspacePromptTitle);
|
|
2216
|
+
}
|
|
2138
2217
|
await this.forwardAgentUpdate(snapshot);
|
|
2218
|
+
if (!createdWorktree && trimmedPrompt) {
|
|
2219
|
+
await this.scheduleAutoNameLocalWorkspaceTitleForFirstAgent({
|
|
2220
|
+
workspaceId,
|
|
2221
|
+
cwd: createAgentConfig.cwd,
|
|
2222
|
+
firstAgentContext,
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2139
2225
|
this.createAgentLifecycleDispatch.registerAutoArchiveIfRequested({
|
|
2140
2226
|
autoArchive,
|
|
2141
2227
|
agentId: snapshot.id,
|
|
@@ -2184,16 +2270,6 @@ export class Session {
|
|
|
2184
2270
|
});
|
|
2185
2271
|
}
|
|
2186
2272
|
}
|
|
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
2273
|
async handleResumeAgentRequest(msg) {
|
|
2198
2274
|
const { handle, overrides, requestId } = msg;
|
|
2199
2275
|
if (!handle) {
|
|
@@ -2283,17 +2359,20 @@ export class Session {
|
|
|
2283
2359
|
const { provider, providerHandleId, requestId } = normalized;
|
|
2284
2360
|
this.sessionLogger.info({ providerHandleId, provider }, `Importing agent ${providerHandleId} (${provider})`);
|
|
2285
2361
|
try {
|
|
2362
|
+
if (!normalized.cwd) {
|
|
2363
|
+
throw new Error("Import requires cwd from the selected provider session");
|
|
2364
|
+
}
|
|
2365
|
+
// An imported agent mints its own workspace; ownership is its workspaceId,
|
|
2366
|
+
// never an existing same-cwd workspace resolved by path.
|
|
2367
|
+
const workspace = await this.createWorkspaceForDirectory(normalized.cwd);
|
|
2286
2368
|
const { snapshot, timelineSize } = await importProviderSession({
|
|
2287
2369
|
request: normalized,
|
|
2370
|
+
workspaceId: workspace.workspaceId,
|
|
2288
2371
|
agentManager: this.agentManager,
|
|
2289
2372
|
agentStorage: this.agentStorage,
|
|
2290
|
-
workspaceGitService: this.workspaceGitService,
|
|
2291
|
-
providerSnapshotManager: this.providerSnapshotManager,
|
|
2292
|
-
daemonConfig: this.readStructuredGenerationDaemonConfig(),
|
|
2293
|
-
paseoHome: this.paseoHome,
|
|
2294
2373
|
logger: this.sessionLogger,
|
|
2295
2374
|
});
|
|
2296
|
-
await this.registerWorkspaceForImportedAgent(
|
|
2375
|
+
await this.registerWorkspaceForImportedAgent(workspace);
|
|
2297
2376
|
const agentPayload = await this.buildAgentPayload(snapshot);
|
|
2298
2377
|
this.emit({
|
|
2299
2378
|
type: "status",
|
|
@@ -2472,18 +2551,20 @@ export class Session {
|
|
|
2472
2551
|
}, config, gitOptions, legacyWorktreeName, firstAgentContext);
|
|
2473
2552
|
}
|
|
2474
2553
|
scheduleAutoNameWorkspaceBranchForFirstAgent(input) {
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
}, 0);
|
|
2554
|
+
this.scheduleWorkspaceNaming(() => this.maybeAutoNameWorkspaceBranchForFirstAgent(input), {
|
|
2555
|
+
cwd: input.workspace.cwd,
|
|
2556
|
+
message: "Failed to auto-name worktree branch",
|
|
2557
|
+
});
|
|
2480
2558
|
}
|
|
2481
2559
|
async maybeAutoNameWorkspaceBranchForFirstAgent(input) {
|
|
2560
|
+
// Capture the generated title from the generator callback so we can write
|
|
2561
|
+
// title := generated title after the branch rename completes.
|
|
2562
|
+
let generatedTitle = null;
|
|
2482
2563
|
const result = await attemptFirstAgentBranchAutoName({
|
|
2483
2564
|
cwd: input.workspace.cwd,
|
|
2484
2565
|
firstAgentContext: input.firstAgentContext,
|
|
2485
2566
|
generateBranchNameFromContext: ({ cwd, firstAgentContext }) => {
|
|
2486
|
-
return
|
|
2567
|
+
return this.generateWorkspaceName({
|
|
2487
2568
|
agentManager: this.agentManager,
|
|
2488
2569
|
cwd,
|
|
2489
2570
|
workspaceGitService: this.workspaceGitService,
|
|
@@ -2492,21 +2573,99 @@ export class Session {
|
|
|
2492
2573
|
currentSelection: this.getFocusedAgentSelectionForCwd(cwd),
|
|
2493
2574
|
firstAgentContext,
|
|
2494
2575
|
logger: this.sessionLogger,
|
|
2576
|
+
}).then((r) => {
|
|
2577
|
+
generatedTitle = r?.title ?? null;
|
|
2578
|
+
return r?.branch ?? null;
|
|
2495
2579
|
});
|
|
2496
2580
|
},
|
|
2497
2581
|
});
|
|
2498
|
-
if (!result.renamed || !
|
|
2499
|
-
return
|
|
2582
|
+
if (!result.renamed || !generatedTitle) {
|
|
2583
|
+
return;
|
|
2500
2584
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2585
|
+
// K4: re-read from the registry before writing so any concurrent upsert
|
|
2586
|
+
// that happened between workspace creation and this async path is not clobbered.
|
|
2587
|
+
// The first-agent rename renamed the git branch too, so persist the new branch
|
|
2588
|
+
// alongside the title — both are this path's own fields.
|
|
2589
|
+
await this.applyGeneratedWorkspaceTitle(input.workspace.workspaceId, {
|
|
2590
|
+
title: generatedTitle,
|
|
2591
|
+
branch: result.branchName,
|
|
2592
|
+
promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext),
|
|
2593
|
+
});
|
|
2507
2594
|
await this.notifyGitMutation(input.workspace.cwd, "rename-branch");
|
|
2508
2595
|
await this.emitWorkspaceUpdateForCwd(input.workspace.cwd);
|
|
2509
|
-
|
|
2596
|
+
}
|
|
2597
|
+
// Generated names may replace the prompt title set at creation, but not a user
|
|
2598
|
+
// rename that landed while the async generator was running.
|
|
2599
|
+
async applyGeneratedWorkspaceTitle(workspaceId, input) {
|
|
2600
|
+
const current = await this.workspaceRegistry.get(workspaceId);
|
|
2601
|
+
if (!current) {
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
let title = current.title;
|
|
2605
|
+
if (!title || (input.promptTitle && title === input.promptTitle)) {
|
|
2606
|
+
title = input.title;
|
|
2607
|
+
}
|
|
2608
|
+
await this.workspaceRegistry.upsert({
|
|
2609
|
+
...current,
|
|
2610
|
+
title,
|
|
2611
|
+
...(input.branch ? { branch: input.branch } : {}),
|
|
2612
|
+
updatedAt: new Date().toISOString(),
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
async writeInitialWorkspaceTitleIfUntitled(workspaceId, title) {
|
|
2616
|
+
if (!title) {
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
const current = await this.workspaceRegistry.get(workspaceId);
|
|
2620
|
+
if (!current || current.title) {
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
await this.workspaceRegistry.upsert({
|
|
2624
|
+
...current,
|
|
2625
|
+
title,
|
|
2626
|
+
updatedAt: new Date().toISOString(),
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
// Wraps the injected workspace-name generator for a directory workspace.
|
|
2630
|
+
async generateWorkspaceTitleFromContext(input) {
|
|
2631
|
+
return this.generateWorkspaceName({
|
|
2632
|
+
agentManager: this.agentManager,
|
|
2633
|
+
cwd: input.cwd,
|
|
2634
|
+
workspaceGitService: this.workspaceGitService,
|
|
2635
|
+
providerSnapshotManager: this.providerSnapshotManager,
|
|
2636
|
+
daemonConfig: this.readStructuredGenerationDaemonConfig(),
|
|
2637
|
+
currentSelection: this.getFocusedAgentSelectionForCwd(input.cwd),
|
|
2638
|
+
firstAgentContext: input.firstAgentContext,
|
|
2639
|
+
logger: this.sessionLogger,
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
// Generates a human title for a directory workspace from the firstAgentContext
|
|
2643
|
+
// prompt. No branch rename — directory workspaces have no worktree git state.
|
|
2644
|
+
// TODO(K7): same-dir directory-workspace display disambiguation not yet implemented.
|
|
2645
|
+
async maybeAutoNameDirectoryWorkspaceTitle(input) {
|
|
2646
|
+
const generated = await this.generateWorkspaceTitleFromContext({
|
|
2647
|
+
cwd: input.cwd,
|
|
2648
|
+
firstAgentContext: input.firstAgentContext,
|
|
2649
|
+
});
|
|
2650
|
+
const title = generated?.title ?? null;
|
|
2651
|
+
if (!title) {
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
// K4: applyGeneratedWorkspaceTitle re-reads from the registry before writing.
|
|
2655
|
+
// Directory workspaces have no branch — write only the title.
|
|
2656
|
+
await this.applyGeneratedWorkspaceTitle(input.workspaceId, {
|
|
2657
|
+
title,
|
|
2658
|
+
promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext),
|
|
2659
|
+
});
|
|
2660
|
+
await this.emitWorkspaceUpdateForWorkspaceId(input.workspaceId);
|
|
2661
|
+
}
|
|
2662
|
+
async scheduleAutoNameLocalWorkspaceTitleForFirstAgent(input) {
|
|
2663
|
+
const workspaceId = input.workspaceId;
|
|
2664
|
+
this.scheduleWorkspaceNaming(() => this.maybeAutoNameDirectoryWorkspaceTitle({
|
|
2665
|
+
workspaceId,
|
|
2666
|
+
cwd: input.cwd,
|
|
2667
|
+
firstAgentContext: input.firstAgentContext,
|
|
2668
|
+
}), { cwd: input.cwd, message: "Failed to auto-name local workspace title" });
|
|
2510
2669
|
}
|
|
2511
2670
|
emitProviderDisabledResponse(kind, provider, requestId, fetchedAt) {
|
|
2512
2671
|
const payload = {
|
|
@@ -2719,6 +2878,7 @@ export class Session {
|
|
|
2719
2878
|
relayPublicEndpoint: relay?.publicEndpoint,
|
|
2720
2879
|
relayUseTls: relay?.useTls,
|
|
2721
2880
|
relayPublicUseTls: relay?.publicUseTls,
|
|
2881
|
+
appBaseUrl: this.daemonRuntimeConfig?.appBaseUrl,
|
|
2722
2882
|
includeQr: true,
|
|
2723
2883
|
logger: this.sessionLogger,
|
|
2724
2884
|
});
|
|
@@ -2874,8 +3034,13 @@ export class Session {
|
|
|
2874
3034
|
const prompt = await buildMetadataPrompt({
|
|
2875
3035
|
cwd,
|
|
2876
3036
|
workspaceGitService: this.workspaceGitService,
|
|
2877
|
-
|
|
2878
|
-
|
|
3037
|
+
contract: "Write a concise git commit message for the changes below.",
|
|
3038
|
+
styles: [
|
|
3039
|
+
{
|
|
3040
|
+
configKey: "commitMessage",
|
|
3041
|
+
default: "Concise, imperative mood, no trailing period.",
|
|
3042
|
+
},
|
|
3043
|
+
],
|
|
2879
3044
|
after: [
|
|
2880
3045
|
"Return JSON only with a single field 'message'.",
|
|
2881
3046
|
"",
|
|
@@ -2942,8 +3107,13 @@ export class Session {
|
|
|
2942
3107
|
const prompt = await buildMetadataPrompt({
|
|
2943
3108
|
cwd,
|
|
2944
3109
|
workspaceGitService: this.workspaceGitService,
|
|
2945
|
-
|
|
2946
|
-
|
|
3110
|
+
contract: "Write a pull request title and body for the changes below.",
|
|
3111
|
+
styles: [
|
|
3112
|
+
{
|
|
3113
|
+
configKey: "pullRequest",
|
|
3114
|
+
default: "Clear, descriptive title; body explaining what changed and why.",
|
|
3115
|
+
},
|
|
3116
|
+
],
|
|
2947
3117
|
after: [
|
|
2948
3118
|
"Return JSON only with fields 'title' and 'body'.",
|
|
2949
3119
|
"",
|
|
@@ -3211,16 +3381,33 @@ export class Session {
|
|
|
3211
3381
|
* Handle client heartbeat for activity tracking
|
|
3212
3382
|
*/
|
|
3213
3383
|
handleClientHeartbeat(msg) {
|
|
3384
|
+
const focusedTerminalId = msg.focusedTerminalId?.trim() || null;
|
|
3214
3385
|
const appVisibilityChangedAt = msg.appVisibilityChangedAt
|
|
3215
3386
|
? new Date(msg.appVisibilityChangedAt)
|
|
3216
3387
|
: new Date(msg.lastActivityAt);
|
|
3217
3388
|
this.clientActivity = {
|
|
3218
3389
|
deviceType: msg.deviceType,
|
|
3219
3390
|
focusedAgentId: msg.focusedAgentId,
|
|
3391
|
+
focusedTerminalId,
|
|
3220
3392
|
lastActivityAt: new Date(msg.lastActivityAt),
|
|
3221
3393
|
appVisible: msg.appVisible,
|
|
3222
3394
|
appVisibilityChangedAt,
|
|
3223
3395
|
};
|
|
3396
|
+
if (msg.appVisible && focusedTerminalId) {
|
|
3397
|
+
void this.clearFocusedTerminalAttention(focusedTerminalId);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
async clearFocusedTerminalAttention(terminalId) {
|
|
3401
|
+
const terminalManager = this.terminalManager;
|
|
3402
|
+
if (!terminalManager) {
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
try {
|
|
3406
|
+
await terminalManager.clearTerminalAttention(terminalId);
|
|
3407
|
+
}
|
|
3408
|
+
catch (error) {
|
|
3409
|
+
this.sessionLogger.warn({ err: error, terminalId }, "Failed to clear terminal attention");
|
|
3410
|
+
}
|
|
3224
3411
|
}
|
|
3225
3412
|
/**
|
|
3226
3413
|
* Handle push token registration
|
|
@@ -3542,7 +3729,7 @@ export class Session {
|
|
|
3542
3729
|
target.watchers.length = 0;
|
|
3543
3730
|
}
|
|
3544
3731
|
async removeWorkspaceGitWatchTarget(cwd) {
|
|
3545
|
-
const normalizedCwd =
|
|
3732
|
+
const normalizedCwd = resolve(cwd);
|
|
3546
3733
|
const target = this.workspaceGitWatchTargets.get(normalizedCwd);
|
|
3547
3734
|
if (target) {
|
|
3548
3735
|
this.closeWorkspaceGitWatchTarget(target);
|
|
@@ -3550,7 +3737,7 @@ export class Session {
|
|
|
3550
3737
|
}
|
|
3551
3738
|
}
|
|
3552
3739
|
removeWorkspaceGitSubscription(cwd) {
|
|
3553
|
-
const normalizedCwd =
|
|
3740
|
+
const normalizedCwd = resolve(cwd);
|
|
3554
3741
|
const target = this.workspaceGitWatchTargets.get(normalizedCwd);
|
|
3555
3742
|
if (target) {
|
|
3556
3743
|
const unsubscribeFetch = this.workspaceGitFetchSubscriptions.get(normalizedCwd);
|
|
@@ -3571,8 +3758,16 @@ export class Session {
|
|
|
3571
3758
|
workspace.diffStat ? [workspace.diffStat.additions, workspace.diffStat.deletions] : null,
|
|
3572
3759
|
]);
|
|
3573
3760
|
}
|
|
3761
|
+
resolveWorkspaceGitWatchTarget(workspaceId) {
|
|
3762
|
+
for (const target of this.workspaceGitWatchTargets.values()) {
|
|
3763
|
+
if (target.workspaceId === workspaceId) {
|
|
3764
|
+
return target;
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
return null;
|
|
3768
|
+
}
|
|
3574
3769
|
shouldSkipWorkspaceGitWatchUpdate(workspaceId, workspace) {
|
|
3575
|
-
const target = this.
|
|
3770
|
+
const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
|
3576
3771
|
if (!target) {
|
|
3577
3772
|
return false;
|
|
3578
3773
|
}
|
|
@@ -3584,7 +3779,7 @@ export class Session {
|
|
|
3584
3779
|
return false;
|
|
3585
3780
|
}
|
|
3586
3781
|
rememberWorkspaceGitDescriptorState(workspaceId, workspace) {
|
|
3587
|
-
const target = this.
|
|
3782
|
+
const target = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
|
3588
3783
|
if (!target) {
|
|
3589
3784
|
return;
|
|
3590
3785
|
}
|
|
@@ -3592,7 +3787,7 @@ export class Session {
|
|
|
3592
3787
|
target.lastBranchName = workspace?.name ?? null;
|
|
3593
3788
|
}
|
|
3594
3789
|
handleWorkspaceGitBranchSnapshot(cwd, branchName) {
|
|
3595
|
-
const target = this.workspaceGitWatchTargets.get(
|
|
3790
|
+
const target = this.workspaceGitWatchTargets.get(resolve(cwd));
|
|
3596
3791
|
if (!target) {
|
|
3597
3792
|
return;
|
|
3598
3793
|
}
|
|
@@ -3613,7 +3808,7 @@ export class Session {
|
|
|
3613
3808
|
}
|
|
3614
3809
|
}
|
|
3615
3810
|
syncWorkspaceGitObserver(cwd, options) {
|
|
3616
|
-
const normalizedCwd =
|
|
3811
|
+
const normalizedCwd = resolve(cwd);
|
|
3617
3812
|
if (!options.isGit) {
|
|
3618
3813
|
this.removeWorkspaceGitSubscription(normalizedCwd);
|
|
3619
3814
|
return;
|
|
@@ -3634,7 +3829,9 @@ export class Session {
|
|
|
3634
3829
|
this.workspaceGitWatchTargets.set(normalizedCwd, target);
|
|
3635
3830
|
const subscription = this.workspaceGitService.registerWorkspace({ cwd: normalizedCwd }, (snapshot) => {
|
|
3636
3831
|
this.handleWorkspaceGitBranchSnapshot(normalizedCwd, snapshot.git.currentBranch ?? null);
|
|
3637
|
-
void this.emitWorkspaceUpdateForCwd(normalizedCwd)
|
|
3832
|
+
void this.emitWorkspaceUpdateForCwd(normalizedCwd).catch((error) => {
|
|
3833
|
+
this.sessionLogger.warn({ err: error, cwd: normalizedCwd }, "Failed to emit workspace update after git branch snapshot");
|
|
3834
|
+
});
|
|
3638
3835
|
this.emitCheckoutStatusUpdate(normalizedCwd, snapshot);
|
|
3639
3836
|
});
|
|
3640
3837
|
this.workspaceGitSubscriptions.set(normalizedCwd, subscription.unsubscribe);
|
|
@@ -3739,10 +3936,14 @@ export class Session {
|
|
|
3739
3936
|
return;
|
|
3740
3937
|
}
|
|
3741
3938
|
try {
|
|
3742
|
-
const result = await renameCurrentBranch(cwd, branch);
|
|
3939
|
+
const result = await this.renameCurrentBranch(cwd, branch);
|
|
3743
3940
|
await this.notifyGitMutation(cwd, "rename-branch", { invalidateGithub: true });
|
|
3744
3941
|
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
|
3745
3942
|
this.handleWorkspaceGitBranchSnapshot(cwd, result.currentBranch);
|
|
3943
|
+
// Branch is a git fact derived per-descriptor from each workspace's own
|
|
3944
|
+
// live git snapshot (id → cwd); the reconciliation pass re-persists the
|
|
3945
|
+
// `branch` field per workspace from its own cwd. No cwd → ids fan-out here.
|
|
3946
|
+
// TODO(K10): PR-binding on branch rename is deferred — see plan K10.
|
|
3746
3947
|
// Push a workspace_update immediately so the sidebar/header reflect
|
|
3747
3948
|
// the new branch name without waiting for the background git watcher.
|
|
3748
3949
|
await this.emitWorkspaceUpdateForCwd(cwd);
|
|
@@ -4348,18 +4549,19 @@ export class Session {
|
|
|
4348
4549
|
async handlePaseoWorktreeArchiveRequest(msg) {
|
|
4349
4550
|
return handleWorktreeArchiveRequest({
|
|
4350
4551
|
paseoHome: this.paseoHome,
|
|
4351
|
-
|
|
4552
|
+
paseoWorktreesBaseRoot: this.worktreesRoot,
|
|
4352
4553
|
github: this.github,
|
|
4353
4554
|
workspaceGitService: this.workspaceGitService,
|
|
4354
4555
|
agentManager: this.agentManager,
|
|
4355
4556
|
agentStorage: this.agentStorage,
|
|
4557
|
+
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
|
|
4558
|
+
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
|
|
4356
4559
|
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
|
4357
4560
|
emit: (message) => this.emit(message),
|
|
4358
4561
|
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
|
|
4359
4562
|
markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
|
|
4360
4563
|
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
|
|
4361
|
-
|
|
4362
|
-
killTerminalsUnderPath: (rootPath) => this.terminalController.killTerminalsUnderPath(rootPath),
|
|
4564
|
+
killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
|
|
4363
4565
|
sessionLogger: this.sessionLogger,
|
|
4364
4566
|
}, msg);
|
|
4365
4567
|
}
|
|
@@ -4569,10 +4771,30 @@ export class Session {
|
|
|
4569
4771
|
});
|
|
4570
4772
|
}
|
|
4571
4773
|
}
|
|
4774
|
+
async listTerminalActivityContributions() {
|
|
4775
|
+
const terminalManager = this.terminalManager;
|
|
4776
|
+
if (!terminalManager) {
|
|
4777
|
+
return [];
|
|
4778
|
+
}
|
|
4779
|
+
const directories = terminalManager.listDirectories();
|
|
4780
|
+
const terminalsByDirectory = await Promise.all(directories.map((cwd) => terminalManager.getTerminals(cwd)));
|
|
4781
|
+
return terminalsByDirectory.flat().map((session) => {
|
|
4782
|
+
const contribution = {
|
|
4783
|
+
cwd: session.cwd,
|
|
4784
|
+
activity: session.getActivity(),
|
|
4785
|
+
};
|
|
4786
|
+
if (session.workspaceId) {
|
|
4787
|
+
contribution.workspaceId = session.workspaceId;
|
|
4788
|
+
}
|
|
4789
|
+
return contribution;
|
|
4790
|
+
});
|
|
4791
|
+
}
|
|
4572
4792
|
/**
|
|
4573
4793
|
* Build the current agent list payload (live + persisted), optionally filtered by labels.
|
|
4574
4794
|
*/
|
|
4575
4795
|
async listAgentPayloads(filter) {
|
|
4796
|
+
const includeArchived = filter?.includeArchived === true;
|
|
4797
|
+
const labelEntries = filter?.labels ? Object.entries(filter.labels) : [];
|
|
4576
4798
|
// Get live agents with session modes
|
|
4577
4799
|
const agentSnapshots = this.agentManager.listAgents();
|
|
4578
4800
|
const liveAgents = await Promise.all(agentSnapshots.map((agent) => this.buildAgentPayload(agent)));
|
|
@@ -4580,18 +4802,23 @@ export class Session {
|
|
|
4580
4802
|
// (excluding internal agents which are for ephemeral system tasks)
|
|
4581
4803
|
const registryRecords = await this.agentStorage.list();
|
|
4582
4804
|
const liveIds = new Set(agentSnapshots.map((a) => a.id));
|
|
4583
|
-
const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
|
|
4805
|
+
const registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds());
|
|
4584
4806
|
const persistedAgents = registryRecords
|
|
4585
4807
|
.filter((record) => !liveIds.has(record.id) && !record.internal)
|
|
4808
|
+
// Keep raw-record filters ahead of projection; seeded homes can carry thousands of archived agents.
|
|
4809
|
+
.filter((record) => includeArchived || !record.archivedAt)
|
|
4810
|
+
.filter((record) => labelEntries.every(([key, value]) => record.labels?.[key] === value))
|
|
4586
4811
|
.filter((record) => filter?.includeUnavailablePersisted === true ||
|
|
4587
4812
|
isStoredAgentProviderAvailable(record, registeredProviderIds))
|
|
4588
4813
|
.map((record) => this.buildStoredAgentPayload(record, registeredProviderIds));
|
|
4589
4814
|
let agents = [...liveAgents, ...persistedAgents];
|
|
4590
4815
|
agents = agents.filter((agent) => this.isProviderVisibleToClient(agent.provider));
|
|
4816
|
+
if (!includeArchived) {
|
|
4817
|
+
agents = agents.filter((agent) => !agent.archivedAt);
|
|
4818
|
+
}
|
|
4591
4819
|
// 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]));
|
|
4820
|
+
if (labelEntries.length > 0) {
|
|
4821
|
+
agents = agents.filter((agent) => labelEntries.every(([key, value]) => agent.labels[key] === value));
|
|
4595
4822
|
}
|
|
4596
4823
|
return agents;
|
|
4597
4824
|
}
|
|
@@ -4653,7 +4880,7 @@ export class Session {
|
|
|
4653
4880
|
const payload = this.buildStoredAgentPayload(record);
|
|
4654
4881
|
return this.isProviderVisibleToClient(payload.provider) ? payload : null;
|
|
4655
4882
|
}
|
|
4656
|
-
async
|
|
4883
|
+
async buildActiveProjectPlacementsByWorkspaceId() {
|
|
4657
4884
|
const [persistedWorkspaces, persistedProjects] = await Promise.all([
|
|
4658
4885
|
this.workspaceRegistry.list(),
|
|
4659
4886
|
this.projectRegistry.list(),
|
|
@@ -4661,7 +4888,7 @@ export class Session {
|
|
|
4661
4888
|
const activeProjects = new Map(persistedProjects
|
|
4662
4889
|
.filter((project) => !project.archivedAt)
|
|
4663
4890
|
.map((project) => [project.projectId, project]));
|
|
4664
|
-
const
|
|
4891
|
+
const placementsByWorkspaceId = new Map();
|
|
4665
4892
|
const pairs = persistedWorkspaces.flatMap((workspace) => {
|
|
4666
4893
|
if (workspace.archivedAt)
|
|
4667
4894
|
return [];
|
|
@@ -4672,9 +4899,9 @@ export class Session {
|
|
|
4672
4899
|
});
|
|
4673
4900
|
const placements = await Promise.all(pairs.map(({ workspace, project }) => this.buildProjectPlacementForWorkspace(workspace, project)));
|
|
4674
4901
|
for (let i = 0; i < pairs.length; i += 1) {
|
|
4675
|
-
|
|
4902
|
+
placementsByWorkspaceId.set(pairs[i].workspace.workspaceId, placements[i]);
|
|
4676
4903
|
}
|
|
4677
|
-
return
|
|
4904
|
+
return placementsByWorkspaceId;
|
|
4678
4905
|
}
|
|
4679
4906
|
async collectFetchAgentsEntries(params) {
|
|
4680
4907
|
const { candidates, limit, getPlacement, filter } = params;
|
|
@@ -4683,7 +4910,7 @@ export class Session {
|
|
|
4683
4910
|
for (let start = 0; start < candidates.length && matchedEntries.length <= limit; start += batchSize) {
|
|
4684
4911
|
const batch = candidates.slice(start, start + batchSize);
|
|
4685
4912
|
const batchEntries = await Promise.all(batch.map(async (agent) => {
|
|
4686
|
-
const project = await getPlacement(agent.
|
|
4913
|
+
const project = await getPlacement(agent.workspaceId);
|
|
4687
4914
|
return project ? { agent, project } : null;
|
|
4688
4915
|
}));
|
|
4689
4916
|
for (const entry of batchEntries) {
|
|
@@ -4714,23 +4941,29 @@ export class Session {
|
|
|
4714
4941
|
const sort = this.agentsPager.normalizeSort(request.sort);
|
|
4715
4942
|
let agents = await this.listAgentPayloads({
|
|
4716
4943
|
labels: filter?.labels,
|
|
4944
|
+
includeArchived: filter?.includeArchived,
|
|
4717
4945
|
includeUnavailablePersisted: request.type === "fetch_agent_history_request",
|
|
4718
4946
|
});
|
|
4719
|
-
const
|
|
4720
|
-
if (
|
|
4721
|
-
agents = agents.filter((agent) => !agent.archivedAt &&
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4947
|
+
const activePlacementsByWorkspaceId = scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceId() : null;
|
|
4948
|
+
if (activePlacementsByWorkspaceId) {
|
|
4949
|
+
agents = agents.filter((agent) => !agent.archivedAt &&
|
|
4950
|
+
agent.workspaceId != null &&
|
|
4951
|
+
activePlacementsByWorkspaceId.has(agent.workspaceId));
|
|
4952
|
+
}
|
|
4953
|
+
const placementByWorkspaceId = new Map();
|
|
4954
|
+
const getPlacement = (workspaceId) => {
|
|
4955
|
+
if (!workspaceId) {
|
|
4956
|
+
return Promise.resolve(null);
|
|
4727
4957
|
}
|
|
4728
|
-
|
|
4958
|
+
if (activePlacementsByWorkspaceId) {
|
|
4959
|
+
return Promise.resolve(activePlacementsByWorkspaceId.get(workspaceId) ?? null);
|
|
4960
|
+
}
|
|
4961
|
+
const existing = placementByWorkspaceId.get(workspaceId);
|
|
4729
4962
|
if (existing) {
|
|
4730
4963
|
return existing;
|
|
4731
4964
|
}
|
|
4732
|
-
const placementPromise = this.
|
|
4733
|
-
|
|
4965
|
+
const placementPromise = this.buildProjectPlacementForWorkspaceId(workspaceId);
|
|
4966
|
+
placementByWorkspaceId.set(workspaceId, placementPromise);
|
|
4734
4967
|
return placementPromise;
|
|
4735
4968
|
};
|
|
4736
4969
|
let candidates = [...agents];
|
|
@@ -4790,7 +5023,8 @@ export class Session {
|
|
|
4790
5023
|
workspaceDirectory: workspace.cwd,
|
|
4791
5024
|
projectKind: (resolvedProjectRecord?.kind ?? "directory") === "git" ? "git" : "non_git",
|
|
4792
5025
|
workspaceKind: workspace.kind,
|
|
4793
|
-
name: workspace
|
|
5026
|
+
name: resolveWorkspaceDisplayName(workspace),
|
|
5027
|
+
title: workspace.title,
|
|
4794
5028
|
archivingAt: null,
|
|
4795
5029
|
status: "done",
|
|
4796
5030
|
statusEnteredAt: null,
|
|
@@ -4847,7 +5081,7 @@ export class Session {
|
|
|
4847
5081
|
const displayName = deriveWorkspaceDisplayName({ cwd: workspace.cwd, checkout });
|
|
4848
5082
|
return {
|
|
4849
5083
|
...base,
|
|
4850
|
-
name: displayName,
|
|
5084
|
+
name: resolveWorkspaceName({ title: workspace.title, derivedDisplayName: displayName }),
|
|
4851
5085
|
diffStat: snapshot.git.diffStat ?? null,
|
|
4852
5086
|
gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot) ?? undefined,
|
|
4853
5087
|
githubRuntime: this.buildWorkspaceGitHubRuntimePayload(snapshot),
|
|
@@ -4866,10 +5100,14 @@ export class Session {
|
|
|
4866
5100
|
workspaceDirectory: result.workspace.cwd,
|
|
4867
5101
|
projectKind: "git",
|
|
4868
5102
|
workspaceKind: result.workspace.kind,
|
|
4869
|
-
name:
|
|
5103
|
+
name: resolveWorkspaceName({
|
|
5104
|
+
title: result.workspace.title,
|
|
5105
|
+
derivedDisplayName: result.worktree.branchName || result.workspace.displayName,
|
|
5106
|
+
}),
|
|
5107
|
+
title: result.workspace.title,
|
|
4870
5108
|
archivingAt: null,
|
|
4871
5109
|
status: "done",
|
|
4872
|
-
statusEnteredAt:
|
|
5110
|
+
statusEnteredAt: result.workspace.createdAt,
|
|
4873
5111
|
activityAt: null,
|
|
4874
5112
|
diffStat: { additions: 0, deletions: 0 },
|
|
4875
5113
|
scripts: [],
|
|
@@ -4900,8 +5138,13 @@ export class Session {
|
|
|
4900
5138
|
async buildWorkspaceDescriptorMap(options) {
|
|
4901
5139
|
return this.workspaceDirectory.buildDescriptorMap(options);
|
|
4902
5140
|
}
|
|
4903
|
-
|
|
4904
|
-
|
|
5141
|
+
// external path→workspace adapter, not ownership. Used by archive-by-path flows
|
|
5142
|
+
// where the request carries a worktree path (unique to one workspace) rather
|
|
5143
|
+
// than a workspaceId. This is a directory lookup for an archive target, not a
|
|
5144
|
+
// status/ownership attribution.
|
|
5145
|
+
async findWorkspaceIdForCwd(cwd) {
|
|
5146
|
+
const workspaces = await this.workspaceRegistry.list();
|
|
5147
|
+
return resolveWorkspaceIdForPath(cwd, workspaces);
|
|
4905
5148
|
}
|
|
4906
5149
|
matchesWorkspaceFilter(input) {
|
|
4907
5150
|
return this.workspaceDirectory.matchesFilter(input);
|
|
@@ -4969,7 +5212,7 @@ export class Session {
|
|
|
4969
5212
|
}
|
|
4970
5213
|
}
|
|
4971
5214
|
async findOrCreateWorkspaceForDirectory(cwd) {
|
|
4972
|
-
const inputCwd =
|
|
5215
|
+
const inputCwd = resolve(cwd);
|
|
4973
5216
|
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd);
|
|
4974
5217
|
const existingWorkspace = await this.findExactWorkspaceByDirectory(normalizedCwd, {
|
|
4975
5218
|
refreshGit: false,
|
|
@@ -4977,22 +5220,26 @@ export class Session {
|
|
|
4977
5220
|
if (existingWorkspace) {
|
|
4978
5221
|
if (existingWorkspace.archivedAt && inputCwd !== normalizedCwd) {
|
|
4979
5222
|
const timestamp = new Date().toISOString();
|
|
4980
|
-
const
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
5223
|
+
const checkout = checkoutLiteFromGitSnapshot(inputCwd, {
|
|
5224
|
+
isGit: false,
|
|
5225
|
+
currentBranch: null,
|
|
5226
|
+
remoteUrl: null,
|
|
5227
|
+
repoRoot: null,
|
|
5228
|
+
isPaseoOwnedWorktree: false,
|
|
5229
|
+
mainRepoRoot: null,
|
|
5230
|
+
});
|
|
5231
|
+
const membership = classifyDirectoryForProjectMembership({ cwd: inputCwd, checkout });
|
|
5232
|
+
const projectRecord = await this.resolveProjectRecordForPlacement({
|
|
5233
|
+
membership,
|
|
5234
|
+
timestamp,
|
|
4988
5235
|
});
|
|
4989
5236
|
await this.projectRegistry.upsert(projectRecord);
|
|
4990
5237
|
const workspaceRecord = createPersistedWorkspaceRecord({
|
|
4991
|
-
workspaceId:
|
|
5238
|
+
workspaceId: generateWorkspaceId(),
|
|
4992
5239
|
projectId: projectRecord.projectId,
|
|
4993
5240
|
cwd: inputCwd,
|
|
4994
|
-
kind:
|
|
4995
|
-
displayName,
|
|
5241
|
+
kind: membership.workspaceKind,
|
|
5242
|
+
displayName: membership.workspaceDisplayName,
|
|
4996
5243
|
createdAt: timestamp,
|
|
4997
5244
|
updatedAt: timestamp,
|
|
4998
5245
|
});
|
|
@@ -5007,7 +5254,16 @@ export class Session {
|
|
|
5007
5254
|
}
|
|
5008
5255
|
return this.createWorkspaceForDirectory(normalizedCwd);
|
|
5009
5256
|
}
|
|
5010
|
-
async
|
|
5257
|
+
async resolveOrCreateWorkspaceIdForCreateAgent(input) {
|
|
5258
|
+
if (input.createdWorktree) {
|
|
5259
|
+
return input.createdWorktree.workspace.workspaceId;
|
|
5260
|
+
}
|
|
5261
|
+
if (input.requestedWorkspaceId) {
|
|
5262
|
+
return input.requestedWorkspaceId;
|
|
5263
|
+
}
|
|
5264
|
+
return (await this.createWorkspaceForDirectory(input.cwd, input.initialTitle)).workspaceId;
|
|
5265
|
+
}
|
|
5266
|
+
async createWorkspaceForDirectory(cwd, title) {
|
|
5011
5267
|
const checkout = await this.workspaceGitService.getCheckout(cwd);
|
|
5012
5268
|
const membership = classifyDirectoryForProjectMembership({ cwd, checkout });
|
|
5013
5269
|
const timestamp = new Date().toISOString();
|
|
@@ -5017,11 +5273,12 @@ export class Session {
|
|
|
5017
5273
|
});
|
|
5018
5274
|
await this.projectRegistry.upsert(projectRecord);
|
|
5019
5275
|
const workspaceRecord = createPersistedWorkspaceRecord({
|
|
5020
|
-
workspaceId:
|
|
5276
|
+
workspaceId: generateWorkspaceId(),
|
|
5021
5277
|
projectId: projectRecord.projectId,
|
|
5022
5278
|
cwd,
|
|
5023
5279
|
kind: membership.workspaceKind,
|
|
5024
5280
|
displayName: membership.workspaceDisplayName,
|
|
5281
|
+
title: title ?? null,
|
|
5025
5282
|
createdAt: timestamp,
|
|
5026
5283
|
updatedAt: timestamp,
|
|
5027
5284
|
});
|
|
@@ -5039,8 +5296,7 @@ export class Session {
|
|
|
5039
5296
|
const projectId = projectRecord.projectId;
|
|
5040
5297
|
const kind = membership.workspaceKind;
|
|
5041
5298
|
const displayName = membership.workspaceDisplayName;
|
|
5042
|
-
if (input.workspace.
|
|
5043
|
-
input.workspace.projectId === projectId &&
|
|
5299
|
+
if (input.workspace.projectId === projectId &&
|
|
5044
5300
|
input.workspace.kind === kind &&
|
|
5045
5301
|
input.workspace.displayName === displayName) {
|
|
5046
5302
|
return this.ensureWorkspaceRecordUnarchived(input.workspace);
|
|
@@ -5048,7 +5304,7 @@ export class Session {
|
|
|
5048
5304
|
await this.projectRegistry.upsert(projectRecord);
|
|
5049
5305
|
const nextWorkspace = {
|
|
5050
5306
|
...input.workspace,
|
|
5051
|
-
workspaceId:
|
|
5307
|
+
workspaceId: input.workspace.workspaceId,
|
|
5052
5308
|
projectId,
|
|
5053
5309
|
cwd: input.cwd,
|
|
5054
5310
|
kind,
|
|
@@ -5122,16 +5378,28 @@ export class Session {
|
|
|
5122
5378
|
});
|
|
5123
5379
|
return result;
|
|
5124
5380
|
}
|
|
5381
|
+
async listActiveWorkspaceRefs() {
|
|
5382
|
+
const workspaces = await this.workspaceRegistry.list();
|
|
5383
|
+
return workspaces
|
|
5384
|
+
.filter((workspace) => !workspace.archivedAt)
|
|
5385
|
+
.map((workspace) => ({
|
|
5386
|
+
workspaceId: workspace.workspaceId,
|
|
5387
|
+
cwd: workspace.cwd,
|
|
5388
|
+
kind: workspace.kind,
|
|
5389
|
+
}));
|
|
5390
|
+
}
|
|
5125
5391
|
async archiveWorkspaceRecord(workspaceId, archivedAt) {
|
|
5126
5392
|
const archiveTimestamp = archivedAt ?? new Date().toISOString();
|
|
5127
5393
|
const existingWorkspace = await archivePersistedWorkspaceRecord({
|
|
5128
5394
|
workspaceId,
|
|
5129
5395
|
archivedAt: archiveTimestamp,
|
|
5130
5396
|
workspaceRegistry: this.workspaceRegistry,
|
|
5131
|
-
projectRegistry: this.projectRegistry,
|
|
5132
5397
|
});
|
|
5133
5398
|
if (!existingWorkspace) {
|
|
5134
|
-
this.
|
|
5399
|
+
const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
|
5400
|
+
if (watchTarget) {
|
|
5401
|
+
this.removeWorkspaceGitSubscription(watchTarget.cwd);
|
|
5402
|
+
}
|
|
5135
5403
|
return;
|
|
5136
5404
|
}
|
|
5137
5405
|
if (!existingWorkspace.archivedAt) {
|
|
@@ -5144,9 +5412,18 @@ export class Session {
|
|
|
5144
5412
|
archivedAt: archiveTimestamp,
|
|
5145
5413
|
}, "Workspace archived");
|
|
5146
5414
|
}
|
|
5147
|
-
await this.
|
|
5148
|
-
|
|
5149
|
-
|
|
5415
|
+
await this.teardownArchivedWorkspace({
|
|
5416
|
+
workspaceId: existingWorkspace.workspaceId,
|
|
5417
|
+
cwd: existingWorkspace.cwd,
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5420
|
+
// Git watch and subscription state is keyed by directory; the script runtime
|
|
5421
|
+
// store is keyed by the opaque workspace id. Each cleanup uses its own key so an
|
|
5422
|
+
// opaque id is never resolved as a filesystem path.
|
|
5423
|
+
async teardownArchivedWorkspace(input) {
|
|
5424
|
+
await this.removeWorkspaceGitWatchTarget(input.cwd);
|
|
5425
|
+
this.scriptRuntimeStore?.removeForWorkspace(input.workspaceId);
|
|
5426
|
+
this.removeWorkspaceGitSubscription(input.cwd);
|
|
5150
5427
|
}
|
|
5151
5428
|
async reconcileAndEmitWorkspaceUpdates() {
|
|
5152
5429
|
if (!this.workspaceUpdatesSubscription) {
|
|
@@ -5178,9 +5455,10 @@ export class Session {
|
|
|
5178
5455
|
await Promise.all(result.changesApplied.map(async (change) => {
|
|
5179
5456
|
switch (change.kind) {
|
|
5180
5457
|
case "workspace_archived":
|
|
5181
|
-
await this.
|
|
5182
|
-
|
|
5183
|
-
|
|
5458
|
+
await this.teardownArchivedWorkspace({
|
|
5459
|
+
workspaceId: change.workspaceId,
|
|
5460
|
+
cwd: change.directory,
|
|
5461
|
+
});
|
|
5184
5462
|
changedWorkspaceIds.add(change.workspaceId);
|
|
5185
5463
|
break;
|
|
5186
5464
|
case "workspace_updated":
|
|
@@ -5223,7 +5501,7 @@ export class Session {
|
|
|
5223
5501
|
this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)) {
|
|
5224
5502
|
continue;
|
|
5225
5503
|
}
|
|
5226
|
-
const watchTarget = this.
|
|
5504
|
+
const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
|
|
5227
5505
|
if (watchTarget && this.onBranchChanged) {
|
|
5228
5506
|
const newBranchName = nextWorkspace?.name ?? null;
|
|
5229
5507
|
if (newBranchName !== watchTarget.lastBranchName) {
|
|
@@ -5236,6 +5514,7 @@ export class Session {
|
|
|
5236
5514
|
this.bufferOrEmitWorkspaceUpdate(subscription, {
|
|
5237
5515
|
kind: "remove",
|
|
5238
5516
|
id: workspaceId,
|
|
5517
|
+
...(await this.resolveEmptyProjectForArchivedWorkspace(workspaceId)),
|
|
5239
5518
|
});
|
|
5240
5519
|
continue;
|
|
5241
5520
|
}
|
|
@@ -5255,10 +5534,38 @@ export class Session {
|
|
|
5255
5534
|
void this.reconcileAndEmitWorkspaceUpdates();
|
|
5256
5535
|
}
|
|
5257
5536
|
}
|
|
5537
|
+
// When a workspace is archived its project may become empty. Resolve the
|
|
5538
|
+
// now-empty project parent so the `remove` update can carry it, keeping the
|
|
5539
|
+
// sidebar's empty project row in sync without a full re-hydration.
|
|
5540
|
+
async resolveEmptyProjectForArchivedWorkspace(workspaceId) {
|
|
5541
|
+
const archivedWorkspace = await this.workspaceRegistry.get(workspaceId);
|
|
5542
|
+
if (!archivedWorkspace) {
|
|
5543
|
+
return null;
|
|
5544
|
+
}
|
|
5545
|
+
const emptyProject = (await this.workspaceDirectory.listEmptyProjects()).find((project) => project.projectId === archivedWorkspace.projectId);
|
|
5546
|
+
return emptyProject ? { emptyProject } : null;
|
|
5547
|
+
}
|
|
5548
|
+
async emitWorkspaceUpdateForTerminalContribution(event) {
|
|
5549
|
+
// A terminal's activity contributes only to the workspace it carries. A
|
|
5550
|
+
// terminal with no workspaceId attributes to nothing — status is per-id.
|
|
5551
|
+
if (!event.workspaceId) {
|
|
5552
|
+
return;
|
|
5553
|
+
}
|
|
5554
|
+
await this.emitWorkspaceUpdatesForWorkspaceIds([event.workspaceId], {
|
|
5555
|
+
skipReconcile: true,
|
|
5556
|
+
});
|
|
5557
|
+
}
|
|
5558
|
+
// A git fact (branch, diff, dirty, PR) changed at `cwd`. Every workspace whose
|
|
5559
|
+
// OWN cwd is this folder re-derives its git facts from that folder (id → cwd)
|
|
5560
|
+
// and emits its own per-id descriptor. This is a deliberate same-folder fan,
|
|
5561
|
+
// not a cwd → id ownership lookup: git never resolves which workspace owns a
|
|
5562
|
+
// path. See `workspaceIdsOnCheckout`.
|
|
5258
5563
|
async emitWorkspaceUpdateForCwd(cwd, options) {
|
|
5259
|
-
const
|
|
5260
|
-
|
|
5261
|
-
|
|
5564
|
+
const workspaceIds = workspaceIdsOnCheckout(await this.workspaceRegistry.list(), cwd);
|
|
5565
|
+
if (workspaceIds.length === 0) {
|
|
5566
|
+
return;
|
|
5567
|
+
}
|
|
5568
|
+
await this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds, options);
|
|
5262
5569
|
}
|
|
5263
5570
|
async handleFetchAgents(request) {
|
|
5264
5571
|
const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
|
|
@@ -5450,17 +5757,165 @@ export class Session {
|
|
|
5450
5757
|
}
|
|
5451
5758
|
return { snapshotByWorkspaceId };
|
|
5452
5759
|
}
|
|
5453
|
-
async registerWorkspaceForImportedAgent(
|
|
5760
|
+
async registerWorkspaceForImportedAgent(workspace) {
|
|
5454
5761
|
try {
|
|
5455
|
-
const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
|
|
5456
5762
|
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
|
5457
5763
|
await this.describeWorkspaceRecord(workspace);
|
|
5458
|
-
await this.
|
|
5764
|
+
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
|
5765
|
+
}
|
|
5766
|
+
catch (error) {
|
|
5767
|
+
this.sessionLogger.warn({ err: error, workspaceId: workspace.workspaceId, cwd: workspace.cwd }, "Failed to register workspace for imported agent");
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
async handleWorkspaceCreateRequest(request) {
|
|
5771
|
+
try {
|
|
5772
|
+
if (request.source.kind === "directory") {
|
|
5773
|
+
await this.handleWorkspaceCreateLocal(request);
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
await this.handleWorkspaceCreateWorktree(request);
|
|
5459
5777
|
}
|
|
5460
5778
|
catch (error) {
|
|
5461
|
-
|
|
5779
|
+
const message = error instanceof Error ? error.message : "Failed to create workspace";
|
|
5780
|
+
this.sessionLogger.error({ err: error, sourceKind: request.source.kind, requestId: request.requestId }, "Failed to create workspace");
|
|
5781
|
+
this.emit({
|
|
5782
|
+
type: "workspace.create.response",
|
|
5783
|
+
payload: {
|
|
5784
|
+
requestId: request.requestId,
|
|
5785
|
+
workspace: null,
|
|
5786
|
+
setupTerminalId: null,
|
|
5787
|
+
error: message,
|
|
5788
|
+
},
|
|
5789
|
+
});
|
|
5462
5790
|
}
|
|
5463
5791
|
}
|
|
5792
|
+
async handleWorkspaceCreateLocal(request) {
|
|
5793
|
+
if (request.source.kind !== "directory") {
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
const cwd = expandTilde(request.source.path);
|
|
5797
|
+
const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
|
|
5798
|
+
if (!directoryExists) {
|
|
5799
|
+
this.emit({
|
|
5800
|
+
type: "workspace.create.response",
|
|
5801
|
+
payload: {
|
|
5802
|
+
requestId: request.requestId,
|
|
5803
|
+
workspace: null,
|
|
5804
|
+
setupTerminalId: null,
|
|
5805
|
+
error: `Directory not found: ${cwd}`,
|
|
5806
|
+
errorCode: "directory_not_found",
|
|
5807
|
+
},
|
|
5808
|
+
});
|
|
5809
|
+
return;
|
|
5810
|
+
}
|
|
5811
|
+
const explicitTitle = request.title?.trim() || null;
|
|
5812
|
+
const promptTitle = resolveFirstAgentPromptTitle(request.firstAgentContext);
|
|
5813
|
+
const workspace = await createLocalCheckoutWorkspace({ cwd, title: explicitTitle ?? promptTitle }, {
|
|
5814
|
+
projectRegistry: this.projectRegistry,
|
|
5815
|
+
workspaceRegistry: this.workspaceRegistry,
|
|
5816
|
+
workspaceGitService: this.workspaceGitService,
|
|
5817
|
+
});
|
|
5818
|
+
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
|
5819
|
+
const descriptor = await this.describeWorkspaceRecord(workspace);
|
|
5820
|
+
this.emit({
|
|
5821
|
+
type: "workspace.create.response",
|
|
5822
|
+
payload: {
|
|
5823
|
+
requestId: request.requestId,
|
|
5824
|
+
workspace: descriptor,
|
|
5825
|
+
setupTerminalId: null,
|
|
5826
|
+
error: null,
|
|
5827
|
+
},
|
|
5828
|
+
});
|
|
5829
|
+
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
|
5830
|
+
void this.workspaceGitService
|
|
5831
|
+
.getSnapshot(workspace.cwd, { force: true, includeGitHub: true, reason: "open_project" })
|
|
5832
|
+
.catch((error) => {
|
|
5833
|
+
this.sessionLogger.warn({ err: error, cwd: workspace.cwd }, "Background snapshot refresh failed after workspace.create");
|
|
5834
|
+
});
|
|
5835
|
+
if (request.firstAgentContext) {
|
|
5836
|
+
const firstAgentContext = request.firstAgentContext;
|
|
5837
|
+
this.scheduleWorkspaceNaming(() => this.maybeAutoNameDirectoryWorkspaceTitle({
|
|
5838
|
+
workspaceId: workspace.workspaceId,
|
|
5839
|
+
cwd: workspace.cwd,
|
|
5840
|
+
firstAgentContext,
|
|
5841
|
+
}), { cwd: workspace.cwd, message: "Failed to auto-name directory workspace title" });
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
// Schedules a background workspace-naming write off the request path. The
|
|
5845
|
+
// setTimeout(0) keeps the LLM call off the hot path.
|
|
5846
|
+
scheduleWorkspaceNaming(run, context) {
|
|
5847
|
+
setTimeout(() => {
|
|
5848
|
+
void run().catch((error) => {
|
|
5849
|
+
this.sessionLogger.warn({ err: error, cwd: context.cwd }, context.message);
|
|
5850
|
+
});
|
|
5851
|
+
}, 0);
|
|
5852
|
+
}
|
|
5853
|
+
async handleWorkspaceCreateWorktree(request) {
|
|
5854
|
+
if (request.source.kind !== "worktree") {
|
|
5855
|
+
return;
|
|
5856
|
+
}
|
|
5857
|
+
const source = request.source;
|
|
5858
|
+
if (!source.cwd && !source.projectId) {
|
|
5859
|
+
this.emit({
|
|
5860
|
+
type: "workspace.create.response",
|
|
5861
|
+
payload: {
|
|
5862
|
+
requestId: request.requestId,
|
|
5863
|
+
workspace: null,
|
|
5864
|
+
setupTerminalId: null,
|
|
5865
|
+
error: "cwd or projectId is required for a worktree-backed workspace",
|
|
5866
|
+
errorCode: "source_required",
|
|
5867
|
+
},
|
|
5868
|
+
});
|
|
5869
|
+
return;
|
|
5870
|
+
}
|
|
5871
|
+
const sourceCwd = await this.resolveWorktreeSourceCwd({
|
|
5872
|
+
cwd: source.cwd,
|
|
5873
|
+
projectId: source.projectId,
|
|
5874
|
+
});
|
|
5875
|
+
const result = await this.createPaseoWorktreeWorkflow({
|
|
5876
|
+
cwd: sourceCwd,
|
|
5877
|
+
projectId: source.projectId,
|
|
5878
|
+
worktreeSlug: source.worktreeSlug,
|
|
5879
|
+
action: source.action,
|
|
5880
|
+
refName: source.refName,
|
|
5881
|
+
githubPrNumber: source.githubPrNumber,
|
|
5882
|
+
firstAgentContext: request.firstAgentContext,
|
|
5883
|
+
}, source.baseBranch
|
|
5884
|
+
? { resolveDefaultBranch: async () => source.baseBranch }
|
|
5885
|
+
: undefined);
|
|
5886
|
+
if (request.title?.trim()) {
|
|
5887
|
+
await this.workspaceRegistry.upsert({
|
|
5888
|
+
...result.workspace,
|
|
5889
|
+
title: request.title.trim(),
|
|
5890
|
+
updatedAt: new Date().toISOString(),
|
|
5891
|
+
});
|
|
5892
|
+
result.workspace.title = request.title.trim();
|
|
5893
|
+
}
|
|
5894
|
+
const descriptor = await this.describeCreatedWorktreeWorkspace(result);
|
|
5895
|
+
this.emit({
|
|
5896
|
+
type: "workspace.create.response",
|
|
5897
|
+
payload: {
|
|
5898
|
+
requestId: request.requestId,
|
|
5899
|
+
workspace: descriptor,
|
|
5900
|
+
setupTerminalId: null,
|
|
5901
|
+
error: null,
|
|
5902
|
+
},
|
|
5903
|
+
});
|
|
5904
|
+
this.emit({
|
|
5905
|
+
type: "workspace_update",
|
|
5906
|
+
payload: { kind: "upsert", workspace: descriptor },
|
|
5907
|
+
});
|
|
5908
|
+
}
|
|
5909
|
+
async resolveWorktreeSourceCwd(input) {
|
|
5910
|
+
if (input.cwd) {
|
|
5911
|
+
return expandTilde(input.cwd);
|
|
5912
|
+
}
|
|
5913
|
+
const project = await this.projectRegistry.get(input.projectId);
|
|
5914
|
+
if (!project || project.archivedAt) {
|
|
5915
|
+
throw new Error(`Project not found: ${input.projectId}`);
|
|
5916
|
+
}
|
|
5917
|
+
return project.rootPath;
|
|
5918
|
+
}
|
|
5464
5919
|
async handleOpenProjectRequest(request) {
|
|
5465
5920
|
const requestedCwd = request.cwd;
|
|
5466
5921
|
const cwd = expandTilde(requestedCwd);
|
|
@@ -5491,7 +5946,7 @@ export class Session {
|
|
|
5491
5946
|
const project = await this.projectRegistry.get(workspace.projectId);
|
|
5492
5947
|
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
|
5493
5948
|
const descriptor = await this.describeWorkspaceRecord(workspace);
|
|
5494
|
-
await this.
|
|
5949
|
+
await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
|
|
5495
5950
|
this.sessionLogger.info({
|
|
5496
5951
|
requestedCwd,
|
|
5497
5952
|
resolvedCwd: cwd,
|
|
@@ -5664,7 +6119,7 @@ export class Session {
|
|
|
5664
6119
|
createPaseoWorktree: (workflowInput, serviceOptions) => this.createPaseoWorktree(workflowInput, serviceOptions),
|
|
5665
6120
|
warmWorkspaceGitData: (workspace) => this.warmWorkspaceGitDataForWorkspace(workspace),
|
|
5666
6121
|
autoNameWorkspaceBranchForFirstAgent: (autoNameInput) => this.scheduleAutoNameWorkspaceBranchForFirstAgent(autoNameInput),
|
|
5667
|
-
|
|
6122
|
+
emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
|
|
5668
6123
|
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) => {
|
|
5669
6124
|
this.workspaceSetupSnapshots.set(workspaceId, snapshot);
|
|
5670
6125
|
},
|
|
@@ -5694,12 +6149,33 @@ export class Session {
|
|
|
5694
6149
|
if (!existing) {
|
|
5695
6150
|
throw new Error(`Workspace not found: ${request.workspaceId}`);
|
|
5696
6151
|
}
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
const
|
|
5701
|
-
await
|
|
5702
|
-
|
|
6152
|
+
const gitSnapshot = await this.workspaceGitService
|
|
6153
|
+
.getSnapshot(existing.cwd)
|
|
6154
|
+
.catch(() => null);
|
|
6155
|
+
const repoRoot = gitSnapshot?.git?.repoRoot ?? null;
|
|
6156
|
+
await archiveByScope({
|
|
6157
|
+
paseoHome: this.paseoHome,
|
|
6158
|
+
paseoWorktreesBaseRoot: this.worktreesRoot,
|
|
6159
|
+
github: this.github,
|
|
6160
|
+
workspaceGitService: this.workspaceGitService,
|
|
6161
|
+
agentManager: this.agentManager,
|
|
6162
|
+
agentStorage: this.agentStorage,
|
|
6163
|
+
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
|
|
6164
|
+
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
|
|
6165
|
+
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
|
6166
|
+
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) => this.emitWorkspaceUpdatesForWorkspaceIds(workspaceIds),
|
|
6167
|
+
markWorkspaceArchiving: (workspaceIds, archivingAt) => this.markWorkspaceArchiving(workspaceIds, archivingAt),
|
|
6168
|
+
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
|
|
6169
|
+
killTerminalsForWorkspace: (workspaceId) => this.terminalController.killTerminalsForWorkspace(workspaceId),
|
|
6170
|
+
sessionLogger: this.sessionLogger,
|
|
6171
|
+
}, {
|
|
6172
|
+
scope: { kind: "workspace", workspaceId: existing.workspaceId },
|
|
6173
|
+
repoRoot,
|
|
6174
|
+
paseoWorktreesBaseRoot: this.worktreesRoot,
|
|
6175
|
+
requestId: request.requestId,
|
|
6176
|
+
});
|
|
6177
|
+
const archivedWorkspace = await this.workspaceRegistry.get(request.workspaceId);
|
|
6178
|
+
const archivedAt = archivedWorkspace?.archivedAt ?? new Date().toISOString();
|
|
5703
6179
|
this.emit({
|
|
5704
6180
|
type: "archive_workspace_response",
|
|
5705
6181
|
payload: {
|
|
@@ -5760,10 +6236,12 @@ export class Session {
|
|
|
5760
6236
|
if (!workspace || workspace.archivedAt) {
|
|
5761
6237
|
throw new Error(`Workspace not found: ${requestedWorkspaceId}`);
|
|
5762
6238
|
}
|
|
5763
|
-
|
|
6239
|
+
// Clearing attention is scoped to the workspace that OWNS the agent, by
|
|
6240
|
+
// workspaceId — never by comparing cwd strings. A sibling workspace
|
|
6241
|
+
// sharing the same directory keeps its own agents' attention.
|
|
5764
6242
|
const clearableAgentIds = agents
|
|
5765
6243
|
.filter((agent) => !agent.archivedAt)
|
|
5766
|
-
.filter((agent) =>
|
|
6244
|
+
.filter((agent) => agent.workspaceId === workspace.workspaceId)
|
|
5767
6245
|
.filter((agent) => agent.requiresAttention === true)
|
|
5768
6246
|
.filter((agent) => (agent.pendingPermissions?.length ?? 0) === 0)
|
|
5769
6247
|
.filter((agent) => agent.attentionReason !== "permission")
|
|
@@ -5791,7 +6269,7 @@ export class Session {
|
|
|
5791
6269
|
};
|
|
5792
6270
|
await this.agentStorage.upsert(nextRecord);
|
|
5793
6271
|
const agent = this.buildStoredAgentPayload(nextRecord);
|
|
5794
|
-
const project = await this.
|
|
6272
|
+
const project = await this.buildProjectPlacementForWorkspace(workspace);
|
|
5795
6273
|
this.emit({
|
|
5796
6274
|
type: "agent_update",
|
|
5797
6275
|
payload: {
|
|
@@ -5862,7 +6340,9 @@ export class Session {
|
|
|
5862
6340
|
});
|
|
5863
6341
|
return;
|
|
5864
6342
|
}
|
|
5865
|
-
const project =
|
|
6343
|
+
const project = agent.workspaceId
|
|
6344
|
+
? await this.buildProjectPlacementForWorkspaceId(agent.workspaceId)
|
|
6345
|
+
: null;
|
|
5866
6346
|
this.emit({
|
|
5867
6347
|
type: "fetch_agent_response",
|
|
5868
6348
|
payload: { requestId, agent, project, error: null },
|
|
@@ -6651,10 +7131,15 @@ export class Session {
|
|
|
6651
7131
|
* Emit a message to the client
|
|
6652
7132
|
*/
|
|
6653
7133
|
emit(msg) {
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
7134
|
+
// JSON.stringify(msg) is only computed when trace is enabled — it runs for
|
|
7135
|
+
// every outbound message otherwise, and trace is disabled by default.
|
|
7136
|
+
// Optional-chained because test logger stubs don't implement isLevelEnabled.
|
|
7137
|
+
if (this.sessionLogger.isLevelEnabled?.("trace")) {
|
|
7138
|
+
this.sessionLogger.trace({
|
|
7139
|
+
messageType: msg.type,
|
|
7140
|
+
payloadBytes: JSON.stringify(msg).length,
|
|
7141
|
+
}, "agent.session.outbound");
|
|
7142
|
+
}
|
|
6658
7143
|
if (msg.type === "audio_output" &&
|
|
6659
7144
|
(process.env.TTS_DEBUG_AUDIO_DIR || isPaseoDictationDebugEnabled()) &&
|
|
6660
7145
|
msg.payload.groupId &&
|
|
@@ -6714,6 +7199,10 @@ export class Session {
|
|
|
6714
7199
|
this.unsubscribeAgentEvents();
|
|
6715
7200
|
this.unsubscribeAgentEvents = null;
|
|
6716
7201
|
}
|
|
7202
|
+
if (this.unsubscribeTerminalWorkspaceContributionEvents) {
|
|
7203
|
+
this.unsubscribeTerminalWorkspaceContributionEvents();
|
|
7204
|
+
this.unsubscribeTerminalWorkspaceContributionEvents = null;
|
|
7205
|
+
}
|
|
6717
7206
|
if (this.unsubscribeProviderSnapshotEvents) {
|
|
6718
7207
|
this.unsubscribeProviderSnapshotEvents();
|
|
6719
7208
|
this.unsubscribeProviderSnapshotEvents = null;
|