# Files changed · PR \#21631 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/21631/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(assets\): preserve the PR repository for GitHub media](https://git.cafe/pingdotgg/t3code/pull/21631.md)

Version: 1

Head commit: 2503bd508df4fa5f827f6c23cef252fc65596134

Base commit: 1451e7cb23034d29774338608368a15d8845df36

Comparison base: 1451e7cb23034d29774338608368a15d8845df36

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx

Status: modified; +4; −3

```diff
@@ -39,9 +39,10 @@
     projectId: target.input.projectId,
   });
   const cwd = project?.workspaceRoot;
+  const repository = props.params.repository;
   const media = useMemo(
-    () => (cwd ? { environmentId: target.environmentId, cwd } : null),
+    () => (cwd ? { environmentId: target.environmentId, cwd, repository } : null),
+    [target.environmentId, cwd, repository],
-    [target.environmentId, cwd],
   );
   return (
     <RepositoryContext value={props.value}>
@@ -61,7 +62,7 @@
   return (
     <ThreadMarkdownImageView
       uri={media.uri}
+      sourceKey={JSON.stringify(media.source)}
-      sourceKey={props.href}
       unavailable={media.unavailable || !/^https?:\/\//i.test(props.href)}
       alt={props.alt}
       onPressPreview={(source) =>
```

## apps/mobile/src/features/pull-requests/PullRequestMedia.tsx

Status: modified; +13; −16

```diff
@@ -1,8 +1,10 @@
 import { createContext, useContext, useMemo, useState } from "react";
-import { pullRequestMediaSource, pullRequestImageSource } from "./pullRequestMediaSource";
+import {
+  pullRequestMediaSource,
+  pullRequestImageSource,
+  type PullRequestMediaLocation,
+} from "./pullRequestMediaSource";
 import { useFilePreviewGallery } from "../../components/FilePreviewGallery";
-import type { AssetResource, EnvironmentId } from "@t3tools/contracts";
-import { githubMediaFetchUrl } from "@t3tools/shared/githubMedia";
 import { videoMimeType } from "@t3tools/shared/video";
 import { Pressable, View } from "react-native";
 import { Image } from "expo-image";
@@ -18,24 +20,18 @@
   type FilePreviewItem,
 } from "../../components/FilePreviewModal";
 
-export const PullRequestMediaContext = createContext<{
-  readonly environmentId: EnvironmentId;
-  readonly cwd: string;
+export const PullRequestMediaContext = createContext<PullRequestMediaLocation | null>(null);
-} | null>(null);
 
 export function usePullRequestMediaUrl(url: string) {
   const context = useContext(PullRequestMediaContext);
+  const source = useMemo(() => pullRequestMediaSource(url, context), [context, url]);
+  const resource = "resource" in source ? (source.resource ?? null) : null;
-  const fetchUrl = githubMediaFetchUrl(url);
-  const resource = useMemo<AssetResource | null>(
-    () => (context && fetchUrl ? { _tag: "github-media", cwd: context.cwd, url: fetchUrl } : null),
-    [context, fetchUrl],
-  );
   const asset = useAssetUrlState(context?.environmentId ?? null, resource);
   const refresh = useRefreshAssetUrl(context?.environmentId ?? null, resource);
   return {
     uri: asset._tag === "Success" ? asset.url : resource ? null : url,
     unavailable: resource !== null && asset._tag === "Failure",
+    source,
-    source: pullRequestMediaSource(url, context),
     resolvePlaybackUri: resource ? async () => (await refresh()) ?? null : undefined,
   };
 }
@@ -51,13 +47,14 @@
   const withGallery = useFilePreviewGallery();
   const [image, setImage] = useState<FilePreviewSource | null>(null);
   const [video, setVideo] = useState<VideoPreviewSource | null>(null);
+  const sourceKey = JSON.stringify(media.source);
   const name = props.caption || new URL(props.url).pathname.split("/").pop() || "Attachment";
   if (props.kind === "image")
     return (
       <>
         <ThreadMarkdownImageView
           uri={media.uri}
-          sourceKey={props.url}
+          sourceKey={sourceKey}
           unavailable={media.unavailable}
           alt={name}
           onPressPreview={(source) =>
@@ -78,7 +75,7 @@
       {props.compact ? (
         <VideoAttachmentTile
           name={name}
+          sourceIdentifier={sourceKey}
-          sourceIdentifier={props.url}
           thumbnailSource={media.uri}
           style={{ width: 176, height: 112, borderRadius: 16 }}
           onPress={() =>
@@ -97,7 +94,7 @@
           uri={media.uri}
           unavailable={media.unavailable}
           name={name}
+          thumbnailKey={sourceKey}
-          thumbnailKey={props.url}
           resolvePlaybackUri={media.resolvePlaybackUri}
         />
       )}
```

