prior-cli 1.7.12 โ 1.7.13
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/bin/prior.js +163 -19
- package/package.json +1 -1
package/bin/prior.js
CHANGED
|
@@ -231,9 +231,9 @@ function expandFileRefs(input, cwd) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
// Live completions for an @file token being typed. `partial` is the text
|
|
234
|
-
// after @ (e.g. "lib/to"). Returns up to `limit` matches,
|
|
234
|
+
// after @ (e.g. "lib/to"). Returns up to `limit` matches, alphabetical (files + folders mixed).
|
|
235
235
|
const REF_SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.cache', 'coverage', 'vendor']);
|
|
236
|
-
function fileRefSuggestions(partial, cwd, limit =
|
|
236
|
+
function fileRefSuggestions(partial, cwd, limit = 10) {
|
|
237
237
|
const slash = Math.max(partial.lastIndexOf('/'), partial.lastIndexOf('\\'));
|
|
238
238
|
const dirPart = slash >= 0 ? partial.slice(0, slash) : '';
|
|
239
239
|
const prefix = (slash >= 0 ? partial.slice(slash + 1) : partial).toLowerCase();
|
|
@@ -250,7 +250,7 @@ function fileRefSuggestions(partial, cwd, limit = 6) {
|
|
|
250
250
|
const ref = (dirPart ? dirPart.replace(/\\/g, '/') + '/' : '') + e.name;
|
|
251
251
|
out.push({ ref, isDir: e.isDirectory() });
|
|
252
252
|
}
|
|
253
|
-
out.sort((a, b) =>
|
|
253
|
+
out.sort((a, b) => a.ref.toLowerCase().localeCompare(b.ref.toLowerCase())); // alphabetical, files + folders mixed
|
|
254
254
|
return out.slice(0, limit);
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -1003,22 +1003,6 @@ async function startChat(opts = {}) {
|
|
|
1003
1003
|
}
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
|
-
// @file completions โ when the token at the end of the line starts with @
|
|
1007
|
-
const atMatch = (line || '').match(/(?:^|\s)@([^\s]*)$/);
|
|
1008
|
-
if (atMatch) {
|
|
1009
|
-
const sugg = fileRefSuggestions(atMatch[1], process.cwd());
|
|
1010
|
-
for (const s of sugg) {
|
|
1011
|
-
const label = s.isDir ? s.ref + '/' : s.ref;
|
|
1012
|
-
const icon = s.isDir ? '๐' : '๐';
|
|
1013
|
-
process.stdout.write(`\x1b[B\r\x1b[2K ${c.brand('@')}${c.white(label.padEnd(30))}${c.dim(icon)}`);
|
|
1014
|
-
rows++;
|
|
1015
|
-
}
|
|
1016
|
-
if (sugg.length) {
|
|
1017
|
-
process.stdout.write(`\x1b[B\r\x1b[2K ${c.dim('tab to complete ยท attaches file contents as context')}`);
|
|
1018
|
-
rows++;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1006
|
process.stdout.write('\x1b[u');
|
|
1023
1007
|
_subRowCount = rows;
|
|
1024
1008
|
}
|
|
@@ -1032,6 +1016,158 @@ async function startChat(opts = {}) {
|
|
|
1032
1016
|
_subRowCount = 1;
|
|
1033
1017
|
}
|
|
1034
1018
|
|
|
1019
|
+
// โโ Interactive @file picker โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1020
|
+
// While open, it OWNS the keyboard (โ/โ/Enter/Tab/Esc/typing) so the
|
|
1021
|
+
// arrow keys don't fall through to readline's history. We swap readline's
|
|
1022
|
+
// keypress listener out for ours, then restore it when the picker closes.
|
|
1023
|
+
let _atActive = false;
|
|
1024
|
+
let _atItems = [];
|
|
1025
|
+
let _atSel = 0;
|
|
1026
|
+
let _atToken = '';
|
|
1027
|
+
let _atStart = 0;
|
|
1028
|
+
let _savedKp = null;
|
|
1029
|
+
let _atRows = 0; // rows currently drawn below the input by the picker
|
|
1030
|
+
let _atView = 0; // index of the first visible item (scrolling viewport)
|
|
1031
|
+
const AT_MAX_VISIBLE = 8;
|
|
1032
|
+
|
|
1033
|
+
function currentAtToken() {
|
|
1034
|
+
const line = rl.line || '';
|
|
1035
|
+
const before = line.slice(0, rl.cursor);
|
|
1036
|
+
const m = before.match(/(?:^|\s)@([^\s]*)$/);
|
|
1037
|
+
if (!m) return null;
|
|
1038
|
+
return { token: m[1], start: rl.cursor - m[1].length - 1 };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function refreshAtItems() {
|
|
1042
|
+
const t = currentAtToken();
|
|
1043
|
+
if (!t) return false;
|
|
1044
|
+
_atToken = t.token;
|
|
1045
|
+
_atStart = t.start;
|
|
1046
|
+
_atItems = fileRefSuggestions(_atToken, process.cwd(), 500); // fetch all; viewport scrolls
|
|
1047
|
+
_atView = 0;
|
|
1048
|
+
if (_atSel >= _atItems.length) _atSel = Math.max(0, _atItems.length - 1);
|
|
1049
|
+
return _atItems.length > 0;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Column of the readline cursor on the input line (visible prompt is 4 cols wide)
|
|
1053
|
+
function atInputCol() { return 4 + (rl.cursor || 0); }
|
|
1054
|
+
|
|
1055
|
+
// Return the terminal cursor to the input line, `fromRows` lines above us.
|
|
1056
|
+
// Uses RELATIVE moves so it survives the terminal scrolling when the picker
|
|
1057
|
+
// is drawn near the bottom of the screen (the bug where the hint overlapped @).
|
|
1058
|
+
function atMoveToInput(fromRows) {
|
|
1059
|
+
if (fromRows > 0) process.stdout.write(`\x1b[${fromRows}A`);
|
|
1060
|
+
process.stdout.write('\r');
|
|
1061
|
+
const col = atInputCol();
|
|
1062
|
+
if (col > 0) process.stdout.write(`\x1b[${col}C`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function clearAtRows() {
|
|
1066
|
+
if (_atRows > 0) {
|
|
1067
|
+
for (let i = 0; i < _atRows; i++) process.stdout.write('\x1b[B\r\x1b[2K');
|
|
1068
|
+
atMoveToInput(_atRows);
|
|
1069
|
+
_atRows = 0;
|
|
1070
|
+
}
|
|
1071
|
+
_subRowCount = 0;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function renderAtList() {
|
|
1075
|
+
clearAtRows(); // assumes cursor is on the input line
|
|
1076
|
+
const total = _atItems.length;
|
|
1077
|
+
// Keep the selected item inside the visible window
|
|
1078
|
+
if (_atSel < _atView) _atView = _atSel;
|
|
1079
|
+
if (_atSel >= _atView + AT_MAX_VISIBLE) _atView = _atSel - AT_MAX_VISIBLE + 1;
|
|
1080
|
+
const end = Math.min(_atView + AT_MAX_VISIBLE, total);
|
|
1081
|
+
|
|
1082
|
+
const lines = [];
|
|
1083
|
+
for (let i = _atView; i < end; i++) {
|
|
1084
|
+
const it = _atItems[i];
|
|
1085
|
+
const label = it.isDir ? it.ref + '/' : it.ref;
|
|
1086
|
+
const arrow = (i === end - 1 && end < total) ? c.dim(' โ')
|
|
1087
|
+
: (i === _atView && _atView > 0) ? c.dim(' โ') : '';
|
|
1088
|
+
lines.push((i === _atSel
|
|
1089
|
+
? c.brand('โฏ ') + c.bold(label)
|
|
1090
|
+
: c.muted(' ') + c.white(label)) + arrow);
|
|
1091
|
+
}
|
|
1092
|
+
const pos = total > AT_MAX_VISIBLE ? ` ยท ${_atSel + 1}/${total}` : '';
|
|
1093
|
+
lines.push(c.dim('โโ select ยท enter/tab insert ยท esc dismiss' + pos));
|
|
1094
|
+
|
|
1095
|
+
for (const ln of lines) process.stdout.write(`\x1b[B\r\x1b[2K ${ln}`);
|
|
1096
|
+
atMoveToInput(lines.length); // relative โ scroll-safe
|
|
1097
|
+
_atRows = lines.length;
|
|
1098
|
+
_subRowCount = lines.length;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function openAtPicker() {
|
|
1102
|
+
_atSel = 0;
|
|
1103
|
+
if (!refreshAtItems()) return;
|
|
1104
|
+
_atActive = true;
|
|
1105
|
+
// Take over keypress handling from readline
|
|
1106
|
+
_savedKp = process.stdin.listeners('keypress').slice();
|
|
1107
|
+
process.stdin.removeAllListeners('keypress');
|
|
1108
|
+
process.stdin.on('keypress', atKeyHandler);
|
|
1109
|
+
renderAtList();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function closeAtPicker() {
|
|
1113
|
+
if (!_atActive) return;
|
|
1114
|
+
_atActive = false;
|
|
1115
|
+
_atItems = [];
|
|
1116
|
+
_atSel = 0;
|
|
1117
|
+
if (_savedKp) {
|
|
1118
|
+
process.stdin.removeListener('keypress', atKeyHandler);
|
|
1119
|
+
for (const l of _savedKp) process.stdin.on('keypress', l);
|
|
1120
|
+
_savedKp = null;
|
|
1121
|
+
}
|
|
1122
|
+
clearAtRows();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function acceptAt() {
|
|
1126
|
+
const it = _atItems[_atSel];
|
|
1127
|
+
if (!it) { closeAtPicker(); return; }
|
|
1128
|
+
const insert = it.isDir ? it.ref + '/' : it.ref;
|
|
1129
|
+
const line = rl.line;
|
|
1130
|
+
const before = line.slice(0, _atStart + 1); // up to & incl. '@'
|
|
1131
|
+
const after = line.slice(_atStart + 1 + _atToken.length);
|
|
1132
|
+
const newLine = before + insert + after;
|
|
1133
|
+
rl.line = newLine;
|
|
1134
|
+
rl.cursor = (before + insert).length;
|
|
1135
|
+
rl._refreshLine();
|
|
1136
|
+
if (it.isDir) { // drill into the folder, keep picking
|
|
1137
|
+
_atSel = 0;
|
|
1138
|
+
if (refreshAtItems()) { renderAtList(); return; }
|
|
1139
|
+
}
|
|
1140
|
+
closeAtPicker();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function atKeyHandler(ch, key) {
|
|
1144
|
+
if (!key) return;
|
|
1145
|
+
if (key.name === 'up') { _atSel = (_atSel - 1 + _atItems.length) % _atItems.length; renderAtList(); return; }
|
|
1146
|
+
if (key.name === 'down') { _atSel = (_atSel + 1) % _atItems.length; renderAtList(); return; }
|
|
1147
|
+
if (key.name === 'escape') { closeAtPicker(); return; }
|
|
1148
|
+
if (key.name === 'tab' || key.name === 'return' || key.name === 'enter') { acceptAt(); return; }
|
|
1149
|
+
if (key.ctrl && key.name === 'c') { closeAtPicker(); process.exit(0); }
|
|
1150
|
+
if (key.name === 'backspace') {
|
|
1151
|
+
if (rl.cursor > 0) {
|
|
1152
|
+
rl.line = rl.line.slice(0, rl.cursor - 1) + rl.line.slice(rl.cursor);
|
|
1153
|
+
rl.cursor = rl.cursor - 1;
|
|
1154
|
+
rl._refreshLine();
|
|
1155
|
+
}
|
|
1156
|
+
if (!refreshAtItems()) { closeAtPicker(); return; }
|
|
1157
|
+
renderAtList();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
// Printable character โ insert it, keep filtering
|
|
1161
|
+
if (ch && !key.ctrl && !key.meta && ch.length === 1 && ch >= ' ') {
|
|
1162
|
+
rl.line = rl.line.slice(0, rl.cursor) + ch + rl.line.slice(rl.cursor);
|
|
1163
|
+
rl.cursor = rl.cursor + 1;
|
|
1164
|
+
rl._refreshLine();
|
|
1165
|
+
if (ch === ' ') { closeAtPicker(); return; } // space ends the @token
|
|
1166
|
+
if (!refreshAtItems()) { closeAtPicker(); return; }
|
|
1167
|
+
renderAtList();
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1035
1171
|
process.stdin.on('keypress', (ch, key) => {
|
|
1036
1172
|
if (!key) return;
|
|
1037
1173
|
|
|
@@ -1070,6 +1206,14 @@ async function startChat(opts = {}) {
|
|
|
1070
1206
|
// Redraw sub-rows on every keypress so backspace / typing never wipes them
|
|
1071
1207
|
_suggTimer = setTimeout(() => {
|
|
1072
1208
|
_suggTimer = null;
|
|
1209
|
+
// If the user just started an @file token, hand control to the picker
|
|
1210
|
+
if (!_atActive) {
|
|
1211
|
+
const t = currentAtToken();
|
|
1212
|
+
if (t && fileRefSuggestions(t.token, process.cwd(), 1).length) {
|
|
1213
|
+
openAtPicker();
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1073
1217
|
renderSubRows(rl.line || '');
|
|
1074
1218
|
}, 50);
|
|
1075
1219
|
});
|