@rotorsoft/gent 1.14.0 → 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/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-SKWCS6Z2.js";
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-C6YHJ637.js");
203
+ const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-LB7X6A3K.js");
205
204
  await setupLabelsCommand2();
206
205
  }
207
206
  }
@@ -1556,9 +1555,6 @@ No commits were created. Please retry later.`
1556
1555
  );
1557
1556
  }
1558
1557
 
1559
- // src/commands/pr.ts
1560
- import inquirer5 from "inquirer";
1561
-
1562
1558
  // src/lib/playwright.ts
1563
1559
  import { execa as execa3 } from "execa";
1564
1560
  var UI_FILE_PATTERNS = [
@@ -1616,17 +1612,17 @@ async function prCommand(options) {
1616
1612
  ]);
1617
1613
  if (!ghAuth) {
1618
1614
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1619
- process.exit(1);
1615
+ return;
1620
1616
  }
1621
1617
  if (!aiOk) {
1622
1618
  logger.error(
1623
1619
  `${providerName} CLI not found. Please install ${provider} CLI first.`
1624
1620
  );
1625
- process.exit(1);
1621
+ return;
1626
1622
  }
1627
1623
  if (await isOnMainBranch()) {
1628
1624
  logger.error("Cannot create PR from main/master branch.");
1629
- process.exit(1);
1625
+ return;
1630
1626
  }
1631
1627
  const existingPr = await getPrForBranch();
1632
1628
  if (existingPr) {
@@ -1641,21 +1637,10 @@ async function prCommand(options) {
1641
1637
  logger.info(`Base: ${colors.branch(baseBranch)}`);
1642
1638
  const hasUnpushed = await getUnpushedCommits();
1643
1639
  if (hasUnpushed) {
1644
- logger.warning("Branch has unpushed commits.");
1645
- const { push } = await inquirer5.prompt([
1646
- {
1647
- type: "confirm",
1648
- name: "push",
1649
- message: "Push to remote before creating PR?",
1650
- default: true
1651
- }
1652
- ]);
1653
- if (push) {
1654
- await withSpinner("Pushing branch...", async () => {
1655
- await pushBranch();
1656
- });
1657
- logger.success("Branch pushed");
1658
- }
1640
+ await withSpinner("Pushing branch...", async () => {
1641
+ await pushBranch();
1642
+ });
1643
+ logger.success("Branch pushed");
1659
1644
  }
1660
1645
  const issueNumber = extractIssueNumber(currentBranch);
1661
1646
  let issue = null;
@@ -1808,7 +1793,7 @@ function generateFallbackBody(issue, commits) {
1808
1793
  }
1809
1794
 
1810
1795
  // src/commands/fix.ts
1811
- import inquirer6 from "inquirer";
1796
+ import inquirer5 from "inquirer";
1812
1797
 
1813
1798
  // src/lib/review-feedback.ts
1814
1799
  var ACTIONABLE_KEYWORDS = [
@@ -2005,7 +1990,7 @@ async function fixCommand(options) {
2005
1990
  const hasChanges = await hasUncommittedChanges();
2006
1991
  if (hasChanges) {
2007
1992
  logger.warning("You have uncommitted changes.");
2008
- const { proceed } = await inquirer6.prompt([
1993
+ const { proceed } = await inquirer5.prompt([
2009
1994
  {
2010
1995
  type: "confirm",
2011
1996
  name: "proceed",
@@ -2165,7 +2150,7 @@ import { homedir } from "os";
2165
2150
  // package.json
2166
2151
  var package_default = {
2167
2152
  name: "@rotorsoft/gent",
2168
- version: "1.14.0",
2153
+ version: "1.14.1",
2169
2154
  description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
2170
2155
  keywords: [
2171
2156
  "cli",
@@ -2587,7 +2572,6 @@ async function statusCommand() {
2587
2572
  }
2588
2573
 
2589
2574
  // src/commands/tui.ts
2590
- import inquirer7 from "inquirer";
2591
2575
  import { execa as execa4 } from "execa";
2592
2576
 
2593
2577
  // src/tui/state.ts
@@ -2868,75 +2852,73 @@ function renderActionPanel(title, content) {
2868
2852
  }
2869
2853
  console.log(botRow(w));
2870
2854
  }
2871
- function renderSettings(state, w) {
2855
+ function renderSettingsTo(state, w, out) {
2872
2856
  const provider = getProviderDisplayName(state.config.ai.provider);
2873
2857
  const provTag = state.isAIProviderAvailable ? chalk3.green(provider) : chalk3.red(provider);
2874
2858
  const ghTag = state.isGhAuthenticated ? chalk3.green("authenticated") : chalk3.red("not authenticated");
2875
- const videoTag = state.config.video.enabled ? chalk3.green("on") : chalk3.dim("off");
2876
- console.log(row(chalk3.dim("Provider: ") + provTag, w));
2877
- console.log(row(chalk3.dim("GitHub: ") + ghTag, w));
2878
- console.log(row(chalk3.dim("Video: ") + videoTag, w));
2859
+ out(row(chalk3.dim("Provider: ") + provTag, w));
2860
+ out(row(chalk3.dim("GitHub: ") + ghTag, w));
2879
2861
  }
2880
- function renderDashboard(state, actions, hint, refreshing) {
2862
+ function buildDashboardLines(state, actions, hint, refreshing) {
2863
+ const lines = [];
2864
+ const out = (line) => lines.push(line);
2881
2865
  const w = termWidth();
2882
2866
  const descMax = w - 8;
2883
2867
  const version2 = getVersion();
2884
2868
  const titleLabel = `gent v${version2}`;
2885
- console.log(topRow(titleLabel, w));
2886
- renderSettings(state, w);
2869
+ out(topRow(titleLabel, w));
2870
+ renderSettingsTo(state, w, out);
2887
2871
  if (!state.isGitRepo) {
2888
- console.log(row(chalk3.red("Not a git repository"), w));
2889
- console.log(
2890
- row(chalk3.dim("Run gent init in a git repo to get started"), w)
2891
- );
2892
- console.log(botRow(w));
2893
- 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;
2894
2876
  }
2895
2877
  if (!state.isGhAuthenticated) {
2896
- console.log(row(chalk3.red("GitHub CLI not authenticated"), w));
2897
- console.log(row(chalk3.dim("Run: gh auth login"), w));
2898
- console.log(botRow(w));
2899
- 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;
2900
2882
  }
2901
2883
  if (state.isOnMain) {
2902
- console.log(
2884
+ out(
2903
2885
  row(
2904
2886
  chalk3.magenta(state.branch) + chalk3.dim(" \xB7 ready to start new work"),
2905
2887
  w
2906
2888
  )
2907
2889
  );
2908
2890
  if (state.hasUncommittedChanges) {
2909
- console.log(row(chalk3.yellow("\u25CF uncommitted changes"), w));
2891
+ out(row(chalk3.yellow("\u25CF uncommitted changes"), w));
2910
2892
  }
2911
2893
  if (hint) {
2912
- console.log(midRow("Hint", w));
2913
- console.log(row(chalk3.yellow(hint), w));
2894
+ out(midRow("Hint", w));
2895
+ out(row(chalk3.yellow(hint), w));
2914
2896
  }
2915
- console.log(divRow(w));
2897
+ out(divRow(w));
2916
2898
  if (refreshing) {
2917
- console.log(row(chalk3.yellow("Refreshing\u2026"), w));
2899
+ out(row(chalk3.yellow("Refreshing\u2026"), w));
2918
2900
  } else {
2919
2901
  for (const line of formatCommandBar(actions, w)) {
2920
- console.log(row(line, w));
2902
+ out(row(line, w));
2921
2903
  }
2922
2904
  }
2923
- console.log(botRow(w));
2924
- console.log();
2925
- return;
2905
+ out(botRow(w));
2906
+ out("");
2907
+ return lines;
2926
2908
  }
2927
2909
  const section = (title) => {
2928
- console.log(midRow(title, w));
2910
+ out(midRow(title, w));
2929
2911
  };
2930
2912
  section("Ticket");
2931
2913
  if (state.issue) {
2932
- console.log(
2914
+ out(
2933
2915
  row(
2934
2916
  chalk3.cyan(`#${state.issue.number}`) + " " + chalk3.bold(truncate(state.issue.title, descMax - 6)),
