Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6c251f7
feat(desktop): remove skip-all onboarding affordance
AviPeltz May 5, 2026
7b235ee
feat(desktop): unify onboarding step layout and stack providers
AviPeltz May 5, 2026
ffe52dd
feat(desktop): auto-capture OpenAI OAuth callback via loopback
AviPeltz May 6, 2026
c6770aa
fix(desktop): route back to providers page after reconnect completes
AviPeltz May 6, 2026
f141360
fix(desktop): bind OAuth loopback to redirect_uri host, not hardcoded…
AviPeltz May 6, 2026
d928d2f
feat(desktop): use real app icon on connect-provider pages
AviPeltz May 6, 2026
65103ae
style(desktop): round superset icon on connect pages to match provide…
AviPeltz May 6, 2026
5e9fd20
style(desktop): scale superset icon so it fills the circular crop
AviPeltz May 6, 2026
fbf5cfa
style(desktop): use inline SupersetIcon SVG on connect pages
AviPeltz May 6, 2026
57eb759
feat(desktop): use docs logo.png for connect provider pages
AviPeltz May 6, 2026
e8c5e10
style(desktop): square off provider logos to match superset icon shape
AviPeltz May 6, 2026
f3beca3
style(desktop): match superset icon size and squared border container
AviPeltz May 6, 2026
9ce8c98
style(desktop): bump superset logo scale so brackets match codex/clau…
AviPeltz May 6, 2026
d5c9b41
style(desktop): leave superset logo at natural size, bump codex/claud…
AviPeltz May 6, 2026
23bdec3
style(desktop): use real superset brand glyph for setup tiles
AviPeltz May 6, 2026
fb31f5d
Merge remote-tracking branch 'origin/main' into v2-onboarding-flow-re…
AviPeltz May 6, 2026
fb945dc
feat(desktop): selectable worktree import in onboarding
AviPeltz May 6, 2026
a3bb042
refactor(desktop): share external-worktree filter between query and i…
AviPeltz May 6, 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
110 changes: 102 additions & 8 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
worktreeExists,
} from "../utils/git";
import { resolveWorktreePath } from "../utils/resolve-worktree-path";
import { selectExternalWorktreesForImport } from "../utils/select-external-worktrees-for-import";
import { copySupersetConfigToWorktree, loadSetupConfig } from "../utils/setup";
import {
createWorkspaceFromExternalWorktree,
Expand Down Expand Up @@ -869,14 +870,10 @@ export const createCreateProcedures = () => {
);
const trackedPaths = new Set(projectWorktrees.map((wt) => wt.path));

const externalWorktrees = allExternalWorktrees.filter((wt) => {
if (wt.path === project.mainRepoPath) return false;
if (wt.isBare) return false;
if (wt.isDetached) return false;
if (!wt.branch) return false;
if (trackedPaths.has(wt.path)) return false;
return true;
});
const externalWorktrees = selectExternalWorktreesForImport(
allExternalWorktrees,
{ mainRepoPath: project.mainRepoPath, trackedPaths },
);

for (const ext of externalWorktrees) {
// biome-ignore lint/style/noNonNullAssertion: filtered above
Expand Down Expand Up @@ -933,6 +930,103 @@ export const createCreateProcedures = () => {
});
}

