Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
8d0c652
feat(web): review local commits in the Diff pane
Bil0000 Oct 15, 2026
5f418e6
fix: merge main into per-commit diff review
Bil0000 Oct 16, 2026
5edb146
fix(web): preserve commit diffs after merging main
Bil0000 Oct 20, 2026
b0614c2
test(server): stabilize repeated Claude steer checks
Bil0000 Oct 20, 2026
e2e00bc
test(release): refresh the pinned smoke lockfile
Bil0000 Oct 20, 2026
5235a68
test(release): validate the refreshed smoke lockfile
Bil0000 Oct 20, 2026
2974a8e
chore: merge main for commit review follow-up
Bil0000 Oct 22, 2026
65b8808
feat(web): open commit links in the Diff pane
Bil0000 Oct 22, 2026
ecbadc4
fix(server): omit signatures from commit listing
Bil0000 Oct 22, 2026
f2a9fdb
fix(web): validate commit links before opening Diff
Bil0000 Oct 22, 2026
5f41c80
fix(web): keep Diff choices during commit lookups
Bil0000 Oct 22, 2026
f525074
fix(web): refresh commit link previews
Bil0000 Oct 22, 2026
75b3506
feat(providers): generate short commit descriptions
Bil0000 Oct 23, 2026
df832d4
feat(review): cache commit descriptions by model
Bil0000 Oct 23, 2026
b237619
feat(web): show commit links and short descriptions
Bil0000 Oct 23, 2026
2c58e4b
fix(providers): frame untrusted commit prompt data
Bil0000 Oct 23, 2026
817ba45
fix(web): clarify commit links and clear selection
Bil0000 Oct 23, 2026
316289f
fix(web): separate commit source menu actions
Bil0000 Oct 23, 2026
af7bee7
fix(web): prepare commit picker details before opening
Bil0000 Oct 23, 2026
fab8766
fix(web): retry failed commit summaries when reopening picker
Bil0000 Oct 23, 2026
910ef71
fix(web): keep commit picker data stable while open
Bil0000 Oct 24, 2026
a36afeb
fix(web): keep commit links compatible with main
Bil0000 Oct 24, 2026
22e573d
fix(web): polish commit picker labels and row layout
Bil0000 Oct 24, 2026
c26ee8d
Merge main compatibility for commit review
Bil0000 Oct 24, 2026
609596f
fix(web): preserve normal diff selector width
Bil0000 Oct 24, 2026
2a13212
test(web): retain desktop link discovery fixture
Bil0000 Oct 24, 2026
8028681
test(web): make summary retry events deterministic
Bil0000 Oct 24, 2026
88827af
fix(github): open Enterprise Cloud commit links
Bil0000 Oct 24, 2026
ff6d786
fix(web): place selected commit SHA before the arrow
Bil0000 Oct 25, 2026
cf4074a
perf(web): prepare commit summaries from active chat
Bil0000 Oct 25, 2026
5334e67
Merge main and guard background commit preparation
Bil0000 Oct 26, 2026
8bdf352
test(review): trim duplicate commit review cases
Bil0000 Oct 26, 2026
be957a0
test(mobile): match current review section inputs
Bil0000 Oct 26, 2026
5b8cd65
test(server): use direct Effect option helper
Bil0000 Oct 26, 2026
f39d0cf
merge: resolve main for commit filtering
Bil0000 Oct 27, 2026
577d832
fix(web): keep PR commit link fallbacks
Bil0000 Oct 27, 2026
fa725e2
merge: update commit review with main
Bil0000 Oct 27, 2026
7230269
merge: sync commit review with current main
Bil0000 Oct 27, 2026
f3cb2a6
feat(source-control): open commit links from more forges
Bil0000 Oct 27, 2026
42eb393
fix(web): keep unavailable PR commit links in app
Bil0000 Oct 27, 2026
09d8849
test(web): cover both desktop commit fallback paths
Bil0000 Oct 27, 2026
26f32b4
merge: sync commit review with current theme tests
Bil0000 Oct 27, 2026
73eac80
Merge branch 'main' into t3/discussion-6924-pr
Bil0000 Oct 27, 2026
2900506
fix(web): match standard dark colors to painted surfaces
Bil0000 Oct 27, 2026
1c2b1c1
merge: preserve latest PR changes and desktop regression coverage
Bil0000 Oct 27, 2026
bcef045
merge: main into commit review takeover
juliusmarminge Oct 28, 2026
d9d7957
test: cut the commit review takeover's tests to the spec
juliusmarminge Oct 28, 2026
e0e294f
refactor(web): commit descriptions without a client ownership cache
juliusmarminge Oct 28, 2026
ea96fdb
refactor: one description map on the server, provider-owned commit li…
juliusmarminge Oct 28, 2026
c3da738
fix(web): commit SHA links reach the code host; unavailable commits k…
juliusmarminge Oct 28, 2026
8601f5d
fix(web): a scrolled commit menu stops re-rendering once its batch is…
juliusmarminge Oct 28, 2026
5cdc026
test(server): commit descriptions stay inside the thread's workspace
juliusmarminge Oct 28, 2026
01220e7
fix: Enterprise Server commit links open in Diff; a late commit list …
juliusmarminge Oct 28, 2026
30852a2
fix(web): an unavailable commit link on web offers its URL instead of…
juliusmarminge Oct 28, 2026
d72a55d
fix(web): Shift and Alt clicks on commit links keep their browser mea…
juliusmarminge Oct 28, 2026
f88001d
merge: main into commit review takeover
juliusmarminge Oct 28, 2026
9050c6e
merge: preserve commit review alongside tool call diffs
Bil0000 Oct 28, 2026
62764d2
fix(server): reject commit diffs with missing shallow parents
Bil0000 Oct 28, 2026
d1c05c6
fix(desktop): route Enterprise commit links to the active thread
Bil0000 Oct 28, 2026
add17c0
fix(web): keep the selected commit link clear of diff controls
Bil0000 Oct 28, 2026
75739c7
test(web): cover late commit rows and native link fallbacks
Bil0000 Oct 28, 2026
23edf99
merge: keep commit review current with main
Bil0000 Oct 28, 2026
f1fb89e
fix(server): return typed commit description cancellation errors
Bil0000 Oct 28, 2026
3d926d1
docs: clarify optional summaries and commit link fallbacks
Bil0000 Oct 28, 2026
b77e8bf
merge: keep commit review current with mobile row fix
Bil0000 Oct 28, 2026
b941938
test: takeover keeps the minimum commit-review tests
juliusmarminge Oct 29, 2026
dbb22e5
test: keep commit descriptions behind operate scope
juliusmarminge Oct 29, 2026
d029a64
refactor: read shallow commit parents from the raw object
juliusmarminge Oct 29, 2026
ad7d486
merge: keep commit review current with main
juliusmarminge Oct 29, 2026
f8222bb
fix(web): modified clicks on PR commit links stay native
juliusmarminge Oct 29, 2026
8c5dee0
docs: commit links need the commit on the checked-out branch
juliusmarminge Oct 29, 2026
64b68b0
fix(web): unavailable commit links offer Open link when preview canno…
juliusmarminge Oct 29, 2026
6e4a3e1
merge: keep commit review current with main
juliusmarminge Oct 29, 2026
cf784cc
test(server): text generation fake covers commit descriptions
juliusmarminge Oct 29, 2026
adac0f2
refactor: drop generated commit descriptions
juliusmarminge Oct 30, 2026
130cec6
merge: keep commit review current with main
juliusmarminge Oct 30, 2026
2813437
fix(server): commit links open any commit the repository has locally
juliusmarminge Oct 30, 2026
c8f6cad
fix(web): a commit link opens the closed Diff pane on that commit
juliusmarminge Oct 30, 2026
d700ada
fix(web): commit menu rows fit on one line
juliusmarminge Oct 30, 2026
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
27 changes: 16 additions & 11 deletions apps/mobile/src/features/review/reviewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,17 +534,22 @@ export function buildReviewSectionItems(input: {
},
);

