@thegitai/cli 1.0.0-preview.6 → 1.0.0-preview.8

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.
@@ -57,7 +57,8 @@ const HELP_MARKDOWN = [
57
57
  '## Keys & clipboard',
58
58
  '',
59
59
  '- **Enter** sends • **Shift+Tab** cycles modes • **Esc** cancels the turn •',
60
- ' **Ctrl+C** quits. These are the same on macOS, Linux, and Windows.',
60
+ ' **Ctrl+C** clears the composer or the queued message, and quits once there',
61
+ ' is nothing left to clear. These are the same on macOS, Linux, and Windows.',
61
62
  `- **Paste** into the composer with your terminal's paste shortcut (\`${PASTE_SHORTCUT}\``,
62
63
  ' on this system) or by right-clicking the composer.',
63
64
  '- **Copy** from the transcript by dragging to select; double-click copies a',
@@ -78,7 +79,7 @@ const HELP_MARKDOWN = [
78
79
  ' browse them, press Enter to expand one and read its output, k to stop it',
79
80
  '- `/jobs output <id>` — print one job\'s full captured output',
80
81
  '- `/jobs kill <id>` — stop one background job',
81
- '- `/clear` — clear the current conversation history',
82
+ '- `/new` — start a new conversation; this session remains saved',
82
83
  '- `/exit` — quit the session',
83
84
  '',
84
85
  '## Safety & approvals',
@@ -69,6 +69,14 @@ export function createSession({ rootDir, autoYes = false, agentMode, modelId, ma
69
69
  serverState: cloneOpaqueState(serverState),
70
70
  };
71
71
  }
72
+ export function startNewConversation(session) {
73
+ clearConversation(session);
74
+ const createdAt = new Date().toISOString();
75
+ session.sessionId = createSessionId();
76
+ session.sessionName = null;
77
+ session.sessionCreatedAt = createdAt;
78
+ session.sessionUpdatedAt = createdAt;
79
+ }
72
80
  export function clearConversation(session) {
73
81
  session.history = [];
74
82
  session.serverState = preserveProviderSelection(session.serverState);
@@ -15,7 +15,7 @@ import { clearCliAuthConfig } from '../api/auth.js';
15
15
  import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
16
16
  import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
17
17
  import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
18
- import { clearConversation, } from '../session.js';
18
+ import { startNewConversation, } from '../session.js';
19
19
  import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../session-store.js';
20
20
  import { truncate } from '../utils.js';
21
21
  import { writeClipboardText } from '../core/clipboard.js';
@@ -116,8 +116,8 @@ export const SLASH_COMMANDS = [
116
116
  description: 'Background jobs: pick to view output or kill',
117
117
  },
118
118
  {
119
- command: '/clear',
120
- description: 'Clear conversation history',
119
+ command: '/new',
120
+ description: 'start a new conversation; this session remains saved',
121
121
  },
122
122
  {
123
123
  command: '/exit',
@@ -1320,6 +1320,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1320
1320
  let latestUsageSummary = null;
1321
1321
  let pendingTurnEntries = [];
1322
1322
  let activeTurnAbort = null;
1323
+ let newConversationInFlight = false;
1323
1324
  let todosTouchedThisTurn = false;
1324
1325
  const syncTodosState = () => {
1325
1326
  store.update((current) => ({ ...current, todos: listTodos() }));
@@ -1521,7 +1522,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1521
1522
  }));
1522
1523
  };
1523
1524
  const cancelActiveTurn = () => {
1524
- if (!store.getState().busy)
1525
+ if (!store.getState().busy || newConversationInFlight)
1525
1526
  return;
1526
1527
  disarmExitConfirm();
1527
1528
  dismissPendingApproval();
@@ -1677,6 +1678,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1677
1678
  return true;
1678
1679
  }
1679
1680
  };
1681
+ const saveActiveSessionOrAbort = 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 false;
1691
+ }
1692
+ };
1680
1693
  const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
1681
1694
  if (exiting || signal?.aborted) {
1682
1695
  resolve(null);
@@ -2243,23 +2256,53 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2243
2256
  }
2244
2257
  return;
2245
2258
  }
