From 619d09b43b65f9ef1ebac176504668477f7b6373 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 27 May 2026 15:53:25 +0100 Subject: [PATCH 1/2] Load file content from GitHub for cloud runs Clicking a file reference in a cloud run's chat showed "File content not available" whenever the agent only ran `grep`/`find` against it (never `Read`/`Edit`/`Write`). Now, when the file isn't in the agent's tool calls but we know the repo and branch, fetch the content from GitHub at that branch via `gh api /repos/{owner}/{repo}/contents/{path}?ref=...` with `Accept: application/vnd.github.raw` and render it in the same editor. Falls back to a "View on GitHub" button if the fetch returns null (404, private without `gh` auth, etc.). Closes #2390 Generated-By: PostHog Code Task-Id: 215cdb33-aeaf-40b8-8fcc-70a0024e69bd --- apps/code/src/main/services/git/schemas.ts | 9 ++ apps/code/src/main/services/git/service.ts | 29 ++++++ apps/code/src/main/trpc/routers/git.ts | 14 +++ .../components/CodeEditorPanel.tsx | 97 +++++++++++++++++-- 4 files changed, 143 insertions(+), 6 deletions(-) diff --git a/apps/code/src/main/services/git/schemas.ts b/apps/code/src/main/services/git/schemas.ts index f25a73f69c..31829da659 100644 --- a/apps/code/src/main/services/git/schemas.ts +++ b/apps/code/src/main/services/git/schemas.ts @@ -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, diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index 99ee93a957..f3800c8a0e 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -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 { + 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, diff --git a/apps/code/src/main/trpc/routers/git.ts b/apps/code/src/main/trpc/routers/git.ts index 21b7e65099..056d38f57c 100644 --- a/apps/code/src/main/trpc/routers/git.ts +++ b/apps/code/src/main/trpc/routers/git.ts @@ -37,6 +37,8 @@ import { getFileAtHeadOutput, getGitBusyStateInput, getGitBusyStateOutput, + getGithubFileContentInput, + getGithubFileContentOutput, getGithubIssueInput, getGithubIssueOutput, getGithubPullRequestInput, @@ -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, { diff --git a/apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx b/apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx index aa7b1f7bca..c78797d756 100644 --- a/apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx +++ b/apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx @@ -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"; @@ -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"; @@ -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); +} + export function CodeEditorPanel({ taskId, - task: _task, + task, absolutePath, }: CodeEditorPanelProps) { const trpcReact = useTRPC(); @@ -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}`, + }; + }, [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( @@ -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({ @@ -198,10 +251,42 @@ export function CodeEditorPanel({ return Loading file...; } - if (isCloudRun && !cloudFile.touched) { + if (isCloudRun && !cloudFile.touched && !shouldFetchFromGithub) { return ( - 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 + + ); + } + + if ( + isCloudRun && + !cloudFile.touched && + shouldFetchFromGithub && + fileContent == null + ) { + return ( + + + + Couldn't load file from GitHub — the agent did not read or write + this file + + {cloudFileMeta && ( + + )} + ); } From 2f9d15ab0bd3235e95f697ee77fab1b373290819 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 27 May 2026 16:21:25 +0100 Subject: [PATCH 2/2] Use exact cloud sandbox prefix and URL-encode blob URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Greptile review feedback: - Replace ambiguous `lastIndexOf("//")` heuristic with the exact cloud sandbox prefix `/tmp/workspace/repos///`. This handles repos whose name matches an inner subdirectory (e.g. `posthog` repo with a `posthog/` package directory) deterministically — the prior heuristic would have stripped one level too many. - URL-encode each segment of `repoRelativePath` (and the branch) when constructing the GitHub blob URL, so paths with spaces, `#`, or `?` produce a valid link. Generated-By: PostHog Code Task-Id: 215cdb33-aeaf-40b8-8fcc-70a0024e69bd --- .../components/CodeEditorPanel.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx b/apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx index c78797d756..ed6a18e7af 100644 --- a/apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx +++ b/apps/code/src/renderer/features/code-editor/components/CodeEditorPanel.tsx @@ -68,16 +68,21 @@ function FilePanelImagePreview({ ); } +const CLOUD_SANDBOX_REPOS_ROOT = "/tmp/workspace/repos"; + function toRepoRelativePath( - repoShortName: string | null, + owner: string, + name: string, 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); + 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("/"); } export function CodeEditorPanel({ @@ -146,14 +151,14 @@ export function CodeEditorPanel({ if (!repo || !branch) return null; const [owner, name] = repo.split("/"); if (!owner || !name) return null; - const repoRelativePath = toRepoRelativePath(name, filePath); + const repoRelativePath = toRepoRelativePath(owner, name, filePath); if (!repoRelativePath) return null; return { owner, name, branch, repoRelativePath, - blobUrl: `https://github.com/${owner}/${name}/blob/${branch}/${repoRelativePath}`, + blobUrl: `https://github.com/${owner}/${name}/blob/${encodeURIComponent(branch)}/${encodePathSegments(repoRelativePath)}`, }; }, [isCloudRun, task, cloudSession, filePath]);