clairo 0.2.0 → 0.4.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 +266 -337
  2. package/package.json +4 -1
package/dist/cli.js CHANGED
@@ -4,13 +4,14 @@
4
4
  import meow from "meow";
5
5
 
6
6
  // src/app.tsx
7
- import { useState as useState9 } from "react";
8
- import { Box as Box13, useApp, useInput as useInput11 } from "ink";
7
+ import { useState as useState8 } from "react";
8
+ import { Box as Box13, useApp, useInput as useInput10 } from "ink";
9
9
 
10
10
  // src/components/github/GitHubView.tsx
11
- import { useCallback, useEffect as useEffect3, useState as useState4 } from "react";
11
+ import { exec as exec3 } from "child_process";
12
+ import { useCallback, useEffect as useEffect3, useState as useState3 } from "react";
12
13
  import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
13
- import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
14
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
14
15
 
15
16
  // src/lib/config/index.ts
16
17
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -127,8 +128,6 @@ function getCurrentBranch() {
127
128
 
128
129
  // src/lib/github/index.ts
129
130
  import { exec } from "child_process";
130
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
131
- import { join as join2 } from "path";
132
131
  import { promisify } from "util";
133
132
  var execAsync = promisify(exec);
134
133
  async function isGhInstalled() {
@@ -241,177 +240,124 @@ async function getPRDetails(prNumber, repo) {
241
240
  };
242
241
  }
243
242
  }
244
- function getPRTemplate(repoPath) {
245
- const templatePaths = [
246
- ".github/PULL_REQUEST_TEMPLATE.md",
247
- ".github/pull_request_template.md",
248
- "PULL_REQUEST_TEMPLATE.md",
249
- "pull_request_template.md",
250
- "docs/PULL_REQUEST_TEMPLATE.md"
251
- ];
252
- for (const templatePath of templatePaths) {
253
- const fullPath = join2(repoPath, templatePath);
254
- if (existsSync2(fullPath)) {
255
- try {
256
- return readFileSync2(fullPath, "utf-8");
257
- } catch {
258
- continue;
243
+
244
+ // src/components/github/PRDetailsBox.tsx
245
+ import { useRef } from "react";
246
+ import open from "open";
247
+ import { TitledBox } from "@mishieck/ink-titled-box";
248
+ import { Box as Box2, Text as Text2, useInput } from "ink";
249
+ import { ScrollView } from "ink-scroll-view";
250
+
251
+ // src/components/ui/Markdown.tsx
252
+ import { Box, Text } from "ink";
253
+ import Link from "ink-link";
254
+ import { marked } from "marked";
255
+ import Table from "cli-table3";
256
+ import { jsx, jsxs } from "react/jsx-runtime";
257
+ function Markdown({ children }) {
258
+ const tokens = marked.lexer(children);
259
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: tokens.map((token, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token }, idx)) });
260
+ }
261
+ function TokenRenderer({ token }) {
262
+ var _a, _b;
263
+ switch (token.type) {
264
+ case "heading":
265
+ return /* @__PURE__ */ jsx(Box, { marginTop: token.depth === 1 ? 0 : 1, children: /* @__PURE__ */ jsx(Text, { bold: true, underline: token.depth === 1, children: renderInline(token.tokens) }) });
266
+ case "paragraph": {
267
+ const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some((t) => {
268
+ var _a2;
269
+ return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
270
+ });
271
+ if (hasLinks) {
272
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
259
273
  }
274
+ return /* @__PURE__ */ jsx(Text, { children: renderInline(token.tokens) });
260
275
  }
276
+ case "code":
277
+ return /* @__PURE__ */ jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: token.text }) });
278
+ case "blockquote":
279
+ return /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
280
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2502 " }),
281
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: (_b = token.tokens) == null ? void 0 : _b.map((t, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, idx)) })
282
+ ] });
283
+ case "list":
284
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: token.items.map((item, idx) => /* @__PURE__ */ jsxs(Box, { children: [
285
+ /* @__PURE__ */ jsx(Text, { children: token.ordered ? `${idx + 1}. ` : "\u2022 " }),
286
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: item.tokens.map((t, i) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, i)) })
287
+ ] }, idx)) });
288
+ case "table":
289
+ return /* @__PURE__ */ jsx(TableRenderer, { token });
290
+ case "hr":
291
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(40) });
292
+ case "space":
293
+ return null;
294
+ default:
295
+ if ("text" in token && typeof token.text === "string") {
296
+ return /* @__PURE__ */ jsx(Text, { children: token.text });
297
+ }
298
+ return null;
261
299
  }
262
- return null;
263
300
  }