## apps/mobile/src/features/pull-requests/pullRequestMediaSource.test.ts

Status: modified; +18; −2

```diff
@@ -4,7 +4,11 @@
 import { mediaVideoPreviewUri, mediaVideoThumbnailKey } from "../../lib/videoPreviewSource";
 import { pullRequestImageSource, pullRequestMediaSource } from "./pullRequestMediaSource";
 
+const location = {
+  environmentId: EnvironmentId.make("test"),
+  cwd: "/repo",
-const location = { environmentId: EnvironmentId.make("test"), cwd: "/repo" };
+  repository: "enterprise/private",
+};
 const url = "https://github.com/user-attachments/assets/abc-123";
 
 describe("PR media previews", () => {
@@ -15,6 +19,15 @@
       mimeType: "video/mp4",
       ...pullRequestMediaSource(url, location),
     };
+    expect(source).toMatchObject({
+      environmentId: location.environmentId,
+      resource: {
+        _tag: "github-media",
+        cwd: location.cwd,
+        url,
+        repository: location.repository,
+      },
+    });
     expect(mediaVideoPreviewUri(source, null)).toBeNull();
     expect(mediaVideoPreviewUri(source, "https://signed/first")).toBe("https://signed/first");
     expect(mediaVideoPreviewUri(source, "https://signed/renewed")).toBe("https://signed/renewed");
@@ -27,7 +40,10 @@
     const selected = { ...second, sourceIdentifier: "tapped" };
     const gallery = filePreviewGallery(selected, [first, second]);
     expect(gallery).toEqual([first, selected]);
+    expect(gallery).toMatchObject([
+      { resource: { url, repository: location.repository } },
+      { resource: { url: `${url}-second`, repository: location.repository } },
-    expect(gallery.every((item) => "resource" in item)).toBe(true);
+    ]);
   });
 
   it("leaves custom-domain media as direct URLs", () => {
```

## apps/mobile/src/features/pull-requests/pullRequestMediaSource.ts

Status: modified; +7; −1

```diff
@@ -5,6 +5,7 @@
 export interface PullRequestMediaLocation {
   readonly environmentId: EnvironmentId;
   readonly cwd: string;
+  readonly repository?: string;
 }
 
 /** Keep the original resource identity so previewers can resolve and renew its capability. */
@@ -13,7 +14,12 @@
   return location && fetchUrl
     ? {
         environmentId: location.environmentId,
-        resource: { _tag: "github-media" as const, cwd: location.cwd, url: fetchUrl },
+        resource: {
+          _tag: "github-media" as const,
+          cwd: location.cwd,
+          url: fetchUrl,
+          ...(location.repository !== undefined ? { repository: location.repository } : {}),
+        },
       }
     : { uri: url };
 }
```

## apps/mobile/src/lib/videoPreviewSource.ts

Status: modified; +1; −0

```diff
@@ -52,6 +52,7 @@
                 source.environmentId,
                 source.resource.cwd,
                 source.resource.url,
+                source.resource.repository ?? "",
               ]
             : [
                 "media-video",
```

## apps/server/src/assets/AssetAccess.test.ts

Status: modified; +27; −0

