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
18 changes: 18 additions & 0 deletions apps/server/src/serverSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,24 @@ it.layer(NodeServices.layer)("server settings", (it) => {
}).pipe(Effect.provide(makeServerSettingsLayer())),
);

it.effect("round-trips addProjectBaseDirectory through patch + decode", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;

const next = yield* serverSettings.updateSettings({
addProjectBaseDirectory: "/Users/tester/Projects",
});

assert.equal(next.addProjectBaseDirectory, "/Users/tester/Projects");

const followup = yield* serverSettings.updateSettings({
addProjectBaseDirectory: "A",
});

assert.equal(followup.addProjectBaseDirectory, "A");
}).pipe(Effect.provide(makeServerSettingsLayer())),
);

it.effect("writes only non-default server settings to disk", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
Expand Down
79 changes: 78 additions & 1 deletion apps/server/src/workspaceEntries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process";

import { afterEach, assert, describe, it, vi } from "vitest";

import { searchWorkspaceEntries } from "./workspaceEntries";
import { browseDirectories, searchWorkspaceEntries } from "./workspaceEntries";

const tempDirs: string[] = [];

Expand Down Expand Up @@ -200,3 +200,80 @@ describe("searchWorkspaceEntries", () => {
assert.isAtMost(peakReads, 32);
});
});

describe("browseDirectories", () => {
afterEach(() => {
vi.restoreAllMocks();
for (const dir of tempDirs.splice(0, tempDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it("returns directory entries relative to cwd with resolvedParent", async () => {
const cwd = makeTempDir("marcode-browse-basic-");
fs.mkdirSync(path.join(cwd, "alpha"));
fs.mkdirSync(path.join(cwd, "beta"));
writeFile(cwd, "readme.md", "");

const result = await browseDirectories({ cwd, pathQuery: "", limit: 100 });
const names = result.entries.map((entry) => entry.path);

assert.sameMembers(names, ["alpha", "beta"]);
assert.equal(result.resolvedParent, path.resolve(cwd));
assert.isFalse(result.truncated);
});

it("resolves absolute paths in pathQuery regardless of cwd", async () => {
const cwd = makeTempDir("marcode-browse-cwd-");
const other = makeTempDir("marcode-browse-abs-");
fs.mkdirSync(path.join(other, "nested"));

const result = await browseDirectories({ cwd, pathQuery: `${other}/`, limit: 100 });
const names = result.entries.map((entry) => entry.path);

assert.equal(result.resolvedParent, path.resolve(other));
assert.include(
names.map((name) => path.basename(name)),
"nested",
);
});

it("expands ~/ in cwd to the user's home directory", async () => {
const homeDir = os.homedir();
const sentinel = `marcode-browse-home-sentinel-${process.pid}-${Date.now()}`;
const sentinelPath = path.join(homeDir, sentinel);
fs.mkdirSync(sentinelPath);
try {
const result = await browseDirectories({ cwd: "~/", pathQuery: "", limit: 100 });

assert.equal(result.resolvedParent, path.resolve(homeDir));
assert.isTrue(result.entries.some((entry) => path.basename(entry.path) === sentinel));
} finally {
fs.rmSync(sentinelPath, { recursive: true, force: true });
}
});

it("resolves ../ relative to cwd", async () => {
const parent = makeTempDir("marcode-browse-parent-");
const child = path.join(parent, "child");
fs.mkdirSync(child);
fs.mkdirSync(path.join(parent, "sibling"));

const result = await browseDirectories({ cwd: child, pathQuery: "../", limit: 100 });

assert.equal(result.resolvedParent, path.resolve(parent));
const names = result.entries.map((entry) => path.basename(entry.path));
assert.includeMembers(names, ["child", "sibling"]);
});

it("returns empty entries and resolvedParent when directory does not exist", async () => {
const cwd = makeTempDir("marcode-browse-missing-");
const missing = path.join(cwd, "does-not-exist");

const result = await browseDirectories({ cwd: missing, pathQuery: "", limit: 100 });

assert.deepEqual([...result.entries], []);
assert.equal(result.resolvedParent, path.resolve(missing));
assert.isFalse(result.truncated);
});
});
19 changes: 15 additions & 4 deletions apps/server/src/workspaceEntries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import type { Dirent } from "node:fs";
import os from "node:os";
import path from "node:path";
import { runProcess } from "./processRunner";

Expand Down Expand Up @@ -580,19 +581,29 @@ function parsePathQueryComponents(pathQuery: string): { parentDir: string; nameP
};
}

function expandHomePath(value: string): string {
if (value === "~") return os.homedir();
if (value.startsWith("~/") || value.startsWith("~\\")) {
return path.join(os.homedir(), value.slice(2));
}
return value;
}

export async function browseDirectories(
input: ProjectBrowseDirectoriesInput,
): Promise<ProjectBrowseDirectoriesResult> {
const { parentDir, namePrefix } = parsePathQueryComponents(input.pathQuery);
const resolvedParent = path.resolve(input.cwd, parentDir);
const expandedCwd = expandHomePath(input.cwd);
const expandedPathQuery = expandHomePath(input.pathQuery);
const { parentDir, namePrefix } = parsePathQueryComponents(expandedPathQuery);
const resolvedParent = path.resolve(expandedCwd, parentDir);
const limit = Math.max(0, Math.floor(input.limit));
const lowerPrefix = namePrefix.toLowerCase();

let dirents: Dirent[];
try {
dirents = await fs.readdir(resolvedParent, { withFileTypes: true });
} catch {
return { entries: [], truncated: false };
return { entries: [], truncated: false, resolvedParent };
}

dirents.sort((a, b) => a.name.localeCompare(b.name));
Expand Down Expand Up @@ -627,5 +638,5 @@ export async function browseDirectories(
});
}

return { entries, truncated };
return { entries, truncated, resolvedParent };
}
2 changes: 2 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5171,6 +5171,8 @@ export default function ChatView({ threadId, environmentId: environmentIdProp }:
>
<ComposerAttachmentsPopover
threadId={activeThread.id}
environmentId={activeThreadEnvironmentId ?? null}
projectCwd={activeProjectCwd}
additionalDirectories={activeThread.additionalDirectories}
onLocalDirectoriesChange={
isLocalDraftThread ? setDraftAdditionalDirectories : undefined
Expand Down
Loading
Loading