264
- async function createPR(repo, title, body, baseBranch) {
265
- if (!await isGhInstalled()) {
266
- return {
267
- success: false,
268
- error: "GitHub CLI (gh) is not installed",
269
- errorType: "not_installed"
270
- };
271
- }
272
- if (!await isGhAuthenticated()) {
273
- return {
274
- success: false,
275
- error: "Not authenticated. Run 'gh auth login'",
276
- errorType: "not_authenticated"
277
- };
278
- }
279
- try {
280
- const baseArg = baseBranch ? `--base "${baseBranch}"` : "";
281
- const fields = "number,title,state,author,createdAt,isDraft";
282
- const escapedTitle = title.replace(/"/g, '\\"');
283
- const escapedBody = body.replace(/"/g, '\\"');
284
- const { stdout } = await execAsync(
285
- `gh pr create --title "${escapedTitle}" --body "${escapedBody}" ${baseArg} --repo "${repo}" --json ${fields}`
286
- );
287
- const pr = JSON.parse(stdout);
288
- return { success: true, data: pr };
289
- } catch (err) {
290
- const message = err instanceof Error ? err.message : "Failed to create PR";
291
- return {
292
- success: false,
293
- error: message,
294
- errorType: "api_error"
295
- };
301
+ function TableRenderer({ token }) {
302
+ const table = new Table({
303
+ head: token.header.map((cell) => renderInlineToString(cell.tokens)),
304
+ style: { head: ["cyan"], border: ["gray"] }
305
+ });
306
+ for (const row of token.rows) {
307
+ table.push(row.map((cell) => renderInlineToString(cell.tokens)));
296
308
  }
309
+ return /* @__PURE__ */ jsx(Text, { children: table.toString() });
297
310
  }
298
-
299
- // src/components/github/CreatePRModal.tsx
300
- import { useState } from "react";
301
- import { Box, Text, useInput } from "ink";
302
-
303
- // src/lib/editor.ts
304
- import { spawnSync } from "child_process";
305
- import { mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
306
- import { tmpdir } from "os";
307
- import { join as join3 } from "path";
308
- function openInEditor(content, filename) {
309
- const editor = process.env.VISUAL || process.env.EDITOR || "vi";
310
- const tempDir = mkdtempSync(join3(tmpdir(), "clairo-"));
311
- const tempFile = join3(tempDir, filename);
312
- try {
313
- writeFileSync2(tempFile, content);
314
- const result = spawnSync(editor, [tempFile], {
315
- stdio: "inherit"
316
- });
317
- process.stdout.write("\x1B[2J\x1B[H");
318
- process.stdout.emit("resize");
319
- if (result.status !== 0) {
320
- return null;
321
- }
322
- return readFileSync3(tempFile, "utf-8");
323
- } finally {
324
- try {
325
- rmSync(tempDir, { recursive: true });
326
- } catch {
311
+ function renderInline(tokens) {
312
+ if (!tokens) return null;
313
+ return tokens.map((token, idx) => {
314
+ switch (token.type) {
315
+ case "text":
316
+ return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
317
+ case "strong":
318
+ return /* @__PURE__ */ jsx(Text, { bold: true, children: renderInline(token.tokens) }, idx);
319
+ case "em":
320
+ return /* @__PURE__ */ jsx(Text, { italic: true, children: renderInline(token.tokens) }, idx);
321
+ case "codespan":
322
+ return /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
323
+ "`",
324
+ token.text,
325
+ "`"
326
+ ] }, idx);
327
+ case "link":
328
+ return /* @__PURE__ */ jsx(Link, { url: token.href, children: /* @__PURE__ */ jsx(Text, { color: "blue", children: renderInlineToString(token.tokens) }) }, idx);
329
+ case "image":
330
+ return /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
331
+ "[Image: ",
332
+ token.text || token.href,
333
+ "]"
334
+ ] }, idx);
335
+ case "br":
336
+ return /* @__PURE__ */ jsx(Text, { children: "\n" }, idx);
337
+ case "del":
338
+ return /* @__PURE__ */ jsx(Text, { strikethrough: true, children: renderInline(token.tokens) }, idx);
339
+ default:
340
+ if ("text" in token && typeof token.text === "string") {
341
+ return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
342
+ }
343
+ return null;
327
344
  }
328
- }
345
+ });
329
346
  }
330
-
331
- // src/components/github/CreatePRModal.tsx
332
- import { jsx, jsxs } from "react/jsx-runtime";
333
- function CreatePRModal({ template, onSubmit, onCancel, loading, error }) {
334
- const [title, setTitle] = useState("");
335
- const [body, setBody] = useState(template ?? "");
336
- const [selectedItem, setSelectedItem] = useState("title");
337
- const items = ["title", "body", "submit"];
338
- useInput(
339
- (input, key) => {
340
- if (loading) return;
341
- if (key.escape) {
342
- onCancel();
343
- return;
344
- }
345
- if (key.upArrow || input === "k") {
346
- setSelectedItem((prev) => {
347
- const idx = items.indexOf(prev);
348
- return items[Math.max(0, idx - 1)];
349
- });
350
- return;
351
- }
352
- if (key.downArrow || input === "j") {
353
- setSelectedItem((prev) => {
354
- const idx = items.indexOf(prev);
355
- return items[Math.min(items.length - 1, idx + 1)];
356
- });
357
- return;
358
- }
359
- if (key.return) {
360
- if (selectedItem === "title") {
361
- const newTitle = openInEditor(title, "PR_TITLE.txt");
362
- if (newTitle !== null) {
363
- setTitle(newTitle.split("\n")[0].trim());
364
- }
365
- } else if (selectedItem === "body") {
366
- const newBody = openInEditor(body, "PR_DESCRIPTION.md");
367
- if (newBody !== null) {
368
- setBody(newBody);
369
- }
370
- } else if (selectedItem === "submit") {
371
- if (title.trim()) {
372
- onSubmit(title.trim(), body);
373
- }
374
- }
375
- }
376
- },
377
- { isActive: !loading }
378
- );
379
- const renderItem = (item, label, value) => {
380
- const isSelected = selectedItem === item;
381
- const prefix = isSelected ? "> " : " ";
382
- const color = isSelected ? "yellow" : void 0;
383
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
384
- /* @__PURE__ */ jsxs(Text, { color, bold: isSelected, children: [
385
- prefix,
386
- label
387
- ] }),
388
- value !== void 0 && /* @__PURE__ */ jsx(Box, { marginLeft: 4, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: value || "(empty - press Enter to edit)" }) })
389
- ] });
390
- };
391
- const truncatedBody = body ? body.split("\n").slice(0, 2).join(" ").slice(0, 60) + (body.length > 60 ? "..." : "") : "";
392
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, children: [
393
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }),
394
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Up/Down to select, Enter to edit, Esc to cancel" }),
395
- /* @__PURE__ */ jsx(Box, { marginTop: 1 }),
396
- error && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) }),
397
- renderItem("title", "Title", title),
398
- /* @__PURE__ */ jsx(Box, { marginTop: 1 }),
399
- renderItem("body", "Description", truncatedBody),
400
- /* @__PURE__ */ jsx(Box, { marginTop: 1 }),
401
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: selectedItem === "submit" ? "green" : void 0, bold: selectedItem === "submit", children: [
402
- selectedItem === "submit" ? "> " : " ",
403
- title.trim() ? "[Submit PR]" : "[Enter title first]"
404
- ] }) }),
405
- loading && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Creating PR..." }) })
406
- ] });
347
+ function renderInlineToString(tokens) {
348
+ if (!tokens) return "";
349
+ return tokens.map((token) => {
350
+ if ("text" in token && typeof token.text === "string") {
351
+ return token.text;
352
+ }
353
+ if ("tokens" in token && Array.isArray(token.tokens)) {
354
+ return renderInlineToString(token.tokens);
355
+ }
356
+ return "";
357
+ }).join("");
407
358
  }