return { imported };
}),
importExternalWorktrees: publicProcedure
.input(
z.object({
projectId: z.string(),
paths: z.array(z.string()).min(1),
}),
)
.mutation(async ({ input }) => {
const project = getProject(input.projectId);
if (!project) {
throw new Error(`Project ${input.projectId} not found`);
}
const knownBranches = await getKnownBranchesSafe(project.mainRepoPath);
const compareBaseBranch = resolveWorkspaceBaseBranch({
workspaceBaseBranch: project.workspaceBaseBranch,
defaultBranch: project.defaultBranch,
knownBranches,
});

const projectWorktrees = localDb
.select({ path: worktrees.path })
.from(worktrees)
.where(eq(worktrees.projectId, input.projectId))
.all();
const trackedPaths = new Set(projectWorktrees.map((wt) => wt.path));

const allExternalWorktrees = await listExternalWorktrees(
project.mainRepoPath,
);

const externalWorktrees = selectExternalWorktreesForImport(
allExternalWorktrees,
{
mainRepoPath: project.mainRepoPath,
trackedPaths,
requested: new Set(input.paths),
},
);

let imported = 0;
for (const ext of externalWorktrees) {
// biome-ignore lint/style/noNonNullAssertion: filtered above
const branch = ext.branch!;

const worktree = localDb
.insert(worktrees)
.values({
projectId: input.projectId,
path: ext.path,
branch,
baseBranch: compareBaseBranch,
gitStatus: {
branch,
needsRebase: false,
ahead: 0,
behind: 0,
lastRefreshed: Date.now(),
},
createdBySuperset: false,
})
.returning()
.get();

const maxTabOrder = getMaxProjectChildTabOrder(input.projectId);
localDb
.insert(workspaces)
.values({
projectId: input.projectId,
worktreeId: worktree.id,
type: "worktree",
branch,
name: branch,
tabOrder: maxTabOrder + 1,
})
.run();

await setBranchBaseConfig({
repoPath: project.mainRepoPath,
branch,
compareBaseBranch,
isExplicit: false,
});

copySupersetConfigToWorktree(project.mainRepoPath, ext.path);
imported++;
}

if (imported > 0) {
activateProject(project);
track("workspaces_bulk_imported", {
project_id: project.id,
imported_count: imported,
});
}

return { imported };
}),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
type PullRequestCommentsTarget,
resolveReviewThread,
} from "../utils/github";
import { selectExternalWorktreesForImport } from "../utils/select-external-worktrees-for-import";
import { getWorkspacePath } from "../utils/worktree";

const gitHubPRCommentsInputSchema = z.object({
Expand Down Expand Up @@ -354,20 +355,14 @@ export const createGitStatusProcedures = () => {
.all();
const trackedPaths = new Set(trackedWorktrees.map((wt) => wt.path));

return allWorktrees
.filter((wt) => {
if (wt.path === project.mainRepoPath) return false;
if (wt.isBare) return false;
if (wt.isDetached) return false;
if (!wt.branch) return false;
if (trackedPaths.has(wt.path)) return false;
return true;
})
.map((wt) => ({
path: wt.path,
// biome-ignore lint/style/noNonNullAssertion: filtered above
branch: wt.branch!,
}));
return selectExternalWorktreesForImport(allWorktrees, {
mainRepoPath: project.mainRepoPath,
trackedPaths,
}).map((wt) => ({
path: wt.path,
// biome-ignore lint/style/noNonNullAssertion: filtered above
branch: wt.branch!,
}));
}),
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { execSync } from "node:child_process";
import {
existsSync,
mkdirSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { listExternalWorktrees } from "./git";
import { selectExternalWorktreesForImport } from "./select-external-worktrees-for-import";

const TEST_DIR = join(
realpathSync(tmpdir()),
`superset-test-select-import-${process.pid}`,
);

function createTestRepo(name: string): string {
const repoPath = join(TEST_DIR, name);
mkdirSync(repoPath, { recursive: true });
execSync("git init -b main", { cwd: repoPath, stdio: "ignore" });
execSync("git config user.email 'test@test.com'", {
cwd: repoPath,
stdio: "ignore",
});
execSync("git config user.name 'Test'", { cwd: repoPath, stdio: "ignore" });
writeFileSync(join(repoPath, "README.md"), "# test\n");
execSync("git add . && git commit -m 'init'", {
cwd: repoPath,
stdio: "ignore",
});
return repoPath;
}

function addWorktree(
mainRepoPath: string,
branch: string,
worktreePath: string,
): void {
mkdirSync(worktreePath, { recursive: true });
execSync(`git worktree add "${worktreePath}" -b ${branch}`, {
cwd: mainRepoPath,
stdio: "ignore",
});
}

function addDetachedWorktree(mainRepoPath: string, worktreePath: string): void {
mkdirSync(worktreePath, { recursive: true });
execSync(`git worktree add --detach "${worktreePath}" HEAD`, {
cwd: mainRepoPath,
stdio: "ignore",
});
}

describe("selectExternalWorktreesForImport (real git worktrees)", () => {
let mainRepoPath: string;
let wtA: string;
let wtB: string;
let wtC: string;

beforeEach(() => {
if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true, force: true });
}
mkdirSync(TEST_DIR, { recursive: true });
mainRepoPath = createTestRepo("main-repo");
wtA = join(TEST_DIR, "wt-a");
wtB = join(TEST_DIR, "wt-b");
wtC = join(TEST_DIR, "wt-c");
addWorktree(mainRepoPath, "feat-a", wtA);
addWorktree(mainRepoPath, "feat-b", wtB);
addWorktree(mainRepoPath, "feat-c", wtC);
});

afterEach(() => {
if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true, force: true });
}
});

