Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e7ca8cb
feat(git): friendly checkout error messages with stash & switch recovery
Marve10s Apr 6, 2026
3699a1b
feat(git): wire stash recovery into EnvironmentApi and fix optimistic UI
Marve10s Apr 9, 2026
5fa9a95
Fix client settings type coverage for thread preview count
Marve10s Apr 10, 2026
a4d5318
Match client settings fixture to desktop contract
Marve10s Apr 10, 2026
766b49a
fix(git): add missing refreshGitStatus to stashDrop and handle persis…
Marve10s Apr 10, 2026
7025fb7
Remove unnecessary git checkout scope requirement
Marve10s Apr 16, 2026
3c844c6
Skip stash pop when stash push creates no entry
Marve10s Apr 16, 2026
bd4148c
fix(git): preserve dirty worktree metadata
Marve10s Apr 17, 2026
fb38720
Merge upstream/main into feat/checkout-dirty-worktree-error-handling
Marve10s Apr 17, 2026
91709b3
fix(git): handle ignored checkout conflicts
Marve10s Apr 18, 2026
301e9cc
fix(git): address review comments on checkout error handling
Marve10s Apr 20, 2026
146b672
style: apply oxfmt to checkout error handling files
Marve10s Apr 20, 2026
c240917
Merge remote-tracking branch 'upstream/main' into feat/checkout-dirty…
Marve10s Apr 24, 2026
26c156d
Add stash discard recovery modal
Marve10s Apr 24, 2026
9345413
fix(web): guard stash discard branch action state
Marve10s Apr 29, 2026
97f198a
fix(web): report rejected nested branch actions
Marve10s Apr 29, 2026
3b76843
fix(server): refresh git status after stash checkout failure
Marve10s Apr 29, 2026
144ec1a
Merge upstream/main into feat/checkout-dirty-worktree-error-handling
Marve10s May 11, 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
21 changes: 21 additions & 0 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
type GitPullRequestRefInput,
type VcsPullResult,
type VcsRemoveWorktreeInput,
type VcsStashAndSwitchInput,
type VcsStashDropInput,
type VcsStashInfoInput,
type VcsStashInfoResult,
type GitResolvePullRequestResult,
type GitRunStackedActionInput,
type GitRunStackedActionResult,
Expand Down Expand Up @@ -67,6 +71,11 @@ export interface GitWorkflowServiceShape {
readonly switchRef: (
input: VcsSwitchRefInput,
) => Effect.Effect<VcsSwitchRefResult, GitCommandError>;
readonly stashAndSwitch: (input: VcsStashAndSwitchInput) => Effect.Effect<void, GitCommandError>;
readonly stashDrop: (input: VcsStashDropInput) => Effect.Effect<void, GitCommandError>;
readonly stashInfo: (
input: VcsStashInfoInput,
) => Effect.Effect<VcsStashInfoResult, GitCommandError>;
readonly renameBranch: (input: {
readonly cwd: string;
readonly oldBranch: string;
Expand Down Expand Up @@ -306,6 +315,18 @@ export const make = Effect.fn("makeGitWorkflowService")(function* () {
ensureGitCommand("GitWorkflowService.switchRef", input.cwd).pipe(
Effect.andThen(Effect.scoped(git.switchRef(input))),
),
stashAndSwitch: (input) =>
ensureGitCommand("GitWorkflowService.stashAndSwitch", input.cwd).pipe(
Effect.andThen(Effect.scoped(git.stashAndSwitch(input))),
),
stashDrop: (input) =>
ensureGitCommand("GitWorkflowService.stashDrop", input.cwd).pipe(
Effect.andThen(git.stashDrop(input)),
),
stashInfo: (input) =>
ensureGitCommand("GitWorkflowService.stashInfo", input.cwd).pipe(
Effect.andThen(git.stashInfo(input)),
),
renameBranch: (input) =>
ensureGit("GitWorkflowService.renameBranch", input.cwd).pipe(
Effect.andThen(git.renameBranch(input)),
Expand Down
56 changes: 56 additions & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2897,6 +2897,62 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("routes websocket rpc vcs.stashAndSwitch errors after refreshing vcs status", () =>
Effect.gen(function* () {
const gitError = new GitCommandError({
operation: "stashAndSwitch",
command: "git stash pop",
cwd: "/tmp/repo",
detail: "stash pop conflict",
});
const refreshed = yield* Deferred.make<void>();
let refreshCalls = 0;
yield* buildAppUnderTest({
layers: {
gitVcsDriver: {
stashAndSwitch: () => Effect.fail(gitError),
},
vcsStatusBroadcaster: {
refreshStatus: (cwd: string) =>
Effect.sync(() => {
assert.equal(cwd, "/tmp/repo");
refreshCalls += 1;
}).pipe(
Effect.andThen(Deferred.succeed(refreshed, undefined)),
Effect.as({
isRepo: true,
hasPrimaryRemote: true,
isDefaultRef: false,
refName: "feature/demo",
hasWorkingTreeChanges: true,
workingTree: { files: [], insertions: 0, deletions: 0 },
hasUpstream: true,
aheadCount: 0,
behindCount: 0,
aheadOfDefaultCount: 0,
pr: null,
}),
),
},
},
});

const wsUrl = yield* getWsServerUrl("/ws");
const result = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) =>
client[WS_METHODS.vcsStashAndSwitch]({
cwd: "/tmp/repo",
refName: "feature/demo",
}).pipe(Effect.result),
),
);

assertFailure(result, gitError);
yield* Deferred.await(refreshed).pipe(Effect.timeout(Duration.seconds(1)));
assert.equal(refreshCalls, 1);
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("routes websocket rpc git.runStackedAction errors after refreshing git status", () =>
Effect.gen(function* () {
const gitError = new GitCommandError({
Expand Down
9 changes: 9 additions & 0 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
type VcsListRefsResult,
type VcsPullResult,
type VcsRemoveWorktreeInput,
type VcsStashAndSwitchInput,
type VcsStashDropInput,
type VcsStashInfoInput,
type VcsStashInfoResult,
type VcsStatusInput,
type VcsStatusResult,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -212,6 +216,11 @@ export interface GitVcsDriverShape {
readonly switchRef: (
input: VcsSwitchRefInput,
) => Effect.Effect<VcsSwitchRefResult, GitCommandError>;
readonly stashAndSwitch: (input: VcsStashAndSwitchInput) => Effect.Effect<void, GitCommandError>;
readonly stashDrop: (input: VcsStashDropInput) => Effect.Effect<void, GitCommandError>;
readonly stashInfo: (
input: VcsStashInfoInput,
) => Effect.Effect<VcsStashInfoResult, GitCommandError>;
readonly initRepo: (input: VcsInitInput) => Effect.Effect<void, GitCommandError>;
readonly listLocalBranchNames: (cwd: string) => Effect.Effect<string[], GitCommandError>;
}
Expand Down
Loading
Loading