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

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 +147 -30
  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 +197 -51
  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,
@@ -1034,6 +1073,13 @@ function splitThinkingLines(text) {
1034
1073
  .map((part) => part.trim())
1035
1074
  .filter(Boolean));
1036
1075
  }
1076
+ const LIVE_STATUS_PANEL_MAX_CHARS = 72;
1077
+ function liveStatusPanelTitle(status) {
1078
+ const text = String(status ?? '').trim();
1079
+ if (!text || text.includes('\n'))
1080
+ return '';
1081
+ return text.length <= LIVE_STATUS_PANEL_MAX_CHARS ? text : '';
1082
+ }
1037
1083
  function thinkingPanelFromStatus(status) {
1038
1084
  const rawText = thinkingNoteFromStatus(status);
1039
1085
  if (!rawText)
@@ -1212,7 +1258,11 @@ export function buildModelPickerOptions(currentModelId, serverModels) {
1212
1258
  return serverModels.map((model) => ({
1213
1259
  id: model.id,
1214
1260
  label: model.label,
1215
- meta: model.id === currentModelId ? 'current' : '',
1261
+ publicId: model.id,
1262
+ costRating: model.costRating,
1263
+ current: model.id === currentModelId,
1264
+ disabled: false,
1265
+ note: model.description,
1216
1266
  }));
1217
1267
  }
1218
1268
  function getDefaultModelPickerIndex(currentModelId, serverModels) {
@@ -1240,23 +1290,22 @@ function appendCommandLog(state, text) {
1240
1290
  };
1241
1291
  }
1242
1292
  async function saveSessionBoth({ serverSessionClient, session, }) {
1293
+ if (!sessionHasUserMessage(session))
1294
+ return;
1243
1295
  saveSessionState(session);
1244
1296
  await serverSessionClient.save(session);
1245
1297
  }
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
1298
  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.');
1299
+ if (process.stdin.isTTY !== true) {
1300
+ throw new Error('stdin is not a terminal');
1301
+ }
1302
+ if (process.stdout.isTTY !== true) {
1303
+ throw new Error('stdout is not a terminal');
1257
1304
  }
1305
+ let fatalError = null;
1258
1306
  await withTuiMode(async () => {
1259
1307
  setBackgroundJobSession(session.sessionId);
1308
+ setScratchSession(session.sessionId);
1260
1309
  setTodoSession(session.sessionId);
1261
1310
  const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
1262
1311
  store.replaceTranscript(createSessionTranscript(session));
@@ -1268,6 +1317,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1268
1317
  let cleanupSudoPasswordPrompt = null;
1269
1318
  let sudoPasswordBuffer = '';
1270
1319
  const bridge = createRatatuiBridge();
1320
+ captureTerminalWrites();
1271
1321
  const { handleShellKeyEvent } = await import('./tui/shell-input.js');
1272
1322
  let terminalCols = 80;
1273
1323
  let terminalRows = 24;
@@ -1280,6 +1330,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1280
1330
  let latestUsageSummary = null;
1281
1331
  let pendingTurnEntries = [];
1282
1332
  let activeTurnAbort = null;
1333
+ let newConversationInFlight = false;
1283
1334
  let todosTouchedThisTurn = false;
1284
1335
  const syncTodosState = () => {
1285
1336
  store.update((current) => ({ ...current, todos: listTodos() }));
@@ -1312,7 +1363,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1312
1363
  store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
1313
1364
  }, 2500);
1314
1365
  };
1315
- const terminalTitle = createTerminalTitleController();
1366
+ const terminalTitle = createTerminalTitleController({
1367
+ write: (title) => bridge.setTitle(title),
1368
+ });
1316
1369
  const syncTerminalTitle = () => {
1317
1370
  const state = store.getState();
1318
1371
  terminalTitle.sync({
@@ -1479,7 +1532,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1479
1532
  }));
1480
1533
  };
1481
1534
  const cancelActiveTurn = () => {
1482
- if (!store.getState().busy)
1535
+ if (!store.getState().busy || newConversationInFlight)
1483
1536
  return;
1484
1537
  disarmExitConfirm();
1485
1538
  dismissPendingApproval();
@@ -1523,14 +1576,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1523
1576
  scheduleLiveFrameRemount();
1524
1577
  void remountTui();
1525
1578
  };
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
1579
  const syncShellStateFromSession = () => {
1535
1580
  store.update((current) => ({
1536
1581
  ...current,
@@ -1618,9 +1663,45 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1618
1663
  status: 'Exiting...',
1619
1664
  }));
1620
1665
  void bridge.close().then(() => {
1666
+ releaseTerminalWrites();
1621
1667
  resolveDone?.();
1622
1668
  });
1623
1669
  };