const gitItems = input.gitSections.map<ReviewSectionItem>((section) => ({
id: `git:${section.kind}`,
kind: section.kind,
title: section.title,
subtitle: gitSubtitle(section),
diff: section.diff,
source: section,
...(section.files ? { files: section.files } : {}),
truncated: section.truncated,
isLoading: false,
}));
const gitItems = input.gitSections
.filter(
(section): section is typeof section & { kind: ReviewSectionKind } =>
section.kind !== "commit",
)
.map<ReviewSectionItem>((section) => ({
id: `git:${section.kind}`,
kind: section.kind,
title: section.title,
subtitle: gitSubtitle(section),
diff: section.diff,
source: section,
...(section.files ? { files: section.files } : {}),
truncated: section.truncated,
isLoading: false,
}));
const hasChangesItem = gitItems.some((item) => item.id === CHANGES_SECTION_ID);
const visibleGitItems =
input.loadingGitSections && !hasChangesItem
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.vcsRefreshStatus]: AuthOrchestrationReadScope,
[WS_METHODS.gitResolvePullRequest]: AuthOrchestrationReadScope,
[WS_METHODS.vcsListRefs]: AuthOrchestrationReadScope,
[WS_METHODS.reviewGetBranchCommits]: AuthFilesystemReadScope,
[WS_METHODS.reviewGetDiffPreview]: AuthFilesystemReadScope,
[WS_METHODS.reviewGetDiffFileContents]: AuthFilesystemReadScope,
[WS_METHODS.terminalOpen]: AuthTerminalOperateScope,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/observability/RpcInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ const RPC_AGGREGATES = {
[WS_METHODS.vcsCreateRef]: "vcs",
[WS_METHODS.vcsSwitchRef]: "vcs",
[WS_METHODS.vcsInit]: "vcs",
[WS_METHODS.reviewGetBranchCommits]: "review",
[WS_METHODS.reviewGetDiffPreview]: "review",
[WS_METHODS.reviewGetDiffFileContents]: "review",
[WS_METHODS.terminalOpen]: "terminal",
Expand Down
36 changes: 31 additions & 5 deletions apps/server/src/review/ReviewService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import * as Path from "effect/Path";
import {
VcsRepositoryDetectionError,
VcsUnsupportedOperationError,
type ReviewBranchCommitsInput,
type ReviewBranchCommitsResult,
type ReviewDiffFileContentsInput,
type ReviewDiffFileContentsResult,
type ReviewDiffPreviewError,
Expand All @@ -20,9 +22,18 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
import * as KnownWorkspaces from "../workspace/KnownWorkspaces.ts";

const WORKSPACE_BOUND_SUBJECTS = {
"ReviewService.getBranchCommits": "commit list",
"ReviewService.getDiffPreview": "diff preview",
"ReviewService.getDiffFileContents": "diff file contents",
} as const;

export class ReviewService extends Context.Service<
ReviewService,
{
readonly getBranchCommits: (
input: ReviewBranchCommitsInput,
) => Effect.Effect<ReviewBranchCommitsResult, ReviewDiffPreviewError>;
readonly getDiffPreview: (
input: ReviewDiffPreviewInput,
) => Effect.Effect<ReviewDiffPreviewResult, ReviewDiffPreviewError>;
Expand Down Expand Up @@ -61,7 +72,7 @@ export const make = Effect.gen(function* () {
};

const assertWorkspaceBoundCwd = Effect.fn("ReviewService.assertWorkspaceBoundCwd")(function* (
operation: "ReviewService.getDiffPreview" | "ReviewService.getDiffFileContents",
operation: keyof typeof WORKSPACE_BOUND_SUBJECTS,
cwd: string,
) {
const [candidate, workspaceRoot] = yield* Effect.all([
Expand All @@ -78,13 +89,27 @@ export const make = Effect.gen(function* () {
return yield* new VcsRepositoryDetectionError({
operation,
cwd,
detail:
operation === "ReviewService.getDiffPreview"
? "Review diff preview cwd must stay within the configured workspace root."
: "Review diff file contents cwd must stay within the configured workspace root.",
detail: `Review ${WORKSPACE_BOUND_SUBJECTS[operation]} cwd must stay within the configured workspace root.`,
});
});

const getBranchCommits: ReviewService["Service"]["getBranchCommits"] = Effect.fn(
"ReviewService.getBranchCommits",
)(function* (input) {
yield* assertWorkspaceBoundCwd("ReviewService.getBranchCommits", input.cwd);

const handle = yield* vcsRegistry.detect({ cwd: input.cwd, requestedKind: "auto" });
if (!handle) return { commits: [], truncated: false };
if (handle.kind !== "git") {
return yield* new VcsUnsupportedOperationError({
operation: "ReviewService.getBranchCommits",
kind: handle.kind,
detail: "Review commit lists require a Git repository.",
});
}
return yield* git.getReviewBranchCommits(input);
});

const getDiffPreview: ReviewService["Service"]["getDiffPreview"] = Effect.fn(
"ReviewService.getDiffPreview",
)(function* (input) {
Expand Down Expand Up @@ -120,6 +145,7 @@ export const make = Effect.gen(function* () {
});

return ReviewService.of({
getBranchCommits,
getDiffPreview,
getDiffFileContents,
});
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
type VcsCreateRefResult,
type VcsCreateWorktreeInput,
type VcsCreateWorktreeResult,
type ReviewBranchCommitsInput,
type ReviewBranchCommitsResult,
type ReviewDiffPreviewInput,
type ReviewDiffPreviewResult,
type ReviewDiffFileContentsInput,
Expand Down Expand Up @@ -328,6 +330,9 @@ export class GitVcsDriver extends Context.Service<
cwd: string,
baseRef: string,
) => Effect.Effect<GitRangeContext, GitCommandError>;
readonly getReviewBranchCommits: (
input: ReviewBranchCommitsInput,
) => Effect.Effect<ReviewBranchCommitsResult, GitCommandError>;
readonly getReviewDiffPreview: (
input: ReviewDiffPreviewInput,
) => Effect.Effect<ReviewDiffPreviewResult, GitCommandError>;
Expand Down
136 changes: 136 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @effect-diagnostics nodeBuiltinImport:off - realpathSync.native resolves Windows 8.3 short names, which the Effect realPath does not.
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodeURL from "node:url";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { assert, it, describe } from "@effect/vitest";
Expand Down Expand Up @@ -1466,6 +1468,140 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
});

describe("review diff previews", () => {
it.effect("diffs a root commit against the empty tree but rejects a shallow boundary", () =>
Effect.gen(function* () {
const upstream = yield* makeTmpDir();
yield* initRepoWithCommit(upstream);
const driver = yield* GitVcsDriver.GitVcsDriver;
const emptyTree = yield* git(upstream, ["hash-object", "-t", "tree", NodeOS.devNull]);
const root = yield* git(upstream, ["rev-parse", "HEAD"]);
const rootSource = (yield* driver.getReviewDiffPreview({ cwd: upstream, commitSha: root }))
.sources[0]!;
assert.equal(rootSource.baseRef, emptyTree);
assert.include(rootSource.diff, "+# test");

yield* writeTextFile(upstream, "README.md", "# boundary\n");
yield* git(upstream, ["commit", "-am", "boundary"]);
const sha = yield* git(upstream, ["rev-parse", "HEAD"]);
const cwd = yield* makeTmpDir();
yield* git(upstream, ["clone", "--depth=1", NodeURL.pathToFileURL(upstream).href, cwd], {
GIT_CONFIG_GLOBAL: NodeOS.devNull,
GIT_CONFIG_NOSYSTEM: "1",
});
assert.equal(yield* git(cwd, ["rev-parse", "--is-shallow-repository"]), "true");
for (const request of [
driver.getReviewDiffPreview({ cwd, commitSha: sha }),
driver.getReviewDiffFileContents(
makeReviewDiffFileContentsInput(cwd, {
sourceKind: "commit",
changeType: "new",
headRef: sha,
baseRef: emptyTree,
}),
),
]) {
const error = yield* Effect.asVoid<unknown, GitCommandError, never>(request).pipe(
Effect.flip,
);
assert.include(error.detail, "parent");
}
}),
);

it.effect("lists the branch's commits newest first, at most 100, and flags the rest", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(cwd, ["checkout", "-b", "feature/commits"]);
for (let index = 0; index < 102; index++) {
yield* git(cwd, ["commit", "--allow-empty", "-m", `feature ${index}`]);
}

const result = yield* driver.getReviewBranchCommits({ cwd, baseRef: initialBranch });
assert.strictEqual(result.truncated, true);
assert.strictEqual(result.commits.length, 100);
assert.strictEqual(result.commits[0]?.sha, yield* git(cwd, ["rev-parse", "HEAD"]));
assert.deepStrictEqual(
[result.commits[0]?.subject, result.commits[99]?.subject],
["feature 101", "feature 2"],
);
}),
);

it.effect("previews one selected commit, a merge against its first parent", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(cwd, ["checkout", "-b", "feature/merge-preview"]);
yield* git(cwd, ["checkout", "-b", "feature/side"]);
yield* writeTextFile(cwd, "README.md", "# side commit\n");
yield* git(cwd, ["add", "README.md"]);
yield* git(cwd, ["commit", "-m", "side change"]);
yield* git(cwd, ["checkout", "feature/merge-preview"]);
yield* writeTextFile(cwd, "feature.txt", "feature commit\n");
yield* git(cwd, ["add", "feature.txt"]);
yield* git(cwd, ["commit", "-m", "feature change"]);
const firstParent = yield* git(cwd, ["rev-parse", "HEAD"]);
yield* git(cwd, ["merge", "--no-ff", "--no-edit", "feature/side"]);
const mergeSha = yield* git(cwd, ["rev-parse", "HEAD"]);
yield* git(cwd, ["checkout", "-b", "feature/elsewhere", initialBranch]);
yield* writeTextFile(cwd, "elsewhere.txt", "elsewhere\n");
yield* git(cwd, ["add", "elsewhere.txt"]);
yield* git(cwd, ["commit", "-m", "elsewhere change"]);
const elsewhereSha = yield* git(cwd, ["rev-parse", "HEAD"]);
yield* git(cwd, ["checkout", "feature/merge-preview"]);
yield* writeTextFile(cwd, "README.md", "# dirty tree\n");

const elsewhere = yield* driver.getReviewDiffPreview({ cwd, commitSha: elsewhereSha });
assert.deepStrictEqual(
elsewhere.sources
.find((candidate) => candidate.kind === "commit")
?.files?.map((file) => file.path),
["elsewhere.txt"],
);

const preview = yield* driver.getReviewDiffPreview({
cwd,
baseRef: initialBranch,
commitSha: mergeSha,
});
const source = preview.sources.find((candidate) => candidate.kind === "commit")!;
assert.strictEqual(source.baseRef, firstParent);
assert.strictEqual(source.headRef, mergeSha);
assert.deepStrictEqual(
source.files?.map((file) => file.path),
["README.md"],
);
assert.include(source.diff, "+# side commit");
assert.notInclude(source.diff, "dirty tree");
assert.notInclude(source.diff, "feature.txt");

const patch = yield* driver.getReviewDiffPreview({
cwd,
baseRef: initialBranch,
commitSha: mergeSha,
file: { path: "README.md", previousPath: null, sourceKind: "commit" },
});
assert.include(
patch.sources.find((candidate) => candidate.kind === "commit")!.diff,
"+# side commit",
);
const contents = yield* driver.getReviewDiffFileContents(
makeReviewDiffFileContentsInput(cwd, {
sourceKind: "commit",
baseRef: source.baseRef,
headRef: source.headRef,
}),
);
assert.deepStrictEqual(contents, {
oldContents: "# test\n",
newContents: "# side commit\n",
});
}),
);

it.effect("loads repository-relative files from a nested project directory", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down
Loading
Loading