@rotorsoft/gent 1.12.1 → 1.13.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
@@ -14,7 +14,6 @@ import {
14
14
  createIssue,
15
15
  createPullRequest,
16
16
  createSpinner,
17
- extractPriorityFromLabels,
18
17
  extractTypeFromLabels,
19
18
  generateDefaultConfig,
20
19
  getConfigPath,
@@ -26,6 +25,7 @@ import {
26
25
  getWorkflowLabels,
27
26
  isValidIssueNumber,
28
27
  listIssues,
28
+ listOpenPrs,
29
29
  loadAgentInstructions,
30
30
  loadConfig,
31
31
  logger,
@@ -35,7 +35,7 @@ import {
35
35
  sortByPriority,
36
36
  updateIssueLabels,
37
37
  withSpinner
38
- } from "./chunk-UWYWIOYZ.js";
38
+ } from "./chunk-KLHUMY5L.js";
39
39
 
40
40
  // src/index.ts
41
41
  import { Command } from "commander";
@@ -196,7 +196,7 @@ async function initCommand(options) {
196
196
  }
197
197
  ]);
198
198
  if (setupLabels) {
199
- const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-5EHRPUPF.js");
199
+ const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-3ANC76NF.js");
200
200
  await setupLabelsCommand2();
201
201
  }
202
202
  }
@@ -808,105 +808,6 @@ Next steps:
808
808
 
809
809
  // src/commands/list.ts
810
810
  import chalk2 from "chalk";
811
- async function listCommand(options) {
812
- const isAuthed = await checkGhAuth();
813
- if (!isAuthed) {
814
- logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
815
- process.exit(1);
816
- }
817
- const config = loadConfig();
818
- const workflowLabels = getWorkflowLabels(config);
819
- const labels = [];
820
- if (options.label) {
821
- labels.push(options.label);
822
- }
823
- if (options.status && options.status !== "all") {
824
- switch (options.status) {
825
- case "ready":
826
- labels.push(workflowLabels.ready);
827
- break;
828
- case "in-progress":
829
- labels.push(workflowLabels.inProgress);
830
- break;
831
- case "completed":
832
- labels.push(workflowLabels.completed);
833
- break;
834
- case "blocked":
835
- labels.push(workflowLabels.blocked);
836
- break;
837
- }
838
- } else if (!options.status) {
839
- labels.push(workflowLabels.ready);
840
- }
841
- let issues;
842
- try {
843
- issues = await listIssues({
844
- labels: labels.length > 0 ? labels : void 0,
845
- state: "open",
846
- limit: options.limit || 20
847
- });
848
- } catch (error) {
849
- logger.error(`Failed to fetch issues: ${error}`);
850
- return;
851
- }
852
- if (issues.length === 0) {
853
- logger.info("No issues found matching the criteria.");
854
- return;
855
- }
856
- sortByPriority(issues);
857
- logger.bold(`Found ${issues.length} issue(s):`);
858
- logger.newline();
859
- for (const issue of issues) {
860
- const type = extractTypeFromLabels(issue.labels);
861
- const priority = extractPriorityFromLabels(issue.labels);
862
- const status = getIssueStatus(issue.labels, workflowLabels);
863
- const priorityColor = getPriorityColor(priority);
864
- const statusColor = getStatusColor(status);
865
- console.log(
866
- ` ${colors.issue(`#${issue.number.toString().padStart(4)}`)} ${priorityColor(`[${priority}]`.padEnd(10))} ${statusColor(`[${status}]`.padEnd(14))} ${colors.label(`[${type}]`.padEnd(10))} ` + issue.title.slice(0, 50) + (issue.title.length > 50 ? "..." : "")
867
- );
868
- }
869
- logger.newline();
870
- logger.dim(`Run ${colors.command("gent run <issue-number>")} to implement an issue`);
871
- logger.dim(`Run ${colors.command("gent run --auto")} to auto-select highest priority`);
872
- }
873
- function getIssueStatus(labels, workflowLabels) {
874
- if (labels.includes(workflowLabels.ready)) return "ready";
875
- if (labels.includes(workflowLabels.inProgress)) return "in-progress";
876
- if (labels.includes(workflowLabels.completed)) return "completed";
877
- if (labels.includes(workflowLabels.blocked)) return "blocked";
878
- return "unknown";
879
- }
880
- function getPriorityColor(priority) {
881
- switch (priority) {
882
- case "critical":
883
- return chalk2.red;
884
- case "high":
885
- return chalk2.yellow;
886
- case "medium":
887
- return chalk2.blue;
888
- case "low":
889
- return chalk2.green;
890
- default:
891
- return chalk2.gray;
892
- }
893
- }
894
- function getStatusColor(status) {
895
- switch (status) {
896
- case "ready":
897
- return chalk2.green;
898
- case "in-progress":
899
- return chalk2.yellow;
900
- case "completed":
901
- return chalk2.blue;
902
- case "blocked":
903
- return chalk2.red;
904
- default:
905
- return chalk2.gray;
906
- }
907
- }
908
-
909
- // src/commands/run.ts
910
811
  import inquirer3 from "inquirer";
