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.
Files changed (2) hide show
  1. package/bin/prior.js +163 -19
  2. 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, dirs first.
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 = 6) {
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) => (a.isDir === b.isDir ? a.ref.localeCompare(b.ref) : a.isDir ? -1 : 1));
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.7.12",
3
+ "version": "1.7.13",
4
4
  "description": "Prior Network AI โ€” command-line interface",
5
5
  "author": "Prior Network",
6
6
  "homepage": "https://priornetwork.com",