2935
2917
  w
2936
2918
  )
2937
2919
  );
2938
2920
  const desc = extractDescription(state.issue.body, descMax);
2939
- if (desc) console.log(row(chalk3.dim(desc), w));
2921
+ if (desc) out(row(chalk3.dim(desc), w));
2940
2922
  const tags = [];
2941
2923
  if (state.workflowStatus !== "none")
2942
2924
  tags.push(workflowBadge(state.workflowStatus));
@@ -2944,12 +2926,12 @@ function renderDashboard(state, actions, hint, refreshing) {
2944
2926
  const l = state.issue.labels.find((x) => x.startsWith(prefix));
2945
2927
  if (l) tags.push(chalk3.dim(l));
2946
2928
  }
2947
- if (tags.length) console.log(row(tags.join(" "), w));
2929
+ if (tags.length) out(row(tags.join(" "), w));
2948
2930
  } else {
2949
- console.log(row(chalk3.dim("No linked issue"), w));
2931
+ out(row(chalk3.dim("No linked issue"), w));
2950
2932
  }
2951
2933
  section("Branch");
2952
- console.log(row(chalk3.magenta(state.branch), w));
2934
+ out(row(chalk3.magenta(state.branch), w));
2953
2935
  const bits = [];
2954
2936
  if (state.commits.length > 0)
