From f1d2a6fc93e8a8bfeaf7bc9335a99322638c37e5 Mon Sep 17 00:00:00 2001 From: jamesng16 Date: Wed, 8 Jul 2026 16:31:00 +0700 Subject: [PATCH 1/2] fix(discover): use GitHub token auth for tree fetch + extend cache TTL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes to prevent GitHub API rate limiting on the Discover page: 1. Extend CACHE_TTL_MS from 5 min → 60 min (12× fewer API calls) 2. Use GITHUB_TOKEN/GH_TOKEN env var for authenticated GitHub API requests in listFolderFiles — anonymous requests hit the 60 req/h limit; authenticated requests get 5 000 req/h. Fixes errors like 'Failed to load registry' and 'Tree fetch failed (403)' that users behind shared IPs (VPNs, NAT, geographic regions) see frequently. --- src/main/registry.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/registry.ts b/src/main/registry.ts index 18763175f..3a1a1cab5 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -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; @@ -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 { 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 = { + 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 ?? [] }; From a3da81d6828687e08820061be40229723aabee0f Mon Sep 17 00:00:00 2001 From: jamesng16 Date: Wed, 8 Jul 2026 17:05:00 +0700 Subject: [PATCH 2/2] fix(chat): add RTL support for message text via dir=auto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentMarkdown now wraps rendered markdown in a
so the browser can auto-detect RTL content (Arabic, Persian, Hebrew, etc.) and render text right-aligned with correct bidirectional ordering. LTR languages (English, Vietnamese, Chinese) are unaffected — dir="auto" falls back to LTR when the first strong character is LTR. No layout changes: the wrapper only affects text direction within the message bubble; the bubble itself and its chrome (avatar, copy button, timestamp) remain LTR. Fixes #788 --- src/renderer/src/components/AgentMarkdown.tsx | 138 +++++++++--------- 1 file changed, 71 insertions(+), 67 deletions(-) diff --git a/src/renderer/src/components/AgentMarkdown.tsx b/src/renderer/src/components/AgentMarkdown.tsx index 79e033352..1132814bc 100644 --- a/src/renderer/src/components/AgentMarkdown.tsx +++ b/src/renderer/src/components/AgentMarkdown.tsx @@ -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 ( - ( - { - e.preventDefault(); - if (!href) return; - try { - const url = new URL(href, "https://placeholder.invalid"); - if (!["http:", "https:", "mailto:"].includes(url.protocol)) { +
+ ( + { + 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} - - ), - 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 ? ( - - ) : ( - - ); - }, - code: ({ className, children, node, ...props }) => { - const isInline = - !className && - typeof children === "string" && - !children.includes("\n"); - if (isInline) { + window.hermesAPI.openExternal(href); + }} + > + {children} + + ), + 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 ? ( + + ) : ( + + ); + }, + code: ({ className, children, node, ...props }) => { + const isInline = + !className && + typeof children === "string" && + !children.includes("\n"); + if (isInline) { + return ( + + {children} + + ); + } + // 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 ( - + {children} - + ); - } - // 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 ( - - {children} - - ); - }, - }} - > - {children} - + }, + }} + > + {children} + +
); });