diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx
index 6e938e6a0..03d0574d9 100644
--- a/packages/app/src/components/git-diff-pane.tsx
+++ b/packages/app/src/components/git-diff-pane.tsx
@@ -876,6 +876,8 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
const hasPullRequest = Boolean(prStatus?.url);
const hasRemote = gitStatus?.hasRemote ?? false;
const isPaseoOwnedWorktree = gitStatus?.isPaseoOwnedWorktree ?? false;
+ const currentBranch = gitStatus?.currentBranch;
+ const isOnBaseBranch = currentBranch === baseRefLabel;
// ==========================================================================
// Git Actions (Data-Oriented)
@@ -950,7 +952,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
if (aheadCount > 0) {
allActions.set("merge-branch", {
id: "merge-branch",
- label: "Merge branch",
+ label: `Merge into ${baseRefLabel}`,
pendingLabel: "Merging...",
successLabel: "Merged",
disabled: mergeDisabled,
@@ -960,17 +962,19 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
});
}
- // Merge from base - always available
- allActions.set("merge-from-base", {
- id: "merge-from-base",
- label: `Merge from ${baseRefLabel}`,
- pendingLabel: "Merging...",
- successLabel: "Merged",
- disabled: mergeFromBaseDisabled,
- status: mergeFromBaseAction.status,
- description: hasUncommittedChanges ? "Requires clean working tree" : undefined,
- handler: mergeFromBaseAction.trigger,
- });
+ // Update from base - only when not on base branch
+ if (!isOnBaseBranch) {
+ allActions.set("merge-from-base", {
+ id: "merge-from-base",
+ label: `Update from ${baseRefLabel}`,
+ pendingLabel: "Updating...",
+ successLabel: "Updated",
+ disabled: mergeFromBaseDisabled,
+ status: mergeFromBaseAction.status,
+ description: hasUncommittedChanges ? "Requires clean working tree" : undefined,
+ handler: mergeFromBaseAction.trigger,
+ });
+ }
// Archive worktree - only for Paseo worktrees
if (isPaseoOwnedWorktree) {
@@ -1033,7 +1037,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
return { primary, secondary, menu };
}, [
- isGit, hasRemote, hasPullRequest, prStatus?.url, aheadCount, isPaseoOwnedWorktree,
+ isGit, hasRemote, hasPullRequest, prStatus?.url, aheadCount, isPaseoOwnedWorktree, isOnBaseBranch,
hasUncommittedChanges, aheadOfOrigin, shipDefault, baseRefLabel,
commitDisabled, pushDisabled, prDisabled, mergeDisabled, mergeFromBaseDisabled, archiveDisabled,
commitAction, pushAction, prCreateAction, mergeAction, mergeFromBaseAction, archiveAction,
@@ -1089,7 +1093,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
>
-
+
{gitActions.secondary.map((action, index) => {
const needsSeparator = action.id === "merge-from-base" || action.id === "push";
return (
@@ -1149,7 +1153,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
) : null}
- {isGit && hasChanges ? (
+ {isGit && (hasUncommittedChanges || aheadCount > 0) ? (
[
diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts
index dc4721be7..dc65cbd1f 100644
--- a/packages/server/src/server/agent/agent-manager.test.ts
+++ b/packages/server/src/server/agent/agent-manager.test.ts
@@ -28,6 +28,10 @@ class TestAgentClient implements AgentClient {
readonly provider = "codex" as const;
readonly capabilities = TEST_CAPABILITIES;
+ async isAvailable(): Promise {
+ return true;
+ }
+
async createSession(config: AgentSessionConfig): Promise {
return new TestAgentSession(config);
}
@@ -368,4 +372,126 @@ describe("AgentManager", () => {
// Should NOT have triggered attention callback for internal agent
expect(attentionCalls).toHaveLength(0);
});
+
+ test("respondToPermission updates currentModeId after plan approval", async () => {
+ const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
+ const storagePath = join(workdir, "agents");
+ const storage = new AgentStorage(storagePath, logger);
+
+ // Create a session that simulates plan approval mode change
+ let sessionMode = "plan";
+ class PlanModeTestSession implements AgentSession {
+ readonly provider = "codex" as const;
+ readonly capabilities = TEST_CAPABILITIES;
+ readonly id = randomUUID();
+
+ async run(): Promise {
+ return { sessionId: this.id, finalText: "", timeline: [] };
+ }
+
+ async *stream(): AsyncGenerator {
+ yield { type: "turn_started", provider: this.provider };
+ yield { type: "turn_completed", provider: this.provider };
+ }
+
+ async *streamHistory(): AsyncGenerator {}
+
+ async getRuntimeInfo() {
+ return { provider: this.provider, sessionId: this.id, model: null, modeId: sessionMode };
+ }
+
+ async getAvailableModes() {
+ return [
+ { id: "plan", label: "Plan" },
+ { id: "acceptEdits", label: "Accept Edits" },
+ ];
+ }
+
+ async getCurrentMode() {
+ return sessionMode;
+ }
+
+ async setMode(modeId: string): Promise {
+ sessionMode = modeId;
+ }
+
+ getPendingPermissions() {
+ return [];
+ }
+
+ async respondToPermission(
+ _requestId: string,
+ response: { behavior: string }
+ ): Promise {
+ // Simulate what claude-agent.ts does: when plan permission is approved,
+ // it calls setMode("acceptEdits") internally
+ if (response.behavior === "allow") {
+ sessionMode = "acceptEdits";
+ }
+ }
+
+ describePersistence() {
+ return { provider: this.provider, sessionId: this.id };
+ }
+
+ async interrupt(): Promise {}
+ async close(): Promise {}
+ }
+
+ class PlanModeTestClient implements AgentClient {
+ readonly provider = "codex" as const;
+ readonly capabilities = TEST_CAPABILITIES;
+
+ async isAvailable(): Promise {
+ return true;
+ }
+
+ async createSession(): Promise {
+ return new PlanModeTestSession();
+ }
+
+ async resumeSession(): Promise {
+ return new PlanModeTestSession();
+ }
+ }
+
+ const manager = new AgentManager({
+ clients: {
+ codex: new PlanModeTestClient(),
+ },
+ registry: storage,
+ logger,
+ idFactory: () => "plan-mode-agent",
+ });
+
+ // Create agent in plan mode
+ const snapshot = await manager.createAgent({
+ provider: "codex",
+ cwd: workdir,
+ modeId: "plan",
+ });
+
+ expect(snapshot.currentModeId).toBe("plan");
+
+ // Simulate a pending plan permission request
+ const agent = manager.getAgent(snapshot.id)!;
+ const permissionRequest = {
+ id: "perm-123",
+ provider: "codex" as const,
+ name: "ExitPlanMode",
+ kind: "plan" as const,
+ input: { plan: "Test plan" },
+ };
+ agent.pendingPermissions.set(permissionRequest.id, permissionRequest);
+
+ // Approve the plan permission
+ await manager.respondToPermission(snapshot.id, "perm-123", {
+ behavior: "allow",
+ });
+
+ // The session's mode has changed to "acceptEdits" internally
+ // The manager should have updated currentModeId to reflect this
+ const updatedAgent = manager.getAgent(snapshot.id);
+ expect(updatedAgent?.currentModeId).toBe("acceptEdits");
+ });
});
diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts
index b09a08a9b..4c5f217ef 100644
--- a/packages/server/src/server/agent/agent-manager.ts
+++ b/packages/server/src/server/agent/agent-manager.ts
@@ -700,6 +700,15 @@ export class AgentManager {
const agent = this.requireAgent(agentId);
await agent.session.respondToPermission(requestId, response);
agent.pendingPermissions.delete(requestId);
+
+ // Update currentModeId - the session may have changed mode internally
+ // (e.g., plan approval changes mode from "plan" to "acceptEdits")
+ try {
+ agent.currentModeId = await agent.session.getCurrentMode();
+ } catch {
+ // Ignore errors from getCurrentMode - mode tracking is best effort
+ }
+
this.emitState(agent);
}