@thegitai/cli 1.0.0-preview.2 → 1.0.0-preview.21
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 +32 -4
- package/dist/bin/ai.js +57 -291
- package/dist/src/agent-mode.js +1 -1
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +72 -3
- package/dist/src/api/chat.js +236 -33
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +9 -4
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +199 -0
- package/dist/src/executor.js +1 -1
- package/dist/src/help-text.js +51 -11
- package/dist/src/permissions.js +243 -0
- package/dist/src/project-index.js +13 -1
- package/dist/src/session-store.js +119 -20
- package/dist/src/session.js +14 -3
- package/dist/src/tool-executor.js +2 -2
- package/dist/src/tools/delete-file.js +14 -0
- package/dist/src/tools/index.js +2 -0
- package/dist/src/tools/patch-file.js +12 -16
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/run-command.js +13 -27
- package/dist/src/tools/run-node-script.js +11 -26
- package/dist/src/tools/str-replace.js +12 -16
- package/dist/src/tools/write-file.js +66 -0
- package/dist/src/turn-failure-marker.js +11 -0
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +579 -154
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +535 -159
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +206 -63
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +18 -6
- package/dist/src/markdown-renderer.js +0 -112
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
2
|
+
const OTHER_CURSOR = '$other';
|
|
3
|
+
const SUBMIT_CURSOR = '$submit';
|
|
4
|
+
const ACCENT_COLOR = 'cyan';
|
|
5
|
+
const SCROLL_PAGE_ROWS = 4;
|
|
6
|
+
function currentQuestion(state) {
|
|
7
|
+
return (state.questions.find((question) => question.id === state.questionId) ??
|
|
8
|
+
state.questions[0]);
|
|
9
|
+
}
|
|
10
|
+
function questionIndex(state) {
|
|
11
|
+
return Math.max(0, state.questions.findIndex((question) => question.id === state.questionId));
|
|
12
|
+
}
|
|
13
|
+
function defaultCursor(question) {
|
|
14
|
+
return question.options[0]?.id ?? OTHER_CURSOR;
|
|
15
|
+
}
|
|
16
|
+
function cursorForQuestion(state, question) {
|
|
17
|
+
return state.cursorByQuestionId[question.id] ?? defaultCursor(question);
|
|
18
|
+
}
|
|
19
|
+
function cursorRows(state, question) {
|
|
20
|
+
const rows = [...question.options.map((option) => option.id), OTHER_CURSOR];
|
|
21
|
+
if (questionIndex(state) === state.questions.length - 1) {
|
|
22
|
+
rows.push(SUBMIT_CURSOR);
|
|
23
|
+
}
|
|
24
|
+
return rows;
|
|
25
|
+
}
|
|
26
|
+
function setQuestion(state, index) {
|
|
27
|
+
const question = state.questions[index];
|
|
28
|
+
if (!question)
|
|
29
|
+
return state;
|
|
30
|
+
return {
|
|
31
|
+
...state,
|
|
32
|
+
questionId: question.id,
|
|
33
|
+
contentScrollAnchor: null,
|
|
34
|
+
contentScrollOffset: 0,
|
|
35
|
+
noteOpen: false,
|
|
36
|
+
noteCursor: (state.customText[question.id] ?? '').length,
|
|
37
|
+
validationMessage: '',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function setCursor(state, question, cursor) {
|
|
41
|
+
return {
|
|
42
|
+
...state,
|
|
43
|
+
contentScrollAnchor: cursor,
|
|
44
|
+
cursorByQuestionId: {
|
|
45
|
+
...state.cursorByQuestionId,
|
|
46
|
+
[question.id]: cursor,
|
|
47
|
+
},
|
|
48
|
+
validationMessage: '',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function moveCursor(state, delta) {
|
|
52
|
+
const question = currentQuestion(state);
|
|
53
|
+
const rows = cursorRows(state, question);
|
|
54
|
+
const current = cursorForQuestion(state, question);
|
|
55
|
+
const index = Math.max(0, rows.indexOf(current));
|
|
56
|
+
const next = Math.max(0, Math.min(index + delta, rows.length - 1));
|
|
57
|
+
return setCursor(state, question, rows[next]);
|
|
58
|
+
}
|
|
59
|
+
function selectedIds(state, question) {
|
|
60
|
+
return state.selectedOptionIds[question.id] ?? [];
|
|
61
|
+
}
|
|
62
|
+
function selectOption(state, optionId) {
|
|
63
|
+
const question = currentQuestion(state);
|
|
64
|
+
const current = selectedIds(state, question);
|
|
65
|
+
const nextSelected = question.multiSelect
|
|
66
|
+
? current.includes(optionId)
|
|
67
|
+
? current.filter((id) => id !== optionId)
|
|
68
|
+
: question.options
|
|
69
|
+
.map((option) => option.id)
|
|
70
|
+
.filter((id) => id === optionId || current.includes(id))
|
|
71
|
+
: [optionId];
|
|
72
|
+
let next = {
|
|
73
|
+
...setCursor(state, question, optionId),
|
|
74
|
+
selectedOptionIds: {
|
|
75
|
+
...state.selectedOptionIds,
|
|
76
|
+
[question.id]: nextSelected,
|
|
77
|
+
},
|
|
78
|
+
customText: question.multiSelect
|
|
79
|
+
? state.customText
|
|
80
|
+
: {
|
|
81
|
+
...state.customText,
|
|
82
|
+
[question.id]: '',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
if (!question.multiSelect) {
|
|
86
|
+
const index = questionIndex(state);
|
|
87
|
+
next =
|
|
88
|
+
index < state.questions.length - 1
|
|
89
|
+
? setQuestion(next, index + 1)
|
|
90
|
+
: setCursor(next, question, SUBMIT_CURSOR);
|
|
91
|
+
}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
function normalizeNoteText(value) {
|
|
95
|
+
return value.replace(/[\r\n\t]+/g, ' ').replace(/ {2,}/g, ' ');
|
|
96
|
+
}
|
|
97
|
+
function previousCharacterBoundary(value, cursor) {
|
|
98
|
+
const previous = Array.from(value.slice(0, cursor)).at(-1);
|
|
99
|
+
return previous ? cursor - previous.length : 0;
|
|
100
|
+
}
|
|
101
|
+
function nextCharacterBoundary(value, cursor) {
|
|
102
|
+
const next = Array.from(value.slice(cursor))[0];
|
|
103
|
+
return next ? cursor + next.length : value.length;
|
|
104
|
+
}
|
|
105
|
+
function openCustomTextEditor(state) {
|
|
106
|
+
const question = currentQuestion(state);
|
|
107
|
+
return {
|
|
108
|
+
...setCursor(state, question, OTHER_CURSOR),
|
|
109
|
+
selectedOptionIds: question.multiSelect
|
|
110
|
+
? state.selectedOptionIds
|
|
111
|
+
: {
|
|
112
|
+
...state.selectedOptionIds,
|
|
113
|
+
[question.id]: [],
|
|
114
|
+
},
|
|
115
|
+
noteOpen: true,
|
|
116
|
+
noteCursor: (state.customText[question.id] ?? '').length,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function insertNote(state, insertedText) {
|
|
120
|
+
const nextState = openCustomTextEditor(state);
|
|
121
|
+
const question = currentQuestion(nextState);
|
|
122
|
+
const current = nextState.customText[question.id] ?? '';
|
|
123
|
+
const inserted = normalizeNoteText(insertedText);
|
|
124
|
+
if (!inserted) {
|
|
125
|
+
return nextState;
|
|
126
|
+
}
|
|
127
|
+
const noteCursor = Math.max(0, Math.min(nextState.noteCursor, current.length));
|
|
128
|
+
const nextText = `${current.slice(0, noteCursor)}${inserted}${current.slice(noteCursor)}`;
|
|
129
|
+
return {
|
|
130
|
+
...nextState,
|
|
131
|
+
customText: {
|
|
132
|
+
...nextState.customText,
|
|
133
|
+
[question.id]: nextText,
|
|
134
|
+
},
|
|
135
|
+
noteCursor: noteCursor + inserted.length,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function editOpenNote(state, event) {
|
|
139
|
+
const question = currentQuestion(state);
|
|
140
|
+
const current = state.customText[question.id] ?? '';
|
|
141
|
+
const cursor = Math.max(0, Math.min(state.noteCursor, current.length));
|
|
142
|
+
if (event.escape || event.returnKey) {
|
|
143
|
+
return { ...state, noteOpen: false };
|
|
144
|
+
}
|
|
145
|
+
if (event.tab)
|
|
146
|
+
return state;
|
|
147
|
+
if ((event.upArrow || event.downArrow) && !current.trim()) {
|
|
148
|
+
return moveCursor({
|
|
149
|
+
...state,
|
|
150
|
+
customText: {
|
|
151
|
+
...state.customText,
|
|
152
|
+
[question.id]: '',
|
|
153
|
+
},
|
|
154
|
+
noteOpen: false,
|
|
155
|
+
noteCursor: 0,
|
|
156
|
+
}, event.upArrow ? -1 : 1);
|
|
157
|
+
}
|
|
158
|
+
if (event.leftArrow) {
|
|
159
|
+
return {
|
|
160
|
+
...state,
|
|
161
|
+
noteCursor: previousCharacterBoundary(current, cursor),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (event.rightArrow) {
|
|
165
|
+
return {
|
|
166
|
+
...state,
|
|
167
|
+
noteCursor: nextCharacterBoundary(current, cursor),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (event.home) {
|
|
171
|
+
return { ...state, noteCursor: 0 };
|
|
172
|
+
}
|
|
173
|
+
if (event.end) {
|
|
174
|
+
return { ...state, noteCursor: current.length };
|
|
175
|
+
}
|
|
176
|
+
if (event.backspace && cursor > 0) {
|
|
177
|
+
const previous = previousCharacterBoundary(current, cursor);
|
|
178
|
+
return {
|
|
179
|
+
...state,
|
|
180
|
+
customText: {
|
|
181
|
+
...state.customText,
|
|
182
|
+
[question.id]: `${current.slice(0, previous)}${current.slice(cursor)}`,
|
|
183
|
+
},
|
|
184
|
+
noteCursor: previous,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (event.delete && cursor < current.length) {
|
|
188
|
+
const next = nextCharacterBoundary(current, cursor);
|
|
189
|
+
return {
|
|
190
|
+
...state,
|
|
191
|
+
customText: {
|
|
192
|
+
...state.customText,
|
|
193
|
+
[question.id]: `${current.slice(0, cursor)}${current.slice(next)}`,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (!event.ctrl && !event.meta && event.input) {
|
|
198
|
+
return insertNote(state, event.input);
|
|
199
|
+
}
|
|
200
|
+
return state;
|
|
201
|
+
}
|
|
202
|
+
function questionAnswered(state, question) {
|
|
203
|
+
return (selectedIds(state, question).length > 0 ||
|
|
204
|
+
Boolean((state.customText[question.id] ?? '').trim()));
|
|
205
|
+
}
|
|
206
|
+
function submitResult(state) {
|
|
207
|
+
const missing = state.questions.find((question) => !questionAnswered(state, question));
|
|
208
|
+
if (missing) {
|
|
209
|
+
const next = setQuestion(state, state.questions.indexOf(missing));
|
|
210
|
+
return {
|
|
211
|
+
state: {
|
|
212
|
+
...next,
|
|
213
|
+
validationMessage: 'Choose an option or add details before submitting.',
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
state,
|
|
219
|
+
result: {
|
|
220
|
+
status: 'submitted',
|
|
221
|
+
answers: Object.fromEntries(state.questions.map((question) => {
|
|
222
|
+
const customText = (state.customText[question.id] ?? '').trim();
|
|
223
|
+
return [
|
|
224
|
+
question.id,
|
|
225
|
+
{
|
|
226
|
+
selectedOptionIds: selectedIds(state, question),
|
|
227
|
+
...(customText ? { customText } : {}),
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
})),
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export function createUserInputPromptState(questions, returnStatus) {
|
|
235
|
+
const first = questions[0];
|
|
236
|
+
return {
|
|
237
|
+
questions,
|
|
238
|
+
questionId: first.id,
|
|
239
|
+
cursorByQuestionId: Object.fromEntries(questions.map((question) => [question.id, defaultCursor(question)])),
|
|
240
|
+
selectedOptionIds: Object.fromEntries(questions.map((question) => [question.id, []])),
|
|
241
|
+
customText: Object.fromEntries(questions.map((question) => [question.id, ''])),
|
|
242
|
+
contentScrollAnchor: null,
|
|
243
|
+
contentScrollOffset: 0,
|
|
244
|
+
noteOpen: false,
|
|
245
|
+
noteCursor: 0,
|
|
246
|
+
returnStatus,
|
|
247
|
+
validationMessage: '',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export function handleUserInputPromptEvent(state, event, viewport) {
|
|
251
|
+
const visibleState = viewport
|
|
252
|
+
? normalizeUserInputScrollState(state, viewport)
|
|
253
|
+
: state;
|
|
254
|
+
if (event.kind === 'paste') {
|
|
255
|
+
return {
|
|
256
|
+
state: visibleState.noteOpen ||
|
|
257
|
+
cursorForQuestion(visibleState, currentQuestion(visibleState)) ===
|
|
258
|
+
OTHER_CURSOR
|
|
259
|
+
? insertNote(visibleState, event.text)
|
|
260
|
+
: visibleState,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (event.pageUp || event.pageDown) {
|
|
264
|
+
if (!viewport) {
|
|
265
|
+
return {
|
|
266
|
+
state: {
|
|
267
|
+
...visibleState,
|
|
268
|
+
contentScrollAnchor: null,
|
|
269
|
+
contentScrollOffset: visibleState.contentScrollOffset +
|
|
270
|
+
(event.pageUp ? -SCROLL_PAGE_ROWS : SCROLL_PAGE_ROWS),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const window = buildUserInputWindow(visibleState, viewport.width, viewport.maxRows);
|
|
275
|
+
return {
|
|
276
|
+
state: {
|
|
277
|
+
...visibleState,
|
|
278
|
+
contentScrollAnchor: null,
|
|
279
|
+
contentScrollOffset: Math.max(0, Math.min(window.offset +
|
|
280
|
+
(event.pageUp ? -SCROLL_PAGE_ROWS : SCROLL_PAGE_ROWS), window.maxOffset)),
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (visibleState.noteOpen) {
|
|
285
|
+
return { state: editOpenNote(visibleState, event) };
|
|
286
|
+
}
|
|
287
|
+
if (event.escape) {
|
|
288
|
+
return { state: visibleState, result: { status: 'cancelled' } };
|
|
289
|
+
}
|
|
290
|
+
if (event.upArrow || event.downArrow) {
|
|
291
|
+
return {
|
|
292
|
+
state: moveCursor(visibleState, event.upArrow ? -1 : 1),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (event.leftArrow) {
|
|
296
|
+
return {
|
|
297
|
+
state: setQuestion(visibleState, questionIndex(visibleState) - 1),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (event.rightArrow) {
|
|
301
|
+
const index = questionIndex(visibleState);
|
|
302
|
+
if (index < visibleState.questions.length - 1) {
|
|
303
|
+
return { state: setQuestion(visibleState, index + 1) };
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
state: setCursor(visibleState, currentQuestion(visibleState), SUBMIT_CURSOR),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const question = currentQuestion(visibleState);
|
|
310
|
+
const numberedRow = /^[1-5]$/.test(event.input)
|
|
311
|
+
? Number(event.input)
|
|
312
|
+
: 0;
|
|
313
|
+
if (numberedRow && !event.ctrl && !event.meta) {
|
|
314
|
+
if (numberedRow === question.options.length + 1) {
|
|
315
|
+
return {
|
|
316
|
+
state: openCustomTextEditor(visibleState),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const numberedOption = question.options[numberedRow - 1];
|
|
320
|
+
if (numberedOption) {
|
|
321
|
+
return { state: selectOption(visibleState, numberedOption.id) };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (event.returnKey) {
|
|
325
|
+
const cursor = cursorForQuestion(visibleState, question);
|
|
326
|
+
if (cursor === OTHER_CURSOR) {
|
|
327
|
+
return {
|
|
328
|
+
state: openCustomTextEditor(visibleState),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (cursor === SUBMIT_CURSOR) {
|
|
332
|
+
return submitResult(visibleState);
|
|
333
|
+
}
|
|
334
|
+
return { state: selectOption(visibleState, cursor) };
|
|
335
|
+
}
|
|
336
|
+
if (!event.ctrl &&
|
|
337
|
+
!event.meta &&
|
|
338
|
+
event.input &&
|
|
339
|
+
event.input >= ' ' &&
|
|
340
|
+
cursorForQuestion(visibleState, currentQuestion(visibleState)) ===
|
|
341
|
+
OTHER_CURSOR) {
|
|
342
|
+
return { state: insertNote(visibleState, event.input) };
|
|
343
|
+
}
|
|
344
|
+
return { state: visibleState };
|
|
345
|
+
}
|
|
346
|
+
function noteInputLine(state, width) {
|
|
347
|
+
const question = currentQuestion(state);
|
|
348
|
+
const text = state.customText[question.id] ?? '';
|
|
349
|
+
const cursor = Math.max(0, Math.min(state.noteCursor, text.length));
|
|
350
|
+
const characters = Array.from(text);
|
|
351
|
+
const characterCursor = Array.from(text.slice(0, cursor)).length;
|
|
352
|
+
const available = Math.max(8, width - 8);
|
|
353
|
+
const windowStart = Math.max(0, characterCursor - available + 1);
|
|
354
|
+
const visible = characters.slice(windowStart, windowStart + available);
|
|
355
|
+
const visibleCursor = characterCursor - windowStart;
|
|
356
|
+
return line(span(' › ', { color: ACCENT_COLOR }), span(visible.slice(0, visibleCursor).join('')), span(visible[visibleCursor] ?? ' ', { inverse: true }), span(visible.slice(visibleCursor + 1).join('')));
|
|
357
|
+
}
|
|
358
|
+
function sliceWithinWidth(text, width) {
|
|
359
|
+
if (width <= 0)
|
|
360
|
+
return '';
|
|
361
|
+
let result = sliceToWidth(text, width);
|
|
362
|
+
while (result && displayWidth(result) > width) {
|
|
363
|
+
result = Array.from(result).slice(0, -1).join('');
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
function fitText(text, width) {
|
|
368
|
+
if (width <= 0)
|
|
369
|
+
return '';
|
|
370
|
+
if (displayWidth(text) <= width)
|
|
371
|
+
return text;
|
|
372
|
+
if (width === 1)
|
|
373
|
+
return '…';
|
|
374
|
+
return `${sliceWithinWidth(text, width - 1).trimEnd()}…`;
|
|
375
|
+
}
|
|
376
|
+
function clipLineToWidth(row, width) {
|
|
377
|
+
let remaining = Math.max(0, width);
|
|
378
|
+
const spans = [];
|
|
379
|
+
for (const item of row.spans) {
|
|
380
|
+
if (remaining <= 0)
|
|
381
|
+
break;
|
|
382
|
+
const text = sliceWithinWidth(item.text, remaining);
|
|
383
|
+
if (text)
|
|
384
|
+
spans.push({ ...item, text });
|
|
385
|
+
remaining -= displayWidth(text);
|
|
386
|
+
}
|
|
387
|
+
return { spans };
|
|
388
|
+
}
|
|
389
|
+
function buildUserInputContent(state, width) {
|
|
390
|
+
const question = currentQuestion(state);
|
|
391
|
+
const index = questionIndex(state);
|
|
392
|
+
const cursor = cursorForQuestion(state, question);
|
|
393
|
+
const selected = new Set(selectedIds(state, question));
|
|
394
|
+
let anchorRange = null;
|
|
395
|
+
const progress = `Question ${index + 1} of ${state.questions.length}`;
|
|
396
|
+
const headerPrefix = ' · ';
|
|
397
|
+
const headerBudget = Math.max(0, width - displayWidth(progress) - displayWidth(headerPrefix));
|
|
398
|
+
const lines = [
|
|
399
|
+
line(span(progress, {
|
|
400
|
+
color: ACCENT_COLOR,
|
|
401
|
+
bold: true,
|
|
402
|
+
}), ...(headerBudget > 0
|
|
403
|
+
? [
|
|
404
|
+
span(`${headerPrefix}${fitText(question.header, headerBudget)}`, { color: 'gray' }),
|
|
405
|
+
]
|
|
406
|
+
: [])),
|
|
407
|
+
plainLine(''),
|
|
408
|
+
...wrapText(question.question, width).map((text) => plainLine(text, { bold: true })),
|
|
409
|
+
plainLine(''),
|
|
410
|
+
];
|
|
411
|
+
question.options.forEach((option, optionIndex) => {
|
|
412
|
+
const focused = cursor === option.id;
|
|
413
|
+
const checked = selected.has(option.id);
|
|
414
|
+
const marker = question.multiSelect
|
|
415
|
+
? checked
|
|
416
|
+
? '[x]'
|
|
417
|
+
: '[ ]'
|
|
418
|
+
: checked
|
|
419
|
+
? '●'
|
|
420
|
+
: '○';
|
|
421
|
+
const blockStart = lines.length;
|
|
422
|
+
const optionPrefix = `${optionIndex + 1}. ${marker} `;
|
|
423
|
+
const recommendation = option.id === question.recommendedOptionId ? ' Recommended' : '';
|
|
424
|
+
const labelBudget = Math.max(1, width -
|
|
425
|
+
displayWidth('› ') -
|
|
426
|
+
displayWidth(optionPrefix) -
|
|
427
|
+
displayWidth(recommendation));
|
|
428
|
+
lines.push(line(span(focused ? '› ' : ' ', {
|
|
429
|
+
color: focused ? ACCENT_COLOR : 'gray',
|
|
430
|
+
}), span(`${optionPrefix}${fitText(option.label, labelBudget)}`, {
|
|
431
|
+
color: focused ? ACCENT_COLOR : undefined,
|
|
432
|
+
bold: focused,
|
|
433
|
+
}), ...(recommendation
|
|
434
|
+
? [span(recommendation, { color: 'green' })]
|
|
435
|
+
: [])));
|
|
436
|
+
for (const description of wrapText(option.description, Math.max(8, width - 5))) {
|
|
437
|
+
lines.push(plainLine(` ${description}`, { color: 'gray' }));
|
|
438
|
+
}
|
|
439
|
+
if (state.contentScrollAnchor === option.id) {
|
|
440
|
+
anchorRange = { start: blockStart, end: lines.length - 1 };
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
const customText = (state.customText[question.id] ?? '').trim();
|
|
444
|
+
const otherFocused = cursor === OTHER_CURSOR;
|
|
445
|
+
const otherAnswered = Boolean(customText);
|
|
446
|
+
const otherMarker = question.multiSelect
|
|
447
|
+
? otherAnswered
|
|
448
|
+
? '[x]'
|
|
449
|
+
: '[ ]'
|
|
450
|
+
: otherAnswered
|
|
451
|
+
? '●'
|
|
452
|
+
: '○';
|
|
453
|
+
const otherNumber = question.options.length + 1;
|
|
454
|
+
const otherStart = lines.length;
|
|
455
|
+
lines.push(plainLine(''));
|
|
456
|
+
lines.push(line(span(otherFocused ? '› ' : ' ', {
|
|
457
|
+
color: otherFocused ? ACCENT_COLOR : 'gray',
|
|
458
|
+
}), span(`${otherNumber}. ${otherMarker} Something else / add details`, {
|
|
459
|
+
color: otherFocused ? ACCENT_COLOR : undefined,
|
|
460
|
+
bold: otherFocused,
|
|
461
|
+
}), ...(customText && !state.noteOpen
|
|
462
|
+
? [
|
|
463
|
+
span(` · ${Array.from(customText)
|
|
464
|
+
.slice(0, Math.max(8, width - 28))
|
|
465
|
+
.join('')}`, { color: 'gray' }),
|
|
466
|
+
]
|
|
467
|
+
: otherFocused && !state.noteOpen
|
|
468
|
+
? [span(' · Type details…', { color: 'gray', dim: true })]
|
|
469
|
+
: [])));
|
|
470
|
+
if (state.noteOpen) {
|
|
471
|
+
lines.push(noteInputLine(state, width));
|
|
472
|
+
}
|
|
473
|
+
lines.push(plainLine(''));
|
|
474
|
+
if (state.contentScrollAnchor === OTHER_CURSOR) {
|
|
475
|
+
anchorRange = { start: otherStart, end: lines.length - 1 };
|
|
476
|
+
}
|
|
477
|
+
if (index === state.questions.length - 1) {
|
|
478
|
+
const submitStart = lines.length - 1;
|
|
479
|
+
lines.push(line(span(cursor === SUBMIT_CURSOR ? '› ' : ' ', {
|
|
480
|
+
color: cursor === SUBMIT_CURSOR ? ACCENT_COLOR : 'gray',
|
|
481
|
+
}), span('Submit answers', {
|
|
482
|
+
color: cursor === SUBMIT_CURSOR ? ACCENT_COLOR : undefined,
|
|
483
|
+
bold: cursor === SUBMIT_CURSOR,
|
|
484
|
+
})));
|
|
485
|
+
lines.push(plainLine(''));
|
|
486
|
+
if (state.contentScrollAnchor === SUBMIT_CURSOR) {
|
|
487
|
+
anchorRange = { start: submitStart, end: lines.length - 1 };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (state.validationMessage) {
|
|
491
|
+
lines.push(plainLine(state.validationMessage, { color: 'yellow' }));
|
|
492
|
+
}
|
|
493
|
+
lines.push(plainLine(state.noteOpen
|
|
494
|
+
? 'Type a single-line note • ↑/↓ moves when empty • Enter saves • Esc closes • PgUp/PgDn scroll'
|
|
495
|
+
: question.multiSelect
|
|
496
|
+
? `↑/↓ move • 1–${otherNumber} choose • Enter toggles/edits • ←/→ questions • PgUp/PgDn scroll • Esc cancels`
|
|
497
|
+
: `↑/↓ move • 1–${otherNumber} choose • Enter selects/edits • ←/→ questions • PgUp/PgDn scroll • Esc cancels`, { color: 'gray' }));
|
|
498
|
+
return {
|
|
499
|
+
anchorRange,
|
|
500
|
+
lines: lines.map((row) => clipLineToWidth(row, width)),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function buildUserInputWindow(state, width, maxRows) {
|
|
504
|
+
const content = buildUserInputContent(state, width);
|
|
505
|
+
if (!Number.isFinite(maxRows) || content.lines.length <= maxRows) {
|
|
506
|
+
return { lines: content.lines, maxOffset: 0, offset: 0 };
|
|
507
|
+
}
|
|
508
|
+
const rowBudget = Math.max(1, Math.floor(maxRows));
|
|
509
|
+
const visibleRowBudget = rowBudget === 1 ? 1 : rowBudget - 1;
|
|
510
|
+
const maxOffset = Math.max(0, content.lines.length - visibleRowBudget);
|
|
511
|
+
let offset = Math.max(0, Math.min(state.contentScrollOffset, maxOffset));
|
|
512
|
+
const range = content.anchorRange;
|
|
513
|
+
if (range) {
|
|
514
|
+
const blockRows = range.end - range.start + 1;
|
|
515
|
+
if (blockRows <= visibleRowBudget) {
|
|
516
|
+
if (range.start < offset) {
|
|
517
|
+
offset = range.start;
|
|
518
|
+
}
|
|
519
|
+
else if (range.end >= offset + visibleRowBudget) {
|
|
520
|
+
offset = range.end - visibleRowBudget + 1;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else if (range.start < offset ||
|
|
524
|
+
range.start >= offset + visibleRowBudget) {
|
|
525
|
+
offset = range.start;
|
|
526
|
+
}
|
|
527
|
+
offset = Math.max(0, Math.min(offset, maxOffset));
|
|
528
|
+
}
|
|
529
|
+
const visible = content.lines.slice(offset, offset + visibleRowBudget);
|
|
530
|
+
if (rowBudget === 1) {
|
|
531
|
+
return { lines: visible, maxOffset, offset };
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
lines: [
|
|
535
|
+
...visible,
|
|
536
|
+
plainLine(fitText(`Rows ${offset + 1}–${offset + visible.length} of ${content.lines.length} · PgUp/PgDn scroll`, width), { color: 'gray' }),
|
|
537
|
+
],
|
|
538
|
+
maxOffset,
|
|
539
|
+
offset,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
export function normalizeUserInputScrollState(state, viewport) {
|
|
543
|
+
const window = buildUserInputWindow(state, viewport.width, viewport.maxRows);
|
|
544
|
+
return window.offset === state.contentScrollOffset
|
|
545
|
+
? state
|
|
546
|
+
: { ...state, contentScrollOffset: window.offset };
|
|
547
|
+
}
|
|
548
|
+
export function buildUserInputOverlayLines(state, width, maxRows = Number.POSITIVE_INFINITY) {
|
|
549
|
+
return buildUserInputWindow(state, width, maxRows).lines;
|
|
550
|
+
}
|
|
551
|
+
export function formatUserInputTranscript(questions, result) {
|
|
552
|
+
if (result.status === 'cancelled') {
|
|
553
|
+
return 'Cancelled without answers.';
|
|
554
|
+
}
|
|
555
|
+
return questions
|
|
556
|
+
.map((question) => {
|
|
557
|
+
const answer = result.answers[question.id];
|
|
558
|
+
const selected = question.options
|
|
559
|
+
.filter((option) => answer?.selectedOptionIds.includes(option.id))
|
|
560
|
+
.map((option) => option.label);
|
|
561
|
+
const values = [
|
|
562
|
+
...selected,
|
|
563
|
+
...(answer?.customText ? [answer.customText] : []),
|
|
564
|
+
];
|
|
565
|
+
return `${question.header} · ${question.question}\n ${values.join(', ')}`;
|
|
566
|
+
})
|
|
567
|
+
.join('\n');
|
|
568
|
+
}
|
package/dist/src/utils.js
CHANGED
|
@@ -17,6 +17,15 @@ export function truncate(text, maxChars = 4000) {
|
|
|
17
17
|
return text;
|
|
18
18
|
return `${text.slice(0, maxChars)}\n... (truncated)`;
|
|
19
19
|
}
|
|
20
|
+
export function singleLinePreview(text, maxChars) {
|
|
21
|
+
const collapsed = String(text ?? '')
|
|
22
|
+
.replace(/\n\.\.\. \(truncated\)\s*$/, '…')
|
|
23
|
+
.replace(/\s+/g, ' ')
|
|
24
|
+
.trim();
|
|
25
|
+
if (collapsed.length <= maxChars)
|
|
26
|
+
return collapsed;
|
|
27
|
+
return `${collapsed.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
|
|
28
|
+
}
|
|
20
29
|
export function clampInteger(value, fallback, max) {
|
|
21
30
|
const numeric = Number(value);
|
|
22
31
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
package/package.json
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
4
|
-
"description": "TheGitAI
|
|
3
|
+
"version": "1.0.0-preview.21",
|
|
4
|
+
"description": "TheGitAI is an AI coding agent for your terminal. It indexes your repository, writes and edits files, runs commands, and builds features with you.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"ai-coding-agent",
|
|
8
|
+
"coding-agent",
|
|
9
|
+
"coding-assistant",
|
|
10
|
+
"terminal",
|
|
11
|
+
"cli",
|
|
12
|
+
"developer-tools"
|
|
13
|
+
],
|
|
5
14
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
15
|
"homepage": "https://thegit.ai",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"email": "support@thegit.ai"
|
|
18
|
+
},
|
|
7
19
|
"type": "module",
|
|
8
20
|
"engines": {
|
|
9
21
|
"node": ">=24"
|
|
@@ -25,10 +37,10 @@
|
|
|
25
37
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
26
38
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
27
39
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
28
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
29
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
30
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
31
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
40
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.21",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.21",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.21",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.21",
|
|
32
44
|
"@vscode/ripgrep": "1.18.0"
|
|
33
45
|
},
|
|
34
46
|
"publishConfig": {
|