```diff
@@ -4,6 +4,7 @@
 import * as NodeFSP from "node:fs/promises";
 import {
   AssetAccessError,
+  AssetCreateUrlInput,
   AssetPreviewTypeValidationError,
   ThreadId,
   TurnItemId,
@@ -44,6 +45,8 @@
   return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) };
 });
 
+const decodeAssetCreateUrlInput = Schema.decodeUnknownEffect(AssetCreateUrlInput);
+
 const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
   prefix: "t3-asset-access-test-",
 });
@@ -1459,6 +1462,30 @@
     }).pipe(Effect.provide(layerTest)),
   );
 
+  it.effect("preserves the PR repository through RPC decoding and signed media capabilities", () =>
+    Effect.gen(function* () {
+      const resource = {
+        _tag: "github-media",
+        cwd: "/personal-fork",
+        url: "https://github.com/user-attachments/assets/1a1842fb-6383-492f-873c-57aa0033fa6c",
+        repository: "enterprise/private",
+      };
+      const input = yield* decodeAssetCreateUrlInput({ resource });
+      const issued = yield* issueAssetUrl(input);
+      const suffix = issued.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length);
+      const separator = suffix.indexOf("/");
+      const resolved = yield* resolveAsset(suffix.slice(0, separator), suffix.slice(separator + 1));
+
+      expect(resolved).toEqual({
+        kind: "github-media",
+        url: resource.url,
+        cwd: resource.cwd,
+        repository: resource.repository,
+        expiresAt: issued.expiresAt,
+      });
+    }).pipe(Effect.provide(layerTest)),
+  );
+
   it.effect("serves GitHub-hosted pull request media through the repository's credential", () =>
     Effect.gen(function* () {
       const resolve = (relativeUrl: string) => {
```

## apps/server/src/assets/AssetAccess.ts

Status: modified; +6; −0

```diff
@@ -162,6 +162,7 @@
     /** Already narrowed to a GitHub media host at mint time; the signature is what keeps it there. */
     url: Schema.String,
     cwd: Schema.String,
+    repository: Schema.optionalKey(Schema.String),
     expiresAt: Schema.Number,
   }),
 ]);
@@ -190,6 +191,7 @@
       readonly kind: "github-media";
       readonly url: string;
       readonly cwd: string;
+      readonly repository?: string;
       readonly expiresAt: number;
     };
 
@@ -775,6 +777,9 @@
         kind: "github-media",
         url: fetchUrl,
         cwd: input.resource.cwd,
+        ...(input.resource.repository !== undefined
+          ? { repository: input.resource.repository }
+          : {}),
         expiresAt,
       };
       fileName = githubMediaFileName(fetchUrl);
@@ -892,6 +897,7 @@
       kind: "github-media",
       url: claims.url,
       cwd: claims.cwd,
+      ...(claims.repository !== undefined ? { repository: claims.repository } : {}),
       expiresAt: claims.expiresAt,
     } satisfies ResolvedAsset;
   }
```

## apps/web/src/components/ChatMarkdown.tsx

Status: modified; +11; −1

```diff
@@ -243,6 +243,7 @@
   /** Loads GitHub-hosted media through `cwd`'s GitHub credential, which a private repository's
       uploads need; without it those images and videos load unauthenticated and 404. */
   githubMedia?: boolean | undefined;
+  githubMediaRepository?: string | undefined;
   headingLevelOffset?: number | undefined;
 }
 
@@ -2342,6 +2343,7 @@
   renderContextReference,
   headingLevelOffset = 0,
   githubMedia = false,
+  githubMediaRepository,
 }: ChatMarkdownProps) {
   const { resolvedTheme } = useTheme();
   const markdownRef = useRef<HTMLDivElement>(null);
@@ -2353,6 +2355,7 @@
     explicitEnvironmentId,
     cwd,
     imageBaseDir,
+    githubMediaRepository,
   ].join("\u0000");
   // Tagged with the scope it was opened in: a request from the previous scope can
   // resolve before the effect cleanup below invalidates it, and must stay hidden.
@@ -2724,6 +2727,7 @@
     expandMedia,
     fileLinkChip,
     githubMedia,
+    githubMediaRepository,
     renderContextReference,
     headingLevelOffset,
     imageBaseDir,
@@ -3174,6 +3178,7 @@
       cwd,
       environmentId,
       githubMedia,
+      githubMediaRepository,
       imageBaseDir,
       threadRef,
       workspaceMutationId,
@@ -3217,7 +3222,12 @@
       return (
         <ChatMarkdownAssetImage
           environmentId={environmentId}
-          resource={{ _tag: "github-media", cwd, url: githubMediaUrl }}
+          resource={{
+            _tag: "github-media",
+            cwd,
+            url: githubMediaUrl,
+            ...(githubMediaRepository !== undefined ? { repository: githubMediaRepository } : {}),
+          }}
           alt={altText}
           kind={kind}
           copyMarkdown={copyMarkdown}
```

