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:
@@ -4,12 +4,13 @@
|
||||
* state with the version (click opens the connection details), the running and
|
||||
* needs-you SESSION counts, today's spend (`date=` on the pricing cost call)
|
||||
* with its tokens and a per-model hover list, and on the Workspace page the
|
||||
* open workspace's and the shown session's totals.
|
||||
* open workspace's and the shown session's totals; the right-end quota chips
|
||||
* (fullest window per tool) and their hover panel.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
@@ -68,6 +69,42 @@ vi.mock("../../lib/api", () => ({
|
||||
],
|
||||
daily_costs: [],
|
||||
})),
|
||||
quota: vi.fn().mockResolvedValue({
|
||||
tools: [
|
||||
{
|
||||
id: "claude",
|
||||
active: true,
|
||||
label: "Work",
|
||||
account: "me@example.com",
|
||||
expiresAt: new Date(Date.now() + 3 * 3600_000 + 30_000).toISOString(),
|
||||
windows: [
|
||||
{
|
||||
label: "5h",
|
||||
usedPercent: 95,
|
||||
resetsAt: new Date(Date.now() + 41 * 60_000 + 30_000).toISOString(),
|
||||
},
|
||||
{ label: "wk", usedPercent: 32, resetsAt: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "claude",
|
||||
active: false,
|
||||
label: "Personal",
|
||||
account: "home@example.com",
|
||||
expiresAt: "2020-01-01T00:00:00.000Z",
|
||||
windows: [],
|
||||
error: "EPROFILEEXPIRED",
|
||||
},
|
||||
{
|
||||
id: "codex",
|
||||
account: "dev@example.com",
|
||||
expiresAt: "2020-01-01T00:00:00.000Z",
|
||||
windows: [],
|
||||
error: "EUPSTREAM",
|
||||
},
|
||||
],
|
||||
checkedAt: "",
|
||||
}),
|
||||
sessionCost: vi.fn().mockImplementation(async (id: string) => ({
|
||||
total_cost: id === "s-live" ? 4.5 : 7,
|
||||
breakdown: [],
|
||||
@@ -144,4 +181,52 @@ describe("StatusBar", () => {
|
||||
});
|
||||
expect(await screen.findByText(/^Session \$4\.50/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows each tool's fullest quota window and the details on hover", async () => {
|
||||
const user = userEvent.setup();
|
||||
at("/", <StatusBar wsConnected left="0" onConnectionClick={() => {}} />);
|
||||
const chips = await screen.findByTestId("status-bar-quota");
|
||||
expect(chips).toHaveTextContent("Claude95% used");
|
||||
expect(chips.textContent?.match(/Claude/g)).toHaveLength(1);
|
||||
expect(screen.getByText("95% used")).toHaveClass("text-status-danger");
|
||||
await user.hover(chips);
|
||||
const panel = screen.getByTestId("status-bar-quota-panel");
|
||||
const [work, personal, codex] = screen.getAllByTestId("status-bar-quota-account");
|
||||
expect(work).toHaveTextContent("Workin use");
|
||||
expect(work).toHaveTextContent("me@example.com · Sign-in expires in 3h 0m");
|
||||
expect(work).toHaveTextContent("5h95%41m");
|
||||
expect(work).toHaveTextContent("Week32%");
|
||||
expect(personal).toHaveTextContent("home@example.com · Sign-in expired");
|
||||
expect(personal).toHaveTextContent("Check quota in Claude Code → Profiles");
|
||||
expect(personal).not.toHaveTextContent("in use");
|
||||
expect(codex).toHaveTextContent("dev@example.com");
|
||||
expect(codex).toHaveTextContent("Sign-in expired");
|
||||
expect(panel).toHaveTextContent("Could not read the quota");
|
||||
// Moving onto the panel keeps it open, so it can be read; leaving it closes it.
|
||||
await user.unhover(chips);
|
||||
await user.hover(panel);
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
expect(screen.getByTestId("status-bar-quota-panel")).toBeInTheDocument();
|
||||
// Only the footer button refreshes: it spins and says so until the read lands.
|
||||
const same = await api.pricing.quota(false);
|
||||
let answer!: (v: unknown) => void;
|
||||
vi.mocked(api.pricing.quota)
|
||||
.mockClear()
|
||||
.mockReturnValueOnce(new Promise((r) => (answer = r)) as never);
|
||||
const refresh = screen.getByTestId("status-bar-quota-refresh");
|
||||
await user.click(refresh);
|
||||
expect(api.pricing.quota).toHaveBeenLastCalledWith(true);
|
||||
expect(refresh).toHaveTextContent("Refreshing…");
|
||||
expect(refresh).toHaveAttribute("aria-busy", "true");
|
||||
await user.click(refresh);
|
||||
expect(api.pricing.quota).toHaveBeenCalledTimes(1);
|
||||
await act(async () => answer(same));
|
||||
expect(screen.getByTestId("status-bar-quota-refresh")).toHaveTextContent("Refresh");
|
||||
expect(screen.getByTestId("status-bar-quota-panel")).toBeInTheDocument();
|
||||
await user.unhover(panel);
|
||||
await waitFor(() => expect(screen.queryByTestId("status-bar-quota-panel")).toBeNull());
|
||||
vi.mocked(api.pricing.quota).mockClear();
|
||||
await user.click(chips);
|
||||
expect(api.pricing.quota).toHaveBeenLastCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user