Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 30 additions & 2 deletions apps/code/src/main/services/agent/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,18 @@ 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.
this.emitAgentFileActivityForCurrentBranch(taskRunId, session, {
reason: "pr-detected",
});
}

/**
Expand All @@ -1626,6 +1638,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, {
Expand All @@ -1634,10 +1662,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,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GitMenuActionId>([
"commit",
"push",
"sync",
"publish",
]);

/**
* Unified actions control shown in the task header. Combines:
* - Git interaction (commit/push/create-PR/branch) for local tasks
Expand All @@ -59,17 +71,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(
Expand All @@ -84,18 +85,40 @@ 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 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);

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.
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;
if (!a.enabled && NO_WORK_SLOTS.has(a.id)) return false;
return true;
});
Comment thread
richardsolomou marked this conversation as resolved.

return (
<>
Expand Down
Loading