408
359
 
409
360
  // src/components/github/PRDetailsBox.tsx
410
- import { useRef } from "react";
411
- import open from "open";
412
- import { TitledBox } from "@mishieck/ink-titled-box";
413
- import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
414
- import { ScrollView } from "ink-scroll-view";
415
361
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
416
362
  function getCheckColor(check) {
417
363
  const conclusion = check.conclusion ?? check.state;
@@ -450,7 +396,7 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
450
396
  return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
451
397
  };
452
398
  const mergeDisplay = getMergeDisplay();
453
- useInput2(
399
+ useInput(
454
400
  (input, key) => {
455
401
  var _a2, _b2;
456
402
  if (key.upArrow || input === "k") {
@@ -466,7 +412,7 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
466
412
  },
467
413
  { isActive: isFocused }
468
414
  );
469
- return /* @__PURE__ */ jsx2(TitledBox, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 2, children: /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx2(ScrollView, { ref: scrollRef, flexGrow: 1, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
415
+ return /* @__PURE__ */ jsx2(TitledBox, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx2(ScrollView, { ref: scrollRef, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
470
416
  loading && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Loading details..." }),
471
417
  error && /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }),
472
418
  !loading && !error && !pr && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Select a PR to view details" }),
@@ -518,16 +464,16 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
518
464
  ] }),
519
465
  pr.body && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
520
466
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Description:" }),
521
- /* @__PURE__ */ jsx2(Text2, { children: pr.body })
467
+ /* @__PURE__ */ jsx2(Markdown, { children: pr.body })
522
468
  ] })
523
469
  ] })
524
470
  ] }) }) }) });
525
471
  }
526
472
 
527
473
  // src/components/github/PullRequestsBox.tsx
528
- import { useEffect, useState as useState2 } from "react";
474
+ import { useEffect, useState } from "react";
529
475
  import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
530
- import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
476
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
531
477
 
532
478
  // src/lib/clipboard.ts
533
479
  import { exec as exec2 } from "child_process";
