Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
217f044
Persist worktree terminal launch context
juliusmarminge Mar 29, 2026
2535cf1
Persist setup terminals for PR worktree bootstrap
juliusmarminge Mar 29, 2026
6a43f7b
Merge origin/main into t3code/persist-script-terminals
juliusmarminge Mar 30, 2026
033be30
Fix ProjectSetupScriptRunner test after main merge
juliusmarminge Mar 30, 2026
e446571
Merge origin/main into t3code/persist-script-terminals
juliusmarminge Mar 30, 2026
e3372a4
Fix: move preparingWorktree=false after dispatchCommand so indicator …
cursoragent Mar 31, 2026
5bcc23c
Merge origin/main
juliusmarminge Mar 31, 2026
7f5ee34
Merge branch 'main' into t3code/persist-script-terminals
juliusmarminge Apr 1, 2026
d3b8b2e
Persist script terminal worktree paths
juliusmarminge Apr 1, 2026
9d56921
Fix wsServer bootstrap effect chaining
juliusmarminge Apr 1, 2026
9883959
Merge origin/main into t3code/persist-script-terminals
juliusmarminge Apr 1, 2026
06e7c4c
Merge origin/main
juliusmarminge Apr 3, 2026
448eec7
Merge origin/main
juliusmarminge Apr 3, 2026
5d80605
rev
juliusmarminge Apr 3, 2026
b42a75d
kewl
juliusmarminge Apr 3, 2026
dcaf3aa
kewl
juliusmarminge Apr 3, 2026
3cacb37
Persist scripted terminal state from server events
juliusmarminge Apr 3, 2026
e1b607a
Persist terminal event buffering
juliusmarminge Apr 3, 2026
d726c1f
Centralize project script helpers in shared package
juliusmarminge Apr 3, 2026
a5a9521
Separate setup script launch and activity recording failures
juliusmarminge Apr 3, 2026
49bf283
Persist worktree metadata for terminal sessions
juliusmarminge Apr 3, 2026
1b1f688
Preserve worktree metadata on terminal reopen
juliusmarminge Apr 3, 2026
2247f18
Include worktreePath in browser terminal RPCs
juliusmarminge Apr 3, 2026
797f437
Honor cleared terminal launch context worktree path
juliusmarminge Apr 3, 2026
58e0ed7
Reset terminal store between ChatView parity tests
juliusmarminge Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function initRepoWithCommit(
});
}

