Story 33: Copy-paste diff commands for agent worktrees
- Add base_branch detection to WorktreeInfo (from project root HEAD)
- Expose base_branch in AgentInfo API response
- Add {{base_branch}} template variable to agent config rendering
- Show git difftool command with copy-to-clipboard in AgentPanel UI
- Add diff command instruction to coder agent prompts
- Add AgentPanel tests for diff command rendering and clipboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
186
frontend/src/components/AgentPanel.test.tsx
Normal file
186
frontend/src/components/AgentPanel.test.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AgentInfo, AgentConfigInfo } from "../api/agents";
|
||||
import { agentsApi } from "../api/agents";
|
||||
|
||||
vi.mock("../api/agents", () => {
|
||||
const agentsApi = {
|
||||
listAgents: vi.fn(),
|
||||
getAgentConfig: vi.fn(),
|
||||
startAgent: vi.fn(),
|
||||
stopAgent: vi.fn(),
|
||||
reloadConfig: vi.fn(),
|
||||
};
|
||||
return { agentsApi, subscribeAgentStream: vi.fn(() => () => {}) };
|
||||
});
|
||||
|
||||
// Dynamic import so the mock is in place before the module loads
|
||||
const { AgentPanel } = await import("./AgentPanel");
|
||||
|
||||
const mockedAgents = {
|
||||
listAgents: vi.mocked(agentsApi.listAgents),
|
||||
getAgentConfig: vi.mocked(agentsApi.getAgentConfig),
|
||||
startAgent: vi.mocked(agentsApi.startAgent),
|
||||
};
|
||||
|
||||
const ROSTER: AgentConfigInfo[] = [
|
||||
{
|
||||
name: "coder-1",
|
||||
role: "Full-stack engineer",
|
||||
model: "sonnet",
|
||||
allowed_tools: null,
|
||||
max_turns: 50,
|
||||
max_budget_usd: 5.0,
|
||||
},
|
||||
];
|
||||
|
||||
describe("AgentPanel diff command", () => {
|
||||
beforeAll(() => {
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockedAgents.getAgentConfig.mockResolvedValue(ROSTER);
|
||||
mockedAgents.listAgents.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("shows diff command when an agent has a worktree path", async () => {
|
||||
const agentList: AgentInfo[] = [
|
||||
{
|
||||
story_id: "33_diff_commands",
|
||||
agent_name: "coder-1",
|
||||
status: "running",
|
||||
session_id: null,
|
||||
worktree_path: "/tmp/project-story-33",
|
||||
base_branch: "master",
|
||||
},
|
||||
];
|
||||
mockedAgents.listAgents.mockResolvedValue(agentList);
|
||||
|
||||
render(
|
||||
<AgentPanel
|
||||
stories={[
|
||||
{ story_id: "33_diff_commands", name: "Diff Commands", error: null },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Expand the agent detail by clicking the expand button
|
||||
const expandButton = await screen.findByText("▶");
|
||||
await userEvent.click(expandButton);
|
||||
|
||||
// The diff command should be rendered
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'cd "/tmp/project-story-33" && git difftool master...HEAD',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// A copy button should exist
|
||||
expect(screen.getByText("Copy")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("copies diff command to clipboard on click", async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText },
|
||||
});
|
||||
|
||||
const agentList: AgentInfo[] = [
|
||||
{
|
||||
story_id: "33_diff_commands",
|
||||
agent_name: "coder-1",
|
||||
status: "completed",
|
||||
session_id: null,
|
||||
worktree_path: "/home/user/my-project-story-33",
|
||||
base_branch: "main",
|
||||
},
|
||||
];
|
||||
mockedAgents.listAgents.mockResolvedValue(agentList);
|
||||
|
||||
render(
|
||||
<AgentPanel
|
||||
stories={[
|
||||
{ story_id: "33_diff_commands", name: "Diff Commands", error: null },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const expandButton = await screen.findByText("▶");
|
||||
await userEvent.click(expandButton);
|
||||
|
||||
const copyButton = await screen.findByText("Copy");
|
||||
await userEvent.click(copyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(writeText).toHaveBeenCalledWith(
|
||||
'cd "/home/user/my-project-story-33" && git difftool main...HEAD',
|
||||
);
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Copied")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses base_branch from the server in the diff command", async () => {
|
||||
const agentList: AgentInfo[] = [
|
||||
{
|
||||
story_id: "33_diff_commands",
|
||||
agent_name: "coder-1",
|
||||
status: "running",
|
||||
session_id: null,
|
||||
worktree_path: "/tmp/wt",
|
||||
base_branch: "develop",
|
||||
},
|
||||
];
|
||||
mockedAgents.listAgents.mockResolvedValue(agentList);
|
||||
|
||||
render(
|
||||
<AgentPanel
|
||||
stories={[
|
||||
{ story_id: "33_diff_commands", name: "Diff Commands", error: null },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const expandButton = await screen.findByText("▶");
|
||||
await userEvent.click(expandButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'cd "/tmp/wt" && git difftool develop...HEAD',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("defaults to master when base_branch is null", async () => {
|
||||
const agentList: AgentInfo[] = [
|
||||
{
|
||||
story_id: "33_diff_commands",
|
||||
agent_name: "coder-1",
|
||||
status: "running",
|
||||
session_id: null,
|
||||
worktree_path: "/tmp/wt",
|
||||
base_branch: null,
|
||||
},
|
||||
];
|
||||
mockedAgents.listAgents.mockResolvedValue(agentList);
|
||||
|
||||
render(
|
||||
<AgentPanel
|
||||
stories={[
|
||||
{ story_id: "33_diff_commands", name: "Diff Commands", error: null },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const expandButton = await screen.findByText("▶");
|
||||
await userEvent.click(expandButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'cd "/tmp/wt" && git difftool master...HEAD',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user