authorgravatar for crazywolf132@gmail.com0x1f99d <crazywolf132@gmail.com> 2026-03-11 04:16:37+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-10 10:16:37-07:00
log1c290767f22f9d8c7d3fa378d9450c8f48184679
tree66ac9b64f8517962269dcd70d471700fc8315499
parent9becb3f40af504244f85823bb19f18bfe507ca49
signature Signed by PGP key B5690EEEBB952194

fix: use commit as the default git action without origin (#642)

* fix(web): avoid push and PR defaults without origin * fmt * object here too --------- Co-authored-by: Julius Marminge <julius0216@outlook.com>

9 files changed, 229 insertions(+), 124 deletions(-)

apps/server/src/git/Layers/GitCore.test.ts+3
......@@ -260,6 +260,7 @@ it.layer(TestLayer)("git integration", (it) => {
260260 yield* initRepoWithCommit(tmp);
261261 const result = yield* listGitBranches({ cwd: tmp });
262262 expect(result.isRepo).toBe(true);
263 expect(result.hasOriginRemote).toBe(false);
263264 expect(result.branches.length).toBeGreaterThanOrEqual(1);
264265 }),
265266 );
......@@ -273,6 +274,7 @@ it.layer(TestLayer)("git integration", (it) => {
273274 const tmp = yield* makeTmpDir();
274275 const result = yield* listGitBranches({ cwd: tmp });
275276 expect(result.isRepo).toBe(false);
277 expect(result.hasOriginRemote).toBe(false);
276278 expect(result.branches).toEqual([]);
277279 }),
278280 );
......@@ -431,6 +433,7 @@ it.layer(TestLayer)("git integration", (it) => {
431433 const result = yield* listGitBranches({ cwd: tmp });
432434 const firstRemoteIndex = result.branches.findIndex((branch) => branch.isRemote);
433435
436 expect(result.hasOriginRemote).toBe(true);
434437 expect(firstRemoteIndex).toBeGreaterThan(0);
435438 expect(result.branches.slice(0, firstRemoteIndex).every((branch) => !branch.isRemote)).toBe(
436439 true,
apps/server/src/git/Layers/GitCore.ts+2-2
......@@ -931,7 +931,7 @@ const makeGitCore = Effect.gen(function* () {
931931 if (localBranchResult.code !== 0) {
932932 const stderr = localBranchResult.stderr.trim();
933933 if (stderr.toLowerCase().includes("not a git repository")) {
934 return { branches: [], isRepo: false };
934 return { branches: [], isRepo: false, hasOriginRemote: false };
935935 }
936936 return yield* createGitCommandError(
937937 "GitCore.listBranches",
......@@ -1097,7 +1097,7 @@ const makeGitCore = Effect.gen(function* () {
10971097
10981098 const branches = [...localBranches, ...remoteBranches];
10991099
1100 return { branches, isRepo: true };
1100 return { branches, isRepo: true, hasOriginRemote: remoteNames.includes("origin") };
11011101 });
11021102
11031103 const createWorktree: GitCoreShape["createWorktree"] = (input) =>
apps/server/src/wsServer.test.ts+2-1
......@@ -1577,6 +1577,7 @@ describe("WebSocket Server", () => {
15771577 Effect.succeed({
15781578 branches: [],
15791579 isRepo: false,
1580 hasOriginRemote: false,
15801581 }),
15811582 );
15821583 const initRepo = vi.fn(() => Effect.void);
......@@ -1608,7 +1609,7 @@ describe("WebSocket Server", () => {
16081609
16091610 const listResponse = await sendRequest(ws, WS_METHODS.gitListBranches, { cwd: "/repo/path" });
16101611 expect(listResponse.error).toBeUndefined();
1611 expect(listResponse.result).toEqual({ branches: [], isRepo: false });
1612 expect(listResponse.result).toEqual({ branches: [], isRepo: false, hasOriginRemote: false });
16121613 expect(listBranches).toHaveBeenCalledWith({ cwd: "/repo/path" });
16131614
16141615 const initResponse = await sendRequest(ws, WS_METHODS.gitInit, { cwd: "/repo/path" });
apps/web/public/mockServiceWorker.js+93-106
......@@ -7,114 +7,111 @@
77 * - Please do NOT modify this file.
88 */
99
10const PACKAGE_VERSION = '2.12.10'
11const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
12const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13const activeClientIds = new Set()
10const PACKAGE_VERSION = "2.12.10";
11const INTEGRITY_CHECKSUM = "4db4a41e972cec1b64cc569c66952d82";
12const IS_MOCKED_RESPONSE = Symbol("isMockedResponse");
13const activeClientIds = new Set();
1414
15addEventListener('install', function () {
16 self.skipWaiting()
17})
15addEventListener("install", function () {
16 self.skipWaiting();
17});
1818
19addEventListener('activate', function (event) {
20 event.waitUntil(self.clients.claim())
21})
19addEventListener("activate", function (event) {
20 event.waitUntil(self.clients.claim());
21});
2222
23addEventListener('message', async function (event) {
24 const clientId = Reflect.get(event.source || {}, 'id')
23addEventListener("message", async function (event) {
24 const clientId = Reflect.get(event.source || {}, "id");
2525
2626 if (!clientId || !self.clients) {
27 return
27 return;
2828 }
2929
30 const client = await self.clients.get(clientId)
30 const client = await self.clients.get(clientId);
3131
3232 if (!client) {
33 return
33 return;
3434 }
3535
3636 const allClients = await self.clients.matchAll({
37 type: 'window',
38 })
37 type: "window",
38 });
3939
4040 switch (event.data) {
41 case 'KEEPALIVE_REQUEST': {
41 case "KEEPALIVE_REQUEST": {
4242 sendToClient(client, {
43 type: 'KEEPALIVE_RESPONSE',
44 })
45 break
43 type: "KEEPALIVE_RESPONSE",
44 });
45 break;
4646 }
4747
48 case 'INTEGRITY_CHECK_REQUEST': {
48 case "INTEGRITY_CHECK_REQUEST": {
4949 sendToClient(client, {
50 type: 'INTEGRITY_CHECK_RESPONSE',
50 type: "INTEGRITY_CHECK_RESPONSE",
5151 payload: {
5252 packageVersion: PACKAGE_VERSION,
5353 checksum: INTEGRITY_CHECKSUM,
5454 },
55 })
56 break
55 });
56 break;
5757 }
5858
59 case 'MOCK_ACTIVATE': {
60 activeClientIds.add(clientId)
59 case "MOCK_ACTIVATE": {
60 activeClientIds.add(clientId);
6161
6262 sendToClient(client, {
63 type: 'MOCKING_ENABLED',
63 type: "MOCKING_ENABLED",
6464 payload: {
6565 client: {
6666 id: client.id,
6767 frameType: client.frameType,
6868 },
6969 },
70 })
71 break
70 });
71 break;
7272 }
7373
74 case 'CLIENT_CLOSED': {
75 activeClientIds.delete(clientId)
74 case "CLIENT_CLOSED": {
75 activeClientIds.delete(clientId);
7676
7777 const remainingClients = allClients.filter((client) => {
78 return client.id !== clientId
79 })
78 return client.id !== clientId;
79 });
8080
8181 // Unregister itself when there are no more clients
8282 if (remainingClients.length === 0) {
83 self.registration.unregister()
83 self.registration.unregister();
8484 }
8585
86 break
86 break;
8787 }
8888 }
89})
89});
9090
91addEventListener('fetch', function (event) {
92 const requestInterceptedAt = Date.now()
91addEventListener("fetch", function (event) {
92 const requestInterceptedAt = Date.now();
9393
9494 // Bypass navigation requests.
95 if (event.request.mode === 'navigate') {
96 return
95 if (event.request.mode === "navigate") {
96 return;
9797 }
9898
9999 // Opening the DevTools triggers the "only-if-cached" request
100100 // that cannot be handled by the worker. Bypass such requests.
101 if (
102 event.request.cache === 'only-if-cached' &&
103 event.request.mode !== 'same-origin'
104 ) {
105 return
101 if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
102 return;
106103 }
107104
108105 // Bypass all requests when there are no active clients.
109106 // Prevents the self-unregistered worked from handling requests
110107 // after it's been terminated (still remains active until the next reload).
111108 if (activeClientIds.size === 0) {
112 return
109 return;
113110 }
114111
115 const requestId = crypto.randomUUID()
116 event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
117})
112 const requestId = crypto.randomUUID();
113 event.respondWith(handleRequest(event, requestId, requestInterceptedAt));
114});
118115
119116/**
120117 * @param {FetchEvent} event
......@@ -122,28 +119,23 @@ addEventListener('fetch', function (event) {
122119 * @param {number} requestInterceptedAt
123120 */
124121async function handleRequest(event, requestId, requestInterceptedAt) {
125 const client = await resolveMainClient(event)
126 const requestCloneForEvents = event.request.clone()
127 const response = await getResponse(
128 event,
129 client,
130 requestId,
131 requestInterceptedAt,
132 )
122 const client = await resolveMainClient(event);
123 const requestCloneForEvents = event.request.clone();
124 const response = await getResponse(event, client, requestId, requestInterceptedAt);
133125
134126 // Send back the response clone for the "response:*" life-cycle events.
135127 // Ensure MSW is active and ready to handle the message, otherwise
136128 // this message will pend indefinitely.
137129 if (client && activeClientIds.has(client.id)) {
138 const serializedRequest = await serializeRequest(requestCloneForEvents)
130 const serializedRequest = await serializeRequest(requestCloneForEvents);
139131
140132 // Clone the response so both the client and the library could consume it.
141 const responseClone = response.clone()
133 const responseClone = response.clone();
142134
143135 sendToClient(
144136 client,
145137 {
146 type: 'RESPONSE',
138 type: "RESPONSE",
147139 payload: {
148140 isMockedResponse: IS_MOCKED_RESPONSE in response,
149141 request: {
......@@ -160,10 +152,10 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
160152 },
161153 },
162154 responseClone.body ? [serializedRequest.body, responseClone.body] : [],
163 )
155 );
164156 }
165157
166 return response
158 return response;
167159}
168160
169161/**
......@@ -175,30 +167,30 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
175167 * @returns {Promise<Client | undefined>}
176168 */
177169async function resolveMainClient(event) {
178 const client = await self.clients.get(event.clientId)
170 const client = await self.clients.get(event.clientId);
179171
180172 if (activeClientIds.has(event.clientId)) {
181 return client
173 return client;
182174 }
183175
184 if (client?.frameType === 'top-level') {
185 return client
176 if (client?.frameType === "top-level") {
177 return client;
186178 }
187179
188180 const allClients = await self.clients.matchAll({
189 type: 'window',
190 })
181 type: "window",
182 });
191183
192184 return allClients
193185 .filter((client) => {
194186 // Get only those clients that are currently visible.
195 return client.visibilityState === 'visible'
187 return client.visibilityState === "visible";
196188 })
197189 .find((client) => {
198190 // Find the client ID that's recorded in the
199191 // set of clients that have registered the worker.
200 return activeClientIds.has(client.id)
201 })
192 return activeClientIds.has(client.id);
193 });
202194}
203195
204196/**
......@@ -211,36 +203,34 @@ async function resolveMainClient(event) {
211203async function getResponse(event, client, requestId, requestInterceptedAt) {
212204 // Clone the request because it might've been already used
213205 // (i.e. its body has been read and sent to the client).
214 const requestClone = event.request.clone()
206 const requestClone = event.request.clone();
215207
216208 function passthrough() {
217209 // Cast the request headers to a new Headers instance
218210 // so the headers can be manipulated with.
219 const headers = new Headers(requestClone.headers)
211 const headers = new Headers(requestClone.headers);
220212
221213 // Remove the "accept" header value that marked this request as passthrough.
222214 // This prevents request alteration and also keeps it compliant with the
223215 // user-defined CORS policies.
224 const acceptHeader = headers.get('accept')
216 const acceptHeader = headers.get("accept");
225217 if (acceptHeader) {
226 const values = acceptHeader.split(',').map((value) => value.trim())
227 const filteredValues = values.filter(
228 (value) => value !== 'msw/passthrough',
229 )
218 const values = acceptHeader.split(",").map((value) => value.trim());
219 const filteredValues = values.filter((value) => value !== "msw/passthrough");
230220
231221 if (filteredValues.length > 0) {
232 headers.set('accept', filteredValues.join(', '))
222 headers.set("accept", filteredValues.join(", "));
233223 } else {
234 headers.delete('accept')
224 headers.delete("accept");
235225 }
236226 }
237227
238 return fetch(requestClone, { headers })
228 return fetch(requestClone, { headers });
239229 }
240230
241231 // Bypass mocking when the client is not active.
242232 if (!client) {
243 return passthrough()
233 return passthrough();
244234 }
245235
246236 // Bypass initial page load requests (i.e. static assets).
......@@ -248,15 +238,15 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
248238 // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
249239 // and is not ready to handle requests.
250240 if (!activeClientIds.has(client.id)) {
251 return passthrough()
241 return passthrough();
252242 }
253243
254244 // Notify the client that a request has been intercepted.
255 const serializedRequest = await serializeRequest(event.request)
245 const serializedRequest = await serializeRequest(event.request);
256246 const clientMessage = await sendToClient(
257247 client,
258248 {
259 type: 'REQUEST',
249 type: "REQUEST",
260250 payload: {
261251 id: requestId,
262252 interceptedAt: requestInterceptedAt,
......@@ -264,19 +254,19 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
264254 },
265255 },
266256 [serializedRequest.body],
267 )
257 );
268258
269259 switch (clientMessage.type) {
270 case 'MOCK_RESPONSE': {
271 return respondWithMock(clientMessage.data)
260 case "MOCK_RESPONSE": {
261 return respondWithMock(clientMessage.data);
272262 }
273263
274 case 'PASSTHROUGH': {
275 return passthrough()
264 case "PASSTHROUGH": {
265 return passthrough();
276266 }
277267 }
278268
279 return passthrough()
269 return passthrough();
280270}
281271
282272/**
......@@ -287,21 +277,18 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
287277 */
288278function sendToClient(client, message, transferrables = []) {
289279 return new Promise((resolve, reject) => {
290 const channel = new MessageChannel()
280 const channel = new MessageChannel();
291281
292282 channel.port1.onmessage = (event) => {
293283 if (event.data && event.data.error) {
294 return reject(event.data.error)
284 return reject(event.data.error);
295285 }
296286
297 resolve(event.data)
298 }
287 resolve(event.data);
288 };
299289
300 client.postMessage(message, [
301 channel.port2,
302 ...transferrables.filter(Boolean),
303 ])
304 })
290 client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
291 });
305292}
306293
307294/**
......@@ -314,17 +301,17 @@ function respondWithMock(response) {
314301 // instance will have status code set to 0. Since it's not possible to create
315302 // a Response instance with status code 0, handle that use-case separately.
316303 if (response.status === 0) {
317 return Response.error()
304 return Response.error();
318305 }
319306
320 const mockedResponse = new Response(response.body, response)
307 const mockedResponse = new Response(response.body, response);
321308
322309 Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
323310 value: true,
324311 enumerable: true,
325 })
312 });
326313
327 return mockedResponse
314 return mockedResponse;
328315}
329316
330317/**
......@@ -345,5 +332,5 @@ async function serializeRequest(request) {
345332 referrerPolicy: request.referrerPolicy,
346333 body: await request.arrayBuffer(),
347334 keepalive: request.keepalive,
348 }
335 };
349336}
apps/web/src/components/ChatView.browser.tsx+1
......@@ -321,6 +321,7 @@ function resolveWsRpc(tag: string): unknown {
321321 if (tag === WS_METHODS.gitListBranches) {
322322 return {
323323 isRepo: true,
324 hasOriginRemote: true,
324325 branches: [
325326 {
326327 name: "main",
apps/web/src/components/GitActionsControl.logic.test.ts+68
......@@ -348,6 +348,21 @@ describe("when: working tree has local changes", () => {
348348 });
349349 });
350350
351 it("resolveQuickAction falls back to commit when no origin remote exists", () => {
352 const quick = resolveQuickAction(
353 status({ hasWorkingTreeChanges: true, hasUpstream: false }),
354 false,
355 false,
356 false,
357 );
358 assert.deepInclude(quick, {
359 kind: "run_action",
360 action: "commit",
361 label: "Commit",
362 disabled: false,
363 });
364 });
365
351366 it("resolveQuickAction returns commit and push when open PR exists", () => {
352367 const quick = resolveQuickAction(
353368 status({
......@@ -623,6 +638,25 @@ describe("when: branch has no upstream configured", () => {
623638 });
624639 });
625640
641 it("resolveQuickAction disables push-and-pr flows when no origin remote exists", () => {
642 const quick = resolveQuickAction(
643 status({
644 hasUpstream: false,
645 aheadCount: 2,
646 pr: null,
647 }),
648 false,
649 false,
650 false,
651 );
652 assert.deepEqual(quick, {
653 kind: "show_hint",
654 label: "Push",
655 hint: 'Add an "origin" remote before pushing or creating a PR.',
656 disabled: true,
657 });
658 });
659
626660 it("buildMenuItems enables create PR when no upstream and commits are ahead", () => {
627661 const items = buildMenuItems(status({ hasUpstream: false, pr: null, aheadCount: 2 }), false);
628662 assert.deepEqual(items, [
......@@ -653,6 +687,40 @@ describe("when: branch has no upstream configured", () => {
653687 ]);
654688 });
655689
690 it("buildMenuItems disables push and create PR when no origin remote exists", () => {
691 const items = buildMenuItems(
692 status({ hasUpstream: false, pr: null, aheadCount: 2 }),
693 false,
694 false,
695 );
696 assert.deepEqual(items, [
697 {
698 id: "commit",
699 label: "Commit",
700 disabled: true,
701 icon: "commit",
702 kind: "open_dialog",
703 dialogAction: "commit",
704 },
705 {
706 id: "push",
707 label: "Push",
708 disabled: true,
709 icon: "push",
710 kind: "open_dialog",
711 dialogAction: "push",
712 },
713 {
714 id: "pr",
715 label: "Create PR",
716 disabled: true,
717 icon: "pr",
718 kind: "open_dialog",
719 dialogAction: "create_pr",
720 },
721 ]);
722 });
723
656724 it("resolveQuickAction is disabled on default branch when no upstream exists and no commits are ahead", () => {
657725 const quick = resolveQuickAction(
658726 status({
apps/web/src/components/GitActionsControl.logic.ts+31-2
......@@ -113,6 +113,7 @@ export function summarizeGitResult(result: GitRunStackedActionResult): {
113113export function buildMenuItems(
114114 gitStatus: GitStatusResult | null,
115115 isBusy: boolean,
116 hasOriginRemote = true,
116117): GitActionMenuItem[] {
117118 if (!gitStatus) return [];
118119
......@@ -120,10 +121,23 @@ export function buildMenuItems(
120121 const hasChanges = gitStatus.hasWorkingTreeChanges;
121122 const hasOpenPr = gitStatus.pr?.state === "open";
122123 const isBehind = gitStatus.behindCount > 0;
124 const canPushWithoutUpstream = hasOriginRemote && !gitStatus.hasUpstream;
123125 const canCommit = !isBusy && hasChanges;
124 const canPush = !isBusy && hasBranch && !hasChanges && !isBehind && gitStatus.aheadCount > 0;
126 const canPush =
127 !isBusy &&
128 hasBranch &&
129 !hasChanges &&
130 !isBehind &&
131 gitStatus.aheadCount > 0 &&
132 (gitStatus.hasUpstream || canPushWithoutUpstream);
125133 const canCreatePr =
126 !isBusy && hasBranch && !hasChanges && !hasOpenPr && gitStatus.aheadCount > 0 && !isBehind;
134 !isBusy &&
135 hasBranch &&
136 !hasChanges &&
137 !hasOpenPr &&
138 gitStatus.aheadCount > 0 &&
139 !isBehind &&
140 (gitStatus.hasUpstream || canPushWithoutUpstream);
127141 const canOpenPr = !isBusy && hasOpenPr;
128142
129143 return [
......@@ -166,6 +180,7 @@ export function resolveQuickAction(
166180 gitStatus: GitStatusResult | null,
167181 isBusy: boolean,
168182 isDefaultBranch = false,
183 hasOriginRemote = true,
169184): GitQuickAction {
170185 if (isBusy) {
171186 return { label: "Commit", disabled: true, kind: "show_hint", hint: "Git action in progress." };
......@@ -197,6 +212,9 @@ export function resolveQuickAction(
197212 }
198213
199214 if (hasChanges) {
215 if (!gitStatus.hasUpstream && !hasOriginRemote) {
216 return { label: "Commit", disabled: false, kind: "run_action", action: "commit" };
217 }
200218 if (hasOpenPr || isDefaultBranch) {
201219 return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" };
202220 }
......@@ -209,6 +227,17 @@ export function resolveQuickAction(
209227 }
210228
211229 if (!gitStatus.hasUpstream) {
230 if (!hasOriginRemote) {
231 if (hasOpenPr && !isAhead) {
232 return { label: "View PR", disabled: false, kind: "open_pr" };
233 }
234 return {
235 label: "Push",
236 disabled: true,
237 kind: "show_hint",
238 hint: 'Add an "origin" remote before pushing or creating a PR.',
239 };
240 }
212241 if (!isAhead) {
213242 if (hasOpenPr) {
214243 return { label: "View PR", disabled: false, kind: "open_pr" };
apps/web/src/components/GitActionsControl.tsx+28-13
......@@ -59,11 +59,17 @@ interface PendingDefaultBranchAction {
5959
6060type GitActionToastId = ReturnType<typeof toastManager.add>;
6161
62function getMenuActionDisabledReason(
63 item: GitActionMenuItem,
64 gitStatus: GitStatusResult | null,
65 isBusy: boolean,
66): string | null {
62function getMenuActionDisabledReason({
63 item,
64 gitStatus,
65 isBusy,
66 hasOriginRemote,
67}: {
68 item: GitActionMenuItem;
69 gitStatus: GitStatusResult | null;
70 isBusy: boolean;
71 hasOriginRemote: boolean;
72}): string | null {
6773 if (!item.disabled) return null;
6874 if (isBusy) return "Git action in progress.";
6975 if (!gitStatus) return "Git status is unavailable.";
......@@ -91,6 +97,9 @@ function getMenuActionDisabledReason(
9197 if (isBehind) {
9298 return "Branch is behind upstream. Pull/rebase before pushing.";
9399 }
100 if (!gitStatus.hasUpstream && !hasOriginRemote) {
101 return 'Add an "origin" remote before pushing.';
102 }
94103 if (!isAhead) {
95104 return "No local commits to push.";
96105 }
......@@ -106,6 +115,9 @@ function getMenuActionDisabledReason(
106115 if (hasChanges) {
107116 return "Commit local changes before creating a PR.";
108117 }
118 if (!gitStatus.hasUpstream && !hasOriginRemote) {
119 return 'Add an "origin" remote before creating a PR.';
120 }
109121 if (!isAhead) {
110122 return "No local commits to include in a PR.";
111123 }
......@@ -154,6 +166,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
154166 const { data: branchList = null } = useQuery(gitBranchesQueryOptions(gitCwd));
155167 // Default to true while loading so we don't flash init controls.
156168 const isRepo = branchList?.isRepo ?? true;
169 const hasOriginRemote = branchList?.hasOriginRemote ?? false;
157170 const currentBranch = branchList?.branches.find((branch) => branch.current)?.name ?? null;
158171 const isGitStatusOutOfSync =
159172 !!gitStatus?.branch && !!currentBranch && gitStatus.branch !== currentBranch;
......@@ -184,12 +197,13 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
184197 }, [branchList?.branches, gitStatusForActions?.branch]);
185198
186199 const gitActionMenuItems = useMemo(
187 () => buildMenuItems(gitStatusForActions, isGitActionRunning),
188 [gitStatusForActions, isGitActionRunning],
200 () => buildMenuItems(gitStatusForActions, isGitActionRunning, hasOriginRemote),
201 [gitStatusForActions, hasOriginRemote, isGitActionRunning],
189202 );
190203 const quickAction = useMemo(
191 () => resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch),
192 [gitStatusForActions, isDefaultBranch, isGitActionRunning],
204 () =>
205 resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch, hasOriginRemote),
206 [gitStatusForActions, hasOriginRemote, isDefaultBranch, isGitActionRunning],
193207 );
194208 const quickActionDisabledReason = quickAction.disabled
195209 ? (quickAction.hint ?? "This action is currently unavailable.")
......@@ -645,11 +659,12 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
645659 </MenuTrigger>
646660 <MenuPopup align="end" className="w-full">
647661 {gitActionMenuItems.map((item) => {
648 const disabledReason = getMenuActionDisabledReason(
662 const disabledReason = getMenuActionDisabledReason({
649663 item,
650 gitStatusForActions,
651 isGitActionRunning,
652 );
664 gitStatus: gitStatusForActions,
665 isBusy: isGitActionRunning,
666 hasOriginRemote,
667 });
653668 if (item.disabled && disabledReason) {
654669 return (
655670 <Popover key={`${item.id}-${item.label}`}>
packages/contracts/src/git.ts+1
......@@ -149,6 +149,7 @@ export type GitStatusResult = typeof GitStatusResult.Type;
149149export const GitListBranchesResult = Schema.Struct({
150150 branches: Schema.Array(GitBranch),
151151 isRepo: Schema.Boolean,
152 hasOriginRemote: Schema.Boolean,
152153});
153154export type GitListBranchesResult = typeof GitListBranchesResult.Type;
154155