@kud/gh-cockpit 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import { useWindowSize, Box, Text, useInput } from 'ink';
2
2
  import { Panel, colors, FooterHints, useListCursor } from '@kud/ink-ui';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
  import { inboxConfig, clipboard, buildCheckoutCmd, runHere, openInTab, runInPane, runInPaneHorizontal } from '@kud/gh-ink';
5
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, PIN_MARK, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, bandOf, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, impliedLabels, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, labelPriority, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, sortByRecency, sortItems, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, workCount, writeCache } from '@kud/gh-ink';
5
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, NEEDS_YOU_SECTION, PIN_MARK, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, bandOf, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, impliedLabels, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, labelPriority, layoutGHItems, matchesFilter, maxViewStart, moveCursor, numberColumnsOf, openInTab, parsePatterns, profileOf, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, sortByRecency, sortItems, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, workCount, writeCache } from '@kud/gh-ink';
6
6
  export { toGHItem } from '@kud/gh-workflow';
7
7
  import { mkdirSync } from 'fs';
8
8
  import { homedir } from 'os';
@@ -1,4 +1,4 @@
1
- import { AiLauncher, seedPromptFor, CopyPromptNotice } from '../chunk-PP3BVSMA.js';
1
+ import { AiLauncher, seedPromptFor, CopyPromptNotice } from '../chunk-B3FS2IVR.js';
2
2
  import { useEffect } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { RepoFilter, InboxExtension, DetailContext, GHItem } from '@kud/gh-ink';
2
+ import { RepoFilter, InboxExtension, GHItem, NeedsYou, DetailContext } from '@kud/gh-ink';
3
3
  export * from '@kud/gh-ink';
4
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, PIN_MARK, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, bandOf, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, impliedLabels, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, labelPriority, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, sortByRecency, sortItems, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, workCount, writeCache } from '@kud/gh-ink';
4
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, MIN_SIDEBAR_COLS, NEEDS_YOU_SECTION, PIN_MARK, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, bandOf, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, impliedLabels, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, labelPriority, layoutGHItems, matchesFilter, maxViewStart, moveCursor, numberColumnsOf, openInTab, parsePatterns, profileOf, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signatureOf, sortByRecency, sortItems, stripGlyph, stripLayout, stripSignature, toCiStatusState, trailingColumnsOf, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, workCount, writeCache } from '@kud/gh-ink';
5
5
  import { Health } from '@kud/gh';
6
6
  export { InboxSource, buildInboxQueries, buildInboxQuery, mergeInboxData } from '@kud/gh';
7
7
  export { CockpitItem, toGHItem } from '@kud/gh-workflow';
