@rotorsoft/gent 1.11.0 → 1.12.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,6 +13,7 @@ import {
13
13
  configExists,
14
14
  createIssue,
15
15
  createPullRequest,
16
+ createSpinner,
16
17
  extractPriorityFromLabels,
17
18
  extractTypeFromLabels,
18
19
  generateDefaultConfig,
@@ -34,7 +35,7 @@ import {
34
35
  sortByPriority,
35
36
  updateIssueLabels,
36
37
  withSpinner
37
- } from "./chunk-3N6U3NTN.js";
38
+ } from "./chunk-UWYWIOYZ.js";
38
39
 
39
40
  // src/index.ts
40
41
  import { Command } from "commander";
@@ -195,7 +196,7 @@ async function initCommand(options) {
195
196
  }
196
197
  ]);
197
198
  if (setupLabels) {
198
- const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-HKXXO65J.js");
199
+ const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-5EHRPUPF.js");
199
200
  await setupLabelsCommand2();
200
201
  }
201
202
  }
@@ -471,7 +472,7 @@ META:type=<type>,priority=<priority>,risk=<risk>,area=<area>
471
472
  Example: META:type=feature,priority=high,risk=low,area=ui`;
472
473
  return basePrompt;
473
474
  }
474
- function buildImplementationPrompt(issue, agentInstructions, progressContent, config, reviewFeedback = null) {
475
+ function buildImplementationPrompt(issue, agentInstructions, progressContent, config, extraContext = null) {
475
476
  const providerName = getProviderDisplayName(config.ai.provider);
476
477
  const providerEmail = getProviderEmail(config.ai.provider);
477
478
  return `GitHub Issue #${issue.number}: ${issue.title}
@@ -486,8 +487,7 @@ ${progressContent ? `## Previous Progress
486
487
  ${progressContent}
487
488
 
488
489
  ` : ""}
