sapper-iq 1.1.29 → 1.1.31

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/sapper.mjs CHANGED
@@ -348,7 +348,7 @@ function getFilesForPicker(dir = '.', prefix = '', maxFiles = 50) {
348
348
  return files.slice(0, maxFiles);
349
349
  }
350
350
 
351
- // Interactive file picker
351
+ // Interactive file picker with arrow keys
352
352
  async function pickFiles() {
353
353
  const files = getFilesForPicker('.', '', 50).filter(f => !f.isDir);
354
354
 
@@ -357,56 +357,117 @@ async function pickFiles() {
357
357
  return [];
358
358
  }
359
359
 
360
- console.log();
361
- console.log(box(
362
- `Select files by number (e.g., ${chalk.cyan('1 3 5')} or ${chalk.cyan('1-5')} or ${chalk.cyan('all')})\n` +
363
- `Press ${chalk.cyan('Enter')} with no input to cancel`,
364
- 'šŸ“Ž File Picker', 'cyan'
365
- ));
366
- console.log();
367
-
368
- // Display files in columns
369
- files.forEach((file, i) => {
370
- const num = chalk.cyan.bold(`[${(i + 1).toString().padStart(2)}]`);
371
- const size = file.size ? chalk.gray(`(${Math.round(file.size/1024)}KB)`) : '';
372
- console.log(` ${num} ${chalk.white(file.path)} ${size}`);
373
- });
360
+ const selected = new Set();
361
+ let cursor = 0;
362
+ const pageSize = Math.min(15, process.stdout.rows - 10 || 15);
374
363
 
375
- console.log();
376
- const selection = await safeQuestion(chalk.cyan('Select files: '));
377
-
378
- if (!selection.trim()) {
379
- console.log(chalk.gray('Cancelled.'));
380
- return [];
364
+ // Enable raw mode for key capture
365
+ if (process.stdin.isTTY) {
366
+ process.stdin.setRawMode(true);
381
367
  }
368
+ process.stdin.resume();
382
369
 
383
- // Parse selection
384
- const selectedFiles = [];
385
- const parts = selection.toLowerCase().split(/[\s,]+/);
386
-
387
- for (const part of parts) {
388
- if (part === 'all') {
389
- return files.map(f => f.path);
370
+ const renderList = () => {
371
+ // Clear screen and move cursor to top
372
+ console.clear();
373
+ console.log(box(
374
+ `${chalk.cyan('↑↓')} Navigate ${chalk.cyan('Space')} Toggle ${chalk.cyan('a')} All ${chalk.cyan('Enter')} Confirm ${chalk.cyan('q/Esc')} Cancel`,
375
+ 'šŸ“Ž Select Files', 'cyan'
376
+ ));
377
+ console.log();
378
+
379
+ // Calculate visible range (pagination)
380
+ const startIdx = Math.max(0, Math.min(cursor - Math.floor(pageSize / 2), files.length - pageSize));
381
+ const endIdx = Math.min(startIdx + pageSize, files.length);
382
+
383
+ // Show scroll indicator if needed
384
+ if (startIdx > 0) {
385
+ console.log(chalk.gray(' ↑ more files above...'));
390
386
  }
391
387
 
392
- // Handle ranges like "1-5"
393
- const rangeMatch = part.match(/^(\d+)-(\d+)$/);
394
- if (rangeMatch) {
395
- const start = parseInt(rangeMatch[1]);
396
- const end = parseInt(rangeMatch[2]);
397
- for (let i = start; i <= end && i <= files.length; i++) {
398
- if (i >= 1) selectedFiles.push(files[i - 1].path);
399
- }
400
- } else {
401
- // Single number
402
- const num = parseInt(part);
403
- if (num >= 1 && num <= files.length) {
404
- selectedFiles.push(files[num - 1].path);
405
- }
388
+ for (let i = startIdx; i < endIdx; i++) {
389
+ const file = files[i];
390
+ const isSelected = selected.has(i);
391
+ const isCursor = i === cursor;
392
+
393
+ const checkbox = isSelected ? chalk.green('ā—‰') : chalk.gray('ā—‹');
394
+ const prefix = isCursor ? chalk.cyan('ā–ø ') : ' ';
395
+ const name = isCursor ? chalk.cyan.bold(file.path) : chalk.white(file.path);
396
+ const size = file.size ? chalk.gray(` (${Math.round(file.size/1024)}KB)`) : '';
397
+
398
+ console.log(`${prefix}${checkbox} ${name}${size}`);
406
399
  }
407
- }
400
+
401
+ if (endIdx < files.length) {
402
+ console.log(chalk.gray(' ↓ more files below...'));
403
+ }
404
+
405
+ console.log();
406
+ console.log(chalk.gray(` Selected: ${selected.size} file${selected.size !== 1 ? 's' : ''}`));
407
+ };
408
408
 
409
- return [...new Set(selectedFiles)]; // Remove duplicates
409
+ return new Promise((resolve) => {
410
+ renderList();
411
+
412
+ const onKeypress = (chunk, key) => {
413
+ if (!key) {
414
+ // Handle raw chunk for arrow keys
415
+ const str = chunk.toString();
416
+ if (str === '\x1b[A') key = { name: 'up' };
417
+ else if (str === '\x1b[B') key = { name: 'down' };
418
+ else if (str === '\x1b[C') key = { name: 'right' };
419
+ else if (str === '\x1b[D') key = { name: 'left' };
420
+ else if (str === ' ') key = { name: 'space' };
421
+ else if (str === '\r' || str === '\n') key = { name: 'return' };
422
+ else if (str === '\x1b' || str === 'q') key = { name: 'escape' };
423
+ else if (str === 'a' || str === 'A') key = { name: 'a' };
424
+ else if (str === '\x03') key = { name: 'c', ctrl: true }; // Ctrl+C
425
+ }
426
+
427
+ if (!key) return;
428
+
429
+ if (key.name === 'up' || key.name === 'k') {
430
+ cursor = cursor > 0 ? cursor - 1 : files.length - 1;
431
+ renderList();
432
+ } else if (key.name === 'down' || key.name === 'j') {
433
+ cursor = cursor < files.length - 1 ? cursor + 1 : 0;
434
+ renderList();
435
+ } else if (key.name === 'space' || key.name === 'right') {
436
+ if (selected.has(cursor)) {
437
+ selected.delete(cursor);
438
+ } else {
439
+ selected.add(cursor);
440
+ }
441
+ renderList();
442
+ } else if (key.name === 'a') {
443
+ // Toggle all
444
+ if (selected.size === files.length) {
445
+ selected.clear();
446
+ } else {
447
+ for (let i = 0; i < files.length; i++) selected.add(i);
448
+ }
449
+ renderList();
450
+ } else if (key.name === 'return') {
451
+ cleanup();
452
+ const selectedFiles = Array.from(selected).map(i => files[i].path);
453
+ console.log(chalk.green(`\nāœ“ Selected ${selectedFiles.length} file${selectedFiles.length !== 1 ? 's' : ''}`));
454
+ resolve(selectedFiles);
455
+ } else if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
456
+ cleanup();
457
+ console.log(chalk.gray('\nCancelled.'));
458
+ resolve([]);
459
+ }
460
+ };
461
+
462
+ const cleanup = () => {
463
+ process.stdin.removeListener('data', onKeypress);
464
+ if (process.stdin.isTTY) {
465
+ process.stdin.setRawMode(false);
466
+ }
467
+ };
468
+
469
+ process.stdin.on('data', onKeypress);
470
+ });
410
471
  }
411
472
 
412
473
  // Format scan results for AI context
@@ -1000,7 +1061,10 @@ TOOL SYNTAX:
1000
1061
  spinner.stop();
1001
1062
 
1002
1063
  let msg = '';
1003
- const MAX_RESPONSE_LENGTH = 29000; // Guard against infinite loops (increased for multi-file reads)
1064
+ const MAX_RESPONSE_LENGTH = 100000; // 100KB - allow long code generation
1065
+ let lastChunkTime = Date.now();
1066
+ let repetitionCount = 0;
1067
+ let lastContent = '';
1004
1068
  abortStream = false; // Reset abort flag before streaming
1005
1069
 
1006
1070
  console.log(chalk.magenta('ā”Œā”€[') + chalk.white.bold('Sapper') + chalk.magenta(']'));
@@ -1016,9 +1080,27 @@ TOOL SYNTAX:
1016
1080
  process.stdout.write(content);
1017
1081
  msg += content;
1018
1082
 
1083
+ // Smart loop detection: check for repetitive content patterns
1084
+ if (msg.length > 10000) {
1085
+ const recentContent = msg.slice(-500);
1086
+ const previousContent = msg.slice(-1000, -500);
1087
+
1088
+ // If last 500 chars are very similar to previous 500, might be looping
1089
+ if (recentContent === previousContent) {
1090
+ repetitionCount++;
1091
+ if (repetitionCount > 3) {
1092
+ console.log(chalk.red('\n\nāš ļø REPETITIVE OUTPUT DETECTED: Stopping to prevent loop.'));
1093
+ break;
1094
+ }
1095
+ } else {
1096
+ repetitionCount = 0;
1097
+ }
1098
+ }
1099
+
1100
+ // Hard limit as final safety net
1019
1101
  if (msg.length > MAX_RESPONSE_LENGTH) {
1020
- console.log(chalk.red('\n\nāš ļø RESPONSE TOO LONG: Forcing stop to prevent infinite loop.'));
1021
- break;
1102
+ console.log(chalk.yellow('\n\nāš ļø Response very long (100KB+). Continuing... (Ctrl+C to stop)'));
1103
+ // Don't break - just warn. User can Ctrl+C if needed
1022
1104
  }
1023
1105
  }
1024
1106
  console.log();