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
20 changes: 16 additions & 4 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,23 @@ function DiffFileSection(props: {
),
[contextMode, filePath, fullContextDiffQuery.data?.diff, resolvedTheme],
);
const resolvedFileDiff =
contextMode === "full"
? (resolveRenderableFileDiff(fullContextPatch, filePath) ?? fileDiff)
: fileDiff;
const fullContextFileDiff =
contextMode === "full" ? resolveRenderableFileDiff(fullContextPatch, filePath) : null;
const resolvedFileDiff = contextMode === "full" ? (fullContextFileDiff ?? fileDiff) : fileDiff;
const fullContextError =
contextMode === "full" && fullContextDiffQuery.error
? fullContextDiffQuery.error instanceof Error
? fullContextDiffQuery.error.message
: "Failed to load full-file context."
: null;
const fullContextFallbackMessage =
contextMode === "full" &&
!fullContextError &&
!fullContextDiffQuery.isLoading &&
fullContextDiffQuery.data &&
fullContextFileDiff === null
? "Full-file context is unavailable for this file. Showing patch context."
: null;

return (
<section
Expand Down Expand Up @@ -324,6 +331,11 @@ function DiffFileSection(props: {
{fullContextError}
</div>
) : null}
{fullContextFallbackMessage ? (
<div className="border-b border-border/60 bg-amber-500/8 px-3 py-2 text-[11px] text-amber-700 dark:text-amber-300/90">
{fullContextFallbackMessage}
</div>
) : null}
<FileDiff
fileDiff={resolvedFileDiff}
options={{
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/i18n/I18nProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,13 @@ export function I18nProvider({ children }: { children: ReactNode }) {
locale={resolvedLocale}
defaultLocale="en"
messages={activeMessages}
onError={(error) => {
if ("code" in error && error.code === "MISSING_TRANSLATION") {
onError={(error: unknown) => {
if (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === "MISSING_TRANSLATION"
) {
return;
}

Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/lib/diffFileReviewState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ describe("setDiffFileContextMode", () => {
"src/a.ts": { accepted: true, collapsed: false, contextMode: "full" },
});
});

it("auto-expands a file when switching to full context", () => {
expect(
setDiffFileContextMode(
{
"src/a.ts": { accepted: false, collapsed: true, contextMode: "patch" },
},
"src/a.ts",
"full",
),
).toEqual({
"src/a.ts": { accepted: false, collapsed: false, contextMode: "full" },
});
});
});

describe("expandDiffFile", () => {
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/diffFileReviewState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function setDiffFileContextMode(
...current,
[path]: {
...previous,
collapsed: contextMode === "full" ? false : previous.collapsed,
contextMode,
},
};
Expand Down
166 changes: 27 additions & 139 deletions apps/web/src/lib/providerReactQuery.test.ts
Original file line number Diff line number Diff line change
@@ -1,161 +1,49 @@
import { ThreadId, type NativeApi } from "@okcode/contracts";
import { QueryClient } from "@tanstack/react-query";
import { afterEach, describe, expect, it, vi } from "vitest";
import { checkpointDiffQueryOptions, providerQueryKeys } from "./providerReactQuery";
import * as nativeApi from "../nativeApi";
import { describe, expect, it } from "vitest";
import { ThreadId } from "@okcode/contracts";
import { providerQueryKeys, checkpointDiffQueryOptions } from "./providerReactQuery";

const threadId = ThreadId.makeUnsafe("thread-id");

function mockNativeApi(input: {
getTurnDiff: ReturnType<typeof vi.fn>;
getFullThreadDiff: ReturnType<typeof vi.fn>;
}) {
vi.spyOn(nativeApi, "ensureNativeApi").mockReturnValue({
orchestration: {
getTurnDiff: input.getTurnDiff,
getFullThreadDiff: input.getFullThreadDiff,
},
} as unknown as NativeApi);
}

afterEach(() => {
vi.restoreAllMocks();
});
const threadId = ThreadId.makeUnsafe("thread-1");

describe("providerQueryKeys.checkpointDiff", () => {
it("includes cacheScope so reused turn counts do not collide", () => {
const baseInput = {
it("distinguishes patch and full-context file queries", () => {
const patchKey = providerQueryKeys.checkpointDiff({
threadId,
fromTurnCount: 1,
toTurnCount: 2,
} as const;

expect(
providerQueryKeys.checkpointDiff({
...baseInput,
cacheScope: "turn:old-turn",
}),
).not.toEqual(
providerQueryKeys.checkpointDiff({
...baseInput,
cacheScope: "turn:new-turn",
}),
);
});
});

describe("checkpointDiffQueryOptions", () => {
it("forwards checkpoint range to the provider API", async () => {
const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" });
const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" });
mockNativeApi({ getTurnDiff, getFullThreadDiff });

const options = checkpointDiffQueryOptions({
threadId,
fromTurnCount: 3,
toTurnCount: 4,
cacheScope: "turn:abc",
});

const queryClient = new QueryClient();
await queryClient.fetchQuery(options);

expect(getTurnDiff).toHaveBeenCalledWith({
threadId,
fromTurnCount: 3,
toTurnCount: 4,
});
expect(getFullThreadDiff).not.toHaveBeenCalled();
});

it("uses explicit full thread diff API when range starts from zero", async () => {
const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" });
const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" });
mockNativeApi({ getTurnDiff, getFullThreadDiff });

const options = checkpointDiffQueryOptions({
threadId,
fromTurnCount: 0,
toTurnCount: 2,
cacheScope: "thread:all",
});

const queryClient = new QueryClient();
await queryClient.fetchQuery(options);

expect(getFullThreadDiff).toHaveBeenCalledWith({
threadId,
toTurnCount: 2,
});
expect(getTurnDiff).not.toHaveBeenCalled();
});

it("fails fast on invalid range and does not call provider RPC", async () => {
const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" });
const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" });
mockNativeApi({ getTurnDiff, getFullThreadDiff });

const options = checkpointDiffQueryOptions({
threadId,
fromTurnCount: 4,
toTurnCount: 3,
cacheScope: "turn:invalid",
relativePath: "src/a.ts",
contextMode: "patch",
});

const queryClient = new QueryClient();

await expect(queryClient.fetchQuery(options)).rejects.toThrow(
"Checkpoint diff is unavailable.",
);
expect(getTurnDiff).not.toHaveBeenCalled();
expect(getFullThreadDiff).not.toHaveBeenCalled();
});

it("retries checkpoint-not-ready errors longer than generic failures", () => {
const options = checkpointDiffQueryOptions({
const fullKey = providerQueryKeys.checkpointDiff({
threadId,
fromTurnCount: 1,
toTurnCount: 2,
cacheScope: "turn:abc",
relativePath: "src/a.ts",
contextMode: "full",
});
const retry = options.retry;
expect(typeof retry).toBe("function");
if (typeof retry !== "function") {
throw new Error("Expected retry to be a function.");
}

expect(retry(1, new Error("Checkpoint turn count 2 exceeds current turn count 1."))).toBe(true);
expect(
retry(11, new Error("Filesystem checkpoint is unavailable for turn 2 in thread thread-1.")),
).toBe(true);
expect(
retry(12, new Error("Filesystem checkpoint is unavailable for turn 2 in thread thread-1.")),
).toBe(false);
expect(retry(2, new Error("Something else failed."))).toBe(true);
expect(retry(3, new Error("Something else failed."))).toBe(false);
expect(patchKey).not.toEqual(fullKey);
});
});

it("backs off longer for checkpoint-not-ready errors", () => {
describe("checkpointDiffQueryOptions", () => {
it("stays enabled for full-thread file-scoped full-context queries", () => {
const options = checkpointDiffQueryOptions({
threadId,
fromTurnCount: 1,
fromTurnCount: 0,
toTurnCount: 2,
cacheScope: "turn:abc",
relativePath: "src/a.ts",
contextMode: "full",
});
const retryDelay = options.retryDelay;
expect(typeof retryDelay).toBe("function");
if (typeof retryDelay !== "function") {
throw new Error("Expected retryDelay to be a function.");
}

const checkpointDelay = retryDelay(
4,
new Error("Checkpoint turn count 2 exceeds current turn count 1."),
expect(options.queryKey).toEqual(
providerQueryKeys.checkpointDiff({
threadId,
fromTurnCount: 0,
toTurnCount: 2,
relativePath: "src/a.ts",
contextMode: "full",
}),
);
const genericDelay = retryDelay(4, new Error("Network failure"));

expect(typeof checkpointDelay).toBe("number");
expect(typeof genericDelay).toBe("number");
expect((checkpointDelay ?? 0) > (genericDelay ?? 0)).toBe(true);
expect(options.enabled).toBe(true);
});
});
2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[install]
linker = "hoisted"
62 changes: 62 additions & 0 deletions scripts/dedupe-effect-node-modules.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const effectRoot = path.join(repoRoot, "node_modules", "effect");
const effectScopeDir = path.join(repoRoot, "node_modules", "@effect");

async function exists(targetPath) {
try {
await fs.lstat(targetPath);
return true;
} catch {
return false;
}
}

async function dedupeNestedEffect(packageDir) {
const nestedEffectDir = path.join(packageDir, "node_modules", "effect");
if (!(await exists(nestedEffectDir))) {
return false;
}

const nestedStat = await fs.lstat(nestedEffectDir);
if (nestedStat.isSymbolicLink()) {
const linkTarget = await fs.readlink(nestedEffectDir);
const resolvedTarget = path.resolve(path.dirname(nestedEffectDir), linkTarget);
if (resolvedTarget === effectRoot) {
return false;
}
}

await fs.rm(nestedEffectDir, { recursive: true, force: true });
const relativeTarget = path.relative(path.dirname(nestedEffectDir), effectRoot);
await fs.symlink(relativeTarget, nestedEffectDir, "dir");
return true;
}

async function main() {
if (!(await exists(effectRoot)) || !(await exists(effectScopeDir))) {
return;
}

const entries = await fs.readdir(effectScopeDir, { withFileTypes: true });
let dedupedCount = 0;
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const changed = await dedupeNestedEffect(path.join(effectScopeDir, entry.name));
if (changed) {
dedupedCount += 1;
}
}

if (dedupedCount > 0) {
console.log(`[dedupe-effect-node-modules] linked root effect into ${dedupedCount} package(s)`);
}
}

await main();
Loading