Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c9346c5
feat(multi-repo-root): multi-repo workspaces
LoganRupe Jun 26, 2026
7be64a1
fix(server): heal migrations skipped by the multi-repo renumbering
LoganRupe Aug 6, 2026
6dda74e
feat(web): show stage artwork on every build stage
LoganRupe Aug 6, 2026
ad4459e
fix(desktop): open multi-repo projects as a workspace, not a folder
logzerroneous Aug 10, 2026
63801b0
fix(server,web): isolated runs work for multi-repo workspaces
logzerroneous Aug 10, 2026
5bc3507
fix(server,client-runtime): update fixtures for the upstream rebase
logzerroneous Aug 10, 2026
78052cf
fix(server): heal main's 037-040 skipped by the second renumbering
logzerroneous Aug 10, 2026
11e8eee
fix(web): carry multi-repo workspaces into the default sidebar
LoganRupe Aug 18, 2026
7849748
fix(web): show turn diffs for multi-repo workspaces
logzerroneous Aug 20, 2026
58f9757
fix(server): heal main's 041 skipped by the third renumbering
logzerroneous Aug 24, 2026
8c2af73
fix(mobile): review multi-repo threads per repo
logzerroneous Aug 27, 2026
97c9e28
fix(web): diff a multi-repo thread's worktrees, not the user's checkouts
LoganRupe Aug 27, 2026
998276d
fix(server): surface workspaceFile in project snapshot queries
logzerroneous Sep 3, 2026
b83f83f
fix(server): heal main's 044-047 skipped by the fifth renumbering
logzerroneous Sep 3, 2026
a26fdb8
test(server): stub remoteBranchExists in the multi-repo bootstrap test
logzerroneous Sep 3, 2026
dce49c7
fix(server): heal main's 048-049 skipped by the sixth renumbering
logzerroneous Sep 7, 2026
d6550f7
test(web): assert the repo root argument on file-save pending changes
logzerroneous Sep 7, 2026
0761abf
fix(web,contracts): remote open-in-editor honors the workspace file
logzerroneous Sep 8, 2026
8a28562
fix(web): namespace the diff file tree by repo in multi-repo turn diffs
logzerroneous Sep 9, 2026
fd98510
fix(server): heal main's 050-051 skipped by the seventh renumbering
logzerroneous Sep 14, 2026
355abaf
test(server): single-root bootstrap keeps upstream's worktree call
logzerroneous Sep 14, 2026
5aca034
feat(cli): open workspace files in desktop app (#267)
Tr1Fecta-7 Sep 18, 2026
5ced658
fix(server): heal main's 052-053 skipped by the eighth renumbering
logzerroneous Sep 18, 2026
6aa9fa9
fix(server): check every repo root before a multi-repo file rewind
logzerroneous Sep 18, 2026
75b9ba2
test: carry multi-repo fields into fixtures added upstream
logzerroneous Sep 18, 2026
b671583
fix: follow upstream API renames after the ninth rebase
logzerroneous Sep 22, 2026
9a0b0d9
style(web): adopt upstream's button and popover variants in multi-rep…
logzerroneous Sep 22, 2026
7364a4f
refactor(server): use the shared home-path expander in workspace code
logzerroneous Sep 22, 2026
b38630a
fix: open isolated multi-repo runs as a multi-root workspace (#294)
LoganRupe Sep 23, 2026
3f692e3
feat: pick a base branch per repo for multi-repo isolated runs (#295)
LoganRupe Sep 23, 2026
711b353
fix(web): reach every PR of a multi-repo thread (#299)
LoganRupe Sep 24, 2026
47e1c63
test(server): carry worktrees into the agent-alert replay fixture add…
logzerroneous Sep 24, 2026
1d386b9
fix(web): follow upstream's composer control and no-restyle rules
logzerroneous Sep 24, 2026
44b00df
fix: multi-repo file links open the file in its own repo (#307)
dotrahat Sep 25, 2026
7d77c68
fix(server): list the repo roots in multi-repo runtime instructions
dotrahat Sep 25, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ release-mock/
apps/web/.playwright
apps/web/playwright-report
apps/web/src/components/__screenshots__
.playwright-mcp/
.vitest-*
__screenshots__/
.tanstack
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const clientSettings: ClientSettings = {
notificationMode: "notifications-and-sound",
inAppNotificationsEnabled: true,
appearanceContrast: 100,
browseShowWorkspaceFiles: false,
browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" },
browserDefaultZoomFactor: 1.25,
browserDefaultAppearance: "dark",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/connection/environment-cache-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ describe("mobile SQLite environment cache store", () => {
branch: null,
pullRequests: [],
worktreePath: null,
worktrees: [],
latestTurn: null,
createdAt: now,
updatedAt: now,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function makeThread(
branch: null,
worktreePath: null,
pullRequests: [],
worktrees: [],
latestTurn: null,
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function makeThread(
branch: null,
worktreePath: null,
pullRequests: [],
worktrees: [],
latestTurn: null,
createdAt: "2026-06-01T00:00:00.000Z",
updatedAt: "2026-06-01T00:00:00.000Z",
Expand Down
20 changes: 16 additions & 4 deletions apps/mobile/src/features/review/nativeReviewDiffAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,17 @@ function mapLineRow(
};
}

/**
* The path the native list shows and review comments quote. The list has no
* repo header row, so files of a multi-repo diff carry their repo folder as a
* prefix instead; that same prefixed path names the file unambiguously in a
* comment the agent reads across several repos. `file.path` itself stays
* repo-relative for language detection.
*/
function displayFilePath(file: ReviewRenderableFile): string {
return file.repoLabel === null ? file.path : `${file.repoLabel}/${file.path}`;
}

function prepareFileRows(file: ReviewRenderableFile): PreparedNativeReviewFileRows {
const cached = nativeReviewFileRowsCache.get(file);
if (cached) return cached;
Expand All @@ -430,7 +441,7 @@ function prepareFileRows(file: ReviewRenderableFile): PreparedNativeReviewFileRo
kind: "file",
id: `${file.id}:header`,
fileId: file.id,
filePath: file.path,
filePath: displayFilePath(file),
previousPath: file.previousPath,
changeType: mapChangeType(file),
additions: file.additions,
Expand All @@ -439,6 +450,7 @@ function prepareFileRows(file: ReviewRenderableFile): PreparedNativeReviewFileRo
];

const lineRows = file.rows.filter((row): row is ReviewRenderableLineRow => row.kind === "line");
const commentFilePath = displayFilePath(file);
let lineIndex = 0;
file.rows.forEach((row, rowIndex) => {
if (row.kind === "hunk") {
Expand All @@ -455,7 +467,7 @@ function prepareFileRows(file: ReviewRenderableFile): PreparedNativeReviewFileRo
rows.push(nativeRow);
rowIdByCommentLineId.set(row.id, nativeRow.id);
commentTargetsByRowId.set(nativeRow.id, {
filePath: file.path,
filePath: commentFilePath,
lines: lineRows,
lineIndex,
});
Expand All @@ -465,7 +477,7 @@ function prepareFileRows(file: ReviewRenderableFile): PreparedNativeReviewFileRo
rows.push(...noticeRowsForFile(file));
const prepared: PreparedNativeReviewFileRows = {
fileId: file.id,
filePath: file.path,
filePath: commentFilePath,
lineCount: lineRows.length,
// Comments must not split the source deletion/addition runs used for word matching.
rows: addNativeWordDiffRanges(rows),
Expand Down Expand Up @@ -537,7 +549,7 @@ function prepareNativeReviewDiffData(parsedDiff: ReviewParsedDiff): PreparedNati

const files = parsedDiff.files.map<NativeReviewDiffFile>((file) => ({
id: file.id,
path: file.path,
path: displayFilePath(file),
language: getLanguageForPath(file.path, file.languageHint),
additions: file.additions,
deletions: file.deletions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function section(id: string, kind: ReviewSectionKind): ReviewSectionItem {
title: id,
subtitle: null,
diff: null,
groups: null,
isLoading: false,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function makeFile(id: string): ReviewRenderableFile {
additions: 0,
deletions: 0,
languageHint: null,
repoLabel: null,
additionLines: [],
deletionLines: [],
rows: [],
Expand Down
120 changes: 117 additions & 3 deletions apps/mobile/src/features/review/reviewModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getDefaultReviewSectionId,
getReviewFilePreviewState,
getReviewSectionIdForCheckpoint,
toReviewTurnDiff,
type ReviewRenderableFile,
} from "./reviewModel";

Expand Down Expand Up @@ -41,6 +42,7 @@ function makeRenderableFile(
additions: 0,
deletions: 0,
languageHint: null,
repoLabel: null,
additionLines: [],
deletionLines: [],
rows: [],
Expand Down Expand Up @@ -88,9 +90,9 @@ describe("buildReviewSectionItems", () => {
const loadedTurnId = getReviewSectionIdForCheckpoint(checkpoints[0]!);
const items = buildReviewSectionItems({
checkpoints,
gitSections,
gitPreviews: [{ repoRoot: null, sources: gitSections }],
turnDiffById: {
[loadedTurnId]: "diff --git a/loaded.ts b/loaded.ts",
[loadedTurnId]: { diff: "diff --git a/loaded.ts b/loaded.ts", groups: null },
},
loadingTurnIds: {
[getReviewSectionIdForCheckpoint(checkpoints[1]!)]: true,
Expand All @@ -109,13 +111,84 @@ describe("buildReviewSectionItems", () => {
isLoading: false,
diff: expect.stringContaining("loaded.ts"),
});
expect(items[2]).toMatchObject({
title: "Dirty worktree",
subtitle: "Tracked, staged, and untracked worktree changes",
diff: "diff --git a/a.ts b/a.ts",
groups: null,
});
expect(items[3]).toMatchObject({ subtitle: "main ... feature", groups: null });
expect(getDefaultReviewSectionId(items)).toBe("turn:2");
});

it("folds one preview per repo into grouped git sections", () => {
const source = (
kind: ReviewDiffPreviewSource["kind"],
diff: string,
baseRef: string | null,
): ReviewDiffPreviewSource => ({
id: kind,
kind,
title: kind === "working-tree" ? "Dirty worktree" : "Against main",
baseRef,
headRef: kind === "working-tree" ? null : "feature",
diff,
diffHash: `${kind}:${diff}`,
truncated: false,
});

const items = buildReviewSectionItems({
checkpoints: [],
gitPreviews: [
{
repoRoot: "/repos/workspace/api",
sources: [
source("working-tree", "", "HEAD"),
source("branch-range", "api-branch", "main"),
],
},
{
repoRoot: "/repos/workspace/web/",
sources: [
source("working-tree", "web-dirty", "HEAD"),
source("branch-range", "web-branch", null),
],
},
],
turnDiffById: {},
loadingTurnIds: {},
loadingGitSections: false,
});

expect(items.map((item) => item.id)).toEqual(["git:working-tree", "git:branch-range"]);
expect(items[0]).toMatchObject({
diff: "\nweb-dirty",
groups: [
{ repoRoot: "/repos/workspace/api", displayName: "api", diff: "" },
{ repoRoot: "/repos/workspace/web/", displayName: "web", diff: "web-dirty" },
],
});
expect(items[1]).toMatchObject({
subtitle: "2 repositories",
diff: "api-branch\nweb-branch",
});
});

it("carries turn diff groups only for multi-repo threads", () => {
const api = { repoRoot: "/repos/api", displayName: "api", diff: "a" };
const web = { repoRoot: "/repos/web", displayName: "web", diff: "b" };
expect(toReviewTurnDiff({ diff: "a", groups: [api] })).toEqual({ diff: "a", groups: null });
expect(toReviewTurnDiff({ diff: "a" })).toEqual({ diff: "a", groups: null });
expect(toReviewTurnDiff({ diff: "a\nb", groups: [api, web] })).toEqual({
diff: "a\nb",
groups: [api, web],
});
});

it("shows dirty worktree while git preview is loading", () => {
const items = buildReviewSectionItems({
checkpoints: [],
gitSections: [],
gitPreviews: [],
turnDiffById: {},
loadingTurnIds: {},
loadingGitSections: true,
Expand Down Expand Up @@ -165,6 +238,47 @@ describe("buildReviewParsedDiff", () => {
});
});

it("labels files by repo when parsing a grouped diff", () => {
const patch = (path: string) =>
[
`diff --git a/${path} b/${path}`,
"index 1111111..2222222 100644",
`--- a/${path}`,
`+++ b/${path}`,
"@@ -1,1 +1,1 @@",
"-const before = 1;",
"+const after = 2;",
].join("\n");
const groups = [
{ repoRoot: "/repos/api", displayName: "api", diff: patch("src/server.ts") },
{ repoRoot: "/repos/web", displayName: "web", diff: "" },
{ repoRoot: "/repos/docs", displayName: "docs", diff: patch("README.md") },
];

const parsed = buildReviewParsedDiff(
groups.map((group) => group.diff).join("\n"),
"unit",
groups,
);

expect(parsed.kind).toBe("files");
if (parsed.kind !== "files") {
return;
}
expect(parsed.files.map((file) => [file.repoLabel, file.path])).toEqual([
["api", "src/server.ts"],
["docs", "README.md"],
]);
expect(new Set(parsed.files.map((file) => file.id)).size).toBe(2);
expect(parsed.additions).toBe(2);
expect(parsed.deletions).toBe(2);

const single = buildReviewParsedDiff(patch("src/server.ts"), "unit", [groups[0]!]);
expect(single.kind === "files" && single.files[0]?.repoLabel).toBe("api");
const flat = buildReviewParsedDiff(patch("src/server.ts"), "unit", null);
expect(flat.kind === "files" && flat.files[0]?.repoLabel).toBeNull();
});

it("builds renderable rows from a unified patch", () => {
const parsed = buildReviewParsedDiff(
[
Expand Down
Loading
Loading