@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 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
- async function readLine(prompt, cwd) {
838
- let SEP = colors.dim(' ' + '─'.repeat(Math.max((process.stdout.columns ?? 80) - 4, 40)));
839
- let sepVisualLen = 2 + Math.max((process.stdout.columns ?? 80) - 4, 40); // track visual width for resize
840
- process.stdout.write('\n' + SEP + '\n' + prompt);
841
- // Draw bottom separator, then move cursor back up to input line
842
- process.stdout.write('\r\n' + SEP + '\x1b[1A\r' + prompt);
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
- const newCols = process.stdout.columns ?? 80;
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
- // ── Header ────────────────────────────────────────────────
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(colors.dim(' ╭─ ') + colors.primary(qLabel) +
1004
- colors.dim(' ' + '─'.repeat(hDashes) + '╮') + '\n');
1005
- atBoxLines++;
1006
- // ── Items ─────────────────────────────────────────────────
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(colors.dim(' │') + ' ' +
1011
- colors.muted(msg) + pad + colors.dim('│') + '\n');
1012
- atBoxLines++;
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 // selected folder: indigo bold
1023
- : sel ? colors.primary.bold(clip) + pad // selected file: indigo bold
1024
- : isDir ? chalk.hex('#38bdf8')(clip) + pad // folder: sky-blue
1025
- : chalk.hex('#94a3b8')(clip + pad); // file: slate-gray
1026
- process.stdout.write(colors.dim(' │') + ` ${mark} ` + name + colors.dim('│') + '\n');
1027
- atBoxLines++;
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
- // ── Footer ────────────────────────────────────────────────
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(colors.dim(' ╰' + '─'.repeat(Math.max(fLeft, 0))) +
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)) + '╯') + '\n');
1038
- atBoxLines++;
1039
- // cursor at A+2+atBoxLines
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
- for (let i = 0; i < atBoxLines; i++)
1044
- process.stdout.write('\x1b[1A\x1b[2K');
1045
- process.stdout.write('\x1b[2A\x1b[2K');
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(colors.dim(' ╭─ ') + colors.primary(qLabel) +
1078
- colors.dim(' ' + '─'.repeat(hDashes) + '╮') + '\n');
1079
- slashBoxLines++;
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(colors.dim(' │') + ' ' + colors.muted(msg) + pad + colors.dim('│') + '\n');
1084
- slashBoxLines++;
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(colors.dim(' │') + ` ${mark} ` + namePaint + ' ' + descPaint + descPad + colors.dim('│') + '\n');
1097
- slashBoxLines++;
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(colors.dim(' ╰' + '─'.repeat(Math.max(fLeft, 0))) +
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)) + '╯') + '\n');
1107
- slashBoxLines++;
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
- for (let i = 0; i < slashBoxLines; i++)
1112
- process.stdout.write('\x1b[1A\x1b[2K');
1113
- process.stdout.write('\x1b[2A\x1b[2K');
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(colors.dim(' ╭─ ') + TC(qLabel) +
1148
- colors.dim(' ' + '─'.repeat(hDashes) + '╮') + '\n');
1149
- tabBoxLines++;
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(colors.dim(' │') + ' ' + colors.muted(msg) + pad + colors.dim('│') + '\n');
1154
- tabBoxLines++;
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(colors.dim(' │') + ` ${mark} ` + name + colors.dim('│') + '\n');
1168
- tabBoxLines++;
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(colors.dim(' ╰' + '─'.repeat(Math.max(fLeft, 0))) +
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)) + '╯') + '\n');
1178
- tabBoxLines++;
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
- for (let i = 0; i < tabBoxLines; i++)
1183
- process.stdout.write('\x1b[1A\x1b[2K');
1184
- process.stdout.write('\x1b[2A\x1b[2K');
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 = process.stdout.columns ?? 80;
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('\r\x1b[K' + inp);
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 input bot sep → top sep, replace with styled sent message
1232
- process.stdout.write('\r\x1b[K'); // clear input line
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
- const cols = process.stdout.columns ?? 80;
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 parts = [
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(parts)) + '\n');
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 screen, redraw input
1286
+ // Ctrl+L — clear chat area, reinit layout
1305
1287
  if (data === '\x0c') {
1306
1288
  if (atMode)
1307
1289
  closePicker();
1308
- console.clear();
1309
- process.stdout.write('\n' + SEP + '\n' + prompt);
1310
- process.stdout.write('\r\n' + SEP + '\x1b[1A\r' + prompt);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taj-special/dravix-code",
3
- "version": "1.1.21",
3
+ "version": "1.1.22",
4
4
  "description": "AI-powered coding assistant CLI — Dravix Code",
5
5
  "type": "module",
6
6
  "bin": {