@thegitai/cli 1.0.0-preview.2 → 1.0.0-preview.20

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 (38) hide show
  1. package/README.md +32 -4
  2. package/dist/bin/ai.js +57 -291
  3. package/dist/src/agent-mode.js +1 -1
  4. package/dist/src/api/auth.js +2 -2
  5. package/dist/src/api/browser-login.js +72 -3
  6. package/dist/src/api/chat.js +232 -33
  7. package/dist/src/api/contracts.js +55 -1
  8. package/dist/src/api/http.js +16 -3
  9. package/dist/src/api/models.js +9 -4
  10. package/dist/src/executor.js +1 -1
  11. package/dist/src/help-text.js +51 -11
  12. package/dist/src/permissions.js +243 -0
  13. package/dist/src/project-index.js +13 -1
  14. package/dist/src/session-store.js +57 -20
  15. package/dist/src/session.js +14 -3
  16. package/dist/src/tool-executor.js +2 -2
  17. package/dist/src/tools/delete-file.js +14 -0
  18. package/dist/src/tools/patch-file.js +12 -16
  19. package/dist/src/tools/replace-document-text.js +28 -18
  20. package/dist/src/tools/run-command.js +13 -27
  21. package/dist/src/tools/run-node-script.js +11 -26
  22. package/dist/src/tools/str-replace.js +12 -16
  23. package/dist/src/tools/write-file.js +66 -0
  24. package/dist/src/turn-failure-marker.js +11 -0
  25. package/dist/src/ui/prompt-history-store.js +1 -1
  26. package/dist/src/ui/repl.js +569 -151
  27. package/dist/src/ui/tui/bridge.js +10 -0
  28. package/dist/src/ui/tui/build-frame.js +535 -159
  29. package/dist/src/ui/tui/markdown-render.js +81 -73
  30. package/dist/src/ui/tui/shell-input.js +155 -45
  31. package/dist/src/ui/tui/terminal-theme.js +28 -0
  32. package/dist/src/ui/tui/terminal-title.js +3 -0
  33. package/dist/src/ui/tui/terminal-writes.js +48 -0
  34. package/dist/src/ui/tui/text.js +158 -4
  35. package/dist/src/ui/tui/user-input.js +568 -0
  36. package/dist/src/utils.js +9 -0
  37. package/package.json +18 -6
  38. 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.2",
4
- "description": "TheGitAI CLI client (source-visible, proprietary)",
3
+ "version": "1.0.0-preview.20",
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.2",
29
- "@thegitai/tui-darwin-x64": "1.0.0-preview.2",
30
- "@thegitai/tui-linux-x64": "1.0.0-preview.2",
31
- "@thegitai/tui-win32-x64": "1.0.0-preview.2",
40
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.20",
41
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.20",
42
+ "@thegitai/tui-linux-x64": "1.0.0-preview.20",
43
+ "@thegitai/tui-win32-x64": "1.0.0-preview.20",
32
44
  "@vscode/ripgrep": "1.18.0"
33
45
  },
34
46
  "publishConfig": {