Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 82 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
# pi-diff-review

This is pure slop, see: https://pi.dev/session/#d4ce533cedbd60040f2622dc3db950e2

It is my hope, that someone takes this idea and makes it gud.

Native diff review window for pi, powered by [Glimpse](https://github.com/hazat/glimpse) and Monaco.

```
pi install git:https://github.com/badlogic/pi-diff-review
```
This fork extends the original package with a branch-aware review mode built around a configurable base ref.

## What it does

Expand All @@ -17,13 +11,93 @@ Adds a `/diff-review` command to pi.
The command:

1. opens a native review window
2. lets you switch between `git diff`, `last commit`, and `all files` scopes
2. lets you switch between `base branch`, `git diff`, `last commit`, and `all files` scopes
3. shows a collapsible sidebar with fuzzy file search
4. shows git status markers in the sidebar for changed files and untracked files
5. lazy-loads file contents on demand as you switch files and scopes
6. lets you draft comments on the original side, modified side, or whole file
7. inserts the resulting feedback prompt into the pi editor when you submit

## New branch-aware behavior

- adds a **base branch** scope that compares `merge-base(<baseRef>, HEAD)` to `HEAD`
- supports a configurable base ref via `/diff-review-set-base`
- supports a special `default` base ref setting that resolves to the repo's default remote branch
- defaults the review window to:
- initial scope: `base branch`
- hide unchanged regions: `on`
- wrap lines: `off`
- supports a one-off base ref override with `/diff-review <base-ref>`
- shows the active resolved base ref in the window chrome
- shows effective config with `/diff-review-config`

## Commands

### `/diff-review`

Open the native review window using the saved config.

```bash
/diff-review
```

Override the base ref for one review session:

```bash
/diff-review origin/main
```

### `/diff-review-set-base`

Persist the default base ref for the current project:

```bash
/diff-review-set-base origin/main
```

Use the repository default remote branch:

```bash
/diff-review-set-base default
```

Persist the setting globally:

```bash
/diff-review-set-base --global default
```

### `/diff-review-config`

Show the configured base ref, the effective resolved base ref, and the config file paths.

## Config files

Project-local override:

```text
<repo>/.pi/diff-review.json
```

Global fallback:

```text
~/.pi/agent/pi-diff-review.json
```

Example:

```json
{
"defaultBaseRef": "default",
"preferredInitialScope": "base-branch",
"preferredHideUnchanged": true,
"preferredWrapLines": false,
"preferredSidebarCollapsed": false,
"autoFetchBaseRef": true
}
```

## Requirements

- macOS, Linux, or Windows
Expand Down
96 changes: 96 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import type { ReviewScope, ReviewWindowPreferences } from "./types.js";

export interface ReviewConfig extends ReviewWindowPreferences {
autoFetchBaseRef: boolean;
}

export interface ReviewConfigLocations {
globalConfigPath: string;
projectConfigPath: string;
}

export const DEFAULT_REVIEW_CONFIG: ReviewConfig = {
defaultBaseRef: "default",
preferredInitialScope: "base-branch",
preferredHideUnchanged: true,
preferredWrapLines: false,
preferredSidebarCollapsed: false,
autoFetchBaseRef: true,
};

function isReviewScope(value: unknown): value is ReviewScope {
return value === "base-branch" || value === "git-diff" || value === "last-commit" || value === "all-files";
}

function sanitizeConfig(value: unknown): Partial<ReviewConfig> {
if (value == null || typeof value !== "object") return {};

const input = value as Record<string, unknown>;
const result: Partial<ReviewConfig> = {};

if (typeof input.defaultBaseRef === "string" && input.defaultBaseRef.trim().length > 0) {
result.defaultBaseRef = input.defaultBaseRef.trim();
}
if (isReviewScope(input.preferredInitialScope)) {
result.preferredInitialScope = input.preferredInitialScope;
}
if (typeof input.preferredHideUnchanged === "boolean") {
result.preferredHideUnchanged = input.preferredHideUnchanged;
}
if (typeof input.preferredWrapLines === "boolean") {
result.preferredWrapLines = input.preferredWrapLines;
}
if (typeof input.preferredSidebarCollapsed === "boolean") {
result.preferredSidebarCollapsed = input.preferredSidebarCollapsed;
}
if (typeof input.autoFetchBaseRef === "boolean") {
result.autoFetchBaseRef = input.autoFetchBaseRef;
}

return result;
}

async function readJsonFile(path: string): Promise<Partial<ReviewConfig>> {
try {
const contents = await readFile(path, "utf8");
return sanitizeConfig(JSON.parse(contents));
} catch {
return {};
}
}

export function getReviewConfigLocations(repoRoot: string): ReviewConfigLocations {
return {
globalConfigPath: join(homedir(), ".pi", "agent", "pi-diff-review.json"),
projectConfigPath: join(repoRoot, ".pi", "diff-review.json"),
};
}

export async function loadReviewConfig(repoRoot: string): Promise<ReviewConfig> {
const { globalConfigPath, projectConfigPath } = getReviewConfigLocations(repoRoot);
const globalConfig = await readJsonFile(globalConfigPath);
const projectConfig = await readJsonFile(projectConfigPath);

return {
...DEFAULT_REVIEW_CONFIG,
...globalConfig,
...projectConfig,
};
}

export async function saveReviewConfig(repoRoot: string, patch: Partial<ReviewConfig>, scope: "global" | "project"): Promise<string> {
const { globalConfigPath, projectConfigPath } = getReviewConfigLocations(repoRoot);
const path = scope === "global" ? globalConfigPath : projectConfigPath;
const current = await readJsonFile(path);
const next = {
...current,
...sanitizeConfig(patch),
} satisfies Partial<ReviewConfig>;

await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
return path;
}
Loading