Skip to content

Commit 200753c

Browse files
authored
feat(git): add local branch workflows to new sessions (#376)
## Summary - add searchable branch selection and quick branch creation to the new-session flow - make branch choice independent from Local / Worktree execution mode - safely preflight Local branch switches, show overlapping changed files and line statistics, and support commit-and-continue - keep Worktree mode isolated from the original checkout and add wire, SDK, engine, mock, cache invalidation, and i18n support - keep the branch search icon and text in separate `InputGroup` layout slots so they cannot overlap Linear: [CODE-527](https://linear.app/arcbox/issue/CODE-527/featgitui-add-local-branch-workflows-to-new-sessions) ## Verification - `pnpm check:ci` - `pnpm test` — 300 test files / 2408 tests passed; 1 file / 1 test skipped - pre-commit hooks - `git diff --check` - observed the complete branch search, creation, Local / Worktree, conflict, and commit-and-continue flow in the mock webview - visually rechecked the search field after merging current `master`; icon and placeholder have a measured 4px gap with no clipping or overlap ## Checklist - [x] `pnpm check:ci` and `pnpm test` both pass (plus `cargo fmt` / `clippy` / `test` for Rust changes) - [x] I ran the affected surface and observed the change working - [x] If a wire message changed: `WIRE_PROTOCOL_VERSION` is bumped - [x] New code and assets are my own work, or their origin and license compatibility are noted above - [x] Docs and comments are updated where behavior changed
2 parents 870e1c5 + e575ba7 commit 200753c

36 files changed

Lines changed: 1814 additions & 116 deletions

packages/client/core/src/client.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
EffortLevel,
1919
FileSuggestion,
2020
GitBranchList,
21+
GitBranchSwitchCheck,
2122
GitDiff,
2223
GitDiffMode,
2324
GitPullRequestStatus,
@@ -509,6 +510,9 @@ export class LinkCodeClient {
509510
case 'git.branch.list.result':
510511
this.pending.resolve('gitBranchList', p.replyTo, p.branchList);
511512
break;
513+
case 'git.branch.switch.check.result':
514+
this.pending.resolve('gitBranchSwitchCheck', p.replyTo, p.check);
515+
break;
512516
case 'git.pr_status.get.result':
513517
this.pending.resolve('gitPrStatus', p.replyTo, p.prStatus);
514518
break;
@@ -1132,6 +1136,18 @@ export class LinkCodeClient {
11321136
return this.control.listGitBranches(cwd);
11331137
}
11341138

1139+
checkGitBranchSwitch(cwd: string, branch: string): Promise<GitBranchSwitchCheck> {
1140+
return this.control.checkGitBranchSwitch(cwd, branch);
1141+
}
1142+
1143+
createGitBranch(cwd: string, branch: string): Promise<RequestAck> {
1144+
return this.control.createGitBranch(cwd, branch);
1145+
}
1146+
1147+
commitGitChanges(cwd: string, message: string): Promise<RequestAck> {
1148+
return this.control.commitGitChanges(cwd, message);
1149+
}
1150+
11351151
getGitPullRequestStatus(cwd: string): Promise<GitPullRequestStatus> {
11361152
return this.control.getGitPullRequestStatus(cwd);
11371153
}

packages/client/core/src/client/control-channel.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
EffortLevel,
2020
FileSuggestion,
2121
GitBranchList,
22+
GitBranchSwitchCheck,
2223
GitDiff,
2324
GitDiffMode,
2425
GitPullRequestStatus,
@@ -598,6 +599,33 @@ export class ControlChannel {
598599
}));
599600
}
600601

