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 393ede5f85..95cdfba7ca 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 9c40a64739..d908d5fa18 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 f2b16d1760..0231ecf33e 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 cb42dc9f1c..5d5028466a 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 56e0bf9c2f..5d490eb61c 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 24abcf3736..da6a463cd3 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 3c31f4067c..0c560b3450 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 1c2636b4f6..16f622bee9 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 27d2cebf68..76ecc3b24c 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 44c29f4685..bc9142c8e4 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 11583b635c..e0a7216abc 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 7c52ed5c17..f2470634c2 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 7ec2aa4cc4..621286e584 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 4f48a141a9..0e70385725 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 189046e0e7..98f41fbf87 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 550c2a4bc8..6b3d5f80cc 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 9f2d541f9b..1b6847a537 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