1670
+ const exitForAuthenticationError = (error) => {
1671
+ if (!isAuthenticationError(error))
1672
+ return false;
1673
+ clearCliAuthConfig(session.env);
1674
+ if (sessionHasUserMessage(session)) {
1675
+ saveSessionState(session);
1676
+ }
1677
+ fatalError = new Error(authenticationErrorMessage(error));
1678
+ requestExit();
1679
+ return true;
1680
+ };
1681
+ const saveActiveSession = async () => {
1682
+ try {
1683
+ await saveSessionBoth({ serverSessionClient, session });
1684
+ return true;
1685
+ }
1686
+ catch (error) {
1687
+ if (exitForAuthenticationError(error))
1688
+ return false;
1689
+ appendError(`Session save failed: ${error.message}`);
1690
+ return true;
1691
+ }
1692
+ };
1693
+ const saveActiveSessionOrAbort = async () => {
1694
+ try {
1695
+ await saveSessionBoth({ serverSessionClient, session });
1696
+ return true;
1697
+ }
1698
+ catch (error) {
1699
+ if (exitForAuthenticationError(error))
1700
+ return false;
1701
+ appendError(`Session save failed: ${error.message}`);
1702
+ return false;
1703
+ }
1704
+ };
1624
1705
  const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
1625
1706
  if (exiting || signal?.aborted) {
1626
1707
  resolve(null);
@@ -1757,7 +1838,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1757
1838
  selectedModelId: selected,
1758
1839
  serverModels: fresh,
1759
1840
  });
1760
- await saveActiveSession();
1841
+ if (!(await saveActiveSession()))
1842
+ return;
1761
1843
  syncShellStateFromSession();
1762
1844
  appendStaticEntry({
1763
1845
  body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
@@ -1876,6 +1958,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1876
1958
  await switchToModel(selected.id);
1877
1959
  }
1878
1960
  catch (error) {
1961
+ if (exitForAuthenticationError(error))
1962
+ return;
1879
1963
  appendError(error.message);
1880
1964
  }
1881
1965
  };
@@ -1933,10 +2017,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1933
2017
  const snapshot = await loadInteractiveSession(selected.id);
1934
2018
  applySessionSnapshot(session, snapshot);
1935
2019
  setBackgroundJobSession(session.sessionId);
2020
+ setScratchSession(session.sessionId);
1936
2021
  syncBackgroundJobsState();
1937
2022
  setTodoSession(session.sessionId);
1938
2023
  syncTodosState();
1939
- await saveActiveSession();
1940
2024
  syncShellStateFromSession();
1941
2025
  store.update((next) => ({
1942
2026
  ...next,
@@ -1959,6 +2043,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1959
2043
  await remountTui();
1960
2044
  }
1961
2045
  catch (error) {
2046
+ if (exitForAuthenticationError(error))
2047
+ return;
1962
2048
  store.update((next) => ({
1963
2049
  ...next,
1964
2050
  busy: false,
@@ -2111,14 +2197,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2111
2197
  });
2112
2198
  }
2113
2199
  catch (error) {
2200
+ if (exitForAuthenticationError(error))
2201
+ return;
2114
2202
  appendError(error.message);
2115
2203
  }
2116
2204
  finally {
2117
- store.update((current) => ({
2118
- ...current,
2119
- busy: false,
2120
- status: 'Ready',
2121
- }));
2205
+ if (!exiting) {
2206
+ store.update((current) => ({
2207
+ ...current,
2208
+ busy: false,
2209
+ status: 'Ready',
2210
+ }));
2211
+ }
2122
2212
  }
2123
2213
  return;
2124
2214
  }
@@ -2132,14 +2222,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2132
2222
  await openResumePicker();
2133
2223
  }
2134
2224
  catch (error) {
2225
+ if (exitForAuthenticationError(error))
2226
+ return;
2135
2227
  appendError(error.message);
2136
2228
  }
2137
2229
  finally {
2138
- store.update((current) => ({
2139
- ...current,
2140
- busy: false,
2141
- status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
2142
- }));
2230
+ if (!exiting) {
2231
+ store.update((current) => ({
2232
+ ...current,
2233
+ busy: false,
2234
+ status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
2235
+ }));
2236
+ }
2143
2237
  }
2144
2238
  return;
2145
2239
  }
@@ -2159,33 +2253,68 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2159
2253
  }
2160
2254
  }
2161
2255
  catch (error) {
2256
+ if (exitForAuthenticationError(error))
2257
+ return;
2162
2258
  appendError(error.message);
2163
2259
  }
