@thegitai/cli 1.0.0-preview.1 → 1.0.0-preview.10

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 (36) hide show
  1. package/README.md +16 -4
  2. package/dist/bin/ai.js +57 -287
  3. package/dist/src/api/auth.js +2 -2
  4. package/dist/src/api/browser-login.js +72 -3
  5. package/dist/src/api/chat.js +125 -24
  6. package/dist/src/api/http.js +16 -3
  7. package/dist/src/api/models.js +9 -4
  8. package/dist/src/help-text.js +19 -7
  9. package/dist/src/patcher.js +96 -9
  10. package/dist/src/project-index.js +13 -1
  11. package/dist/src/project-orientation.js +99 -0
  12. package/dist/src/scratch-dir.js +51 -33
  13. package/dist/src/session-store.js +52 -20
  14. package/dist/src/session.js +8 -0
  15. package/dist/src/tool-executor.js +38 -6
  16. package/dist/src/tools/delete-file.js +22 -4
  17. package/dist/src/tools/patch-file.js +30 -5
  18. package/dist/src/tools/read-file.js +3 -1
  19. package/dist/src/tools/replace-document-text.js +7 -1
  20. package/dist/src/tools/run-command.js +37 -19
  21. package/dist/src/tools/run-node-script.js +24 -4
  22. package/dist/src/tools/str-replace.js +30 -5
  23. package/dist/src/tools/write-file.js +25 -5
  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 +188 -49
  27. package/dist/src/ui/tui/bridge.js +3 -0
  28. package/dist/src/ui/tui/build-frame.js +179 -82
  29. package/dist/src/ui/tui/markdown-render.js +72 -73
  30. package/dist/src/ui/tui/shell-input.js +42 -13
  31. package/dist/src/ui/tui/terminal-title.js +3 -0
  32. package/dist/src/ui/tui/terminal-writes.js +48 -0
  33. package/dist/src/ui/tui/text.js +158 -4
  34. package/dist/src/utils.js +9 -0
  35. package/package.json +18 -6
  36. package/dist/src/markdown-renderer.js +0 -112
@@ -1,17 +1,22 @@
1
1
  import { createRatatuiBridge } from './tui/bridge.js';
2
2
  import { buildTuiFrame, formatJobElapsed, formatTodoProgress, renderTranscriptEntryLines, } from './tui/build-frame.js';
3
3
  import { createTerminalTitleController } from './tui/terminal-title.js';
4
+ import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
4
5
  export { getSlashCommandSuggestions } from './tui/build-frame.js';
5
6
  import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
6
7
  import { chat, models } from '../api/index.js';
7
8
  import { isTurnCancelledError } from '../api/chat.js';
8
9
  import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
9
10
  import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
11
+ import { setScratchSession } from '../scratch-dir.js';
10
12
  import { cancelActiveCommand } from '../executor.js';
13
+ import { isTurnFailureMarker } from '../turn-failure-marker.js';
14
+ import { clearCliAuthConfig } from '../api/auth.js';
15
+ import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
11
16
  import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
12
17
  import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
13
- import { clearConversation, } from '../session.js';
14
- import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
18
+ import { startNewConversation, } from '../session.js';
19
+ import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, sessionHasUserMessage, } from '../session-store.js';
15
20
  import { truncate } from '../utils.js';
16
21
  import { writeClipboardText } from '../core/clipboard.js';
17
22
  import { openUrl } from '../core/open-url.js';
@@ -111,8 +116,8 @@ export const SLASH_COMMANDS = [
111
116
  description: 'Background jobs: pick to view output or kill',
112
117
  },
113
118
  {
114
- command: '/clear',
115
- description: 'Clear conversation history',
119
+ command: '/new',
120
+ description: 'start a new conversation; this session remains saved',
116
121
  },
117
122
  {
118
123
  command: '/exit',
@@ -558,6 +563,31 @@ function buildFileChangeEntry(event) {
558
563
  title: skipped ? `${verb} skipped: ${filePath}` : `${verb} failed: ${filePath}`,
559
564
  };
560
565
  }
