@thegitai/cli 1.0.0-preview.32 → 1.0.0-preview.34

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.
@@ -4,6 +4,7 @@ import { approvalScrollLimit, buildTuiFrame, formatJobElapsed, formatTodoProgres
4
4
  import { createTerminalTitleController } from './tui/terminal-title.js';
5
5
  import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
6
6
  export { getSlashCommandSuggestions } from './tui/build-frame.js';
7
+ import { composerInputWidth, frameContentWidth } from './tui/composer-layout.js';
7
8
  import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
8
9
  import { chat, models } from '../api/index.js';
9
10
  import { isTurnCancelledError } from '../api/chat.js';
@@ -2951,11 +2952,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2951
2952
  };
2952
2953
  const shellInputHandlers = {
2953
2954
  getApprovalScrollLimit: () => {
2954
- const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
2955
+ const contentWidth = frameContentWidth(terminalCols);
2955
2956
  return approvalScrollLimit(store.getState().approvalPrompt, contentWidth, terminalRows);
2956
2957
  },
2958
+ getComposerInputWidth: () => composerInputWidth(terminalCols, store.getState()),
2957
2959
  getTranscriptScrollLimit: () => {
2958
- const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
2960
+ const contentWidth = frameContentWidth(terminalCols);
2959
2961
  const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
2960
2962
  return blocks.reduce((total, block, index) => total + block.length + (index > 0 ? 1 : 0), 0);
2961
2963
  },
@@ -5,6 +5,7 @@ import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdo
5
5
  import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, } from './text.js';
6
6
  import { isDarkTerminalBackground, mutedColor, mutedStyle } from './terminal-theme.js';
7
7
  import { buildUserInputOverlayLines, } from './user-input.js';
8
+ import { composerPromptLabel, frameContentWidth, splitComposerInput, } from './composer-layout.js';
8
9
  const WORKING_CLOCK_ICON = '◷';
9
10
  const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
10
11
  const TODO_PANEL_MAX_ROWS = 12;
@@ -40,51 +41,6 @@ const MODEL_PICKER_BORDER_COLOR = 'cyan';
40
41
  const MODEL_PICKER_ACCENT_COLOR = 'cyan';
41
42
  const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
42
43
  const MODEL_PICKER_META_INDENT = ' ';
43
- function splitComposerInput(input, cursor, width) {
44
- const safeWidth = Math.max(1, width);
45
- const normalizedCursor = Math.min(Math.max(cursor, 0), input.length);
46
- const rows = [''];
47
- let cursorRow = 0;
48
- let cursorCol = 0;
49
- let capturedCursor = false;
50
- const captureCursor = () => {
51
- if (capturedCursor)
52
- return;
53
- const current = rows[rows.length - 1] ?? '';
54
- if (current.length >= safeWidth) {
55
- rows.push('');
56
- cursorRow = rows.length - 1;
57
- cursorCol = 0;
58
- }
59
- else {
60
- cursorRow = rows.length - 1;
61
- cursorCol = current.length;
62
- }
63
- capturedCursor = true;
64
- };
65
- for (let index = 0; index < input.length; index += 1) {
66
- const char = input[index] ?? '';
67
- if (char !== '\n' &&
68
- char !== '\r' &&
69
- (rows[rows.length - 1]?.length ?? 0) >= safeWidth) {
70
- rows.push('');
71
- }
72
- if (index === normalizedCursor) {
73
- captureCursor();
74
- }
75
- if (char === '\r')
76
- continue;
77
- if (char === '\n') {
78
- rows.push('');
79
- continue;
80
- }
81
- rows[rows.length - 1] = `${rows[rows.length - 1] ?? ''}${char}`;
82
- }
83
- if (!capturedCursor) {
84
- captureCursor();
85
- }
86
- return { cursorCol, cursorRow, rows };
87
- }
88
44
  function buildComposerInputLines(input, cursor, promptLabel, placeholder, width) {
89
45
  if (!input) {
90
46
  return [
@@ -1190,7 +1146,7 @@ function countSectionLines(sections) {
1190
1146
  return sections.reduce((sum, section) => sum + section.lines.length, 0);
1191
1147
  }
1192
1148
  export function userInputViewportForFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
1193
- const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
1149
+ const contentWidth = frameContentWidth(cols);
1194
1150
  const liveRows = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs).length;
1195
1151
  const overlayHeight = Math.max(0, rows - liveRows - composerFooterLines(state).length);
1196
1152
  return {
@@ -1208,7 +1164,7 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
1208
1164
  return lines.slice(start, start + maxLines);
1209
1165
  }
1210
1166
  export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
1211
- const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
1167
+ const contentWidth = frameContentWidth(cols);
1212
1168
  const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
1213
1169
  const buildTranscriptLines = (wrapWidth) => {
1214
1170
  const blocks = state.transcript.map((entry) => cachedTranscriptEntryLines(entry, wrapWidth));
@@ -1240,7 +1196,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
1240
1196
  : ' sends it to the agent now', { color: 'gray' }), span(' · ↑', { color: 'cyan', bold: true }), span(' edit', { color: 'gray' }), span(' · Esc', { color: 'cyan', bold: true }), span(' discard', { color: 'gray' })));
