opencode-agora 0.2.2 → 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.
Files changed (175) hide show
  1. package/README.md +217 -52
  2. package/dist/cli/app.d.ts +3 -0
  3. package/dist/cli/app.d.ts.map +1 -1
  4. package/dist/cli/app.js +1202 -227
  5. package/dist/cli/app.js.map +1 -1
  6. package/dist/cli/chat-renderer.d.ts +31 -0
  7. package/dist/cli/chat-renderer.d.ts.map +1 -0
  8. package/dist/cli/chat-renderer.js +275 -0
  9. package/dist/cli/chat-renderer.js.map +1 -0
  10. package/dist/cli/commands-meta.d.ts +21 -0
  11. package/dist/cli/commands-meta.d.ts.map +1 -0
  12. package/dist/cli/commands-meta.js +600 -0
  13. package/dist/cli/commands-meta.js.map +1 -0
  14. package/dist/cli/completions.d.ts +18 -0
  15. package/dist/cli/completions.d.ts.map +1 -0
  16. package/dist/cli/completions.js +190 -0
  17. package/dist/cli/completions.js.map +1 -0
  18. package/dist/cli/mcp-server.d.ts +4 -0
  19. package/dist/cli/mcp-server.d.ts.map +1 -0
  20. package/dist/cli/mcp-server.js +277 -0
  21. package/dist/cli/mcp-server.js.map +1 -0
  22. package/dist/cli/menu.d.ts +7 -0
  23. package/dist/cli/menu.d.ts.map +1 -0
  24. package/dist/cli/menu.js +164 -0
  25. package/dist/cli/menu.js.map +1 -0
  26. package/dist/cli/pages/community.d.ts +3 -0
  27. package/dist/cli/pages/community.d.ts.map +1 -0
  28. package/dist/cli/pages/community.js +276 -0
  29. package/dist/cli/pages/community.js.map +1 -0
  30. package/dist/cli/pages/helpers.d.ts +32 -0
  31. package/dist/cli/pages/helpers.d.ts.map +1 -0
  32. package/dist/cli/pages/helpers.js +67 -0
  33. package/dist/cli/pages/helpers.js.map +1 -0
  34. package/dist/cli/pages/home.d.ts +3 -0
  35. package/dist/cli/pages/home.d.ts.map +1 -0
  36. package/dist/cli/pages/home.js +148 -0
  37. package/dist/cli/pages/home.js.map +1 -0
  38. package/dist/cli/pages/marketplace.d.ts +3 -0
  39. package/dist/cli/pages/marketplace.d.ts.map +1 -0
  40. package/dist/cli/pages/marketplace.js +179 -0
  41. package/dist/cli/pages/marketplace.js.map +1 -0
  42. package/dist/cli/pages/news.d.ts +3 -0
  43. package/dist/cli/pages/news.d.ts.map +1 -0
  44. package/dist/cli/pages/news.js +561 -0
  45. package/dist/cli/pages/news.js.map +1 -0
  46. package/dist/cli/pages/settings.d.ts +3 -0
  47. package/dist/cli/pages/settings.d.ts.map +1 -0
  48. package/dist/cli/pages/settings.js +166 -0
  49. package/dist/cli/pages/settings.js.map +1 -0
  50. package/dist/cli/pages/types.d.ts +67 -0
  51. package/dist/cli/pages/types.d.ts.map +1 -0
  52. package/dist/cli/pages/types.js +2 -0
  53. package/dist/cli/pages/types.js.map +1 -0
  54. package/dist/cli/prompter.d.ts +135 -0
  55. package/dist/cli/prompter.d.ts.map +1 -0
  56. package/dist/cli/prompter.js +675 -0
  57. package/dist/cli/prompter.js.map +1 -0
  58. package/dist/cli/shell.d.ts +23 -0
  59. package/dist/cli/shell.d.ts.map +1 -0
  60. package/dist/cli/shell.js +819 -0
  61. package/dist/cli/shell.js.map +1 -0
  62. package/dist/cli/tui.d.ts +7 -0
  63. package/dist/cli/tui.d.ts.map +1 -0
  64. package/dist/cli/tui.js +373 -0
  65. package/dist/cli/tui.js.map +1 -0
  66. package/dist/cli.js +1 -1
  67. package/dist/cli.js.map +1 -1
  68. package/dist/commands.d.ts +14 -0
  69. package/dist/commands.d.ts.map +1 -0
  70. package/dist/commands.js +28 -0
  71. package/dist/commands.js.map +1 -0
  72. package/dist/community/client.d.ts +47 -0
  73. package/dist/community/client.d.ts.map +1 -0
  74. package/dist/community/client.js +245 -0
  75. package/dist/community/client.js.map +1 -0
  76. package/dist/community/types.d.ts +50 -0
  77. package/dist/community/types.d.ts.map +1 -0
  78. package/dist/community/types.js +11 -0
  79. package/dist/community/types.js.map +1 -0
  80. package/dist/config-files.d.ts.map +1 -1
  81. package/dist/config-files.js +11 -8
  82. package/dist/config-files.js.map +1 -1
  83. package/dist/config.d.ts +8 -8
  84. package/dist/config.d.ts.map +1 -1
  85. package/dist/config.js +16 -27
  86. package/dist/config.js.map +1 -1
  87. package/dist/data.d.ts +1 -1
  88. package/dist/data.d.ts.map +1 -1
  89. package/dist/data.js +1013 -545
  90. package/dist/data.js.map +1 -1
  91. package/dist/format.d.ts +5 -39
  92. package/dist/format.d.ts.map +1 -1
  93. package/dist/format.js +5 -118
  94. package/dist/format.js.map +1 -1
  95. package/dist/history.d.ts +13 -0
  96. package/dist/history.d.ts.map +1 -0
  97. package/dist/history.js +37 -0
  98. package/dist/history.js.map +1 -0
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +151 -236
  101. package/dist/index.js.map +1 -1
  102. package/dist/init.d.ts +4 -1
  103. package/dist/init.d.ts.map +1 -1
  104. package/dist/init.js +103 -51
  105. package/dist/init.js.map +1 -1
  106. package/dist/live.d.ts +4 -0
  107. package/dist/live.d.ts.map +1 -1
  108. package/dist/live.js +87 -19
  109. package/dist/live.js.map +1 -1
  110. package/dist/marketplace.d.ts +9 -0
  111. package/dist/marketplace.d.ts.map +1 -1
  112. package/dist/marketplace.js +128 -33
  113. package/dist/marketplace.js.map +1 -1
  114. package/dist/news/cache.d.ts +13 -0
  115. package/dist/news/cache.d.ts.map +1 -0
  116. package/dist/news/cache.js +65 -0
  117. package/dist/news/cache.js.map +1 -0
  118. package/dist/news/score.d.ts +4 -0
  119. package/dist/news/score.d.ts.map +1 -0
  120. package/dist/news/score.js +43 -0
  121. package/dist/news/score.js.map +1 -0
  122. package/dist/news/sources/arxiv.d.ts +9 -0
  123. package/dist/news/sources/arxiv.d.ts.map +1 -0
  124. package/dist/news/sources/arxiv.js +103 -0
  125. package/dist/news/sources/arxiv.js.map +1 -0
  126. package/dist/news/sources/github-trending.d.ts +9 -0
  127. package/dist/news/sources/github-trending.d.ts.map +1 -0
  128. package/dist/news/sources/github-trending.js +93 -0
  129. package/dist/news/sources/github-trending.js.map +1 -0
  130. package/dist/news/sources/hn.d.ts +9 -0
  131. package/dist/news/sources/hn.d.ts.map +1 -0
  132. package/dist/news/sources/hn.js +53 -0
  133. package/dist/news/sources/hn.js.map +1 -0
  134. package/dist/news/sources/reddit.d.ts +9 -0
  135. package/dist/news/sources/reddit.d.ts.map +1 -0
  136. package/dist/news/sources/reddit.js +68 -0
  137. package/dist/news/sources/reddit.js.map +1 -0
  138. package/dist/news/sources/rss.d.ts +14 -0
  139. package/dist/news/sources/rss.d.ts.map +1 -0
  140. package/dist/news/sources/rss.js +102 -0
  141. package/dist/news/sources/rss.js.map +1 -0
  142. package/dist/news/types.d.ts +39 -0
  143. package/dist/news/types.d.ts.map +1 -0
  144. package/dist/news/types.js +47 -0
  145. package/dist/news/types.js.map +1 -0
  146. package/dist/preferences.d.ts +14 -0
  147. package/dist/preferences.d.ts.map +1 -0
  148. package/dist/preferences.js +31 -0
  149. package/dist/preferences.js.map +1 -0
  150. package/dist/settings.d.ts +26 -0
  151. package/dist/settings.d.ts.map +1 -0
  152. package/dist/settings.js +257 -0
  153. package/dist/settings.js.map +1 -0
  154. package/dist/state.d.ts.map +1 -1
  155. package/dist/state.js +7 -6
  156. package/dist/state.js.map +1 -1
  157. package/dist/transcript.d.ts +28 -0
  158. package/dist/transcript.d.ts.map +1 -0
  159. package/dist/transcript.js +79 -0
  160. package/dist/transcript.js.map +1 -0
  161. package/dist/types.d.ts +6 -1
  162. package/dist/types.d.ts.map +1 -1
  163. package/dist/ui.d.ts +157 -0
  164. package/dist/ui.d.ts.map +1 -0
  165. package/dist/ui.js +296 -0
  166. package/dist/ui.js.map +1 -0
  167. package/package.json +21 -10
  168. package/dist/api.d.ts +0 -69
  169. package/dist/api.d.ts.map +0 -1
  170. package/dist/api.js +0 -109
  171. package/dist/api.js.map +0 -1
  172. package/dist/logger.d.ts +0 -20
  173. package/dist/logger.d.ts.map +0 -1
  174. package/dist/logger.js +0 -59
  175. 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