function buildLargeText(lineCount = 20_000): string {
function buildLargeText(lineCount = 5_000): string {
return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`)
.join("\n")
.concat("\n");
Expand Down Expand Up @@ -115,7 +115,7 @@ it.layer(TestLayer)("CheckpointStoreLive", (it) => {

expect(diff).toContain("diff --git");
expect(diff).not.toContain("[truncated]");
expect(diff).toContain("+line 19999");
expect(diff).toContain("+line 04999");
}),
);
});
Expand Down
130 changes: 128 additions & 2 deletions apps/server/src/git/Layers/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
import { expect } from "vitest";
import type { GitActionProgressEvent, ModelSelection } from "@t3tools/contracts";
import type {
GitActionProgressEvent,
GitPreparePullRequestThreadInput,
ModelSelection,
ThreadId,
} from "@t3tools/contracts";

import { GitCommandError, GitHubCliError, TextGenerationError } from "@t3tools/contracts";
import { type GitManagerShape } from "../Services/GitManager.ts";
Expand All @@ -21,6 +26,11 @@ import { GitCore } from "../Services/GitCore.ts";
import { makeGitManager } from "./GitManager.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import {
ProjectSetupScriptRunner,
type ProjectSetupScriptRunnerInput,
type ProjectSetupScriptRunnerShape,
} from "../../project/Services/ProjectSetupScriptRunner.ts";

interface FakeGhScenario {
prListSequence?: string[];
Expand Down Expand Up @@ -593,14 +603,15 @@ function resolvePullRequest(manager: GitManagerShape, input: { cwd: string; refe

function preparePullRequestThread(
manager: GitManagerShape,
input: { cwd: string; reference: string; mode: "local" | "worktree" },
input: GitPreparePullRequestThreadInput,
) {
return manager.preparePullRequestThread(input);
}

function makeManager(input?: {
ghScenario?: FakeGhScenario;
textGeneration?: Partial<FakeGitTextGeneration>;
setupScriptRunner?: ProjectSetupScriptRunnerShape;
}) {
const { service: gitHubCli, ghCalls } = createGitHubCliWithFakeGh(input?.ghScenario);
const textGeneration = createTextGeneration(input?.textGeneration);
Expand All @@ -618,6 +629,12 @@ function makeManager(input?: {
const managerLayer = Layer.mergeAll(
Layer.succeed(GitHubCli, gitHubCli),
Layer.succeed(TextGeneration, textGeneration),
Layer.succeed(
ProjectSetupScriptRunner,
input?.setupScriptRunner ?? {
runForThread: () => Effect.succeed({ status: "no-script" as const }),
},
),
gitCoreLayer,
serverSettingsLayer,
).pipe(Layer.provideMerge(NodeServices.layer));
Expand All @@ -628,6 +645,8 @@ function makeManager(input?: {
);
}

const asThreadId = (threadId: string) => threadId as ThreadId;

const GitManagerTestLayer = GitCoreLive.pipe(
Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" })),
Layer.provideMerge(NodeServices.layer),
Expand Down Expand Up @@ -2176,6 +2195,59 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("launches setup only when creating a new PR worktree", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
yield* runGit(repoDir, ["checkout", "-b", "feature/pr-worktree-setup"]);
fs.writeFileSync(path.join(repoDir, "setup.txt"), "setup\n");
yield* runGit(repoDir, ["add", "setup.txt"]);
yield* runGit(repoDir, ["commit", "-m", "PR worktree setup branch"]);
yield* runGit(repoDir, ["push", "-u", "origin", "feature/pr-worktree-setup"]);
yield* runGit(repoDir, ["push", "origin", "HEAD:refs/pull/177/head"]);
yield* runGit(repoDir, ["checkout", "main"]);

const setupCalls: ProjectSetupScriptRunnerInput[] = [];
const { manager } = yield* makeManager({
ghScenario: {
pullRequest: {
number: 177,
title: "Worktree setup PR",
url: "https://github.com/pingdotgg/codething-mvp/pull/177",
baseRefName: "main",
headRefName: "feature/pr-worktree-setup",
state: "open",
},
},
setupScriptRunner: {
runForThread: (setupInput) =>
Effect.sync(() => {
setupCalls.push(setupInput);
return { status: "no-script" as const };
}),
},
});

const result = yield* preparePullRequestThread(manager, {
cwd: repoDir,
reference: "177",
mode: "worktree",
threadId: asThreadId("thread-pr-setup"),
});

expect(result.worktreePath).not.toBeNull();
expect(setupCalls).toHaveLength(1);
expect(setupCalls[0]).toEqual({
threadId: "thread-pr-setup",
projectCwd: repoDir,
worktreePath: result.worktreePath as string,
});
}),
);

it.effect("preserves fork upstream tracking when preparing a worktree PR thread", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down Expand Up @@ -2360,6 +2432,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
const worktreePath = path.join(repoDir, "..", `pr-existing-${Date.now()}`);
yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-existing-worktree"]);

const setupCalls: ProjectSetupScriptRunnerInput[] = [];
const { manager } = yield* makeManager({
ghScenario: {
pullRequest: {
Expand All @@ -2371,18 +2444,27 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
state: "open",
},
},
setupScriptRunner: {
runForThread: (setupInput) =>
Effect.sync(() => {
setupCalls.push(setupInput);
return { status: "no-script" as const };
}),
},
});

const result = yield* preparePullRequestThread(manager, {
cwd: repoDir,
reference: "78",
mode: "worktree",
threadId: asThreadId("thread-pr-existing-worktree"),
});

expect(result.worktreePath && fs.realpathSync.native(result.worktreePath)).toBe(
fs.realpathSync.native(worktreePath),
);
expect(result.branch).toBe("feature/pr-existing-worktree");
expect(setupCalls).toHaveLength(0);
}),
);

Expand Down Expand Up @@ -2562,6 +2644,50 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("does not fail PR worktree prep when setup terminal startup fails", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
yield* runGit(repoDir, ["checkout", "-b", "feature/pr-setup-failure"]);
fs.writeFileSync(path.join(repoDir, "setup-failure.txt"), "setup failure\n");
yield* runGit(repoDir, ["add", "setup-failure.txt"]);
yield* runGit(repoDir, ["commit", "-m", "PR setup failure branch"]);
yield* runGit(repoDir, ["push", "-u", "origin", "feature/pr-setup-failure"]);
yield* runGit(repoDir, ["push", "origin", "HEAD:refs/pull/184/head"]);
yield* runGit(repoDir, ["checkout", "main"]);

const { manager } = yield* makeManager({
ghScenario: {
pullRequest: {
number: 184,
title: "Setup failure PR",
url: "https://github.com/pingdotgg/codething-mvp/pull/184",
baseRefName: "main",
headRefName: "feature/pr-setup-failure",
state: "open",
},
},
setupScriptRunner: {
runForThread: () => Effect.fail(new Error("terminal start failed")),
},
});

const result = yield* preparePullRequestThread(manager, {
cwd: repoDir,
reference: "184",
mode: "worktree",
threadId: asThreadId("thread-pr-setup-failure"),
});

expect(result.branch).toBe("feature/pr-setup-failure");
expect(result.worktreePath).not.toBeNull();
expect(fs.existsSync(result.worktreePath as string)).toBe(true);
}),
);

it.effect("rejects worktree prep when the PR head branch is checked out in the main repo", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
21 changes: 21 additions & 0 deletions apps/server/src/git/Layers/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { GitCore, GitStatusDetails } from "../Services/GitCore.ts";
import { GitHubCli, type GitHubPullRequestSummary } from "../Services/GitHubCli.ts";
import { TextGeneration } from "../Services/TextGeneration.ts";
import { ProjectSetupScriptRunner } from "../../project/Services/ProjectSetupScriptRunner.ts";
import { extractBranchNameFromRemoteRef } from "../remoteRefs.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import type { GitManagerServiceError } from "@t3tools/contracts";
Expand Down Expand Up @@ -552,6 +553,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
const gitCore = yield* GitCore;
const gitHubCli = yield* GitHubCli;
const textGeneration = yield* TextGeneration;
const projectSetupScriptRunner = yield* ProjectSetupScriptRunner;
const serverSettingsService = yield* ServerSettingsService;

const createProgressEmitter = (
Expand Down Expand Up @@ -1329,6 +1331,24 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
const preparePullRequestThread: GitManagerShape["preparePullRequestThread"] = Effect.fn(
"preparePullRequestThread",
)(function* (input) {
const maybeRunSetupScript = (worktreePath: string) => {
if (!input.threadId) {
return Effect.void;
}
return projectSetupScriptRunner
.runForThread({
threadId: input.threadId,
projectCwd: input.cwd,
worktreePath,
})
.pipe(
Effect.catch((error) =>
Effect.logWarning(
`GitManager.preparePullRequestThread: failed to launch worktree setup script for thread ${input.threadId} in ${worktreePath}: ${error.message}`,
).pipe(Effect.asVoid),
),
);
};
return yield* Effect.gen(function* () {
const normalizedReference = normalizePullRequestReference(input.reference);
const rootWorktreePath = canonicalizeExistingPath(input.cwd);
Expand Down Expand Up @@ -1461,6 +1481,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
path: null,
});
yield* ensureExistingWorktreeUpstream(worktree.worktree.path);
yield* maybeRunSetupScript(worktree.worktree.path);

return {
pullRequest,
Expand Down
Loading
Loading