clairo 1.0.8 → 1.1.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.
Files changed (2) hide show
  1. package/dist/cli.js +390 -275
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,11 +4,11 @@
4
4
  import meow from "meow";
5
5
 
6
6
  // src/app.tsx
7
- import { useCallback as useCallback10, useMemo as useMemo2, useState as useState17 } from "react";
7
+ import { useCallback as useCallback9, useMemo as useMemo2, useState as useState16 } from "react";
8
8
  import { Box as Box16, useApp, useInput as useInput13 } from "ink";
9
9
 
10
10
  // src/components/github/GitHubView.tsx
11
- import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef5, useState as useState12 } from "react";
11
+ import { useCallback as useCallback8, useEffect as useEffect8, useRef as useRef5, useState as useState11 } from "react";
12
12
  import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
13
13
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
14
14
 
@@ -173,6 +173,37 @@ function findRemoteWithBranch(branch) {
173
173
  import { exec } from "child_process";
174
174
  import { promisify } from "util";
175
175
  var execAsync = promisify(exec);
176
+ function resolveCheckStatus(check) {
177
+ const conclusion = check.conclusion ?? check.state;
178
+ if (conclusion === "SUCCESS" || check.status === "COMPLETED") return "success";
179
+ if (conclusion === "FAILURE" || conclusion === "ERROR") return "failure";
180
+ if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "skipped";
181
+ if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
182
+ return "pending";
183
+ return "pending";
184
+ }
185
+ var CHECK_COLORS = {
186
+ success: "green",
187
+ failure: "red",
188
+ pending: "yellow",
189
+ skipped: "gray"
190
+ };
191
+ var CHECK_ICONS = { success: "\u2713", failure: "\u2717", pending: "\u25CF", skipped: "\u25CB" };
192
+ var CHECK_SORT_ORDER = { failure: 0, pending: 1, skipped: 2, success: 3 };
193
+ function resolveReviewDisplay(reviewDecision) {
194
+ const status = reviewDecision ?? "PENDING";
195
+ if (status === "APPROVED") return { text: status, color: "green" };
196
+ if (status === "CHANGES_REQUESTED") return { text: status, color: "red" };
197
+ return { text: status, color: "yellow" };
198
+ }
199
+ function resolveMergeDisplay(pr) {
200
+ if (!pr) return { text: "UNKNOWN", color: "yellow" };
201
+ if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
202
+ if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
203
+ if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
204
+ if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
205
+ return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
206
+ }
176
207
  async function isGhInstalled() {
177
208
  try {
178
209
  await execAsync("gh --version");
@@ -393,6 +424,9 @@ function useGitRepo() {
393
424
  };
394
425
  }
395
426
 
427
+ // src/hooks/github/usePullRequests.ts
428
+ import { useCallback as useCallback3, useState as useState4 } from "react";
429
+
396
430
  // src/hooks/github/usePRPolling.ts
397
431
  import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState3 } from "react";
398
432
  function usePRPolling() {
@@ -455,11 +489,11 @@ function usePRPolling() {
455
489
  }
456
490
 
457
491
  // src/hooks/github/usePullRequests.ts
458
- import { useCallback as useCallback3, useState as useState4 } from "react";
459
492
  function usePullRequests() {
460
493
  const [prs, setPrs] = useState4([]);
461
494
  const [selectedPR, setSelectedPR] = useState4(null);
462
495
  const [prDetails, setPrDetails] = useState4(null);
496
+ const polling = usePRPolling();
463
497
  const [loading, setLoading] = useState4({
464
498
  prs: false,
465
499
  details: false
@@ -527,6 +561,26 @@ function usePullRequests() {
527
561
  const setError = useCallback3((key, message) => {
528
562
  setErrors((prev) => ({ ...prev, [key]: message }));
529
563
  }, []);
564
+ const pollForNewPR = useCallback3(
565
+ (options) => {
566
+ const existingPRNumbers = prs.map((pr) => pr.number);
567
+ polling.startPolling({
568
+ branch: options.branch,
569
+ repoSlug: options.repoSlug,
570
+ existingPRNumbers,
571
+ onPRsUpdated: (updatedPrs) => {
572
+ setPrs(updatedPrs);
573
+ },
574
+ onNewPR: (newPR) => {
575
+ var _a;
576
+ setSelectedPR(newPR);
577
+ refreshDetails(newPR, options.repoSlug);
578
+ (_a = options.onNewPR) == null ? void 0 : _a.call(options, newPR);
579
+ }
580
+ });
581
+ },
582
+ [prs, polling.startPolling, refreshDetails]
583
+ );
530
584
  return {
531
585
  prs,
532
586
  selectedPR,
@@ -538,9 +592,9 @@ function usePullRequests() {
538
592
  loading,
539
593
  errors,
540
594
  setError,
541
- // Expose setters for cases where external code needs to update state directly
542
- setPrs,
543
- setSelectedPR
595
+ pollForNewPR,
596
+ stopPolling: polling.stopPolling,
597
+ isPolling: polling.isPolling
544
598
  };
545
599
  }
546
600
 
@@ -573,6 +627,26 @@ function extractTicketKey(text) {
573
627
  }
574
628
 
575
629
  // src/lib/jira/config.ts
630
+ function getExistingJiraConfigs(excludeRepoPath) {
631
+ const config = loadConfig();
632
+ const repos = config.repositories ?? {};
633
+ const configs = [];
634
+ const seen = /* @__PURE__ */ new Set();
635
+ for (const [repoPath, repoConfig] of Object.entries(repos)) {
636
+ if (repoPath === excludeRepoPath) continue;
637
+ if (!repoConfig.jiraSiteUrl || !repoConfig.jiraEmail || !repoConfig.jiraApiToken) continue;
638
+ const key = `${repoConfig.jiraSiteUrl}|${repoConfig.jiraEmail}`;
639
+ if (seen.has(key)) continue;
640
+ seen.add(key);
641
+ configs.push({
642
+ repoPath,
643
+ siteUrl: repoConfig.jiraSiteUrl,
644
+ email: repoConfig.jiraEmail,
645
+ apiToken: repoConfig.jiraApiToken
646
+ });
647
+ }
648
+ return configs;
649
+ }
576
650
  function isJiraConfigured(repoPath) {
577
651
  const config = getRepoConfig(repoPath);
578
652
  return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
@@ -594,6 +668,13 @@ function getJiraCredentials(repoPath) {
594
668
  function setJiraCredentials(repoPath, email, apiToken) {
595
669
  updateRepoConfig(repoPath, { jiraEmail: email, jiraApiToken: apiToken });
596
670
  }
671
+ function clearJiraConfig(repoPath) {
672
+ updateRepoConfig(repoPath, {
673
+ jiraSiteUrl: void 0,
674
+ jiraEmail: void 0,
675
+ jiraApiToken: void 0
676
+ });
677
+ }
597
678
  function getLinkedTickets(repoPath, branch) {
598
679
  var _a;
599
680
  const config = getRepoConfig(repoPath);
@@ -1015,52 +1096,14 @@ function renderInlineToString(tokens) {
1015
1096
 
1016
1097
  // src/components/github/PRDetailsBox.tsx
1017
1098
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1018
- function getCheckColor(check) {
1019
- const conclusion = check.conclusion ?? check.state;
1020
- if (conclusion === "SUCCESS") return "green";
1021
- if (conclusion === "FAILURE" || conclusion === "ERROR") return "red";
1022
- if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "gray";
1023
- if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
1024
- return "yellow";
1025
- if (check.status === "COMPLETED") return "green";
1026
- return void 0;
1027
- }
1028
- function getCheckIcon(check) {
1029
- const conclusion = check.conclusion ?? check.state;
1030
- if (conclusion === "SUCCESS") return "\u2713";
1031
- if (conclusion === "FAILURE" || conclusion === "ERROR") return "\u2717";
1032
- if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "\u25CB";
1033
- if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
1034
- return "\u25CF";
1035
- if (check.status === "COMPLETED") return "\u2713";
1036
- return "?";
1037
- }
1038
- function getCheckSortOrder(check) {
1039
- const conclusion = check.conclusion ?? check.state;
1040
- if (conclusion === "FAILURE" || conclusion === "ERROR") return 0;
1041
- if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
1042
- return 1;
1043
- if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return 2;
1044
- if (conclusion === "SUCCESS" || check.status === "COMPLETED") return 3;
1045
- return 4;
1046
- }
1047
- function PRDetailsBox({ pr, loading, error, isFocused }) {
1099
+ function PRDetailsBox({ pr, loading, error, isActive }) {
1048
1100
  var _a, _b, _c, _d, _e, _f, _g;
1049
1101
  const scrollRef = useRef2(null);
1050
1102
  const title = "[3] PR Details";
1051
- const borderColor = isFocused ? "yellow" : void 0;
1103
+ const borderColor = isActive ? "yellow" : void 0;
1052
1104
  const displayTitle = pr ? `${title} - #${pr.number}` : title;
1053
- const reviewStatus = (pr == null ? void 0 : pr.reviewDecision) ?? "PENDING";
1054
- const reviewColor = reviewStatus === "APPROVED" ? "green" : reviewStatus === "CHANGES_REQUESTED" ? "red" : "yellow";
1055
- const getMergeDisplay = () => {
1056
- if (!pr) return { text: "UNKNOWN", color: "yellow" };
1057
- if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
1058
- if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
1059
- if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
1060
- if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
1061
- return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
1062
- };
1063
- const mergeDisplay = getMergeDisplay();
1105
+ const reviewDisplay = resolveReviewDisplay((pr == null ? void 0 : pr.reviewDecision) ?? null);
1106
+ const mergeDisplay = resolveMergeDisplay(pr);
1064
1107
  useInput(
1065
1108
  (input, key) => {
1066
1109
  var _a2, _b2;
@@ -1075,7 +1118,7 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
1075
1118
  });
1076
1119
  }
1077
1120
  },
1078
- { isActive: isFocused }
1121
+ { isActive }
1079
1122
  );
1080
1123
  const { stdout } = useStdout();
1081
1124
  const terminalWidth = (stdout == null ? void 0 : stdout.columns) ?? 80;
@@ -1110,7 +1153,7 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
1110
1153
  ] }),
1111
1154
  /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
1112
1155
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Review: " }),
1113
- /* @__PURE__ */ jsx2(Text2, { color: reviewColor, children: reviewStatus }),
1156
+ /* @__PURE__ */ jsx2(Text2, { color: reviewDisplay.color, children: reviewDisplay.text }),
1114
1157
  /* @__PURE__ */ jsx2(Text2, { children: " | " }),
1115
1158
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Status: " }),
1116
1159
  /* @__PURE__ */ jsx2(Text2, { color: mergeDisplay.color, children: mergeDisplay.text })
@@ -1147,12 +1190,13 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
1147
1190
  }
1148
1191
  return acc;
1149
1192
  }, /* @__PURE__ */ new Map()).values()) ?? []
1150
- ).sort((a, b) => getCheckSortOrder(a) - getCheckSortOrder(b)).map((check, idx) => {
1193
+ ).sort((a, b) => CHECK_SORT_ORDER[resolveCheckStatus(a)] - CHECK_SORT_ORDER[resolveCheckStatus(b)]).map((check, idx) => {
1151
1194
  const jobName = check.name ?? check.context;
1152
1195
  const displayName = check.workflowName ? `${check.workflowName} / ${jobName}` : jobName;
1153
- return /* @__PURE__ */ jsxs2(Text2, { color: getCheckColor(check), children: [
1196
+ const status = resolveCheckStatus(check);
1197
+ return /* @__PURE__ */ jsxs2(Text2, { color: CHECK_COLORS[status], children: [
1154
1198
  " ",
1155
- getCheckIcon(check),
1199
+ CHECK_ICONS[status],
1156
1200
  " ",
1157
1201
  displayName
1158
1202
  ] }, idx);
@@ -1171,9 +1215,9 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
1171
1215
 
1172
1216
  // src/components/github/PullRequestsBox.tsx
1173
1217
  import open2 from "open";
1174
- import { useEffect as useEffect7, useState as useState10 } from "react";
1218
+ import { useState as useState10 } from "react";
1175
1219
  import { TitledBox } from "@mishieck/ink-titled-box";
1176
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
1220
+ import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
1177
1221
  import { ScrollView as ScrollView2 } from "ink-scroll-view";
1178
1222
 
1179
1223
  // src/hooks/jira/useJiraTickets.ts
@@ -1431,25 +1475,8 @@ function useModal() {
1431
1475
  }
1432
1476
 
1433
1477
  // src/hooks/useListNavigation.ts
1434
- import { useCallback as useCallback7, useState as useState8 } from "react";
1435
- function useListNavigation(length) {
1436
- const [index, setIndex] = useState8(0);
1437
- const prev = useCallback7(() => {
1438
- setIndex((i) => Math.max(0, i - 1));
1439
- }, []);
1440
- const next = useCallback7(() => {
1441
- setIndex((i) => Math.min(length - 1, i + 1));
1442
- }, [length]);
1443
- const clampedIndex = Math.min(index, Math.max(0, length - 1));
1444
- const reset = useCallback7(() => setIndex(0), []);
1445
- return {
1446
- index: length === 0 ? 0 : clampedIndex,
1447
- prev,
1448
- next,
1449
- reset,
1450
- setIndex
1451
- };
1452
- }
1478
+ import { useEffect as useEffect6, useState as useState8 } from "react";
1479
+ import { useInput as useInput2 } from "ink";
1453
1480
 
1454
1481
  // src/hooks/useScrollToIndex.ts
1455
1482
  import { useEffect as useEffect5, useRef as useRef4 } from "react";
@@ -1471,8 +1498,53 @@ function useScrollToIndex(index) {
1471
1498
  return scrollRef;
1472
1499
  }
1473
1500
 
1501
+ // src/hooks/useListNavigation.ts
1502
+ function useListNavigation({
1503
+ items,
1504
+ totalItems,
1505
+ selectedIndex,
1506
+ onSelect,
1507
+ isActive
1508
+ }) {
1509
+ const navLength = totalItems ?? items.length;
1510
+ const [highlightedIndex, setHighlightedIndex] = useState8(0);
1511
+ const scrollRef = useScrollToIndex(highlightedIndex);
1512
+ useEffect6(() => {
1513
+ if (selectedIndex !== void 0 && selectedIndex >= 0) {
1514
+ setHighlightedIndex(selectedIndex);
1515
+ }
1516
+ }, [selectedIndex]);
1517
+ const prev = () => setHighlightedIndex((i) => Math.max(0, i - 1));
1518
+ const next = () => setHighlightedIndex((i) => Math.min(navLength - 1, i + 1));
1519
+ useInput2(
1520
+ (input, key) => {
1521
+ if (navLength === 0) return;
1522
+ if (key.upArrow || input === "k") {
1523
+ prev();
1524
+ }
1525
+ if (key.downArrow || input === "j") {
1526
+ next();
1527
+ }
1528
+ if (input === " " && onSelect) {
1529
+ onSelect(highlightedIndex);
1530
+ }
1531
+ },
1532
+ { isActive: isActive === true }
1533
+ );
1534
+ const clampedIndex = navLength === 0 ? 0 : Math.min(highlightedIndex, Math.max(0, navLength - 1));
1535
+ return {
1536
+ highlightedIndex: clampedIndex,
1537
+ index: clampedIndex,
1538
+ scrollRef,
1539
+ prev,
1540
+ next,
1541
+ reset: () => setHighlightedIndex(0),
1542
+ setIndex: setHighlightedIndex
1543
+ };
1544
+ }
1545
+
1474
1546
  // src/hooks/useRubberDuck.ts
1475
- import { useCallback as useCallback8, useEffect as useEffect6, useState as useState9 } from "react";
1547
+ import { useCallback as useCallback7, useEffect as useEffect7, useState as useState9 } from "react";
1476
1548
  var DUCK_MESSAGES = [
1477
1549
  "Quack.",
1478
1550
  "Quack quack quack.",
@@ -1510,18 +1582,18 @@ function useRubberDuck() {
1510
1582
  visible: false,
1511
1583
  message: DUCK_MESSAGES[0]
1512
1584
  });
1513
- const getRandomMessage = useCallback8(() => {
1585
+ const getRandomMessage = useCallback7(() => {
1514
1586
  const index = Math.floor(Math.random() * DUCK_MESSAGES.length);
1515
1587
  return DUCK_MESSAGES[index];
1516
1588
  }, []);
1517
- const toggleDuck = useCallback8(() => {
1589
+ const toggleDuck = useCallback7(() => {
1518
1590
  setState((prev) => ({
1519
1591
  ...prev,
1520
1592
  visible: !prev.visible,
1521
1593
  message: !prev.visible ? getRandomMessage() : prev.message
1522
1594
  }));
1523
1595
  }, [getRandomMessage]);
1524
- const quack = useCallback8(() => {
1596
+ const quack = useCallback7(() => {
1525
1597
  if (state.visible) {
1526
1598
  setState((prev) => ({
1527
1599
  ...prev,
@@ -1529,11 +1601,11 @@ function useRubberDuck() {
1529
1601
  }));
1530
1602
  }
1531
1603
  }, [state.visible, getRandomMessage]);
1532
- const getReactionMessage = useCallback8((event) => {
1604
+ const getReactionMessage = useCallback7((event) => {
1533
1605
  const messages = REACTION_MESSAGES[event];
1534
1606
  return messages[Math.floor(Math.random() * messages.length)];
1535
1607
  }, []);
1536
- useEffect6(() => {
1608
+ useEffect7(() => {
1537
1609
  const unsubscribe = duckEvents.subscribe((event) => {
1538
1610
  setState((prev) => ({
1539
1611
  ...prev,
@@ -1583,32 +1655,25 @@ function PullRequestsBox({
1583
1655
  error,
1584
1656
  branch,
1585
1657
  repoSlug,
1586
- isFocused
1658
+ isActive
1587
1659
  }) {
1588
- const [highlightedIndex, setHighlightedIndex] = useState10(0);
1589
1660
  const [copied, setCopied] = useState10(false);
1590
- const scrollRef = useScrollToIndex(highlightedIndex);
1591
- const totalItems = prs.length + 1;
1592
- useEffect7(() => {
1593
- const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
1594
- if (idx >= 0) setHighlightedIndex(idx);
1595
- }, [selectedPR, prs]);
1596
- useInput2(
1597
- (input, key) => {
1598
- if (!isFocused) return;
1599
- if (key.upArrow || input === "k") {
1600
- setHighlightedIndex((prev) => Math.max(0, prev - 1));
1601
- }
1602
- if (key.downArrow || input === "j") {
1603
- setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
1604
- }
1605
- if (input === " ") {
1606
- if (highlightedIndex === prs.length) {
1607
- onCreatePR();
1608
- } else if (prs[highlightedIndex]) {
1609
- onSelect(prs[highlightedIndex]);
1610
- }
1661
+ const selectedIndex = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
1662
+ const { highlightedIndex, scrollRef } = useListNavigation({
1663
+ items: prs,
1664
+ totalItems: prs.length + 1,
1665
+ selectedIndex: selectedIndex >= 0 ? selectedIndex : void 0,
1666
+ onSelect: (index) => {
1667
+ if (index === prs.length) {
1668
+ onCreatePR();
1669
+ } else if (prs[index]) {
1670
+ onSelect(prs[index]);
1611
1671
  }
1672
+ },
1673
+ isActive
1674
+ });
1675
+ useInput3(
1676
+ (input) => {
1612
1677
  if (input === "y" && repoSlug && prs[highlightedIndex]) {
1613
1678
  const pr = prs[highlightedIndex];
1614
1679
  const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
@@ -1623,12 +1688,12 @@ function PullRequestsBox({
1623
1688
  });
1624
1689
  }
1625
1690
  },
1626
- { isActive: isFocused }
1691
+ { isActive }
1627
1692
  );
1628
1693
  const title = "[2] Pull Requests";
1629
1694
  const subtitle = branch ? ` (${branch})` : "";
1630
1695
  const copiedIndicator = copied ? " [Copied!]" : "";
1631
- const borderColor = isFocused ? "yellow" : void 0;
1696
+ const borderColor = isActive ? "yellow" : void 0;
1632
1697
  return /* @__PURE__ */ jsx3(
1633
1698
  TitledBox,
1634
1699
  {
@@ -1642,7 +1707,7 @@ function PullRequestsBox({
1642
1707
  !loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
1643
1708
  prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
1644
1709
  prs.map((pr, idx) => {
1645
- const isHighlighted = isFocused && idx === highlightedIndex;
1710
+ const isHighlighted = isActive && idx === highlightedIndex;
1646
1711
  const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
1647
1712
  const cursor = isHighlighted ? ">" : " ";
1648
1713
  const indicator = isSelected ? " *" : "";
@@ -1662,7 +1727,7 @@ function PullRequestsBox({
1662
1727
  ] }, pr.number);
1663
1728
  }),
1664
1729
  /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
1665
- isFocused && highlightedIndex === prs.length ? "> " : " ",
1730
+ isActive && highlightedIndex === prs.length ? "> " : " ",
1666
1731
  "+ Create new PR"
1667
1732
  ] }, "create")
1668
1733
  ] })
@@ -1672,41 +1737,26 @@ function PullRequestsBox({
1672
1737
  }
1673
1738
 
1674
1739
  // src/components/github/RemotesBox.tsx
1675
- import { useEffect as useEffect8, useState as useState11 } from "react";
1676
1740
  import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
1677
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
1741
+ import { Box as Box4, Text as Text4 } from "ink";
1678
1742
  import { ScrollView as ScrollView3 } from "ink-scroll-view";
1679
1743
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1680
- function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
1681
- const [highlightedIndex, setHighlightedIndex] = useState11(0);
1682
- const scrollRef = useScrollToIndex(highlightedIndex);
1683
- useEffect8(() => {
1684
- const idx = remotes.findIndex((r) => r.name === selectedRemote);
1685
- if (idx >= 0) setHighlightedIndex(idx);
1686
- }, [selectedRemote, remotes]);
1687
- useInput3(
1688
- (input, key) => {
1689
- if (!isFocused || remotes.length === 0) return;
1690
- if (key.upArrow || input === "k") {
1691
- setHighlightedIndex((prev) => Math.max(0, prev - 1));
1692
- }
1693
- if (key.downArrow || input === "j") {
1694
- setHighlightedIndex((prev) => Math.min(remotes.length - 1, prev + 1));
1695
- }
1696
- if (input === " ") {
1697
- onSelect(remotes[highlightedIndex].name);
1698
- }
1699
- },
1700
- { isActive: isFocused }
1701
- );
1744
+ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isActive }) {
1745
+ const selectedIndex = remotes.findIndex((r) => r.name === selectedRemote);
1746
+ const { highlightedIndex, scrollRef } = useListNavigation({
1747
+ items: remotes,
1748
+ selectedIndex: selectedIndex >= 0 ? selectedIndex : void 0,
1749
+ onSelect: (index) => onSelect(remotes[index].name),
1750
+ isActive
1751
+ });
1702
1752
  const title = "[1] Remotes";
1703
- const borderColor = isFocused ? "yellow" : void 0;
1753
+ const borderColor = isActive ? "yellow" : void 0;
1704
1754
  return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
1705
1755
  loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
1706
1756
  error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
1707
1757
  !loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
1708
1758
  !loading && !error && remotes.length > 0 && /* @__PURE__ */ jsx4(ScrollView3, { ref: scrollRef, children: remotes.map((remote, idx) => {
1709
- const isHighlighted = isFocused && idx === highlightedIndex;
1759
+ const isHighlighted = isActive && idx === highlightedIndex;
1710
1760
  const isSelected = remote.name === selectedRemote;
1711
1761
  const cursor = isHighlighted ? ">" : " ";
1712
1762
  const indicator = isSelected ? " *" : "";
@@ -1729,89 +1779,70 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
1729
1779
 
1730
1780
  // src/components/github/GitHubView.tsx
1731
1781
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1732
- function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1782
+ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
1733
1783
  const repo = useGitRepo();
1734
1784
  const pullRequests = usePullRequests();
1735
- const polling = usePRPolling();
1736
- const [focusedBox, setFocusedBox] = useState12("remotes");
1737
- const lastFetchedRef = useRef5(null);
1738
- useEffect9(() => {
1785
+ const [focusedBox, setFocusedBox] = useState11("remotes");
1786
+ useEffect8(() => {
1739
1787
  if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
1740
- const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
1741
- const last = lastFetchedRef.current;
1742
- if (last && last.branch === current.branch && last.repoSlug === current.repoSlug) return;
1743
- lastFetchedRef.current = current;
1744
1788
  pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
1745
1789
  }, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
1746
- useEffect9(() => {
1747
- if (isFocused) {
1790
+ useEffect8(() => {
1791
+ if (isActive) {
1748
1792
  repo.refreshBranch();
1749
1793
  }
1750
- }, [isFocused, repo.refreshBranch]);
1751
- useEffect9(() => {
1794
+ }, [isActive, repo.refreshBranch]);
1795
+ useEffect8(() => {
1752
1796
  onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
1753
1797
  }, [focusedBox, onFocusedBoxChange]);
1754
- const handleRemoteSelect = useCallback9(
1798
+ const handleRemoteSelect = useCallback8(
1755
1799
  (remoteName) => {
1756
1800
  repo.selectRemote(remoteName);
1757
- const remote = repo.remotes.find((r) => r.name === remoteName);
1758
- if (!remote || !repo.currentBranch) return;
1759
- const repoSlug = getRepoFromRemote(remote.url);
1760
- if (!repoSlug) return;
1761
- lastFetchedRef.current = { branch: repo.currentBranch, repoSlug };
1762
- pullRequests.fetchPRsAndDetails(repo.currentBranch, repoSlug);
1763
1801
  },
1764
- [repo.selectRemote, repo.remotes, repo.currentBranch, pullRequests.fetchPRsAndDetails]
1802
+ [repo.selectRemote]
1765
1803
  );
1766
- const handlePRSelect = useCallback9(
1804
+ const handlePRSelect = useCallback8(
1767
1805
  (pr) => {
1768
1806
  pullRequests.selectPR(pr, repo.currentRepoSlug);
1769
1807
  },
1770
1808
  [pullRequests.selectPR, repo.currentRepoSlug]
1771
1809
  );
1772
- const createPRContext = useRef5({ repo, pullRequests, onLogUpdated });
1773
- createPRContext.current = { repo, pullRequests, onLogUpdated };
1774
- const handleCreatePR = useCallback9(() => {
1775
- const { repo: repo2, pullRequests: pullRequests2 } = createPRContext.current;
1776
- if (!repo2.currentBranch) {
1777
- pullRequests2.setError("prs", "No branch detected");
1810
+ const onLogUpdatedRef = useRef5(onLogUpdated);
1811
+ onLogUpdatedRef.current = onLogUpdated;
1812
+ const handleCreatePR = useCallback8(() => {
1813
+ if (!repo.currentBranch) {
1814
+ pullRequests.setError("prs", "No branch detected");
1778
1815
  duckEvents.emit("error");
1779
1816
  return;
1780
1817
  }
1781
- const remoteResult = findRemoteWithBranch(repo2.currentBranch);
1818
+ const remoteResult = findRemoteWithBranch(repo.currentBranch);
1782
1819
  if (!remoteResult.success) {
1783
- pullRequests2.setError("prs", "Push your branch to a remote first");
1820
+ pullRequests.setError("prs", "Push your branch to a remote first");
1784
1821
  duckEvents.emit("error");
1785
1822
  return;
1786
1823
  }
1787
- openPRCreationPage(remoteResult.data.owner, repo2.currentBranch, (error) => {
1824
+ openPRCreationPage(remoteResult.data.owner, repo.currentBranch, (error) => {
1788
1825
  if (error) {
1789
- pullRequests2.setError("prs", `Failed to create PR: ${error.message}`);
1826
+ pullRequests.setError("prs", `Failed to create PR: ${error.message}`);
1790
1827
  duckEvents.emit("error");
1791
1828
  }
1792
1829
  });
1793
- if (!repo2.currentRepoSlug) return;
1794
- polling.startPolling({
1795
- branch: repo2.currentBranch,
1796
- repoSlug: repo2.currentRepoSlug,
1797
- existingPRNumbers: pullRequests2.prs.map((pr) => pr.number),
1798
- onPRsUpdated: (prs) => {
1799
- pullRequests2.setPrs(prs);
1800
- },
1830
+ if (!repo.currentRepoSlug) return;
1831
+ const repoPath = repo.repoPath;
1832
+ const branch = repo.currentBranch;
1833
+ const repoSlug = repo.currentRepoSlug;
1834
+ pullRequests.pollForNewPR({
1835
+ branch,
1836
+ repoSlug,
1801
1837
  onNewPR: (newPR) => {
1802
1838
  var _a;
1803
- const ctx = createPRContext.current;
1804
- const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
1839
+ const tickets = repoPath && branch ? getLinkedTickets(repoPath, branch).map((t) => t.key) : [];
1805
1840
  logPRCreated(newPR.number, newPR.title, tickets);
1806
1841
  duckEvents.emit("pr:opened");
1807
- (_a = ctx.onLogUpdated) == null ? void 0 : _a.call(ctx);
1808
- ctx.pullRequests.setSelectedPR(newPR);
1809
- if (ctx.repo.currentRepoSlug) {
1810
- ctx.pullRequests.refreshDetails(newPR, ctx.repo.currentRepoSlug);
1811
- }
1842
+ (_a = onLogUpdatedRef.current) == null ? void 0 : _a.call(onLogUpdatedRef);
1812
1843
  }
1813
1844
  });
1814
- }, [polling.startPolling]);
1845
+ }, [repo.currentBranch, repo.currentRepoSlug, repo.repoPath, pullRequests.setError, pullRequests.pollForNewPR]);
1815
1846
  useInput4(
1816
1847
  (input) => {
1817
1848
  if (input === "1") setFocusedBox("remotes");
@@ -1830,7 +1861,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1830
1861
  handleCreatePR();
1831
1862
  }
1832
1863
  },
1833
- { isActive: isFocused }
1864
+ { isActive }
1834
1865
  );
1835
1866
  if (repo.isRepo === false) {
1836
1867
  return /* @__PURE__ */ jsx5(TitledBox3, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
@@ -1844,7 +1875,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1844
1875
  onSelect: handleRemoteSelect,
1845
1876
  loading: repo.loading,
1846
1877
  error: repo.error,
1847
- isFocused: isFocused && focusedBox === "remotes"
1878
+ isActive: isActive && focusedBox === "remotes"
1848
1879
  }
1849
1880
  ),
1850
1881
  /* @__PURE__ */ jsx5(
@@ -1858,7 +1889,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1858
1889
  error: pullRequests.errors.prs,
1859
1890
  branch: repo.currentBranch,
1860
1891
  repoSlug: repo.currentRepoSlug,
1861
- isFocused: isFocused && focusedBox === "prs"
1892
+ isActive: isActive && focusedBox === "prs"
1862
1893
  }
1863
1894
  ),
1864
1895
  /* @__PURE__ */ jsx5(
@@ -1867,7 +1898,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1867
1898
  pr: pullRequests.prDetails,
1868
1899
  loading: pullRequests.loading.details,
1869
1900
  error: pullRequests.errors.details,
1870
- isFocused: isFocused && focusedBox === "details"
1901
+ isActive: isActive && focusedBox === "details"
1871
1902
  }
1872
1903
  )
1873
1904
  ] });
@@ -1875,10 +1906,10 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
1875
1906
 
1876
1907
  // src/components/jira/JiraView.tsx
1877
1908
  import open3 from "open";
1878
- import { useEffect as useEffect11, useRef as useRef6 } from "react";
1909
+ import { useEffect as useEffect10, useRef as useRef6 } from "react";
1879
1910
 
1880
1911
  // src/components/jira/LinkTicketModal.tsx
1881
- import { useState as useState13 } from "react";
1912
+ import { useState as useState12 } from "react";
1882
1913
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
1883
1914
 
1884
1915
  // src/components/ui/TextInput.tsx
@@ -1913,7 +1944,7 @@ function TextInput({ value, onChange, placeholder, isActive, mask }) {
1913
1944
  // src/components/jira/LinkTicketModal.tsx
1914
1945
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1915
1946
  function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
1916
- const [ticketInput, setTicketInput] = useState13("");
1947
+ const [ticketInput, setTicketInput] = useState12("");
1917
1948
  const canSubmit = ticketInput.trim().length > 0;
1918
1949
  useInput6(
1919
1950
  (_input, key) => {
@@ -1947,16 +1978,16 @@ import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
1947
1978
  import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
1948
1979
 
1949
1980
  // src/components/jira/ChangeStatusModal.tsx
1950
- import { useEffect as useEffect10, useState as useState14 } from "react";
1981
+ import { useEffect as useEffect9, useState as useState13 } from "react";
1951
1982
  import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
1952
1983
  import SelectInput from "ink-select-input";
1953
1984
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1954
1985
  function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
1955
- const [transitions, setTransitions] = useState14([]);
1956
- const [loading, setLoading] = useState14(true);
1957
- const [applying, setApplying] = useState14(false);
1958
- const [error, setError] = useState14(null);
1959
- useEffect10(() => {
1986
+ const [transitions, setTransitions] = useState13([]);
1987
+ const [loading, setLoading] = useState13(true);
1988
+ const [applying, setApplying] = useState13(false);
1989
+ const [error, setError] = useState13(null);
1990
+ useEffect9(() => {
1960
1991
  const fetchTransitions = async () => {
1961
1992
  const siteUrl = getJiraSiteUrl(repoPath);
1962
1993
  const creds = getJiraCredentials(repoPath);
@@ -2033,8 +2064,9 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
2033
2064
  }
2034
2065
 
2035
2066
  // src/components/jira/ConfigureJiraSiteModal.tsx
2036
- import { useState as useState15 } from "react";
2067
+ import { useState as useState14 } from "react";
2037
2068
  import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
2069
+ import { ScrollView as ScrollView4 } from "ink-scroll-view";
2038
2070
 
2039
2071
  // src/lib/editor.ts
2040
2072
  import { spawnSync as spawnSync2 } from "child_process";
@@ -2066,27 +2098,57 @@ function openInEditor(content, filename) {
2066
2098
 
2067
2099
  // src/components/jira/ConfigureJiraSiteModal.tsx
2068
2100
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2101
+ var MAX_VISIBLE_ITEMS = 4;
2069
2102
  function ConfigureJiraSiteModal({
2070
2103
  initialSiteUrl,
2071
2104
  initialEmail,
2105
+ existingConfigs = [],
2072
2106
  onSubmit,
2073
2107
  onCancel,
2074
2108
  loading,
2075
2109
  error
2076
2110
  }) {
2077
- const [siteUrl, setSiteUrl] = useState15(initialSiteUrl ?? "");
2078
- const [email, setEmail] = useState15(initialEmail ?? "");
2079
- const [apiToken, setApiToken] = useState15("");
2080
- const [selectedItem, setSelectedItem] = useState15("siteUrl");
2111
+ const hasExisting = existingConfigs.length > 0;
2112
+ const [mode, setMode] = useState14(hasExisting ? "choose" : "manual");
2113
+ const [selectedExisting, setSelectedExisting] = useState14(0);
2114
+ const scrollRef = useScrollToIndex(selectedExisting);
2115
+ const [siteUrl, setSiteUrl] = useState14(initialSiteUrl ?? "");
2116
+ const [email, setEmail] = useState14(initialEmail ?? "");
2117
+ const [apiToken, setApiToken] = useState14("");
2118
+ const [selectedItem, setSelectedItem] = useState14("siteUrl");
2081
2119
  const items = ["siteUrl", "email", "apiToken", "submit"];
2082
2120
  const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
2121
+ const chooseItems = existingConfigs.length + 1;
2083
2122
  useInput8(
2084
2123
  (input, key) => {
2085
2124
  if (loading) return;
2086
2125
  if (key.escape) {
2126
+ if (mode === "manual" && hasExisting) {
2127
+ setMode("choose");
2128
+ return;
2129
+ }
2087
2130
  onCancel();
2088
2131
  return;
2089
2132
  }
2133
+ if (mode === "choose") {
2134
+ if (key.upArrow || input === "k") {
2135
+ setSelectedExisting((prev) => Math.max(0, prev - 1));
2136
+ return;
2137
+ }
2138
+ if (key.downArrow || input === "j") {
2139
+ setSelectedExisting((prev) => Math.min(chooseItems - 1, prev + 1));
2140
+ return;
2141
+ }
2142
+ if (key.return) {
2143
+ if (selectedExisting < existingConfigs.length) {
2144
+ const config = existingConfigs[selectedExisting];
2145
+ onSubmit(config.siteUrl, config.email, config.apiToken);
2146
+ } else {
2147
+ setMode("manual");
2148
+ }
2149
+ }
2150
+ return;
2151
+ }
2090
2152
  if (key.upArrow || input === "k") {
2091
2153
  setSelectedItem((prev) => {
2092
2154
  const idx = items.indexOf(prev);
@@ -2137,9 +2199,49 @@ function ConfigureJiraSiteModal({
2137
2199
  value !== void 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: displayValue || "(empty - press Enter to edit)" }) })
2138
2200
  ] });
2139
2201
  };
2202
+ if (mode === "choose") {
2203
+ const totalItems = existingConfigs.length + 1;
2204
+ const listHeight = Math.min(totalItems * 2, MAX_VISIBLE_ITEMS * 2);
2205
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
2206
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
2207
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Select an existing configuration or enter new credentials" }),
2208
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2209
+ error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
2210
+ /* @__PURE__ */ jsx9(Box9, { height: listHeight, overflow: "hidden", children: /* @__PURE__ */ jsxs9(ScrollView4, { ref: scrollRef, children: [
2211
+ existingConfigs.map((config, idx) => {
2212
+ const isSelected = selectedExisting === idx;
2213
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2214
+ /* @__PURE__ */ jsxs9(Text9, { color: isSelected ? "yellow" : void 0, bold: isSelected, children: [
2215
+ isSelected ? "> " : " ",
2216
+ config.siteUrl
2217
+ ] }),
2218
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2219
+ " ",
2220
+ config.email
2221
+ ] })
2222
+ ] }, config.siteUrl + config.email);
2223
+ }),
2224
+ /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(
2225
+ Text9,
2226
+ {
2227
+ color: selectedExisting === existingConfigs.length ? "yellow" : void 0,
2228
+ bold: selectedExisting === existingConfigs.length,
2229
+ children: [
2230
+ selectedExisting === existingConfigs.length ? "> " : " ",
2231
+ "Enter new credentials..."
2232
+ ]
2233
+ }
2234
+ ) })
2235
+ ] }) }),
2236
+ loading && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Validating credentials..." }) })
2237
+ ] });
2238
+ }
2140
2239
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
2141
2240
  /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Configure Jira Site" }),
2142
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
2241
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2242
+ "Up/Down to select, Enter to edit, Esc to ",
2243
+ hasExisting ? "go back" : "cancel"
2244
+ ] }),
2143
2245
  /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2144
2246
  error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
2145
2247
  renderItem("siteUrl", "Site URL (e.g., https://company.atlassian.net)", siteUrl),
@@ -2178,13 +2280,14 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
2178
2280
 
2179
2281
  // src/components/jira/JiraView.tsx
2180
2282
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2181
- function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated }) {
2283
+ function JiraView({ isActive, onModalChange, onJiraStateChange, onLogUpdated }) {
2182
2284
  const repo = useGitRepo();
2183
2285
  const jira = useJiraTickets();
2184
2286
  const modal = useModal();
2185
- const nav = useListNavigation(jira.tickets.length);
2287
+ const nav = useListNavigation({ items: jira.tickets });
2288
+ const currentTicket = jira.tickets[nav.index] ?? null;
2186
2289
  const lastInitRef = useRef6(null);
2187
- useEffect11(() => {
2290
+ useEffect10(() => {
2188
2291
  if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
2189
2292
  const current = { branch: repo.currentBranch };
2190
2293
  const last = lastInitRef.current;
@@ -2192,17 +2295,17 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2192
2295
  lastInitRef.current = current;
2193
2296
  jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
2194
2297
  }, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
2195
- useEffect11(() => {
2196
- if (isFocused) {
2298
+ useEffect10(() => {
2299
+ if (isActive) {
2197
2300
  repo.refreshBranch();
2198
2301
  } else {
2199
2302
  modal.close();
2200
2303
  }
2201
- }, [isFocused, repo.refreshBranch, modal.close]);
2202
- useEffect11(() => {
2304
+ }, [isActive, repo.refreshBranch, modal.close]);
2305
+ useEffect10(() => {
2203
2306
  onModalChange == null ? void 0 : onModalChange(modal.isOpen);
2204
2307
  }, [modal.isOpen, onModalChange]);
2205
- useEffect11(() => {
2308
+ useEffect10(() => {
2206
2309
  onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
2207
2310
  }, [jira.jiraState, onJiraStateChange]);
2208
2311
  const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
@@ -2215,42 +2318,39 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2215
2318
  const success = await jira.linkTicket(repo.repoPath, repo.currentBranch, ticketInput);
2216
2319
  if (success) modal.close();
2217
2320
  };
2321
+ const getTicketUrl = () => {
2322
+ if (!repo.repoPath || !currentTicket) return null;
2323
+ const siteUrl = getJiraSiteUrl(repo.repoPath);
2324
+ return siteUrl ? `${siteUrl}/browse/${currentTicket.key}` : null;
2325
+ };
2218
2326
  const handleUnlinkTicket = () => {
2219
- if (!repo.repoPath || !repo.currentBranch || jira.tickets.length === 0) return;
2220
- const ticket = jira.tickets[nav.index];
2221
- if (ticket) {
2222
- jira.unlinkTicket(repo.repoPath, repo.currentBranch, ticket.key);
2223
- jira.refreshTickets(repo.repoPath, repo.currentBranch);
2224
- nav.prev();
2225
- }
2327
+ if (!repo.repoPath || !repo.currentBranch || !currentTicket) return;
2328
+ jira.unlinkTicket(repo.repoPath, repo.currentBranch, currentTicket.key);
2329
+ jira.refreshTickets(repo.repoPath, repo.currentBranch);
2330
+ nav.prev();
2226
2331
  };
2227
2332
  const handleOpenInBrowser = () => {
2228
- if (!repo.repoPath || jira.tickets.length === 0) return;
2229
- const ticket = jira.tickets[nav.index];
2230
- const siteUrl = getJiraSiteUrl(repo.repoPath);
2231
- if (ticket && siteUrl) {
2232
- open3(`${siteUrl}/browse/${ticket.key}`).catch(() => {
2233
- });
2234
- }
2333
+ const url = getTicketUrl();
2334
+ if (url) open3(url).catch(() => {
2335
+ });
2235
2336
  };
2236
2337
  const handleCopyLink = () => {
2237
- if (!repo.repoPath || jira.tickets.length === 0) return;
2238
- const ticket = jira.tickets[nav.index];
2239
- const siteUrl = getJiraSiteUrl(repo.repoPath);
2240
- if (ticket && siteUrl) {
2241
- copyToClipboard(`${siteUrl}/browse/${ticket.key}`);
2242
- }
2338
+ const url = getTicketUrl();
2339
+ if (url) copyToClipboard(url);
2243
2340
  };
2244
2341
  const handleStatusComplete = (newStatus) => {
2245
- if (!repo.repoPath || !repo.currentBranch) return;
2246
- const ticket = jira.tickets[nav.index];
2247
- if (!ticket) return;
2248
- updateTicketStatus(repo.repoPath, repo.currentBranch, ticket.key, newStatus);
2249
- logJiraStatusChanged(ticket.key, ticket.summary, ticket.status, newStatus);
2342
+ if (!repo.repoPath || !repo.currentBranch || !currentTicket) return;
2343
+ updateTicketStatus(repo.repoPath, repo.currentBranch, currentTicket.key, newStatus);
2344
+ logJiraStatusChanged(currentTicket.key, currentTicket.summary, currentTicket.status, newStatus);
2250
2345
  onLogUpdated == null ? void 0 : onLogUpdated();
2251
2346
  modal.close();
2252
2347
  jira.refreshTickets(repo.repoPath, repo.currentBranch);
2253
2348
  };
2349
+ const handleRemoveConfig = () => {
2350
+ if (!repo.repoPath || !repo.currentBranch) return;
2351
+ clearJiraConfig(repo.repoPath);
2352
+ jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
2353
+ };
2254
2354
  useInput9(
2255
2355
  (input, key) => {
2256
2356
  if (input === "c" && jira.jiraState === "not_configured") {
@@ -2261,6 +2361,10 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2261
2361
  modal.open("link");
2262
2362
  return;
2263
2363
  }
2364
+ if (input === "r" && jira.jiraState !== "not_configured") {
2365
+ handleRemoveConfig();
2366
+ return;
2367
+ }
2264
2368
  if (jira.jiraState === "has_tickets") {
2265
2369
  if (key.upArrow || input === "k") nav.prev();
2266
2370
  if (key.downArrow || input === "j") nav.next();
@@ -2270,7 +2374,7 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2270
2374
  if (input === "y") handleCopyLink();
2271
2375
  }
2272
2376
  },
2273
- { isActive: isFocused && !modal.isOpen }
2377
+ { isActive: isActive && !modal.isOpen }
2274
2378
  );
2275
2379
  if (repo.isRepo === false) {
2276
2380
  return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: ["Jira"], flexShrink: 0, children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: "Not a git repository" }) });
@@ -2278,11 +2382,13 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2278
2382
  if (modal.type === "configure") {
2279
2383
  const siteUrl = repo.repoPath ? getJiraSiteUrl(repo.repoPath) : void 0;
2280
2384
  const creds = repo.repoPath ? getJiraCredentials(repo.repoPath) : { email: null, apiToken: null };
2385
+ const existingConfigs = getExistingJiraConfigs(repo.repoPath ?? void 0);
2281
2386
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
2282
2387
  ConfigureJiraSiteModal,
2283
2388
  {
2284
2389
  initialSiteUrl: siteUrl ?? void 0,
2285
2390
  initialEmail: creds.email ?? void 0,
2391
+ existingConfigs,
2286
2392
  onSubmit: handleConfigureSubmit,
2287
2393
  onCancel: () => {
2288
2394
  modal.close();
@@ -2307,21 +2413,20 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
2307
2413
  }
2308
2414
  ) });
2309
2415
  }
2310
- if (modal.type === "status" && repo.repoPath && repo.currentBranch && jira.tickets[nav.index]) {
2311
- const ticket = jira.tickets[nav.index];
2416
+ if (modal.type === "status" && repo.repoPath && repo.currentBranch && currentTicket) {
2312
2417
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
2313
2418
  ChangeStatusModal,
2314
2419
  {
2315
2420
  repoPath: repo.repoPath,
2316
- ticketKey: ticket.key,
2317
- currentStatus: ticket.status,
2421
+ ticketKey: currentTicket.key,
2422
+ currentStatus: currentTicket.status,
2318
2423
  onComplete: handleStatusComplete,
2319
2424
  onCancel: modal.close
2320
2425
  }
2321
2426
  ) });
2322
2427
  }
2323
2428
  const title = "[4] Jira";
2324
- const borderColor = isFocused ? "yellow" : void 0;
2429
+ const borderColor = isActive ? "yellow" : void 0;
2325
2430
  return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, children: [
2326
2431
  jira.jiraState === "not_configured" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No Jira site configured" }),
2327
2432
  jira.jiraState === "no_tickets" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No tickets linked to this branch" }),
@@ -2343,10 +2448,10 @@ import { useEffect as useEffect12 } from "react";
2343
2448
  import { Box as Box14, useInput as useInput12 } from "ink";
2344
2449
 
2345
2450
  // src/components/logs/LogViewerBox.tsx
2346
- import { useRef as useRef7, useState as useState16 } from "react";
2451
+ import { useEffect as useEffect11, useRef as useRef7, useState as useState15 } from "react";
2347
2452
  import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
2348
2453
  import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
2349
- import { ScrollView as ScrollView4 } from "ink-scroll-view";
2454
+ import { ScrollView as ScrollView5 } from "ink-scroll-view";
2350
2455
  import TextInput2 from "ink-text-input";
2351
2456
 
2352
2457
  // src/lib/claude/api.ts
@@ -2434,15 +2539,15 @@ Generate the standup notes:`;
2434
2539
 
2435
2540
  // src/components/logs/LogViewerBox.tsx
2436
2541
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2437
- function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
2542
+ function LogViewerBox({ date, content, isActive, onRefresh, onLogCreated }) {
2438
2543
  const scrollRef = useRef7(null);
2439
- const [isInputMode, setIsInputMode] = useState16(false);
2440
- const [inputValue, setInputValue] = useState16("");
2441
- const [isGeneratingStandup, setIsGeneratingStandup] = useState16(false);
2442
- const [standupResult, setStandupResult] = useState16(null);
2544
+ const [isInputMode, setIsInputMode] = useState15(false);
2545
+ const [inputValue, setInputValue] = useState15("");
2546
+ const [isGeneratingStandup, setIsGeneratingStandup] = useState15(false);
2547
+ const [standupResult, setStandupResult] = useState15(null);
2443
2548
  const claudeProcessRef = useRef7(null);
2444
2549
  const title = "[6] Log Content";
2445
- const borderColor = isFocused ? "yellow" : void 0;
2550
+ const borderColor = isActive ? "yellow" : void 0;
2446
2551
  const displayTitle = date ? `${title} - ${date}.md` : title;
2447
2552
  useInput10(
2448
2553
  (input, key) => {
@@ -2504,8 +2609,14 @@ function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
2504
2609
  });
2505
2610
  }
2506
2611
  },
2507
- { isActive: isFocused }
2612
+ { isActive }
2508
2613
  );
2614
+ useEffect11(() => {
2615
+ return () => {
2616
+ var _a;
2617
+ (_a = claudeProcessRef.current) == null ? void 0 : _a.cancel();
2618
+ };
2619
+ }, []);
2509
2620
  const handleInputSubmit = (value) => {
2510
2621
  if (!date || !value.trim()) {
2511
2622
  setIsInputMode(false);
@@ -2524,7 +2635,7 @@ ${value.trim()}
2524
2635
  onRefresh();
2525
2636
  };
2526
2637
  return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
2527
- /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView4, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
2638
+ /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView5, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
2528
2639
  !date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
2529
2640
  date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
2530
2641
  date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
@@ -2560,7 +2671,7 @@ ${value.trim()}
2560
2671
  // src/components/logs/LogsHistoryBox.tsx
2561
2672
  import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
2562
2673
  import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
2563
- import { ScrollView as ScrollView5 } from "ink-scroll-view";
2674
+ import { ScrollView as ScrollView6 } from "ink-scroll-view";
2564
2675
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2565
2676
  function LogsHistoryBox({
2566
2677
  logFiles,
@@ -2568,11 +2679,11 @@ function LogsHistoryBox({
2568
2679
  highlightedIndex,
2569
2680
  onHighlight,
2570
2681
  onSelect,
2571
- isFocused
2682
+ isActive
2572
2683
  }) {
2573
2684
  const scrollRef = useScrollToIndex(highlightedIndex);
2574
2685
  const title = "[5] Logs";
2575
- const borderColor = isFocused ? "yellow" : void 0;
2686
+ const borderColor = isActive ? "yellow" : void 0;
2576
2687
  useInput11(
2577
2688
  (input, key) => {
2578
2689
  if (logFiles.length === 0) return;
@@ -2582,18 +2693,18 @@ function LogsHistoryBox({
2582
2693
  if (key.downArrow || input === "j") {
2583
2694
  onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
2584
2695
  }
2585
- if (key.return) {
2696
+ if (input === " ") {
2586
2697
  const file = logFiles[highlightedIndex];
2587
2698
  if (file) {
2588
2699
  onSelect(file.date);
2589
2700
  }
2590
2701
  }
2591
2702
  },
2592
- { isActive: isFocused }
2703
+ { isActive }
2593
2704
  );
2594
2705
  return /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
2595
2706
  logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
2596
- logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView5, { ref: scrollRef, children: logFiles.map((file, idx) => {
2707
+ logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView6, { ref: scrollRef, children: logFiles.map((file, idx) => {
2597
2708
  const isHighlighted = idx === highlightedIndex;
2598
2709
  const isSelected = file.date === selectedDate;
2599
2710
  const cursor = isHighlighted ? ">" : " ";
@@ -2613,7 +2724,7 @@ function LogsHistoryBox({
2613
2724
 
2614
2725
  // src/components/logs/LogsView.tsx
2615
2726
  import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2616
- function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
2727
+ function LogsView({ isActive, refreshKey, focusedBox, onFocusedBoxChange }) {
2617
2728
  const logs = useLogs();
2618
2729
  useEffect12(() => {
2619
2730
  if (refreshKey !== void 0 && refreshKey > 0) {
@@ -2625,7 +2736,7 @@ function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
2625
2736
  if (input === "5") onFocusedBoxChange("history");
2626
2737
  if (input === "6") onFocusedBoxChange("viewer");
2627
2738
  },
2628
- { isActive: isFocused }
2739
+ { isActive }
2629
2740
  );
2630
2741
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", flexGrow: 1, children: [
2631
2742
  /* @__PURE__ */ jsx14(
@@ -2636,7 +2747,7 @@ function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
2636
2747
  highlightedIndex: logs.highlightedIndex,
2637
2748
  onHighlight: logs.setHighlightedIndex,
2638
2749
  onSelect: logs.selectDate,
2639
- isFocused: isFocused && focusedBox === "history"
2750
+ isActive: isActive && focusedBox === "history"
2640
2751
  }
2641
2752
  ),
2642
2753
  /* @__PURE__ */ jsx14(
@@ -2644,7 +2755,7 @@ function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
2644
2755
  {
2645
2756
  date: logs.selectedDate,
2646
2757
  content: logs.logContent,
2647
- isFocused: isFocused && focusedBox === "viewer",
2758
+ isActive: isActive && focusedBox === "viewer",
2648
2759
  onRefresh: logs.refresh,
2649
2760
  onLogCreated: logs.handleLogCreated
2650
2761
  }
@@ -2695,19 +2806,23 @@ var GITHUB_KEYBINDINGS = {
2695
2806
  // src/constants/jira.ts
2696
2807
  var JIRA_KEYBINDINGS = {
2697
2808
  not_configured: [{ key: "c", label: "Configure Jira" }],
2698
- no_tickets: [{ key: "l", label: "Link Ticket" }],
2809
+ no_tickets: [
2810
+ { key: "l", label: "Link Ticket" },
2811
+ { key: "r", label: "Remove Config", color: "red" }
2812
+ ],
2699
2813
  has_tickets: [
2700
2814
  { key: "l", label: "Link" },
2701
2815
  { key: "s", label: "Status" },
2702
2816
  { key: "d", label: "Unlink", color: "red" },
2703
2817
  { key: "o", label: "Open", color: "green" },
2704
- { key: "y", label: "Copy Link" }
2818
+ { key: "y", label: "Copy Link" },
2819
+ { key: "r", label: "Remove Config", color: "red" }
2705
2820
  ]
2706
2821
  };
2707
2822
 
2708
2823
  // src/constants/logs.ts
2709
2824
  var LOGS_KEYBINDINGS = {
2710
- history: [{ key: "Enter", label: "Select" }],
2825
+ history: [{ key: "Space", label: "Select" }],
2711
2826
  viewer: [
2712
2827
  { key: "i", label: "Add Entry" },
2713
2828
  { key: "e", label: "Edit" },
@@ -2736,13 +2851,13 @@ function computeKeybindings(focusedView, state) {
2736
2851
  import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
2737
2852
  function App() {
2738
2853
  const { exit } = useApp();
2739
- const [focusedView, setFocusedView] = useState17("github");
2740
- const [modalOpen, setModalOpen] = useState17(false);
2741
- const [logRefreshKey, setLogRefreshKey] = useState17(0);
2854
+ const [focusedView, setFocusedView] = useState16("github");
2855
+ const [modalOpen, setModalOpen] = useState16(false);
2856
+ const [logRefreshKey, setLogRefreshKey] = useState16(0);
2742
2857
  const duck = useRubberDuck();
2743
- const [githubFocusedBox, setGithubFocusedBox] = useState17("remotes");
2744
- const [jiraState, setJiraState] = useState17("not_configured");
2745
- const [logsFocusedBox, setLogsFocusedBox] = useState17("history");
2858
+ const [githubFocusedBox, setGithubFocusedBox] = useState16("remotes");
2859
+ const [jiraState, setJiraState] = useState16("not_configured");
2860
+ const [logsFocusedBox, setLogsFocusedBox] = useState16("history");
2746
2861
  const keybindings = useMemo2(
2747
2862
  () => computeKeybindings(focusedView, {
2748
2863
  github: { focusedBox: githubFocusedBox },
@@ -2751,7 +2866,7 @@ function App() {
2751
2866
  }),
2752
2867
  [focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
2753
2868
  );
2754
- const handleLogUpdated = useCallback10(() => {
2869
+ const handleLogUpdated = useCallback9(() => {
2755
2870
  setLogRefreshKey((prev) => prev + 1);
2756
2871
  }, []);
2757
2872
  useInput13(
@@ -2788,7 +2903,7 @@ function App() {
2788
2903
  /* @__PURE__ */ jsx16(
2789
2904
  GitHubView,
2790
2905
  {
2791
- isFocused: focusedView === "github",
2906
+ isActive: focusedView === "github",
2792
2907
  onFocusedBoxChange: setGithubFocusedBox,
2793
2908
  onLogUpdated: handleLogUpdated
2794
2909
  }
@@ -2796,7 +2911,7 @@ function App() {
2796
2911
  /* @__PURE__ */ jsx16(
2797
2912
  JiraView,
2798
2913
  {
2799
- isFocused: focusedView === "jira",
2914
+ isActive: focusedView === "jira",
2800
2915
  onModalChange: setModalOpen,
2801
2916
  onJiraStateChange: setJiraState,
2802
2917
  onLogUpdated: handleLogUpdated
@@ -2806,7 +2921,7 @@ function App() {
2806
2921
  /* @__PURE__ */ jsx16(Box16, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: /* @__PURE__ */ jsx16(
2807
2922
  LogsView,
2808
2923
  {
2809
- isFocused: focusedView === "logs",
2924
+ isActive: focusedView === "logs",
2810
2925
  refreshKey: logRefreshKey,
2811
2926
  focusedBox: logsFocusedBox,
2812
2927
  onFocusedBoxChange: setLogsFocusedBox
@@ -2828,13 +2943,13 @@ function App() {
2828
2943
  import { render as inkRender } from "ink";
2829
2944
 
2830
2945
  // src/lib/Screen.tsx
2831
- import { useCallback as useCallback11, useEffect as useEffect13, useState as useState18 } from "react";
2946
+ import { useCallback as useCallback10, useEffect as useEffect13, useState as useState17 } from "react";
2832
2947
  import { Box as Box17, useStdout as useStdout2 } from "ink";
2833
2948
  import { jsx as jsx17 } from "react/jsx-runtime";
2834
2949
  function Screen({ children }) {
2835
2950
  const { stdout } = useStdout2();
2836
- const getSize = useCallback11(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
2837
- const [size, setSize] = useState18(getSize);
2951
+ const getSize = useCallback10(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
2952
+ const [size, setSize] = useState17(getSize);
2838
2953
  useEffect13(() => {
2839
2954
  const onResize = () => setSize(getSize());
2840
2955
  stdout.on("resize", onResize);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clairo",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",