2246
- if (input === '/clear') {
2247
- clearConversation(session);
2259
+ if (input === '/new') {
2260
+ newConversationInFlight = true;
2261
+ store.update((current) => ({
2262
+ ...current,
2263
+ busy: true,
2264
+ busySince: Date.now(),
2265
+ imageAttachments: [],
2266
+ queuedMessage: null,
2267
+ status: 'Starting a new conversation...',
2268
+ }));
2269
+ if (!(await saveActiveSessionOrAbort())) {
2270
+ newConversationInFlight = false;
2271
+ store.update((current) => ({
2272
+ ...current,
2273
+ busy: false,
2274
+ busySince: null,
2275
+ status: 'Ready',
2276
+ }));
2277
+ await flushQueuedMessage();
2278
+ return;
2279
+ }
2280
+ startNewConversation(session);
2281
+ setBackgroundJobSession(session.sessionId);
2282
+ setScratchSession(session.sessionId);
2283
+ setTodoSession(session.sessionId);
2248
2284
  clearTodos();
2285
+ syncBackgroundJobsState();
2249
2286
  syncTodosState();
2250
2287
  latestUsageSummary = null;
2251
- if (!(await saveActiveSession()))
2252
- return;
2253
2288
  store.replaceTranscript([
2254
2289
  {
2255
- body: 'Conversation cleared.',
2290
+ body: 'Started a new conversation. The previous session remains saved.',
2256
2291
  kind: 'system',
2257
- title: 'System',
2292
+ title: 'Session',
2258
2293
  },
2259
2294
  ]);
2295
+ await saveActiveSession();
2260
2296
  syncShellStateFromSession();
2261
- store.update((current) => ({ ...current, queuedMessage: null }));
2297
+ newConversationInFlight = false;
2298
+ store.update((current) => ({
2299
+ ...current,
2300
+ busy: false,
2301
+ busySince: null,
2302
+ status: 'Ready',
2303
+ }));
2262
2304
  await remountTui();
2305
+ await flushQueuedMessage();
2263
2306
  return;
2264
2307
  }
2265
2308
  latestUsageSummary = null;
@@ -163,7 +163,7 @@ const CLIENT_SLASH_COMMANDS = [
163
163
  { command: '/model', description: 'Switch the active model' },
164
164
  { command: '/resume', description: 'Open the session picker to resume a previous session' },
165
165
  { command: '/jobs', description: 'Background jobs: pick to view output or kill' },
166
- { command: '/clear', description: 'Clear conversation history' },
166
+ { command: '/new', description: 'start a new conversation; this session remains saved' },
167
167
  { command: '/exit', description: 'Quit the current session' },
168
168
  ];
169
169
  function buildModelPickerOptions(currentModelId, serverModels) {
@@ -405,15 +405,18 @@ function composerFooterLines(state) {
405
405
  }));
406
406
  return lines;
407
407
  }
408
+ const busyHelperText = state.queuedMessage
409
+ ? 'Enter re-queues • ↑ edit queued • Esc / Ctrl+C clear queued'
410
+ : state.input
411
+ ? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
412
+ : 'Enter queues • Esc / Ctrl+C cancel turn';
408
413
  const helperText = state.busy
409
- ? state.queuedMessage
410
- ? 'Enter re-queues • ↑ edit queued • Esc cancels queued'
411
- : 'Enter queues • Esc / Ctrl+C cancel turn'
414
+ ? busyHelperText
412
415
  : process.platform === 'win32'
413
- ? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C quits'
416
+ ? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
414
417
  : process.platform === 'darwin'
415
- ? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits'
416
- : 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C quits';
418
+ ? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
419
+ : 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
417
420
  const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
418
421
  const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