602+
checkGitBranchSwitch(cwd: string, branch: string): Promise<GitBranchSwitchCheck> {
603+
return this.sendCorrelated('gitBranchSwitchCheck', (clientReqId) => ({
604+
kind: 'git.branch.switch.check',
605+
clientReqId,
606+
cwd,
607+
branch,
608+
}));
609+
}
610+
611+
createGitBranch(cwd: string, branch: string): Promise<RequestAck> {
612+
return this.sendCorrelated('ack', (clientReqId) => ({
613+
kind: 'git.branch.create',
614+
clientReqId,
615+
cwd,
616+
branch,
617+
}));
618+
}
619+
620+
commitGitChanges(cwd: string, message: string): Promise<RequestAck> {
621+
return this.sendCorrelated('ack', (clientReqId) => ({
622+
kind: 'git.commit',
623+
clientReqId,
624+
cwd,
625+
message,
626+
}));
627+
}
628+
601629
/** Hosting-provider PR state for a directory's current branch. */
602630
getGitPullRequestStatus(cwd: string): Promise<GitPullRequestStatus> {
603631
return this.sendCorrelated('gitPrStatus', (clientReqId) => ({

packages/client/core/src/client/pending-registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
CustomMcpServerPublic,
99
FileSuggestion,
1010
GitBranchList,
11+
GitBranchSwitchCheck,
1112
GitDiff,
1213
GitPullRequestStatus,
1314
GitStatus,
@@ -111,6 +112,7 @@ export interface PendingValueMap {
111112
assetEnsure: ManagedAssetStatus;
112113
gitStatus: GitStatus;
113114
gitBranchList: GitBranchList;
115+
gitBranchSwitchCheck: GitBranchSwitchCheck;
114116
gitPrStatus: GitPullRequestStatus;
115117
gitDiff: GitDiff;
116118
fileRead: WorkspaceFile;
@@ -172,6 +174,7 @@ export class PendingRegistry {
172174
assetEnsure: new Map(),
173175
gitStatus: new Map(),
174176
gitBranchList: new Map(),
177+
gitBranchSwitchCheck: new Map(),
175178
gitPrStatus: new Map(),
176179
gitDiff: new Map(),
177180
fileRead: new Map(),

packages/client/core/tests/integration/control-client.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,35 @@ import { createConnectedLocalClient } from '../support/local-client';
1616
const sessionId = 'sess-control' as SessionId;
1717

1818
describe('LinkCodeClient control API', () => {
19+
it('routes git branch switch checks and mutation acknowledgements', async () => {
20+
const { client, serverTransport } = await createConnectedLocalClient();
21+
serverTransport.onMessage((msg) => {
22+
const p = msg.payload;
23+
if (p.kind === 'git.branch.switch.check') {
24+
serverTransport.send(
25+
createWireMessage({
26+
kind: 'git.branch.switch.check.result',
27+
replyTo: p.clientReqId,
28+
check: { status: 'ready' },
29+
}),
30+
);
31+
} else if (p.kind === 'git.branch.create' || p.kind === 'git.commit') {
32+
serverTransport.send(
33+
createWireMessage({ kind: 'request.succeeded', replyTo: p.clientReqId }),
34+
);
35+
}
36+
});
37+
38+
await expect(client.checkGitBranchSwitch('/repo', 'main')).resolves.toEqual({
39+
status: 'ready',
40+
});
41+
await expect(client.createGitBranch('/repo', 'feature')).resolves.toEqual({ ok: true });
42+
await expect(client.commitGitChanges('/repo', 'save')).resolves.toEqual({ ok: true });
43+
44+
client.dispose();
45+
serverTransport.close();
46+
});
47+
1948
it('preserves MCP warnings on detailed session-start results', async () => {
2049
const { client, serverTransport } = await createConnectedLocalClient();
2150
serverTransport.onMessage((msg) => {

packages/client/sdk/src/client.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
EffortLevel,
2727
FileSuggestion,
2828
GitBranchList,
29+
GitBranchSwitchCheck,
2930
GitDiff,
3031
GitDiffMode,
3132
GitPullRequestStatus,
@@ -380,6 +381,18 @@ export class LinkCodeSdkClient {
380381
return toResult(this.raw.listGitBranches(cwd));
381382
}
382383

384+
checkGitBranchSwitch(cwd: string, branch: string): RequestResult<GitBranchSwitchCheck> {
385+
return toResult(this.raw.checkGitBranchSwitch(cwd, branch));
386+
}
387+
388+
createGitBranch(cwd: string, branch: string): RequestResult<{ ok: true }> {
389+
return toResult(this.raw.createGitBranch(cwd, branch));
390+
}
391+
392+
commitGitChanges(cwd: string, message: string): RequestResult<{ ok: true }> {
393+
return toResult(this.raw.commitGitChanges(cwd, message));
394+
}
395+
383396
/** Hosting-provider PR state for a directory's current branch. */
384397
getGitPullRequestStatus(cwd: string): RequestResult<GitPullRequestStatus> {
385398
return toResult(this.raw.getGitPullRequestStatus(cwd));

packages/client/sdk/src/operations.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
EffortLevel,
2424
FileSuggestion,
2525
GitBranchList,
26+
GitBranchSwitchCheck,
2627
GitDiff,
2728
GitDiffMode,
2829
GitPullRequestStatus,
@@ -353,6 +354,24 @@ export function listGitBranches(options: Options<{ cwd: string }>): RequestResul
353354
return resolveClient(options).listGitBranches(options.cwd);
354355
}
355356

357+
export function checkGitBranchSwitch(
358+
options: Options<{ cwd: string; branch: string }>,
359+
): RequestResult<GitBranchSwitchCheck> {
360+
return resolveClient(options).checkGitBranchSwitch(options.cwd, options.branch);
361+
}
362+
363+
export function createGitBranch(
364+
options: Options<{ cwd: string; branch: string }>,
365+
): RequestResult<{ ok: true }> {
366+
return resolveClient(options).createGitBranch(options.cwd, options.branch);
367+
}
368+
369+
export function commitGitChanges(
370+
options: Options<{ cwd: string; message: string }>,
371+
): RequestResult<{ ok: true }> {
372+
return resolveClient(options).commitGitChanges(options.cwd, options.message);
373+
}
374+
356375
/** Hosting-provider PR state for a directory's current branch. */
357376
export function getGitPullRequestStatus(
358377
options: Options<{ cwd: string }>,

0 commit comments

Comments
 (0)