feat(ui): forge write operations — comments, replies, close/reopen, edit, reviews, draft, metadata

- server: write routes for all three providers (issue/PR comments, inline review-comment replies, issue/MR updates w/ labels-assignees-milestone, review submit, draft toggle)
- ui: ForgeProvider gains six write ops; shared action components (composer, thread reply, state/review/draft/metadata/edit) wired into ForgeEntityDetailView and GitHub PR Overview
This commit is contained in:
2026-08-16 16:29:24 +00:00
parent 92f0eced34
commit 8f5cfdcd62
44 changed files with 5383 additions and 62 deletions
+73
View File
@@ -2,7 +2,11 @@ import type {
GitHubAPI,
GitHubAuthStatus,
GitHubIssueCommentsResult,
GitHubIssueCommentInput,
GitHubIssueCommentResult,
GitHubIssueGetResult,
GitHubIssueUpdateInput,
GitHubIssueUpdateResult,
GitHubIssuesListResult,
GitHubPullRequestContextResult,
GitHubPullRequestCommitsResult,
@@ -16,7 +20,11 @@ import type {
GitHubPullRequestReadyResult,
GitHubPullRequestUpdateInput,
GitHubPullRequestStatus,
GitHubPullRequestReviewInput,
GitHubPullRequestReviewResult,
GitHubRepoUpstreamResult,
GitHubReviewCommentInput,
GitHubReviewCommentResult,
GitHubDeviceFlowComplete,
GitHubDeviceFlowStart,
GitHubUserSummary,
@@ -325,4 +333,69 @@ export const createWebGitHubAPI = ({ urls }: WebGitHubAPIOptions): GitHubAPI =>
}
return payload;
},
async issueComment(input: GitHubIssueCommentInput): Promise<GitHubIssueCommentResult> {
const response = await runtimeFetch('/api/github/issues/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubIssueCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post GitHub comment');
}
return body;
},
async issueUpdate(input: GitHubIssueUpdateInput): Promise<GitHubIssueUpdateResult> {
const response = await runtimeFetch('/api/github/issues/update', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubIssueUpdateResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to update GitHub issue');
}
return body;
},
async prComment(input: GitHubIssueCommentInput): Promise<GitHubIssueCommentResult> {
const response = await runtimeFetch('/api/github/pulls/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubIssueCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post GitHub PR comment');
}
return body;
},
async prReviewComment(input: GitHubReviewCommentInput): Promise<GitHubReviewCommentResult> {
const response = await runtimeFetch('/api/github/pulls/review-comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubReviewCommentResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to post GitHub review comment');
}
return body;
},
async prSubmitReview(input: GitHubPullRequestReviewInput): Promise<GitHubPullRequestReviewResult> {
const response = await runtimeFetch('/api/github/pulls/review', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(input),
});
const body = await jsonOrNull<GitHubPullRequestReviewResult & { error?: string }>(response);
if (!response.ok || !body) {
throw new Error(body?.error || response.statusText || 'Failed to submit GitHub review');
}
return body;
},
});