@@ -563,13 +509,13 @@ function PullRequestsBox({
563
509
  repoSlug,
564
510
  isFocused
565
511
  }) {
566
- const [highlightedIndex, setHighlightedIndex] = useState2(0);
512
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
567
513
  const totalItems = prs.length + 1;
568
514
  useEffect(() => {
569
515
  const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
570
516
  if (idx >= 0) setHighlightedIndex(idx);
571
517
  }, [selectedPR, prs]);
572
- useInput3(
518
+ useInput2(
573
519
  (input, key) => {
574
520
  if (!isFocused) return;
575
521
  if (key.upArrow || input === "k") {
@@ -623,17 +569,17 @@ function PullRequestsBox({
623
569
  }
624
570
 
625
571
  // src/components/github/RemotesBox.tsx
626
- import { useEffect as useEffect2, useState as useState3 } from "react";
572
+ import { useEffect as useEffect2, useState as useState2 } from "react";
627
573
  import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
628
- import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
574
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
629
575
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
630
576
  function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
631
- const [highlightedIndex, setHighlightedIndex] = useState3(0);
577
+ const [highlightedIndex, setHighlightedIndex] = useState2(0);
632
578
  useEffect2(() => {
633
579
  const idx = remotes.findIndex((r) => r.name === selectedRemote);
634
580
  if (idx >= 0) setHighlightedIndex(idx);
635
581
  }, [selectedRemote, remotes]);
636
- useInput4(
582
+ useInput3(
637
583
  (input, key) => {
638
584
  if (!isFocused || remotes.length === 0) return;
639
585
  if (key.upArrow || input === "k") {
@@ -671,37 +617,25 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
671
617
 
672
618
  // src/components/github/GitHubView.tsx
673
619
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
674
- function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
675
- const [isRepo, setIsRepo] = useState4(null);
676
- const [repoPath, setRepoPath] = useState4(null);
677
- const [remotes, setRemotes] = useState4([]);
678
- const [currentBranch, setCurrentBranch] = useState4(null);
679
- const [currentRepoSlug, setCurrentRepoSlug] = useState4(null);
680
- const [selectedRemote, setSelectedRemote] = useState4(null);
681
- const [selectedPR, setSelectedPR] = useState4(null);
682
- const [prs, setPrs] = useState4([]);
683
- const [prDetails, setPrDetails] = useState4(null);
684
- const [loading, setLoading] = useState4({
620
+ function GitHubView({ isFocused, onKeybindingsChange }) {
621
+ const [isRepo, setIsRepo] = useState3(null);
622
+ const [repoPath, setRepoPath] = useState3(null);
623
+ const [remotes, setRemotes] = useState3([]);
624
+ const [currentBranch, setCurrentBranch] = useState3(null);
625
+ const [currentRepoSlug, setCurrentRepoSlug] = useState3(null);
626
+ const [selectedRemote, setSelectedRemote] = useState3(null);
627
+ const [selectedPR, setSelectedPR] = useState3(null);
628
+ const [prs, setPrs] = useState3([]);
629
+ const [prDetails, setPrDetails] = useState3(null);
630
+ const [loading, setLoading] = useState3({
685
631
  remotes: true,
686
632
  prs: false,
687
- details: false,
688
- createPR: false
633
+ details: false
689
634
  });
690
- const [errors, setErrors] = useState4({});
691
- const [showCreatePR, setShowCreatePR] = useState4(false);
692
- const [prTemplate, setPrTemplate] = useState4(null);
693
- const [focusedBox, setFocusedBox] = useState4("remotes");
635
+ const [errors, setErrors] = useState3({});
636
+ const [focusedBox, setFocusedBox] = useState3("remotes");
694
637
  useEffect3(() => {
695
638
  if (!isFocused) {
696
- setShowCreatePR(false);
697
- setErrors((prev) => ({ ...prev, createPR: void 0 }));
698
- }
699
- }, [isFocused]);
700
- useEffect3(() => {
701
- onModalChange == null ? void 0 : onModalChange(showCreatePR);
702
- }, [showCreatePR, onModalChange]);
703
- useEffect3(() => {
704
- if (!isFocused || showCreatePR) {
705
639
  onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
706
640
  return;
707
641
  }
@@ -710,13 +644,15 @@ function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
710
644
  bindings.push({ key: "Enter", label: "Select Remote" });
711
645
  } else if (focusedBox === "prs") {
712
646
  bindings.push({ key: "n", label: "New PR", color: "green" });
647
+ bindings.push({ key: "r", label: "Refresh" });
713
648
  bindings.push({ key: "o", label: "Open", color: "green" });
714
649
  bindings.push({ key: "y", label: "Copy Link" });
715
650
  } else if (focusedBox === "details") {
651
+ bindings.push({ key: "r", label: "Refresh" });
716
652
  bindings.push({ key: "o", label: "Open", color: "green" });
717
653
  }
718
654
  onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
719
- }, [isFocused, focusedBox, showCreatePR, onKeybindingsChange]);
655
+ }, [isFocused, focusedBox, onKeybindingsChange]);
720
656
  useEffect3(() => {
721
657
  const gitRepoCheck = isGitRepo();
722
658
  setIsRepo(gitRepoCheck);
@@ -728,8 +664,6 @@ function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
728
664
  const rootResult = getRepoRoot();
729
665
  if (rootResult.success) {
730
666
  setRepoPath(rootResult.data);
731
- const template = getPRTemplate(rootResult.data);
732
- setPrTemplate(template);
733
667
  }
734
668
  const branchResult = getCurrentBranch();
735
669
  if (branchResult.success) {
@@ -746,6 +680,43 @@ function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
746
680
  }
747
681
  setLoading((prev) => ({ ...prev, remotes: false }));
748
682
  }, []);
683
+ const refreshPRs = useCallback(async () => {
684
+ if (!currentBranch || !currentRepoSlug) return;
685
+ setLoading((prev) => ({ ...prev, prs: true }));
686
+ try {
687
+ const result = await listPRsForBranch(currentBranch, currentRepoSlug);
688
+ if (result.success) {
689
+ setPrs(result.data);
690
+ if (result.data.length > 0) {
691
+ setSelectedPR((prev) => prev ?? result.data[0]);
692
+ }
693
+ setErrors((prev) => ({ ...prev, prs: void 0 }));
694
+ } else {
695
+ setErrors((prev) => ({ ...prev, prs: result.error }));
696
+ }
697
+ } catch (err) {
698
+ setErrors((prev) => ({ ...prev, prs: String(err) }));
699
+ } finally {
700
+ setLoading((prev) => ({ ...prev, prs: false }));
701
+ }
702
+ }, [currentBranch, currentRepoSlug]);
703
+ const refreshDetails = useCallback(async () => {
704
+ if (!selectedPR || !currentRepoSlug) return;
705
+ setLoading((prev) => ({ ...prev, details: true }));
706
+ try {
707
+ const result = await getPRDetails(selectedPR.number, currentRepoSlug);
708
+ if (result.success) {
709
+ setPrDetails(result.data);
710
+ setErrors((prev) => ({ ...prev, details: void 0 }));
711
+ } else {
712
+ setErrors((prev) => ({ ...prev, details: result.error }));
713
+ }
714
+ } catch (err) {
715
+ setErrors((prev) => ({ ...prev, details: String(err) }));
716
+ } finally {
717
+ setLoading((prev) => ({ ...prev, details: false }));
718
+ }
719
+ }, [selectedPR, currentRepoSlug]);
749
720
  useEffect3(() => {
750
721
  if (!selectedRemote || !currentBranch) return;
751
722
  const remote = remotes.find((r) => r.name === selectedRemote);
@@ -753,52 +724,21 @@ function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
753
724
  const repo = getRepoFromRemote(remote.url);
754
725
  if (!repo) return;
755
726
  setCurrentRepoSlug(repo);
756
- setLoading((prev) => ({ ...prev, prs: true }));
757
727
  setPrs([]);
758
728
  setSelectedPR(null);
759
- const fetchPRs = async () => {
760
- try {
761
- const result = await listPRsForBranch(currentBranch, repo);
762
- if (result.success) {
763
- setPrs(result.data);
764
- if (result.data.length > 0) {
765
- setSelectedPR(result.data[0]);
766
- }
767
- setErrors((prev) => ({ ...prev, prs: void 0 }));
768
- } else {
769
- setErrors((prev) => ({ ...prev, prs: result.error }));
770
- }
771
- } catch (err) {
772
- setErrors((prev) => ({ ...prev, prs: String(err) }));
773
- } finally {
774
- setLoading((prev) => ({ ...prev, prs: false }));
775
- }
776
- };
777
- fetchPRs();
778
729
  }, [selectedRemote, currentBranch, remotes]);
730
+ useEffect3(() => {
731
+ if (currentRepoSlug && currentBranch) {
732
+ refreshPRs();
733
+ }
734
+ }, [currentRepoSlug, currentBranch, refreshPRs]);
779
735
  useEffect3(() => {
780
736
  if (!selectedPR || !currentRepoSlug) {
781
737
  setPrDetails(null);
782
738
  return;
783
739
  }
784
- setLoading((prev) => ({ ...prev, details: true }));
785
- const fetchDetails = async () => {
786
- try {
787
- const result = await getPRDetails(selectedPR.number, currentRepoSlug);
788
- if (result.success) {
789
- setPrDetails(result.data);
790
- setErrors((prev) => ({ ...prev, details: void 0 }));
791
- } else {
792
- setErrors((prev) => ({ ...prev, details: result.error }));
793
- }
794
- } catch (err) {
795
- setErrors((prev) => ({ ...prev, details: String(err) }));
796
- } finally {
797
- setLoading((prev) => ({ ...prev, details: false }));
798
- }
799
- };
800
- fetchDetails();
801
- }, [selectedPR, currentRepoSlug]);
740
+ refreshDetails();
741
+ }, [selectedPR, currentRepoSlug, refreshDetails]);
802
742
  const handleRemoteSelect = useCallback(
803
743
  (remoteName) => {
804
744
  setSelectedRemote(remoteName);
@@ -812,51 +752,21 @@ function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
812
752
  setSelectedPR(pr);
813
753
  }, []);
814
754
  const handleCreatePR = useCallback(() => {
815
- setShowCreatePR(true);
816
- setErrors((prev) => ({ ...prev, createPR: void 0 }));
817
- }, []);
818
- const handleCreatePRSubmit = useCallback(
819
- async (title, body) => {
820
- if (!currentRepoSlug) return;
821
- setLoading((prev) => ({ ...prev, createPR: true }));
822
- setErrors((prev) => ({ ...prev, createPR: void 0 }));
823
- try {
824
- const result = await createPR(currentRepoSlug, title, body);
825
- if (result.success) {
826
- setShowCreatePR(false);
827
- if (currentBranch) {
828
- const prsResult = await listPRsForBranch(currentBranch, currentRepoSlug);
829
- if (prsResult.success) {
830
- setPrs(prsResult.data);
831
- const newPR = prsResult.data.find((p) => p.number === result.data.number);
832
- if (newPR) {
833
- setSelectedPR(newPR);
834
- }
835
- }
836
- }
837
- } else {
838
- setErrors((prev) => ({ ...prev, createPR: result.error }));
839
- }
840
- } catch (err) {
841
- setErrors((prev) => ({ ...prev, createPR: String(err) }));
842
- } finally {
843
- setLoading((prev) => ({ ...prev, createPR: false }));
844
- }
845
- },
846
- [currentRepoSlug, currentBranch]
847
- );
848
- const handleCreatePRCancel = useCallback(() => {
849
- setShowCreatePR(false);
850
- setErrors((prev) => ({ ...prev, createPR: void 0 }));
755
+ exec3("gh pr create --web", () => {
756
+ process.stdout.emit("resize");
757
+ });
851
758
  }, []);
852
- useInput5(
759
+ useInput4(
853
760
  (input) => {
854
- if (showCreatePR) return;
855
761
  if (input === "1") setFocusedBox("remotes");
856
762
  if (input === "2") setFocusedBox("prs");
857
763
  if (input === "3") setFocusedBox("details");
764
+ if (input === "r") {
765
+ if (focusedBox === "prs") refreshPRs();
766
+ if (focusedBox === "details") refreshDetails();
767
+ }
858
768
  },
859
- { isActive: isFocused && !showCreatePR }
769
+ { isActive: isFocused }
860
770
  );
861
771
  if (isRepo === false) {
862
772
  return /* @__PURE__ */ jsx5(TitledBox4, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
@@ -870,7 +780,7 @@ function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
870
780
  onSelect: handleRemoteSelect,
871
781
  loading: loading.remotes,
872
782
  error: errors.remotes,
873
- isFocused: isFocused && !showCreatePR && focusedBox === "remotes"
783
+ isFocused: isFocused && focusedBox === "remotes"
874
784
  }
875
785
  ),
876
786
  /* @__PURE__ */ jsx5(
@@ -884,17 +794,7 @@ function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
884
794
  error: errors.prs,
885
795
  branch: currentBranch,
886
796
  repoSlug: currentRepoSlug,
887
- isFocused: isFocused && !showCreatePR && focusedBox === "prs"
888
- }
889
- ),
890
- showCreatePR && /* @__PURE__ */ jsx5(
891
- CreatePRModal,
892
- {
893
- template: prTemplate,
894
- onSubmit: handleCreatePRSubmit,
895
- onCancel: handleCreatePRCancel,
896
- loading: loading.createPR,
897
- error: errors.createPR
797
+ isFocused: isFocused && focusedBox === "prs"
898
798
  }
899
799
  ),
900
800
  /* @__PURE__ */ jsx5(
@@ -903,17 +803,17 @@ function GitHubView({ isFocused, onModalChange, onKeybindingsChange }) {
903
803
  pr: prDetails,
904
804
  loading: loading.details,
905
805
  error: errors.details,
906
- isFocused: isFocused && !showCreatePR && focusedBox === "details"
806
+ isFocused: isFocused && focusedBox === "details"
907
807
  }
908
808
  )
909
809
  ] });
910
810
  }
911
811
 
912
812
  // src/components/jira/JiraView.tsx
913
- import { useCallback as useCallback2, useEffect as useEffect5, useState as useState8 } from "react";
813
+ import { useCallback as useCallback2, useEffect as useEffect5, useState as useState7 } from "react";
914
814
  import open2 from "open";
915
815
  import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
916
- import { Box as Box11, Text as Text11, useInput as useInput10 } from "ink";
816
+ import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
917
817
 
918
818
  // src/lib/jira/parser.ts
919
819
  var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
@@ -1139,15 +1039,15 @@ async function applyTransition(auth, ticketKey, transitionId) {
1139
1039
  }
1140
1040
 
1141
1041
  // src/components/jira/ChangeStatusModal.tsx
1142
- import { useEffect as useEffect4, useState as useState5 } from "react";
1143
- import { Box as Box6, Text as Text6, useInput as useInput6 } from "ink";
1042
+ import { useEffect as useEffect4, useState as useState4 } from "react";
1043
+ import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
1144
1044
  import SelectInput from "ink-select-input";
1145
1045
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1146
1046
  function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
1147
- const [transitions, setTransitions] = useState5([]);
1148
- const [loading, setLoading] = useState5(true);
1149
- const [applying, setApplying] = useState5(false);
1150
- const [error, setError] = useState5(null);
1047
+ const [transitions, setTransitions] = useState4([]);
1048
+ const [loading, setLoading] = useState4(true);
1049
+ const [applying, setApplying] = useState4(false);
1050
+ const [error, setError] = useState4(null);
1151
1051
  useEffect4(() => {
1152
1052
  const fetchTransitions = async () => {
1153
1053
  const siteUrl = getJiraSiteUrl(repoPath);
@@ -1189,7 +1089,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1189
1089
  setApplying(false);
1190
1090
  }
1191
1091
  };
1192
- useInput6(
1092
+ useInput5(
1193
1093
  (_input, key) => {
1194
1094
  if (key.escape && !applying) {
1195
1095
  onCancel();
@@ -1217,8 +1117,38 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
1217
1117
  }
1218
1118
 
1219
1119
  // src/components/jira/ConfigureJiraSiteModal.tsx
1220
- import { useState as useState6 } from "react";
1221
- import { Box as Box7, Text as Text7, useInput as useInput7 } from "ink";
1120
+ import { useState as useState5 } from "react";
1121
+ import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
1122
+
1123
+ // src/lib/editor.ts
1124
+ import { spawnSync } from "child_process";
1125
+ import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
1126
+ import { tmpdir } from "os";
1127
+ import { join as join2 } from "path";
1128
+ function openInEditor(content, filename) {
1129
+ const editor = process.env.VISUAL || process.env.EDITOR || "vi";
1130
+ const tempDir = mkdtempSync(join2(tmpdir(), "clairo-"));
1131
+ const tempFile = join2(tempDir, filename);
1132
+ try {
1133
+ writeFileSync2(tempFile, content);
1134
+ const result = spawnSync(editor, [tempFile], {
1135
+ stdio: "inherit"
1136
+ });
1137
+ process.stdout.write("\x1B[2J\x1B[H");
1138
+ process.stdout.emit("resize");
1139
+ if (result.status !== 0) {
1140
+ return null;
1141
+ }
1142
+ return readFileSync2(tempFile, "utf-8");
1143
+ } finally {
1144
+ try {
1145
+ rmSync(tempDir, { recursive: true });
1146
+ } catch {
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ // src/components/jira/ConfigureJiraSiteModal.tsx
1222
1152
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1223
1153
  function ConfigureJiraSiteModal({
1224
1154
  initialSiteUrl,
@@ -1228,13 +1158,13 @@ function ConfigureJiraSiteModal({
1228
1158
  loading,
1229
1159
  error
1230
1160
  }) {
1231
- const [siteUrl, setSiteUrl] = useState6(initialSiteUrl ?? "");
1232
- const [email, setEmail] = useState6(initialEmail ?? "");
1233
- const [apiToken, setApiToken] = useState6("");
1234
- const [selectedItem, setSelectedItem] = useState6("siteUrl");
1161
+ const [siteUrl, setSiteUrl] = useState5(initialSiteUrl ?? "");
1162
+ const [email, setEmail] = useState5(initialEmail ?? "");
1163
+ const [apiToken, setApiToken] = useState5("");
1164
+ const [selectedItem, setSelectedItem] = useState5("siteUrl");
1235
1165
  const items = ["siteUrl", "email", "apiToken", "submit"];
1236
1166
  const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
1237
- useInput7(
1167
+ useInput6(
1238
1168
  (input, key) => {
1239
1169
  if (loading) return;
1240
1170
  if (key.escape) {
@@ -1312,14 +1242,14 @@ function ConfigureJiraSiteModal({
1312
1242
  }
1313
1243
 
1314
1244
  // src/components/jira/LinkTicketModal.tsx
1315
- import { useState as useState7 } from "react";
1316
- import { Box as Box9, Text as Text9, useInput as useInput9 } from "ink";
1245
+ import { useState as useState6 } from "react";
1246
+ import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
1317
1247
 
1318
1248
  // src/components/ui/TextInput.tsx
1319
- import { Box as Box8, Text as Text8, useInput as useInput8 } from "ink";
1249
+ import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
1320
1250
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1321
1251
  function TextInput({ value, onChange, placeholder, isActive, mask }) {
1322
- useInput8(
1252
+ useInput7(
1323
1253
  (input, key) => {
1324
1254
  if (key.backspace || key.delete) {
1325
1255
  if (value.length > 0) {
@@ -1347,9 +1277,9 @@ function TextInput({ value, onChange, placeholder, isActive, mask }) {
1347
1277
  // src/components/jira/LinkTicketModal.tsx
1348
1278
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1349
1279
  function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
1350
- const [ticketInput, setTicketInput] = useState7("");
1280
+ const [ticketInput, setTicketInput] = useState6("");
1351
1281
  const canSubmit = ticketInput.trim().length > 0;
1352
- useInput9(
1282
+ useInput8(
1353
1283
  (_input, key) => {
1354
1284
  if (loading) return;
1355
1285
  if (key.escape) {
@@ -1406,17 +1336,17 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
1406
1336
  // src/components/jira/JiraView.tsx
1407
1337
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1408
1338
  function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
1409
- const [repoPath, setRepoPath] = useState8(null);
1410
- const [currentBranch, setCurrentBranch] = useState8(null);
1411
- const [isRepo, setIsRepo] = useState8(null);
1412
- const [jiraState, setJiraState] = useState8("not_configured");
1413
- const [tickets, setTickets] = useState8([]);
1414
- const [highlightedIndex, setHighlightedIndex] = useState8(0);
1415
- const [showConfigureModal, setShowConfigureModal] = useState8(false);
1416
- const [showLinkModal, setShowLinkModal] = useState8(false);
1417
- const [showStatusModal, setShowStatusModal] = useState8(false);
1418
- const [loading, setLoading] = useState8({ configure: false, link: false });
1419
- const [errors, setErrors] = useState8({});
1339
+ const [repoPath, setRepoPath] = useState7(null);
1340
+ const [currentBranch, setCurrentBranch] = useState7(null);
1341
+ const [isRepo, setIsRepo] = useState7(null);
1342
+ const [jiraState, setJiraState] = useState7("not_configured");
1343
+ const [tickets, setTickets] = useState7([]);
1344
+ const [highlightedIndex, setHighlightedIndex] = useState7(0);
1345
+ const [showConfigureModal, setShowConfigureModal] = useState7(false);
1346
+ const [showLinkModal, setShowLinkModal] = useState7(false);
1347
+ const [showStatusModal, setShowStatusModal] = useState7(false);
1348
+ const [loading, setLoading] = useState7({ configure: false, link: false });
1349
+ const [errors, setErrors] = useState7({});
1420
1350
  useEffect5(() => {
1421
1351
  if (!isFocused) {
1422
1352
  setShowConfigureModal(false);
@@ -1579,7 +1509,7 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
1579
1509
  });
1580
1510
  }
1581
1511
  }, [repoPath, tickets, highlightedIndex]);
1582
- useInput10(
1512
+ useInput9(
1583
1513
  (input, key) => {
1584
1514
  if (showConfigureModal || showLinkModal || showStatusModal) return;
1585
1515
  if (input === "c" && jiraState === "not_configured") {
@@ -1711,10 +1641,10 @@ function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
1711
1641
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1712
1642
  function App() {
1713
1643
  const { exit } = useApp();
1714
- const [focusedView, setFocusedView] = useState9("github");
1715
- const [modalOpen, setModalOpen] = useState9(false);
1716
- const [contextBindings, setContextBindings] = useState9([]);
1717
- useInput11(
1644
+ const [focusedView, setFocusedView] = useState8("github");
1645
+ const [modalOpen, setModalOpen] = useState8(false);
1646
+ const [contextBindings, setContextBindings] = useState8([]);
1647
+ useInput10(
1718
1648
  (input, key) => {
1719
1649
  if (key.ctrl && input === "c") {
1720
1650
  exit();
@@ -1733,7 +1663,6 @@ function App() {
1733
1663
  GitHubView,
1734
1664
  {
1735
1665
  isFocused: focusedView === "github",
1736
- onModalChange: setModalOpen,
1737
1666
  onKeybindingsChange: focusedView === "github" ? setContextBindings : void 0
1738
1667
  }
1739
1668
  ),
@@ -1754,7 +1683,7 @@ import { render as inkRender } from "ink";
1754
1683
 
1755
1684
  // src/lib/Screen.tsx
1756
1685
  import { Box as Box14, useStdout } from "ink";
1757
- import { useCallback as useCallback3, useEffect as useEffect6, useState as useState10 } from "react";
1686
+ import { useCallback as useCallback3, useEffect as useEffect6, useState as useState9 } from "react";
1758
1687
  import { jsx as jsx14 } from "react/jsx-runtime";
1759
1688
  function Screen({ children }) {
1760
1689
  const { stdout } = useStdout();
@@ -1762,7 +1691,7 @@ function Screen({ children }) {
1762
1691
  () => ({ height: stdout.rows, width: stdout.columns }),
1763
1692
  [stdout]
1764
1693
  );
1765
- const [size, setSize] = useState10(getSize);
1694
+ const [size, setSize] = useState9(getSize);
1766
1695
  useEffect6(() => {
1767
1696
  const onResize = () => setSize(getSize());
1768
1697
  stdout.on("resize", onResize);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clairo",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,9 +22,12 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "@mishieck/ink-titled-box": "^0.4.2",
25
+ "cli-table3": "^0.6.5",
25
26
  "ink": "^6.6.0",
27
+ "ink-link": "^5.0.0",
26
28
  "ink-scroll-view": "^0.3.5",
27
29
  "ink-select-input": "^6.2.0",
30
+ "marked": "^17.0.1",
28
31
  "meow": "^11.0.0",
29
32
  "open": "^11.0.0",
30
33
  "react": "^19.2.4"