@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.4

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 CHANGED
@@ -1,8 +1,10 @@
1
- # TheGitAI
1
+ # TheGitAI — AI coding agent for your terminal
2
2
 
3
- Interactive terminal coding agent. Talk to your repo in plain English — it reads
4
- your code, makes edits, runs the commands a task needs, and keeps long-running
5
- processes going while you work, all with your approval.
3
+ TheGitAI is an AI coding agent for your terminal. It indexes your repository,
4
+ writes and edits files, runs commands, and builds features with you.
5
+
6
+ Talk to your repo in plain English. TheGitAI can keep long-running processes
7
+ going while you work, all with your approval.
6
8
 
7
9
  ## Install
8
10
 
@@ -26,6 +28,12 @@ ai --version print the version and exit
26
28
 
27
29
  Run `ai --help` for sessions, modes, keys, and chat commands.
28
30
 
31
+ Coding sessions require an interactive terminal on stdin and stdout. Piped
32
+ one-shot prompts are not supported. Saved history is local to the repo and can
33
+ always be viewed or resumed from that computer. Continuing it through the
34
+ service requires the TheGitAI account used for that session. Local sessions can
35
+ also be listed while signed out or offline.
36
+
29
37
  ## Visible to-do list
30
38
 
31
39
  For larger multi-step tasks, the agent keeps a compact to-do list on screen so
package/dist/bin/ai.js CHANGED
@@ -5,20 +5,16 @@ import readline from 'node:readline/promises';
5
5
  import { ServerApi } from '../src/api/index.js';
6
6
  import { loginViaBrowser } from '../src/api/browser-login.js';
7
7
  import { isTransientNetworkError } from '../src/api/http.js';
8
- import { formatCliHelpText, formatInteractiveHelpText, } from '../src/help-text.js';
9
- import { renderMarkdownForTerminal } from '../src/markdown-renderer.js';
8
+ import { formatCliHelpText } from '../src/help-text.js';
10
9
  import { createIndex } from '../src/project-index.js';
11
- import { createSession, clearConversation } from '../src/session.js';
10
+ import { createSession } from '../src/session.js';
12
11
  import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
13
- import { runClientInteractive, shouldUseClientRatatuiShell, } from '../src/ui/repl.js';
12
+ import { runClientInteractive } from '../src/ui/repl.js';
14
13
  import { appendPromptToFile } from '../src/ui/prompt-history-store.js';
15
14
  import { formatSessionExitNotice } from '../src/session-exit.js';
16
15
  import { formatUsageText } from '../src/usage.js';
17
16
  import { formatVersionLine } from '../src/version.js';
18
17
  import { parseArgs } from '../src/cli-args.js';
19
- import { getJobBufferedOutput, killBackgroundJob, killAllBackgroundJobs, listBackgroundJobs, setBackgroundJobSession, } from '../src/background-jobs.js';
20
- import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../src/tool-executor.js';
21
- import { setScratchSession } from '../src/scratch-dir.js';
22
18
  const DEFAULT_SERVER_URL = 'https://thegit.ai';
23
19
  const { auth, chat, models, sessions } = ServerApi;
24
20
  function printUsage() {
@@ -112,52 +108,12 @@ function printSessionList(rootDir, sessions, serverModels) {
112
108
  }
113
109
  }
114
110
  }
115
- function printStartupBanner(rootDir, modelLabel, autoYes) {
116
- console.log(chalk.dim(`Project: ${rootDir}`));
117
- console.log(chalk.dim(`Model: ${modelLabel}`));
118
- if (autoYes) {
119
- console.log(chalk.dim('Auto-confirm: enabled'));
120
- }
121
- }
122
- function printSessionStartup(session) {
123
- console.log(chalk.dim(`Session: ${session.sessionName ? `${session.sessionName} ` : ''}${session.sessionId}`));
124
- }
125
111
  function printSessionExit(session) {
126
112
  console.log(chalk.dim(`\n${formatSessionExitNotice(session.sessionId)}\n`));
127
113
  }