2164
2260
  finally {
2261
+ if (!exiting) {
2262
+ store.update((current) => ({
2263
+ ...current,
2264
+ busy: false,
2265
+ status: current.modelPickerOpen ? 'Select a model' : 'Ready',
2266
+ }));
2267
+ }
2268
+ }
2269
+ return;
2270
+ }
2271
+ if (input === '/new') {
2272
+ newConversationInFlight = true;
2273
+ store.update((current) => ({
2274
+ ...current,
2275
+ busy: true,
2276
+ busySince: Date.now(),
2277
+ imageAttachments: [],
2278
+ queuedMessage: null,
2279
+ status: 'Starting a new conversation...',
2280
+ }));
2281
+ if (!(await saveActiveSessionOrAbort())) {
2282
+ newConversationInFlight = false;
2165
2283
  store.update((current) => ({
2166
2284
  ...current,
2167
2285
  busy: false,
2168
- status: current.modelPickerOpen ? 'Select a model' : 'Ready',
2286
+ busySince: null,
2287
+ status: 'Ready',
2169
2288
  }));
2289
+ await flushQueuedMessage();
2290
+ return;
2170
2291
  }
2171
- return;
2172
- }
2173
- if (input === '/clear') {
2174
- clearConversation(session);
2292
+ startNewConversation(session);
2293
+ setBackgroundJobSession(session.sessionId);
2294
+ setScratchSession(session.sessionId);
2295
+ setTodoSession(session.sessionId);
2175
2296
  clearTodos();
2297
+ syncBackgroundJobsState();
2176
2298
  syncTodosState();
2177
2299
  latestUsageSummary = null;
2178
- await saveActiveSession();
2179
2300
  store.replaceTranscript([
2180
2301
  {
2181
- body: 'Conversation cleared.',
2302
+ body: 'Started a new conversation. The previous session remains saved.',
2182
2303
  kind: 'system',
2183
- title: 'System',
2304
+ title: 'Session',
2184
2305
  },
2185
2306
  ]);
2307
+ await saveActiveSession();
2186
2308
  syncShellStateFromSession();
2187
- store.update((current) => ({ ...current, queuedMessage: null }));
2309
+ newConversationInFlight = false;
2310
+ store.update((current) => ({
2311
+ ...current,
2312
+ busy: false,
2313
+ busySince: null,
2314
+ status: 'Ready',
2315
+ }));
2188
2316
  await remountTui();
2317
+ await flushQueuedMessage();
2189
2318
  return;
2190
2319
  }
2191
2320
  latestUsageSummary = null;
@@ -2196,6 +2325,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2196
2325
  activeTurnAbort = turnAbort;
2197
2326
  lastTurnStartedAt = turnStartedAt;
2198
2327
  todosTouchedThisTurn = false;
2328
+ clearTodos();
2329
+ syncTodosState();
2199
2330
  const userEntry = {
2200
2331
  body: input,
2201
2332
  kind: 'user',
@@ -2208,6 +2339,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2208
2339
  ...current,
2209
2340
  activeTurnInput: input,
2210
2341
  activeTurnInputPreformatted: preformatted,
2342
+ analyzingImages: 0,
2211
2343
  busy: true,
2212
2344
  busySince: turnStartedAt,
2213
2345
  clockNow: turnStartedAt,
@@ -2232,7 +2364,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2232
2364
  return;
2233
2365
  latestUsageSummary = result.usageSummary ?? null;
2234
2366
  const turnEntries = takePendingTurnEntries();
2235
- await saveActiveSession();
2367
+ if (!(await saveActiveSession()))
2368
+ return;
2236
2369
  syncShellStateFromSession();
2237
2370
  store.update((current) => ({
2238
2371
  ...current,
@@ -2283,6 +2416,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2283
2416
  return;
2284
2417
  }
2285
2418
  const cancelled = isTurnCancelledError(error);
2419
+ if (exitForAuthenticationError(error))
2420
+ return;
2286
2421
  store.update((current) => ({
2287
2422
  ...current,
2288
2423
  activeTurnInput: '',
@@ -2307,7 +2442,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2307
2442
  title: 'System',
2308
2443
  },
2309
2444
  ]);
2310
- await saveActiveSession();
2445
+ if (!(await saveActiveSession()))
2446
+ return;
2311
2447
  }
2312
2448
  else {
2313
2449
  appendStaticEntries(turnEntries);
@@ -2327,13 +2463,19 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2327
2463
  }
2328
2464
  }
2329
2465
  };
2466
+ session.onImageAnalysis = (activeImageCount) => {
2467
+ store.update((current) => ({
2468
+ ...current,
2469
+ analyzingImages: Math.max(0, activeImageCount),
2470
+ }));
2471
+ };
2330
2472
  session.onStatus = (message) => {
2331
2473
  const panel = thinkingPanelFromStatus(message);
2332
2474
  store.update((current) => ({
2333
2475
  ...current,
2334
2476
  status: message,
2335
- thinkingTitle: panel?.title ?? current.thinkingTitle,
2336
- thinkingNotes: panel?.notes ?? current.thinkingNotes,
2477
+ thinkingTitle: panel ? panel.title : liveStatusPanelTitle(message),
2478
+ thinkingNotes: panel ? panel.notes : [],
2337
2479
  tokenUsage: formatClientTokenUsage(current.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
2338
2480
  }));
2339
2481
  };
@@ -2531,10 +2673,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2531
2673
  terminalTitle.dispose();
2532
2674
  killAllBackgroundJobs();
2533
2675
  setBackgroundJobSession(null);
2676
+ setScratchSession(null);
2534
2677
  setTodoSession(null);
2535
2678
  setBackgroundJobUpdateHook(null);
2536
2679
  await bridge.close();
2680
+ releaseTerminalWrites();
2537
2681
  setCommandOutputHook(null);
2538
2682
  }
2539
2683
  });
2684
+ if (fatalError)
2685
+ throw fatalError;
2540
2686
  }
@@ -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;