@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 +357 -181
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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 (
|
|
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
|
|
3113
|
-
|
|
3114
|
-
|
|
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
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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;
|