feat(tui): git view, project-scoped task stats, polished layout/theme

- Add a Git interactive view (hotkey [t]/[4]): branch + ahead/behind,
  recent commits with detail strip, staged/unstaged/untracked files
  panel, branches list, worktrees panel, and a [P] push modal with
  pre-flight commit list and capture of stdout/stderr. ←→ cycles
  status → branches → worktrees → commits → changes; ↑↓ navigates
  rows in the focused list. 5s poll while mounted.
- Project-scoped task stats: BoardView pushes its selected project
  path into the controller; refreshTUIStats now reads from a per-
  project TaskStore via a shared getProjectStore helper. Project
  switch triggers an immediate refresh via onBoardScopeChange.
- Unified MainHeader used by status and interactive modes — section
  tabs ([1]–[5]) and interactive view tabs ([b]/[a]/[g]/[t]) always
  visible; previously the interactive header replaced the main one.
- Logs panel keeps its size when expanding a single entry.
- 'f' cycles severity filter from any panel in status mode.
- BoardView: explicit cross-view shortcuts so g/a/t always switch
  views regardless of input-handler ordering.
- BoardView column overlap fix: flexShrink={0} on structural rows.
- TaskCard always shows the title with wrap="wrap"; single short-id
  pill, no duplicate id-as-title fallback.
- Stats panel reorg: StatRow helper, bold section headers, narrow-
  width wrap for Heap and Memory trailing fragments.
- Lighter blue palette: cyanBright fg accents / cyan active bg /
  logo gradient whiteBright → white → cyanBright → cyan → blue.
- System stats sampler: RSS, heap (V8 limit-aware color), external,
  CPU%, load avg, system used/free. Heap thresholds scale off
  --max-old-space-size automatically.
- Test fixture updated for the new InteractiveData.git block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-24 23:32:45 -07:00
parent 8c9dfc2f56
commit 07d7bac165
10 changed files with 1431 additions and 212 deletions

View File

@@ -0,0 +1,75 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { AuthTokenRecoveryDialog } from "../AuthTokenRecoveryDialog";
import { clearAuthToken, setAuthToken } from "../../auth";
vi.mock("../../auth", () => ({
setAuthToken: vi.fn(),
clearAuthToken: vi.fn(),
}));
describe("AuthTokenRecoveryDialog", () => {
const originalLocation = window.location;
let reloadSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
reloadSpy = vi.fn();
Object.defineProperty(window, "location", {
configurable: true,
value: { ...originalLocation, reload: reloadSpy },
});
});
it("does not render when closed", () => {
render(<AuthTokenRecoveryDialog open={false} />);
expect(screen.queryByRole("dialog", { name: "Authentication token required" })).toBeNull();
});
it("renders a blocking dialog with disabled set button until token is entered", () => {
render(<AuthTokenRecoveryDialog open={true} />);
expect(screen.getByRole("dialog", { name: "Authentication token required" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /close/i })).toBeNull();
const setTokenButton = screen.getByRole("button", { name: "Set token and reload" });
expect(setTokenButton).toBeDisabled();
fireEvent.change(screen.getByLabelText("Replacement token"), { target: { value: "abc123" } });
expect(setTokenButton).toBeEnabled();
});
it("trims and stores replacement token before reloading", () => {
render(<AuthTokenRecoveryDialog open={true} />);
fireEvent.change(screen.getByLabelText("Replacement token"), { target: { value: " new-token " } });
fireEvent.click(screen.getByRole("button", { name: "Set token and reload" }));
expect(setAuthToken).toHaveBeenCalledWith("new-token");
expect(reloadSpy).toHaveBeenCalledTimes(1);
});
it("clears token and reloads when user retries without replacement token", () => {
render(<AuthTokenRecoveryDialog open={true} />);
fireEvent.click(screen.getByRole("button", { name: "Clear token and retry" }));
expect(clearAuthToken).toHaveBeenCalledTimes(1);
expect(reloadSpy).toHaveBeenCalledTimes(1);
});
it("does not dismiss on Escape key", () => {
render(<AuthTokenRecoveryDialog open={true} />);
const overlay = document.querySelector(".auth-token-recovery-overlay");
expect(overlay).toBeTruthy();
if (!overlay) {
throw new Error("Expected auth token recovery overlay");
}
fireEvent.keyDown(overlay, { key: "Escape" });
expect(screen.getByRole("dialog", { name: "Authentication token required" })).toBeInTheDocument();
});
});