opencode-agora 0.3.0 → 0.4.0
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/README.md +217 -52
- package/dist/cli/app.d.ts +3 -0
- package/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +1053 -183
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/chat-renderer.d.ts +31 -0
- package/dist/cli/chat-renderer.d.ts.map +1 -0
- package/dist/cli/chat-renderer.js +275 -0
- package/dist/cli/chat-renderer.js.map +1 -0
- package/dist/cli/commands-meta.d.ts +21 -0
- package/dist/cli/commands-meta.d.ts.map +1 -0
- package/dist/cli/commands-meta.js +600 -0
- package/dist/cli/commands-meta.js.map +1 -0
- package/dist/cli/completions.d.ts +18 -0
- package/dist/cli/completions.d.ts.map +1 -0
- package/dist/cli/completions.js +190 -0
- package/dist/cli/completions.js.map +1 -0
- package/dist/cli/mcp-server.d.ts +4 -0
- package/dist/cli/mcp-server.d.ts.map +1 -0
- package/dist/cli/mcp-server.js +277 -0
- package/dist/cli/mcp-server.js.map +1 -0
- package/dist/cli/menu.d.ts +7 -0
- package/dist/cli/menu.d.ts.map +1 -0
- package/dist/cli/menu.js +164 -0
- package/dist/cli/menu.js.map +1 -0
- package/dist/cli/pages/community.d.ts +3 -0
- package/dist/cli/pages/community.d.ts.map +1 -0
- package/dist/cli/pages/community.js +276 -0
- package/dist/cli/pages/community.js.map +1 -0
- package/dist/cli/pages/helpers.d.ts +32 -0
- package/dist/cli/pages/helpers.d.ts.map +1 -0
- package/dist/cli/pages/helpers.js +67 -0
- package/dist/cli/pages/helpers.js.map +1 -0
- package/dist/cli/pages/home.d.ts +3 -0
- package/dist/cli/pages/home.d.ts.map +1 -0
- package/dist/cli/pages/home.js +148 -0
- package/dist/cli/pages/home.js.map +1 -0
- package/dist/cli/pages/marketplace.d.ts +3 -0
- package/dist/cli/pages/marketplace.d.ts.map +1 -0
- package/dist/cli/pages/marketplace.js +179 -0
- package/dist/cli/pages/marketplace.js.map +1 -0
- package/dist/cli/pages/news.d.ts +3 -0
- package/dist/cli/pages/news.d.ts.map +1 -0
- package/dist/cli/pages/news.js +561 -0
- package/dist/cli/pages/news.js.map +1 -0
- package/dist/cli/pages/settings.d.ts +3 -0
- package/dist/cli/pages/settings.d.ts.map +1 -0
- package/dist/cli/pages/settings.js +166 -0
- package/dist/cli/pages/settings.js.map +1 -0
- package/dist/cli/pages/types.d.ts +67 -0
- package/dist/cli/pages/types.d.ts.map +1 -0
- package/dist/cli/pages/types.js +2 -0
- package/dist/cli/pages/types.js.map +1 -0
- package/dist/cli/prompter.d.ts +135 -0
- package/dist/cli/prompter.d.ts.map +1 -0
- package/dist/cli/prompter.js +675 -0
- package/dist/cli/prompter.js.map +1 -0
- package/dist/cli/shell.d.ts +23 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +819 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.d.ts.map +1 -0
- package/dist/cli/tui.js +373 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +14 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +28 -0
- package/dist/commands.js.map +1 -0
- package/dist/community/client.d.ts +47 -0
- package/dist/community/client.d.ts.map +1 -0
- package/dist/community/client.js +245 -0
- package/dist/community/client.js.map +1 -0
- package/dist/community/types.d.ts +50 -0
- package/dist/community/types.d.ts.map +1 -0
- package/dist/community/types.js +11 -0
- package/dist/community/types.js.map +1 -0
- package/dist/config.d.ts +1 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -17
- package/dist/config.js.map +1 -1
- package/dist/data.d.ts +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +667 -3
- package/dist/data.js.map +1 -1
- package/dist/format.d.ts +5 -39
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +5 -120
- package/dist/format.js.map +1 -1
- package/dist/history.d.ts +13 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +37 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -234
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -9
- package/dist/init.js.map +1 -1
- package/dist/live.d.ts +4 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +11 -3
- package/dist/live.js.map +1 -1
- package/dist/marketplace.d.ts +9 -0
- package/dist/marketplace.d.ts.map +1 -1
- package/dist/marketplace.js +105 -15
- package/dist/marketplace.js.map +1 -1
- package/dist/news/cache.d.ts +13 -0
- package/dist/news/cache.d.ts.map +1 -0
- package/dist/news/cache.js +65 -0
- package/dist/news/cache.js.map +1 -0
- package/dist/news/score.d.ts +4 -0
- package/dist/news/score.d.ts.map +1 -0
- package/dist/news/score.js +43 -0
- package/dist/news/score.js.map +1 -0
- package/dist/news/sources/arxiv.d.ts +9 -0
- package/dist/news/sources/arxiv.d.ts.map +1 -0
- package/dist/news/sources/arxiv.js +103 -0
- package/dist/news/sources/arxiv.js.map +1 -0
- package/dist/news/sources/github-trending.d.ts +9 -0
- package/dist/news/sources/github-trending.d.ts.map +1 -0
- package/dist/news/sources/github-trending.js +93 -0
- package/dist/news/sources/github-trending.js.map +1 -0
- package/dist/news/sources/hn.d.ts +9 -0
- package/dist/news/sources/hn.d.ts.map +1 -0
- package/dist/news/sources/hn.js +53 -0
- package/dist/news/sources/hn.js.map +1 -0
- package/dist/news/sources/reddit.d.ts +9 -0
- package/dist/news/sources/reddit.d.ts.map +1 -0
- package/dist/news/sources/reddit.js +68 -0
- package/dist/news/sources/reddit.js.map +1 -0
- package/dist/news/sources/rss.d.ts +14 -0
- package/dist/news/sources/rss.d.ts.map +1 -0
- package/dist/news/sources/rss.js +102 -0
- package/dist/news/sources/rss.js.map +1 -0
- package/dist/news/types.d.ts +39 -0
- package/dist/news/types.d.ts.map +1 -0
- package/dist/news/types.js +47 -0
- package/dist/news/types.js.map +1 -0
- package/dist/preferences.d.ts +14 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +31 -0
- package/dist/preferences.js.map +1 -0
- package/dist/settings.d.ts +26 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +257 -0
- package/dist/settings.js.map +1 -0
- package/dist/transcript.d.ts +28 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +79 -0
- package/dist/transcript.js.map +1 -0
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +157 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +296 -0
- package/dist/ui.js.map +1 -0
- package/package.json +11 -9
- package/dist/api.d.ts +0 -72
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -109
- package/dist/api.js.map +0 -1
- package/dist/logger.d.ts +0 -20
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -59
- package/dist/logger.js.map +0 -1
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw-mode line editor with tab completion, ghost-text suggestions, and
|
|
3
|
+
* history navigation. The public surface is readLine(); the internal state
|
|
4
|
+
* machine is exposed via applyKeyEvent() for unit testing without I/O.
|
|
5
|
+
*/
|
|
6
|
+
export function makeInitialState(history) {
|
|
7
|
+
return {
|
|
8
|
+
line: '',
|
|
9
|
+
cursor: 0,
|
|
10
|
+
history,
|
|
11
|
+
historyIndex: history.length,
|
|
12
|
+
draft: '',
|
|
13
|
+
ghost: null,
|
|
14
|
+
mode: 'normal',
|
|
15
|
+
searchQuery: '',
|
|
16
|
+
searchIndex: -1,
|
|
17
|
+
tabCycle: null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// ── Pure state machine ──────────────────────────────────────────────────────
|
|
21
|
+
export function applyKeyEvent(state, event, opts = {}) {
|
|
22
|
+
// Reverse-i-search mode has its own dispatch
|
|
23
|
+
if (state.mode === 'reverse-search') {
|
|
24
|
+
return applySearchEvent(state, event);
|
|
25
|
+
}
|
|
26
|
+
switch (event.kind) {
|
|
27
|
+
case 'ctrl-c':
|
|
28
|
+
return { state, output: { kind: 'abort' } };
|
|
29
|
+
case 'ctrl-d': {
|
|
30
|
+
if (state.line === '') {
|
|
31
|
+
return { state, output: { kind: 'eof' } };
|
|
32
|
+
}
|
|
33
|
+
// Forward delete
|
|
34
|
+
const chars = Array.from(state.line);
|
|
35
|
+
if (state.cursor < chars.length) {
|
|
36
|
+
chars.splice(state.cursor, 1);
|
|
37
|
+
const next = withGhost({ ...state, line: chars.join('') }, opts);
|
|
38
|
+
return { state: next };
|
|
39
|
+
}
|
|
40
|
+
return { state };
|
|
41
|
+
}
|
|
42
|
+
case 'enter': {
|
|
43
|
+
const next = { ...state, ghost: null, tabCycle: null };
|
|
44
|
+
return { state: next, output: { kind: 'line', value: state.line } };
|
|
45
|
+
}
|
|
46
|
+
case 'char': {
|
|
47
|
+
const chars = Array.from(state.line);
|
|
48
|
+
chars.splice(state.cursor, 0, ...Array.from(event.data));
|
|
49
|
+
const newCursor = state.cursor + Array.from(event.data).length;
|
|
50
|
+
const newLine = chars.join('');
|
|
51
|
+
const next = withGhost({ ...state, line: newLine, cursor: newCursor, tabCycle: null }, opts);
|
|
52
|
+
// Auto-complete: show slash-command completions as soon as '/' is typed
|
|
53
|
+
if (newLine.startsWith('/') && opts.completer) {
|
|
54
|
+
const result = opts.completer(newLine, newCursor);
|
|
55
|
+
if (result.matches.length > 0) {
|
|
56
|
+
return { state: next, sideEffect: 'show-completions', completionsToShow: result.matches.slice(0, 6) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { state: next };
|
|
60
|
+
}
|
|
61
|
+
case 'backspace': {
|
|
62
|
+
if (state.cursor === 0)
|
|
63
|
+
return { state };
|
|
64
|
+
const chars = Array.from(state.line);
|
|
65
|
+
chars.splice(state.cursor - 1, 1);
|
|
66
|
+
const newLine = chars.join('');
|
|
67
|
+
const next = withGhost({ ...state, line: newLine, cursor: state.cursor - 1, tabCycle: null }, opts);
|
|
68
|
+
// Auto-complete: update slash-command completions when backspacing inside a slash prefix
|
|
69
|
+
if (newLine.startsWith('/') && opts.completer) {
|
|
70
|
+
const result = opts.completer(newLine, Math.min(newLine.length, state.cursor - 1));
|
|
71
|
+
if (result.matches.length > 0) {
|
|
72
|
+
return { state: next, sideEffect: 'show-completions', completionsToShow: result.matches.slice(0, 6) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { state: next };
|
|
76
|
+
}
|
|
77
|
+
case 'left': {
|
|
78
|
+
const next = { ...state, cursor: Math.max(0, state.cursor - 1), tabCycle: null };
|
|
79
|
+
return { state: next };
|
|
80
|
+
}
|
|
81
|
+
case 'right': {
|
|
82
|
+
const chars = Array.from(state.line);
|
|
83
|
+
if (state.cursor >= chars.length && state.ghost) {
|
|
84
|
+
// Accept ghost
|
|
85
|
+
const newLine = state.line + state.ghost;
|
|
86
|
+
const next = withGhost({ ...state, line: newLine, cursor: newLine.length, ghost: null, tabCycle: null }, opts);
|
|
87
|
+
return { state: next };
|
|
88
|
+
}
|
|
89
|
+
const next = {
|
|
90
|
+
...state,
|
|
91
|
+
cursor: Math.min(chars.length, state.cursor + 1),
|
|
92
|
+
tabCycle: null
|
|
93
|
+
};
|
|
94
|
+
return { state: next };
|
|
95
|
+
}
|
|
96
|
+
case 'ctrl-f': {
|
|
97
|
+
const chars = Array.from(state.line);
|
|
98
|
+
if (state.cursor >= chars.length && state.ghost) {
|
|
99
|
+
const newLine = state.line + state.ghost;
|
|
100
|
+
const next = withGhost({ ...state, line: newLine, cursor: newLine.length, ghost: null, tabCycle: null }, opts);
|
|
101
|
+
return { state: next };
|
|
102
|
+
}
|
|
103
|
+
const next = {
|
|
104
|
+
...state,
|
|
105
|
+
cursor: Math.min(chars.length, state.cursor + 1),
|
|
106
|
+
tabCycle: null
|
|
107
|
+
};
|
|
108
|
+
return { state: next };
|
|
109
|
+
}
|
|
110
|
+
case 'home': {
|
|
111
|
+
return { state: { ...state, cursor: 0, tabCycle: null } };
|
|
112
|
+
}
|
|
113
|
+
case 'end': {
|
|
114
|
+
return { state: { ...state, cursor: Array.from(state.line).length, tabCycle: null } };
|
|
115
|
+
}
|
|
116
|
+
case 'up': {
|
|
117
|
+
if (state.historyIndex <= 0)
|
|
118
|
+
return { state };
|
|
119
|
+
const saveDraft = state.historyIndex === state.history.length ? state.line : state.draft;
|
|
120
|
+
const newIndex = state.historyIndex - 1;
|
|
121
|
+
const newLine = state.history[newIndex] ?? '';
|
|
122
|
+
const next = withGhost({
|
|
123
|
+
...state,
|
|
124
|
+
line: newLine,
|
|
125
|
+
cursor: Array.from(newLine).length,
|
|
126
|
+
historyIndex: newIndex,
|
|
127
|
+
draft: saveDraft,
|
|
128
|
+
tabCycle: null
|
|
129
|
+
}, opts);
|
|
130
|
+
return { state: next };
|
|
131
|
+
}
|
|
132
|
+
case 'down': {
|
|
133
|
+
if (state.historyIndex >= state.history.length)
|
|
134
|
+
return { state };
|
|
135
|
+
const newIndex = state.historyIndex + 1;
|
|
136
|
+
const newLine = newIndex === state.history.length ? state.draft : (state.history[newIndex] ?? '');
|
|
137
|
+
const next = withGhost({
|
|
138
|
+
...state,
|
|
139
|
+
line: newLine,
|
|
140
|
+
cursor: Array.from(newLine).length,
|
|
141
|
+
historyIndex: newIndex,
|
|
142
|
+
tabCycle: null
|
|
143
|
+
}, opts);
|
|
144
|
+
return { state: next };
|
|
145
|
+
}
|
|
146
|
+
case 'ctrl-l': {
|
|
147
|
+
return { state, sideEffect: 'clear-screen' };
|
|
148
|
+
}
|
|
149
|
+
case 'ctrl-r': {
|
|
150
|
+
const searchIdx = findPrevMatch(state.history, state.searchQuery, state.history.length - 1);
|
|
151
|
+
return {
|
|
152
|
+
state: {
|
|
153
|
+
...state,
|
|
154
|
+
mode: 'reverse-search',
|
|
155
|
+
searchQuery: '',
|
|
156
|
+
searchIndex: searchIdx,
|
|
157
|
+
tabCycle: null
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
case 'ctrl-w': {
|
|
162
|
+
// Delete word backward
|
|
163
|
+
const chars = Array.from(state.line);
|
|
164
|
+
let pos = state.cursor;
|
|
165
|
+
// Skip trailing spaces
|
|
166
|
+
while (pos > 0 && chars[pos - 1] === ' ')
|
|
167
|
+
pos -= 1;
|
|
168
|
+
// Delete word chars
|
|
169
|
+
while (pos > 0 && chars[pos - 1] !== ' ')
|
|
170
|
+
pos -= 1;
|
|
171
|
+
chars.splice(pos, state.cursor - pos);
|
|
172
|
+
const next = withGhost({ ...state, line: chars.join(''), cursor: pos, tabCycle: null }, opts);
|
|
173
|
+
return { state: next };
|
|
174
|
+
}
|
|
175
|
+
case 'ctrl-u': {
|
|
176
|
+
const chars = Array.from(state.line);
|
|
177
|
+
chars.splice(0, state.cursor);
|
|
178
|
+
const next = withGhost({ ...state, line: chars.join(''), cursor: 0, tabCycle: null }, opts);
|
|
179
|
+
return { state: next };
|
|
180
|
+
}
|
|
181
|
+
case 'ctrl-k': {
|
|
182
|
+
const chars = Array.from(state.line);
|
|
183
|
+
const newLine = chars.slice(0, state.cursor).join('');
|
|
184
|
+
const next = withGhost({ ...state, line: newLine, tabCycle: null }, opts);
|
|
185
|
+
return { state: next };
|
|
186
|
+
}
|
|
187
|
+
case 'esc': {
|
|
188
|
+
return { state: { ...state, ghost: null, tabCycle: null } };
|
|
189
|
+
}
|
|
190
|
+
case 'tab': {
|
|
191
|
+
if (!opts.completer)
|
|
192
|
+
return { state };
|
|
193
|
+
// If there's an active tabCycle, cycle through its stored matches
|
|
194
|
+
if (state.tabCycle && state.tabCycle.matches.length > 1) {
|
|
195
|
+
const nextIndex = (state.tabCycle.index + 1) % state.tabCycle.matches.length;
|
|
196
|
+
const match = state.tabCycle.matches[nextIndex];
|
|
197
|
+
const newLine = spliceMatch(state.line, state.tabCycle.replaceFrom, state.cursor, match);
|
|
198
|
+
const next = withGhost({
|
|
199
|
+
...state,
|
|
200
|
+
line: newLine,
|
|
201
|
+
cursor: state.tabCycle.replaceFrom + Array.from(match).length,
|
|
202
|
+
tabCycle: { ...state.tabCycle, index: nextIndex }
|
|
203
|
+
}, opts);
|
|
204
|
+
return { state: next };
|
|
205
|
+
}
|
|
206
|
+
const result = opts.completer(state.line, state.cursor);
|
|
207
|
+
if (result.matches.length === 0)
|
|
208
|
+
return { state };
|
|
209
|
+
if (result.matches.length === 1) {
|
|
210
|
+
const match = result.matches[0];
|
|
211
|
+
const newLine = spliceMatch(state.line, result.replaceFrom, state.cursor, match);
|
|
212
|
+
const newCursor = result.replaceFrom + Array.from(match).length;
|
|
213
|
+
const next = withGhost({ ...state, line: newLine, cursor: newCursor, tabCycle: null }, opts);
|
|
214
|
+
return { state: next };
|
|
215
|
+
}
|
|
216
|
+
// Multiple matches: show list, replace with longest common prefix
|
|
217
|
+
const lcp = longestCommonPrefix(result.matches);
|
|
218
|
+
const newLine = spliceMatch(state.line, result.replaceFrom, state.cursor, lcp);
|
|
219
|
+
const newCursor = result.replaceFrom + Array.from(lcp).length;
|
|
220
|
+
const next = withGhost({
|
|
221
|
+
...state,
|
|
222
|
+
line: newLine,
|
|
223
|
+
cursor: newCursor,
|
|
224
|
+
tabCycle: { matches: result.matches, index: -1, replaceFrom: result.replaceFrom }
|
|
225
|
+
}, opts);
|
|
226
|
+
return {
|
|
227
|
+
state: next,
|
|
228
|
+
sideEffect: 'show-completions',
|
|
229
|
+
completionsToShow: result.matches.slice(0, 6)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
return { state };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// ── Reverse-i-search ─────────────────────────────────────────────────────────
|
|
237
|
+
function applySearchEvent(state, event) {
|
|
238
|
+
if (event.kind === 'esc') {
|
|
239
|
+
return {
|
|
240
|
+
state: {
|
|
241
|
+
...state,
|
|
242
|
+
mode: 'normal',
|
|
243
|
+
searchQuery: '',
|
|
244
|
+
searchIndex: -1
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (event.kind === 'enter') {
|
|
249
|
+
const matched = state.searchIndex >= 0 ? (state.history[state.searchIndex] ?? '') : state.line;
|
|
250
|
+
return {
|
|
251
|
+
state: {
|
|
252
|
+
...state,
|
|
253
|
+
mode: 'normal',
|
|
254
|
+
line: matched,
|
|
255
|
+
cursor: Array.from(matched).length,
|
|
256
|
+
searchQuery: '',
|
|
257
|
+
searchIndex: -1
|
|
258
|
+
},
|
|
259
|
+
output: { kind: 'line', value: matched }
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (event.kind === 'ctrl-r') {
|
|
263
|
+
// Cycle to older match
|
|
264
|
+
const startFrom = state.searchIndex > 0 ? state.searchIndex - 1 : state.history.length - 1;
|
|
265
|
+
const nextIdx = findPrevMatch(state.history, state.searchQuery, startFrom);
|
|
266
|
+
return { state: { ...state, searchIndex: nextIdx } };
|
|
267
|
+
}
|
|
268
|
+
if (event.kind === 'ctrl-c') {
|
|
269
|
+
return {
|
|
270
|
+
state: { ...state, mode: 'normal', searchQuery: '', searchIndex: -1 },
|
|
271
|
+
output: { kind: 'abort' }
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (event.kind === 'backspace') {
|
|
275
|
+
const newQuery = state.searchQuery.slice(0, -1);
|
|
276
|
+
const idx = findPrevMatch(state.history, newQuery, state.history.length - 1);
|
|
277
|
+
return { state: { ...state, searchQuery: newQuery, searchIndex: idx } };
|
|
278
|
+
}
|
|
279
|
+
if (event.kind === 'char') {
|
|
280
|
+
const newQuery = state.searchQuery + event.data;
|
|
281
|
+
const idx = findPrevMatch(state.history, newQuery, state.history.length - 1);
|
|
282
|
+
return { state: { ...state, searchQuery: newQuery, searchIndex: idx } };
|
|
283
|
+
}
|
|
284
|
+
return { state };
|
|
285
|
+
}
|
|
286
|
+
function findPrevMatch(history, query, startFrom) {
|
|
287
|
+
if (!query)
|
|
288
|
+
return -1;
|
|
289
|
+
for (let i = startFrom; i >= 0; i -= 1) {
|
|
290
|
+
if (history[i].includes(query))
|
|
291
|
+
return i;
|
|
292
|
+
}
|
|
293
|
+
return -1;
|
|
294
|
+
}
|
|
295
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
296
|
+
function withGhost(state, opts) {
|
|
297
|
+
if (!opts.ghostSuggester)
|
|
298
|
+
return { ...state, ghost: null };
|
|
299
|
+
const ghost = opts.ghostSuggester(state.line, state.history);
|
|
300
|
+
return { ...state, ghost };
|
|
301
|
+
}
|
|
302
|
+
function spliceMatch(line, replaceFrom, cursor, match) {
|
|
303
|
+
const chars = Array.from(line);
|
|
304
|
+
chars.splice(replaceFrom, cursor - replaceFrom, ...Array.from(match));
|
|
305
|
+
return chars.join('');
|
|
306
|
+
}
|
|
307
|
+
function longestCommonPrefix(strs) {
|
|
308
|
+
if (strs.length === 0)
|
|
309
|
+
return '';
|
|
310
|
+
let prefix = strs[0];
|
|
311
|
+
for (let i = 1; i < strs.length; i += 1) {
|
|
312
|
+
while (!strs[i].startsWith(prefix)) {
|
|
313
|
+
prefix = prefix.slice(0, -1);
|
|
314
|
+
if (!prefix)
|
|
315
|
+
return '';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return prefix;
|
|
319
|
+
}
|
|
320
|
+
// ── Rendering helpers ────────────────────────────────────────────────────────
|
|
321
|
+
/** Visible cell width of a string with ANSI CSI escape sequences stripped. */
|
|
322
|
+
export function visibleWidth(s) {
|
|
323
|
+
// eslint-disable-next-line no-control-regex
|
|
324
|
+
return Array.from(s.replace(/\x1b\[[0-9;]*m/g, '')).length;
|
|
325
|
+
}
|
|
326
|
+
const EMPTY_FRAME_POSITION = { totalRows: 0, cursorRow: 0, promptRows: 0 };
|
|
327
|
+
/**
|
|
328
|
+
* Returns the ANSI bytes that paint a fresh prompt frame and the position
|
|
329
|
+
* the new frame occupies. Pass `width` (terminal columns) and `prev` (the
|
|
330
|
+
* previous frame's position) so the renderer can erase exactly the rows
|
|
331
|
+
* that were previously drawn — including any that wrapped past the
|
|
332
|
+
* declared width. With `width=Infinity` (default) the wrapping math
|
|
333
|
+
* collapses to legacy single-row behaviour and the only redraw side-effect
|
|
334
|
+
* is the leading `\r\x1b[K` that clears the current row.
|
|
335
|
+
*/
|
|
336
|
+
export function renderPromptFrame(state, prompt, footer, width = Infinity, prev = EMPTY_FRAME_POSITION) {
|
|
337
|
+
const safeWidth = Number.isFinite(width) && width > 0 ? width : Infinity;
|
|
338
|
+
const wrapRows = (text) => {
|
|
339
|
+
if (!Number.isFinite(safeWidth))
|
|
340
|
+
return 1;
|
|
341
|
+
const w = visibleWidth(text);
|
|
342
|
+
return Math.max(1, Math.ceil(Math.max(1, w) / safeWidth));
|
|
343
|
+
};
|
|
344
|
+
const chars = Array.from(state.line);
|
|
345
|
+
const before = chars.slice(0, state.cursor).join('');
|
|
346
|
+
const after = chars.slice(state.cursor).join('');
|
|
347
|
+
const ghostAnsi = state.ghost ? `\x1b[2m${state.ghost}\x1b[0m` : '';
|
|
348
|
+
const suffix = after + ghostAnsi;
|
|
349
|
+
const promptVisible = visibleWidth(prompt);
|
|
350
|
+
const beforeLen = Array.from(before).length;
|
|
351
|
+
const afterLen = Array.from(after).length;
|
|
352
|
+
const ghostLen = Array.from(state.ghost ?? '').length;
|
|
353
|
+
const promptTotalVisible = promptVisible + beforeLen + afterLen + ghostLen;
|
|
354
|
+
const promptRows = Number.isFinite(safeWidth)
|
|
355
|
+
? Math.max(1, Math.ceil(Math.max(1, promptTotalVisible) / safeWidth))
|
|
356
|
+
: 1;
|
|
357
|
+
const cursorLogicalCol = promptVisible + beforeLen;
|
|
358
|
+
const cursorRow = Number.isFinite(safeWidth)
|
|
359
|
+
? Math.floor(cursorLogicalCol / safeWidth)
|
|
360
|
+
: 0;
|
|
361
|
+
const cursorCol = Number.isFinite(safeWidth)
|
|
362
|
+
? cursorLogicalCol % safeWidth
|
|
363
|
+
: cursorLogicalCol;
|
|
364
|
+
const footerLines = footer ? footer.split('\n') : [];
|
|
365
|
+
const footerRowsPhysical = footerLines.reduce((sum, line) => sum + wrapRows(line), 0);
|
|
366
|
+
const totalRows = promptRows + footerRowsPhysical;
|
|
367
|
+
let frame = '';
|
|
368
|
+
if (prev.totalRows > 0) {
|
|
369
|
+
// Erase the previous frame: go to its top-left corner, then clear from
|
|
370
|
+
// there to the end of the screen. This wipes any wrapped continuation
|
|
371
|
+
// rows that single-row `\r\x1b[K` would have missed.
|
|
372
|
+
frame += '\r';
|
|
373
|
+
if (prev.cursorRow > 0)
|
|
374
|
+
frame += `\x1b[${prev.cursorRow}A`;
|
|
375
|
+
frame += '\x1b[J';
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// First render: assume the row we're on may have stale content and clear it.
|
|
379
|
+
frame += '\r\x1b[K';
|
|
380
|
+
}
|
|
381
|
+
frame += prompt + before + suffix;
|
|
382
|
+
if (footer) {
|
|
383
|
+
for (const line of footerLines) {
|
|
384
|
+
frame += '\n\r\x1b[K' + line;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// After writing, terminal cursor sits at the end of the last printed text.
|
|
388
|
+
// `\r` brings it to col 0 of the row it ended up on (the last physical row
|
|
389
|
+
// of the last logical line). Move up to the target cursor row, then right.
|
|
390
|
+
frame += '\r';
|
|
391
|
+
const endRow = totalRows - 1;
|
|
392
|
+
const moveUp = endRow - cursorRow;
|
|
393
|
+
if (moveUp > 0)
|
|
394
|
+
frame += `\x1b[${moveUp}A`;
|
|
395
|
+
if (cursorCol > 0)
|
|
396
|
+
frame += `\x1b[${cursorCol}C`;
|
|
397
|
+
return {
|
|
398
|
+
frame,
|
|
399
|
+
position: { totalRows, cursorRow, promptRows }
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function renderSearch(state, _prompt) {
|
|
403
|
+
const matched = state.searchIndex >= 0 ? (state.history[state.searchIndex] ?? '') : '';
|
|
404
|
+
return `\r\x1b[K(reverse-i-search)\`${state.searchQuery}\`: ${matched}`;
|
|
405
|
+
}
|
|
406
|
+
// ── Main readLine ────────────────────────────────────────────────────────────
|
|
407
|
+
export async function readLine(opts) {
|
|
408
|
+
const out = opts.out ?? process.stdout;
|
|
409
|
+
const inp = opts.in ?? process.stdin;
|
|
410
|
+
return new Promise((resolve) => {
|
|
411
|
+
let state = makeInitialState(opts.history);
|
|
412
|
+
// Compute initial ghost
|
|
413
|
+
if (opts.ghostSuggester) {
|
|
414
|
+
state = { ...state, ghost: opts.ghostSuggester('', opts.history) };
|
|
415
|
+
}
|
|
416
|
+
// Emit initial prompt (with suffix computed for empty initial line)
|
|
417
|
+
out.write(opts.prompt + (opts.promptSuffix ? opts.promptSuffix('') : ''));
|
|
418
|
+
// Accumulated escape sequence buffer
|
|
419
|
+
let escBuf = '';
|
|
420
|
+
let escTimer = null;
|
|
421
|
+
function flushEsc() {
|
|
422
|
+
if (escTimer) {
|
|
423
|
+
clearTimeout(escTimer);
|
|
424
|
+
escTimer = null;
|
|
425
|
+
}
|
|
426
|
+
if (!escBuf)
|
|
427
|
+
return;
|
|
428
|
+
const seq = escBuf;
|
|
429
|
+
escBuf = '';
|
|
430
|
+
dispatchEscSequence(seq);
|
|
431
|
+
}
|
|
432
|
+
function dispatchEscSequence(seq) {
|
|
433
|
+
// Arrow keys and navigation
|
|
434
|
+
if (seq === '\x1b[A' || seq === '\x1bOA')
|
|
435
|
+
return dispatchEvent({ kind: 'up' });
|
|
436
|
+
if (seq === '\x1b[B' || seq === '\x1bOB')
|
|
437
|
+
return dispatchEvent({ kind: 'down' });
|
|
438
|
+
if (seq === '\x1b[C' || seq === '\x1bOC')
|
|
439
|
+
return dispatchEvent({ kind: 'right' });
|
|
440
|
+
if (seq === '\x1b[D' || seq === '\x1bOD')
|
|
441
|
+
return dispatchEvent({ kind: 'left' });
|
|
442
|
+
if (seq === '\x1b[H' || seq === '\x1bOH')
|
|
443
|
+
return dispatchEvent({ kind: 'home' });
|
|
444
|
+
if (seq === '\x1b[F' || seq === '\x1bOF')
|
|
445
|
+
return dispatchEvent({ kind: 'end' });
|
|
446
|
+
// Bare Esc
|
|
447
|
+
if (seq === '\x1b')
|
|
448
|
+
return dispatchEvent({ kind: 'esc' });
|
|
449
|
+
}
|
|
450
|
+
// Position of the last redraw; threaded into the next call so we can
|
|
451
|
+
// clear exactly the rows we drew (including wrapped continuations).
|
|
452
|
+
let framePos = EMPTY_FRAME_POSITION;
|
|
453
|
+
// Completion hints to show in the footer area on next redraw
|
|
454
|
+
let pendingCompletions = null;
|
|
455
|
+
function dispatchEvent(event) {
|
|
456
|
+
const result = applyKeyEvent(state, event, {
|
|
457
|
+
completer: opts.completer,
|
|
458
|
+
ghostSuggester: opts.ghostSuggester
|
|
459
|
+
});
|
|
460
|
+
state = result.state;
|
|
461
|
+
if (result.sideEffect === 'clear-screen') {
|
|
462
|
+
out.write('\x1b[2J\x1b[H');
|
|
463
|
+
}
|
|
464
|
+
if (result.sideEffect === 'show-completions' && result.completionsToShow) {
|
|
465
|
+
pendingCompletions = result.completionsToShow;
|
|
466
|
+
}
|
|
467
|
+
redraw();
|
|
468
|
+
if (result.output) {
|
|
469
|
+
cleanup();
|
|
470
|
+
resolve(result.output);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function redraw() {
|
|
474
|
+
if (state.mode === 'reverse-search') {
|
|
475
|
+
framePos = EMPTY_FRAME_POSITION;
|
|
476
|
+
pendingCompletions = null;
|
|
477
|
+
out.write(renderSearch(state, opts.prompt));
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const suffix = opts.promptSuffix ? opts.promptSuffix(state.line) : '';
|
|
481
|
+
const footerBase = opts.footer ? opts.footer(state.line) : '';
|
|
482
|
+
const parts = [footerBase];
|
|
483
|
+
if (pendingCompletions && pendingCompletions.length > 0) {
|
|
484
|
+
parts.push(`\x1b[2m${pendingCompletions.slice(0, 6).join(' · ')}\x1b[0m`);
|
|
485
|
+
pendingCompletions = null;
|
|
486
|
+
}
|
|
487
|
+
const footer = parts.filter(Boolean).join('\n');
|
|
488
|
+
const width = typeof out.columns === 'number'
|
|
489
|
+
? out.columns
|
|
490
|
+
: Infinity;
|
|
491
|
+
const result = renderPromptFrame(state, opts.prompt + suffix, footer, width, framePos);
|
|
492
|
+
framePos = result.position;
|
|
493
|
+
out.write(result.frame);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function onData(chunk) {
|
|
497
|
+
// Defensive: if a previous consumer left stdin in string-encoding mode
|
|
498
|
+
// (e.g. setEncoding('utf8') uncleared), `chunk` is a string and
|
|
499
|
+
// `[...string]` yields chars, not bytes. Normalise to Buffer so the
|
|
500
|
+
// byte-level switch logic below stays correct.
|
|
501
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
502
|
+
const bytes = [...buf];
|
|
503
|
+
let i = 0;
|
|
504
|
+
while (i < bytes.length) {
|
|
505
|
+
const b = bytes[i];
|
|
506
|
+
// Escape or continuation of escape sequence
|
|
507
|
+
if (b === 0x1b || escBuf) {
|
|
508
|
+
flushEsc();
|
|
509
|
+
// Start new escape sequence
|
|
510
|
+
escBuf = String.fromCharCode(b);
|
|
511
|
+
// Collect more bytes greedily (up to 8)
|
|
512
|
+
let j = i + 1;
|
|
513
|
+
while (j < bytes.length && j - i < 8) {
|
|
514
|
+
const nb = bytes[j];
|
|
515
|
+
// Stop at printable ASCII that signals end of sequence (letters, ~)
|
|
516
|
+
escBuf += String.fromCharCode(nb);
|
|
517
|
+
j += 1;
|
|
518
|
+
if (nb >= 0x40 &&
|
|
519
|
+
nb <= 0x7e // final byte of CSI
|
|
520
|
+
) {
|
|
521
|
+
// Don't break on '[' (0x5B) — it introduces CSI sequences,
|
|
522
|
+
// so there must be at least one more byte coming.
|
|
523
|
+
if (nb === 0x5b)
|
|
524
|
+
continue;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
i = j;
|
|
529
|
+
// Dispatch immediately if we have a complete sequence
|
|
530
|
+
if (escBuf.length > 1) {
|
|
531
|
+
const seq = escBuf;
|
|
532
|
+
escBuf = '';
|
|
533
|
+
dispatchEscSequence(seq);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// Wait a moment for follow-up bytes (Esc alone vs. Esc[...)
|
|
537
|
+
escTimer = setTimeout(() => {
|
|
538
|
+
const s = escBuf;
|
|
539
|
+
escBuf = '';
|
|
540
|
+
dispatchEscSequence(s || '\x1b');
|
|
541
|
+
}, 10);
|
|
542
|
+
}
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
// Control codes
|
|
546
|
+
if (b === 0x03) {
|
|
547
|
+
dispatchEvent({ kind: 'ctrl-c' });
|
|
548
|
+
i++;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (b === 0x04) {
|
|
552
|
+
dispatchEvent({ kind: 'ctrl-d' });
|
|
553
|
+
i++;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (b === 0x01) {
|
|
557
|
+
dispatchEvent({ kind: 'home' });
|
|
558
|
+
i++;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (b === 0x05) {
|
|
562
|
+
dispatchEvent({ kind: 'end' });
|
|
563
|
+
i++;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (b === 0x06) {
|
|
567
|
+
dispatchEvent({ kind: 'ctrl-f' });
|
|
568
|
+
i++;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (b === 0x07) {
|
|
572
|
+
i++;
|
|
573
|
+
continue;
|
|
574
|
+
} // bell, ignore
|
|
575
|
+
if (b === 0x08 || b === 0x7f) {
|
|
576
|
+
dispatchEvent({ kind: 'backspace' });
|
|
577
|
+
i++;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (b === 0x09) {
|
|
581
|
+
dispatchEvent({ kind: 'tab' });
|
|
582
|
+
i++;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (b === 0x0a || b === 0x0d) {
|
|
586
|
+
dispatchEvent({ kind: 'enter' });
|
|
587
|
+
i++;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (b === 0x0b) {
|
|
591
|
+
dispatchEvent({ kind: 'ctrl-k' });
|
|
592
|
+
i++;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (b === 0x0c) {
|
|
596
|
+
dispatchEvent({ kind: 'ctrl-l' });
|
|
597
|
+
i++;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (b === 0x12) {
|
|
601
|
+
dispatchEvent({ kind: 'ctrl-r' });
|
|
602
|
+
i++;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (b === 0x15) {
|
|
606
|
+
dispatchEvent({ kind: 'ctrl-u' });
|
|
607
|
+
i++;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (b === 0x17) {
|
|
611
|
+
dispatchEvent({ kind: 'ctrl-w' });
|
|
612
|
+
i++;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
// Printable ASCII or UTF-8 multi-byte: batch until next control
|
|
616
|
+
let printable = '';
|
|
617
|
+
while (i < bytes.length) {
|
|
618
|
+
const cb = bytes[i];
|
|
619
|
+
if (cb < 0x20 || cb === 0x7f)
|
|
620
|
+
break;
|
|
621
|
+
if (cb === 0x1b)
|
|
622
|
+
break;
|
|
623
|
+
// UTF-8 multi-byte: pass through
|
|
624
|
+
printable += String.fromCharCode(cb);
|
|
625
|
+
i++;
|
|
626
|
+
}
|
|
627
|
+
if (printable) {
|
|
628
|
+
dispatchEvent({ kind: 'char', data: printable });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function cleanup() {
|
|
633
|
+
inp.removeListener('data', onData);
|
|
634
|
+
inp.pause();
|
|
635
|
+
if (escTimer)
|
|
636
|
+
clearTimeout(escTimer);
|
|
637
|
+
if (framePos.totalRows > 0) {
|
|
638
|
+
// Wipe the whole frame, re-print the prompt line without footer or
|
|
639
|
+
// ghost text so the submitted input stays visible in the scrollback,
|
|
640
|
+
// and park the cursor on the row immediately below.
|
|
641
|
+
const suffix = opts.promptSuffix ? opts.promptSuffix(state.line) : '';
|
|
642
|
+
let seq = '\r';
|
|
643
|
+
if (framePos.cursorRow > 0)
|
|
644
|
+
seq += `\x1b[${framePos.cursorRow}A`;
|
|
645
|
+
seq += '\x1b[J';
|
|
646
|
+
seq += opts.prompt + suffix + state.line + '\r\n';
|
|
647
|
+
out.write(seq);
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
out.write('\n');
|
|
651
|
+
}
|
|
652
|
+
framePos = EMPTY_FRAME_POSITION;
|
|
653
|
+
if (inp.setRawMode) {
|
|
654
|
+
try {
|
|
655
|
+
inp.setRawMode(false);
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
/* ignore */
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Enter raw mode
|
|
663
|
+
if (inp.setRawMode) {
|
|
664
|
+
try {
|
|
665
|
+
inp.setRawMode(true);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
/* ignore if not a TTY */
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
inp.resume();
|
|
672
|
+
inp.on('data', onData);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
//# sourceMappingURL=prompter.js.map
|