clairo 1.0.9 → 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 +551 -372
- package/package.json +1 -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 { useCallback as
|
|
7
|
+
import { useCallback as useCallback9, useMemo as useMemo2, useState as useState16 } from "react";
|
|
8
8
|
import { Box as Box16, useApp, useInput as useInput13 } from "ink";
|
|
9
9
|
|
|
10
10
|
// src/components/github/GitHubView.tsx
|
|
11
|
-
import { useCallback as
|
|
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,9 +171,40 @@ 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);
|
|
177
|
+
function resolveCheckStatus(check) {
|
|
178
|
+
const conclusion = check.conclusion ?? check.state;
|
|
179
|
+
if (conclusion === "SUCCESS" || check.status === "COMPLETED") return "success";
|
|
180
|
+
if (conclusion === "FAILURE" || conclusion === "ERROR") return "failure";
|
|
181
|
+
if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "skipped";
|
|
182
|
+
if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
|
|
183
|
+
return "pending";
|
|
184
|
+
return "pending";
|
|
185
|
+
}
|
|
186
|
+
var CHECK_COLORS = {
|
|
187
|
+
success: "green",
|
|
188
|
+
failure: "red",
|
|
189
|
+
pending: "yellow",
|
|
190
|
+
skipped: "gray"
|
|
191
|
+
};
|
|
192
|
+
var CHECK_ICONS = { success: "\u2713", failure: "\u2717", pending: "\u25CF", skipped: "\u25CB" };
|
|
193
|
+
var CHECK_SORT_ORDER = { failure: 0, pending: 1, skipped: 2, success: 3 };
|
|
194
|
+
function resolveReviewDisplay(reviewDecision) {
|
|
195
|
+
const status = reviewDecision ?? "PENDING";
|
|
196
|
+
if (status === "APPROVED") return { text: status, color: "green" };
|
|
197
|
+
if (status === "CHANGES_REQUESTED") return { text: status, color: "red" };
|
|
198
|
+
return { text: status, color: "yellow" };
|
|
199
|
+
}
|
|
200
|
+
function resolveMergeDisplay(pr) {
|
|
201
|
+
if (!pr) return { text: "UNKNOWN", color: "yellow" };
|
|
202
|
+
if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
|
|
203
|
+
if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
|
|
204
|
+
if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
|
|
205
|
+
if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
|
|
206
|
+
return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
|
|
207
|
+
}
|
|
176
208
|
async function isGhInstalled() {
|
|
177
209
|
try {
|
|
178
210
|
await execAsync("gh --version");
|
|
@@ -220,12 +252,10 @@ async function listPRsForBranch(branch, repo) {
|
|
|
220
252
|
}
|
|
221
253
|
try {
|
|
222
254
|
const { stdout } = await execAsync(
|
|
223
|
-
`gh pr list --state open --json ${fields}
|
|
255
|
+
`gh pr list --state open --head "${branch}" --json ${fields} --repo "${repo}" 2>/dev/null`
|
|
224
256
|
);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
const result = prs.map(({ headRefName: _headRefName, ...rest }) => rest);
|
|
228
|
-
return { success: true, data: result };
|
|
257
|
+
const prs = JSON.parse(stdout);
|
|
258
|
+
return { success: true, data: prs };
|
|
229
259
|
} catch {
|
|
230
260
|
return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
|
|
231
261
|
}
|
|
@@ -282,6 +312,34 @@ function openPRCreationPage(owner, branch, onComplete) {
|
|
|
282
312
|
onComplete == null ? void 0 : onComplete(error);
|
|
283
313
|
});
|
|
284
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
|
+
}
|
|
285
343
|
|
|
286
344
|
// src/hooks/useTerminalFocus.ts
|
|
287
345
|
import { useEffect, useState } from "react";
|
|
@@ -393,6 +451,9 @@ function useGitRepo() {
|
|
|
393
451
|
};
|
|
394
452
|
}
|
|
395
453
|
|
|
454
|
+
// src/hooks/github/usePullRequests.ts
|
|
455
|
+
import { useCallback as useCallback3, useState as useState4 } from "react";
|
|
456
|
+
|
|
396
457
|
// src/hooks/github/usePRPolling.ts
|
|
397
458
|
import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState3 } from "react";
|
|
398
459
|
function usePRPolling() {
|
|
@@ -455,11 +516,11 @@ function usePRPolling() {
|
|
|
455
516
|
}
|
|
456
517
|
|
|
457
518
|
// src/hooks/github/usePullRequests.ts
|
|
458
|
-
import { useCallback as useCallback3, useState as useState4 } from "react";
|
|
459
519
|
function usePullRequests() {
|
|
460
520
|
const [prs, setPrs] = useState4([]);
|
|
461
521
|
const [selectedPR, setSelectedPR] = useState4(null);
|
|
462
522
|
const [prDetails, setPrDetails] = useState4(null);
|
|
523
|
+
const polling = usePRPolling();
|
|
463
524
|
const [loading, setLoading] = useState4({
|
|
464
525
|
prs: false,
|
|
465
526
|
details: false
|
|
@@ -527,6 +588,26 @@ function usePullRequests() {
|
|
|
527
588
|
const setError = useCallback3((key, message) => {
|
|
528
589
|
setErrors((prev) => ({ ...prev, [key]: message }));
|
|
529
590
|
}, []);
|
|
591
|
+
const pollForNewPR = useCallback3(
|
|
592
|
+
(options) => {
|
|
593
|
+
const existingPRNumbers = prs.map((pr) => pr.number);
|
|
594
|
+
polling.startPolling({
|
|
595
|
+
branch: options.branch,
|
|
596
|
+
repoSlug: options.repoSlug,
|
|
597
|
+
existingPRNumbers,
|
|
598
|
+
onPRsUpdated: (updatedPrs) => {
|
|
599
|
+
setPrs(updatedPrs);
|
|
600
|
+
},
|
|
601
|
+
onNewPR: (newPR) => {
|
|
602
|
+
var _a;
|
|
603
|
+
setSelectedPR(newPR);
|
|
604
|
+
refreshDetails(newPR, options.repoSlug);
|
|
605
|
+
(_a = options.onNewPR) == null ? void 0 : _a.call(options, newPR);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
},
|
|
609
|
+
[prs, polling.startPolling, refreshDetails]
|
|
610
|
+
);
|
|
530
611
|
return {
|
|
531
612
|
prs,
|
|
532
613
|
selectedPR,
|
|
@@ -538,12 +619,105 @@ function usePullRequests() {
|
|
|
538
619
|
loading,
|
|
539
620
|
errors,
|
|
540
621
|
setError,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
622
|
+
pollForNewPR,
|
|
623
|
+
stopPolling: polling.stopPolling,
|
|
624
|
+
isPolling: polling.isPolling
|
|
544
625
|
};
|
|
545
626
|
}
|
|
546
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
|
+
|
|
547
721
|
// src/lib/jira/parser.ts
|
|
548
722
|
var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
549
723
|
function isValidTicketKeyFormat(key) {
|
|
@@ -1042,52 +1216,14 @@ function renderInlineToString(tokens) {
|
|
|
1042
1216
|
|
|
1043
1217
|
// src/components/github/PRDetailsBox.tsx
|
|
1044
1218
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1045
|
-
function
|
|
1046
|
-
const conclusion = check.conclusion ?? check.state;
|
|
1047
|
-
if (conclusion === "SUCCESS") return "green";
|
|
1048
|
-
if (conclusion === "FAILURE" || conclusion === "ERROR") return "red";
|
|
1049
|
-
if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "gray";
|
|
1050
|
-
if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
|
|
1051
|
-
return "yellow";
|
|
1052
|
-
if (check.status === "COMPLETED") return "green";
|
|
1053
|
-
return void 0;
|
|
1054
|
-
}
|
|
1055
|
-
function getCheckIcon(check) {
|
|
1056
|
-
const conclusion = check.conclusion ?? check.state;
|
|
1057
|
-
if (conclusion === "SUCCESS") return "\u2713";
|
|
1058
|
-
if (conclusion === "FAILURE" || conclusion === "ERROR") return "\u2717";
|
|
1059
|
-
if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "\u25CB";
|
|
1060
|
-
if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
|
|
1061
|
-
return "\u25CF";
|
|
1062
|
-
if (check.status === "COMPLETED") return "\u2713";
|
|
1063
|
-
return "?";
|
|
1064
|
-
}
|
|
1065
|
-
function getCheckSortOrder(check) {
|
|
1066
|
-
const conclusion = check.conclusion ?? check.state;
|
|
1067
|
-
if (conclusion === "FAILURE" || conclusion === "ERROR") return 0;
|
|
1068
|
-
if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
|
|
1069
|
-
return 1;
|
|
1070
|
-
if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return 2;
|
|
1071
|
-
if (conclusion === "SUCCESS" || check.status === "COMPLETED") return 3;
|
|
1072
|
-
return 4;
|
|
1073
|
-
}
|
|
1074
|
-
function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
1219
|
+
function PRDetailsBox({ pr, loading, error, isActive }) {
|
|
1075
1220
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
1076
1221
|
const scrollRef = useRef2(null);
|
|
1077
1222
|
const title = "[3] PR Details";
|
|
1078
|
-
const borderColor =
|
|
1223
|
+
const borderColor = isActive ? "yellow" : void 0;
|
|
1079
1224
|
const displayTitle = pr ? `${title} - #${pr.number}` : title;
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1082
|
-
const getMergeDisplay = () => {
|
|
1083
|
-
if (!pr) return { text: "UNKNOWN", color: "yellow" };
|
|
1084
|
-
if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
|
|
1085
|
-
if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
|
|
1086
|
-
if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
|
|
1087
|
-
if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
|
|
1088
|
-
return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
|
|
1089
|
-
};
|
|
1090
|
-
const mergeDisplay = getMergeDisplay();
|
|
1225
|
+
const reviewDisplay = resolveReviewDisplay((pr == null ? void 0 : pr.reviewDecision) ?? null);
|
|
1226
|
+
const mergeDisplay = resolveMergeDisplay(pr);
|
|
1091
1227
|
useInput(
|
|
1092
1228
|
(input, key) => {
|
|
1093
1229
|
var _a2, _b2;
|
|
@@ -1102,7 +1238,7 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
|
1102
1238
|
});
|
|
1103
1239
|
}
|
|
1104
1240
|
},
|
|
1105
|
-
{ isActive
|
|
1241
|
+
{ isActive }
|
|
1106
1242
|
);
|
|
1107
1243
|
const { stdout } = useStdout();
|
|
1108
1244
|
const terminalWidth = (stdout == null ? void 0 : stdout.columns) ?? 80;
|
|
@@ -1137,7 +1273,7 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
|
1137
1273
|
] }),
|
|
1138
1274
|
/* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
|
|
1139
1275
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Review: " }),
|
|
1140
|
-
/* @__PURE__ */ jsx2(Text2, { color:
|
|
1276
|
+
/* @__PURE__ */ jsx2(Text2, { color: reviewDisplay.color, children: reviewDisplay.text }),
|
|
1141
1277
|
/* @__PURE__ */ jsx2(Text2, { children: " | " }),
|
|
1142
1278
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Status: " }),
|
|
1143
1279
|
/* @__PURE__ */ jsx2(Text2, { color: mergeDisplay.color, children: mergeDisplay.text })
|
|
@@ -1174,12 +1310,13 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
|
1174
1310
|
}
|
|
1175
1311
|
return acc;
|
|
1176
1312
|
}, /* @__PURE__ */ new Map()).values()) ?? []
|
|
1177
|
-
).sort((a, b) =>
|
|
1313
|
+
).sort((a, b) => CHECK_SORT_ORDER[resolveCheckStatus(a)] - CHECK_SORT_ORDER[resolveCheckStatus(b)]).map((check, idx) => {
|
|
1178
1314
|
const jobName = check.name ?? check.context;
|
|
1179
1315
|
const displayName = check.workflowName ? `${check.workflowName} / ${jobName}` : jobName;
|
|
1180
|
-
|
|
1316
|
+
const status = resolveCheckStatus(check);
|
|
1317
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: CHECK_COLORS[status], children: [
|
|
1181
1318
|
" ",
|
|
1182
|
-
|
|
1319
|
+
CHECK_ICONS[status],
|
|
1183
1320
|
" ",
|
|
1184
1321
|
displayName
|
|
1185
1322
|
] }, idx);
|
|
@@ -1198,9 +1335,9 @@ function PRDetailsBox({ pr, loading, error, isFocused }) {
|
|
|
1198
1335
|
|
|
1199
1336
|
// src/components/github/PullRequestsBox.tsx
|
|
1200
1337
|
import open2 from "open";
|
|
1201
|
-
import {
|
|
1338
|
+
import { useState as useState10 } from "react";
|
|
1202
1339
|
import { TitledBox } from "@mishieck/ink-titled-box";
|
|
1203
|
-
import { Box as Box3, Text as Text3, useInput as
|
|
1340
|
+
import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
|
|
1204
1341
|
import { ScrollView as ScrollView2 } from "ink-scroll-view";
|
|
1205
1342
|
|
|
1206
1343
|
// src/hooks/jira/useJiraTickets.ts
|
|
@@ -1458,25 +1595,8 @@ function useModal() {
|
|
|
1458
1595
|
}
|
|
1459
1596
|
|
|
1460
1597
|
// src/hooks/useListNavigation.ts
|
|
1461
|
-
import {
|
|
1462
|
-
|
|
1463
|
-
const [index, setIndex] = useState8(0);
|
|
1464
|
-
const prev = useCallback7(() => {
|
|
1465
|
-
setIndex((i) => Math.max(0, i - 1));
|
|
1466
|
-
}, []);
|
|
1467
|
-
const next = useCallback7(() => {
|
|
1468
|
-
setIndex((i) => Math.min(length - 1, i + 1));
|
|
1469
|
-
}, [length]);
|
|
1470
|
-
const clampedIndex = Math.min(index, Math.max(0, length - 1));
|
|
1471
|
-
const reset = useCallback7(() => setIndex(0), []);
|
|
1472
|
-
return {
|
|
1473
|
-
index: length === 0 ? 0 : clampedIndex,
|
|
1474
|
-
prev,
|
|
1475
|
-
next,
|
|
1476
|
-
reset,
|
|
1477
|
-
setIndex
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1598
|
+
import { useEffect as useEffect6, useState as useState8 } from "react";
|
|
1599
|
+
import { useInput as useInput2 } from "ink";
|
|
1480
1600
|
|
|
1481
1601
|
// src/hooks/useScrollToIndex.ts
|
|
1482
1602
|
import { useEffect as useEffect5, useRef as useRef4 } from "react";
|
|
@@ -1498,8 +1618,53 @@ function useScrollToIndex(index) {
|
|
|
1498
1618
|
return scrollRef;
|
|
1499
1619
|
}
|
|
1500
1620
|
|
|
1621
|
+
// src/hooks/useListNavigation.ts
|
|
1622
|
+
function useListNavigation({
|
|
1623
|
+
items,
|
|
1624
|
+
totalItems,
|
|
1625
|
+
selectedIndex,
|
|
1626
|
+
onSelect,
|
|
1627
|
+
isActive
|
|
1628
|
+
}) {
|
|
1629
|
+
const navLength = totalItems ?? items.length;
|
|
1630
|
+
const [highlightedIndex, setHighlightedIndex] = useState8(0);
|
|
1631
|
+
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
1632
|
+
useEffect6(() => {
|
|
1633
|
+
if (selectedIndex !== void 0 && selectedIndex >= 0) {
|
|
1634
|
+
setHighlightedIndex(selectedIndex);
|
|
1635
|
+
}
|
|
1636
|
+
}, [selectedIndex]);
|
|
1637
|
+
const prev = () => setHighlightedIndex((i) => Math.max(0, i - 1));
|
|
1638
|
+
const next = () => setHighlightedIndex((i) => Math.min(navLength - 1, i + 1));
|
|
1639
|
+
useInput2(
|
|
1640
|
+
(input, key) => {
|
|
1641
|
+
if (navLength === 0) return;
|
|
1642
|
+
if (key.upArrow || input === "k") {
|
|
1643
|
+
prev();
|
|
1644
|
+
}
|
|
1645
|
+
if (key.downArrow || input === "j") {
|
|
1646
|
+
next();
|
|
1647
|
+
}
|
|
1648
|
+
if (input === " " && onSelect) {
|
|
1649
|
+
onSelect(highlightedIndex);
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
{ isActive: isActive === true }
|
|
1653
|
+
);
|
|
1654
|
+
const clampedIndex = navLength === 0 ? 0 : Math.min(highlightedIndex, Math.max(0, navLength - 1));
|
|
1655
|
+
return {
|
|
1656
|
+
highlightedIndex: clampedIndex,
|
|
1657
|
+
index: clampedIndex,
|
|
1658
|
+
scrollRef,
|
|
1659
|
+
prev,
|
|
1660
|
+
next,
|
|
1661
|
+
reset: () => setHighlightedIndex(0),
|
|
1662
|
+
setIndex: setHighlightedIndex
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1501
1666
|
// src/hooks/useRubberDuck.ts
|
|
1502
|
-
import { useCallback as
|
|
1667
|
+
import { useCallback as useCallback7, useEffect as useEffect7, useState as useState9 } from "react";
|
|
1503
1668
|
var DUCK_MESSAGES = [
|
|
1504
1669
|
"Quack.",
|
|
1505
1670
|
"Quack quack quack.",
|
|
@@ -1537,18 +1702,18 @@ function useRubberDuck() {
|
|
|
1537
1702
|
visible: false,
|
|
1538
1703
|
message: DUCK_MESSAGES[0]
|
|
1539
1704
|
});
|
|
1540
|
-
const getRandomMessage =
|
|
1705
|
+
const getRandomMessage = useCallback7(() => {
|
|
1541
1706
|
const index = Math.floor(Math.random() * DUCK_MESSAGES.length);
|
|
1542
1707
|
return DUCK_MESSAGES[index];
|
|
1543
1708
|
}, []);
|
|
1544
|
-
const toggleDuck =
|
|
1709
|
+
const toggleDuck = useCallback7(() => {
|
|
1545
1710
|
setState((prev) => ({
|
|
1546
1711
|
...prev,
|
|
1547
1712
|
visible: !prev.visible,
|
|
1548
1713
|
message: !prev.visible ? getRandomMessage() : prev.message
|
|
1549
1714
|
}));
|
|
1550
1715
|
}, [getRandomMessage]);
|
|
1551
|
-
const quack =
|
|
1716
|
+
const quack = useCallback7(() => {
|
|
1552
1717
|
if (state.visible) {
|
|
1553
1718
|
setState((prev) => ({
|
|
1554
1719
|
...prev,
|
|
@@ -1556,11 +1721,11 @@ function useRubberDuck() {
|
|
|
1556
1721
|
}));
|
|
1557
1722
|
}
|
|
1558
1723
|
}, [state.visible, getRandomMessage]);
|
|
1559
|
-
const getReactionMessage =
|
|
1724
|
+
const getReactionMessage = useCallback7((event) => {
|
|
1560
1725
|
const messages = REACTION_MESSAGES[event];
|
|
1561
1726
|
return messages[Math.floor(Math.random() * messages.length)];
|
|
1562
1727
|
}, []);
|
|
1563
|
-
|
|
1728
|
+
useEffect7(() => {
|
|
1564
1729
|
const unsubscribe = duckEvents.subscribe((event) => {
|
|
1565
1730
|
setState((prev) => ({
|
|
1566
1731
|
...prev,
|
|
@@ -1579,12 +1744,12 @@ function useRubberDuck() {
|
|
|
1579
1744
|
}
|
|
1580
1745
|
|
|
1581
1746
|
// src/lib/clipboard.ts
|
|
1582
|
-
import { exec as
|
|
1747
|
+
import { exec as exec3 } from "child_process";
|
|
1583
1748
|
async function copyToClipboard(text) {
|
|
1584
1749
|
var _a, _b;
|
|
1585
1750
|
const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
|
|
1586
1751
|
try {
|
|
1587
|
-
const child =
|
|
1752
|
+
const child = exec3(command);
|
|
1588
1753
|
(_a = child.stdin) == null ? void 0 : _a.write(text);
|
|
1589
1754
|
(_b = child.stdin) == null ? void 0 : _b.end();
|
|
1590
1755
|
await new Promise((resolve, reject) => {
|
|
@@ -1610,32 +1775,26 @@ function PullRequestsBox({
|
|
|
1610
1775
|
error,
|
|
1611
1776
|
branch,
|
|
1612
1777
|
repoSlug,
|
|
1613
|
-
|
|
1778
|
+
isActive,
|
|
1779
|
+
isGeneratingPR
|
|
1614
1780
|
}) {
|
|
1615
|
-
const [highlightedIndex, setHighlightedIndex] = useState10(0);
|
|
1616
1781
|
const [copied, setCopied] = useState10(false);
|
|
1617
|
-
const
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (
|
|
1626
|
-
|
|
1627
|
-
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
1628
|
-
}
|
|
1629
|
-
if (key.downArrow || input === "j") {
|
|
1630
|
-
setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
|
|
1631
|
-
}
|
|
1632
|
-
if (input === " ") {
|
|
1633
|
-
if (highlightedIndex === prs.length) {
|
|
1634
|
-
onCreatePR();
|
|
1635
|
-
} else if (prs[highlightedIndex]) {
|
|
1636
|
-
onSelect(prs[highlightedIndex]);
|
|
1637
|
-
}
|
|
1782
|
+
const selectedIndex = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
|
|
1783
|
+
const { highlightedIndex, scrollRef } = useListNavigation({
|
|
1784
|
+
items: prs,
|
|
1785
|
+
totalItems: prs.length + 1,
|
|
1786
|
+
selectedIndex: selectedIndex >= 0 ? selectedIndex : void 0,
|
|
1787
|
+
onSelect: (index) => {
|
|
1788
|
+
if (index === prs.length) {
|
|
1789
|
+
onCreatePR();
|
|
1790
|
+
} else if (prs[index]) {
|
|
1791
|
+
onSelect(prs[index]);
|
|
1638
1792
|
}
|
|
1793
|
+
},
|
|
1794
|
+
isActive
|
|
1795
|
+
});
|
|
1796
|
+
useInput3(
|
|
1797
|
+
(input) => {
|
|
1639
1798
|
if (input === "y" && repoSlug && prs[highlightedIndex]) {
|
|
1640
1799
|
const pr = prs[highlightedIndex];
|
|
1641
1800
|
const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
|
|
@@ -1650,12 +1809,12 @@ function PullRequestsBox({
|
|
|
1650
1809
|
});
|
|
1651
1810
|
}
|
|
1652
1811
|
},
|
|
1653
|
-
{ isActive
|
|
1812
|
+
{ isActive }
|
|
1654
1813
|
);
|
|
1655
1814
|
const title = "[2] Pull Requests";
|
|
1656
1815
|
const subtitle = branch ? ` (${branch})` : "";
|
|
1657
1816
|
const copiedIndicator = copied ? " [Copied!]" : "";
|
|
1658
|
-
const borderColor =
|
|
1817
|
+
const borderColor = isActive ? "yellow" : void 0;
|
|
1659
1818
|
return /* @__PURE__ */ jsx3(
|
|
1660
1819
|
TitledBox,
|
|
1661
1820
|
{
|
|
@@ -1666,10 +1825,11 @@ function PullRequestsBox({
|
|
|
1666
1825
|
children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
1667
1826
|
loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
|
|
1668
1827
|
error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
1828
|
+
isGeneratingPR && /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "Generating PR with Claude... (Esc to cancel)" }),
|
|
1669
1829
|
!loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
|
|
1670
1830
|
prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
|
|
1671
1831
|
prs.map((pr, idx) => {
|
|
1672
|
-
const isHighlighted =
|
|
1832
|
+
const isHighlighted = isActive && idx === highlightedIndex;
|
|
1673
1833
|
const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
|
|
1674
1834
|
const cursor = isHighlighted ? ">" : " ";
|
|
1675
1835
|
const indicator = isSelected ? " *" : "";
|
|
@@ -1689,7 +1849,7 @@ function PullRequestsBox({
|
|
|
1689
1849
|
] }, pr.number);
|
|
1690
1850
|
}),
|
|
1691
1851
|
/* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
|
|
1692
|
-
|
|
1852
|
+
isActive && highlightedIndex === prs.length ? "> " : " ",
|
|
1693
1853
|
"+ Create new PR"
|
|
1694
1854
|
] }, "create")
|
|
1695
1855
|
] })
|
|
@@ -1699,41 +1859,26 @@ function PullRequestsBox({
|
|
|
1699
1859
|
}
|
|
1700
1860
|
|
|
1701
1861
|
// src/components/github/RemotesBox.tsx
|
|
1702
|
-
import { useEffect as useEffect8, useState as useState11 } from "react";
|
|
1703
1862
|
import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
|
|
1704
|
-
import { Box as Box4, Text as Text4
|
|
1863
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
1705
1864
|
import { ScrollView as ScrollView3 } from "ink-scroll-view";
|
|
1706
1865
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1707
|
-
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error,
|
|
1708
|
-
const
|
|
1709
|
-
const scrollRef =
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
(input, key) => {
|
|
1716
|
-
if (!isFocused || remotes.length === 0) return;
|
|
1717
|
-
if (key.upArrow || input === "k") {
|
|
1718
|
-
setHighlightedIndex((prev) => Math.max(0, prev - 1));
|
|
1719
|
-
}
|
|
1720
|
-
if (key.downArrow || input === "j") {
|
|
1721
|
-
setHighlightedIndex((prev) => Math.min(remotes.length - 1, prev + 1));
|
|
1722
|
-
}
|
|
1723
|
-
if (input === " ") {
|
|
1724
|
-
onSelect(remotes[highlightedIndex].name);
|
|
1725
|
-
}
|
|
1726
|
-
},
|
|
1727
|
-
{ isActive: isFocused }
|
|
1728
|
-
);
|
|
1866
|
+
function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isActive }) {
|
|
1867
|
+
const selectedIndex = remotes.findIndex((r) => r.name === selectedRemote);
|
|
1868
|
+
const { highlightedIndex, scrollRef } = useListNavigation({
|
|
1869
|
+
items: remotes,
|
|
1870
|
+
selectedIndex: selectedIndex >= 0 ? selectedIndex : void 0,
|
|
1871
|
+
onSelect: (index) => onSelect(remotes[index].name),
|
|
1872
|
+
isActive
|
|
1873
|
+
});
|
|
1729
1874
|
const title = "[1] Remotes";
|
|
1730
|
-
const borderColor =
|
|
1875
|
+
const borderColor = isActive ? "yellow" : void 0;
|
|
1731
1876
|
return /* @__PURE__ */ jsx4(TitledBox2, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
|
|
1732
1877
|
loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
|
|
1733
1878
|
error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
|
|
1734
1879
|
!loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
|
|
1735
1880
|
!loading && !error && remotes.length > 0 && /* @__PURE__ */ jsx4(ScrollView3, { ref: scrollRef, children: remotes.map((remote, idx) => {
|
|
1736
|
-
const isHighlighted =
|
|
1881
|
+
const isHighlighted = isActive && idx === highlightedIndex;
|
|
1737
1882
|
const isSelected = remote.name === selectedRemote;
|
|
1738
1883
|
const cursor = isHighlighted ? ">" : " ";
|
|
1739
1884
|
const indicator = isSelected ? " *" : "";
|
|
@@ -1756,91 +1901,199 @@ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocus
|
|
|
1756
1901
|
|
|
1757
1902
|
// src/components/github/GitHubView.tsx
|
|
1758
1903
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1759
|
-
function GitHubView({
|
|
1904
|
+
function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
|
|
1760
1905
|
const repo = useGitRepo();
|
|
1761
1906
|
const pullRequests = usePullRequests();
|
|
1762
|
-
const
|
|
1763
|
-
const [
|
|
1764
|
-
const
|
|
1765
|
-
|
|
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);
|
|
1912
|
+
useEffect8(() => {
|
|
1766
1913
|
if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
|
|
1767
|
-
const current = { branch: repo.currentBranch, repoSlug: repo.currentRepoSlug };
|
|
1768
|
-
const last = lastFetchedRef.current;
|
|
1769
|
-
if (last && last.branch === current.branch && last.repoSlug === current.repoSlug) return;
|
|
1770
|
-
lastFetchedRef.current = current;
|
|
1771
1914
|
pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
|
|
1772
1915
|
}, [repo.loading, repo.currentBranch, repo.currentRepoSlug, pullRequests.fetchPRsAndDetails]);
|
|
1773
|
-
|
|
1774
|
-
if (
|
|
1916
|
+
useEffect8(() => {
|
|
1917
|
+
if (isActive) {
|
|
1775
1918
|
repo.refreshBranch();
|
|
1776
1919
|
}
|
|
1777
|
-
}, [
|
|
1778
|
-
|
|
1920
|
+
}, [isActive, repo.refreshBranch]);
|
|
1921
|
+
useEffect8(() => {
|
|
1779
1922
|
onFocusedBoxChange == null ? void 0 : onFocusedBoxChange(focusedBox);
|
|
1780
1923
|
}, [focusedBox, onFocusedBoxChange]);
|
|
1781
|
-
const handleRemoteSelect =
|
|
1924
|
+
const handleRemoteSelect = useCallback8(
|
|
1782
1925
|
(remoteName) => {
|
|
1783
1926
|
repo.selectRemote(remoteName);
|
|
1784
|
-
const remote = repo.remotes.find((r) => r.name === remoteName);
|
|
1785
|
-
if (!remote || !repo.currentBranch) return;
|
|
1786
|
-
const repoSlug = getRepoFromRemote(remote.url);
|
|
1787
|
-
if (!repoSlug) return;
|
|
1788
|
-
lastFetchedRef.current = { branch: repo.currentBranch, repoSlug };
|
|
1789
|
-
pullRequests.fetchPRsAndDetails(repo.currentBranch, repoSlug);
|
|
1790
1927
|
},
|
|
1791
|
-
[repo.selectRemote
|
|
1928
|
+
[repo.selectRemote]
|
|
1792
1929
|
);
|
|
1793
|
-
const handlePRSelect =
|
|
1930
|
+
const handlePRSelect = useCallback8(
|
|
1794
1931
|
(pr) => {
|
|
1795
1932
|
pullRequests.selectPR(pr, repo.currentRepoSlug);
|
|
1796
1933
|
},
|
|
1797
1934
|
[pullRequests.selectPR, repo.currentRepoSlug]
|
|
1798
1935
|
);
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1936
|
+
const onLogUpdatedRef = useRef5(onLogUpdated);
|
|
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]);
|
|
2026
|
+
const handleCreatePR = useCallback8(() => {
|
|
2027
|
+
if (!repo.currentBranch) {
|
|
2028
|
+
pullRequests.setError("prs", "No branch detected");
|
|
1805
2029
|
duckEvents.emit("error");
|
|
1806
2030
|
return;
|
|
1807
2031
|
}
|
|
1808
|
-
const remoteResult = findRemoteWithBranch(
|
|
2032
|
+
const remoteResult = findRemoteWithBranch(repo.currentBranch);
|
|
1809
2033
|
if (!remoteResult.success) {
|
|
1810
|
-
|
|
2034
|
+
pullRequests.setError("prs", "Push your branch to a remote first");
|
|
1811
2035
|
duckEvents.emit("error");
|
|
1812
2036
|
return;
|
|
1813
2037
|
}
|
|
1814
|
-
openPRCreationPage(remoteResult.data.owner,
|
|
2038
|
+
openPRCreationPage(remoteResult.data.owner, repo.currentBranch, (error) => {
|
|
1815
2039
|
if (error) {
|
|
1816
|
-
|
|
2040
|
+
pullRequests.setError("prs", `Failed to create PR: ${error.message}`);
|
|
1817
2041
|
duckEvents.emit("error");
|
|
1818
2042
|
}
|
|
1819
2043
|
});
|
|
1820
|
-
if (!
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
},
|
|
2044
|
+
if (!repo.currentRepoSlug) return;
|
|
2045
|
+
const repoPath = repo.repoPath;
|
|
2046
|
+
const branch = repo.currentBranch;
|
|
2047
|
+
const repoSlug = repo.currentRepoSlug;
|
|
2048
|
+
pullRequests.pollForNewPR({
|
|
2049
|
+
branch,
|
|
2050
|
+
repoSlug,
|
|
1828
2051
|
onNewPR: (newPR) => {
|
|
1829
2052
|
var _a;
|
|
1830
|
-
const
|
|
1831
|
-
const tickets = ctx.repo.repoPath && ctx.repo.currentBranch ? getLinkedTickets(ctx.repo.repoPath, ctx.repo.currentBranch).map((t) => t.key) : [];
|
|
2053
|
+
const tickets = repoPath && branch ? getLinkedTickets(repoPath, branch).map((t) => t.key) : [];
|
|
1832
2054
|
logPRCreated(newPR.number, newPR.title, tickets);
|
|
1833
2055
|
duckEvents.emit("pr:opened");
|
|
1834
|
-
(_a =
|
|
1835
|
-
ctx.pullRequests.setSelectedPR(newPR);
|
|
1836
|
-
if (ctx.repo.currentRepoSlug) {
|
|
1837
|
-
ctx.pullRequests.refreshDetails(newPR, ctx.repo.currentRepoSlug);
|
|
1838
|
-
}
|
|
2056
|
+
(_a = onLogUpdatedRef.current) == null ? void 0 : _a.call(onLogUpdatedRef);
|
|
1839
2057
|
}
|
|
1840
2058
|
});
|
|
1841
|
-
}, [
|
|
2059
|
+
}, [repo.currentBranch, repo.currentRepoSlug, repo.repoPath, pullRequests.setError, pullRequests.pollForNewPR]);
|
|
1842
2060
|
useInput4(
|
|
1843
|
-
(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
|
+
}
|
|
1844
2097
|
if (input === "1") setFocusedBox("remotes");
|
|
1845
2098
|
if (input === "2") setFocusedBox("prs");
|
|
1846
2099
|
if (input === "3") setFocusedBox("details");
|
|
@@ -1853,11 +2106,14 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1853
2106
|
pullRequests.refreshDetails(pullRequests.selectedPR, repo.currentRepoSlug);
|
|
1854
2107
|
}
|
|
1855
2108
|
}
|
|
2109
|
+
if (input === "c" && focusedBox === "prs" && !isGeneratingPR) {
|
|
2110
|
+
handleAICreatePR();
|
|
2111
|
+
}
|
|
1856
2112
|
if (input === "n" && focusedBox === "prs") {
|
|
1857
2113
|
handleCreatePR();
|
|
1858
2114
|
}
|
|
1859
2115
|
},
|
|
1860
|
-
{ isActive
|
|
2116
|
+
{ isActive }
|
|
1861
2117
|
);
|
|
1862
2118
|
if (repo.isRepo === false) {
|
|
1863
2119
|
return /* @__PURE__ */ jsx5(TitledBox3, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
|
|
@@ -1871,7 +2127,7 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1871
2127
|
onSelect: handleRemoteSelect,
|
|
1872
2128
|
loading: repo.loading,
|
|
1873
2129
|
error: repo.error,
|
|
1874
|
-
|
|
2130
|
+
isActive: isActive && !prPreview && focusedBox === "remotes"
|
|
1875
2131
|
}
|
|
1876
2132
|
),
|
|
1877
2133
|
/* @__PURE__ */ jsx5(
|
|
@@ -1885,7 +2141,8 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1885
2141
|
error: pullRequests.errors.prs,
|
|
1886
2142
|
branch: repo.currentBranch,
|
|
1887
2143
|
repoSlug: repo.currentRepoSlug,
|
|
1888
|
-
|
|
2144
|
+
isActive: isActive && !prPreview && focusedBox === "prs",
|
|
2145
|
+
isGeneratingPR
|
|
1889
2146
|
}
|
|
1890
2147
|
),
|
|
1891
2148
|
/* @__PURE__ */ jsx5(
|
|
@@ -1894,18 +2151,26 @@ function GitHubView({ isFocused, onFocusedBoxChange, onLogUpdated }) {
|
|
|
1894
2151
|
pr: pullRequests.prDetails,
|
|
1895
2152
|
loading: pullRequests.loading.details,
|
|
1896
2153
|
error: pullRequests.errors.details,
|
|
1897
|
-
|
|
2154
|
+
isActive: isActive && !prPreview && focusedBox === "details"
|
|
1898
2155
|
}
|
|
1899
|
-
)
|
|
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
|
+
] }) })
|
|
1900
2165
|
] });
|
|
1901
2166
|
}
|
|
1902
2167
|
|
|
1903
2168
|
// src/components/jira/JiraView.tsx
|
|
1904
2169
|
import open3 from "open";
|
|
1905
|
-
import { useEffect as
|
|
2170
|
+
import { useEffect as useEffect10, useRef as useRef6 } from "react";
|
|
1906
2171
|
|
|
1907
2172
|
// src/components/jira/LinkTicketModal.tsx
|
|
1908
|
-
import { useState as
|
|
2173
|
+
import { useState as useState12 } from "react";
|
|
1909
2174
|
import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
|
|
1910
2175
|
|
|
1911
2176
|
// src/components/ui/TextInput.tsx
|
|
@@ -1940,7 +2205,7 @@ function TextInput({ value, onChange, placeholder, isActive, mask }) {
|
|
|
1940
2205
|
// src/components/jira/LinkTicketModal.tsx
|
|
1941
2206
|
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1942
2207
|
function LinkTicketModal({ onSubmit, onCancel, loading, error }) {
|
|
1943
|
-
const [ticketInput, setTicketInput] =
|
|
2208
|
+
const [ticketInput, setTicketInput] = useState12("");
|
|
1944
2209
|
const canSubmit = ticketInput.trim().length > 0;
|
|
1945
2210
|
useInput6(
|
|
1946
2211
|
(_input, key) => {
|
|
@@ -1974,16 +2239,16 @@ import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
|
|
|
1974
2239
|
import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
|
|
1975
2240
|
|
|
1976
2241
|
// src/components/jira/ChangeStatusModal.tsx
|
|
1977
|
-
import { useEffect as
|
|
2242
|
+
import { useEffect as useEffect9, useState as useState13 } from "react";
|
|
1978
2243
|
import { Box as Box8, Text as Text8, useInput as useInput7 } from "ink";
|
|
1979
2244
|
import SelectInput from "ink-select-input";
|
|
1980
2245
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1981
2246
|
function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onCancel }) {
|
|
1982
|
-
const [transitions, setTransitions] =
|
|
1983
|
-
const [loading, setLoading] =
|
|
1984
|
-
const [applying, setApplying] =
|
|
1985
|
-
const [error, setError] =
|
|
1986
|
-
|
|
2247
|
+
const [transitions, setTransitions] = useState13([]);
|
|
2248
|
+
const [loading, setLoading] = useState13(true);
|
|
2249
|
+
const [applying, setApplying] = useState13(false);
|
|
2250
|
+
const [error, setError] = useState13(null);
|
|
2251
|
+
useEffect9(() => {
|
|
1987
2252
|
const fetchTransitions = async () => {
|
|
1988
2253
|
const siteUrl = getJiraSiteUrl(repoPath);
|
|
1989
2254
|
const creds = getJiraCredentials(repoPath);
|
|
@@ -2060,9 +2325,9 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
|
|
|
2060
2325
|
}
|
|
2061
2326
|
|
|
2062
2327
|
// src/components/jira/ConfigureJiraSiteModal.tsx
|
|
2063
|
-
import { useState as
|
|
2328
|
+
import { useState as useState14 } from "react";
|
|
2064
2329
|
import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
|
|
2065
|
-
import { ScrollView as
|
|
2330
|
+
import { ScrollView as ScrollView5 } from "ink-scroll-view";
|
|
2066
2331
|
|
|
2067
2332
|
// src/lib/editor.ts
|
|
2068
2333
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -2105,13 +2370,13 @@ function ConfigureJiraSiteModal({
|
|
|
2105
2370
|
error
|
|
2106
2371
|
}) {
|
|
2107
2372
|
const hasExisting = existingConfigs.length > 0;
|
|
2108
|
-
const [mode, setMode] =
|
|
2109
|
-
const [selectedExisting, setSelectedExisting] =
|
|
2373
|
+
const [mode, setMode] = useState14(hasExisting ? "choose" : "manual");
|
|
2374
|
+
const [selectedExisting, setSelectedExisting] = useState14(0);
|
|
2110
2375
|
const scrollRef = useScrollToIndex(selectedExisting);
|
|
2111
|
-
const [siteUrl, setSiteUrl] =
|
|
2112
|
-
const [email, setEmail] =
|
|
2113
|
-
const [apiToken, setApiToken] =
|
|
2114
|
-
const [selectedItem, setSelectedItem] =
|
|
2376
|
+
const [siteUrl, setSiteUrl] = useState14(initialSiteUrl ?? "");
|
|
2377
|
+
const [email, setEmail] = useState14(initialEmail ?? "");
|
|
2378
|
+
const [apiToken, setApiToken] = useState14("");
|
|
2379
|
+
const [selectedItem, setSelectedItem] = useState14("siteUrl");
|
|
2115
2380
|
const items = ["siteUrl", "email", "apiToken", "submit"];
|
|
2116
2381
|
const canSubmit = siteUrl.trim() && email.trim() && apiToken.trim();
|
|
2117
2382
|
const chooseItems = existingConfigs.length + 1;
|
|
@@ -2203,7 +2468,7 @@ function ConfigureJiraSiteModal({
|
|
|
2203
2468
|
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Select an existing configuration or enter new credentials" }),
|
|
2204
2469
|
/* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
|
|
2205
2470
|
error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
|
|
2206
|
-
/* @__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: [
|
|
2207
2472
|
existingConfigs.map((config, idx) => {
|
|
2208
2473
|
const isSelected = selectedExisting === idx;
|
|
2209
2474
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
@@ -2212,7 +2477,7 @@ function ConfigureJiraSiteModal({
|
|
|
2212
2477
|
config.siteUrl
|
|
2213
2478
|
] }),
|
|
2214
2479
|
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
2215
|
-
"
|
|
2480
|
+
" ",
|
|
2216
2481
|
config.email
|
|
2217
2482
|
] })
|
|
2218
2483
|
] }, config.siteUrl + config.email);
|
|
@@ -2276,13 +2541,14 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
|
|
|
2276
2541
|
|
|
2277
2542
|
// src/components/jira/JiraView.tsx
|
|
2278
2543
|
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2279
|
-
function JiraView({
|
|
2544
|
+
function JiraView({ isActive, onModalChange, onJiraStateChange, onLogUpdated }) {
|
|
2280
2545
|
const repo = useGitRepo();
|
|
2281
2546
|
const jira = useJiraTickets();
|
|
2282
2547
|
const modal = useModal();
|
|
2283
|
-
const nav = useListNavigation(jira.tickets
|
|
2548
|
+
const nav = useListNavigation({ items: jira.tickets });
|
|
2549
|
+
const currentTicket = jira.tickets[nav.index] ?? null;
|
|
2284
2550
|
const lastInitRef = useRef6(null);
|
|
2285
|
-
|
|
2551
|
+
useEffect10(() => {
|
|
2286
2552
|
if (repo.loading || !repo.repoPath || !repo.currentBranch) return;
|
|
2287
2553
|
const current = { branch: repo.currentBranch };
|
|
2288
2554
|
const last = lastInitRef.current;
|
|
@@ -2290,17 +2556,17 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2290
2556
|
lastInitRef.current = current;
|
|
2291
2557
|
jira.initializeJiraState(repo.repoPath, repo.currentBranch, repo.currentRepoSlug);
|
|
2292
2558
|
}, [repo.loading, repo.repoPath, repo.currentBranch, repo.currentRepoSlug, jira.initializeJiraState]);
|
|
2293
|
-
|
|
2294
|
-
if (
|
|
2559
|
+
useEffect10(() => {
|
|
2560
|
+
if (isActive) {
|
|
2295
2561
|
repo.refreshBranch();
|
|
2296
2562
|
} else {
|
|
2297
2563
|
modal.close();
|
|
2298
2564
|
}
|
|
2299
|
-
}, [
|
|
2300
|
-
|
|
2565
|
+
}, [isActive, repo.refreshBranch, modal.close]);
|
|
2566
|
+
useEffect10(() => {
|
|
2301
2567
|
onModalChange == null ? void 0 : onModalChange(modal.isOpen);
|
|
2302
2568
|
}, [modal.isOpen, onModalChange]);
|
|
2303
|
-
|
|
2569
|
+
useEffect10(() => {
|
|
2304
2570
|
onJiraStateChange == null ? void 0 : onJiraStateChange(jira.jiraState);
|
|
2305
2571
|
}, [jira.jiraState, onJiraStateChange]);
|
|
2306
2572
|
const handleConfigureSubmit = async (siteUrl, email, apiToken) => {
|
|
@@ -2313,38 +2579,30 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2313
2579
|
const success = await jira.linkTicket(repo.repoPath, repo.currentBranch, ticketInput);
|
|
2314
2580
|
if (success) modal.close();
|
|
2315
2581
|
};
|
|
2582
|
+
const getTicketUrl = () => {
|
|
2583
|
+
if (!repo.repoPath || !currentTicket) return null;
|
|
2584
|
+
const siteUrl = getJiraSiteUrl(repo.repoPath);
|
|
2585
|
+
return siteUrl ? `${siteUrl}/browse/${currentTicket.key}` : null;
|
|
2586
|
+
};
|
|
2316
2587
|
const handleUnlinkTicket = () => {
|
|
2317
|
-
if (!repo.repoPath || !repo.currentBranch ||
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
jira.refreshTickets(repo.repoPath, repo.currentBranch);
|
|
2322
|
-
nav.prev();
|
|
2323
|
-
}
|
|
2588
|
+
if (!repo.repoPath || !repo.currentBranch || !currentTicket) return;
|
|
2589
|
+
jira.unlinkTicket(repo.repoPath, repo.currentBranch, currentTicket.key);
|
|
2590
|
+
jira.refreshTickets(repo.repoPath, repo.currentBranch);
|
|
2591
|
+
nav.prev();
|
|
2324
2592
|
};
|
|
2325
2593
|
const handleOpenInBrowser = () => {
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
if (ticket && siteUrl) {
|
|
2330
|
-
open3(`${siteUrl}/browse/${ticket.key}`).catch(() => {
|
|
2331
|
-
});
|
|
2332
|
-
}
|
|
2594
|
+
const url = getTicketUrl();
|
|
2595
|
+
if (url) open3(url).catch(() => {
|
|
2596
|
+
});
|
|
2333
2597
|
};
|
|
2334
2598
|
const handleCopyLink = () => {
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
const siteUrl = getJiraSiteUrl(repo.repoPath);
|
|
2338
|
-
if (ticket && siteUrl) {
|
|
2339
|
-
copyToClipboard(`${siteUrl}/browse/${ticket.key}`);
|
|
2340
|
-
}
|
|
2599
|
+
const url = getTicketUrl();
|
|
2600
|
+
if (url) copyToClipboard(url);
|
|
2341
2601
|
};
|
|
2342
2602
|
const handleStatusComplete = (newStatus) => {
|
|
2343
|
-
if (!repo.repoPath || !repo.currentBranch) return;
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
updateTicketStatus(repo.repoPath, repo.currentBranch, ticket.key, newStatus);
|
|
2347
|
-
logJiraStatusChanged(ticket.key, ticket.summary, ticket.status, newStatus);
|
|
2603
|
+
if (!repo.repoPath || !repo.currentBranch || !currentTicket) return;
|
|
2604
|
+
updateTicketStatus(repo.repoPath, repo.currentBranch, currentTicket.key, newStatus);
|
|
2605
|
+
logJiraStatusChanged(currentTicket.key, currentTicket.summary, currentTicket.status, newStatus);
|
|
2348
2606
|
onLogUpdated == null ? void 0 : onLogUpdated();
|
|
2349
2607
|
modal.close();
|
|
2350
2608
|
jira.refreshTickets(repo.repoPath, repo.currentBranch);
|
|
@@ -2377,7 +2635,7 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2377
2635
|
if (input === "y") handleCopyLink();
|
|
2378
2636
|
}
|
|
2379
2637
|
},
|
|
2380
|
-
{ isActive:
|
|
2638
|
+
{ isActive: isActive && !modal.isOpen }
|
|
2381
2639
|
);
|
|
2382
2640
|
if (repo.isRepo === false) {
|
|
2383
2641
|
return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: ["Jira"], flexShrink: 0, children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: "Not a git repository" }) });
|
|
@@ -2416,21 +2674,20 @@ function JiraView({ isFocused, onModalChange, onJiraStateChange, onLogUpdated })
|
|
|
2416
2674
|
}
|
|
2417
2675
|
) });
|
|
2418
2676
|
}
|
|
2419
|
-
if (modal.type === "status" && repo.repoPath && repo.currentBranch &&
|
|
2420
|
-
const ticket = jira.tickets[nav.index];
|
|
2677
|
+
if (modal.type === "status" && repo.repoPath && repo.currentBranch && currentTicket) {
|
|
2421
2678
|
return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx11(
|
|
2422
2679
|
ChangeStatusModal,
|
|
2423
2680
|
{
|
|
2424
2681
|
repoPath: repo.repoPath,
|
|
2425
|
-
ticketKey:
|
|
2426
|
-
currentStatus:
|
|
2682
|
+
ticketKey: currentTicket.key,
|
|
2683
|
+
currentStatus: currentTicket.status,
|
|
2427
2684
|
onComplete: handleStatusComplete,
|
|
2428
2685
|
onCancel: modal.close
|
|
2429
2686
|
}
|
|
2430
2687
|
) });
|
|
2431
2688
|
}
|
|
2432
2689
|
const title = "[4] Jira";
|
|
2433
|
-
const borderColor =
|
|
2690
|
+
const borderColor = isActive ? "yellow" : void 0;
|
|
2434
2691
|
return /* @__PURE__ */ jsx11(TitledBox4, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, children: [
|
|
2435
2692
|
jira.jiraState === "not_configured" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No Jira site configured" }),
|
|
2436
2693
|
jira.jiraState === "no_tickets" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "No tickets linked to this branch" }),
|
|
@@ -2452,106 +2709,21 @@ import { useEffect as useEffect12 } from "react";
|
|
|
2452
2709
|
import { Box as Box14, useInput as useInput12 } from "ink";
|
|
2453
2710
|
|
|
2454
2711
|
// src/components/logs/LogViewerBox.tsx
|
|
2455
|
-
import { useRef as useRef7, useState as
|
|
2712
|
+
import { useEffect as useEffect11, useRef as useRef7, useState as useState15 } from "react";
|
|
2456
2713
|
import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
|
|
2457
2714
|
import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
|
|
2458
|
-
import { ScrollView as
|
|
2715
|
+
import { ScrollView as ScrollView6 } from "ink-scroll-view";
|
|
2459
2716
|
import TextInput2 from "ink-text-input";
|
|
2460
|
-
|
|
2461
|
-
// src/lib/claude/api.ts
|
|
2462
|
-
import { exec as exec3 } from "child_process";
|
|
2463
|
-
function runClaudePrompt(prompt) {
|
|
2464
|
-
let childProcess = null;
|
|
2465
|
-
let cancelled = false;
|
|
2466
|
-
const promise = new Promise((resolve) => {
|
|
2467
|
-
const escapedPrompt = prompt.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
|
|
2468
|
-
const command = `claude -p $'${escapedPrompt}' --output-format json < /dev/null`;
|
|
2469
|
-
childProcess = exec3(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
2470
|
-
if (cancelled) {
|
|
2471
|
-
resolve({
|
|
2472
|
-
success: false,
|
|
2473
|
-
error: "Cancelled",
|
|
2474
|
-
errorType: "execution_error"
|
|
2475
|
-
});
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
if (error) {
|
|
2479
|
-
if (error.message.includes("command not found") || error.message.includes("ENOENT") || error.code === "ENOENT") {
|
|
2480
|
-
resolve({
|
|
2481
|
-
success: false,
|
|
2482
|
-
error: "Claude CLI not installed. Run: npm install -g @anthropic-ai/claude-code",
|
|
2483
|
-
errorType: "not_installed"
|
|
2484
|
-
});
|
|
2485
|
-
return;
|
|
2486
|
-
}
|
|
2487
|
-
resolve({
|
|
2488
|
-
success: false,
|
|
2489
|
-
error: (stderr == null ? void 0 : stderr.trim()) || error.message,
|
|
2490
|
-
errorType: "execution_error"
|
|
2491
|
-
});
|
|
2492
|
-
return;
|
|
2493
|
-
}
|
|
2494
|
-
if (!(stdout == null ? void 0 : stdout.trim())) {
|
|
2495
|
-
resolve({
|
|
2496
|
-
success: false,
|
|
2497
|
-
error: (stderr == null ? void 0 : stderr.trim()) || "Claude returned empty response",
|
|
2498
|
-
errorType: "execution_error"
|
|
2499
|
-
});
|
|
2500
|
-
return;
|
|
2501
|
-
}
|
|
2502
|
-
try {
|
|
2503
|
-
const json = JSON.parse(stdout.trim());
|
|
2504
|
-
if (json.is_error) {
|
|
2505
|
-
resolve({
|
|
2506
|
-
success: false,
|
|
2507
|
-
error: json.result || "Claude returned an error",
|
|
2508
|
-
errorType: "execution_error"
|
|
2509
|
-
});
|
|
2510
|
-
return;
|
|
2511
|
-
}
|
|
2512
|
-
resolve({
|
|
2513
|
-
success: true,
|
|
2514
|
-
data: json.result || stdout.trim()
|
|
2515
|
-
});
|
|
2516
|
-
} catch {
|
|
2517
|
-
resolve({
|
|
2518
|
-
success: true,
|
|
2519
|
-
data: stdout.trim()
|
|
2520
|
-
});
|
|
2521
|
-
}
|
|
2522
|
-
});
|
|
2523
|
-
});
|
|
2524
|
-
const cancel = () => {
|
|
2525
|
-
cancelled = true;
|
|
2526
|
-
if (childProcess) {
|
|
2527
|
-
childProcess.kill("SIGTERM");
|
|
2528
|
-
}
|
|
2529
|
-
};
|
|
2530
|
-
return { promise, cancel };
|
|
2531
|
-
}
|
|
2532
|
-
function generateStandupNotes(logContent) {
|
|
2533
|
-
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.
|
|
2534
|
-
|
|
2535
|
-
Format the output as bullet points grouped by category (e.g., Features, Bug Fixes, Refactoring, etc.). Keep it brief and professional.
|
|
2536
|
-
|
|
2537
|
-
Log entries:
|
|
2538
|
-
${logContent}
|
|
2539
|
-
|
|
2540
|
-
Generate the standup notes:`;
|
|
2541
|
-
return runClaudePrompt(prompt);
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
// src/components/logs/LogViewerBox.tsx
|
|
2545
2717
|
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2546
|
-
function LogViewerBox({ date, content,
|
|
2718
|
+
function LogViewerBox({ date, content, isActive, onRefresh, onLogCreated }) {
|
|
2547
2719
|
const scrollRef = useRef7(null);
|
|
2548
|
-
const [isInputMode, setIsInputMode] =
|
|
2549
|
-
const [inputValue, setInputValue] =
|
|
2550
|
-
const [isGeneratingStandup, setIsGeneratingStandup] =
|
|
2551
|
-
const [standupResult, setStandupResult] =
|
|
2720
|
+
const [isInputMode, setIsInputMode] = useState15(false);
|
|
2721
|
+
const [inputValue, setInputValue] = useState15("");
|
|
2722
|
+
const [isGeneratingStandup, setIsGeneratingStandup] = useState15(false);
|
|
2723
|
+
const [standupResult, setStandupResult] = useState15(null);
|
|
2552
2724
|
const claudeProcessRef = useRef7(null);
|
|
2553
2725
|
const title = "[6] Log Content";
|
|
2554
|
-
const borderColor =
|
|
2726
|
+
const borderColor = isActive ? "yellow" : void 0;
|
|
2555
2727
|
const displayTitle = date ? `${title} - ${date}.md` : title;
|
|
2556
2728
|
useInput10(
|
|
2557
2729
|
(input, key) => {
|
|
@@ -2613,8 +2785,14 @@ function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
|
|
|
2613
2785
|
});
|
|
2614
2786
|
}
|
|
2615
2787
|
},
|
|
2616
|
-
{ isActive
|
|
2788
|
+
{ isActive }
|
|
2617
2789
|
);
|
|
2790
|
+
useEffect11(() => {
|
|
2791
|
+
return () => {
|
|
2792
|
+
var _a;
|
|
2793
|
+
(_a = claudeProcessRef.current) == null ? void 0 : _a.cancel();
|
|
2794
|
+
};
|
|
2795
|
+
}, []);
|
|
2618
2796
|
const handleInputSubmit = (value) => {
|
|
2619
2797
|
if (!date || !value.trim()) {
|
|
2620
2798
|
setIsInputMode(false);
|
|
@@ -2633,7 +2811,7 @@ ${value.trim()}
|
|
|
2633
2811
|
onRefresh();
|
|
2634
2812
|
};
|
|
2635
2813
|
return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
|
|
2636
|
-
/* @__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: [
|
|
2637
2815
|
!date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
|
|
2638
2816
|
date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
|
|
2639
2817
|
date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
|
|
@@ -2669,7 +2847,7 @@ ${value.trim()}
|
|
|
2669
2847
|
// src/components/logs/LogsHistoryBox.tsx
|
|
2670
2848
|
import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
|
|
2671
2849
|
import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
|
|
2672
|
-
import { ScrollView as
|
|
2850
|
+
import { ScrollView as ScrollView7 } from "ink-scroll-view";
|
|
2673
2851
|
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2674
2852
|
function LogsHistoryBox({
|
|
2675
2853
|
logFiles,
|
|
@@ -2677,11 +2855,11 @@ function LogsHistoryBox({
|
|
|
2677
2855
|
highlightedIndex,
|
|
2678
2856
|
onHighlight,
|
|
2679
2857
|
onSelect,
|
|
2680
|
-
|
|
2858
|
+
isActive
|
|
2681
2859
|
}) {
|
|
2682
2860
|
const scrollRef = useScrollToIndex(highlightedIndex);
|
|
2683
2861
|
const title = "[5] Logs";
|
|
2684
|
-
const borderColor =
|
|
2862
|
+
const borderColor = isActive ? "yellow" : void 0;
|
|
2685
2863
|
useInput11(
|
|
2686
2864
|
(input, key) => {
|
|
2687
2865
|
if (logFiles.length === 0) return;
|
|
@@ -2691,18 +2869,18 @@ function LogsHistoryBox({
|
|
|
2691
2869
|
if (key.downArrow || input === "j") {
|
|
2692
2870
|
onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
|
|
2693
2871
|
}
|
|
2694
|
-
if (
|
|
2872
|
+
if (input === " ") {
|
|
2695
2873
|
const file = logFiles[highlightedIndex];
|
|
2696
2874
|
if (file) {
|
|
2697
2875
|
onSelect(file.date);
|
|
2698
2876
|
}
|
|
2699
2877
|
}
|
|
2700
2878
|
},
|
|
2701
|
-
{ isActive
|
|
2879
|
+
{ isActive }
|
|
2702
2880
|
);
|
|
2703
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: [
|
|
2704
2882
|
logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
|
|
2705
|
-
logFiles.length > 0 && /* @__PURE__ */ jsx13(
|
|
2883
|
+
logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView7, { ref: scrollRef, children: logFiles.map((file, idx) => {
|
|
2706
2884
|
const isHighlighted = idx === highlightedIndex;
|
|
2707
2885
|
const isSelected = file.date === selectedDate;
|
|
2708
2886
|
const cursor = isHighlighted ? ">" : " ";
|
|
@@ -2722,7 +2900,7 @@ function LogsHistoryBox({
|
|
|
2722
2900
|
|
|
2723
2901
|
// src/components/logs/LogsView.tsx
|
|
2724
2902
|
import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2725
|
-
function LogsView({
|
|
2903
|
+
function LogsView({ isActive, refreshKey, focusedBox, onFocusedBoxChange }) {
|
|
2726
2904
|
const logs = useLogs();
|
|
2727
2905
|
useEffect12(() => {
|
|
2728
2906
|
if (refreshKey !== void 0 && refreshKey > 0) {
|
|
@@ -2734,7 +2912,7 @@ function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
|
|
|
2734
2912
|
if (input === "5") onFocusedBoxChange("history");
|
|
2735
2913
|
if (input === "6") onFocusedBoxChange("viewer");
|
|
2736
2914
|
},
|
|
2737
|
-
{ isActive
|
|
2915
|
+
{ isActive }
|
|
2738
2916
|
);
|
|
2739
2917
|
return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", flexGrow: 1, children: [
|
|
2740
2918
|
/* @__PURE__ */ jsx14(
|
|
@@ -2745,7 +2923,7 @@ function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
|
|
|
2745
2923
|
highlightedIndex: logs.highlightedIndex,
|
|
2746
2924
|
onHighlight: logs.setHighlightedIndex,
|
|
2747
2925
|
onSelect: logs.selectDate,
|
|
2748
|
-
|
|
2926
|
+
isActive: isActive && focusedBox === "history"
|
|
2749
2927
|
}
|
|
2750
2928
|
),
|
|
2751
2929
|
/* @__PURE__ */ jsx14(
|
|
@@ -2753,7 +2931,7 @@ function LogsView({ isFocused, refreshKey, focusedBox, onFocusedBoxChange }) {
|
|
|
2753
2931
|
{
|
|
2754
2932
|
date: logs.selectedDate,
|
|
2755
2933
|
content: logs.logContent,
|
|
2756
|
-
|
|
2934
|
+
isActive: isActive && focusedBox === "viewer",
|
|
2757
2935
|
onRefresh: logs.refresh,
|
|
2758
2936
|
onLogCreated: logs.handleLogCreated
|
|
2759
2937
|
}
|
|
@@ -2790,6 +2968,7 @@ var GITHUB_KEYBINDINGS = {
|
|
|
2790
2968
|
remotes: [{ key: "Space", label: "Select Remote" }],
|
|
2791
2969
|
prs: [
|
|
2792
2970
|
{ key: "Space", label: "Select" },
|
|
2971
|
+
{ key: "c", label: "Generate PR", color: "green" },
|
|
2793
2972
|
{ key: "n", label: "New PR", color: "green" },
|
|
2794
2973
|
{ key: "r", label: "Refresh" },
|
|
2795
2974
|
{ key: "o", label: "Open", color: "green" },
|
|
@@ -2820,7 +2999,7 @@ var JIRA_KEYBINDINGS = {
|
|
|
2820
2999
|
|
|
2821
3000
|
// src/constants/logs.ts
|
|
2822
3001
|
var LOGS_KEYBINDINGS = {
|
|
2823
|
-
history: [{ key: "
|
|
3002
|
+
history: [{ key: "Space", label: "Select" }],
|
|
2824
3003
|
viewer: [
|
|
2825
3004
|
{ key: "i", label: "Add Entry" },
|
|
2826
3005
|
{ key: "e", label: "Edit" },
|
|
@@ -2849,13 +3028,13 @@ function computeKeybindings(focusedView, state) {
|
|
|
2849
3028
|
import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2850
3029
|
function App() {
|
|
2851
3030
|
const { exit } = useApp();
|
|
2852
|
-
const [focusedView, setFocusedView] =
|
|
2853
|
-
const [modalOpen, setModalOpen] =
|
|
2854
|
-
const [logRefreshKey, setLogRefreshKey] =
|
|
3031
|
+
const [focusedView, setFocusedView] = useState16("github");
|
|
3032
|
+
const [modalOpen, setModalOpen] = useState16(false);
|
|
3033
|
+
const [logRefreshKey, setLogRefreshKey] = useState16(0);
|
|
2855
3034
|
const duck = useRubberDuck();
|
|
2856
|
-
const [githubFocusedBox, setGithubFocusedBox] =
|
|
2857
|
-
const [jiraState, setJiraState] =
|
|
2858
|
-
const [logsFocusedBox, setLogsFocusedBox] =
|
|
3035
|
+
const [githubFocusedBox, setGithubFocusedBox] = useState16("remotes");
|
|
3036
|
+
const [jiraState, setJiraState] = useState16("not_configured");
|
|
3037
|
+
const [logsFocusedBox, setLogsFocusedBox] = useState16("history");
|
|
2859
3038
|
const keybindings = useMemo2(
|
|
2860
3039
|
() => computeKeybindings(focusedView, {
|
|
2861
3040
|
github: { focusedBox: githubFocusedBox },
|
|
@@ -2864,7 +3043,7 @@ function App() {
|
|
|
2864
3043
|
}),
|
|
2865
3044
|
[focusedView, githubFocusedBox, jiraState, modalOpen, logsFocusedBox]
|
|
2866
3045
|
);
|
|
2867
|
-
const handleLogUpdated =
|
|
3046
|
+
const handleLogUpdated = useCallback9(() => {
|
|
2868
3047
|
setLogRefreshKey((prev) => prev + 1);
|
|
2869
3048
|
}, []);
|
|
2870
3049
|
useInput13(
|
|
@@ -2901,7 +3080,7 @@ function App() {
|
|
|
2901
3080
|
/* @__PURE__ */ jsx16(
|
|
2902
3081
|
GitHubView,
|
|
2903
3082
|
{
|
|
2904
|
-
|
|
3083
|
+
isActive: focusedView === "github",
|
|
2905
3084
|
onFocusedBoxChange: setGithubFocusedBox,
|
|
2906
3085
|
onLogUpdated: handleLogUpdated
|
|
2907
3086
|
}
|
|
@@ -2909,7 +3088,7 @@ function App() {
|
|
|
2909
3088
|
/* @__PURE__ */ jsx16(
|
|
2910
3089
|
JiraView,
|
|
2911
3090
|
{
|
|
2912
|
-
|
|
3091
|
+
isActive: focusedView === "jira",
|
|
2913
3092
|
onModalChange: setModalOpen,
|
|
2914
3093
|
onJiraStateChange: setJiraState,
|
|
2915
3094
|
onLogUpdated: handleLogUpdated
|
|
@@ -2919,7 +3098,7 @@ function App() {
|
|
|
2919
3098
|
/* @__PURE__ */ jsx16(Box16, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: /* @__PURE__ */ jsx16(
|
|
2920
3099
|
LogsView,
|
|
2921
3100
|
{
|
|
2922
|
-
|
|
3101
|
+
isActive: focusedView === "logs",
|
|
2923
3102
|
refreshKey: logRefreshKey,
|
|
2924
3103
|
focusedBox: logsFocusedBox,
|
|
2925
3104
|
onFocusedBoxChange: setLogsFocusedBox
|
|
@@ -2941,13 +3120,13 @@ function App() {
|
|
|
2941
3120
|
import { render as inkRender } from "ink";
|
|
2942
3121
|
|
|
2943
3122
|
// src/lib/Screen.tsx
|
|
2944
|
-
import { useCallback as
|
|
3123
|
+
import { useCallback as useCallback10, useEffect as useEffect13, useState as useState17 } from "react";
|
|
2945
3124
|
import { Box as Box17, useStdout as useStdout2 } from "ink";
|
|
2946
3125
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2947
3126
|
function Screen({ children }) {
|
|
2948
3127
|
const { stdout } = useStdout2();
|
|
2949
|
-
const getSize =
|
|
2950
|
-
const [size, setSize] =
|
|
3128
|
+
const getSize = useCallback10(() => ({ height: stdout.rows, width: stdout.columns }), [stdout]);
|
|
3129
|
+
const [size, setSize] = useState17(getSize);
|
|
2951
3130
|
useEffect13(() => {
|
|
2952
3131
|
const onResize = () => setSize(getSize());
|
|
2953
3132
|
stdout.on("resize", onResize);
|