clairo 2.2.1 → 3.0.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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +463 -70
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,6 +11,8 @@ dashboard tui for github PRs, jira tickets, and daily logs.
11
11
  - view open PRs with status, reviews, and checks
12
12
  - see full PR details with description, labels, and assignees
13
13
  - create new PRs from the terminal
14
+ - browse all PRs across the repo with state, search, and status filters
15
+ - checkout PRs directly from the detail view
14
16
  - jira integration
15
17
  - auto ticket detection based on branch name
16
18
  - link tickets and change status from the terminal
package/dist/cli.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import meow from "meow";
5
5
 
6
6
  // src/app.tsx
7
- import { useCallback as useCallback11, useMemo as useMemo4, useState as useState22 } from "react";
8
- import { Box as Box22, Text as Text21, useApp, useInput as useInput18 } from "ink";
7
+ import { useCallback as useCallback12, useMemo as useMemo4, useState as useState23 } from "react";
8
+ import { Box as Box23, Text as Text22, useApp, useInput as useInput19 } from "ink";
9
9
 
10
10
  // src/components/github/GitHubView.tsx
11
11
  import { useCallback as useCallback8, useEffect as useEffect9, useRef as useRef5, useState as useState12 } from "react";
@@ -194,7 +194,7 @@ function resolveCheckStatus(check) {
194
194
  const conclusion = check.conclusion ?? check.state;
195
195
  if (conclusion === "SUCCESS") return "success";
196
196
  if (conclusion === "FAILURE" || conclusion === "ERROR") return "failure";
197
- if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "skipped";
197
+ if (conclusion === "SKIPPED" || conclusion === "NEUTRAL" || conclusion === "CANCELLED") return "skipped";
198
198
  if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
199
199
  return "pending";
200
200
  return "pending";
@@ -261,7 +261,7 @@ async function listPRsForBranch(branch, repo) {
261
261
  errorType: "not_authenticated"
262
262
  };
263
263
  }