1241
1197
  }
1242
1198
  else {
1243
- const promptLabel = state.busy ? 'queue> ' : '❯ ';
1199
+ const promptLabel = composerPromptLabel(state);
1244
1200
  const placeholder = state.busy
1245
1201
  ? 'Type to queue the next message'
1246
1202
  : 'Type a request or /help';
@@ -0,0 +1,76 @@
1
+ export const COMPOSER_PROMPT_LABEL_IDLE = '❯ ';
2
+ export const COMPOSER_PROMPT_LABEL_BUSY = 'queue> ';
3
+ export function composerPromptLabel(state) {
4
+ return state.busy ? COMPOSER_PROMPT_LABEL_BUSY : COMPOSER_PROMPT_LABEL_IDLE;
5
+ }
6
+ export function frameContentWidth(cols) {
7
+ return Math.max(20, Math.floor(cols * 0.95) - 2);
8
+ }
9
+ export function composerInputWidth(cols, state) {
10
+ return Math.max(1, frameContentWidth(cols) - composerPromptLabel(state).length);
11
+ }
12
+ function buildComposerLayout(input, cursor, width) {
13
+ const safeWidth = Math.max(1, width);
14
+ const normalizedCursor = Math.min(Math.max(cursor, 0), input.length);
15
+ const rows = [{ cursorOffsets: [0], text: '' }];
16
+ let cursorRow = 0;
17
+ let cursorCol = 0;
18
+ let capturedCursor = false;
19
+ const currentRow = () => rows[rows.length - 1];
20
+ const addRow = (offset) => {
21
+ rows.push({ cursorOffsets: [offset], text: '' });
22
+ };
23
+ const captureCursor = () => {
24
+ if (capturedCursor)
25
+ return;
26
+ if (currentRow().text.length >= safeWidth) {
27
+ addRow(normalizedCursor);
28
+ }
29
+ cursorRow = rows.length - 1;
30
+ cursorCol = currentRow().text.length;
31
+ capturedCursor = true;
32
+ };
33
+ for (let index = 0; index < input.length; index += 1) {
34
+ const char = input[index] ?? '';
35
+ if (char !== '\n' &&
36
+ char !== '\r' &&
37
+ currentRow().text.length >= safeWidth) {
38
+ addRow(index);
39
+ }
40
+ if (index === normalizedCursor) {
41
+ captureCursor();
42
+ }
43
+ if (char === '\r') {
44
+ currentRow().cursorOffsets[currentRow().text.length] = index + 1;
45
+ continue;
46
+ }
47
+ if (char === '\n') {
48
+ currentRow().cursorOffsets[currentRow().text.length] =
49
+ input[index - 1] === '\r' ? index - 1 : index;
50
+ addRow(index + 1);
51
+ continue;
52
+ }
53
+ currentRow().text += char;
54
+ currentRow().cursorOffsets.push(index + 1);
55
+ }
56
+ if (!capturedCursor) {
57
+ captureCursor();
58
+ }
59
+ return { cursorCol, cursorRow, rows };
60
+ }
61
+ export function splitComposerInput(input, cursor, width) {
62
+ const layout = buildComposerLayout(input, cursor, width);
63
+ return {
64
+ cursorCol: layout.cursorCol,
65
+ cursorRow: layout.cursorRow,
66
+ rows: layout.rows.map((row) => row.text),
67
+ };
68
+ }
69
+ export function moveComposerCursorVertically(input, cursor, width, direction) {
70
+ const layout = buildComposerLayout(input, cursor, width);
71
+ const targetRow = layout.rows[layout.cursorRow + direction];
72
+ if (!targetRow)
73
+ return null;
74
+ const targetCol = Math.min(layout.cursorCol, targetRow.text.length);
75
+ return targetRow.cursorOffsets[targetCol] ?? null;
76
+ }
@@ -1,3 +1,4 @@
1
+ import { moveComposerCursorVertically } from './composer-layout.js';
1
2
  import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
2
3
  import { MAX_IMAGES_PER_MESSAGE, MAX_TOTAL_IMAGE_BYTES_PER_MESSAGE, approximateBase64DecodedBytes, totalAttachmentBytes, } from '../../core/image-limits.js';
3
4
  import { tryCacheAttachmentBytes } from '../../core/session-image-store.js';
@@ -479,9 +480,13 @@ export function handleShellKeyEvent(store, handlers, event) {
479
480
  });
