caroushell 0.1.12 → 0.1.14

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.
@@ -5,6 +5,27 @@ exports.generateContent = generateContent;
5
5
  exports.listModels = listModels;
6
6
  const logs_1 = require("./logs");
7
7
  const config_1 = require("./config");
8
+ function debounceAsync(fn, delayMs) {
9
+ let tim = null;
10
+ let rejectLast = null;
11
+ return (...args) => {
12
+ if (tim)
13
+ clearTimeout(tim);
14
+ return new Promise((resolve, reject) => {
15
+ if (rejectLast) {
16
+ rejectLast(new Error("debounced"));
17
+ }
18
+ rejectLast = reject;
19
+ tim = setTimeout(() => {
20
+ tim = null;
21
+ rejectLast = null;
22
+ fn(...args)
23
+ .then(resolve)
24
+ .catch(reject);
25
+ }, delayMs);
26
+ });
27
+ };
28
+ }
8
29
  async function generateContent(prompt, options) {
9
30
  const apiKey = options?.apiKey ||
10
31
  process.env.CAROUSHELL_API_KEY ||
@@ -100,6 +121,8 @@ async function extractText(json) {
100
121
  class AISuggester {
101
122
  constructor(opts) {
102
123
  this.prefix = "🤖";
124
+ this.latestSuggestions = [];
125
+ this.debouncedSuggest = debounceAsync(this.runSuggestNow.bind(this), 350);
103
126
  this.apiKey = opts?.apiKey;
104
127
  this.apiUrl = opts?.apiUrl;
105
128
  this.model = opts?.model;
@@ -118,11 +141,29 @@ class AISuggester {
118
141
  descriptionForAi() {
119
142
  return "";
120
143
  }
121
- async suggest(carousel, maxDisplayed) {
144
+ latest() {
145
+ return this.latestSuggestions;
146
+ }
147
+ async refreshSuggestions(carousel, maxDisplayed) {
122
148
  if (!this.apiKey || !this.apiUrl || !this.model) {
123
149
  (0, logs_1.logLine)("AI generation skipped: missing API configuration");
124
- return [];
150
+ this.latestSuggestions = [];
151
+ carousel.render();
152
+ return;
125
153
  }
154
+ try {
155
+ const suggestions = await this.debouncedSuggest(carousel, maxDisplayed);
156
+ this.latestSuggestions = suggestions.slice(0, maxDisplayed);
157
+ }
158
+ catch (err) {
159
+ if (err?.message !== "debounced") {
160
+ (0, logs_1.logLine)("ai suggest error: " + err?.message);
161
+ }
162
+ this.latestSuggestions = [];
163
+ }
164
+ carousel.render();
165
+ }
166
+ async runSuggestNow(carousel, maxDisplayed) {
126
167
  const descriptions = [];
127
168
  for (const suggester of carousel.getSuggesters()) {
128
169
  const desc = suggester.descriptionForAi();
package/dist/app.js CHANGED
@@ -8,15 +8,6 @@ const history_suggester_1 = require("./history-suggester");
8
8
  const ai_suggester_1 = require("./ai-suggester");
9
9
  const file_suggester_1 = require("./file-suggester");
10
10
  const spawner_1 = require("./spawner");
11
- function debounce(fn, ms) {
12
- // Debounce function to limit the rate at which a function can fire
13
- let t = null;
14
- return (...args) => {
15
- if (t)
16
- clearTimeout(t);
17
- t = setTimeout(() => fn(...args), ms);
18
- };
19
- }
20
11
  class App {
21
12
  constructor() {
22
13
  this.usingFileSuggestions = false;
@@ -32,9 +23,9 @@ class App {
32
23
  bottomRows: 2,
33
24
  terminal: this.terminal,
34
25
  });
35
- this.queueUpdateSuggestions = debounce(async () => {
36
- await this.carousel.updateSuggestions();
37
- }, 300);
26
+ this.queueUpdateSuggestions = () => {
27
+ void this.carousel.updateSuggestions();
28
+ };
38
29
  this.handlers = {
39
30
  "ctrl-c": () => {
40
31
  if (this.carousel.isPromptRowSelected() && !this.carousel.hasInput()) {
@@ -142,7 +133,7 @@ class App {
142
133
  }
143
134
  async run() {
144
135
  await this.init();
145
- this.keyboard.start();
136
+ this.keyboard.enableCapture();
146
137
  this.keyboard.on("key", (evt) => {
147
138
  void this.handleKey(evt);
148
139
  });
@@ -174,7 +165,8 @@ class App {
174
165
  this.terminal.renderBlock(lines);
175
166
  // Ensure command output starts on the next line
176
167
  this.terminal.write("\n");
177
- this.keyboard.pause();
168
+ this.keyboard.disableCapture();
169
+ this.terminal.disableWrites();
178
170
  try {
179
171
  const storeInHistory = await (0, spawner_1.runUserCommand)(cmd);
180
172
  if (storeInHistory) {
@@ -182,13 +174,14 @@ class App {
182
174
  }
183
175
  }
184
176
  finally {
185
- this.keyboard.resume();
177
+ this.terminal.enableWrites();
178
+ this.keyboard.enableCapture();
186
179
  }
187
180
  }
188
181
  exit() {
189
182
  // Clear terminal contents before shutting down to leave a clean screen.
190
183
  this.terminal.renderBlock([]);
191
- this.keyboard.stop();
184
+ this.keyboard.disableCapture();
192
185
  process.exit(0);
193
186
  }
194
187
  async tryAutocompleteFile() {
package/dist/carousel.js CHANGED
@@ -5,61 +5,49 @@ const logs_1 = require("./logs");
5
5
  const terminal_1 = require("./terminal");
6
6
  class Carousel {
7
7
  constructor(opts) {
8
- this.latestTop = [];
9
- this.latestBottom = [];
10
8
  this.index = 0;
11
9
  this.inputBuffer = "";
12
10
  this.inputCursor = 0;
13
- this.emptyRow = "---";
14
11
  this.terminal = opts.terminal;
15
12
  this.top = opts.top;
16
13
  this.bottom = opts.bottom;
17
14
  this.topRowCount = opts.topRows;
18
15
  this.bottomRowCount = opts.bottomRows;
19
- this.latestTop = this.createEmptyRows(this.topRowCount);
20
- this.latestBottom = this.createEmptyRows(this.bottomRowCount);
21
- }
22
- createEmptyRows(count) {
23
- return Array(count).fill(this.emptyRow);
24
16
  }
25
17
  async updateSuggestions(input) {
26
18
  if (typeof input === "string") {
27
19
  this.setInputBuffer(input);
28
20
  }
29
- const topPromise = this.top.suggest(this, this.topRowCount);
30
- const bottomPromise = this.bottom.suggest(this, this.bottomRowCount);
31
- void topPromise.then((r) => {
32
- this.latestTop = r;
33
- this.render();
34
- });
35
- void bottomPromise.then((r) => {
36
- this.latestBottom = r;
37
- this.render();
38
- });
21
+ void this.top.refreshSuggestions(this, this.topRowCount);
22
+ void this.bottom.refreshSuggestions(this, this.bottomRowCount);
39
23
  }
40
24
  up() {
41
25
  this.index += 1;
42
- if (this.index >= this.latestTop.length) {
43
- this.index = this.latestTop.length;
26
+ const topLength = this.top.latest().length;
27
+ if (this.index >= topLength) {
28
+ this.index = topLength;
44
29
  }
45
30
  }
46
31
  down() {
47
32
  this.index -= 1;
48
- if (-this.index >= this.latestBottom.length) {
49
- this.index = -this.latestBottom.length;
33
+ const bottomLength = this.bottom.latest().length;
34
+ if (-this.index >= bottomLength) {
35
+ this.index = -bottomLength;
50
36
  }
51
37
  }
52
38
  getRow(rowIndex) {
39
+ const latestTop = this.top.latest();
40
+ const latestBottom = this.bottom.latest();
53
41
  if (rowIndex < 0) {
54
42
  const bottomIndex = -rowIndex - 1;
55
- return this.latestBottom[bottomIndex] || "";
43
+ return latestBottom[bottomIndex] || "";
56
44
  }
57
45
  if (rowIndex === 0) {
58
46
  return this.inputBuffer;
59
47
  }
60
48
  if (rowIndex > 0) {
61
49
  const topIndex = rowIndex - 1;
62
- return this.latestTop[topIndex] || "";
50
+ return latestTop[topIndex] || "";
63
51
  }
64
52
  return "";
65
53
  }
@@ -259,7 +247,10 @@ class Carousel {
259
247
  if (this.top === suggester)
260
248
  return;
261
249
  this.top = suggester;
262
- this.latestTop = this.createEmptyRows(this.topRowCount);
250
+ if (this.index > 0) {
251
+ const topLength = this.top.latest().length;
252
+ this.index = Math.min(this.index, topLength);
253
+ }
263
254
  }
264
255
  getSuggesters() {
265
256
  return [this.top, this.bottom];
@@ -12,6 +12,7 @@ class FileSuggester {
12
12
  constructor() {
13
13
  this.prefix = "📂";
14
14
  this.files = [];
15
+ this.latestSuggestions = [];
15
16
  }
16
17
  async init() {
17
18
  await this.refreshFiles();
@@ -75,10 +76,13 @@ class FileSuggester {
75
76
  const converted = dirDisplay.replace(/\//g, path_1.default.sep);
76
77
  return path_1.default.resolve(process.cwd(), converted);
77
78
  }
78
- async suggest(carousel, maxDisplayed) {
79
+ latest() {
80
+ return this.latestSuggestions;
81
+ }
82
+ async refreshSuggestions(carousel, maxDisplayed) {
79
83
  const { prefix } = carousel.getWordInfoAtCursor();
80
- const matches = await this.getMatchingFiles(prefix);
81
- return matches;
84
+ this.latestSuggestions = await this.getMatchingFiles(prefix);
85
+ carousel.render();
82
86
  }
83
87
  async findUniqueMatch(prefix) {
84
88
  const normalized = prefix.trim();
@@ -41,6 +41,7 @@ const fs_1 = require("fs");
41
41
  const path = __importStar(require("path"));
42
42
  const readline_1 = __importDefault(require("readline"));
43
43
  const ai_suggester_1 = require("./ai-suggester");
44
+ const preferredModels = ["gemini-2.5-flash-lite", "gpt-4o-mini"];
44
45
  function isInteractive() {
45
46
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
46
47
  }
@@ -49,6 +50,17 @@ async function prompt(question, rl) {
49
50
  rl.question(question, (answer) => resolve(answer));
50
51
  });
51
52
  }
53
+ function findShortestMatches(models, preferredList) {
54
+ const matches = [];
55
+ for (const pref of preferredList) {
56
+ const hits = models.filter((modelId) => modelId.includes(pref));
57
+ if (hits.length) {
58
+ const shortest = hits.reduce((best, candidate) => candidate.length < best.length ? candidate : best);
59
+ matches.push(shortest);
60
+ }
61
+ }
62
+ return [...new Set(matches)];
63
+ }
52
64
  async function runHelloNewUserFlow(configPath) {
53
65
  if (!isInteractive()) {
54
66
  throw new Error(`Missing config at ${configPath} and no interactive terminal is available.\n` +
@@ -92,10 +104,17 @@ async function runHelloNewUserFlow(configPath) {
92
104
  }
93
105
  const models = await (0, ai_suggester_1.listModels)(apiUrl, apiKey);
94
106
  if (models.length > 0) {
107
+ const preferred = findShortestMatches(models, preferredModels);
95
108
  console.log("Here are a few example model ids from your api service. Choose a fast and cheap model because AI suggestions happen as you type.");
96
109
  for (const model of models.slice(0, 5)) {
97
110
  console.log(` - ${model}`);
98
111
  }
112
+ if (preferred.length) {
113
+ console.log("Recommended models from your provider:");
114
+ for (const model of preferred) {
115
+ console.log(` - ${model}`);
116
+ }
117
+ }
99
118
  }
100
119
  let model = "";
101
120
  while (!model) {
@@ -44,24 +44,31 @@ class HistorySuggester {
44
44
  .catch(() => { });
45
45
  await fs_1.promises.appendFile(this.filePath, this.serializeHistoryEntry(command), "utf8");
46
46
  }
47
- async suggest(carousel, maxDisplayed) {
47
+ latest() {
48
+ return this.items;
49
+ }
50
+ async refreshSuggestions(carousel, maxDisplayed) {
48
51
  const input = carousel.getCurrentRow();
52
+ let results;
49
53
  if (!input) {
50
54
  // this.items 0 index is newest
51
- return this.items;
55
+ results = this.items;
52
56
  }
53
- const q = input.toLowerCase();
54
- const matched = [];
55
- // iterate from newest to oldest so we skip older duplicates
56
- const seen = new Set();
57
- for (let i = 0; i < this.items.length; i++) {
58
- const it = this.items[i];
59
- if (it.toLowerCase().includes(q) && !seen.has(it)) {
60
- seen.add(it);
61
- matched.push(it);
57
+ else {
58
+ const q = input.toLowerCase();
59
+ const matched = [];
60
+ // iterate from newest to oldest so we skip older duplicates
61
+ const seen = new Set();
62
+ for (let i = 0; i < this.items.length; i++) {
63
+ const it = this.items[i];
64
+ if (it.toLowerCase().includes(q) && !seen.has(it)) {
65
+ seen.add(it);
66
+ matched.push(it);
67
+ }
62
68
  }
69
+ results = matched;
63
70
  }
64
- return matched;
71
+ carousel.render();
65
72
  }
66
73
  descriptionForAi() {
67
74
  const lines = [];
package/dist/keyboard.js CHANGED
@@ -50,38 +50,15 @@ for (const seq of Object.keys(KEYMAP)) {
50
50
  class Keyboard extends events_1.EventEmitter {
51
51
  constructor() {
52
52
  super(...arguments);
53
- this.active = false;
54
53
  this.capturing = false;
55
54
  this.buffer = '';
56
55
  this.stdin = process.stdin;
57
56
  this.onData = (data) => this.handleData(data);
58
57
  }
59
- start() {
60
- if (this.active)
61
- return;
62
- this.active = true;
63
- this.stdin.setEncoding('utf8');
64
- this.enableCapture();
65
- }
66
- stop() {
67
- if (!this.active)
68
- return;
69
- this.active = false;
70
- this.disableCapture();
71
- }
72
- pause() {
73
- if (!this.active)
74
- return;
75
- this.disableCapture();
76
- }
77
- resume() {
78
- if (!this.active)
79
- return;
80
- this.enableCapture();
81
- }
82
58
  enableCapture() {
83
59
  if (this.capturing)
84
60
  return;
61
+ this.stdin.setEncoding('utf8');
85
62
  if (this.stdin.isTTY)
86
63
  this.stdin.setRawMode(true);
87
64
  this.stdin.on('data', this.onData);
package/dist/terminal.js CHANGED
@@ -21,6 +21,16 @@ class Terminal {
21
21
  this.activeRows = 0;
22
22
  this.cursorRow = 0;
23
23
  this.cursorCol = 0;
24
+ this.writesDisabled = false;
25
+ }
26
+ disableWrites() {
27
+ this.writesDisabled = true;
28
+ }
29
+ enableWrites() {
30
+ this.writesDisabled = false;
31
+ }
32
+ canWrite() {
33
+ return !this.writesDisabled;
24
34
  }
25
35
  moveCursorToTopOfBlock() {
26
36
  if (this.activeRows === 0)
@@ -51,6 +61,8 @@ class Terminal {
51
61
  }
52
62
  }
53
63
  write(text) {
64
+ if (!this.canWrite())
65
+ return;
54
66
  this.out.write(text);
55
67
  }
56
68
  hideCursor() {
@@ -61,6 +73,8 @@ class Terminal {
61
73
  }
62
74
  // Render a block of lines by clearing previous block (if any) and writing fresh
63
75
  renderBlock(lines, cursorRow, cursorCol) {
76
+ if (!this.canWrite())
77
+ return;
64
78
  this.withCork(() => {
65
79
  this.moveCursorToTopOfBlock();
66
80
  if (this.activeRows > 0) {
@@ -68,9 +82,9 @@ class Terminal {
68
82
  readline_1.default.clearScreenDown(this.out);
69
83
  }
70
84
  for (let i = 0; i < lines.length; i++) {
71
- this.out.write(lines[i]);
85
+ this.write(lines[i]);
72
86
  if (i < lines.length - 1)
73
- this.out.write("\n");
87
+ this.write("\n");
74
88
  }
75
89
  this.activeRows = lines.length;
76
90
  this.cursorRow = Math.max(0, this.activeRows - 1);
@@ -87,6 +101,8 @@ class Terminal {
87
101
  });
88
102
  }
89
103
  moveCursorTo(lineIndex, column) {
104
+ if (!this.canWrite())
105
+ return;
90
106
  if (this.activeRows === 0)
91
107
  return;
92
108
  const safeLine = Math.min(Math.max(lineIndex, 0), Math.max(0, this.activeRows - 1));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caroushell",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Terminal carousel that suggests commands from history, config, and AI.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/main.js",