clairo 1.1.0 → 1.2.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.
- package/README.md +13 -13
- package/dist/cli.js +282 -105
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
# clairo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
dashboard tui for github PRs, jira tickets, and daily logs.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## features
|
|
6
|
+
|
|
7
|
+
- branch aware github dashboard: see open PR details, create new PRs
|
|
8
|
+
- claude code integration (requires claude code to be set up) for generating standup notes
|
|
9
|
+
- link jira tickets and change ticket status from the terminal
|
|
10
|
+
- auto jira ticket detection based on branch name
|
|
11
|
+
- daily logs that update automatically with tui actions that can be used for generateStandupNotes
|
|
12
|
+
|
|
13
|
+
## requirements
|
|
6
14
|
|
|
7
15
|
- Node.js 18+
|
|
8
16
|
- [GitHub CLI](https://cli.github.com/) (`gh`) installed and authenticated
|
|
9
17
|
|
|
10
|
-
##
|
|
18
|
+
## usage
|
|
11
19
|
|
|
12
20
|
```bash
|
|
13
21
|
npx clairo
|
|
14
22
|
```
|
|
15
23
|
|
|
16
|
-
###
|
|
24
|
+
### options
|
|
17
25
|
|
|
18
26
|
```
|
|
19
27
|
--cwd <path>, -C Run in a different directory
|
|
@@ -21,7 +29,7 @@ npx clairo
|
|
|
21
29
|
--help Show help
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
###
|
|
32
|
+
### examples
|
|
25
33
|
|
|
26
34
|
```bash
|
|
27
35
|
# Run in current directory
|
|
@@ -30,11 +38,3 @@ npx clairo
|
|
|
30
38
|
# Run in a different repo
|
|
31
39
|
npx clairo --cwd ~/projects/other-repo
|
|
32
40
|
```
|
|
33
|
-
|
|
34
|
-
## Keyboard
|
|
35
|
-
|
|
36
|
-
- `1-6` - Switch between boxes
|
|
37
|
-
- `j/k` - Navigate lists
|
|
38
|
-
- `Enter` - Select
|
|
39
|
-
- `o` - Open in browser
|
|
40
|
-
- `Ctrl+C` - Quit
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { Box as Box16, useApp, useInput as useInput13 } from "ink";
|
|
|
11
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
|
+
import { ScrollView as ScrollView4 } from "ink-scroll-view";
|
|
14
15
|
|
|
15
16
|
// src/hooks/github/useGitRepo.ts
|
|
16
17
|
import { useCallback, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
|
|
@@ -170,7 +171,7 @@ function findRemoteWithBranch(branch) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
// src/lib/github/index.ts
|
|
173
|
-
import { exec } from "child_process";
|
|
174
|
+
import { exec, execFile } from "child_process";
|
|
174
175
|
import { promisify } from "util";
|
|
175
176
|
var execAsync = promisify(exec);
|
|
176
177
|
function resolveCheckStatus(check) {
|
|
@@ -251,12 +252,10 @@ async function listPRsForBranch(branch, repo) {
|
|
|
251
252
|
}
|
|
252
253
|
try {
|
|
253
254
|
const { stdout } = await execAsync(
|
|
254
|
-
`gh pr list --state open --json ${fields}
|
|
255
|
+
`gh pr list --state open --head "${branch}" --json ${fields} --repo "${repo}" 2>/dev/null`
|
|
255
256
|
);
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const result = prs.map(({ headRefName: _headRefName, ...rest }) => rest);
|
|
259
|
-
return { success: true, data: result };
|
|
257
|
+
const prs = JSON.parse(stdout);
|
|
258
|
+
return { success: true, data: prs };
|
|
260
259
|
} catch {
|
|
261
260
|
return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
|
|
262
261
|
}
|
|
@@ -313,6 +312,34 @@ function openPRCreationPage(owner, branch, onComplete) {
|
|
|
313
312
|
onComplete == null ? void 0 : onComplete(error);
|
|
314
313
|
});
|
|
315
314
|
}
|
|
315
|
+
function openPRCreationPageWithContent(owner, branch, title, body, onComplete) {
|
|
316
|
+
const headFlag = `${owner}:${branch}`;
|
|
317
|
+
const args = ["pr", "create", "--web", "--head", headFlag, "--title", title, "--body", body];
|
|
318
|
+
execFile("gh", args, (error) => {
|
|
319
|
+
process.stdout.emit("resize");
|
|
320
|
+
onComplete == null ? void 0 : onComplete(error);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function editPRDescription(prNumber, repo, title, body, onComplete) {
|
|
324
|
+
const args = [
|
|
325
|
+
"api",
|
|
326
|
+
`repos/${repo}/pulls/${prNumber}`,
|
|
327
|
+
"--method",
|
|
328
|
+
"PATCH",
|
|
329
|
+
"--field",
|
|
330
|
+
`title=${title}`,
|
|
331
|
+
"--field",
|
|
332
|
+
`body=${body}`
|
|
333
|
+
];
|
|
334
|
+
execFile("gh", args, (error, _stdout, stderr) => {
|
|
335
|
+
if (error) {
|
|
336
|
+
const message = (stderr == null ? void 0 : stderr.trim()) || error.message;
|
|
337
|
+
onComplete == null ? void 0 : onComplete(new Error(message));
|
|
338
|
+
} else {
|
|
339
|
+
onComplete == null ? void 0 : onComplete(null);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
316
343
|
|
|
317
344
|
// src/hooks/useTerminalFocus.ts
|
|
318
345
|
import { useEffect, useState } from "react";
|
|
@@ -598,6 +625,99 @@ function usePullRequests() {
|
|
|
598
625
|
};
|
|
599
626
|
}
|
|
600
627
|
|
|
628
|
+
// src/lib/claude/api.ts
|
|
629
|
+
import { exec as exec2 } from "child_process";
|
|
630
|
+
function runClaudePrompt(prompt) {
|
|
631
|
+
let childProcess = null;
|
|
632
|
+
let cancelled = false;
|
|
633
|
+
const promise = new Promise((resolve) => {
|
|
634
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
|
|
635
|
+
const command = `claude -p $'${escapedPrompt}' --output-format json < /dev/null`;
|
|
636
|
+
childProcess = exec2(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
637
|
+
if (cancelled) {
|
|
638
|
+
resolve({
|
|
639
|
+
success: false,
|
|
640
|
+
error: "Cancelled",
|
|
641
|
+
errorType: "execution_error"
|
|
642
|
+
});
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (error) {
|
|
646
|
+
if (error.message.includes("command not found") || error.message.includes("ENOENT") || error.code === "ENOENT") {
|
|
647
|
+
resolve({
|
|
648
|
+
success: false,
|
|
649
|
+
error: "Claude CLI not installed. Run: npm install -g @anthropic-ai/claude-code",
|
|
650
|
+
errorType: "not_installed"
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
resolve({
|
|
655
|
+
success: false,
|
|
656
|
+
error: (stderr == null ? void 0 : stderr.trim()) || error.message,
|
|
657
|
+
errorType: "execution_error"
|
|
658
|
+
});
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (!(stdout == null ? void 0 : stdout.trim())) {
|
|
662
|
+
resolve({
|
|
663
|
+
success: false,
|
|
664
|
+
error: (stderr == null ? void 0 : stderr.trim()) || "Claude returned empty response",
|
|
665
|
+
errorType: "execution_error"
|
|
666
|
+
});
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
const json = JSON.parse(stdout.trim());
|
|
671
|
+
if (json.is_error) {
|
|
672
|
+
resolve({
|
|
673
|
+
success: false,
|
|
674
|
+
error: json.result || "Claude returned an error",
|
|
675
|
+
errorType: "execution_error"
|
|
676
|
+
});
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
resolve({
|
|
680
|
+
success: true,
|
|
681
|
+
data: json.result || stdout.trim()
|
|
682
|
+
});
|
|
683
|
+
} catch {
|
|
684
|
+
resolve({
|
|
685
|
+
success: true,
|
|
686
|
+
data: stdout.trim()
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
const cancel = () => {
|
|
692
|
+
cancelled = true;
|
|
693
|
+
if (childProcess) {
|
|
694
|
+
childProcess.kill("SIGTERM");
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
return { promise, cancel };
|
|
698
|
+
}
|
|
699
|
+
function generatePRContent() {
|
|
700
|
+
const prompt = `Your output will be used directly to pre-fill a GitHub pull request creation form. Analyze the current branch's changes and return ONLY a raw JSON object (no markdown fences, no explanation) with "title" and "body" fields.
|
|
701
|
+
|
|
702
|
+
Rules:
|
|
703
|
+
- "title": a concise PR title in imperative mood, under 72 characters
|
|
704
|
+
- "body": a markdown PR description summarizing what changed and why. Use bullet points for multiple changes.
|
|
705
|
+
- Do not include any text outside the JSON object
|
|
706
|
+
- Do not add any "Co-Authored-By" or attribution lines`;
|
|
707
|
+
return runClaudePrompt(prompt);
|
|
708
|
+
}
|
|
709
|
+
function generateStandupNotes(logContent) {
|
|
710
|
+
const prompt = `You are helping a developer prepare standup notes. Based on the following log entries, generate concise standup notes that summarize what was accomplished.
|
|
711
|
+
|
|
712
|
+
Format the output as bullet points grouped by category (e.g., Features, Bug Fixes, Refactoring, etc.). Keep it brief and professional.
|
|
713
|
+
|
|
714
|
+
Log entries:
|
|
715
|
+
${logContent}
|
|
716
|
+
|
|
717
|
+
Generate the standup notes:`;
|
|
718
|
+
return runClaudePrompt(prompt);
|
|
719
|
+
}
|
|
720
|
+
|
|
601
721
|
// src/lib/jira/parser.ts
|
|
602
722
|
var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
603
723
|
function isValidTicketKeyFormat(key) {
|
|
@@ -1624,12 +1744,12 @@ function useRubberDuck() {
|
|
|
1624
1744
|
}
|
|
1625
1745
|
|
|
1626
1746
|
// src/lib/clipboard.ts
|
|
1627
|
-
import { exec as
|
|
1747
|
+
import { exec as exec3 } from "child_process";
|
|
1628
1748
|
async function copyToClipboard(text) {
|
|
1629
1749
|
var _a, _b;
|
|
1630
1750
|
const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
|
|
1631
1751
|
try {
|
|
1632
|
-
const child =
|
|
1752
|
+
const child = exec3(command);
|
|
1633
1753
|
(_a = child.stdin) == null ? void 0 : _a.write(text);
|
|
1634
1754
|
(_b = child.stdin) == null ? void 0 : _b.end();
|
|
1635
1755
|
await new Promise((resolve, reject) => {
|
|
@@ -1655,7 +1775,8 @@ function PullRequestsBox({
|
|
|
1655
1775
|
error,
|
|
1656
1776
|
branch,
|
|
1657
1777
|
repoSlug,
|
|
1658
|
-
isActive
|
|
1778
|
+
isActive,
|
|
1779
|
+
isGeneratingPR
|
|
1659
1780
|
}) {
|
|
1660
1781
|
const [copied, setCopied] = useState10(false);
|
|
1661
1782
|
const selectedIndex = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
@@ -1704,6 +1825,7 @@ function PullRequestsBox({
|
|
|
1704
1825
|
children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
1705
1826
|
loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
|
|
1706
1827
|
error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
1828
|
+
isGeneratingPR && /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "Generating PR with Claude... (Esc to cancel)" }),
|
|
1707
1829
|
!loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
|
|
1708
1830
|
prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
|
|
1709
1831
|
prs.map((pr, idx) => {
|
|
@@ -1783,6 +1905,10 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1783
1905
|
const repo = useGitRepo();
|
|
1784
1906
|
const pullRequests = usePullRequests();
|
|
1785
1907
|
const [focusedBox, setFocusedBox] = useState11("remotes");
|
|
1908
|
+
const [isGeneratingPR, setIsGeneratingPR] = useState11(false);
|
|
1909
|
+
const claudeProcessRef = useRef5(null);
|
|
1910
|
+
const previewScrollRef = useRef5(null);
|
|
1911
|
+
const [prPreview, setPrPreview] = useState11(null);
|
|
1786
1912
|
useEffect8(() => {
|
|
1787
1913
|
if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
|
|
1788
1914
|
pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
|
|
@@ -1809,6 +1935,94 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1809
1935
|
);
|
|
1810
1936
|
const onLogUpdatedRef = useRef5(onLogUpdated);
|
|
1811
1937
|
onLogUpdatedRef.current = onLogUpdated;
|
|
1938
|
+
const prsRef = useRef5(pullRequests.prs);
|
|
1939
|
+
prsRef.current = pullRequests.prs;
|
|
1940
|
+
useEffect8(() => {
|
|
1941
|
+
return () => {
|
|
1942
|
+
var _a;
|
|
1943
|
+
(_a = claudeProcessRef.current) == null ? void 0 : _a.cancel();
|
|
1944
|
+
};
|
|
1945
|
+
}, []);
|
|
1946
|
+
const handleAICreatePR = useCallback8(() => {
|
|
1947
|
+
if (!repo.currentBranch || !repo.currentRepoSlug) {
|
|
1948
|
+
pullRequests.setError("prs", "No branch or repo detected");
|
|
1949
|
+
duckEvents.emit("error");
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
const remoteResult = findRemoteWithBranch(repo.currentBranch);
|
|
1953
|
+
if (!remoteResult.success) {
|
|
1954
|
+
pullRequests.setError("prs", "Push your branch to a remote first");
|
|
1955
|
+
duckEvents.emit("error");
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
setIsGeneratingPR(true);
|
|
1959
|
+
const repoSlug = repo.currentRepoSlug;
|
|
1960
|
+
const branch = repo.currentBranch;
|
|
1961
|
+
const repoPath = repo.repoPath;
|
|
1962
|
+
const owner = remoteResult.data.owner;
|
|
1963
|
+
const process2 = generatePRContent();
|
|
1964
|
+
claudeProcessRef.current = process2;
|
|
1965
|
+
process2.promise.then((result) => {
|
|
1966
|
+
claudeProcessRef.current = null;
|
|
1967
|
+
setIsGeneratingPR(false);
|
|
1968
|
+
if (!result.success) {
|
|
1969
|
+
if (result.error !== "Cancelled") {
|
|
1970
|
+
pullRequests.setError("prs", result.error);
|
|
1971
|
+
duckEvents.emit("error");
|
|
1972
|
+
}
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
let title;
|
|
1976
|
+
let body;
|
|
1977
|
+
try {
|
|
1978
|
+
const cleaned = result.data.replace(/^```(?:json)?\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1979
|
+
const parsed = JSON.parse(cleaned);
|
|
1980
|
+
title = parsed.title;
|
|
1981
|
+
body = parsed.body;
|
|
1982
|
+
} catch {
|
|
1983
|
+
pullRequests.setError("prs", "Failed to parse AI response");
|
|
1984
|
+
duckEvents.emit("error");
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const tickets = repoPath && branch ? getLinkedTickets(repoPath, branch) : [];
|
|
1988
|
+
if (tickets.length > 0) {
|
|
1989
|
+
const siteUrl = repoPath ? getJiraSiteUrl(repoPath) : null;
|
|
1990
|
+
const prefix = tickets.map((t) => t.key).join(" ");
|
|
1991
|
+
title = `${prefix} ${title}`;
|
|
1992
|
+
if (siteUrl) {
|
|
1993
|
+
const baseUrl = siteUrl.replace(/\/$/, "");
|
|
1994
|
+
const links = tickets.map((t) => `[${t.key}](${baseUrl}/browse/${t.key})`).join(", ");
|
|
1995
|
+
const label = tickets.length === 1 ? "Ticket" : "Tickets";
|
|
1996
|
+
body = `${label}: ${links}
|
|
1997
|
+
|
|
1998
|
+
${body}`;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
const currentPrs = prsRef.current;
|
|
2002
|
+
const existingPR = currentPrs.length > 0 ? currentPrs[0] : null;
|
|
2003
|
+
if (existingPR) {
|
|
2004
|
+
setPrPreview({ title, body, prNumber: existingPR.number, repoSlug, branch });
|
|
2005
|
+
} else {
|
|
2006
|
+
openPRCreationPageWithContent(owner, branch, title, body, (error) => {
|
|
2007
|
+
if (error) {
|
|
2008
|
+
pullRequests.setError("prs", `Failed to create PR: ${error.message}`);
|
|
2009
|
+
duckEvents.emit("error");
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
pullRequests.pollForNewPR({
|
|
2013
|
+
branch,
|
|
2014
|
+
repoSlug,
|
|
2015
|
+
onNewPR: (newPR) => {
|
|
2016
|
+
var _a;
|
|
2017
|
+
const ticketKeys = repoPath && branch ? getLinkedTickets(repoPath, branch).map((t) => t.key) : [];
|
|
2018
|
+
logPRCreated(newPR.number, newPR.title, ticketKeys);
|
|
2019
|
+
duckEvents.emit("pr:opened");
|
|
2020
|
+
(_a = onLogUpdatedRef.current) == null ? void 0 : _a.call(onLogUpdatedRef);
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
}, [repo.currentBranch, repo.currentRepoSlug, repo.repoPath, pullRequests.setError, pullRequests.pollForNewPR]);
|
|
1812
2026
|
const handleCreatePR = useCallback8(() => {
|
|
1813
2027
|
if (!repo.currentBranch) {
|
|
1814
2028
|
pullRequests.setError("prs", "No branch detected");
|
|
@@ -1844,7 +2058,42 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1844
2058
|
});
|
|
1845
2059
|
}, [repo.currentBranch, repo.currentRepoSlug, repo.repoPath, pullRequests.setError, pullRequests.pollForNewPR]);
|
|
1846
2060
|
useInput4(
|
|
1847
|
-
(input) => {
|
|
2061
|
+
(input, key) => {
|
|
2062
|
+
var _a, _b, _c;
|
|
2063
|
+
if (key.escape && isGeneratingPR) {
|
|
2064
|
+
(_a = claudeProcessRef.current) == null ? void 0 : _a.cancel();
|
|
2065
|
+
claudeProcessRef.current = null;
|
|
2066
|
+
setIsGeneratingPR(false);
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
if (prPreview) {
|
|
2070
|
+
if (key.escape) {
|
|
2071
|
+
setPrPreview(null);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if (key.upArrow || input === "k") {
|
|
2075
|
+
(_b = previewScrollRef.current) == null ? void 0 : _b.scrollBy(-1);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
if (key.downArrow || input === "j") {
|
|
2079
|
+
(_c = previewScrollRef.current) == null ? void 0 : _c.scrollBy(1);
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
if (key.return) {
|
|
2083
|
+
const { prNumber, repoSlug, branch, title, body } = prPreview;
|
|
2084
|
+
setPrPreview(null);
|
|
2085
|
+
editPRDescription(prNumber, repoSlug, title, body, (error) => {
|
|
2086
|
+
if (error) {
|
|
2087
|
+
pullRequests.setError("prs", `Failed to update PR: ${error.message}`);
|
|
2088
|
+
duckEvents.emit("error");
|
|
2089
|
+
} else {
|
|
2090
|
+
pullRequests.fetchPRsAndDetails(branch, repoSlug);
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
1848
2097
|
if (input === "1") setFocusedBox("remotes");
|
|
1849
2098
|
if (input === "2") setFocusedBox("prs");
|
|
1850
2099
|
if (input === "3") setFocusedBox("details");
|
|
@@ -1857,6 +2106,9 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1857
2106
|
pullRequests.refreshDetails(pullRequests.selectedPR, repo.currentRepoSlug);
|
|
1858
2107
|
}
|
|
1859
2108
|
}
|
|
2109
|
+
if (input === "c" && focusedBox === "prs" && !isGeneratingPR) {
|
|
2110
|
+
handleAICreatePR();
|
|
2111
|
+
}
|
|
1860
2112
|
if (input === "n" && focusedBox === "prs") {
|
|
1861
2113
|
handleCreatePR();
|
|
1862
2114
|
}
|
|
@@ -1875,7 +2127,7 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1875
2127
|
onSelect: handleRemoteSelect,
|
|
1876
2128
|
loading: repo.loading,
|
|
1877
2129
|
error: repo.error,
|
|
1878
|
-
isActive: isActive && focusedBox === "remotes"
|
|
2130
|
+
isActive: isActive && !prPreview && focusedBox === "remotes"
|
|
1879
2131
|
}
|
|
1880
2132
|
),
|
|
1881
2133
|
/* @__PURE__ */ jsx5(
|
|
@@ -1889,7 +2141,8 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1889
2141
|
error: pullRequests.errors.prs,
|
|
1890
2142
|
branch: repo.currentBranch,
|
|
1891
2143
|
repoSlug: repo.currentRepoSlug,
|
|
1892
|
-
isActive: isActive && focusedBox === "prs"
|
|
2144
|
+
isActive: isActive && !prPreview && focusedBox === "prs",
|
|
2145
|
+
isGeneratingPR
|
|
1893
2146
|
}
|
|
1894
2147
|
),
|
|
1895
2148
|
/* @__PURE__ */ jsx5(
|
|
@@ -1898,9 +2151,17 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1898
2151
|
pr: pullRequests.prDetails,
|
|
1899
2152
|
loading: pullRequests.loading.details,
|
|
1900
2153
|
error: pullRequests.errors.details,
|
|
1901
|
-
isActive: isActive && focusedBox === "details"
|
|
2154
|
+
isActive: isActive && !prPreview && focusedBox === "details"
|
|
1902
2155
|
}
|
|
1903
|
-
)
|
|
2156
|
+
),
|
|
2157
|
+
prPreview && /* @__PURE__ */ jsx5(TitledBox3, { borderStyle: "round", titles: ["PR Preview"], borderColor: "yellow", flexGrow: 1, flexBasis: 0, children: /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, flexGrow: 1, flexBasis: 0, overflow: "hidden", children: [
|
|
2158
|
+
/* @__PURE__ */ jsx5(Box5, { flexGrow: 1, flexBasis: 0, overflow: "hidden", children: /* @__PURE__ */ jsx5(ScrollView4, { ref: previewScrollRef, children: /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
2159
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: prPreview.title }),
|
|
2160
|
+
/* @__PURE__ */ jsx5(Text5, { children: "" }),
|
|
2161
|
+
/* @__PURE__ */ jsx5(Text5, { children: prPreview.body })
|
|
2162
|
+
] }) }) }),
|
|
2163
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Enter to apply, Esc to dismiss, j/k to scroll" })
|
|
2164
|
+
] }) })
|
|
1904
2165
|
] });
|
|
1905
2166
|
}
|
|
1906
2167
|
|
|
@@ -2066,7 +2327,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
2066
2327
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
2067
2328
|
import { useState as useState14 } from "react";
|
|
2068
2329
|
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
2069
|
-
import { ScrollView as
|
|
2330
|
+
import { ScrollView as ScrollView5 } from "ink-scroll-view";
|
|
2070
2331
|
|
|
2071
2332
|
// src/lib/editor.ts
|
|
2072
2333
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -2207,7 +2468,7 @@ function ConfigureJiraSiteModal({
|
|
|
2207
2468
|
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Select an existing configuration or enter new credentials" }),
|
|
2208
2469
|
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
2209
2470
|
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(
|
|
2471
|
+
/* @__PURE__ */ jsx9(Box9, { height: listHeight, overflow: "hidden", children: /* @__PURE__ */ jsxs9(ScrollView5, { ref: scrollRef, children: [
|
|
2211
2472
|
existingConfigs.map((config, idx) => {
|
|
2212
2473
|
const isSelected = selectedExisting === idx;
|
|
2213
2474
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
@@ -2451,93 +2712,8 @@ import { Box as Box14, useInput as useInput12 } from "ink";
|
|
|
2451
2712
|
import { useEffect as useEffect11, useRef as useRef7, useState as useState15 } from "react";
|
|
2452
2713
|
import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
|
|
2453
2714
|
import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
|
|
2454
|
-
import { ScrollView as
|
|
2715
|
+
import { ScrollView as ScrollView6 } from "ink-scroll-view";
|
|
2455
2716
|
import TextInput2 from "ink-text-input";
|
|
2456
|
-
|
|
2457
|
-
// src/lib/claude/api.ts
|
|
2458
|
-
import { exec as exec3 } from "child_process";
|
|
2459
|
-
function runClaudePrompt(prompt) {
|
|
2460
|
-
let childProcess = null;
|
|
2461
|
-
let cancelled = false;
|
|
2462
|
-
const promise = new Promise((resolve) => {
|
|
2463
|
-
const escapedPrompt = prompt.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
|
|
2464
|
-
const command = `claude -p $'${escapedPrompt}' --output-format json < /dev/null`;
|
|
2465
|
-
childProcess = exec3(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
2466
|
-
if (cancelled) {
|
|
2467
|
-
resolve({
|
|
2468
|
-
success: false,
|
|
2469
|
-
error: "Cancelled",
|
|
2470
|
-
errorType: "execution_error"
|
|
2471
|
-
});
|
|
2472
|
-
return;
|
|
2473
|
-
}
|
|
2474
|
-
if (error) {
|
|
2475
|
-
if (error.message.includes("command not found") || error.message.includes("ENOENT") || error.code === "ENOENT") {
|
|
2476
|
-
resolve({
|
|
2477
|
-
success: false,
|
|
2478
|
-
error: "Claude CLI not installed. Run: npm install -g @anthropic-ai/claude-code",
|
|
2479
|
-
errorType: "not_installed"
|
|
2480
|
-
});
|
|
2481
|
-
return;
|
|
2482
|
-
}
|
|
2483
|
-
resolve({
|
|
2484
|
-
success: false,
|
|
2485
|
-
error: (stderr == null ? void 0 : stderr.trim()) || error.message,
|
|
2486
|
-
errorType: "execution_error"
|
|
2487
|
-
});
|
|
2488
|
-
return;
|
|
2489
|
-
}
|
|
2490
|
-
if (!(stdout == null ? void 0 : stdout.trim())) {
|
|
2491
|
-
resolve({
|
|
2492
|
-
success: false,
|
|
2493
|
-
error: (stderr == null ? void 0 : stderr.trim()) || "Claude returned empty response",
|
|
2494
|
-
errorType: "execution_error"
|
|
2495
|
-
});
|
|
2496
|
-
return;
|
|
2497
|
-
}
|
|
2498
|
-
try {
|
|
2499
|
-
const json = JSON.parse(stdout.trim());
|
|
2500
|
-
if (json.is_error) {
|
|
2501
|
-
resolve({
|
|
2502
|
-
success: false,
|
|
2503
|
-
error: json.result || "Claude returned an error",
|
|
2504
|
-
errorType: "execution_error"
|
|
2505
|
-
});
|
|
2506
|
-
return;
|
|
2507
|
-
}
|
|
2508
|
-
resolve({
|
|
2509
|
-
success: true,
|
|
2510
|
-
data: json.result || stdout.trim()
|
|
2511
|
-
});
|
|
2512
|
-
} catch {
|
|
2513
|
-
resolve({
|
|
2514
|
-
success: true,
|
|
2515
|
-
data: stdout.trim()
|
|
2516
|
-
});
|
|
2517
|
-
}
|
|
2518
|
-
});
|
|
2519
|
-
});
|
|
2520
|
-
const cancel = () => {
|
|
2521
|
-
cancelled = true;
|
|
2522
|
-
if (childProcess) {
|
|
2523
|
-
childProcess.kill("SIGTERM");
|
|
2524
|
-
}
|
|
2525
|
-
};
|
|
2526
|
-
return { promise, cancel };
|
|
2527
|
-
}
|
|
2528
|
-
function generateStandupNotes(logContent) {
|
|
2529
|
-
const prompt = `You are helping a developer prepare standup notes. Based on the following log entries, generate concise standup notes that summarize what was accomplished.
|
|
2530
|
-
|
|
2531
|
-
Format the output as bullet points grouped by category (e.g., Features, Bug Fixes, Refactoring, etc.). Keep it brief and professional.
|
|
2532
|
-
|
|
2533
|
-
Log entries:
|
|
2534
|
-
${logContent}
|
|
2535
|
-
|
|
2536
|
-
Generate the standup notes:`;
|
|
2537
|
-
return runClaudePrompt(prompt);
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
// src/components/logs/LogViewerBox.tsx
|
|
2541
2717
|
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2542
2718
|
function LogViewerBox({ date, content, isActive, onRefresh, onLogCreated }) {
|
|
2543
2719
|
const scrollRef = useRef7(null);
|
|
@@ -2635,7 +2811,7 @@ ${value.trim()}
|
|
|
2635
2811
|
onRefresh();
|
|
2636
2812
|
};
|
|
2637
2813
|
return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
|
|
2638
|
-
/* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(
|
|
2814
|
+
/* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView6, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
|
|
2639
2815
|
!date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
|
|
2640
2816
|
date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
|
|
2641
2817
|
date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
|
|
@@ -2671,7 +2847,7 @@ ${value.trim()}
|
|
|
2671
2847
|
// src/components/logs/LogsHistoryBox.tsx
|
|
2672
2848
|
import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
|
|
2673
2849
|
import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
|
|
2674
|
-
import { ScrollView as
|
|
2850
|
+
import { ScrollView as ScrollView7 } from "ink-scroll-view";
|
|
2675
2851
|
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2676
2852
|
function LogsHistoryBox({
|
|
2677
2853
|
logFiles,
|
|
@@ -2704,7 +2880,7 @@ function LogsHistoryBox({
|
|
|
2704
2880
|
);
|
|
2705
2881
|
return /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
2706
2882
|
logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
|
|
2707
|
-
logFiles.length > 0 && /* @__PURE__ */ jsx13(
|
|
2883
|
+
logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView7, { ref: scrollRef, children: logFiles.map((file, idx) => {
|
|
2708
2884
|
const isHighlighted = idx === highlightedIndex;
|
|
2709
2885
|
const isSelected = file.date === selectedDate;
|
|
2710
2886
|
const cursor = isHighlighted ? ">" : " ";
|
|
@@ -2792,6 +2968,7 @@ var GITHUB_KEYBINDINGS = {
|
|
|
2792
2968
|
remotes: [{ key: "Space", label: "Select Remote" }],
|
|
2793
2969
|
prs: [
|
|
2794
2970
|
{ key: "Space", label: "Select" },
|
|
2971
|
+
{ key: "c", label: "Generate PR", color: "green" },
|
|
2795
2972
|
{ key: "n", label: "New PR", color: "green" },
|
|
2796
2973
|
{ key: "r", label: "Refresh" },
|
|
2797
2974
|
{ key: "o", label: "Open", color: "green" },
|