@rotorsoft/gent 1.17.1 → 1.19.0

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
@@ -913,6 +913,27 @@ async function getAuthorInitials() {
913
913
  }
914
914
  return "dev";
915
915
  }
916
+ async function getRepoInfo() {
917
+ try {
918
+ const { stdout } = await execa2("git", [
919
+ "config",
920
+ "--get",
921
+ "remote.origin.url"
922
+ ]);
923
+ const url = stdout.trim();
924
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/([^.]+)/);
925
+ if (sshMatch) {
926
+ return { owner: sshMatch[1], repo: sshMatch[2] };
927
+ }
928
+ const httpsMatch = url.match(/github\.com\/([^/]+)\/([^.]+)/);
929
+ if (httpsMatch) {
930
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
931
+ }
932
+ return null;
933
+ } catch {
934
+ return null;
935
+ }
936
+ }
916
937
  async function getCommitsSinceBase(base = "main") {
917
938
  try {
918
939
  const { stdout } = await execa2("git", [
@@ -2221,7 +2242,7 @@ import { homedir } from "os";
2221
2242
  // package.json
2222
2243
  var package_default = {
2223
2244
  name: "@rotorsoft/gent",
2224
- version: "1.17.1",
2245
+ version: "1.19.0",
2225
2246
  description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
2226
2247
  keywords: [
2227
2248
  "cli",
@@ -2671,7 +2692,8 @@ async function aggregateState() {
2671
2692
  reviewFeedback: [],
2672
2693
  hasActionableFeedback: false,
2673
2694
  hasUIChanges: false,
2674
- isPlaywrightAvailable: false
2695
+ isPlaywrightAvailable: false,
2696
+ hasValidRemote: false
2675
2697
  };
2676
2698
  }
2677
2699
  const config = loadConfig();
@@ -2689,11 +2711,12 @@ async function aggregateState() {
2689
2711
  };
2690
2712
  }
2691
2713
  const { isGhAuthenticated, isAIProviderAvailable } = envCache;
2692
- const [branch, isOnMain, uncommitted, baseBranch] = await Promise.all([
2714
+ const [branch, isOnMain, uncommitted, baseBranch, repoInfo] = await Promise.all([
2693
2715
  getCurrentBranch(),
2694
2716
  isOnMainBranch(),
2695
2717
  hasUncommittedChanges(),
2696
- getDefaultBranch()
2718
+ getDefaultBranch(),
2719
+ getRepoInfo()
2697
2720
  ]);
2698
2721
  const hasConfig = configExists();
2699
2722
  const hasProgress = progressExists(config);
@@ -2769,7 +2792,8 @@ async function aggregateState() {
2769
2792
  reviewFeedback,
2770
2793
  hasActionableFeedback,
2771
2794
  hasUIChanges: uiChanges,
2772
- isPlaywrightAvailable: playwrightAvailable
2795
+ isPlaywrightAvailable: playwrightAvailable,
2796
+ hasValidRemote: repoInfo !== null
2773
2797
  };
2774
2798
  }
2775
2799
 
@@ -2780,7 +2804,9 @@ function getAvailableActions(state) {
2780
2804
  actions.push({ id: "quit", label: "quit", shortcut: "q" });
2781
2805
  return actions;
2782
2806
  }
2783
- actions.push({ id: "create", label: "new", shortcut: "n" });
2807
+ if (state.hasValidRemote) {
2808
+ actions.push({ id: "create", label: "new", shortcut: "n" });
2809
+ }
2784
2810
  if (!state.isOnMain) {
2785
2811
  if (state.hasUncommittedChanges) {
2786
2812
  actions.push({ id: "commit", label: "commit", shortcut: "c" });
@@ -2788,7 +2814,7 @@ function getAvailableActions(state) {
2788
2814
  if (state.hasUnpushedCommits && state.commits.length > 0) {
2789
2815
  actions.push({ id: "push", label: "push", shortcut: "s" });
2790
2816
  }
2791
- if (!state.pr && state.commits.length > 0) {
2817
+ if (state.hasValidRemote && !state.pr && state.commits.length > 0) {
2792
2818
  actions.push({ id: "pr", label: "pr", shortcut: "p" });
2793
2819
  }
2794
2820
  if (state.issue && state.pr?.state !== "merged") {
@@ -3083,7 +3109,17 @@ function buildDashboardLines(state, actions, hint, refreshing, versionCheck) {
3083
3109
  out(row(chalk3.dim(" No commits"), w));
3084
3110
  }
3085
3111
  }
3086
- if (hint) {
3112
+ if (!state.hasValidRemote) {
3113
+ section("Hint");
3114
+ out(
3115
+ row(
3116
+ chalk3.yellow(
3117
+ "Add a GitHub remote to create tickets and pull requests"
3118
+ ),
3119
+ w
3120
+ )
3121
+ );
3122
+ } else if (hint) {
3087
3123
  section("Hint");
3088
3124
  out(row(chalk3.yellow(hint), w));
3089
3125
  }
@@ -3109,142 +3145,9 @@ function clearScreen() {
3109
3145
  }
3110
3146
 
3111
3147
  // src/tui/modal.ts
3112
- import chalk4 from "chalk";
3113
- function modalTopRow(title, w) {
3114
- const label = ` ${title} `;
3115
- const fill = w - 2 - label.length;
3116
- return chalk4.bold("\u250C") + chalk4.bold.cyan(label) + chalk4.bold("\u2500".repeat(Math.max(0, fill)) + "\u2510");
3117
- }
3118
- function modalDivRow(w) {
3119
- return chalk4.bold("\u251C" + "\u2500".repeat(w - 2) + "\u2524");
3120
- }
3121
- function modalBotRow(w) {
3122
- return chalk4.bold("\u2514" + "\u2500".repeat(w - 2) + "\u2518");
3123
- }
3124
- function modalRow(text, w) {
3125
- const inner = w - 4;
3126
- const fitted = truncateAnsi(text, inner);
3127
- const pad = Math.max(0, inner - visibleLen(fitted));
3128
- return chalk4.bold("\u2502") + " " + fitted + " ".repeat(pad) + " " + chalk4.bold("\u2502");
3129
- }
3130
- function modalEmptyRow(w) {
3131
- return modalRow("", w);
3132
- }
3133
- function buildModalFrame(title, contentLines, footerText, width) {
3134
- const lines = [];
3135
- lines.push(modalTopRow(title, width));
3136
- lines.push(modalEmptyRow(width));
3137
- for (const line of contentLines) {
3138
- lines.push(modalRow(line, width));
3139
- }
3140
- lines.push(modalEmptyRow(width));
3141
- lines.push(modalDivRow(width));
3142
- lines.push(modalRow(chalk4.dim(footerText), width));
3143
- lines.push(modalBotRow(width));
3144
- return lines;
3145
- }
3146
- function isSeparator(entry) {
3147
- return "separator" in entry;
3148
- }
3149
- function buildSelectContent(items, selectedIndex, maxWidth, currentIndex) {
3150
- const lines = [];
3151
- let selectableIdx = 0;
3152
- for (const item of items) {
3153
- if (isSeparator(item)) {
3154
- lines.push(chalk4.dim(item.separator));
3155
- } else {
3156
- const isSelected = selectableIdx === selectedIndex;
3157
- const isCurrent = currentIndex != null && selectableIdx === currentIndex;
3158
- const prefix = isSelected ? chalk4.cyan.bold("> ") : " ";
3159
- const bullet = chalk4.dim("\xB7 ");
3160
- const label = truncateAnsi(item.name, maxWidth - 4);
3161
- const styledLabel = isSelected ? chalk4.bold(label) : isCurrent ? chalk4.cyan(label) : label;
3162
- lines.push(prefix + bullet + styledLabel);
3163
- selectableIdx++;
3164
- }
3165
- }
3166
- return lines;
3167
- }
3168
- function buildConfirmContent(message, selectedYes) {
3169
- const yes = selectedYes ? chalk4.cyan.bold("> Yes") : chalk4.dim(" Yes");
3170
- const no = !selectedYes ? chalk4.cyan.bold("> No") : chalk4.dim(" No");
3171
- return [message, "", yes, no];
3172
- }
3173
- function buildInputContent(label, value, cursorVisible) {
3174
- const cursor = cursorVisible ? chalk4.cyan("_") : " ";
3175
- return [label, "", chalk4.cyan("> ") + value + cursor];
3176
- }
3177
- function buildMultilineInputContent(label, value, cursorVisible, maxWidth) {
3178
- const cursor = cursorVisible ? chalk4.cyan("_") : " ";
3179
- const lines = [label, ""];
3180
- if (value === "") {
3181
- lines.push(chalk4.cyan(" ") + cursor);
3182
- } else {
3183
- const inputLines = value.split("\n");
3184
- const contentWidth = maxWidth - 2;
3185
- for (let i = 0; i < inputLines.length; i++) {
3186
- const raw = inputLines[i];
3187
- const wrapped = wrapLine(raw, contentWidth);
3188
- for (let j = 0; j < wrapped.length; j++) {
3189
- const isLastLine = i === inputLines.length - 1 && j === wrapped.length - 1;
3190
- const text = wrapped[j] + (isLastLine ? cursor : "");
3191
- lines.push(chalk4.cyan(" ") + text);
3192
- }
3193
- }
3194
- }
3195
- return lines;
3196
- }
3197
- function wrapLine(text, width) {
3198
- if (width <= 0) return [text];
3199
- if (text.length <= width) return [text];
3200
- const result = [];
3201
- let remaining = text;
3202
- while (remaining.length > width) {
3203
- let breakAt = remaining.lastIndexOf(" ", width);
3204
- if (breakAt <= 0) breakAt = width;
3205
- result.push(remaining.slice(0, breakAt));
3206
- remaining = remaining.slice(breakAt).replace(/^ /, "");
3207
- }
3208
- if (remaining.length > 0 || result.length === 0) {
3209
- result.push(remaining);
3210
- }
3211
- return result;
3212
- }
3213
- function termSize() {
3214
- return {
3215
- cols: process.stdout.columns || 80,
3216
- rows: process.stdout.rows || 24
3217
- };
3218
- }
3219
- function moveTo(row2, col) {
3220
- return `\x1B[${row2};${col}H`;
3221
- }
3222
- function hideCursor() {
3223
- return "\x1B[?25l";
3224
- }
3225
- function showCursor() {
3226
- return "\x1B[?25h";
3227
- }
3228
- function modalWidth() {
3229
- const cols = process.stdout.columns || 80;
3230
- return Math.min(60, cols - 4);
3231
- }
3232
- function renderOverlay(dashboardLines, modalLines, mWidth) {
3233
- const { cols, rows } = termSize();
3234
- process.stdout.write("\x1B[2J\x1B[0f");
3235
- process.stdout.write(hideCursor());
3236
- for (let i = 0; i < dashboardLines.length && i < rows; i++) {
3237
- process.stdout.write(
3238
- moveTo(i + 1, 1) + chalk4.dim(stripAnsi(dashboardLines[i]))
3239
- );
3240
- }
3241
- const startRow = Math.max(1, Math.floor((rows - modalLines.length) / 2));
3242
- const startCol = Math.max(1, Math.floor((cols - mWidth) / 2));
3243
- for (let i = 0; i < modalLines.length; i++) {
3244
- process.stdout.write(moveTo(startRow + i, startCol) + modalLines[i]);
3245
- }
3246
- process.stdout.write(moveTo(startRow + modalLines.length + 1, 1));
3247
- }
3148
+ import chalk8 from "chalk";
3149
+
3150
+ // src/tui/key-reader.ts
3248
3151
  function readKey() {
3249
3152
  return new Promise((resolve) => {
3250
3153
  const { stdin } = process;
@@ -3268,6 +3171,20 @@ function readKey() {
3268
3171
  resolve({ name: "right", raw: data });
3269
3172
  } else if (data === "\x1B[D") {
3270
3173
  resolve({ name: "left", raw: data });
3174
+ } else if (data === "\x1B[1;5C" || data === "\x1B[5C") {
3175
+ resolve({ name: "ctrl-right", raw: data });
3176
+ } else if (data === "\x1B[1;5D" || data === "\x1B[5D") {
3177
+ resolve({ name: "ctrl-left", raw: data });
3178
+ } else if (data === "\x1B[3~") {
3179
+ resolve({ name: "delete", raw: data });
3180
+ } else if (data === "\x1B[H" || data === "\x1B[1~") {
3181
+ resolve({ name: "home", raw: data });
3182
+ } else if (data === "\x1B[F" || data === "\x1B[4~") {
3183
+ resolve({ name: "end", raw: data });
3184
+ } else if (data === "") {
3185
+ resolve({ name: "home", raw: data });
3186
+ } else if (data === "") {
3187
+ resolve({ name: "end", raw: data });
3271
3188
  } else if (data === "\r" || data === "\n") {
3272
3189
  resolve({ name: "enter", raw: data });
3273
3190
  } else if (data === "\x7F" || data === "\b") {
@@ -3287,39 +3204,30 @@ function readKey() {
3287
3204
  stdin.on("data", onData);
3288
3205
  });
3289
3206
  }
3290
- async function showConfirm(opts) {
3291
- const w = modalWidth();
3292
- let selectedYes = true;
3293
- const render = () => {
3294
- const content = buildConfirmContent(opts.message, selectedYes);
3295
- const footer = "\u2191\u2193 Select Enter Confirm Esc Cancel";
3296
- const lines = buildModalFrame(opts.title, content, footer, w);
3297
- renderOverlay(opts.dashboardLines, lines, w);
3298
- };
3299
- render();
3300
- while (true) {
3301
- const key = await readKey();
3302
- switch (key.name) {
3303
- case "up":
3304
- case "down":
3305
- case "tab":
3306
- selectedYes = !selectedYes;
3307
- render();
3308
- break;
3309
- case "enter":
3310
- process.stdout.write(showCursor());
3311
- return selectedYes;
3312
- case "escape":
3313
- process.stdout.write(showCursor());
3314
- return false;
3315
- case "y":
3316
- process.stdout.write(showCursor());
3317
- return true;
3318
- case "n":
3319
- process.stdout.write(showCursor());
3320
- return false;
3207
+
3208
+ // src/tui/select-dialog.ts
3209
+ import chalk4 from "chalk";
3210
+ function isSeparator(entry) {
3211
+ return "separator" in entry;
3212
+ }
3213
+ function buildSelectContent(items, selectedIndex, maxWidth, currentIndex) {
3214
+ const lines = [];
3215
+ let selectableIdx = 0;
3216
+ for (const item of items) {
3217
+ if (isSeparator(item)) {
3218
+ lines.push(chalk4.dim(item.separator));
3219
+ } else {
3220
+ const isSelected = selectableIdx === selectedIndex;
3221
+ const isCurrent = currentIndex != null && selectableIdx === currentIndex;
3222
+ const prefix = isSelected ? chalk4.cyan.bold("> ") : " ";
3223
+ const bullet = chalk4.dim("\xB7 ");
3224
+ const label = truncateAnsi(item.name, maxWidth - 4);
3225
+ const styledLabel = isSelected ? chalk4.bold(label) : isCurrent ? chalk4.cyan(label) : label;
3226
+ lines.push(prefix + bullet + styledLabel);
3227
+ selectableIdx++;
3321
3228
  }
3322
3229
  }
3230
+ return lines;
3323
3231
  }
3324
3232
  function selectableCount(items) {
3325
3233
  return items.filter((i) => !isSeparator(i)).length;
@@ -3364,6 +3272,55 @@ async function showSelect(opts) {
3364
3272
  }
3365
3273
  }
3366
3274
  }
3275
+
3276
+ // src/tui/confirm-dialog.ts
3277
+ import chalk5 from "chalk";
3278
+ function buildConfirmContent(message, selectedYes) {
3279
+ const yes = selectedYes ? chalk5.cyan.bold("> Yes") : chalk5.dim(" Yes");
3280
+ const no = !selectedYes ? chalk5.cyan.bold("> No") : chalk5.dim(" No");
3281
+ return [message, "", yes, no];
3282
+ }
3283
+ async function showConfirm(opts) {
3284
+ const w = modalWidth();
3285
+ let selectedYes = true;
3286
+ const render = () => {
3287
+ const content = buildConfirmContent(opts.message, selectedYes);
3288
+ const footer = "\u2191\u2193 Select Enter Confirm Esc Cancel";
3289
+ const lines = buildModalFrame(opts.title, content, footer, w);
3290
+ renderOverlay(opts.dashboardLines, lines, w);
3291
+ };
3292
+ render();
3293
+ while (true) {
3294
+ const key = await readKey();
3295
+ switch (key.name) {
3296
+ case "up":
3297
+ case "down":
3298
+ case "tab":
3299
+ selectedYes = !selectedYes;
3300
+ render();
3301
+ break;
3302
+ case "enter":
3303
+ process.stdout.write(showCursor());
3304
+ return selectedYes;
3305
+ case "escape":
3306
+ process.stdout.write(showCursor());
3307
+ return false;
3308
+ case "y":
3309
+ process.stdout.write(showCursor());
3310
+ return true;
3311
+ case "n":
3312
+ process.stdout.write(showCursor());
3313
+ return false;
3314
+ }
3315
+ }
3316
+ }
3317
+
3318
+ // src/tui/input-dialog.ts
3319
+ import chalk6 from "chalk";
3320
+ function buildInputContent(label, value, cursorVisible) {
3321
+ const cursorChar = cursorVisible ? chalk6.inverse(" ") : "";
3322
+ return [label, "", chalk6.cyan("> ") + value + cursorChar];
3323
+ }
3367
3324
  async function showInput(opts) {
3368
3325
  const w = modalWidth();
3369
3326
  let value = "";
@@ -3402,9 +3359,115 @@ async function showInput(opts) {
3402
3359
  }
3403
3360
  }
3404
3361
  }
3362
+
3363
+ // src/tui/multiline-input.ts
3364
+ import chalk7 from "chalk";
3365
+ function wrapLineWithMap(text, width) {
3366
+ if (width <= 0) return [{ text, offset: 0 }];
3367
+ if (text.length <= width) return [{ text, offset: 0 }];
3368
+ const result = [];
3369
+ let pos = 0;
3370
+ let remaining = text;
3371
+ while (remaining.length > width) {
3372
+ let breakAt = remaining.lastIndexOf(" ", width);
3373
+ if (breakAt <= 0) breakAt = width;
3374
+ result.push({ text: remaining.slice(0, breakAt), offset: pos });
3375
+ pos += breakAt;
3376
+ remaining = remaining.slice(breakAt);
3377
+ if (remaining.startsWith(" ")) {
3378
+ remaining = remaining.slice(1);
3379
+ pos += 1;
3380
+ }
3381
+ }
3382
+ if (remaining.length > 0 || result.length === 0) {
3383
+ result.push({ text: remaining, offset: pos });
3384
+ }
3385
+ return result;
3386
+ }
3387
+ function getVisualLines(value, contentWidth) {
3388
+ const inputLines = value.split("\n");
3389
+ const result = [];
3390
+ let globalPos = 0;
3391
+ for (let i = 0; i < inputLines.length; i++) {
3392
+ const raw = inputLines[i];
3393
+ const wrapped = wrapLineWithMap(raw, contentWidth);
3394
+ for (const seg of wrapped) {
3395
+ result.push({
3396
+ text: seg.text,
3397
+ globalOffset: globalPos + seg.offset,
3398
+ length: seg.text.length
3399
+ });
3400
+ }
3401
+ globalPos += raw.length + 1;
3402
+ }
3403
+ return result;
3404
+ }
3405
+ function findCursorVisualPos(visualLines, cursorPos) {
3406
+ for (let i = 0; i < visualLines.length; i++) {
3407
+ const vl = visualLines[i];
3408
+ if (cursorPos >= vl.globalOffset && cursorPos <= vl.globalOffset + vl.length) {
3409
+ return { row: i, col: cursorPos - vl.globalOffset };
3410
+ }
3411
+ }
3412
+ const last = visualLines[visualLines.length - 1];
3413
+ return { row: visualLines.length - 1, col: last.length };
3414
+ }
3415
+ function moveCursorVertical(value, cursorPos, contentWidth, direction) {
3416
+ const visualLines = getVisualLines(value, contentWidth);
3417
+ const { row: row2, col } = findCursorVisualPos(visualLines, cursorPos);
3418
+ const newRow = row2 + direction;
3419
+ if (newRow < 0 || newRow >= visualLines.length) return cursorPos;
3420
+ const targetLine = visualLines[newRow];
3421
+ const newCol = Math.min(col, targetLine.length);
3422
+ return targetLine.globalOffset + newCol;
3423
+ }
3424
+ function moveCursorHome(value, cursorPos, contentWidth) {
3425
+ const visualLines = getVisualLines(value, contentWidth);
3426
+ const { row: row2 } = findCursorVisualPos(visualLines, cursorPos);
3427
+ return visualLines[row2].globalOffset;
3428
+ }
3429
+ function moveCursorEnd(value, cursorPos, contentWidth) {
3430
+ const visualLines = getVisualLines(value, contentWidth);
3431
+ const { row: row2 } = findCursorVisualPos(visualLines, cursorPos);
3432
+ return visualLines[row2].globalOffset + visualLines[row2].length;
3433
+ }
3434
+ function moveCursorWordLeft(value, cursorPos) {
3435
+ if (cursorPos <= 0) return 0;
3436
+ let pos = cursorPos - 1;
3437
+ while (pos > 0 && !/\w/.test(value[pos])) pos--;
3438
+ while (pos > 0 && /\w/.test(value[pos - 1])) pos--;
3439
+ return pos;
3440
+ }
3441
+ function moveCursorWordRight(value, cursorPos) {
3442
+ if (cursorPos >= value.length) return value.length;
3443
+ let pos = cursorPos;
3444
+ while (pos < value.length && /\w/.test(value[pos])) pos++;
3445
+ while (pos < value.length && !/\w/.test(value[pos])) pos++;
3446
+ return pos;
3447
+ }
3448
+ function buildMultilineInputContent(label, value, cursorVisible, maxWidth, cursorPos) {
3449
+ const cp = cursorPos ?? value.length;
3450
+ const lines = [label, ""];
3451
+ const contentWidth = maxWidth - 2;
3452
+ const visualLines = getVisualLines(value, contentWidth);
3453
+ const { row: cursorRow, col: cursorCol } = findCursorVisualPos(visualLines, cp);
3454
+ for (let i = 0; i < visualLines.length; i++) {
3455
+ const text = visualLines[i].text;
3456
+ if (i === cursorRow && cursorVisible) {
3457
+ const charUnderCursor = cursorCol < text.length ? text[cursorCol] : " ";
3458
+ lines.push(
3459
+ chalk7.cyan(" ") + text.slice(0, cursorCol) + chalk7.inverse(charUnderCursor) + text.slice(cursorCol + 1)
3460
+ );
3461
+ } else {
3462
+ lines.push(chalk7.cyan(" ") + text);
3463
+ }
3464
+ }
3465
+ return lines;
3466
+ }
3405
3467
  async function showMultilineInput(opts) {
3406
3468
  const w = modalWidth();
3407
3469
  let value = "";
3470
+ let cursorPos = 0;
3408
3471
  let cursorBlink = true;
3409
3472
  const contentWidth = w - 6;
3410
3473
  const render = () => {
@@ -3412,7 +3475,8 @@ async function showMultilineInput(opts) {
3412
3475
  opts.label,
3413
3476
  value,
3414
3477
  cursorBlink,
3415
- contentWidth
3478
+ contentWidth,
3479
+ cursorPos
3416
3480
  );
3417
3481
  const footer = "Enter Newline Ctrl+S Submit Esc Cancel";
3418
3482
  const lines = buildModalFrame(opts.title, content, footer, w);
@@ -3426,7 +3490,8 @@ async function showMultilineInput(opts) {
3426
3490
  process.stdout.write(showCursor());
3427
3491
  return value.trim() || null;
3428
3492
  case "enter":
3429
- value += "\n";
3493
+ value = value.slice(0, cursorPos) + "\n" + value.slice(cursorPos);
3494
+ cursorPos++;
3430
3495
  cursorBlink = true;
3431
3496
  render();
3432
3497
  break;
@@ -3434,14 +3499,54 @@ async function showMultilineInput(opts) {
3434
3499
  process.stdout.write(showCursor());
3435
3500
  return null;
3436
3501
  case "backspace":
3437
- if (value.length > 0) {
3438
- value = value.slice(0, -1);
3502
+ if (cursorPos > 0) {
3503
+ value = value.slice(0, cursorPos - 1) + value.slice(cursorPos);
3504
+ cursorPos--;
3505
+ }
3506
+ render();
3507
+ break;
3508
+ case "delete":
3509
+ if (cursorPos < value.length) {
3510
+ value = value.slice(0, cursorPos) + value.slice(cursorPos + 1);
3439
3511
  }
3440
3512
  render();
3441
3513
  break;
3514
+ case "left":
3515
+ if (cursorPos > 0) cursorPos--;
3516
+ render();
3517
+ break;
3518
+ case "right":
3519
+ if (cursorPos < value.length) cursorPos++;
3520
+ render();
3521
+ break;
3522
+ case "up":
3523
+ cursorPos = moveCursorVertical(value, cursorPos, contentWidth - 2, -1);
3524
+ render();
3525
+ break;
3526
+ case "down":
3527
+ cursorPos = moveCursorVertical(value, cursorPos, contentWidth - 2, 1);
3528
+ render();
3529
+ break;
3530
+ case "home":
3531
+ cursorPos = moveCursorHome(value, cursorPos, contentWidth - 2);
3532
+ render();
3533
+ break;
3534
+ case "end":
3535
+ cursorPos = moveCursorEnd(value, cursorPos, contentWidth - 2);
3536
+ render();
3537
+ break;
3538
+ case "ctrl-left":
3539
+ cursorPos = moveCursorWordLeft(value, cursorPos);
3540
+ render();
3541
+ break;
3542
+ case "ctrl-right":
3543
+ cursorPos = moveCursorWordRight(value, cursorPos);
3544
+ render();
3545
+ break;
3442
3546
  default:
3443
3547
  if (key.raw.length === 1 && key.raw.charCodeAt(0) >= 32) {
3444
- value += key.raw;
3548
+ value = value.slice(0, cursorPos) + key.raw + value.slice(cursorPos);
3549
+ cursorPos++;
3445
3550
  cursorBlink = true;
3446
3551
  render();
3447
3552
  }
@@ -3449,6 +3554,76 @@ async function showMultilineInput(opts) {
3449
3554
  }
3450
3555
  }
3451
3556
  }
3557
+
3558
+ // src/tui/modal.ts
3559
+ function modalTopRow(title, w) {
3560
+ const label = ` ${title} `;
3561
+ const fill = w - 2 - label.length;
3562
+ return chalk8.bold("\u250C") + chalk8.bold.cyan(label) + chalk8.bold("\u2500".repeat(Math.max(0, fill)) + "\u2510");
3563
+ }
3564
+ function modalDivRow(w) {
3565
+ return chalk8.bold("\u251C" + "\u2500".repeat(w - 2) + "\u2524");
3566
+ }
3567
+ function modalBotRow(w) {
3568
+ return chalk8.bold("\u2514" + "\u2500".repeat(w - 2) + "\u2518");
3569
+ }
3570
+ function modalRow(text, w) {
3571
+ const inner = w - 4;
3572
+ const fitted = truncateAnsi(text, inner);
3573
+ const pad = Math.max(0, inner - visibleLen(fitted));
3574
+ return chalk8.bold("\u2502") + " " + fitted + " ".repeat(pad) + " " + chalk8.bold("\u2502");
3575
+ }
3576
+ function modalEmptyRow(w) {
3577
+ return modalRow("", w);
3578
+ }
3579
+ function buildModalFrame(title, contentLines, footerText, width) {
3580
+ const lines = [];
3581
+ lines.push(modalTopRow(title, width));
3582
+ lines.push(modalEmptyRow(width));
3583
+ for (const line of contentLines) {
3584
+ lines.push(modalRow(line, width));
3585
+ }
3586
+ lines.push(modalEmptyRow(width));
3587
+ lines.push(modalDivRow(width));
3588
+ lines.push(modalRow(chalk8.dim(footerText), width));
3589
+ lines.push(modalBotRow(width));
3590
+ return lines;
3591
+ }
3592
+ function termSize() {
3593
+ return {
3594
+ cols: process.stdout.columns || 80,
3595
+ rows: process.stdout.rows || 24
3596
+ };
3597
+ }
3598
+ function moveTo(row2, col) {
3599
+ return `\x1B[${row2};${col}H`;
3600
+ }
3601
+ function hideCursor() {
3602
+ return "\x1B[?25l";
3603
+ }
3604
+ function showCursor() {
3605
+ return "\x1B[?25h";
3606
+ }
3607
+ function modalWidth() {
3608
+ const cols = process.stdout.columns || 80;
3609
+ return Math.min(60, cols - 4);
3610
+ }
3611
+ function renderOverlay(dashboardLines, modalLines, mWidth) {
3612
+ const { cols, rows } = termSize();
3613
+ process.stdout.write("\x1B[2J\x1B[0f");
3614
+ process.stdout.write(hideCursor());
3615
+ for (let i = 0; i < dashboardLines.length && i < rows; i++) {
3616
+ process.stdout.write(
3617
+ moveTo(i + 1, 1) + chalk8.dim(stripAnsi(dashboardLines[i]))
3618
+ );
3619
+ }
3620
+ const startRow = Math.max(1, Math.floor((rows - modalLines.length) / 2));
3621
+ const startCol = Math.max(1, Math.floor((cols - mWidth) / 2));
3622
+ for (let i = 0; i < modalLines.length; i++) {
3623
+ process.stdout.write(moveTo(startRow + i, startCol) + modalLines[i]);
3624
+ }
3625
+ process.stdout.write(moveTo(startRow + modalLines.length + 1, 1));
3626
+ }
3452
3627
  function showStatus(title, message, dashboardLines) {
3453
3628
  const w = modalWidth();
3454
3629
  const content = [message];
@@ -3884,7 +4059,8 @@ async function tuiCommand() {
3884
4059
  reviewFeedback: [],
3885
4060
  hasActionableFeedback: false,
3886
4061
  hasUIChanges: false,
3887
- isPlaywrightAvailable: false
4062
+ isPlaywrightAvailable: false,
4063
+ hasValidRemote: true
3888
4064
  };
3889
4065
  let needsRefresh = true;
3890
4066
  let isFirstLoad = true;