@thegitai/cli 1.0.0-preview.24 → 1.0.0-preview.26
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/dist/bin/ai.js +54 -30
- package/dist/src/api/auth.js +4 -2
- package/dist/src/api/chat.js +18 -1
- package/dist/src/api/http.js +13 -1
- package/dist/src/api/models.js +17 -8
- package/dist/src/session-store.js +2 -0
- package/dist/src/tools/index.js +11 -0
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/ui/repl.js +10 -0
- package/dist/src/ui/tui/build-frame.js +7 -5
- package/package.json +5 -5
package/dist/bin/ai.js
CHANGED
|
@@ -4,7 +4,7 @@ import { stdin as input, stdout as output } from 'node:process';
|
|
|
4
4
|
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
|
-
import { authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
|
|
7
|
+
import { STARTUP_RETRY_BUDGET, authenticationErrorMessage, isAuthenticationError, isTransientNetworkError, } from '../src/api/http.js';
|
|
8
8
|
import { formatCliHelpText } from '../src/help-text.js';
|
|
9
9
|
import { createIndex } from '../src/project-index.js';
|
|
10
10
|
import { createSession } from '../src/session.js';
|
|
@@ -20,6 +20,20 @@ const { auth, chat, models, sessions } = ServerApi;
|
|
|
20
20
|
function printUsage() {
|
|
21
21
|
console.log(formatCliHelpText({ color: process.stdout.isTTY === true }));
|
|
22
22
|
}
|
|
23
|
+
function unreachableReason(error) {
|
|
24
|
+
const err = error;
|
|
25
|
+
const code = err?.code ?? err?.cause?.code;
|
|
26
|
+
if (err?.name === 'TimeoutError' || err?.name === 'AbortError') {
|
|
27
|
+
return 'network timeout';
|
|
28
|
+
}
|
|
29
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN')
|
|
30
|
+
return 'DNS lookup failed';
|
|
31
|
+
if (code === 'ECONNREFUSED')
|
|
32
|
+
return 'connection refused';
|
|
33
|
+
if (code === 'ECONNRESET' || code === 'EPIPE')
|
|
34
|
+
return 'connection reset';
|
|
35
|
+
return err?.message ? String(err.message) : 'network error';
|
|
36
|
+
}
|
|
23
37
|
async function promptText(question, fallback = null) {
|
|
24
38
|
const rl = readline.createInterface({ input, output });
|
|
25
39
|
try {
|
|
@@ -184,41 +198,51 @@ export async function main() {
|
|
|
184
198
|
const authConfig = requireCliAuthConfig();
|
|
185
199
|
const serverSessionClient = sessions.createServerSessionClient({ config: authConfig });
|
|
186
200
|
const cachedModels = models.selectCacheForServer(models.readCachedServerModels(), authConfig.serverUrl);
|
|
201
|
+
const [modelsOutcome, whoamiOutcome] = await Promise.allSettled([
|
|
202
|
+
models.fetchServerModels({
|
|
203
|
+
config: authConfig,
|
|
204
|
+
budget: STARTUP_RETRY_BUDGET,
|
|
205
|
+
}),
|
|
206
|
+
auth.fetchWhoamiResponse({
|
|
207
|
+
config: authConfig,
|
|
208
|
+
budget: STARTUP_RETRY_BUDGET,
|
|
209
|
+
}),
|
|
210
|
+
]);
|
|
187
211
|
let offlineNotice = null;
|
|
188
212
|
let serverModels;
|
|
189
|
-
|
|
190
|
-
serverModels =
|
|
213
|
+
if (modelsOutcome.status === 'fulfilled') {
|
|
214
|
+
serverModels = modelsOutcome.value;
|
|
191
215
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
216
|
+
else if (isTransientNetworkError(modelsOutcome.reason) &&
|
|
217
|
+
cachedModels?.models.length) {
|
|
218
|
+
serverModels = { models: cachedModels.models };
|
|
219
|
+
offlineNotice = unreachableReason(modelsOutcome.reason);
|
|
220
|
+
}
|
|
221
|
+
else if (isTransientNetworkError(modelsOutcome.reason)) {
|
|
222
|
+
throw new Error(`Couldn't reach TheGitAI to load your models (${unreachableReason(modelsOutcome.reason)}).\nCheck your internet connection and run \`ai\` again.`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
throw modelsOutcome.reason;
|
|
200
226
|
}
|
|
201
227
|
let whoami;
|
|
202
|
-
|
|
203
|
-
whoami =
|
|
228
|
+
if (whoamiOutcome.status === 'fulfilled') {
|
|
229
|
+
whoami = whoamiOutcome.value;
|
|
204
230
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
throw error;
|
|
221
|
-
}
|
|
231
|
+
else if (isTransientNetworkError(whoamiOutcome.reason)) {
|
|
232
|
+
whoami = {
|
|
233
|
+
customer: {
|
|
234
|
+
id: '',
|
|
235
|
+
uuid: '',
|
|
236
|
+
email: authConfig.email,
|
|
237
|
+
customer_type: authConfig.customerType ?? 'USER',
|
|
238
|
+
scopes: [],
|
|
239
|
+
},
|
|
240
|
+
debugUi: { showSessionId: false },
|
|
241
|
+
};
|
|
242
|
+
offlineNotice ??= unreachableReason(whoamiOutcome.reason);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
throw whoamiOutcome.reason;
|
|
222
246
|
}
|
|
223
247
|
if (offlineNotice) {
|
|
224
248
|
console.error(chalk.yellow(`⚠ Couldn't reach TheGitAI (${offlineNotice}). Starting with cached settings — it will reconnect on your next message.`));
|
package/dist/src/api/auth.js
CHANGED
|
@@ -50,12 +50,14 @@ export async function fetchWhoami({ config, fetchImpl = globalThis.fetch, }) {
|
|
|
50
50
|
const data = await fetchWhoamiResponse({ config, fetchImpl });
|
|
51
51
|
return data.customer;
|
|
52
52
|
}
|
|
53
|
-
export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch, }) {
|
|
53
|
+
export async function fetchWhoamiResponse({ config, fetchImpl = globalThis.fetch, budget = {}, }) {
|
|
54
|
+
const { timeoutMs, ...ladder } = budget;
|
|
54
55
|
const data = (await retryTransient(() => authorizedJson({
|
|
55
56
|
config,
|
|
56
57
|
path: '/v1/auth/whoami',
|
|
57
58
|
fetchImpl,
|
|
58
|
-
|
|
59
|
+
...(timeoutMs == null ? {} : { timeoutMs }),
|
|
60
|
+
}), ladder));
|
|
59
61
|
if (!data?.customer?.email) {
|
|
60
62
|
throw new Error('Server returned an invalid whoami response.');
|
|
61
63
|
}
|
package/dist/src/api/chat.js
CHANGED
|
@@ -2,6 +2,7 @@ import { drainBackgroundJobNotifications } from '../background-jobs.js';
|
|
|
2
2
|
import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
|
|
3
3
|
import { applySessionSnapshot, saveSessionState, snapshotFromSession, } from '../session-store.js';
|
|
4
4
|
import { executeLocalToolCall } from '../tool-executor.js';
|
|
5
|
+
import { saveGeneratedImage } from '../tools/save-generated-image.js';
|
|
5
6
|
import { isUserInputQuestionArray } from './contracts.js';
|
|
6
7
|
import { createTraceContext, gatewayFailureCategory, normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
7
8
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
@@ -165,6 +166,9 @@ function publicStatusMessage(data) {
|
|
|
165
166
|
if (event.phase === 'analyzing_image') {
|
|
166
167
|
return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
|
|
167
168
|
}
|
|
169
|
+
if (event.phase === 'generating_image') {
|
|
170
|
+
return 'Generating image...';
|
|
171
|
+
}
|
|
168
172
|
if (event.phase === 'running_tool')
|
|
169
173
|
return `Running ${toolName}...`;
|
|
170
174
|
if (event.phase === 'waiting_for_tool')
|
|
@@ -312,7 +316,14 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
312
316
|
}
|
|
313
317
|
try {
|
|
314
318
|
const call = normalizeShellJobToolCall(event.call);
|
|
315
|
-
const rawResult =
|
|
319
|
+
const rawResult = event.generatedImage
|
|
320
|
+
? saveGeneratedImage({
|
|
321
|
+
base64Data: event.generatedImage.base64Data,
|
|
322
|
+
mimeType: event.generatedImage.mimeType,
|
|
323
|
+
suggestedFilename: event.generatedImage.suggestedFilename ||
|
|
324
|
+
String(call.args?.filename ?? call.args?.file_name ?? ''),
|
|
325
|
+
})
|
|
326
|
+
: await executeLocalToolCall({ projectIndex }, session, call);
|
|
316
327
|
preserveCancelledTurnToolResult(session, input, { ...event, call }, rawResult);
|
|
317
328
|
if (signal?.aborted) {
|
|
318
329
|
throw new TurnCancelledError();
|
|
@@ -383,9 +394,15 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
383
394
|
const data = event.data;
|
|
384
395
|
if (data?.phase === 'analyzing_image') {
|
|
385
396
|
session.onImageAnalysis?.(Math.max(1, Number(data.imageCount ?? 1) || 1));
|
|
397
|
+
session.onImageGeneration?.(false);
|
|
398
|
+
}
|
|
399
|
+
else if (data?.phase === 'generating_image') {
|
|
400
|
+
session.onImageGeneration?.(true);
|
|
401
|
+
session.onImageAnalysis?.(0);
|
|
386
402
|
}
|
|
387
403
|
else if (data?.phase) {
|
|
388
404
|
session.onImageAnalysis?.(0);
|
|
405
|
+
session.onImageGeneration?.(false);
|
|
389
406
|
}
|
|
390
407
|
const message = publicStatusMessage(event.data);
|
|
391
408
|
if (message)
|
package/dist/src/api/http.js
CHANGED
|
@@ -29,6 +29,14 @@ export function createTraceContext(traceId = createTraceId()) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
export const REQUEST_TIMEOUT_MS = 8000;
|
|
32
|
+
export const STARTUP_REQUEST_TIMEOUT_MS = 3000;
|
|
33
|
+
export const STARTUP_DEADLINE_MS = 10_000;
|
|
34
|
+
export const STARTUP_RETRY_BUDGET = {
|
|
35
|
+
retries: 3,
|
|
36
|
+
baseDelayMs: 200,
|
|
37
|
+
deadlineMs: STARTUP_DEADLINE_MS,
|
|
38
|
+
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS,
|
|
39
|
+
};
|
|
32
40
|
const TRANSIENT_NETWORK_CODES = new Set([
|
|
33
41
|
'ECONNRESET',
|
|
34
42
|
'ECONNREFUSED',
|
|
@@ -60,7 +68,9 @@ export function isTransientNetworkError(error) {
|
|
|
60
68
|
}
|
|
61
69
|
return false;
|
|
62
70
|
}
|
|
63
|
-
export async function retryTransient(run, { retries = 2, baseDelayMs = 400 } = {}) {
|
|
71
|
+
export async function retryTransient(run, { retries = 2, baseDelayMs = 400, deadlineMs, now = () => Date.now(), } = {}) {
|
|
72
|
+
const startedAt = now();
|
|
73
|
+
const remainingMs = () => deadlineMs == null ? Infinity : deadlineMs - (now() - startedAt);
|
|
64
74
|
let attempt = 0;
|
|
65
75
|
for (;;) {
|
|
66
76
|
try {
|
|
@@ -70,6 +80,8 @@ export async function retryTransient(run, { retries = 2, baseDelayMs = 400 } = {
|
|
|
70
80
|
if (attempt >= retries || !isTransientNetworkError(error))
|
|
71
81
|
throw error;
|
|
72
82
|
const delayMs = baseDelayMs * 2 ** attempt;
|
|
83
|
+
if (remainingMs() <= delayMs)
|
|
84
|
+
throw error;
|
|
73
85
|
attempt += 1;
|
|
74
86
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
75
87
|
}
|
package/dist/src/api/models.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getClientStateDir } from '../client-state.js';
|
|
4
4
|
import { REQUEST_TIMEOUT_MS, ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
|
|
@@ -58,12 +58,21 @@ export function selectCacheForServer(cached, serverUrl) {
|
|
|
58
58
|
export function writeCachedServerModels(cache, env = process.env) {
|
|
59
59
|
const filePath = getModelsCachePath(env);
|
|
60
60
|
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(tempPath, `${JSON.stringify(cache, null, 2)}\n`, {
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
mode: 0o600,
|
|
66
|
+
});
|
|
67
|
+
renameSync(tempPath, filePath);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
rmSync(tempPath, { force: true });
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
65
73
|
}
|
|
66
|
-
export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, }) {
|
|
74
|
+
export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, budget = {}, }) {
|
|
75
|
+
const { timeoutMs = REQUEST_TIMEOUT_MS, ...ladder } = budget;
|
|
67
76
|
return retryTransient(async () => {
|
|
68
77
|
const trace = createTraceContext();
|
|
69
78
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/models`, {
|
|
@@ -71,7 +80,7 @@ export async function fetchServerModels({ config, fetchImpl = globalThis.fetch,
|
|
|
71
80
|
authorization: `Bearer ${config.token}`,
|
|
72
81
|
...trace.headers,
|
|
73
82
|
},
|
|
74
|
-
signal: AbortSignal.timeout(
|
|
83
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
75
84
|
});
|
|
76
85
|
const data = (await readJsonResponse(response));
|
|
77
86
|
if (!response.ok) {
|
|
@@ -84,7 +93,7 @@ export async function fetchServerModels({ config, fetchImpl = globalThis.fetch,
|
|
|
84
93
|
throw new Error('Server returned an invalid model list.');
|
|
85
94
|
}
|
|
86
95
|
return { models };
|
|
87
|
-
});
|
|
96
|
+
}, ladder);
|
|
88
97
|
}
|
|
89
98
|
export function selectServerModel({ requestedModelId, cached, serverModels, }) {
|
|
90
99
|
const supportedIds = new Set(serverModels.models.map((model) => model.id));
|
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, w
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { getClientStateDir } from './client-state.js';
|
|
6
6
|
import { findStoredImageByContent, pruneSessionImages, readSessionImage, sweepOrphanSessionImages, } from './core/session-image-store.js';
|
|
7
|
+
import { sweepGeneratedImages } from './tools/save-generated-image.js';
|
|
7
8
|
import { normalizeAssistantEditJournal } from './edit-journal.js';
|
|
8
9
|
import { createSessionGrants } from './permissions.js';
|
|
9
10
|
import { cloneSessionSafetyState, createSessionSafetyState, mergeLocalSessionSafetyState, normalizeSessionSafetyState, } from './session-safety.js';
|
|
@@ -224,6 +225,7 @@ export function pruneSavedSessions(rootDir, env = process.env) {
|
|
|
224
225
|
activeSessionIds: listAllSessionIds(env),
|
|
225
226
|
env,
|
|
226
227
|
});
|
|
228
|
+
sweepGeneratedImages({ env });
|
|
227
229
|
}
|
|
228
230
|
export function readGitBranch(rootDir) {
|
|
229
231
|
try {
|
package/dist/src/tools/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { strReplace } from './str-replace.js';
|
|
|
23
23
|
import { undoEdit } from './undo-edit.js';
|
|
24
24
|
import { updateTodos } from './update-todos.js';
|
|
25
25
|
import { readImageFile } from './read-image-file.js';
|
|
26
|
+
import { saveGeneratedImage } from './save-generated-image.js';
|
|
26
27
|
import { writeFile } from './write-file.js';
|
|
27
28
|
export const TOOL_MAP = {
|
|
28
29
|
search_code: (context, args) => searchCode(context.projectIndex, args),
|
|
@@ -52,6 +53,16 @@ export const TOOL_MAP = {
|
|
|
52
53
|
shell_job_kill: shellJobKill,
|
|
53
54
|
update_todos: updateTodos,
|
|
54
55
|
analyze_image: (context, args) => readImageFile(context, args),
|
|
56
|
+
generate_image: (_context, args) => {
|
|
57
|
+
if (!String(args.base64Data ?? '').trim()) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: 'This CLI build cannot generate images without server-supplied image bytes. Update TheGitAI CLI.',
|
|
61
|
+
failureCategory: 'tool_exception',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return saveGeneratedImage(args);
|
|
65
|
+
},
|
|
55
66
|
};
|
|
56
67
|
function invalidToolCall(error) {
|
|
57
68
|
return {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getClientStateDir } from '../client-state.js';
|
|
4
|
+
import { MAX_IMAGE_SIZE_BYTES, sniffImageMimeType, } from '../core/image-limits.js';
|
|
5
|
+
const IMAGE_FILE_MODE = 0o600;
|
|
6
|
+
const IMAGE_DIR_MODE = 0o700;
|
|
7
|
+
export const GENERATED_IMAGES_SUBDIR = 'generated_images';
|
|
8
|
+
export const GENERATED_IMAGE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
9
|
+
export function getGeneratedImagesDir(env = process.env) {
|
|
10
|
+
return path.join(getClientStateDir(env), GENERATED_IMAGES_SUBDIR);
|
|
11
|
+
}
|
|
12
|
+
export function sanitizeGeneratedImageBasename(raw) {
|
|
13
|
+
const trimmed = String(raw ?? '').trim();
|
|
14
|
+
const base = trimmed
|
|
15
|
+
.replace(/\\/g, '/')
|
|
16
|
+
.split('/')
|
|
17
|
+
.pop()
|
|
18
|
+
?.replace(/^\.+/, '')
|
|
19
|
+
.replace(/[^\w.\-]+/g, '-')
|
|
20
|
+
.replace(/-+/g, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '')
|
|
22
|
+
.replace(/-\.|\.-/g, '.')
|
|
23
|
+
.replace(/^-+|-+$/g, '')
|
|
24
|
+
.slice(0, 80);
|
|
25
|
+
if (!base || base === '.' || base === '..') {
|
|
26
|
+
return `generated-${Date.now()}.png`;
|
|
27
|
+
}
|
|
28
|
+
if (/\.png$/i.test(base)) {
|
|
29
|
+
return base.replace(/\.png$/i, '.png');
|
|
30
|
+
}
|
|
31
|
+
const withoutExt = base.replace(/\.[^.]+$/, '').replace(/-+$/g, '');
|
|
32
|
+
return `${withoutExt || 'generated'}.png`;
|
|
33
|
+
}
|
|
34
|
+
function uniquePath(dir, basename) {
|
|
35
|
+
const candidate = path.join(dir, basename);
|
|
36
|
+
if (!existsSync(candidate)) {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
const ext = path.extname(basename) || '.png';
|
|
40
|
+
const stem = path.basename(basename, ext);
|
|
41
|
+
for (let i = 2; i < 10_000; i += 1) {
|
|
42
|
+
const next = path.join(dir, `${stem}-${i}${ext}`);
|
|
43
|
+
if (!existsSync(next)) {
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return path.join(dir, `${stem}-${Date.now()}${ext}`);
|
|
48
|
+
}
|
|
49
|
+
export function sweepGeneratedImages({ maxAgeMs = GENERATED_IMAGE_MAX_AGE_MS, env = process.env, now = Date.now(), } = {}) {
|
|
50
|
+
const dir = getGeneratedImagesDir(env);
|
|
51
|
+
if (!existsSync(dir))
|
|
52
|
+
return 0;
|
|
53
|
+
let removed = 0;
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(dir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const filePath = path.join(dir, entry);
|
|
63
|
+
try {
|
|
64
|
+
if (now - statSync(filePath).mtimeMs < maxAgeMs)
|
|
65
|
+
continue;
|
|
66
|
+
rmSync(filePath, { force: true });
|
|
67
|
+
removed += 1;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return removed;
|
|
73
|
+
}
|
|
74
|
+
export function saveGeneratedImage(args) {
|
|
75
|
+
const base64Data = String(args.base64Data ?? '').trim();
|
|
76
|
+
if (!base64Data) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: 'This CLI build cannot generate images without server-supplied image bytes. Update TheGitAI CLI.',
|
|
80
|
+
failureCategory: 'tool_exception',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
let bytes;
|
|
84
|
+
try {
|
|
85
|
+
bytes = Buffer.from(base64Data, 'base64');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: 'Generated image bytes are invalid.',
|
|
91
|
+
failureCategory: 'invalid_argument',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (bytes.length === 0 || bytes.length > MAX_IMAGE_SIZE_BYTES) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: 'Generated image exceeded the allowed size limit.',
|
|
98
|
+
failureCategory: 'invalid_argument',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const sniffed = sniffImageMimeType(bytes);
|
|
102
|
+
if (sniffed !== 'image/png') {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: 'Generated image must be a PNG.',
|
|
106
|
+
failureCategory: 'invalid_argument',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const env = args.env ?? process.env;
|
|
110
|
+
const dir = getGeneratedImagesDir(env);
|
|
111
|
+
mkdirSync(dir, { recursive: true, mode: IMAGE_DIR_MODE });
|
|
112
|
+
const basename = sanitizeGeneratedImageBasename(args.suggestedFilename ?? args.filename);
|
|
113
|
+
const target = uniquePath(dir, basename);
|
|
114
|
+
writeFileSync(target, bytes, { mode: IMAGE_FILE_MODE });
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
path: target,
|
|
118
|
+
message: `Image saved to ${target}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -982,6 +982,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
982
982
|
activeTurnInputPreformatted: false,
|
|
983
983
|
agentMode: session.agentMode,
|
|
984
984
|
analyzingImages: 0,
|
|
985
|
+
generatingImage: false,
|
|
985
986
|
approvalCursor: 0,
|
|
986
987
|
approvalPrompt: null,
|
|
987
988
|
approvalScrollOffset: 0,
|
|
@@ -2623,6 +2624,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2623
2624
|
activeTurnInput: input,
|
|
2624
2625
|
activeTurnInputPreformatted: preformatted,
|
|
2625
2626
|
analyzingImages: 0,
|
|
2627
|
+
generatingImage: false,
|
|
2626
2628
|
busy: true,
|
|
2627
2629
|
busyPausedAt: null,
|
|
2628
2630
|
busySince: turnStartedAt,
|
|
@@ -2777,6 +2779,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2777
2779
|
store.update((current) => ({
|
|
2778
2780
|
...current,
|
|
2779
2781
|
analyzingImages: Math.max(0, activeImageCount),
|
|
2782
|
+
generatingImage: activeImageCount > 0 ? false : current.generatingImage,
|
|
2783
|
+
}));
|
|
2784
|
+
};
|
|
2785
|
+
session.onImageGeneration = (active) => {
|
|
2786
|
+
store.update((current) => ({
|
|
2787
|
+
...current,
|
|
2788
|
+
generatingImage: Boolean(active),
|
|
2789
|
+
analyzingImages: active ? 0 : current.analyzingImages,
|
|
2780
2790
|
}));
|
|
2781
2791
|
};
|
|
2782
2792
|
session.onStatus = (message) => {
|
|
@@ -953,11 +953,13 @@ export function buildWorkingClockLine(state, elapsedSeconds) {
|
|
|
953
953
|
if (state.busyPausedAt != null) {
|
|
954
954
|
return `${WORKING_CLOCK_ICON} Paused · ${elapsed} · waiting for your response`;
|
|
955
955
|
}
|
|
956
|
-
const label = state.
|
|
957
|
-
?
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
956
|
+
const label = state.generatingImage
|
|
957
|
+
? 'Generating image'
|
|
958
|
+
: state.analyzingImages > 0
|
|
959
|
+
? state.analyzingImages > 1
|
|
960
|
+
? 'Analyzing images'
|
|
961
|
+
: 'Analyzing image'
|
|
962
|
+
: 'Working';
|
|
961
963
|
return `${WORKING_CLOCK_ICON} ${label} · ${elapsed}`;
|
|
962
964
|
}
|
|
963
965
|
export function approvalPanelInnerWidth(width) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.26",
|
|
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.
|
|
41
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
42
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
43
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
40
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.26",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.26",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.26",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.26",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|