From 7b2766b6371a12c363f6f83539e730fe900afa38 Mon Sep 17 00:00:00 2001 From: omartelo Date: Sat, 25 Jul 2026 11:14:38 -0300 Subject: [PATCH 1/5] chore(frontend): adopt biome for formatting and lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frontend had no formatter. 80 files wrote `import {x}`, 54 wrote `import { x }`, and no single file mixed the two — a clean split down the middle, with a few files (SessionCard, WorktreeDialog) carrying indentation no tool would produce. eslint was never installed either, so the five `eslint-disable` directives in the tree were addressing nobody. biome replaces both in one binary. `biome.jsonc` encodes the style the code already mostly held — 2-space indent, double quotes, no semicolons — at lineWidth 100, picked by measuring the reflow at 80/88/96/100/110 and taking the least-churn width. Import order stays unenforced: it is an opinion of its own and would bury the reflow it rode in with. Rules that were wrong every time they fired are off, each with its reason in the config: useExhaustiveDependencies (narrow deps are the pattern here — poll signals, once-per-launch gates, refs read for the latest value) and useOptionalChain (`!x || !x.y` states the guard; `!x?.y` folds two reads into one). The a11y group is warn rather than error — those are real findings, but their fixes are DOM changes, and they cluster in the duplicated close affordance and resize handle that the component extraction collapses into one place to fix. `useImportType` is off under `components/ui`, where the source is shadcn CLI output and rewriting it only makes the next `shadcn add` diverge. Everything else the linter flagged is fixed here, and nothing in it changes behaviour: - ClaudePluginGate types `let action` as PluginAction, matching AppUpdateGate. - providers-store and useSessionStatus stop returning the callee's value out of a forEach callback. - WorktreeDialog's local `valueOf` is renamed off Object.prototype's name. - FilesPanel and SessionCard use template literals over concatenation. - Deliberate exceptions carry a `biome-ignore` naming the reason: the palette and rename autofocus, the changelog's positional keys, the branch validator's control-character class, and the generic Label primitive. CI gains a `pnpm check` step beside gofmt and vet, and both CLAUDE.md gates list it. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 6 + CLAUDE.md | 1 + frontend/CLAUDE.md | 14 ++ frontend/biome.jsonc | 65 +++++++ frontend/package.json | 7 +- frontend/pnpm-lock.yaml | 95 ++++++++++ frontend/src/App.tsx | 11 +- frontend/src/components/AppUpdateGate.tsx | 34 ++-- frontend/src/components/ClaudePluginGate.tsx | 28 +-- frontend/src/components/CommandPalette.tsx | 6 +- frontend/src/components/ConfirmDialog.tsx | 4 +- frontend/src/components/ContextRing.test.ts | 4 +- frontend/src/components/ContextRing.tsx | 4 +- frontend/src/components/DiffStat.tsx | 8 +- frontend/src/components/FileTree.tsx | 38 ++-- frontend/src/components/FooterBar.tsx | 148 +++++++--------- frontend/src/components/Home.tsx | 4 +- frontend/src/components/Markdown.tsx | 10 +- frontend/src/components/PatchNotesDialog.tsx | 16 +- frontend/src/components/PatchNotesGate.tsx | 10 +- .../src/components/ResumeSessionDialog.tsx | 12 +- frontend/src/components/SessionModel.tsx | 10 +- frontend/src/components/TerminalHost.tsx | 23 +-- frontend/src/components/TerminalView.tsx | 17 +- .../src/components/diff/DiscardDialog.tsx | 12 +- frontend/src/components/diff/FileDiff.tsx | 64 +++---- frontend/src/components/diff/ReviewPanel.tsx | 26 +-- frontend/src/components/diff/useDiffEditor.ts | 12 +- frontend/src/components/dock/FilesPanel.tsx | 71 +++----- frontend/src/components/dock/RightDock.tsx | 73 ++++---- frontend/src/components/dock/useFileEditor.ts | 10 +- frontend/src/components/pulls/Pulls.tsx | 166 +++++++++++------- frontend/src/components/pulls/PullsChecks.tsx | 32 ++-- frontend/src/components/pulls/PullsFiles.tsx | 45 +++-- .../src/components/settings/FontSetting.tsx | 5 +- .../components/settings/HotkeysSettings.tsx | 3 +- .../components/settings/ProvidersSettings.tsx | 4 +- .../components/settings/SegmentedControl.tsx | 4 +- .../src/components/settings/SettingBlock.tsx | 12 +- frontend/src/components/settings/Settings.tsx | 7 +- .../components/settings/UpdatesSettings.tsx | 34 ++-- .../sidebar/CloseWorktreeDialog.tsx | 10 +- .../components/sidebar/PullRequestCard.tsx | 14 +- .../src/components/sidebar/SessionCard.tsx | 115 ++++++------ .../src/components/sidebar/SessionGroup.tsx | 22 +-- .../src/components/sidebar/SessionSidebar.tsx | 71 ++++---- .../components/sidebar/SessionStatusIcon.tsx | 10 +- .../src/components/sidebar/WorktreeDialog.tsx | 93 +++++----- .../components/tabs/NotificationsButton.tsx | 47 ++--- frontend/src/components/tabs/ProjectTab.tsx | 22 +-- frontend/src/components/tabs/ProjectTabs.tsx | 41 +++-- frontend/src/components/ui/button.tsx | 2 +- frontend/src/components/ui/card.tsx | 29 +-- frontend/src/components/ui/checkbox.tsx | 4 +- frontend/src/components/ui/context-menu.tsx | 70 +++----- frontend/src/components/ui/dialog.tsx | 42 ++--- frontend/src/components/ui/dropdown-menu.tsx | 50 +++--- frontend/src/components/ui/input.tsx | 2 +- frontend/src/components/ui/label.tsx | 3 +- frontend/src/components/ui/select.tsx | 39 ++-- frontend/src/components/ui/separator.tsx | 8 +- frontend/src/components/ui/switch.tsx | 2 +- frontend/src/components/ui/tabs.tsx | 15 +- frontend/src/components/ui/tooltip.tsx | 20 +-- frontend/src/lib/app-events.test.ts | 6 +- frontend/src/lib/app-events.ts | 5 +- frontend/src/lib/app-update-gate.test.ts | 25 +-- frontend/src/lib/app-update-gate.ts | 14 +- frontend/src/lib/branch-name.ts | 6 +- frontend/src/lib/card-store.test.ts | 4 +- frontend/src/lib/check-duration.test.ts | 4 +- frontend/src/lib/codemirror.ts | 49 ++---- frontend/src/lib/command-palette.test.ts | 4 +- frontend/src/lib/diff.test.ts | 27 ++- frontend/src/lib/diff.ts | 22 ++- frontend/src/lib/file-icon.tsx | 56 +++--- frontend/src/lib/file-tree.test.ts | 10 +- frontend/src/lib/file-tree.ts | 4 +- frontend/src/lib/git-status-store.test.ts | 18 +- frontend/src/lib/git-status-store.ts | 5 +- frontend/src/lib/hotkeys.test.ts | 9 +- frontend/src/lib/hotkeys.ts | 16 +- frontend/src/lib/lang-badge.test.ts | 6 +- frontend/src/lib/lang-badge.ts | 36 ++-- frontend/src/lib/model-name.test.ts | 4 +- frontend/src/lib/notifications.test.ts | 34 ++-- frontend/src/lib/notifications.ts | 7 +- frontend/src/lib/panel-width.test.ts | 15 +- frontend/src/lib/paste-queue.test.ts | 4 +- frontend/src/lib/patch-notes-gate.test.ts | 18 +- frontend/src/lib/patch-notes-gate.ts | 16 +- frontend/src/lib/plugin-gate.test.ts | 32 ++-- frontend/src/lib/plugin-gate.ts | 15 +- frontend/src/lib/projects.tsx | 50 +++--- frontend/src/lib/provider-icons.tsx | 6 +- frontend/src/lib/providers-store.ts | 25 +-- frontend/src/lib/pull-request-lookup.test.ts | 6 +- frontend/src/lib/pull-request-lookup.ts | 4 +- frontend/src/lib/pull-request-viewed.test.ts | 8 +- frontend/src/lib/pull-request-viewed.ts | 2 +- frontend/src/lib/pulls-card-store.test.ts | 6 +- frontend/src/lib/pulls-card-store.ts | 2 +- frontend/src/lib/replay-buffer.test.ts | 4 +- frontend/src/lib/rpc.test.ts | 4 +- frontend/src/lib/rpc.ts | 17 +- frontend/src/lib/session-agent-store.test.ts | 50 +++--- frontend/src/lib/session-agent-store.ts | 8 +- frontend/src/lib/session-cwd-store.test.ts | 40 ++--- frontend/src/lib/session-cwd-store.ts | 6 +- frontend/src/lib/session-events.test.ts | 62 +++---- frontend/src/lib/session-events.ts | 42 ++--- frontend/src/lib/session-status-store.test.ts | 74 ++++---- frontend/src/lib/session-status-store.ts | 24 +-- frontend/src/lib/session-usage-store.test.ts | 74 +++++--- frontend/src/lib/session-usage-store.ts | 6 +- frontend/src/lib/sessions.test.ts | 22 +-- frontend/src/lib/sessions.ts | 23 +-- frontend/src/lib/settings-card-store.ts | 2 +- frontend/src/lib/settings.tsx | 22 +-- frontend/src/lib/setup-queue.test.ts | 4 +- frontend/src/lib/term-fit.ts | 6 +- frontend/src/lib/term-frame.test.ts | 4 +- frontend/src/lib/term-keys.test.ts | 52 +++--- frontend/src/lib/term-keys.ts | 8 +- frontend/src/lib/term-perf.ts | 1 - frontend/src/lib/term-transport.ts | 4 +- frontend/src/lib/toast-async.test.ts | 8 +- frontend/src/lib/toast-async.ts | 8 +- frontend/src/lib/update-check.test.ts | 6 +- frontend/src/lib/update-check.ts | 2 +- frontend/src/lib/use-panel-width.ts | 25 +-- frontend/src/lib/use-sortable-list.ts | 4 +- frontend/src/lib/useActiveSession.ts | 10 +- frontend/src/lib/useGitStatus.ts | 19 +- frontend/src/lib/usePullRequest.ts | 8 +- frontend/src/lib/usePullRequestDetail.ts | 18 +- frontend/src/lib/usePullRequestDiff.ts | 10 +- frontend/src/lib/useSessionAgent.ts | 10 +- frontend/src/lib/useSessionCwd.ts | 8 +- frontend/src/lib/useSessionStatus.ts | 24 ++- frontend/src/lib/useSessionUsage.ts | 12 +- frontend/src/main.tsx | 10 +- frontend/vite.config.ts | 10 +- 143 files changed, 1581 insertions(+), 1636 deletions(-) create mode 100644 frontend/biome.jsonc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38ea6b04f..9ed9836c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,12 @@ jobs: pnpm install --frozen-lockfile pnpm build + # biome is the frontend's gofmt + vet: formatting and lint in one pass. + # Warnings (the a11y backlog) are reported without failing; errors fail. + - name: Frontend format + lint + working-directory: frontend + run: pnpm check + - name: gofmt run: | unformatted="$(gofmt -l .)" diff --git a/CLAUDE.md b/CLAUDE.md index dd92d6780..9cc9b195a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,6 +41,7 @@ Frontend in isolation: `cd frontend && pnpm run build` (runs `tsc` + `vite build ## Local Gate (before every commit / PR) - `gofmt -l .` clean (fix with `gofmt -w .`) and `go vet ./...` clean. +- `cd frontend && pnpm check` clean — biome is the frontend's gofmt + vet (fix with `pnpm format`). - `go test ./...` (backend) and `cd frontend && pnpm test` (frontend) green — or `task test` for both. - `cd frontend && pnpm build` succeeds (tsc typecheck + vite). - Touched an OS seam or a `_test.go` build tag? Run the same cross-compile loop CI runs: diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index af10ee604..dfb0ba0c8 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -28,11 +28,25 @@ React 18 + TypeScript + Vite, built to a static bundle that the Go binary embeds ```bash pnpm build # tsc typecheck + vite build (the real gate) pnpm test # vitest run +pnpm check # biome format + lint check (CI runs this; errors fail) +pnpm format # biome, writing the fixes ``` Wart: `pnpm test`/`build` can trip an implicit install that dies on `ERR_PNPM_IGNORED_BUILDS`. When it does, run the binaries directly: `./node_modules/.bin/vitest run` or `pnpm exec vitest run`. +## Formatting and lint — biome + +**biome** (`biome.jsonc`) is the frontend's gofmt + vet, in one binary. There is no prettier and no eslint; +`eslint-disable` comments are dead here — the directive is `biome-ignore : `. + +- 2-space indent, double quotes, no semicolons, `lineWidth` 100 (measured as the least-churn width over the + source at adoption). +- A few recommended rules are off on purpose, each with its reason in `biome.jsonc` — read it before turning + one back on. Import order is deliberately not enforced. +- Warnings do not fail the gate; errors do. The standing warning backlog is the a11y one: `role="button"` + spans that take a click but no key, and the `role="separator"` resize handles. + ## State — no state library zustand and tanstack-query were both rejected (PR #13). The pattern is a **module-level store + diff --git a/frontend/biome.jsonc b/frontend/biome.jsonc new file mode 100644 index 000000000..ab4e6791e --- /dev/null +++ b/frontend/biome.jsonc @@ -0,0 +1,65 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.5/schema.json", + "files": { + "includes": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "vitest.config.ts"] + }, + // The style the codebase already mostly held, so adopting the tool is a + // reflow, not a restyle: 2-space indent, double quotes, no semicolons. + // lineWidth 100 measured as the least-churn width over the existing source. + "formatter": { + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded", + "quoteStyle": "double", + "jsxQuoteStyle": "double", + "arrowParentheses": "always", + "trailingCommas": "all", + "bracketSpacing": true + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + // Narrow dependency lists are the app's pattern, not an oversight: the + // git-status poll drives invalidation off status counts, gates run once + // per launch, and the latest project/session is read through a ref. + // Every hit of this rule so far has been deliberate. + "useExhaustiveDependencies": "off" + }, + "complexity": { + // `!x || !x.y` states the guard; `!x?.y` folds two conditions into one + // read. Equivalent, and the explicit form is the one kept here. + "useOptionalChain": "off" + }, + "a11y": { + // Reported, not enforced: the fixes are DOM changes, and the offenders + // are the duplicated close-affordance and resize handle that the + // component extraction collapses into one place to fix. + "useFocusableInteractive": "warn", + "useKeyWithClickEvents": "warn", + "useSemanticElements": "warn", + "useAriaPropsForRole": "warn", + "useAriaPropsSupportedByRole": "warn" + } + } + }, + // Import order is an opinion of its own; keep it out of the format pass. + "assist": { + "actions": { "source": { "organizeImports": "off" } } + }, + "overrides": [ + { + // shadcn primitives are CLI output adapted to DESIGN.md, not hand-written + // (see CLAUDE.md). Their `import * as React` is what the generator emits; + // rewriting it only makes the next `shadcn add` diverge. + "includes": ["src/components/ui/**"], + "linter": { "rules": { "style": { "useImportType": "off" } } } + } + ] +} diff --git a/frontend/package.json b/frontend/package.json index bc12b96bc..ffb7624a3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "react-ts-latest", + "name": "lich-frontend", "private": true, "version": "0.0.0", "type": "module", @@ -9,7 +9,9 @@ "build:dev": "tsc && vite build --minify false --mode development", "build": "tsc && vite build --mode production", "preview": "vite preview", - "test": "vitest run" + "test": "vitest run", + "check": "biome check .", + "format": "biome check --write ." }, "dependencies": { "@base-ui/react": "^1.6.0", @@ -42,6 +44,7 @@ "tw-animate-css": "^1.4.0" }, "devDependencies": { + "@biomejs/biome": "^2.5.5", "@tailwindcss/vite": "^4.3.2", "@types/node": "^26.1.0", "@types/react": "^18.2.43", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index c4fe8020c..dc5096ca6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -93,6 +93,9 @@ importers: specifier: ^1.4.0 version: 1.4.0 devDependencies: + '@biomejs/biome': + specifier: ^2.5.5 + version: 2.5.5 '@tailwindcss/vite': specifier: ^4.3.2 version: 4.3.2(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)) @@ -290,6 +293,63 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@biomejs/biome@2.5.5': + resolution: {integrity: sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.5': + resolution: {integrity: sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.5': + resolution: {integrity: sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.5': + resolution: {integrity: sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.5': + resolution: {integrity: sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.5': + resolution: {integrity: sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.5': + resolution: {integrity: sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.5': + resolution: {integrity: sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.5': + resolution: {integrity: sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@codemirror/autocomplete@6.20.3': resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} @@ -2769,6 +2829,41 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@biomejs/biome@2.5.5': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.5 + '@biomejs/cli-darwin-x64': 2.5.5 + '@biomejs/cli-linux-arm64': 2.5.5 + '@biomejs/cli-linux-arm64-musl': 2.5.5 + '@biomejs/cli-linux-x64': 2.5.5 + '@biomejs/cli-linux-x64-musl': 2.5.5 + '@biomejs/cli-win32-arm64': 2.5.5 + '@biomejs/cli-win32-x64': 2.5.5 + + '@biomejs/cli-darwin-arm64@2.5.5': + optional: true + + '@biomejs/cli-darwin-x64@2.5.5': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.5': + optional: true + + '@biomejs/cli-linux-arm64@2.5.5': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.5': + optional: true + + '@biomejs/cli-linux-x64@2.5.5': + optional: true + + '@biomejs/cli-win32-arm64@2.5.5': + optional: true + + '@biomejs/cli-win32-x64@2.5.5': + optional: true + '@codemirror/autocomplete@6.20.3': dependencies: '@codemirror/language': 6.12.4 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3394018b7..835018cc8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,8 +22,7 @@ import { CommandPalette } from "@/components/CommandPalette" // top of the terminals. function Layout() { const [dock, setDock] = useState(null) - const toggleDock = (tab: DockTab) => - setDock((cur) => (cur === tab ? null : tab)) + const toggleDock = (tab: DockTab) => setDock((cur) => (cur === tab ? null : tab)) return (
@@ -36,13 +35,7 @@ function Layout() {
- {dock && ( - setDock(null)} - /> - )} + {dock && setDock(null)} />} diff --git a/frontend/src/components/AppUpdateGate.tsx b/frontend/src/components/AppUpdateGate.tsx index ee23ac993..e8abdabb0 100644 --- a/frontend/src/components/AppUpdateGate.tsx +++ b/frontend/src/components/AppUpdateGate.tsx @@ -1,13 +1,13 @@ -import {useEffect, useRef} from "react" -import {toast} from "sonner" -import {useMatch, useNavigate} from "react-router-dom" -import {Button} from "@/components/ui/button" -import {decideUpdateAction, UPDATE_DISMISSED_KEY, type UpdateAction} from "@/lib/app-update-gate" -import {AppUpdate, System} from "@/lib/rpc" -import {useProjects} from "@/lib/projects" -import {queuePaste} from "@/lib/paste-queue" -import {registerUpdateChecker} from "@/lib/update-check" -import {errorText} from "@/lib/utils" +import { useEffect, useRef } from "react" +import { toast } from "sonner" +import { useMatch, useNavigate } from "react-router-dom" +import { Button } from "@/components/ui/button" +import { decideUpdateAction, UPDATE_DISMISSED_KEY, type UpdateAction } from "@/lib/app-update-gate" +import { AppUpdate, System } from "@/lib/rpc" +import { useProjects } from "@/lib/projects" +import { queuePaste } from "@/lib/paste-queue" +import { registerUpdateChecker } from "@/lib/update-check" +import { errorText } from "@/lib/utils" // How often to re-check for a release after startup, so a long-running session // eventually notices one. Hourly is plenty — releases are rare and the @@ -20,7 +20,7 @@ const POLL_INTERVAL_MS = 60 * 60 * 1000 // paste the install command into a terminal (the user runs it) or open the // release page. Any failure is silent — it must never block or break startup. export function AppUpdateGate() { - const {newSession, ensureHomeProject} = useProjects() + const { newSession, ensureHomeProject } = useProjects() const navigate = useNavigate() const activeProjectId = useMatch("/projects/:projectId")?.params.projectId ?? null @@ -43,7 +43,7 @@ export function AppUpdateGate() { return () => registerUpdateChecker(null) }) - const prompt = (action: UpdateAction & {kind: "update"}) => { + const prompt = (action: UpdateAction & { kind: "update" }) => { promptedVersion.current = action.version if (action.canSelfApply) { promptSelfApply(action.version) @@ -81,8 +81,8 @@ export function AppUpdateGate() { const promptSelfApply = (version: string) => { toast(`lich ${version} is available`, { duration: Infinity, - action: {label: "Update & install", onClick: () => void runApply()}, - cancel: {label: "Later", onClick: () => dismiss(version)}, + action: { label: "Update & install", onClick: () => void runApply() }, + cancel: { label: "Later", onClick: () => dismiss(version) }, }) } @@ -94,13 +94,13 @@ export function AppUpdateGate() { try { await AppUpdate.Apply() } catch (error) { - toast.error(`Update failed: ${errorText(error)}`, {id}) + toast.error(`Update failed: ${errorText(error)}`, { id }) return } toast.success("lich updated", { id, duration: Infinity, - action: {label: "Restart", onClick: () => void runRestart()}, + action: { label: "Restart", onClick: () => void runRestart() }, }) } @@ -155,7 +155,7 @@ export function AppUpdateGate() { ), - {duration: Infinity}, + { duration: Infinity }, ) } diff --git a/frontend/src/components/ClaudePluginGate.tsx b/frontend/src/components/ClaudePluginGate.tsx index 945552e41..0b512f791 100644 --- a/frontend/src/components/ClaudePluginGate.tsx +++ b/frontend/src/components/ClaudePluginGate.tsx @@ -1,6 +1,6 @@ -import {useEffect, useRef, useState} from "react" -import {toast} from "sonner" -import {Button} from "@/components/ui/button" +import { useEffect, useRef, useState } from "react" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" import { Dialog, DialogContent, @@ -13,11 +13,12 @@ import { decidePluginAction, DISMISSED_FLAG, INSTALL_DISMISSED_KEY, + type PluginAction, UPDATE_DISMISSED_KEY, } from "@/lib/plugin-gate" -import {ClaudePlugin} from "@/lib/rpc" -import {errorText} from "@/lib/utils" -import {runWithToast} from "@/lib/toast-async" +import { ClaudePlugin } from "@/lib/rpc" +import { errorText } from "@/lib/utils" +import { runWithToast } from "@/lib/toast-async" const RESTART_HINT = "restart your Claude sessions to apply." @@ -39,7 +40,7 @@ export function ClaudePluginGate() { }, []) const check = async () => { - let action + let action: PluginAction try { const status = await ClaudePlugin.Status() action = decidePluginAction( @@ -60,7 +61,7 @@ export function ClaudePluginGate() { const promptUpdate = (version: string) => { toast(`lich plugin ${version} is available`, { duration: Infinity, - action: {label: "Update", onClick: () => void runUpdate()}, + action: { label: "Update", onClick: () => void runUpdate() }, cancel: { label: "Later", onClick: () => localStorage.setItem(UPDATE_DISMISSED_KEY, version), @@ -95,14 +96,17 @@ export function ClaudePluginGate() { } return ( - !open && !installing && setInstallOpen(false)}> + !open && !installing && setInstallOpen(false)} + > Enable the Claude Code integration - Install the lich plugin for Claude Code to get the most out of lich. - It deepens the integration between your sessions and the app, and gains - new capabilities with each release. + Install the lich plugin for Claude Code to get the most out of lich. It deepens the + integration between your sessions and the app, and gains new capabilities with each + release. diff --git a/frontend/src/components/CommandPalette.tsx b/frontend/src/components/CommandPalette.tsx index 57f17a738..14c4a29e6 100644 --- a/frontend/src/components/CommandPalette.tsx +++ b/frontend/src/components/CommandPalette.tsx @@ -103,6 +103,7 @@ export function CommandPalette() {
setQuery(e.target.value)} @@ -197,7 +198,10 @@ function Row({ selected, onSelect, onRun, children }: RowProps) { > {children} ) diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx index ad4b379a6..f78d733fe 100644 --- a/frontend/src/components/ConfirmDialog.tsx +++ b/frontend/src/components/ConfirmDialog.tsx @@ -38,9 +38,7 @@ export function ConfirmDialog({ {title} - - {description} - + {description} -
diff --git a/frontend/src/components/diff/DiscardDialog.tsx b/frontend/src/components/diff/DiscardDialog.tsx index cd51f820c..5b90f2be4 100644 --- a/frontend/src/components/diff/DiscardDialog.tsx +++ b/frontend/src/components/diff/DiscardDialog.tsx @@ -1,6 +1,6 @@ -import {Button} from "@/components/ui/button" -import {ConfirmDialog} from "@/components/ConfirmDialog" -import type {DiffFile} from "@/lib/diff" +import { Button } from "@/components/ui/button" +import { ConfirmDialog } from "@/components/ConfirmDialog" +import type { DiffFile } from "@/lib/diff" interface DiscardDialogProps { /** The file whose changes are about to be reverted, or null when hidden. */ @@ -12,7 +12,7 @@ interface DiscardDialogProps { // DiscardDialog confirms reverting one file's uncommitted changes: a tracked // file goes back to HEAD, a new file is deleted from disk. Either way the // changes are gone for good. -export function DiscardDialog({file, onCancel, onDiscard}: DiscardDialogProps) { +export function DiscardDialog({ file, onCancel, onDiscard }: DiscardDialogProps) { return ( Revert all uncommitted changes to{" "} {file?.newPath}? - {file?.status === "added" && " The file will be deleted from disk."}{" "} - This cannot be undone. + {file?.status === "added" && " The file will be deleted from disk."} This cannot be + undone. } > diff --git a/frontend/src/components/diff/FileDiff.tsx b/frontend/src/components/diff/FileDiff.tsx index dea6cea61..a2c07203c 100644 --- a/frontend/src/components/diff/FileDiff.tsx +++ b/frontend/src/components/diff/FileDiff.tsx @@ -1,18 +1,14 @@ -import {useEffect, useMemo, useRef, useState} from "react" -import type {ReactNode} from "react" -import {ChevronDown, ChevronRight, Paperclip, Undo2} from "lucide-react" +import { useEffect, useMemo, useRef, useState } from "react" +import type { ReactNode } from "react" +import { ChevronDown, ChevronRight, Paperclip, Undo2 } from "lucide-react" import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu" -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip" -import {Checkbox} from "@/components/ui/checkbox" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { Checkbox } from "@/components/ui/checkbox" import { buildFileDoc, formatLineRef, @@ -21,10 +17,10 @@ import { type FileDoc, type NewLineRange, } from "@/lib/diff" -import {languageAbbr, splitPath} from "@/lib/lang-badge" -import {cn} from "@/lib/utils" -import {DiffStat} from "@/components/DiffStat" -import {useDiffEditor} from "./useDiffEditor" +import { languageAbbr, splitPath } from "@/lib/lang-badge" +import { cn } from "@/lib/utils" +import { DiffStat } from "@/components/DiffStat" +import { useDiffEditor } from "./useDiffEditor" // Files whose rendered diff exceeds this many lines start collapsed, so one // giant lockfile doesn't swamp the panel (expanding is one click away). @@ -55,11 +51,9 @@ interface FileDiffProps { // The card must not clip overflow — a clipping ancestor would break the // sticky header. -export function FileDiff({file, onInject, onDiscard, bulk, viewed, onViewed}: FileDiffProps) { +export function FileDiff({ file, onInject, onDiscard, bulk, viewed, onViewed }: FileDiffProps) { const doc = useMemo(() => buildFileDoc(file), [file]) - const [expanded, setExpanded] = useState( - !file.binary && doc.lineMeta.length <= LARGE_FILE_LINES, - ) + const [expanded, setExpanded] = useState(!file.binary && doc.lineMeta.length <= LARGE_FILE_LINES) // The nonce guard skips the initial mount so each file keeps its own // large-file default until the user actually triggers a bulk action. const lastNonce = useRef(bulk?.nonce) @@ -71,7 +65,7 @@ export function FileDiff({file, onInject, onDiscard, bulk, viewed, onViewed}: Fi }, [bulk]) const Chevron = expanded ? ChevronDown : ChevronRight const badge = languageAbbr(file.newPath) - const {dir, base} = splitPath(file.newPath) + const { dir, base } = splitPath(file.newPath) return (
@@ -89,7 +83,7 @@ export function FileDiff({file, onInject, onDiscard, bulk, viewed, onViewed}: Fi viewed && "opacity-60", )} > - + @@ -98,19 +92,17 @@ export function FileDiff({file, onInject, onDiscard, bulk, viewed, onViewed}: Fi {file.status === "renamed" ? `${file.oldPath} → ${base}` : base} - {dir && ( - {dir} - )} + {dir && {dir}} - + onInject(`@${file.newPath} `)}> - + {onDiscard && ( - + )} {onViewed && ( @@ -136,7 +128,7 @@ export function FileDiff({file, onInject, onDiscard, bulk, viewed, onViewed}: Fi (file.binary ? (

Binary file

) : ( - + ))}
) @@ -148,7 +140,7 @@ interface HeaderActionProps { children: ReactNode } -export function HeaderAction({label, onClick, children}: HeaderActionProps) { +export function HeaderAction({ label, onClick, children }: HeaderActionProps) { return ( (null) // Resolve the selection when the menu opens, not on every selection change. @@ -188,25 +180,19 @@ function DiffBody({doc, path, onInject}: DiffBodyProps) { return } const selected = getSelectedDocLines() - setRange( - selected ? newLineRange(doc.lineMeta, selected.from, selected.to) : null, - ) + setRange(selected ? newLineRange(doc.lineMeta, selected.from, selected.to) : null) } return ( - }/> + } /> - onInject(`@${path} `)}> - Inject file - + onInject(`@${path} `)}>Inject file range && onInject(`${path}:${formatLineRef(range)} `)} > - {range === null - ? "Inject lines" - : `Inject lines ${formatLineRef(range)}`} + {range === null ? "Inject lines" : `Inject lines ${formatLineRef(range)}`} diff --git a/frontend/src/components/diff/ReviewPanel.tsx b/frontend/src/components/diff/ReviewPanel.tsx index 50af07c8f..03f8c0f47 100644 --- a/frontend/src/components/diff/ReviewPanel.tsx +++ b/frontend/src/components/diff/ReviewPanel.tsx @@ -1,12 +1,12 @@ -import {useCallback, useEffect, useState} from "react" -import {toast} from "sonner" -import {ProjectService, Terminal as TerminalService} from "@/lib/rpc" -import {useActiveSession} from "@/lib/useActiveSession" -import {discardTargets, parseDiff, type DiffFile} from "@/lib/diff" -import {useGitStatus} from "@/lib/useGitStatus" -import {errorText} from "@/lib/utils" -import {DiscardDialog} from "./DiscardDialog" -import {FileDiff, type DiffBulk} from "./FileDiff" +import { useCallback, useEffect, useState } from "react" +import { toast } from "sonner" +import { ProjectService, Terminal as TerminalService } from "@/lib/rpc" +import { useActiveSession } from "@/lib/useActiveSession" +import { discardTargets, parseDiff, type DiffFile } from "@/lib/diff" +import { useGitStatus } from "@/lib/useGitStatus" +import { errorText } from "@/lib/utils" +import { DiscardDialog } from "./DiscardDialog" +import { FileDiff, type DiffBulk } from "./FileDiff" // ReviewPanel is the Review tab's body: the active session's uncommitted diff, // one collapsible file at a time. Context-menu actions write file/line @@ -14,8 +14,8 @@ import {FileDiff, type DiffBulk} from "./FileDiff" // It follows the active session — a worktree session reviews its checkout, not // the project root. The dock (RightDock) owns the surrounding chrome: width, // full screen, the tab bar and the close button. -export function ReviewPanel({bulk}: {bulk: DiffBulk}) { - const {sessionId, path} = useActiveSession() +export function ReviewPanel({ bulk }: { bulk: DiffBulk }) { + const { sessionId, path } = useActiveSession() const status = useGitStatus(path) const [files, setFiles] = useState(null) const [failed, setFailed] = useState(false) @@ -92,7 +92,7 @@ interface PanelBodyProps { bulk: DiffBulk } -function PanelBody({files, failed, onInject, onDiscard, bulk}: PanelBodyProps) { +function PanelBody({ files, failed, onInject, onDiscard, bulk }: PanelBodyProps) { if (failed) { return Not a git repository } @@ -117,6 +117,6 @@ function PanelBody({files, failed, onInject, onDiscard, bulk}: PanelBodyProps) { ) } -function PanelNotice({children}: {children: string}) { +function PanelNotice({ children }: { children: string }) { return

{children}

} diff --git a/frontend/src/components/diff/useDiffEditor.ts b/frontend/src/components/diff/useDiffEditor.ts index bbbfb18ff..49bbd2811 100644 --- a/frontend/src/components/diff/useDiffEditor.ts +++ b/frontend/src/components/diff/useDiffEditor.ts @@ -1,7 +1,7 @@ -import {useEffect, useRef} from "react" -import type {RefObject} from "react" -import {EditorState} from "@codemirror/state" -import {EditorView} from "@codemirror/view" +import { useEffect, useRef } from "react" +import type { RefObject } from "react" +import { EditorState } from "@codemirror/state" +import { EditorView } from "@codemirror/view" import { buildLineDecorations, diffGutter, @@ -10,7 +10,7 @@ import { selectedDocLines, type DocLineSelection, } from "@/lib/codemirror" -import type {FileDoc} from "@/lib/diff" +import type { FileDoc } from "@/lib/diff" export interface DiffEditor { containerRef: RefObject @@ -51,5 +51,5 @@ export function useDiffEditor(doc: FileDoc, filename: string): DiffEditor { const getSelectedDocLines = (): DocLineSelection | null => viewRef.current ? selectedDocLines(viewRef.current) : null - return {containerRef, getSelectedDocLines} + return { containerRef, getSelectedDocLines } } diff --git a/frontend/src/components/dock/FilesPanel.tsx b/frontend/src/components/dock/FilesPanel.tsx index bfa587b9b..fc0ce7844 100644 --- a/frontend/src/components/dock/FilesPanel.tsx +++ b/frontend/src/components/dock/FilesPanel.tsx @@ -1,22 +1,22 @@ -import {useCallback, useEffect, useState} from "react" -import {ChevronLeft} from "lucide-react" -import {ProjectService, System, Terminal as TerminalService} from "@/lib/rpc" -import {useActiveSession} from "@/lib/useActiveSession" -import {useProjects} from "@/lib/projects" -import {queuePaste} from "@/lib/paste-queue" -import {useGitStatus} from "@/lib/useGitStatus" -import {buildTree, type TreeNode} from "@/lib/file-tree" -import {FileTree} from "@/components/FileTree" -import {formatLineRef, parseDiff, type DiffFile} from "@/lib/diff" -import {errorText} from "@/lib/utils" -import type {DocLineSelection} from "@/lib/codemirror" +import { useCallback, useEffect, useState } from "react" +import { ChevronLeft } from "lucide-react" +import { ProjectService, System, Terminal as TerminalService } from "@/lib/rpc" +import { useActiveSession } from "@/lib/useActiveSession" +import { useProjects } from "@/lib/projects" +import { queuePaste } from "@/lib/paste-queue" +import { useGitStatus } from "@/lib/useGitStatus" +import { buildTree, type TreeNode } from "@/lib/file-tree" +import { FileTree } from "@/components/FileTree" +import { formatLineRef, parseDiff, type DiffFile } from "@/lib/diff" +import { errorText } from "@/lib/utils" +import type { DocLineSelection } from "@/lib/codemirror" import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu" -import {useFileEditor} from "./useFileEditor" +import { useFileEditor } from "./useFileEditor" // FilesPanel is the Files tab of the right dock: a read-only tree of the active // session's tracked files, master-detail with an in-dock preview. It follows the @@ -25,8 +25,8 @@ import {useFileEditor} from "./useFileEditor" // terminal it belongs to. It never edits; clicks only navigate and inject // path/line references into the session's PTY. export function FilesPanel() { - const {projectId, sessionId, path} = useActiveSession() - const {newSession, activateSession} = useProjects() + const { projectId, sessionId, path } = useActiveSession() + const { newSession, activateSession } = useProjects() const status = useGitStatus(path) const [tree, setTree] = useState(null) const [stats, setStats] = useState>(new Map()) @@ -89,30 +89,17 @@ export function FilesPanel() { return } const id = newSession(projectId, "shell", path) - queuePaste(id, command + "\n") + queuePaste(id, `${command}\n`) activateSession(projectId, id) }) .catch(() => undefined) } if (open !== null) { - return ( - setOpen(null)} - onInject={inject} - /> - ) + return setOpen(null)} onInject={inject} /> } return ( - + ) } @@ -138,7 +125,7 @@ interface TreeBodyProps { onEditor: (rel: string) => void } -function TreeBody({tree, stats, failed, onOpen, onEditor}: TreeBodyProps) { +function TreeBody({ tree, stats, failed, onOpen, onEditor }: TreeBodyProps) { if (failed) { return Not a git repository } @@ -166,7 +153,7 @@ interface FilePreviewProps { onInject: (text: string) => void } -function FilePreview({path, rel, onBack, onInject}: FilePreviewProps) { +function FilePreview({ path, rel, onBack, onInject }: FilePreviewProps) { const [text, setText] = useState(null) const [error, setError] = useState(null) @@ -199,7 +186,7 @@ function FilePreview({path, rel, onBack, onInject}: FilePreviewProps) { aria-label="Back to file tree" className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground" > - + {rel} @@ -214,7 +201,7 @@ function FilePreview({path, rel, onBack, onInject}: FilePreviewProps) { ) : text === null ? ( Loading… ) : ( - + )} @@ -230,8 +217,8 @@ interface PreviewBodyProps { // PreviewBody renders the file in a read-only CodeMirror view whose selection // drives the same inject context menu as the diff review — file lines map // straight through (doc line === file line), so the range needs no remap. -function PreviewBody({text, rel, onInject}: PreviewBodyProps) { - const {containerRef, getSelectedLines} = useFileEditor(text, rel) +function PreviewBody({ text, rel, onInject }: PreviewBodyProps) { + const { containerRef, getSelectedLines } = useFileEditor(text, rel) const [range, setRange] = useState(null) // Resolve the selection when the menu opens, not on every selection change. @@ -241,14 +228,12 @@ function PreviewBody({text, rel, onInject}: PreviewBodyProps) { } } - const lineRef = range && formatLineRef({start: range.from, end: range.to}) + const lineRef = range && formatLineRef({ start: range.from, end: range.to }) return ( - }/> + } /> - onInject(`@${rel} `)}> - Inject file - + onInject(`@${rel} `)}>Inject file lineRef && onInject(`${rel}:${lineRef} `)} @@ -260,6 +245,6 @@ function PreviewBody({text, rel, onInject}: PreviewBodyProps) { ) } -function Notice({children}: {children: string}) { +function Notice({ children }: { children: string }) { return

{children}

} diff --git a/frontend/src/components/dock/RightDock.tsx b/frontend/src/components/dock/RightDock.tsx index 14d78d77d..6d4ef53f8 100644 --- a/frontend/src/components/dock/RightDock.tsx +++ b/frontend/src/components/dock/RightDock.tsx @@ -1,14 +1,22 @@ -import {useState} from "react" -import {ChevronsDownUp, ChevronsUpDown, Code, FileDiff, Maximize2, Minimize2, X} from "lucide-react" -import {Button} from "@/components/ui/button" -import {Tabs, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {DiffStat} from "@/components/DiffStat" -import {HeaderAction} from "@/components/diff/FileDiff" -import {ReviewPanel} from "@/components/diff/ReviewPanel" -import {useActiveSession} from "@/lib/useActiveSession" -import {useGitStatus} from "@/lib/useGitStatus" -import {usePanelWidth} from "@/lib/use-panel-width" -import {FilesPanel} from "./FilesPanel" +import { useState } from "react" +import { + ChevronsDownUp, + ChevronsUpDown, + Code, + FileDiff, + Maximize2, + Minimize2, + X, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { DiffStat } from "@/components/DiffStat" +import { HeaderAction } from "@/components/diff/FileDiff" +import { ReviewPanel } from "@/components/diff/ReviewPanel" +import { useActiveSession } from "@/lib/useActiveSession" +import { useGitStatus } from "@/lib/useGitStatus" +import { usePanelWidth } from "@/lib/use-panel-width" +import { FilesPanel } from "./FilesPanel" export type DockTab = "files" | "review" @@ -23,16 +31,15 @@ interface RightDockProps { // (persisted), full-screen toggle, the tab bar and the close button — so the // two panels stay pure bodies. One dock width serves both tabs: it is a single // panel that swaps contents, not two panels competing for the edge. -export function RightDock({tab, onTab, onClose}: RightDockProps) { - const {path} = useActiveSession() +export function RightDock({ tab, onTab, onClose }: RightDockProps) { + const { path } = useActiveSession() const status = useGitStatus(path) const [fullscreen, setFullscreen] = useState(false) // One collapse/expand-all directive for the review panel; the nonce re-fires // the sync even when every file already holds the target state. - const [bulk, setBulk] = useState({open: true, nonce: 0}) - const toggleAll = () => - setBulk((b) => ({open: !b.open, nonce: b.nonce + 1})) - const {width, handleProps} = usePanelWidth({ + const [bulk, setBulk] = useState({ open: true, nonce: 0 }) + const toggleAll = () => setBulk((b) => ({ open: !b.open, nonce: b.nonce + 1 })) + const { width, handleProps } = usePanelWidth({ storageKey: "lich.dock.width", minRem: 20, maxRem: 60, @@ -48,20 +55,26 @@ export function RightDock({tab, onTab, onClose}: RightDockProps) { ? "absolute inset-0 z-20 flex flex-col bg-sidebar" : "relative flex shrink-0 flex-col border-l border-border bg-sidebar" } - style={fullscreen ? undefined : {width: `${width}rem`}} + style={fullscreen ? undefined : { width: `${width}rem` }} >
onTab(value as DockTab)} className={"h-8"}> - - + + Code - - + + Review {status && status.files > 0 && ( - + )} @@ -73,9 +86,9 @@ export function RightDock({tab, onTab, onClose}: RightDockProps) { onClick={toggleAll} > {bulk.open ? ( - + ) : ( - + )} )} @@ -83,11 +96,7 @@ export function RightDock({tab, onTab, onClose}: RightDockProps) { label={fullscreen ? "Exit full screen" : "Full screen"} onClick={() => setFullscreen((v) => !v)} > - {fullscreen ? ( - - ) : ( - - )} + {fullscreen ? : }
- {tab === "files" ? : } + {tab === "files" ? : }
{!fullscreen && (
viewRef.current ? selectedDocLines(viewRef.current) : null - return {containerRef, getSelectedLines} + return { containerRef, getSelectedLines } } diff --git a/frontend/src/components/pulls/Pulls.tsx b/frontend/src/components/pulls/Pulls.tsx index 54797cf99..c8e8826bb 100644 --- a/frontend/src/components/pulls/Pulls.tsx +++ b/frontend/src/components/pulls/Pulls.tsx @@ -1,6 +1,6 @@ -import {useState, type ReactNode} from "react" -import {useNavigate, useParams} from "react-router-dom" -import {toast} from "sonner" +import { useState, type ReactNode } from "react" +import { useNavigate, useParams } from "react-router-dom" +import { toast } from "sonner" import { Check, ChevronDown, @@ -14,17 +14,17 @@ import { X, type LucideIcon, } from "lucide-react" -import {ProjectService, Store, System, Terminal as TerminalService} from "@/lib/rpc" -import type {ChecksRollup, MergeMethod, PullRequestDetail} from "@/lib/api-types" -import {useProjects} from "@/lib/projects" -import {baseName} from "@/lib/paths" -import {closePulls} from "@/lib/pulls-card-store" -import {activeTarget, sessionsOf} from "@/lib/sessions" -import {useGitStatus} from "@/lib/useGitStatus" -import {usePullRequestDetail} from "@/lib/usePullRequestDetail" -import {cn, errorText} from "@/lib/utils" -import {Markdown} from "@/components/Markdown" -import {Button} from "@/components/ui/button" +import { ProjectService, Store, System, Terminal as TerminalService } from "@/lib/rpc" +import type { ChecksRollup, MergeMethod, PullRequestDetail } from "@/lib/api-types" +import { useProjects } from "@/lib/projects" +import { baseName } from "@/lib/paths" +import { closePulls } from "@/lib/pulls-card-store" +import { activeTarget, sessionsOf } from "@/lib/sessions" +import { useGitStatus } from "@/lib/useGitStatus" +import { usePullRequestDetail } from "@/lib/usePullRequestDetail" +import { cn, errorText } from "@/lib/utils" +import { Markdown } from "@/components/Markdown" +import { Button } from "@/components/ui/button" import { Dialog, DialogContent, @@ -40,10 +40,10 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import {Input} from "@/components/ui/input" -import {Label} from "@/components/ui/label" -import {PullsChecks} from "./PullsChecks" -import {PullsFiles} from "./PullsFiles" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { PullsChecks } from "./PullsChecks" +import { PullsFiles } from "./PullsFiles" // Pulls is the per-project pull-request screen: it fills the main area on top of // the persistent terminals (like Settings), showing the active session's branch @@ -51,15 +51,15 @@ import {PullsFiles} from "./PullsFiles" // resolves its path from the route's project id plus the active session (the // exact-match useActiveSession returns empty on this subroute). export function Pulls() { - const {projectId} = useParams() + const { projectId } = useParams() const navigate = useNavigate() - const {projects, sessions, closeSession} = useProjects() + const { projects, sessions, closeSession } = useProjects() const projectPath = projects.find((p) => p.id === projectId)?.path ?? "" - const {sessionId, path} = activeTarget(sessions, projectId ?? null, projectPath) + const { sessionId, path } = activeTarget(sessions, projectId ?? null, projectPath) const status = useGitStatus(path) const branch = status?.branch ?? "" const head = status?.head ?? "" - const {detail, loading, error, refresh} = usePullRequestDetail(path, branch, head) + const { detail, loading, error, refresh } = usePullRequestDetail(path, branch, head) const inject = (text: string) => { if (sessionId) { @@ -106,7 +106,7 @@ export function Pulls() { } toast.success(merged, { duration: 10_000, - action: {label: "Remove worktree", onClick: () => void removeWorktree()}, + action: { label: "Remove worktree", onClick: () => void removeWorktree() }, }) } @@ -129,7 +129,7 @@ export function Pulls() { } else if (loading) { body = Loading… } else { - body = + body = } return
{body}
@@ -156,7 +156,14 @@ interface PullRequestViewProps { onInject: (text: string) => void } -function PullRequestView({path, head, detail, onRefresh, onMerged, onInject}: PullRequestViewProps) { +function PullRequestView({ + path, + head, + detail, + onRefresh, + onMerged, + onInject, +}: PullRequestViewProps) { const [merging, setMerging] = useState(false) const [edit, setEdit] = useState(null) const [tab, setTab] = useState<"overview" | "files" | "checks">("overview") @@ -212,9 +219,9 @@ function PullRequestView({path, head, detail, onRefresh, onMerged, onInject}: Pu - + {merging ? "Merging…" : "Merge"} - + } /> @@ -228,7 +235,7 @@ function PullRequestView({path, head, detail, onRefresh, onMerged, onInject}: Pu void merge("rebase")}> Rebase and merge - + openEdit("squash")}> Squash and merge, edit message… @@ -241,10 +248,10 @@ function PullRequestView({path, head, detail, onRefresh, onMerged, onInject}: Pu {/* HEAD moving refetches on its own; this covers what it can't see — a review, a check finishing, a PR opened from the terminal. */}
@@ -257,11 +264,11 @@ function PullRequestView({path, head, detail, onRefresh, onMerged, onInject}: Pu detail.isDraft ? "text-amber-500" : "text-emerald-500", )} > - + {detail.isDraft ? "Draft" : "Open"}
- + {detail.headRefName} → {detail.baseRefName} {/* The counter is where the eye lands when CI is red; make it the way @@ -272,12 +279,12 @@ function PullRequestView({path, head, detail, onRefresh, onMerged, onInject}: Pu onClick={() => setTab("checks")} className="rounded-sm transition-opacity hover:opacity-80" > - + ) : ( - + )} - +
@@ -301,11 +308,11 @@ function PullRequestView({path, head, detail, onRefresh, onMerged, onInject}: Pu
{tab === "files" ? ( - + ) : (
{tab === "checks" ? ( - + ) : (
{detail.body.trim() !== "" ? ( @@ -332,7 +339,15 @@ function PullRequestView({path, head, detail, onRefresh, onMerged, onInject}: Pu ) } -function TabButton({active, onClick, children}: {active: boolean; onClick: () => void; children: ReactNode}) { +function TabButton({ + active, + onClick, + children, +}: { + active: boolean + onClick: () => void + children: ReactNode +}) { return (
@@ -382,7 +403,7 @@ function MergeMessageDialog({edit, merging, onChange, onCancel, onConfirm}: Merg