480
481
  return;
481
482
  }
482
- if (state.busy)
483
+ const nextCursor = moveComposerCursorVertically(state.input, state.cursor, handlers.getComposerInputWidth?.() ?? Number.POSITIVE_INFINITY, -1);
484
+ if (nextCursor === null && state.busy)
483
485
  return;
484
486
  store.update((current) => {
487
+ if (nextCursor !== null) {
488
+ return { ...current, commandCursor: 0, cursor: nextCursor };
489
+ }
485
490
  const next = navigatePromptHistory(current, 'previous');
486
491
  prepareForComposerInputChange(current, next.input);
487
492
  return next;
@@ -489,9 +494,13 @@ export function handleShellKeyEvent(store, handlers, event) {
489
494
  return;
490
495
  }
491
496
  if (key.downArrow) {
492
- if (state.busy)
497
+ const nextCursor = moveComposerCursorVertically(state.input, state.cursor, handlers.getComposerInputWidth?.() ?? Number.POSITIVE_INFINITY, 1);
498
+ if (nextCursor === null && state.busy)
493
499
  return;
494
500
  store.update((current) => {
501
+ if (nextCursor !== null) {
502
+ return { ...current, commandCursor: 0, cursor: nextCursor };
503
+ }
495
504
  const next = navigatePromptHistory(current, 'next');
496
505
  prepareForComposerInputChange(current, next.input);
497
506
  return next;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.32",
3
+ "version": "1.0.0-preview.34",
4
4
  "description": "TheGitAI is an agentic AI coding tool for your terminal. It reads and searches your repository, writes and edits files, runs your tests, and verifies the change before handing it back.",
5
5
  "keywords": [
6
6
  "agentic-ai",
@@ -44,10 +44,11 @@
44
44
  "@lydell/node-pty-linux-x64": "1.1.0",
45
45
  "@lydell/node-pty-win32-arm64": "1.1.0",
46
46
  "@lydell/node-pty-win32-x64": "1.1.0",
47
- "@thegitai/tui-darwin-arm64": "1.0.0-preview.32",
48
- "@thegitai/tui-darwin-x64": "1.0.0-preview.32",
49
- "@thegitai/tui-linux-x64": "1.0.0-preview.32",
50
- "@thegitai/tui-win32-x64": "1.0.0-preview.32",
47
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.34",
48
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.34",
49
+ "@thegitai/tui-linux-arm64": "1.0.0-preview.34",
50
+ "@thegitai/tui-linux-x64": "1.0.0-preview.34",
51
+ "@thegitai/tui-win32-x64": "1.0.0-preview.34",
51
52
  "@vscode/ripgrep": "1.18.0"
52
53
  },
53
54
  "repository": {