911
812
 
912
813
  // src/lib/git.ts
@@ -1028,6 +929,22 @@ async function getLastCommitTimestamp() {
1028
929
  const { stdout } = await execa2("git", ["log", "-1", "--format=%cI"]);
1029
930
  return stdout.trim();
1030
931
  }
932
+ async function listLocalBranches() {
933
+ const { stdout } = await execa2("git", ["branch", "--format=%(refname:short)"]);
934
+ return stdout.trim().split("\n").filter(Boolean);
935
+ }
936
+ async function remoteBranchExists(name) {
937
+ try {
938
+ await execa2("git", ["ls-remote", "--exit-code", "--heads", "origin", name]);
939
+ return true;
940
+ } catch {
941
+ return false;
942
+ }
943
+ }
944
+ async function fetchAndCheckout(name) {
945
+ await execa2("git", ["fetch", "origin", `${name}:${name}`]);
946
+ await execa2("git", ["checkout", name]);
947
+ }
1031
948
 
1032
949
  // src/lib/branch.ts
1033
950
  async function generateBranchName(config, issueNumber, issueTitle, type) {
@@ -1100,7 +1017,308 @@ function extractIssueNumber(branchName) {
1100
1017
  return info?.issueNumber ?? null;
1101
1018
  }
1102
1019
 
1020
+ // src/commands/list.ts
1021
+ function findBranchForIssue(issueNumber, branches) {
1022
+ for (const branch of branches) {
1023
+ const info = parseBranchName(branch);
1024
+ if (info && info.issueNumber === issueNumber) {
1025
+ return branch;
1026
+ }
1027
+ }
1028
+ return null;
1029
+ }
1030
+ function buildTicketChoices(inProgressIssues, readyIssues, openPrs, localBranches) {
1031
+ const choices = [];
1032
+ const seen = /* @__PURE__ */ new Set();
1033
+ const prByIssue = /* @__PURE__ */ new Map();
1034
+ for (const pr of openPrs) {
1035
+ const info = parseBranchName(pr.headRefName);
1036
+ if (info) {
1037
+ prByIssue.set(info.issueNumber, pr);
1038
+ }
1039
+ }
1040
+ for (const issue of inProgressIssues) {
1041
+ if (seen.has(issue.number)) continue;
1042
+ seen.add(issue.number);
1043
+ const branch = findBranchForIssue(issue.number, localBranches);
1044
+ const pr = prByIssue.get(issue.number);
1045
+ choices.push({
1046
+ issueNumber: issue.number,
1047
+ title: issue.title,
1048
+ branch: branch || pr?.headRefName || null,
1049
+ category: pr ? "open-pr" : "in-progress"
1050
+ });
1051
+ }
1052
+ for (const [issueNumber, pr] of prByIssue) {
1053
+ if (seen.has(issueNumber)) continue;
1054
+ seen.add(issueNumber);
1055
+ const issue = [...inProgressIssues, ...readyIssues].find(
1056
+ (i) => i.number === issueNumber
1057
+ );
1058
+ choices.push({
1059
+ issueNumber,
1060
+ title: issue?.title || pr.title,
1061
+ branch: pr.headRefName,
1062
+ category: "open-pr"
1063
+ });
1064
+ }
1065
+ for (const issue of readyIssues) {
1066
+ if (seen.has(issue.number)) continue;
1067
+ seen.add(issue.number);
1068
+ const branch = findBranchForIssue(issue.number, localBranches);
1069
+ choices.push({
1070
+ issueNumber: issue.number,
1071
+ title: issue.title,
1072
+ branch,
1073
+ category: "ready"
1074
+ });
1075
+ }
1076
+ return choices;
1077
+ }
1078
+ function categoryLabel(category) {
1079
+ switch (category) {
1080
+ case "in-progress":
1081
+ return chalk2.yellow("[in progress]");
1082
+ case "open-pr":
1083
+ return chalk2.blue("[open PR]");
1084
+ case "ready":
1085
+ return chalk2.green("[ready]");
1086
+ }
1087
+ }
1088
+ function formatChoice(choice) {
1089
+ const num = colors.issue(`#${choice.issueNumber}`);
1090
+ const cat = categoryLabel(choice.category);
1091
+ const title = choice.title.length > 50 ? choice.title.slice(0, 50) + "..." : choice.title;
1092
+ return `${num} ${cat} ${title}`;
1093
+ }
1094
+ async function listCommand(options) {
1095
+ const isAuthed = await checkGhAuth();
1096
+ if (!isAuthed) {
1097
+ logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1098
+ return;
1099
+ }
1100
+ const config = loadConfig();
1101
+ const workflowLabels = getWorkflowLabels(config);
1102
+ const currentBranch = await getCurrentBranch();
1103
+ const defaultBranch = await getDefaultBranch();
1104
+ const statusFilter = options.status;
1105
+ const limit = options.limit || 20;
1106
+ let inProgressIssues = [];
1107
+ let readyIssues = [];
1108
+ if (statusFilter && statusFilter !== "all") {
1109
+ const labels = [];
1110
+ if (options.label) labels.push(options.label);
1111
+ switch (statusFilter) {
1112
+ case "ready":
1113
+ labels.push(workflowLabels.ready);
1114
+ break;
1115
+ case "in-progress":
1116
+ labels.push(workflowLabels.inProgress);
1117
+ break;
1118
+ case "completed":
1119
+ labels.push(workflowLabels.completed);
1120
+ break;
1121
+ case "blocked":
1122
+ labels.push(workflowLabels.blocked);
1123
+ break;
1124
+ }
1125
+ const [issues, localBranches2] = await withSpinner(
1126
+ "Fetching tickets...",
1127
+ () => Promise.all([
1128
+ listIssues({ labels, state: "open", limit }),
1129
+ listLocalBranches()
1130
+ ])
1131
+ );
1132
+ sortByPriority(issues);
1133
+ if (statusFilter === "in-progress") {
1134
+ inProgressIssues = issues;
1135
+ } else {
1136
+ readyIssues = issues;
1137
+ }
1138
+ const choices2 = buildTicketChoices(
1139
+ inProgressIssues,
1140
+ readyIssues,
1141
+ [],
1142
+ localBranches2
1143
+ );
1144
+ if (choices2.length === 0) {
1145
+ logger.info("No issues found matching the criteria.");
1146
+ return;
1147
+ }
1148
+ await presentSelector(
1149
+ choices2,
1150
+ currentBranch,
1151
+ defaultBranch,
1152
+ config
1153
+ );
1154
+ return;
1155
+ }
1156
+ const labelFilter = options.label ? [options.label] : [];
1157
+ const [inProgress, ready, prs, localBranches] = await withSpinner(
1158
+ "Fetching tickets...",
1159
+ () => Promise.all([
1160
+ listIssues({
1161
+ labels: [workflowLabels.inProgress, ...labelFilter],
1162
+ state: "open",
1163
+ limit
1164
+ }),
1165
+ listIssues({
1166
+ labels: [workflowLabels.ready, ...labelFilter],
1167
+ state: "open",
1168
+ limit
1169
+ }),
1170
+ listOpenPrs(30),
1171
+ listLocalBranches()
1172
+ ])
1173
+ );
1174
+ sortByPriority(inProgress);
1175
+ sortByPriority(ready);
1176
+ const choices = buildTicketChoices(inProgress, ready, prs, localBranches);
1177
+ if (choices.length === 0) {
1178
+ logger.info("No tickets found.");
1179
+ logger.dim(
1180
+ `Create a ticket with ${colors.command("gent create")} or add the '${workflowLabels.ready}' label to an issue.`
1181
+ );
1182
+ return;
1183
+ }
1184
+ await presentSelector(choices, currentBranch, defaultBranch, config);
1185
+ }
1186
+ async function presentSelector(choices, currentBranch, defaultBranch, config) {
1187
+ const dirty = await hasUncommittedChanges();
1188
+ const inquirerChoices = [];
1189
+ inquirerChoices.push({
1190
+ name: `${chalk2.magenta(defaultBranch)}${currentBranch === defaultBranch ? chalk2.dim(" (current)") : ""}`,
1191
+ value: "__main__"
1192
+ });
1193
+ inquirerChoices.push(new inquirer3.Separator("\u2500"));
1194
+ const inProgress = choices.filter((c) => c.category === "in-progress");
1195
+ const openPrChoices = choices.filter((c) => c.category === "open-pr");
1196
+ const ready = choices.filter((c) => c.category === "ready");
1197
+ if (inProgress.length > 0) {
1198
+ inquirerChoices.push(new inquirer3.Separator(chalk2.yellow(" In Progress")));
1199
+ for (const c of inProgress) {
1200
+ inquirerChoices.push({
1201
+ name: formatChoice(c),
1202
+ value: String(c.issueNumber)
1203
+ });
1204
+ }
1205
+ }
1206
+ if (openPrChoices.length > 0) {
1207
+ inquirerChoices.push(new inquirer3.Separator(chalk2.blue(" Open PRs")));
1208
+ for (const c of openPrChoices) {
1209
+ inquirerChoices.push({
1210
+ name: formatChoice(c),
1211
+ value: String(c.issueNumber)
1212
+ });
1213
+ }
1214
+ }
1215
+ if (ready.length > 0) {
1216
+ inquirerChoices.push(new inquirer3.Separator(chalk2.green(" Ready")));
1217
+ for (const c of ready) {
1218
+ inquirerChoices.push({
1219
+ name: formatChoice(c),
1220
+ value: String(c.issueNumber)
1221
+ });
1222
+ }
1223
+ }
1224
+ const { selected } = await inquirer3.prompt([
1225
+ {
1226
+ type: "list",
1227
+ name: "selected",
1228
+ message: "Select a ticket to switch to:",
1229
+ choices: inquirerChoices,
1230
+ pageSize: 20
1231
+ }
1232
+ ]);
1233
+ if (selected === "__main__") {
1234
+ if (currentBranch === defaultBranch) {
1235
+ logger.info(`Already on ${colors.branch(defaultBranch)}`);
1236
+ return;
1237
+ }
1238
+ if (dirty) {
1239
+ const ok = await confirmDirty();
1240
+ if (!ok) return;
1241
+ }
1242
+ await withSpinner(`Switching to ${defaultBranch}...`, async () => {
1243
+ await checkoutBranch(defaultBranch);
1244
+ });
1245
+ logger.success(`Switched to ${colors.branch(defaultBranch)}`);
1246
+ return;
1247
+ }
1248
+ const issueNumber = parseInt(selected, 10);
1249
+ const ticket = choices.find((c) => c.issueNumber === issueNumber);
1250
+ if (!ticket) return;
1251
+ if (dirty) {
1252
+ const ok = await confirmDirty();
1253
+ if (!ok) return;
1254
+ }
1255
+ const targetBranch = ticket.branch;
1256
+ if (targetBranch) {
1257
+ if (await branchExists(targetBranch)) {
1258
+ await withSpinner(`Switching to ${targetBranch}...`, async () => {
1259
+ await checkoutBranch(targetBranch);
1260
+ });
1261
+ logger.success(`Switched to ${colors.branch(targetBranch)}`);
1262
+ } else if (await remoteBranchExists(targetBranch)) {
1263
+ await withSpinner(
1264
+ `Fetching ${targetBranch} from remote...`,
1265
+ async () => {
1266
+ await fetchAndCheckout(targetBranch);
1267
+ }
1268
+ );
1269
+ logger.success(
1270
+ `Fetched and switched to ${colors.branch(targetBranch)}`
1271
+ );
1272
+ } else {
1273
+ logger.warning(
1274
+ `Branch ${colors.branch(targetBranch)} not found locally or on remote.`
1275
+ );
1276
+ await offerCreateBranch(config, issueNumber, ticket.title);
1277
+ }
1278
+ } else {
1279
+ await offerCreateBranch(config, issueNumber, ticket.title);
1280
+ }
1281
+ }
1282
+ async function confirmDirty() {
1283
+ logger.warning("You have uncommitted changes.");
1284
+ const { proceed } = await inquirer3.prompt([
1285
+ {
1286
+ type: "confirm",
1287
+ name: "proceed",
1288
+ message: "Continue anyway? (changes will carry over to the new branch)",
1289
+ default: false
1290
+ }
1291
+ ]);
1292
+ if (!proceed) {
1293
+ logger.info("Aborting. Please commit or stash your changes first.");
1294
+ }
1295
+ return proceed;
1296
+ }
1297
+ async function offerCreateBranch(config, issueNumber, title) {
1298
+ const branchName = await generateBranchName(
1299
+ config,
1300
+ issueNumber,
1301
+ title,
1302
+ "feature"
1303
+ );
1304
+ const { create } = await inquirer3.prompt([
1305
+ {
1306
+ type: "confirm",
1307
+ name: "create",
1308
+ message: `No branch exists. Create ${colors.branch(branchName)}?`,
1309
+ default: true
1310
+ }
1311
+ ]);
1312
+ if (!create) return;
1313
+ const defaultBranch = await getDefaultBranch();
1314
+ await withSpinner(`Creating branch ${branchName}...`, async () => {
1315
+ await createBranch(branchName, defaultBranch);
1316
+ });
1317
+ logger.success(`Created and switched to ${colors.branch(branchName)}`);
1318
+ }
1319
+
1103
1320
  // src/commands/run.ts
1321
+ import inquirer4 from "inquirer";
1104
1322
  async function runCommand(issueNumberArg, options) {
1105
1323
  logger.bold("Running AI implementation workflow...");
1106
1324
  logger.newline();
@@ -1122,7 +1340,7 @@ async function runCommand(issueNumberArg, options) {
1122
1340
  const hasChanges = await hasUncommittedChanges();
1123
1341
  if (hasChanges) {
1124
1342
  logger.warning("You have uncommitted changes.");
1125
- const { proceed } = await inquirer3.prompt([
1343
+ const { proceed } = await inquirer4.prompt([
1126
1344
  {
1127
1345
  type: "confirm",
1128
1346
  name: "proceed",
@@ -1137,22 +1355,14 @@ async function runCommand(issueNumberArg, options) {
1137
1355
  }
1138
1356
  const workflowLabels = getWorkflowLabels(config);
1139
1357
  let issueNumber;
1140
- if (options.auto) {
1141
- const autoIssue = await autoSelectIssue(workflowLabels.ready);
1142
- if (!autoIssue) {
1143
- logger.error("No ai-ready issues found.");
1144
- return;
1145
- }
1146
- issueNumber = autoIssue.number;
1147
- logger.info(`Auto-selected: ${colors.issue(`#${issueNumber}`)} - ${autoIssue.title}`);
1148
- } else if (issueNumberArg) {
1358
+ if (issueNumberArg) {
1149
1359
  if (!isValidIssueNumber(issueNumberArg)) {
1150
1360
  logger.error("Invalid issue number.");
1151
1361
  return;
1152
1362
  }
1153
1363
  issueNumber = parseInt(issueNumberArg, 10);
1154
1364
  } else {
1155
- logger.error("Please provide an issue number or use --auto");
1365
+ logger.error("Please provide an issue number. Use 'gent switch' to browse tickets.");
1156
1366
  return;
1157
1367
  }
1158
1368
  let issue;
@@ -1166,7 +1376,7 @@ async function runCommand(issueNumberArg, options) {
1166
1376
  }
1167
1377
  if (!issue.labels.includes(workflowLabels.ready)) {
1168
1378
  logger.warning(`Issue #${issueNumber} does not have the '${workflowLabels.ready}' label.`);
1169
- const { proceed } = await inquirer3.prompt([
1379
+ const { proceed } = await inquirer4.prompt([
1170
1380
  {
1171
1381
  type: "confirm",
1172
1382
  name: "proceed",
@@ -1188,7 +1398,7 @@ Labels: ${issue.labels.join(", ")}`);
1188
1398
  const onMain = await isOnMainBranch();
1189
1399
  if (await branchExists(branchName)) {
1190
1400
  logger.info(`Branch ${colors.branch(branchName)} already exists.`);
1191
- const { action } = await inquirer3.prompt([
1401
+ const { action } = await inquirer4.prompt([
1192
1402
  {
1193
1403
  type: "list",
1194
1404
  name: "action",
@@ -1210,7 +1420,7 @@ Labels: ${issue.labels.join(", ")}`);
1210
1420
  } else {
1211
1421
  if (!onMain) {
1212
1422
  logger.warning(`Not on main branch (currently on ${colors.branch(currentBranch)}).`);
1213
- const { fromMain } = await inquirer3.prompt([
1423
+ const { fromMain } = await inquirer4.prompt([
1214
1424
  {
1215
1425
  type: "confirm",
1216
1426
  name: "fromMain",
@@ -1331,31 +1541,9 @@ No commits were created. Please retry later.`
1331
1541
  3. Push branch: ${colors.command("git push -u origin " + branchName)}
1332
1542
  4. Create PR: ${colors.command("gent pr")}`);
1333
1543
  }
1334
- async function autoSelectIssue(readyLabel) {
1335
- let issues = await listIssues({
1336
- labels: [readyLabel, "priority:critical"],
1337
- state: "open",
1338
- limit: 1
1339
- });
1340
- if (issues.length > 0) return issues[0];
1341
- issues = await listIssues({
1342
- labels: [readyLabel, "priority:high"],
1343
- state: "open",
1344
- limit: 1
1345
- });
1346
- if (issues.length > 0) return issues[0];
1347
- issues = await listIssues({
1348
- labels: [readyLabel],
1349
- state: "open",
1350
- limit: 10
1351
- });
1352
- if (issues.length === 0) return null;
1353
- sortByPriority(issues);
1354
- return issues[0];
1355
- }
1356
1544
 
1357
1545
  // src/commands/pr.ts
1358
- import inquirer4 from "inquirer";
1546
+ import inquirer5 from "inquirer";
1359
1547
 
1360
1548
  // src/lib/playwright.ts
1361
1549
  import { execa as execa3 } from "execa";
@@ -1436,7 +1624,7 @@ async function prCommand(options) {
1436
1624
  const hasUnpushed = await getUnpushedCommits();
1437
1625
  if (hasUnpushed) {
1438
1626
  logger.warning("Branch has unpushed commits.");
1439
- const { push } = await inquirer4.prompt([
1627
+ const { push } = await inquirer5.prompt([
1440
1628
  {
1441
1629
  type: "confirm",
1442
1630
  name: "push",
@@ -1573,7 +1761,7 @@ function generateFallbackBody(issue, commits) {
1573
1761
  }
1574
1762
 
1575
1763
  // src/commands/fix.ts
1576
- import inquirer5 from "inquirer";
1764
+ import inquirer6 from "inquirer";
1577
1765
 
1578
1766
  // src/lib/review-feedback.ts
1579
1767
  var ACTIONABLE_KEYWORDS = [
@@ -1764,7 +1952,7 @@ async function fixCommand(options) {
1764
1952
  const hasChanges = await hasUncommittedChanges();
1765
1953
  if (hasChanges) {
1766
1954
  logger.warning("You have uncommitted changes.");
1767
- const { proceed } = await inquirer5.prompt([
1955
+ const { proceed } = await inquirer6.prompt([
1768
1956
  {
1769
1957
  type: "confirm",
1770
1958
  name: "proceed",
@@ -1893,7 +2081,7 @@ import { homedir } from "os";
1893
2081
  // package.json
1894
2082
  var package_default = {
1895
2083
  name: "@rotorsoft/gent",
1896
- version: "1.12.1",
2084
+ version: "1.13.1",
1897
2085
  description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
1898
2086
  keywords: [
1899
2087
  "cli",
@@ -2303,7 +2491,7 @@ async function statusCommand() {
2303
2491
  }
2304
2492
 
2305
2493
  // src/commands/tui.ts
2306
- import inquirer6 from "inquirer";
2494
+ import inquirer7 from "inquirer";
2307
2495
  import { execa as execa4 } from "execa";
2308
2496
 
2309
2497
  // src/tui/state.ts
@@ -2432,7 +2620,6 @@ function getAvailableActions(state) {
2432
2620
  }
2433
2621
  if (state.isOnMain) {
2434
2622
  actions.push({ id: "create", label: "new", shortcut: "n" });
2435
- actions.push({ id: "run-auto", label: "run next", shortcut: "r" });
2436
2623
  actions.push({ id: "list", label: "list", shortcut: "l" });
2437
2624
  actions.push({ id: "switch-provider", label: "switch", shortcut: "s" });
2438
2625
  actions.push({ id: "quit", label: "quit", shortcut: "q" });
@@ -2447,7 +2634,7 @@ function getAvailableActions(state) {
2447
2634
  if (!state.pr && state.commits.length > 0) {
2448
2635
  actions.push({ id: "pr", label: "Create pr", shortcut: "C" });
2449
2636
  }
2450
- if (state.issue) {
2637
+ if (state.issue && state.pr?.state !== "merged") {
2451
2638
  actions.push({ id: "implement", label: "implement", shortcut: "i" });
2452
2639
  }
2453
2640
  if (state.pr && state.pr.state === "open") {
@@ -2458,6 +2645,7 @@ function getAvailableActions(state) {
2458
2645
  if (state.pr && (state.pr.state === "merged" || state.pr.state === "closed")) {
2459
2646
  actions.push({ id: "checkout-main", label: "main", shortcut: "m" });
2460
2647
  }
2648
+ actions.push({ id: "list", label: "list", shortcut: "l" });
2461
2649
  actions.push({ id: "switch-provider", label: "switch", shortcut: "s" });
2462
2650
  actions.push({ id: "quit", label: "quit", shortcut: "q" });
2463
2651
  return actions;
@@ -2470,9 +2658,6 @@ var visibleLen = (str) => stripAnsi(str).length;
2470
2658
  function termWidth() {
2471
2659
  return Math.min(process.stdout.columns || 80, 90);
2472
2660
  }
2473
- function termHeight() {
2474
- return process.stdout.rows || 24;
2475
- }
2476
2661
  function truncate(text, max) {
2477
2662
  if (text.length <= max) return text;
2478
2663
  return text.slice(0, max - 1) + "\u2026";
@@ -2582,25 +2767,6 @@ function formatCommandBar(actions, w) {
2582
2767
  if (cur.length > 0) lines.push(cur);
2583
2768
  return lines;
2584
2769
  }
2585
- function renderModal(message) {
2586
- const w = termWidth();
2587
- const h = termHeight();
2588
- const modalW = Math.min(w - 4, Math.max(30, visibleLen(message) + 8));
2589
- const padX = Math.max(0, Math.floor((w - modalW) / 2));
2590
- const padY = Math.max(0, Math.floor((h - 5) / 2));
2591
- const indent = " ".repeat(padX);
2592
- const inner = modalW - 4;
2593
- const textPad = Math.max(0, inner - visibleLen(message));
2594
- const lines = [
2595
- indent + chalk3.dim("\u250C" + "\u2500".repeat(modalW - 2) + "\u2510"),
2596
- indent + chalk3.dim("\u2502") + " ".repeat(modalW - 2) + chalk3.dim("\u2502"),
2597
- indent + chalk3.dim("\u2502") + " " + chalk3.bold(message) + " ".repeat(textPad) + " " + chalk3.dim("\u2502"),
2598
- indent + chalk3.dim("\u2502") + " ".repeat(modalW - 2) + chalk3.dim("\u2502"),
2599
- indent + chalk3.dim("\u2514" + "\u2500".repeat(modalW - 2) + "\u2518")
2600
- ];
2601
- for (let i = 0; i < padY; i++) console.log();
2602
- for (const line of lines) console.log(line);
2603
- }
2604
2770
  function renderActionPanel(title, content) {
2605
2771
  const w = termWidth();
2606
2772
  console.log(topRow(title, w));
@@ -2618,11 +2784,11 @@ function renderSettings(state, w) {
2618
2784
  console.log(row(chalk3.dim("GitHub: ") + ghTag, w));
2619
2785
  console.log(row(chalk3.dim("Video: ") + videoTag, w));
2620
2786
  }
2621
- function renderDashboard(state, actions, hint) {
2787
+ function renderDashboard(state, actions, hint, refreshing) {
2622
2788
  const w = termWidth();
2623
2789
  const descMax = w - 8;
2624
2790
  const version2 = getVersion();
2625
- const titleLabel = `gent v${version2}`;
2791
+ const titleLabel = refreshing ? `gent v${version2} ${chalk3.yellow("Refreshing\u2026")}` : `gent v${version2}`;
2626
2792
  console.log(topRow(titleLabel, w));
2627
2793
  if (!state.isGitRepo) {
2628
2794
  console.log(row(chalk3.red("Not a git repository"), w));
@@ -2739,7 +2905,7 @@ function clearScreen() {
2739
2905
  // src/commands/tui.ts
2740
2906
  var CANCEL = /* @__PURE__ */ Symbol("cancel");
2741
2907
  async function confirm(message) {
2742
- const { ok } = await inquirer6.prompt([
2908
+ const { ok } = await inquirer7.prompt([
2743
2909
  {
2744
2910
  type: "confirm",
2745
2911
  name: "ok",
@@ -2749,12 +2915,6 @@ async function confirm(message) {
2749
2915
  ]);
2750
2916
  return ok;
2751
2917
  }
2752
- async function loadState() {
2753
- clearScreen();
2754
- renderModal("Loading workflow state...");
2755
- const state = await aggregateState();
2756
- return state;
2757
- }
2758
2918
  async function waitForKey(validKeys) {
2759
2919
  return new Promise((resolve) => {
2760
2920
  const { stdin } = process;
@@ -2784,25 +2944,19 @@ async function executeAction(actionId, state, providerSetter) {
2784
2944
  switch (actionId) {
2785
2945
  case "quit":
2786
2946
  return false;
2787
- case "list":
2947
+ case "list": {
2788
2948
  clearScreen();
2789
- await listCommand({ status: "ready", limit: 20 });
2790
- await promptContinue();
2791
- return true;
2792
- case "run-auto": {
2793
- clearScreen();
2794
- if (!await confirm("Start AI agent to implement next ticket?")) return true;
2795
2949
  try {
2796
- await runCommand(void 0, { auto: true });
2950
+ await listCommand({});
2797
2951
  } catch (error) {
2798
- logger.error(`Run failed: ${error}`);
2799
- await promptContinue();
2952
+ logger.error(`List failed: ${error}`);
2800
2953
  }
2801
- return false;
2954
+ await promptContinue();
2955
+ return true;
2802
2956
  }
2803
2957
  case "create": {
2804
2958
  clearScreen();
2805
- const { description } = await inquirer6.prompt([
2959
+ const { description } = await inquirer7.prompt([
2806
2960
  {
2807
2961
  type: "input",
2808
2962
  name: "description",
@@ -2936,7 +3090,7 @@ async function generateCommitMessage(diffContent, issueNumber, issueTitle, state
2936
3090
  } catch {
2937
3091
  logger.warning("AI commit message generation failed");
2938
3092
  console.log();
2939
- const { message } = await inquirer6.prompt([
3093
+ const { message } = await inquirer7.prompt([
2940
3094
  {
2941
3095
  type: "input",
2942
3096
  name: "message",
@@ -3005,7 +3159,7 @@ async function handlePush() {
3005
3159
  var PROVIDERS = ["claude", "gemini", "codex"];
3006
3160
  async function handleSwitchProvider(state, setProvider) {
3007
3161
  const current = state.config.ai.provider;
3008
- const { provider } = await inquirer6.prompt([
3162
+ const { provider } = await inquirer7.prompt([
3009
3163
  {
3010
3164
  type: "list",
3011
3165
  name: "provider",
@@ -3060,7 +3214,7 @@ async function handleVideoCapture(state) {
3060
3214
  }
3061
3215
  async function promptContinue() {
3062
3216
  console.log();
3063
- await inquirer6.prompt([
3217
+ await inquirer7.prompt([
3064
3218
  {
3065
3219
  type: "input",
3066
3220
  name: "continue",
@@ -3071,16 +3225,44 @@ async function promptContinue() {
3071
3225
  async function tuiCommand() {
3072
3226
  let running = true;
3073
3227
  let providerOverride = null;
3228
+ let lastActions = [];
3074
3229
  const setProvider = (p) => {
3075
3230
  providerOverride = p;
3076
3231
  };
3232
+ const config = loadConfig();
3233
+ let lastState = {
3234
+ isGitRepo: true,
3235
+ isGhAuthenticated: true,
3236
+ isAIProviderAvailable: true,
3237
+ config,
3238
+ hasConfig: true,
3239
+ hasProgress: false,
3240
+ branch: "",
3241
+ branchInfo: null,
3242
+ isOnMain: true,
3243
+ hasUncommittedChanges: false,
3244
+ hasUnpushedCommits: false,
3245
+ commits: [],
3246
+ baseBranch: "main",
3247
+ issue: null,
3248
+ workflowStatus: "none",
3249
+ pr: null,
3250
+ reviewFeedback: [],
3251
+ hasActionableFeedback: false,
3252
+ hasUIChanges: false,
3253
+ isPlaywrightAvailable: false
3254
+ };
3077
3255
  while (running) {
3078
- const state = await loadState();
3256
+ clearScreen();
3257
+ renderDashboard(lastState, lastActions, void 0, true);
3258
+ const state = await aggregateState();
3079
3259
  if (providerOverride) {
3080
3260
  state.config.ai.provider = providerOverride;
3081
3261
  }
3082
- clearScreen();
3083
3262
  const actions = getAvailableActions(state);
3263
+ lastState = state;
3264
+ lastActions = actions;
3265
+ clearScreen();
3084
3266
  let hint;
3085
3267
  if (state.isOnMain) {
3086
3268
  hint = "Select an action to get started";
@@ -3126,15 +3308,15 @@ program.command("setup-labels").description("Setup GitHub labels for AI workflow
3126
3308
  program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").option("-t, --title <title>", "Override the generated issue title").action(async (description, options) => {
3127
3309
  await createCommand(description, { yes: options.yes, provider: options.provider, title: options.title });
3128
3310
  });
3129
- program.command("list").description("List GitHub issues by label/status").option("-l, --label <label>", "Filter by label").option("-s, --status <status>", "Filter by workflow status (ready, in-progress, completed, blocked, all)").option("-n, --limit <number>", "Maximum number of issues to show", "20").action(async (options) => {
3311
+ program.command("list").description("List and switch to GitHub issues").option("-l, --label <label>", "Filter by label").option("-s, --status <status>", "Filter by workflow status (ready, in-progress, completed, blocked, all)").option("-n, --limit <number>", "Maximum number of issues to show", "20").action(async (options) => {
3130
3312
  await listCommand({
3131
3313
  label: options.label,
3132
3314
  status: options.status,
3133
3315
  limit: parseInt(options.limit, 10)
3134
3316
  });
3135
3317
  });
3136
- program.command("run [issue-number]").description("Run AI to implement a GitHub issue").option("-a, --auto", "Auto-select highest priority ai-ready issue").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").action(async (issueNumber, options) => {
3137
- await runCommand(issueNumber, { auto: options.auto, provider: options.provider });
3318
+ program.command("run [issue-number]").description("Run AI to implement a GitHub issue").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").action(async (issueNumber, options) => {
3319
+ await runCommand(issueNumber, { provider: options.provider });
3138
3320
  });
3139
3321
  program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").option("--no-video", "Disable video capture for UI changes").action(async (options) => {
3140
3322
  await prCommand({