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:
2026-10-06 15:38:25 +07:00
parent f0500dc592
commit daf5b7be92
65 changed files with 2414 additions and 587 deletions
@@ -49,7 +49,7 @@ describe("LaneGitHeader", () => {
behind: null,
};
it("renders the live branch, head, subject and uncommitted counts", async () => {
it("renders the live branch, head and subject", async () => {
vi.mocked(api.lanes.git).mockResolvedValueOnce(facts);
render(<Harness laneId={1} />);
@@ -57,7 +57,6 @@ describe("LaneGitHeader", () => {
expect(block.textContent).toContain("feat/rename-metric");
expect(block.textContent).toContain("9b3e74a");
expect(block.textContent).toContain("free-text rule mode in the form");
expect(screen.getByTestId("lane-git-counts").textContent).toContain("2");
});
it("drops the previous lane's facts the moment the lane changes", async () => {
@@ -119,16 +118,26 @@ describe("LaneGitHeader", () => {
render(<LaneGitRailCounts laneId={1} />);
fireEvent.mouseEnter(await screen.findByTestId("lane-git-counts"));
expect(screen.getByTestId("lane-git-files").textContent).toContain("40 files changed");
expect(screen.getByTestId("lane-git-files").textContent).toContain("37 more");
});
it("shows the counts in the open panel's header without the hover list", async () => {
it("opens the Changes panel from the file list's “more files”", async () => {
vi.mocked(api.lanes.git).mockResolvedValueOnce({ ...facts, dirty: 39, untracked: 1 });
const onShowAll = vi.fn();
render(<LaneGitRailCounts laneId={1} onShowAll={onShowAll} />);
fireEvent.mouseEnter(await screen.findByTestId("lane-git-counts"));
fireEvent.click(screen.getByTestId("lane-git-files-more"));
expect(onShowAll).toHaveBeenCalledOnce();
expect(screen.queryByTestId("lane-git-files")).toBeNull();
});
it("shows no counts in the open panel's header: the list below already does", async () => {
vi.mocked(api.lanes.git).mockResolvedValueOnce(facts);
render(<Harness laneId={1} />);
const counts = await screen.findByTestId("lane-git-counts");
fireEvent.mouseEnter(counts);
fireEvent.focus(counts);
expect(screen.queryByTestId("lane-git-files")).toBeNull();
await screen.findByTestId("lane-git");
expect(screen.queryByTestId("lane-git-counts")).toBeNull();
});
it("draws the rail's counts vertical", async () => {