Skip to content
Merged
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
22 changes: 21 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,23 @@ such detail into a token. New layout spacing must use the scale.
context. Keep the secondary scope heading and muted monospace path inline;
hide the path at 768–1011px and both below 768px. Group ghost Refresh/theme
actions at the trailing edge; share `--hover` feedback with the trigger.
- Projects drawer: the outermost full-height column, `--projects-width` (16rem)
on `--panel` with a separator edge. Its header matches `--topbar-height` and
holds the brand and a ghost “Hide projects” action (panel-left icon). Below it:
a search row styled like the file filter that opens the review picker, a quiet
semibold “Projects” caption, then repositories ordered by latest arrival.
Repository rows match tree-row height with a chevron, repository icon, medium
name and muted monospace age. Stream rows indent under them: branch icon, name,
trailing monospace changed-file count, and a muted metadata line (worktree,
harness, host only when a repository has several, age). Show only current
heads with changes. The current stream uses `--accent-bg`, accent text and
`aria-current="page"`. Unopened arrivals show a small accent dot labeled “New
changes”; a collapsed repository carries its streams' dot. While hidden, the
topbar starts with a “Show projects” action, the brand and divider. The file
sidebar toggle uses a list-tree icon so the two panel controls stay distinct.
Collapse state persists per browser.
- A newer head for the viewed stream shows a one-line `--accent-bg` notice below
the header with an outline-muted “View latest” action. Never swap the open review.
- Status bar: `--statusbar-height` (28), with shortcuts shed before server metrics.
- The diff display toolbar uses `--panel`, matching the header and status bar.
- Desktop sidebar: `--sidebar-width` defaults to 330px. `use-sidebar.ts` owns
Expand Down Expand Up @@ -421,6 +438,8 @@ Do not add motion for decoration. New animation must honor
Keep the linked-worktree icon visible when its text label is hidden.
- Collapse navigation to the toggle-controlled modal drawer; its width is
`min(320px, 88vw)`. Keep file selection, comments, and export reachable there.
The projects drawer follows the same modal pattern from its own toggle; opening
it closes the file drawer, and choosing a stream closes it.
Its tab strip matches `--topbar-height`, including the tablet header height.
File/folder rows, filter, summary toggle, and review progress retain desktop
control heights and spacing; do not apply the general mobile enlargement here.
Expand Down Expand Up @@ -488,7 +507,8 @@ from the canonical tokens. Zero, automatic spacing
and 1px optical insets remain valid; dimensions, breakpoints and dynamic measured
geometry require review rather than a blanket literal-value ban.

