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
14 changes: 10 additions & 4 deletions src/main/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const EMPTY_CATALOG: RegistryCatalog = {

// Short-lived cache so flipping between Discover sub-tabs doesn't refetch.
let cache: { at: number; data: RegistryCatalog } | null = null;
const CACHE_TTL_MS = 5 * 60 * 1000;
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour – was 5 min; reduces GitHub API rate-limit pressure

function authorName(author: IndexEntry["author"]): string | undefined {
if (!author) return undefined;
Expand Down Expand Up @@ -337,9 +337,15 @@ let treeCache: { at: number; blobs: TreeBlob[] } | null = null;
/** All file paths under a folder, via the cached recursive git tree. */
async function listFolderFiles(folder: string): Promise<string[]> {
if (!treeCache || Date.now() - treeCache.at >= CACHE_TTL_MS) {
const res = await fetch(TREE_URL, {
headers: { Accept: "application/vnd.github+json" },
});
// Use GITHUB_TOKEN / GH_TOKEN when available to avoid anonymous
// rate limits (60 req/h) on api.github.com. Authenticated requests
// get 5 000 req/h instead.
const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
};
if (ghToken) headers.Authorization = `Bearer ${ghToken}`;
const res = await fetch(TREE_URL, { headers });
if (!res.ok) throw new Error(`Tree fetch failed (${res.status})`);
const json = (await res.json()) as { tree?: TreeBlob[] };
treeCache = { at: Date.now(), blobs: json.tree ?? [] };
Expand Down
138 changes: 71 additions & 67 deletions src/renderer/src/components/AgentMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,79 +208,83 @@ const AgentMarkdown = memo(function AgentMarkdown({
}: {
children: string;
}): React.JSX.Element {
// Detect RTL content so Arabic, Persian, Hebrew etc. render right-aligned.
// dir="auto" lets the browser decide based on the first strong character.
return (
<Markdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ href, children }) => (
<a
href={href}
onClick={(e) => {
e.preventDefault();
if (!href) return;
try {
const url = new URL(href, "https://placeholder.invalid");
if (!["http:", "https:", "mailto:"].includes(url.protocol)) {
<div dir="auto">
<Markdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ href, children }) => (
<a
href={href}
onClick={(e) => {
e.preventDefault();
if (!href) return;
try {
const url = new URL(href, "https://placeholder.invalid");
if (!["http:", "https:", "mailto:"].includes(url.protocol)) {
return;
}
if (url.protocol === "http:" || url.protocol === "https:") {
const event = new CustomEvent("web-preview:navigate", {
detail: href,
});
document.dispatchEvent(event);
return;
}
} catch {
return;
}
if (url.protocol === "http:" || url.protocol === "https:") {
const event = new CustomEvent("web-preview:navigate", {
detail: href,
});
document.dispatchEvent(event);
return;
}
} catch {
return;
}
window.hermesAPI.openExternal(href);
}}
>
{children}
</a>
),
img: ({ src }) => {
if (typeof src !== "string" || src.length === 0) return null;
// ![alt](file.pdf) parses as a markdown image but isn't an image —
// route those to the download chip instead of letting MediaImage
// try to load a non-image MIME and fail. (Follow-up from #303.)
const token = describeImageSrc(src);
return token.isImage ? (
<MediaImage token={token} />
) : (
<DownloadChip token={token} />
);
},
code: ({ className, children, node, ...props }) => {
const isInline =
!className &&
typeof children === "string" &&
!children.includes("\n");
if (isInline) {
window.hermesAPI.openExternal(href);
}}
>
{children}
</a>
),
img: ({ src }) => {
if (typeof src !== "string" || src.length === 0) return null;
// ![alt](file.pdf) parses as a markdown image but isn't an image —
// route those to the download chip instead of letting MediaImage
// try to load a non-image MIME and fail. (Follow-up from #303.)
const token = describeImageSrc(src);
return token.isImage ? (
<MediaImage token={token} />
) : (
<DownloadChip token={token} />
);
},
code: ({ className, children, node, ...props }) => {
const isInline =
!className &&
typeof children === "string" &&
!children.includes("\n");
if (isInline) {
return (
<code className={className} {...props}>
{children}
</code>
);
}
// Source offset of the opening fence is stable as the block streams,
// so it survives react-markdown's streaming remounts (unlike index
// keys) and uniquely identifies this block within the message.
const start = node?.position?.start;
const blockId =
start != null
? `${start.offset ?? start.line}:${className ?? ""}`
: undefined;
return (
<code className={className} {...props}>
<CodeBlock className={className} blockId={blockId}>
{children}
</code>
</CodeBlock>
);
}
// Source offset of the opening fence is stable as the block streams,
// so it survives react-markdown's streaming remounts (unlike index
// keys) and uniquely identifies this block within the message.
const start = node?.position?.start;
const blockId =
start != null
? `${start.offset ?? start.line}:${className ?? ""}`
: undefined;
return (
<CodeBlock className={className} blockId={blockId}>
{children}
</CodeBlock>
);
},
}}
>
{children}
</Markdown>
},
}}
>
{children}
</Markdown>
</div>
);
});

Expand Down