caroushell 0.1.7 → 0.1.8

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.
@@ -47,8 +47,8 @@ async function generateContent(prompt, options) {
47
47
  }
48
48
  const out = await extractText(await res.json());
49
49
  const text = typeof out === "string" ? out : "";
50
- const duration = Date.now() - start;
51
- (0, logs_1.logLine)(`AI duration: ${duration} ms`);
50
+ const duration = (Date.now() - start) / 1000;
51
+ (0, logs_1.logLine)(`AI duration: ${duration} seconds`);
52
52
  return text;
53
53
  }
54
54
  catch (err) {
package/dist/app.js CHANGED
@@ -6,6 +6,7 @@ const keyboard_1 = require("./keyboard");
6
6
  const carousel_1 = require("./carousel");
7
7
  const history_suggester_1 = require("./history-suggester");
8
8
  const ai_suggester_1 = require("./ai-suggester");
9
+ const file_suggester_1 = require("./file-suggester");
9
10
  const spawner_1 = require("./spawner");
10
11
  function debounce(fn, ms) {
11
12
  // Debounce function to limit the rate at which a function can fire
@@ -18,10 +19,12 @@ function debounce(fn, ms) {
18
19
  }
19
20
  class App {
20
21
  constructor() {
22
+ this.usingFileSuggestions = false;
21
23
  this.terminal = new terminal_1.Terminal();
22
24
  this.keyboard = new keyboard_1.Keyboard();
23
25
  this.history = new history_suggester_1.HistorySuggester();
24
26
  this.ai = new ai_suggester_1.AISuggester();
27
+ this.files = new file_suggester_1.FileSuggester();
25
28
  this.carousel = new carousel_1.Carousel({
26
29
  top: this.history,
27
30
  bottom: this.ai,
@@ -64,6 +67,9 @@ class App {
64
67
  this.queueUpdateSuggestions();
65
68
  },
66
69
  enter: async () => {
70
+ if (this.tryAcceptHighlightedFileSuggestion()) {
71
+ return;
72
+ }
67
73
  const cmd = this.carousel.getCurrentRow().trim();
68
74
  this.carousel.setInputBuffer("", 0);
69
75
  await this.runCommand(cmd);
@@ -99,6 +105,14 @@ class App {
99
105
  this.carousel.moveCursorRight();
100
106
  this.render();
101
107
  },
108
+ "ctrl-left": () => {
109
+ this.carousel.moveCursorWordLeft();
110
+ this.render();
111
+ },
112
+ "ctrl-right": () => {
113
+ this.carousel.moveCursorWordRight();
114
+ this.render();
115
+ },
102
116
  home: () => {
103
117
  this.carousel.moveCursorHome();
104
118
  this.render();
@@ -112,12 +126,19 @@ class App {
112
126
  this.render();
113
127
  this.queueUpdateSuggestions();
114
128
  },
129
+ tab: async () => {
130
+ const completed = await this.tryAutocompleteFile();
131
+ if (completed)
132
+ return;
133
+ this.toggleTopSuggester();
134
+ },
115
135
  escape: () => { },
116
136
  };
117
137
  }
118
138
  async init() {
119
139
  await this.history.init();
120
140
  await this.ai.init();
141
+ await this.files.init();
121
142
  }
122
143
  async run() {
123
144
  await this.init();
@@ -170,5 +191,62 @@ class App {
170
191
  this.keyboard.stop();
171
192
  process.exit(0);
172
193
  }
194
+ async tryAutocompleteFile() {
195
+ const wordInfo = this.carousel.getWordInfoAtCursor();
196
+ if (!wordInfo.prefix)
197
+ return false;
198
+ const match = await this.files.findUniqueMatch(wordInfo.prefix);
199
+ if (!match)
200
+ return false;
201
+ const current = this.carousel.getRow(0);
202
+ const before = current.slice(0, wordInfo.start);
203
+ const after = current.slice(wordInfo.end);
204
+ const next = `${before}${match}${after}`;
205
+ this.carousel.setInputBuffer(next, wordInfo.start + match.length);
206
+ this.render();
207
+ this.queueUpdateSuggestions();
208
+ return true;
209
+ }
210
+ tryAcceptHighlightedFileSuggestion() {
211
+ const currentSuggester = this.carousel.getCurrentRowSuggester();
212
+ if (currentSuggester !== this.files)
213
+ return false;
214
+ const suggestion = this.carousel.getCurrentRow();
215
+ if (!suggestion)
216
+ return false;
217
+ const wordInfo = this.carousel.getWordInfoAtCursor();
218
+ const current = this.carousel.getRow(0);
219
+ const before = current.slice(0, wordInfo.start);
220
+ const after = current.slice(wordInfo.end);
221
+ const nextInput = `${before}${suggestion}${after}`;
222
+ this.carousel.setInputBuffer(nextInput, wordInfo.start + suggestion.length);
223
+ this.carousel.resetIndex();
224
+ this.showHistorySuggestions();
225
+ this.render();
226
+ this.queueUpdateSuggestions();
227
+ return true;
228
+ }
229
+ toggleTopSuggester() {
230
+ if (this.usingFileSuggestions) {
231
+ this.showHistorySuggestions();
232
+ }
233
+ else {
234
+ this.showFileSuggestions();
235
+ }
236
+ this.render();
237
+ this.queueUpdateSuggestions();
238
+ }
239
+ showHistorySuggestions() {
240
+ if (!this.usingFileSuggestions)
241
+ return;
242
+ this.usingFileSuggestions = false;
243
+ this.carousel.setTopSuggester(this.history);
244
+ }
245
+ showFileSuggestions() {
246
+ if (this.usingFileSuggestions)
247
+ return;
248
+ this.usingFileSuggestions = true;
249
+ this.carousel.setTopSuggester(this.files);
250
+ }
173
251
  }
174
252
  exports.App = App;
package/dist/carousel.js CHANGED
@@ -10,14 +10,17 @@ class Carousel {
10
10
  this.index = 0;
11
11
  this.inputBuffer = "";
12
12
  this.inputCursor = 0;
13
+ this.emptyRow = "---";
13
14
  this.terminal = opts.terminal;
14
15
  this.top = opts.top;
15
16
  this.bottom = opts.bottom;
16
17
  this.topRowCount = opts.topRows;
17
18
  this.bottomRowCount = opts.bottomRows;
18
- const empty = "---";
19
- this.latestTop = Array(this.topRowCount).fill(empty);
20
- this.latestBottom = Array(this.bottomRowCount).fill(empty);
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);
21
24
  }
22
25
  async updateSuggestions(input) {
23
26
  if (typeof input === "string") {
@@ -89,6 +92,13 @@ class Carousel {
89
92
  getCurrentRow() {
90
93
  return this.getRow(this.index);
91
94
  }
95
+ getCurrentRowSuggester() {
96
+ if (this.index > 0)
97
+ return this.top;
98
+ if (this.index < 0)
99
+ return this.bottom;
100
+ return null;
101
+ }
92
102
  setInputBuffer(value, cursorPos = value.length) {
93
103
  this.inputBuffer = value;
94
104
  this.inputCursor = Math.max(0, Math.min(cursorPos, this.inputBuffer.length));
@@ -127,12 +137,46 @@ class Carousel {
127
137
  return;
128
138
  this.inputCursor -= 1;
129
139
  }
140
+ isWhitespace(char) {
141
+ return /\s/.test(char);
142
+ }
143
+ moveCursorWordLeft() {
144
+ this.adoptSelectionIntoInput();
145
+ if (this.inputCursor === 0)
146
+ return;
147
+ let pos = this.inputCursor;
148
+ // Skip any whitespace directly to the left of the cursor
149
+ while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
150
+ pos -= 1;
151
+ }
152
+ // Skip the word characters to the left
153
+ while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
154
+ pos -= 1;
155
+ }
156
+ this.inputCursor = pos;
157
+ }
130
158
  moveCursorRight() {
131
159
  this.adoptSelectionIntoInput();
132
160
  if (this.inputCursor >= this.inputBuffer.length)
133
161
  return;
134
162
  this.inputCursor += 1;
135
163
  }
164
+ moveCursorWordRight() {
165
+ this.adoptSelectionIntoInput();
166
+ if (this.inputCursor >= this.inputBuffer.length)
167
+ return;
168
+ let pos = this.inputCursor;
169
+ const len = this.inputBuffer.length;
170
+ // Skip any whitespace to the right of the cursor
171
+ while (pos < len && this.isWhitespace(this.inputBuffer[pos])) {
172
+ pos += 1;
173
+ }
174
+ // Skip through the next word
175
+ while (pos < len && !this.isWhitespace(this.inputBuffer[pos])) {
176
+ pos += 1;
177
+ }
178
+ this.inputCursor = pos;
179
+ }
136
180
  moveCursorHome() {
137
181
  this.adoptSelectionIntoInput();
138
182
  this.inputCursor = 0;
@@ -167,6 +211,26 @@ class Carousel {
167
211
  isPromptRowSelected() {
168
212
  return this.index === 0;
169
213
  }
214
+ getInputCursor() {
215
+ return this.inputCursor;
216
+ }
217
+ getWordInfoAtCursor() {
218
+ let start = this.inputCursor;
219
+ while (start > 0 && !this.isWhitespace(this.inputBuffer[start - 1])) {
220
+ start -= 1;
221
+ }
222
+ let end = this.inputCursor;
223
+ const len = this.inputBuffer.length;
224
+ while (end < len && !this.isWhitespace(this.inputBuffer[end])) {
225
+ end += 1;
226
+ }
227
+ return {
228
+ start,
229
+ end,
230
+ prefix: this.inputBuffer.slice(start, this.inputCursor),
231
+ word: this.inputBuffer.slice(start, end),
232
+ };
233
+ }
170
234
  getPromptCursorColumn() {
171
235
  const prefix = this.getPrefixByIndex(0);
172
236
  return prefix.length + this.inputCursor;
@@ -191,6 +255,12 @@ class Carousel {
191
255
  const cursorCol = this.getPromptCursorColumn();
192
256
  this.terminal.renderBlock(lines.map((line) => line.text), cursorRow, cursorCol);
193
257
  }
258
+ setTopSuggester(suggester) {
259
+ if (this.top === suggester)
260
+ return;
261
+ this.top = suggester;
262
+ this.latestTop = this.createEmptyRows(this.topRowCount);
263
+ }
194
264
  getSuggesters() {
195
265
  return [this.top, this.bottom];
196
266
  }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileSuggester = void 0;
4
+ const fs_1 = require("fs");
5
+ const maxFileAiLines = 10;
6
+ class FileSuggester {
7
+ constructor() {
8
+ this.prefix = "📂";
9
+ this.files = [];
10
+ }
11
+ async init() {
12
+ await this.refreshFiles();
13
+ }
14
+ async refreshFiles() {
15
+ try {
16
+ const entries = await fs_1.promises.readdir(process.cwd());
17
+ this.files = entries.sort((a, b) => a.localeCompare(b));
18
+ }
19
+ catch {
20
+ this.files = [];
21
+ }
22
+ }
23
+ async getMatchingFiles(queryRaw) {
24
+ await this.refreshFiles();
25
+ const query = queryRaw.trim().toLowerCase();
26
+ if (!query) {
27
+ return [...this.files];
28
+ }
29
+ return this.files.filter((file) => file.toLowerCase().indexOf(query.toLowerCase()) === 0);
30
+ }
31
+ async suggest(carousel, maxDisplayed) {
32
+ const { prefix } = carousel.getWordInfoAtCursor();
33
+ const matches = await this.getMatchingFiles(prefix);
34
+ return matches;
35
+ }
36
+ async findUniqueMatch(prefix) {
37
+ const normalized = prefix.trim();
38
+ if (!normalized)
39
+ return null;
40
+ const matches = await this.getMatchingFiles(normalized);
41
+ return matches.length === 1 ? matches[0] : null;
42
+ }
43
+ descriptionForAi() {
44
+ const filesForAi = this.files.slice(0, maxFileAiLines);
45
+ const list = filesForAi.length > 0 ? filesForAi.join("\n") : "(directory is empty)";
46
+ return `# File context
47
+
48
+ The current directory is ${process.cwd()}.
49
+
50
+ The files in the current directory are:
51
+
52
+ ${list}`;
53
+ }
54
+ }
55
+ exports.FileSuggester = FileSuggester;
package/dist/keyboard.js CHANGED
@@ -8,6 +8,7 @@ const KEYMAP = {
8
8
  '\u0003': { name: 'ctrl-c', ctrl: true }, // ^C
9
9
  '\u0004': { name: 'ctrl-d', ctrl: true }, // ^D
10
10
  '\u0015': { name: 'ctrl-u', ctrl: true }, // ^U
11
+ '\t': { name: 'tab' },
11
12
  '\r': { name: 'enter' },
12
13
  '\n': { name: 'enter' },
13
14
  '\u007f': { name: 'backspace' }, // DEL
@@ -18,6 +19,10 @@ const KEYMAP = {
18
19
  '\u001b[B': { name: 'down' },
19
20
  '\u001b[C': { name: 'right' },
20
21
  '\u001b[D': { name: 'left' },
22
+ '\u001b[1;5C': { name: 'ctrl-right', ctrl: true },
23
+ '\u001b[1;5D': { name: 'ctrl-left', ctrl: true },
24
+ '\u001b[5C': { name: 'ctrl-right', ctrl: true },
25
+ '\u001b[5D': { name: 'ctrl-left', ctrl: true },
21
26
  // Home/End/Delete variants
22
27
  '\u001b[H': { name: 'home' },
23
28
  '\u001b[F': { name: 'end' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caroushell",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Terminal carousel that suggests commands from history, config, and AI.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/main.js",