@@ -137,6 +137,27 @@ type PromptForms = {
137
137
  /** Supply the host's prompt forms. Call once, before rendering. */
138
138
  declare const registerPrompts: (forms: PromptForms) => void;
139
139
 
140
+ /**
141
+ * What the hook is told: the item's identity and labels, which is what a label
142
+ * is cleared by. Not the full row — the issue drill holds only a subset of one,
143
+ * and a hook that needs more is better off asking GitHub than trusting a row
144
+ * cached before the answer was posted.
145
+ */
146
+ type DecisionTarget = Pick<GHItem, "repo" | "number" | "url" | "labels"> & {
147
+ kind: "pr" | "issue";
148
+ };
149
+ type DecisionHooks = {
150
+ /** Called after an answer posted from a drill view. */
151
+ answered?: (target: DecisionTarget) => void | Promise<void>;
152
+ };
153
+ /** Supply the host's decision follow-through. Call once, before rendering. */
154
+ declare const registerDecisionHooks: (hooks: DecisionHooks) => void;
155
+ declare const decisionHooks: () => DecisionHooks;
156
+
157
+ declare const DecisionBlock: ({ needsYou }: {
158
+ needsYou: NeedsYou;
159
+ }) => React.JSX.Element;
160
+
140
161
  declare const detailFor: (ctx: DetailContext) => React.JSX.Element;
141
162
 
142
163
  declare const DrillView: ({ title, subtitle, hints, children, }: {
@@ -159,18 +180,21 @@ declare const PrView: ({ item, login, onBack, defaultTab, onRefresh, onRemove, o
159
180
  onTyping?: (typing: boolean) => void;
160
181
  }) => React.JSX.Element;
161
182
 
162
- declare const IssueView: ({ item, login, onBack, registerPeel, onTyping, }: {
183
+ declare const IssueView: ({ item, login, onBack, onRefresh, onRemove, registerPeel, onTyping, }: {
163
184
  item: {
164
185
  number: number;
165
186
  repo: string;
166
187
  url: string;
167
188
  title?: string;
168
189
  labels?: readonly string[];
190
+ needsYou?: NeedsYou;
169
191
  };
170
192
  login: string;
171
193
  onBack: () => void;
194
+ onRefresh?: () => void;
195
+ onRemove?: () => void;
172
196
  registerPeel?: (peel: (() => boolean) | null) => void;
173
197
  onTyping?: (typing: boolean) => void;
174
198
  }) => React.JSX.Element;
175
199
 
176
- export { type CheckDrill, type CheckDrillContext, type CockpitArgs, type CockpitConfig, DrillView, IssueView, PrView, type PromptContext, type PromptForms, type TabSearch, type TabSpec, checkDrillFor, computeHealth, defineCockpit, detailFor, hasPatterns, parseArgs, registerCheckDrills, registerPrompts, signalPath, withRetry };
200
+ export { type CheckDrill, type CheckDrillContext, type CockpitArgs, type CockpitConfig, DecisionBlock, type DecisionHooks, type DecisionTarget, DrillView, IssueView, PrView, type PromptContext, type PromptForms, type TabSearch, type TabSpec, checkDrillFor, computeHealth, decisionHooks, defineCockpit, detailFor, hasPatterns, parseArgs, registerCheckDrills, registerDecisionHooks, registerPrompts, signalPath, withRetry };
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { useActionMenu, buildActions, AiLauncher, seedPromptFor, CopyPromptNotice, DrillView, ActionMenu, resolveRepoPath, openInTab } from './chunk-PP3BVSMA.js';
2
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, DrillView, HealthPanel, MIN_SIDEBAR_COLS, PIN_MARK, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, bandOf, budgetNotice, buildActions, buildCheckoutCmd, buildInboxQueries, buildInboxQuery, checkoutDirs, clipboard, computeHealth, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, impliedLabels, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, labelPriority, layoutGHItems, matchesFilter, maxViewStart, mergeInboxData, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, registerPrompts, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signalPath, signatureOf, sortByRecency, sortItems, stripGlyph, stripLayout, stripSignature, toCiStatusState, toGHItem, trailingColumnsOf, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withRetry, withoutItem, workCount, writeCache } from './chunk-PP3BVSMA.js';
1
+ import { useActionMenu, buildActions, AiLauncher, seedPromptFor, CopyPromptNotice, DrillView, ActionMenu, resolveRepoPath, openInTab } from './chunk-B3FS2IVR.js';
2
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, DrillView, HealthPanel, MIN_SIDEBAR_COLS, NEEDS_YOU_SECTION, PIN_MARK, PrRow, SIDEBAR_COLS, SidePanel, StatusStripLine, ageLabel, bandOf, budgetNotice, buildActions, buildCheckoutCmd, buildInboxQueries, buildInboxQuery, checkoutDirs, clipboard, computeHealth, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, impliedLabels, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, labelPriority, layoutGHItems, matchesFilter, maxViewStart, mergeInboxData, moveCursor, numberColumnsOf, openInTab, parsePatterns, profileOf, railCapacity, railHeights, railRowsOf, railWidth, railsOf, readCache, registerPrompts, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, sectionShedOf, signalPath, signatureOf, sortByRecency, sortItems, stripGlyph, stripLayout, stripSignature, toCiStatusState, toGHItem, trailingColumnsOf, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withRetry, withoutItem, workCount, writeCache } from './chunk-B3FS2IVR.js';
3
3
  import { parsePatterns, HealthPanel, CommentsPanel, inboxConfig } from '@kud/gh-ink';
4
+ import { Box, Text, useStdin, useInput } from 'ink';
5
+ import { colors, useTabs, Tabs, TextInput, useListCursor, ScrollView } from '@kud/ink-ui';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
7
  import { $ } from 'zx';
8
+ import { spawnSync } from 'child_process';
5
9
  import React3, { useState, useRef, useEffect } from 'react';
6
- import { useInput, Box, Text } from 'ink';
7
- import { useTabs, colors, Tabs, useListCursor, ScrollView } from '@kud/ink-ui';
8
- import { fetchHealth, fetchPrComments, fetchDefaultBranch } from '@kud/gh';
10
+ import { mergePr, fetchHealth, fetchPrComments, fetchDefaultBranch } from '@kud/gh';
9
11
  import { sizePartsOf, sizeOf, filesOf } from '@kud/gh-workflow';
10
- import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
11
12
  import { readFileSync, mkdirSync, writeFileSync } from 'fs';
12
13
  import { homedir } from 'os';
13
14
  import { join } from 'path';
@@ -53,6 +54,17 @@ var registerCheckDrills = (drills) => {
53
54
  registered = drills;
54
55
  };
55
56
  var checkDrillFor = (url) => registered.find((d) => d.match(url)) ?? null;
57
+
58
+ // src/decision.ts
59
+ var registered2 = {};
60
+ var registerDecisionHooks = (hooks) => {
61
+ registered2 = hooks;
62
+ };
63
+ var decisionHooks = () => registered2;
64
+ var DecisionBlock = ({ needsYou }) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
65
+ /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: true, children: "Decision" }),
66
+ needsYou.decision ? /* @__PURE__ */ jsx(Text, { children: needsYou.decision }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Nothing to decide: ready to merge." })
67
+ ] });
56
68
  var fetchComments = async (repo, number, kind = "pr") => {
57
69
  if (kind === "issue") return fetchIssueConversation(repo, number);
58
70
  const [owner, name] = repo.split("/");
@@ -375,6 +387,11 @@ var PrView = ({
375
387
  const [files, setFiles] = useState(false);
376
388
  const [copy, setCopy] = useState(false);
377
389
  const [replying, setReplying] = useState(false);
390
+ const [answering, setAnswering] = useState(false);
391
+ const [confirm, setConfirm] = useState(null);
392
+ const [note, setNote] = useState(null);
393
+ const [paging, setPaging] = useState(false);
394
+ const { setRawMode } = useStdin();
378
395
  const menu = useActionMenu();
379
396
  const comments = useCachedResource(
380
397
  `pr-comments-${item.repo}-${item.number}`,
@@ -388,7 +405,7 @@ var PrView = ({
388
405
  { value: "health", label: "Health" },
389
406
  { value: "conversation", label: conversationLabel }
390
407
  ];
391
- const inputActive = log === null && !ai && !files && !replying && !copy;
408
+ const inputActive = log === null && !ai && !files && !replying && !copy && !answering;
392
409
  const menuOpen = menu.actions !== null;
393
410
  const { active, setActive } = useTabs(tabItems, {
394
411
  initial: defaultTab ?? "health",
@@ -421,6 +438,8 @@ var PrView = ({
421
438
  ].filter((c) => !!c.text);
422
439
  const aiPeel = useRef(null);
423
440
  const peel = () => {
441
+ if (answering) return setAnswering(false), true;
442
+ if (confirm !== null) return setConfirm(null), true;
424
443
  if (menu.actions !== null) return menu.close(), true;
425
444
  if (replying) return true;
426
445
  if (log !== null) return setLog(null), true;
@@ -434,8 +453,8 @@ var PrView = ({
434
453
  return () => registerPeel?.(null);
435
454
  });
436
455
  useEffect(() => {
437
- onTyping?.(replying);
438
- }, [replying, onTyping]);
456
+ onTyping?.(replying || answering);
457
+ }, [replying, answering, onTyping]);
439
458
  const checkLabel = (c) => c.workflowName ? `${c.workflowName} / ${c.context ?? c.name}` : c.context ?? c.name ?? "";
440
459
  const onOpenCheck = (c) => {
441
460
  const url = c.detailsUrl ?? c.targetUrl ?? "";
@@ -445,9 +464,118 @@ var PrView = ({
445
464
  else if (url) $`open ${url}`.catch(() => {
446
465
  });
447
466
  };
467
+ const doMerge = async () => {
468
+ setNote("\u22EF merging\u2026");
469
+ try {
470
+ await mergePr(item.repo, item.number);
471
+ setNote(null);
472
+ if (onMerged) onMerged(item);
473
+ else onRefresh?.();
474
+ } catch (e) {
475
+ setNote(`\u2717 merge failed: ${e.message}`);
476
+ }
477
+ };
478
+ const doClose = async () => {
479
+ setNote("\u22EF closing\u2026");
480
+ try {
481
+ await $`gh pr close ${item.number} --repo ${item.repo}`.quiet();
482
+ setNote(null);
483
+ if (onRemove) onRemove(item);
484
+ else onBack();
485
+ } catch (e) {
486
+ setNote(`\u2717 close failed: ${e.message}`);
487
+ }
488
+ };
489
+ const markReady = async () => {
490
+ if (item.health !== "draft") {
491
+ setNote("Not a draft \u2014 nothing to mark ready");
492
+ return;
493
+ }
494
+ setNote("\u22EF marking ready\u2026");
495
+ try {
496
+ await $`gh pr ready ${item.number} --repo ${item.repo}`.quiet();
497
+ setNote(null);
498
+ onRefresh?.();
499
+ } catch (e) {
500
+ setNote(`\u2717 ready failed: ${e.message}`);
501
+ }
502
+ };
503
+ const submitAnswer = async (body) => {
504
+ const text = body.trim();
505
+ setAnswering(false);
506
+ if (!text) return;
507
+ setNote("Posting\u2026");
508
+ try {
509
+ await $`gh pr comment ${item.number} --repo ${item.repo} --body ${text}`.quiet();
510
+ setNote("\u2713 Answered");
511
+ await decisionHooks().answered?.({
512
+ kind: "pr",
513
+ repo: item.repo,
514
+ number: item.number,
515
+ url: item.url,
516
+ labels: item.labels
517
+ });
518
+ } catch (e) {
519
+ setNote(`\u2717 ${e.message}`);
520
+ }
521
+ };
522
+ useEffect(() => {
523
+ if (!paging) return;
524
+ const timer = setTimeout(() => {
525
+ setRawMode(false);
526
+ spawnSync(
527
+ "sh",
528
+ [
529
+ "-c",
530
+ 'gh pr diff "$0" --repo "$1" --color=always | ${PAGER:-less -R}',
531
+ String(item.number),
532
+ item.repo
533
+ ],
534
+ { stdio: "inherit" }
535
+ );
536
+ process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
537
+ setRawMode(true);
538
+ setNote(null);
539
+ setPaging(false);
540
+ }, 50);
541
+ return () => clearTimeout(timer);
542
+ }, [paging]);
448
543
  useInput(
449
544
  (input, key) => {
450
545
  if (menu.handleKey(key)) return;
546
+ if (item.needsYou) {
547
+ if (confirm === "merge") {
548
+ if (input === "y" || input === "Y") void doMerge();
549
+ setConfirm(null);
550
+ return;
551
+ }
552
+ if (confirm === "close") {
553
+ if (input === "y" || input === "Y") void doClose();
554
+ setConfirm(null);
555
+ return;
556
+ }
557
+ if (input === "c") {
558
+ setAnswering(true);
559
+ return;
560
+ }
561
+ if (input === "d") {
562
+ setNote("\u22EF opening the diff in your pager\u2026");
563
+ setPaging(true);
564
+ return;
565
+ }
566
+ if (input === "X") {
567
+ setConfirm("close");
568
+ return;
569
+ }
570
+ if (input === "m" && tab === "conversation") {
571
+ setConfirm("merge");
572
+ return;
573
+ }
574
+ if (input === "R" && tab === "health") {
575
+ void markReady();
576
+ return;
577
+ }
578
+ }
451
579
  if (input === "M") {
452
580
  menu.open([
453
581
  ...buildActions(
@@ -542,11 +670,23 @@ var PrView = ({
542
670
  }
543
671
  );
544
672
  }
673
+ const decisionHints = !item.needsYou ? [] : tab === "health" ? [
674
+ ["c", "answer"],
675
+ ["d", "diff"],
676
+ ["X", "close"],
677
+ ["R", "ready"]
678
+ ] : [
679
+ ["c", "answer"],
680
+ ["d", "diff"],
681
+ ["m", "merge"],
682
+ ["X", "close"]
683
+ ];
545
684
  const hints = tab === "health" ? [
546
685
  ["\u2191\u2193", "nav"],
547
686
  ["\u21B5/l", "log"],
548
687
  ["r", "retrigger"],
549
688
  ["m", "merge"],
689
+ ...decisionHints,
550
690
  ["a", "AI"],
551
691
  ["y", "copy prompt"],
552
692
  ["e", "files"],
@@ -559,6 +699,7 @@ var PrView = ({
559
699
  ["x", "resolve"],
560
700
  ["r", "reply"],
561
701
  ["R", "show resolved"],
702
+ ...decisionHints,
562
703
  ["M", "actions"],
563
704
  ["a", "AI"],
564
705
  ["y", "copy prompt"],
@@ -583,8 +724,25 @@ var PrView = ({
583
724
  /* @__PURE__ */ jsx(Text, { color: colors.error, children: sizeParts.removed })
584
725
  ] }) : key === "files" || key === "base" ? /* @__PURE__ */ jsx(Text, { children: text }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: text })
585
726
  ] }, key)) }) : null,
727
+ item.needsYou ? /* @__PURE__ */ jsx(DecisionBlock, { needsYou: item.needsYou }) : null,
586
728
  /* @__PURE__ */ jsx(Box, { marginBottom: 1, marginTop: 1, children: /* @__PURE__ */ jsx(Tabs, { active: tab, items: tabItems }) }),
587
- menu.actions ? /* @__PURE__ */ jsx(ActionMenu, { item, actions: menu.actions, cursor: menu.cursor }) : tab === "health" ? /* @__PURE__ */ jsx(
729
+ confirm === "merge" ? /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.warning, children: [
730
+ ` Merge #${item.number}? `,
731
+ /* @__PURE__ */ jsx(Text, { color: colors.success, children: "y" }),
732
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " confirm \xB7 any other key cancels" })
733
+ ] }) }) : confirm === "close" ? /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.warning, children: [
734
+ ` Close #${item.number}? `,
735
+ /* @__PURE__ */ jsx(Text, { color: colors.success, children: "y" }),
736
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " confirm \xB7 any other key cancels" })
737
+ ] }) }) : note ? /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: colors.warning, bold: true, children: " " + note }) }) : null,
738
+ answering ? /* @__PURE__ */ jsx(
739
+ TextInput,
740
+ {
741
+ placeholder: "Answer\u2026 (\u21B5 send \xB7 esc cancel)",
742
+ onSubmit: (value) => void submitAnswer(value),
743
+ onCancel: () => setAnswering(false)
744
+ }
745
+ ) : menu.actions ? /* @__PURE__ */ jsx(ActionMenu, { item, actions: menu.actions, cursor: menu.cursor }) : tab === "health" ? /* @__PURE__ */ jsx(
588
746
  HealthPanel,
589
747
  {
590
748
  repo: item.repo,
@@ -615,12 +773,17 @@ var IssueView = ({
615
773
  item,
616
774
  login,
617
775
  onBack,
776
+ onRefresh,
777
+ onRemove,
618
778
  registerPeel,
619
779
  onTyping
620
780
  }) => {
621
781
  const [replying, setReplying] = useState(false);
622
782
  const [ai, setAi] = useState(false);
623
783
  const [copy, setCopy] = useState(false);
784
+ const [answering, setAnswering] = useState(false);
785
+ const [confirmClose, setConfirmClose] = useState(false);
786
+ const [note, setNote] = useState(null);
624
787
  const comments = useCachedResource(
625
788
  `issue-comments-${item.repo}-${item.number}`,
626
789
  () => fetchComments(item.repo, item.number, "issue")
@@ -630,6 +793,8 @@ var IssueView = ({
630
793
  ];
631
794
  const aiPeel = useRef(null);
632
795
  const peel = () => {
796
+ if (answering) return setAnswering(false), true;
797
+ if (confirmClose) return setConfirmClose(false), true;
633
798
  if (replying) return true;
634
799
  if (ai) return aiPeel.current?.() ?? (setAi(false), true);
635
800
  if (copy) return setCopy(false), true;
@@ -640,16 +805,61 @@ var IssueView = ({
640
805
  return () => registerPeel?.(null);
641
806
  });
642
807
  useEffect(() => {
643
- onTyping?.(replying);
644
- }, [replying, onTyping]);
808
+ onTyping?.(replying || answering);
809
+ }, [replying, answering, onTyping]);
810
+ const doClose = async () => {
811
+ setNote("\u22EF closing\u2026");
812
+ try {
813
+ await $`gh issue close ${item.number} --repo ${item.repo}`.quiet();
814
+ setNote(null);
815
+ if (onRemove) onRemove();
816
+ else onBack();
817
+ } catch (e) {
818
+ setNote(`\u2717 close failed: ${e.message}`);
819
+ }
820
+ };
821
+ const submitAnswer = async (body) => {
822
+ const text = body.trim();
823
+ setAnswering(false);
824
+ if (!text) return;
825
+ setNote("Posting\u2026");
826
+ try {
827
+ await $`gh issue comment ${item.number} --repo ${item.repo} --body ${text}`.quiet();
828
+ setNote("\u2713 Answered");
829
+ await decisionHooks().answered?.({
830
+ kind: "issue",
831
+ repo: item.repo,
832
+ number: item.number,
833
+ url: item.url,
834
+ labels: item.labels
835
+ });
836
+ } catch (e) {
837
+ setNote(`\u2717 ${e.message}`);
838
+ }
839
+ };
645
840
  useInput(
646
841
  (input, key) => {
842
+ if (item.needsYou) {
843
+ if (confirmClose) {
844
+ if (input === "y" || input === "Y") void doClose();
845
+ setConfirmClose(false);
846
+ return;
847
+ }
848
+ if (input === "c") {
849
+ setAnswering(true);
850
+ return;
851
+ }
852
+ if (input === "X") {
853
+ setConfirmClose(true);
854
+ return;
855
+ }
856
+ }
647
857
  if (input === "o") $`open ${item.url}`.catch(() => {
648
858
  });
649
859
  if (input === "a") setAi(true);
650
860
  if (input === "y") setCopy(true);
651
861
  },
652
- { isActive: !replying && !ai && !copy }
862
+ { isActive: !replying && !ai && !copy && !answering }
653
863
  );
654
864
  if (ai)
655
865
  return /* @__PURE__ */ jsx(
@@ -677,14 +887,31 @@ var IssueView = ({
677
887
  subtitle: item.title,
678
888
  hints: [
679
889
  ["\u2191\u2193", "scroll"],
890
+ ...item.needsYou ? [
891
+ ["c", "answer"],
892
+ ["X", "close"]
893
+ ] : [],
680
894
  ["a", "AI"],
681
895
  ["y", "copy prompt"],
682
896
  ["o", "open in browser"],
683
897
  ["esc", "back"]
684
898
  ],
685
899
  children: [
900
+ item.needsYou ? /* @__PURE__ */ jsx(DecisionBlock, { needsYou: item.needsYou }) : null,
901
+ confirmClose ? /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.warning, children: [
902
+ ` Close #${item.number}? `,
903
+ /* @__PURE__ */ jsx(Text, { color: colors.success, children: "y" }),
904
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " confirm \xB7 any other key cancels" })
905
+ ] }) }) : note ? /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: colors.warning, bold: true, children: " " + note }) }) : null,
686
906
  /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Tabs, { active: "conversation", items: tabItems }) }),
