mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
feat: improve git actions UI and mode tracking after plan approval
This commit is contained in:
@@ -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) {
|
||||
>
|
||||
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" width={260} testID="changes-primary-cta-menu">
|
||||
<DropdownMenuContent align="end" testID="changes-primary-cta-menu">
|
||||
{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}
|
||||
</View>
|
||||
|
||||
{isGit && hasChanges ? (
|
||||
{isGit && (hasUncommittedChanges || aheadCount > 0) ? (
|
||||
<View style={styles.diffStatusContainer}>
|
||||
<Pressable
|
||||
style={({ hovered }) => [
|
||||
|
||||
@@ -28,6 +28,10 @@ class TestAgentClient implements AgentClient {
|
||||
readonly provider = "codex" as const;
|
||||
readonly capabilities = TEST_CAPABILITIES;
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
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<AgentRunResult> {
|
||||
return { sessionId: this.id, finalText: "", timeline: [] };
|
||||
}
|
||||
|
||||
async *stream(): AsyncGenerator<AgentStreamEvent> {
|
||||
yield { type: "turn_started", provider: this.provider };
|
||||
yield { type: "turn_completed", provider: this.provider };
|
||||
}
|
||||
|
||||
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {}
|
||||
|
||||
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<void> {
|
||||
sessionMode = modeId;
|
||||
}
|
||||
|
||||
getPendingPermissions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async respondToPermission(
|
||||
_requestId: string,
|
||||
response: { behavior: string }
|
||||
): Promise<void> {
|
||||
// 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<void> {}
|
||||
async close(): Promise<void> {}
|
||||
}
|
||||
|
||||
class PlanModeTestClient implements AgentClient {
|
||||
readonly provider = "codex" as const;
|
||||
readonly capabilities = TEST_CAPABILITIES;
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async createSession(): Promise<AgentSession> {
|
||||
return new PlanModeTestSession();
|
||||
}
|
||||
|
||||
async resumeSession(): Promise<AgentSession> {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user