@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/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
  }
@@ -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
- process.exit(1);
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
- process.exit(1);
1621
+ return;
1666
1622
  }
1667
1623
  if (await isOnMainBranch()) {
1668
1624
  logger.error("Cannot create PR from main/master branch.");
1669
- process.exit(1);
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
- logger.warning("Branch has unpushed commits.");
1685
- const { push } = await inquirer5.prompt([
1686
- {
1687
- type: "confirm",
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 inquirer6 from "inquirer";
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 inquirer6.prompt([
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.13.4",
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 renderSettings(state, w) {
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
- const videoTag = state.config.video.enabled ? chalk3.green("on") : chalk3.dim("off");
2904
- console.log(row(chalk3.dim("Provider: ") + provTag, w));
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 renderDashboard(state, actions, hint, refreshing) {
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
- console.log(topRow(titleLabel, w));
2914
- renderSettings(state, w);
2869
+ out(topRow(titleLabel, w));
2870
+ renderSettingsTo(state, w, out);
2915
2871
  if (!state.isGitRepo) {
2916
- console.log(row(chalk3.red("Not a git repository"), w));
2917
- console.log(
2918
- row(chalk3.dim("Run gent init in a git repo to get started"), w)
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
- console.log(row(chalk3.red("GitHub CLI not authenticated"), w));
2925
- console.log(row(chalk3.dim("Run: gh auth login"), w));
2926
- console.log(botRow(w));
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
- console.log(
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
- console.log(row(chalk3.yellow("\u25CF uncommitted changes"), w));
2891
+ out(row(chalk3.yellow("\u25CF uncommitted changes"), w));
2938
2892
  }
2939
2893
  if (hint) {
2940
- console.log(midRow("Hint", w));
2941
- console.log(row(chalk3.yellow(hint), w));
2894
+ out(midRow("Hint", w));
2895
+ out(row(chalk3.yellow(hint), w));
2942
2896
  }
2943
- console.log(divRow(w));
2897
+ out(divRow(w));
2944
2898
  if (refreshing) {
2945
- console.log(row(chalk3.yellow("Refreshing\u2026"), w));
2899
+ out(row(chalk3.yellow("Refreshing\u2026"), w));
2946
2900
  } else {
2947
2901
  for (const line of formatCommandBar(actions, w)) {
2948
- console.log(row(line, w));
2902
+ out(row(line, w));
2949
2903
  }
2950
2904
  }
2951
- console.log(botRow(w));
2952
- console.log();
2953
- return;
2905
+ out(botRow(w));
2906
+ out("");
2907
+ return lines;
2954
2908
  }
2955
2909
  const section = (title) => {
2956
- console.log(midRow(title, w));
2910
+ out(midRow(title, w));
2957
2911
  };
2958
2912
  section("Ticket");
2959
2913
  if (state.issue) {
2960
- console.log(
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) console.log(row(chalk3.dim(desc), w));
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) console.log(row(tags.join(" "), w));
2929
+ if (tags.length) out(row(tags.join(" "), w));
2976
2930
  } else {
2977
- console.log(row(chalk3.dim("No linked issue"), w));
2931
+ out(row(chalk3.dim("No linked issue"), w));
2978
2932
  }
2979
2933
  section("Branch");
2980
- console.log(row(chalk3.magenta(state.branch), w));
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) console.log(row(bits.join(chalk3.dim(" \xB7 ")), w));
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
- console.log(row(chalk3.cyan(`#${state.pr.number}`) + titleText, w));
2994
- console.log(
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
- console.log(
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
- console.log(
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
- console.log(row(chalk3.dim(state.pr.url), w));
2971
+ out(row(chalk3.dim(state.pr.url), w));
3018
2972
  } else {
3019
- console.log(row(chalk3.dim("No PR created"), w));
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
- console.log(row(c.substring(0, w - 5), w));
2979
+ out(row(c.substring(0, w - 5), w));
3026
2980
  }
3027
2981
  if (state.commits.length > max) {
3028
- console.log(
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
- console.log(row(chalk3.dim("No commits"), w));
2985
+ out(row(chalk3.dim("No commits"), w));
3034
2986
  }
3035
2987
  if (hint) {
3036
2988
  section("Hint");
3037
- console.log(row(chalk3.yellow(hint), w));
2989
+ out(row(chalk3.yellow(hint), w));
3038
2990
  }
3039
- console.log(divRow(w));
2991
+ out(divRow(w));
3040
2992
  if (refreshing) {
3041
- console.log(row(chalk3.yellow("Refreshing\u2026"), w));
2993
+ out(row(chalk3.yellow("Refreshing\u2026"), w));
3042
2994
  } else {
3043
2995
  for (const line of formatCommandBar(actions, w)) {
3044
- console.log(row(line, w));
2996
+ out(row(line, w));
3045
2997
  }
3046
2998
  }
3047
- 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
+ }
3048
3007
  }
3049
3008
  function clearScreen() {
3050
3009
  process.stdout.write("\x1B[2J\x1B[0f");
3051
3010
  }
3052
3011
 
3053
- // src/commands/tui.ts
3054
- var CANCEL = /* @__PURE__ */ Symbol("cancel");
3055
- async function confirm(message) {
3056
- const { ok } = await inquirer7.prompt([
3057
- {
3058
- type: "confirm",
3059
- name: "ok",
3060
- message,
3061
- 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
+ }
3062
3036
  }
3063
- ]);
3064
- return ok;
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
- 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) {
3092
3323
  switch (actionId) {
3093
3324
  case "quit":
3094
- return false;
3325
+ return QUIT;
3095
3326
  case "list": {
3096
- clearScreen();
3097
- try {
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
- await promptContinue();
3124
- return true;
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
- clearScreen();
3133
- await handlePush();
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
- await promptContinue();
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
- await promptContinue();
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
- clearScreen();
3169
- await handleSwitchProvider(state);
3170
- return true;
3171
- case "checkout-main": {
3172
- clearScreen();
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 true;
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
- logger.info("No changes to commit");
3186
- return;
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 { mode } = await inquirer7.prompt([
3204
- {
3205
- type: "list",
3206
- name: "mode",
3207
- message: "How would you like to provide the commit message?",
3208
- choices: [
3209
- { name: `Generate with ${providerName}`, value: "ai" },
3210
- { name: "Enter manually", value: "manual" }
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 { manualInput } = await inquirer7.prompt([
3217
- {
3218
- type: "input",
3219
- name: "manualInput",
3220
- message: "Commit message (empty to cancel):"
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
- logger.info(`Generating commit message with ${providerName}...`);
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
- logger.info("Cancelled");
3236
- return;
3423
+ return false;
3237
3424
  }
3238
- console.log();
3239
- logger.info(`Message: ${message}`);
3240
- console.log();
3241
- 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) {
3242
3431
  await execa4("git", ["reset", "HEAD"]);
3243
- logger.info("Commit cancelled");
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
- const spinner = createSpinner("Committing...");
3251
- spinner.start();
3438
+ showStatus("Committing", "Committing changes...", dashboardLines);
3252
3439
  await execa4("git", ["commit", "-m", fullMessage]);
3253
- spinner.succeed("Changes committed");
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: true }, state.config);
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
- logger.warning("AI commit message generation failed");
3277
- console.log();
3278
- const { message } = await inquirer7.prompt([
3279
- {
3280
- type: "input",
3281
- name: "message",
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
- if (!await confirm(`Push ${branch.trim()} to remote?`)) return;
3336
- const spinner = createSpinner("Pushing...");
3337
- spinner.start();
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 { provider } = await inquirer7.prompt([
3348
- {
3349
- type: "list",
3350
- name: "provider",
3351
- message: "Select AI provider:",
3352
- choices: PROVIDERS.map((p) => ({
3353
- name: p.charAt(0).toUpperCase() + p.slice(1) + (p === current ? " (current)" : ""),
3354
- value: p
3355
- })),
3356
- default: current
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
- logger.success(`Provider switched to ${provider} (session only)`);
3541
+ state.config.ai.provider = provider;
3362
3542
  }
3363
- async function handleCheckoutMain() {
3543
+ async function handleCheckoutMain(dashboardLines) {
3364
3544
  try {
3365
- const spinner = createSpinner("Switching to main...");
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 handleVideoCapture(state) {
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
- const agentInstructions = loadAgentInstructions();
3388
- const videoPrompt = buildVideoPrompt(
3389
- state.issue.number,
3390
- state.issue.title,
3391
- state.config.video,
3392
- agentInstructions
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
- await invokeAIInteractive(videoPrompt, state.config);
3395
- logger.success("Video capture completed");
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(`Video capture failed: ${error}`);
3674
+ logger.error(`List failed: ${error}`);
3675
+ return false;
3398
3676
  }
3399
3677
  }
3400
- async function promptContinue() {
3401
- console.log();
3402
- await inquirer7.prompt([
3403
- {
3404
- type: "input",
3405
- name: "continue",
3406
- message: "Press Enter to continue..."
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
- clearScreen();
3438
- renderDashboard(lastState, lastActions, void 0, true);
3439
- const state = await aggregateState();
3440
- const actions = getAvailableActions(state);
3441
- lastState = state;
3442
- lastActions = actions;
3443
- 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
+ }
3444
3734
  let hint;
3445
- if (state.isOnMain) {
3735
+ if (lastState.isOnMain) {
3446
3736
  hint = "Select an action to get started";
3447
- } else if (state.hasUncommittedChanges && !state.pr) {
3737
+ } else if (lastState.hasUncommittedChanges && !lastState.pr) {
3448
3738
  hint = "Commit your changes before creating a PR";
3449
- } else if (state.hasActionableFeedback) {
3739
+ } else if (lastState.hasActionableFeedback) {
3450
3740
  hint = "Review feedback needs attention";
3451
3741
  }
3452
- renderDashboard(state, actions, hint);
3453
- 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);
3454
3748
  const key = await waitForKey(validKeys);
3455
- const action = actions.find((a) => a.shortcut === key);
3749
+ const action = lastActions.find((a) => a.shortcut === key);
3456
3750
  if (action) {
3457
- running = await executeAction(action.id, state);
3751
+ const result = await executeAction(action.id, lastState, lastDashboardLines);
3752
+ running = result.running;
3753
+ needsRefresh = result.refresh;
3458
3754
  }
3459
3755
  }
3460
3756
  }