flake-update-20260505
1/**
2 * Issue action handlers for GitHub extension
3 */
4
5import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
6import type { GhDetails } from "../types";
7import {
8 parseIssueList,
9 getErrorMessage,
10 extractIssueNumber,
11 extractIssueUrl,
12 buildIssueCreateConfirmation,
13 buildCommentConfirmation,
14 buildSubIssueConfirmation,
15 truncate,
16 execGh,
17 resolveIssueNodeId,
18 addSubIssue,
19 removeSubIssue,
20 approvalGate,
21 approvalGateWithBodyPreview,
22 buildModifyResult,
23 buildRejectResult,
24} from "../utils";
25
26const ISSUE_LIST_FIELDS = "number,title,state,author,url,labels,assignees,createdAt,updatedAt,body,comments";
27
28/**
29 * List issues
30 */
31export async function handleIssueList(
32 pi: ExtensionAPI,
33 params: any,
34 signal: AbortSignal | undefined,
35 onUpdate: any,
36 ctx: ExtensionContext,
37 currentUser: string,
38): Promise<any> {
39 const args = ["issue", "list", "--json", ISSUE_LIST_FIELDS];
40
41 if (params.state) args.push("--state", params.state);
42 if (params.label) args.push("--label", params.label);
43 if (params.assignee) {
44 args.push("--assignee", params.assignee === "me" ? (currentUser || "@me") : params.assignee);
45 }
46 if (params.milestone) args.push("--milestone", params.milestone);
47 if (params.limit) args.push("--limit", String(params.limit));
48 else args.push("--limit", "20");
49
50 onUpdate?.({ content: [{ type: "text", text: "Fetching issues..." }] });
51
52 const result = await execGh(pi, ctx, args, { signal, timeout: 30000 });
53
54 if (result.code !== 0) {
55 return {
56 content: [{ type: "text", text: getErrorMessage(result.stderr, "List issues") }],
57 details: { action: "issue-list", error: result.stderr } as GhDetails,
58 isError: true,
59 };
60 }
61
62 const issues = parseIssueList(result.stdout);
63
64 let output = "";
65 if (issues.length === 0) {
66 output = "No issues found.";
67 } else {
68 output = issues
69 .map((issue) => {
70 const labels = issue.labels.length > 0 ? ` [${issue.labels.join(", ")}]` : "";
71 const assignees = issue.assignees.length > 0 ? ` @${issue.assignees.join(", @")}` : "";
72 return `#${issue.number} ${issue.title}${labels}${assignees} (${issue.state})`;
73 })
74 .join("\n");
75 }
76
77 const issueNumbers = issues.map((i) => i.number);
78
79 return {
80 content: [{ type: "text", text: output }],
81 details: { action: "issue-list", output, issueNumbers } as GhDetails,
82 };
83}
84
85/**
86 * View issue details
87 */
88export async function handleIssueView(
89 pi: ExtensionAPI,
90 params: any,
91 signal: AbortSignal | undefined,
92 onUpdate: any,
93 ctx: ExtensionContext,
94): Promise<any> {
95 if (!params.number) {
96 return {
97 content: [{ type: "text", text: "Error: 'number' parameter is required for issue-view action" }],
98 details: { action: "issue-view", error: "missing_number" } as GhDetails,
99 isError: true,
100 };
101 }
102
103 onUpdate?.({ content: [{ type: "text", text: `Fetching issue #${params.number}...` }] });
104
105 const result = await execGh(pi, ctx,
106 ["issue", "view", String(params.number), "--json", `${ISSUE_LIST_FIELDS},milestone`],
107 { signal, timeout: 20000 },
108 );
109
110 if (result.code !== 0) {
111 return {
112 content: [{ type: "text", text: getErrorMessage(result.stderr, "View issue") }],
113 details: { action: "issue-view", error: result.stderr, issueNumber: params.number } as GhDetails,
114 isError: true,
115 };
116 }
117
118 let data: any;
119 try {
120 data = JSON.parse(result.stdout);
121 } catch {
122 return {
123 content: [{ type: "text", text: result.stdout }],
124 details: { action: "issue-view", output: result.stdout, issueNumber: params.number } as GhDetails,
125 };
126 }
127
128 let output = "";
129 output += `# Issue #${data.number}: ${data.title}\n\n`;
130 output += `State: ${data.state}\n`;
131 output += `Author: @${data.author?.login ?? "?"}\n`;
132 output += `URL: ${data.url}\n`;
133
134 if (data.labels?.length > 0) {
135 output += `Labels: ${data.labels.map((l: any) => l.name ?? l).join(", ")}\n`;
136 }
137 if (data.assignees?.length > 0) {
138 output += `Assignees: ${data.assignees.map((a: any) => `@${a.login ?? a}`).join(", ")}\n`;
139 }
140 if (data.milestone) {
141 output += `Milestone: ${data.milestone.title ?? data.milestone}\n`;
142 }
143
144 if (data.body) {
145 output += `\n## Description\n\n${data.body}\n`;
146 }
147
148 // Comments are already included in the initial JSON response as an array
149 const comments = Array.isArray(data.comments) ? data.comments : [];
150 if (comments.length > 0) {
151 output += `\n## Comments (${comments.length})\n`;
152 for (const comment of comments) {
153 output += `\n@${comment.author?.login ?? "?"} (${comment.createdAt ?? ""}):\n${comment.body}\n`;
154 }
155 }
156
157 return {
158 content: [{ type: "text", text: output }],
159 details: {
160 action: "issue-view",
161 output,
162 issueNumber: data.number,
163 issueUrl: data.url,
164 } as GhDetails,
165 };
166}
167
168/**
169 * Create issue (requires approval)
170 */
171export async function handleIssueCreate(
172 pi: ExtensionAPI,
173 params: any,
174 signal: AbortSignal | undefined,
175 onUpdate: any,
176 ctx: ExtensionContext,
177): Promise<any> {
178 if (!params.title) {
179 return {
180 content: [{ type: "text", text: "Error: 'title' parameter is required for issue-create action" }],
181 details: { action: "issue-create", error: "missing_title" } as GhDetails,
182 isError: true,
183 };
184 }
185
186 // APPROVAL GATE
187 if (ctx.hasUI) {
188 let confirmMessage = buildIssueCreateConfirmation(params);
189 if (params.parent) {
190 confirmMessage = confirmMessage.replace(
191 "This will create a new issue on GitHub.",
192 `This will create a new issue on GitHub and link it as a sub-issue of #${params.parent}.`,
193 );
194 }
195
196 // If body is long, offer to preview full content
197 if (params.body && params.body.length > 200) {
198 confirmMessage += `\n\n📝 Body: ${params.body.length} characters (truncated in preview)`;
199
200 const choice = await ctx.ui.select(
201 `Create Issue?\n\n${confirmMessage}`,
202 ["✓ Accept", "👁 Preview body first", "✎ Modify", "✗ Reject"]
203 );
204
205 if (choice === undefined || choice === "✗ Reject") {
206 ctx.ui.notify("Issue creation rejected", "info");
207 return buildRejectResult("issue creation", { action: "issue-create" });
208 }
209
210 if (choice === "✎ Modify") {
211 ctx.ui.notify("Issue creation paused for modifications", "info");
212 return buildModifyResult("issue creation", { action: "issue-create" });
213 }
214
215 if (choice === "👁 Preview body first") {
216 await ctx.ui.editor(
217 `Issue Body Preview (${params.body.length} chars):\n\nTitle: ${params.title}\n\n---\n\n`,
218 params.body
219 );
220
221 // Ask again after preview
222 const approval = await approvalGate(ctx, "Create Issue?", confirmMessage);
223 if (approval.outcome === "modify") {
224 ctx.ui.notify("Issue creation paused for modifications", "info");
225 return buildModifyResult("issue creation", { action: "issue-create" });
226 }
227 if (approval.outcome === "rejected") {
228 ctx.ui.notify("Issue creation rejected", "info");
229 return buildRejectResult("issue creation", { action: "issue-create" });
230 }
231 }
232 } else {
233 const approval = await approvalGate(ctx, "Create Issue?", confirmMessage);
234 if (approval.outcome === "modify") {
235 ctx.ui.notify("Issue creation paused for modifications", "info");
236 return buildModifyResult("issue creation", { action: "issue-create" });
237 }
238 if (approval.outcome === "rejected") {
239 ctx.ui.notify("Issue creation rejected", "info");
240 return buildRejectResult("issue creation", { action: "issue-create" });
241 }
242 }
243 }
244
245 const args = ["issue", "create", "--title", params.title];
246
247 if (params.body) args.push("--body", params.body);
248 if (params.labels?.length) {
249 for (const label of params.labels) args.push("--label", label);
250 }
251 if (params.assignees?.length) {
252 for (const assignee of params.assignees) args.push("--assignee", assignee);
253 }
254 if (params.milestone) args.push("--milestone", params.milestone);
255
256 onUpdate?.({ content: [{ type: "text", text: "Creating issue..." }] });
257
258 const result = await execGh(pi, ctx, args, { signal, timeout: 30000 });
259
260 if (result.code !== 0) {
261 return {
262 content: [{ type: "text", text: getErrorMessage(result.stderr, "Create issue") }],
263 details: { action: "issue-create", error: result.stderr } as GhDetails,
264 isError: true,
265 };
266 }
267
268 const issueNumber = extractIssueNumber(result.stdout);
269 const issueUrl = extractIssueUrl(result.stdout) || result.stdout.trim();
270
271 // If parent is specified, link this issue as a sub-issue
272 let parentLinked = false;
273 let parentError: string | undefined;
274 if (params.parent && issueNumber) {
275 onUpdate?.({ content: [{ type: "text", text: `Created issue #${issueNumber}, linking as sub-issue of #${params.parent}...` }] });
276
277 const parentNode = await resolveIssueNodeId(pi, ctx, params.parent, signal);
278 const childNode = await resolveIssueNodeId(pi, ctx, issueNumber, signal);
279
280 if (!parentNode) {
281 parentError = `Could not resolve parent issue #${params.parent}`;
282 } else if (!childNode) {
283 parentError = `Could not resolve created issue #${issueNumber} for sub-issue linking`;
284 } else {
285 const linkResult = await addSubIssue(pi, ctx, parentNode.id, childNode.id, signal);
286 if (linkResult.success) {
287 parentLinked = true;
288 } else {
289 parentError = linkResult.error;
290 }
291 }
292 }
293
294 let text = `Created issue${issueNumber ? ` #${issueNumber}` : ""}: ${issueUrl}`;
295 if (parentLinked) {
296 text += ` (sub-issue of #${params.parent})`;
297 } else if (parentError) {
298 text += `\nWarning: Failed to link as sub-issue of #${params.parent}: ${parentError}`;
299 }
300
301 return {
302 content: [{ type: "text", text }],
303 details: {
304 action: "issue-create",
305 output: result.stdout.trim(),
306 issueNumber: issueNumber ?? undefined,
307 issueUrl: issueUrl ?? undefined,
308 parentNumber: parentLinked ? params.parent : undefined,
309 } as GhDetails,
310 };
311}
312
313/**
314 * Close issue (requires approval)
315 */
316export async function handleIssueClose(
317 pi: ExtensionAPI,
318 params: any,
319 signal: AbortSignal | undefined,
320 onUpdate: any,
321 ctx: ExtensionContext,
322): Promise<any> {
323 if (!params.number) {
324 return {
325 content: [{ type: "text", text: "Error: 'number' parameter is required for issue-close action" }],
326 details: { action: "issue-close", error: "missing_number" } as GhDetails,
327 isError: true,
328 };
329 }
330
331 // APPROVAL GATE
332 if (ctx.hasUI) {
333 const reason = params.reason || "completed";
334 const approval = await approvalGate(ctx, `Close issue #${params.number}?`, `This will close the issue as "${reason}".`);
335 if (approval.outcome === "modify") {
336 ctx.ui.notify("Close paused for modifications", "info");
337 return buildModifyResult("issue close", { action: "issue-close", issueNumber: params.number });
338 }
339 if (approval.outcome === "rejected") {
340 ctx.ui.notify("Close rejected", "info");
341 return buildRejectResult("issue close", { action: "issue-close", issueNumber: params.number });
342 }
343 }
344
345 const args = ["issue", "close", String(params.number)];
346 if (params.reason) args.push("--reason", params.reason);
347
348 const result = await execGh(pi, ctx, args, { signal, timeout: 20000 });
349
350 if (result.code !== 0) {
351 return {
352 content: [{ type: "text", text: getErrorMessage(result.stderr, "Close issue") }],
353 details: { action: "issue-close", error: result.stderr, issueNumber: params.number } as GhDetails,
354 isError: true,
355 };
356 }
357
358 return {
359 content: [{ type: "text", text: `Closed issue #${params.number}` }],
360 details: { action: "issue-close", output: result.stdout.trim(), issueNumber: params.number } as GhDetails,
361 };
362}
363
364/**
365 * Comment on issue (requires approval)
366 */
367export async function handleIssueComment(
368 pi: ExtensionAPI,
369 params: any,
370 signal: AbortSignal | undefined,
371 onUpdate: any,
372 ctx: ExtensionContext,
373): Promise<any> {
374 if (!params.number) {
375 return {
376 content: [{ type: "text", text: "Error: 'number' parameter is required for issue-comment action" }],
377 details: { action: "issue-comment", error: "missing_number" } as GhDetails,
378 isError: true,
379 };
380 }
381
382 if (!params.body) {
383 return {
384 content: [{ type: "text", text: "Error: 'body' parameter is required for issue-comment action" }],
385 details: { action: "issue-comment", error: "missing_body" } as GhDetails,
386 isError: true,
387 };
388 }
389
390 // APPROVAL GATE
391 if (ctx.hasUI) {
392 const confirmMessage = buildCommentConfirmation("Issue", params.number, params.body);
393 const approval = await approvalGateWithBodyPreview(
394 ctx,
395 `Comment on issue #${params.number}?`,
396 confirmMessage,
397 `Issue #${params.number} Comment Preview (${params.body.length} chars):`,
398 params.body,
399 );
400 if (approval.outcome === "modify") {
401 ctx.ui.notify("Comment paused for modifications", "info");
402 return buildModifyResult("comment", { action: "issue-comment", issueNumber: params.number });
403 }
404 if (approval.outcome === "rejected") {
405 ctx.ui.notify("Comment rejected", "info");
406 return buildRejectResult("comment", { action: "issue-comment", issueNumber: params.number });
407 }
408 }
409
410 onUpdate?.({ content: [{ type: "text", text: `Adding comment to issue #${params.number}...` }] });
411
412 const result = await execGh(pi, ctx,
413 ["issue", "comment", String(params.number), "--body", params.body],
414 { signal, timeout: 20000 },
415 );
416
417 if (result.code !== 0) {
418 return {
419 content: [{ type: "text", text: getErrorMessage(result.stderr, "Comment on issue") }],
420 details: { action: "issue-comment", error: result.stderr, issueNumber: params.number } as GhDetails,
421 isError: true,
422 };
423 }
424
425 return {
426 content: [{ type: "text", text: `Added comment to issue #${params.number}` }],
427 details: { action: "issue-comment", output: result.stdout.trim(), issueNumber: params.number } as GhDetails,
428 };
429}
430
431/**
432 * Edit issue (requires approval)
433 */
434export async function handleIssueEdit(
435 pi: ExtensionAPI,
436 params: any,
437 signal: AbortSignal | undefined,
438 onUpdate: any,
439 ctx: ExtensionContext,
440): Promise<any> {
441 if (!params.number) {
442 return {
443 content: [{ type: "text", text: "Error: 'number' parameter is required for issue-edit action" }],
444 details: { action: "issue-edit", error: "missing_number" } as GhDetails,
445 isError: true,
446 };
447 }
448
449 // Build description of what we're editing
450 const changes: string[] = [];
451 if (params.title) changes.push(`Title: "${params.title}"`);
452 if (params.body) changes.push(`Body: "${truncate(params.body, 100)}"`);
453 if (params.addLabels?.length) changes.push(`Add labels: ${params.addLabels.join(", ")}`);
454 if (params.removeLabels?.length) changes.push(`Remove labels: ${params.removeLabels.join(", ")}`);
455 if (params.addAssignees?.length) changes.push(`Add assignees: ${params.addAssignees.join(", ")}`);
456 if (params.removeAssignees?.length) changes.push(`Remove assignees: ${params.removeAssignees.join(", ")}`);
457 if (params.milestone) changes.push(`Milestone: ${params.milestone}`);
458
459 if (changes.length === 0) {
460 return {
461 content: [{ type: "text", text: "Error: No fields to update specified" }],
462 details: { action: "issue-edit", error: "no_changes" } as GhDetails,
463 isError: true,
464 };
465 }
466
467 // APPROVAL GATE
468 if (ctx.hasUI) {
469 const confirmMessage = `Issue: #${params.number}\n\nChanges:\n${changes.join("\n")}\n\nThis will modify the issue.`;
470 const approval = params.body
471 ? await approvalGateWithBodyPreview(
472 ctx,
473 `Edit issue #${params.number}?`,
474 confirmMessage,
475 `Issue #${params.number} New Body Preview (${params.body.length} chars):`,
476 params.body,
477 )
478 : await approvalGate(ctx, `Edit issue #${params.number}?`, confirmMessage);
479 if (approval.outcome === "modify") {
480 ctx.ui.notify("Edit paused for modifications", "info");
481 return buildModifyResult("issue edit", { action: "issue-edit", issueNumber: params.number });
482 }
483 if (approval.outcome === "rejected") {
484 ctx.ui.notify("Edit rejected", "info");
485 return buildRejectResult("issue edit", { action: "issue-edit", issueNumber: params.number });
486 }
487 }
488
489 const args = ["issue", "edit", String(params.number)];
490
491 if (params.title) args.push("--title", params.title);
492 if (params.body) args.push("--body", params.body);
493 if (params.addLabels?.length) {
494 for (const label of params.addLabels) args.push("--add-label", label);
495 }
496 if (params.removeLabels?.length) {
497 for (const label of params.removeLabels) args.push("--remove-label", label);
498 }
499 if (params.addAssignees?.length) {
500 for (const assignee of params.addAssignees) args.push("--add-assignee", assignee);
501 }
502 if (params.removeAssignees?.length) {
503 for (const assignee of params.removeAssignees) args.push("--remove-assignee", assignee);
504 }
505 if (params.milestone) args.push("--milestone", params.milestone);
506
507 onUpdate?.({ content: [{ type: "text", text: `Editing issue #${params.number}...` }] });
508
509 const result = await execGh(pi, ctx, args, { signal, timeout: 20000 });
510
511 if (result.code !== 0) {
512 return {
513 content: [{ type: "text", text: getErrorMessage(result.stderr, "Edit issue") }],
514 details: { action: "issue-edit", error: result.stderr, issueNumber: params.number } as GhDetails,
515 isError: true,
516 };
517 }
518
519 return {
520 content: [{ type: "text", text: `Updated issue #${params.number}: ${changes.join("; ")}` }],
521 details: {
522 action: "issue-edit",
523 output: result.stdout.trim(),
524 issueNumber: params.number,
525 } as GhDetails,
526 };
527}
528
529/**
530 * Add sub-issue relationship (requires approval)
531 */
532export async function handleIssueAddSubIssue(
533 pi: ExtensionAPI,
534 params: any,
535 signal: AbortSignal | undefined,
536 onUpdate: any,
537 ctx: ExtensionContext,
538): Promise<any> {
539 if (!params.number) {
540 return {
541 content: [{ type: "text", text: "Error: 'number' (parent issue) parameter is required for issue-add-sub-issue action" }],
542 details: { action: "issue-add-sub-issue", error: "missing_number" } as GhDetails,
543 isError: true,
544 };
545 }
546
547 if (!params.subIssueNumber) {
548 return {
549 content: [{ type: "text", text: "Error: 'subIssueNumber' parameter is required for issue-add-sub-issue action" }],
550 details: { action: "issue-add-sub-issue", error: "missing_sub_issue_number" } as GhDetails,
551 isError: true,
552 };
553 }
554
555 // APPROVAL GATE
556 if (ctx.hasUI) {
557 const confirmMessage = buildSubIssueConfirmation("add", params.number, params.subIssueNumber);
558 const approval = await approvalGate(ctx, `Add sub-issue #${params.subIssueNumber} to #${params.number}?`, confirmMessage);
559 if (approval.outcome === "modify") {
560 ctx.ui.notify("Add sub-issue paused for modifications", "info");
561 return buildModifyResult("add sub-issue", { action: "issue-add-sub-issue", parentNumber: params.number, subIssueNumber: params.subIssueNumber });
562 }
563 if (approval.outcome === "rejected") {
564 ctx.ui.notify("Add sub-issue rejected", "info");
565 return buildRejectResult("add sub-issue", { action: "issue-add-sub-issue", parentNumber: params.number, subIssueNumber: params.subIssueNumber });
566 }
567 }
568
569 onUpdate?.({ content: [{ type: "text", text: `Linking #${params.subIssueNumber} as sub-issue of #${params.number}...` }] });
570
571 const parentNode = await resolveIssueNodeId(pi, ctx, params.number, signal);
572 if (!parentNode) {
573 return {
574 content: [{ type: "text", text: `Error: Could not resolve parent issue #${params.number}` }],
575 details: { action: "issue-add-sub-issue", error: "parent_not_found", parentNumber: params.number } as GhDetails,
576 isError: true,
577 };
578 }
579
580 const childNode = await resolveIssueNodeId(pi, ctx, params.subIssueNumber, signal);
581 if (!childNode) {
582 return {
583 content: [{ type: "text", text: `Error: Could not resolve sub-issue #${params.subIssueNumber}` }],
584 details: { action: "issue-add-sub-issue", error: "sub_issue_not_found", subIssueNumber: params.subIssueNumber } as GhDetails,
585 isError: true,
586 };
587 }
588
589 const result = await addSubIssue(pi, ctx, parentNode.id, childNode.id, signal);
590
591 if (!result.success) {
592 return {
593 content: [{ type: "text", text: `Error: ${result.error}` }],
594 details: { action: "issue-add-sub-issue", error: result.error, parentNumber: params.number, subIssueNumber: params.subIssueNumber } as GhDetails,
595 isError: true,
596 };
597 }
598
599 return {
600 content: [{ type: "text", text: `Added #${params.subIssueNumber} as sub-issue of #${params.number}` }],
601 details: {
602 action: "issue-add-sub-issue",
603 parentNumber: params.number,
604 subIssueNumber: params.subIssueNumber,
605 } as GhDetails,
606 };
607}
608
609/**
610 * Remove sub-issue relationship (requires approval)
611 */
612export async function handleIssueRemoveSubIssue(
613 pi: ExtensionAPI,
614 params: any,
615 signal: AbortSignal | undefined,
616 onUpdate: any,
617 ctx: ExtensionContext,
618): Promise<any> {
619 if (!params.number) {
620 return {
621 content: [{ type: "text", text: "Error: 'number' (parent issue) parameter is required for issue-remove-sub-issue action" }],
622 details: { action: "issue-remove-sub-issue", error: "missing_number" } as GhDetails,
623 isError: true,
624 };
625 }
626
627 if (!params.subIssueNumber) {
628 return {
629 content: [{ type: "text", text: "Error: 'subIssueNumber' parameter is required for issue-remove-sub-issue action" }],
630 details: { action: "issue-remove-sub-issue", error: "missing_sub_issue_number" } as GhDetails,
631 isError: true,
632 };
633 }
634
635 // APPROVAL GATE
636 if (ctx.hasUI) {
637 const confirmMessage = buildSubIssueConfirmation("remove", params.number, params.subIssueNumber);
638 const approval = await approvalGate(ctx, `Remove sub-issue #${params.subIssueNumber} from #${params.number}?`, confirmMessage);
639 if (approval.outcome === "modify") {
640 ctx.ui.notify("Remove sub-issue paused for modifications", "info");
641 return buildModifyResult("remove sub-issue", { action: "issue-remove-sub-issue", parentNumber: params.number, subIssueNumber: params.subIssueNumber });
642 }
643 if (approval.outcome === "rejected") {
644 ctx.ui.notify("Remove sub-issue rejected", "info");
645 return buildRejectResult("remove sub-issue", { action: "issue-remove-sub-issue", parentNumber: params.number, subIssueNumber: params.subIssueNumber });
646 }
647 }
648
649 onUpdate?.({ content: [{ type: "text", text: `Removing #${params.subIssueNumber} as sub-issue of #${params.number}...` }] });
650
651 const parentNode = await resolveIssueNodeId(pi, ctx, params.number, signal);
652 if (!parentNode) {
653 return {
654 content: [{ type: "text", text: `Error: Could not resolve parent issue #${params.number}` }],
655 details: { action: "issue-remove-sub-issue", error: "parent_not_found", parentNumber: params.number } as GhDetails,
656 isError: true,
657 };
658 }
659
660 const childNode = await resolveIssueNodeId(pi, ctx, params.subIssueNumber, signal);
661 if (!childNode) {
662 return {
663 content: [{ type: "text", text: `Error: Could not resolve sub-issue #${params.subIssueNumber}` }],
664 details: { action: "issue-remove-sub-issue", error: "sub_issue_not_found", subIssueNumber: params.subIssueNumber } as GhDetails,
665 isError: true,
666 };
667 }
668
669 const result = await removeSubIssue(pi, ctx, parentNode.id, childNode.id, signal);
670
671 if (!result.success) {
672 return {
673 content: [{ type: "text", text: `Error: ${result.error}` }],
674 details: { action: "issue-remove-sub-issue", error: result.error, parentNumber: params.number, subIssueNumber: params.subIssueNumber } as GhDetails,
675 isError: true,
676 };
677 }
678
679 return {
680 content: [{ type: "text", text: `Removed #${params.subIssueNumber} as sub-issue of #${params.number}` }],
681 details: {
682 action: "issue-remove-sub-issue",
683 parentNumber: params.number,
684 subIssueNumber: params.subIssueNumber,
685 } as GhDetails,
686 };
687}