Files
paseo/packages/server/src/server/workspace-directory.ts
T
Mohamed Boudra eb6d19969d refactor: explicit returns + void floating promises (T3.a typeaware sweep) (#756)
Cluster T3.a — mechanical fixes for consistent-return and no-floating-promises.

Files touched (15):
- server/shared/tool-call-display.ts — default throw in buildCanonicalDetailDisplay switch
- server/agent/prompt-attachments.ts — default throw in renderPromptAttachmentAsText switch
- server/workspace-directory.ts — default throw in getSortValue switch
- server/agent/mcp-server.ts — default throw in mcpCreateWorktreeInput switch
- server/terminal/terminal-session-controller.ts — default return in dispatch switch
- server/daemon-e2e/agent-configs.ts — default return false in isProviderAvailable switch
- server/agent/agent-manager.ts — void dispatchSessionEvent fire-and-forget call
- server/agent/mcp-shared.ts — void 4 notify() floating calls
- app/agent-status-bar.utils.ts — default throw in getStatusSelectorHint switch
- app/keyboard-shortcuts.ts — default throw in resolvePayload switch
- app/resolve-agent-form.ts — default throw in resolveAgentForm switch
- app/ui/dropdown-menu.tsx — void .then() + return undefined for promise/always-return
- app/use-agent-form-state.ts — void refreshSnapshot() floating call
- cli/worktree/create.ts — default throw in toDaemonCreateInput switch
- cli/onboard.ts — return process.exit(1) to satisfy consistent-return in catch

Deferred (outside 15-file cap or in EXCLUDE list):
- server/worktree-session.ts — getSortValue (same pattern, cap reached)
- Multiple files with other errors (unbound-method, unsafe-type-assertion) are separate clusters
2026-05-05 19:58:56 +07:00

350 lines
11 KiB
TypeScript

import { homedir } from "node:os";
import { sep } from "node:path";
import type pino from "pino";
import type {
AgentSnapshotPayload,
SessionInboundMessage,
SessionOutboundMessage,
WorkspaceDescriptorPayload,
WorkspaceStateBucket,
} from "./messages.js";
import { SortablePager } from "./pagination/sortable-pager.js";
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js";
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
const FETCH_WORKSPACES_SORT_KEYS = [
"status_priority",
"activity_at",
"name",
"project_id",
] as const;
type FetchWorkspacesRequestMessage = Extract<
SessionInboundMessage,
{ type: "fetch_workspaces_request" }
>;
type FetchWorkspacesRequestFilter = NonNullable<FetchWorkspacesRequestMessage["filter"]>;
type FetchWorkspacesRequestSort = NonNullable<FetchWorkspacesRequestMessage["sort"]>[number];
type FetchWorkspacesResponsePayload = Extract<
SessionOutboundMessage,
{ type: "fetch_workspaces_response" }
>["payload"];
type FetchWorkspacesResponseEntry = FetchWorkspacesResponsePayload["entries"][number];
type FetchWorkspacesResponsePageInfo = FetchWorkspacesResponsePayload["pageInfo"];
export type WorkspaceUpdatesFilter = FetchWorkspacesRequestFilter;
export interface WorkspaceDirectoryDeps {
logger: pino.Logger;
projectRegistry: {
list(): Promise<PersistedProjectRecord[]>;
};
workspaceRegistry: {
list(): Promise<PersistedWorkspaceRecord[]>;
};
listAgentPayloads(): Promise<AgentSnapshotPayload[]>;
isProviderVisibleToClient(provider: string): boolean;
buildWorkspaceDescriptor(input: {
workspace: PersistedWorkspaceRecord;
projectRecord?: PersistedProjectRecord | null;
includeGitData: boolean;
}): Promise<WorkspaceDescriptorPayload>;
}
export function summarizeFetchWorkspacesEntries(entries: Iterable<FetchWorkspacesResponseEntry>): {
count: number;
projectIds: string[];
statusCounts: Record<string, number>;
workspaces: Array<{
id: string;
projectId: string;
projectDisplayName: string;
name: string;
status: FetchWorkspacesResponseEntry["status"];
workspaceKind: FetchWorkspacesResponseEntry["workspaceKind"];
activityAt: string | null;
}>;
} {
const workspaces = Array.from(entries, (entry) => ({
id: entry.id,
projectId: entry.projectId,
projectDisplayName: entry.projectDisplayName,
name: entry.name,
status: entry.status,
workspaceKind: entry.workspaceKind,
activityAt: entry.activityAt,
}));
const statusCounts = new Map<string, number>();
for (const workspace of workspaces) {
statusCounts.set(workspace.status, (statusCounts.get(workspace.status) ?? 0) + 1);
}
return {
count: workspaces.length,
projectIds: [...new Set(workspaces.map((workspace) => workspace.projectId))],
statusCounts: Object.fromEntries(statusCounts),
workspaces,
};
}
export class WorkspaceDirectory {
private readonly archivingByWorkspaceId = new Map<string, string>();
private readonly workspaceStatePriority: Record<WorkspaceStateBucket, number> = {
needs_input: 0,
failed: 1,
running: 2,
attention: 3,
done: 4,
};
private readonly pager = new SortablePager<
WorkspaceDescriptorPayload,
FetchWorkspacesRequestSort["key"]
>({
validKeys: FETCH_WORKSPACES_SORT_KEYS,
defaultSort: [{ key: "activity_at", direction: "desc" }],
label: "fetch_workspaces",
getId: (workspace) => workspace.id,
getSortValue: (workspace, key) => {
switch (key) {
case "status_priority":
return this.workspaceStatePriority[workspace.status];
case "activity_at":
return workspace.activityAt ? Date.parse(workspace.activityAt) : null;
case "name":
return workspace.name.toLocaleLowerCase();
case "project_id":
return workspace.projectId.toLocaleLowerCase();
default:
throw new Error("unreachable");
}
},
});
constructor(private readonly deps: WorkspaceDirectoryDeps) {}
markArchiving(workspaceIds: Iterable<string>, archivingAt: string): void {
for (const workspaceId of workspaceIds) {
this.archivingByWorkspaceId.set(workspaceId, archivingAt);
}
}
clearArchiving(workspaceIds: Iterable<string>): void {
for (const workspaceId of workspaceIds) {
this.archivingByWorkspaceId.delete(workspaceId);
}
}
async buildDescriptorMap(options: {
includeGitData: boolean;
workspaceIds?: Iterable<string>;
}): Promise<Map<string, WorkspaceDescriptorPayload>> {
const [agents, persistedWorkspaces, persistedProjects] = await Promise.all([
this.deps.listAgentPayloads(),
this.deps.workspaceRegistry.list(),
this.deps.projectRegistry.list(),
]);
const activeProjects = new Map(
persistedProjects
.filter((project) => !project.archivedAt)
.map((project) => [project.projectId, project] as const),
);
const archivedProjectIds = new Set(
persistedProjects.filter((project) => project.archivedAt).map((project) => project.projectId),
);
const activeRecords = persistedWorkspaces.filter(
(workspace) => !workspace.archivedAt && !archivedProjectIds.has(workspace.projectId),
);
const descriptorsByWorkspaceId = new Map<string, WorkspaceDescriptorPayload>();
const workspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
const workspaceIdsByDirectory = new Map(
activeRecords.map(
(workspace) => [normalizeWorkspaceId(workspace.cwd), workspace.workspaceId] as const,
),
);
const includedWorkspaces = activeRecords.filter(
(workspace) => !workspaceIds || workspaceIds.has(workspace.workspaceId),
);
const workspaceDescriptors = await Promise.all(
includedWorkspaces.map((workspace) =>
this.deps.buildWorkspaceDescriptor({
workspace,
projectRecord: activeProjects.get(workspace.projectId) ?? null,
includeGitData: options.includeGitData,
}),
),
);
for (let i = 0; i < includedWorkspaces.length; i += 1) {
const workspaceId = includedWorkspaces[i].workspaceId;
descriptorsByWorkspaceId.set(workspaceId, {
...workspaceDescriptors[i],
archivingAt: this.archivingByWorkspaceId.get(workspaceId) ?? null,
});
}
for (const agent of agents) {
if (agent.archivedAt) {
continue;
}
if (!this.deps.isProviderVisibleToClient(agent.provider)) {
continue;
}
const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(agent.cwd));
if (workspaceId === undefined) {
continue;
}
const existing = descriptorsByWorkspaceId.get(workspaceId);
if (!existing) {
continue;
}
const bucket = this.deriveStateBucket(agent);
if (this.workspaceStatePriority[bucket] < this.workspaceStatePriority[existing.status]) {
existing.status = bucket;
}
}
return descriptorsByWorkspaceId;
}
resolveRegisteredWorkspaceIdForCwd(cwd: string, workspaces: PersistedWorkspaceRecord[]): string {
const normalizedCwd = normalizeWorkspaceId(cwd);
const exact = workspaces.find((workspace) => workspace.cwd === normalizedCwd);
if (exact) {
return exact.workspaceId;
}
const userHome = homedir();
let bestMatch: PersistedWorkspaceRecord | null = null;
for (const workspace of workspaces) {
if (workspace.cwd === userHome) continue;
if (workspace.archivedAt) continue;
const prefix = workspace.cwd.endsWith(sep) ? workspace.cwd : `${workspace.cwd}${sep}`;
if (!normalizedCwd.startsWith(prefix)) {
continue;
}
if (!bestMatch || workspace.cwd.length > bestMatch.cwd.length) {
bestMatch = workspace;
}
}
return bestMatch?.workspaceId ?? normalizedCwd;
}
async listDescriptors(): Promise<WorkspaceDescriptorPayload[]> {
return Array.from(
(
await this.buildDescriptorMap({
includeGitData: true,
})
).values(),
);
}
matchesFilter(input: {
workspace: WorkspaceDescriptorPayload;
filter: FetchWorkspacesRequestFilter | undefined;
}): boolean {
const { workspace, filter } = input;
if (!filter) {
return true;
}
if (filter.projectId && filter.projectId.trim().length > 0) {
if (workspace.projectId !== filter.projectId.trim()) {
return false;
}
}
if (filter.idPrefix && filter.idPrefix.trim().length > 0) {
if (!workspace.id.startsWith(filter.idPrefix.trim())) {
return false;
}
}
if (filter.query && filter.query.trim().length > 0) {
const query = filter.query.trim().toLocaleLowerCase();
const haystacks = [workspace.name, workspace.projectId, workspace.id];
if (!haystacks.some((value) => value.toLocaleLowerCase().includes(query))) {
return false;
}
}
return true;
}
async listFetchEntries(request: FetchWorkspacesRequestMessage): Promise<{
entries: FetchWorkspacesResponseEntry[];
pageInfo: FetchWorkspacesResponsePageInfo;
}> {
const filter = request.filter;
const sort = this.pager.normalizeSort(request.sort);
let entries = await this.listDescriptors();
const listedCount = entries.length;
entries = entries.filter((workspace) => this.matchesFilter({ workspace, filter }));
const filteredCount = entries.length;
entries.sort((left, right) => this.pager.compare(left, right, sort));
const cursorToken = request.page?.cursor;
if (cursorToken) {
const cursor = this.pager.decode(cursorToken, sort);
entries = entries.filter(
(workspace) => this.pager.compareWithCursor(workspace, cursor, sort) > 0,
);
}
const limit = request.page?.limit ?? 200;
const pagedEntries = entries.slice(0, limit);
const hasMore = entries.length > limit;
const nextCursor =
hasMore && pagedEntries.length > 0
? this.pager.encode(pagedEntries[pagedEntries.length - 1], sort)
: null;
this.deps.logger.debug(
{
requestId: request.requestId,
filter: request.filter ?? null,
sort,
page: request.page ?? null,
listedCount,
filteredCount,
returnedCount: pagedEntries.length,
hasMore,
nextCursor,
},
"fetch_workspaces_entries_listed",
);
return {
entries: pagedEntries,
pageInfo: {
nextCursor,
prevCursor: request.page?.cursor ?? null,
hasMore,
},
};
}
private deriveStateBucket(agent: AgentSnapshotPayload): WorkspaceStateBucket {
const pendingPermissionCount = agent.pendingPermissions?.length ?? 0;
if (pendingPermissionCount > 0 || agent.attentionReason === "permission") {
return "needs_input";
}
if (agent.status === "error" || agent.attentionReason === "error") {
return "failed";
}
if (agent.status === "running") {
return "running";
}
if (agent.requiresAttention) {
return "attention";
}
return "done";
}
}