Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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,21 @@ function FilePanelImagePreview({
);
}

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);
}
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 +138,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(name, filePath);
if (!repoRelativePath) return null;
return {
owner,
name,
branch,
repoRelativePath,
blobUrl: `https://github.com/${owner}/${name}/blob/${branch}/${repoRelativePath}`,
Comment thread
k11kirky marked this conversation as resolved.
Outdated
};
}, [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 +196,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 +251,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
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}>
<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