2955
2937
  bits.push(chalk3.dim(`${state.commits.length} ahead`));
@@ -2958,12 +2940,12 @@ function renderDashboard(state, actions, hint, refreshing) {
2958
2940
  if (!state.hasUncommittedChanges && !state.hasUnpushedCommits && state.commits.length > 0) {
2959
2941
  bits.push(chalk3.green("\u25CF synced"));
2960
2942
  }
2961
- if (bits.length) console.log(row(bits.join(chalk3.dim(" \xB7 ")), w));
2943
+ if (bits.length) out(row(bits.join(chalk3.dim(" \xB7 ")), w));
2962
2944
  section("Pull Request");
2963
2945
  if (state.pr) {
2964
2946
  const titleText = state.pr.title ? " " + truncate(state.pr.title, descMax - 12) : "";
2965
- console.log(row(chalk3.cyan(`#${state.pr.number}`) + titleText, w));
2966
- console.log(
2947
+ out(row(chalk3.cyan(`#${state.pr.number}`) + titleText, w));
2948
+ out(
2967
2949
  row(
2968
2950
  prBadge(state.pr.state, state.pr.isDraft) + reviewBadge(state.pr.reviewDecision),
2969
2951
  w
@@ -2971,7 +2953,7 @@ function renderDashboard(state, actions, hint, refreshing) {
2971
2953
  );
2972
2954
  if (state.hasActionableFeedback) {
2973
2955
  const n = state.reviewFeedback.length;
2974
- console.log(
2956
+ out(
2975
2957
  row(
2976
2958
  chalk3.yellow(`${n} actionable comment${n !== 1 ? "s" : ""} pending`),
2977
2959
  w
@@ -2979,62 +2961,336 @@ function renderDashboard(state, actions, hint, refreshing) {
2979
2961
  );
2980
2962
  }
2981
2963
  if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled && state.pr.state === "open") {
2982
- console.log(
2964
+ out(
2983
2965
  row(
2984
2966
  chalk3.cyan("UI changes detected") + chalk3.dim(" \xB7 video capture available"),
2985
2967
  w
2986
2968
  )
2987
2969
  );
2988
2970
  }
2989
- console.log(row(chalk3.dim(state.pr.url), w));
2971
+ out(row(chalk3.dim(state.pr.url), w));
2990
2972
  } else {
2991
- console.log(row(chalk3.dim("No PR created"), w));
2973
+ out(row(chalk3.dim("No PR created"), w));
2992
2974
  }
2993
2975
  section("Commits");
2994
2976
  if (state.commits.length > 0) {
2995
2977
  const max = 6;
2996
2978
  for (const c of state.commits.slice(0, max)) {
2997
- console.log(row(c.substring(0, w - 5), w));
2979
+ out(row(c.substring(0, w - 5), w));
2998
2980
  }
2999
2981
  if (state.commits.length > max) {
3000
- console.log(
3001
- row(chalk3.dim(`\u2026 and ${state.commits.length - max} more`), w)
3002
- );
2982
+ out(row(chalk3.dim(`\u2026 and ${state.commits.length - max} more`), w));
3003
2983
  }
3004
2984
  } else {
3005
- console.log(row(chalk3.dim("No commits"), w));
2985
+ out(row(chalk3.dim("No commits"), w));
3006
2986
  }
3007
2987
  if (hint) {
3008
2988
  section("Hint");
3009
- console.log(row(chalk3.yellow(hint), w));
2989
+ out(row(chalk3.yellow(hint), w));
3010
2990
  }
3011
- console.log(divRow(w));
2991
+ out(divRow(w));
3012
2992
  if (refreshing) {
3013
- console.log(row(chalk3.yellow("Refreshing\u2026"), w));
2993
+ out(row(chalk3.yellow("Refreshing\u2026"), w));
3014
2994
  } else {
3015
2995
  for (const line of formatCommandBar(actions, w)) {
3016
- console.log(row(line, w));
2996
+ out(row(line, w));
3017
2997
  }
3018
2998
  }
3019
- console.log(botRow(w));
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
+ }
3020
3007
  }
3021
3008
  function clearScreen() {
3022
3009
  process.stdout.write("\x1B[2J\x1B[0f");
3023
3010
  }
3024
3011
 
3025
- // src/commands/tui.ts
3026
- var CANCEL = /* @__PURE__ */ Symbol("cancel");
3027
- async function confirm(message) {
3028
- const { ok } = await inquirer7.prompt([
3029
- {
3030
- type: "confirm",
3031
- name: "ok",
3032
- message,
3033
- default: true
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
+ }
3034
3036
  }
3035
- ]);
3036
- return ok;
3037
+ visible++;
3038
+ i++;
3039
+ }
3040
+ return text.slice(0, i) + "\x1B[0m\u2026";
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);
3037
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");
3038
3294
  async function waitForKey(validKeys) {
3039
3295
  return new Promise((resolve) => {
3040
3296
  const { stdin } = process;
@@ -3060,99 +3316,66 @@ async function waitForKey(validKeys) {
3060
3316
  stdin.on("data", onData);
3061
3317
  });
3062
3318
  }
3063
- async function executeAction(actionId, state) {
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) {
3064
3323
  switch (actionId) {
3065
3324
  case "quit":
3066
- return false;
3325
+ return QUIT;
3067
3326
  case "list": {
3068
- clearScreen();
3069
- try {
3070
- await listCommand({});
3071
- } catch (error) {
3072
- logger.error(`List failed: ${error}`);
3073
- }
3074
- await promptContinue();
3075
- return true;
3327
+ const switched = await handleList(dashboardLines);
3328
+ return switched ? CONTINUE : SKIP_REFRESH;
3076
3329
  }
3077
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;
3078
3337
  clearScreen();
3079
- const { description } = await inquirer7.prompt([
3080
- {
3081
- type: "input",
3082
- name: "description",
3083
- message: "Describe the ticket (empty to cancel):"
3084
- }
3085
- ]);
3086
- if (!description.trim()) {
3087
- logger.info("Cancelled");
3088
- return true;
3089
- }
3090
3338
  try {
3091
3339
  await createCommand(description, {});
3092
3340
  } catch (error) {
3093
3341
  logger.error(`Create failed: ${error}`);
3094
3342
  }
3095
- await promptContinue();
3096
- return true;
3343
+ return CONTINUE;
3344
+ }
3345
+ case "commit": {
3346
+ const committed = await handleCommit(state, dashboardLines);
3347
+ return committed ? CONTINUE : SKIP_REFRESH;
3097
3348
  }
3098
- case "commit":
3099
- clearScreen();
3100
- await handleCommit(state);
3101
- await promptContinue();
3102
- return true;
3103
3349
  case "push":
3104
- clearScreen();
3105
- await handlePush();
3106
- await promptContinue();
3107
- return true;
3350
+ await handlePush(dashboardLines);
3351
+ return CONTINUE;
3108
3352
  case "pr": {
3109
3353
  clearScreen();
3110
- if (!await confirm("Create a pull request?")) return true;
3111
3354
  await prCommand({});
3112
- await promptContinue();
3113
- return true;
3355
+ return CONTINUE;
3114
3356
  }
3115
3357
  case "run": {
3116
- clearScreen();
3117
- const hasCommits = state.commits.length > 0;
3118
- const hasFeedback = state.hasActionableFeedback;
3119
- let msg;
3120
- if (hasFeedback && hasCommits) {
3121
- msg = "Start AI agent to address review feedback?";
3122
- } else if (hasCommits) {
3123
- msg = "Start AI agent to continue implementation from existing commits?";
3124
- } else {
3125
- msg = "Start AI agent to implement this ticket from scratch?";
3126
- }
3127
- if (!await confirm(msg)) return true;
3128
3358
  await handleRun(state);
3129
- await promptContinue();
3130
- return true;
3359
+ return CONTINUE;
3131
3360
  }
3132
3361
  case "switch-provider":
3133
- clearScreen();
3134
- await handleSwitchProvider(state);
3135
- return true;
3136
- case "checkout-main": {
3137
- clearScreen();
3138
- if (!await confirm("Switch to main branch?")) return true;
3139
- await handleCheckoutMain();
3140
- return true;
3141
- }
3362
+ await handleSwitchProvider(state, dashboardLines);
3363
+ return SKIP_REFRESH;
3364
+ case "checkout-main":
3365
+ await handleCheckoutMain(dashboardLines);
3366
+ return CONTINUE;
3142
3367
  default:
3143
- return true;
3368
+ return SKIP_REFRESH;
3144
3369
  }
3145
3370
  }
3146
- async function handleCommit(state) {
3371
+ async function handleCommit(state, dashboardLines) {
3147
3372
  try {
3148
3373
  const { stdout: status } = await execa4("git", ["status", "--short"]);
3149
3374
  if (!status.trim()) {
3150
- logger.info("No changes to commit");
3151
- return;
3375
+ showStatus("Commit", "No changes to commit", dashboardLines);
3376
+ await new Promise((r) => setTimeout(r, 1500));
3377
+ return false;
3152
3378
  }
3153
- logger.info("Changes:");
3154
- console.log(status);
3155
- console.log();
3156
3379
  await execa4("git", ["add", "-A"]);
3157
3380
  const { stdout: diffStat } = await execa4("git", [
3158
3381
  "diff",
@@ -3165,69 +3388,69 @@ async function handleCommit(state) {
3165
3388
  const issueTitle = state.issue?.title ?? null;
3166
3389
  const provider = state.config.ai.provider;
3167
3390
  const providerName = getProviderDisplayName(provider);
3168
- const { mode } = await inquirer7.prompt([
3169
- {
3170
- type: "list",
3171
- name: "mode",
3172
- message: "How would you like to provide the commit message?",
3173
- choices: [
3174
- { name: `Generate with ${providerName}`, value: "ai" },
3175
- { name: "Enter manually", value: "manual" }
3176
- ]
3177
- }
3178
- ]);
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
+ }
3179
3403
  let message;
3180
3404
  if (mode === "manual") {
3181
- const { manualInput } = await inquirer7.prompt([
3182
- {
3183
- type: "input",
3184
- name: "manualInput",
3185
- message: "Commit message (empty to cancel):"
3186
- }
3187
- ]);
3188
- 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;
3189
3411
  } else {
3190
- logger.info(`Generating commit message with ${providerName}...`);
3412
+ showStatus("Generating", `Generating commit message with ${providerName}...`, dashboardLines);
3191
3413
  message = await generateCommitMessage(
3192
3414
  diffContent,
3193
3415
  issueNumber,
3194
3416
  issueTitle,
3195
- state
3417
+ state,
3418
+ dashboardLines
3196
3419
  );
3197
3420
  }
3198
3421
  if (message === CANCEL) {
3199
3422
  await execa4("git", ["reset", "HEAD"]);
3200
- logger.info("Cancelled");
3201
- return;
3423
+ return false;
3202
3424
  }
3203
- console.log();
3204
- logger.info(`Message: ${message}`);
3205
- console.log();
3206
- if (!await confirm("Commit with this message?")) {
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) {
3207
3431
  await execa4("git", ["reset", "HEAD"]);
3208
- logger.info("Commit cancelled");
3209
- return;
3432
+ return false;
3210
3433
  }
3211
3434
  const providerEmail = getProviderEmail(provider);
3212
3435
  const fullMessage = `${message}
3213
3436
 
3214
3437
  Co-Authored-By: ${providerName} <${providerEmail}>`;
3215
- const spinner = createSpinner("Committing...");
3216
- spinner.start();
3438
+ showStatus("Committing", "Committing changes...", dashboardLines);
3217
3439
  await execa4("git", ["commit", "-m", fullMessage]);
3218
- spinner.succeed("Changes committed");
3440
+ return true;
3219
3441
  } catch (error) {
3220
3442
  logger.error(`Commit failed: ${error}`);
3443
+ return false;
3221
3444
  }
3222
3445
  }
3223
- async function generateCommitMessage(diffContent, issueNumber, issueTitle, state) {
3446
+ async function generateCommitMessage(diffContent, issueNumber, issueTitle, state, dashboardLines) {
3224
3447
  try {
3225
3448
  const prompt = buildCommitMessagePrompt(
3226
3449
  diffContent,
3227
3450
  issueNumber,
3228
3451
  issueTitle
3229
3452
  );
3230
- const result = await invokeAI({ prompt, streamOutput: true }, state.config);
3453
+ const result = await invokeAI({ prompt, streamOutput: false }, state.config);
3231
3454
  let message = result.output.trim().split("\n")[0].trim();
3232
3455
  for (const q of ['"', "'", "`"]) {
3233
3456
  if (message.startsWith(q) && message.endsWith(q)) {
@@ -3238,16 +3461,12 @@ async function generateCommitMessage(diffContent, issueNumber, issueTitle, state
3238
3461
  message = message.replace(/^```\w*\s*/, "").replace(/\s*```$/, "");
3239
3462
  return message;
3240
3463
  } catch {
3241
- logger.warning("AI commit message generation failed");
3242
- console.log();
3243
- const { message } = await inquirer7.prompt([
3244
- {
3245
- type: "input",
3246
- name: "message",
3247
- message: "Commit message (empty to cancel):"
3248
- }
3249
- ]);
3250
- 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;
3251
3470
  }
3252
3471
  }
3253
3472
  async function handleRun(state) {
@@ -3295,61 +3514,190 @@ ${feedbackLines}`);
3295
3514
  logger.error(`${providerName} session failed: ${error}`);
3296
3515
  }
3297
3516
  }
3298
- async function handlePush() {
3517
+ async function handlePush(dashboardLines) {
3299
3518
  try {
3300
3519
  const { stdout: branch } = await execa4("git", ["branch", "--show-current"]);
3301
- if (!await confirm(`Push ${branch.trim()} to remote?`)) return;
3302
- const spinner = createSpinner("Pushing...");
3303
- spinner.start();
3304
- await execa4("git", ["push", "-u", "origin", branch.trim()]);
3305
- 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]);
3306
3523
  } catch (error) {
3307
3524
  logger.error(`Push failed: ${error}`);
3308
3525
  }
3309
3526
  }
3310
3527
  var PROVIDERS = ["claude", "gemini", "codex"];
3311
- async function handleSwitchProvider(state) {
3528
+ async function handleSwitchProvider(state, dashboardLines) {
3312
3529
  const current = state.config.ai.provider;
3313
- const { provider } = await inquirer7.prompt([
3314
- {
3315
- type: "list",
3316
- name: "provider",
3317
- message: "Select AI provider:",
3318
- choices: PROVIDERS.map((p) => ({
3319
- name: p.charAt(0).toUpperCase() + p.slice(1) + (p === current ? " (current)" : ""),
3320
- value: p
3321
- })),
3322
- default: current
3323
- }
3324
- ]);
3325
- 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;
3326
3540
  setRuntimeProvider(provider);
3327
- logger.success(`Provider switched to ${provider} (session only)`);
3541
+ state.config.ai.provider = provider;
3328
3542
  }
3329
- async function handleCheckoutMain() {
3543
+ async function handleCheckoutMain(dashboardLines) {
3330
3544
  try {
3331
- const spinner = createSpinner("Switching to main...");
3332
- spinner.start();
3545
+ showStatus("Switching", "Switching to main...", dashboardLines);
3333
3546
  await execa4("git", ["checkout", "main"]);
3334
3547
  await execa4("git", ["pull"]);
3335
- spinner.succeed("Switched to main");
3336
3548
  } catch (error) {
3337
3549
  logger.error(`Checkout failed: ${error}`);
3338
3550
  }
3339
3551
  }
3340
- async function promptContinue() {
3341
- console.log();
3342
- await inquirer7.prompt([
3343
- {
3344
- type: "input",
3345
- name: "continue",
3346
- message: "Press Enter to continue..."
3552
+ async function handleList(dashboardLines) {
3553
+ try {
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"
3581
+ );
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
+ }
3347
3592
  }
3348
- ]);
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
+ }
3673
+ } catch (error) {
3674
+ logger.error(`List failed: ${error}`);
3675
+ return false;
3676
+ }
3677
+ }
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;
3349
3696
  }
3350
3697
  async function tuiCommand() {
3351
3698
  let running = true;
3352
3699
  let lastActions = [];
3700
+ let lastDashboardLines = [];
3353
3701
  const config = loadConfig();
3354
3702
  let lastState = {
3355
3703
  isGitRepo: true,
@@ -3373,28 +3721,36 @@ async function tuiCommand() {
3373
3721
  hasUIChanges: false,
3374
3722
  isPlaywrightAvailable: false
3375
3723
  };
3724
+ let needsRefresh = true;
3376
3725
  while (running) {
3377
- clearScreen();
3378
- renderDashboard(lastState, lastActions, void 0, true);
3379
- const state = await aggregateState();
3380
- const actions = getAvailableActions(state);
3381
- lastState = state;
3382
- lastActions = actions;
3383
- clearScreen();
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
+ }
3384
3734
  let hint;
3385
- if (state.isOnMain) {
3735
+ if (lastState.isOnMain) {
3386
3736
  hint = "Select an action to get started";
3387
- } else if (state.hasUncommittedChanges && !state.pr) {
3737
+ } else if (lastState.hasUncommittedChanges && !lastState.pr) {
3388
3738
  hint = "Commit your changes before creating a PR";
3389
- } else if (state.hasActionableFeedback) {
3739
+ } else if (lastState.hasActionableFeedback) {
3390
3740
  hint = "Review feedback needs attention";
3391
3741
  }
3392
- renderDashboard(state, actions, hint);
3393
- const validKeys = actions.map((a) => a.shortcut);
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);
3394
3748
  const key = await waitForKey(validKeys);
3395
- const action = actions.find((a) => a.shortcut === key);
3749
+ const action = lastActions.find((a) => a.shortcut === key);
3396
3750
  if (action) {
3397
- running = await executeAction(action.id, state);
3751
+ const result = await executeAction(action.id, lastState, lastDashboardLines);
3752
+ running = result.running;
3753
+ needsRefresh = result.refresh;
3398
3754
  }
3399
3755
  }
3400
3756
  }