Skip to content
Open
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
9 changes: 9 additions & 0 deletions apps/code/src/main/services/git/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,15 @@ export const getGithubPullRequestInput = getGithubIssueInput;

export const getGithubPullRequestOutput = getGithubIssueOutput;

export const getGithubFileContentInput = z.object({
owner: z.string(),
repo: z.string(),
filePath: z.string(),
ref: z.string(),
});

export const getGithubFileContentOutput = z.string().nullable();

export const createPrProgressPayload = z.object({
flowId: z.string(),
step: createPrStep,
Expand Down
29 changes: 29 additions & 0 deletions apps/code/src/main/services/git/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,35 @@ ${truncatedDiff || "(no diff available)"}${contextSection}`;
return refs[0] ?? null;
}

public async getGithubFileContent(
owner: string,
repo: string,
filePath: string,
ref: string,
): Promise<string | null> {
const encodedPath = filePath
.split("/")
.map((segment) => encodeURIComponent(segment))
.join("/");
const result = await execGh([
"api",
`/repos/${owner}/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`,
"-H",
"Accept: application/vnd.github.raw",
]);
if (result.exitCode !== 0) {
log.info("Failed to fetch file from GitHub", {
owner,
repo,
filePath,
ref,
stderr: result.stderr,
});
return null;
}
return result.stdout;
}

private async fetchGhRefs(
args: string[],
repo: string,
Expand Down
14 changes: 14 additions & 0 deletions apps/code/src/main/trpc/routers/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
getFileAtHeadOutput,
getGitBusyStateInput,
getGitBusyStateOutput,
getGithubFileContentInput,
getGithubFileContentOutput,
getGithubIssueInput,
getGithubIssueOutput,
getGithubPullRequestInput,
Expand Down Expand Up @@ -463,6 +465,18 @@ export const gitRouter = router({
getService().getGithubPullRequest(input.owner, input.repo, input.number),
),

getGithubFileContent: publicProcedure
.input(getGithubFileContentInput)
.output(getGithubFileContentOutput)
.query(({ input }) =>
getService().getGithubFileContent(
input.owner,
input.repo,
input.filePath,
input.ref,
),
),

onCreatePrProgress: publicProcedure.subscription(async function* (opts) {
const service = getService();
const iterable = service.toIterable(GitServiceEvent.CreatePrProgress, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { isMarkdownFile } from "@features/code-editor/utils/markdownUtils";
import { getRelativePath } from "@features/code-editor/utils/pathUtils";
import { usePanelLayoutStore } from "@features/panels";
import { useFileTreeStore } from "@features/right-sidebar/stores/fileTreeStore";
import { useSessionForTask } from "@features/sessions/hooks/useSession";
import { useCwd } from "@features/sidebar/hooks/useCwd";
import { useIsWorkspaceCloudRun } from "@features/workspace/hooks/useWorkspace";
import { Check, Copy } from "@phosphor-icons/react";
Expand All @@ -17,7 +18,7 @@ import {
isRasterImageFile,
parseImageDataUrl,
} from "@posthog/shared";
import { Box, Flex, IconButton, Text } from "@radix-ui/themes";
import { Box, Button, Flex, IconButton, Text } from "@radix-ui/themes";
import { trpcClient, useTRPC } from "@renderer/trpc/client";
import type { Task } from "@shared/types";

Expand Down Expand Up @@ -67,9 +68,26 @@ function FilePanelImagePreview({
);
}

const CLOUD_SANDBOX_REPOS_ROOT = "/tmp/workspace/repos";

function toRepoRelativePath(
owner: string,
name: string,
path: string,
): string | null {
if (!path.startsWith("/")) return path;
const prefix = `${CLOUD_SANDBOX_REPOS_ROOT}/${owner}/${name}/`;
if (!path.startsWith(prefix)) return null;
return path.slice(prefix.length);
}

function encodePathSegments(path: string): string {
return path.split("/").map(encodeURIComponent).join("/");
}
Comment on lines +73 to +86
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 lastIndexOf incorrectly strips the path when a subdirectory inside the repo shares the repo's short name — a very common pattern in Python projects (e.g., a posthog repo that contains a posthog/ package directory). For an absolute path like /workspace/runner/abc/posthog/posthog/utils.py, lastIndexOf("/posthog/") matches at the inner segment, returning utils.py instead of posthog/utils.py. The GitHub API call then fetches the wrong file (or a 404), and the blob URL is equally broken.

Suggested change
function toRepoRelativePath(
repoShortName: string | null,
path: string,
): string | null {
if (!path.startsWith("/")) return path;
if (!repoShortName) return null;
const marker = `/${repoShortName}/`;
const idx = path.lastIndexOf(marker);
if (idx < 0) return null;
return path.slice(idx + marker.length);
}
function toRepoRelativePath(
repoShortName: string | null,
path: string,
): string | null {
if (!path.startsWith("/")) return path;
if (!repoShortName) return null;
const marker = `/${repoShortName}/`;
const idx = path.indexOf(marker);
if (idx < 0) return null;
return path.slice(idx + marker.length);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx
Line: 71-81

Comment:
`lastIndexOf` incorrectly strips the path when a subdirectory inside the repo shares the repo's short name — a very common pattern in Python projects (e.g., a `posthog` repo that contains a `posthog/` package directory). For an absolute path like `/workspace/runner/abc/posthog/posthog/utils.py`, `lastIndexOf("/posthog/")` matches at the *inner* segment, returning `utils.py` instead of `posthog/utils.py`. The GitHub API call then fetches the wrong file (or a 404), and the blob URL is equally broken.

```suggestion
function toRepoRelativePath(
  repoShortName: string | null,
  path: string,
): string | null {
  if (!path.startsWith("/")) return path;
  if (!repoShortName) return null;
  const marker = `/${repoShortName}/`;
  const idx = path.indexOf(marker);
  if (idx < 0) return null;
  return path.slice(idx + marker.length);
}
```

How can I resolve this? If you propose a fix, please make it concise.


export function CodeEditorPanel({
taskId,
task: _task,
task,
absolutePath,
}: CodeEditorPanelProps) {
const trpcReact = useTRPC();
Expand Down Expand Up @@ -125,6 +143,38 @@ export function CodeEditorPanel({
filePath,
isCloudRun && !isImage,
);
const cloudSession = useSessionForTask(isCloudRun ? taskId : undefined);
const cloudFileMeta = useMemo(() => {
if (!isCloudRun) return null;
const repo = task.repository ?? null;
const branch = task.latest_run?.branch ?? cloudSession?.cloudBranch ?? null;
if (!repo || !branch) return null;
const [owner, name] = repo.split("/");
if (!owner || !name) return null;
const repoRelativePath = toRepoRelativePath(owner, name, filePath);
if (!repoRelativePath) return null;
return {
owner,
name,
branch,
repoRelativePath,
blobUrl: `https://github.com/${owner}/${name}/blob/${encodeURIComponent(branch)}/${encodePathSegments(repoRelativePath)}`,
};
}, [isCloudRun, task, cloudSession, filePath]);