## apps/web/src/components/pullRequest/PullRequestMarkdown.tsx

Status: modified; +14; −2

```diff
@@ -1,6 +1,7 @@
 import { ExternalLinkIcon, PaperclipIcon } from "lucide-react";
 import { markdownImageSourceFragment } from "@t3tools/client-runtime/markdown-images";
 import { githubMediaFetchUrl } from "@t3tools/shared/githubMedia";
+import { parseGitHubRepositoryNameWithOwnerFromRemoteUrl } from "@t3tools/shared/git";
 import type { AssetResource, EnvironmentId, ScopedThreadRef } from "@t3tools/contracts";
 import { createContext, useContext } from "react";
 import type { Options as ReactMarkdownOptions } from "react-markdown";
@@ -23,13 +24,20 @@
   cwd,
   url,
   fetchUrl,
+  repository,
 }: {
   environmentId: EnvironmentId;
   cwd: string;
   url: string;
   fetchUrl: string;
+  repository: string | undefined;
 }) {
-  const resource: AssetResource = { _tag: "github-media", cwd, url: fetchUrl };
+  const resource: AssetResource = {
+    _tag: "github-media",
+    cwd,
+    url: fetchUrl,
+    ...(repository !== undefined ? { repository } : {}),
+  };
   const assetUrl = useAssetUrlState(environmentId, resource);
   const refreshAssetUrl = useAssetUrlRefresh(environmentId, resource);
   const src =
@@ -62,6 +70,8 @@
   const segments = splitPullRequestBody(text);
   const context = useContext(PullRequestMarkdownContext);
   const repositoryUrl = context?.repositoryUrl;
+  const repository =
+    parseGitHubRepositoryNameWithOwnerFromRemoteUrl(repositoryUrl ?? null) ?? undefined;
   const resolvedThreadRef = threadRef ?? context?.threadRef ?? undefined;
   const extraRemarkPlugins: NonNullable<ReactMarkdownOptions["remarkPlugins"]> = repositoryUrl
     ? [[remarkPullRequestAutolinks, { repositoryUrl }]]
@@ -86,6 +96,7 @@
               environmentId={environmentId}
               extraRemarkPlugins={extraRemarkPlugins}
               githubMedia
+              githubMediaRepository={repository}
             />
           );
         }
@@ -93,11 +104,12 @@
         if (githubMediaUrl !== null) {
           return (
             <PullRequestGitHubVideo
-              key={`${segment.id}:${segment.url}`}
+              key={`${repository ?? ""}:${segment.id}:${segment.url}`}
               environmentId={environmentId}
               cwd={cwd}
               url={segment.url}
               fetchUrl={githubMediaUrl}
+              repository={repository}
             />
           );
         }
```

## packages/contracts/src/assets.ts

Status: modified; +3; −3

```diff
@@ -47,12 +47,12 @@
   Schema.TaggedStruct("native-app-icon", {
     app: ToolActivityNativeAppReference,
   }),
-  // An upload a pull request body points at on GitHub. A private repository serves these only
-  // to a request that carries a credential, which the client has none of, so the server fetches
-  // them with the `gh` credential the repository at `cwd` authenticates with.
+  // Private GitHub media needs the server credential; repository is the PR context, which
+  // can differ from the checkout at cwd when viewing an upstream or unrelated PR.
   Schema.TaggedStruct("github-media", {
     cwd: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)),
     url: TrimmedNonEmptyString.check(Schema.isMaxLength(2048)),
+    repository: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(512))),
   }),
 ]);
 export type AssetResource = typeof AssetResource.Type;
```