264
- const fields = "number,title,state,author,createdAt,isDraft";
264
+ const fields = "number,title,state,author,createdAt,isDraft,reviewDecision,statusCheckRollup";
265
265
  try {
266
266
  const { stdout } = await execAsync(`gh pr view --json ${fields} 2>/dev/null`);
267
267
  const pr = JSON.parse(stdout);
@@ -1557,6 +1557,7 @@ import open from "open";
1557
1557
  import { useRef as useRef2 } from "react";
1558
1558
  import { Box as Box3, Text as Text3, useInput, useStdout } from "ink";
1559
1559
  import { ScrollView } from "ink-scroll-view";
1560
+ import Spinner from "ink-spinner";
1560
1561
 
1561
1562
  // src/components/ui/Badge.tsx
1562
1563
  import { Text } from "ink";
@@ -1701,10 +1702,9 @@ function renderInlineToString(tokens) {
1701
1702
 
1702
1703
  // src/components/github/PRDetailsBox.tsx
1703
1704
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1704
- function PRDetailsBox({ pr, loading, error, isActive }) {
1705
+ function PRDetailsBox({ pr, loading, error, isActive, title = "[3] PR Details" }) {
1705
1706
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
1706
1707
  const scrollRef = useRef2(null);
1707
- const title = "[3] PR Details";
1708
1708
  const borderColor = isActive ? "yellow" : void 0;
1709
1709
  const displayTitle = pr ? `${title} - #${pr.number}` : title;
1710
1710
  const reviewDisplay = resolveReviewDisplay((pr == null ? void 0 : pr.reviewDecision) ?? null);
@@ -1744,7 +1744,10 @@ function PRDetailsBox({ pr, loading, error, isActive }) {
1744
1744
  borderTop: false,
1745
1745
  borderColor,
1746
1746
  children: /* @__PURE__ */ jsx4(ScrollView, { ref: scrollRef, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, children: [
1747
- loading && /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "Loading details..." }),
1747
+ loading && /* @__PURE__ */ jsxs3(Text3, { color: "yellow", children: [
1748
+ /* @__PURE__ */ jsx4(Spinner, { type: "dots" }),
1749
+ " Loading details..."
1750
+ ] }),
1748
1751
  error && /* @__PURE__ */ jsx4(Text3, { color: "red", children: error }),
1749
1752
  !loading && !error && !pr && /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "Select a PR to view details" }),
1750
1753
  !loading && !error && pr && /* @__PURE__ */ jsxs3(Fragment2, { children: [
@@ -2095,13 +2098,13 @@ function useLogs() {
2095
2098
  import { useCallback as useCallback6, useState as useState7 } from "react";
2096
2099
  function useModal() {
2097
2100
  const [modalType, setModalType] = useState7("none");
2098
- const open6 = useCallback6((type) => setModalType(type), []);
2101
+ const open7 = useCallback6((type) => setModalType(type), []);
2099
2102
  const close = useCallback6(() => setModalType("none"), []);
2100
2103
  const isOpen = modalType !== "none";
2101
2104
  return {
2102
2105
  type: modalType,
2103
2106
  isOpen,
2104
- open: open6,
2107
+ open: open7,
2105
2108
  close
2106
2109
  };
2107
2110
  }
@@ -3067,18 +3070,18 @@ var proto = Object.defineProperties(() => {
3067
3070
  }
3068
3071
  }
3069
3072
  });
3070
- var createStyler = (open6, close, parent) => {
3073
+ var createStyler = (open7, close, parent) => {
3071
3074
  let openAll;
3072
3075
  let closeAll;
3073
3076
  if (parent === void 0) {
3074
- openAll = open6;
3077
+ openAll = open7;
3075
3078
  closeAll = close;
3076
3079
  } else {
3077
- openAll = parent.openAll + open6;
3080
+ openAll = parent.openAll + open7;
3078
3081
  closeAll = close + parent.closeAll;
3079
3082
  }
3080
3083
  return {
3081
- open: open6,
3084
+ open: open7,
3082
3085
  close,
3083
3086
  openAll,
3084
3087
  closeAll,
@@ -3787,7 +3790,7 @@ import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMe
3787
3790
  import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
3788
3791
  import { Box as Box10, Text as Text11, useInput as useInput8 } from "ink";
3789
3792
  import { ScrollView as ScrollView6 } from "ink-scroll-view";
3790
- import Spinner2 from "ink-spinner";
3793
+ import Spinner3 from "ink-spinner";
3791
3794
 
3792
3795
  // src/components/jira-browser/JiraIssueDetailView.tsx
3793
3796
  import open3 from "open";
@@ -3795,7 +3798,7 @@ import { useEffect as useEffect10, useRef as useRef6, useState as useState14 } f
3795
3798
  import { Box as Box9, Text as Text10, useInput as useInput7 } from "ink";
3796
3799
  import { ScrollView as ScrollView5 } from "ink-scroll-view";
3797
3800
  import SelectInput from "ink-select-input";
3798
- import Spinner from "ink-spinner";
3801
+ import Spinner2 from "ink-spinner";
3799
3802
  import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
3800
3803
  function JiraIssueDetailView({
3801
3804
  issueKey,
@@ -3954,7 +3957,7 @@ function JiraIssueDetailView({
3954
3957
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", flexGrow: 1, children: [
3955
3958
  /* @__PURE__ */ jsxs9(Box9, { flexGrow: 1, flexBasis: 0, overflow: "hidden", children: [
3956
3959
  loading && /* @__PURE__ */ jsx11(Box9, { paddingX: 1, children: /* @__PURE__ */ jsxs9(Text10, { color: "yellow", children: [
3957
- /* @__PURE__ */ jsx11(Spinner, { type: "dots" }),
3960
+ /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
3958
3961
  " Loading issue details..."
3959
3962
  ] }) }),
3960
3963
  error && /* @__PURE__ */ jsx11(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text10, { color: "red", children: error }) }),
@@ -4011,13 +4014,13 @@ function JiraIssueDetailView({
4011
4014
  ] }),
4012
4015
  /* @__PURE__ */ jsxs9(Box9, { paddingX: 1, flexDirection: "column", children: [
4013
4016
  actionLoading && /* @__PURE__ */ jsxs9(Text10, { color: "yellow", children: [
4014
- /* @__PURE__ */ jsx11(Spinner, { type: "dots" }),
4017
+ /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
4015
4018
  " ",
4016
4019
  actionLoading
4017
4020
  ] }),
4018
4021
  actionError && /* @__PURE__ */ jsx11(Text10, { color: "red", children: actionError }),
4019
4022
  transitionsLoading && /* @__PURE__ */ jsxs9(Text10, { color: "yellow", children: [
4020
- /* @__PURE__ */ jsx11(Spinner, { type: "dots" }),
4023
+ /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
4021
4024
  " Loading transitions..."
4022
4025
  ] }),
4023
4026
  !actionLoading && !transitionsLoading && mode === "normal" && /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "Esc close \xB7 j/k scroll \xB7 s status \xB7 a assign \xB7 A unassign \xB7 o open \xB7 y copy" })
@@ -4316,7 +4319,7 @@ function JiraSavedViewBrowserBox({
4316
4319
  /* @__PURE__ */ jsxs10(Box10, { flexGrow: 1, flexBasis: 0, overflow: "hidden", children: [
4317
4320
  !view && /* @__PURE__ */ jsx12(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "Select a view to browse issues" }) }),
4318
4321
  view && loading && issues.length === 0 && /* @__PURE__ */ jsx12(Box10, { paddingX: 1, children: /* @__PURE__ */ jsxs10(Text11, { color: "yellow", children: [
4319
- /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
4322
+ /* @__PURE__ */ jsx12(Spinner3, { type: "dots" }),
4320
4323
  " Loading issues..."
4321
4324
  ] }) }),
4322
4325
  view && error && /* @__PURE__ */ jsx12(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx12(Text11, { color: "red", children: error }) }),
@@ -5371,9 +5374,371 @@ function LogsView({ isActive, refreshKey, focusedBox, onFocusedBoxChange }) {
5371
5374
  ] });
5372
5375
  }
5373
5376
 
5377
+ // src/components/pull-requests/AllPullRequestsView.tsx
5378
+ import open6 from "open";
5379
+ import { useCallback as useCallback11, useEffect as useEffect18, useState as useState22 } from "react";
5380
+ import { TitledBox as TitledBox9 } from "@mishieck/ink-titled-box";
5381
+ import { Box as Box21, Text as Text20, useInput as useInput18 } from "ink";
5382
+ import { ScrollView as ScrollView11 } from "ink-scroll-view";
5383
+ import Spinner4 from "ink-spinner";
5384
+
5385
+ // src/lib/github/pr-list.ts
5386
+ import { exec as exec4 } from "child_process";
5387
+ import { promisify as promisify2 } from "util";
5388
+ var execAsync2 = promisify2(exec4);
5389
+ async function listPRs(repo, opts) {
5390
+ if (!await isGhInstalled()) {
5391
+ return {
5392
+ success: false,
5393
+ error: "GitHub CLI (gh) is not installed. Install from https://cli.github.com",
5394
+ errorType: "not_installed"
5395
+ };
5396
+ }
5397
+ if (!await isGhAuthenticated()) {
5398
+ return {
5399
+ success: false,
5400
+ error: "Not authenticated. Run 'gh auth login' to authenticate.",
5401
+ errorType: "not_authenticated"
5402
+ };
5403
+ }
5404
+ const fields = "number,title,state,author,createdAt,isDraft,reviewDecision,statusCheckRollup";
5405
+ const state = (opts == null ? void 0 : opts.state) ?? "open";
5406
+ const limit = (opts == null ? void 0 : opts.limit) ?? 30;
5407
+ const args = [`gh pr list`, `--state ${state}`, `--limit ${limit}`, `--json ${fields}`, `--repo "${repo}"`];
5408
+ if (opts == null ? void 0 : opts.search) {
5409
+ args.push(`--search "${opts.search.replace(/"/g, '\\"')}"`);
5410
+ }
5411
+ try {
5412
+ const { stdout } = await execAsync2(`${args.join(" ")} 2>/dev/null`);
5413
+ const prs = JSON.parse(stdout);
5414
+ return { success: true, data: prs };
5415
+ } catch {
5416
+ return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
5417
+ }
5418
+ }
5419
+ async function checkoutPR(prNumber, repo) {
5420
+ if (!await isGhInstalled()) {
5421
+ return { success: false, error: "gh CLI not installed", errorType: "not_installed" };
5422
+ }
5423
+ if (!await isGhAuthenticated()) {
5424
+ return { success: false, error: "gh CLI not authenticated", errorType: "not_authenticated" };
5425
+ }
5426
+ try {
5427
+ await execAsync2(`gh pr checkout ${prNumber} --repo "${repo}" 2>&1`);
5428
+ return { success: true, data: `Checked out PR #${prNumber}` };
5429
+ } catch (e) {
5430
+ const msg = e instanceof Error ? e.message.split("\n").pop() ?? e.message : "Failed to checkout PR";
5431
+ return { success: false, error: msg, errorType: "api_error" };
5432
+ }
5433
+ }
5434
+
5435
+ // src/components/pull-requests/AllPullRequestsView.tsx
5436
+ import { Fragment as Fragment5, jsx as jsx23, jsxs as jsxs21 } from "react/jsx-runtime";
5437
+ var STATE_CYCLE = ["open", "closed", "all"];
5438
+ function computeOverallCheck(checks) {
5439
+ if (!checks || checks.length === 0) return null;
5440
+ const statuses = checks.map(resolveCheckStatus);
5441
+ if (statuses.some((s) => s === "failure")) return "failure";
5442
+ if (statuses.some((s) => s === "pending")) return "pending";
5443
+ return "success";
5444
+ }
5445
+ function AllPullRequestsView({ isActive, onModalChange }) {
5446
+ const repo = useGitRepo();
5447
+ const [prs, setPrs] = useState22([]);
5448
+ const [loading, setLoading] = useState22(false);
5449
+ const [error, setError] = useState22(null);
5450
+ const [detailPR, setDetailPR] = useState22(null);
5451
+ const [prDetails, setPrDetails] = useState22(null);
5452
+ const [detailsLoading, setDetailsLoading] = useState22(false);
5453
+ const [detailsError, setDetailsError] = useState22(void 0);
5454
+ const [stateFilter, setStateFilter] = useState22("open");
5455
+ const [searchText, setSearchText] = useState22("");
5456
+ const [inputText, setInputText] = useState22("");
5457
+ const [limit, setLimit] = useState22(30);
5458
+ const [isSearching, setIsSearching] = useState22(false);
5459
+ const [checkoutLoading, setCheckoutLoading] = useState22(false);
5460
+ const [checkoutResult, setCheckoutResult] = useState22(null);
5461
+ useEffect18(() => {
5462
+ onModalChange == null ? void 0 : onModalChange(isSearching || detailPR !== null);
5463
+ }, [isSearching, detailPR, onModalChange]);
5464
+ const doFetch = useCallback11(
5465
+ async (state, search, fetchLimit = 30) => {
5466
+ if (!repo.currentRepoSlug) return;
5467
+ setLoading(true);
5468
+ setError(null);
5469
+ setPrs([]);
5470
+ const result = await listPRs(repo.currentRepoSlug, {
5471
+ state,
5472
+ search: search || void 0,
5473
+ limit: fetchLimit
5474
+ });
5475
+ if (result.success) {
5476
+ setPrs(result.data);
5477
+ } else {
5478
+ setError(result.error);
5479
+ }
5480
+ setLoading(false);
5481
+ },
5482
+ [repo.currentRepoSlug]
5483
+ );
5484
+ useEffect18(() => {
5485
+ if (repo.currentRepoSlug) {
5486
+ setStateFilter("open");
5487
+ setSearchText("");
5488
+ setInputText("");
5489
+ doFetch("open", "");
5490
+ }
5491
+ }, [repo.currentRepoSlug]);
5492
+ const fetchDetails = useCallback11(
5493
+ async (pr) => {
5494
+ if (!repo.currentRepoSlug) return;
5495
+ setDetailsLoading(true);
5496
+ setDetailsError(void 0);
5497
+ const result = await getPRDetails(pr.number, repo.currentRepoSlug);
5498
+ if (result.success) {
5499
+ setPrDetails(result.data);
5500
+ setDetailsError(void 0);
5501
+ } else {
5502
+ setDetailsError(result.error);
5503
+ }
5504
+ setDetailsLoading(false);
5505
+ },
5506
+ [repo.currentRepoSlug]
5507
+ );
5508
+ const handleSelect = useCallback11(
5509
+ (index) => {
5510
+ const pr = prs[index];
5511
+ if (pr) {
5512
+ setDetailPR(pr);
5513
+ fetchDetails(pr);
5514
+ }
5515
+ },
5516
+ [prs, fetchDetails]
5517
+ );
5518
+ const { highlightedIndex, scrollRef } = useListNavigation({
5519
+ items: prs,
5520
+ selectedIndex: detailPR ? prs.findIndex((p) => p.number === detailPR.number) : -1,
5521
+ onSelect: handleSelect,
5522
+ isActive: isActive && !detailPR && !isSearching
5523
+ });
5524
+ const currentPR = prs[highlightedIndex] ?? null;
5525
+ const getPRUrl = (pr) => {
5526
+ if (!repo.currentRepoSlug) return null;
5527
+ return `https://github.com/${repo.currentRepoSlug}/pull/${pr.number}`;
5528
+ };
5529
+ const hasActiveFilters = stateFilter !== "open" || searchText.length > 0;
5530
+ useInput18(
5531
+ (input, key) => {
5532
+ if (isSearching) {
5533
+ if (key.escape) {
5534
+ setIsSearching(false);
5535
+ setInputText(searchText);
5536
+ return;
5537
+ }
5538
+ if (key.return) {
5539
+ setIsSearching(false);
5540
+ const newSearch = inputText.trim();
5541
+ if (newSearch !== searchText) {
5542
+ setSearchText(newSearch);
5543
+ setLimit(30);
5544
+ doFetch(stateFilter, newSearch);
5545
+ }
5546
+ return;
5547
+ }
5548
+ if (key.backspace || key.delete) {
5549
+ setInputText((prev) => prev.slice(0, -1));
5550
+ return;
5551
+ }
5552
+ if (input && input.length > 0) {
5553
+ const printable = input.replace(/[^\x20-\x7E\u00A0-\uFFFF]/g, "");
5554
+ if (printable.length > 0) {
5555
+ setInputText((prev) => prev + printable);
5556
+ }
5557
+ return;
5558
+ }
5559
+ return;
5560
+ }
5561
+ if (input === "/") {
5562
+ setIsSearching(true);
5563
+ setInputText(searchText);
5564
+ return;
5565
+ }
5566
+ if (input === "s") {
5567
+ const idx = STATE_CYCLE.indexOf(stateFilter);
5568
+ const newState = STATE_CYCLE[(idx + 1) % STATE_CYCLE.length];
5569
+ setStateFilter(newState);
5570
+ setLimit(30);
5571
+ doFetch(newState, searchText);
5572
+ return;
5573
+ }
5574
+ if (input === "x") {
5575
+ setStateFilter("open");
5576
+ setSearchText("");
5577
+ setInputText("");
5578
+ setLimit(30);
5579
+ doFetch("open", "");
5580
+ return;
5581
+ }
5582
+ if (input === "o" && currentPR) {
5583
+ const url = getPRUrl(currentPR);
5584
+ if (url) open6(url).catch(() => {
5585
+ });
5586
+ }
5587
+ if (input === "y" && currentPR) {
5588
+ const url = getPRUrl(currentPR);
5589
+ if (url) copyToClipboard(url);
5590
+ }
5591
+ if (input === "l") {
5592
+ const newLimit = limit + 30;
5593
+ setLimit(newLimit);
5594
+ doFetch(stateFilter, searchText, newLimit);
5595
+ return;
5596
+ }
5597
+ if (input === "r") {
5598
+ doFetch(stateFilter, searchText, limit);
5599
+ }
5600
+ },
5601
+ { isActive: isActive && !detailPR }
5602
+ );
5603
+ useInput18(
5604
+ (input, key) => {
5605
+ if (key.escape) {
5606
+ setDetailPR(null);
5607
+ setCheckoutResult(null);
5608
+ return;
5609
+ }
5610
+ if (input === "y" && detailPR) {
5611
+ const url = getPRUrl(detailPR);
5612
+ if (url) copyToClipboard(url);
5613
+ }
5614
+ if (input === "c" && detailPR && !checkoutLoading && repo.currentRepoSlug) {
5615
+ setCheckoutLoading(true);
5616
+ setCheckoutResult(null);
5617
+ checkoutPR(detailPR.number, repo.currentRepoSlug).then((result) => {
5618
+ setCheckoutLoading(false);
5619
+ if (result.success) {
5620
+ setCheckoutResult({ success: true, message: `Checked out #${detailPR.number}` });
5621
+ repo.refreshBranch();
5622
+ } else {
5623
+ setCheckoutResult({ success: false, message: result.error });
5624
+ }
5625
+ });
5626
+ }
5627
+ },
5628
+ { isActive: isActive && detailPR !== null }
5629
+ );
5630
+ const filterParts = [];
5631
+ if (stateFilter !== "open") filterParts.push(stateFilter);
5632
+ if (searchText) filterParts.push(`"${searchText}"`);
5633
+ const borderColor = isActive ? "yellow" : void 0;
5634
+ const stateColor = (pr) => {
5635
+ const display = resolveMergeDisplay({
5636
+ state: pr.state,
5637
+ isDraft: pr.isDraft,
5638
+ mergeable: "UNKNOWN"
5639
+ });
5640
+ return display.color;
5641
+ };
5642
+ const stateLabel = (pr) => {
5643
+ if (pr.isDraft) return "Draft";
5644
+ if (pr.state === "MERGED") return "Merged";
5645
+ if (pr.state === "CLOSED") return "Closed";
5646
+ return "Open";
5647
+ };
5648
+ if (detailPR) {
5649
+ return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", flexGrow: 1, children: [
5650
+ /* @__PURE__ */ jsx23(
5651
+ PRDetailsBox,
5652
+ {
5653
+ pr: prDetails,
5654
+ loading: detailsLoading,
5655
+ error: detailsError,
5656
+ isActive,
5657
+ title: "[5] Pull Requests"
5658
+ }
5659
+ ),
5660
+ checkoutLoading ? /* @__PURE__ */ jsxs21(Text20, { color: "yellow", children: [
5661
+ " ",
5662
+ /* @__PURE__ */ jsx23(Spinner4, { type: "dots" }),
5663
+ " Checking out..."
5664
+ ] }) : checkoutResult ? /* @__PURE__ */ jsxs21(Text20, { color: checkoutResult.success ? "green" : "red", children: [
5665
+ " ",
5666
+ checkoutResult.message
5667
+ ] }) : /* @__PURE__ */ jsx23(Text20, { dimColor: true, children: " Esc back \xB7 j/k scroll \xB7 o open \xB7 y copy \xB7 c checkout" })
5668
+ ] });
5669
+ }
5670
+ return /* @__PURE__ */ jsx23(Box21, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx23(TitledBox9, { borderStyle: "round", titles: ["[5] Pull Requests"], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
5671
+ (isSearching || hasActiveFilters) && /* @__PURE__ */ jsxs21(Box21, { children: [
5672
+ /* @__PURE__ */ jsx23(Text20, { color: "blue", children: "Filter: " }),
5673
+ isSearching ? /* @__PURE__ */ jsxs21(Fragment5, { children: [
5674
+ /* @__PURE__ */ jsx23(Text20, { children: inputText }),
5675
+ /* @__PURE__ */ jsx23(Text20, { backgroundColor: "yellow", children: " " })
5676
+ ] }) : /* @__PURE__ */ jsxs21(Fragment5, { children: [
5677
+ /* @__PURE__ */ jsx23(Text20, { children: filterParts.join(" + ") }),
5678
+ /* @__PURE__ */ jsxs21(Text20, { dimColor: true, children: [
5679
+ " (",
5680
+ prs.length,
5681
+ ")"
5682
+ ] })
5683
+ ] })
5684
+ ] }),
5685
+ loading && /* @__PURE__ */ jsxs21(Text20, { color: "yellow", children: [
5686
+ /* @__PURE__ */ jsx23(Spinner4, { type: "dots" }),
5687
+ " Loading PRs..."
5688
+ ] }),
5689
+ error && /* @__PURE__ */ jsx23(Text20, { color: "red", children: error }),
5690
+ !loading && !error && prs.length === 0 && /* @__PURE__ */ jsx23(Text20, { dimColor: true, children: hasActiveFilters ? "No PRs match filter" : "No open PRs" }),
5691
+ !loading && !error && prs.length > 0 && /* @__PURE__ */ jsx23(ScrollView11, { ref: scrollRef, children: prs.map((pr, idx) => {
5692
+ const isHighlighted = isActive && idx === highlightedIndex;
5693
+ const cursor = isHighlighted ? ">" : " ";
5694
+ const review = resolveReviewDisplay(pr.reviewDecision);
5695
+ const overallCheck = computeOverallCheck(pr.statusCheckRollup);
5696
+ return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
5697
+ /* @__PURE__ */ jsxs21(Box21, { children: [
5698
+ /* @__PURE__ */ jsxs21(Text20, { color: isHighlighted ? "yellow" : void 0, children: [
5699
+ cursor,
5700
+ " "
5701
+ ] }),
5702
+ /* @__PURE__ */ jsxs21(Text20, { children: [
5703
+ "#",
5704
+ pr.number
5705
+ ] }),
5706
+ /* @__PURE__ */ jsxs21(Text20, { children: [
5707
+ " ",
5708
+ pr.title,
5709
+ " "
5710
+ ] }),
5711
+ (pr.state !== "OPEN" || pr.isDraft) && /* @__PURE__ */ jsxs21(Text20, { color: stateColor(pr), children: [
5712
+ "[",
5713
+ stateLabel(pr),
5714
+ "]"
5715
+ ] })
5716
+ ] }),
5717
+ /* @__PURE__ */ jsxs21(Box21, { children: [
5718
+ /* @__PURE__ */ jsx23(Text20, { children: " " }),
5719
+ /* @__PURE__ */ jsxs21(Text20, { dimColor: true, children: [
5720
+ " ",
5721
+ pr.author.login,
5722
+ " \xB7 ",
5723
+ timeAgo(pr.createdAt)
5724
+ ] }),
5725
+ pr.reviewDecision && /* @__PURE__ */ jsxs21(Text20, { dimColor: true, children: [
5726
+ " \xB7 ",
5727
+ review.text
5728
+ ] }),
5729
+ overallCheck && /* @__PURE__ */ jsxs21(Text20, { color: CHECK_COLORS[overallCheck], children: [
5730
+ " ",
5731
+ CHECK_ICONS[overallCheck]
5732
+ ] })
5733
+ ] })
5734
+ ] }, pr.number);
5735
+ }) })
5736
+ ] }) }) });
5737
+ }
5738
+
5374
5739
  // src/components/ui/KeybindingsBar.tsx
5375
- import { Box as Box21, Text as Text20 } from "ink";
5376
- import { jsx as jsx23, jsxs as jsxs21 } from "react/jsx-runtime";
5740
+ import { Box as Box22, Text as Text21 } from "ink";
5741
+ import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
5377
5742
  var globalBindings = [
5378
5743
  { key: "1-4", label: "Focus" },
5379
5744
  { key: "Tab", label: "Switch Tab" },
@@ -5384,14 +5749,14 @@ var modalBindings = [{ key: "Esc", label: "Cancel" }];
5384
5749
  var DUCK_ASCII = "<(')___";
5385
5750
  function KeybindingsBar({ contextBindings = [], modalOpen = false, duck }) {
5386
5751
  const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
5387
- return /* @__PURE__ */ jsxs21(Box21, { flexShrink: 0, paddingX: 1, gap: 2, children: [
5388
- allBindings.map((binding) => /* @__PURE__ */ jsxs21(Box21, { gap: 1, children: [
5389
- /* @__PURE__ */ jsx23(Text20, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
5390
- /* @__PURE__ */ jsx23(Text20, { dimColor: true, children: binding.label })
5752
+ return /* @__PURE__ */ jsxs22(Box22, { flexShrink: 0, paddingX: 1, gap: 2, children: [
5753
+ allBindings.map((binding) => /* @__PURE__ */ jsxs22(Box22, { gap: 1, children: [
5754
+ /* @__PURE__ */ jsx24(Text21, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
5755
+ /* @__PURE__ */ jsx24(Text21, { dimColor: true, children: binding.label })
5391
5756
  ] }, binding.key)),
5392
- (duck == null ? void 0 : duck.visible) && /* @__PURE__ */ jsxs21(Box21, { flexGrow: 1, justifyContent: "flex-end", gap: 1, children: [
5393
- /* @__PURE__ */ jsx23(Text20, { children: DUCK_ASCII }),
5394
- /* @__PURE__ */ jsx23(Text20, { dimColor: true, children: duck.message })
5757
+ (duck == null ? void 0 : duck.visible) && /* @__PURE__ */ jsxs22(Box22, { flexGrow: 1, justifyContent: "flex-end", gap: 1, children: [
5758
+ /* @__PURE__ */ jsx24(Text21, { children: DUCK_ASCII }),
5759
+ /* @__PURE__ */ jsx24(Text21, { dimColor: true, children: duck.message })
5395
5760
  ] })
5396
5761
  ] });
5397
5762
  }
@@ -5399,7 +5764,8 @@ function KeybindingsBar({ contextBindings = [], modalOpen = false, duck }) {
5399
5764
  // src/constants/tabs.ts
5400
5765
  var COLUMN2_TABS = [
5401
5766
  { id: "logs", label: "Logs" },
5402
- { id: "jira-browser", label: "Jira" }
5767
+ { id: "jira-browser", label: "Jira" },
5768
+ { id: "pull-requests", label: "PRs" }
5403
5769
  ];
5404
5770
 
5405
5771
  // src/constants/github.ts
@@ -5469,6 +5835,18 @@ var LOGS_KEYBINDINGS = {
5469
5835
  ]
5470
5836
  };
5471
5837
 
5838
+ // src/constants/pull-requests.ts
5839
+ var PULL_REQUESTS_KEYBINDINGS = [
5840
+ { key: "Space", label: "Details" },
5841
+ { key: "/", label: "Search" },
5842
+ { key: "s", label: "State" },
5843
+ { key: "l", label: "Load More" },
5844
+ { key: "x", label: "Clear Filters" },
5845
+ { key: "o", label: "Open", color: "green" },
5846
+ { key: "y", label: "Copy Link" },
5847
+ { key: "r", label: "Refresh" }
5848
+ ];
5849
+
5472
5850
  // src/lib/keybindings.ts
5473
5851
  function computeKeybindings(focusedView, state) {
5474
5852
  switch (focusedView) {
@@ -5482,39 +5860,53 @@ function computeKeybindings(focusedView, state) {
5482
5860
  case "jira-browser":
5483
5861
  if (state["jira-browser"].modalOpen) return [];
5484
5862
  return JIRA_BROWSER_KEYBINDINGS[state["jira-browser"].focusedBox];
5863
+ case "pull-requests":
5864
+ if (state["pull-requests"].modalOpen) return [];
5865
+ return PULL_REQUESTS_KEYBINDINGS;
5485
5866
  default:
5486
5867
  return [];
5487
5868
  }
5488
5869
  }
5489
5870
 
5490
5871
  // src/app.tsx
5491
- import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
5872
+ import { jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
5492
5873
  function App() {
5493
5874
  const { exit } = useApp();
5494
- const [focusedView, setFocusedView] = useState22("github");
5495
- const [modalOpen, setModalOpen] = useState22(false);
5496
- const [logRefreshKey, setLogRefreshKey] = useState22(0);
5497
- const [activeTab, setActiveTab] = useState22("logs");
5875
+ const [focusedView, setFocusedView] = useState23("github");
5876
+ const [modalOpen, setModalOpen] = useState23(false);
5877
+ const [logRefreshKey, setLogRefreshKey] = useState23(0);
5878
+ const [activeTab, setActiveTab] = useState23("logs");
5498
5879
  const duck = useRubberDuck();
5499
- const [githubFocusedBox, setGithubFocusedBox] = useState22("remotes");
5500
- const [jiraState, setJiraState] = useState22("not_configured");
5501
- const [logsFocusedBox, setLogsFocusedBox] = useState22("history");
5502
- const [jiraBrowserFocusedBox, setJiraBrowserFocusedBox] = useState22("saved-views");
5503
- const [jiraBrowserModalOpen, setJiraBrowserModalOpen] = useState22(false);
5880
+ const [githubFocusedBox, setGithubFocusedBox] = useState23("remotes");
5881
+ const [jiraState, setJiraState] = useState23("not_configured");
5882
+ const [logsFocusedBox, setLogsFocusedBox] = useState23("history");
5883
+ const [jiraBrowserFocusedBox, setJiraBrowserFocusedBox] = useState23("saved-views");
5884
+ const [jiraBrowserModalOpen, setJiraBrowserModalOpen] = useState23(false);
5885
+ const [pullRequestsModalOpen, setPullRequestsModalOpen] = useState23(false);
5504
5886
  const keybindings = useMemo4(
5505
5887
  () => computeKeybindings(focusedView, {
5506
5888
  github: { focusedBox: githubFocusedBox },
5507
5889
  jira: { jiraState, modalOpen },
5508
5890
  logs: { focusedBox: logsFocusedBox },
5509
- "jira-browser": { focusedBox: jiraBrowserFocusedBox, modalOpen: jiraBrowserModalOpen }
5891
+ "jira-browser": { focusedBox: jiraBrowserFocusedBox, modalOpen: jiraBrowserModalOpen },
5892
+ "pull-requests": { modalOpen: pullRequestsModalOpen }
5510
5893
  }),
5511
- [focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox, jiraBrowserFocusedBox, jiraBrowserModalOpen]
5894
+ [
5895
+ focusedView,
5896
+ githubFocusedBox,
5897
+ jiraState,
5898
+ modalOpen,
5899
+ logsFocusedBox,
5900
+ jiraBrowserFocusedBox,
5901
+ jiraBrowserModalOpen,
5902
+ pullRequestsModalOpen
5903
+ ]
5512
5904
  );
5513
- const handleLogUpdated = useCallback11(() => {
5905
+ const handleLogUpdated = useCallback12(() => {
5514
5906
  setLogRefreshKey((prev) => prev + 1);
5515
5907
  }, []);
5516
- const anyModalOpen = modalOpen || jiraBrowserModalOpen;
5517
- useInput18(
5908
+ const anyModalOpen = modalOpen || jiraBrowserModalOpen || pullRequestsModalOpen;
5909
+ useInput19(
5518
5910
  (input, key) => {
5519
5911
  if (key.ctrl && input === "c") {
5520
5912
  exit();
@@ -5553,17 +5945,17 @@ function App() {
5553
5945
  },
5554
5946
  { isActive: !anyModalOpen }
5555
5947
  );
5556
- return /* @__PURE__ */ jsxs22(Box22, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: [
5557
- /* @__PURE__ */ jsxs22(Box22, { height: 1, flexDirection: "row", columnGap: 1, children: [
5558
- /* @__PURE__ */ jsx24(Box22, { flexGrow: 1, paddingX: 1, flexBasis: 0, children: /* @__PURE__ */ jsx24(Text21, { color: "gray", children: "Current branch" }) }),
5559
- /* @__PURE__ */ jsxs22(Box22, { flexGrow: 1, gap: 1, flexBasis: 0, children: [
5560
- /* @__PURE__ */ jsx24(Text21, { color: "gray", children: "Dashboards" }),
5561
- COLUMN2_TABS.map((tab) => /* @__PURE__ */ jsx24(Text21, { bold: true, dimColor: activeTab !== tab.id, children: tab.label }, tab.id))
5948
+ return /* @__PURE__ */ jsxs23(Box23, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: [
5949
+ /* @__PURE__ */ jsxs23(Box23, { height: 1, flexDirection: "row", columnGap: 1, children: [
5950
+ /* @__PURE__ */ jsx25(Box23, { flexGrow: 1, paddingX: 1, flexBasis: 0, children: /* @__PURE__ */ jsx25(Text22, { color: "gray", children: "Current branch" }) }),
5951
+ /* @__PURE__ */ jsxs23(Box23, { flexGrow: 1, gap: 1, flexBasis: 0, children: [
5952
+ /* @__PURE__ */ jsx25(Text22, { color: "gray", children: "Dashboards" }),
5953
+ COLUMN2_TABS.map((tab) => /* @__PURE__ */ jsx25(Text22, { bold: true, dimColor: activeTab !== tab.id, children: tab.label }, tab.id))
5562
5954
  ] })
5563
5955
  ] }),
5564
- /* @__PURE__ */ jsxs22(Box22, { flexGrow: 1, flexDirection: "row", columnGap: 1, children: [
5565
- /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
5566
- /* @__PURE__ */ jsx24(
5956
+ /* @__PURE__ */ jsxs23(Box23, { flexGrow: 1, flexDirection: "row", columnGap: 1, children: [
5957
+ /* @__PURE__ */ jsxs23(Box23, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
5958
+ /* @__PURE__ */ jsx25(
5567
5959
  GitHubView,
5568
5960
  {
5569
5961
  isActive: focusedView === "github",
@@ -5571,7 +5963,7 @@ function App() {
5571
5963
  onLogUpdated: handleLogUpdated
5572
5964
  }
5573
5965
  ),
5574
- /* @__PURE__ */ jsx24(
5966
+ /* @__PURE__ */ jsx25(
5575
5967
  JiraView,
5576
5968
  {
5577
5969
  isActive: focusedView === "jira",
@@ -5581,8 +5973,8 @@ function App() {
5581
5973
  }
5582
5974
  )
5583
5975
  ] }),
5584
- /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
5585
- activeTab === "logs" && /* @__PURE__ */ jsx24(
5976
+ /* @__PURE__ */ jsxs23(Box23, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
5977
+ activeTab === "logs" && /* @__PURE__ */ jsx25(
5586
5978
  LogsView,
5587
5979
  {
5588
5980
  isActive: focusedView === "logs",
@@ -5591,7 +5983,7 @@ function App() {
5591
5983
  onFocusedBoxChange: setLogsFocusedBox
5592
5984
  }
5593
5985
  ),
5594
- activeTab === "jira-browser" && /* @__PURE__ */ jsx24(
5986
+ activeTab === "jira-browser" && /* @__PURE__ */ jsx25(
5595
5987
  JiraBrowserView,
5596
5988
  {
5597
5989
  isActive: focusedView === "jira-browser",
@@ -5600,10 +5992,11 @@ function App() {
5600
5992
  onModalChange: setJiraBrowserModalOpen,
5601
5993
  onLogUpdated: handleLogUpdated
5602
5994
  }
5603
- )
5995
+ ),
5996
+ activeTab === "pull-requests" && /* @__PURE__ */ jsx25(AllPullRequestsView, { isActive: focusedView === "pull-requests", onModalChange: setPullRequestsModalOpen })
5604
5997
  ] })
5605
5998
  ] }),
5606
- /* @__PURE__ */ jsx24(
5999
+ /* @__PURE__ */ jsx25(
5607
6000
  KeybindingsBar,
5608
6001
  {
5609
6002
  contextBindings: keybindings,
@@ -5618,31 +6011,31 @@ function App() {
5618
6011
  import { render as inkRender } from "ink";
5619
6012
 
5620
6013
  // src/lib/Screen.tsx
5621
- import { useCallback as useCallback12, useEffect as useEffect18, useState as useState23 } from "react";
5622
- import { Box as Box23, useStdout as useStdout2 } from "ink";
5623
- import { jsx as jsx25 } from "react/jsx-runtime";
6014
+ import { useCallback as useCallback13, useEffect as useEffect19, useState as useState24 } from "react";
6015
+ import { Box as Box24, useStdout as useStdout2 } from "ink";
6016
+ import { jsx as jsx26 } from "react/jsx-runtime";
5624
6017
  function Screen({ children }) {
5625
6018
  const { stdout } = useStdout2();
5626
- const getSize = useCallback12(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
5627
- const [size, setSize] = useState23(getSize);
5628
- useEffect18(() => {
6019
+ const getSize = useCallback13(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
6020
+ const [size, setSize] = useState24(getSize);
6021
+ useEffect19(() => {
5629
6022
  const onResize = () => setSize(getSize());
5630
6023
  stdout.on("resize", onResize);
5631
6024
  return () => {
5632
6025
  stdout.off("resize", onResize);
5633
6026
  };
5634
6027
  }, [stdout, getSize]);
5635
- return /* @__PURE__ */ jsx25(Box23, { height: size.height, width: size.width, children });
6028
+ return /* @__PURE__ */ jsx26(Box24, { height: size.height, width: size.width, children });
5636
6029
  }
5637
6030
 
5638
6031
  // src/lib/render.tsx
5639
- import { jsx as jsx26 } from "react/jsx-runtime";
6032
+ import { jsx as jsx27 } from "react/jsx-runtime";
5640
6033
  var ENTER_ALT_BUFFER = "\x1B[?1049h";
5641
6034
  var EXIT_ALT_BUFFER = "\x1B[?1049l";
5642
6035
  var CLEAR_SCREEN = "\x1B[2J\x1B[H";
5643
6036
  function render(node, options) {
5644
6037
  process.stdout.write(ENTER_ALT_BUFFER + CLEAR_SCREEN);
5645
- const element = /* @__PURE__ */ jsx26(Screen, { children: node });
6038
+ const element = /* @__PURE__ */ jsx27(Screen, { children: node });
5646
6039
  const instance = inkRender(element, options);
5647
6040
  setImmediate(() => instance.rerender(element));
5648
6041
  const cleanup = () => process.stdout.write(EXIT_ALT_BUFFER);
@@ -5663,7 +6056,7 @@ function render(node, options) {
5663
6056
  }
5664
6057
 
5665
6058
  // src/cli.tsx
5666
- import { jsx as jsx27 } from "react/jsx-runtime";
6059
+ import { jsx as jsx28 } from "react/jsx-runtime";
5667
6060
  var cli = meow(
5668
6061
  `
5669
6062
  Usage
@@ -5696,4 +6089,4 @@ if (cli.flags.cwd) {
5696
6089
  process.exit(1);
5697
6090
  }
5698
6091
  }
5699
- render(/* @__PURE__ */ jsx27(App, {}));
6092
+ render(/* @__PURE__ */ jsx28(App, {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clairo",
3
- "version": "2.2.1",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",