489
- ${reviewFeedback ? `## Review Feedback
490
- ${reviewFeedback}
490
+ ${extraContext ? `${extraContext}
491
491
 
492
492
  ` : ""}
493
493
 
@@ -540,6 +540,22 @@ ${issue ? `Closes #${issue.number}` : ""}
540
540
 
541
541
  Only output the PR description, nothing else.`;
542
542
  }
543
+ function buildCommitMessagePrompt(diff, issueNumber, issueTitle) {
544
+ const issueContext = issueNumber ? `
545
+ Related Issue: #${issueNumber}${issueTitle ? ` - ${issueTitle}` : ""}
546
+ ` : "";
547
+ return `Generate a concise git commit message for the following changes.
548
+ ${issueContext}
549
+ ## Diff
550
+ ${diff}
551
+
552
+ Rules:
553
+ - Use conventional commit format: <type>: <short description>
554
+ - Types: feat, fix, refactor, chore, docs, test, style, perf
555
+ - Keep the first line under 72 characters
556
+ - Do NOT include a body or footer
557
+ - Output ONLY the commit message, nothing else`;
558
+ }
543
559
  function parseTicketMeta(output) {
544
560
  const metaMatch = output.match(
545
561
  /META:type=(\w+),priority=(\w+),risk=(\w+),area=(\w+)/
@@ -592,6 +608,46 @@ function generateFallbackTitle(description) {
592
608
  }
593
609
  return truncated;
594
610
  }
611
+ function buildVideoPrompt(issueNumber, issueTitle, videoConfig, agentInstructions) {
612
+ return `You are helping capture a Playwright video demonstration of UI changes for GitHub Issue #${issueNumber}: ${issueTitle}
613
+
614
+ ${agentInstructions ? `## Project-Specific Instructions
615
+ ${agentInstructions}
616
+
617
+ ` : ""}
618
+
619
+ ## Task: Record UI Demo Video
620
+
621
+ Create a short video (max ${videoConfig.max_duration}s) demonstrating the UI changes made for this issue.
622
+
623
+ ### Video Requirements
624
+ - Resolution: ${videoConfig.width}x${videoConfig.height}
625
+ - Format: WebM or MP4
626
+ - Duration: Under ${videoConfig.max_duration} seconds
627
+ - Show the key UI interactions and visual changes
628
+
629
+ ### Steps
630
+
631
+ 1. **Start the development server** if not already running
632
+ 2. **Use Playwright to record video** of the relevant UI interactions:
633
+ - Navigate to the affected pages/components
634
+ - Demonstrate the new or changed functionality
635
+ - Show before/after if applicable
636
+
637
+ 3. **Upload video to GitHub** as a release asset or use GitHub's drag-drop upload:
638
+ - Create a GitHub release or upload to issue comments
639
+ - Get the permanent URL for the video
640
+ - Do NOT commit video files to the repository
641
+
642
+ 4. **Add video to PR** by commenting with the video URL or embedding it
643
+
644
+ ### Important
645
+ - Upload video to GitHub assets, NOT to the repository
646
+ - Keep the video concise - focus on demonstrating the changes
647
+ - Ensure the video clearly shows the UI improvements
648
+
649
+ Output the GitHub URL where the video was uploaded when complete.`;
650
+ }
595
651
 
596
652
  // src/commands/create.ts
597
653
  async function createCommand(description, options) {
@@ -606,11 +662,11 @@ async function createCommand(description, options) {
606
662
  ]);
607
663
  if (!ghAuth) {
608
664
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
609
- process.exit(1);
665
+ return;
610
666
  }
611
667
  if (!aiOk) {
612
668
  logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
613
- process.exit(1);
669
+ return;
614
670
  }
615
671
  const agentInstructions = loadAgentInstructions();
616
672
  let aiOutput;
@@ -1057,11 +1113,11 @@ async function runCommand(issueNumberArg, options) {
1057
1113
  ]);
1058
1114
  if (!ghAuth) {
1059
1115
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1060
- process.exit(1);
1116
+ return;
1061
1117
  }
1062
1118
  if (!aiOk) {
1063
1119
  logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
1064
- process.exit(1);
1120
+ return;
1065
1121
  }
1066
1122
  const hasChanges = await hasUncommittedChanges();
1067
1123
  if (hasChanges) {
@@ -1076,7 +1132,7 @@ async function runCommand(issueNumberArg, options) {
1076
1132
  ]);
1077
1133
  if (!proceed) {
1078
1134
  logger.info("Aborting. Please commit or stash your changes first.");
1079
- process.exit(0);
1135
+ return;
1080
1136
  }
1081
1137
  }
1082
1138
  const workflowLabels = getWorkflowLabels(config);
@@ -1085,19 +1141,19 @@ async function runCommand(issueNumberArg, options) {
1085
1141
  const autoIssue = await autoSelectIssue(workflowLabels.ready);
1086
1142
  if (!autoIssue) {
1087
1143
  logger.error("No ai-ready issues found.");
1088
- process.exit(1);
1144
+ return;
1089
1145
  }
1090
1146
  issueNumber = autoIssue.number;
1091
1147
  logger.info(`Auto-selected: ${colors.issue(`#${issueNumber}`)} - ${autoIssue.title}`);
1092
1148
  } else if (issueNumberArg) {
1093
1149
  if (!isValidIssueNumber(issueNumberArg)) {
1094
1150
  logger.error("Invalid issue number.");
1095
- process.exit(1);
1151
+ return;
1096
1152
  }
1097
1153
  issueNumber = parseInt(issueNumberArg, 10);
1098
1154
  } else {
1099
1155
  logger.error("Please provide an issue number or use --auto");
1100
- process.exit(1);
1156
+ return;
1101
1157
  }
1102
1158
  let issue;
1103
1159
  try {
@@ -1756,7 +1812,8 @@ async function fixCommand(options) {
1756
1812
  });
1757
1813
  const agentInstructions = loadAgentInstructions();
1758
1814
  const progressContent = readProgress(config);
1759
- const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config, summary);
1815
+ const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config, `## Review Feedback
1816
+ ${summary}`);
1760
1817
  logger.newline();
1761
1818
  logger.info(`Starting ${colors.provider(providerName)} fix session...`);
1762
1819
  logger.dim("Review feedback will be appended to the implementation prompt.");
@@ -1836,7 +1893,7 @@ import { homedir } from "os";
1836
1893
  // package.json
1837
1894
  var package_default = {
1838
1895
  name: "@rotorsoft/gent",
1839
- version: "1.11.0",
1896
+ version: "1.12.1",
1840
1897
  description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
1841
1898
  keywords: [
1842
1899
  "cli",
@@ -2245,6 +2302,803 @@ async function statusCommand() {
2245
2302
  }
2246
2303
  }
2247
2304
 
2305
+ // src/commands/tui.ts
2306
+ import inquirer6 from "inquirer";
2307
+ import { execa as execa4 } from "execa";
2308
+
2309
+ // src/tui/state.ts
2310
+ async function aggregateState() {
2311
+ const isGitRepo = await checkGitRepo();
2312
+ if (!isGitRepo) {
2313
+ const config2 = loadConfig();
2314
+ return {
2315
+ isGitRepo: false,
2316
+ isGhAuthenticated: false,
2317
+ isAIProviderAvailable: false,
2318
+ config: config2,
2319
+ hasConfig: false,
2320
+ hasProgress: false,
2321
+ branch: "",
2322
+ branchInfo: null,
2323
+ isOnMain: false,
2324
+ hasUncommittedChanges: false,
2325
+ hasUnpushedCommits: false,
2326
+ commits: [],
2327
+ baseBranch: "main",
2328
+ issue: null,
2329
+ workflowStatus: "none",
2330
+ pr: null,
2331
+ reviewFeedback: [],
2332
+ hasActionableFeedback: false,
2333
+ hasUIChanges: false,
2334
+ isPlaywrightAvailable: false
2335
+ };
2336
+ }
2337
+ const config = loadConfig();
2338
+ const workflowLabels = getWorkflowLabels(config);
2339
+ const [
2340
+ isGhAuthenticated,
2341
+ isAIProviderAvailable,
2342
+ branch,
2343
+ isOnMain,
2344
+ uncommitted,
2345
+ baseBranch
2346
+ ] = await Promise.all([
2347
+ checkGhAuth(),
2348
+ checkAIProvider(config.ai.provider),
2349
+ getCurrentBranch(),
2350
+ isOnMainBranch(),
2351
+ hasUncommittedChanges(),
2352
+ getDefaultBranch()
2353
+ ]);
2354
+ const hasConfig = configExists();
2355
+ const hasProgress = progressExists(config);
2356
+ const branchInfo = parseBranchName(branch);
2357
+ const [commits, unpushed] = await Promise.all([
2358
+ getCommitsSinceBase(baseBranch),
2359
+ getUnpushedCommits()
2360
+ ]);
2361
+ let issue = null;
2362
+ let workflowStatus = "none";
2363
+ let pr = null;
2364
+ let reviewFeedback = [];
2365
+ let hasActionableFeedback = false;
2366
+ let uiChanges = false;
2367
+ let playwrightAvailable = false;
2368
+ if (!isOnMain) {
2369
+ const issueNumber = extractIssueNumber(branch);
2370
+ const [issueResult, prResult, changedFiles, playwrightResult] = await Promise.all([
2371
+ issueNumber ? getIssue(issueNumber).catch(() => null) : Promise.resolve(null),
2372
+ getPrStatus().catch(() => null),
2373
+ getChangedFiles(baseBranch),
2374
+ isPlaywrightAvailable()
2375
+ ]);
2376
+ issue = issueResult;
2377
+ pr = prResult;
2378
+ uiChanges = hasUIChanges(changedFiles);
2379
+ playwrightAvailable = playwrightResult;
2380
+ if (issue) {
2381
+ if (issue.labels.includes(workflowLabels.ready)) {
2382
+ workflowStatus = "ready";
2383
+ } else if (issue.labels.includes(workflowLabels.inProgress)) {
2384
+ workflowStatus = "in-progress";
2385
+ } else if (issue.labels.includes(workflowLabels.completed)) {
2386
+ workflowStatus = "completed";
2387
+ } else if (issue.labels.includes(workflowLabels.blocked)) {
2388
+ workflowStatus = "blocked";
2389
+ }
2390
+ }
2391
+ if (pr && pr.state === "open") {
2392
+ try {
2393
+ const lastCommitTimestamp = await getLastCommitTimestamp();
2394
+ const reviewData = await getPrReviewData(pr.number);
2395
+ const { items } = summarizeReviewFeedback(reviewData, { afterTimestamp: lastCommitTimestamp });
2396
+ reviewFeedback = items;
2397
+ hasActionableFeedback = items.length > 0;
2398
+ } catch {
2399
+ }
2400
+ }
2401
+ }
2402
+ return {
2403
+ isGitRepo,
2404
+ isGhAuthenticated,
2405
+ isAIProviderAvailable,
2406
+ config,
2407
+ hasConfig,
2408
+ hasProgress,
2409
+ branch,
2410
+ branchInfo,
2411
+ isOnMain,
2412
+ hasUncommittedChanges: uncommitted,
2413
+ hasUnpushedCommits: unpushed,
2414
+ commits,
2415
+ baseBranch,
2416
+ issue,
2417
+ workflowStatus,
2418
+ pr,
2419
+ reviewFeedback,
2420
+ hasActionableFeedback,
2421
+ hasUIChanges: uiChanges,
2422
+ isPlaywrightAvailable: playwrightAvailable
2423
+ };
2424
+ }
2425
+
2426
+ // src/tui/actions.ts
2427
+ function getAvailableActions(state) {
2428
+ const actions = [];
2429
+ if (!state.isGitRepo || !state.isGhAuthenticated) {
2430
+ actions.push({ id: "quit", label: "quit", shortcut: "q" });
2431
+ return actions;
2432
+ }
2433
+ if (state.isOnMain) {
2434
+ actions.push({ id: "create", label: "new", shortcut: "n" });
2435
+ actions.push({ id: "run-auto", label: "run next", shortcut: "r" });
2436
+ actions.push({ id: "list", label: "list", shortcut: "l" });
2437
+ actions.push({ id: "switch-provider", label: "switch", shortcut: "s" });
2438
+ actions.push({ id: "quit", label: "quit", shortcut: "q" });
2439
+ return actions;
2440
+ }
2441
+ if (state.hasUncommittedChanges) {
2442
+ actions.push({ id: "commit", label: "commit", shortcut: "c" });
2443
+ }
2444
+ if (state.hasUnpushedCommits && state.commits.length > 0) {
2445
+ actions.push({ id: "push", label: "Push", shortcut: "P" });
2446
+ }
2447
+ if (!state.pr && state.commits.length > 0) {
2448
+ actions.push({ id: "pr", label: "Create pr", shortcut: "C" });
2449
+ }
2450
+ if (state.issue) {
2451
+ actions.push({ id: "implement", label: "implement", shortcut: "i" });
2452
+ }
2453
+ if (state.pr && state.pr.state === "open") {
2454
+ if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled) {
2455
+ actions.push({ id: "video", label: "video", shortcut: "v" });
2456
+ }
2457
+ }
2458
+ if (state.pr && (state.pr.state === "merged" || state.pr.state === "closed")) {
2459
+ actions.push({ id: "checkout-main", label: "main", shortcut: "m" });
2460
+ }
2461
+ actions.push({ id: "switch-provider", label: "switch", shortcut: "s" });
2462
+ actions.push({ id: "quit", label: "quit", shortcut: "q" });
2463
+ return actions;
2464
+ }
2465
+
2466
+ // src/tui/display.ts
2467
+ import chalk3 from "chalk";
2468
+ var stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, "");
2469
+ var visibleLen = (str) => stripAnsi(str).length;
2470
+ function termWidth() {
2471
+ return Math.min(process.stdout.columns || 80, 90);
2472
+ }
2473
+ function termHeight() {
2474
+ return process.stdout.rows || 24;
2475
+ }
2476
+ function truncate(text, max) {
2477
+ if (text.length <= max) return text;
2478
+ return text.slice(0, max - 1) + "\u2026";
2479
+ }
2480
+ function extractDescription(body, maxLen) {
2481
+ const lines = body.split("\n");
2482
+ for (const line of lines) {
2483
+ const trimmed = line.trim();
2484
+ if (!trimmed) continue;
2485
+ if (trimmed.startsWith("#")) continue;
2486
+ if (trimmed.startsWith("---")) continue;
2487
+ if (trimmed.startsWith("META:")) continue;
2488
+ if (trimmed.startsWith("**Type:**")) continue;
2489
+ const clean = trimmed.replace(/\*\*/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
2490
+ return truncate(clean, maxLen);
2491
+ }
2492
+ return "";
2493
+ }
2494
+ function topRow(title, w) {
2495
+ const label = ` ${title} `;
2496
+ const fill = w - 2 - label.length;
2497
+ return chalk3.dim("\u250C") + chalk3.bold.cyan(label) + chalk3.dim("\u2500".repeat(Math.max(0, fill)) + "\u2510");
2498
+ }
2499
+ function midRow(title, w) {
2500
+ const label = ` ${title} `;
2501
+ const fill = w - 2 - label.length;
2502
+ return chalk3.dim("\u251C") + chalk3.bold.cyan(label) + chalk3.dim("\u2500".repeat(Math.max(0, fill)) + "\u2524");
2503
+ }
2504
+ function divRow(w) {
2505
+ return chalk3.dim("\u251C" + "\u2500".repeat(w - 2) + "\u2524");
2506
+ }
2507
+ function botRow(w) {
2508
+ return chalk3.dim("\u2514" + "\u2500".repeat(w - 2) + "\u2518");
2509
+ }
2510
+ function row(text, w) {
2511
+ const inner = w - 4;
2512
+ const pad = Math.max(0, inner - visibleLen(text));
2513
+ return chalk3.dim("\u2502") + " " + text + " ".repeat(pad) + " " + chalk3.dim("\u2502");
2514
+ }
2515
+ function workflowBadge(status) {
2516
+ switch (status) {
2517
+ case "ready":
2518
+ return chalk3.bgGreen.black(" READY ");
2519
+ case "in-progress":
2520
+ return chalk3.bgYellow.black(" IN PROGRESS ");
2521
+ case "completed":
2522
+ return chalk3.bgBlue.white(" COMPLETED ");
2523
+ case "blocked":
2524
+ return chalk3.bgRed.white(" BLOCKED ");
2525
+ default:
2526
+ return "";
2527
+ }
2528
+ }
2529
+ function prBadge(state, draft) {
2530
+ if (state === "merged") return chalk3.bgMagenta.white(" MERGED ");
2531
+ if (state === "closed") return chalk3.bgRed.white(" CLOSED ");
2532
+ return draft ? chalk3.bgYellow.black(" DRAFT ") : chalk3.bgGreen.black(" OPEN ");
2533
+ }
2534
+ function reviewBadge(decision) {
2535
+ if (!decision) return "";
2536
+ switch (decision) {
2537
+ case "APPROVED":
2538
+ return " " + chalk3.green("Approved");
2539
+ case "CHANGES_REQUESTED":
2540
+ return " " + chalk3.red("Changes requested");
2541
+ case "REVIEW_REQUIRED":
2542
+ return " " + chalk3.yellow("Review pending");
2543
+ default:
2544
+ return "";
2545
+ }
2546
+ }
2547
+ var shortcutColors = [
2548
+ chalk3.cyan.bold,
2549
+ chalk3.green.bold,
2550
+ chalk3.yellow.bold,
2551
+ chalk3.magenta.bold,
2552
+ chalk3.blue.bold,
2553
+ chalk3.red.bold
2554
+ ];
2555
+ function formatAction(a, color) {
2556
+ const key = a.shortcut;
2557
+ const idx = a.label.indexOf(key);
2558
+ if (idx >= 0) {
2559
+ const before = a.label.slice(0, idx);
2560
+ const after = a.label.slice(idx + key.length);
2561
+ return chalk3.dim(before) + color(key) + chalk3.dim(after);
2562
+ }
2563
+ return color(key) + " " + chalk3.dim(a.label);
2564
+ }
2565
+ function formatCommandBar(actions, w) {
2566
+ const parts = actions.map((a, i) => {
2567
+ const color = shortcutColors[i % shortcutColors.length];
2568
+ return formatAction(a, color);
2569
+ });
2570
+ const inner = w - 4;
2571
+ const lines = [];
2572
+ let cur = "";
2573
+ for (const part of parts) {
2574
+ const next = cur + (cur.length > 0 ? " " : "") + part;
2575
+ if (visibleLen(next) > inner) {
2576
+ lines.push(cur);
2577
+ cur = part;
2578
+ } else {
2579
+ cur = next;
2580
+ }
2581
+ }
2582
+ if (cur.length > 0) lines.push(cur);
2583
+ return lines;
2584
+ }
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
+ function renderActionPanel(title, content) {
2605
+ const w = termWidth();
2606
+ console.log(topRow(title, w));
2607
+ for (const line of content) {
2608
+ console.log(row(line, w));
2609
+ }
2610
+ console.log(botRow(w));
2611
+ }
2612
+ function renderSettings(state, w) {
2613
+ const provider = getProviderDisplayName(state.config.ai.provider);
2614
+ const provTag = state.isAIProviderAvailable ? chalk3.green(provider) : chalk3.red(provider);
2615
+ const ghTag = state.isGhAuthenticated ? chalk3.green("authenticated") : chalk3.red("not authenticated");
2616
+ const videoTag = state.config.video.enabled ? chalk3.green("on") : chalk3.dim("off");
2617
+ console.log(row(chalk3.dim("Provider: ") + provTag, w));
2618
+ console.log(row(chalk3.dim("GitHub: ") + ghTag, w));
2619
+ console.log(row(chalk3.dim("Video: ") + videoTag, w));
2620
+ }
2621
+ function renderDashboard(state, actions, hint) {
2622
+ const w = termWidth();
2623
+ const descMax = w - 8;
2624
+ const version2 = getVersion();
2625
+ const titleLabel = `gent v${version2}`;
2626
+ console.log(topRow(titleLabel, w));
2627
+ if (!state.isGitRepo) {
2628
+ console.log(row(chalk3.red("Not a git repository"), w));
2629
+ console.log(row(chalk3.dim("Run gent init in a git repo to get started"), w));
2630
+ console.log(botRow(w));
2631
+ return;
2632
+ }
2633
+ if (!state.isGhAuthenticated) {
2634
+ console.log(row(chalk3.red("GitHub CLI not authenticated"), w));
2635
+ console.log(row(chalk3.dim("Run: gh auth login"), w));
2636
+ console.log(botRow(w));
2637
+ return;
2638
+ }
2639
+ if (state.isOnMain) {
2640
+ console.log(row(chalk3.magenta(state.branch) + chalk3.dim(" \xB7 ready to start new work"), w));
2641
+ if (state.hasUncommittedChanges) {
2642
+ console.log(row(chalk3.yellow("\u25CF uncommitted changes"), w));
2643
+ }
2644
+ console.log(midRow("Settings", w));
2645
+ renderSettings(state, w);
2646
+ if (hint) {
2647
+ console.log(midRow("Hint", w));
2648
+ console.log(row(chalk3.yellow(hint), w));
2649
+ }
2650
+ console.log(divRow(w));
2651
+ for (const line of formatCommandBar(actions, w)) {
2652
+ console.log(row(line, w));
2653
+ }
2654
+ console.log(botRow(w));
2655
+ console.log();
2656
+ return;
2657
+ }
2658
+ const section = (title) => {
2659
+ console.log(midRow(title, w));
2660
+ };
2661
+ section("Ticket");
2662
+ if (state.issue) {
2663
+ console.log(row(
2664
+ chalk3.cyan(`#${state.issue.number}`) + " " + chalk3.bold(truncate(state.issue.title, descMax - 6)),
2665
+ w
2666
+ ));
2667
+ const desc = extractDescription(state.issue.body, descMax);
2668
+ if (desc) console.log(row(chalk3.dim(desc), w));
2669
+ const tags = [];
2670
+ if (state.workflowStatus !== "none") tags.push(workflowBadge(state.workflowStatus));
2671
+ for (const prefix of ["type:", "priority:", "risk:", "area:"]) {
2672
+ const l = state.issue.labels.find((x) => x.startsWith(prefix));
2673
+ if (l) tags.push(chalk3.dim(l));
2674
+ }
2675
+ if (tags.length) console.log(row(tags.join(" "), w));
2676
+ } else {
2677
+ console.log(row(chalk3.dim("No linked issue"), w));
2678
+ }
2679
+ section("Branch");
2680
+ console.log(row(chalk3.magenta(state.branch), w));
2681
+ const bits = [];
2682
+ if (state.commits.length > 0) bits.push(chalk3.dim(`${state.commits.length} ahead`));
2683
+ if (state.hasUncommittedChanges) bits.push(chalk3.yellow("\u25CF uncommitted"));
2684
+ if (state.hasUnpushedCommits) bits.push(chalk3.yellow("\u25CF unpushed"));
2685
+ if (!state.hasUncommittedChanges && !state.hasUnpushedCommits && state.commits.length > 0) {
2686
+ bits.push(chalk3.green("\u25CF synced"));
2687
+ }
2688
+ if (bits.length) console.log(row(bits.join(chalk3.dim(" \xB7 ")), w));
2689
+ section("Pull Request");
2690
+ if (state.pr) {
2691
+ const titleText = state.pr.title ? " " + truncate(state.pr.title, descMax - 12) : "";
2692
+ console.log(row(
2693
+ chalk3.cyan(`#${state.pr.number}`) + titleText,
2694
+ w
2695
+ ));
2696
+ console.log(row(
2697
+ prBadge(state.pr.state, state.pr.isDraft) + reviewBadge(state.pr.reviewDecision),
2698
+ w
2699
+ ));
2700
+ if (state.hasActionableFeedback) {
2701
+ const n = state.reviewFeedback.length;
2702
+ console.log(row(chalk3.yellow(`${n} actionable comment${n !== 1 ? "s" : ""} pending`), w));
2703
+ }
2704
+ if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled && state.pr.state === "open") {
2705
+ console.log(row(chalk3.cyan("UI changes detected") + chalk3.dim(" \xB7 video capture available"), w));
2706
+ }
2707
+ console.log(row(chalk3.dim(state.pr.url), w));
2708
+ } else {
2709
+ console.log(row(chalk3.dim("No PR created"), w));
2710
+ }
2711
+ section("Commits");
2712
+ if (state.commits.length > 0) {
2713
+ const max = 6;
2714
+ for (const c of state.commits.slice(0, max)) {
2715
+ console.log(row(c, w));
2716
+ }
2717
+ if (state.commits.length > max) {
2718
+ console.log(row(chalk3.dim(`\u2026 and ${state.commits.length - max} more`), w));
2719
+ }
2720
+ } else {
2721
+ console.log(row(chalk3.dim("No commits"), w));
2722
+ }
2723
+ section("Settings");
2724
+ renderSettings(state, w);
2725
+ if (hint) {
2726
+ section("Hint");
2727
+ console.log(row(chalk3.yellow(hint), w));
2728
+ }
2729
+ console.log(divRow(w));
2730
+ for (const line of formatCommandBar(actions, w)) {
2731
+ console.log(row(line, w));
2732
+ }
2733
+ console.log(botRow(w));
2734
+ }
2735
+ function clearScreen() {
2736
+ process.stdout.write("\x1B[2J\x1B[0f");
2737
+ }
2738
+
2739
+ // src/commands/tui.ts
2740
+ var CANCEL = /* @__PURE__ */ Symbol("cancel");
2741
+ async function confirm(message) {
2742
+ const { ok } = await inquirer6.prompt([
2743
+ {
2744
+ type: "confirm",
2745
+ name: "ok",
2746
+ message,
2747
+ default: true
2748
+ }
2749
+ ]);
2750
+ return ok;
2751
+ }
2752
+ async function loadState() {
2753
+ clearScreen();
2754
+ renderModal("Loading workflow state...");
2755
+ const state = await aggregateState();
2756
+ return state;
2757
+ }
2758
+ async function waitForKey(validKeys) {
2759
+ return new Promise((resolve) => {
2760
+ const { stdin } = process;
2761
+ const wasRaw = stdin.isRaw;
2762
+ stdin.setRawMode(true);
2763
+ stdin.resume();
2764
+ stdin.setEncoding("utf8");
2765
+ const onData = (key) => {
2766
+ if (key === "") {
2767
+ stdin.setRawMode(wasRaw ?? false);
2768
+ stdin.pause();
2769
+ stdin.removeListener("data", onData);
2770
+ resolve("q");
2771
+ return;
2772
+ }
2773
+ if (validKeys.includes(key)) {
2774
+ stdin.setRawMode(wasRaw ?? false);
2775
+ stdin.pause();
2776
+ stdin.removeListener("data", onData);
2777
+ resolve(key);
2778
+ }
2779
+ };
2780
+ stdin.on("data", onData);
2781
+ });
2782
+ }
2783
+ async function executeAction(actionId, state, providerSetter) {
2784
+ switch (actionId) {
2785
+ case "quit":
2786
+ return false;
2787
+ case "list":
2788
+ 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
+ try {
2796
+ await runCommand(void 0, { auto: true });
2797
+ } catch (error) {
2798
+ logger.error(`Run failed: ${error}`);
2799
+ await promptContinue();
2800
+ }
2801
+ return false;
2802
+ }
2803
+ case "create": {
2804
+ clearScreen();
2805
+ const { description } = await inquirer6.prompt([
2806
+ {
2807
+ type: "input",
2808
+ name: "description",
2809
+ message: "Describe the ticket (empty to cancel):"
2810
+ }
2811
+ ]);
2812
+ if (!description.trim()) {
2813
+ logger.info("Cancelled");
2814
+ return true;
2815
+ }
2816
+ try {
2817
+ await createCommand(description, {});
2818
+ } catch (error) {
2819
+ logger.error(`Create failed: ${error}`);
2820
+ }
2821
+ await promptContinue();
2822
+ return true;
2823
+ }
2824
+ case "commit":
2825
+ clearScreen();
2826
+ await handleCommit(state);
2827
+ await promptContinue();
2828
+ return true;
2829
+ case "push":
2830
+ clearScreen();
2831
+ await handlePush();
2832
+ await promptContinue();
2833
+ return true;
2834
+ case "pr": {
2835
+ clearScreen();
2836
+ if (!await confirm("Create a pull request?")) return true;
2837
+ await prCommand({});
2838
+ await promptContinue();
2839
+ return true;
2840
+ }
2841
+ case "implement": {
2842
+ clearScreen();
2843
+ const hasCommits = state.commits.length > 0;
2844
+ const hasFeedback = state.hasActionableFeedback;
2845
+ let msg;
2846
+ if (hasFeedback && hasCommits) {
2847
+ msg = "Start AI agent to address review feedback?";
2848
+ } else if (hasCommits) {
2849
+ msg = "Start AI agent to continue implementation from existing commits?";
2850
+ } else {
2851
+ msg = "Start AI agent to implement this ticket from scratch?";
2852
+ }
2853
+ if (!await confirm(msg)) return true;
2854
+ await handleImplement(state);
2855
+ return false;
2856
+ }
2857
+ case "video": {
2858
+ clearScreen();
2859
+ if (!await confirm("Record video of UI changes?")) return true;
2860
+ await handleVideoCapture(state);
2861
+ await promptContinue();
2862
+ return true;
2863
+ }
2864
+ case "switch-provider":
2865
+ clearScreen();
2866
+ await handleSwitchProvider(state, providerSetter);
2867
+ return true;
2868
+ case "checkout-main": {
2869
+ clearScreen();
2870
+ if (!await confirm("Switch to main branch?")) return true;
2871
+ await handleCheckoutMain();
2872
+ return true;
2873
+ }
2874
+ default:
2875
+ return true;
2876
+ }
2877
+ }
2878
+ async function handleCommit(state) {
2879
+ try {
2880
+ const { stdout: status } = await execa4("git", ["status", "--short"]);
2881
+ if (!status.trim()) {
2882
+ logger.info("No changes to commit");
2883
+ return;
2884
+ }
2885
+ logger.info("Changes:");
2886
+ console.log(status);
2887
+ console.log();
2888
+ await execa4("git", ["add", "-A"]);
2889
+ const { stdout: diffStat } = await execa4("git", ["diff", "--cached", "--stat"]);
2890
+ const { stdout: diffPatch } = await execa4("git", ["diff", "--cached"]);
2891
+ const diffContent = (diffStat + "\n\n" + diffPatch).slice(0, 4e3);
2892
+ const issueNumber = state.issue?.number ?? null;
2893
+ const issueTitle = state.issue?.title ?? null;
2894
+ const provider = state.config.ai.provider;
2895
+ const providerName = getProviderDisplayName(provider);
2896
+ logger.info(`Generating commit message with ${providerName}...`);
2897
+ const message = await generateCommitMessage(diffContent, issueNumber, issueTitle, state);
2898
+ if (message === CANCEL) {
2899
+ await execa4("git", ["reset", "HEAD"]);
2900
+ logger.info("Cancelled");
2901
+ return;
2902
+ }
2903
+ console.log();
2904
+ logger.info(`Message: ${message}`);
2905
+ console.log();
2906
+ if (!await confirm("Commit with this message?")) {
2907
+ await execa4("git", ["reset", "HEAD"]);
2908
+ logger.info("Commit cancelled");
2909
+ return;
2910
+ }
2911
+ const providerEmail = getProviderEmail(provider);
2912
+ const fullMessage = `${message}
2913
+
2914
+ Co-Authored-By: ${providerName} <${providerEmail}>`;
2915
+ const spinner = createSpinner("Committing...");
2916
+ spinner.start();
2917
+ await execa4("git", ["commit", "-m", fullMessage]);
2918
+ spinner.succeed("Changes committed");
2919
+ } catch (error) {
2920
+ logger.error(`Commit failed: ${error}`);
2921
+ }
2922
+ }
2923
+ async function generateCommitMessage(diffContent, issueNumber, issueTitle, state) {
2924
+ try {
2925
+ const prompt = buildCommitMessagePrompt(diffContent, issueNumber, issueTitle);
2926
+ const result = await invokeAI({ prompt, streamOutput: true }, state.config);
2927
+ let message = result.output.trim().split("\n")[0].trim();
2928
+ for (const q of ['"', "'", "`"]) {
2929
+ if (message.startsWith(q) && message.endsWith(q)) {
2930
+ message = message.slice(1, -1);
2931
+ break;
2932
+ }
2933
+ }
2934
+ message = message.replace(/^```\w*\s*/, "").replace(/\s*```$/, "");
2935
+ return message;
2936
+ } catch {
2937
+ logger.warning("AI commit message generation failed");
2938
+ console.log();
2939
+ const { message } = await inquirer6.prompt([
2940
+ {
2941
+ type: "input",
2942
+ name: "message",
2943
+ message: "Commit message (empty to cancel):"
2944
+ }
2945
+ ]);
2946
+ return message.trim() || CANCEL;
2947
+ }
2948
+ }
2949
+ async function handleImplement(state) {
2950
+ if (!state.issue) {
2951
+ logger.error("No linked issue found");
2952
+ return;
2953
+ }
2954
+ const agentInstructions = loadAgentInstructions();
2955
+ const progressContent = readProgress(state.config);
2956
+ const contextParts = [];
2957
+ if (state.commits.length > 0) {
2958
+ contextParts.push(
2959
+ `## Current Progress
2960
+ There are ${state.commits.length} existing commit(s) on this branch:
2961
+ ${state.commits.map((c) => `- ${c}`).join("\n")}
2962
+
2963
+ Continue the implementation from where it left off. Review the existing work and complete any remaining tasks.`
2964
+ );
2965
+ }
2966
+ if (state.hasActionableFeedback && state.reviewFeedback.length > 0) {
2967
+ const feedbackLines = state.reviewFeedback.map((f) => `- [${f.source}] ${f.body.slice(0, 200)}`).join("\n");
2968
+ contextParts.push(`## Review Feedback
2969
+ ${feedbackLines}`);
2970
+ }
2971
+ const extraContext = contextParts.length > 0 ? contextParts.join("\n\n") : null;
2972
+ const prompt = buildImplementationPrompt(
2973
+ state.issue,
2974
+ agentInstructions,
2975
+ progressContent,
2976
+ state.config,
2977
+ extraContext
2978
+ );
2979
+ const providerName = getProviderDisplayName(state.config.ai.provider);
2980
+ clearScreen();
2981
+ renderActionPanel(`${providerName} Session`, [
2982
+ `Implementing: #${state.issue.number} ${state.issue.title}`,
2983
+ state.commits.length > 0 ? `Continuing from ${state.commits.length} existing commit(s)` : "Starting fresh implementation",
2984
+ ...state.hasActionableFeedback ? [`Includes ${state.reviewFeedback.length} review feedback item(s)`] : []
2985
+ ]);
2986
+ console.log();
2987
+ try {
2988
+ await invokeAIInteractive(prompt, state.config);
2989
+ } catch (error) {
2990
+ logger.error(`${providerName} session failed: ${error}`);
2991
+ }
2992
+ }
2993
+ async function handlePush() {
2994
+ try {
2995
+ const { stdout: branch } = await execa4("git", ["branch", "--show-current"]);
2996
+ if (!await confirm(`Push ${branch.trim()} to remote?`)) return;
2997
+ const spinner = createSpinner("Pushing...");
2998
+ spinner.start();
2999
+ await execa4("git", ["push", "-u", "origin", branch.trim()]);
3000
+ spinner.succeed("Pushed to remote");
3001
+ } catch (error) {
3002
+ logger.error(`Push failed: ${error}`);
3003
+ }
3004
+ }
3005
+ var PROVIDERS = ["claude", "gemini", "codex"];
3006
+ async function handleSwitchProvider(state, setProvider) {
3007
+ const current = state.config.ai.provider;
3008
+ const { provider } = await inquirer6.prompt([
3009
+ {
3010
+ type: "list",
3011
+ name: "provider",
3012
+ message: "Select AI provider:",
3013
+ choices: PROVIDERS.map((p) => ({
3014
+ name: p.charAt(0).toUpperCase() + p.slice(1) + (p === current ? " (current)" : ""),
3015
+ value: p
3016
+ })),
3017
+ default: current
3018
+ }
3019
+ ]);
3020
+ if (provider === current) return;
3021
+ setProvider(provider);
3022
+ logger.success(`Provider switched to ${provider} (session only)`);
3023
+ }
3024
+ async function handleCheckoutMain() {
3025
+ try {
3026
+ const spinner = createSpinner("Switching to main...");
3027
+ spinner.start();
3028
+ await execa4("git", ["checkout", "main"]);
3029
+ await execa4("git", ["pull"]);
3030
+ spinner.succeed("Switched to main");
3031
+ } catch (error) {
3032
+ logger.error(`Checkout failed: ${error}`);
3033
+ }
3034
+ }
3035
+ async function handleVideoCapture(state) {
3036
+ if (!state.issue) {
3037
+ logger.error("No linked issue found");
3038
+ return;
3039
+ }
3040
+ const providerName = getProviderDisplayName(state.config.ai.provider);
3041
+ clearScreen();
3042
+ renderActionPanel("Video Capture", [
3043
+ `Recording: #${state.issue.number} ${state.issue.title}`,
3044
+ `Provider: ${providerName}`
3045
+ ]);
3046
+ console.log();
3047
+ try {
3048
+ const agentInstructions = loadAgentInstructions();
3049
+ const videoPrompt = buildVideoPrompt(
3050
+ state.issue.number,
3051
+ state.issue.title,
3052
+ state.config.video,
3053
+ agentInstructions
3054
+ );
3055
+ await invokeAIInteractive(videoPrompt, state.config);
3056
+ logger.success("Video capture completed");
3057
+ } catch (error) {
3058
+ logger.error(`Video capture failed: ${error}`);
3059
+ }
3060
+ }
3061
+ async function promptContinue() {
3062
+ console.log();
3063
+ await inquirer6.prompt([
3064
+ {
3065
+ type: "input",
3066
+ name: "continue",
3067
+ message: "Press Enter to continue..."
3068
+ }
3069
+ ]);
3070
+ }
3071
+ async function tuiCommand() {
3072
+ let running = true;
3073
+ let providerOverride = null;
3074
+ const setProvider = (p) => {
3075
+ providerOverride = p;
3076
+ };
3077
+ while (running) {
3078
+ const state = await loadState();
3079
+ if (providerOverride) {
3080
+ state.config.ai.provider = providerOverride;
3081
+ }
3082
+ clearScreen();
3083
+ const actions = getAvailableActions(state);
3084
+ let hint;
3085
+ if (state.isOnMain) {
3086
+ hint = "Select an action to get started";
3087
+ } else if (state.hasUncommittedChanges && !state.pr) {
3088
+ hint = "Commit your changes before creating a PR";
3089
+ } else if (state.hasActionableFeedback) {
3090
+ hint = "Review feedback needs attention";
3091
+ }
3092
+ renderDashboard(state, actions, hint);
3093
+ const validKeys = actions.map((a) => a.shortcut);
3094
+ const key = await waitForKey(validKeys);
3095
+ const action = actions.find((a) => a.shortcut === key);
3096
+ if (action) {
3097
+ running = await executeAction(action.id, state, setProvider);
3098
+ }
3099
+ }
3100
+ }
3101
+
2248
3102
  // src/index.ts
2249
3103
  var version = getVersion();
2250
3104
  function startVersionCheck() {
@@ -2295,5 +3149,11 @@ program.command("fix").description("Apply PR review feedback using AI").option("
2295
3149
  program.command("status").description("Show current workflow status").action(async () => {
2296
3150
  await statusCommand();
2297
3151
  });
3152
+ program.command("ui").description("Launch interactive dashboard").action(async () => {
3153
+ await tuiCommand();
3154
+ });
3155
+ program.action(async () => {
3156
+ await tuiCommand();
3157
+ });
2298
3158
  program.parse();
2299
3159
  //# sourceMappingURL=index.js.map