@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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { sep } from "node:path";
|
|
1
|
+
import { resolve } from "node:path";
|
|
3
2
|
import { deriveAgentStateBucket, getWorkspaceStateBucketPriority, } from "@getpaseo/protocol/agent-state-bucket";
|
|
4
3
|
import { getParentAgentIdFromLabels, isDelegatedAgent } from "@getpaseo/protocol/agent-labels";
|
|
5
4
|
import { SortablePager } from "./pagination/sortable-pager.js";
|
|
6
|
-
import {
|
|
5
|
+
import { resolveProjectDisplayName } from "./workspace-registry.js";
|
|
6
|
+
import { deriveTerminalActivityStatusBucket, } from "@getpaseo/protocol/terminal-activity";
|
|
7
7
|
const FETCH_WORKSPACES_SORT_KEYS = [
|
|
8
8
|
"status_priority",
|
|
9
9
|
"activity_at",
|
|
@@ -31,6 +31,19 @@ export function summarizeFetchWorkspacesEntries(entries) {
|
|
|
31
31
|
workspaces,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Git facts (branch, diff, dirty, PR) belong to a checkout on disk, not to a
|
|
36
|
+
* workspace identity. Every workspace whose own cwd is that checkout re-derives
|
|
37
|
+
* its git facts from the same folder. This returns the ids of those workspaces
|
|
38
|
+
* so a git change can fan out to all of them. This is git-fact display, NOT
|
|
39
|
+
* ownership: do not use it to decide which workspace owns an arbitrary path.
|
|
40
|
+
*/
|
|
41
|
+
export function workspaceIdsOnCheckout(workspaces, cwd) {
|
|
42
|
+
const resolvedCwd = resolve(cwd);
|
|
43
|
+
return Array.from(workspaces)
|
|
44
|
+
.filter((workspace) => !workspace.archivedAt && resolve(workspace.cwd) === resolvedCwd)
|
|
45
|
+
.map((workspace) => workspace.workspaceId);
|
|
46
|
+
}
|
|
34
47
|
export class WorkspaceDirectory {
|
|
35
48
|
constructor(deps) {
|
|
36
49
|
this.deps = deps;
|
|
@@ -73,10 +86,11 @@ export class WorkspaceDirectory {
|
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
async buildDescriptorMap(options) {
|
|
76
|
-
const [agents, persistedWorkspaces, persistedProjects] = await Promise.all([
|
|
89
|
+
const [agents, persistedWorkspaces, persistedProjects, terminalContributions] = await Promise.all([
|
|
77
90
|
this.deps.listAgentPayloads(),
|
|
78
91
|
this.deps.workspaceRegistry.list(),
|
|
79
92
|
this.deps.projectRegistry.list(),
|
|
93
|
+
this.deps.listTerminalActivityContributions(),
|
|
80
94
|
]);
|
|
81
95
|
const activeProjects = new Map(persistedProjects
|
|
82
96
|
.filter((project) => !project.archivedAt)
|
|
@@ -85,8 +99,9 @@ export class WorkspaceDirectory {
|
|
|
85
99
|
const activeRecords = persistedWorkspaces.filter((workspace) => !workspace.archivedAt && !archivedProjectIds.has(workspace.projectId));
|
|
86
100
|
const descriptorsByWorkspaceId = new Map();
|
|
87
101
|
const workspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
|
|
88
|
-
const
|
|
102
|
+
const activeWorkspaceIds = new Set(activeRecords.map((workspace) => workspace.workspaceId));
|
|
89
103
|
const includedWorkspaces = activeRecords.filter((workspace) => !workspaceIds || workspaceIds.has(workspace.workspaceId));
|
|
104
|
+
const activeRecordsByWorkspaceId = new Map(activeRecords.map((workspace) => [workspace.workspaceId, workspace]));
|
|
90
105
|
const workspaceDescriptors = await Promise.all(includedWorkspaces.map((workspace) => this.deps.buildWorkspaceDescriptor({
|
|
91
106
|
workspace,
|
|
92
107
|
projectRecord: activeProjects.get(workspace.projectId) ?? null,
|
|
@@ -100,6 +115,44 @@ export class WorkspaceDirectory {
|
|
|
100
115
|
});
|
|
101
116
|
}
|
|
102
117
|
const activeAgents = agents.filter((agent) => !agent.archivedAt && this.deps.isProviderVisibleToClient(agent.provider));
|
|
118
|
+
this.applyAgentBucketContributions({
|
|
119
|
+
activeAgents,
|
|
120
|
+
descriptorsByWorkspaceId,
|
|
121
|
+
});
|
|
122
|
+
// Terminal activity contributions: working terminal → running bucket.
|
|
123
|
+
const terminalEntriesByWorkspaceId = this.applyTerminalContributions(terminalContributions, descriptorsByWorkspaceId);
|
|
124
|
+
const contributingAgentsByWorkspaceId = groupAgentsByWorkspaceId(activeAgents, activeWorkspaceIds);
|
|
125
|
+
// Resolve the workspace-level `statusEnteredAt` (see aggregate semantics
|
|
126
|
+
// on `resolveStatusEnteredAt`).
|
|
127
|
+
const nowIso = new Date().toISOString();
|
|
128
|
+
for (const [workspaceId, descriptor] of descriptorsByWorkspaceId) {
|
|
129
|
+
const contributingAgents = contributingAgentsByWorkspaceId.get(workspaceId) ?? [];
|
|
130
|
+
const terminalEntries = terminalEntriesByWorkspaceId.get(workspaceId) ?? [];
|
|
131
|
+
const result = this.resolveStatusEnteredAt({
|
|
132
|
+
workspaceId,
|
|
133
|
+
winningBucket: descriptor.status,
|
|
134
|
+
contributingAgents,
|
|
135
|
+
terminalEntries,
|
|
136
|
+
previous: this.bucketHistoryByWorkspaceId.get(workspaceId) ?? null,
|
|
137
|
+
workspaceCreatedAt: activeRecordsByWorkspaceId.get(workspaceId)?.createdAt ?? null,
|
|
138
|
+
nowIso,
|
|
139
|
+
});
|
|
140
|
+
descriptor.statusEnteredAt = result.statusEnteredAt;
|
|
141
|
+
if (result.recordUpdate) {
|
|
142
|
+
this.bucketHistoryByWorkspaceId.set(workspaceId, result.recordUpdate);
|
|
143
|
+
}
|
|
144
|
+
else if (result.recordDelete) {
|
|
145
|
+
this.bucketHistoryByWorkspaceId.delete(workspaceId);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return descriptorsByWorkspaceId;
|
|
149
|
+
}
|
|
150
|
+
// Aggregate each agent's state bucket into its owning workspace descriptor,
|
|
151
|
+
// keeping the highest-priority bucket. A record's owner IS its `workspaceId`;
|
|
152
|
+
// status never fans out to same-cwd siblings. Delegated agents contribute to
|
|
153
|
+
// their delegation root's workspace; their own status is ignored unless running.
|
|
154
|
+
applyAgentBucketContributions(params) {
|
|
155
|
+
const { activeAgents, descriptorsByWorkspaceId } = params;
|
|
103
156
|
const activeAgentsById = new Map(activeAgents.map((agent) => [agent.id, agent]));
|
|
104
157
|
for (const agent of activeAgents) {
|
|
105
158
|
let workspaceAgent = agent;
|
|
@@ -123,8 +176,8 @@ export class WorkspaceDirectory {
|
|
|
123
176
|
attentionReason: agent.attentionReason ?? null,
|
|
124
177
|
});
|
|
125
178
|
}
|
|
126
|
-
const workspaceId =
|
|
127
|
-
if (workspaceId
|
|
179
|
+
const workspaceId = workspaceAgent.workspaceId;
|
|
180
|
+
if (!workspaceId) {
|
|
128
181
|
continue;
|
|
129
182
|
}
|
|
130
183
|
const existing = descriptorsByWorkspaceId.get(workspaceId);
|
|
@@ -135,46 +188,55 @@ export class WorkspaceDirectory {
|
|
|
135
188
|
existing.status = bucket;
|
|
136
189
|
}
|
|
137
190
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
contributingAgents,
|
|
149
|
-
previous: this.bucketHistoryByWorkspaceId.get(workspaceId) ?? null,
|
|
150
|
-
nowIso,
|
|
151
|
-
});
|
|
152
|
-
descriptor.statusEnteredAt = result.statusEnteredAt;
|
|
153
|
-
if (result.recordUpdate) {
|
|
154
|
-
this.bucketHistoryByWorkspaceId.set(workspaceId, result.recordUpdate);
|
|
191
|
+
}
|
|
192
|
+
// Apply working terminal contributions to descriptor statuses and build a map
|
|
193
|
+
// of terminal timestamp entries per workspace for use in `resolveStatusEnteredAt`.
|
|
194
|
+
// A terminal contributes only to the workspace it carries; same-cwd siblings
|
|
195
|
+
// are untouched.
|
|
196
|
+
applyTerminalContributions(terminalContributions, descriptorsByWorkspaceId) {
|
|
197
|
+
const terminalEntriesByWorkspaceId = new Map();
|
|
198
|
+
for (const { workspaceId, activity } of terminalContributions) {
|
|
199
|
+
if (!activity || !workspaceId) {
|
|
200
|
+
continue;
|
|
155
201
|
}
|
|
156
|
-
|
|
157
|
-
|
|
202
|
+
const bucket = deriveTerminalActivityStatusBucket(activity);
|
|
203
|
+
if (!bucket)
|
|
204
|
+
continue;
|
|
205
|
+
const existing = descriptorsByWorkspaceId.get(workspaceId);
|
|
206
|
+
if (!existing) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (getWorkspaceStateBucketPriority(bucket) < getWorkspaceStateBucketPriority(existing.status)) {
|
|
210
|
+
existing.status = bucket;
|
|
158
211
|
}
|
|
212
|
+
const entries = terminalEntriesByWorkspaceId.get(workspaceId) ?? [];
|
|
213
|
+
entries.push({ bucket, changedAtIso: new Date(activity.changedAt).toISOString() });
|
|
214
|
+
terminalEntriesByWorkspaceId.set(workspaceId, entries);
|
|
159
215
|
}
|
|
160
|
-
return
|
|
216
|
+
return terminalEntriesByWorkspaceId;
|
|
161
217
|
}
|
|
162
218
|
// Aggregate the workspace-level `statusEnteredAt` from its contributing
|
|
163
|
-
// agents. Aggregate semantics:
|
|
164
|
-
// - winning bucket = highest-priority across contributing agents;
|
|
165
|
-
// - entry time = best-effort timestamp from agents in the winning bucket;
|
|
219
|
+
// agents and terminals. Aggregate semantics:
|
|
220
|
+
// - winning bucket = highest-priority across contributing agents and terminals;
|
|
221
|
+
// - entry time = best-effort timestamp from agents/terminals in the winning bucket;
|
|
166
222
|
// - priority unmasking: when the winning bucket transitions (e.g. a
|
|
167
223
|
// higher-priority bucket cleared), the new entry time is "now";
|
|
168
224
|
// - same-bucket emits reuse the previous entered-at;
|
|
169
|
-
// - empty workspaces that never had contributing agents
|
|
170
|
-
// `
|
|
225
|
+
// - empty workspaces that never had contributing agents or terminals use
|
|
226
|
+
// their workspace creation time as their initial `done` entry time.
|
|
171
227
|
// - when archived agents leave a previously active workspace empty, keep
|
|
172
228
|
// the previous done timestamp or stamp the transition to done now.
|
|
173
229
|
resolveStatusEnteredAt(params) {
|
|
174
|
-
const { winningBucket, contributingAgents, previous, nowIso } = params;
|
|
175
|
-
if (contributingAgents.length === 0) {
|
|
230
|
+
const { winningBucket, contributingAgents, terminalEntries, previous, workspaceCreatedAt, nowIso, } = params;
|
|
231
|
+
if (contributingAgents.length === 0 && terminalEntries.length === 0) {
|
|
176
232
|
if (!previous) {
|
|
177
|
-
|
|
233
|
+
if (!workspaceCreatedAt) {
|
|
234
|
+
return { statusEnteredAt: null };
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
statusEnteredAt: workspaceCreatedAt,
|
|
238
|
+
recordUpdate: { bucket: "done", enteredAt: workspaceCreatedAt },
|
|
239
|
+
};
|
|
178
240
|
}
|
|
179
241
|
const enteredAt = previous.bucket === "done" ? previous.enteredAt : nowIso;
|
|
180
242
|
return {
|
|
@@ -183,7 +245,7 @@ export class WorkspaceDirectory {
|
|
|
183
245
|
};
|
|
184
246
|
}
|
|
185
247
|
if (!previous) {
|
|
186
|
-
const newestInWinningBucket = this.
|
|
248
|
+
const newestInWinningBucket = this.findNewestTimestampInBucket(contributingAgents, terminalEntries, winningBucket);
|
|
187
249
|
const enteredAt = newestInWinningBucket ?? nowIso;
|
|
188
250
|
return {
|
|
189
251
|
statusEnteredAt: enteredAt,
|
|
@@ -201,13 +263,13 @@ export class WorkspaceDirectory {
|
|
|
201
263
|
recordUpdate: previous,
|
|
202
264
|
};
|
|
203
265
|
}
|
|
204
|
-
// Best-effort newest timestamp across contributing agents
|
|
205
|
-
// bucket matches `winningBucket`.
|
|
266
|
+
// Best-effort newest timestamp across contributing agents and terminal entries
|
|
267
|
+
// whose bucket matches `winningBucket`. For agents, uses:
|
|
206
268
|
// - `attentionTimestamp` when attention is set (covers attention/failed)
|
|
207
269
|
// - `updatedAt` as a general fallback for any bucket
|
|
208
|
-
// Returns `null` if no matching
|
|
209
|
-
|
|
210
|
-
const
|
|
270
|
+
// Returns `null` if no matching contributor has a parseable timestamp.
|
|
271
|
+
findNewestTimestampInBucket(contributingAgents, terminalEntries, winningBucket) {
|
|
272
|
+
const agentTimestamps = contributingAgents
|
|
211
273
|
.filter((agent) => {
|
|
212
274
|
const derived = deriveAgentStateBucket({
|
|
213
275
|
status: agent.status,
|
|
@@ -226,32 +288,33 @@ export class WorkspaceDirectory {
|
|
|
226
288
|
// Fall back to updatedAt as a general proxy for recent activity.
|
|
227
289
|
return agent.updatedAt;
|
|
228
290
|
})
|
|
229
|
-
.filter((value) => typeof value === "string" && value.length > 0)
|
|
230
|
-
|
|
291
|
+
.filter((value) => typeof value === "string" && value.length > 0);
|
|
292
|
+
const terminalTimestamps = terminalEntries
|
|
293
|
+
.filter((entry) => entry.bucket === winningBucket)
|
|
294
|
+
.map((entry) => entry.changedAtIso);
|
|
295
|
+
const candidates = [...agentTimestamps, ...terminalTimestamps].sort();
|
|
231
296
|
return candidates.at(-1) ?? null;
|
|
232
297
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
return bestMatch?.workspaceId ?? normalizedCwd;
|
|
298
|
+
// Project parents that have no active workspaces. These persist as first-class
|
|
299
|
+
// empty projects so the sidebar can render an empty project row with a
|
|
300
|
+
// "+ New workspace" affordance.
|
|
301
|
+
async listEmptyProjects() {
|
|
302
|
+
const [persistedWorkspaces, persistedProjects] = await Promise.all([
|
|
303
|
+
this.deps.workspaceRegistry.list(),
|
|
304
|
+
this.deps.projectRegistry.list(),
|
|
305
|
+
]);
|
|
306
|
+
const projectIdsWithActiveWorkspaces = new Set(persistedWorkspaces
|
|
307
|
+
.filter((workspace) => !workspace.archivedAt)
|
|
308
|
+
.map((workspace) => workspace.projectId));
|
|
309
|
+
return persistedProjects
|
|
310
|
+
.filter((project) => !project.archivedAt && !projectIdsWithActiveWorkspaces.has(project.projectId))
|
|
311
|
+
.map((project) => ({
|
|
312
|
+
projectId: project.projectId,
|
|
313
|
+
projectDisplayName: resolveProjectDisplayName(project),
|
|
314
|
+
projectCustomName: project.customName ?? null,
|
|
315
|
+
projectRootPath: project.rootPath,
|
|
316
|
+
projectKind: project.kind,
|
|
317
|
+
}));
|
|
255
318
|
}
|
|
256
319
|
async listDescriptors() {
|
|
257
320
|
return Array.from((await this.buildDescriptorMap({
|
|
@@ -268,11 +331,6 @@ export class WorkspaceDirectory {
|
|
|
268
331
|
return false;
|
|
269
332
|
}
|
|
270
333
|
}
|
|
271
|
-
if (filter.idPrefix && filter.idPrefix.trim().length > 0) {
|
|
272
|
-
if (!workspace.id.startsWith(filter.idPrefix.trim())) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
334
|
if (filter.query && filter.query.trim().length > 0) {
|
|
277
335
|
const query = filter.query.trim().toLocaleLowerCase();
|
|
278
336
|
const haystacks = [workspace.name, workspace.projectId, workspace.id];
|
|
@@ -301,6 +359,12 @@ export class WorkspaceDirectory {
|
|
|
301
359
|
const nextCursor = hasMore && pagedEntries.length > 0
|
|
302
360
|
? this.pager.encode(pagedEntries[pagedEntries.length - 1], sort)
|
|
303
361
|
: null;
|
|
362
|
+
// Empty project parents ride only on the first page so the sidebar can render
|
|
363
|
+
// them without them being duplicated across pagination.
|
|
364
|
+
const projectIdFilter = filter?.projectId?.trim();
|
|
365
|
+
const emptyProjects = cursorToken
|
|
366
|
+
? []
|
|
367
|
+
: (await this.listEmptyProjects()).filter((project) => !projectIdFilter || project.projectId === projectIdFilter);
|
|
304
368
|
this.deps.logger.debug({
|
|
305
369
|
requestId: request.requestId,
|
|
306
370
|
filter: request.filter ?? null,
|
|
@@ -314,6 +378,7 @@ export class WorkspaceDirectory {
|
|
|
314
378
|
}, "fetch_workspaces_entries_listed");
|
|
315
379
|
return {
|
|
316
380
|
entries: pagedEntries,
|
|
381
|
+
emptyProjects,
|
|
317
382
|
pageInfo: {
|
|
318
383
|
nextCursor,
|
|
319
384
|
prevCursor: request.page?.cursor ?? null,
|
|
@@ -322,6 +387,19 @@ export class WorkspaceDirectory {
|
|
|
322
387
|
};
|
|
323
388
|
}
|
|
324
389
|
}
|
|
390
|
+
function groupAgentsByWorkspaceId(agents, activeWorkspaceIds) {
|
|
391
|
+
const byWorkspaceId = new Map();
|
|
392
|
+
for (const agent of agents) {
|
|
393
|
+
const workspaceId = agent.workspaceId;
|
|
394
|
+
if (!workspaceId || !activeWorkspaceIds.has(workspaceId)) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const entries = byWorkspaceId.get(workspaceId) ?? [];
|
|
398
|
+
entries.push(agent);
|
|
399
|
+
byWorkspaceId.set(workspaceId, entries);
|
|
400
|
+
}
|
|
401
|
+
return byWorkspaceId;
|
|
402
|
+
}
|
|
325
403
|
function resolveDelegationRootAgent(agent, activeAgentsById) {
|
|
326
404
|
const seen = new Set([agent.id]);
|
|
327
405
|
let current = agent;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
2
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
4
|
import { LRUCache } from "lru-cache";
|
|
5
5
|
import pLimit from "p-limit";
|
|
6
6
|
import { getCheckoutDiff, getCheckoutSnapshotFacts, getCheckoutShortstat, getCheckoutStatus, getPullRequestStatus, hasOriginRemote, listBranchSuggestions, resolveRepositoryDefaultBranch, resolveBranchCheckout, resolveAbsoluteGitDir, } from "../utils/checkout-git.js";
|
|
@@ -11,7 +11,7 @@ import { resolveGitHubRemote } from "../utils/github-remote.js";
|
|
|
11
11
|
import { listPaseoWorktrees } from "../utils/worktree.js";
|
|
12
12
|
import { READ_ONLY_GIT_ENV } from "./checkout-git-utils.js";
|
|
13
13
|
import { buildWorkspaceGitMetadataFromSnapshot, } from "./workspace-git-metadata.js";
|
|
14
|
-
import { checkoutLiteFromGitSnapshot
|
|
14
|
+
import { checkoutLiteFromGitSnapshot } from "./workspace-registry-model.js";
|
|
15
15
|
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 1000;
|
|
16
16
|
const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180000;
|
|
17
17
|
export const WORKSPACE_GIT_SELF_HEAL_INTERVAL_MS = 60000;
|
|
@@ -75,7 +75,7 @@ export class WorkspaceGitServiceImpl {
|
|
|
75
75
|
this.deps = resolveWorkspaceGitServiceDeps(options.deps);
|
|
76
76
|
}
|
|
77
77
|
registerWorkspace(params, listener) {
|
|
78
|
-
const cwd =
|
|
78
|
+
const cwd = resolve(params.cwd);
|
|
79
79
|
const target = this.ensureWorkspaceTarget(cwd);
|
|
80
80
|
target.listeners.add(listener);
|
|
81
81
|
if (target.listeners.size === 1) {
|
|
@@ -100,7 +100,7 @@ export class WorkspaceGitServiceImpl {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
async getSnapshot(cwd, options) {
|
|
103
|
-
cwd =
|
|
103
|
+
cwd = resolve(cwd);
|
|
104
104
|
const request = this.normalizeRefreshRequest(options, "getSnapshot", true);
|
|
105
105
|
const target = this.ensureWorkspaceTarget(cwd);
|
|
106
106
|
if (!request.force && target.latestSnapshot) {
|
|
@@ -109,7 +109,7 @@ export class WorkspaceGitServiceImpl {
|
|
|
109
109
|
return this.requestWorkspaceSnapshot(target, request);
|
|
110
110
|
}
|
|
111
111
|
async getCheckout(cwd) {
|
|
112
|
-
const normalizedCwd =
|
|
112
|
+
const normalizedCwd = resolve(cwd);
|
|
113
113
|
try {
|
|
114
114
|
const status = await this.deps.getCheckoutStatus(normalizedCwd, {
|
|
115
115
|
paseoHome: this.paseoHome,
|
|
@@ -147,11 +147,11 @@ export class WorkspaceGitServiceImpl {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
peekSnapshot(cwd) {
|
|
150
|
-
cwd =
|
|
150
|
+
cwd = resolve(cwd);
|
|
151
151
|
return this.workspaceTargets.get(cwd)?.latestSnapshot ?? null;
|
|
152
152
|
}
|
|
153
153
|
getCheckoutDiff(cwd, options, readOptions) {
|
|
154
|
-
const normalizedCwd =
|
|
154
|
+
const normalizedCwd = resolve(cwd);
|
|
155
155
|
const normalizedOptions = this.normalizeCheckoutDiffOptions(options);
|
|
156
156
|
const key = this.buildCheckoutDiffCacheKey(normalizedCwd, normalizedOptions);
|
|
157
157
|
return this.readAuxiliaryCache(this.checkoutDiffCache, key, readOptions, () => this.deps.getCheckoutDiff(normalizedCwd, normalizedOptions, {
|
|
@@ -182,13 +182,13 @@ export class WorkspaceGitServiceImpl {
|
|
|
182
182
|
]);
|
|
183
183
|
}
|
|
184
184
|
validateBranchRef(cwd, ref, options) {
|
|
185
|
-
const normalizedCwd =
|
|
185
|
+
const normalizedCwd = resolve(cwd);
|
|
186
186
|
const normalizedRef = ref.trim();
|
|
187
187
|
const key = JSON.stringify(["branch-validation", normalizedCwd, normalizedRef]);
|
|
188
188
|
return this.readAuxiliaryCache(this.branchValidationCache, key, options, () => this.deps.resolveBranchCheckout(normalizedCwd, normalizedRef));
|
|
189
189
|
}
|
|
190
190
|
hasLocalBranch(cwd, branch, options) {
|
|
191
|
-
const normalizedCwd =
|
|
191
|
+
const normalizedCwd = resolve(cwd);
|
|
192
192
|
const normalizedBranch = branch.trim();
|
|
193
193
|
const ref = `refs/heads/${normalizedBranch}`;
|
|
194
194
|
const key = JSON.stringify(["local-branch", normalizedCwd, ref]);
|
|
@@ -202,14 +202,14 @@ export class WorkspaceGitServiceImpl {
|
|
|
202
202
|
});
|
|
203
203
|
}
|
|
204
204
|
suggestBranchesForCwd(cwd, options, readOptions) {
|
|
205
|
-
const normalizedCwd =
|
|
205
|
+
const normalizedCwd = resolve(cwd);
|
|
206
206
|
const query = options?.query ?? "";
|
|
207
207
|
const limit = options?.limit;
|
|
208
208
|
const key = JSON.stringify(["branch-suggestions", normalizedCwd, query, limit ?? null]);
|
|
209
209
|
return this.readAuxiliaryCache(this.branchSuggestionsCache, key, readOptions, () => this.deps.listBranchSuggestions(normalizedCwd, options));
|
|
210
210
|
}
|
|
211
211
|
listStashes(cwd, options, readOptions) {
|
|
212
|
-
const normalizedCwd =
|
|
212
|
+
const normalizedCwd = resolve(cwd);
|
|
213
213
|
const paseoOnly = options?.paseoOnly !== false;
|
|
214
214
|
const key = JSON.stringify(["stashes", normalizedCwd, paseoOnly]);
|
|
215
215
|
return this.readAuxiliaryCache(this.stashListCache, key, readOptions, async () => {
|
|
@@ -235,11 +235,11 @@ export class WorkspaceGitServiceImpl {
|
|
|
235
235
|
throw new Error("Create worktree requires a git repository");
|
|
236
236
|
}
|
|
237
237
|
return snapshot.git.isPaseoOwnedWorktree
|
|
238
|
-
? (snapshot.git.mainRepoRoot ?? snapshot.git.repoRoot ??
|
|
239
|
-
: (snapshot.git.repoRoot ??
|
|
238
|
+
? (snapshot.git.mainRepoRoot ?? snapshot.git.repoRoot ?? resolve(cwd))
|
|
239
|
+
: (snapshot.git.repoRoot ?? resolve(cwd));
|
|
240
240
|
}
|
|
241
241
|
async resolveDefaultBranch(cwdOrRepoRoot, options) {
|
|
242
|
-
const cwd =
|
|
242
|
+
const cwd = resolve(cwdOrRepoRoot);
|
|
243
243
|
const key = JSON.stringify(["default-branch", cwd]);
|
|
244
244
|
return this.readAuxiliaryCache(this.defaultBranchCache, key, options, async () => {
|
|
245
245
|
const defaultBranch = await this.deps.resolveRepositoryDefaultBranch(cwd);
|
|
@@ -251,9 +251,9 @@ export class WorkspaceGitServiceImpl {
|
|
|
251
251
|
}
|
|
252
252
|
async getWorkspaceGitMetadata(cwd, options) {
|
|
253
253
|
const snapshot = await this.getSnapshot(cwd, options);
|
|
254
|
-
const directoryName = options?.directoryName ??
|
|
254
|
+
const directoryName = options?.directoryName ?? basename(cwd) ?? cwd;
|
|
255
255
|
return buildWorkspaceGitMetadataFromSnapshot({
|
|
256
|
-
cwd:
|
|
256
|
+
cwd: resolve(cwd),
|
|
257
257
|
directoryName,
|
|
258
258
|
isGit: snapshot.git.isGit,
|
|
259
259
|
repoRoot: snapshot.git.repoRoot,
|
|
@@ -267,7 +267,7 @@ export class WorkspaceGitServiceImpl {
|
|
|
267
267
|
return snapshot.git.remoteUrl;
|
|
268
268
|
}
|
|
269
269
|
async refresh(cwd, _options) {
|
|
270
|
-
cwd =
|
|
270
|
+
cwd = resolve(cwd);
|
|
271
271
|
const target = this.ensureWorkspaceTarget(cwd);
|
|
272
272
|
await this.refreshWorkspaceTarget(target, {
|
|
273
273
|
force: false,
|
|
@@ -278,7 +278,7 @@ export class WorkspaceGitServiceImpl {
|
|
|
278
278
|
this.scheduleWorkspaceObservationSetup(target);
|
|
279
279
|
}
|
|
280
280
|
async requestWorkingTreeWatch(cwd, onChange) {
|
|
281
|
-
cwd =
|
|
281
|
+
cwd = resolve(cwd);
|
|
282
282
|
const target = await this.ensureWorkingTreeWatchTarget(cwd);
|
|
283
283
|
target.listeners.add(onChange);
|
|
284
284
|
return {
|
|
@@ -289,14 +289,14 @@ export class WorkspaceGitServiceImpl {
|
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
291
|
scheduleRefreshForCwd(cwd) {
|
|
292
|
-
cwd =
|
|
292
|
+
cwd = resolve(cwd);
|
|
293
293
|
const target = this.workspaceTargets.get(cwd);
|
|
294
294
|
if (target) {
|
|
295
295
|
this.scheduleWorkspaceRefresh(target);
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
onWorkspaceStateMayHaveChanged(cwd) {
|
|
299
|
-
const normalizedCwd =
|
|
299
|
+
const normalizedCwd = resolve(cwd);
|
|
300
300
|
const target = this.workspaceTargets.get(normalizedCwd);
|
|
301
301
|
if (!target || target.closed) {
|
|
302
302
|
return;
|
|
@@ -649,7 +649,7 @@ export class WorkspaceGitServiceImpl {
|
|
|
649
649
|
}
|
|
650
650
|
scheduleWorkspaceRefresh(targetOrCwd, options) {
|
|
651
651
|
const target = typeof targetOrCwd === "string"
|
|
652
|
-
? this.workspaceTargets.get(
|
|
652
|
+
? this.workspaceTargets.get(resolve(targetOrCwd))
|
|
653
653
|
: targetOrCwd;
|
|
654
654
|
if (!target || target.closed || this.workspaceTargets.get(target.cwd) !== target) {
|
|
655
655
|
return;
|
|
@@ -20,7 +20,7 @@ export type ReconciliationChange = {
|
|
|
20
20
|
kind: "workspace_updated";
|
|
21
21
|
workspaceId: string;
|
|
22
22
|
directory: string;
|
|
23
|
-
fields: Partial<Pick<PersistedWorkspaceRecord, "projectId" | "
|
|
23
|
+
fields: Partial<Pick<PersistedWorkspaceRecord, "projectId" | "branch" | "kind">>;
|
|
24
24
|
};
|
|
25
25
|
export interface ReconciliationResult {
|
|
26
26
|
changesApplied: ReconciliationChange[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
3
|
const DEFAULT_RECONCILE_INTERVAL_MS = 60000;
|
|
4
4
|
function deriveWorkspaceKindFromMetadata(metadata) {
|
|
5
5
|
if (metadata.projectKind !== "git")
|
|
@@ -97,27 +97,18 @@ export class WorkspaceReconciliationService {
|
|
|
97
97
|
}));
|
|
98
98
|
// 2. Merge duplicate active project records that point at the same repo root.
|
|
99
99
|
await this.mergeDuplicateProjectsByRoot(activeProjects, workspacesByProject, changes);
|
|
100
|
-
// 3.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
changes.push({
|
|
109
|
-
kind: "project_archived",
|
|
110
|
-
projectId: project.projectId,
|
|
111
|
-
directory: project.rootPath,
|
|
112
|
-
reason: "no_active_workspaces",
|
|
113
|
-
});
|
|
114
|
-
}));
|
|
115
|
-
// 4. Reconcile git metadata for active projects whose directories still exist
|
|
100
|
+
// 3. Reconcile git metadata for active projects whose directories still exist.
|
|
101
|
+
// A project with zero active workspaces is a first-class empty project — it
|
|
102
|
+
// persists until explicitly removed and still reconciles its own metadata.
|
|
103
|
+
// Skip projects archived earlier in this pass (e.g. merged duplicates) so we
|
|
104
|
+
// don't resurrect them by upserting a stale, non-archived copy.
|
|
105
|
+
const archivedProjectIds = new Set(changes
|
|
106
|
+
.filter((change) => change.kind === "project_archived")
|
|
107
|
+
.map((change) => change.projectId));
|
|
116
108
|
const projectsToReconcile = activeProjects.filter((project) => {
|
|
117
109
|
if (project.archivedAt)
|
|
118
110
|
return false;
|
|
119
|
-
|
|
120
|
-
if (siblings.length === 0)
|
|
111
|
+
if (archivedProjectIds.has(project.projectId))
|
|
121
112
|
return false;
|
|
122
113
|
if (!existsSync(project.rootPath))
|
|
123
114
|
return false;
|
|
@@ -143,7 +134,7 @@ export class WorkspaceReconciliationService {
|
|
|
143
134
|
if (project.kind !== "git") {
|
|
144
135
|
continue;
|
|
145
136
|
}
|
|
146
|
-
const rootKey =
|
|
137
|
+
const rootKey = resolve(project.rootPath);
|
|
147
138
|
const group = projectsByRoot.get(rootKey) ?? [];
|
|
148
139
|
group.push(project);
|
|
149
140
|
projectsByRoot.set(rootKey, group);
|
|
@@ -177,6 +168,14 @@ export class WorkspaceReconciliationService {
|
|
|
177
168
|
})));
|
|
178
169
|
for (const project of duplicateProjects) {
|
|
179
170
|
workspacesByProject.set(project.projectId, []);
|
|
171
|
+
const timestamp = new Date().toISOString();
|
|
172
|
+
await this.projectRegistry.archive(project.projectId, timestamp);
|
|
173
|
+
changes.push({
|
|
174
|
+
kind: "project_archived",
|
|
175
|
+
projectId: project.projectId,
|
|
176
|
+
directory: project.rootPath,
|
|
177
|
+
reason: "merged_duplicate",
|
|
178
|
+
});
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
181
|
}
|
|
@@ -236,8 +235,8 @@ export class WorkspaceReconciliationService {
|
|
|
236
235
|
const wsGit = await this.readWorkspaceGitMetadata(workspace.cwd, wsDirName);
|
|
237
236
|
const expectedKind = deriveWorkspaceKindFromMetadata(wsGit);
|
|
238
237
|
const workspaceUpdates = {};
|
|
239
|
-
if (wsGit.projectKind === "git" && workspace.
|
|
240
|
-
workspaceUpdates.
|
|
238
|
+
if (wsGit.projectKind === "git" && workspace.branch !== wsGit.currentBranch) {
|
|
239
|
+
workspaceUpdates.branch = wsGit.currentBranch;
|
|
241
240
|
}
|
|
242
241
|
if (workspace.kind !== expectedKind) {
|
|
243
242
|
workspaceUpdates.kind = expectedKind;
|