From 071d27d1c355324f6ab7fed121d08f5fd1b4c520 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 12 Aug 2026 21:05:56 -0500 Subject: [PATCH] feat(desktop): paste a PR review comment as structured composer context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pasted GitHub PR comment deep link (#discussion_r… / #issuecomment-…) now lands as a typed review attachment instead of a bare url chip. The card attaches optimistically and resolves through gh in the background — author, file:line anchor, body, and the diff hunk — expanding at send into an anchored fenced block, so "address this" carries exactly what "this" is. When gh can't answer (offline, unauthenticated, foreign repo, remote gateway) the card downgrades to the plain url ref and nothing is lost. --- apps/desktop/electron/git-review-ops.ts | 72 +++++++++++++++++++ apps/desktop/electron/main.ts | 4 ++ apps/desktop/electron/preload.ts | 1 + .../src/app/chat/composer/attachments.tsx | 21 ++++-- apps/desktop/src/app/chat/composer/index.tsx | 13 ++++ apps/desktop/src/app/chat/composer/types.ts | 3 + .../app/chat/hooks/use-composer-actions.ts | 50 ++++++++++++- apps/desktop/src/app/chat/index.tsx | 3 + apps/desktop/src/app/chat/session-tile.tsx | 3 +- apps/desktop/src/app/contrib/types.ts | 1 + apps/desktop/src/app/contrib/wiring.tsx | 1 + apps/desktop/src/global.d.ts | 19 +++++ apps/desktop/src/lib/chat-runtime.test.ts | 28 ++++++++ apps/desktop/src/lib/chat-runtime.ts | 35 +++++++++ apps/desktop/src/lib/desktop-git.ts | 4 ++ apps/desktop/src/lib/icons.ts | 2 + apps/desktop/src/store/composer.ts | 2 +- 17 files changed, 255 insertions(+), 7 deletions(-) diff --git a/apps/desktop/electron/git-review-ops.ts b/apps/desktop/electron/git-review-ops.ts index 393ede5f85a43..95cdfba7ca39b 100644 --- a/apps/desktop/electron/git-review-ops.ts +++ b/apps/desktop/electron/git-review-ops.ts @@ -618,6 +618,77 @@ const prPayload = pr => ({ url: String(pr.url || '') }) +// A GitHub review-comment / issue-comment URL, as pasted from the browser. +// Captures owner, repo, PR number, and the comment kind + id. Review threads +// deep-link as `#discussion_r`; conversation-tab comments as +// `#issuecomment-`. +const PR_COMMENT_URL_RE = + /^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)(?:\/[^#\s]*)?#(discussion_r|issuecomment-)(\d+)$/ + +function parsePrCommentUrl(url) { + const match = PR_COMMENT_URL_RE.exec(String(url || '').trim()) + + if (!match) { + return null + } + + const [, owner, repo, prNumber, kind, id] = match + + return { id, kind: kind === 'discussion_r' ? 'review' : 'issue', owner, prNumber: Number(prNumber), repo } +} + +// Resolve a pasted PR comment URL into the structured context the composer +// attaches: author, body, and — for review comments — the file, line range, +// and the diff hunk the comment anchors to. Reads only; any failure (gh +// missing, unauthenticated, private repo, deleted comment) yields null and the +// paste falls back to being a plain URL. +async function reviewFetchPrComment(repoPath, ghBin, url) { + const parsed = parsePrCommentUrl(url) + + if (!parsed) { + return null + } + + let cwd + + try { + cwd = resolveRequestedPathForIpc(repoPath, { purpose: 'Review comment fetch' }) + } catch { + return null + } + + const endpoint = + parsed.kind === 'review' + ? `repos/${parsed.owner}/${parsed.repo}/pulls/comments/${parsed.id}` + : `repos/${parsed.owner}/${parsed.repo}/issues/comments/${parsed.id}` + + const res = await runGh(['api', endpoint], cwd, ghBin) + + if (!res.ok) { + return null + } + + try { + const data = JSON.parse(res.stdout) + + return { + author: String(data?.user?.login || ''), + body: String(data?.body || ''), + diffHunk: parsed.kind === 'review' ? String(data?.diff_hunk || '') : '', + kind: parsed.kind, + // `line` is the comment's anchor in the current diff; null once the code + // moved on (outdated comment) — `original_line` still says where it was. + line: data?.line ?? data?.original_line ?? null, + path: parsed.kind === 'review' ? String(data?.path || '') : '', + prNumber: parsed.prNumber, + startLine: data?.start_line ?? data?.original_start_line ?? null, + url: String(data?.html_url || url) + } + } catch { + return null + } +} + // The PR for each of the given branches, keyed by branch. Asks GitHub about the // branches we actually have sessions on rather than listing the repo's newest // PRs and hoping ours are in the page — on a busy repo they are not. One @@ -820,6 +891,7 @@ export { reviewCommitContext, reviewCreatePr, reviewDiff, + reviewFetchPrComment, reviewList, reviewPrList, reviewPush, diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 9c40a64739afd..d908d5fa18538 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -109,6 +109,7 @@ import { reviewCommitContext, reviewCreatePr, reviewDiff, + reviewFetchPrComment, reviewList, reviewPrList, reviewPush, @@ -11807,6 +11808,9 @@ ipcMain.handle('hermes:git:review:shipInfo', async (_event, repoPath) => reviewS ipcMain.handle('hermes:git:review:prList', async (_event, repoPath, branches, numbers) => reviewPrList(repoPath, resolveGhBinary(), branches, numbers) ) +ipcMain.handle('hermes:git:review:fetchPrComment', async (_event, repoPath, url) => + reviewFetchPrComment(repoPath, resolveGhBinary(), url) +) ipcMain.handle('hermes:git:review:createPr', async (_event, repoPath) => reviewCreatePr(repoPath, resolveGitBinary(), resolveGhBinary()) ) diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts index f2b16d1760576..0231ecf33e6c0 100644 --- a/apps/desktop/electron/preload.ts +++ b/apps/desktop/electron/preload.ts @@ -235,6 +235,7 @@ contextBridge.exposeInMainWorld('hermesDesktop', { shipInfo: repoPath => ipcRenderer.invoke('hermes:git:review:shipInfo', repoPath), prList: (repoPath, branches, numbers) => ipcRenderer.invoke('hermes:git:review:prList', repoPath, branches, numbers), + fetchPrComment: (repoPath, url) => ipcRenderer.invoke('hermes:git:review:fetchPrComment', repoPath, url), createPr: repoPath => ipcRenderer.invoke('hermes:git:review:createPr', repoPath) } }, diff --git a/apps/desktop/src/app/chat/composer/attachments.tsx b/apps/desktop/src/app/chat/composer/attachments.tsx index cb42dc9f1c99c..5d5028466a9c2 100644 --- a/apps/desktop/src/app/chat/composer/attachments.tsx +++ b/apps/desktop/src/app/chat/composer/attachments.tsx @@ -7,7 +7,7 @@ import { Codicon } from '@/components/ui/codicon' import { Tip } from '@/components/ui/tooltip' import { useImageDownload } from '@/hooks/use-image-download' import { useI18n } from '@/i18n' -import { AlertCircle, FileText, FolderOpen, ImageIcon, Link, Loader2, Terminal } from '@/lib/icons' +import { AlertCircle, FileText, FolderOpen, ImageIcon, Link, Loader2, MessageCode, Terminal } from '@/lib/icons' import { normalizeOrLocalPreviewTarget } from '@/lib/local-preview' import { cn } from '@/lib/utils' import type { ComposerAttachment } from '@/store/composer' @@ -33,14 +33,27 @@ export function AttachmentList({ function AttachmentPill({ attachment, onRemove }: { attachment: ComposerAttachment; onRemove?: (id: string) => void }) { const { t } = useI18n() const c = t.composer - const Icon = { folder: FolderOpen, url: Link, image: ImageIcon, file: FileText, terminal: Terminal }[attachment.kind] + const Icon = { + file: FileText, + folder: FolderOpen, + image: ImageIcon, + review: MessageCode, + terminal: Terminal, + url: Link + }[attachment.kind] // The tile's cwd when this pill lives in a tile composer, not the primary's: // a relative attachment path has to resolve against its own session's root. const cwd = useStore(useSessionView().$cwd) const isUploading = attachment.uploadState === 'uploading' const hasUploadError = attachment.uploadState === 'error' - const canPreview = attachment.kind !== 'folder' && attachment.kind !== 'terminal' && !isUploading - const detail = attachment.detail && attachment.detail !== attachment.label ? attachment.detail : undefined + // A review card's detail is its resolved-comment JSON, not a previewable + // path — clicking it should do nothing rather than toast a bogus failure. + const canPreview = + attachment.kind !== 'folder' && attachment.kind !== 'terminal' && attachment.kind !== 'review' && !isUploading + const detail = + attachment.kind !== 'review' && attachment.detail && attachment.detail !== attachment.label + ? attachment.detail + : undefined // An attached image already holds its full bytes as a data URL, so it belongs // in the same lightbox the thread uses. The rail is for files you read or // edit — not a picture you just want to look at. Images that never resolved a diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 56e0bf9c2f7b8..5d490eb61c412 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button' import { Slot as ContribSlot } from '@/contrib/react/slot' import { useI18n } from '@/i18n' import { chatMessageText } from '@/lib/chat-messages' +import { PR_COMMENT_URL_RE } from '@/lib/chat-runtime' import { sanitizeComposerInput } from '@/lib/composer-input-sanitize' import { DATA_IMAGE_URL_RE } from '@/lib/embedded-images' import { triggerHaptic } from '@/lib/haptics' @@ -93,6 +94,7 @@ export function ChatBar({ onAddUrl, onAttachDroppedItems, onAttachImageBlob, + onAttachPrCommentUrl, onPasteClipboardImage, onPickFiles, onPickFolders, @@ -514,6 +516,17 @@ export function ChatBar({ return } + // A pasted GitHub PR-comment deep link resolves to a structured review + // attachment (author, body, file:line anchor, diff hunk) instead of a bare + // `@url:` chip. Optimistic card first, resolve via gh in the background — + // if gh can't answer (offline, unauthenticated, foreign repo) the card + // swaps back to the plain URL ref so nothing is lost. + if (PR_COMMENT_URL_RE.test(pastedText) && onAttachPrCommentUrl?.(pastedText)) { + event.preventDefault() + + return + } + event.preventDefault() // Links in the paste land as `@url:` chips rather than a wall of URL text — diff --git a/apps/desktop/src/app/chat/composer/types.ts b/apps/desktop/src/app/chat/composer/types.ts index 24abcf3736971..da6a463cd3866 100644 --- a/apps/desktop/src/app/chat/composer/types.ts +++ b/apps/desktop/src/app/chat/composer/types.ts @@ -46,6 +46,9 @@ export interface ChatBarProps { onAddUrl?: (url: string) => void onAttachImageBlob?: (blob: Blob) => Promise | boolean | void onAttachDroppedItems?: (candidates: DroppedFile[]) => Promise | boolean | void + /** Pasted GitHub PR-comment deep link → structured review attachment. + * Returns true when the paste was consumed as an attachment. */ + onAttachPrCommentUrl?: (url: string) => boolean onPasteClipboardImage?: (opts?: { silent?: boolean }) => Promise | void onPickFiles?: () => void onPickFolders?: () => void diff --git a/apps/desktop/src/app/chat/hooks/use-composer-actions.ts b/apps/desktop/src/app/chat/hooks/use-composer-actions.ts index 3c31f4067c7fe..0c560b3450cc0 100644 --- a/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +++ b/apps/desktop/src/app/chat/hooks/use-composer-actions.ts @@ -6,12 +6,14 @@ import { formatRefValue } from '@/components/assistant-ui/directive-text' import { useI18n } from '@/i18n' import { attachmentId, contextPath, pathLabel } from '@/lib/chat-runtime' import { readDesktopFileDataUrl, selectDesktopPaths } from '@/lib/desktop-fs' +import { desktopGit } from '@/lib/desktop-git' import { normalize } from '@/lib/text' import { addComposerAttachment, type ComposerAttachment, removeComposerAttachment, - setComposerTerminalSelection + setComposerTerminalSelection, + updateComposerAttachment } from '@/store/composer' import { notify, notifyError } from '@/store/notifications' @@ -260,12 +262,14 @@ export function partitionDroppedFiles(candidates: DroppedFile[]): { interface ComposerActionsScope { add: (attachment: ComposerAttachment) => void remove: (id: string) => ComposerAttachment | null + update: (attachment: ComposerAttachment) => boolean target: string } const MAIN_ACTIONS_SCOPE: ComposerActionsScope = { add: addComposerAttachment, remove: removeComposerAttachment, + update: updateComposerAttachment, target: 'main' } @@ -331,6 +335,49 @@ export function useComposerActions({ [attachToMain] ) + // A pasted GitHub PR-comment deep link → structured `review` attachment. + // Optimistic: the card lands immediately with the URL as its ref, then the + // background gh resolve fills in author/anchor (label + detail). If gh can't + // answer — offline, unauthenticated, foreign repo, remote gateway — the card + // downgrades to a plain `url` attachment so the paste is never lost. + const attachPrCommentUrl = useCallback( + (url: string): boolean => { + const id = attachmentId('review', url) + const refText = `@url:${formatRefValue(url)}` + + attachToMain({ + id, + kind: 'review', + label: url.replace(/^https:\/\/github\.com\//, '').replace(/#.*$/, ''), + refText, + uploadState: 'uploading' + }) + + void (async () => { + const comment = currentCwd + ? await (desktopGit()?.review.fetchPrComment(currentCwd, url).catch(() => null) ?? null) + : null + + if (comment) { + scope.update({ + id, + kind: 'review', + label: comment.path + ? `${pathLabel(comment.path)}${comment.line ? `:${comment.line}` : ''} — @${comment.author}` + : `PR #${comment.prNumber} — @${comment.author}`, + detail: JSON.stringify(comment), + refText + }) + } else { + scope.update({ id, kind: 'url', label: pathLabel(url), refText }) + } + })() + + return true + }, + [attachToMain, currentCwd, scope] + ) + const pickContextPaths = useCallback( async (kind: 'file' | 'folder') => { const paths = await selectDesktopPaths({ @@ -653,6 +700,7 @@ export function useComposerActions({ attachDroppedItems, attachImageBlob, attachImagePath, + attachPrCommentUrl, insertContextPathInlineRef, pasteClipboardImage, pickContextPaths, diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index 1c2636b4f66e8..16f622bee9bcf 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -78,6 +78,7 @@ interface ChatViewProps extends Omit, 'onSubmit'> { maxVoiceRecordingSeconds?: number onAttachImageBlob: (blob: Blob) => Promise | boolean | void onAttachDroppedItems: (candidates: DroppedFile[]) => Promise | boolean | void + onAttachPrCommentUrl?: (url: string) => boolean onPasteClipboardImage: (opts?: { silent?: boolean }) => Promise | void onPickFiles: () => void onPickFolders: () => void @@ -289,6 +290,7 @@ export const ChatView = memo(function ChatView({ onAddUrl, onAttachImageBlob, onAttachDroppedItems, + onAttachPrCommentUrl, onBranchInNewChat, maxVoiceRecordingSeconds, onPasteClipboardImage, @@ -616,6 +618,7 @@ export const ChatView = memo(function ChatView({ onAddUrl={onAddUrl} onAttachDroppedItems={onAttachDroppedItems} onAttachImageBlob={onAttachImageBlob} + onAttachPrCommentUrl={onAttachPrCommentUrl} onCancel={onCancel} onPasteClipboardImage={onPasteClipboardImage} onPickFiles={onPickFiles} diff --git a/apps/desktop/src/app/chat/session-tile.tsx b/apps/desktop/src/app/chat/session-tile.tsx index 27d2cebf68ecf..76ecc3b24cd9e 100644 --- a/apps/desktop/src/app/chat/session-tile.tsx +++ b/apps/desktop/src/app/chat/session-tile.tsx @@ -149,7 +149,7 @@ function TileChat({ activeSessionId: runtimeId, currentCwd: cwd, requestGateway, - scope: { add: attachments.add, remove: attachments.remove, target: scope.target } + scope: { add: attachments.add, remove: attachments.remove, target: scope.target, update: attachments.update } }) // ChatView is memo()d — every callback prop must be referentially stable or @@ -200,6 +200,7 @@ function TileChat({ onAddUrl={onAddUrl} onAttachDroppedItems={composer.attachDroppedItems} onAttachImageBlob={composer.attachImageBlob} + onAttachPrCommentUrl={composer.attachPrCommentUrl} onCancel={actions.cancelRun} onDeleteSelectedSession={noop} onDismissError={actions.dismissError} diff --git a/apps/desktop/src/app/contrib/types.ts b/apps/desktop/src/app/contrib/types.ts index 44c29f468532b..bc9142c8e4e91 100644 --- a/apps/desktop/src/app/contrib/types.ts +++ b/apps/desktop/src/app/contrib/types.ts @@ -31,6 +31,7 @@ export type ChatActions = Pick< | 'onAddUrl' | 'onAttachDroppedItems' | 'onAttachImageBlob' + | 'onAttachPrCommentUrl' | 'onBranchInNewChat' | 'onCancel' | 'onDeleteSelectedSession' diff --git a/apps/desktop/src/app/contrib/wiring.tsx b/apps/desktop/src/app/contrib/wiring.tsx index 11583b635cde4..e0a7216abcc2e 100644 --- a/apps/desktop/src/app/contrib/wiring.tsx +++ b/apps/desktop/src/app/contrib/wiring.tsx @@ -864,6 +864,7 @@ export function ContribWiring({ children }: { children: ReactNode }) { onArchiveSession: sessionId => void archiveSession(sessionId), onAttachDroppedItems: composer.attachDroppedItems, onAttachImageBlob: composer.attachImageBlob, + onAttachPrCommentUrl: composer.attachPrCommentUrl, onBranchInNewChat: messageId => void branchInNewChat(messageId), onBranchSession: sessionId => void branchStoredSession(sessionId), onCancel: cancelRun, diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts index 7c52ed5c17917..f2470634c2fac 100644 --- a/apps/desktop/src/global.d.ts +++ b/apps/desktop/src/global.d.ts @@ -272,6 +272,10 @@ declare global { // number — for badging a list of sessions in one request instead of // one `pr view` per checkout. prList: (repoPath: string, branches: string[], numbers?: number[]) => Promise + // A pasted PR review/issue comment URL resolved to its structured + // context (author, body, file + line anchor, diff hunk). Null when + // gh can't answer — the paste stays a plain URL. + fetchPrComment: (repoPath: string, url: string) => Promise createPr: (repoPath: string) => Promise<{ url: string }> } // Repo-first discovery: scan bounded roots for git repos (depth-capped). @@ -978,6 +982,21 @@ export interface HermesRepoPullRequests { prs: HermesBranchPullRequest[] } +// A PR review/issue comment resolved from a pasted GitHub URL — the composer's +// review-comment attachment context. `path`/`line`/`diffHunk` are empty for +// conversation-tab (issue) comments; `line` is null when the comment is +// outdated and only `original_line` remained. +export interface HermesPrComment { + author: string + body: string + diffHunk: string + kind: 'issue' | 'review' + line: null | number + path: string + prNumber: number + startLine: null | number + url: string +} // gh availability/auth + the current branch's PR — drives the review pane's PR // button (disabled when gh isn't ready, "Open PR" vs "Create PR" otherwise). export interface HermesReviewShipInfo { diff --git a/apps/desktop/src/lib/chat-runtime.test.ts b/apps/desktop/src/lib/chat-runtime.test.ts index 7ec2aa4cc4d8e..621286e584ab8 100644 --- a/apps/desktop/src/lib/chat-runtime.test.ts +++ b/apps/desktop/src/lib/chat-runtime.test.ts @@ -70,6 +70,34 @@ describe('attachmentDisplayText', () => { it('still resolves a normal file ref', () => { expect(attachmentDisplayText(attachment({ kind: 'file', refText: '@file:src/a.ts' }))).toBe('@file:src/a.ts') }) + + it('expands a review attachment into an anchored fenced block', () => { + const detail = JSON.stringify({ + author: 'teknium1', + body: 'this cap looks wrong', + diffHunk: '@@ -1,2 +1,2 @@\n-const CAP = 5\n+const CAP = 50', + kind: 'review', + line: 12, + path: 'src/limits.ts', + prNumber: 123, + startLine: null, + url: 'https://github.com/o/r/pull/123#discussion_r1' + }) + + const block = attachmentDisplayText(attachment({ kind: 'review', detail, refText: '@url:`https://x`' })) + + // The contract: anchor (file:line), author, body, and the hunk all ride. + expect(block).toContain('review-comment src/limits.ts:12') + expect(block).toContain('@teknium1') + expect(block).toContain('this cap looks wrong') + expect(block).toContain('const CAP = 50') + }) + + it('falls back to the url ref when a review detail is malformed', () => { + expect(attachmentDisplayText(attachment({ kind: 'review', detail: 'not json', refText: '@url:`https://x`' }))).toBe( + '@url:`https://x`' + ) + }) }) describe('coerceThinkingText', () => { diff --git a/apps/desktop/src/lib/chat-runtime.ts b/apps/desktop/src/lib/chat-runtime.ts index 4f48a141a9488..0e703857258a2 100644 --- a/apps/desktop/src/lib/chat-runtime.ts +++ b/apps/desktop/src/lib/chat-runtime.ts @@ -172,6 +172,29 @@ export function attachmentId(kind: ComposerAttachment['kind'], value: string): s return `${kind}:${normalizeAttachmentValue(kind, value)}` } +/** A GitHub PR review-thread (`#discussion_r`) or conversation + * (`#issuecomment-`) deep link — the one paste shape that can resolve to + * a structured review attachment instead of a plain `@url:` chip. */ +export const PR_COMMENT_URL_RE = + /^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+(?:\/[^#\s]*)?#(?:discussion_r|issuecomment-)\d+$/ + +/** The send-time expansion of a `review` attachment. `detail` holds the + * resolved comment as JSON (HermesPrComment shape); a malformed payload falls + * back to the attachment's URL ref so the send never throws. */ +export function reviewCommentBlock(detail: string): null | string { + try { + const c = JSON.parse(detail) + const anchor = c.path + ? `${c.path}${c.line ? `:${c.startLine && c.startLine !== c.line ? `${c.startLine}-` : ''}${c.line}` : ''}` + : `PR #${c.prNumber}` + const hunk = c.diffHunk ? `\n--- diff hunk ---\n${String(c.diffHunk).trim()}` : '' + + return `\`\`\`review-comment ${anchor}\n@${c.author} on ${c.url}\n\n${String(c.body).trim()}${hunk}\n\`\`\`` + } catch { + return null + } +} + export function pathLabel(path: string): string { return path.split(/[\\/]/).filter(Boolean).pop() || path } @@ -188,6 +211,18 @@ export function attachmentDisplayText(attachment: ComposerAttachment): string | return `\`\`\`terminal\n${attachment.detail.trim()}\n\`\`\`` } + // A resolved PR review comment: expand to a fenced block carrying the + // anchor (file:line), author, body, and — when present — the diff hunk the + // comment sits on, so "address this" needs no re-explaining what "this" is. + // A malformed payload falls through to the refText (the pasted URL). + if (attachment.kind === 'review' && attachment.detail) { + const block = reviewCommentBlock(attachment.detail) + + if (block) { + return block + } + } + if (attachment.refText) { return attachment.refText } diff --git a/apps/desktop/src/lib/desktop-git.ts b/apps/desktop/src/lib/desktop-git.ts index 189046e0e72c9..98f41fbf87e0b 100644 --- a/apps/desktop/src/lib/desktop-git.ts +++ b/apps/desktop/src/lib/desktop-git.ts @@ -96,6 +96,10 @@ const remoteGit: GitBridge = { prList: (repoPath, branches, numbers) => gitPost('review/pr-list', { branches, numbers: numbers ?? [], path: repoPath }), + // Remote gateways have no PR-comment route yet; resolve to null so the + // paste degrades to a plain URL instead of throwing mid-paste. + fetchPrComment: async () => null, + createPr: repoPath => gitPost('review/create-pr', { path: repoPath }) }, diff --git a/apps/desktop/src/lib/icons.ts b/apps/desktop/src/lib/icons.ts index 550c2a4bc88fc..6b3d5f80cc7ba 100644 --- a/apps/desktop/src/lib/icons.ts +++ b/apps/desktop/src/lib/icons.ts @@ -69,6 +69,7 @@ import { IconMail as Mail, IconMaximize as Maximize, IconMessageCircle as MessageCircle, + IconMessageCode as MessageCode, IconMessageQuestion as MessageQuestion, IconMessage2 as MessageSquareText, IconMicrophone as Mic, @@ -195,6 +196,7 @@ export { Mail, Maximize, MessageCircle, + MessageCode, MessageQuestion, MessageSquareText, Mic, diff --git a/apps/desktop/src/store/composer.ts b/apps/desktop/src/store/composer.ts index 9f2d541f9b9aa..1b6847a537b15 100644 --- a/apps/desktop/src/store/composer.ts +++ b/apps/desktop/src/store/composer.ts @@ -5,7 +5,7 @@ import { triggerHaptic } from '@/lib/haptics' export interface ComposerAttachment { id: string - kind: 'image' | 'file' | 'folder' | 'terminal' | 'url' + kind: 'file' | 'folder' | 'image' | 'review' | 'terminal' | 'url' label: string detail?: string refText?: string