The guard has exact exceptions for status circles, SVG monogram units, icon line
The guard has exact exceptions for status circles (including the projects
drawer's new-changes dot), SVG monogram units, icon line
boxes, compact comment counts, the overlapping sidebar resizer, and Pierre's
line-surface mixes/inset strokes. Existing generated tabs' 3px inset and active
shadow, and the dialog title's unit line-height, are bounded legacy exceptions;
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ FROM scratch
COPY --from=build /diffx-server /diffx-server
COPY --from=build --chown=10001:10001 /data /data
USER 10001:10001
ENV DIFFX_HOST=0.0.0.0
ENV DIFFX_PORT=7981
ENV DIFFX_STATE=/data/state.db
ENV DIFFX_SERVER_HOST=0.0.0.0
ENV DIFFX_SERVER_PORT=7980
ENV DIFFX_SERVER_STATE=/data/state.db
VOLUME ["/data"]
EXPOSE 7981
EXPOSE 7980
STOPSIGNAL SIGTERM
ENTRYPOINT ["/diffx-server"]
69 changes: 50 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ using an unverified PID.
Publish once to a remote server, including every registered worktree:

```sh
diffx config set server https://reviews.example.com
diffx config set token - < /path/to/private-token
diffx config set remote.url https://reviews.example.com
diffx config set remote.token - < /path/to/private-token
diffx remote status
diffx sync
diffx sync --print
diffx sync --debug
Expand All @@ -74,7 +75,8 @@ Cancelling a wait does not cancel an accepted server job.

`diffx .` and piped diffs always run locally, even with remote config. Sync
and plugins require a remote URL and bearer token; they never bootstrap a local
server.
server. `diffx remote status` checks the destination, credential and protocol,
and reports agent-hook deliveries still waiting.

The all scope includes branch changes from the local default branch's merge base
plus working changes. Staged/unstaged retain HEAD/index semantics.
Expand All @@ -83,11 +85,27 @@ Matching captures reuse review state. Retention defaults to seven days after the
last fresh submission; retries do not extend it.

Defaults live in `~/.config/diffx/config.json`; `--config-file` or
`DIFFX_CONFIG_PATH` overrides the location. Precedence: defaults, JSON,
environment, explicit flags. `diffx config {set|get|remove}` edits persisted
settings. Token reads are masked; `set token -` reads stdin.
`DIFFX_CONFIG_PATH` overrides the location. One file configures both modes in
separate sections, so a local review and a remote server can run side by side:

```json
{
"local": { "host": "127.0.0.1", "port": null, "retentionDays": 7 },
"remote": { "url": "http://127.0.0.1:7980", "token": "…" }
}
```

The default listener binds to `127.0.0.1` on an available port from 7981–7990.
`local.*` configures only `diffx [PATH]` and piped diffs; `remote.*` configures
only `sync` and agent hooks. Environment overrides use the same namespaces:
`DIFFX_LOCAL_HOST`, `DIFFX_LOCAL_PORT`, `DIFFX_LOCAL_STATE`, `DIFFX_LOCAL_WEB_DIR`,
`DIFFX_LOCAL_RETENTION_DAYS`, `DIFFX_REMOTE_URL` and `DIFFX_REMOTE_TOKEN`.
Precedence: defaults, JSON, environment, explicit flags. `diffx config
{set|get|remove} KEY` edits persisted settings by dotted key, such as
`local.port` or `remote.url`. Token reads are masked; `set remote.token -` reads
stdin. Older flat config files are rejected with the key's new name; move each
key into its section.

The default local listener binds to `127.0.0.1` on an available port from 7981–7990.
`--port 0` chooses an OS-assigned port. A non-loopback listener exposes local
reviews to reachable clients. The browser opens a loopback URL for wildcard binds.

Expand Down Expand Up @@ -154,8 +172,9 @@ mise run docker:up
mise run docker:down
```

Compose publishes HTTP on `127.0.0.1:7981` and preserves the named SQLite volume
when stopped or recreated. Set `DIFFX_PORT=7982` to use another port. Retrieve
Compose publishes HTTP on `127.0.0.1:7980`, outside the local 7981–7990 range,
and preserves the named SQLite volume when stopped or recreated. Set
`DIFFX_SERVER_PORT=7979` to publish another host port. Retrieve
the initial credential with `docker compose cp server:/data/state.db.admin-token ./admin-token`.
See [server development](docs/development.md#server-development) for configuration
and persistence details. Deploy behind HTTPS when exposing the server remotely.
Expand All @@ -164,7 +183,7 @@ Or build and run the image directly:

```sh
mise run docker:build
docker run --name diffx-server --read-only -p 127.0.0.1:7981:7981 \
docker run --name diffx-server --read-only -p 127.0.0.1:7980:7980 \
--mount source=diffx-data,target=/data diffx-server:local
```

Expand All @@ -179,7 +198,7 @@ Build/install the remote server with Go:

```sh
go install github.com/flexdinesh/diffx/cmd/diffx-server@main
DIFFX_HOST=127.0.0.1 DIFFX_PORT=7981 DIFFX_STATE=./test-data/state.db diffx-server
DIFFX_SERVER_HOST=127.0.0.1 DIFFX_SERVER_PORT=7980 DIFFX_SERVER_STATE=./test-data/state.db diffx-server
```

First startup creates `admin` with a generated token saved privately at
Expand All @@ -193,15 +212,17 @@ The database stores credential hashes. To add a user, stop the server, run:
diffx-server user create --name alice --state /data/state.db
```

Save the printed token, then restart the server. `--account` / `DIFFX_ACCOUNT` and `DIFFX_TOKEN`
Save the printed token, then restart the server. `--account` / `DIFFX_SERVER_ACCOUNT` and `DIFFX_SERVER_TOKEN`
can supply initial bootstrap credentials; existing credentials are never replaced.
Use `--retention-days 14` or config/environment to change server retention.
The server defaults to `127.0.0.1:7981` and `./data/state.db`, independent of
personal CLI settings. Its image defaults to `0.0.0.0:7981` and `/data/state.db`;
env vars override image defaults, and explicit flags override env vars.
The server defaults to `127.0.0.1:7980` and `./data/state.db`, independent of
personal CLI settings. Its image defaults to `0.0.0.0:7980` and `/data/state.db`;
`DIFFX_SERVER_*` env vars override image defaults, and explicit flags override
env vars. The CLI never reads `DIFFX_SERVER_*`.

Collectors select that destination with config `server`, `DIFFX_SERVER_URL`
or `--server`, and authenticate with config `token`, `DIFFX_TOKEN` or `--token`:
Collectors select that destination with config `remote.url`, `DIFFX_REMOTE_URL`
or `--server`, and authenticate with config `remote.token`, `DIFFX_REMOTE_TOKEN`
or `--token`:

```sh
diffx sync --server https://reviews.example.com
Expand All @@ -214,7 +235,16 @@ the bundled plugins invoke the Go collector.

## Reviewing changes

Select a repository, then an observation. Search/filter stored metadata to find
The Projects drawer lists repositories by recent activity, each with its current
streams: one per source, checkout, branch and comparison. Each stream shows its
newest snapshot only; clean or merged streams and superseded snapshots are hidden.
A dot marks streams with changes you have not opened in this browser. Collapse the
drawer from its header; it stays collapsed across visits. When a newer snapshot
arrives for the stream you are reviewing, a notice offers **View latest** without
replacing the open review.

Search reviews (Ctrl/Cmd+K) to reach older or empty snapshots. Select a
repository, then an observation. Search/filter stored metadata to find
a branch, worktree, host, source or agent run. Switch between its collected all,
staged and unstaged scopes. Use the file tree to navigate, mark files as reviewed
and comment on lines. **Copy unresolved** and **Copy all** export comments as
Expand Down Expand Up @@ -260,7 +290,8 @@ Public v1 routes and synchronous ingestion are removed.

`/api/v2/contexts` lists stored observations with `q`, `repository`, `branch`,
`worktree`, `hostname`, `sourceId`, `runId`, `harness`, `sessionId` and `sessionName`
filters. Session filters search submission associations. Scoped review operations
filters; `latest=true` omits stale snapshots. Each context names its `streamId` and
the stream's current `latestContextId`. Session filters search submission associations. Scoped review operations
use `/api/v2/contexts/{id}/...`; `/api/v2/events` sends ingestion notifications.
Latest means the most recently collected submission for each source, repository,
checkout, branch and comparison policy; arrival order breaks ties. It does not guarantee current
Expand Down
1 change: 1 addition & 0 deletions apps/web/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ A local review workspace that combines a browser-based Git diff with comments th
- Reviews can cover all, staged, or unstaged changes. The file tree, reviewed-file marks, and comments help track progress.
- Comments can be copied as agent-ready XML. The same review comments are available to compatible coding agents through MCP and, in supported browsers, WebMCP.
- The CLI can also display a piped Git patch as a fixed review.
- A remote diffx server can collect from many agent sessions across many repositories. The Projects drawer keeps that work navigable: each repository lists only its current changed branches, newest first, and new arrivals are marked without replacing the review in progress.
- Review data is stored by the local diffx server, as documented in the README.

## Capabilities and Constraints
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useReviewState } from "./app-state.tsx";
import { DiffWorkspace } from "./DiffWorkspace.tsx";
import { FileExplorerNav, SidebarResizer } from "./FileExplorerNav.tsx";
import { Header } from "./Header.tsx";
import { NewerSnapshotNotice } from "./ProjectsNav.tsx";
import { CopyDialog } from "./review.tsx";
import { capabilityEnabled, useSession } from "./session-context.tsx";
import { useSidebarState } from "./sidebar-context.tsx";
Expand All @@ -12,6 +13,7 @@ export function App() {
<>
<WebMCPTools />
<Header />
<NewerSnapshotNotice />
<SidebarBackdrop />
<div className="workspace">
<FileExplorerNav />
Expand Down
16 changes: 4 additions & 12 deletions apps/web/src/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {
MonitorIcon,
MoonIcon,
PanelLeftCloseIcon,
PanelLeftOpenIcon,
ListTreeIcon,
RefreshCwIcon,
SunIcon,
} from "lucide-react";
Expand All @@ -14,8 +13,8 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { DeleteContextButton } from "./DeleteContextButton.tsx";
import { ProjectsToggle } from "./ProjectsNav.tsx";
import { useDiffSource } from "./app-state.tsx";
import { useAppearance } from "./appearance-context.tsx";
import { useSidebarState } from "./sidebar-context.tsx";
Expand All @@ -42,6 +41,7 @@ export function Header() {
const refreshEnabled = capabilityEnabled(capabilities.diff.refresh);
return (
<header className="topbar">
<ProjectsToggle />
<Button
type="button"
id="sidebar-toggle"
Expand All @@ -53,16 +53,8 @@ export function Header() {
title={`${sidebar.expanded ? "Hide" : "Show"} file sidebar`}
onClick={sidebar.toggle}
>
{sidebar.expanded ? (
<PanelLeftCloseIcon className="size-(--icon-base)" />
) : (
<PanelLeftOpenIcon className="size-(--icon-base)" />
)}
<ListTreeIcon className="size-(--icon-base)" />
</Button>
<a className="brand" href="/" aria-label="diffx home">
diffx
</a>
<Separator className="header-divider" orientation="vertical" />
<ContextSwitcher />
<div className="header-heading">
<h1
Expand Down
Loading
Loading