mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
feat: Approve and review PR to github (#352)
* Approve and review PR to github
* fix: GitHub review improvements — multi-line ranges, own-PR detection, error handling
- Add start_line/start_side to review payload for multi-line annotations
- Detect current GitHub user and disable Approve on own PRs with tooltip
- Fix loading state: approve button no longer shows "Approving..." during feedback
- Fix empty submit: block Post Comments when no annotations and no comment
- Fix Cmd+Enter in GitHub comment dialog
- Fix completion screen text for GitHub mode ("submitted to GitHub")
- Fix agent label fallback to "Your agent" instead of "OpenCode"
- Improve gh-not-installed error message with install URL
- Fix stale useCallback dependency array on handleGitHubAction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(review): destination dropdown, icon components, and UI polish
Replace Agent/GitHub toggle with a compact destination dropdown.
Buttons stay consistent ("Send Feedback"/"Post Comments" + "Approve")
with behavior routed by destination. Double-tap Option to toggle.
- Add GitHubIcon, RepoIcon, PullRequestIcon components (currentColor)
- Add repo/PR icons to editor header
- Add keyboard hint in destination dropdown
- Add double-Option shortcut to settings
- Hide duplicate filename from Pierre diff header
- Refine FileHeader typography (text-xs font-semibold, no mono)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: include VS Code editor annotations in GitHub PR reviews
Editor annotations were silently dropped when submitting to GitHub.
Now converts them to inline PR comments with selected text as context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rename visibility, own-PR keyboard guard, and editor annotation filtering
- Keep data-prev-name visible in Pierre header for rename diffs
- Block Cmd+Enter approve path for own PRs (was only blocked via mouse)
- Filter editor annotations to diff files only to prevent GitHub API rejection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,7 +157,13 @@ if (args[0] === "sessions") {
|
||||
try {
|
||||
await checkGhAuth();
|
||||
} catch (err) {
|
||||
console.error(err instanceof Error ? err.message : "GitHub CLI auth check failed");
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes("not found") || msg.includes("ENOENT")) {
|
||||
console.error("GitHub CLI (gh) is not installed.");
|
||||
console.error("Install it from https://cli.github.com");
|
||||
} else {
|
||||
console.error(msg);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@plannotator/server": "workspace:*",
|
||||
"@plannotator/ui": "workspace:*",
|
||||
"@pierre/diffs": "^1.1.0-beta.19",
|
||||
"dompurify": "^3.3.3",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.1.18",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "plannotator",
|
||||
@@ -55,7 +56,7 @@
|
||||
},
|
||||
"apps/opencode-plugin": {
|
||||
"name": "@plannotator/opencode",
|
||||
"version": "0.14.2",
|
||||
"version": "0.14.3",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "^1.1.10",
|
||||
},
|
||||
@@ -77,7 +78,7 @@
|
||||
},
|
||||
"apps/pi-extension": {
|
||||
"name": "@plannotator/pi-extension",
|
||||
"version": "0.14.2",
|
||||
"version": "0.14.3",
|
||||
"peerDependencies": {
|
||||
"@mariozechner/pi-coding-agent": ">=0.53.0",
|
||||
},
|
||||
@@ -109,6 +110,7 @@
|
||||
"@plannotator/server": "workspace:*",
|
||||
"@plannotator/ui": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"dompurify": "^3.3.3",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.1.18",
|
||||
@@ -157,7 +159,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@plannotator/server",
|
||||
"version": "0.14.2",
|
||||
"version": "0.14.3",
|
||||
"dependencies": {
|
||||
"@plannotator/shared": "workspace:*",
|
||||
},
|
||||
|
||||
+406
-35
@@ -6,6 +6,9 @@ import { Settings } from '@plannotator/ui/components/Settings';
|
||||
import { UpdateBanner } from '@plannotator/ui/components/UpdateBanner';
|
||||
import { storage } from '@plannotator/ui/utils/storage';
|
||||
import { CompletionOverlay } from '@plannotator/ui/components/CompletionOverlay';
|
||||
import { GitHubIcon } from '@plannotator/ui/components/GitHubIcon';
|
||||
import { RepoIcon } from '@plannotator/ui/components/RepoIcon';
|
||||
import { PullRequestIcon } from '@plannotator/ui/components/PullRequestIcon';
|
||||
import { getIdentity } from '@plannotator/ui/utils/identity';
|
||||
import { getAgentSwitchSettings, getEffectiveAgentName } from '@plannotator/ui/utils/agentSwitch';
|
||||
import { CodeAnnotation, CodeAnnotationType, SelectedLineRange } from '@plannotator/ui/types';
|
||||
@@ -26,6 +29,9 @@ import type { PRMetadata } from '@plannotator/shared/pr-provider';
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
|
||||
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
|
||||
const altKey = isMac ? '⌥' : 'Alt';
|
||||
|
||||
interface DiffFile {
|
||||
path: string;
|
||||
oldPath?: string;
|
||||
@@ -116,6 +122,19 @@ const ReviewApp: React.FC = () => {
|
||||
}, [repoInfo]);
|
||||
|
||||
const [prMetadata, setPrMetadata] = useState<PRMetadata | null>(null);
|
||||
const [reviewDestination, setReviewDestination] = useState<'agent' | 'github'>(() =>
|
||||
storage.getItem('plannotator-review-dest') === 'agent' ? 'agent' : 'github'
|
||||
);
|
||||
const [showDestinationMenu, setShowDestinationMenu] = useState(false);
|
||||
const [isGitHubActioning, setIsGitHubActioning] = useState(false);
|
||||
const [githubActionError, setGithubActionError] = useState<string | null>(null);
|
||||
const [ghUser, setGhUser] = useState<string | null>(null);
|
||||
const [githubCommentDialog, setGithubCommentDialog] = useState<{ action: 'approve' | 'comment' } | null>(null);
|
||||
const [githubGeneralComment, setGithubGeneralComment] = useState('');
|
||||
const [githubOpenPR, setGithubOpenPR] = useState(() => storage.getItem('plannotator-github-open-pr') !== 'false');
|
||||
|
||||
// Derived: GitHub mode is active when destination is GitHub AND we have PR metadata
|
||||
const githubMode = reviewDestination === 'github' && !!prMetadata;
|
||||
|
||||
const identity = useMemo(() => getIdentity(), []);
|
||||
|
||||
@@ -191,7 +210,9 @@ const ReviewApp: React.FC = () => {
|
||||
|
||||
// Escape closes modals or clears search
|
||||
if (e.key === 'Escape') {
|
||||
if (showExportModal) {
|
||||
if (showDestinationMenu) {
|
||||
setShowDestinationMenu(false);
|
||||
} else if (showExportModal) {
|
||||
setShowExportModal(false);
|
||||
} else if (searchQuery) {
|
||||
clearSearch();
|
||||
@@ -207,7 +228,7 @@ const ReviewApp: React.FC = () => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showExportModal, searchQuery, searchMatches, openSearch, stepSearchMatch, clearSearch, files, gitContext?.diffOptions]);
|
||||
}, [showExportModal, showDestinationMenu, searchQuery, searchMatches, openSearch, stepSearchMatch, clearSearch, files, gitContext?.diffOptions]);
|
||||
|
||||
// Get annotations for active file
|
||||
const activeFileAnnotations = useMemo(() => {
|
||||
@@ -232,6 +253,7 @@ const ReviewApp: React.FC = () => {
|
||||
sharingEnabled?: boolean;
|
||||
repoInfo?: { display: string; branch?: string };
|
||||
prMetadata?: PRMetadata;
|
||||
ghUser?: string;
|
||||
error?: string;
|
||||
}) => {
|
||||
const apiFiles = parseDiffToFiles(data.rawPatch);
|
||||
@@ -251,6 +273,7 @@ const ReviewApp: React.FC = () => {
|
||||
if (data.sharingEnabled !== undefined) setSharingEnabled(data.sharingEnabled);
|
||||
if (data.repoInfo) setRepoInfo(data.repoInfo);
|
||||
if (data.prMetadata) setPrMetadata(data.prMetadata);
|
||||
if (data.ghUser) setGhUser(data.ghUser);
|
||||
if (data.error) setDiffError(data.error);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -585,24 +608,191 @@ const ReviewApp: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Build the payload for /api/pr-action from current annotations
|
||||
const buildPRReviewPayload = useCallback((action: 'approve' | 'comment', generalComment?: string) => {
|
||||
const fileAnnotations = annotations.filter(a => (a.scope ?? 'line') === 'line');
|
||||
const fileScoped = annotations.filter(a => a.scope === 'file');
|
||||
|
||||
// Top-level body: file-scoped comments
|
||||
const bodyParts: string[] = [];
|
||||
if (fileScoped.length > 0) {
|
||||
for (const ann of fileScoped) {
|
||||
if (ann.text) bodyParts.push(`**${ann.filePath}:** ${ann.text}`);
|
||||
}
|
||||
}
|
||||
const body = bodyParts.length > 0
|
||||
? `${generalComment ? generalComment + '\n\n' : ''}Review from Plannotator\n\n${bodyParts.join('\n\n')}`
|
||||
: generalComment || 'Review from Plannotator';
|
||||
|
||||
// Inline file comments
|
||||
const fileComments = fileAnnotations.map(ann => {
|
||||
let commentBody = ann.text ?? '';
|
||||
if (ann.suggestedCode) {
|
||||
commentBody += `\n\n\`\`\`suggestion\n${ann.suggestedCode}\n\`\`\``;
|
||||
}
|
||||
const side = (ann.side === 'old' ? 'LEFT' : 'RIGHT') as 'LEFT' | 'RIGHT';
|
||||
const isMultiLine = ann.lineStart != null && ann.lineEnd != null && ann.lineStart !== ann.lineEnd;
|
||||
return {
|
||||
path: ann.filePath,
|
||||
line: ann.lineEnd ?? ann.lineStart,
|
||||
side,
|
||||
body: commentBody.trim(),
|
||||
...(isMultiLine && {
|
||||
start_line: ann.lineStart,
|
||||
start_side: side,
|
||||
}),
|
||||
};
|
||||
}).filter(c => c.body.length > 0);
|
||||
|
||||
// Editor annotations (VS Code extension) — always on new/RIGHT side
|
||||
// Only include annotations targeting files in the diff to avoid GitHub API rejection
|
||||
const diffPaths = new Set(files.map(f => f.path));
|
||||
for (const ea of editorAnnotations) {
|
||||
if (!diffPaths.has(ea.filePath)) continue;
|
||||
const body = ea.comment || `> ${ea.selectedText}`;
|
||||
if (!body.trim()) continue;
|
||||
const isMultiLine = ea.lineStart !== ea.lineEnd;
|
||||
fileComments.push({
|
||||
path: ea.filePath,
|
||||
line: ea.lineEnd,
|
||||
side: 'RIGHT' as const,
|
||||
body: ea.comment ? `> ${ea.selectedText}\n\n${ea.comment}` : `> ${ea.selectedText}`,
|
||||
...(isMultiLine && {
|
||||
start_line: ea.lineStart,
|
||||
start_side: 'RIGHT' as const,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return { action, body, fileComments };
|
||||
}, [annotations, editorAnnotations, files]);
|
||||
|
||||
// Submit a review directly to GitHub
|
||||
const handleGitHubAction = useCallback(async (action: 'approve' | 'comment', generalComment?: string) => {
|
||||
setIsGitHubActioning(true);
|
||||
setGithubActionError(null);
|
||||
try {
|
||||
const payload = buildPRReviewPayload(action, generalComment);
|
||||
const prRes = await fetch('/api/pr-action', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const prData = await prRes.json() as { ok?: boolean; prUrl?: string; error?: string };
|
||||
if (!prRes.ok || prData.error) {
|
||||
setGithubActionError(prData.error ?? 'Failed to submit PR review');
|
||||
setIsGitHubActioning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open PR in browser (if opted in)
|
||||
if (prData.prUrl && githubOpenPR) {
|
||||
window.open(prData.prUrl, '_blank');
|
||||
}
|
||||
|
||||
// Close the local session with a neutral message — don't send annotations to the agent
|
||||
const agentSwitchSettings = getAgentSwitchSettings();
|
||||
const effectiveAgent = getEffectiveAgentName(agentSwitchSettings);
|
||||
const prLink = prData.prUrl ?? '';
|
||||
const statusMessage = action === 'approve'
|
||||
? `Pull request approved on GitHub${prLink ? ': ' + prLink : ''}`
|
||||
: `Pull request reviewed on GitHub${prLink ? ': ' + prLink : ''}`;
|
||||
await fetch('/api/feedback', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
approved: false,
|
||||
feedback: statusMessage,
|
||||
annotations: [],
|
||||
...(effectiveAgent && { agentSwitch: effectiveAgent }),
|
||||
}),
|
||||
});
|
||||
setSubmitted(action === 'approve' ? 'approved' : 'feedback');
|
||||
} catch (err) {
|
||||
setGithubActionError(err instanceof Error ? err.message : 'Failed to submit PR review');
|
||||
setIsGitHubActioning(false);
|
||||
}
|
||||
}, [buildPRReviewPayload, githubOpenPR]);
|
||||
|
||||
// Double-tap Option/Alt to toggle review destination (PR mode only)
|
||||
useEffect(() => {
|
||||
if (!prMetadata) return;
|
||||
let lastAltUp = 0;
|
||||
const DOUBLE_TAP_WINDOW = 300;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Alt' || e.repeat) return;
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Alt') return;
|
||||
const now = Date.now();
|
||||
if (now - lastAltUp < DOUBLE_TAP_WINDOW) {
|
||||
setReviewDestination(prev => {
|
||||
const next = prev === 'github' ? 'agent' : 'github';
|
||||
storage.setItem('plannotator-review-dest', next);
|
||||
setGithubActionError(null);
|
||||
return next;
|
||||
});
|
||||
lastAltUp = 0;
|
||||
} else {
|
||||
lastAltUp = now;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
};
|
||||
}, [prMetadata]);
|
||||
|
||||
// Cmd/Ctrl+Enter keyboard shortcut to approve or send feedback
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Enter' || !(e.metaKey || e.ctrlKey)) return;
|
||||
|
||||
// If the GitHub comment dialog is open, Cmd+Enter submits it
|
||||
if (githubCommentDialog) {
|
||||
if (submitted || isGitHubActioning) return;
|
||||
const isApproveAction = githubCommentDialog.action === 'approve';
|
||||
const canSubmit = isApproveAction || totalAnnotationCount > 0 || githubGeneralComment.trim();
|
||||
if (!canSubmit) return;
|
||||
e.preventDefault();
|
||||
const { action } = githubCommentDialog;
|
||||
setGithubCommentDialog(null);
|
||||
handleGitHubAction(action, githubGeneralComment);
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
||||
if (showExportModal || showNoAnnotationsDialog || showApproveWarning) return;
|
||||
if (submitted || isSendingFeedback || isApproving) return;
|
||||
if (submitted || isSendingFeedback || isApproving || isGitHubActioning) return;
|
||||
if (!origin) return; // Demo mode
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// No annotations → Approve, otherwise → Send Feedback
|
||||
if (totalAnnotationCount === 0) {
|
||||
handleApprove();
|
||||
if (githubMode) {
|
||||
// GitHub mode: No annotations → Approve on GitHub, otherwise → Post Review
|
||||
const isOwnPR = !!ghUser && prMetadata?.author === ghUser;
|
||||
if (totalAnnotationCount === 0 && !isOwnPR) {
|
||||
setGithubGeneralComment('');
|
||||
setGithubCommentDialog({ action: 'approve' });
|
||||
} else {
|
||||
setGithubGeneralComment('');
|
||||
setGithubCommentDialog({ action: 'comment' });
|
||||
}
|
||||
} else {
|
||||
handleSendFeedback();
|
||||
// Agent mode: No annotations → Approve, otherwise → Send Feedback
|
||||
if (totalAnnotationCount === 0) {
|
||||
handleApprove();
|
||||
} else {
|
||||
handleSendFeedback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -610,8 +800,10 @@ const ReviewApp: React.FC = () => {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [
|
||||
showExportModal, showNoAnnotationsDialog, showApproveWarning,
|
||||
submitted, isSendingFeedback, isApproving, origin, totalAnnotationCount,
|
||||
handleApprove, handleSendFeedback
|
||||
githubCommentDialog, githubGeneralComment,
|
||||
submitted, isSendingFeedback, isApproving, isGitHubActioning,
|
||||
origin, githubMode, ghUser, prMetadata, totalAnnotationCount,
|
||||
handleApprove, handleSendFeedback, handleGitHubAction
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -654,23 +846,26 @@ const ReviewApp: React.FC = () => {
|
||||
{prMetadata ? (
|
||||
<>
|
||||
<span className="text-muted-foreground/40 hidden md:inline">|</span>
|
||||
<span className="text-xs text-muted-foreground/60 font-mono hidden md:inline">
|
||||
<span className="text-xs text-muted-foreground/60 hidden md:inline-flex items-center gap-1">
|
||||
<RepoIcon className="w-3 h-3" />
|
||||
{prMetadata.owner}/{prMetadata.repo}
|
||||
</span>
|
||||
<a
|
||||
href={prMetadata.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-accent/80 hover:text-accent hidden md:inline truncate max-w-[300px] transition-colors"
|
||||
className="text-xs text-accent/80 hover:text-accent hidden md:inline-flex items-center gap-1 truncate max-w-[300px] transition-colors"
|
||||
title={prMetadata.title}
|
||||
>
|
||||
<PullRequestIcon className="w-3 h-3 flex-shrink-0" />
|
||||
#{prMetadata.number} {prMetadata.title}
|
||||
</a>
|
||||
</>
|
||||
) : repoInfo ? (
|
||||
<>
|
||||
<span className="text-muted-foreground/40 hidden md:inline">|</span>
|
||||
<span className="text-xs text-muted-foreground/60 font-mono hidden md:inline truncate max-w-[200px]" title={repoInfo.display}>
|
||||
<span className="text-xs text-muted-foreground/60 hidden md:inline-flex items-center gap-1 truncate max-w-[200px]" title={repoInfo.display}>
|
||||
<RepoIcon className="w-3 h-3 flex-shrink-0" />
|
||||
{repoInfo.display}
|
||||
</span>
|
||||
</>
|
||||
@@ -727,55 +922,168 @@ const ReviewApp: React.FC = () => {
|
||||
|
||||
{origin ? (
|
||||
<>
|
||||
{/* Send Feedback button - accent color, disabled if no annotations */}
|
||||
{/* Destination dropdown (PR mode only) */}
|
||||
{prMetadata && (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDestinationMenu(prev => !prev)}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium bg-muted hover:bg-muted/80 transition-colors"
|
||||
title={reviewDestination === 'github' ? 'Posting to GitHub PR' : 'Sending to agent session'}
|
||||
>
|
||||
{reviewDestination === 'github' ? (
|
||||
<>
|
||||
<GitHubIcon className="w-3.5 h-3.5" />
|
||||
<span>GitHub</span>
|
||||
</>
|
||||
) : 'Agent'}
|
||||
<svg className="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{showDestinationMenu && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowDestinationMenu(false)} />
|
||||
<div className="absolute right-0 top-full mt-1 py-1 bg-popover border border-border rounded-lg shadow-xl z-50 min-w-[160px]">
|
||||
<button
|
||||
onClick={() => {
|
||||
setReviewDestination('github');
|
||||
storage.setItem('plannotator-review-dest', 'github');
|
||||
setShowDestinationMenu(false);
|
||||
setGithubActionError(null);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-1.5 text-xs transition-colors ${
|
||||
reviewDestination === 'github'
|
||||
? 'text-foreground bg-muted/50'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">GitHub</div>
|
||||
<div className="text-muted-foreground/60">Post to PR</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setReviewDestination('agent');
|
||||
storage.setItem('plannotator-review-dest', 'agent');
|
||||
setShowDestinationMenu(false);
|
||||
setGithubActionError(null);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-1.5 text-xs transition-colors ${
|
||||
reviewDestination === 'agent'
|
||||
? 'text-foreground bg-muted/50'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">Agent</div>
|
||||
<div className="text-muted-foreground/60">Send to session</div>
|
||||
</button>
|
||||
<div className="border-t border-border/50 mt-1 pt-1 px-3 py-1">
|
||||
<span className="text-[10px] text-muted-foreground/40">
|
||||
<kbd className="inline-flex items-center justify-center min-w-[18px] h-[16px] px-1 rounded bg-muted border border-border/60 border-b-[2px] text-[9px] font-mono leading-none text-foreground/60 shadow-sm">{altKey}</kbd>
|
||||
<kbd className="inline-flex items-center justify-center min-w-[18px] h-[16px] px-1 rounded bg-muted border border-border/60 border-b-[2px] text-[9px] font-mono leading-none text-foreground/60 shadow-sm ml-0.5">{altKey}</kbd>
|
||||
<span className="ml-1.5">to toggle</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* GitHub error message */}
|
||||
{githubActionError && (
|
||||
<div
|
||||
className="text-xs text-destructive px-2 py-1 bg-destructive/10 rounded border border-destructive/20 max-w-[200px] truncate"
|
||||
title={githubActionError}
|
||||
>
|
||||
{githubActionError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Send Feedback button — always the same label */}
|
||||
<button
|
||||
onClick={handleSendFeedback}
|
||||
disabled={isSendingFeedback || isApproving || totalAnnotationCount === 0}
|
||||
onClick={() => {
|
||||
if (githubMode) {
|
||||
setGithubGeneralComment('');
|
||||
setGithubCommentDialog({ action: 'comment' });
|
||||
} else {
|
||||
handleSendFeedback();
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isSendingFeedback || isApproving || isGitHubActioning ||
|
||||
(!githubMode && totalAnnotationCount === 0)
|
||||
}
|
||||
className={`p-1.5 md:px-2.5 md:py-1 rounded-md text-xs font-medium transition-all ${
|
||||
isSendingFeedback || isApproving
|
||||
isSendingFeedback || isApproving || isGitHubActioning
|
||||
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
|
||||
: totalAnnotationCount === 0
|
||||
: !githubMode && totalAnnotationCount === 0
|
||||
? 'opacity-50 cursor-not-allowed bg-accent/10 text-accent/50'
|
||||
: 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/30'
|
||||
}`}
|
||||
title={totalAnnotationCount === 0 ? "Add annotations to send feedback" : "Send feedback"}
|
||||
title={!githubMode && totalAnnotationCount === 0 ? "Add annotations to send feedback" : "Send feedback"}
|
||||
>
|
||||
<svg className="w-4 h-4 md:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
<span className="hidden md:inline">{isSendingFeedback ? 'Sending...' : 'Send Feedback'}</span>
|
||||
<span className="hidden md:inline">{
|
||||
isSendingFeedback || isGitHubActioning
|
||||
? (githubMode ? 'Posting...' : 'Sending...')
|
||||
: (githubMode ? 'Post Comments' : 'Send Feedback')
|
||||
}</span>
|
||||
</button>
|
||||
|
||||
{/* Approve button - green/success, dimmed if annotations exist */}
|
||||
{/* Approve button — always the same label */}
|
||||
<div className="relative group/approve">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (totalAnnotationCount > 0) {
|
||||
setShowApproveWarning(true);
|
||||
if (githubMode) {
|
||||
if (ghUser && prMetadata?.author === ghUser) return;
|
||||
setGithubGeneralComment('');
|
||||
setGithubCommentDialog({ action: 'approve' });
|
||||
} else {
|
||||
handleApprove();
|
||||
if (totalAnnotationCount > 0) {
|
||||
setShowApproveWarning(true);
|
||||
} else {
|
||||
handleApprove();
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={isSendingFeedback || isApproving}
|
||||
disabled={
|
||||
isSendingFeedback || isApproving || isGitHubActioning ||
|
||||
(githubMode && !!ghUser && prMetadata?.author === ghUser)
|
||||
}
|
||||
className={`px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
|
||||
isSendingFeedback || isApproving
|
||||
isSendingFeedback || isApproving || isGitHubActioning
|
||||
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
|
||||
: totalAnnotationCount > 0
|
||||
? 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground'
|
||||
: 'bg-success text-success-foreground hover:opacity-90'
|
||||
: githubMode && ghUser && prMetadata?.author === ghUser
|
||||
? 'opacity-40 cursor-not-allowed bg-muted text-muted-foreground'
|
||||
: !githubMode && totalAnnotationCount > 0
|
||||
? 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground'
|
||||
: 'bg-success text-success-foreground hover:opacity-90'
|
||||
}`}
|
||||
title="Approve - no changes needed"
|
||||
title={
|
||||
githubMode && ghUser && prMetadata?.author === ghUser
|
||||
? "You can't approve your own PR"
|
||||
: "Approve - no changes needed"
|
||||
}
|
||||
>
|
||||
<span className="md:hidden">{isApproving ? '...' : 'OK'}</span>
|
||||
<span className="hidden md:inline">{isApproving ? 'Approving...' : 'Approve'}</span>
|
||||
</button>
|
||||
{totalAnnotationCount > 0 && (
|
||||
{/* Tooltip: own PR warning OR annotations-lost warning */}
|
||||
{githubMode && ghUser && prMetadata?.author === ghUser ? (
|
||||
<div className="absolute top-full right-0 mt-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl text-xs text-foreground w-48 text-center opacity-0 invisible group-hover/approve:opacity-100 group-hover/approve:visible transition-all pointer-events-none z-50">
|
||||
<div className="absolute bottom-full right-4 border-4 border-transparent border-b-border" />
|
||||
<div className="absolute bottom-full right-4 mt-px border-4 border-transparent border-b-popover" />
|
||||
You can't approve your own pull request on GitHub.
|
||||
</div>
|
||||
) : !githubMode && totalAnnotationCount > 0 ? (
|
||||
<div className="absolute top-full right-0 mt-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl text-xs text-foreground w-56 text-center opacity-0 invisible group-hover/approve:opacity-100 group-hover/approve:visible transition-all pointer-events-none z-50">
|
||||
<div className="absolute bottom-full right-4 border-4 border-transparent border-b-border" />
|
||||
<div className="absolute bottom-full right-4 mt-px border-4 border-transparent border-b-popover" />
|
||||
Your {totalAnnotationCount} annotation{totalAnnotationCount !== 1 ? 's' : ''} won't be sent if you approve.
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -1057,15 +1365,78 @@ const ReviewApp: React.FC = () => {
|
||||
submitted={submitted}
|
||||
title={submitted === 'approved' ? 'Changes Approved' : 'Feedback Sent'}
|
||||
subtitle={
|
||||
submitted === 'approved'
|
||||
? `${origin === 'claude-code' ? 'Claude Code' : origin === 'pi' ? 'Pi' : 'OpenCode'} will proceed with the changes.`
|
||||
: `${origin === 'claude-code' ? 'Claude Code' : origin === 'pi' ? 'Pi' : 'OpenCode'} will address your review feedback.`
|
||||
githubMode
|
||||
? submitted === 'approved'
|
||||
? 'Your approval was submitted to GitHub.'
|
||||
: 'Your feedback was submitted to GitHub.'
|
||||
: submitted === 'approved'
|
||||
? `${origin === 'claude-code' ? 'Claude Code' : origin === 'opencode' ? 'OpenCode' : origin === 'pi' ? 'Pi' : 'Your agent'} will proceed with the changes.`
|
||||
: `${origin === 'claude-code' ? 'Claude Code' : origin === 'opencode' ? 'OpenCode' : origin === 'pi' ? 'Pi' : 'Your agent'} will address your review feedback.`
|
||||
}
|
||||
agentLabel={origin === 'claude-code' ? 'Claude Code' : origin === 'pi' ? 'Pi' : 'OpenCode'}
|
||||
agentLabel={origin === 'claude-code' ? 'Claude Code' : origin === 'opencode' ? 'OpenCode' : origin === 'pi' ? 'Pi' : 'Your agent'}
|
||||
/>
|
||||
|
||||
{/* Update notification */}
|
||||
<UpdateBanner origin={origin} />
|
||||
|
||||
{/* GitHub general comment dialog */}
|
||||
{githubCommentDialog && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
|
||||
<div className="bg-card border border-border rounded-xl w-full max-w-sm shadow-2xl p-6">
|
||||
<h3 className="font-semibold mb-1">
|
||||
{githubCommentDialog.action === 'approve' ? 'Approve PR' : 'Post Review Comment'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Add a general comment to the review (optional).
|
||||
</p>
|
||||
<textarea
|
||||
autoFocus
|
||||
value={githubGeneralComment}
|
||||
onChange={e => setGithubGeneralComment(e.target.value)}
|
||||
placeholder="Leave a comment..."
|
||||
rows={4}
|
||||
className="w-full rounded-md border border-border bg-background text-sm px-3 py-2 resize-none focus:outline-none focus:ring-1 focus:ring-primary mb-3"
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-muted-foreground mb-4 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={githubOpenPR}
|
||||
onChange={e => {
|
||||
setGithubOpenPR(e.target.checked);
|
||||
storage.setItem('plannotator-github-open-pr', String(e.target.checked));
|
||||
}}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
Open PR after submitting
|
||||
</label>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setGithubCommentDialog(null)}
|
||||
className="px-4 py-2 rounded-md text-sm font-medium bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const { action } = githubCommentDialog;
|
||||
setGithubCommentDialog(null);
|
||||
handleGitHubAction(action, githubGeneralComment);
|
||||
}}
|
||||
disabled={githubCommentDialog.action !== 'approve' && totalAnnotationCount === 0 && !githubGeneralComment.trim()}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-opacity ${
|
||||
githubCommentDialog.action !== 'approve' && totalAnnotationCount === 0 && !githubGeneralComment.trim()
|
||||
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
|
||||
: githubCommentDialog.action === 'approve'
|
||||
? 'bg-success text-success-foreground hover:opacity-90'
|
||||
: 'bg-primary text-primary-foreground hover:opacity-90'
|
||||
}`}
|
||||
>
|
||||
{githubCommentDialog.action === 'approve' ? 'Approve' : 'Post Comments'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -239,6 +239,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
pre, code { background-color: ${bg} !important; }
|
||||
[data-file-info] { background-color: ${muted} !important; }
|
||||
[data-column-number] { background-color: ${bg} !important; }
|
||||
[data-diffs-header] [data-title] { display: none !important; }
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export const FileHeader: React.FC<FileHeaderProps> = ({
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-10 px-4 py-2 bg-card/95 backdrop-blur border-b border-border flex items-center justify-between">
|
||||
<span className="font-mono text-sm text-foreground">{filePath}</span>
|
||||
<span className="text-xs font-semibold text-foreground">{filePath}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{onToggleViewed && (
|
||||
<button
|
||||
|
||||
+37
-1
@@ -10,14 +10,17 @@ import {
|
||||
type PRMetadata,
|
||||
type PRContext,
|
||||
type PRRuntime,
|
||||
type PRReviewFileComment,
|
||||
parsePRUrl as parsePRUrlCore,
|
||||
checkGhAuth as checkGhAuthCore,
|
||||
fetchPR as fetchPRCore,
|
||||
fetchPRFileContent as fetchPRFileContentCore,
|
||||
fetchPRContext as fetchPRContextCore,
|
||||
submitPRReview as submitPRReviewCore,
|
||||
getGhUser as getGhUserCore,
|
||||
} from "@plannotator/shared/pr-provider";
|
||||
|
||||
export type { PRRef, PRMetadata, PRContext } from "@plannotator/shared/pr-provider";
|
||||
export type { PRRef, PRMetadata, PRContext, PRReviewFileComment } from "@plannotator/shared/pr-provider";
|
||||
|
||||
const runtime: PRRuntime = {
|
||||
async runCommand(cmd, args) {
|
||||
@@ -34,6 +37,25 @@ const runtime: PRRuntime = {
|
||||
|
||||
return { stdout, stderr, exitCode };
|
||||
},
|
||||
|
||||
async runCommandWithInput(cmd, args, input) {
|
||||
const proc = Bun.spawn([cmd, ...args], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
});
|
||||
|
||||
proc.stdin.write(input);
|
||||
proc.stdin.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
return { stdout, stderr, exitCode };
|
||||
},
|
||||
};
|
||||
|
||||
export const parsePRUrl = parsePRUrlCore;
|
||||
@@ -61,3 +83,17 @@ export function fetchPRFileContent(
|
||||
): Promise<string | null> {
|
||||
return fetchPRFileContentCore(runtime, ref, sha, filePath);
|
||||
}
|
||||
|
||||
export function getGhUser(): Promise<string | null> {
|
||||
return getGhUserCore(runtime);
|
||||
}
|
||||
|
||||
export function submitPRReview(
|
||||
ref: PRRef,
|
||||
headSha: string,
|
||||
action: "approve" | "comment",
|
||||
body: string,
|
||||
fileComments: PRReviewFileComment[],
|
||||
): Promise<void> {
|
||||
return submitPRReviewCore(runtime, ref, headSha, action, body, fileComments);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getRepoInfo } from "./repo";
|
||||
import { handleImage, handleUpload, handleAgents, handleServerReady, handleDraftSave, handleDraftLoad, handleDraftDelete, handleFavicon, type OpencodeClient } from "./shared-handlers";
|
||||
import { contentHash, deleteDraft } from "./draft";
|
||||
import { createEditorAnnotationHandler } from "./editor-annotations";
|
||||
import { type PRMetadata, fetchPRFileContent, fetchPRContext } from "./pr";
|
||||
import { type PRMetadata, type PRReviewFileComment, fetchPRFileContent, fetchPRContext, submitPRReview, getGhUser } from "./pr";
|
||||
|
||||
// Re-export utilities
|
||||
export { isRemoteSession, getServerPort } from "./remote";
|
||||
@@ -108,6 +108,9 @@ export async function startReviewServer(
|
||||
? { display: `${prMetadata.owner}/${prMetadata.repo}`, branch: `PR #${prMetadata.number}` }
|
||||
: await getRepoInfo();
|
||||
|
||||
// Fetch current GitHub user (for own-PR detection)
|
||||
const ghUser = isPRMode ? await getGhUser() : null;
|
||||
|
||||
// Decision promise
|
||||
let resolveDecision: (result: {
|
||||
approved: boolean;
|
||||
@@ -146,7 +149,7 @@ export async function startReviewServer(
|
||||
sharingEnabled,
|
||||
shareBaseUrl,
|
||||
repoInfo,
|
||||
...(isPRMode && { prMetadata }),
|
||||
...(isPRMode && { prMetadata, ghUser }),
|
||||
...(currentError && { error: currentError }),
|
||||
});
|
||||
}
|
||||
@@ -346,6 +349,41 @@ export async function startReviewServer(
|
||||
}
|
||||
}
|
||||
|
||||
// API: Submit PR review directly to GitHub (PR mode only)
|
||||
if (url.pathname === "/api/pr-action" && req.method === "POST") {
|
||||
if (!isPRMode || !prMetadata) {
|
||||
return Response.json({ error: "Not in PR mode" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const body = (await req.json()) as {
|
||||
action: "approve" | "comment";
|
||||
body: string;
|
||||
fileComments: PRReviewFileComment[];
|
||||
};
|
||||
|
||||
const prRef = {
|
||||
platform: "github" as const,
|
||||
owner: prMetadata.owner,
|
||||
repo: prMetadata.repo,
|
||||
number: prMetadata.number,
|
||||
};
|
||||
|
||||
await submitPRReview(
|
||||
prRef,
|
||||
prMetadata.headSha,
|
||||
body.action,
|
||||
body.body,
|
||||
body.fileComments,
|
||||
);
|
||||
|
||||
return Response.json({ ok: true, prUrl: prMetadata.url });
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Failed to submit PR review";
|
||||
return Response.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Favicon
|
||||
if (url.pathname === "/favicon.svg") return handleFavicon();
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ export interface PRRuntime {
|
||||
cmd: string,
|
||||
args: string[],
|
||||
) => Promise<CommandResult>;
|
||||
runCommandWithInput?: (
|
||||
cmd: string,
|
||||
args: string[],
|
||||
input: string,
|
||||
) => Promise<CommandResult>;
|
||||
}
|
||||
|
||||
export interface PRRef {
|
||||
@@ -128,6 +133,18 @@ export async function checkGhAuth(runtime: PRRuntime): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGhUser(runtime: PRRuntime): Promise<string | null> {
|
||||
try {
|
||||
const result = await runtime.runCommand("gh", ["api", "user", "--jq", ".login"]);
|
||||
if (result.exitCode === 0 && result.stdout.trim()) {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fetch PR ---
|
||||
|
||||
export async function fetchPR(
|
||||
@@ -296,3 +313,51 @@ export async function fetchPRFileContent(
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submit PR Review ---
|
||||
|
||||
export interface PRReviewFileComment {
|
||||
path: string;
|
||||
line: number;
|
||||
side: "LEFT" | "RIGHT";
|
||||
body: string;
|
||||
start_line?: number;
|
||||
start_side?: "LEFT" | "RIGHT";
|
||||
}
|
||||
|
||||
export async function submitPRReview(
|
||||
runtime: PRRuntime,
|
||||
ref: PRRef,
|
||||
headSha: string,
|
||||
action: "approve" | "comment",
|
||||
body: string,
|
||||
fileComments: PRReviewFileComment[],
|
||||
): Promise<void> {
|
||||
const payload = JSON.stringify({
|
||||
commit_id: headSha,
|
||||
body,
|
||||
event: action === "approve" ? "APPROVE" : "COMMENT",
|
||||
comments: fileComments,
|
||||
});
|
||||
|
||||
const endpoint = `repos/${ref.owner}/${ref.repo}/pulls/${ref.number}/reviews`;
|
||||
|
||||
let result: CommandResult;
|
||||
|
||||
if (runtime.runCommandWithInput) {
|
||||
result = await runtime.runCommandWithInput(
|
||||
"gh",
|
||||
["api", endpoint, "--method", "POST", "--input", "-"],
|
||||
payload,
|
||||
);
|
||||
} else {
|
||||
// Fallback: write payload to a temp file approach via gh api with --input flag
|
||||
// This branch should not be reached in practice since Bun runtime provides runCommandWithInput
|
||||
throw new Error("Runtime does not support stdin input; cannot submit PR review");
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
const message = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
|
||||
throw new Error(`Failed to submit PR review: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
export const GitHubIcon: React.FC<{ className?: string }> = ({ className }) => (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -118,6 +118,7 @@ const reviewShortcuts: ShortcutSection[] = [
|
||||
title: 'Actions',
|
||||
shortcuts: [
|
||||
{ keys: [mod, enter], desc: 'Approve / Send feedback' },
|
||||
{ keys: [alt, alt], desc: 'Toggle destination', hint: 'Double-tap to switch between GitHub and Agent in PR review mode' },
|
||||
{ keys: [mod, '⇧', 'C'], desc: 'Toggle comment mode' },
|
||||
{ keys: ['Esc'], desc: 'Collapse sidebar' },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export const PullRequestIcon: React.FC<{ className?: string }> = ({ className }) => (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 768 1024"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M704 722s0-306 0-402-96-192-192-192c-64 0-64 0-64 0v-128l-192 192 192 192v-128s32 0 64 0 64 32 64 64 0 402 0 402c-38 22-64 63-64 110 0 71 57 128 128 128s128-57 128-128c0-47-26-88-64-110z m-64 174c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64z m-512-832c-71 0-128 57-128 128 0 47 26 88 64 110v419c-38 22-64 63-64 110 0 71 57 128 128 128s128-57 128-128c0-47-26-88-64-110v-419c38-22 64-63 64-110 0-71-57-128-128-128z m0 832c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64z m0-640c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64z" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export const RepoIcon: React.FC<{ className?: string }> = ({ className }) => (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 768 1024"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M320 256h-64v64h64v-64z m0-128h-64v64h64v-64z m384-128c-32 0-608 0-640 0s-64 32-64 64 0 736 0 768 32 64 64 64 128 0 128 0v128l96-96 96 96v-128s288 0 320 0 64-32 64-64 0-736 0-768-32-64-64-64z m0 800c0 16-15 32-32 32s-288 0-288 0v-64h-192v64s-79 0-96 0-32-17-32-32 0-96 0-96h640s0 80 0 96z m0-160h-512v-576h513l-1 576z m-384-128h-64v64h64v-64z m0-128h-64v64h64v-64z" />
|
||||
</svg>
|
||||
);
|
||||
Reference in New Issue
Block a user