feat: edit in the Changes panel, git graph history, plan quota (v2.3.0)
- Changes panel: a working file opens in a CodeMirror editor with its diff inline (DiffEditor.tsx, @codemirror/merge, lazy-loaded); Changes folds the unchanged lines, Whole file shows them; side by side at 50/50 and full width; a Revert per block; original from HEAD or the branch's merge base (`mergeBase` on GET /git/changes?range=branch) - Every diff: green = line added whole, red = removed whole, blue = edited in place (--diff-mod) - History drawn as a commit graph (#58: `parents` on GET /git/log, --date-order; lib/commitGraph.ts) - Per-workspace never-stage list, drawn orange in the Files tree (--kept); closed Terminal rows past three deleted - Status bar plan quota for Claude (per account) and Codex (GET /api/pricing/quota, server/lib/quota.js); compact hover panel that stays open, Refresh button with a spinner - Settings → Appearance holds language, theme and pet; Ctrl+Shift+C copies; compact sidebar navigation and short session times
This commit is contained in:
@@ -22,6 +22,9 @@ vi.mock("../Settings", () => ({
|
||||
<div data-testid={`settings-${section}`} data-settings-anchor={`${section}-row`} />
|
||||
),
|
||||
}));
|
||||
vi.mock("../../components/settings/PetSettings", () => ({
|
||||
PetSettings: () => <div data-testid="pet-settings" />,
|
||||
}));
|
||||
vi.mock("../CcConfig", () => ({ CcConfig: () => <div data-testid="cc-config" /> }));
|
||||
vi.mock("../CodexConfig", () => ({ CodexConfig: () => <div data-testid="codex-config" /> }));
|
||||
vi.mock("../AiToolConfig", () => ({
|
||||
@@ -114,7 +117,9 @@ describe("SettingsHub", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["/settings", "/settings/general"],
|
||||
["/settings", "/settings/appearance"],
|
||||
["/settings/general", "/settings/appearance"],
|
||||
["/settings/pet", "/settings/appearance"],
|
||||
["/settings?tool=codex", "/settings/agents/codex"],
|
||||
["/settings?tool=claude-code", "/settings/agents/claude-code"],
|
||||
["/settings?tool=gemini", "/settings/agents/gemini"],
|
||||
@@ -125,10 +130,12 @@ describe("SettingsHub", () => {
|
||||
expect(screen.getByTestId("where").textContent).toBe(to);
|
||||
});
|
||||
|
||||
it("sends an unknown section to General once the lists have loaded", async () => {
|
||||
it("sends an unknown section to Appearance once the lists have loaded", async () => {
|
||||
renderAt("/settings/nope");
|
||||
expect(await screen.findByTestId("settings-general")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("where").textContent).toBe("/settings/general");
|
||||
expect(
|
||||
await screen.findByRole("heading", { level: 1, name: "Appearance" })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("where").textContent).toBe("/settings/appearance");
|
||||
});
|
||||
|
||||
it("hides a disabled tool's page and gives an installed, enabled catalog tool one", async () => {
|
||||
@@ -145,8 +152,8 @@ describe("SettingsHub", () => {
|
||||
});
|
||||
|
||||
it("navigates from the mobile select", async () => {
|
||||
renderAt("/settings/general");
|
||||
await screen.findByTestId("settings-general");
|
||||
renderAt("/settings/data");
|
||||
await screen.findByTestId("settings-data");
|
||||
fireEvent.change(screen.getByRole("combobox", { name: "Settings sections" }), {
|
||||
target: { value: "pricing" },
|
||||
});
|
||||
@@ -156,8 +163,8 @@ describe("SettingsHub", () => {
|
||||
|
||||
it("filters the nav by search, opens the first match on Enter and clears on Escape", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderAt("/settings/general");
|
||||
await screen.findByTestId("settings-general");
|
||||
renderAt("/settings/data");
|
||||
await screen.findByTestId("settings-data");
|
||||
const box = screen.getByRole("searchbox", { name: "Search settings" });
|
||||
await user.type(box, "pricing");
|
||||
const links = within(nav())
|
||||
@@ -177,8 +184,8 @@ describe("SettingsHub", () => {
|
||||
|
||||
it("says when nothing matches, and Ctrl+F focuses the search", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderAt("/settings/general");
|
||||
await screen.findByTestId("settings-general");
|
||||
renderAt("/settings/data");
|
||||
await screen.findByTestId("settings-data");
|
||||
fireEvent.keyDown(window, { key: "f", ctrlKey: true });
|
||||
const box = screen.getByRole("searchbox", { name: "Search settings" });
|
||||
expect(box).toHaveFocus();
|
||||
@@ -187,8 +194,8 @@ describe("SettingsHub", () => {
|
||||
});
|
||||
|
||||
it("Ctrl+F leaves the search alone while a modal dialog is open", async () => {
|
||||
renderAt("/settings/general");
|
||||
await screen.findByTestId("settings-general");
|
||||
renderAt("/settings/data");
|
||||
await screen.findByTestId("settings-data");
|
||||
const dialog = document.createElement("div");
|
||||
dialog.setAttribute("role", "dialog");
|
||||
dialog.setAttribute("aria-modal", "true");
|
||||
@@ -204,8 +211,8 @@ describe("SettingsHub", () => {
|
||||
|
||||
it("a second Ctrl+F with the search focused selects its text instead of reaching the browser", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderAt("/settings/general");
|
||||
await screen.findByTestId("settings-general");
|
||||
renderAt("/settings/data");
|
||||
await screen.findByTestId("settings-data");
|
||||
const box = screen.getByRole<HTMLInputElement>("searchbox", { name: "Search settings" });
|
||||
fireEvent.keyDown(window, { key: "f", ctrlKey: true });
|
||||
await user.type(box, "pet");
|
||||
@@ -217,8 +224,8 @@ describe("SettingsHub", () => {
|
||||
|
||||
it("lists matching rows and deep-links a Claude Code sub-tab", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderAt("/settings/general");
|
||||
await screen.findByTestId("settings-general");
|
||||
renderAt("/settings/data");
|
||||
await screen.findByTestId("settings-data");
|
||||
await user.type(screen.getByRole("searchbox", { name: "Search settings" }), "marketplaces");
|
||||
const list = screen.getByRole("list", { name: "Search results" });
|
||||
await user.click(within(list).getAllByRole("link")[0]!);
|
||||
@@ -227,14 +234,16 @@ describe("SettingsHub", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("puts Pricing in its own Cost group and splits General into General, Data and About", async () => {
|
||||
it("puts Pricing in its own Cost group and folds language and pet into Appearance", async () => {
|
||||
renderAt("/settings/data");
|
||||
expect(await screen.findByTestId("settings-data")).toBeInTheDocument();
|
||||
expect(within(nav()).getByText("Cost")).toBeInTheDocument();
|
||||
const links = within(nav())
|
||||
.getAllByRole("link")
|
||||
.map((a) => a.textContent);
|
||||
expect(links.slice(0, 3)).toEqual(["General", "Data", "About"]);
|
||||
expect(links.slice(0, 3)).toEqual(["Data", "About", "Appearance"]);
|
||||
expect(links).not.toContain("General");
|
||||
expect(links).not.toContain("Pet");
|
||||
});
|
||||
|
||||
it("scrolls to and highlights the anchor the URL names", async () => {
|
||||
@@ -258,11 +267,11 @@ describe("SettingsHub", () => {
|
||||
expect(backDelta(0, 2)).toBe(-1);
|
||||
});
|
||||
|
||||
it("toasts when an unknown section falls back to General", async () => {
|
||||
it("toasts when an unknown section falls back to Appearance", async () => {
|
||||
let seen: ToastItem[] = [];
|
||||
const off = subscribeToasts((i) => (seen = i));
|
||||
renderAt("/settings/nope");
|
||||
await screen.findByTestId("settings-general");
|
||||
await screen.findByTestId("pet-settings");
|
||||
expect(seen.some((x) => x.message.includes("nope"))).toBe(true);
|
||||
off();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user