687
- /* @__PURE__ */ jsx(
907
+ answering ? /* @__PURE__ */ jsx(
908
+ TextInput,
909
+ {
910
+ placeholder: "Answer\u2026 (\u21B5 send \xB7 esc cancel)",
911
+ onSubmit: (value) => void submitAnswer(value),
912
+ onCancel: () => setAnswering(false)
913
+ }
914
+ ) : /* @__PURE__ */ jsx(
688
915
  CommentsPanel,
689
916
  {
690
917
  repo: item.repo,
@@ -713,19 +940,22 @@ var detailFor = (ctx) => ctx.kind === "pr" ? /* @__PURE__ */ jsx(
713
940
  onTyping: ctx.onTyping
714
941
  }
715
942
  ) : (
716
- // No refresh/remove/merged: IssueView has no action menu to hang them off,
717
- // so passing them would type-check into a handler nothing ever calls. They
718
- // belong here the day it grows the `M` menu PrView has.
943
+ // Refresh/remove ride along for the close key (`X`): `onRemove` drops the
944
+ // row the way PrView's does, closing over the full item here because
945
+ // IssueView only holds its subset and reports removal without handing a
946
+ // row back. `onMerged` stays absent — an issue never merges.
719
947
  /* @__PURE__ */ jsx(
720
948
  IssueView,
721
949
  {
722
950
  item: ctx.item,
723
951
  login: ctx.login,
724
952
  onBack: ctx.onBack,
953
+ onRefresh: ctx.onRefresh,
954
+ onRemove: () => ctx.onRemove(ctx.item),
725
955
  registerPeel: ctx.registerPeel,
726
956
  onTyping: ctx.onTyping
727
957
  }
728
958
  )
729
959
  );
730
960
 
731
- export { IssueView, PrView, checkDrillFor, defineCockpit, detailFor, hasPatterns, parseArgs, registerCheckDrills };
961
+ export { DecisionBlock, IssueView, PrView, checkDrillFor, decisionHooks, defineCockpit, detailFor, hasPatterns, parseArgs, registerCheckDrills, registerDecisionHooks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-cockpit",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "A configurable GitHub cockpit for the terminal — your PRs, reviews and issues in one Ink TUI, grouped by whose move it is.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,11 +53,11 @@
53
53
  "react": ">=19"
54
54
  },
55
55
  "dependencies": {
56
- "@kud/gh": "0.18.0",
57
- "@kud/gh-ink": "0.62.1",
56
+ "@kud/gh": "0.19.0",
57
+ "@kud/gh-ink": "0.64.0",
58
58
  "@kud/ink-ui": "0.31.0",
59
59
  "zx": "8.8.5",
60
- "@kud/gh-workflow": "0.13.1"
60
+ "@kud/gh-workflow": "0.14.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "22.20.1",