caroushell 0.1.21 → 0.1.24
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 +1 -1
- package/dist/app.js +81 -22
- package/dist/carousel.js +245 -55
- package/dist/config.js +0 -4
- package/dist/hello-new-user.js +17 -5
- package/dist/history-suggester.js +2 -1
- package/dist/keyboard.js +55 -34
- package/dist/main.js +7 -1
- package/dist/spawner.js +13 -4
- package/package.json +1 -1
package/dist/ai-suggester.js
CHANGED
|
@@ -80,7 +80,7 @@ async function generateContent(prompt, options) {
|
|
|
80
80
|
async function listModels(apiUrl, apiKey) {
|
|
81
81
|
const url = apiUrl.replace("/chat/completions", "") + "/models";
|
|
82
82
|
const res = await fetch(url, { headers: headers(apiKey) });
|
|
83
|
-
const models = await res.json();
|
|
83
|
+
const models = (await res.json());
|
|
84
84
|
return models.data.map((m) => m.id);
|
|
85
85
|
}
|
|
86
86
|
function headers(apiKey) {
|
package/dist/app.js
CHANGED
|
@@ -5,24 +5,26 @@ const terminal_1 = require("./terminal");
|
|
|
5
5
|
const keyboard_1 = require("./keyboard");
|
|
6
6
|
const carousel_1 = require("./carousel");
|
|
7
7
|
const history_suggester_1 = require("./history-suggester");
|
|
8
|
-
const ai_suggester_1 = require("./ai-suggester");
|
|
9
8
|
const file_suggester_1 = require("./file-suggester");
|
|
10
9
|
const spawner_1 = require("./spawner");
|
|
11
10
|
const logs_1 = require("./logs");
|
|
11
|
+
function collapseLineContinuations(input) {
|
|
12
|
+
return input.replace(/\\\r?\n/g, "");
|
|
13
|
+
}
|
|
12
14
|
class App {
|
|
13
15
|
constructor(deps = {}) {
|
|
14
16
|
this.usingFileSuggestions = false;
|
|
15
17
|
this.terminal = deps.terminal ?? new terminal_1.Terminal();
|
|
16
18
|
this.keyboard = deps.keyboard ?? new keyboard_1.Keyboard();
|
|
17
19
|
this.history = deps.topPanel ?? new history_suggester_1.HistorySuggester();
|
|
18
|
-
this.
|
|
20
|
+
this.bottomSuggester = deps.bottomPanel ?? new carousel_1.NullSuggester();
|
|
19
21
|
this.files = deps.files ?? new file_suggester_1.FileSuggester();
|
|
20
|
-
this.suggesters = deps.suggesters ?? [this.history, this.
|
|
22
|
+
this.suggesters = deps.suggesters ?? [this.history, this.bottomSuggester, this.files];
|
|
21
23
|
this.carousel = new carousel_1.Carousel({
|
|
22
24
|
top: this.history,
|
|
23
|
-
bottom: this.
|
|
25
|
+
bottom: this.bottomSuggester,
|
|
24
26
|
topRows: 2,
|
|
25
|
-
bottomRows: 2,
|
|
27
|
+
bottomRows: this.bottomSuggester instanceof carousel_1.NullSuggester ? 0 : 2,
|
|
26
28
|
terminal: this.terminal,
|
|
27
29
|
});
|
|
28
30
|
this.queueUpdateSuggestions = () => {
|
|
@@ -63,17 +65,12 @@ class App {
|
|
|
63
65
|
if (this.tryAcceptHighlightedFileSuggestion()) {
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.terminal.resetBlockTracking();
|
|
73
|
-
// Render the prompt, without this we'd wait for the suggestions to call render
|
|
74
|
-
// and it would appear slow
|
|
75
|
-
this.render();
|
|
76
|
-
this.queueUpdateSuggestions();
|
|
68
|
+
if (this.carousel.isPromptRowSelected()) {
|
|
69
|
+
await this.enterOnPrompt();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await this.enterOnSuggestion();
|
|
73
|
+
}
|
|
77
74
|
},
|
|
78
75
|
char: (evt) => {
|
|
79
76
|
this.carousel.insertAtCursor(evt.sequence);
|
|
@@ -83,10 +80,20 @@ class App {
|
|
|
83
80
|
this.queueUpdateSuggestions();
|
|
84
81
|
},
|
|
85
82
|
up: () => {
|
|
83
|
+
if (this.carousel.shouldUpMoveMultilineCursor()) {
|
|
84
|
+
this.carousel.moveMultilineCursorUp();
|
|
85
|
+
this.render();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
86
88
|
this.carousel.up();
|
|
87
89
|
this.render();
|
|
88
90
|
},
|
|
89
91
|
down: () => {
|
|
92
|
+
if (this.carousel.shouldDownMoveMultilineCursor()) {
|
|
93
|
+
this.carousel.moveMultilineCursorDown();
|
|
94
|
+
this.render();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
90
97
|
this.carousel.down();
|
|
91
98
|
this.render();
|
|
92
99
|
},
|
|
@@ -129,20 +136,28 @@ class App {
|
|
|
129
136
|
};
|
|
130
137
|
}
|
|
131
138
|
async init() {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
for (const s of this.suggesters) {
|
|
140
|
+
await s.init();
|
|
141
|
+
}
|
|
135
142
|
}
|
|
136
143
|
async run() {
|
|
137
144
|
await this.init();
|
|
138
145
|
this.keyboard.enableCapture();
|
|
139
|
-
this.
|
|
146
|
+
this.onKeyHandler = (evt) => {
|
|
140
147
|
void this.handleKey(evt);
|
|
141
|
-
}
|
|
148
|
+
};
|
|
149
|
+
this.keyboard.on("key", this.onKeyHandler);
|
|
142
150
|
// Initial draw
|
|
143
151
|
this.render();
|
|
144
152
|
await this.carousel.updateSuggestions();
|
|
145
153
|
}
|
|
154
|
+
end() {
|
|
155
|
+
if (this.onKeyHandler) {
|
|
156
|
+
this.keyboard.off("key", this.onKeyHandler);
|
|
157
|
+
this.onKeyHandler = undefined;
|
|
158
|
+
}
|
|
159
|
+
this.keyboard.disableCapture();
|
|
160
|
+
}
|
|
146
161
|
async handleKey(evt) {
|
|
147
162
|
const fn = this.handlers[evt.name];
|
|
148
163
|
if (fn) {
|
|
@@ -169,6 +184,7 @@ class App {
|
|
|
169
184
|
this.terminal.write("\n");
|
|
170
185
|
this.keyboard.disableCapture();
|
|
171
186
|
this.terminal.disableWrites();
|
|
187
|
+
await this.preBroadcastCommand(cmd);
|
|
172
188
|
try {
|
|
173
189
|
const storeInHistory = await (0, spawner_1.runUserCommand)(cmd);
|
|
174
190
|
if (storeInHistory) {
|
|
@@ -181,10 +197,40 @@ class App {
|
|
|
181
197
|
this.keyboard.enableCapture();
|
|
182
198
|
}
|
|
183
199
|
}
|
|
200
|
+
async enterOnPrompt() {
|
|
201
|
+
// Check for '\' line continuation
|
|
202
|
+
const lineInfo = this.carousel.getInputLineInfoAtCursor();
|
|
203
|
+
if (lineInfo.lineText.endsWith("\\") &&
|
|
204
|
+
lineInfo.column === lineInfo.lineText.length) {
|
|
205
|
+
this.carousel.insertAtCursor("\n");
|
|
206
|
+
this.render();
|
|
207
|
+
this.queueUpdateSuggestions();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const rawInput = this.carousel.getInputBuffer();
|
|
211
|
+
const cmd = collapseLineContinuations(rawInput).trim();
|
|
212
|
+
await this.confirmCommandRun(cmd);
|
|
213
|
+
}
|
|
214
|
+
async enterOnSuggestion() {
|
|
215
|
+
const cmd = this.carousel.getCurrentRow().trim();
|
|
216
|
+
await this.confirmCommandRun(cmd);
|
|
217
|
+
}
|
|
218
|
+
async confirmCommandRun(cmd) {
|
|
219
|
+
this.carousel.setInputBuffer("", 0);
|
|
220
|
+
await this.runCommand(cmd);
|
|
221
|
+
// Carousel should point to the prompt
|
|
222
|
+
this.carousel.resetIndex();
|
|
223
|
+
// After arbitrary output, reset render block tracking
|
|
224
|
+
this.terminal.resetBlockTracking();
|
|
225
|
+
// Render the prompt, without this we'd wait for the suggestions to call render
|
|
226
|
+
// and it would appear slow
|
|
227
|
+
this.render();
|
|
228
|
+
this.queueUpdateSuggestions();
|
|
229
|
+
}
|
|
184
230
|
exit() {
|
|
185
231
|
// Clear terminal contents before shutting down to leave a clean screen.
|
|
186
232
|
this.terminal.renderBlock([]);
|
|
187
|
-
this.
|
|
233
|
+
this.end();
|
|
188
234
|
process.exit(0);
|
|
189
235
|
}
|
|
190
236
|
async tryAutocompleteFile() {
|
|
@@ -245,6 +291,19 @@ class App {
|
|
|
245
291
|
this.usingFileSuggestions = true;
|
|
246
292
|
this.carousel.setTopSuggester(this.files);
|
|
247
293
|
}
|
|
294
|
+
async preBroadcastCommand(cmd) {
|
|
295
|
+
const listeners = this.suggesters
|
|
296
|
+
.map((suggester) => suggester.onCommandWillRun?.(cmd))
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
if (listeners.length === 0)
|
|
299
|
+
return;
|
|
300
|
+
try {
|
|
301
|
+
await Promise.all(listeners);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
(0, logs_1.logLine)("suggester onCommandWillRun error: " + err?.message);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
248
307
|
async broadcastCommand(cmd) {
|
|
249
308
|
const listeners = this.suggesters
|
|
250
309
|
.map((suggester) => suggester.onCommandRan?.(cmd))
|
package/dist/carousel.js
CHANGED
|
@@ -1,13 +1,77 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Carousel = void 0;
|
|
3
|
+
exports.Carousel = exports.NullSuggester = void 0;
|
|
4
|
+
exports.getDisplayWidth = getDisplayWidth;
|
|
4
5
|
const logs_1 = require("./logs");
|
|
5
6
|
const terminal_1 = require("./terminal");
|
|
7
|
+
const ANSI_ESCAPE_REGEX = /\x1b\[[0-9;]*m/g;
|
|
8
|
+
const COMBINING_MARK_REGEX = /^\p{Mark}+$/u;
|
|
9
|
+
const EMOJI_REGEX = /\p{Extended_Pictographic}/u;
|
|
10
|
+
const GRAPHEME_SEGMENTER = typeof Intl !== "undefined" && "Segmenter" in Intl
|
|
11
|
+
? new Intl.Segmenter("en", { granularity: "grapheme" })
|
|
12
|
+
: null;
|
|
13
|
+
function isFullWidthCodePoint(codePoint) {
|
|
14
|
+
return (codePoint >= 0x1100 &&
|
|
15
|
+
(codePoint <= 0x115f ||
|
|
16
|
+
codePoint === 0x2329 ||
|
|
17
|
+
codePoint === 0x232a ||
|
|
18
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
19
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
20
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
21
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
22
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
23
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
24
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
25
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f6ff) ||
|
|
26
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
27
|
+
(codePoint >= 0x1fa70 && codePoint <= 0x1faff) ||
|
|
28
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
29
|
+
}
|
|
30
|
+
function getDisplayWidth(text) {
|
|
31
|
+
const stripped = text.replace(ANSI_ESCAPE_REGEX, "");
|
|
32
|
+
let width = 0;
|
|
33
|
+
if (GRAPHEME_SEGMENTER) {
|
|
34
|
+
for (const { segment } of GRAPHEME_SEGMENTER.segment(stripped)) {
|
|
35
|
+
if (!segment)
|
|
36
|
+
continue;
|
|
37
|
+
if (COMBINING_MARK_REGEX.test(segment))
|
|
38
|
+
continue;
|
|
39
|
+
if (EMOJI_REGEX.test(segment)) {
|
|
40
|
+
width += 2;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const codePoint = segment.codePointAt(0) ?? 0;
|
|
44
|
+
width += isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
45
|
+
}
|
|
46
|
+
return width;
|
|
47
|
+
}
|
|
48
|
+
for (const char of stripped) {
|
|
49
|
+
if (COMBINING_MARK_REGEX.test(char))
|
|
50
|
+
continue;
|
|
51
|
+
if (EMOJI_REGEX.test(char)) {
|
|
52
|
+
width += 2;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
56
|
+
width += isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
57
|
+
}
|
|
58
|
+
return width;
|
|
59
|
+
}
|
|
60
|
+
class NullSuggester {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.prefix = "";
|
|
63
|
+
}
|
|
64
|
+
async init() { }
|
|
65
|
+
async refreshSuggestions() { }
|
|
66
|
+
latest() { return []; }
|
|
67
|
+
descriptionForAi() { return ""; }
|
|
68
|
+
}
|
|
69
|
+
exports.NullSuggester = NullSuggester;
|
|
6
70
|
class Carousel {
|
|
7
71
|
constructor(opts) {
|
|
8
72
|
this.index = 0;
|
|
9
73
|
this.inputBuffer = "";
|
|
10
|
-
this.
|
|
74
|
+
this.cursorIndex = 0;
|
|
11
75
|
this.terminal = opts.terminal;
|
|
12
76
|
this.top = opts.top;
|
|
13
77
|
this.bottom = opts.bottom;
|
|
@@ -27,6 +91,7 @@ class Carousel {
|
|
|
27
91
|
if (this.index >= topLength) {
|
|
28
92
|
this.index = topLength;
|
|
29
93
|
}
|
|
94
|
+
this.clampCursorToActiveRow();
|
|
30
95
|
}
|
|
31
96
|
down() {
|
|
32
97
|
this.index -= 1;
|
|
@@ -34,6 +99,7 @@ class Carousel {
|
|
|
34
99
|
if (-this.index >= bottomLength) {
|
|
35
100
|
this.index = -bottomLength;
|
|
36
101
|
}
|
|
102
|
+
this.clampCursorToActiveRow();
|
|
37
103
|
}
|
|
38
104
|
getRow(rowIndex) {
|
|
39
105
|
const latestTop = this.top.latest();
|
|
@@ -60,22 +126,32 @@ class Carousel {
|
|
|
60
126
|
}
|
|
61
127
|
return "$> ";
|
|
62
128
|
}
|
|
63
|
-
|
|
129
|
+
getFormattedSuggestionRow(rowIndex) {
|
|
64
130
|
const rowStr = this.getRow(rowIndex);
|
|
65
|
-
let prefix = this.
|
|
66
|
-
const {
|
|
131
|
+
let prefix = this.getSuggestionPrefix(rowIndex, rowStr);
|
|
132
|
+
const { reset, dim } = terminal_1.colors;
|
|
67
133
|
let color = dim;
|
|
68
134
|
if (this.index === rowIndex) {
|
|
69
135
|
color = terminal_1.colors.purple;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
136
|
+
}
|
|
137
|
+
return `${color}${prefix}${rowStr}${reset}`;
|
|
138
|
+
}
|
|
139
|
+
getSuggestionPrefix(rowIndex, rowStr) {
|
|
140
|
+
let prefix = this.getPrefixByIndex(rowIndex);
|
|
141
|
+
if (this.index === rowIndex && rowIndex !== 0) {
|
|
142
|
+
prefix = `${prefix}> `;
|
|
73
143
|
}
|
|
74
144
|
if (rowIndex !== 0 && !rowStr) {
|
|
75
145
|
// The edge of the top or bottom panel
|
|
76
146
|
prefix = "---";
|
|
77
147
|
}
|
|
78
|
-
return
|
|
148
|
+
return prefix;
|
|
149
|
+
}
|
|
150
|
+
getFormattedPromptRow(lineIndex, lineText, promptSelected) {
|
|
151
|
+
const { reset, dim } = terminal_1.colors;
|
|
152
|
+
const color = promptSelected ? terminal_1.colors.purple : dim;
|
|
153
|
+
const prefix = this.getPromptPrefix(lineIndex);
|
|
154
|
+
return `${color}${prefix}${lineText}${reset}`;
|
|
79
155
|
}
|
|
80
156
|
getCurrentRow() {
|
|
81
157
|
return this.getRow(this.index);
|
|
@@ -89,50 +165,67 @@ class Carousel {
|
|
|
89
165
|
}
|
|
90
166
|
setInputBuffer(value, cursorPos = value.length) {
|
|
91
167
|
this.inputBuffer = value;
|
|
92
|
-
this.
|
|
168
|
+
this.cursorIndex = Math.max(0, Math.min(cursorPos, this.inputBuffer.length));
|
|
169
|
+
}
|
|
170
|
+
getInputBuffer() {
|
|
171
|
+
return this.inputBuffer;
|
|
93
172
|
}
|
|
94
173
|
resetIndex() {
|
|
95
174
|
this.index = 0;
|
|
175
|
+
this.cursorIndex = Math.min(this.cursorIndex, this.inputBuffer.length);
|
|
96
176
|
}
|
|
97
177
|
adoptSelectionIntoInput() {
|
|
178
|
+
// When you highlighted a suggestion row (history/AI) and then type
|
|
179
|
+
// or edit, we want to pull that selected row into the input buffer
|
|
98
180
|
if (this.index === 0)
|
|
99
181
|
return;
|
|
100
182
|
const current = this.getRow(this.index);
|
|
101
|
-
this.setInputBuffer(current, current.length);
|
|
183
|
+
this.setInputBuffer(current, Math.min(this.cursorIndex, current.length));
|
|
102
184
|
this.index = 0;
|
|
103
185
|
}
|
|
186
|
+
getActiveRowLength() {
|
|
187
|
+
if (this.isPromptRowSelected())
|
|
188
|
+
return this.inputBuffer.length;
|
|
189
|
+
return this.getRow(this.index).length;
|
|
190
|
+
}
|
|
191
|
+
canMoveRight() {
|
|
192
|
+
return this.cursorIndex < this.getActiveRowLength();
|
|
193
|
+
}
|
|
194
|
+
clampCursorToActiveRow() {
|
|
195
|
+
const len = this.getActiveRowLength();
|
|
196
|
+
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, len));
|
|
197
|
+
}
|
|
104
198
|
insertAtCursor(text) {
|
|
105
199
|
if (!text)
|
|
106
200
|
return;
|
|
107
201
|
this.adoptSelectionIntoInput();
|
|
108
|
-
const before = this.inputBuffer.slice(0, this.
|
|
109
|
-
const after = this.inputBuffer.slice(this.
|
|
202
|
+
const before = this.inputBuffer.slice(0, this.cursorIndex);
|
|
203
|
+
const after = this.inputBuffer.slice(this.cursorIndex);
|
|
110
204
|
this.inputBuffer = `${before}${text}${after}`;
|
|
111
|
-
this.
|
|
205
|
+
this.cursorIndex += text.length;
|
|
112
206
|
}
|
|
113
207
|
deleteBeforeCursor() {
|
|
114
208
|
this.adoptSelectionIntoInput();
|
|
115
|
-
if (this.
|
|
209
|
+
if (this.cursorIndex === 0)
|
|
116
210
|
return;
|
|
117
|
-
const before = this.inputBuffer.slice(0, this.
|
|
118
|
-
const after = this.inputBuffer.slice(this.
|
|
211
|
+
const before = this.inputBuffer.slice(0, this.cursorIndex - 1);
|
|
212
|
+
const after = this.inputBuffer.slice(this.cursorIndex);
|
|
119
213
|
this.inputBuffer = `${before}${after}`;
|
|
120
|
-
this.
|
|
214
|
+
this.cursorIndex -= 1;
|
|
121
215
|
}
|
|
122
216
|
moveCursorLeft() {
|
|
123
|
-
this.
|
|
124
|
-
if (this.inputCursor === 0)
|
|
217
|
+
if (this.cursorIndex === 0)
|
|
125
218
|
return;
|
|
126
|
-
this.
|
|
219
|
+
this.cursorIndex -= 1;
|
|
127
220
|
}
|
|
128
221
|
isWhitespace(char) {
|
|
129
222
|
return /\s/.test(char);
|
|
130
223
|
}
|
|
131
224
|
moveCursorWordLeft() {
|
|
132
225
|
this.adoptSelectionIntoInput();
|
|
133
|
-
if (this.
|
|
226
|
+
if (this.cursorIndex === 0)
|
|
134
227
|
return;
|
|
135
|
-
let pos = this.
|
|
228
|
+
let pos = this.cursorIndex;
|
|
136
229
|
// Skip any whitespace directly to the left of the cursor
|
|
137
230
|
while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
138
231
|
pos -= 1;
|
|
@@ -141,19 +234,46 @@ class Carousel {
|
|
|
141
234
|
while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
142
235
|
pos -= 1;
|
|
143
236
|
}
|
|
144
|
-
this.
|
|
237
|
+
this.cursorIndex = pos;
|
|
145
238
|
}
|
|
146
239
|
moveCursorRight() {
|
|
240
|
+
if (!this.canMoveRight())
|
|
241
|
+
return;
|
|
242
|
+
this.cursorIndex += 1;
|
|
243
|
+
}
|
|
244
|
+
shouldUpMoveMultilineCursor() {
|
|
245
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
246
|
+
return this.isPromptRowSelected() && info.lineIndex > 0;
|
|
247
|
+
}
|
|
248
|
+
shouldDownMoveMultilineCursor() {
|
|
249
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
250
|
+
return this.isPromptRowSelected() && info.lineIndex < info.lines.length - 1;
|
|
251
|
+
}
|
|
252
|
+
moveMultilineCursorUp() {
|
|
253
|
+
this.adoptSelectionIntoInput();
|
|
254
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
255
|
+
if (info.lineIndex === 0)
|
|
256
|
+
return;
|
|
257
|
+
const targetIndex = info.lineIndex - 1;
|
|
258
|
+
const targetStart = this.getLineStartIndex(targetIndex, info.lines);
|
|
259
|
+
const targetLen = info.lines[targetIndex].length;
|
|
260
|
+
this.cursorIndex = targetStart + Math.min(info.column, targetLen);
|
|
261
|
+
}
|
|
262
|
+
moveMultilineCursorDown() {
|
|
147
263
|
this.adoptSelectionIntoInput();
|
|
148
|
-
|
|
264
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
265
|
+
if (info.lineIndex >= info.lines.length - 1)
|
|
149
266
|
return;
|
|
150
|
-
|
|
267
|
+
const targetIndex = info.lineIndex + 1;
|
|
268
|
+
const targetStart = this.getLineStartIndex(targetIndex, info.lines);
|
|
269
|
+
const targetLen = info.lines[targetIndex].length;
|
|
270
|
+
this.cursorIndex = targetStart + Math.min(info.column, targetLen);
|
|
151
271
|
}
|
|
152
272
|
moveCursorWordRight() {
|
|
153
273
|
this.adoptSelectionIntoInput();
|
|
154
|
-
if (this.
|
|
274
|
+
if (this.cursorIndex >= this.inputBuffer.length)
|
|
155
275
|
return;
|
|
156
|
-
let pos = this.
|
|
276
|
+
let pos = this.cursorIndex;
|
|
157
277
|
const len = this.inputBuffer.length;
|
|
158
278
|
// Skip any whitespace to the right of the cursor
|
|
159
279
|
while (pos < len && this.isWhitespace(this.inputBuffer[pos])) {
|
|
@@ -163,30 +283,35 @@ class Carousel {
|
|
|
163
283
|
while (pos < len && !this.isWhitespace(this.inputBuffer[pos])) {
|
|
164
284
|
pos += 1;
|
|
165
285
|
}
|
|
166
|
-
this.
|
|
286
|
+
this.cursorIndex = pos;
|
|
167
287
|
}
|
|
168
288
|
moveCursorHome() {
|
|
169
289
|
this.adoptSelectionIntoInput();
|
|
170
|
-
this.
|
|
290
|
+
this.cursorIndex = this.getLineInfoAtPosition(this.cursorIndex).lineStart;
|
|
171
291
|
}
|
|
172
292
|
moveCursorEnd() {
|
|
173
293
|
this.adoptSelectionIntoInput();
|
|
174
|
-
this.
|
|
294
|
+
this.cursorIndex = this.getLineInfoAtPosition(this.cursorIndex).lineEnd;
|
|
175
295
|
}
|
|
176
296
|
deleteAtCursor() {
|
|
177
297
|
this.adoptSelectionIntoInput();
|
|
178
|
-
if (this.
|
|
298
|
+
if (this.cursorIndex >= this.inputBuffer.length)
|
|
179
299
|
return;
|
|
180
|
-
const before = this.inputBuffer.slice(0, this.
|
|
181
|
-
const after = this.inputBuffer.slice(this.
|
|
300
|
+
const before = this.inputBuffer.slice(0, this.cursorIndex);
|
|
301
|
+
const after = this.inputBuffer.slice(this.cursorIndex + 1);
|
|
182
302
|
this.inputBuffer = `${before}${after}`;
|
|
183
303
|
}
|
|
184
304
|
deleteToLineStart() {
|
|
185
305
|
this.adoptSelectionIntoInput();
|
|
186
|
-
if (this.
|
|
306
|
+
if (this.cursorIndex === 0)
|
|
187
307
|
return;
|
|
188
|
-
const
|
|
189
|
-
|
|
308
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
309
|
+
if (info.column === 0)
|
|
310
|
+
return;
|
|
311
|
+
const before = this.inputBuffer.slice(0, info.lineStart);
|
|
312
|
+
const after = this.inputBuffer.slice(this.cursorIndex);
|
|
313
|
+
this.inputBuffer = `${before}${after}`;
|
|
314
|
+
this.cursorIndex = info.lineStart;
|
|
190
315
|
}
|
|
191
316
|
clearInput() {
|
|
192
317
|
this.adoptSelectionIntoInput();
|
|
@@ -200,14 +325,14 @@ class Carousel {
|
|
|
200
325
|
return this.index === 0;
|
|
201
326
|
}
|
|
202
327
|
getInputCursor() {
|
|
203
|
-
return this.
|
|
328
|
+
return this.cursorIndex;
|
|
204
329
|
}
|
|
205
330
|
getWordInfoAtCursor() {
|
|
206
|
-
let start = this.
|
|
331
|
+
let start = this.cursorIndex;
|
|
207
332
|
while (start > 0 && !this.isWhitespace(this.inputBuffer[start - 1])) {
|
|
208
333
|
start -= 1;
|
|
209
334
|
}
|
|
210
|
-
let end = this.
|
|
335
|
+
let end = this.cursorIndex;
|
|
211
336
|
const len = this.inputBuffer.length;
|
|
212
337
|
while (end < len && !this.isWhitespace(this.inputBuffer[end])) {
|
|
213
338
|
end += 1;
|
|
@@ -215,33 +340,98 @@ class Carousel {
|
|
|
215
340
|
return {
|
|
216
341
|
start,
|
|
217
342
|
end,
|
|
218
|
-
prefix: this.inputBuffer.slice(start, this.
|
|
343
|
+
prefix: this.inputBuffer.slice(start, this.cursorIndex),
|
|
219
344
|
word: this.inputBuffer.slice(start, end),
|
|
220
345
|
};
|
|
221
346
|
}
|
|
347
|
+
getInputLineInfoAtCursor() {
|
|
348
|
+
return this.getLineInfoAtPosition(this.cursorIndex);
|
|
349
|
+
}
|
|
222
350
|
getPromptCursorColumn() {
|
|
223
|
-
const
|
|
224
|
-
|
|
351
|
+
const info = this.getLineInfoAtPosition(this.cursorIndex);
|
|
352
|
+
const prefix = this.getPromptPrefix(info.lineIndex);
|
|
353
|
+
const linePrefix = info.lineText.slice(0, info.column);
|
|
354
|
+
return getDisplayWidth(prefix) + getDisplayWidth(linePrefix);
|
|
355
|
+
}
|
|
356
|
+
getLineInfoAtPosition(pos) {
|
|
357
|
+
// Map a buffer index to its line/column and line boundaries.
|
|
358
|
+
const lines = this.getInputLines();
|
|
359
|
+
let start = 0;
|
|
360
|
+
for (let i = 0; i < lines.length; i++) {
|
|
361
|
+
const line = lines[i];
|
|
362
|
+
const end = start + line.length;
|
|
363
|
+
if (pos <= end) {
|
|
364
|
+
// Cursor is on this line or at its end.
|
|
365
|
+
return {
|
|
366
|
+
lines,
|
|
367
|
+
lineIndex: i,
|
|
368
|
+
lineText: line,
|
|
369
|
+
lineStart: start,
|
|
370
|
+
lineEnd: end,
|
|
371
|
+
column: pos - start,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
start = end + 1;
|
|
375
|
+
}
|
|
376
|
+
const lastIndex = Math.max(0, lines.length - 1);
|
|
377
|
+
const lastStart = Math.max(0, this.inputBuffer.length - lines[lastIndex].length);
|
|
378
|
+
// Fallback when pos is beyond the buffer end.
|
|
379
|
+
return {
|
|
380
|
+
lines,
|
|
381
|
+
lineIndex: lastIndex,
|
|
382
|
+
lineText: lines[lastIndex] ?? "",
|
|
383
|
+
lineStart: lastStart,
|
|
384
|
+
lineEnd: lastStart + (lines[lastIndex]?.length ?? 0),
|
|
385
|
+
column: Math.max(0, pos - lastStart),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
getInputLines() {
|
|
389
|
+
return this.inputBuffer.split("\n");
|
|
390
|
+
}
|
|
391
|
+
getLineStartIndex(lineIndex, lines) {
|
|
392
|
+
let start = 0;
|
|
393
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
394
|
+
start += lines[i].length + 1;
|
|
395
|
+
}
|
|
396
|
+
return start;
|
|
397
|
+
}
|
|
398
|
+
getPromptPrefix(lineIndex) {
|
|
399
|
+
return lineIndex === 0 ? "$> " : "> ";
|
|
225
400
|
}
|
|
226
401
|
render() {
|
|
227
402
|
(0, logs_1.logLine)("Rendering carousel");
|
|
228
|
-
// Draw all the lines
|
|
229
403
|
const width = process.stdout.columns || 80;
|
|
230
|
-
const { brightWhite, reset, dim } = terminal_1.colors;
|
|
231
404
|
const lines = [];
|
|
232
|
-
const start = this.index + this.topRowCount;
|
|
233
405
|
const rowCount = this.topRowCount + this.bottomRowCount + 1;
|
|
406
|
+
const start = this.index + this.topRowCount;
|
|
234
407
|
const end = start - rowCount;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
408
|
+
const promptLines = this.getInputLines();
|
|
409
|
+
const promptSelected = this.index === 0;
|
|
410
|
+
const lineInfo = this.getLineInfoAtPosition(this.cursorIndex);
|
|
411
|
+
let cursorRow = 0;
|
|
412
|
+
let cursorCol = 0;
|
|
413
|
+
for (let rowIndex = start; rowIndex > end; rowIndex--) {
|
|
414
|
+
if (rowIndex === 0) {
|
|
415
|
+
for (let i = 0; i < promptLines.length; i++) {
|
|
416
|
+
if (this.index === 0 && i === lineInfo.lineIndex) {
|
|
417
|
+
cursorRow = lines.length;
|
|
418
|
+
cursorCol = this.getPromptCursorColumn();
|
|
419
|
+
}
|
|
420
|
+
lines.push(this.getFormattedPromptRow(i, promptLines[i], promptSelected));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
if (this.index === rowIndex) {
|
|
425
|
+
const rowStr = this.getRow(rowIndex);
|
|
426
|
+
const prefix = this.getSuggestionPrefix(rowIndex, rowStr);
|
|
427
|
+
cursorRow = lines.length;
|
|
428
|
+
const cursorText = rowStr.slice(0, Math.min(this.cursorIndex, rowStr.length));
|
|
429
|
+
cursorCol = getDisplayWidth(prefix) + getDisplayWidth(cursorText);
|
|
430
|
+
}
|
|
431
|
+
lines.push(this.getFormattedSuggestionRow(rowIndex));
|
|
432
|
+
}
|
|
240
433
|
}
|
|
241
|
-
|
|
242
|
-
const cursorRow = this.topRowCount;
|
|
243
|
-
const cursorCol = this.getPromptCursorColumn();
|
|
244
|
-
this.terminal.renderBlock(lines.map((line) => line.text), cursorRow, cursorCol);
|
|
434
|
+
this.terminal.renderBlock(lines.map((line) => line.slice(0, width - 2)), cursorRow, cursorCol);
|
|
245
435
|
}
|
|
246
436
|
setTopSuggester(suggester) {
|
|
247
437
|
if (this.top === suggester)
|
package/dist/config.js
CHANGED
|
@@ -88,7 +88,6 @@ async function doesConfigExist() {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
async function getConfig() {
|
|
91
|
-
const configPath = getConfigPath();
|
|
92
91
|
const raw = await readConfigFile();
|
|
93
92
|
const envApiKey = process.env.CAROUSHELL_API_KEY || process.env.GEMINI_API_KEY || undefined;
|
|
94
93
|
const envApiUrl = process.env.CAROUSHELL_API_URL || undefined;
|
|
@@ -107,9 +106,6 @@ async function getConfig() {
|
|
|
107
106
|
if (!resolved.model && geminiApiKey) {
|
|
108
107
|
resolved.model = GEMINI_DEFAULT_MODEL;
|
|
109
108
|
}
|
|
110
|
-
if (!resolved.apiUrl || !resolved.apiKey || !resolved.model) {
|
|
111
|
-
throw new Error(`Config at ${configPath} is missing required fields. Please include apiUrl, apiKey, and model (or just GEMINI_API_KEY).`);
|
|
112
|
-
}
|
|
113
109
|
return resolved;
|
|
114
110
|
}
|
|
115
111
|
function isGeminiUrl(url) {
|
package/dist/hello-new-user.js
CHANGED
|
@@ -70,7 +70,23 @@ async function runHelloNewUserFlow(configPath) {
|
|
|
70
70
|
await fs_1.promises.mkdir(dir, { recursive: true });
|
|
71
71
|
console.log("");
|
|
72
72
|
console.log("Welcome to Caroushell!");
|
|
73
|
-
console.log(
|
|
73
|
+
console.log("");
|
|
74
|
+
const rl = readline_1.default.createInterface({
|
|
75
|
+
input: process.stdin,
|
|
76
|
+
output: process.stdout,
|
|
77
|
+
});
|
|
78
|
+
const wantsAi = (await prompt("Do you want to set up AI auto-complete? (y/n): ", rl))
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
if (wantsAi !== "y" && wantsAi !== "yes") {
|
|
82
|
+
rl.close();
|
|
83
|
+
// Writing noAi or any key will skip the new user flow next run
|
|
84
|
+
await fs_1.promises.writeFile(configPath, "noAi = true\n", "utf8");
|
|
85
|
+
console.log("\nSkipping AI setup. You can set it up later by editing " + configPath);
|
|
86
|
+
console.log("");
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
console.log(`\nLet's set up AI suggestions. You'll need an API endpoint URL, a key, and model id. These will be stored at ${configPath}`);
|
|
74
90
|
console.log("");
|
|
75
91
|
console.log("Some example endpoints you can paste:");
|
|
76
92
|
console.log(" - OpenRouter: https://openrouter.ai/api/v1");
|
|
@@ -78,10 +94,6 @@ async function runHelloNewUserFlow(configPath) {
|
|
|
78
94
|
console.log(" - Google: https://generativelanguage.googleapis.com/v1beta/openai");
|
|
79
95
|
console.log("");
|
|
80
96
|
console.log("Press Ctrl+C any time to abort.\n");
|
|
81
|
-
const rl = readline_1.default.createInterface({
|
|
82
|
-
input: process.stdin,
|
|
83
|
-
output: process.stdout,
|
|
84
|
-
});
|
|
85
97
|
let apiUrl = "";
|
|
86
98
|
while (!apiUrl) {
|
|
87
99
|
const answer = (await prompt("API URL: ", rl)).trim();
|
|
@@ -48,9 +48,10 @@ class HistorySuggester {
|
|
|
48
48
|
.catch(() => { });
|
|
49
49
|
await fs_1.promises.appendFile(this.filePath, this.serializeHistoryEntry(command), "utf8");
|
|
50
50
|
}
|
|
51
|
-
async
|
|
51
|
+
async onCommandWillRun(command) {
|
|
52
52
|
await this.add(command);
|
|
53
53
|
}
|
|
54
|
+
async onCommandRan(command) { }
|
|
54
55
|
latest() {
|
|
55
56
|
return this.filteredItems;
|
|
56
57
|
}
|
package/dist/keyboard.js
CHANGED
|
@@ -1,45 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Keyboard = void 0;
|
|
3
|
+
exports.Keyboard = exports.KEY_SEQUENCES = void 0;
|
|
4
|
+
exports.keySequence = keySequence;
|
|
4
5
|
const events_1 = require("events");
|
|
5
|
-
// Map escape/control sequences
|
|
6
|
-
const
|
|
6
|
+
// Map semantic key names to escape/control sequences
|
|
7
|
+
const KEY_DEFINITIONS = {
|
|
7
8
|
// Control keys
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
|
|
12
|
-
'\r'
|
|
13
|
-
'\
|
|
14
|
-
|
|
15
|
-
'\u0008': { name: 'backspace' }, // BS (Windows)
|
|
16
|
-
'\u001b': { name: 'escape' },
|
|
9
|
+
'ctrl-c': [{ sequence: '\u0003', ctrl: true }], // ^C
|
|
10
|
+
'ctrl-d': [{ sequence: '\u0004', ctrl: true }], // ^D
|
|
11
|
+
'ctrl-u': [{ sequence: '\u0015', ctrl: true }], // ^U
|
|
12
|
+
tab: [{ sequence: '\t' }],
|
|
13
|
+
enter: [{ sequence: '\r' }, { sequence: '\n' }],
|
|
14
|
+
backspace: [{ sequence: '\u007f' }, { sequence: '\u0008' }], // DEL, BS (Windows)
|
|
15
|
+
escape: [{ sequence: '\u001b' }],
|
|
17
16
|
// Arrows (ANSI)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
up: [{ sequence: '\u001b[A' }],
|
|
18
|
+
down: [{ sequence: '\u001b[B' }],
|
|
19
|
+
right: [{ sequence: '\u001b[C' }],
|
|
20
|
+
left: [{ sequence: '\u001b[D' }],
|
|
21
|
+
'ctrl-right': [
|
|
22
|
+
{ sequence: '\u001b[1;5C', ctrl: true },
|
|
23
|
+
{ sequence: '\u001b[5C', ctrl: true },
|
|
24
|
+
// Option/Alt-based word jumps (macOS/iTerm send meta-modified arrows or ESC+b/f)
|
|
25
|
+
{ sequence: '\u001b[1;3C', meta: true },
|
|
26
|
+
{ sequence: '\u001b[1;9C', meta: true },
|
|
27
|
+
{ sequence: '\u001bf', meta: true },
|
|
28
|
+
],
|
|
29
|
+
'ctrl-left': [
|
|
30
|
+
{ sequence: '\u001b[1;5D', ctrl: true },
|
|
31
|
+
{ sequence: '\u001b[5D', ctrl: true },
|
|
32
|
+
// Option/Alt-based word jumps (macOS/iTerm send meta-modified arrows or ESC+b/f)
|
|
33
|
+
{ sequence: '\u001b[1;3D', meta: true },
|
|
34
|
+
{ sequence: '\u001b[1;9D', meta: true },
|
|
35
|
+
{ sequence: '\u001bb', meta: true },
|
|
36
|
+
],
|
|
33
37
|
// Home/End/Delete variants
|
|
34
|
-
'\u001b[H'
|
|
35
|
-
'\u001b[F'
|
|
36
|
-
|
|
37
|
-
'\u001b[4~': { name: 'end' },
|
|
38
|
-
'\u001b[3~': { name: 'delete' },
|
|
38
|
+
home: [{ sequence: '\u001b[H' }, { sequence: '\u001b[1~' }],
|
|
39
|
+
end: [{ sequence: '\u001b[F' }, { sequence: '\u001b[4~' }],
|
|
40
|
+
delete: [{ sequence: '\u001b[3~' }],
|
|
39
41
|
// Focus in/out (sent by some terminals on focus change - swallow these)
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
+
'focus-in': [{ sequence: '\u001b[I' }],
|
|
43
|
+
'focus-out': [{ sequence: '\u001b[O' }],
|
|
42
44
|
};
|
|
45
|
+
exports.KEY_SEQUENCES = Object.fromEntries(Object.entries(KEY_DEFINITIONS).map(([name, defs]) => [
|
|
46
|
+
name,
|
|
47
|
+
defs.map((def) => def.sequence),
|
|
48
|
+
]));
|
|
49
|
+
function keySequence(name) {
|
|
50
|
+
return exports.KEY_SEQUENCES[name][0];
|
|
51
|
+
}
|
|
52
|
+
// Map escape/control sequences to semantic key names
|
|
53
|
+
const KEYMAP = {};
|
|
54
|
+
for (const [name, defs] of Object.entries(KEY_DEFINITIONS)) {
|
|
55
|
+
for (const def of defs) {
|
|
56
|
+
KEYMAP[def.sequence] = {
|
|
57
|
+
name,
|
|
58
|
+
ctrl: def.ctrl,
|
|
59
|
+
meta: def.meta,
|
|
60
|
+
shift: def.shift,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
43
64
|
// For efficient prefix checks
|
|
44
65
|
const KEY_PREFIXES = new Set();
|
|
45
66
|
for (const seq of Object.keys(KEYMAP)) {
|
package/dist/main.js
CHANGED
|
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
const app_1 = require("./app");
|
|
7
|
+
const ai_suggester_1 = require("./ai-suggester");
|
|
8
|
+
const carousel_1 = require("./carousel");
|
|
7
9
|
const hello_new_user_1 = require("./hello-new-user");
|
|
8
10
|
const logs_1 = require("./logs");
|
|
9
11
|
const config_1 = require("./config");
|
|
@@ -25,7 +27,11 @@ async function main() {
|
|
|
25
27
|
if (!(await (0, config_1.doesConfigExist)())) {
|
|
26
28
|
await (0, hello_new_user_1.runHelloNewUserFlow)((0, config_1.getConfigPath)());
|
|
27
29
|
}
|
|
28
|
-
const
|
|
30
|
+
const config = await (0, config_1.getConfig)();
|
|
31
|
+
const bottomPanel = config.apiUrl && config.apiKey && config.model
|
|
32
|
+
? new ai_suggester_1.AISuggester()
|
|
33
|
+
: new carousel_1.NullSuggester();
|
|
34
|
+
const app = new app_1.App({ bottomPanel });
|
|
29
35
|
await app.run();
|
|
30
36
|
}
|
|
31
37
|
main().catch((err) => {
|
package/dist/spawner.js
CHANGED
|
@@ -144,10 +144,19 @@ async function runUserCommand(command) {
|
|
|
144
144
|
stdio: "inherit",
|
|
145
145
|
windowsVerbatimArguments: true,
|
|
146
146
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
// While a user command owns the terminal, Ctrl+C should interrupt that command
|
|
148
|
+
// without taking down the parent Caroushell process.
|
|
149
|
+
const ignoreSigint = () => { };
|
|
150
|
+
process.on("SIGINT", ignoreSigint);
|
|
151
|
+
try {
|
|
152
|
+
await new Promise((resolve, reject) => {
|
|
153
|
+
proc.on("error", reject);
|
|
154
|
+
proc.on("close", () => resolve());
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
process.off("SIGINT", ignoreSigint);
|
|
159
|
+
}
|
|
151
160
|
// Why save failed commands? Well eg sometimes we want to run a test
|
|
152
161
|
// many times until we fix it.
|
|
153
162
|
return true;
|