@taj-special/dravix-code 1.1.21 → 1.1.22
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/cli/repl.js +142 -151
- package/dist/utils/layout.js +68 -0
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -10,6 +10,7 @@ import { printOpResult, printError, printInfo, colors } from '../utils/display.j
|
|
|
10
10
|
import { saveConversation, loadConversation, listConversations, generateTitle, generateId } from '../services/conversations.js';
|
|
11
11
|
import { getToken } from '../services/auth.js';
|
|
12
12
|
import { checkUsage, reportUsage, estimateTokens, usageBar, fmtNum, formatResetTime } from '../services/usage.js';
|
|
13
|
+
import { ChatLayout } from '../utils/layout.js';
|
|
13
14
|
let _serverWebDesignerSkill = '';
|
|
14
15
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
15
16
|
const SLASH_COMMANDS = [
|
|
@@ -834,12 +835,14 @@ async function askPermission(label, key, alwaysAllowed, noAlways, diffShown, pre
|
|
|
834
835
|
process.stdin.on('data', onData);
|
|
835
836
|
});
|
|
836
837
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
838
|
+
const HINT_TEXT = (C) => colors.muted(' ') + C('Tab') + colors.muted(' path complete ') +
|
|
839
|
+
C('@') + colors.muted(' file picker ') +
|
|
840
|
+
C('Ctrl+C') + colors.muted(' cancel ') +
|
|
841
|
+
C('/help') + colors.muted(' commands');
|
|
842
|
+
async function readLine(prompt, cwd, layout) {
|
|
843
|
+
const C = chalk.hex('#818cf8');
|
|
844
|
+
layout.drawHint(HINT_TEXT(C));
|
|
845
|
+
layout.posInput(prompt);
|
|
843
846
|
return new Promise((resolve) => {
|
|
844
847
|
let prefix = '';
|
|
845
848
|
let pasteBlock = null;
|
|
@@ -867,44 +870,25 @@ async function readLine(prompt, cwd) {
|
|
|
867
870
|
let tabDrawn = false;
|
|
868
871
|
let tabPreWord = ''; // prefix text before the tab-completed token
|
|
869
872
|
function onResize() {
|
|
870
|
-
|
|
871
|
-
const oldSepRows = Math.ceil(sepVisualLen / newCols);
|
|
872
|
-
SEP = colors.dim(' ' + '─'.repeat(Math.max(newCols - 4, 40)));
|
|
873
|
-
sepVisualLen = 2 + Math.max(newCols - 4, 40);
|
|
873
|
+
layout.resize();
|
|
874
874
|
if (atMode) {
|
|
875
|
-
for (let i = 0; i < atBoxLines + 3; i++)
|
|
876
|
-
process.stdout.write('\x1b[1A');
|
|
877
|
-
process.stdout.write('\r\x1b[0J');
|
|
878
875
|
atDrawn = false;
|
|
879
876
|
atBoxLines = 0;
|
|
880
877
|
drawPicker();
|
|
881
878
|
}
|
|
882
879
|
else if (slashMode) {
|
|
883
|
-
for (let i = 0; i < slashBoxLines + 3; i++)
|
|
884
|
-
process.stdout.write('\x1b[1A');
|
|
885
|
-
process.stdout.write('\r\x1b[0J');
|
|
886
880
|
slashDrawn = false;
|
|
887
881
|
slashBoxLines = 0;
|
|
888
882
|
drawSlashPicker();
|
|
889
883
|
}
|
|
890
884
|
else if (tabMode) {
|
|
891
|
-
for (let i = 0; i < tabBoxLines + 3; i++)
|
|
892
|
-
process.stdout.write('\x1b[1A');
|
|
893
|
-
process.stdout.write('\r\x1b[0J');
|
|
894
885
|
tabDrawn = false;
|
|
895
886
|
tabBoxLines = 0;
|
|
896
887
|
drawTabPicker();
|
|
897
888
|
}
|
|
898
889
|
else {
|
|
899
|
-
// Clear input line, then bottom SEP below it
|
|
900
|
-
process.stdout.write('\r\x1b[K');
|
|
901
|
-
process.stdout.write('\x1b[1B\x1b[2K\x1b[1A');
|
|
902
|
-
// Go up clearing old top SEP rows + blank line above them
|
|
903
|
-
for (let i = 0; i < oldSepRows + 1; i++)
|
|
904
|
-
process.stdout.write('\x1b[1A\x1b[2K');
|
|
905
|
-
// Redraw: blank line + new SEP + input + bottom SEP
|
|
906
|
-
process.stdout.write('\n' + SEP + '\n');
|
|
907
890
|
redraw();
|
|
891
|
+
layout.drawHint(HINT_TEXT(C));
|
|
908
892
|
}
|
|
909
893
|
}
|
|
910
894
|
process.stdout.on('resize', onResize);
|
|
@@ -973,43 +957,37 @@ async function readLine(prompt, cwd) {
|
|
|
973
957
|
// then \x1b[2A\x1b[2K to skip bot-sep and clear input
|
|
974
958
|
let atDrawn = false;
|
|
975
959
|
function drawPicker() {
|
|
976
|
-
if (atDrawn) {
|
|
977
|
-
for (let i = 0; i < atBoxLines; i++)
|
|
978
|
-
process.stdout.write('\x1b[1A\x1b[2K');
|
|
979
|
-
process.stdout.write('\x1b[2A\x1b[2K');
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
process.stdout.write('\r\x1b[K');
|
|
983
|
-
atDrawn = true;
|
|
984
|
-
}
|
|
985
|
-
atBoxLines = 0;
|
|
986
|
-
// Re-draw input line + bot sep, cursor lands at A+2
|
|
987
|
-
const inputLine = colors.primary(' › ') + chalk.white(prefix) +
|
|
988
|
-
(pasteBlock !== null
|
|
989
|
-
? colors.muted(`[paste: ${pasteCount} lines]`) + chalk.white(suffix)
|
|
990
|
-
: '') +
|
|
991
|
-
chalk.hex('#818cf8')('@') + chalk.white(atQuery);
|
|
992
|
-
process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
|
|
993
|
-
// Box dimensions — DW = total visual width including borders
|
|
994
|
-
const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
|
|
995
|
-
const IW = DW - 6; // visible filename area width
|
|
996
960
|
const filtered = filterFiles(atQuery);
|
|
997
961
|
if (atSel >= filtered.length && filtered.length > 0)
|
|
998
962
|
atSel = filtered.length - 1;
|
|
999
963
|
const shown = filtered.slice(0, 10);
|
|
1000
|
-
|
|
964
|
+
const DW = Math.min(Math.max(layout.cols - 6, 44), 72);
|
|
965
|
+
const IW = DW - 6;
|
|
966
|
+
const itemCount = Math.max(shown.length, 1);
|
|
967
|
+
atBoxLines = itemCount + 2; // header + items/no-match + footer
|
|
968
|
+
const totalRows = 2 + atBoxLines; // input preview + blank + box
|
|
969
|
+
const startRow = Math.max(layout.divRow - totalRows, 1);
|
|
970
|
+
process.stdout.write('\x1b7'); // save cursor at inputRow
|
|
971
|
+
// Input preview
|
|
972
|
+
const inputLine = colors.primary(' › ') + chalk.white(prefix) +
|
|
973
|
+
(pasteBlock !== null ? colors.muted(`[paste: ${pasteCount} lines]`) + chalk.white(suffix) : '') +
|
|
974
|
+
chalk.hex('#818cf8')('@') + chalk.white(atQuery);
|
|
975
|
+
process.stdout.write(`\x1b[${startRow};1H\x1b[2K${inputLine}`);
|
|
976
|
+
process.stdout.write(`\x1b[${startRow + 1};1H\x1b[2K`); // blank separator
|
|
977
|
+
let row = startRow + 2;
|
|
978
|
+
// Header
|
|
1001
979
|
const qLabel = atQuery ? `@${atQuery}` : 'files';
|
|
1002
980
|
const hDashes = Math.max(DW - 5 - qLabel.length, 1);
|
|
1003
|
-
process.stdout.write(
|
|
1004
|
-
colors.dim(' ' + '─'.repeat(hDashes) + '╮')
|
|
1005
|
-
|
|
1006
|
-
//
|
|
981
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
982
|
+
colors.dim(' ╭─ ') + colors.primary(qLabel) + colors.dim(' ' + '─'.repeat(hDashes) + '╮'));
|
|
983
|
+
row++;
|
|
984
|
+
// Items
|
|
1007
985
|
if (shown.length === 0) {
|
|
1008
986
|
const msg = 'no matches';
|
|
1009
987
|
const pad = ' '.repeat(Math.max(IW - msg.length, 0));
|
|
1010
|
-
process.stdout.write(
|
|
1011
|
-
colors.muted(msg) + pad + colors.dim('│')
|
|
1012
|
-
|
|
988
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
989
|
+
colors.dim(' │') + ' ' + colors.muted(msg) + pad + colors.dim('│'));
|
|
990
|
+
row++;
|
|
1013
991
|
}
|
|
1014
992
|
else {
|
|
1015
993
|
for (let i = 0; i < shown.length; i++) {
|
|
@@ -1019,69 +997,70 @@ async function readLine(prompt, cwd) {
|
|
|
1019
997
|
const mark = sel ? colors.primary('>') : ' ';
|
|
1020
998
|
const clip = f.length > IW ? f.slice(0, IW - 1) + '…' : f;
|
|
1021
999
|
const pad = ' '.repeat(Math.max(IW - clip.length, 0));
|
|
1022
|
-
const name = sel && isDir ? colors.primary.bold(clip) + pad
|
|
1023
|
-
: sel ? colors.primary.bold(clip) + pad
|
|
1024
|
-
: isDir ? chalk.hex('#38bdf8')(clip) + pad
|
|
1025
|
-
: chalk.hex('#94a3b8')(clip + pad);
|
|
1026
|
-
process.stdout.write(
|
|
1027
|
-
|
|
1000
|
+
const name = sel && isDir ? colors.primary.bold(clip) + pad
|
|
1001
|
+
: sel ? colors.primary.bold(clip) + pad
|
|
1002
|
+
: isDir ? chalk.hex('#38bdf8')(clip) + pad
|
|
1003
|
+
: chalk.hex('#94a3b8')(clip + pad);
|
|
1004
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1005
|
+
colors.dim(' │') + ` ${mark} ` + name + colors.dim('│'));
|
|
1006
|
+
row++;
|
|
1028
1007
|
}
|
|
1029
1008
|
}
|
|
1030
|
-
//
|
|
1009
|
+
// Footer
|
|
1031
1010
|
const hint = ' ↑↓ Enter Esc ';
|
|
1032
1011
|
const fInner = DW - 2 - hint.length;
|
|
1033
1012
|
const fLeft = Math.floor(fInner / 2);
|
|
1034
1013
|
const fRight = fInner - fLeft;
|
|
1035
|
-
process.stdout.write(
|
|
1014
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1015
|
+
colors.dim(' ╰' + '─'.repeat(Math.max(fLeft, 0))) +
|
|
1036
1016
|
colors.muted(hint) +
|
|
1037
|
-
colors.dim('─'.repeat(Math.max(fRight, 0)) + '╯')
|
|
1038
|
-
|
|
1039
|
-
// cursor
|
|
1017
|
+
colors.dim('─'.repeat(Math.max(fRight, 0)) + '╯'));
|
|
1018
|
+
atDrawn = true;
|
|
1019
|
+
process.stdout.write('\x1b8'); // restore cursor to inputRow
|
|
1040
1020
|
}
|
|
1041
1021
|
function closePicker() {
|
|
1042
1022
|
if (atDrawn) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
process.stdout.write('\
|
|
1023
|
+
const totalRows = 2 + atBoxLines;
|
|
1024
|
+
const startRow = Math.max(layout.divRow - totalRows, 1);
|
|
1025
|
+
process.stdout.write('\x1b7');
|
|
1026
|
+
for (let r = startRow; r < layout.divRow; r++) {
|
|
1027
|
+
process.stdout.write(`\x1b[${r};1H\x1b[2K`);
|
|
1028
|
+
}
|
|
1029
|
+
process.stdout.write('\x1b8');
|
|
1046
1030
|
atDrawn = false;
|
|
1047
1031
|
}
|
|
1048
|
-
else {
|
|
1049
|
-
process.stdout.write('\r\x1b[K');
|
|
1050
|
-
}
|
|
1051
1032
|
atBoxLines = 0;
|
|
1052
1033
|
atMode = false;
|
|
1053
1034
|
atQuery = '';
|
|
1054
1035
|
atSel = 0;
|
|
1055
1036
|
}
|
|
1056
1037
|
function drawSlashPicker() {
|
|
1057
|
-
if (slashDrawn) {
|
|
1058
|
-
for (let i = 0; i < slashBoxLines; i++)
|
|
1059
|
-
process.stdout.write('\x1b[1A\x1b[2K');
|
|
1060
|
-
process.stdout.write('\x1b[2A\x1b[2K');
|
|
1061
|
-
}
|
|
1062
|
-
else {
|
|
1063
|
-
process.stdout.write('\r\x1b[K');
|
|
1064
|
-
slashDrawn = true;
|
|
1065
|
-
}
|
|
1066
|
-
slashBoxLines = 0;
|
|
1067
|
-
const inputLine = colors.primary(' › ') + colors.primary('/') + chalk.white(slashQuery);
|
|
1068
|
-
process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
|
|
1069
|
-
const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
|
|
1070
|
-
const nameW = 10;
|
|
1071
|
-
const descW = DW - nameW - 8;
|
|
1072
1038
|
const filtered = filterSlash(slashQuery);
|
|
1073
1039
|
if (slashSel >= filtered.length && filtered.length > 0)
|
|
1074
1040
|
slashSel = filtered.length - 1;
|
|
1041
|
+
const DW = Math.min(Math.max(layout.cols - 6, 44), 72);
|
|
1042
|
+
const nameW = 10;
|
|
1043
|
+
const descW = DW - nameW - 8;
|
|
1044
|
+
const itemCount = Math.max(filtered.length, 1);
|
|
1045
|
+
slashBoxLines = itemCount + 2;
|
|
1046
|
+
const totalRows = 2 + slashBoxLines;
|
|
1047
|
+
const startRow = Math.max(layout.divRow - totalRows, 1);
|
|
1048
|
+
process.stdout.write('\x1b7');
|
|
1049
|
+
const inputLine = colors.primary(' › ') + colors.primary('/') + chalk.white(slashQuery);
|
|
1050
|
+
process.stdout.write(`\x1b[${startRow};1H\x1b[2K${inputLine}`);
|
|
1051
|
+
process.stdout.write(`\x1b[${startRow + 1};1H\x1b[2K`);
|
|
1052
|
+
let row = startRow + 2;
|
|
1075
1053
|
const qLabel = slashQuery ? `/${slashQuery}` : 'commands';
|
|
1076
1054
|
const hDashes = Math.max(DW - 5 - qLabel.length, 1);
|
|
1077
|
-
process.stdout.write(
|
|
1078
|
-
colors.dim(' ' + '─'.repeat(hDashes) + '╮')
|
|
1079
|
-
|
|
1055
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1056
|
+
colors.dim(' ╭─ ') + colors.primary(qLabel) + colors.dim(' ' + '─'.repeat(hDashes) + '╮'));
|
|
1057
|
+
row++;
|
|
1080
1058
|
if (filtered.length === 0) {
|
|
1081
1059
|
const msg = 'no matching commands';
|
|
1082
1060
|
const pad = ' '.repeat(Math.max(DW - 4 - msg.length, 0));
|
|
1083
|
-
process.stdout.write(
|
|
1084
|
-
|
|
1061
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1062
|
+
colors.dim(' │') + ' ' + colors.muted(msg) + pad + colors.dim('│'));
|
|
1063
|
+
row++;
|
|
1085
1064
|
}
|
|
1086
1065
|
else {
|
|
1087
1066
|
for (let i = 0; i < filtered.length; i++) {
|
|
@@ -1093,65 +1072,66 @@ async function readLine(prompt, cwd) {
|
|
|
1093
1072
|
const descPad = ' '.repeat(Math.max(descW - descStr.length, 0));
|
|
1094
1073
|
const namePaint = sel ? colors.primary.bold(nameStr) : chalk.hex('#818cf8')(nameStr);
|
|
1095
1074
|
const descPaint = sel ? colors.muted(descStr) : chalk.hex('#4b5563')(descStr);
|
|
1096
|
-
process.stdout.write(
|
|
1097
|
-
|
|
1075
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1076
|
+
colors.dim(' │') + ` ${mark} ` + namePaint + ' ' + descPaint + descPad + colors.dim('│'));
|
|
1077
|
+
row++;
|
|
1098
1078
|
}
|
|
1099
1079
|
}
|
|
1100
1080
|
const hint = ' ↑↓ Enter Esc ';
|
|
1101
1081
|
const fInner = DW - 2 - hint.length;
|
|
1102
1082
|
const fLeft = Math.floor(fInner / 2);
|
|
1103
1083
|
const fRight = fInner - fLeft;
|
|
1104
|
-
process.stdout.write(
|
|
1084
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1085
|
+
colors.dim(' ╰' + '─'.repeat(Math.max(fLeft, 0))) +
|
|
1105
1086
|
colors.muted(hint) +
|
|
1106
|
-
colors.dim('─'.repeat(Math.max(fRight, 0)) + '╯')
|
|
1107
|
-
|
|
1087
|
+
colors.dim('─'.repeat(Math.max(fRight, 0)) + '╯'));
|
|
1088
|
+
slashDrawn = true;
|
|
1089
|
+
process.stdout.write('\x1b8');
|
|
1108
1090
|
}
|
|
1109
1091
|
function closeSlashPicker() {
|
|
1110
1092
|
if (slashDrawn) {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
process.stdout.write('\
|
|
1093
|
+
const totalRows = 2 + slashBoxLines;
|
|
1094
|
+
const startRow = Math.max(layout.divRow - totalRows, 1);
|
|
1095
|
+
process.stdout.write('\x1b7');
|
|
1096
|
+
for (let r = startRow; r < layout.divRow; r++) {
|
|
1097
|
+
process.stdout.write(`\x1b[${r};1H\x1b[2K`);
|
|
1098
|
+
}
|
|
1099
|
+
process.stdout.write('\x1b8');
|
|
1114
1100
|
slashDrawn = false;
|
|
1115
1101
|
}
|
|
1116
|
-
else {
|
|
1117
|
-
process.stdout.write('\r\x1b[K');
|
|
1118
|
-
}
|
|
1119
1102
|
slashBoxLines = 0;
|
|
1120
1103
|
slashMode = false;
|
|
1121
1104
|
slashQuery = '';
|
|
1122
1105
|
slashSel = 0;
|
|
1123
1106
|
}
|
|
1124
1107
|
function drawTabPicker() {
|
|
1125
|
-
if (tabDrawn) {
|
|
1126
|
-
for (let i = 0; i < tabBoxLines; i++)
|
|
1127
|
-
process.stdout.write('\x1b[1A\x1b[2K');
|
|
1128
|
-
process.stdout.write('\x1b[2A\x1b[2K');
|
|
1129
|
-
}
|
|
1130
|
-
else {
|
|
1131
|
-
process.stdout.write('\r\x1b[K');
|
|
1132
|
-
tabDrawn = true;
|
|
1133
|
-
}
|
|
1134
|
-
tabBoxLines = 0;
|
|
1135
|
-
const inputLine = colors.primary(' › ') + chalk.white(tabPreWord) +
|
|
1136
|
-
chalk.hex('#34d399')(tabQuery);
|
|
1137
|
-
process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
|
|
1138
|
-
const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
|
|
1139
|
-
const IW = DW - 6;
|
|
1140
1108
|
const filtered = filterFiles(tabQuery);
|
|
1141
1109
|
if (tabSel >= filtered.length && filtered.length > 0)
|
|
1142
1110
|
tabSel = filtered.length - 1;
|
|
1143
1111
|
const shown = filtered.slice(0, 10);
|
|
1112
|
+
const DW = Math.min(Math.max(layout.cols - 6, 44), 72);
|
|
1113
|
+
const IW = DW - 6;
|
|
1144
1114
|
const TC = chalk.hex('#34d399');
|
|
1115
|
+
const itemCount = Math.max(shown.length, 1);
|
|
1116
|
+
tabBoxLines = itemCount + 2;
|
|
1117
|
+
const totalRows = 2 + tabBoxLines;
|
|
1118
|
+
const startRow = Math.max(layout.divRow - totalRows, 1);
|
|
1119
|
+
process.stdout.write('\x1b7');
|
|
1120
|
+
const inputLine = colors.primary(' › ') + chalk.white(tabPreWord) + TC(tabQuery);
|
|
1121
|
+
process.stdout.write(`\x1b[${startRow};1H\x1b[2K${inputLine}`);
|
|
1122
|
+
process.stdout.write(`\x1b[${startRow + 1};1H\x1b[2K`);
|
|
1123
|
+
let row = startRow + 2;
|
|
1145
1124
|
const qLabel = tabQuery || 'tab complete';
|
|
1146
1125
|
const hDashes = Math.max(DW - 5 - qLabel.length, 1);
|
|
1147
|
-
process.stdout.write(
|
|
1148
|
-
colors.dim(' ' + '─'.repeat(hDashes) + '╮')
|
|
1149
|
-
|
|
1126
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1127
|
+
colors.dim(' ╭─ ') + TC(qLabel) + colors.dim(' ' + '─'.repeat(hDashes) + '╮'));
|
|
1128
|
+
row++;
|
|
1150
1129
|
if (shown.length === 0) {
|
|
1151
1130
|
const msg = 'no matches';
|
|
1152
1131
|
const pad = ' '.repeat(Math.max(IW - msg.length, 0));
|
|
1153
|
-
process.stdout.write(
|
|
1154
|
-
|
|
1132
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1133
|
+
colors.dim(' │') + ' ' + colors.muted(msg) + pad + colors.dim('│'));
|
|
1134
|
+
row++;
|
|
1155
1135
|
}
|
|
1156
1136
|
else {
|
|
1157
1137
|
for (let i = 0; i < shown.length; i++) {
|
|
@@ -1164,29 +1144,33 @@ async function readLine(prompt, cwd) {
|
|
|
1164
1144
|
const name = sel ? TC.bold(clip) + pad
|
|
1165
1145
|
: isDir ? chalk.hex('#38bdf8')(clip) + pad
|
|
1166
1146
|
: chalk.hex('#94a3b8')(clip + pad);
|
|
1167
|
-
process.stdout.write(
|
|
1168
|
-
|
|
1147
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1148
|
+
colors.dim(' │') + ` ${mark} ` + name + colors.dim('│'));
|
|
1149
|
+
row++;
|
|
1169
1150
|
}
|
|
1170
1151
|
}
|
|
1171
1152
|
const hint = ' Tab/↑↓ Enter Esc ';
|
|
1172
1153
|
const fInner = DW - 2 - hint.length;
|
|
1173
1154
|
const fLeft = Math.floor(fInner / 2);
|
|
1174
1155
|
const fRight = fInner - fLeft;
|
|
1175
|
-
process.stdout.write(
|
|
1156
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K` +
|
|
1157
|
+
colors.dim(' ╰' + '─'.repeat(Math.max(fLeft, 0))) +
|
|
1176
1158
|
colors.muted(hint) +
|
|
1177
|
-
colors.dim('─'.repeat(Math.max(fRight, 0)) + '╯')
|
|
1178
|
-
|
|
1159
|
+
colors.dim('─'.repeat(Math.max(fRight, 0)) + '╯'));
|
|
1160
|
+
tabDrawn = true;
|
|
1161
|
+
process.stdout.write('\x1b8');
|
|
1179
1162
|
}
|
|
1180
1163
|
function closeTabPicker() {
|
|
1181
1164
|
if (tabDrawn) {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
process.stdout.write('\
|
|
1165
|
+
const totalRows = 2 + tabBoxLines;
|
|
1166
|
+
const startRow = Math.max(layout.divRow - totalRows, 1);
|
|
1167
|
+
process.stdout.write('\x1b7');
|
|
1168
|
+
for (let r = startRow; r < layout.divRow; r++) {
|
|
1169
|
+
process.stdout.write(`\x1b[${r};1H\x1b[2K`);
|
|
1170
|
+
}
|
|
1171
|
+
process.stdout.write('\x1b8');
|
|
1185
1172
|
tabDrawn = false;
|
|
1186
1173
|
}
|
|
1187
|
-
else {
|
|
1188
|
-
process.stdout.write('\r\x1b[K');
|
|
1189
|
-
}
|
|
1190
1174
|
tabBoxLines = 0;
|
|
1191
1175
|
tabMode = false;
|
|
1192
1176
|
tabQuery = '';
|
|
@@ -1194,9 +1178,8 @@ async function readLine(prompt, cwd) {
|
|
|
1194
1178
|
tabPreWord = '';
|
|
1195
1179
|
}
|
|
1196
1180
|
function redraw() {
|
|
1197
|
-
const cols =
|
|
1181
|
+
const cols = layout.cols;
|
|
1198
1182
|
const maxLen = Math.max(cols - 6, 10);
|
|
1199
|
-
// Truncate to terminal width — prevents line wrapping on resize
|
|
1200
1183
|
let displayPre = prefix;
|
|
1201
1184
|
if (displayPre.length > maxLen)
|
|
1202
1185
|
displayPre = '…' + displayPre.slice(-(maxLen - 1));
|
|
@@ -1208,8 +1191,7 @@ async function readLine(prompt, cwd) {
|
|
|
1208
1191
|
else {
|
|
1209
1192
|
inp = colors.primary(' › ') + chalk.white(displayPre);
|
|
1210
1193
|
}
|
|
1211
|
-
process.stdout.write(
|
|
1212
|
-
process.stdout.write('\r\n' + SEP + '\x1b[1A\r' + inp);
|
|
1194
|
+
process.stdout.write(`\x1b[${layout.inputRow};1H\x1b[2K${inp}`);
|
|
1213
1195
|
}
|
|
1214
1196
|
function submit() {
|
|
1215
1197
|
if (atMode)
|
|
@@ -1228,27 +1210,27 @@ async function readLine(prompt, cwd) {
|
|
|
1228
1210
|
const text = parts.join('\n');
|
|
1229
1211
|
const lns = parts.length > 0 ? parts : (text ? [text] : []);
|
|
1230
1212
|
cleanup();
|
|
1231
|
-
// Clear
|
|
1232
|
-
|
|
1233
|
-
process.stdout.write('\x1b[1B\x1b[2K'); // down to bot sep, clear
|
|
1234
|
-
process.stdout.write('\x1b[2A\x1b[2K'); // up 2 to top sep, clear
|
|
1213
|
+
// Clear the fixed input area
|
|
1214
|
+
layout.clearInput();
|
|
1235
1215
|
if (!text) {
|
|
1236
1216
|
resolve(null);
|
|
1237
1217
|
return;
|
|
1238
1218
|
}
|
|
1239
|
-
|
|
1219
|
+
// Move into the chat scroll region and print the user message
|
|
1220
|
+
layout.enterChat();
|
|
1221
|
+
const cols = layout.cols;
|
|
1240
1222
|
const msgBg = chalk.bgHex('#334155').hex('#f1f5f9');
|
|
1241
1223
|
const fillMsg = (raw) => (' > ' + raw + ' ').padEnd(cols);
|
|
1242
1224
|
if (pasteBlock !== null) {
|
|
1243
|
-
const
|
|
1225
|
+
const disp = [
|
|
1244
1226
|
prefix ? prefix : '',
|
|
1245
1227
|
`[${pasteCount} lines]`,
|
|
1246
1228
|
suffix.trim() ? suffix.trim() : '',
|
|
1247
1229
|
].filter(Boolean).join(' ');
|
|
1248
|
-
process.stdout.write(msgBg(fillMsg(
|
|
1230
|
+
process.stdout.write('\n' + msgBg(fillMsg(disp)) + '\n');
|
|
1249
1231
|
}
|
|
1250
1232
|
else {
|
|
1251
|
-
process.stdout.write(msgBg(fillMsg(text)) + '\n');
|
|
1233
|
+
process.stdout.write('\n' + msgBg(fillMsg(text)) + '\n');
|
|
1252
1234
|
}
|
|
1253
1235
|
resolve({ text, lines: lns });
|
|
1254
1236
|
}
|
|
@@ -1301,13 +1283,18 @@ async function readLine(prompt, cwd) {
|
|
|
1301
1283
|
resolve(null);
|
|
1302
1284
|
return;
|
|
1303
1285
|
}
|
|
1304
|
-
// Ctrl+L — clear
|
|
1286
|
+
// Ctrl+L — clear chat area, reinit layout
|
|
1305
1287
|
if (data === '\x0c') {
|
|
1306
1288
|
if (atMode)
|
|
1307
1289
|
closePicker();
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1290
|
+
if (slashMode)
|
|
1291
|
+
closeSlashPicker();
|
|
1292
|
+
if (tabMode)
|
|
1293
|
+
closeTabPicker();
|
|
1294
|
+
layout.clearScrollRegion();
|
|
1295
|
+
layout.redrawDivider();
|
|
1296
|
+
layout.drawHint(HINT_TEXT(C));
|
|
1297
|
+
layout.posInput(prompt);
|
|
1311
1298
|
return;
|
|
1312
1299
|
}
|
|
1313
1300
|
// ── @ picker mode ─────────────────────────────────────────
|
|
@@ -2189,6 +2176,10 @@ export async function startRepl(cwd) {
|
|
|
2189
2176
|
process.stdin.setRawMode(true);
|
|
2190
2177
|
process.stdin.resume();
|
|
2191
2178
|
process.stdin.setEncoding('utf8');
|
|
2179
|
+
// Initialize sticky-bottom layout (scroll region + fixed input bar)
|
|
2180
|
+
const layout = new ChatLayout();
|
|
2181
|
+
layout.init();
|
|
2182
|
+
process.on('exit', () => layout.cleanup());
|
|
2192
2183
|
// Sequential loop — queued input lets user type during AI stream
|
|
2193
2184
|
while (true) {
|
|
2194
2185
|
try {
|
|
@@ -2254,7 +2245,7 @@ export async function startRepl(cwd) {
|
|
|
2254
2245
|
process.stdout.write(msgBgQ((' > ' + q.text + ' ').padEnd(cols)) + '\n');
|
|
2255
2246
|
}
|
|
2256
2247
|
else {
|
|
2257
|
-
result = await readLine(PROMPT, activeCwd);
|
|
2248
|
+
result = await readLine(PROMPT, activeCwd, layout);
|
|
2258
2249
|
}
|
|
2259
2250
|
if (!result)
|
|
2260
2251
|
continue;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export class ChatLayout {
|
|
3
|
+
rows;
|
|
4
|
+
cols;
|
|
5
|
+
constructor() {
|
|
6
|
+
this.rows = process.stdout.rows ?? 24;
|
|
7
|
+
this.cols = process.stdout.columns ?? 80;
|
|
8
|
+
}
|
|
9
|
+
// Scroll region occupies rows 1 … scrollEnd
|
|
10
|
+
get scrollEnd() { return Math.max(this.rows - 3, 5); }
|
|
11
|
+
get divRow() { return this.rows - 2; } // ────── divider
|
|
12
|
+
get inputRow() { return this.rows - 1; } // › user input
|
|
13
|
+
get hintRow() { return this.rows; } // Tab / @ / … hints
|
|
14
|
+
makeSep() {
|
|
15
|
+
return chalk.hex('#374151')(' ' + '─'.repeat(Math.max(this.cols - 4, 40)));
|
|
16
|
+
}
|
|
17
|
+
/** Call once at the start of startRepl — sets scroll region + draws divider. */
|
|
18
|
+
init() {
|
|
19
|
+
this.rows = process.stdout.rows ?? 24;
|
|
20
|
+
this.cols = process.stdout.columns ?? 80;
|
|
21
|
+
process.stdout.write(`\x1b[1;${this.scrollEnd}r`);
|
|
22
|
+
this.redrawDivider();
|
|
23
|
+
}
|
|
24
|
+
/** Redraw the divider line at divRow (outside the scroll region). */
|
|
25
|
+
redrawDivider() {
|
|
26
|
+
process.stdout.write(`\x1b[${this.divRow};1H\x1b[2K${this.makeSep()}`);
|
|
27
|
+
}
|
|
28
|
+
/** Place cursor at inputRow, optionally with text. */
|
|
29
|
+
posInput(prompt, text = '') {
|
|
30
|
+
process.stdout.write(`\x1b[${this.inputRow};1H\x1b[2K${prompt}${text}`);
|
|
31
|
+
}
|
|
32
|
+
/** Clear the input and hint rows. */
|
|
33
|
+
clearInput() {
|
|
34
|
+
process.stdout.write(`\x1b[${this.inputRow};1H\x1b[2K`);
|
|
35
|
+
process.stdout.write(`\x1b[${this.hintRow};1H\x1b[2K`);
|
|
36
|
+
}
|
|
37
|
+
/** Write hint text at hintRow without moving cursor. */
|
|
38
|
+
drawHint(hint) {
|
|
39
|
+
process.stdout.write('\x1b7');
|
|
40
|
+
process.stdout.write(`\x1b[${this.hintRow};1H\x1b[2K${hint}`);
|
|
41
|
+
process.stdout.write('\x1b8');
|
|
42
|
+
}
|
|
43
|
+
/** Move cursor to the bottom of the scroll region (ready for chat output). */
|
|
44
|
+
enterChat() {
|
|
45
|
+
process.stdout.write(`\x1b[${this.scrollEnd};1H`);
|
|
46
|
+
}
|
|
47
|
+
/** Handle terminal resize: recalculate dimensions and redraw fixed rows. */
|
|
48
|
+
resize() {
|
|
49
|
+
this.rows = process.stdout.rows ?? 24;
|
|
50
|
+
this.cols = process.stdout.columns ?? 80;
|
|
51
|
+
process.stdout.write(`\x1b[1;${this.scrollEnd}r`);
|
|
52
|
+
this.redrawDivider();
|
|
53
|
+
process.stdout.write(`\x1b[${this.inputRow};1H\x1b[2K`);
|
|
54
|
+
process.stdout.write(`\x1b[${this.hintRow};1H\x1b[2K`);
|
|
55
|
+
}
|
|
56
|
+
/** Clear all chat lines in the scroll region. */
|
|
57
|
+
clearScrollRegion() {
|
|
58
|
+
for (let r = 1; r <= this.scrollEnd; r++) {
|
|
59
|
+
process.stdout.write(`\x1b[${r};1H\x1b[2K`);
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write(`\x1b[1;1H`);
|
|
62
|
+
}
|
|
63
|
+
/** Reset scroll region and clean up on process exit. */
|
|
64
|
+
cleanup() {
|
|
65
|
+
process.stdout.write('\x1b[r');
|
|
66
|
+
process.stdout.write(`\x1b[${this.rows};1H\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|