const shouldFetchFromGithub =
isCloudRun && !isImage && !cloudFile.touched && cloudFileMeta != null;
const githubFileQuery = useQuery(
trpcReact.git.getGithubFileContent.queryOptions(
{
owner: cloudFileMeta?.owner ?? "",
repo: cloudFileMeta?.name ?? "",
filePath: cloudFileMeta?.repoRelativePath ?? "",
ref: cloudFileMeta?.branch ?? "",
},
{ enabled: shouldFetchFromGithub, staleTime: 5 * 60 * 1000 },
),
);

const repoQuery = useQuery(
trpcReact.fs.readRepoFile.queryOptions(
Expand All @@ -151,8 +201,16 @@ export function CodeEditorPanel({
);

const localQuery = isInsideRepo ? repoQuery : absoluteQuery;
const fileContent = isCloudRun ? cloudFile.content : localQuery.data;
const isLoading = isCloudRun ? cloudFile.isLoading : localQuery.isLoading;
const cloudContentQuery = shouldFetchFromGithub
? {
content: githubFileQuery.data ?? null,
isLoading: githubFileQuery.isLoading,
}
: { content: cloudFile.content, isLoading: cloudFile.isLoading };
const fileContent = isCloudRun ? cloudContentQuery.content : localQuery.data;
const isLoading = isCloudRun
? cloudContentQuery.isLoading
: localQuery.isLoading;
const error = isCloudRun ? null : localQuery.error;

const enrichment = useFileEnrichment({
Expand Down Expand Up @@ -198,10 +256,42 @@ export function CodeEditorPanel({
return <PanelMessage>Loading file...</PanelMessage>;
}

if (isCloudRun && !cloudFile.touched) {
if (isCloudRun && !cloudFile.touched && !shouldFetchFromGithub) {
return (
<PanelMessage detail={filePath}>
File content not available — the agent did not read or write this file,
and the cloud run's branch could not be resolved
</PanelMessage>
);
}

if (
isCloudRun &&
!cloudFile.touched &&
shouldFetchFromGithub &&
fileContent == null
) {
return (
<PanelMessage detail={filePath}>
File content not available — the agent did not read or write this file
<Flex direction="column" align="center" gap="2">
<Text className="text-sm">
Couldn't load file from GitHub — the agent did not read or write
this file
</Text>
{cloudFileMeta && (
<Button
size="1"
variant="soft"
onClick={() =>
trpcClient.os.openExternal.mutate({
url: cloudFileMeta.blobUrl,
})
}
>
View on GitHub
</Button>
)}
</Flex>
</PanelMessage>
);
}
Expand Down
Loading