test("with no requested filter, returns all three external worktrees and excludes main repo", async () => {
const all = await listExternalWorktrees(mainRepoPath);
expect(all.map((w) => w.path).sort()).toEqual(
[mainRepoPath, wtA, wtB, wtC].sort(),
);

const result = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set(),
});
expect(result.map((w) => w.path).sort()).toEqual([wtA, wtB, wtC].sort());
expect(result.map((w) => w.branch).sort()).toEqual(
["feat-a", "feat-b", "feat-c"].sort(),
);
});

test("with requested = {wtA, wtC}, returns only those two", async () => {
const all = await listExternalWorktrees(mainRepoPath);

const result = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set(),
requested: new Set([wtA, wtC]),
});
expect(result.map((w) => w.path).sort()).toEqual([wtA, wtC].sort());
});

test("paths already tracked in the DB are skipped even when requested", async () => {
const all = await listExternalWorktrees(mainRepoPath);

const result = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set([wtB]),
requested: new Set([wtA, wtB, wtC]),
});
expect(result.map((w) => w.path).sort()).toEqual([wtA, wtC].sort());
});

test("requested set containing a path that no longer exists is silently ignored", async () => {
const all = await listExternalWorktrees(mainRepoPath);
const ghostPath = join(TEST_DIR, "wt-ghost");

const result = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set(),
requested: new Set([wtA, ghostPath]),
});
expect(result.map((w) => w.path)).toEqual([wtA]);
});

test("detached HEAD worktrees are skipped even when requested", async () => {
const wtDetached = join(TEST_DIR, "wt-detached");
addDetachedWorktree(mainRepoPath, wtDetached);

const all = await listExternalWorktrees(mainRepoPath);
const detachedEntry = all.find((w) => w.path === wtDetached);
expect(detachedEntry).toBeDefined();
expect(detachedEntry?.isDetached).toBe(true);

const result = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set(),
requested: new Set([wtA, wtDetached]),
});
expect(result.map((w) => w.path)).toEqual([wtA]);
});

test("empty requested set returns no worktrees", async () => {
const all = await listExternalWorktrees(mainRepoPath);

const result = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set(),
requested: new Set(),
});
expect(result).toEqual([]);
});

test("main repo path in the requested set never gets imported", async () => {
const all = await listExternalWorktrees(mainRepoPath);

const result = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set(),
requested: new Set([mainRepoPath, wtA]),
});
expect(result.map((w) => w.path)).toEqual([wtA]);
});

test("re-running selection after one worktree is marked tracked converges", async () => {
const all = await listExternalWorktrees(mainRepoPath);

// First pass: import wtA only
const firstPass = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set(),
requested: new Set([wtA]),
});
expect(firstPass.map((w) => w.path)).toEqual([wtA]);

// Simulate wtA now being tracked. A second pass with all three requested
// should return only wtB and wtC.
const secondPass = selectExternalWorktreesForImport(all, {
mainRepoPath,
trackedPaths: new Set([wtA]),
requested: new Set([wtA, wtB, wtC]),
});
expect(secondPass.map((w) => w.path).sort()).toEqual([wtB, wtC].sort());
});
});
Loading
Loading