419
422
  const footerSpans = [
@@ -920,7 +923,10 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
920
923
  const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
921
924
  if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
922
925
  const composerLines = [];
923
- if (state.queuedMessage) {
926
+ if (state.busy && state.status === 'Starting a new conversation...') {
927
+ composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
928
+ }
929
+ else if (state.queuedMessage) {
924
930
  const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
925
931
  const imageCount = state.queuedMessage.imageAttachments.length;
926
932
  composerLines.push(line(span(`↳ Queued · "${preview}"`, { color: 'gray', dim: true }), ...(imageCount > 0
@@ -9,6 +9,17 @@ function isClipboardImagePasteKey(key) {
9
9
  }
10
10
  return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
11
11
  }
12
+ function composerIsEmpty(state) {
13
+ return (!state.input &&
14
+ state.cursor === 0 &&
15
+ state.pastedChunks.length === 0 &&
16
+ state.promptHistoryCursor === null);
17
+ }
18
+ function composerHasDiscardableDraft(state) {
19
+ if (!composerIsEmpty(state))
20
+ return true;
21
+ return !state.busy && state.imageAttachments.length > 0;
22
+ }
12
23
  function shouldShowCommandPalette(state) {
13
24
  if (state.busy ||
14
25
  state.exiting ||
@@ -132,11 +143,41 @@ export function handleShellKeyEvent(store, handlers, event) {
132
143
  return;
133
144
  const key = event;
134
145
  const state = store.getState();
146
+ const commandPaletteActive = shouldShowCommandPalette(state);
147
+ const commandSuggestions = commandPaletteActive
148
+ ? getSlashCommandSuggestions(state.input)
149
+ : [];
150
+ const prepareForComposerInputChange = (current, nextInput) => {
151
+ if (shouldRemountLiveFrameForComposerInputChange(current, nextInput)) {
152
+ handlers.onLiveFrameShapeChange();
153
+ }
154
+ };
135
155
  if (key.ctrl && key.input === 'c') {
136
156
  if (state.sudoPrompt) {
137
157
  handlers.onSudoPasswordInput({ kind: 'cancel' });
138
158
  return;
139
159
  }
160
+ if (state.queuedMessage) {
161
+ handlers.onLiveFrameShapeChange();
162
+ store.update((current) => ({ ...current, queuedMessage: null }));
163
+ return;
164
+ }
165
+ if (composerHasDiscardableDraft(state)) {
166
+ store.update((current) => {
167
+ prepareForComposerInputChange(current, '');
168
+ return {
169
+ ...current,
170
+ commandCursor: 0,
171
+ cursor: 0,
172
+ imageAttachments: current.busy ? current.imageAttachments : [],
173
+ input: '',
174
+ pastedChunks: [],
175
+ promptHistoryCursor: null,
176
+ promptHistoryDraft: '',
177
+ };
178
+ });
179
+ return;
180
+ }
140
181
  if (handlers.onCtrlC) {
141
182
  handlers.onCtrlC();
142
183
  return;
@@ -144,15 +185,6 @@ export function handleShellKeyEvent(store, handlers, event) {
144
185
  handlers.onRequestExit();
145
186
  return;
146
187
  }
147
- const commandPaletteActive = shouldShowCommandPalette(state);
148
- const commandSuggestions = commandPaletteActive
149
- ? getSlashCommandSuggestions(state.input)
150
- : [];
151
- const prepareForComposerInputChange = (current, nextInput) => {
152
- if (shouldRemountLiveFrameForComposerInputChange(current, nextInput)) {
153
- handlers.onLiveFrameShapeChange();
154
- }
155
- };
156
188
  if (state.sudoPrompt) {
157
189
  if (key.escape) {
158
190
  handlers.onSudoPasswordInput({ kind: 'cancel' });
@@ -317,10 +349,7 @@ export function handleShellKeyEvent(store, handlers, event) {
317
349
  return;
318
350
  }
319
351
  store.update((current) => {
320
- if (!current.input &&
321
- current.cursor === 0 &&
322
- current.pastedChunks.length === 0 &&
323
- current.promptHistoryCursor === null) {
352
+ if (composerIsEmpty(current)) {
324
353
  return current;
325
354
  }
326
355
  prepareForComposerInputChange(current, '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.6",
3
+ "version": "1.0.0-preview.8",
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
5
  "keywords": [
6
6
  "ai",
@@ -37,10 +37,10 @@
37
37
  "@lydell/node-pty-linux-x64": "1.1.0",
38
38
  "@lydell/node-pty-win32-arm64": "1.1.0",
39
39
  "@lydell/node-pty-win32-x64": "1.1.0",
40
- "@thegitai/tui-darwin-arm64": "1.0.0-preview.6",
41
- "@thegitai/tui-darwin-x64": "1.0.0-preview.6",
42
- "@thegitai/tui-linux-x64": "1.0.0-preview.6",
43
- "@thegitai/tui-win32-x64": "1.0.0-preview.6",
40
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.8",
41
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.8",
42
+ "@thegitai/tui-linux-x64": "1.0.0-preview.8",
43
+ "@thegitai/tui-win32-x64": "1.0.0-preview.8",
44
44
  "@vscode/ripgrep": "1.18.0"
45
45
  },
46
46
  "publishConfig": {