authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-13 14:58:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-13 14:58:03-07:00
log9dcea68bce25e1c30be7a6439bfe607b236740a0
tree1538eda948b6476fcd5952b6e8022e4588e4990c
parent5f7ec73a22b28e2775987b03a744f75e5e151152
signature Signed by PGP key B5690EEEBB952194

Refresh git status after branch rename and worktree setup (#2005)


4 files changed, 70 insertions(+), 1 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+34
...@@ -27,6 +27,10 @@ import {...@@ -27,6 +27,10 @@ import {
27 type ProviderServiceShape,27 type ProviderServiceShape,
28} from "../../provider/Services/ProviderService.ts";28} from "../../provider/Services/ProviderService.ts";
29import { GitCore, type GitCoreShape } from "../../git/Services/GitCore.ts";29import { GitCore, type GitCoreShape } from "../../git/Services/GitCore.ts";
30import {
31 GitStatusBroadcaster,
32 type GitStatusBroadcasterShape,
33} from "../../git/Services/GitStatusBroadcaster.ts";
30import { TextGeneration, type TextGenerationShape } from "../../git/Services/TextGeneration.ts";34import { TextGeneration, type TextGenerationShape } from "../../git/Services/TextGeneration.ts";
31import { RepositoryIdentityResolverLive } from "../../project/Layers/RepositoryIdentityResolver.ts";35import { RepositoryIdentityResolverLive } from "../../project/Layers/RepositoryIdentityResolver.ts";
32import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";36import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
...@@ -177,6 +181,24 @@ describe("ProviderCommandReactor", () => {...@@ -177,6 +181,24 @@ describe("ProviderCommandReactor", () => {
177 : "renamed-branch",181 : "renamed-branch",
178 }),182 }),
179 );183 );
184 const refreshStatus = vi.fn((_: string) =>
185 Effect.succeed({
186 isRepo: true,
187 hasOriginRemote: true,
188 isDefaultBranch: false,
189 branch: "renamed-branch",
190 hasWorkingTreeChanges: false,
191 workingTree: {
192 files: [],
193 insertions: 0,
194 deletions: 0,
195 },
196 hasUpstream: true,
197 aheadCount: 0,
198 behindCount: 0,
199 pr: null,
200 }),
201 );
180 const generateBranchName = vi.fn<TextGenerationShape["generateBranchName"]>((_) =>202 const generateBranchName = vi.fn<TextGenerationShape["generateBranchName"]>((_) =>
181 Effect.fail(203 Effect.fail(
182 new TextGenerationError({204 new TextGenerationError({
...@@ -225,6 +247,15 @@ describe("ProviderCommandReactor", () => {...@@ -225,6 +247,15 @@ describe("ProviderCommandReactor", () => {
225 Layer.provideMerge(orchestrationLayer),247 Layer.provideMerge(orchestrationLayer),
226 Layer.provideMerge(Layer.succeed(ProviderService, service)),248 Layer.provideMerge(Layer.succeed(ProviderService, service)),
227 Layer.provideMerge(Layer.succeed(GitCore, { renameBranch } as unknown as GitCoreShape)),249 Layer.provideMerge(Layer.succeed(GitCore, { renameBranch } as unknown as GitCoreShape)),
250 Layer.provideMerge(
251 Layer.succeed(GitStatusBroadcaster, {
252 getStatus: () => Effect.die("getStatus should not be called in this test"),
253 refreshLocalStatus: () =>
254 Effect.die("refreshLocalStatus should not be called in this test"),
255 refreshStatus,
256 streamStatus: () => Stream.die("streamStatus should not be called in this test"),
257 } satisfies GitStatusBroadcasterShape),
258 ),
228 Layer.provideMerge(259 Layer.provideMerge(
229 Layer.mock(TextGeneration, {260 Layer.mock(TextGeneration, {
230 generateBranchName,261 generateBranchName,
...@@ -279,6 +310,7 @@ describe("ProviderCommandReactor", () => {...@@ -279,6 +310,7 @@ describe("ProviderCommandReactor", () => {
279 respondToUserInput,310 respondToUserInput,
280 stopSession,311 stopSession,
281 renameBranch,312 renameBranch,
313 refreshStatus,
282 generateBranchName,314 generateBranchName,
283 generateThreadTitle,315 generateThreadTitle,
284 stateDir,316 stateDir,
...@@ -513,9 +545,11 @@ describe("ProviderCommandReactor", () => {...@@ -513,9 +545,11 @@ describe("ProviderCommandReactor", () => {
513 );545 );
514546
515 await waitFor(() => harness.generateBranchName.mock.calls.length === 1);547 await waitFor(() => harness.generateBranchName.mock.calls.length === 1);
548 await waitFor(() => harness.refreshStatus.mock.calls.length === 1);
516 expect(harness.generateBranchName.mock.calls[0]?.[0]).toMatchObject({549 expect(harness.generateBranchName.mock.calls[0]?.[0]).toMatchObject({
517 message: "Add a safer reconnect backoff.",550 message: "Add a safer reconnect backoff.",
518 });551 });
552 expect(harness.refreshStatus.mock.calls[0]?.[0]).toBe("/tmp/provider-project-worktree");
519 });553 });
520554
521 it("forwards codex model options through session start and turn send", async () => {555 it("forwards codex model options through session start and turn send", async () => {
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+3
...@@ -17,6 +17,7 @@ import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";...@@ -17,6 +17,7 @@ import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
1717
18import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";18import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
19import { GitCore } from "../../git/Services/GitCore.ts";19import { GitCore } from "../../git/Services/GitCore.ts";
20import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts";
20import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts";21import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts";
21import { ProviderAdapterRequestError, ProviderServiceError } from "../../provider/Errors.ts";22import { ProviderAdapterRequestError, ProviderServiceError } from "../../provider/Errors.ts";
22import { TextGeneration } from "../../git/Services/TextGeneration.ts";23import { TextGeneration } from "../../git/Services/TextGeneration.ts";
...@@ -145,6 +146,7 @@ const make = Effect.gen(function* () {...@@ -145,6 +146,7 @@ const make = Effect.gen(function* () {
145 const orchestrationEngine = yield* OrchestrationEngineService;146 const orchestrationEngine = yield* OrchestrationEngineService;
146 const providerService = yield* ProviderService;147 const providerService = yield* ProviderService;
147 const git = yield* GitCore;148 const git = yield* GitCore;
149 const gitStatusBroadcaster = yield* GitStatusBroadcaster;
148 const textGeneration = yield* TextGeneration;150 const textGeneration = yield* TextGeneration;
149 const serverSettingsService = yield* ServerSettingsService;151 const serverSettingsService = yield* ServerSettingsService;
150 const handledTurnStartKeys = yield* Cache.make<string, true>({152 const handledTurnStartKeys = yield* Cache.make<string, true>({
...@@ -447,6 +449,7 @@ const make = Effect.gen(function* () {...@@ -447,6 +449,7 @@ const make = Effect.gen(function* () {
447 branch: renamed.branch,449 branch: renamed.branch,
448 worktreePath: cwd,450 worktreePath: cwd,
449 });451 });
452 yield* gitStatusBroadcaster.refreshStatus(cwd).pipe(Effect.ignoreCause({ log: true }));
450 }).pipe(453 }).pipe(
451 Effect.catchCause((cause) =>454 Effect.catchCause((cause) =>
452 Effect.logWarning("provider command reactor failed to generate or rename worktree branch", {455 Effect.logWarning("provider command reactor failed to generate or rename worktree branch", {
apps/server/src/server.test.ts+32-1
...@@ -58,6 +58,10 @@ import {...@@ -58,6 +58,10 @@ import {
58import { GitCore, type GitCoreShape } from "./git/Services/GitCore.ts";58import { GitCore, type GitCoreShape } from "./git/Services/GitCore.ts";
59import { GitManager, type GitManagerShape } from "./git/Services/GitManager.ts";59import { GitManager, type GitManagerShape } from "./git/Services/GitManager.ts";
60import { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster.ts";60import { GitStatusBroadcasterLive } from "./git/Layers/GitStatusBroadcaster.ts";
61import {
62 GitStatusBroadcaster,
63 type GitStatusBroadcasterShape,
64} from "./git/Services/GitStatusBroadcaster.ts";
61import { Keybindings, type KeybindingsShape } from "./keybindings.ts";65import { Keybindings, type KeybindingsShape } from "./keybindings.ts";
62import { Open, type OpenShape } from "./open.ts";66import { Open, type OpenShape } from "./open.ts";
63import {67import {
...@@ -293,6 +297,7 @@ const buildAppUnderTest = (options?: {...@@ -293,6 +297,7 @@ const buildAppUnderTest = (options?: {
293 open?: Partial<OpenShape>;297 open?: Partial<OpenShape>;
294 gitCore?: Partial<GitCoreShape>;298 gitCore?: Partial<GitCoreShape>;
295 gitManager?: Partial<GitManagerShape>;299 gitManager?: Partial<GitManagerShape>;
300 gitStatusBroadcaster?: Partial<GitStatusBroadcasterShape>;
296 projectSetupScriptRunner?: Partial<ProjectSetupScriptRunnerShape>;301 projectSetupScriptRunner?: Partial<ProjectSetupScriptRunnerShape>;
297 terminalManager?: Partial<TerminalManagerShape>;302 terminalManager?: Partial<TerminalManagerShape>;
298 orchestrationEngine?: Partial<OrchestrationEngineShape>;303 orchestrationEngine?: Partial<OrchestrationEngineShape>;
...@@ -341,7 +346,11 @@ const buildAppUnderTest = (options?: {...@@ -341,7 +346,11 @@ const buildAppUnderTest = (options?: {
341 const gitManagerLayer = Layer.mock(GitManager)({346 const gitManagerLayer = Layer.mock(GitManager)({
342 ...options?.layers?.gitManager,347 ...options?.layers?.gitManager,
343 });348 });
344 const gitStatusBroadcasterLayer = GitStatusBroadcasterLive.pipe(Layer.provide(gitManagerLayer));349 const gitStatusBroadcasterLayer = options?.layers?.gitStatusBroadcaster
350 ? Layer.mock(GitStatusBroadcaster)({
351 ...options.layers.gitStatusBroadcaster,
352 })
353 : GitStatusBroadcasterLive.pipe(Layer.provide(gitManagerLayer));
345354
346 const servedRoutesLayer = HttpRouter.serve(makeRoutesLayer, {355 const servedRoutesLayer = HttpRouter.serve(makeRoutesLayer, {
347 disableListenLog: true,356 disableListenLog: true,
...@@ -2913,6 +2922,24 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -2913,6 +2922,24 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
2913 () =>2922 () =>
2914 Effect.gen(function* () {2923 Effect.gen(function* () {
2915 const dispatchedCommands: Array<OrchestrationCommand> = [];2924 const dispatchedCommands: Array<OrchestrationCommand> = [];
2925 const refreshStatus = vi.fn((_: string) =>
2926 Effect.succeed({
2927 isRepo: true,
2928 hasOriginRemote: true,
2929 isDefaultBranch: false,
2930 branch: "t3code/bootstrap-branch",
2931 hasWorkingTreeChanges: false,
2932 workingTree: {
2933 files: [],
2934 insertions: 0,
2935 deletions: 0,
2936 },
2937 hasUpstream: true,
2938 aheadCount: 0,
2939 behindCount: 0,
2940 pr: null,
2941 }),
2942 );
2916 const createWorktree = vi.fn((_: Parameters<GitCoreShape["createWorktree"]>[0]) =>2943 const createWorktree = vi.fn((_: Parameters<GitCoreShape["createWorktree"]>[0]) =>
2917 Effect.succeed({2944 Effect.succeed({
2918 worktree: {2945 worktree: {
...@@ -2937,6 +2964,9 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -2937,6 +2964,9 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
2937 gitCore: {2964 gitCore: {
2938 createWorktree,2965 createWorktree,
2939 },2966 },
2967 gitStatusBroadcaster: {
2968 refreshStatus,
2969 },
2940 orchestrationEngine: {2970 orchestrationEngine: {
2941 dispatch: (command) =>2971 dispatch: (command) =>
2942 Effect.sync(() => {2972 Effect.sync(() => {
...@@ -3014,6 +3044,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3014,6 +3044,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3014 projectCwd: "/tmp/project",3044 projectCwd: "/tmp/project",
3015 worktreePath: "/tmp/bootstrap-worktree",3045 worktreePath: "/tmp/bootstrap-worktree",
3016 });3046 });
3047 assert.deepEqual(refreshStatus.mock.calls[0]?.[0], "/tmp/bootstrap-worktree");
30173048
3018 const setupActivities = dispatchedCommands.filter(3049 const setupActivities = dispatchedCommands.filter(
3019 (command): command is Extract<OrchestrationCommand, { type: "thread.activity.append" }> =>3050 (command): command is Extract<OrchestrationCommand, { type: "thread.activity.append" }> =>
apps/server/src/ws.ts+1
...@@ -464,6 +464,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>...@@ -464,6 +464,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
464 branch: worktree.worktree.branch,464 branch: worktree.worktree.branch,
465 worktreePath: targetWorktreePath,465 worktreePath: targetWorktreePath,
466 });466 });
467 yield* refreshGitStatus(targetWorktreePath);
467 }468 }
468469
469 yield* runSetupProgram();470 yield* runSetupProgram();