refactor(server): extract checkout read subsystem into CheckoutSession (#1644)
Extract the checkout read & live-stream side (status, branch validate/suggest, manual refresh, diff + status subscriptions) out of session.ts into session/checkout/checkout-session.ts behind a narrow CheckoutSessionHost seam and a CheckoutDiffSubscriber port. session.ts -234 net lines; 12 unit tests via injected fakes (no module mocks); 6 session tests moved onto the public handleMessage path. Also fixes a pre-existing tilde-resolution bug in the refresh handler (TDD).
This commit is contained in:
@@ -2493,7 +2493,7 @@ describe("session checkout refresh handling", () => {
|
||||
messages,
|
||||
});
|
||||
|
||||
await asSessionInternals(session).handleCheckoutRefreshRequest({
|
||||
await session.handleMessage({
|
||||
type: "checkout.refresh.request",
|
||||
cwd: "/tmp/request-worktree",
|
||||
requestId: "request-refresh",
|
||||
@@ -2531,7 +2531,7 @@ describe("session checkout refresh handling", () => {
|
||||
messages,
|
||||
});
|
||||
|
||||
await asSessionInternals(session).handleCheckoutRefreshRequest({
|
||||
await session.handleMessage({
|
||||
type: "checkout.refresh.request",
|
||||
cwd: "/tmp/request-worktree",
|
||||
requestId: "request-refresh-error",
|
||||
@@ -2559,7 +2559,7 @@ describe("session checkout status handling", () => {
|
||||
};
|
||||
const session = createSessionForTest({ workspaceGitService, messages });
|
||||
|
||||
await asSessionInternals(session).handleCheckoutStatusRequest({
|
||||
await session.handleMessage({
|
||||
type: "checkout_status_request",
|
||||
cwd: "/tmp/service-worktree",
|
||||
requestId: "request-status",
|
||||
@@ -2608,7 +2608,7 @@ describe("session checkout status handling", () => {
|
||||
};
|
||||
const session = createSessionForTest({ workspaceGitService, messages });
|
||||
|
||||
await asSessionInternals(session).handleCheckoutStatusRequest({
|
||||
await session.handleMessage({
|
||||
type: "checkout_status_request",
|
||||
cwd: "/tmp/cold-worktree",
|
||||
requestId: "request-cold-status",
|
||||
@@ -2856,7 +2856,7 @@ describe("session branch validation", () => {
|
||||
};
|
||||
const session = createSessionForTest({ workspaceGitService, messages });
|
||||
|
||||
await asSessionInternals(session).handleValidateBranchRequest({
|
||||
await session.handleMessage({
|
||||
type: "validate_branch_request",
|
||||
cwd: "/tmp/repo",
|
||||
branchName: "feature",
|
||||
@@ -3279,7 +3279,7 @@ describe("session branch suggestions handling", () => {
|
||||
};
|
||||
const session = createSessionForTest({ workspaceGitService, messages });
|
||||
|
||||
await asSessionInternals(session).handleBranchSuggestionsRequest({
|
||||
await session.handleMessage({
|
||||
type: "branch_suggestions_request",
|
||||
cwd: "/tmp/repo",
|
||||
query: "service",
|
||||
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
type CheckoutRenameBranchRequest,
|
||||
type StartWorkspaceScriptRequest,
|
||||
type CloseItemsRequest,
|
||||
type SubscribeCheckoutDiffRequest,
|
||||
type UnsubscribeCheckoutDiffRequest,
|
||||
type DirectorySuggestionsRequest,
|
||||
type ProjectPlacementPayload,
|
||||
type WorkspaceSetupSnapshot,
|
||||
@@ -170,6 +168,7 @@ import {
|
||||
import { wrapSpokenInput } from "./voice-config.js";
|
||||
import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
|
||||
import { VoiceSession } from "./voice/voice-session.js";
|
||||
import { CheckoutSession } from "./session/checkout/checkout-session.js";
|
||||
import {
|
||||
listDirectoryEntries,
|
||||
readExplorerFile,
|
||||
@@ -206,11 +205,8 @@ import { getProjectIcon } from "../utils/project-icon.js";
|
||||
import { expandTilde } from "../utils/path.js";
|
||||
import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
|
||||
import { toCheckoutError } from "./checkout-git-utils.js";
|
||||
import { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
||||
import {
|
||||
buildCheckoutPrStatusPayloadFromSnapshot,
|
||||
buildCheckoutStatusPayloadFromSnapshot,
|
||||
} from "./checkout/status-projection.js";
|
||||
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
||||
import { buildCheckoutPrStatusPayloadFromSnapshot } from "./checkout/status-projection.js";
|
||||
import type { Resolvable } from "./speech/provider-resolver.js";
|
||||
import type { SpeechReadinessSnapshot } from "./speech/speech-runtime.js";
|
||||
import type pino from "pino";
|
||||
@@ -711,7 +707,6 @@ export class Session {
|
||||
private readonly chatService: FileBackedChatService;
|
||||
private readonly scheduleService: ScheduleService;
|
||||
private readonly loopService: LoopService;
|
||||
private readonly checkoutDiffManager: CheckoutDiffManager;
|
||||
private readonly github: GitHubService;
|
||||
private readonly renameCurrentBranch: typeof renameCurrentBranchDefault;
|
||||
private readonly generateWorkspaceName: typeof generateBranchNameFromFirstAgentContext;
|
||||
@@ -750,7 +745,6 @@ export class Session {
|
||||
private readonly terminalController: TerminalSessionController;
|
||||
private inflightRequests = 0;
|
||||
private peakInflightRequests = 0;
|
||||
private readonly checkoutDiffSubscriptions = new Map<string, () => void>();
|
||||
private readonly workspaceGitWatchTargets = new Map<string, WorkspaceGitWatchTarget>();
|
||||
private readonly workspaceSetupSnapshots: Map<string, WorkspaceSetupSnapshot>;
|
||||
private readonly workspaceGitFetchSubscriptions = new Map<string, () => void>();
|
||||
@@ -758,6 +752,7 @@ export class Session {
|
||||
private readonly fileUploads: FileUploadStore;
|
||||
private readonly workspaceDirectory: WorkspaceDirectory;
|
||||
private readonly voiceSession: VoiceSession;
|
||||
private readonly checkoutSession: CheckoutSession;
|
||||
private readonly serverId: string | undefined;
|
||||
private readonly daemonVersion: string | undefined;
|
||||
private readonly daemonRuntimeConfig: SessionOptions["daemonRuntimeConfig"];
|
||||
@@ -839,11 +834,19 @@ export class Session {
|
||||
this.chatService = chatService;
|
||||
this.scheduleService = scheduleService;
|
||||
this.loopService = loopService;
|
||||
this.checkoutDiffManager = checkoutDiffManager;
|
||||
this.github = github ?? createGitHubService();
|
||||
this.renameCurrentBranch = renameCurrentBranch ?? renameCurrentBranchDefault;
|
||||
this.generateWorkspaceName = generateWorkspaceName ?? generateBranchNameFromFirstAgentContext;
|
||||
this.workspaceGitService = workspaceGitService;
|
||||
this.checkoutSession = new CheckoutSession({
|
||||
host: {
|
||||
emit: (msg) => this.emit(msg),
|
||||
},
|
||||
workspaceGitService: this.workspaceGitService,
|
||||
github: this.github,
|
||||
checkoutDiffManager,
|
||||
logger: this.sessionLogger,
|
||||
});
|
||||
this.daemonConfigStore = daemonConfigStore;
|
||||
this.mcpBaseUrl = mcpBaseUrl ?? null;
|
||||
this.terminalManager = terminalManager;
|
||||
@@ -2007,17 +2010,17 @@ export class Session {
|
||||
private dispatchCheckoutMessage(msg: SessionInboundMessage): Promise<void> | undefined {
|
||||
switch (msg.type) {
|
||||
case "checkout_status_request":
|
||||
return this.handleCheckoutStatusRequest(msg);
|
||||
return this.checkoutSession.handleStatusRequest(msg);
|
||||
case "validate_branch_request":
|
||||
return this.handleValidateBranchRequest(msg);
|
||||
return this.checkoutSession.handleValidateBranchRequest(msg);
|
||||
case "branch_suggestions_request":
|
||||
return this.handleBranchSuggestionsRequest(msg);
|
||||
return this.checkoutSession.handleBranchSuggestionsRequest(msg);
|
||||
case "directory_suggestions_request":
|
||||
return this.handleDirectorySuggestionsRequest(msg);
|
||||
case "subscribe_checkout_diff_request":
|
||||
return this.handleSubscribeCheckoutDiffRequest(msg);
|
||||
return this.checkoutSession.handleSubscribeDiffRequest(msg);
|
||||
case "unsubscribe_checkout_diff_request":
|
||||
this.handleUnsubscribeCheckoutDiffRequest(msg);
|
||||
this.checkoutSession.handleUnsubscribeDiffRequest(msg);
|
||||
return undefined;
|
||||
case "checkout_switch_branch_request":
|
||||
return this.handleCheckoutSwitchBranchRequest(msg);
|
||||
@@ -2034,7 +2037,7 @@ export class Session {
|
||||
case "checkout_push_request":
|
||||
return this.handleCheckoutPushRequest(msg);
|
||||
case "checkout.refresh.request":
|
||||
return this.handleCheckoutRefreshRequest(msg);
|
||||
return this.checkoutSession.handleRefreshRequest(msg);
|
||||
case "checkout_pr_create_request":
|
||||
return this.handleCheckoutPrCreateRequest(msg);
|
||||
case "checkout_pr_merge_request":
|
||||
@@ -4520,144 +4523,6 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCheckoutStatusRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "checkout_status_request" }>,
|
||||
): Promise<void> {
|
||||
const { cwd, requestId } = msg;
|
||||
const resolvedCwd = expandTilde(cwd);
|
||||
|
||||
try {
|
||||
const snapshot = await this.workspaceGitService.getSnapshot(resolvedCwd);
|
||||
this.emit({
|
||||
type: "checkout_status_response",
|
||||
payload: buildCheckoutStatusPayloadFromSnapshot({
|
||||
cwd,
|
||||
requestId,
|
||||
snapshot,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
this.emit({
|
||||
type: "checkout_status_response",
|
||||
payload: {
|
||||
cwd,
|
||||
isGit: false,
|
||||
repoRoot: null,
|
||||
currentBranch: null,
|
||||
isDirty: null,
|
||||
baseRef: null,
|
||||
aheadBehind: null,
|
||||
aheadOfOrigin: null,
|
||||
behindOfOrigin: null,
|
||||
hasRemote: false,
|
||||
remoteUrl: null,
|
||||
isPaseoOwnedWorktree: false,
|
||||
error: toCheckoutError(error),
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleValidateBranchRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "validate_branch_request" }>,
|
||||
): Promise<void> {
|
||||
const { cwd, branchName, requestId } = msg;
|
||||
|
||||
try {
|
||||
const resolvedCwd = expandTilde(cwd);
|
||||
this.assertSafeGitRef(branchName, "branch");
|
||||
|
||||
const resolution = await this.workspaceGitService.validateBranchRef(resolvedCwd, branchName);
|
||||
switch (resolution.kind) {
|
||||
case "local":
|
||||
this.emit({
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: true,
|
||||
resolvedRef: resolution.name,
|
||||
isRemote: false,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
case "remote-only":
|
||||
this.emit({
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: true,
|
||||
resolvedRef: resolution.remoteRef,
|
||||
isRemote: true,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
case "not-found":
|
||||
this.emit({
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: false,
|
||||
resolvedRef: null,
|
||||
isRemote: false,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
default: {
|
||||
const exhaustiveCheck: never = resolution;
|
||||
throw new Error(`Unhandled branch resolution: ${getErrorMessage(exhaustiveCheck)}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.emit({
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: false,
|
||||
resolvedRef: null,
|
||||
isRemote: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleBranchSuggestionsRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "branch_suggestions_request" }>,
|
||||
): Promise<void> {
|
||||
const { cwd, query, limit, requestId } = msg;
|
||||
|
||||
try {
|
||||
const resolvedCwd = expandTilde(cwd);
|
||||
const branchDetails = await this.workspaceGitService.suggestBranchesForCwd(resolvedCwd, {
|
||||
query,
|
||||
limit,
|
||||
});
|
||||
this.emit({
|
||||
type: "branch_suggestions_response",
|
||||
payload: {
|
||||
branches: branchDetails.map((branch) => branch.name),
|
||||
branchDetails,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.emit({
|
||||
type: "branch_suggestions_response",
|
||||
payload: {
|
||||
branches: [],
|
||||
branchDetails: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGitHubSearchRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "github_search_request" }>,
|
||||
): Promise<void> {
|
||||
@@ -4885,73 +4750,12 @@ export class Session {
|
||||
"Failed to emit workspace update after git branch snapshot",
|
||||
);
|
||||
});
|
||||
this.emitCheckoutStatusUpdate(normalizedCwd, snapshot);
|
||||
this.checkoutSession.emitStatusUpdate(normalizedCwd, snapshot);
|
||||
},
|
||||
);
|
||||
this.workspaceGitSubscriptions.set(normalizedCwd, subscription.unsubscribe);
|
||||
}
|
||||
|
||||
private async handleSubscribeCheckoutDiffRequest(
|
||||
msg: SubscribeCheckoutDiffRequest,
|
||||
): Promise<void> {
|
||||
const cwd = expandTilde(msg.cwd);
|
||||
this.checkoutDiffSubscriptions.get(msg.subscriptionId)?.();
|
||||
this.checkoutDiffSubscriptions.delete(msg.subscriptionId);
|
||||
const subscription = await this.checkoutDiffManager.subscribe(
|
||||
{ cwd, compare: msg.compare },
|
||||
(snapshot) => {
|
||||
this.emit({
|
||||
type: "checkout_diff_update",
|
||||
payload: {
|
||||
subscriptionId: msg.subscriptionId,
|
||||
...snapshot,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
this.checkoutDiffSubscriptions.set(msg.subscriptionId, subscription.unsubscribe);
|
||||
|
||||
this.emit({
|
||||
type: "subscribe_checkout_diff_response",
|
||||
payload: {
|
||||
subscriptionId: msg.subscriptionId,
|
||||
...subscription.initial,
|
||||
requestId: msg.requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleUnsubscribeCheckoutDiffRequest(msg: UnsubscribeCheckoutDiffRequest): void {
|
||||
this.checkoutDiffSubscriptions.get(msg.subscriptionId)?.();
|
||||
this.checkoutDiffSubscriptions.delete(msg.subscriptionId);
|
||||
}
|
||||
|
||||
private emitCheckoutStatusUpdate(cwd: string, snapshot: WorkspaceGitRuntimeSnapshot): void {
|
||||
try {
|
||||
const requestId = `subscription:${cwd}`;
|
||||
this.emit({
|
||||
type: "checkout_status_update",
|
||||
payload: {
|
||||
...buildCheckoutStatusPayloadFromSnapshot({
|
||||
cwd,
|
||||
requestId,
|
||||
snapshot,
|
||||
}),
|
||||
prStatus: buildCheckoutPrStatusPayloadFromSnapshot({
|
||||
cwd,
|
||||
requestId,
|
||||
snapshot,
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.sessionLogger.warn(
|
||||
{ err: error, cwd },
|
||||
"Failed to emit workspace checkout status update",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCheckoutSwitchBranchRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "checkout_switch_branch_request" }>,
|
||||
): Promise<void> {
|
||||
@@ -4959,7 +4763,7 @@ export class Session {
|
||||
|
||||
try {
|
||||
const checkoutResult = await this.checkoutExistingBranch(cwd, branch);
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.checkoutSession.scheduleDiffRefresh(cwd);
|
||||
|
||||
// Push a workspace_update immediately so the sidebar/header reflect
|
||||
// the new branch name without waiting for the background git watcher.
|
||||
@@ -5011,7 +4815,7 @@ export class Session {
|
||||
try {
|
||||
const result = await this.renameCurrentBranch(cwd, branch);
|
||||
await this.notifyGitMutation(cwd, "rename-branch", { invalidateGithub: true });
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.checkoutSession.scheduleDiffRefresh(cwd);
|
||||
this.handleWorkspaceGitBranchSnapshot(cwd, result.currentBranch);
|
||||
|
||||
// Branch is a git fact derived per-descriptor from each workspace's own
|
||||
@@ -5066,7 +4870,7 @@ export class Session {
|
||||
cwd,
|
||||
});
|
||||
await this.notifyGitMutation(cwd, "stash-push");
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.checkoutSession.scheduleDiffRefresh(cwd);
|
||||
this.emit({
|
||||
type: "stash_save_response",
|
||||
payload: { cwd, success: true, error: null, requestId },
|
||||
@@ -5088,7 +4892,7 @@ export class Session {
|
||||
cwd,
|
||||
});
|
||||
await this.notifyGitMutation(cwd, "stash-pop");
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.checkoutSession.scheduleDiffRefresh(cwd);
|
||||
this.emit({
|
||||
type: "stash_pop_response",
|
||||
payload: { cwd, success: true, error: null, requestId },
|
||||
@@ -5140,7 +4944,7 @@ export class Session {
|
||||
addAll: msg.addAll ?? true,
|
||||
});
|
||||
await this.notifyGitMutation(cwd, "commit-changes");
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.checkoutSession.scheduleDiffRefresh(cwd);
|
||||
|
||||
this.emit({
|
||||
type: "checkout_commit_response",
|
||||
@@ -5201,7 +5005,7 @@ export class Session {
|
||||
this.notifyGitMutation(mutatedCwd, "merge-to-base", { invalidateGithub: true }),
|
||||
...(mutatedCwd !== cwd ? [this.notifyGitMutation(cwd, "merge-to-base")] : []),
|
||||
]);
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.checkoutSession.scheduleDiffRefresh(cwd);
|
||||
|
||||
this.emit({
|
||||
type: "checkout_merge_response",
|
||||
@@ -5243,7 +5047,7 @@ export class Session {
|
||||
requireCleanTarget: msg.requireCleanTarget ?? true,
|
||||
});
|
||||
await this.notifyGitMutation(cwd, "merge-from-base", { invalidateGithub: true });
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.checkoutSession.scheduleDiffRefresh(cwd);
|
||||
|
||||
this.emit({
|
||||
type: "checkout_merge_from_base_response",
|
||||
@@ -5275,7 +5079,7 @@ export class Session {
|
||||
try {
|
||||
await pullCurrentBranch(cwd);
|
||||
await this.notifyGitMutation(cwd, "pull", { invalidateGithub: true });
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.checkoutSession.scheduleDiffRefresh(cwd);
|
||||
|
||||
this.emit({
|
||||
type: "checkout_pull_response",
|
||||
@@ -5329,41 +5133,6 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCheckoutRefreshRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "checkout.refresh.request" }>,
|
||||
): Promise<void> {
|
||||
const { cwd, requestId } = msg;
|
||||
|
||||
try {
|
||||
this.github.invalidate({ cwd });
|
||||
await this.workspaceGitService.getSnapshot(cwd, {
|
||||
force: true,
|
||||
includeGitHub: true,
|
||||
reason: "manual-refresh",
|
||||
});
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
this.emit({
|
||||
type: "checkout.refresh.response",
|
||||
payload: {
|
||||
cwd,
|
||||
success: true,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.emit({
|
||||
type: "checkout.refresh.response",
|
||||
payload: {
|
||||
cwd,
|
||||
success: false,
|
||||
error: toCheckoutError(error),
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCheckoutPrCreateRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "checkout_pr_create_request" }>,
|
||||
): Promise<void> {
|
||||
@@ -8683,10 +8452,7 @@ export class Session {
|
||||
|
||||
this.terminalController.dispose();
|
||||
|
||||
for (const unsubscribe of this.checkoutDiffSubscriptions.values()) {
|
||||
unsubscribe();
|
||||
}
|
||||
this.checkoutDiffSubscriptions.clear();
|
||||
this.checkoutSession.cleanup();
|
||||
|
||||
for (const unsubscribe of this.workspaceGitSubscriptions.values()) {
|
||||
unsubscribe();
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import pino from "pino";
|
||||
import {
|
||||
type CheckoutDiffSubscriber,
|
||||
CheckoutSession,
|
||||
type CheckoutSessionHost,
|
||||
} from "./checkout-session.js";
|
||||
import { createGitHubService } from "../../../services/github-service.js";
|
||||
import type { SessionOutboundMessage } from "../../messages.js";
|
||||
import type {
|
||||
CheckoutDiffCompareInput,
|
||||
CheckoutDiffSnapshotPayload,
|
||||
} from "../../checkout-diff-manager.js";
|
||||
import type {
|
||||
WorkspaceGitRuntimeSnapshot,
|
||||
WorkspaceGitService,
|
||||
} from "../../workspace-git-service.js";
|
||||
import {
|
||||
createNoGitWorkspaceRuntimeSnapshot,
|
||||
createNoopWorkspaceGitService,
|
||||
} from "../../test-utils/workspace-git-service-stub.js";
|
||||
import { expandTilde } from "../../../utils/path.js";
|
||||
|
||||
interface FakeDiffSubscription {
|
||||
cwd: string;
|
||||
compare: CheckoutDiffCompareInput;
|
||||
listener: (snapshot: CheckoutDiffSnapshotPayload) => void;
|
||||
unsubscribeCalls: number;
|
||||
}
|
||||
|
||||
function createFakeDiffSubscriber(initial: CheckoutDiffSnapshotPayload) {
|
||||
const subscriptions: FakeDiffSubscription[] = [];
|
||||
const refreshedCwds: string[] = [];
|
||||
const subscriber: CheckoutDiffSubscriber = {
|
||||
subscribe: async (params, listener) => {
|
||||
const subscription: FakeDiffSubscription = {
|
||||
cwd: params.cwd,
|
||||
compare: params.compare,
|
||||
listener,
|
||||
unsubscribeCalls: 0,
|
||||
};
|
||||
subscriptions.push(subscription);
|
||||
return {
|
||||
initial: { ...initial, cwd: params.cwd },
|
||||
unsubscribe: () => {
|
||||
subscription.unsubscribeCalls += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
scheduleRefreshForCwd: (cwd) => {
|
||||
refreshedCwds.push(cwd);
|
||||
},
|
||||
};
|
||||
return { subscriber, subscriptions, refreshedCwds };
|
||||
}
|
||||
|
||||
function makeCheckoutSession(options?: {
|
||||
git?: Partial<WorkspaceGitService>;
|
||||
diff?: CheckoutDiffSubscriber;
|
||||
}) {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const host: CheckoutSessionHost = { emit: (msg) => emitted.push(msg) };
|
||||
const checkout = new CheckoutSession({
|
||||
host,
|
||||
workspaceGitService: createNoopWorkspaceGitService(options?.git),
|
||||
github: createGitHubService(),
|
||||
checkoutDiffManager:
|
||||
options?.diff ?? createFakeDiffSubscriber({ cwd: "", files: [], error: null }).subscriber,
|
||||
logger: pino({ level: "silent" }),
|
||||
});
|
||||
return { checkout, emitted };
|
||||
}
|
||||
|
||||
function createGitSnapshot(cwd: string, currentBranch: string): WorkspaceGitRuntimeSnapshot {
|
||||
return {
|
||||
cwd,
|
||||
git: {
|
||||
isGit: true,
|
||||
repoRoot: cwd,
|
||||
mainRepoRoot: cwd,
|
||||
currentBranch,
|
||||
remoteUrl: null,
|
||||
isPaseoOwnedWorktree: false,
|
||||
isDirty: false,
|
||||
baseRef: null,
|
||||
aheadBehind: null,
|
||||
aheadOfOrigin: null,
|
||||
behindOfOrigin: null,
|
||||
hasRemote: false,
|
||||
diffStat: null,
|
||||
},
|
||||
github: { featuresEnabled: false, pullRequest: null, error: null },
|
||||
};
|
||||
}
|
||||
|
||||
describe("CheckoutSession", () => {
|
||||
describe("status", () => {
|
||||
it("emits a checkout status response built from the git snapshot", async () => {
|
||||
const { checkout, emitted } = makeCheckoutSession({
|
||||
git: { getSnapshot: async () => createGitSnapshot("/repo", "main") },
|
||||
});
|
||||
|
||||
await checkout.handleStatusRequest({
|
||||
type: "checkout_status_request",
|
||||
cwd: "/repo",
|
||||
requestId: "r1",
|
||||
});
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "checkout_status_response",
|
||||
payload: expect.objectContaining({
|
||||
cwd: "/repo",
|
||||
requestId: "r1",
|
||||
isGit: true,
|
||||
currentBranch: "main",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits an error status response when the git snapshot read fails", async () => {
|
||||
const { checkout, emitted } = makeCheckoutSession({
|
||||
git: {
|
||||
getSnapshot: async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkout.handleStatusRequest({
|
||||
type: "checkout_status_request",
|
||||
cwd: "/repo",
|
||||
requestId: "r2",
|
||||
});
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "checkout_status_response",
|
||||
payload: expect.objectContaining({
|
||||
cwd: "/repo",
|
||||
requestId: "r2",
|
||||
isGit: false,
|
||||
error: { code: "UNKNOWN", message: "boom" },
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validate branch", () => {
|
||||
it("validates an existing local branch", async () => {
|
||||
const { checkout, emitted } = makeCheckoutSession({
|
||||
git: { validateBranchRef: async () => ({ kind: "local", name: "feature" }) },
|
||||
});
|
||||
|
||||
await checkout.handleValidateBranchRequest({
|
||||
type: "validate_branch_request",
|
||||
cwd: "/repo",
|
||||
branchName: "feature",
|
||||
requestId: "r3",
|
||||
});
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: true,
|
||||
resolvedRef: "feature",
|
||||
isRemote: false,
|
||||
error: null,
|
||||
requestId: "r3",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports a missing branch as not found", async () => {
|
||||
const { checkout, emitted } = makeCheckoutSession({
|
||||
git: { validateBranchRef: async () => ({ kind: "not-found" }) },
|
||||
});
|
||||
|
||||
await checkout.handleValidateBranchRequest({
|
||||
type: "validate_branch_request",
|
||||
cwd: "/repo",
|
||||
branchName: "ghost",
|
||||
requestId: "r4",
|
||||
});
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: false,
|
||||
resolvedRef: null,
|
||||
isRemote: false,
|
||||
error: null,
|
||||
requestId: "r4",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects an unsafe branch ref before touching git", async () => {
|
||||
let validateCalls = 0;
|
||||
const { checkout, emitted } = makeCheckoutSession({
|
||||
git: {
|
||||
validateBranchRef: async () => {
|
||||
validateCalls += 1;
|
||||
return { kind: "not-found" };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkout.handleValidateBranchRequest({
|
||||
type: "validate_branch_request",
|
||||
cwd: "/repo",
|
||||
branchName: "bad ref!",
|
||||
requestId: "r5",
|
||||
});
|
||||
|
||||
expect(validateCalls).toBe(0);
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: false,
|
||||
resolvedRef: null,
|
||||
isRemote: false,
|
||||
error: "Invalid branch: bad ref!",
|
||||
requestId: "r5",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch suggestions", () => {
|
||||
it("emits branch names and details", async () => {
|
||||
const { checkout, emitted } = makeCheckoutSession({
|
||||
git: {
|
||||
suggestBranchesForCwd: async () => [
|
||||
{ name: "main", committerDate: 1, hasLocal: true, hasRemote: true },
|
||||
{ name: "dev", committerDate: 2, hasLocal: true, hasRemote: false },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await checkout.handleBranchSuggestionsRequest({
|
||||
type: "branch_suggestions_request",
|
||||
cwd: "/repo",
|
||||
requestId: "r6",
|
||||
});
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "branch_suggestions_response",
|
||||
payload: {
|
||||
branches: ["main", "dev"],
|
||||
branchDetails: [
|
||||
{ name: "main", committerDate: 1, hasLocal: true, hasRemote: true },
|
||||
{ name: "dev", committerDate: 2, hasLocal: true, hasRemote: false },
|
||||
],
|
||||
error: null,
|
||||
requestId: "r6",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("refresh", () => {
|
||||
it("forces a github-inclusive snapshot, nudges diffs, and confirms success", async () => {
|
||||
const snapshotCalls: Array<{ cwd: string; options: unknown }> = [];
|
||||
const { subscriber, refreshedCwds } = createFakeDiffSubscriber({
|
||||
cwd: "",
|
||||
files: [],
|
||||
error: null,
|
||||
});
|
||||
const { checkout, emitted } = makeCheckoutSession({
|
||||
git: {
|
||||
getSnapshot: async (cwd, snapshotOptions) => {
|
||||
snapshotCalls.push({ cwd, options: snapshotOptions });
|
||||
return createNoGitWorkspaceRuntimeSnapshot(cwd);
|
||||
},
|
||||
},
|
||||
diff: subscriber,
|
||||
});
|
||||
|
||||
await checkout.handleRefreshRequest({
|
||||
type: "checkout.refresh.request",
|
||||
cwd: "/repo",
|
||||
requestId: "r7",
|
||||
});
|
||||
|
||||
expect(snapshotCalls).toEqual([
|
||||
{ cwd: "/repo", options: { force: true, includeGitHub: true, reason: "manual-refresh" } },
|
||||
]);
|
||||
expect(refreshedCwds).toEqual(["/repo"]);
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "checkout.refresh.response",
|
||||
payload: { cwd: "/repo", success: true, error: null, requestId: "r7" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("expands a tilde cwd before refreshing git and diffs", async () => {
|
||||
const snapshotCalls: string[] = [];
|
||||
const { subscriber, refreshedCwds } = createFakeDiffSubscriber({
|
||||
cwd: "",
|
||||
files: [],
|
||||
error: null,
|
||||
});
|
||||
const { checkout } = makeCheckoutSession({
|
||||
git: {
|
||||
getSnapshot: async (cwd) => {
|
||||
snapshotCalls.push(cwd);
|
||||
return createNoGitWorkspaceRuntimeSnapshot(cwd);
|
||||
},
|
||||
},
|
||||
diff: subscriber,
|
||||
});
|
||||
|
||||
await checkout.handleRefreshRequest({
|
||||
type: "checkout.refresh.request",
|
||||
cwd: "~/repo",
|
||||
requestId: "r-tilde",
|
||||
});
|
||||
|
||||
const resolvedCwd = expandTilde("~/repo");
|
||||
expect(snapshotCalls).toEqual([resolvedCwd]);
|
||||
expect(refreshedCwds).toEqual([resolvedCwd]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("diff subscriptions", () => {
|
||||
it("opens a subscription, streams updates tagged with the id, and tears down on unsubscribe", async () => {
|
||||
const { subscriber, subscriptions } = createFakeDiffSubscriber({
|
||||
cwd: "/repo",
|
||||
files: [],
|
||||
error: null,
|
||||
});
|
||||
const { checkout, emitted } = makeCheckoutSession({ diff: subscriber });
|
||||
|
||||
await checkout.handleSubscribeDiffRequest({
|
||||
type: "subscribe_checkout_diff_request",
|
||||
subscriptionId: "s1",
|
||||
cwd: "/repo",
|
||||
compare: { mode: "uncommitted" },
|
||||
requestId: "r8",
|
||||
});
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "subscribe_checkout_diff_response",
|
||||
payload: { subscriptionId: "s1", cwd: "/repo", files: [], error: null, requestId: "r8" },
|
||||
},
|
||||
]);
|
||||
expect(subscriptions).toHaveLength(1);
|
||||
|
||||
subscriptions[0].listener({
|
||||
cwd: "/repo",
|
||||
files: [],
|
||||
error: { code: "UNKNOWN", message: "transient" },
|
||||
});
|
||||
|
||||
expect(emitted[1]).toEqual({
|
||||
type: "checkout_diff_update",
|
||||
payload: {
|
||||
subscriptionId: "s1",
|
||||
cwd: "/repo",
|
||||
files: [],
|
||||
error: { code: "UNKNOWN", message: "transient" },
|
||||
},
|
||||
});
|
||||
|
||||
checkout.handleUnsubscribeDiffRequest({
|
||||
type: "unsubscribe_checkout_diff_request",
|
||||
subscriptionId: "s1",
|
||||
});
|
||||
|
||||
expect(subscriptions[0].unsubscribeCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("replaces an existing subscription when the same id subscribes again", async () => {
|
||||
const { subscriber, subscriptions } = createFakeDiffSubscriber({
|
||||
cwd: "/repo",
|
||||
files: [],
|
||||
error: null,
|
||||
});
|
||||
const { checkout } = makeCheckoutSession({ diff: subscriber });
|
||||
|
||||
await checkout.handleSubscribeDiffRequest({
|
||||
type: "subscribe_checkout_diff_request",
|
||||
subscriptionId: "s1",
|
||||
cwd: "/repo",
|
||||
compare: { mode: "uncommitted" },
|
||||
requestId: "first",
|
||||
});
|
||||
await checkout.handleSubscribeDiffRequest({
|
||||
type: "subscribe_checkout_diff_request",
|
||||
subscriptionId: "s1",
|
||||
cwd: "/repo",
|
||||
compare: { mode: "uncommitted" },
|
||||
requestId: "second",
|
||||
});
|
||||
|
||||
expect(subscriptions).toHaveLength(2);
|
||||
expect(subscriptions[0].unsubscribeCalls).toBe(1);
|
||||
expect(subscriptions[1].unsubscribeCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("unsubscribes every live subscription on cleanup", async () => {
|
||||
const { subscriber, subscriptions } = createFakeDiffSubscriber({
|
||||
cwd: "/repo",
|
||||
files: [],
|
||||
error: null,
|
||||
});
|
||||
const { checkout } = makeCheckoutSession({ diff: subscriber });
|
||||
|
||||
await checkout.handleSubscribeDiffRequest({
|
||||
type: "subscribe_checkout_diff_request",
|
||||
subscriptionId: "s1",
|
||||
cwd: "/repo",
|
||||
compare: { mode: "uncommitted" },
|
||||
requestId: "r",
|
||||
});
|
||||
await checkout.handleSubscribeDiffRequest({
|
||||
type: "subscribe_checkout_diff_request",
|
||||
subscriptionId: "s2",
|
||||
cwd: "/repo",
|
||||
compare: { mode: "uncommitted" },
|
||||
requestId: "r",
|
||||
});
|
||||
|
||||
checkout.cleanup();
|
||||
|
||||
expect(subscriptions[0].unsubscribeCalls).toBe(1);
|
||||
expect(subscriptions[1].unsubscribeCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("status updates", () => {
|
||||
it("emits a checkout status update for a workspace git snapshot", () => {
|
||||
const { checkout, emitted } = makeCheckoutSession();
|
||||
|
||||
checkout.emitStatusUpdate("/repo", createGitSnapshot("/repo", "main"));
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "checkout_status_update",
|
||||
payload: expect.objectContaining({ cwd: "/repo", currentBranch: "main" }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,317 @@
|
||||
import type pino from "pino";
|
||||
import { getErrorMessage } from "@getpaseo/protocol/error-utils";
|
||||
import type {
|
||||
BranchSuggestionsRequest,
|
||||
CheckoutRefreshRequest,
|
||||
CheckoutStatusRequest,
|
||||
SessionOutboundMessage,
|
||||
SubscribeCheckoutDiffRequest,
|
||||
UnsubscribeCheckoutDiffRequest,
|
||||
ValidateBranchRequest,
|
||||
} from "../../messages.js";
|
||||
import type {
|
||||
CheckoutDiffCompareInput,
|
||||
CheckoutDiffSnapshotPayload,
|
||||
} from "../../checkout-diff-manager.js";
|
||||
import { toCheckoutError } from "../../checkout-git-utils.js";
|
||||
import {
|
||||
buildCheckoutPrStatusPayloadFromSnapshot,
|
||||
buildCheckoutStatusPayloadFromSnapshot,
|
||||
} from "../../checkout/status-projection.js";
|
||||
import type {
|
||||
WorkspaceGitRuntimeSnapshot,
|
||||
WorkspaceGitService,
|
||||
} from "../../workspace-git-service.js";
|
||||
import { assertSafeGitRef } from "../../worktree-session.js";
|
||||
import type { GitHubService } from "../../../services/github-service.js";
|
||||
import { expandTilde } from "../../../utils/path.js";
|
||||
|
||||
export interface CheckoutSessionHost {
|
||||
emit(msg: SessionOutboundMessage): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The slice of CheckoutDiffManager that CheckoutSession needs: open a live diff
|
||||
* subscription, and nudge open subscriptions to recompute after a mutation. The
|
||||
* real CheckoutDiffManager satisfies this structurally; tests supply a fake.
|
||||
*/
|
||||
export interface CheckoutDiffSubscriber {
|
||||
subscribe(
|
||||
params: { cwd: string; compare: CheckoutDiffCompareInput },
|
||||
listener: (snapshot: CheckoutDiffSnapshotPayload) => void,
|
||||
): Promise<{ initial: CheckoutDiffSnapshotPayload; unsubscribe: () => void }>;
|
||||
scheduleRefreshForCwd(cwd: string): void;
|
||||
}
|
||||
|
||||
export interface CheckoutSessionOptions {
|
||||
host: CheckoutSessionHost;
|
||||
workspaceGitService: WorkspaceGitService;
|
||||
github: GitHubService;
|
||||
checkoutDiffManager: CheckoutDiffSubscriber;
|
||||
logger: pino.Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The read & live-stream side of a client's checkout view: status queries,
|
||||
* branch validation/suggestions, manual refresh, and the live git-diff and
|
||||
* checkout-status subscriptions.
|
||||
*
|
||||
* The command operations (switch/rename/commit/merge/pull/push/stash) and the
|
||||
* GitHub-PR operations still live on Session; they keep the diff in sync by
|
||||
* calling scheduleDiffRefresh(), and the workspace git observer streams branch
|
||||
* changes through emitStatusUpdate().
|
||||
*/
|
||||
export class CheckoutSession {
|
||||
private readonly host: CheckoutSessionHost;
|
||||
private readonly workspaceGitService: WorkspaceGitService;
|
||||
private readonly github: GitHubService;
|
||||
private readonly checkoutDiffManager: CheckoutDiffSubscriber;
|
||||
private readonly logger: pino.Logger;
|
||||
private readonly diffSubscriptions = new Map<string, () => void>();
|
||||
|
||||
constructor(options: CheckoutSessionOptions) {
|
||||
this.host = options.host;
|
||||
this.workspaceGitService = options.workspaceGitService;
|
||||
this.github = options.github;
|
||||
this.checkoutDiffManager = options.checkoutDiffManager;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
async handleStatusRequest(msg: CheckoutStatusRequest): Promise<void> {
|
||||
const { cwd, requestId } = msg;
|
||||
const resolvedCwd = expandTilde(cwd);
|
||||
|
||||
try {
|
||||
const snapshot = await this.workspaceGitService.getSnapshot(resolvedCwd);
|
||||
this.host.emit({
|
||||
type: "checkout_status_response",
|
||||
payload: buildCheckoutStatusPayloadFromSnapshot({
|
||||
cwd,
|
||||
requestId,
|
||||
snapshot,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
this.host.emit({
|
||||
type: "checkout_status_response",
|
||||
payload: {
|
||||
cwd,
|
||||
isGit: false,
|
||||
repoRoot: null,
|
||||
currentBranch: null,
|
||||
isDirty: null,
|
||||
baseRef: null,
|
||||
aheadBehind: null,
|
||||
aheadOfOrigin: null,
|
||||
behindOfOrigin: null,
|
||||
hasRemote: false,
|
||||
remoteUrl: null,
|
||||
isPaseoOwnedWorktree: false,
|
||||
error: toCheckoutError(error),
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleValidateBranchRequest(msg: ValidateBranchRequest): Promise<void> {
|
||||
const { cwd, branchName, requestId } = msg;
|
||||
|
||||
try {
|
||||
const resolvedCwd = expandTilde(cwd);
|
||||
assertSafeGitRef(branchName, "branch");
|
||||
|
||||
const resolution = await this.workspaceGitService.validateBranchRef(resolvedCwd, branchName);
|
||||
switch (resolution.kind) {
|
||||
case "local":
|
||||
this.host.emit({
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: true,
|
||||
resolvedRef: resolution.name,
|
||||
isRemote: false,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
case "remote-only":
|
||||
this.host.emit({
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: true,
|
||||
resolvedRef: resolution.remoteRef,
|
||||
isRemote: true,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
case "not-found":
|
||||
this.host.emit({
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: false,
|
||||
resolvedRef: null,
|
||||
isRemote: false,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
default: {
|
||||
const exhaustiveCheck: never = resolution;
|
||||
throw new Error(`Unhandled branch resolution: ${getErrorMessage(exhaustiveCheck)}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.host.emit({
|
||||
type: "validate_branch_response",
|
||||
payload: {
|
||||
exists: false,
|
||||
resolvedRef: null,
|
||||
isRemote: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleBranchSuggestionsRequest(msg: BranchSuggestionsRequest): Promise<void> {
|
||||
const { cwd, query, limit, requestId } = msg;
|
||||
|
||||
try {
|
||||
const resolvedCwd = expandTilde(cwd);
|
||||
const branchDetails = await this.workspaceGitService.suggestBranchesForCwd(resolvedCwd, {
|
||||
query,
|
||||
limit,
|
||||
});
|
||||
this.host.emit({
|
||||
type: "branch_suggestions_response",
|
||||
payload: {
|
||||
branches: branchDetails.map((branch) => branch.name),
|
||||
branchDetails,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.host.emit({
|
||||
type: "branch_suggestions_response",
|
||||
payload: {
|
||||
branches: [],
|
||||
branchDetails: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleSubscribeDiffRequest(msg: SubscribeCheckoutDiffRequest): Promise<void> {
|
||||
const cwd = expandTilde(msg.cwd);
|
||||
this.diffSubscriptions.get(msg.subscriptionId)?.();
|
||||
this.diffSubscriptions.delete(msg.subscriptionId);
|
||||
const subscription = await this.checkoutDiffManager.subscribe(
|
||||
{ cwd, compare: msg.compare },
|
||||
(snapshot) => {
|
||||
this.host.emit({
|
||||
type: "checkout_diff_update",
|
||||
payload: {
|
||||
subscriptionId: msg.subscriptionId,
|
||||
...snapshot,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
this.diffSubscriptions.set(msg.subscriptionId, subscription.unsubscribe);
|
||||
|
||||
this.host.emit({
|
||||
type: "subscribe_checkout_diff_response",
|
||||
payload: {
|
||||
subscriptionId: msg.subscriptionId,
|
||||
...subscription.initial,
|
||||
requestId: msg.requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleUnsubscribeDiffRequest(msg: UnsubscribeCheckoutDiffRequest): void {
|
||||
this.diffSubscriptions.get(msg.subscriptionId)?.();
|
||||
this.diffSubscriptions.delete(msg.subscriptionId);
|
||||
}
|
||||
|
||||
async handleRefreshRequest(msg: CheckoutRefreshRequest): Promise<void> {
|
||||
const { cwd, requestId } = msg;
|
||||
const resolvedCwd = expandTilde(cwd);
|
||||
|
||||
try {
|
||||
this.github.invalidate({ cwd: resolvedCwd });
|
||||
await this.workspaceGitService.getSnapshot(resolvedCwd, {
|
||||
force: true,
|
||||
includeGitHub: true,
|
||||
reason: "manual-refresh",
|
||||
});
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(resolvedCwd);
|
||||
this.host.emit({
|
||||
type: "checkout.refresh.response",
|
||||
payload: {
|
||||
cwd,
|
||||
success: true,
|
||||
error: null,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.host.emit({
|
||||
type: "checkout.refresh.response",
|
||||
payload: {
|
||||
cwd,
|
||||
success: false,
|
||||
error: toCheckoutError(error),
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
emitStatusUpdate(cwd: string, snapshot: WorkspaceGitRuntimeSnapshot): void {
|
||||
try {
|
||||
const requestId = `subscription:${cwd}`;
|
||||
this.host.emit({
|
||||
type: "checkout_status_update",
|
||||
payload: {
|
||||
...buildCheckoutStatusPayloadFromSnapshot({
|
||||
cwd,
|
||||
requestId,
|
||||
snapshot,
|
||||
}),
|
||||
prStatus: buildCheckoutPrStatusPayloadFromSnapshot({
|
||||
cwd,
|
||||
requestId,
|
||||
snapshot,
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn({ err: error, cwd }, "Failed to emit workspace checkout status update");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the live diff subscriptions that the working tree at `cwd` changed.
|
||||
* Called by the command handlers that still live on Session after they mutate
|
||||
* the repository.
|
||||
*/
|
||||
scheduleDiffRefresh(cwd: string): void {
|
||||
this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
for (const unsubscribe of this.diffSubscriptions.values()) {
|
||||
unsubscribe();
|
||||
}
|
||||
this.diffSubscriptions.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user