@thegitai/cli 1.0.0-preview.3 → 1.0.0-preview.5
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 +12 -4
- package/dist/bin/ai.js +46 -288
- package/dist/src/api/chat.js +101 -22
- package/dist/src/help-text.js +16 -5
- package/dist/src/project-index.js +13 -1
- package/dist/src/turn-failure-marker.js +11 -0
- package/dist/src/ui/repl.js +24 -13
- package/dist/src/ui/tui/bridge.js +3 -0
- package/dist/src/ui/tui/build-frame.js +19 -11
- package/dist/src/ui/tui/markdown-render.js +72 -73
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/package.json +18 -6
- package/dist/src/markdown-renderer.js +0 -112
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# TheGitAI
|
|
1
|
+
# TheGitAI — AI coding agent for your terminal
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
10
|
+
import { createSession } from '../src/session.js';
|
|
12
11
|
import { applySessionSnapshot, listSessionMetadata, loadSessionSnapshot, saveSessionState, } from '../src/session-store.js';
|
|
13
|
-
import { runClientInteractive
|
|
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
|
|
130
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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`));
|
package/dist/src/api/chat.js
CHANGED
|
@@ -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.') {
|
|
@@ -51,6 +52,7 @@ function parseSseBlock(block) {
|
|
|
51
52
|
return { event, data: text };
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
let toolStateSeqCounter = 0;
|
|
54
56
|
function toolStateFromSession(session) {
|
|
55
57
|
return {
|
|
56
58
|
autoYes: session.autoYes,
|
|
@@ -102,7 +104,7 @@ function preserveFailedTurnInput(session, input, category) {
|
|
|
102
104
|
session.history.push({ role: 'user', parts: [{ text }], kind: 'turnStart' });
|
|
103
105
|
session.history.push({
|
|
104
106
|
role: 'model',
|
|
105
|
-
parts: [{ text:
|
|
107
|
+
parts: [{ text: formatTurnFailureMarker(category) }],
|
|
106
108
|
});
|
|
107
109
|
}
|
|
108
110
|
function historyHasToolCall(session, callId) {
|
|
@@ -184,6 +186,7 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
184
186
|
toolCallId: event.call.id,
|
|
185
187
|
result,
|
|
186
188
|
toolState: toolStateFromSession(session),
|
|
189
|
+
toolStateSeq: ++toolStateSeqCounter,
|
|
187
190
|
};
|
|
188
191
|
const trace = createTraceContext(traceId);
|
|
189
192
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
|
|
@@ -202,6 +205,29 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
202
205
|
throw await readErrorResponse(response, trace.traceId);
|
|
203
206
|
}
|
|
204
207
|
}
|
|
208
|
+
const turnIdOverrides = new WeakMap();
|
|
209
|
+
function enterServerTurnId(session, serverSessionTurnId) {
|
|
210
|
+
const active = turnIdOverrides.get(session);
|
|
211
|
+
if (active) {
|
|
212
|
+
active.depth += 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
turnIdOverrides.set(session, {
|
|
216
|
+
previousTurnId: session.turnState.id,
|
|
217
|
+
depth: 1,
|
|
218
|
+
});
|
|
219
|
+
session.turnState.id = serverSessionTurnId;
|
|
220
|
+
}
|
|
221
|
+
function exitServerTurnId(session) {
|
|
222
|
+
const active = turnIdOverrides.get(session);
|
|
223
|
+
if (!active)
|
|
224
|
+
return;
|
|
225
|
+
active.depth -= 1;
|
|
226
|
+
if (active.depth === 0) {
|
|
227
|
+
session.turnState.id = active.previousTurnId;
|
|
228
|
+
turnIdOverrides.delete(session);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
205
231
|
async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
|
|
206
232
|
const turnId = String(event?.turnId ?? '').trim();
|
|
207
233
|
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
@@ -210,10 +236,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
210
236
|
if (signal?.aborted) {
|
|
211
237
|
throw new TurnCancelledError();
|
|
212
238
|
}
|
|
213
|
-
const previousTurnId = session.turnState.id;
|
|
214
239
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
215
240
|
if (serverSessionTurnId) {
|
|
216
|
-
session
|
|
241
|
+
enterServerTurnId(session, serverSessionTurnId);
|
|
217
242
|
if (!session.clientState.safety.checkpoints.some((checkpoint) => checkpoint.turnId === serverSessionTurnId)) {
|
|
218
243
|
createPromptCheckpoint(session.clientState.safety, 'prompt boundary', serverSessionTurnId);
|
|
219
244
|
}
|
|
@@ -236,7 +261,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
236
261
|
});
|
|
237
262
|
}
|
|
238
263
|
finally {
|
|
239
|
-
|
|
264
|
+
if (serverSessionTurnId) {
|
|
265
|
+
exitServerTurnId(session);
|
|
266
|
+
}
|
|
240
267
|
}
|
|
241
268
|
}
|
|
242
269
|
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
|
|
@@ -249,6 +276,31 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
249
276
|
const finalResult = {
|
|
250
277
|
current: null,
|
|
251
278
|
};
|
|
279
|
+
const pendingParallelTools = [];
|
|
280
|
+
let firstParallelFailure = null;
|
|
281
|
+
let rejectOnParallelFailure = null;
|
|
282
|
+
const parallelToolFailure = new Promise((_, reject) => {
|
|
283
|
+
rejectOnParallelFailure = reject;
|
|
284
|
+
});
|
|
285
|
+
parallelToolFailure.catch(() => { });
|
|
286
|
+
function recordParallelFailure(error) {
|
|
287
|
+
const failure = error ?? new Error('Local tool execution failed.');
|
|
288
|
+
if (firstParallelFailure == null) {
|
|
289
|
+
firstParallelFailure = failure;
|
|
290
|
+
rejectOnParallelFailure?.(failure);
|
|
291
|
+
}
|
|
292
|
+
return failure;
|
|
293
|
+
}
|
|
294
|
+
async function drainParallelTools() {
|
|
295
|
+
if (!pendingParallelTools.length)
|
|
296
|
+
return;
|
|
297
|
+
const pending = pendingParallelTools.splice(0);
|
|
298
|
+
const outcomes = await Promise.all(pending);
|
|
299
|
+
for (const outcome of outcomes) {
|
|
300
|
+
if (outcome != null)
|
|
301
|
+
throw outcome;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
252
304
|
async function handleEvent(event) {
|
|
253
305
|
if (event.event === 'status') {
|
|
254
306
|
const message = publicStatusMessage(event.data);
|
|
@@ -260,11 +312,26 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
260
312
|
return;
|
|
261
313
|
}
|
|
262
314
|
if (event.event === 'tool-call') {
|
|
315
|
+
const data = event.data;
|
|
316
|
+
if (data?.parallelSafe === true) {
|
|
317
|
+
pendingParallelTools.push(executeAndPostToolResult({
|
|
318
|
+
config,
|
|
319
|
+
projectIndex,
|
|
320
|
+
session,
|
|
321
|
+
event: data,
|
|
322
|
+
input,
|
|
323
|
+
fetchImpl,
|
|
324
|
+
signal,
|
|
325
|
+
traceId,
|
|
326
|
+
}).then(() => null, (error) => recordParallelFailure(error)));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
await drainParallelTools();
|
|
263
330
|
await executeAndPostToolResult({
|
|
264
331
|
config,
|
|
265
332
|
projectIndex,
|
|
266
333
|
session,
|
|
267
|
-
event:
|
|
334
|
+
event: data,
|
|
268
335
|
input,
|
|
269
336
|
fetchImpl,
|
|
270
337
|
signal,
|
|
@@ -280,10 +347,12 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
280
347
|
return;
|
|
281
348
|
}
|
|
282
349
|
if (event.event === 'result') {
|
|
350
|
+
await drainParallelTools();
|
|
283
351
|
finalResult.current = event.data;
|
|
284
352
|
return;
|
|
285
353
|
}
|
|
286
354
|
if (event.event === 'cancelled' || event.event === 'error') {
|
|
355
|
+
await drainParallelTools().catch(() => { });
|
|
287
356
|
const message = String(event.data?.message ?? 'Server chat failed.');
|
|
288
357
|
if (event.event === 'cancelled') {
|
|
289
358
|
throw new TurnCancelledError(message);
|
|
@@ -291,24 +360,34 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
291
360
|
throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable), typeof event.data?.traceId === 'string' ? event.data.traceId : traceId);
|
|
292
361
|
}
|
|
293
362
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
363
|
+
try {
|
|
364
|
+
while (true) {
|
|
365
|
+
if (signal?.aborted) {
|
|
366
|
+
await reader.cancel().catch(() => { });
|
|
367
|
+
throw new TurnCancelledError();
|
|
368
|
+
}
|
|
369
|
+
const read = await Promise.race([reader.read(), parallelToolFailure]);
|
|
370
|
+
if (read.done)
|
|
371
|
+
break;
|
|
372
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
373
|
+
let separatorIndex = buffer.indexOf('\n\n');
|
|
374
|
+
while (separatorIndex !== -1) {
|
|
375
|
+
const block = buffer.slice(0, separatorIndex);
|
|
376
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
377
|
+
const event = parseSseBlock(block);
|
|
378
|
+
if (event)
|
|
379
|
+
await handleEvent(event);
|
|
380
|
+
separatorIndex = buffer.indexOf('\n\n');
|
|
381
|
+
}
|
|
311
382
|
}
|
|
383
|
+
await drainParallelTools();
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
await reader.cancel().catch(() => { });
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
await drainParallelTools().catch(() => { });
|
|
312
391
|
}
|
|
313
392
|
buffer += decoder.decode();
|
|
314
393
|
const tail = buffer.trim();
|
package/dist/src/help-text.js
CHANGED
|
@@ -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
|
|
39
|
-
'
|
|
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
|
-
'-
|
|
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+(.*)$/);
|