Implement GitHub issue close reasons and tabbed comment composer (#5235)

- Support closing GitHub issues with specific reasons ('completed', 'not planned', or duplicate) via main-process mutations and gh CLI wrappers.
- Replace the legacy comment composer with a tabbed layout supporting a real-time markdown preview, image/attachment inputs, and state transition buttons.
- Refactor assignee and label popovers into dedicated reusable components to clean up the item dialog.
This commit is contained in:
Jinjing
2026-06-11 19:49:41 -07:00
committed by GitHub
parent 04f394c17a
commit 36e439907a
26 changed files with 1858 additions and 490 deletions
+13 -4
View File
@@ -235,10 +235,19 @@ export async function updateIssue(
if (updates.state) {
await acquire()
try {
const cmd = updates.state === 'closed' ? 'close' : 'reopen'
await ghExecFileAsync(['issue', cmd, String(issueNumber), '--repo', repo], {
...ghOptions
})
if (updates.state === 'closed') {
const closeArgs = ['issue', 'close', String(issueNumber), '--repo', repo]
if (updates.stateReason === 'completed') {
closeArgs.push('--reason', 'completed')
} else if (updates.stateReason === 'not_planned') {
closeArgs.push('--reason', 'not planned')
} else if (updates.stateReason === 'duplicate' && updates.duplicateOf) {
closeArgs.push('--duplicate-of', String(updates.duplicateOf))
}
await ghExecFileAsync(closeArgs, ghOptions)
} else {
await ghExecFileAsync(['issue', 'reopen', String(issueNumber), '--repo', repo], ghOptions)
}
} catch (err) {
const stderr = err instanceof Error ? err.message : String(err)
// Treat "already closed/open" as a no-op
+23 -3
View File
@@ -171,15 +171,29 @@ export async function updateIssueBySlug(
if (!args.updates || typeof args.updates !== 'object') {
return { ok: false, error: { type: 'validation_error', message: 'Updates required.' } }
}
const { title, body, state, addLabels, removeLabels, addAssignees, removeAssignees } =
args.updates
const {
title,
body,
state,
stateReason,
duplicateOf,
addLabels,
removeLabels,
addAssignees,
removeAssignees
} = args.updates
// Title / body / state go through PATCH /repos/{owner}/{repo}/issues/{n}.
// Labels/assignees go through their dedicated endpoints.
const base = `repos/${args.owner}/${args.repo}/issues/${args.number}`
// 1) PATCH body
if (title !== undefined || body !== undefined || state !== undefined) {
if (
title !== undefined ||
body !== undefined ||
state !== undefined ||
stateReason !== undefined
) {
const patchArgs: string[] = ['-X', 'PATCH', base]
if (title !== undefined) {
patchArgs.push('--raw-field', `title=${title}`)
@@ -190,6 +204,12 @@ export async function updateIssueBySlug(
if (state !== undefined) {
patchArgs.push('--raw-field', `state=${state}`)
}
if (stateReason !== undefined) {
patchArgs.push('--raw-field', `state_reason=${stateReason}`)
}
if (duplicateOf !== undefined) {
patchArgs.push('--raw-field', `duplicate_of=${duplicateOf}`)
}
const r = await runRest<unknown>(patchArgs)
if (!r.ok) {
return { ok: false, error: r.error }