@rotorsoft/gent 1.13.4 → 1.14.1
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/dist/{chunk-SKWCS6Z2.js → chunk-JHIW57FY.js} +1 -2
- package/dist/index.js +617 -321
- package/dist/index.js.map +1 -1
- package/dist/{setup-labels-C6YHJ637.js → setup-labels-LB7X6A3K.js} +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-SKWCS6Z2.js.map → chunk-JHIW57FY.js.map} +0 -0
- /package/dist/{setup-labels-C6YHJ637.js.map → setup-labels-LB7X6A3K.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
configExists,
|
|
14
14
|
createIssue,
|
|
15
15
|
createPullRequest,
|
|
16
|
-
createSpinner,
|
|
17
16
|
extractTypeFromLabels,
|
|
18
17
|
generateDefaultConfig,
|
|
19
18
|
getConfigPath,
|
|
@@ -37,7 +36,7 @@ import {
|
|
|
37
36
|
sortByPriority,
|
|
38
37
|
updateIssueLabels,
|
|
39
38
|
withSpinner
|
|
40
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-JHIW57FY.js";
|
|
41
40
|
|
|
42
41
|
// src/index.ts
|
|
43
42
|
import { Command } from "commander";
|
|
@@ -201,7 +200,7 @@ async function initCommand(options) {
|
|
|
201
200
|
}
|
|
202
201
|
]);
|
|
203
202
|
if (setupLabels) {
|
|
204
|
-
const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-
|
|
203
|
+
const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-LB7X6A3K.js");
|
|
205
204
|
await setupLabelsCommand2();
|
|
206
205
|
}
|
|
207
206
|
}
|
|
@@ -615,46 +614,6 @@ function generateFallbackTitle(description) {
|
|
|
615
614
|
}
|
|
616
615
|
return truncated;
|
|
617
616
|
}
|
|
618
|
-
function buildVideoPrompt(issueNumber, issueTitle, videoConfig, agentInstructions) {
|
|
619
|
-
return `You are helping capture a Playwright video demonstration of UI changes for GitHub Issue #${issueNumber}: ${issueTitle}
|
|
620
|
-
|
|
621
|
-
${agentInstructions ? `## Project-Specific Instructions
|
|
622
|
-
${agentInstructions}
|
|
623
|
-
|
|
624
|
-
` : ""}
|
|
625
|
-
|
|
626
|
-
## Task: Record UI Demo Video
|
|
627
|
-
|
|
628
|
-
Create a short video (max ${videoConfig.max_duration}s) demonstrating the UI changes made for this issue.
|
|
629
|
-
|
|
630
|
-
### Video Requirements
|
|
631
|
-
- Resolution: ${videoConfig.width}x${videoConfig.height}
|
|
632
|
-
- Format: WebM or MP4
|
|
633
|
-
- Duration: Under ${videoConfig.max_duration} seconds
|
|
634
|
-
- Show the key UI interactions and visual changes
|
|
635
|
-
|
|
636
|
-
### Steps
|
|
637
|
-
|
|
638
|
-
1. **Start the development server** if not already running
|
|
639
|
-
2. **Use Playwright to record video** of the relevant UI interactions:
|
|
640
|
-
- Navigate to the affected pages/components
|
|
641
|
-
- Demonstrate the new or changed functionality
|
|
642
|
-
- Show before/after if applicable
|
|
643
|
-
|
|
644
|
-
3. **Upload video to GitHub** as a release asset or use GitHub's drag-drop upload:
|
|
645
|
-
- Create a GitHub release or upload to issue comments
|
|
646
|
-
- Get the permanent URL for the video
|
|
647
|
-
- Do NOT commit video files to the repository
|
|
648
|
-
|
|
649
|
-
4. **Add video to PR** by commenting with the video URL or embedding it
|
|
650
|
-
|
|
651
|
-
### Important
|
|
652
|
-
- Upload video to GitHub assets, NOT to the repository
|
|
653
|
-
- Keep the video concise - focus on demonstrating the changes
|
|
654
|
-
- Ensure the video clearly shows the UI improvements
|
|
655
|
-
|
|
656
|
-
Output the GitHub URL where the video was uploaded when complete.`;
|
|
657
|
-
}
|
|
658
617
|
|
|
659
618
|
// src/commands/create.ts
|
|
660
619
|
async function createCommand(description, options) {
|
|
@@ -1596,9 +1555,6 @@ No commits were created. Please retry later.`
|
|
|
1596
1555
|
);
|
|
1597
1556
|
}
|
|
1598
1557
|
|
|
1599
|
-
// src/commands/pr.ts
|
|
1600
|
-
import inquirer5 from "inquirer";
|
|
1601
|
-
|
|
1602
1558
|
// src/lib/playwright.ts
|
|
1603
1559
|
import { execa as execa3 } from "execa";
|
|
1604
1560
|
var UI_FILE_PATTERNS = [
|
|
@@ -1656,17 +1612,17 @@ async function prCommand(options) {
|
|
|
1656
1612
|
]);
|
|
1657
1613
|
if (!ghAuth) {
|
|
1658
1614
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
1659
|
-
|
|
1615
|
+
return;
|
|
1660
1616
|
}
|
|
1661
1617
|
if (!aiOk) {
|
|
1662
1618
|
logger.error(
|
|
1663
1619
|
`${providerName} CLI not found. Please install ${provider} CLI first.`
|
|
1664
1620
|
);
|
|
1665
|
-
|
|
1621
|
+
return;
|
|
1666
1622
|
}
|
|
1667
1623
|
if (await isOnMainBranch()) {
|
|
1668
1624
|
logger.error("Cannot create PR from main/master branch.");
|
|
1669
|
-
|
|
1625
|
+
return;
|
|
1670
1626
|
}
|
|
1671
1627
|
const existingPr = await getPrForBranch();
|
|
1672
1628
|
if (existingPr) {
|
|
@@ -1681,21 +1637,10 @@ async function prCommand(options) {
|
|
|
1681
1637
|
logger.info(`Base: ${colors.branch(baseBranch)}`);
|
|
1682
1638
|
const hasUnpushed = await getUnpushedCommits();
|
|
1683
1639
|
if (hasUnpushed) {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
name: "push",
|
|
1689
|
-
message: "Push to remote before creating PR?",
|
|
1690
|
-
default: true
|
|
1691
|
-
}
|
|
1692
|
-
]);
|
|
1693
|
-
if (push) {
|
|
1694
|
-
await withSpinner("Pushing branch...", async () => {
|
|
1695
|
-
await pushBranch();
|
|
1696
|
-
});
|
|
1697
|
-
logger.success("Branch pushed");
|
|
1698
|
-
}
|
|
1640
|
+
await withSpinner("Pushing branch...", async () => {
|
|
1641
|
+
await pushBranch();
|
|
1642
|
+
});
|
|
1643
|
+
logger.success("Branch pushed");
|
|
1699
1644
|
}
|
|
1700
1645
|
const issueNumber = extractIssueNumber(currentBranch);
|
|
1701
1646
|
let issue = null;
|
|
@@ -1809,6 +1754,23 @@ IMPORTANT: This PR contains UI changes. Use the Playwright MCP plugin to:
|
|
|
1809
1754
|
if (options.draft) {
|
|
1810
1755
|
logger.dim("Created as draft. Mark as ready for review when done.");
|
|
1811
1756
|
}
|
|
1757
|
+
if (shouldCaptureVideo) {
|
|
1758
|
+
const changedFilesForHint = captureVideoInstructions !== "" ? [] : await getChangedFiles(baseBranch);
|
|
1759
|
+
const uiChangesForHint = captureVideoInstructions !== "" ? true : hasUIChanges(changedFilesForHint);
|
|
1760
|
+
if (uiChangesForHint) {
|
|
1761
|
+
const playwrightOk = await isPlaywrightAvailable();
|
|
1762
|
+
if (playwrightOk) {
|
|
1763
|
+
logger.bold("Next Steps");
|
|
1764
|
+
logger.info(
|
|
1765
|
+
"Run `claude` with Playwright MCP to record a demo video of the changes."
|
|
1766
|
+
);
|
|
1767
|
+
logger.info(
|
|
1768
|
+
"Upload the result to GitHub Assets to keep the repo light."
|
|
1769
|
+
);
|
|
1770
|
+
logger.newline();
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1812
1774
|
}
|
|
1813
1775
|
function generateFallbackBody(issue, commits) {
|
|
1814
1776
|
let body = "## Summary\n\n";
|
|
@@ -1831,7 +1793,7 @@ function generateFallbackBody(issue, commits) {
|
|
|
1831
1793
|
}
|
|
1832
1794
|
|
|
1833
1795
|
// src/commands/fix.ts
|
|
1834
|
-
import
|
|
1796
|
+
import inquirer5 from "inquirer";
|
|
1835
1797
|
|
|
1836
1798
|
// src/lib/review-feedback.ts
|
|
1837
1799
|
var ACTIONABLE_KEYWORDS = [
|
|
@@ -2028,7 +1990,7 @@ async function fixCommand(options) {
|
|
|
2028
1990
|
const hasChanges = await hasUncommittedChanges();
|
|
2029
1991
|
if (hasChanges) {
|
|
2030
1992
|
logger.warning("You have uncommitted changes.");
|
|
2031
|
-
const { proceed } = await
|
|
1993
|
+
const { proceed } = await inquirer5.prompt([
|
|
2032
1994
|
{
|
|
2033
1995
|
type: "confirm",
|
|
2034
1996
|
name: "proceed",
|
|
@@ -2188,7 +2150,7 @@ import { homedir } from "os";
|
|
|
2188
2150
|
// package.json
|
|
2189
2151
|
var package_default = {
|
|
2190
2152
|
name: "@rotorsoft/gent",
|
|
2191
|
-
version: "1.
|
|
2153
|
+
version: "1.14.1",
|
|
2192
2154
|
description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
|
|
2193
2155
|
keywords: [
|
|
2194
2156
|
"cli",
|
|
@@ -2610,7 +2572,6 @@ async function statusCommand() {
|
|
|
2610
2572
|
}
|
|
2611
2573
|
|
|
2612
2574
|
// src/commands/tui.ts
|
|
2613
|
-
import inquirer7 from "inquirer";
|
|
2614
2575
|
import { execa as execa4 } from "execa";
|
|
2615
2576
|
|
|
2616
2577
|
// src/tui/state.ts
|
|
@@ -2758,11 +2719,6 @@ function getAvailableActions(state) {
|
|
|
2758
2719
|
if (state.issue && state.pr?.state !== "merged") {
|
|
2759
2720
|
actions.push({ id: "run", label: "run", shortcut: "u" });
|
|
2760
2721
|
}
|
|
2761
|
-
if (state.pr && state.pr.state === "open") {
|
|
2762
|
-
if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled) {
|
|
2763
|
-
actions.push({ id: "video", label: "video", shortcut: "v" });
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
2722
|
if (state.pr && (state.pr.state === "merged" || state.pr.state === "closed")) {
|
|
2767
2723
|
actions.push({ id: "checkout-main", label: "main", shortcut: "m" });
|
|
2768
2724
|
}
|
|
@@ -2896,75 +2852,73 @@ function renderActionPanel(title, content) {
|
|
|
2896
2852
|
}
|
|
2897
2853
|
console.log(botRow(w));
|
|
2898
2854
|
}
|
|
2899
|
-
function
|
|
2855
|
+
function renderSettingsTo(state, w, out) {
|
|
2900
2856
|
const provider = getProviderDisplayName(state.config.ai.provider);
|
|
2901
2857
|
const provTag = state.isAIProviderAvailable ? chalk3.green(provider) : chalk3.red(provider);
|
|
2902
2858
|
const ghTag = state.isGhAuthenticated ? chalk3.green("authenticated") : chalk3.red("not authenticated");
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
console.log(row(chalk3.dim("GitHub: ") + ghTag, w));
|
|
2906
|
-
console.log(row(chalk3.dim("Video: ") + videoTag, w));
|
|
2859
|
+
out(row(chalk3.dim("Provider: ") + provTag, w));
|
|
2860
|
+
out(row(chalk3.dim("GitHub: ") + ghTag, w));
|
|
2907
2861
|
}
|
|
2908
|
-
function
|
|
2862
|
+
function buildDashboardLines(state, actions, hint, refreshing) {
|
|
2863
|
+
const lines = [];
|
|
2864
|
+
const out = (line) => lines.push(line);
|
|
2909
2865
|
const w = termWidth();
|
|
2910
2866
|
const descMax = w - 8;
|
|
2911
2867
|
const version2 = getVersion();
|
|
2912
2868
|
const titleLabel = `gent v${version2}`;
|
|
2913
|
-
|
|
2914
|
-
|
|
2869
|
+
out(topRow(titleLabel, w));
|
|
2870
|
+
renderSettingsTo(state, w, out);
|
|
2915
2871
|
if (!state.isGitRepo) {
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
console.log(botRow(w));
|
|
2921
|
-
return;
|
|
2872
|
+
out(row(chalk3.red("Not a git repository"), w));
|
|
2873
|
+
out(row(chalk3.dim("Run gent init in a git repo to get started"), w));
|
|
2874
|
+
out(botRow(w));
|
|
2875
|
+
return lines;
|
|
2922
2876
|
}
|
|
2923
2877
|
if (!state.isGhAuthenticated) {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
return;
|
|
2878
|
+
out(row(chalk3.red("GitHub CLI not authenticated"), w));
|
|
2879
|
+
out(row(chalk3.dim("Run: gh auth login"), w));
|
|
2880
|
+
out(botRow(w));
|
|
2881
|
+
return lines;
|
|
2928
2882
|
}
|
|
2929
2883
|
if (state.isOnMain) {
|
|
2930
|
-
|
|
2884
|
+
out(
|
|
2931
2885
|
row(
|
|
2932
2886
|
chalk3.magenta(state.branch) + chalk3.dim(" \xB7 ready to start new work"),
|
|
2933
2887
|
w
|
|
2934
2888
|
)
|
|
2935
2889
|
);
|
|
2936
2890
|
if (state.hasUncommittedChanges) {
|
|
2937
|
-
|
|
2891
|
+
out(row(chalk3.yellow("\u25CF uncommitted changes"), w));
|
|
2938
2892
|
}
|
|
2939
2893
|
if (hint) {
|
|
2940
|
-
|
|
2941
|
-
|
|
2894
|
+
out(midRow("Hint", w));
|
|
2895
|
+
out(row(chalk3.yellow(hint), w));
|
|
2942
2896
|
}
|
|
2943
|
-
|
|
2897
|
+
out(divRow(w));
|
|
2944
2898
|
if (refreshing) {
|
|
2945
|
-
|
|
2899
|
+
out(row(chalk3.yellow("Refreshing\u2026"), w));
|
|
2946
2900
|
} else {
|
|
2947
2901
|
for (const line of formatCommandBar(actions, w)) {
|
|
2948
|
-
|
|
2902
|
+
out(row(line, w));
|
|
2949
2903
|
}
|
|
2950
2904
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
return;
|
|
2905
|
+
out(botRow(w));
|
|
2906
|
+
out("");
|
|
2907
|
+
return lines;
|
|
2954
2908
|
}
|
|
2955
2909
|
const section = (title) => {
|
|
2956
|
-
|
|
2910
|
+
out(midRow(title, w));
|
|
2957
2911
|
};
|
|
2958
2912
|
section("Ticket");
|
|
2959
2913
|
if (state.issue) {
|
|
2960
|
-
|
|
2914
|
+
out(
|
|
2961
2915
|
row(
|
|
2962
2916
|
chalk3.cyan(`#${state.issue.number}`) + " " + chalk3.bold(truncate(state.issue.title, descMax - 6)),
|
|
2963
2917
|
w
|
|
2964
2918
|
)
|
|
2965
2919
|
);
|
|
2966
2920
|
const desc = extractDescription(state.issue.body, descMax);
|
|
2967
|
-
if (desc)
|
|
2921
|
+
if (desc) out(row(chalk3.dim(desc), w));
|
|
2968
2922
|
const tags = [];
|
|
2969
2923
|
if (state.workflowStatus !== "none")
|
|
2970
2924
|
tags.push(workflowBadge(state.workflowStatus));
|
|
@@ -2972,12 +2926,12 @@ function renderDashboard(state, actions, hint, refreshing) {
|
|
|
2972
2926
|
const l = state.issue.labels.find((x) => x.startsWith(prefix));
|
|
2973
2927
|
if (l) tags.push(chalk3.dim(l));
|
|
2974
2928
|
}
|
|
2975
|
-
if (tags.length)
|
|
2929
|
+
if (tags.length) out(row(tags.join(" "), w));
|
|
2976
2930
|
} else {
|
|
2977
|
-
|
|
2931
|
+
out(row(chalk3.dim("No linked issue"), w));
|
|
2978
2932
|
}
|
|
2979
2933
|
section("Branch");
|
|
2980
|
-
|
|
2934
|
+
out(row(chalk3.magenta(state.branch), w));
|
|
2981
2935
|
const bits = [];
|
|
2982
2936
|
if (state.commits.length > 0)
|
|
2983
2937
|
bits.push(chalk3.dim(`${state.commits.length} ahead`));
|
|
@@ -2986,12 +2940,12 @@ function renderDashboard(state, actions, hint, refreshing) {
|
|
|
2986
2940
|
if (!state.hasUncommittedChanges && !state.hasUnpushedCommits && state.commits.length > 0) {
|
|
2987
2941
|
bits.push(chalk3.green("\u25CF synced"));
|
|
2988
2942
|
}
|
|
2989
|
-
if (bits.length)
|
|
2943
|
+
if (bits.length) out(row(bits.join(chalk3.dim(" \xB7 ")), w));
|
|
2990
2944
|
section("Pull Request");
|
|
2991
2945
|
if (state.pr) {
|
|
2992
2946
|
const titleText = state.pr.title ? " " + truncate(state.pr.title, descMax - 12) : "";
|
|
2993
|
-
|
|
2994
|
-
|
|
2947
|
+
out(row(chalk3.cyan(`#${state.pr.number}`) + titleText, w));
|
|
2948
|
+
out(
|
|
2995
2949
|
row(
|
|
2996
2950
|
prBadge(state.pr.state, state.pr.isDraft) + reviewBadge(state.pr.reviewDecision),
|
|
2997
2951
|
w
|
|
@@ -2999,7 +2953,7 @@ function renderDashboard(state, actions, hint, refreshing) {
|
|
|
2999
2953
|
);
|
|
3000
2954
|
if (state.hasActionableFeedback) {
|
|
3001
2955
|
const n = state.reviewFeedback.length;
|
|
3002
|
-
|
|
2956
|
+
out(
|
|
3003
2957
|
row(
|
|
3004
2958
|
chalk3.yellow(`${n} actionable comment${n !== 1 ? "s" : ""} pending`),
|
|
3005
2959
|
w
|
|
@@ -3007,62 +2961,336 @@ function renderDashboard(state, actions, hint, refreshing) {
|
|
|
3007
2961
|
);
|
|
3008
2962
|
}
|
|
3009
2963
|
if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled && state.pr.state === "open") {
|
|
3010
|
-
|
|
2964
|
+
out(
|
|
3011
2965
|
row(
|
|
3012
2966
|
chalk3.cyan("UI changes detected") + chalk3.dim(" \xB7 video capture available"),
|
|
3013
2967
|
w
|
|
3014
2968
|
)
|
|
3015
2969
|
);
|
|
3016
2970
|
}
|
|
3017
|
-
|
|
2971
|
+
out(row(chalk3.dim(state.pr.url), w));
|
|
3018
2972
|
} else {
|
|
3019
|
-
|
|
2973
|
+
out(row(chalk3.dim("No PR created"), w));
|
|
3020
2974
|
}
|
|
3021
2975
|
section("Commits");
|
|
3022
2976
|
if (state.commits.length > 0) {
|
|
3023
2977
|
const max = 6;
|
|
3024
2978
|
for (const c of state.commits.slice(0, max)) {
|
|
3025
|
-
|
|
2979
|
+
out(row(c.substring(0, w - 5), w));
|
|
3026
2980
|
}
|
|
3027
2981
|
if (state.commits.length > max) {
|
|
3028
|
-
|
|
3029
|
-
row(chalk3.dim(`\u2026 and ${state.commits.length - max} more`), w)
|
|
3030
|
-
);
|
|
2982
|
+
out(row(chalk3.dim(`\u2026 and ${state.commits.length - max} more`), w));
|
|
3031
2983
|
}
|
|
3032
2984
|
} else {
|
|
3033
|
-
|
|
2985
|
+
out(row(chalk3.dim("No commits"), w));
|
|
3034
2986
|
}
|
|
3035
2987
|
if (hint) {
|
|
3036
2988
|
section("Hint");
|
|
3037
|
-
|
|
2989
|
+
out(row(chalk3.yellow(hint), w));
|
|
3038
2990
|
}
|
|
3039
|
-
|
|
2991
|
+
out(divRow(w));
|
|
3040
2992
|
if (refreshing) {
|
|
3041
|
-
|
|
2993
|
+
out(row(chalk3.yellow("Refreshing\u2026"), w));
|
|
3042
2994
|
} else {
|
|
3043
2995
|
for (const line of formatCommandBar(actions, w)) {
|
|
3044
|
-
|
|
2996
|
+
out(row(line, w));
|
|
3045
2997
|
}
|
|
3046
2998
|
}
|
|
3047
|
-
|
|
2999
|
+
out(botRow(w));
|
|
3000
|
+
return lines;
|
|
3001
|
+
}
|
|
3002
|
+
function renderDashboard(state, actions, hint, refreshing) {
|
|
3003
|
+
const lines = buildDashboardLines(state, actions, hint, refreshing);
|
|
3004
|
+
for (const line of lines) {
|
|
3005
|
+
console.log(line);
|
|
3006
|
+
}
|
|
3048
3007
|
}
|
|
3049
3008
|
function clearScreen() {
|
|
3050
3009
|
process.stdout.write("\x1B[2J\x1B[0f");
|
|
3051
3010
|
}
|
|
3052
3011
|
|
|
3053
|
-
// src/
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3012
|
+
// src/tui/modal.ts
|
|
3013
|
+
import chalk4 from "chalk";
|
|
3014
|
+
function modalTopRow(title, w) {
|
|
3015
|
+
const label = ` ${title} `;
|
|
3016
|
+
const fill = w - 2 - label.length;
|
|
3017
|
+
return chalk4.bold("\u250C") + chalk4.bold.cyan(label) + chalk4.bold("\u2500".repeat(Math.max(0, fill)) + "\u2510");
|
|
3018
|
+
}
|
|
3019
|
+
function modalDivRow(w) {
|
|
3020
|
+
return chalk4.bold("\u251C" + "\u2500".repeat(w - 2) + "\u2524");
|
|
3021
|
+
}
|
|
3022
|
+
function modalBotRow(w) {
|
|
3023
|
+
return chalk4.bold("\u2514" + "\u2500".repeat(w - 2) + "\u2518");
|
|
3024
|
+
}
|
|
3025
|
+
function truncateAnsi(text, max) {
|
|
3026
|
+
if (visibleLen(text) <= max) return text;
|
|
3027
|
+
let visible = 0;
|
|
3028
|
+
let i = 0;
|
|
3029
|
+
while (i < text.length && visible < max - 1) {
|
|
3030
|
+
if (text[i] === "\x1B") {
|
|
3031
|
+
const end = text.indexOf("m", i);
|
|
3032
|
+
if (end !== -1) {
|
|
3033
|
+
i = end + 1;
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3062
3036
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3037
|
+
visible++;
|
|
3038
|
+
i++;
|
|
3039
|
+
}
|
|
3040
|
+
return text.slice(0, i) + "\x1B[0m\u2026";
|
|
3065
3041
|
}
|
|
3042
|
+
function modalRow(text, w) {
|
|
3043
|
+
const inner = w - 4;
|
|
3044
|
+
const fitted = truncateAnsi(text, inner);
|
|
3045
|
+
const pad = Math.max(0, inner - visibleLen(fitted));
|
|
3046
|
+
return chalk4.bold("\u2502") + " " + fitted + " ".repeat(pad) + " " + chalk4.bold("\u2502");
|
|
3047
|
+
}
|
|
3048
|
+
function modalEmptyRow(w) {
|
|
3049
|
+
return modalRow("", w);
|
|
3050
|
+
}
|
|
3051
|
+
function buildModalFrame(title, contentLines, footerText, width) {
|
|
3052
|
+
const lines = [];
|
|
3053
|
+
lines.push(modalTopRow(title, width));
|
|
3054
|
+
lines.push(modalEmptyRow(width));
|
|
3055
|
+
for (const line of contentLines) {
|
|
3056
|
+
lines.push(modalRow(line, width));
|
|
3057
|
+
}
|
|
3058
|
+
lines.push(modalEmptyRow(width));
|
|
3059
|
+
lines.push(modalDivRow(width));
|
|
3060
|
+
lines.push(modalRow(chalk4.dim(footerText), width));
|
|
3061
|
+
lines.push(modalBotRow(width));
|
|
3062
|
+
return lines;
|
|
3063
|
+
}
|
|
3064
|
+
function isSeparator(entry) {
|
|
3065
|
+
return "separator" in entry;
|
|
3066
|
+
}
|
|
3067
|
+
function buildSelectContent(items, selectedIndex, maxWidth) {
|
|
3068
|
+
const lines = [];
|
|
3069
|
+
let selectableIdx = 0;
|
|
3070
|
+
for (const item of items) {
|
|
3071
|
+
if (isSeparator(item)) {
|
|
3072
|
+
lines.push(chalk4.dim(item.separator));
|
|
3073
|
+
} else {
|
|
3074
|
+
const isSelected = selectableIdx === selectedIndex;
|
|
3075
|
+
const prefix = isSelected ? chalk4.cyan.bold("> ") : " ";
|
|
3076
|
+
const label = item.name.length > maxWidth - 2 ? item.name.slice(0, maxWidth - 3) + "\u2026" : item.name;
|
|
3077
|
+
lines.push(
|
|
3078
|
+
prefix + (isSelected ? chalk4.bold(label) : label)
|
|
3079
|
+
);
|
|
3080
|
+
selectableIdx++;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return lines;
|
|
3084
|
+
}
|
|
3085
|
+
function buildConfirmContent(message, selectedYes) {
|
|
3086
|
+
const yes = selectedYes ? chalk4.cyan.bold("> Yes") : chalk4.dim(" Yes");
|
|
3087
|
+
const no = !selectedYes ? chalk4.cyan.bold("> No") : chalk4.dim(" No");
|
|
3088
|
+
return [message, "", yes, no];
|
|
3089
|
+
}
|
|
3090
|
+
function buildInputContent(label, value, cursorVisible) {
|
|
3091
|
+
const cursor = cursorVisible ? chalk4.cyan("_") : " ";
|
|
3092
|
+
return [label, "", chalk4.cyan("> ") + value + cursor];
|
|
3093
|
+
}
|
|
3094
|
+
function termSize() {
|
|
3095
|
+
return {
|
|
3096
|
+
cols: process.stdout.columns || 80,
|
|
3097
|
+
rows: process.stdout.rows || 24
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
function moveTo(row2, col) {
|
|
3101
|
+
return `\x1B[${row2};${col}H`;
|
|
3102
|
+
}
|
|
3103
|
+
function hideCursor() {
|
|
3104
|
+
return "\x1B[?25l";
|
|
3105
|
+
}
|
|
3106
|
+
function showCursor() {
|
|
3107
|
+
return "\x1B[?25h";
|
|
3108
|
+
}
|
|
3109
|
+
function modalWidth() {
|
|
3110
|
+
const cols = process.stdout.columns || 80;
|
|
3111
|
+
return Math.min(60, cols - 4);
|
|
3112
|
+
}
|
|
3113
|
+
function renderOverlay(dashboardLines, modalLines, mWidth) {
|
|
3114
|
+
const { cols, rows } = termSize();
|
|
3115
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
3116
|
+
process.stdout.write(hideCursor());
|
|
3117
|
+
for (let i = 0; i < dashboardLines.length && i < rows; i++) {
|
|
3118
|
+
process.stdout.write(
|
|
3119
|
+
moveTo(i + 1, 1) + chalk4.dim(stripAnsi(dashboardLines[i]))
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
const startRow = Math.max(1, Math.floor((rows - modalLines.length) / 2));
|
|
3123
|
+
const startCol = Math.max(1, Math.floor((cols - mWidth) / 2));
|
|
3124
|
+
for (let i = 0; i < modalLines.length; i++) {
|
|
3125
|
+
process.stdout.write(moveTo(startRow + i, startCol) + modalLines[i]);
|
|
3126
|
+
}
|
|
3127
|
+
process.stdout.write(moveTo(startRow + modalLines.length + 1, 1));
|
|
3128
|
+
}
|
|
3129
|
+
function readKey() {
|
|
3130
|
+
return new Promise((resolve) => {
|
|
3131
|
+
const { stdin } = process;
|
|
3132
|
+
const wasRaw = stdin.isRaw;
|
|
3133
|
+
stdin.setRawMode(true);
|
|
3134
|
+
stdin.resume();
|
|
3135
|
+
stdin.setEncoding("utf8");
|
|
3136
|
+
const onData = (data) => {
|
|
3137
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
3138
|
+
stdin.pause();
|
|
3139
|
+
stdin.removeListener("data", onData);
|
|
3140
|
+
if (data === "") {
|
|
3141
|
+
resolve({ name: "escape", raw: data });
|
|
3142
|
+
} else if (data === "\x1B" || data === "\x1B\x1B") {
|
|
3143
|
+
resolve({ name: "escape", raw: data });
|
|
3144
|
+
} else if (data === "\x1B[A") {
|
|
3145
|
+
resolve({ name: "up", raw: data });
|
|
3146
|
+
} else if (data === "\x1B[B") {
|
|
3147
|
+
resolve({ name: "down", raw: data });
|
|
3148
|
+
} else if (data === "\x1B[C") {
|
|
3149
|
+
resolve({ name: "right", raw: data });
|
|
3150
|
+
} else if (data === "\x1B[D") {
|
|
3151
|
+
resolve({ name: "left", raw: data });
|
|
3152
|
+
} else if (data === "\r" || data === "\n") {
|
|
3153
|
+
resolve({ name: "enter", raw: data });
|
|
3154
|
+
} else if (data === "\x7F" || data === "\b") {
|
|
3155
|
+
resolve({ name: "backspace", raw: data });
|
|
3156
|
+
} else if (data === " ") {
|
|
3157
|
+
resolve({ name: "tab", raw: data });
|
|
3158
|
+
} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
3159
|
+
resolve({ name: data, raw: data });
|
|
3160
|
+
} else {
|
|
3161
|
+
stdin.setRawMode(true);
|
|
3162
|
+
stdin.resume();
|
|
3163
|
+
stdin.on("data", onData);
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
stdin.on("data", onData);
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
async function showConfirm(opts) {
|
|
3170
|
+
const w = modalWidth();
|
|
3171
|
+
let selectedYes = true;
|
|
3172
|
+
const render = () => {
|
|
3173
|
+
const content = buildConfirmContent(opts.message, selectedYes);
|
|
3174
|
+
const footer = "\u2191\u2193 Select Enter Confirm Esc Cancel";
|
|
3175
|
+
const lines = buildModalFrame(opts.title, content, footer, w);
|
|
3176
|
+
renderOverlay(opts.dashboardLines, lines, w);
|
|
3177
|
+
};
|
|
3178
|
+
render();
|
|
3179
|
+
while (true) {
|
|
3180
|
+
const key = await readKey();
|
|
3181
|
+
switch (key.name) {
|
|
3182
|
+
case "up":
|
|
3183
|
+
case "down":
|
|
3184
|
+
case "tab":
|
|
3185
|
+
selectedYes = !selectedYes;
|
|
3186
|
+
render();
|
|
3187
|
+
break;
|
|
3188
|
+
case "enter":
|
|
3189
|
+
process.stdout.write(showCursor());
|
|
3190
|
+
return selectedYes;
|
|
3191
|
+
case "escape":
|
|
3192
|
+
process.stdout.write(showCursor());
|
|
3193
|
+
return false;
|
|
3194
|
+
case "y":
|
|
3195
|
+
process.stdout.write(showCursor());
|
|
3196
|
+
return true;
|
|
3197
|
+
case "n":
|
|
3198
|
+
process.stdout.write(showCursor());
|
|
3199
|
+
return false;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
function selectableCount(items) {
|
|
3204
|
+
return items.filter((i) => !isSeparator(i)).length;
|
|
3205
|
+
}
|
|
3206
|
+
async function showSelect(opts) {
|
|
3207
|
+
const w = modalWidth();
|
|
3208
|
+
const maxItems = selectableCount(opts.items);
|
|
3209
|
+
if (maxItems === 0) return null;
|
|
3210
|
+
let selectedIndex = 0;
|
|
3211
|
+
const render = () => {
|
|
3212
|
+
const content = buildSelectContent(opts.items, selectedIndex, w - 6);
|
|
3213
|
+
const footer = "\u2191\u2193 Navigate Enter Select Esc Cancel";
|
|
3214
|
+
const lines = buildModalFrame(opts.title, content, footer, w);
|
|
3215
|
+
renderOverlay(opts.dashboardLines, lines, w);
|
|
3216
|
+
};
|
|
3217
|
+
render();
|
|
3218
|
+
while (true) {
|
|
3219
|
+
const key = await readKey();
|
|
3220
|
+
switch (key.name) {
|
|
3221
|
+
case "up":
|
|
3222
|
+
selectedIndex = (selectedIndex - 1 + maxItems) % maxItems;
|
|
3223
|
+
render();
|
|
3224
|
+
break;
|
|
3225
|
+
case "down":
|
|
3226
|
+
selectedIndex = (selectedIndex + 1) % maxItems;
|
|
3227
|
+
render();
|
|
3228
|
+
break;
|
|
3229
|
+
case "enter": {
|
|
3230
|
+
process.stdout.write(showCursor());
|
|
3231
|
+
let idx = 0;
|
|
3232
|
+
for (const item of opts.items) {
|
|
3233
|
+
if (!isSeparator(item)) {
|
|
3234
|
+
if (idx === selectedIndex) return item.value;
|
|
3235
|
+
idx++;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return null;
|
|
3239
|
+
}
|
|
3240
|
+
case "escape":
|
|
3241
|
+
process.stdout.write(showCursor());
|
|
3242
|
+
return null;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
async function showInput(opts) {
|
|
3247
|
+
const w = modalWidth();
|
|
3248
|
+
let value = "";
|
|
3249
|
+
let cursorBlink = true;
|
|
3250
|
+
const render = () => {
|
|
3251
|
+
const maxLen = w - 10;
|
|
3252
|
+
const displayValue = value.length > maxLen ? value.slice(value.length - maxLen) : value;
|
|
3253
|
+
const content = buildInputContent(opts.label, displayValue, cursorBlink);
|
|
3254
|
+
const footer = "Enter Submit Esc Cancel";
|
|
3255
|
+
const lines = buildModalFrame(opts.title, content, footer, w);
|
|
3256
|
+
renderOverlay(opts.dashboardLines, lines, w);
|
|
3257
|
+
};
|
|
3258
|
+
render();
|
|
3259
|
+
while (true) {
|
|
3260
|
+
const key = await readKey();
|
|
3261
|
+
switch (key.name) {
|
|
3262
|
+
case "enter":
|
|
3263
|
+
process.stdout.write(showCursor());
|
|
3264
|
+
return value.trim() || null;
|
|
3265
|
+
case "escape":
|
|
3266
|
+
process.stdout.write(showCursor());
|
|
3267
|
+
return null;
|
|
3268
|
+
case "backspace":
|
|
3269
|
+
if (value.length > 0) {
|
|
3270
|
+
value = value.slice(0, -1);
|
|
3271
|
+
}
|
|
3272
|
+
render();
|
|
3273
|
+
break;
|
|
3274
|
+
default:
|
|
3275
|
+
if (key.raw.length === 1 && key.raw.charCodeAt(0) >= 32) {
|
|
3276
|
+
value += key.raw;
|
|
3277
|
+
cursorBlink = true;
|
|
3278
|
+
render();
|
|
3279
|
+
}
|
|
3280
|
+
break;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
function showStatus(title, message, dashboardLines) {
|
|
3285
|
+
const w = modalWidth();
|
|
3286
|
+
const content = [message];
|
|
3287
|
+
const footer = "";
|
|
3288
|
+
const lines = buildModalFrame(title, content, footer, w);
|
|
3289
|
+
renderOverlay(dashboardLines, lines, w);
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
// src/commands/tui.ts
|
|
3293
|
+
var CANCEL = /* @__PURE__ */ Symbol("cancel");
|
|
3066
3294
|
async function waitForKey(validKeys) {
|
|
3067
3295
|
return new Promise((resolve) => {
|
|
3068
3296
|
const { stdin } = process;
|
|
@@ -3088,106 +3316,66 @@ async function waitForKey(validKeys) {
|
|
|
3088
3316
|
stdin.on("data", onData);
|
|
3089
3317
|
});
|
|
3090
3318
|
}
|
|
3091
|
-
|
|
3319
|
+
var CONTINUE = { running: true, refresh: true };
|
|
3320
|
+
var SKIP_REFRESH = { running: true, refresh: false };
|
|
3321
|
+
var QUIT = { running: false, refresh: false };
|
|
3322
|
+
async function executeAction(actionId, state, dashboardLines) {
|
|
3092
3323
|
switch (actionId) {
|
|
3093
3324
|
case "quit":
|
|
3094
|
-
return
|
|
3325
|
+
return QUIT;
|
|
3095
3326
|
case "list": {
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
await listCommand({});
|
|
3099
|
-
} catch (error) {
|
|
3100
|
-
logger.error(`List failed: ${error}`);
|
|
3101
|
-
}
|
|
3102
|
-
await promptContinue();
|
|
3103
|
-
return true;
|
|
3327
|
+
const switched = await handleList(dashboardLines);
|
|
3328
|
+
return switched ? CONTINUE : SKIP_REFRESH;
|
|
3104
3329
|
}
|
|
3105
3330
|
case "create": {
|
|
3331
|
+
const description = await showInput({
|
|
3332
|
+
title: "New Ticket",
|
|
3333
|
+
label: "Describe the ticket:",
|
|
3334
|
+
dashboardLines
|
|
3335
|
+
});
|
|
3336
|
+
if (!description) return SKIP_REFRESH;
|
|
3106
3337
|
clearScreen();
|
|
3107
|
-
const { description } = await inquirer7.prompt([
|
|
3108
|
-
{
|
|
3109
|
-
type: "input",
|
|
3110
|
-
name: "description",
|
|
3111
|
-
message: "Describe the ticket (empty to cancel):"
|
|
3112
|
-
}
|
|
3113
|
-
]);
|
|
3114
|
-
if (!description.trim()) {
|
|
3115
|
-
logger.info("Cancelled");
|
|
3116
|
-
return true;
|
|
3117
|
-
}
|
|
3118
3338
|
try {
|
|
3119
3339
|
await createCommand(description, {});
|
|
3120
3340
|
} catch (error) {
|
|
3121
3341
|
logger.error(`Create failed: ${error}`);
|
|
3122
3342
|
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3343
|
+
return CONTINUE;
|
|
3344
|
+
}
|
|
3345
|
+
case "commit": {
|
|
3346
|
+
const committed = await handleCommit(state, dashboardLines);
|
|
3347
|
+
return committed ? CONTINUE : SKIP_REFRESH;
|
|
3125
3348
|
}
|
|
3126
|
-
case "commit":
|
|
3127
|
-
clearScreen();
|
|
3128
|
-
await handleCommit(state);
|
|
3129
|
-
await promptContinue();
|
|
3130
|
-
return true;
|
|
3131
3349
|
case "push":
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
await promptContinue();
|
|
3135
|
-
return true;
|
|
3350
|
+
await handlePush(dashboardLines);
|
|
3351
|
+
return CONTINUE;
|
|
3136
3352
|
case "pr": {
|
|
3137
3353
|
clearScreen();
|
|
3138
|
-
if (!await confirm("Create a pull request?")) return true;
|
|
3139
3354
|
await prCommand({});
|
|
3140
|
-
|
|
3141
|
-
return true;
|
|
3355
|
+
return CONTINUE;
|
|
3142
3356
|
}
|
|
3143
3357
|
case "run": {
|
|
3144
|
-
clearScreen();
|
|
3145
|
-
const hasCommits = state.commits.length > 0;
|
|
3146
|
-
const hasFeedback = state.hasActionableFeedback;
|
|
3147
|
-
let msg;
|
|
3148
|
-
if (hasFeedback && hasCommits) {
|
|
3149
|
-
msg = "Start AI agent to address review feedback?";
|
|
3150
|
-
} else if (hasCommits) {
|
|
3151
|
-
msg = "Start AI agent to continue implementation from existing commits?";
|
|
3152
|
-
} else {
|
|
3153
|
-
msg = "Start AI agent to implement this ticket from scratch?";
|
|
3154
|
-
}
|
|
3155
|
-
if (!await confirm(msg)) return true;
|
|
3156
3358
|
await handleRun(state);
|
|
3157
|
-
|
|
3158
|
-
return true;
|
|
3159
|
-
}
|
|
3160
|
-
case "video": {
|
|
3161
|
-
clearScreen();
|
|
3162
|
-
if (!await confirm("Record video of UI changes?")) return true;
|
|
3163
|
-
await handleVideoCapture(state);
|
|
3164
|
-
await promptContinue();
|
|
3165
|
-
return true;
|
|
3359
|
+
return CONTINUE;
|
|
3166
3360
|
}
|
|
3167
3361
|
case "switch-provider":
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
if (!await confirm("Switch to main branch?")) return true;
|
|
3174
|
-
await handleCheckoutMain();
|
|
3175
|
-
return true;
|
|
3176
|
-
}
|
|
3362
|
+
await handleSwitchProvider(state, dashboardLines);
|
|
3363
|
+
return SKIP_REFRESH;
|
|
3364
|
+
case "checkout-main":
|
|
3365
|
+
await handleCheckoutMain(dashboardLines);
|
|
3366
|
+
return CONTINUE;
|
|
3177
3367
|
default:
|
|
3178
|
-
return
|
|
3368
|
+
return SKIP_REFRESH;
|
|
3179
3369
|
}
|
|
3180
3370
|
}
|
|
3181
|
-
async function handleCommit(state) {
|
|
3371
|
+
async function handleCommit(state, dashboardLines) {
|
|
3182
3372
|
try {
|
|
3183
3373
|
const { stdout: status } = await execa4("git", ["status", "--short"]);
|
|
3184
3374
|
if (!status.trim()) {
|
|
3185
|
-
|
|
3186
|
-
|
|
3375
|
+
showStatus("Commit", "No changes to commit", dashboardLines);
|
|
3376
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
3377
|
+
return false;
|
|
3187
3378
|
}
|
|
3188
|
-
logger.info("Changes:");
|
|
3189
|
-
console.log(status);
|
|
3190
|
-
console.log();
|
|
3191
3379
|
await execa4("git", ["add", "-A"]);
|
|
3192
3380
|
const { stdout: diffStat } = await execa4("git", [
|
|
3193
3381
|
"diff",
|
|
@@ -3200,69 +3388,69 @@ async function handleCommit(state) {
|
|
|
3200
3388
|
const issueTitle = state.issue?.title ?? null;
|
|
3201
3389
|
const provider = state.config.ai.provider;
|
|
3202
3390
|
const providerName = getProviderDisplayName(provider);
|
|
3203
|
-
const
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
name: "
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3391
|
+
const mode = await showSelect({
|
|
3392
|
+
title: "Commit",
|
|
3393
|
+
items: [
|
|
3394
|
+
{ name: `Generate with ${providerName}`, value: "ai" },
|
|
3395
|
+
{ name: "Enter manually", value: "manual" }
|
|
3396
|
+
],
|
|
3397
|
+
dashboardLines
|
|
3398
|
+
});
|
|
3399
|
+
if (!mode) {
|
|
3400
|
+
await execa4("git", ["reset", "HEAD"]);
|
|
3401
|
+
return false;
|
|
3402
|
+
}
|
|
3214
3403
|
let message;
|
|
3215
3404
|
if (mode === "manual") {
|
|
3216
|
-
const
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
]);
|
|
3223
|
-
message = manualInput.trim() || CANCEL;
|
|
3405
|
+
const input = await showInput({
|
|
3406
|
+
title: "Commit Message",
|
|
3407
|
+
label: "Enter commit message:",
|
|
3408
|
+
dashboardLines
|
|
3409
|
+
});
|
|
3410
|
+
message = input || CANCEL;
|
|
3224
3411
|
} else {
|
|
3225
|
-
|
|
3412
|
+
showStatus("Generating", `Generating commit message with ${providerName}...`, dashboardLines);
|
|
3226
3413
|
message = await generateCommitMessage(
|
|
3227
3414
|
diffContent,
|
|
3228
3415
|
issueNumber,
|
|
3229
3416
|
issueTitle,
|
|
3230
|
-
state
|
|
3417
|
+
state,
|
|
3418
|
+
dashboardLines
|
|
3231
3419
|
);
|
|
3232
3420
|
}
|
|
3233
3421
|
if (message === CANCEL) {
|
|
3234
3422
|
await execa4("git", ["reset", "HEAD"]);
|
|
3235
|
-
|
|
3236
|
-
return;
|
|
3423
|
+
return false;
|
|
3237
3424
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3425
|
+
const confirmed = await showConfirm({
|
|
3426
|
+
title: "Commit",
|
|
3427
|
+
message: `Message: ${message.length > 50 ? message.slice(0, 50) + "\u2026" : message}`,
|
|
3428
|
+
dashboardLines
|
|
3429
|
+
});
|
|
3430
|
+
if (!confirmed) {
|
|
3242
3431
|
await execa4("git", ["reset", "HEAD"]);
|
|
3243
|
-
|
|
3244
|
-
return;
|
|
3432
|
+
return false;
|
|
3245
3433
|
}
|
|
3246
3434
|
const providerEmail = getProviderEmail(provider);
|
|
3247
3435
|
const fullMessage = `${message}
|
|
3248
3436
|
|
|
3249
3437
|
Co-Authored-By: ${providerName} <${providerEmail}>`;
|
|
3250
|
-
|
|
3251
|
-
spinner.start();
|
|
3438
|
+
showStatus("Committing", "Committing changes...", dashboardLines);
|
|
3252
3439
|
await execa4("git", ["commit", "-m", fullMessage]);
|
|
3253
|
-
|
|
3440
|
+
return true;
|
|
3254
3441
|
} catch (error) {
|
|
3255
3442
|
logger.error(`Commit failed: ${error}`);
|
|
3443
|
+
return false;
|
|
3256
3444
|
}
|
|
3257
3445
|
}
|
|
3258
|
-
async function generateCommitMessage(diffContent, issueNumber, issueTitle, state) {
|
|
3446
|
+
async function generateCommitMessage(diffContent, issueNumber, issueTitle, state, dashboardLines) {
|
|
3259
3447
|
try {
|
|
3260
3448
|
const prompt = buildCommitMessagePrompt(
|
|
3261
3449
|
diffContent,
|
|
3262
3450
|
issueNumber,
|
|
3263
3451
|
issueTitle
|
|
3264
3452
|
);
|
|
3265
|
-
const result = await invokeAI({ prompt, streamOutput:
|
|
3453
|
+
const result = await invokeAI({ prompt, streamOutput: false }, state.config);
|
|
3266
3454
|
let message = result.output.trim().split("\n")[0].trim();
|
|
3267
3455
|
for (const q of ['"', "'", "`"]) {
|
|
3268
3456
|
if (message.startsWith(q) && message.endsWith(q)) {
|
|
@@ -3273,16 +3461,12 @@ async function generateCommitMessage(diffContent, issueNumber, issueTitle, state
|
|
|
3273
3461
|
message = message.replace(/^```\w*\s*/, "").replace(/\s*```$/, "");
|
|
3274
3462
|
return message;
|
|
3275
3463
|
} catch {
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
message: "Commit message (empty to cancel):"
|
|
3283
|
-
}
|
|
3284
|
-
]);
|
|
3285
|
-
return message.trim() || CANCEL;
|
|
3464
|
+
const input = await showInput({
|
|
3465
|
+
title: "Commit Message",
|
|
3466
|
+
label: "AI generation failed. Enter commit message:",
|
|
3467
|
+
dashboardLines
|
|
3468
|
+
});
|
|
3469
|
+
return input || CANCEL;
|
|
3286
3470
|
}
|
|
3287
3471
|
}
|
|
3288
3472
|
async function handleRun(state) {
|
|
@@ -3324,92 +3508,196 @@ ${feedbackLines}`);
|
|
|
3324
3508
|
]);
|
|
3325
3509
|
console.log();
|
|
3326
3510
|
try {
|
|
3327
|
-
await invokeAIInteractive(prompt, state.config);
|
|
3511
|
+
const { result } = await invokeAIInteractive(prompt, state.config);
|
|
3512
|
+
await result;
|
|
3328
3513
|
} catch (error) {
|
|
3329
3514
|
logger.error(`${providerName} session failed: ${error}`);
|
|
3330
3515
|
}
|
|
3331
3516
|
}
|
|
3332
|
-
async function handlePush() {
|
|
3517
|
+
async function handlePush(dashboardLines) {
|
|
3333
3518
|
try {
|
|
3334
3519
|
const { stdout: branch } = await execa4("git", ["branch", "--show-current"]);
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
await execa4("git", ["push", "-u", "origin", branch.trim()]);
|
|
3339
|
-
spinner.succeed("Pushed to remote");
|
|
3520
|
+
const branchName = branch.trim();
|
|
3521
|
+
showStatus("Pushing", `Pushing ${branchName} to remote...`, dashboardLines);
|
|
3522
|
+
await execa4("git", ["push", "-u", "origin", branchName]);
|
|
3340
3523
|
} catch (error) {
|
|
3341
3524
|
logger.error(`Push failed: ${error}`);
|
|
3342
3525
|
}
|
|
3343
3526
|
}
|
|
3344
3527
|
var PROVIDERS = ["claude", "gemini", "codex"];
|
|
3345
|
-
async function handleSwitchProvider(state) {
|
|
3528
|
+
async function handleSwitchProvider(state, dashboardLines) {
|
|
3346
3529
|
const current = state.config.ai.provider;
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
}
|
|
3358
|
-
]);
|
|
3359
|
-
if (provider === current) return;
|
|
3530
|
+
const items = PROVIDERS.map((p) => ({
|
|
3531
|
+
name: p.charAt(0).toUpperCase() + p.slice(1) + (p === current ? " (current)" : ""),
|
|
3532
|
+
value: p
|
|
3533
|
+
}));
|
|
3534
|
+
const provider = await showSelect({
|
|
3535
|
+
title: "AI Provider",
|
|
3536
|
+
items,
|
|
3537
|
+
dashboardLines
|
|
3538
|
+
});
|
|
3539
|
+
if (!provider || provider === current) return;
|
|
3360
3540
|
setRuntimeProvider(provider);
|
|
3361
|
-
|
|
3541
|
+
state.config.ai.provider = provider;
|
|
3362
3542
|
}
|
|
3363
|
-
async function handleCheckoutMain() {
|
|
3543
|
+
async function handleCheckoutMain(dashboardLines) {
|
|
3364
3544
|
try {
|
|
3365
|
-
|
|
3366
|
-
spinner.start();
|
|
3545
|
+
showStatus("Switching", "Switching to main...", dashboardLines);
|
|
3367
3546
|
await execa4("git", ["checkout", "main"]);
|
|
3368
3547
|
await execa4("git", ["pull"]);
|
|
3369
|
-
spinner.succeed("Switched to main");
|
|
3370
3548
|
} catch (error) {
|
|
3371
3549
|
logger.error(`Checkout failed: ${error}`);
|
|
3372
3550
|
}
|
|
3373
3551
|
}
|
|
3374
|
-
async function
|
|
3375
|
-
if (!state.issue) {
|
|
3376
|
-
logger.error("No linked issue found");
|
|
3377
|
-
return;
|
|
3378
|
-
}
|
|
3379
|
-
const providerName = getProviderDisplayName(state.config.ai.provider);
|
|
3380
|
-
clearScreen();
|
|
3381
|
-
renderActionPanel("Video Capture", [
|
|
3382
|
-
`Recording: #${state.issue.number} ${state.issue.title}`,
|
|
3383
|
-
`Provider: ${providerName}`
|
|
3384
|
-
]);
|
|
3385
|
-
console.log();
|
|
3552
|
+
async function handleList(dashboardLines) {
|
|
3386
3553
|
try {
|
|
3387
|
-
|
|
3388
|
-
const
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3554
|
+
showStatus("Loading", "Fetching tickets...", dashboardLines);
|
|
3555
|
+
const config = loadConfig();
|
|
3556
|
+
const workflowLabels = getWorkflowLabels(config);
|
|
3557
|
+
const [inProgress, ready, prs, localBranches] = await Promise.all([
|
|
3558
|
+
listIssues({
|
|
3559
|
+
labels: [workflowLabels.inProgress],
|
|
3560
|
+
state: "open",
|
|
3561
|
+
limit: 20
|
|
3562
|
+
}),
|
|
3563
|
+
listIssues({
|
|
3564
|
+
labels: [workflowLabels.ready],
|
|
3565
|
+
state: "open",
|
|
3566
|
+
limit: 20
|
|
3567
|
+
}),
|
|
3568
|
+
listOpenPrs(30),
|
|
3569
|
+
listLocalBranches()
|
|
3570
|
+
]);
|
|
3571
|
+
sortByPriority(inProgress);
|
|
3572
|
+
sortByPriority(ready);
|
|
3573
|
+
const choices = buildTicketChoices(inProgress, ready, prs, localBranches);
|
|
3574
|
+
const currentBranch = await getCurrentBranch();
|
|
3575
|
+
const defaultBranch = await getDefaultBranch();
|
|
3576
|
+
const items = [];
|
|
3577
|
+
const mainLabel = defaultBranch + (currentBranch === defaultBranch ? " (current)" : "");
|
|
3578
|
+
items.push({ name: mainLabel, value: "__main__" });
|
|
3579
|
+
const inProgressChoices = choices.filter(
|
|
3580
|
+
(c) => c.category === "in-progress"
|
|
3393
3581
|
);
|
|
3394
|
-
|
|
3395
|
-
|
|
3582
|
+
const openPrChoices = choices.filter((c) => c.category === "open-pr");
|
|
3583
|
+
const readyChoices = choices.filter((c) => c.category === "ready");
|
|
3584
|
+
if (inProgressChoices.length > 0) {
|
|
3585
|
+
items.push({ separator: "\u2500\u2500 In Progress \u2500\u2500" });
|
|
3586
|
+
for (const c of inProgressChoices) {
|
|
3587
|
+
items.push({
|
|
3588
|
+
name: `#${c.issueNumber} ${c.title}`,
|
|
3589
|
+
value: String(c.issueNumber)
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (openPrChoices.length > 0) {
|
|
3594
|
+
items.push({ separator: "\u2500\u2500 Open PRs \u2500\u2500" });
|
|
3595
|
+
for (const c of openPrChoices) {
|
|
3596
|
+
items.push({
|
|
3597
|
+
name: `#${c.issueNumber} ${c.title}`,
|
|
3598
|
+
value: String(c.issueNumber)
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
if (readyChoices.length > 0) {
|
|
3603
|
+
items.push({ separator: "\u2500\u2500 Ready \u2500\u2500" });
|
|
3604
|
+
for (const c of readyChoices) {
|
|
3605
|
+
items.push({
|
|
3606
|
+
name: `#${c.issueNumber} ${c.title}`,
|
|
3607
|
+
value: String(c.issueNumber)
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
if (choices.length === 0) {
|
|
3612
|
+
showStatus("List", "No tickets found", dashboardLines);
|
|
3613
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
3614
|
+
return false;
|
|
3615
|
+
}
|
|
3616
|
+
const selected = await showSelect({
|
|
3617
|
+
title: "Switch Ticket",
|
|
3618
|
+
items,
|
|
3619
|
+
dashboardLines
|
|
3620
|
+
});
|
|
3621
|
+
if (!selected) return false;
|
|
3622
|
+
if (selected === "__main__") {
|
|
3623
|
+
if (currentBranch === defaultBranch) return false;
|
|
3624
|
+
const dirty2 = await hasUncommittedChanges();
|
|
3625
|
+
if (dirty2) {
|
|
3626
|
+
const ok = await showConfirm({
|
|
3627
|
+
title: "Uncommitted Changes",
|
|
3628
|
+
message: "You have uncommitted changes. Continue?",
|
|
3629
|
+
dashboardLines
|
|
3630
|
+
});
|
|
3631
|
+
if (!ok) return false;
|
|
3632
|
+
}
|
|
3633
|
+
showStatus("Switching", `Switching to ${defaultBranch}...`, dashboardLines);
|
|
3634
|
+
await checkoutBranch(defaultBranch);
|
|
3635
|
+
return true;
|
|
3636
|
+
}
|
|
3637
|
+
const issueNumber = parseInt(selected, 10);
|
|
3638
|
+
const ticket = choices.find((c) => c.issueNumber === issueNumber);
|
|
3639
|
+
if (!ticket) return false;
|
|
3640
|
+
const dirty = await hasUncommittedChanges();
|
|
3641
|
+
if (dirty) {
|
|
3642
|
+
const ok = await showConfirm({
|
|
3643
|
+
title: "Uncommitted Changes",
|
|
3644
|
+
message: "You have uncommitted changes. Continue?",
|
|
3645
|
+
dashboardLines
|
|
3646
|
+
});
|
|
3647
|
+
if (!ok) return false;
|
|
3648
|
+
}
|
|
3649
|
+
const targetBranch = ticket.branch;
|
|
3650
|
+
if (targetBranch) {
|
|
3651
|
+
if (await branchExists(targetBranch)) {
|
|
3652
|
+
showStatus("Switching", `Switching to ${targetBranch}...`, dashboardLines);
|
|
3653
|
+
await checkoutBranch(targetBranch);
|
|
3654
|
+
return true;
|
|
3655
|
+
} else if (await remoteBranchExists(targetBranch)) {
|
|
3656
|
+
showStatus("Fetching", `Fetching ${targetBranch}...`, dashboardLines);
|
|
3657
|
+
await fetchAndCheckout(targetBranch);
|
|
3658
|
+
return true;
|
|
3659
|
+
} else {
|
|
3660
|
+
return await offerCreateBranch2(
|
|
3661
|
+
issueNumber,
|
|
3662
|
+
ticket.title,
|
|
3663
|
+
dashboardLines
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
} else {
|
|
3667
|
+
return await offerCreateBranch2(
|
|
3668
|
+
issueNumber,
|
|
3669
|
+
ticket.title,
|
|
3670
|
+
dashboardLines
|
|
3671
|
+
);
|
|
3672
|
+
}
|
|
3396
3673
|
} catch (error) {
|
|
3397
|
-
logger.error(`
|
|
3674
|
+
logger.error(`List failed: ${error}`);
|
|
3675
|
+
return false;
|
|
3398
3676
|
}
|
|
3399
3677
|
}
|
|
3400
|
-
async function
|
|
3401
|
-
|
|
3402
|
-
await
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3678
|
+
async function offerCreateBranch2(issueNumber, title, dashboardLines) {
|
|
3679
|
+
const config = loadConfig();
|
|
3680
|
+
const branchName = await generateBranchName(
|
|
3681
|
+
config,
|
|
3682
|
+
issueNumber,
|
|
3683
|
+
title,
|
|
3684
|
+
"feature"
|
|
3685
|
+
);
|
|
3686
|
+
const create = await showConfirm({
|
|
3687
|
+
title: "Create Branch",
|
|
3688
|
+
message: `No branch found. Create ${branchName}?`,
|
|
3689
|
+
dashboardLines
|
|
3690
|
+
});
|
|
3691
|
+
if (!create) return false;
|
|
3692
|
+
const defaultBranch = await getDefaultBranch();
|
|
3693
|
+
showStatus("Creating", `Creating ${branchName}...`, dashboardLines);
|
|
3694
|
+
await createBranch(branchName, defaultBranch);
|
|
3695
|
+
return true;
|
|
3409
3696
|
}
|
|
3410
3697
|
async function tuiCommand() {
|
|
3411
3698
|
let running = true;
|
|
3412
3699
|
let lastActions = [];
|
|
3700
|
+
let lastDashboardLines = [];
|
|
3413
3701
|
const config = loadConfig();
|
|
3414
3702
|
let lastState = {
|
|
3415
3703
|
isGitRepo: true,
|
|
@@ -3433,28 +3721,36 @@ async function tuiCommand() {
|
|
|
3433
3721
|
hasUIChanges: false,
|
|
3434
3722
|
isPlaywrightAvailable: false
|
|
3435
3723
|
};
|
|
3724
|
+
let needsRefresh = true;
|
|
3436
3725
|
while (running) {
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3726
|
+
if (needsRefresh) {
|
|
3727
|
+
clearScreen();
|
|
3728
|
+
renderDashboard(lastState, lastActions, void 0, true);
|
|
3729
|
+
const state = await aggregateState();
|
|
3730
|
+
const actions = getAvailableActions(state);
|
|
3731
|
+
lastState = state;
|
|
3732
|
+
lastActions = actions;
|
|
3733
|
+
}
|
|
3444
3734
|
let hint;
|
|
3445
|
-
if (
|
|
3735
|
+
if (lastState.isOnMain) {
|
|
3446
3736
|
hint = "Select an action to get started";
|
|
3447
|
-
} else if (
|
|
3737
|
+
} else if (lastState.hasUncommittedChanges && !lastState.pr) {
|
|
3448
3738
|
hint = "Commit your changes before creating a PR";
|
|
3449
|
-
} else if (
|
|
3739
|
+
} else if (lastState.hasActionableFeedback) {
|
|
3450
3740
|
hint = "Review feedback needs attention";
|
|
3451
3741
|
}
|
|
3452
|
-
|
|
3453
|
-
|
|
3742
|
+
lastDashboardLines = buildDashboardLines(lastState, lastActions, hint);
|
|
3743
|
+
clearScreen();
|
|
3744
|
+
for (const line of lastDashboardLines) {
|
|
3745
|
+
console.log(line);
|
|
3746
|
+
}
|
|
3747
|
+
const validKeys = lastActions.map((a) => a.shortcut);
|
|
3454
3748
|
const key = await waitForKey(validKeys);
|
|
3455
|
-
const action =
|
|
3749
|
+
const action = lastActions.find((a) => a.shortcut === key);
|
|
3456
3750
|
if (action) {
|
|
3457
|
-
|
|
3751
|
+
const result = await executeAction(action.id, lastState, lastDashboardLines);
|
|
3752
|
+
running = result.running;
|
|
3753
|
+
needsRefresh = result.refresh;
|
|
3458
3754
|
}
|
|
3459
3755
|
}
|
|
3460
3756
|
}
|