566
+ if (result?.scratch === true) {
567
+ const scratchName = truncate(filePath.split(/[\\/]/).filter(Boolean).at(-1) ?? filePath, 72);
568
+ if (call.name === 'delete_file') {
569
+ return {
570
+ body: '',
571
+ filePath,
572
+ kind: 'tool',
573
+ title: result?.deleted === true
574
+ ? `Removed scratch file: ${scratchName}`
575
+ : `Scratch delete skipped: ${scratchName}`,
576
+ };
577
+ }
578
+ const content = typeof call.args?.content === 'string' ? call.args.content : '';
579
+ const lineSummary = call.name === 'write_file' && content
580
+ ? ` (${splitDiffLines(content).length} lines)`
581
+ : '';
582
+ return {
583
+ body: '',
584
+ filePath,
585
+ kind: 'tool',
586
+ title: call.name === 'write_file'
587
+ ? `Wrote scratch file: ${scratchName}${lineSummary}`
588
+ : `Edited scratch file: ${scratchName}`,
589
+ };
590
+ }
561
591
  if (call.name === 'undo_edit') {
562
592
  const dryRun = result?.dryRun === true ||
563
593
  result?.dry_run === true ||
@@ -754,7 +784,7 @@ function displayUserTextFromHistoryEntry(entry) {
754
784
  .slice(contentStart, contentEnd === -1 ? text.length : contentEnd)
755
785
  .trim();
756
786
  }
757
- function buildTranscriptFromSessionHistory(history) {
787
+ export function buildTranscriptFromSessionHistory(history) {
758
788
  const entries = [];
759
789
  const pendingCalls = new Map();
760
790
  for (const entry of history) {
@@ -800,6 +830,14 @@ function buildTranscriptFromSessionHistory(history) {
800
830
  }
801
831
  const text = textFromHistoryEntry(entry);
802
832
  if ((entry.role === 'model' || entry.role === 'assistant') && text) {
833
+ if (isTurnFailureMarker(text)) {
834
+ entries.push({
835
+ body: 'This request did not complete.',
836
+ kind: 'system',
837
+ title: 'Previous turn',
838
+ });
839
+ continue;
840
+ }
803
841
  entries.push({ body: text, kind: 'assistant', title: 'Response' });
804
842
  }
805
843
  }
@@ -925,6 +963,7 @@ function createInitialShellState(session, serverModels, debugUi) {
925
963
  activeTurnInput: '',
926
964
  activeTurnInputPreformatted: false,
927
965
  agentMode: session.agentMode,
966
+ analyzingImages: 0,
928
967
  approvalCursor: getDefaultApprovalCursor(),
929
968
  approvalPrompt: null,
930
969
  autoYes: session.autoYes,
@@ -1212,7 +1251,11 @@ export function buildModelPickerOptions(currentModelId, serverModels) {
1212
1251
  return serverModels.map((model) => ({
1213
1252
  id: model.id,
1214
1253
  label: model.label,
1215
- meta: model.id === currentModelId ? 'current' : '',
1254
+ publicId: model.id,
1255
+ costRating: model.costRating,
1256
+ current: model.id === currentModelId,
1257
+ disabled: false,
1258
+ note: model.description,
1216
1259
  }));
1217
1260
  }
1218
1261
  function getDefaultModelPickerIndex(currentModelId, serverModels) {
@@ -1240,23 +1283,22 @@ function appendCommandLog(state, text) {
1240
1283
  };
1241
1284
  }
1242
1285
  async function saveSessionBoth({ serverSessionClient, session, }) {
1286
+ if (!sessionHasUserMessage(session))
1287
+ return;
1243
1288
  saveSessionState(session);
1244
1289
  await serverSessionClient.save(session);
1245
1290
  }
1246
- function shouldUseRatatuiShell() {
1247
- if (process.env.THEGITAI_PLAIN === '1')
1248
- return false;
1249
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
1250
- }
1251
- export function shouldUseClientRatatuiShell() {
1252
- return shouldUseRatatuiShell();
1253
- }
1254
1291
  export async function runClientInteractive({ appendPromptHistory, authConfig, debugUi, projectIndex, serverModels, serverSessionClient, session, usageText, initialPrompt, }) {
1255
- if (!shouldUseRatatuiShell()) {
1256
- throw new Error('Client TUI requires an interactive terminal.');
1292
+ if (process.stdin.isTTY !== true) {
1293
+ throw new Error('stdin is not a terminal');
1257
1294
  }
1295
+ if (process.stdout.isTTY !== true) {
1296
+ throw new Error('stdout is not a terminal');
1297
+ }
1298
+ let fatalError = null;
1258
1299
  await withTuiMode(async () => {
1259
1300
  setBackgroundJobSession(session.sessionId);
1301
+ setScratchSession(session.sessionId);
1260
1302
  setTodoSession(session.sessionId);
1261
1303
  const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
1262
1304
  store.replaceTranscript(createSessionTranscript(session));
@@ -1268,6 +1310,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1268
1310
  let cleanupSudoPasswordPrompt = null;
1269
1311
  let sudoPasswordBuffer = '';
1270
1312
  const bridge = createRatatuiBridge();
1313
+ captureTerminalWrites();
1271
1314
  const { handleShellKeyEvent } = await import('./tui/shell-input.js');
1272
1315
  let terminalCols = 80;
1273
1316
  let terminalRows = 24;
@@ -1280,6 +1323,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1280
1323
  let latestUsageSummary = null;
1281
1324
  let pendingTurnEntries = [];
1282
1325
  let activeTurnAbort = null;
1326
+ let newConversationInFlight = false;
1283
1327
  let todosTouchedThisTurn = false;
1284
1328
  const syncTodosState = () => {
1285
1329
  store.update((current) => ({ ...current, todos: listTodos() }));
@@ -1312,7 +1356,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1312
1356
  store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
1313
1357
  }, 2500);
1314
1358
  };
1315
- const terminalTitle = createTerminalTitleController();
1359
+ const terminalTitle = createTerminalTitleController({
1360
+ write: (title) => bridge.setTitle(title),
1361
+ });
1316
1362
  const syncTerminalTitle = () => {
1317
1363
  const state = store.getState();
1318
1364
  terminalTitle.sync({
@@ -1479,7 +1525,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1479
1525
  }));
1480
1526
  };
1481
1527
  const cancelActiveTurn = () => {
1482
- if (!store.getState().busy)
1528
+ if (!store.getState().busy || newConversationInFlight)
1483
1529
  return;
1484
1530
  disarmExitConfirm();
1485
1531
  dismissPendingApproval();
@@ -1523,14 +1569,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1523
1569
  scheduleLiveFrameRemount();
1524
1570
  void remountTui();
1525
1571
  };
1526
- const saveActiveSession = async () => {
1527
- try {
1528
- await saveSessionBoth({ serverSessionClient, session });
1529
- }
1530
- catch (error) {
1531
- appendError(`Session save failed: ${error.message}`);
1532
- }
1533
- };
1534
1572
  const syncShellStateFromSession = () => {
1535
1573
  store.update((current) => ({
1536
1574
  ...current,
@@ -1618,9 +1656,45 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1618
1656
  status: 'Exiting...',
1619
1657
  }));
1620
1658
  void bridge.close().then(() => {
1659
+ releaseTerminalWrites();
1621
1660
  resolveDone?.();
1622
1661
  });
1623
1662
  };
1663
+ const exitForAuthenticationError = (error) => {
1664
+ if (!isAuthenticationError(error))
1665
+ return false;
1666
+ clearCliAuthConfig(session.env);
1667
+ if (sessionHasUserMessage(session)) {
1668
+ saveSessionState(session);
1669
+ }
1670
+ fatalError = new Error(authenticationErrorMessage(error));
1671
+ requestExit();
1672
+ return true;
1673
+ };
1674
+ const saveActiveSession = async () => {
1675
+ try {
1676
+ await saveSessionBoth({ serverSessionClient, session });
1677
+ return true;
1678
+ }
1679
+ catch (error) {
1680
+ if (exitForAuthenticationError(error))
1681
+ return false;
1682
+ appendError(`Session save failed: ${error.message}`);
1683
+ return true;
1684
+ }
1685
+ };
1686
+ const saveActiveSessionOrAbort = async () => {
1687
+ try {
1688
+ await saveSessionBoth({ serverSessionClient, session });
1689
+ return true;
1690
+ }
1691
+ catch (error) {
1692
+ if (exitForAuthenticationError(error))
1693
+ return false;
1694
+ appendError(`Session save failed: ${error.message}`);
1695
+ return false;
1696
+ }
1697
+ };
1624
1698
  const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
1625
1699
  if (exiting || signal?.aborted) {
1626
1700
  resolve(null);
@@ -1757,7 +1831,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1757
1831
  selectedModelId: selected,
1758
1832
  serverModels: fresh,
1759
1833
  });
1760
- await saveActiveSession();
1834
+ if (!(await saveActiveSession()))
1835
+ return;
1761
1836
  syncShellStateFromSession();
1762
1837
  appendStaticEntry({
1763
1838
  body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
@@ -1876,6 +1951,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1876
1951
  await switchToModel(selected.id);
1877
1952
  }
1878
1953
  catch (error) {
1954
+ if (exitForAuthenticationError(error))
1955
+ return;
1879
1956
  appendError(error.message);
1880
1957
  }
1881
1958
  };
@@ -1933,10 +2010,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1933
2010
  const snapshot = await loadInteractiveSession(selected.id);
1934
2011
  applySessionSnapshot(session, snapshot);
1935
2012
  setBackgroundJobSession(session.sessionId);
2013
+ setScratchSession(session.sessionId);
1936
2014
  syncBackgroundJobsState();
1937
2015
  setTodoSession(session.sessionId);
1938
2016
  syncTodosState();
1939
- await saveActiveSession();
1940
2017
  syncShellStateFromSession();
1941
2018
  store.update((next) => ({
1942
2019
  ...next,
@@ -1959,6 +2036,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1959
2036
  await remountTui();
1960
2037
  }
1961
2038
  catch (error) {
2039
+ if (exitForAuthenticationError(error))
2040
+ return;
1962
2041
  store.update((next) => ({
1963
2042
  ...next,
1964
2043
  busy: false,
@@ -2111,14 +2190,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2111
2190
  });
2112
2191
  }
2113
2192
  catch (error) {
2193
+ if (exitForAuthenticationError(error))
2194
+ return;
2114
2195
  appendError(error.message);
2115
2196
  }
2116
2197
  finally {
2117
- store.update((current) => ({
2118
- ...current,
2119
- busy: false,
2120
- status: 'Ready',
2121
- }));
2198
+ if (!exiting) {
2199
+ store.update((current) => ({
2200
+ ...current,
2201
+ busy: false,
2202
+ status: 'Ready',
2203
+ }));
2204
+ }
2122
2205
  }
2123
2206
  return;
2124
2207
  }
@@ -2132,14 +2215,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2132
2215
  await openResumePicker();
2133
2216
  }
2134
2217
  catch (error) {
2218
+ if (exitForAuthenticationError(error))
2219
+ return;
2135
2220
  appendError(error.message);
2136
2221
  }
2137
2222
  finally {
2138
- store.update((current) => ({
2139
- ...current,
2140
- busy: false,
2141
- status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
2142
- }));
2223
+ if (!exiting) {
2224
+ store.update((current) => ({
2225
+ ...current,
2226
+ busy: false,
2227
+ status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
2228
+ }));
2229
+ }
2143
2230
  }
2144
2231
  return;
2145
2232
  }
@@ -2159,33 +2246,68 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2159
2246
  }
2160
2247
  }
2161
2248
  catch (error) {
2249
+ if (exitForAuthenticationError(error))
2250
+ return;
2162
2251
  appendError(error.message);
2163
2252
  }
2164
2253
  finally {
2254
+ if (!exiting) {
2255
+ store.update((current) => ({
2256
+ ...current,
2257
+ busy: false,
2258
+ status: current.modelPickerOpen ? 'Select a model' : 'Ready',
2259
+ }));
2260
+ }
2261
+ }
2262
+ return;
2263
+ }
2264
+ if (input === '/new') {
2265
+ newConversationInFlight = true;
2266
+ store.update((current) => ({
2267
+ ...current,
2268
+ busy: true,
2269
+ busySince: Date.now(),
2270
+ imageAttachments: [],
2271
+ queuedMessage: null,
2272
+ status: 'Starting a new conversation...',
2273
+ }));
2274
+ if (!(await saveActiveSessionOrAbort())) {
2275
+ newConversationInFlight = false;
2165
2276
  store.update((current) => ({
2166
2277
  ...current,
2167
2278
  busy: false,
2168
- status: current.modelPickerOpen ? 'Select a model' : 'Ready',
2279
+ busySince: null,
2280
+ status: 'Ready',
2169
2281
  }));
2282
+ await flushQueuedMessage();
2283
+ return;
2170
2284
  }
2171
- return;
2172
- }
2173
- if (input === '/clear') {
2174
- clearConversation(session);
2285
+ startNewConversation(session);
2286
+ setBackgroundJobSession(session.sessionId);
2287
+ setScratchSession(session.sessionId);
2288
+ setTodoSession(session.sessionId);
2175
2289
  clearTodos();
2290
+ syncBackgroundJobsState();
2176
2291
  syncTodosState();
2177
2292
  latestUsageSummary = null;
2178
- await saveActiveSession();
2179
2293
  store.replaceTranscript([
2180
2294
  {
2181
- body: 'Conversation cleared.',
2295
+ body: 'Started a new conversation. The previous session remains saved.',
2182
2296
  kind: 'system',
2183
- title: 'System',
2297
+ title: 'Session',
2184
2298
  },
2185
2299
  ]);
2300
+ await saveActiveSession();
2186
2301
  syncShellStateFromSession();
2187
- store.update((current) => ({ ...current, queuedMessage: null }));
2302
+ newConversationInFlight = false;
2303
+ store.update((current) => ({
2304
+ ...current,
2305
+ busy: false,
2306
+ busySince: null,
2307
+ status: 'Ready',
2308
+ }));
2188
2309
  await remountTui();
2310
+ await flushQueuedMessage();
2189
2311
  return;
2190
2312
  }
2191
2313
  latestUsageSummary = null;
@@ -2196,6 +2318,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2196
2318
  activeTurnAbort = turnAbort;
2197
2319
  lastTurnStartedAt = turnStartedAt;
2198
2320
  todosTouchedThisTurn = false;
2321
+ clearTodos();
2322
+ syncTodosState();
2199
2323
  const userEntry = {
2200
2324
  body: input,
2201
2325
  kind: 'user',
@@ -2208,6 +2332,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2208
2332
  ...current,
2209
2333
  activeTurnInput: input,
2210
2334
  activeTurnInputPreformatted: preformatted,
2335
+ analyzingImages: 0,
2211
2336
  busy: true,
2212
2337
  busySince: turnStartedAt,
2213
2338
  clockNow: turnStartedAt,
@@ -2232,7 +2357,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2232
2357
  return;
2233
2358
  latestUsageSummary = result.usageSummary ?? null;
2234
2359
  const turnEntries = takePendingTurnEntries();
2235
- await saveActiveSession();
2360
+ if (!(await saveActiveSession()))
2361
+ return;
2236
2362
  syncShellStateFromSession();
2237
2363
  store.update((current) => ({
2238
2364
  ...current,
@@ -2283,6 +2409,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2283
2409
  return;
2284
2410
  }
2285
2411
  const cancelled = isTurnCancelledError(error);
2412
+ if (exitForAuthenticationError(error))
2413
+ return;
2286
2414
  store.update((current) => ({
2287
2415
  ...current,
2288
2416
  activeTurnInput: '',
@@ -2307,7 +2435,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2307
2435
  title: 'System',
2308
2436
  },
2309
2437
  ]);
2310
- await saveActiveSession();
2438
+ if (!(await saveActiveSession()))
2439
+ return;
2311
2440
  }
2312
2441
  else {
2313
2442
  appendStaticEntries(turnEntries);
@@ -2327,6 +2456,12 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2327
2456
  }
2328
2457
  }
2329
2458
  };
2459
+ session.onImageAnalysis = (activeImageCount) => {
2460
+ store.update((current) => ({
2461
+ ...current,
2462
+ analyzingImages: Math.max(0, activeImageCount),
2463
+ }));
2464
+ };
2330
2465
  session.onStatus = (message) => {
2331
2466
  const panel = thinkingPanelFromStatus(message);
2332
2467
  store.update((current) => ({
@@ -2531,10 +2666,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2531
2666
  terminalTitle.dispose();
2532
2667
  killAllBackgroundJobs();
2533
2668
  setBackgroundJobSession(null);
2669
+ setScratchSession(null);
2534
2670
  setTodoSession(null);
2535
2671
  setBackgroundJobUpdateHook(null);
2536
2672
  await bridge.close();
2673
+ releaseTerminalWrites();
2537
2674
  setCommandOutputHook(null);
2538
2675
  }
2539
2676
  });
2677
+ if (fatalError)
2678
+ throw fatalError;
2540
2679
  }
@@ -155,6 +155,9 @@ export function createRatatuiBridge() {
155
155
  clear() {
156
156
  writeParent({ op: 'clear' });
157
157
  },
158
+ setTitle(title) {
159
+ writeParent({ op: 'title', text: title });
160
+ },
158
161
  async close() {
159
162
  if (closed)
160
163
  return;