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.
- package/dist/ai-suggester.js +2 -2
- package/dist/app.js +78 -0
- package/dist/carousel.js +73 -3
- package/dist/file-suggester.js +55 -0
- package/dist/keyboard.js +5 -0
- package/package.json +1 -1
package/dist/ai-suggester.js
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
19
|
-
this.
|
|
20
|
-
|
|
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' },
|