From b5496140a5ab18a69c370c97290fac3162b6716a Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Mon, 4 May 2026 14:52:57 +0300 Subject: [PATCH 1/3] fix(code): show unified PR badge for externally-created PRs The unified badge depended on `workspace.linkedBranch`, which only got set when the in-app create-PR flow ran. Skills that run `gh pr create` via bash never went through that flow, so the badge fell back to the old "View PR" button (and a dropdown of disabled commit/push items) even though `getPrStatus` had already discovered the PR on the current branch. - TaskActionsMenu: fall back to `gitState.prUrl` when no `linkedBranch` is recorded so the badge appears for any worktree whose current branch has a PR. - TaskActionsMenu: when a PR exists, drop disabled commit/push entries from the adjacent dropdown so it only surfaces actionable items. - AgentService: when a PR URL is detected in bash output, emit `AgentFileActivity` so `WorkspaceService` records the current branch as `linkedBranch`. Keeps PR-aware UI (branch mismatch, diff source) in sync without waiting for the next file edit. Generated-By: PostHog Code Task-Id: 66a40920-2b25-40b6-983e-1a489353431d --- apps/code/src/main/services/agent/service.ts | 23 +++++++++++ .../components/TaskActionsMenu.tsx | 41 +++++++++++-------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index af80ec302..645afd3e1 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -1628,6 +1628,29 @@ For git operations while detached: error: err, }); }); + + // The user-initiated PR-creation flow links the current branch to the + // workspace atomically (see GitService.createPr). PRs created via bash — + // e.g. an agent running a `/commit-and-pr` skill — never go through that + // flow, so `workspace.linkedBranch` would otherwise stay unset and + // PR-aware UI (the unified PR badge, branch mismatch warning, diff + // source) would have no anchor. Emit AgentFileActivity here too so + // WorkspaceService.handleAgentFileActivity links the current feature + // branch the moment we observe a PR for it. + getCurrentBranch(session.repoPath) + .then((branchName) => { + this.emit(AgentServiceEvent.AgentFileActivity, { + taskId: session.taskId, + branchName, + }); + }) + .catch((err) => { + log.warn("Failed to resolve branch for PR auto-link", { + taskRunId, + taskId: session.taskId, + error: err, + }); + }); } /** diff --git a/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx b/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx index d8b38d6e1..4450f8fde 100644 --- a/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx @@ -59,17 +59,6 @@ interface TaskActionsMenuProps { * list. Cloud tasks without a PR render nothing. */ export function TaskActionsMenu({ taskId, isCloud }: TaskActionsMenuProps) { - // PR URL resolution — pick the right source based on task kind. - const cloudPrUrl = useCloudPrUrl(taskId); - const linkedPrUrl = useLinkedBranchPrUrl(taskId); - const prUrl = isCloud ? cloudPrUrl : linkedPrUrl; - - const { - meta: { state: prState, merged, draft }, - } = usePrDetails(prUrl); - const { execute: executePrAction, isPending: isPrActionPending } = - usePrActions(prUrl); - // Git state (skipped for cloud — useGitInteraction handles undefined repo). const workspace = useWorkspace(taskId); const isFocused = useFocusStore( @@ -84,18 +73,38 @@ export function TaskActionsMenu({ taskId, isCloud }: TaskActionsMenuProps) { actions: gitActions, } = useGitInteraction(taskId, isCloud ? undefined : localRepoPath); + // PR URL resolution — pick the right source based on task kind. + // For local tasks, prefer the explicitly linked-branch lookup but fall back + // to whatever `getPrStatus` found on the current branch. The fallback + // catches PRs created outside the in-app flow (e.g. agents/skills running + // `gh pr create` via bash) where `workspace.linkedBranch` may not be set + // yet. + const cloudPrUrl = useCloudPrUrl(taskId); + const linkedPrUrl = useLinkedBranchPrUrl(taskId); + const prUrl = isCloud ? cloudPrUrl : (linkedPrUrl ?? gitState.prUrl ?? null); + + const { + meta: { state: prState, merged, draft }, + } = usePrDetails(prUrl); + const { execute: executePrAction, isPending: isPrActionPending } = + usePrActions(prUrl); + const pr = prUrl && prState !== null ? { url: prUrl, state: prState } : null; // Cloud tasks only appear when they have a PR. if (isCloud && !pr) return null; - // "view-pr" is redundant when the badge itself links to the PR; - // "create-pr" is redundant once a PR exists. + // When a PR exists the badge handles "view PR" and "create PR" is moot. + // Disabled commit/push entries (no changes, branch up to date) just clutter + // the dropdown next to the badge — drop them so the menu only surfaces + // actions the user can actually take. const gitItems = isCloud ? [] - : gitState.actions.filter( - (a) => !(pr && (a.id === "view-pr" || a.id === "create-pr")), - ); + : gitState.actions.filter((a) => { + if (!pr) return true; + if (a.id === "view-pr" || a.id === "create-pr") return false; + return a.enabled; + }); return ( <> From b4d4a9a40ecf4a2078d43f3752491a323f8aa31e Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Mon, 4 May 2026 14:57:20 +0300 Subject: [PATCH 2/3] fix(code): preserve disabledReason tooltips for non-work-shipping actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrow the dropdown filter to only drop disabled commit / push / sync / publish entries — these slots flip to disabled solely to signal "no work to do," so they're noise next to a PR badge. Other disabled actions stay visible so their disabledReason tooltip can still explain why they're unavailable (network issues, diverged branch, etc). Generated-By: PostHog Code Task-Id: 66a40920-2b25-40b6-983e-1a489353431d --- .../components/TaskActionsMenu.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx b/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx index 4450f8fde..bc5ec9990 100644 --- a/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx @@ -95,15 +95,24 @@ export function TaskActionsMenu({ taskId, isCloud }: TaskActionsMenuProps) { if (isCloud && !pr) return null; // When a PR exists the badge handles "view PR" and "create PR" is moot. - // Disabled commit/push entries (no changes, branch up to date) just clutter - // the dropdown next to the badge — drop them so the menu only surfaces - // actions the user can actually take. + // The work-shipping slots (commit and the push/sync/publish trio) only get + // disabled to signal "nothing to do" (no changes, branch up to date, no + // commits to publish) — that's noise next to a PR badge, so drop them. + // Other disabled actions stay so their `disabledReason` tooltip can still + // explain why they're unavailable. + const noWorkSlots = new Set([ + "commit", + "push", + "sync", + "publish", + ]); const gitItems = isCloud ? [] : gitState.actions.filter((a) => { if (!pr) return true; if (a.id === "view-pr" || a.id === "create-pr") return false; - return a.enabled; + if (!a.enabled && noWorkSlots.has(a.id)) return false; + return true; }); return ( From 9d48484b7bb2d1eee205c4a952c34c352b463ffa Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Thu, 7 May 2026 11:12:14 +0300 Subject: [PATCH 3/3] refactor: address review feedback on PR badge fallback - Extract emitAgentFileActivityForCurrentBranch helper so detectAndAttachPrUrl and trackAgentFileActivity share a single branch-link path. - Hoist NO_WORK_SLOTS to module scope. - Tighten the fallback comment to acknowledge the agent emit is the primary path and that gitState.prUrl only covers the unfocused-on-worktree case. Generated-By: PostHog Code Task-Id: 285de649-2c86-45de-8919-0521f6344cc7 --- apps/code/src/main/services/agent/service.ts | 37 ++++++++++-------- .../components/TaskActionsMenu.tsx | 39 +++++++++++-------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index 645afd3e1..60442bccf 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -1637,20 +1637,9 @@ For git operations while detached: // source) would have no anchor. Emit AgentFileActivity here too so // WorkspaceService.handleAgentFileActivity links the current feature // branch the moment we observe a PR for it. - getCurrentBranch(session.repoPath) - .then((branchName) => { - this.emit(AgentServiceEvent.AgentFileActivity, { - taskId: session.taskId, - branchName, - }); - }) - .catch((err) => { - log.warn("Failed to resolve branch for PR auto-link", { - taskRunId, - taskId: session.taskId, - error: err, - }); - }); + this.emitAgentFileActivityForCurrentBranch(taskRunId, session, { + reason: "pr-detected", + }); } /** @@ -1673,6 +1662,22 @@ For git operations while detached: if (!session) return; if (!AgentService.FILE_MODIFYING_TOOLS.has(toolName)) return; + this.emitAgentFileActivityForCurrentBranch(taskRunId, session, { + reason: "file-edit", + toolName, + }); + } + + /** + * Resolve the current branch in the session's repo and emit AgentFileActivity + * so WorkspaceService can link the branch to the task. Best-effort — branch + * resolution failures are logged but never thrown. + */ + private emitAgentFileActivityForCurrentBranch( + taskRunId: string, + session: ManagedSession, + context: { reason: "file-edit" | "pr-detected"; toolName?: string }, + ): void { getCurrentBranch(session.repoPath) .then((branchName) => { this.emit(AgentServiceEvent.AgentFileActivity, { @@ -1681,10 +1686,10 @@ For git operations while detached: }); }) .catch((err) => { - log.error("Failed to emit agent file activity event", { + log.warn("Failed to emit agent file activity event", { taskRunId, taskId: session.taskId, - toolName, + ...context, error: err, }); }); diff --git a/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx b/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx index bc5ec9990..24482cdc9 100644 --- a/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/TaskActionsMenu.tsx @@ -48,6 +48,18 @@ interface TaskActionsMenuProps { isCloud: boolean; } +// Work-shipping slots flip to disabled solely to signal "nothing to do" (no +// changes, branch up to date, no commits to publish). Next to a PR badge that +// noise isn't useful, so we drop them when a PR exists. Other disabled +// actions stay visible so their `disabledReason` tooltip can still explain +// why they're unavailable. +const NO_WORK_SLOTS = new Set([ + "commit", + "push", + "sync", + "publish", +]); + /** * Unified actions control shown in the task header. Combines: * - Git interaction (commit/push/create-PR/branch) for local tasks @@ -74,11 +86,15 @@ export function TaskActionsMenu({ taskId, isCloud }: TaskActionsMenuProps) { } = useGitInteraction(taskId, isCloud ? undefined : localRepoPath); // PR URL resolution — pick the right source based on task kind. - // For local tasks, prefer the explicitly linked-branch lookup but fall back - // to whatever `getPrStatus` found on the current branch. The fallback - // catches PRs created outside the in-app flow (e.g. agents/skills running - // `gh pr create` via bash) where `workspace.linkedBranch` may not be set - // yet. + // For local tasks, prefer the linked-branch lookup. The agent-side + // AgentFileActivity emit is the primary path for keeping `linkedBranch` in + // sync with PRs created via bash (see AgentService.detectAndAttachPrUrl); + // until that link lands we fall back to whatever `getPrStatus` found on + // `localRepoPath`'s current branch. Coverage is partial — when the user is + // focused on the worktree, `localRepoPath` is the main repo and + // `gitState.prUrl` won't see the worktree's feature-branch PR — but the + // primary path closes that gap once the next bash tool call observes the + // PR URL. const cloudPrUrl = useCloudPrUrl(taskId); const linkedPrUrl = useLinkedBranchPrUrl(taskId); const prUrl = isCloud ? cloudPrUrl : (linkedPrUrl ?? gitState.prUrl ?? null); @@ -95,23 +111,12 @@ export function TaskActionsMenu({ taskId, isCloud }: TaskActionsMenuProps) { if (isCloud && !pr) return null; // When a PR exists the badge handles "view PR" and "create PR" is moot. - // The work-shipping slots (commit and the push/sync/publish trio) only get - // disabled to signal "nothing to do" (no changes, branch up to date, no - // commits to publish) — that's noise next to a PR badge, so drop them. - // Other disabled actions stay so their `disabledReason` tooltip can still - // explain why they're unavailable. - const noWorkSlots = new Set([ - "commit", - "push", - "sync", - "publish", - ]); const gitItems = isCloud ? [] : gitState.actions.filter((a) => { if (!pr) return true; if (a.id === "view-pr" || a.id === "create-pr") return false; - if (!a.enabled && noWorkSlots.has(a.id)) return false; + if (!a.enabled && NO_WORK_SLOTS.has(a.id)) return false; return true; });