128
114
  function modelLabel(serverModels, modelId) {
129
- return (serverModels.models.find((model) => model.id === modelId)?.label ??
130
- 'Unknown model');
131
- }
132
- function formatJobElapsedMs(ms) {
133
- const totalSeconds = Math.max(0, Math.floor(ms / 1000));
134
- if (totalSeconds < 60)
135
- return `${totalSeconds}s`;
136
- const minutes = Math.floor(totalSeconds / 60);
137
- if (minutes < 60)
138
- return `${minutes}m${String(totalSeconds % 60).padStart(2, '0')}s`;
139
- return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;
140
- }
141
- function makeConfirmCommand(session) {
142
- return async (command) => {
143
- console.log(chalk.bold(`\nCommand approval needed:\n${command}\n`));
144
- const answer = (await promptText('Approve? [y]es/[a]ll/[n]o', 'n')).toLowerCase();
145
- if (answer === 'a' || answer === 'all') {
146
- session.autoYes = true;
147
- return true;
148
- }
149
- return answer === 'y' || answer === 'yes';
150
- };
151
- }
152
- function makeConfirmPatch(session) {
153
- return async (filePath) => {
154
- const answer = (await promptText(`Apply patch to ${filePath}? [y]es/[a]ll/[n]o`, 'n')).toLowerCase();
155
- if (answer === 'a' || answer === 'all') {
156
- session.autoYes = true;
157
- return true;
158
- }
159
- return answer === 'y' || answer === 'yes';
160
- };
115
+ return (serverModels?.models.find((model) => model.id === modelId)?.label ??
116
+ `Model ${modelId}`);
161
117
  }
162
118
  async function saveSessionBoth({ session, serverSessionClient, }) {
163
119
  saveSessionState(session);
@@ -168,197 +124,18 @@ async function saveSessionBoth({ session, serverSessionClient, }) {
168
124
  console.error(chalk.yellow(`Warning: session save failed: ${error.message}`));
169
125
  }
170
126
  }
