@rotorsoft/gent 1.17.0 → 1.18.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
@@ -2221,7 +2221,7 @@ import { homedir } from "os";
2221
2221
  // package.json
2222
2222
  var package_default = {
2223
2223
  name: "@rotorsoft/gent",
2224
- version: "1.17.0",
2224
+ version: "1.18.0",
2225
2225
  description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
2226
2226
  keywords: [
2227
2227
  "cli",
@@ -2786,13 +2786,13 @@ function getAvailableActions(state) {
2786
2786
  actions.push({ id: "commit", label: "commit", shortcut: "c" });
2787
2787
  }
2788
2788
  if (state.hasUnpushedCommits && state.commits.length > 0) {
2789
- actions.push({ id: "push", label: "push", shortcut: "p" });
2789
+ actions.push({ id: "push", label: "push", shortcut: "s" });
2790
2790
  }
2791
2791
  if (!state.pr && state.commits.length > 0) {
2792
- actions.push({ id: "pr", label: "pr", shortcut: "r" });
2792
+ actions.push({ id: "pr", label: "pr", shortcut: "p" });
2793
2793
  }
2794
2794
  if (state.issue && state.pr?.state !== "merged") {
2795
- actions.push({ id: "run", label: "run", shortcut: "u" });
2795
+ actions.push({ id: "run", label: "run", shortcut: "r" });
2796
2796
  }
2797
2797
  }
2798
2798
  actions.push({ id: "list", label: "list", shortcut: "l" });
@@ -2907,14 +2907,14 @@ var shortcutColors = [
2907
2907
  chalk3.red.bold
2908
2908
  ];
2909
2909
  function formatAction(a, color) {
2910
- const key = a.shortcut;
2911
- const idx = a.label.indexOf(key);
2910
+ const idx = a.label.indexOf(a.shortcut);
2911
+ const styledKey = color(chalk3.underline(a.shortcut));
2912
2912
  if (idx >= 0) {
2913
2913
  const before = a.label.slice(0, idx);
2914
- const after = a.label.slice(idx + key.length);
2915
- return chalk3.dim(before) + color(key) + chalk3.dim(after);
2914
+ const after = a.label.slice(idx + a.shortcut.length);
2915
+ return chalk3.dim(before) + styledKey + chalk3.dim(after);
2916
2916
  }
2917
- return color(key) + " " + chalk3.dim(a.label);
2917
+ return styledKey + " " + chalk3.dim(a.label);
2918
2918
  }
2919
2919
  function formatCommandBar(actions, w) {
2920
2920
  const parts = actions.map((a, i) => {
@@ -2971,14 +2971,22 @@ function buildDashboardLines(state, actions, hint, refreshing, versionCheck) {
2971
2971
  out(topRow(titleLabel, w));
2972
2972
  renderSettingsTo(state, w, out, versionCheck);
2973
2973
  if (!state.isGitRepo) {
2974
- out(row(chalk3.red("Not a git repository"), w));
2975
- out(row(chalk3.dim("Run gent init in a git repo to get started"), w));
2974
+ out(row(chalk3.yellow("Not a git repository"), w));
2975
+ out(row(chalk3.dim("Navigate to a git repository to get started"), w));
2976
+ out(divRow(w));
2977
+ for (const line of formatCommandBar(actions, w)) {
2978
+ out(row(line, w));
2979
+ }
2976
2980
  out(botRow(w));
2977
2981
  return lines;
2978
2982
  }
2979
2983
  if (!state.isGhAuthenticated) {
2980
2984
  out(row(chalk3.red("GitHub CLI not authenticated"), w));
2981
2985
  out(row(chalk3.dim("Run: gh auth login"), w));
2986
+ out(divRow(w));
2987
+ for (const line of formatCommandBar(actions, w)) {
2988
+ out(row(line, w));
2989
+ }
2982
2990
  out(botRow(w));
2983
2991
  return lines;
2984
2992
  }
@@ -3101,142 +3109,9 @@ function clearScreen() {
3101
3109
  }
3102
3110
 
3103
3111
  // src/tui/modal.ts
3104
- import chalk4 from "chalk";
3105
- function modalTopRow(title, w) {
3106
- const label = ` ${title} `;
3107
- const fill = w - 2 - label.length;
3108
- return chalk4.bold("\u250C") + chalk4.bold.cyan(label) + chalk4.bold("\u2500".repeat(Math.max(0, fill)) + "\u2510");
3109
- }
3110
- function modalDivRow(w) {
3111
- return chalk4.bold("\u251C" + "\u2500".repeat(w - 2) + "\u2524");
3112
- }
3113
- function modalBotRow(w) {
3114
- return chalk4.bold("\u2514" + "\u2500".repeat(w - 2) + "\u2518");
3115
- }
3116
- function modalRow(text, w) {
3117
- const inner = w - 4;
3118
- const fitted = truncateAnsi(text, inner);
3119
- const pad = Math.max(0, inner - visibleLen(fitted));
3120
- return chalk4.bold("\u2502") + " " + fitted + " ".repeat(pad) + " " + chalk4.bold("\u2502");
3121
- }
3122
- function modalEmptyRow(w) {
3123
- return modalRow("", w);
3124
- }
3125
- function buildModalFrame(title, contentLines, footerText, width) {
3126
- const lines = [];
3127
- lines.push(modalTopRow(title, width));
3128
- lines.push(modalEmptyRow(width));
3129
- for (const line of contentLines) {
3130
- lines.push(modalRow(line, width));
3131
- }
3132
- lines.push(modalEmptyRow(width));
3133
- lines.push(modalDivRow(width));
3134
- lines.push(modalRow(chalk4.dim(footerText), width));
3135
- lines.push(modalBotRow(width));
3136
- return lines;
3137
- }
3138
- function isSeparator(entry) {
3139
- return "separator" in entry;
3140
- }
3141
- function buildSelectContent(items, selectedIndex, maxWidth, currentIndex) {
3142
- const lines = [];
3143
- let selectableIdx = 0;
3144
- for (const item of items) {
3145
- if (isSeparator(item)) {
3146
- lines.push(chalk4.dim(item.separator));
3147
- } else {
3148
- const isSelected = selectableIdx === selectedIndex;
3149
- const isCurrent = currentIndex != null && selectableIdx === currentIndex;
3150
- const prefix = isSelected ? chalk4.cyan.bold("> ") : " ";
3151
- const bullet = chalk4.dim("\xB7 ");
3152
- const label = truncateAnsi(item.name, maxWidth - 4);
3153
- const styledLabel = isSelected ? chalk4.bold(label) : isCurrent ? chalk4.cyan(label) : label;
3154
- lines.push(prefix + bullet + styledLabel);
3155
- selectableIdx++;
3156
- }
3157
- }
3158
- return lines;
3159
- }
3160
- function buildConfirmContent(message, selectedYes) {
3161
- const yes = selectedYes ? chalk4.cyan.bold("> Yes") : chalk4.dim(" Yes");
3162
- const no = !selectedYes ? chalk4.cyan.bold("> No") : chalk4.dim(" No");
3163
- return [message, "", yes, no];
3164
- }
3165
- function buildInputContent(label, value, cursorVisible) {
3166
- const cursor = cursorVisible ? chalk4.cyan("_") : " ";
3167
- return [label, "", chalk4.cyan("> ") + value + cursor];
3168
- }
3169
- function buildMultilineInputContent(label, value, cursorVisible, maxWidth) {
3170
- const cursor = cursorVisible ? chalk4.cyan("_") : " ";
3171
- const lines = [label, ""];
3172
- if (value === "") {
3173
- lines.push(chalk4.cyan(" ") + cursor);
3174
- } else {
3175
- const inputLines = value.split("\n");
3176
- const contentWidth = maxWidth - 2;
3177
- for (let i = 0; i < inputLines.length; i++) {
3178
- const raw = inputLines[i];
3179
- const wrapped = wrapLine(raw, contentWidth);
3180
- for (let j = 0; j < wrapped.length; j++) {
3181
- const isLastLine = i === inputLines.length - 1 && j === wrapped.length - 1;
3182
- const text = wrapped[j] + (isLastLine ? cursor : "");
3183
- lines.push(chalk4.cyan(" ") + text);
3184
- }
3185
- }
3186
- }
3187
- return lines;
3188
- }
3189
- function wrapLine(text, width) {
3190
- if (width <= 0) return [text];
3191
- if (text.length <= width) return [text];
3192
- const result = [];
3193
- let remaining = text;
3194
- while (remaining.length > width) {
3195
- let breakAt = remaining.lastIndexOf(" ", width);
3196
- if (breakAt <= 0) breakAt = width;
3197
- result.push(remaining.slice(0, breakAt));
3198
- remaining = remaining.slice(breakAt).replace(/^ /, "");
3199
- }
3200
- if (remaining.length > 0 || result.length === 0) {
3201
- result.push(remaining);
3202
- }
3203
- return result;
3204
- }
3205
- function termSize() {
3206
- return {
3207
- cols: process.stdout.columns || 80,
3208
- rows: process.stdout.rows || 24
3209
- };
3210
- }
3211
- function moveTo(row2, col) {
3212
- return `\x1B[${row2};${col}H`;
3213
- }
3214
- function hideCursor() {
3215
- return "\x1B[?25l";
3216
- }
3217
- function showCursor() {
3218
- return "\x1B[?25h";
3219
- }
3220
- function modalWidth() {
3221
- const cols = process.stdout.columns || 80;
3222
- return Math.min(60, cols - 4);
3223
- }
3224
- function renderOverlay(dashboardLines, modalLines, mWidth) {
3225
- const { cols, rows } = termSize();
3226
- process.stdout.write("\x1B[2J\x1B[0f");
3227
- process.stdout.write(hideCursor());
3228
- for (let i = 0; i < dashboardLines.length && i < rows; i++) {
3229
- process.stdout.write(
3230
- moveTo(i + 1, 1) + chalk4.dim(stripAnsi(dashboardLines[i]))
3231
- );
3232
- }
3233
- const startRow = Math.max(1, Math.floor((rows - modalLines.length) / 2));
3234
- const startCol = Math.max(1, Math.floor((cols - mWidth) / 2));
3235
- for (let i = 0; i < modalLines.length; i++) {
3236
- process.stdout.write(moveTo(startRow + i, startCol) + modalLines[i]);
3237
- }
3238
- process.stdout.write(moveTo(startRow + modalLines.length + 1, 1));
3239
- }
3112
+ import chalk8 from "chalk";
3113
+
3114
+ // src/tui/key-reader.ts
3240
3115
  function readKey() {
3241
3116
  return new Promise((resolve) => {
3242
3117
  const { stdin } = process;
@@ -3260,6 +3135,20 @@ function readKey() {
3260
3135
  resolve({ name: "right", raw: data });
3261
3136
  } else if (data === "\x1B[D") {
3262
3137
  resolve({ name: "left", raw: data });
3138
+ } else if (data === "\x1B[1;5C" || data === "\x1B[5C") {
3139
+ resolve({ name: "ctrl-right", raw: data });
3140
+ } else if (data === "\x1B[1;5D" || data === "\x1B[5D") {
3141
+ resolve({ name: "ctrl-left", raw: data });
3142
+ } else if (data === "\x1B[3~") {
3143
+ resolve({ name: "delete", raw: data });
3144
+ } else if (data === "\x1B[H" || data === "\x1B[1~") {
3145
+ resolve({ name: "home", raw: data });
3146
+ } else if (data === "\x1B[F" || data === "\x1B[4~") {
3147
+ resolve({ name: "end", raw: data });
3148
+ } else if (data === "") {
3149
+ resolve({ name: "home", raw: data });
3150
+ } else if (data === "") {
3151
+ resolve({ name: "end", raw: data });
3263
3152
  } else if (data === "\r" || data === "\n") {
3264
3153
  resolve({ name: "enter", raw: data });
3265
3154
  } else if (data === "\x7F" || data === "\b") {
@@ -3279,39 +3168,30 @@ function readKey() {
3279
3168
  stdin.on("data", onData);
3280
3169
  });
3281
3170
  }
3282
- async function showConfirm(opts) {
3283
- const w = modalWidth();
3284
- let selectedYes = true;
3285
- const render = () => {
3286
- const content = buildConfirmContent(opts.message, selectedYes);
3287
- const footer = "\u2191\u2193 Select Enter Confirm Esc Cancel";
3288
- const lines = buildModalFrame(opts.title, content, footer, w);
3289
- renderOverlay(opts.dashboardLines, lines, w);
3290
- };
3291
- render();
3292
- while (true) {
3293
- const key = await readKey();
3294
- switch (key.name) {
3295
- case "up":
3296
- case "down":
3297
- case "tab":
3298
- selectedYes = !selectedYes;
3299
- render();
3300
- break;
3301
- case "enter":
3302
- process.stdout.write(showCursor());
3303
- return selectedYes;
3304
- case "escape":
3305
- process.stdout.write(showCursor());
3306
- return false;
3307
- case "y":
3308
- process.stdout.write(showCursor());
3309
- return true;
3310
- case "n":
3311
- process.stdout.write(showCursor());
3312
- return false;
3171
+
3172
+ // src/tui/select-dialog.ts
3173
+ import chalk4 from "chalk";
3174
+ function isSeparator(entry) {
3175
+ return "separator" in entry;
3176
+ }
3177
+ function buildSelectContent(items, selectedIndex, maxWidth, currentIndex) {
3178
+ const lines = [];
3179
+ let selectableIdx = 0;
3180
+ for (const item of items) {
3181
+ if (isSeparator(item)) {
3182
+ lines.push(chalk4.dim(item.separator));
3183
+ } else {
3184
+ const isSelected = selectableIdx === selectedIndex;
3185
+ const isCurrent = currentIndex != null && selectableIdx === currentIndex;
3186
+ const prefix = isSelected ? chalk4.cyan.bold("> ") : " ";
3187
+ const bullet = chalk4.dim("\xB7 ");
3188
+ const label = truncateAnsi(item.name, maxWidth - 4);
3189
+ const styledLabel = isSelected ? chalk4.bold(label) : isCurrent ? chalk4.cyan(label) : label;
3190
+ lines.push(prefix + bullet + styledLabel);
3191
+ selectableIdx++;
3313
3192
  }
3314
3193
  }
3194
+ return lines;
3315
3195
  }
3316
3196
  function selectableCount(items) {
3317
3197
  return items.filter((i) => !isSeparator(i)).length;
@@ -3356,6 +3236,55 @@ async function showSelect(opts) {
3356
3236
  }
3357
3237
  }
3358
3238
  }
3239
+
3240
+ // src/tui/confirm-dialog.ts
3241
+ import chalk5 from "chalk";
3242
+ function buildConfirmContent(message, selectedYes) {
3243
+ const yes = selectedYes ? chalk5.cyan.bold("> Yes") : chalk5.dim(" Yes");
3244
+ const no = !selectedYes ? chalk5.cyan.bold("> No") : chalk5.dim(" No");
3245
+ return [message, "", yes, no];
3246
+ }
3247
+ async function showConfirm(opts) {
3248
+ const w = modalWidth();
3249
+ let selectedYes = true;
3250
+ const render = () => {
3251
+ const content = buildConfirmContent(opts.message, selectedYes);
3252
+ const footer = "\u2191\u2193 Select Enter Confirm Esc Cancel";
3253
+ const lines = buildModalFrame(opts.title, content, footer, w);
3254
+ renderOverlay(opts.dashboardLines, lines, w);
3255
+ };
3256
+ render();
3257
+ while (true) {
3258
+ const key = await readKey();
3259
+ switch (key.name) {
3260
+ case "up":
3261
+ case "down":
3262
+ case "tab":
3263
+ selectedYes = !selectedYes;
3264
+ render();
3265
+ break;
3266
+ case "enter":
3267
+ process.stdout.write(showCursor());
3268
+ return selectedYes;
3269
+ case "escape":
3270
+ process.stdout.write(showCursor());
3271
+ return false;
3272
+ case "y":
3273
+ process.stdout.write(showCursor());
3274
+ return true;
3275
+ case "n":
3276
+ process.stdout.write(showCursor());
3277
+ return false;
3278
+ }
3279
+ }
3280
+ }
3281
+
3282
+ // src/tui/input-dialog.ts
3283
+ import chalk6 from "chalk";
3284
+ function buildInputContent(label, value, cursorVisible) {
3285
+ const cursorChar = cursorVisible ? chalk6.inverse(" ") : "";
3286
+ return [label, "", chalk6.cyan("> ") + value + cursorChar];
3287
+ }
3359
3288
  async function showInput(opts) {
3360
3289
  const w = modalWidth();
3361
3290
  let value = "";
@@ -3394,9 +3323,115 @@ async function showInput(opts) {
3394
3323
  }
3395
3324
  }
3396
3325
  }
3326
+
3327
+ // src/tui/multiline-input.ts
3328
+ import chalk7 from "chalk";
3329
+ function wrapLineWithMap(text, width) {
3330
+ if (width <= 0) return [{ text, offset: 0 }];
3331
+ if (text.length <= width) return [{ text, offset: 0 }];
3332
+ const result = [];
3333
+ let pos = 0;
3334
+ let remaining = text;
3335
+ while (remaining.length > width) {
3336
+ let breakAt = remaining.lastIndexOf(" ", width);
3337
+ if (breakAt <= 0) breakAt = width;
3338
+ result.push({ text: remaining.slice(0, breakAt), offset: pos });
3339
+ pos += breakAt;
3340
+ remaining = remaining.slice(breakAt);
3341
+ if (remaining.startsWith(" ")) {
3342
+ remaining = remaining.slice(1);
3343
+ pos += 1;
3344
+ }
3345
+ }
3346
+ if (remaining.length > 0 || result.length === 0) {
3347
+ result.push({ text: remaining, offset: pos });
3348
+ }
3349
+ return result;
3350
+ }
3351
+ function getVisualLines(value, contentWidth) {
3352
+ const inputLines = value.split("\n");
3353
+ const result = [];
3354
+ let globalPos = 0;
3355
+ for (let i = 0; i < inputLines.length; i++) {
3356
+ const raw = inputLines[i];
3357
+ const wrapped = wrapLineWithMap(raw, contentWidth);
3358
+ for (const seg of wrapped) {
3359
+ result.push({
3360
+ text: seg.text,
3361
+ globalOffset: globalPos + seg.offset,
3362
+ length: seg.text.length
3363
+ });
3364
+ }
3365
+ globalPos += raw.length + 1;
3366
+ }
3367
+ return result;
3368
+ }
3369
+ function findCursorVisualPos(visualLines, cursorPos) {
3370
+ for (let i = 0; i < visualLines.length; i++) {
3371
+ const vl = visualLines[i];
3372
+ if (cursorPos >= vl.globalOffset && cursorPos <= vl.globalOffset + vl.length) {
3373
+ return { row: i, col: cursorPos - vl.globalOffset };
3374
+ }
3375
+ }
3376
+ const last = visualLines[visualLines.length - 1];
3377
+ return { row: visualLines.length - 1, col: last.length };
3378
+ }
3379
+ function moveCursorVertical(value, cursorPos, contentWidth, direction) {
3380
+ const visualLines = getVisualLines(value, contentWidth);
3381
+ const { row: row2, col } = findCursorVisualPos(visualLines, cursorPos);
3382
+ const newRow = row2 + direction;
3383
+ if (newRow < 0 || newRow >= visualLines.length) return cursorPos;
3384
+ const targetLine = visualLines[newRow];
3385
+ const newCol = Math.min(col, targetLine.length);
3386
+ return targetLine.globalOffset + newCol;
3387
+ }
3388
+ function moveCursorHome(value, cursorPos, contentWidth) {
3389
+ const visualLines = getVisualLines(value, contentWidth);
3390
+ const { row: row2 } = findCursorVisualPos(visualLines, cursorPos);
3391
+ return visualLines[row2].globalOffset;
3392
+ }
3393
+ function moveCursorEnd(value, cursorPos, contentWidth) {
3394
+ const visualLines = getVisualLines(value, contentWidth);
3395
+ const { row: row2 } = findCursorVisualPos(visualLines, cursorPos);
3396
+ return visualLines[row2].globalOffset + visualLines[row2].length;
3397
+ }
3398
+ function moveCursorWordLeft(value, cursorPos) {
3399
+ if (cursorPos <= 0) return 0;
3400
+ let pos = cursorPos - 1;
3401
+ while (pos > 0 && !/\w/.test(value[pos])) pos--;
3402
+ while (pos > 0 && /\w/.test(value[pos - 1])) pos--;
3403
+ return pos;
3404
+ }
3405
+ function moveCursorWordRight(value, cursorPos) {
3406
+ if (cursorPos >= value.length) return value.length;
3407
+ let pos = cursorPos;
3408
+ while (pos < value.length && /\w/.test(value[pos])) pos++;
3409
+ while (pos < value.length && !/\w/.test(value[pos])) pos++;
3410
+ return pos;
3411
+ }
3412
+ function buildMultilineInputContent(label, value, cursorVisible, maxWidth, cursorPos) {
3413
+ const cp = cursorPos ?? value.length;
3414
+ const lines = [label, ""];
3415
+ const contentWidth = maxWidth - 2;
3416
+ const visualLines = getVisualLines(value, contentWidth);
3417
+ const { row: cursorRow, col: cursorCol } = findCursorVisualPos(visualLines, cp);
3418
+ for (let i = 0; i < visualLines.length; i++) {
3419
+ const text = visualLines[i].text;
3420
+ if (i === cursorRow && cursorVisible) {
3421
+ const charUnderCursor = cursorCol < text.length ? text[cursorCol] : " ";
3422
+ lines.push(
3423
+ chalk7.cyan(" ") + text.slice(0, cursorCol) + chalk7.inverse(charUnderCursor) + text.slice(cursorCol + 1)
3424
+ );
3425
+ } else {
3426
+ lines.push(chalk7.cyan(" ") + text);
3427
+ }
3428
+ }
3429
+ return lines;
3430
+ }
3397
3431
  async function showMultilineInput(opts) {
3398
3432
  const w = modalWidth();
3399
3433
  let value = "";
3434
+ let cursorPos = 0;
3400
3435
  let cursorBlink = true;
3401
3436
  const contentWidth = w - 6;
3402
3437
  const render = () => {
@@ -3404,7 +3439,8 @@ async function showMultilineInput(opts) {
3404
3439
  opts.label,
3405
3440
  value,
3406
3441
  cursorBlink,
3407
- contentWidth
3442
+ contentWidth,
3443
+ cursorPos
3408
3444
  );
3409
3445
  const footer = "Enter Newline Ctrl+S Submit Esc Cancel";
3410
3446
  const lines = buildModalFrame(opts.title, content, footer, w);
@@ -3418,7 +3454,8 @@ async function showMultilineInput(opts) {
3418
3454
  process.stdout.write(showCursor());
3419
3455
  return value.trim() || null;
3420
3456
  case "enter":
3421
- value += "\n";
3457
+ value = value.slice(0, cursorPos) + "\n" + value.slice(cursorPos);
3458
+ cursorPos++;
3422
3459
  cursorBlink = true;
3423
3460
  render();
3424
3461
  break;
@@ -3426,14 +3463,54 @@ async function showMultilineInput(opts) {
3426
3463
  process.stdout.write(showCursor());
3427
3464
  return null;
3428
3465
  case "backspace":
3429
- if (value.length > 0) {
3430
- value = value.slice(0, -1);
3466
+ if (cursorPos > 0) {
3467
+ value = value.slice(0, cursorPos - 1) + value.slice(cursorPos);
3468
+ cursorPos--;
3469
+ }
3470
+ render();
3471
+ break;
3472
+ case "delete":
3473
+ if (cursorPos < value.length) {
3474
+ value = value.slice(0, cursorPos) + value.slice(cursorPos + 1);
3431
3475
  }
3432
3476
  render();
3433
3477
  break;
3478
+ case "left":
3479
+ if (cursorPos > 0) cursorPos--;
3480
+ render();
3481
+ break;
3482
+ case "right":
3483
+ if (cursorPos < value.length) cursorPos++;
3484
+ render();
3485
+ break;
3486
+ case "up":
3487
+ cursorPos = moveCursorVertical(value, cursorPos, contentWidth - 2, -1);
3488
+ render();
3489
+ break;
3490
+ case "down":
3491
+ cursorPos = moveCursorVertical(value, cursorPos, contentWidth - 2, 1);
3492
+ render();
3493
+ break;
3494
+ case "home":
3495
+ cursorPos = moveCursorHome(value, cursorPos, contentWidth - 2);
3496
+ render();
3497
+ break;
3498
+ case "end":
3499
+ cursorPos = moveCursorEnd(value, cursorPos, contentWidth - 2);
3500
+ render();
3501
+ break;
3502
+ case "ctrl-left":
3503
+ cursorPos = moveCursorWordLeft(value, cursorPos);
3504
+ render();
3505
+ break;
3506
+ case "ctrl-right":
3507
+ cursorPos = moveCursorWordRight(value, cursorPos);
3508
+ render();
3509
+ break;
3434
3510
  default:
3435
3511
  if (key.raw.length === 1 && key.raw.charCodeAt(0) >= 32) {
3436
- value += key.raw;
3512
+ value = value.slice(0, cursorPos) + key.raw + value.slice(cursorPos);
3513
+ cursorPos++;
3437
3514
  cursorBlink = true;
3438
3515
  render();
3439
3516
  }
@@ -3441,6 +3518,76 @@ async function showMultilineInput(opts) {
3441
3518
  }
3442
3519
  }
3443
3520
  }
3521
+
3522
+ // src/tui/modal.ts
3523
+ function modalTopRow(title, w) {
3524
+ const label = ` ${title} `;
3525
+ const fill = w - 2 - label.length;
3526
+ return chalk8.bold("\u250C") + chalk8.bold.cyan(label) + chalk8.bold("\u2500".repeat(Math.max(0, fill)) + "\u2510");
3527
+ }
3528
+ function modalDivRow(w) {
3529
+ return chalk8.bold("\u251C" + "\u2500".repeat(w - 2) + "\u2524");
3530
+ }
3531
+ function modalBotRow(w) {
3532
+ return chalk8.bold("\u2514" + "\u2500".repeat(w - 2) + "\u2518");
3533
+ }
3534
+ function modalRow(text, w) {
3535
+ const inner = w - 4;
3536
+ const fitted = truncateAnsi(text, inner);
3537
+ const pad = Math.max(0, inner - visibleLen(fitted));
3538
+ return chalk8.bold("\u2502") + " " + fitted + " ".repeat(pad) + " " + chalk8.bold("\u2502");
3539
+ }
3540
+ function modalEmptyRow(w) {
3541
+ return modalRow("", w);
3542
+ }
3543
+ function buildModalFrame(title, contentLines, footerText, width) {
3544
+ const lines = [];
3545
+ lines.push(modalTopRow(title, width));
3546
+ lines.push(modalEmptyRow(width));
3547
+ for (const line of contentLines) {
3548
+ lines.push(modalRow(line, width));
3549
+ }
3550
+ lines.push(modalEmptyRow(width));
3551
+ lines.push(modalDivRow(width));
3552
+ lines.push(modalRow(chalk8.dim(footerText), width));
3553
+ lines.push(modalBotRow(width));
3554
+ return lines;
3555
+ }
3556
+ function termSize() {
3557
+ return {
3558
+ cols: process.stdout.columns || 80,
3559
+ rows: process.stdout.rows || 24
3560
+ };
3561
+ }
3562
+ function moveTo(row2, col) {
3563
+ return `\x1B[${row2};${col}H`;
3564
+ }
3565
+ function hideCursor() {
3566
+ return "\x1B[?25l";
3567
+ }
3568
+ function showCursor() {
3569
+ return "\x1B[?25h";
3570
+ }
3571
+ function modalWidth() {
3572
+ const cols = process.stdout.columns || 80;
3573
+ return Math.min(60, cols - 4);
3574
+ }
3575
+ function renderOverlay(dashboardLines, modalLines, mWidth) {
3576
+ const { cols, rows } = termSize();
3577
+ process.stdout.write("\x1B[2J\x1B[0f");
3578
+ process.stdout.write(hideCursor());
3579
+ for (let i = 0; i < dashboardLines.length && i < rows; i++) {
3580
+ process.stdout.write(
3581
+ moveTo(i + 1, 1) + chalk8.dim(stripAnsi(dashboardLines[i]))
3582
+ );
3583
+ }
3584
+ const startRow = Math.max(1, Math.floor((rows - modalLines.length) / 2));
3585
+ const startCol = Math.max(1, Math.floor((cols - mWidth) / 2));
3586
+ for (let i = 0; i < modalLines.length; i++) {
3587
+ process.stdout.write(moveTo(startRow + i, startCol) + modalLines[i]);
3588
+ }
3589
+ process.stdout.write(moveTo(startRow + modalLines.length + 1, 1));
3590
+ }
3444
3591
  function showStatus(title, message, dashboardLines) {
3445
3592
  const w = modalWidth();
3446
3593
  const content = [message];