Centralize git/GitHub runtime state into WorkspaceGitService
Consolidate workspace git watch lifecycle, background fetch scheduling, and GitHub PR polling from session.ts and BackgroundGitFetchManager into a single daemon-scoped WorkspaceGitService. Sessions now subscribe to the service for push-driven updates instead of coordinating watchers and fetch intervals themselves. Workspace payloads gain optional gitRuntime and githubRuntime fields for richer client-side display.
This commit is contained in:
@@ -1855,6 +1855,50 @@ export const ProjectPlacementPayloadSchema = z.object({
|
||||
checkout: ProjectCheckoutLitePayloadSchema,
|
||||
});
|
||||
|
||||
const WorkspaceGitRuntimePayloadSchema = z
|
||||
.object({
|
||||
currentBranch: z.string().nullable().optional(),
|
||||
remoteUrl: z.string().nullable().optional(),
|
||||
isPaseoOwnedWorktree: z.boolean().optional(),
|
||||
isDirty: z.boolean().nullable().optional(),
|
||||
aheadBehind: z
|
||||
.object({
|
||||
ahead: z.number(),
|
||||
behind: z.number(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
aheadOfOrigin: z.number().nullable().optional(),
|
||||
behindOfOrigin: z.number().nullable().optional(),
|
||||
})
|
||||
.optional()
|
||||
.nullable();
|
||||
|
||||
const WorkspaceGitHubRuntimePayloadSchema = z
|
||||
.object({
|
||||
featuresEnabled: z.boolean().optional(),
|
||||
pullRequest: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
state: z.string(),
|
||||
baseRefName: z.string(),
|
||||
headRefName: z.string(),
|
||||
isMerged: z.boolean(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
error: z
|
||||
.object({
|
||||
message: z.string(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
refreshedAt: z.string().nullable().optional(),
|
||||
})
|
||||
.optional()
|
||||
.nullable();
|
||||
|
||||
export const WorkspaceDescriptorPayloadSchema = z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
@@ -1872,6 +1916,8 @@ export const WorkspaceDescriptorPayloadSchema = z.object({
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
gitRuntime: WorkspaceGitRuntimePayloadSchema,
|
||||
githubRuntime: WorkspaceGitHubRuntimePayloadSchema,
|
||||
});
|
||||
|
||||
export const AgentUpdateMessageSchema = z.object({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { SessionInboundMessageSchema, SessionOutboundMessageSchema } from "./messages.js";
|
||||
|
||||
@@ -117,6 +118,162 @@ describe("workspace message schemas", () => {
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
test("parses fetch_workspaces_response with optional runtime fields", () => {
|
||||
const parsed = SessionOutboundMessageSchema.parse({
|
||||
type: "fetch_workspaces_response",
|
||||
payload: {
|
||||
requestId: "req-workspaces",
|
||||
entries: [
|
||||
{
|
||||
id: "/tmp/repo",
|
||||
projectId: "remote:github.com/acme/repo",
|
||||
projectDisplayName: "acme/repo",
|
||||
projectRootPath: "/tmp/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "local_checkout",
|
||||
name: "main",
|
||||
status: "done",
|
||||
activityAt: null,
|
||||
diffStat: {
|
||||
additions: 3,
|
||||
deletions: 1,
|
||||
},
|
||||
gitRuntime: {
|
||||
currentBranch: "main",
|
||||
remoteUrl: "https://github.com/acme/repo.git",
|
||||
isPaseoOwnedWorktree: false,
|
||||
isDirty: true,
|
||||
aheadBehind: {
|
||||
ahead: 2,
|
||||
behind: 1,
|
||||
},
|
||||
aheadOfOrigin: 2,
|
||||
behindOfOrigin: 1,
|
||||
},
|
||||
githubRuntime: {
|
||||
featuresEnabled: true,
|
||||
pullRequest: {
|
||||
url: "https://github.com/acme/repo/pull/123",
|
||||
title: "Runtime payloads",
|
||||
state: "open",
|
||||
baseRefName: "main",
|
||||
headRefName: "workspace-git-service",
|
||||
isMerged: false,
|
||||
},
|
||||
error: null,
|
||||
refreshedAt: "2026-04-12T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
hasMore: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.type).toBe("fetch_workspaces_response");
|
||||
expect(parsed.payload.entries[0]?.gitRuntime).toMatchObject({
|
||||
currentBranch: "main",
|
||||
isDirty: true,
|
||||
aheadOfOrigin: 2,
|
||||
});
|
||||
expect(parsed.payload.entries[0]?.githubRuntime?.pullRequest?.title).toBe("Runtime payloads");
|
||||
});
|
||||
|
||||
test("older workspace parsers ignore additive runtime fields", () => {
|
||||
const message = {
|
||||
type: "fetch_workspaces_response",
|
||||
payload: {
|
||||
requestId: "req-workspaces",
|
||||
entries: [
|
||||
{
|
||||
id: "/tmp/repo",
|
||||
projectId: "remote:github.com/acme/repo",
|
||||
projectDisplayName: "acme/repo",
|
||||
projectRootPath: "/tmp/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "local_checkout",
|
||||
name: "main",
|
||||
status: "done",
|
||||
activityAt: null,
|
||||
diffStat: null,
|
||||
gitRuntime: {
|
||||
currentBranch: "main",
|
||||
remoteUrl: "https://github.com/acme/repo.git",
|
||||
isPaseoOwnedWorktree: false,
|
||||
isDirty: false,
|
||||
aheadBehind: {
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
},
|
||||
aheadOfOrigin: 0,
|
||||
behindOfOrigin: 0,
|
||||
},
|
||||
githubRuntime: {
|
||||
featuresEnabled: true,
|
||||
pullRequest: null,
|
||||
error: null,
|
||||
refreshedAt: "2026-04-12T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
hasMore: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const legacyWorkspaceSchema = z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
projectDisplayName: z.string(),
|
||||
projectRootPath: z.string(),
|
||||
projectKind: z.enum(["git", "non_git"]),
|
||||
workspaceKind: z.enum(["local_checkout", "worktree", "directory"]),
|
||||
name: z.string(),
|
||||
status: z.enum(["needs_input", "failed", "running", "attention", "done"]),
|
||||
activityAt: z.string().nullable(),
|
||||
diffStat: z
|
||||
.object({
|
||||
additions: z.number(),
|
||||
deletions: z.number(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
});
|
||||
const legacyMessageSchema = z.object({
|
||||
type: z.literal("fetch_workspaces_response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
entries: z.array(legacyWorkspaceSchema),
|
||||
pageInfo: z.object({
|
||||
nextCursor: z.string().nullable(),
|
||||
prevCursor: z.string().nullable(),
|
||||
hasMore: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const parsed = legacyMessageSchema.parse(message);
|
||||
|
||||
expect(parsed.payload.entries[0]).toEqual({
|
||||
id: "/tmp/repo",
|
||||
projectId: "remote:github.com/acme/repo",
|
||||
projectDisplayName: "acme/repo",
|
||||
projectRootPath: "/tmp/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "local_checkout",
|
||||
name: "main",
|
||||
status: "done",
|
||||
activityAt: null,
|
||||
diffStat: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("parses legacy fetch_agents_response checkout payloads without worktreeRoot", () => {
|
||||
const result = SessionOutboundMessageSchema.safeParse({
|
||||
type: "fetch_agents_response",
|
||||
|
||||
Reference in New Issue
Block a user