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
35 changes: 29 additions & 6 deletions packages/opencode/rift-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type RiftAdapterOptions = {

type ExperimentalWorkspaceInput = {
experimental_workspace?: { register(type: string, adapter: WorkspaceAdapter): void };
project?: { id?: string; path?: string };
project?: { id?: string; worktree?: string };
worktree?: string;
directory?: string;
};
Expand Down Expand Up @@ -70,6 +70,28 @@ function workspaceDirectory(sourceDirectory: string, name: string) {
return path.join(managedRoot(sourceDirectory), name);
}

function availableWorkspaceName(rift: RiftModule, sourceDirectory: string, requested: string) {
let existing: Set<string>;
try {
existing = new Set(rift.list({ of: sourceDirectory }).map((directory) => path.basename(directory)));
} catch {
return requested;
}
if (!existing.has(requested)) return requested;
let suffix = 2;
while (existing.has(`${requested}-${suffix}`)) suffix += 1;
return `${requested}-${suffix}`;
}

export function resolveRiftSourceDirectory(directory: string) {
const resolved = path.resolve(directory);
const marker = `${path.sep}.rifts${path.sep}`;
const markerIndex = resolved.indexOf(marker);
if (markerIndex < 0) return resolved;
const namespace = resolved.slice(markerIndex + marker.length).split(path.sep)[0];
return namespace ? path.join(resolved.slice(0, markerIndex), namespace) : resolved;
}

function requireDirectory(config: WorkspaceInfo) {
if (!config.directory) throw new Error("Rift workspace is missing a directory");
return config.directory;
Expand All @@ -82,7 +104,7 @@ export function createRiftWorkspaceAdapter(rift: RiftModule, options: RiftAdapte
name: "Rift",
description: "Create a copy-on-write Rift workspace",
configure(config) {
const name = workspaceName(config);
const name = availableWorkspaceName(rift, sourceDirectory, workspaceName(config));
return {
...config,
type: "rift",
Expand Down Expand Up @@ -136,10 +158,11 @@ export function createRiftWorkspaceAdapter(rift: RiftModule, options: RiftAdapte
export async function registerRiftWorkspaceAdapter(input: ExperimentalWorkspaceInput, logger: Logger) {
const registrar = input.experimental_workspace;
const projectID = input.project?.id;
// A plugin loaded inside a managed workspace still belongs to the stable project checkout.
// Using input.worktree here would recursively nest new Rifts below the current Rift.
const sourceDirectory = input.project?.path ?? input.directory ?? input.worktree;
if (!registrar || !projectID || !sourceDirectory) return;
// Plugin directory/worktree values may identify a managed workspace. The project's
// worktree is OpenCode's canonical checkout and remains stable across workspaces.
const sourceCandidate = input.project?.worktree ?? input.directory ?? input.worktree;
if (!registrar || !projectID || !sourceCandidate) return;
const sourceDirectory = resolveRiftSourceDirectory(sourceCandidate);

let rift: RiftModule;
try {
Expand Down
38 changes: 35 additions & 3 deletions packages/opencode/test/tool-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import path from "node:path";
import { promisify } from "node:util";

import { createOpenCodeTools, OpenCodeCompassPlugin } from "../index.ts";
import { createRiftWorkspaceAdapter } from "../rift-workspace.ts";
import { createRiftWorkspaceAdapter, resolveRiftSourceDirectory } from "../rift-workspace.ts";

const execFileAsync = promisify(execFile);

Expand Down Expand Up @@ -145,7 +145,9 @@ describe("createOpenCodeTools", () => {
},
list: (options) => {
calls.push({ name: "list", options });
return [path.join(path.dirname(sourceDirectory), ".rifts", path.basename(sourceDirectory), "parser-fix")];
return calls.filter((call) => call.name === "list").length === 1
? []
: [path.join(path.dirname(sourceDirectory), ".rifts", path.basename(sourceDirectory), "parser-fix")];
},
}, { sourceDirectory, projectID: "project-1" });

Expand All @@ -168,7 +170,7 @@ describe("createOpenCodeTools", () => {
assert.deepEqual(target, { type: "local", directory: configured.directory });
assert.equal(listed?.[0]?.type, "rift");
assert.equal(listed?.[0]?.projectID, "project-1");
assert.deepEqual(calls.map((call) => call.name), ["init", "create", "list", "remove"]);
assert.deepEqual(calls.map((call) => call.name), ["list", "init", "create", "list", "remove"]);
assert.deepEqual(calls.find((call) => call.name === "create")?.options, {
from: sourceDirectory,
name: "parser-fix",
Expand Down Expand Up @@ -199,6 +201,36 @@ describe("createOpenCodeTools", () => {
assert.deepEqual(removed, [path.join(os.tmpdir(), "unexpected-rift")]);
});

test("Rift workspace adapter suffixes a registered workspace name", async () => {
const sourceDirectory = path.join(os.tmpdir(), "kompass-rift-source");
const existing = path.join(path.dirname(sourceDirectory), ".rifts", path.basename(sourceDirectory), "parser-fix");
const adapter = createRiftWorkspaceAdapter({
init: () => null,
create: () => "",
remove: () => undefined,
list: () => [existing],
}, { sourceDirectory, projectID: "project-1" });
const configured = await adapter.configure({
id: "wrk_1",
type: "rift",
name: "Parser Fix",
branch: null,
directory: null,
extra: null,
projectID: "project-1",
});

assert.equal(configured.name, "parser-fix-2");
assert.equal(configured.directory, `${existing}-2`);
});

test("Rift source resolution unwraps a removed managed workspace", () => {
assert.equal(
resolveRiftSourceDirectory("/projects/.rifts/repo/removed-workspace"),
"/projects/repo",
);
});

test("registers Navigator by default", async () => {
await withTempHome(async () => {
const navigatorClient = {
Expand Down
Loading