171
- async function promptForModelSelection(currentModelId, authConfig) {
172
- const serverModels = await models.fetchServerModels({ config: authConfig });
173
- console.log(chalk.bold('\nAvailable models'));
174
- for (const model of serverModels.models) {
175
- const marker = model.id === currentModelId ? '*' : ' ';
176
- console.log(`${marker} ${model.id} ${model.label}`);
177
- }
178
- console.log();
179
- const selected = (await promptText('Model id', '')).trim() || null;
180
- if (!selected) {
181
- return { selected: null, serverModels };
182
- }
183
- return {
184
- selected: models.validateServerModel(selected, serverModels),
185
- serverModels,
186
- };
187
- }
188
- async function promptForResumeSession(rootDir, serverModels) {
189
- const sessions = listSessionMetadata(rootDir);
190
- if (!sessions.length) {
191
- console.log(chalk.dim('No saved sessions for this repo.\n'));
192
- return null;
193
- }
194
- printSessionList(rootDir, sessions, serverModels);
195
- console.log();
196
- const identifier = (await promptText('Session id or name', '')).trim();
197
- if (!identifier) {
198
- return null;
199
- }
200
- return loadSessionSnapshot(rootDir, identifier);
201
- }
202
- async function runTurn({ authConfig, projectIndex, serverSessionClient, session, inputText, }) {
203
- const prompt = String(inputText ?? '').trim();
204
- if (!prompt)
205
- return;
206
- appendPromptHistory(prompt, session.env);
207
- const result = await chat.sendServerUserMessage({
208
- config: authConfig,
209
- projectIndex,
210
- session,
211
- input: prompt,
212
- });
213
- if (result.text) {
214
- console.log(`\n${chalk.green('TheGitAI>')}`);
215
- console.log(renderMarkdownForTerminal(result.text));
216
- console.log();
217
- }
218
- await saveSessionBoth({ session, serverSessionClient });
219
- }
220
- async function mainInteractive({ authConfig, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
221
- if (initialPrompt) {
222
- console.log(chalk.dim(`Prompt: "${initialPrompt}"`));
223
- await runTurn({
224
- authConfig,
225
- projectIndex,
226
- serverSessionClient,
227
- session,
228
- inputText: initialPrompt,
229
- });
230
- }
231
- while (true) {
232
- const inputText = await promptText('you');
233
- const trimmed = inputText.trim();
234
- if (!trimmed)
235
- continue;
236
- if (trimmed === '/exit')
237
- return;
238
- if (trimmed === '/help') {
239
- console.log(renderMarkdownForTerminal(formatInteractiveHelpText()));
240
- continue;
241
- }
242
- if (trimmed === '/usage') {
243
- console.log(await usageText());
244
- continue;
245
- }
246
- if (trimmed === '/clear') {
247
- clearConversation(session);
248
- await saveSessionBoth({ session, serverSessionClient });
249
- console.log(chalk.dim('Conversation cleared.\n'));
250
- continue;
251
- }
252
- if (trimmed === '/jobs' || trimmed.startsWith('/jobs ')) {
253
- const jobsArgs = trimmed.slice('/jobs'.length).trim();
254
- const killMatch = jobsArgs.match(/^kill\s+(\S+)$/);
255
- if (killMatch) {
256
- const jobId = killMatch[1];
257
- const killed = await killBackgroundJob(jobId);
258
- await collectBackgroundJobUiKillMutations({
259
- session,
260
- projectIndex,
261
- jobId,
262
- result: killed,
263
- });
264
- if (!killed.ok) {
265
- console.log(chalk.red(killed.error ?? 'Background job kill failed.'));
266
- }
267
- else if (killed.alreadyFinished) {
268
- console.log(chalk.dim(`${jobId} had already finished.`));
269
- }
270
- continue;
271
- }
272
- const outputMatch = jobsArgs.match(/^output\s+(\S+)$/);
273
- if (outputMatch) {
274
- const jobId = outputMatch[1];
275
- await collectBackgroundJobUiOutputMutations({
276
- session,
277
- projectIndex,
278
- jobId,
279
- });
280
- const job = listBackgroundJobs().find((candidate) => candidate.id === jobId);
281
- const buffered = getJobBufferedOutput(jobId);
282
- if (!job || !buffered) {
283
- console.log(chalk.red(`Unknown background job id: ${jobId}`));
284
- continue;
285
- }
286
- if (buffered.droppedChars > 0) {
287
- console.log(chalk.dim(`... (${buffered.droppedChars} chars of older output dropped) ...`));
288
- }
289
- console.log(buffered.output || chalk.dim('(no output captured)'));
290
- continue;
291
- }
292
- if (jobsArgs) {
293
- console.log(chalk.dim('Usage: /jobs — list · /jobs output <id> — full output · /jobs kill <id> — kill'));
294
- continue;
295
- }
296
- const jobsList = listBackgroundJobs();
297
- if (!jobsList.length) {
298
- console.log(chalk.dim('No background jobs in this session.'));
299
- continue;
300
- }
301
- for (const job of jobsList) {
302
- const elapsed = formatJobElapsedMs((job.endedAt ?? Date.now()) - job.startedAt);
303
- const stateText = job.status === 'running'
304
- ? `running · ${elapsed}`
305
- : job.status === 'killed'
306
- ? `killed · ran ${elapsed}`
307
- : job.status === 'error'
308
- ? 'failed to start'
309
- : `exited (code ${job.exitCode ?? 1}) · ran ${elapsed}`;
310
- console.log(`${job.id} · ${stateText}\n $ ${job.command}`);
311
- }
312
- continue;
313
- }
314
- if (trimmed === '/resume') {
315
- const snapshot = await promptForResumeSession(session.rootDir, serverModels);
316
- if (!snapshot) {
317
- console.log(chalk.dim('Resume cancelled.\n'));
318
- continue;
319
- }
320
- applySessionSnapshot(session, snapshot);
321
- setBackgroundJobSession(session.sessionId);
322
- setScratchSession(session.sessionId);
323
- await saveSessionBoth({ session, serverSessionClient });
324
- console.log(chalk.dim(`Resumed session${session.sessionName ? ` "${session.sessionName}"` : ''} (${session.sessionId})\n`));
325
- continue;
326
- }
327
- if (trimmed === '/model' || trimmed.startsWith('/model ')) {
328
- const inline = trimmed.slice('/model'.length).trim();
329
- let serverModels;
330
- let selected = null;
331
- if (inline) {
332
- serverModels = await models.fetchServerModels({ config: authConfig });
333
- selected = models.validateServerModel(inline, serverModels);
334
- }
335
- else {
336
- const response = await promptForModelSelection(session.modelId, authConfig);
337
- serverModels = response.serverModels;
338
- selected = response.selected;
339
- }
340
- if (!selected) {
341
- console.log(chalk.dim('Model selection cancelled.\n'));
342
- continue;
343
- }
344
- session.modelId = selected;
345
- models.updateSelectedModelCache({
346
- config: authConfig,
347
- selectedModelId: selected,
348
- serverModels,
349
- });
350
- await saveSessionBoth({ session, serverSessionClient });
351
- console.log(chalk.dim(`Switched to ${modelLabel(serverModels, selected)}.\n`));
352
- continue;
353
- }
354
- await runTurn({
355
- authConfig,
356
- projectIndex,
357
- serverSessionClient,
358
- session,
359
- inputText: trimmed,
360
- });
361
- }
127
+ function requireInteractiveTerminal() {
128
+ if (process.stdin.isTTY !== true) {
129
+ console.error('Error: stdin is not a terminal');
130
+ process.exitCode = 1;
131
+ return false;
132
+ }
133
+ if (process.stdout.isTTY !== true) {
134
+ console.error('Error: stdout is not a terminal');
135
+ process.exitCode = 1;
136
+ return false;
137
+ }
138
+ return true;
362
139
  }
363
140
  export async function main() {
364
141
  const { autoYes, help, version, usage, command, commandArgs, session: sessionIdentifier, listSessions, unknownOption, prompt, } = parseArgs(process.argv);
@@ -386,6 +163,22 @@ export async function main() {
386
163
  return;
387
164
  }
388
165
  const rootDir = process.cwd();
166
+ if (listSessions) {
167
+ const activeServerUrl = auth.readCliAuthConfig()?.serverUrl ?? DEFAULT_SERVER_URL;
168
+ printSessionList(rootDir, listSessionMetadata(rootDir), models.selectCacheForServer(models.readCachedServerModels(), activeServerUrl));
169
+ return;
170
+ }
171
+ const sourceSnapshot = sessionIdentifier
172
+ ? loadSessionSnapshot(rootDir, sessionIdentifier)
173
+ : null;
174
+ if (sessionIdentifier && !sourceSnapshot) {
175
+ console.error(`Error: No saved session named or identified by "${sessionIdentifier}" is available for this repo. Run \`ai --list-sessions\`.`);
176
+ process.exitCode = 1;
177
+ return;
178
+ }
179
+ if (!requireInteractiveTerminal()) {
180
+ return;
181
+ }
389
182
  const authConfig = requireCliAuthConfig();
390
183
  const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
391
184
  const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
@@ -428,13 +221,6 @@ export async function main() {
428
221
  if (offlineNotice) {
429
222
  console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
430
223
  }
431
- if (listSessions) {
432
- printSessionList(rootDir, listSessionMetadata(rootDir), serverModels);
433
- return;
434
- }
435
- const sourceSnapshot = sessionIdentifier
436
- ? loadSessionSnapshot(rootDir, sessionIdentifier)
437
- : null;
438
224
  const selectedModelId = models.selectServerModel({
439
225
  requestedModelId: sourceSnapshot?.modelId ?? null,
440
226
  cached: cachedModels,
@@ -450,13 +236,9 @@ export async function main() {
450
236
  autoYes,
451
237
  modelId: selectedModelId,
452
238
  });
453
- session.confirmCommand = makeConfirmCommand(session);
454
- session.confirmPatch = makeConfirmPatch(session);
455
239
  if (sourceSnapshot) {
456
240
  applySessionSnapshot(session, sourceSnapshot);
457
- await saveSessionBoth({ session, serverSessionClient });
458
241
  }
459
- setScratchSession(session.sessionId);
460
242
  const projectIndex = createIndex({
461
243
  rootDir,
462
244
  onStatus: (message) => {
@@ -471,43 +253,19 @@ export async function main() {
471
253
  },
472
254
  });
473
255
  const initialPrompt = prompt || undefined;
474
- if (shouldUseClientRatatuiShell()) {
475
- await runClientInteractive({
476
- appendPromptHistory: (value) => appendPromptHistory(value, session.env),
477
- authConfig,
478
- debugUi: whoami.debugUi,
479
- projectIndex,
480
- serverModels,
481
- serverSessionClient,
482
- session,
483
- initialPrompt,
484
- usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
485
- });
486
- await saveSessionBoth({ session, serverSessionClient });
487
- printSessionExit(session);
488
- return;
489
- }
490
- printStartupBanner(rootDir, modelLabel(serverModels, session.modelId), session.autoYes);
491
- printSessionStartup(session);
492
- setBackgroundJobSession(session.sessionId);
493
- try {
494
- await mainInteractive({
495
- authConfig,
496
- projectIndex,
497
- serverModels,
498
- serverSessionClient,
499
- session,
500
- usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
501
- initialPrompt,
502
- });
503
- await saveSessionBoth({ session, serverSessionClient });
504
- printSessionExit(session);
505
- }
506
- finally {
507
- killAllBackgroundJobs({ sessionId: session.sessionId, remove: true });
508
- setBackgroundJobSession(null);
509
- setScratchSession(null);
510
- }
256
+ await runClientInteractive({
257
+ appendPromptHistory: (value) => appendPromptHistory(value, session.env),
258
+ authConfig,
259
+ debugUi: whoami.debugUi,
260
+ projectIndex,
261
+ serverModels,
262
+ serverSessionClient,
263
+ session,
264
+ initialPrompt,
265
+ usageText: async () => formatUsageText(await auth.fetchWhoamiResponse({ config: authConfig })),
266
+ });
267
+ await saveSessionBoth({ session, serverSessionClient });
268
+ printSessionExit(session);
511
269
  }
512
270
  main().catch((error) => {
513
271
  console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
@@ -6,6 +6,7 @@ import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './ht
6
6
  import { collectClientEnvironment } from '../client-environment.js';
7
7
  import { collectProjectOrientation } from '../project-orientation.js';
8
8
  import { autoAttachImages } from '../core/image-path-extractor.js';
9
+ import { formatTurnFailureMarker } from '../turn-failure-marker.js';
9
10
  export class TurnCancelledError extends Error {
10
11
  name = 'TurnCancelledError';
11
12
  constructor(message = 'Turn cancelled.') {
@@ -102,7 +103,7 @@ function preserveFailedTurnInput(session, input, category) {
102
103
  session.history.push({ role: 'user', parts: [{ text }], kind: 'turnStart' });
103
104
  session.history.push({
104
105
  role: 'model',
105
- parts: [{ text: `Turn failed before completion: ${category}.` }],
106
+ parts: [{ text: formatTurnFailureMarker(category) }],
106
107
  });
107
108
  }
108
109
  function historyHasToolCall(session, callId) {
@@ -22,6 +22,7 @@ const HELP_MARKDOWN = [
22
22
  '',
23
23
  '- `ai` — start an interactive chat session in the current repo',
24
24
  '- `ai "<request>"` — start an interactive session with `<request>` as the first message',
25
+ '- Coding sessions require terminal stdin and stdout; piped prompts are not supported.',
25
26
  '',
26
27
  '## Auth',
27
28
  '',
@@ -35,8 +36,8 @@ const HELP_MARKDOWN = [
35
36
  '',
36
37
  '- `ai --list-sessions` — list saved sessions for this repo',
37
38
  '- `ai --session <id|name>` — resume a saved session by id or name',
38
- '- Sessions are stored locally and scoped to the current repo. The five',
39
- ' most recent sessions per repo are kept.',
39
+ '- Sessions are stored locally and can be listed or resumed in the same repo.',
40
+ ' Continuing one requires the TheGitAI account used for that session.',
40
41
  '',
41
42
  '## Options',
42
43
  '',
@@ -105,7 +106,10 @@ const HELP_MARKDOWN = [
105
106
  '- Auth or permission errors → run `ai whoami` to confirm the signed-in',
106
107
  ' account.',
107
108
  '- Usage or quota errors → run `ai --usage`.',
108
- '- Stuck on the wrong account → `ai logout`, then `ai login` again.',
109
+ '- Signed in with the wrong credentials → `ai logout`, then `ai login` with',
110
+ ' the account you intended to use.',
111
+ '- A local session was used with a different sign-in → sign in with the',
112
+ ' account you used for that session or start a new session.',
109
113
  '- For anything else, re-run the command and report the printed error',
110
114
  ' message — there is no client-side debug mode by design.',
111
115
  ].join('\n');
@@ -126,8 +130,15 @@ export function formatInteractiveHelpText() {
126
130
  return HELP_MARKDOWN;
127
131
  }
128
132
  export function formatCliHelpText({ color = false } = {}) {
129
- if (!color)
130
- return HELP_MARKDOWN;
133
+ if (!color) {
134
+ return HELP_MARKDOWN.split('\n')
135
+ .map((line) => line
136
+ .replace(/^#{1,6}\s+/, '')
137
+ .replace(/^-\s+/, ' ')
138
+ .replace(/`([^`]+)`/g, '$1')
139
+ .replace(/\*\*([^*]+)\*\*/g, '$1'))
140
+ .join('\n');
141
+ }
131
142
  return HELP_MARKDOWN.split('\n')
132
143
  .map((line) => {
133
144
  const heading = line.match(/^(#{1,6})\s+(.*)$/);
@@ -0,0 +1,11 @@
1
+ const TURN_FAILURE_MARKER_PATTERN = /^Turn failed before completion: ([a-z][a-z0-9_-]*)\.?$/i;
2
+ export function formatTurnFailureMarker(category) {
3
+ const normalized = category.trim().toLowerCase();
4
+ const safeCategory = /^[a-z][a-z0-9_-]*$/.test(normalized)
5
+ ? normalized
6
+ : 'unknown_error';
7
+ return `Turn failed before completion: ${safeCategory}.`;
8
+ }
9
+ export function isTurnFailureMarker(text) {
10
+ return TURN_FAILURE_MARKER_PATTERN.test(text.trim());
11
+ }
@@ -9,6 +9,7 @@ import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, ki
9
9
  import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
10
10
  import { setScratchSession } from '../scratch-dir.js';
11
11
  import { cancelActiveCommand } from '../executor.js';
12
+ import { isTurnFailureMarker } from '../turn-failure-marker.js';
12
13
  import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
13
14
  import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
14
15
  import { clearConversation, } from '../session.js';
@@ -780,7 +781,7 @@ function displayUserTextFromHistoryEntry(entry) {
780
781
  .slice(contentStart, contentEnd === -1 ? text.length : contentEnd)
781
782
  .trim();
782
783
  }
783
- function buildTranscriptFromSessionHistory(history) {
784
+ export function buildTranscriptFromSessionHistory(history) {
784
785
  const entries = [];
785
786
  const pendingCalls = new Map();
786
787
  for (const entry of history) {
@@ -826,6 +827,14 @@ function buildTranscriptFromSessionHistory(history) {
826
827
  }
827
828
  const text = textFromHistoryEntry(entry);
828
829
  if ((entry.role === 'model' || entry.role === 'assistant') && text) {
830
+ if (isTurnFailureMarker(text)) {
831
+ entries.push({
832
+ body: 'This request did not complete.',
833
+ kind: 'system',
834
+ title: 'Previous turn',
835
+ });
836
+ continue;
837
+ }
829
838
  entries.push({ body: text, kind: 'assistant', title: 'Response' });
830
839
  }
831
840
  }
@@ -1273,17 +1282,12 @@ async function saveSessionBoth({ serverSessionClient, session, }) {
1273
1282
  saveSessionState(session);
1274
1283
  await serverSessionClient.save(session);
1275
1284
  }
1276
- function shouldUseRatatuiShell() {
1277
- if (process.env.THEGITAI_PLAIN === '1')
1278
- return false;
1279
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
1280
- }
1281
- export function shouldUseClientRatatuiShell() {
1282
- return shouldUseRatatuiShell();
1283
- }
1284
1285
  export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
1285
- if (!shouldUseRatatuiShell()) {
1286
- throw new Error('Client TUI requires an interactive terminal.');
1286
+ if (process.stdin.isTTY !== true) {
1287
+ throw new Error('stdin is not a terminal');
1288
+ }
1289
+ if (process.stdout.isTTY !== true) {
1290
+ throw new Error('stdout is not a terminal');
1287
1291
  }
1288
1292
  await withTuiMode(async () => {
1289
1293
  setBackgroundJobSession(session.sessionId);
@@ -1968,7 +1972,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1968
1972
  syncBackgroundJobsState();
1969
1973
  setTodoSession(session.sessionId);
1970
1974
  syncTodosState();
1971
- await saveActiveSession();
1972
1975
  syncShellStateFromSession();
1973
1976
  store.update((next) => ({
1974
1977
  ...next,
@@ -17,7 +17,7 @@ const OVERLAY_PANEL_MAX_WIDTH = 86;
17
17
  const OVERLAY_PANEL_MARGIN_LINES = 2;
18
18
  const OVERLAY_BORDER_COLOR = 'yellow';
19
19
  const OVERLAY_WARNING_COLOR = 'ansi256(208)';
20
- const MODEL_PICKER_PANEL_MAX_WIDTH = 120;
20
+ const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
21
21
  const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
22
22
  const MODEL_PICKER_BORDER_COLOR = 'cyan';
23
23
  const MODEL_PICKER_ACCENT_COLOR = 'cyan';
@@ -689,10 +689,15 @@ function modelPickerHeaderLine(innerWidth) {
689
689
  }
690
690
  return line(modelPickerCell('#', MODEL_PICKER_NUMBER_WIDTH, heading), modelPickerCell('Model', MODEL_PICKER_MODEL_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Cost', MODEL_PICKER_COST_WIDTH, heading), span(MODEL_PICKER_SEPARATOR, { color: 'gray' }), modelPickerCell('Notes', modelPickerNotesWidth(innerWidth), heading));
691
691
  }
692
- function buildModelPickerPanel(options, selectedIndex, width) {
692
+ function buildModelPickerPanel(options, selectedIndex, width, availableHeight) {
693
693
  const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
694
694
  const innerWidth = Math.max(1, panelWidth - 4);
695
- const margin = Array.from({ length: MODEL_PICKER_PANEL_MARGIN_LINES }, () => plainLine(''));
695
+ const compactBodyLineCount = options.length + 6;
696
+ const spacerLineCount = Math.max(0, options.length - 1);
697
+ const useRowSpacing = compactBodyLineCount + spacerLineCount <= availableHeight;
698
+ const bodyLineCount = compactBodyLineCount + (useRowSpacing ? spacerLineCount : 0);
699
+ const marginLineCount = Math.max(0, Math.min(MODEL_PICKER_PANEL_MARGIN_LINES, Math.floor((availableHeight - bodyLineCount) / 2)));
700
+ const margin = Array.from({ length: marginLineCount }, () => plainLine(''));
696
701
  const body = [
697
702
  modelPickerTopBorder(panelWidth),
698
703
  modelPickerPanelSideLine(modelPickerHeaderLine(innerWidth), innerWidth),
@@ -700,6 +705,9 @@ function buildModelPickerPanel(options, selectedIndex, width) {
700
705
  ];
701
706
  options.forEach((option, index) => {
702
707
  body.push(...modelPickerItemLines(option, index === selectedIndex, innerWidth));
708
+ if (useRowSpacing && index < options.length - 1) {
709
+ body.push(modelPickerPanelSideLine(plainLine(''), innerWidth));
710
+ }
703
711
  });
704
712
  const fullHint = '↑/↓ navigate • Enter select • Esc cancel';
705
713
  const compactHint = '↑/↓ • enter • esc';
@@ -774,7 +782,7 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
774
782
  body.push(modelPickerPanelSideLine(plainLine(''), innerWidth), modelPickerPanelSideLine(line(span('─'.repeat(innerWidth), { color: MODEL_PICKER_BORDER_COLOR })), innerWidth), modelPickerPanelSideLine(plainLine('↑/↓ choose • Tab or Enter accept • Esc cancel', { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
775
783
  return [...margin, ...body, ...margin];
776
784
  }
777
- function buildOverlayLines(state, width, nowMs) {
785
+ function buildOverlayLines(state, width, height, nowMs) {
778
786
  const lines = [];
779
787
  const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
780
788
  const innerWidth = Math.max(1, panelWidth - 4);
@@ -847,7 +855,7 @@ function buildOverlayLines(state, width, nowMs) {
847
855
  }
848
856
  if (state.modelPickerOpen) {
849
857
  const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
850
- lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width));
858
+ lines.push(...buildModelPickerPanel(options, state.modelPickerIndex, width, height));
851
859
  }
852
860
  if (state.jobsPickerOpen) {
853
861
  lines.push(...buildJobsPickerLines(state, width, nowMs));
@@ -931,7 +939,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
931
939
  lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
932
940
  });
933
941
  }
934
- const overlayLines = buildOverlayLines(state, contentWidth, nowMs);
942
+ const overlayLines = buildOverlayLines(state, contentWidth, rows, nowMs);
935
943
  if (overlayLines.length > 0) {
936
944
  sections.push({ kind: 'overlay', lines: overlayLines });
937
945
  }
package/package.json CHANGED
@@ -1,9 +1,21 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.3",
4
- "description": "TheGitAI CLI client (source-visible, proprietary)",
3
+ "version": "1.0.0-preview.4",
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.3",
29
- "@thegitai/tui-darwin-x64": "1.0.0-preview.3",
30
- "@thegitai/tui-linux-x64": "1.0.0-preview.3",
31
- "@thegitai/tui-win32-x64": "1.0.0-preview.3",
40
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.4",
41
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.4",
42
+ "@thegitai/tui-linux-x64": "1.0.0-preview.4",
43
+ "@thegitai/tui-win32-x64": "1.0.0-preview.4",
32
44
  "@vscode/ripgrep": "1.18.0"
33
45
  },
34
46
  "publishConfig": {